Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/fairseq2/metrics/recorders/composite.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
Expand All @@ -15,17 +15,27 @@

@final
class CompositeMetricRecorder(MetricRecorder):
"""
Represents a collection defining a recorder for multiple metrics
"""
def __init__(self, recorders: Collection[MetricRecorder]) -> None:
self._recorders = recorders

@override
def record_metric_values(
self, category: str, values: Mapping[str, object], step_nr: int | None = None
) -> None:
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to add documentation for overridden methods. The docstr of the method on the corresponding interface (in this case MetricRecorder) should be descriptive enough to explain the expected behavior of the metod.

Iterates through recorders for all metrics and records the values
For each metric type, the category, values, and step number are recorded
"""
for recorder in self._recorders:
recorder.record_metric_values(category, values, step_nr)

@override
def close(self) -> None:
"""
Closes and removes the :class:MetricRecorder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

"""
for recorder in self._recorders:
recorder.close()
10 changes: 10 additions & 0 deletions src/fairseq2/metrics/recorders/descriptor.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
Expand All @@ -17,6 +17,10 @@

@dataclass
class MetricDescriptor:
"""
Represents a description of a metric including high level name,
name to display, and formatting
"""
name: str
display_name: str
priority: int
Expand All @@ -32,8 +36,14 @@

@final
class MetricDescriptorRegistry:
"""
Represents a way to store descriptors for multiple metrics in a composite metric
"""
def __init__(self, descriptors: Iterable[MetricDescriptor]) -> None:
self._descriptors = {d.name: d for d in descriptors}

def maybe_get(self, name: str) -> MetricDescriptor | None:
"""
Returns a metric descriptor if it exists
"""
return self._descriptors.get(name)
17 changes: 17 additions & 0 deletions src/fairseq2/metrics/recorders/jsonl.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def __init__(
def record_metric_values(
self, category: str, values: Mapping[str, object], step_nr: int | None = None
) -> None:
"""
Gets, sorts, and maps metrics, their values, and descriptions to a ``dict``

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, no need to override the docstr of base interface. Any behavior specific to this subclass should ideally be described in the class docstr.

as a stream. Dumps output to a json file.
:raises OSError: If unable to write to file
"""
stream = self._get_stream(category)

values_and_descriptors = []
Expand All @@ -69,6 +74,10 @@ def record_metric_values(
values_and_descriptors.sort(key=lambda p: (p[1].priority, p[1].display_name))

def sanitize(value: object) -> object:
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an inline function. No need for docstr.

Sanitizes `value` by enforcing object type
:raise ValueError: If `value` is not of type ``int``, ``float``, ``Tensor``, ``str``
"""
if isinstance(value, Tensor):
if value.numel() != 1:
return value.tolist()
Expand Down Expand Up @@ -103,6 +112,11 @@ def sanitize(value: object) -> object:
raise_operational_system_error(ex)

def _get_stream(self, category: str) -> TextIO:
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to document private functions.

Opens a stream for a given category
:raise ValueError: If regex catches nonalphnumeric chars or dash, underscore, forward slash
:raise OSError: If an operating system error occurs when making directory or creating a file
"""
category = category.strip()

fp = self._streams.get(category)
Expand Down Expand Up @@ -133,6 +147,9 @@ def _get_stream(self, category: str) -> TextIO:

@override
def close(self) -> None:
"""
Closes the stream and clears object

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

"""
for stream in self._streams.values():
stream.close()

Expand Down
9 changes: 9 additions & 0 deletions src/fairseq2/metrics/recorders/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ def __init__(self, metric_descriptors: MetricDescriptorRegistry) -> None:
def record_metric_values(
self, category: str, values: Mapping[str, object], step_nr: int | None = None
) -> None:
"""
Retrieves names, descriptors, and values for all metrics from a :class:`Mapping`.
Stores and sorts (values, descriptors) as ``tuples`` and formats a ``str``
with | separated tabular values. Splits metrics into multiple category parts
where they exist.
"""
if not log.is_enabled_for_info():
return

Expand Down Expand Up @@ -81,4 +87,7 @@ def record_metric_values(

@override
def close(self) -> None:
"""
Close the :class:`LogMetricRecorder`
"""
pass
19 changes: 19 additions & 0 deletions src/fairseq2/metrics/recorders/tensorboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ def __init__(
def record_metric_values(
self, category: str, values: Mapping[str, object], step_nr: int | None = None
) -> None:
"""
Retrieves TensorBoard :class:`SummaryWriter` ``objects`` for the metric scalars.
Maps descriptors to metric, step number, name, and scalar value and
flushes output
:raises OSError: If an operational system error occurs (FileNotFound, PermissionError, ConnectionError)
:raises RuntimeError: If the values cannot be written
"""
writer = self._get_writer(category)

try:
Expand All @@ -74,6 +81,11 @@ def record_metric_values(
def _add_value(
self, writer: SummaryWriter, step_nr: int | None, name: str, value: object
) -> None:
"""
Adds a `value` ``object`` to the `SummaryWriter`.
Can be text, a scalar, or torch Tensor
:raises ValueError: If `value` is not of type `int`, `float`, `Tensor` or `str`
"""
if isinstance(value, str):
writer.add_text(name, value, step_nr)

Expand All @@ -95,6 +107,10 @@ def _add_value(
)

def _get_writer(self, category: str) -> SummaryWriter:
"""
Instantiates a `SummaryWriter` and adds it to the ``writers`` `dict`
Stores `writer` under its category as key
"""
writer = self._writers.get(category)
if writer is None:
path = self._output_dir.joinpath(category)
Expand All @@ -107,6 +123,9 @@ def _get_writer(self, category: str) -> SummaryWriter:

@override
def close(self) -> None:
"""
Close out and destroy every ``writer``
"""
for writer in self._writers.values():
writer.close()

Expand Down
12 changes: 12 additions & 0 deletions src/fairseq2/metrics/recorders/wandb.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def __init__(
def record_metric_values(
self, category: str, values: Mapping[str, object], step_nr: int | None = None
) -> None:
"""
Retrieves and stores a `descriptor` and `value` as a ``Mapping`` for each metric
:raises OSError: If an operational system error occurs (file not found, permission issue, connection problem)
:raises RuntimeError: If unable to write the metrics to wandb
"""
output: dict[str, object] = {}

for name, value in values.items():
Expand All @@ -61,6 +66,10 @@ def record_metric_values(
) from ex

def _add_value(self, name: str, value: object, output: dict[str, object]) -> None:
"""
Adds a value to the output dictionary
:raises ValueError: If `values` are not of type int, float, Tensor or str
"""
if isinstance(value, (int, float, Tensor, str)):
output[name] = value

Expand All @@ -78,4 +87,7 @@ def _add_value(self, name: str, value: object, output: dict[str, object]) -> Non

@override
def close(self) -> None:
"""
Close wandb logger and end run
"""
self._run.finish()
Loading