From 05ab5daa95214c1c71428a3f8f2751c35a686c1a Mon Sep 17 00:00:00 2001 From: Alexandros Koumparoulis Date: Mon, 24 Nov 2025 13:06:11 -0800 Subject: [PATCH 1/5] log actual yaml to stdout Signed-off-by: Alexandros Koumparoulis --- nemo_automodel/components/config/loader.py | 87 +++++++++++++++++++++- nemo_automodel/recipes/base_recipe.py | 40 ++++++---- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/nemo_automodel/components/config/loader.py b/nemo_automodel/components/config/loader.py index 7071b8b7f..a64cb618f 100644 --- a/nemo_automodel/components/config/loader.py +++ b/nemo_automodel/components/config/loader.py @@ -425,6 +425,91 @@ def to_dict(self): k: self._unwrap(v) for k, v in self.__dict__.items() if k not in ("raise_on_missing_attr", "_raw_config") } + def _to_dotted_path(self, obj): + """ + Convert a callable/class/method object to a dotted path string. + + Best-effort normalization for a few common cases to produce concise, user-friendly paths. + """ + # Bound method on a class (e.g., Class.from_pretrained) + try: + import inspect as _inspect # local alias to avoid confusion with top-level import + if _inspect.ismethod(obj): + owner = getattr(obj, "__self__", None) + if _inspect.isclass(owner): + method_name = getattr(obj, "__name__", "unknown") + module_name = getattr(owner, "__module__", None) or "" + class_name = getattr(owner, "__name__", "UnknownClass") + # Prefer shortened top-level for NeMoAutoModel* classes if possible + if class_name.startswith("NeMoAutoModel"): + module_name = "nemo_automodel" + dotted = f"{module_name}.{class_name}.{method_name}".lstrip(".") + else: + # Bound to instance – fall back to module + qualname + module_name = getattr(obj, "__module__", None) or "" + qualname = getattr(obj, "__qualname__", getattr(obj, "__name__", "unknown")) + dotted = f"{module_name}.{qualname}".lstrip(".") + elif _inspect.isfunction(obj): + module_name = getattr(obj, "__module__", None) or "" + qualname = getattr(obj, "__qualname__", getattr(obj, "__name__", "unknown")) + dotted = f"{module_name}.{qualname}".lstrip(".") + elif _inspect.isclass(obj): + module_name = getattr(obj, "__module__", None) or "" + class_name = getattr(obj, "__name__", "UnknownClass") + dotted = f"{module_name}.{class_name}".lstrip(".") + else: + module_name = getattr(obj, "__module__", None) or "" + qualname = getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj))) + dotted = f"{module_name}.{qualname}".lstrip(".") + except Exception: + # Fallback to repr if anything goes wrong + return repr(obj) + + # Tidy up a few known verbose module paths + dotted = dotted.replace( + "torchdata.stateful_dataloader.stateful_dataloader.StatefulDataLoader", + "torchdata.stateful_dataloader.StatefulDataLoader", + ) + dotted = dotted.replace("torch.optim.adamw.AdamW", "torch.optim.AdamW") + return dotted + + def to_yaml_dict(self): + """ + Convert configuration to a YAML-ready dictionary: + - Preserves typed scalars (ints, floats, bools) + - Converts callables/classes/methods (e.g., _target_, *_fn) to dotted path strings + - Recurses through nested ConfigNodes and lists + """ + + def _convert(key, value): + # Nested config + if isinstance(value, ConfigNode): + return value.to_yaml_dict() + # Lists + if isinstance(value, list): + return [_convert(None, v) for v in value] + # Dicts (shouldn't normally appear because we wrap into ConfigNode, but handle defensively) + if isinstance(value, dict): + return {k: _convert(k, v) for k, v in value.items()} + # Convert targets/functions to dotted path strings + is_target_like = key == "_target_" or (isinstance(key, str) and key.endswith("_fn")) or key == "collate_fn" + try: + import inspect as _inspect + if is_target_like and (callable(value) or _inspect.ismethod(value) or _inspect.isclass(value)): + return self._to_dotted_path(value) + # Even if the key isn't target-like, convert bare callables to dotted path to avoid repr + if callable(value) or _inspect.ismethod(value) or _inspect.isclass(value): + return self._to_dotted_path(value) + except Exception: + pass + # Primitive – already typed via translate_value/_wrap + return value + + # Walk live attributes to preserve translated scalars + return { + k: _convert(k, v) for k, v in self.__dict__.items() if k not in ("raise_on_missing_attr", "_raw_config") + } + def _unwrap(self, v): """ Recursively convert wrapped configuration values to basic Python types. @@ -508,7 +593,7 @@ def __repr__(self, level=0): for key, value in self.__dict__.items() if key not in ("raise_on_missing_attr", "_raw_config") ] - return "\n#path: " + "\n".join(lines) + f"\n{indent}" + return "\n".join(lines) + f"\n{indent}" def _repr_value(self, value, level): """ diff --git a/nemo_automodel/recipes/base_recipe.py b/nemo_automodel/recipes/base_recipe.py index 3866b19e9..6b49cbc14 100644 --- a/nemo_automodel/recipes/base_recipe.py +++ b/nemo_automodel/recipes/base_recipe.py @@ -394,22 +394,32 @@ def _log_experiment_details(self): # Resolved config try: cfg_obj = getattr(self, "cfg", None) - cfg_dict = ( - cfg_obj.to_dict() if hasattr(cfg_obj, "to_dict") else (dict(cfg_obj) if cfg_obj is not None else {}) - ) + # Prefer YAML-ready dict that converts callables/classes to dotted paths and preserves typed scalars + if hasattr(cfg_obj, "to_yaml_dict"): + cfg_dict = cfg_obj.to_yaml_dict() + elif hasattr(cfg_obj, "to_dict"): + cfg_dict = cfg_obj.to_dict() + else: + cfg_dict = dict(cfg_obj) if cfg_obj is not None else {} - def rec_print(log_fn, cfg_dict: dict | None, indent: int = 2): - if cfg_dict is None: - return - for k, v in cfg_dict.items(): - if isinstance(v, dict): - log_fn(f"{' ' * indent}{k}:") - rec_print(log_fn, v, indent + 2) - else: - log_fn(f"{' ' * indent}{k}: {v}") - - logging.info("Recipe config:") - rec_print(logging.info, cfg_dict) + # Print as clean YAML on stdout for easy copy/paste and readability + if _yaml is not None: + cfg_yaml = _yaml.safe_dump(cfg_dict, sort_keys=False, default_flow_style=False).strip() + print(cfg_yaml, flush=True) + else: + # Fallback structured print if yaml is unavailable + def rec_print(d: dict | None, indent: int = 0): + if d is None: + return + pad = " " * indent + for k, v in d.items(): + if isinstance(v, dict): + print(f"{pad}{k}:", flush=True) + rec_print(v, indent + 1) + else: + print(f"{pad}{k}: {v}", flush=True) + + rec_print(cfg_dict, 0) except Exception: logging.info("Recipe config: ") From 4a8bd3237ba9f37cf7d1f4c460f977cdd4762b9b Mon Sep 17 00:00:00 2001 From: Alexandros Koumparoulis Date: Tue, 9 Dec 2025 11:24:43 -0800 Subject: [PATCH 2/5] simplofy Signed-off-by: Alexandros Koumparoulis --- nemo_automodel/recipes/base_recipe.py | 32 +++++---------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/nemo_automodel/recipes/base_recipe.py b/nemo_automodel/recipes/base_recipe.py index 6b49cbc14..7d3ca9abb 100644 --- a/nemo_automodel/recipes/base_recipe.py +++ b/nemo_automodel/recipes/base_recipe.py @@ -36,10 +36,7 @@ from nemo_automodel.components.training.rng import StatefulRNG from nemo_automodel.components.training.step_scheduler import StepScheduler -try: - import yaml as _yaml -except Exception: - _yaml = None +import yaml from transformers.processing_utils import ProcessorMixin from transformers.tokenization_utils import PreTrainedTokenizerBase @@ -384,11 +381,9 @@ def _log_experiment_details(self): and getattr(self.cfg.model, "pretrained_model_name_or_path", None), } try: - if _yaml is not None: - details_yaml = _yaml.safe_dump(details, sort_keys=False, default_flow_style=False).strip() - else: - details_yaml = "\n".join(f"{k}: {v}" for k, v in details.items()) - list(map(logging.info, ("Experiment_details:\n" + details_yaml).splitlines())) + details_yaml = yaml.safe_dump(details, sort_keys=False, default_flow_style=False).strip() + for line in ("Experiment_details:\n" + details_yaml).splitlines(): + logging.info(line) except Exception: logging.info(f"Experiment details: {details}") # Resolved config @@ -403,23 +398,8 @@ def _log_experiment_details(self): cfg_dict = dict(cfg_obj) if cfg_obj is not None else {} # Print as clean YAML on stdout for easy copy/paste and readability - if _yaml is not None: - cfg_yaml = _yaml.safe_dump(cfg_dict, sort_keys=False, default_flow_style=False).strip() - print(cfg_yaml, flush=True) - else: - # Fallback structured print if yaml is unavailable - def rec_print(d: dict | None, indent: int = 0): - if d is None: - return - pad = " " * indent - for k, v in d.items(): - if isinstance(v, dict): - print(f"{pad}{k}:", flush=True) - rec_print(v, indent + 1) - else: - print(f"{pad}{k}: {v}", flush=True) - - rec_print(cfg_dict, 0) + cfg_yaml = yaml.safe_dump(cfg_dict, sort_keys=False, default_flow_style=False).strip() + print(cfg_yaml, flush=True) except Exception: logging.info("Recipe config: ") From 9efeff1a372b414a5e6ec75e18742c88c1ba120d Mon Sep 17 00:00:00 2001 From: Alexandros Koumparoulis Date: Tue, 9 Dec 2025 11:26:21 -0800 Subject: [PATCH 3/5] fix Signed-off-by: Alexandros Koumparoulis --- nemo_automodel/components/config/loader.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/nemo_automodel/components/config/loader.py b/nemo_automodel/components/config/loader.py index a64cb618f..207f01c73 100644 --- a/nemo_automodel/components/config/loader.py +++ b/nemo_automodel/components/config/loader.py @@ -464,13 +464,6 @@ def _to_dotted_path(self, obj): except Exception: # Fallback to repr if anything goes wrong return repr(obj) - - # Tidy up a few known verbose module paths - dotted = dotted.replace( - "torchdata.stateful_dataloader.stateful_dataloader.StatefulDataLoader", - "torchdata.stateful_dataloader.StatefulDataLoader", - ) - dotted = dotted.replace("torch.optim.adamw.AdamW", "torch.optim.AdamW") return dotted def to_yaml_dict(self): From d0750cc0a4a33d5e4815de148794ab7db48c5c26 Mon Sep 17 00:00:00 2001 From: Alexandros Koumparoulis Date: Tue, 9 Dec 2025 11:45:22 -0800 Subject: [PATCH 4/5] lint Signed-off-by: Alexandros Koumparoulis --- nemo_automodel/recipes/base_recipe.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nemo_automodel/recipes/base_recipe.py b/nemo_automodel/recipes/base_recipe.py index 7d3ca9abb..840fd8a69 100644 --- a/nemo_automodel/recipes/base_recipe.py +++ b/nemo_automodel/recipes/base_recipe.py @@ -24,6 +24,7 @@ import torch import torch.distributed as dist import torch.nn as nn +import yaml from torch.optim import Optimizer from torchdata.stateful_dataloader import StatefulDataLoader from transformers.processing_utils import ProcessorMixin @@ -36,10 +37,6 @@ from nemo_automodel.components.training.rng import StatefulRNG from nemo_automodel.components.training.step_scheduler import StepScheduler -import yaml -from transformers.processing_utils import ProcessorMixin -from transformers.tokenization_utils import PreTrainedTokenizerBase - def has_load_restore_state(object): """ From bdb60a303cb264a9de2ac34f5126bbd71d2eca62 Mon Sep 17 00:00:00 2001 From: Alexandros Koumparoulis Date: Tue, 9 Dec 2025 14:36:31 -0800 Subject: [PATCH 5/5] fix Signed-off-by: Alexandros Koumparoulis --- nemo_automodel/components/config/loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nemo_automodel/components/config/loader.py b/nemo_automodel/components/config/loader.py index 207f01c73..2f0442357 100644 --- a/nemo_automodel/components/config/loader.py +++ b/nemo_automodel/components/config/loader.py @@ -434,6 +434,7 @@ def _to_dotted_path(self, obj): # Bound method on a class (e.g., Class.from_pretrained) try: import inspect as _inspect # local alias to avoid confusion with top-level import + if _inspect.ismethod(obj): owner = getattr(obj, "__self__", None) if _inspect.isclass(owner): @@ -488,6 +489,7 @@ def _convert(key, value): is_target_like = key == "_target_" or (isinstance(key, str) and key.endswith("_fn")) or key == "collate_fn" try: import inspect as _inspect + if is_target_like and (callable(value) or _inspect.ismethod(value) or _inspect.isclass(value)): return self._to_dotted_path(value) # Even if the key isn't target-like, convert bare callables to dotted path to avoid repr