Skip to content

Commit 1579652

Browse files
feat(tracking): register custom tracker classes by name
Add register_tracker_class to register a custom GeneralTracker subclass into the LOGGER_TYPE_TO_CLASS registry so it can be selected by string name in Accelerator(log_with=...), the same way as the built-in trackers. Expose a module-level register_tracker_class in accelerate.tracking. Validate that the class subclasses GeneralTracker and defines name and requires_logging_directory, and warn when an existing name is overwritten. Extend filter_trackers so registered custom names resolve through the registry, including when combined with 'all', with the same logging-directory guard as the built-in trackers. Add tests and documentation for registering and using a custom tracker by name. Fixes #2734
1 parent 507ea7b commit 1579652

4 files changed

Lines changed: 286 additions & 3 deletions

File tree

docs/source/package_reference/tracking.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ rendered properly in your Markdown viewer.
1919

2020
[[autodoc]] tracking.GeneralTracker
2121

22+
## register_tracker_class
23+
24+
[[autodoc]] tracking.register_tracker_class
25+
2226
## TensorBoardTracker
2327

2428
[[autodoc]] tracking.TensorBoardTracker

docs/source/usage_guides/tracking.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,25 @@ tracker = MyCustomTracker("some_run_name")
170170
accelerator = Accelerator(log_with=[tracker, "all"])
171171
```
172172

173+
### Registering a custom tracker by name
174+
175+
Instead of passing an instance, you can register a custom tracker class so it can be selected by its `name`, just like the built-in trackers. This is handy when the trackers to use are read from a config file or the command line:
176+
177+
```python
178+
from accelerate import Accelerator
179+
from accelerate.tracking import GeneralTracker, register_tracker_class
180+
181+
182+
class MyCustomTracker(GeneralTracker):
183+
name = "my_tracker"
184+
requires_logging_directory = False
185+
# ... implement the rest of the `GeneralTracker` interface
186+
187+
188+
register_tracker_class(MyCustomTracker)
189+
accelerator = Accelerator(log_with="my_tracker")
190+
```
191+
173192
## Accessing the internal tracker
174193

175194
If some custom interactions with a tracker might be wanted directly, you can quickly access one using the

src/accelerate/tracking.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,56 @@ def finish(self):
12591259
}
12601260

12611261

1262+
def register_tracker_class(tracker_class: type[GeneralTracker]):
1263+
"""
1264+
Registers a custom [`GeneralTracker`] subclass so it can be referenced by its `name` in the `log_with` argument of
1265+
[`Accelerator`], the same way as the built-in trackers.
1266+
1267+
The tracker must be registered before instantiating the [`Accelerator`] that uses it.
1268+
1269+
Args:
1270+
tracker_class (subclass of [`GeneralTracker`]):
1271+
The tracker class to register. It must subclass [`GeneralTracker`] and define a non-empty `name` class
1272+
attribute and a `requires_logging_directory` class attribute. The class is instantiated with the
1273+
`project_name` passed to [`Accelerator.init_trackers`] as the first positional argument. When
1274+
`requires_logging_directory` is `True`, the logging directory is passed as the second positional argument
1275+
(matching the built-in tracker convention). Tracker-specific keyword arguments from `init_kwargs` are
1276+
forwarded as `**kwargs`.
1277+
1278+
Example:
1279+
1280+
```python
1281+
from accelerate import Accelerator
1282+
from accelerate.tracking import GeneralTracker, register_tracker_class
1283+
1284+
1285+
class MyTracker(GeneralTracker):
1286+
name = "my_tracker"
1287+
requires_logging_directory = False
1288+
# ... implement the rest of the `GeneralTracker` interface
1289+
1290+
1291+
register_tracker_class(MyTracker)
1292+
accelerator = Accelerator(log_with="my_tracker")
1293+
```
1294+
"""
1295+
if not (isinstance(tracker_class, type) and issubclass(tracker_class, GeneralTracker)):
1296+
raise ValueError(f"`tracker_class` must be a subclass of `GeneralTracker`, but got {tracker_class}.")
1297+
if not getattr(tracker_class, "name", None):
1298+
raise ValueError("The tracker class to register must define a non-empty `name` attribute.")
1299+
if not hasattr(tracker_class, "requires_logging_directory"):
1300+
raise ValueError(
1301+
"The tracker class to register must define a `requires_logging_directory` class attribute. "
1302+
"Set it to `True` if the tracker needs a logging directory, or `False` otherwise."
1303+
)
1304+
if tracker_class.name in LOGGER_TYPE_TO_CLASS:
1305+
logger.warning(
1306+
f"A tracker with the name '{tracker_class.name}' is already registered and will be overwritten by "
1307+
f"{tracker_class}. This is especially significant when shadowing a built-in tracker."
1308+
)
1309+
LOGGER_TYPE_TO_CLASS[tracker_class.name] = tracker_class
1310+
1311+
12621312
def filter_trackers(
12631313
log_with: list[Union[str, LoggerType, GeneralTracker]],
12641314
logging_dir: Optional[Union[str, os.PathLike]] = None,
@@ -1293,14 +1343,28 @@ def filter_trackers(
12931343
if not isinstance(log_with, (list, tuple)):
12941344
log_with = [log_with]
12951345
if "all" in log_with or LoggerType.ALL in log_with:
1296-
loggers = [o for o in log_with if issubclass(type(o), GeneralTracker)] + get_available_trackers()
1346+
loggers = [o for o in log_with if issubclass(type(o), GeneralTracker)]
1347+
# Also keep custom trackers registered via `register_tracker_class` that are named alongside "all".
1348+
# Apply the same logging-dir guard as the explicit-name branch for consistency.
1349+
for custom_name in log_with:
1350+
if custom_name not in LoggerType and custom_name in LOGGER_TYPE_TO_CLASS:
1351+
tracker_init = LOGGER_TYPE_TO_CLASS[str(custom_name)]
1352+
if tracker_init.requires_logging_directory and logging_dir is None:
1353+
raise ValueError(f"Logging with `{custom_name}` requires a `logging_dir` to be passed in.")
1354+
if custom_name not in loggers:
1355+
loggers.append(custom_name)
1356+
loggers += get_available_trackers()
12971357
else:
12981358
for log_type in log_with:
1299-
if log_type not in LoggerType and not issubclass(type(log_type), GeneralTracker):
1359+
if (
1360+
log_type not in LoggerType
1361+
and not issubclass(type(log_type), GeneralTracker)
1362+
and str(log_type) not in LOGGER_TYPE_TO_CLASS
1363+
):
13001364
raise ValueError(f"Unsupported logging capability: {log_type}. Choose between {LoggerType.list()}")
13011365
if issubclass(type(log_type), GeneralTracker):
13021366
loggers.append(log_type)
1303-
else:
1367+
elif log_type in LoggerType:
13041368
log_type = LoggerType(log_type)
13051369
if log_type not in loggers:
13061370
if log_type in get_available_trackers():
@@ -1313,5 +1377,12 @@ def filter_trackers(
13131377
loggers.append(log_type)
13141378
else:
13151379
logger.debug(f"Tried adding logger {log_type}, but package is unavailable in the system.")
1380+
else:
1381+
# Custom tracker class registered via `register_tracker_class`
1382+
if log_type not in loggers:
1383+
tracker_init = LOGGER_TYPE_TO_CLASS[str(log_type)]
1384+
if tracker_init.requires_logging_directory and logging_dir is None:
1385+
raise ValueError(f"Logging with `{log_type}` requires a `logging_dir` to be passed in.")
1386+
loggers.append(log_type)
13161387

13171388
return loggers

tests/test_tracking.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
skip,
5151
)
5252
from accelerate.tracking import (
53+
LOGGER_TYPE_TO_CLASS,
5354
AimTracker,
5455
ClearMLTracker,
5556
CometMLTracker,
@@ -60,6 +61,8 @@
6061
TensorBoardTracker,
6162
TrackioTracker,
6263
WandBTracker,
64+
filter_trackers,
65+
register_tracker_class,
6366
)
6467
from accelerate.utils import (
6568
ProjectConfiguration,
@@ -731,6 +734,192 @@ def test_log(self):
731734
assert data == truth
732735

733736

737+
class MyRegisteredTracker(GeneralTracker):
738+
"Minimal tracker following the built-in `(run_name, **kwargs)` convention, used to test class registration."
739+
740+
name = "my_registered_tracker"
741+
requires_logging_directory = False
742+
743+
def __init__(self, run_name: str, **kwargs):
744+
super().__init__()
745+
self.run_name = run_name
746+
self.config = {}
747+
self.values = {}
748+
749+
def start(self):
750+
pass
751+
752+
@property
753+
def tracker(self):
754+
return self
755+
756+
def store_init_configuration(self, values: dict):
757+
self.config.update(values)
758+
759+
def log(self, values: dict, step: Optional[int] = None, **kwargs):
760+
self.values.update(values)
761+
762+
def finish(self):
763+
pass
764+
765+
766+
class RegisterTrackerClassTest(unittest.TestCase):
767+
def setUp(self):
768+
self._registry_snapshot = dict(LOGGER_TYPE_TO_CLASS)
769+
super().setUp()
770+
771+
def tearDown(self):
772+
LOGGER_TYPE_TO_CLASS.clear()
773+
LOGGER_TYPE_TO_CLASS.update(self._registry_snapshot)
774+
super().tearDown()
775+
776+
def test_register_and_use_by_name(self):
777+
register_tracker_class(MyRegisteredTracker)
778+
assert MyRegisteredTracker.name in LOGGER_TYPE_TO_CLASS
779+
780+
accelerator = Accelerator(log_with=MyRegisteredTracker.name)
781+
config = {"num_iterations": 12, "learning_rate": 1e-2}
782+
accelerator.init_trackers("some_project", config)
783+
accelerator.log({"total_loss": 0.1}, step=0)
784+
tracker = accelerator.get_tracker(MyRegisteredTracker.name)
785+
accelerator.end_training()
786+
787+
assert isinstance(tracker, MyRegisteredTracker)
788+
assert tracker.config == config
789+
assert tracker.values == {"total_loss": 0.1}
790+
791+
def test_registered_tracker_included_with_all(self):
792+
register_tracker_class(MyRegisteredTracker)
793+
loggers = filter_trackers([MyRegisteredTracker.name, "all"])
794+
assert MyRegisteredTracker.name in loggers
795+
796+
def test_register_non_tracker_raises(self):
797+
with self.assertRaises(ValueError):
798+
register_tracker_class(dict)
799+
800+
def test_register_without_name_raises(self):
801+
class NamelessTracker(GeneralTracker):
802+
requires_logging_directory = False
803+
804+
with self.assertRaises(ValueError):
805+
register_tracker_class(NamelessTracker)
806+
807+
def test_register_missing_requires_logging_directory_raises(self):
808+
class NoAttrTracker(GeneralTracker):
809+
name = "no_attr_tracker"
810+
# intentionally missing requires_logging_directory
811+
812+
with self.assertRaises(ValueError):
813+
register_tracker_class(NoAttrTracker)
814+
815+
def test_register_overwrites_existing_warns(self):
816+
class FirstTracker(GeneralTracker):
817+
name = "collision_tracker"
818+
requires_logging_directory = False
819+
820+
@property
821+
def tracker(self):
822+
return self
823+
824+
def store_init_configuration(self, values: dict):
825+
pass
826+
827+
def log(self, values: dict, step=None, **kwargs):
828+
pass
829+
830+
def finish(self):
831+
pass
832+
833+
class SecondTracker(GeneralTracker):
834+
name = "collision_tracker"
835+
requires_logging_directory = False
836+
837+
@property
838+
def tracker(self):
839+
return self
840+
841+
def store_init_configuration(self, values: dict):
842+
pass
843+
844+
def log(self, values: dict, step=None, **kwargs):
845+
pass
846+
847+
def finish(self):
848+
pass
849+
850+
register_tracker_class(FirstTracker)
851+
# Registering a second class with the same name must warn
852+
with mock.patch("accelerate.tracking.logger") as mock_logger:
853+
register_tracker_class(SecondTracker)
854+
mock_logger.warning.assert_called_once()
855+
warning_msg = mock_logger.warning.call_args[0][0]
856+
assert "collision_tracker" in warning_msg
857+
858+
def test_all_branch_requires_logging_directory(self):
859+
class DirRequiringTracker(GeneralTracker):
860+
name = "dir_requiring_custom"
861+
requires_logging_directory = True
862+
863+
@property
864+
def tracker(self):
865+
return self
866+
867+
def store_init_configuration(self, values: dict):
868+
pass
869+
870+
def log(self, values: dict, step=None, **kwargs):
871+
pass
872+
873+
def finish(self):
874+
pass
875+
876+
register_tracker_class(DirRequiringTracker)
877+
with self.assertRaises(ValueError):
878+
# No logging_dir provided - should raise because custom tracker requires one
879+
filter_trackers([DirRequiringTracker.name, "all"], logging_dir=None)
880+
881+
def test_requires_logging_directory_custom_tracker(self):
882+
class DirTracker(GeneralTracker):
883+
name = "dir_tracker_test"
884+
requires_logging_directory = True
885+
886+
def __init__(self, run_name: str, logging_dir: str, **kwargs):
887+
super().__init__()
888+
self.run_name = run_name
889+
self.logging_dir = logging_dir
890+
self.config = {}
891+
self.values = {}
892+
893+
@property
894+
def tracker(self):
895+
return self
896+
897+
def store_init_configuration(self, values: dict):
898+
self.config.update(values)
899+
900+
def log(self, values: dict, step=None, **kwargs):
901+
self.values.update(values)
902+
903+
def finish(self):
904+
pass
905+
906+
register_tracker_class(DirTracker)
907+
908+
# Without project_dir, Accelerator should raise ValueError when trying to use it
909+
with self.assertRaises(ValueError):
910+
Accelerator(log_with=DirTracker.name)
911+
912+
# With a project_dir it should succeed and produce the right tracker type
913+
with tempfile.TemporaryDirectory() as tmpdir:
914+
accelerator = Accelerator(log_with=DirTracker.name, project_dir=tmpdir)
915+
accelerator.init_trackers("test_dir_project")
916+
tracker = accelerator.get_tracker(DirTracker.name)
917+
assert isinstance(tracker, DirTracker)
918+
assert tracker.run_name == "test_dir_project"
919+
assert tracker.logging_dir == tmpdir
920+
accelerator.end_training()
921+
922+
734923
@require_dvclive
735924
@mock.patch("dvclive.live.get_dvc_repo", return_value=None)
736925
class DVCLiveTrackingTest(unittest.TestCase):

0 commit comments

Comments
 (0)