Skip to content

Commit 1f4f83e

Browse files
HuiyingLiclaudeakoumpa
authored
refactor: expose shared recipe builders from components (#2190)
* refactor: expose shared recipe builders from components Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor: share recipe optimizer builder Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor: reuse shared distributed builder in mining recipe Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor: keep config targets out of components Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * revert: restore recipe files to main (defer recipe changes to follow-up) Revert all recipe-side changes introduced by the shared-builder commits. This PR now only moves builder helpers into components/ and adds _component_builders.py — recipes remain unchanged and will be updated in the follow-up Engine PR. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(components): standardize builder files to api.py, replace Any with concrete types Rename all five component builder files to `api.py` to follow the reviewer-requested `config.py` + `api.py` two-file convention: checkpoint/config_builder.py → checkpoint/api.py distributed/build.py → distributed/api.py loss/build.py → loss/api.py optim/build.py → optim/api.py training/step_builder.py → training/api.py Replace `Any` type annotations with concrete types: - optim/api.py: DistributedConfig, DeviceMesh, StepScheduler, list[Optimizer] | Optimizer (under TYPE_CHECKING guard) - loss/api.py: Callable[..., nn.Module] and nn.Module return - training/api.py: DataLoader (under TYPE_CHECKING guard) Fix stale build_mlflow import in _component_builders.py after the merge-main resolution reverted mlflow_utils.py to configure_mlflow. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): resolve ConfigNode at recipe boundary for optimizer builder Use _callable_and_kwargs() to extract (optimizer_factory, optimizer_kwargs) from the ConfigNode before calling is_dion_optimizer / build_dion_optimizer, so the component-layer utils receive typed Python args instead of ConfigNodes. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(components): add config.py re-exports for checkpoint, optim, training Standardize the config.py + api.py two-file convention: checkpoint/config.py → re-exports CheckpointingConfig optim/config.py → re-exports OptimizerParamScheduler training/config.py → re-exports StepScheduler distributed/config.py already existed. loss/ and loggers/ have no config dataclasses to expose. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): extract LRSchedulerConfig dataclass into config.py Pull the LR scheduler parameters out of the OptimizerParamScheduler constructor into a standalone LRSchedulerConfig dataclass that lives in config.py. build_lr_scheduler in api.py now accepts LRSchedulerConfig and resolves defaults (warmup, decay, LR fractions) from the training schedule + optimizer base LR. This completes the config.py + api.py two-file convention for the optim component: config.py defines what you can configure, api.py builds it. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(training): extract StepSchedulerConfig dataclass into config.py Pull the YAML-configurable step scheduler parameters (global_batch_size, num_epochs, max_steps, checkpoint/val/gc intervals, etc.) into a StepSchedulerConfig dataclass in config.py. build_step_scheduler in api.py now accepts StepSchedulerConfig plus runtime-only args (dataloader, dp_group_size, local_batch_size). Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(checkpoint): extract CheckpointConfig dataclass into config.py Pull the YAML-configurable checkpoint parameters (checkpoint_dir, model_save_format, save_consolidated, is_async, etc.) into a CheckpointConfig dataclass in config.py. build_checkpoint_config in api.py now accepts CheckpointConfig plus runtime-only args (cache_dir, model_repo_id, is_peft) and produces the internal CheckpointingConfig. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(loggers): add config.py + api.py for WandB, MLflow, Comet Add typed config dataclasses: WandbConfig — project, entity, name, group, tags, save_dir, notes MLflowConfig — experiment_name, run_name, tracking_uri, tags, resume, etc. CometConfig — project_name, workspace, api_key, experiment_name, tags Add api.py with build_wandb(config, ...) and build_comet(config). MLflow stays in mlflow_utils.py (configure_mlflow) for now since it takes the full config object with resume/sidecar logic; MLflowConfig documents the YAML surface for discoverability. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(loggers): add build_mlflow to api.py with typed MLflowConfig Implement build_mlflow(config, checkpoint_dir, run_config) that takes MLflowConfig instead of the full ConfigNode. Handles experiment creation, run resume via sidecar, failure hook installation, and config logging — same behaviour as configure_mlflow but without ConfigNode coupling. The recipe-facing _component_builders.build_mlflow now bridges the ConfigNode → MLflowConfig boundary, extracting checkpoint_dir and run_config before delegating to the typed builder. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): route wandb/mlflow through _component_builders Delete the inline build_wandb() from train_ft.py and vlm/finetune.py. Replace configure_mlflow() calls with build_mlflow() from _component_builders. All downstream recipes (diffusion, retrieval, train_seq_cls) now import build_wandb from _component_builders instead of from train_ft.py. Remove the superseded build_wandb from wandb_utils.py — the typed version in loggers/api.py is the canonical implementation. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(checkpoint): drop CheckpointConfig, re-export CheckpointingConfig in config.py CheckpointingConfig is already a clean dataclass — no need for a separate user-facing wrapper. config.py now re-exports it directly. api.py reverts to accepting Mapping[str, Any] kwargs (same as before the CheckpointConfig detour). Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(checkpoint): move CheckpointingConfig into config.py Move the CheckpointingConfig dataclass and its _is_geq_torch_2_9 helper from checkpointing.py into config.py (the canonical config surface). checkpointing.py re-imports both so existing callers are unaffected. This avoids pulling in checkpointing.py's heavy dependencies (DCP, safetensors, etc.) when only the config type is needed. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * feat(optim): add OptimizerConfig with string-based optimizer resolution Add OptimizerConfig dataclass to config.py with: name: dotted import path (e.g. "torch.optim.AdamW") lr, weight_decay: common hyper-parameters as explicit fields extra_kwargs: pass-through dict for optimizer-specific params build_optimizer in api.py now accepts either OptimizerConfig (preferred for external integrations like veRL — zero importlib boilerplate) or the existing (optimizer_factory, optimizer_kwargs) pair for backward compat with _component_builders. This follows the veRL/VeOmni pattern where a single config with a name string + common fields is the most integration-friendly approach. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * feat(optim): add per-optimizer typed config hierarchy Replace the flat OptimizerConfig with a hierarchy of typed subclasses so that every parameter is discoverable by reading config.py: OptimizerConfig (generic fallback — name + extra_kwargs) ├── AdamConfig (betas, eps, amsgrad) ├── AdamWConfig (betas, eps, amsgrad, fused) ├── FusedAdamConfig (betas, eps, adam_w_mode, bias_correction, │ master_weights, master_weight_dtype) ├── FlashAdamWConfig (betas, eps, master_weight_bits) └── MuonConfig (mu, betas, epsilon, adjust_lr, scalar_opt, scalar_betas, scalar_eps) Each config has a to_kwargs() method that returns the full kwargs dict for the optimizer constructor. The base OptimizerConfig still works as a generic fallback for third-party optimizers via extra_kwargs. Covers all 5 optimizer classes currently used across Automodel's YAML configs (188× Adam, 95× AdamW, 9× FlashAdamW, 9× FusedAdam, 1× Muon). Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * feat(optim): add OptimizerConfig.from_name() registry for string-based dispatch Add a classmethod factory that resolves an optimizer name string to the correct typed config subclass: OptimizerConfig.from_name("torch.optim.AdamW", lr=1e-4, betas=(0.9, 0.95)) → AdamWConfig(lr=1e-4, betas=(0.9, 0.95)) OptimizerConfig.from_name("some.custom.Optimizer", lr=1e-4, momentum=0.9) → OptimizerConfig(name=..., extra_kwargs={"momentum": 0.9}) Known optimizers (Adam, AdamW, FusedAdam, FlashAdamW, Muon) return typed subclasses with full field validation. Unknown optimizers fall back to the base OptimizerConfig with extra_kwargs. This is the single entry point for all callers — veRL CLI, YAML recipes, Python scripts — no integration-specific code needed. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * feat(loss): add typed LossConfig hierarchy with from_name() registry Add config.py with per-loss typed configs following the same pattern as OptimizerConfig: LossConfig (generic fallback — name + extra_kwargs) ├── MaskedCrossEntropyConfig (fp32_upcast, ignore_index, reduction) ├── FusedLinearCEConfig (ignore_index, logit_softcapping, reduction) ├── TEParallelCEConfig (ignore_index, reduction) └── KDLossConfig (ignore_index, temperature, fp32_upcast) LossConfig.from_name() resolves the dotted name string to the correct typed subclass. build_loss_fn() accepts either LossConfig or the existing (loss_factory, loss_kwargs) pair for backward compat. Covers all 4 loss classes used across Automodel's YAML configs (290× MaskedCE, 4× TEParallelCE, 2× FusedLinearCE, 2× KDLoss). Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): route build_loss_fn through _component_builders Delete inline build_loss_fn from train_ft.py and vlm/finetune.py. Both now import from _component_builders, same pattern as build_wandb and build_mlflow. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * test: add unit tests for config.py + api.py across all components 68 new tests covering: optim/ — OptimizerConfig hierarchy, from_name() registry dispatch, _resolve_optimizer, to_kwargs for all 5 subclasses, LRSchedulerConfig defaults loss/ — LossConfig hierarchy, from_name() dispatch, _resolve_loss, to_kwargs for all 4 subclasses, build_loss_fn with config loggers/ — WandbConfig, MLflowConfig, CometConfig defaults + custom training/ — StepSchedulerConfig defaults, custom values, resume fields checkpoint/ — CheckpointingConfig construction + __post_init__ validation, backward compat import from checkpointing.py, build_checkpoint_config with defaults/overrides/PEFT fallback Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(loss): remove string-based resolution from loss component Move dotted-path resolution (_resolve_loss, _LOSS_REGISTRY, from_name) out of the component layer. Loss config.py is now pure dataclasses; api.py maps config type → loss class directly via _get_loss_class. String-to-class resolution belongs in the integration layer (recipes, veRL adapter), not in the component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): remove string-based resolution from optim component Remove _resolve_optimizer, _OPTIMIZER_REGISTRY, and from_name() from optim config.py. When using OptimizerConfig, the caller must also provide optimizer_factory (the resolved class). String-to-class resolution belongs in the integration layer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): TorchTitan-style typed configs with build() + RL escape hatch Collapse optim config.py + api.py into a single optimizer.py. Each typed optimizer config (Adam/AdamW/FusedAdam/FlashAdamW/Muon) exposes its full parameter surface as named fields and a build() method — no extra_kwargs, no to_kwargs(), no dotted-path resolution. - build_optimizer(model, config, ...): Automodel-native orchestration (parts loop, TP foreach, Dion grouping, Megatron-FSDP sharding). - build_optimizer_for_rl(optimizer, model, **kwargs): kwargs-accepting escape hatch for external integrations (veRL). Accepts a dotted path or a resolved class; adding new typed configs never requires changing callers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): collapse build_optimizer + build_optimizer_for_rl into one Single build_optimizer(model, optimizer, **kwargs) orchestration entry that dispatches on its second argument: - OptimizerConfig instance -> native typed path (delegates per-part to config.build); rejects stray **kwargs. - dotted path / class + **kwargs -> integration escape hatch (veRL); unknown optimizers work without a typed config. The per-part loop (TP foreach, Dion grouping, Megatron-FSDP sharding) now lives in exactly one place. cfg.build(params) remains the clean per-optimizer constructor. Guards reject a config passed with kwargs and a config class passed instead of an instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): inline build_optimizer dispatch, drop make_one/make_dion closures Replace the two per-branch closures with a single pre-loop prep block plus an explicit 4-way dispatch inside the parts loop ({config, factory} x {dion, normal}). All construction paths are now visible at the point of use, matching the repo's explicit-over-implicit style. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): rename optim/utils.py to optim/dion.py The module is 100% Dion (is_dion_optimizer, _separate_param_groups, _get_dion_mesh, build_dion_optimizer); "utils" was a junk-drawer name. Rename to dion.py so the Dion construction subsystem is self-describing and sits beside optimizer.py/scheduler.py. Update all importers; MuonConfig.build_dion still delegates to dion.build_dion_optimizer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): route train_ft through component build APIs Replace train_ft.py's local build_optimizer / build_lr_scheduler / build_checkpoint_config / build_step_scheduler / build_distributed reimplementations with re-export imports from _component_builders (and distributed.api). The recipe now consumes the refactored component layer; the ~210 lines of duplicated builder logic are deleted. Names remain importable from train_ft, so sibling recipes (train_seq_cls, diffusion, kd, retrieval) and tests that do `from ...train_ft import build_optimizer` keep working. Remove obsolete recipe tests that asserted build_optimizer mutates cfg_opt in place (an abandoned contract; already failing on the branch). dtype-string resolution and TP-foreach behavior are covered at the component level in tests/unit_tests/optim/test_optim_config.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): route vlm/finetune through component build APIs VLM's build_optimizer was an outdated copy of the LLM one: git history shows it never received the TE FusedAdam dtype-string resolution (PR #1417) or Dion support that the LLM recipe gained, and its Megatron path discarded the sharded optimizer's return value. Replace vlm/finetune.py's local build_optimizer / build_lr_scheduler / build_checkpoint_config / build_step_scheduler / build_distributed with re-export imports from _component_builders (and distributed.api), bringing VLM to parity with LLM and ending the drift. Names stay importable from vlm.finetune for siblings (kd) and tests. Remove two obsolete tests asserting build_optimizer mutates cfg_opt.foreach in place; repoint the dist-env test's monkeypatch to distributed.api where build_distributed now resolves initialize_distributed. TP-foreach/dtype behavior is covered at the component level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(loss): TorchTitan-style typed configs with build(), single builder Mirror the optim refactor: collapse loss config.py + api.py into a single loss/loss.py. Each typed loss config (MaskedCrossEntropy / FusedLinearCE / TEParallelCE / KDLoss) exposes its full parameter surface as named fields and a build() method — no name/extra_kwargs/to_kwargs, no _get_loss_class mapping. build_loss_fn(loss, **kwargs) is the single entry point, dispatching on: - a typed LossConfig instance -> native path (delegates to config.build()); rejects stray **kwargs. - a dotted path / class + **kwargs -> integration / YAML escape hatch; unknown losses work without a typed config. Guards reject a config passed with kwargs and a config class passed instead of an instance. Update _component_builders + loss/__init__ + tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(components): single-file style for checkpoint/distributed/loggers/training Eliminate the config.py + api.py two-file split in the four remaining components, matching the optim/loss style: - training: StepSchedulerConfig + StepSchedulerConfig.build() -> step_scheduler.py - loggers: Wandb/MLflow/Comet configs each gain .build(); -> loggers/loggers.py (closed, section-named set: no free builder / dispatcher needed) - checkpoint: build_checkpoint_config -> config.py (kept light; checkpointing.py is the heavy engine and imports CheckpointingConfig from here) - distributed: build_distributed -> init_utils.py (beside initialize_distributed); config.py untouched (central strategy-config module, 16+ importers) Loggers and step-scheduler put build() on the config (no _target_ dispatch, no cross-cutting orchestration). _component_builders constructs the typed config and calls .build(); its recipe-facing names/signatures are unchanged so recipes and sibling re-exports are unaffected. No api.py remains under components/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim,loss): drop dotted-path resolution from build_optimizer/build_loss_fn The dotted-path string branch (_resolve_dotted_path) was the YAML _target_ convention leaking into the component layer, and was exercised only by tests — the recipe path passes an already-resolved callable via _component_builders. build_optimizer / build_loss_fn now accept only a typed config or a resolved class/callable (OptimizerConfig|Callable, LossConfig|Callable); a non-callable raises TypeError directing the caller to resolve dotted paths itself. Components now carry zero _target_/importlib knowledge. Update tests to pass the class object instead of a dotted-path string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): move build_lr_scheduler onto LRSchedulerConfig.build() The LR scheduler has no _target_ dispatch and no special orchestration, so it fits the config.build() pattern like step-scheduler/loggers. Replace the free build_lr_scheduler(config, optimizer, step_scheduler) with LRSchedulerConfig.build(optimizer, step_scheduler), which always builds; the "disabled" case (cfg is None -> None) moves to the _component_builders adapter where YAML `lr_scheduler: null` is interpreted. build_optimizer is now the only free function in the optim component (it alone needs the config-or-callable dispatch + Dion/Megatron orchestration). _component_builders.build_lr_scheduler keeps its name/signature, so recipes and sibling re-exports are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * fix(recipes): strip runtime args from step_scheduler YAML block before typed config build_step_scheduler built StepSchedulerConfig(**_as_dict(cfg)) directly, but real YAML step_scheduler blocks carry local_batch_size (a runtime arg passed separately to .build()), which is not a StepSchedulerConfig field — so it crashed with TypeError on configs like gptoss/deepseekv3/nanogpt. Regression introduced when train_ft was routed through _component_builders.build_step_scheduler (the prior recipe-local builder fed StepScheduler directly, which does accept local_batch_size). Pop the runtime sizing args (local_batch_size, dp_size, dataloader) before constructing the typed config. Add a regression test with local_batch_size in the YAML block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): typed RecipeConfig boundary for train_ft (reference) Reviewer-requested structure: __init__ converts YAML to a typed container; setup/methods consume it and call .build() there (not in a per-call adapter). - Add recipes/_typed_config.py: RecipeConfig wraps the raw ConfigNode and exposes wandb / mlflow / step_scheduler / lr_scheduler as typed component configs (cached_property); everything else (model, optimizer, loss, ... — _target_-based or untyped) falls through to the raw ConfigNode. YAML-schema knowledge (e.g. stripping the step_scheduler block's runtime keys) lives here, not in components. - train_ft.__init__ wraps cfg in RecipeConfig; setup calls self.cfg.<section>.build(...) for those four sections. - dist_env (build_distributed), comet (comet_utils), optimizer/loss/checkpoint (_target_-based) remain via their builders for now — documented exceptions to extend in the full migration. The corresponding _component_builders names stay re-exported from train_ft for sibling recipes (noqa). This is a reference for one recipe; the other recipes + remaining sections follow the same shape. _component_builders is retired as recipes migrate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(checkpoint): add CheckpointingConfig.build() and rewire recipes Give CheckpointingConfig a `.build(dp_rank, tp_rank, pp_rank, moe_mesh) -> Checkpointer` method so the checkpoint component follows the same `config.build()` surface as the optim/loss/loggers/training components. Checkpointer is imported lazily inside build() to avoid the config.py<->checkpointing.py circular import. Rewire all 7 recipes (train_ft, vlm/finetune, train_seq_cls, diffusion, retrieval/bi_encoder, eagle1, eagle3) to call `<config>.build(...)` instead of constructing `Checkpointer(...)` directly, and drop the now-unused `Checkpointer` imports. Update the three recipe tests that stubbed `build_checkpoint_config` to expose a `.build()` on the stub config; the recipes no longer reference the `Checkpointer` symbol so the separate Checkpointer monkeypatches are removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(distributed): move build_distributed out of the component into the recipe adapter build_distributed parses a YAML `dist_env` dict (backend/timeout_minutes) and delegates to initialize_distributed. Per the component-purity rule, config-dict parsing must not live in components/, so move build_distributed to recipes/_component_builders.py alongside the other YAML->component adapters (build_optimizer/build_loss_fn/build_checkpoint_config/...). The component now exposes only initialize_distributed (explicit kwargs); build_distributed is dropped from the distributed package's public surface. train_ft and vlm/finetune import it from the adapter; sibling recipes and functional tests that import it via train_ft keep working through the re-export, so no call sites or tests change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * Revert "refactor(distributed): move build_distributed out of the component into the recipe adapter" This reverts commit 1260de9. Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): type optimizer/loss/checkpoint in RecipeConfig; migrate train_ft (Stage 1) Make RecipeConfig the home for YAML->component resolution. Add OptimizerSpec, LossSpec and CheckpointSpec (recipe-layer wrappers that resolve _target_ / assemble checkpoint kwargs and delegate to the pure component builders), and expose cfg.optimizer / cfg.loss_fn / cfg.checkpoint cached_properties. The _target_ resolution helpers (_as_dict, _callable_and_kwargs, _model_name_from_cfg) are relocated into _typed_config.py; components stay YAML-free. train_ft.setup() now calls self.cfg.loss_fn.build() / self.cfg.optimizer.build(...) / self.cfg.checkpoint.build(...) instead of the free builders. The _component_builders import block is reduced to a transitional re-export shim (build_checkpoint_config/build_lr_scheduler/build_optimizer/build_step_scheduler) so the not-yet-migrated sibling recipes keep working; removed in Stage 3. test_train_ft setup stubs now patch OptimizerSpec/LossSpec/CheckpointSpec.build instead of train_ft.build_*. Direct resolution tests still use the shim re-export and migrate in Stage 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): migrate vlm/seq_cls/diffusion/retrieval to RecipeConfig (Stage 2) Wrap each recipe's cfg in RecipeConfig in __init__ and consume the typed sections in setup(): optimizer/loss_fn/checkpoint/wandb/mlflow/step_scheduler/ lr_scheduler now go through self.cfg.<section>.build(...) instead of the free _component_builders functions. Drop the per-recipe builder imports. Gotchas handled: - hasattr(cfg, "wandb"/"mlflow") -> cfg.wandb/.mlflow is not None (the typed accessors always exist and return None when the section is absent). - retrieval keeps self.cfg.get("optimizer").instantiate(params=...) (raw node) for its param-group optimizer construction (OptimizerSpec has no .instantiate). - diffusion reads step_scheduler.local_batch_size (a runtime key absent from the typed StepSchedulerConfig) via self.cfg.get("step_scheduler.local_batch_size"). - diffusion keeps its own local build_lr_scheduler and direct StepScheduler(). Tests: vlm-helpers/cp_wiring setup stubs now patch the spec/config .build methods (LossSpec/OptimizerSpec/CheckpointSpec/StepSchedulerConfig/ LRSchedulerConfig.build). The behavior tests that exercise the builders are repointed to import them from _component_builders (still present until Stage 3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): delete _component_builders.py; finalize tests on RecipeConfig (Stage 3) All recipes now consume the typed RecipeConfig, so the free builder module is removed along with the transitional re-export shim in train_ft. The YAML/_target_ resolution that lived in _component_builders now lives only in _typed_config (specs + _as_dict/_callable_and_kwargs/_model_name_from_cfg) and the pure component builders. Behavior tests that exercised the old builders are repointed onto the production path: small module-level helpers in test_train_ft.py / test_finetune_vlm_helpers.py mirror RecipeConfig.<section>.build via OptimizerSpec/CheckpointSpec/ StepSchedulerConfig/LRSchedulerConfig, so the test bodies are unchanged. The step_scheduler "_target_" test now asserts _target_ is dropped by the typed boundary (RecipeConfig drops it via _section_kwargs) rather than rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(recipes): import build_distributed/calculate_loss from their components, not train_ft These two names only lived in train_ft as re-exports; point consumers at their real home so sibling recipes stop reaching into train_ft for them: - build_distributed -> nemo_automodel.components.distributed.init_utils (diffusion/train, retrieval/train_bi_encoder, llm/train_seq_cls) - calculate_loss -> nemo_automodel.components.loss.utils (llm/kd) Also repoint the functional tests that imported these from train_ft (megatron data/checkpoint tests for build_distributed; checkpoint dcp/peft/ hf_sharded/hf_consolidated_llm tests for calculate_loss), keeping the other names (build_dataloader, TrainFinetuneRecipeForNextTokenPrediction) sourced from train_ft unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): unify optimizer config normalization and type the bf16 Adam warning Replace the recipe-layer OptimizerSpec with a single component-level build_optimizer_config that normalizes any optimizer target (OptimizerConfig instance/subclass, registry name or import path, or optimizer callable) into an OptimizerConfig. RecipeConfig.optimizer now returns the typed OptimizerConfig directly, and build_optimizer delegates string/tuple resolution to it. Add the Dion-family configs and move Megatron-FSDP optimizer sharding into maybe_shard_optimizer. Type warn_if_torch_adam_with_bf16_params' optimizer_cfg as OptimizerConfig and detect torch Adam/AdamW via the typed config (OptimizerFromFactoryConfig.factory and AdamConfig/AdamWConfig) instead of a raw _target_ node; update recipe call sites accordingly. Update unit tests for the new build_optimizer_config API and the typed warning. Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * refactor(loss): typed loss configs with registry and unified builder Mirror the optimizer refactor for losses: add a LossConfig hierarchy (MaskedCrossEntropy, FusedLinearCE, TEParallelCE, KDLoss) exposing each loss's full constructor surface as typed fields, plus LossFromFactoryConfig as the escape hatch for arbitrary callables. - Add LOSS_CONFIG_REGISTRY and build_loss_config to normalize a LossConfig instance, a registry name, or a loss class/callable into a LossConfig, upgrading registered classes to their typed config when kwargs fit and falling back to the factory wrapper otherwise. - Rename build_loss_fn -> build_loss_module (thin build_loss_config(...).build()). - Deprecate LossSpec: RecipeConfig.loss_fn now returns a LossConfig directly. - Update tests, docs, and the recipe-development skill accordingly. Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * refactor(checkpoint): construct CheckpointingConfig directly, drop builder + spec Remove the component-level build_checkpoint_config and the recipe-layer CheckpointSpec wrapper in favor of constructing CheckpointingConfig directly. - Give every CheckpointingConfig field a default and move the cache-dir fallback and the PEFT/torch_save -> safetensors fallback into __post_init__. - RecipeConfig.checkpoint now returns a CheckpointingConfig, filling the model-derived model_repo_id/model_cache_dir/is_peft from the surrounding YAML (and dropping restore_from, which is consumed separately). - Recipes use self.cfg.checkpoint directly instead of .build(cache_dir, ...). - Update checkpoint/recipe tests and the recipe-development skill doc. Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * refactor(distributed): drop build_distributed wrapper, call initialize_distributed directly build_distributed only read backend/timeout_minutes from the dist_env dict and delegated, so remove the wrapper and have recipes call the pure initialize_distributed entry point directly (matching what eagle1/eagle3 already do; defaults preserved as nccl / 1). De-export it from the distributed package __all__ and drop the now-unused `Any` import in init_utils. Inlined in: train_ft, vlm/finetune, train_seq_cls, retrieval/train_bi_encoder, diffusion. retrieval/mine_hard_negatives keeps its own local build_distributed (separate recipe-local helper). Tests repoint the build_distributed monkeypatches to initialize_distributed, drop the wrapper-only test_setup_initializes_dist_env, and inline the two functional megatron tests. Also drop a stray import of the non-existent nemo_automodel.components.datasets.loader.Dataloader (and its use as a base class of DummyIterableDataset) in test_train_ft.py, which was breaking test collection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(loss): add typed MTPLossConfig; build MTP via the typed boundary Represent the Multi-Token-Prediction auxiliary loss as a typed MTPLossConfig (scaling_factor / ignore_index) in components/loss/mtp.py, consistent with the other typed loss/optimizer/checkpoint configs. PipelineCausalLMLoss gains optional scaling_factor/ignore_index whose defaults preserve the existing model-driven behavior (scaling_factor=None -> model value; ignore_index=-100). RecipeConfig exposes cfg.mtp as a default MTPLossConfig — MTP params are model-driven and intentionally not exposed via YAML, so no mtp: block is read and no config schema changes. train_ft and vlm/finetune now build the pipeline-schedule MTP loss via cfg.mtp.build(...) instead of constructing PipelineCausalLMLoss directly. Behavior is identical for all existing configs. test_train_ft imports PipelineCausalLMLoss from the component. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * fix(recipes): recognize RecipeConfig in base_recipe checkpoint I/O + benchmark step_scheduler access Two regressions from the RecipeConfig refactor, surfaced by a 3-step smoke sweep (unit tests missed them: they stub the checkpointer and don't run the benchmark recipe). - base_recipe.py: state-tracking, checkpoint-save config discovery, and load only recognized ConfigNode, so self.cfg (now a RecipeConfig) was neither tracked nor found, and save_checkpoint hit "AttributeError: 'NoneType' object has no attribute 'raw_config'". Recognize RecipeConfig alongside ConfigNode at the 3 sites (RecipeConfig.raw_config already delegates to the ConfigNode). - llm/benchmark.py: read self.cfg.step_scheduler.local_batch_size off the typed StepSchedulerConfig (which strips runtime keys) at 3 sites -> use self.cfg.get("step_scheduler.local_batch_size"), matching the other recipes. Verified by 3-step smokes (8x H100): qwen3.5-4b VL, qwen3.5-35b-A3B VL, qwen3-30b-A3B, qwen3.5-35b-A3B benchmark, moonlight-16b, qwen2.5-7b, llama3.1-8b all train + checkpoint cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * test(benchmark): make ConfigNamespace.get resolve dotted keys (match RecipeConfig.get) The benchmark.py fix (8fed671) switched step_scheduler.local_batch_size to self.cfg.get("step_scheduler.local_batch_size"); the test shim's .get only did flat getattr, so it returned None and broke 9 benchmark tests. Make the shim resolve dotted keys like the real ConfigNode/RecipeConfig.get. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * fix(optim): epoch_len-based LR scheduler, dion _target_ routing, foreach gating - LRSchedulerConfig.build: derive total_steps from step_scheduler.epoch_len (already grad-acc'd + ceil'd) instead of calling len(dataloader) unconditionally, which crashed on iterable/streaming datasets; fall back to max_steps and raise a clear ValueError when neither is set. - build_optimizer_config: route a resolved dion.* class (YAML _target_: dion.Muon) to its typed Muon/NorMuon/Dion2/Dion config (parameter grouping) instead of the flat-params factory escape hatch. - OptimizerFromFactoryConfig.build: only inject foreach when the factory signature accepts it (TE FusedAdam under TP>1 no longer gets a spurious foreach). - Fold the four near-identical dion builds into _DionConfigBase: strips grouping-only fields (scalar_*/*_lr) before the dion ctor and asserts Dion is incompatible with Megatron-FSDP sharding via maybe_shard_optimizer(allow=False) instead of silently returning an unsharded optimizer; add scalar_lr/embed_lr/ lm_head_lr and Muon nesterov/flatten/use_triton fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * fix(loggers): wandb passthrough, wire CometConfig, dedup configure_mlflow - WandbConfig: forward arbitrary wandb.init kwargs (mode/dir/...) via an `extra` passthrough + from_kwargs() that routes unknown keys there; drop the save_dir field so wandb's native `dir` flows through. The closed dataclass previously crashed RecipeConfig.wandb on any non-field key. - CometConfig: fold the model-tag + experiment-name derivation into build(model_name) and wire it through RecipeConfig.comet + train_ft (self.cfg.comet.build(...)). build_comet is now a thin back-compat shim over the typed config. - configure_mlflow: reduce to a back-compat shim that builds an MLflowConfig from a raw cfg and delegates to MLflowConfig.build (the single implementation), removing the duplicate ~100-line reimplementation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * fix(checkpoint): clarify PEFT+torch_save coercion preserves other fields The PEFT + torch_save path flips only model_save_format -> safetensors and save_consolidated -> True; all other user-set fields (staging_dir, single_rank_consolidation, v4_compatible, ...) are preserved. Reword the now misleading "preserving checkpoint_dir" docstring/warning and add a test asserting those fields survive the coercion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * test(loss): cover MTPLossConfig defaults and scaling_factor override Asserts MTPLossConfig() defaults (scaling_factor None, ignore_index -100) and that MTPLossConfig(scaling_factor=0.5).build(loss_fn, model) overrides the model-provided factor while the default falls back to it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: HuiyingLi <willwin.lee@gmail.com> * refactor(optim): keep optim free of distributed/training cross-imports Resolve the two import-linter "Components must not import each other" violations and the ty errors on the optim component, on top of the dion-config restructure. - Move all Megatron-FSDP optimizer sharding out of OptimizerConfig / _DionConfigBase / OptimizerFromFactoryConfig build() into the recipe layer (recipes/_dist_setup.shard_optimizers_for_megatron_fsdp); optim no longer imports components.distributed.megatron_fsdp. supports_megatron_fsdp_sharding (True for Adam/factory, False for dion) is threaded to the helper as `allow`, preserving the "Dion cannot use Megatron-FSDP" assert at the recipe layer. - Move precision_warnings into components/optim and emit the bf16/torch-Adam warning from build() (object-based detection), dropping the components.training -> components.optim import. Recipes building via cfg.optimizer.build no longer call it explicitly. - Fix ty: dion-family config Protocol, Optimizer-narrowed list in LRSchedulerConfig.build, and a mesh_dim_names None-guard in _foreach_for_mesh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * fix(recipes): guard supports_megatron_fsdp_sharding lookup; repoint stale diffusion test patch Fix L0_Unit_Tests_CPU failures: - Recipes read cfg.optimizer.supports_megatron_fsdp_sharding, which broke unit tests that stub cfg.optimizer as a SimpleNamespace. Use getattr(..., True) so stubs default to shard-capable; the recipe-layer shard helper is a no-op for non-MegatronFSDP configs (distributed_config=None in those tests). - test_diffusion_train_metrics monkeypatched diffusion.train.build_distributed, which an earlier branch commit replaced with initialize_distributed; repoint the patch. Pre-existing breakage that only surfaced now that the import-linter fix lets the test stage run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * test(train_ft): stop forwarding distributed_config in the build_optimizer helper L0_Unit_Tests_GPU: 7 GPU-marked tests in test_train_ft.py call the local build_optimizer helper, which still forwarded distributed_config to OptimizerConfig.build() (removed in the optim refactor) -> TypeError. Drop the forward (matches the equivalent fix already applied to test_finetune_vlm_helpers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * fix(recipes): assign self.optimizer once (shard a local, not a re-assign) L2 functional tests (HF_Transformer/PEFT/DCP, Pretrain, KD, Seq_Cls) crashed with "RuntimeError: State key 'optimizer' is already tracked". BaseRecipe.__setattr__ state-tracks self.optimizer (is_optimizer), and the recipe assigned it twice: once from cfg.optimizer.build(), then again from the Megatron-FSDP shard helper. Build into a local `optimizer` and assign self.optimizer exactly once (the sharded list). Unit tests passed earlier because their SimpleNamespace stub optimizers aren't detected by is_optimizer, so neither assignment was tracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> * style(recipes): collapse single-line optimizer build (ruff format) The previous fix shortened `self.optimizer = ...build(` to `optimizer = ...build(`, which now fits in 120 cols, so ruff format collapses the call to one line. Pure formatting; no behavior change. (Fixes the `linting` job; everything else was green.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> --------- Signed-off-by: HuiyingLi <willwin.lee@gmail.com> Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Alexandros Koumparoulis <akoumparouli@nvidia.com> Co-authored-by: Alexandros Koumparoulis <153118171+akoumpa@users.noreply.github.com>
1 parent e2c8895 commit 1f4f83e

52 files changed

Lines changed: 3627 additions & 1672 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/guides/llm/finetune.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ finetune_config.yaml
100100
101101
┌────┴─────────────────────────────────┐
102102
▼ ▼ ▼ ▼
103-
build_model build_optimizer build_dataloader build_loss_fn ...
103+
build_model build_optimizer build_dataloader build_loss_module ...
104104
│ │ │ │
105105
▼ ▼ ▼ ▼
106106
cfg.model cfg.optimizer cfg.dataset cfg.loss_fn

fern/versions/nightly/pages/guides/llm/finetune.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ finetune_config.yaml
103103
104104
┌────┴─────────────────────────────────┐
105105
▼ ▼ ▼ ▼
106-
build_model build_optimizer build_dataloader build_loss_fn ...
106+
build_model build_optimizer build_dataloader build_loss_module ...
107107
│ │ │ │
108108
▼ ▼ ▼ ▼
109109
cfg.model cfg.optimizer cfg.dataset cfg.loss_fn

fern/versions/v0.4/pages/guides/llm/finetune.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ finetune_config.yaml
103103
104104
┌────┴─────────────────────────────────┐
105105
▼ ▼ ▼ ▼
106-
build_model build_optimizer build_dataloader build_loss_fn ...
106+
build_model build_optimizer build_dataloader build_loss_module ...
107107
│ │ │ │
108108
▼ ▼ ▼ ▼
109109
cfg.model cfg.optimizer cfg.dataset cfg.loss_fn

nemo_automodel/components/checkpoint/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
from ._torch_backports import apply_async_checkpoint_patch as _nemo__apply_async_patch
1818
from ._torch_backports import apply_patches as _nemo__apply_patches
19+
from .config import CheckpointingConfig
20+
21+
__all__ = ["CheckpointingConfig"]
1922

2023
if vparse(torch.__version__).base_version <= "2.7.1":
2124
_nemo__apply_patches()

nemo_automodel/components/checkpoint/checkpointing.py

Lines changed: 2 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import os
1919
import time
2020
from dataclasses import dataclass
21-
from enum import Enum
2221
from pathlib import Path
2322
from typing import TYPE_CHECKING, Any, Optional
2423

@@ -32,7 +31,6 @@
3231
except ImportError:
3332
HF_HUB_CACHE = None
3433

35-
from packaging.version import parse
3634
from safetensors.torch import load_file, save_file
3735
from torch import nn
3836
from torch.distributed.device_mesh import DeviceMesh
@@ -72,41 +70,14 @@
7270
from transformers.tokenization_utils_base import PreTrainedTokenizerBase
7371

7472

73+
from nemo_automodel.components.checkpoint.config import CheckpointingConfig, SaveConsolidatedMode, _is_geq_torch_2_9
74+
7575
_CONSOLIDATED_SIZE_WARNING_THRESHOLD_BYTES = 50 * 1024**3
7676
_DEFAULT_HF_CONSOLIDATED_SHARD_SIZE_BYTES = 5 * 1024**3
7777

7878
logger = logging.getLogger(__name__)
7979

8080

81-
class SaveConsolidatedMode(str, Enum):
82-
"""Controls when consolidated HF safetensors are exported."""
83-
84-
FALSE = "false"
85-
FINAL = "final"
86-
EVERY = "every"
87-
88-
89-
def _normalize_save_consolidated(value: bool | str | SaveConsolidatedMode) -> SaveConsolidatedMode:
90-
"""Normalize legacy bools and string aliases to a consolidated export mode."""
91-
if isinstance(value, SaveConsolidatedMode):
92-
return value
93-
if isinstance(value, bool):
94-
return SaveConsolidatedMode.EVERY if value else SaveConsolidatedMode.FALSE
95-
if isinstance(value, str):
96-
normalized = value.strip().lower()
97-
if normalized in {"true", "every"}:
98-
return SaveConsolidatedMode.EVERY
99-
if normalized == "final":
100-
return SaveConsolidatedMode.FINAL
101-
if normalized == "false":
102-
return SaveConsolidatedMode.FALSE
103-
raise ValueError(
104-
"Unsupported save_consolidated value: {}. Supported values are false, final, every, and legacy booleans.".format(
105-
value
106-
)
107-
)
108-
109-
11081
def _normalize_dtype_mapping_to_state_dict_keys(
11182
fqn_to_dtype_mapping: dict[str, str], state_dict_keys: list[str], base_model_prefix: str | None = None
11283
) -> dict[str, str]:
@@ -128,13 +99,6 @@ def _normalize_dtype_mapping_to_state_dict_keys(
12899
return normalized
129100

130101

131-
def _is_geq_torch_2_9() -> bool:
132-
"""
133-
Check if the current torch version is greater than or equal to 2.9.0.
134-
"""
135-
return parse(torch.__version__).base_version >= "2.9.0"
136-
137-
138102
def _is_safetensors_checkpoint(path: str) -> bool:
139103
"""Return True if path looks like a safetensors checkpoint (so we can preserve dtype); else DCP or other."""
140104
if os.path.isfile(path):
@@ -207,65 +171,6 @@ class _AsyncSaveContext:
207171
staging_active: bool = False
208172

209173

210-
@dataclass
211-
class CheckpointingConfig:
212-
"""
213-
Configuration for checkpointing.
214-
"""
215-
216-
enabled: bool
217-
checkpoint_dir: str | Path
218-
model_save_format: str
219-
model_cache_dir: str | Path
220-
model_repo_id: str
221-
save_consolidated: bool | str | SaveConsolidatedMode
222-
is_peft: bool
223-
model_state_dict_keys: list[str] = (
224-
None # copy of the model state dict keys before any parallelization. Kept for BW compatibility.
225-
)
226-
is_async: bool = False
227-
dequantize_base_checkpoint: bool | None = None
228-
original_model_root_dir: str | None = None
229-
skip_task_head_prefixes_for_base_model: list[str] | None = (
230-
None # Parameter prefixes to skip when loading base model
231-
)
232-
single_rank_consolidation: bool = False # If True, only rank 0 performs consolidation.
233-
# This should be used for remote storage systems that don't support direct-append or non-sequential writes.
234-
staging_dir: str | None = None # Optional directory for staging files during consolidation.
235-
# If provided, temp files will be created here instead of system temp. Useful when system temp has limited space.
236-
v4_compatible: bool = False # If True, save the original pretrained config.json (with quantization_config removed)
237-
# instead of the in-memory v5 config. Useful when downstream consumers (e.g. vLLM) expect a v4-format config.
238-
diffusers_compatible: bool = False # If True, use diffusers-compatible index filename
239-
# (diffusion_pytorch_model.safetensors.index.json) so checkpoints are loadable via diffusers from_pretrained().
240-
best_metric_key: str = "default" # Validation metric key used to select the best checkpoint.
241-
242-
def __post_init__(self):
243-
"""
244-
Convert a raw string such as "safetensors" into the right Enum.
245-
"""
246-
formats = [v.value for v in SerializationFormat]
247-
assert self.model_save_format in formats, (
248-
f"Unsupported model save format: {self.model_save_format}. Supported formats: {formats}"
249-
)
250-
self.model_save_format = SerializationFormat[self.model_save_format.upper()]
251-
self.save_consolidated = _normalize_save_consolidated(self.save_consolidated)
252-
if self.save_consolidated != SaveConsolidatedMode.FALSE and not self.is_peft:
253-
if not self.v4_compatible:
254-
logger.warning(
255-
"save_consolidated=%s but v4_compatible=False; "
256-
"checkpoint assets may be not compatible with transformers v4; "
257-
"[experimental] set --checkpoint.v4_compatible=True to enable",
258-
self.save_consolidated.value,
259-
)
260-
else:
261-
logger.warning("[experimental] v4_compatible=True enables transformers v4 compatibility")
262-
263-
# Async is only enabled for torch >= 2.9.0 currently because of large API changes in async DCP from 2.8.0 to 2.9.0
264-
if self.is_async and not _is_geq_torch_2_9():
265-
logging.error("Async mode is only supported for torch >= 2.9.0, disabling async mode")
266-
self.is_async = False
267-
268-
269174
def _should_write_hf_metadata(config: CheckpointingConfig) -> bool:
270175
"""Whether to write HF metadata/artifacts for a checkpoint."""
271176
return config.model_save_format == SerializationFormat.SAFETENSORS and not config.is_peft
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Public config surface for the checkpoint component.
16+
17+
``CheckpointingConfig`` holds the typed parameters that drive checkpointing
18+
behaviour and exposes ``.build()`` to construct the :class:`Checkpointer`
19+
engine (defined in ``checkpointing.py``). Every field has a sensible default
20+
so the recipe layer can construct it directly from the YAML ``checkpoint:``
21+
block plus the model-derived ``model_repo_id`` / ``model_cache_dir`` /
22+
``is_peft`` arguments — there is no separate builder/adapter.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import logging
28+
from dataclasses import dataclass
29+
from enum import Enum
30+
from pathlib import Path
31+
from typing import TYPE_CHECKING
32+
33+
import torch
34+
from huggingface_hub import constants as hf_constants
35+
from packaging.version import parse
36+
37+
from nemo_automodel.components.checkpoint._backports.filesystem import SerializationFormat
38+
39+
if TYPE_CHECKING:
40+
from torch.distributed.device_mesh import DeviceMesh
41+
42+
from nemo_automodel.components.checkpoint.checkpointing import Checkpointer
43+
44+
45+
def _is_geq_torch_2_9() -> bool:
46+
"""Check if the current torch version is greater than or equal to 2.9.0."""
47+
return parse(torch.__version__).base_version >= "2.9.0"
48+
49+
50+
class SaveConsolidatedMode(str, Enum):
51+
"""Controls when consolidated HF safetensors are exported."""
52+
53+
FALSE = "false"
54+
FINAL = "final"
55+
EVERY = "every"
56+
57+
58+
def _normalize_save_consolidated(value: bool | str | SaveConsolidatedMode) -> SaveConsolidatedMode:
59+
"""Normalize legacy bools and string aliases to a consolidated export mode."""
60+
if isinstance(value, SaveConsolidatedMode):
61+
return value
62+
if isinstance(value, bool):
63+
return SaveConsolidatedMode.EVERY if value else SaveConsolidatedMode.FALSE
64+
if isinstance(value, str):
65+
normalized = value.strip().lower()
66+
if normalized in {"true", "every"}:
67+
return SaveConsolidatedMode.EVERY
68+
if normalized == "final":
69+
return SaveConsolidatedMode.FINAL
70+
if normalized == "false":
71+
return SaveConsolidatedMode.FALSE
72+
raise ValueError(
73+
"Unsupported save_consolidated value: {}. Supported values are false, final, every, and legacy booleans.".format(
74+
value
75+
)
76+
)
77+
78+
79+
@dataclass
80+
class CheckpointingConfig:
81+
"""Configuration for checkpointing.
82+
83+
Every field has a default so the recipe layer can construct this directly
84+
from the YAML ``checkpoint:`` block merged with the model-derived
85+
``model_repo_id`` / ``model_cache_dir`` / ``is_peft`` values. When
86+
``model_cache_dir`` is ``None`` it falls back to the HF hub cache.
87+
"""
88+
89+
enabled: bool = True
90+
checkpoint_dir: str | Path = "checkpoints/"
91+
model_save_format: str = "safetensors"
92+
model_cache_dir: str | Path | None = None
93+
model_repo_id: str | None = None
94+
save_consolidated: bool | str | SaveConsolidatedMode = "final"
95+
is_peft: bool = False
96+
model_state_dict_keys: list[str] | None = (
97+
None # copy of the model state dict keys before any parallelization. Kept for BW compatibility.
98+
)
99+
is_async: bool = False
100+
dequantize_base_checkpoint: bool | None = None
101+
original_model_root_dir: str | None = None
102+
skip_task_head_prefixes_for_base_model: list[str] | None = (
103+
None # Parameter prefixes to skip when loading base model
104+
)
105+
single_rank_consolidation: bool = False # If True, only rank 0 performs consolidation.
106+
# This should be used for remote storage systems that don't support direct-append or non-sequential writes.
107+
staging_dir: str | None = None # Optional directory for staging files during consolidation.
108+
# If provided, temp files will be created here instead of system temp. Useful when system temp has limited space.
109+
v4_compatible: bool = False # If True, save the original pretrained config.json (with quantization_config removed)
110+
# instead of the in-memory v5 config. Useful when downstream consumers (e.g. vLLM) expect a v4-format config.
111+
diffusers_compatible: bool = False # If True, use diffusers-compatible index filename
112+
# (diffusion_pytorch_model.safetensors.index.json) so checkpoints are loadable via diffusers from_pretrained().
113+
best_metric_key: str = "default" # Validation metric key used to select the best checkpoint.
114+
115+
def __post_init__(self):
116+
"""Resolve the cache dir, enforce PEFT constraints, and coerce the save format/mode."""
117+
if self.model_cache_dir is None:
118+
self.model_cache_dir = hf_constants.HF_HUB_CACHE
119+
120+
# PEFT checkpointing is not supported for `torch_save`; flip only the two
121+
# incompatible fields (format -> safetensors, save_consolidated -> FINAL).
122+
# All other user-set fields (is_async, staging_dir, v4_compatible,
123+
# single_rank_consolidation, ...) are preserved.
124+
if self.is_peft and self.model_save_format == "torch_save":
125+
logging.warning(
126+
"PEFT checkpointing is not supported for `torch_save` format; "
127+
"falling back to `safetensors` (all other checkpoint settings are preserved)."
128+
)
129+
self.model_save_format = "safetensors"
130+
self.save_consolidated = SaveConsolidatedMode.FINAL
131+
132+
# Convert a raw string such as "safetensors" into the right Enum.
133+
formats = [v.value for v in SerializationFormat]
134+
assert self.model_save_format in formats, (
135+
f"Unsupported model save format: {self.model_save_format}. Supported formats: {formats}"
136+
)
137+
self.model_save_format = SerializationFormat[self.model_save_format.upper()]
138+
139+
# Normalize legacy bools and string aliases to a consolidated export mode.
140+
self.save_consolidated = _normalize_save_consolidated(self.save_consolidated)
141+
if self.save_consolidated != SaveConsolidatedMode.FALSE and not self.is_peft:
142+
if not self.v4_compatible:
143+
logging.warning(
144+
"save_consolidated=%s but v4_compatible=False; "
145+
"checkpoint assets may be not compatible with transformers v4; "
146+
"[experimental] set --checkpoint.v4_compatible=True to enable",
147+
self.save_consolidated.value,
148+
)
149+
else:
150+
logging.warning("[experimental] v4_compatible=True enables transformers v4 compatibility")
151+
152+
# Async is only enabled for torch >= 2.9.0 currently because of large API changes in async DCP from 2.8.0 to 2.9.0
153+
if self.is_async and not _is_geq_torch_2_9():
154+
logging.error("Async mode is only supported for torch >= 2.9.0, disabling async mode")
155+
self.is_async = False
156+
157+
def build(
158+
self,
159+
dp_rank: int,
160+
tp_rank: int,
161+
pp_rank: int,
162+
moe_mesh: DeviceMesh | None = None,
163+
) -> Checkpointer:
164+
"""Build the :class:`Checkpointer` engine for this config.
165+
166+
``Checkpointer`` is imported lazily to avoid a circular import
167+
(``checkpointing.py`` imports ``CheckpointingConfig`` from this module)
168+
and to keep the heavy DCP/safetensors deps out of module load.
169+
170+
Args:
171+
dp_rank: Data-parallel rank.
172+
tp_rank: Tensor-parallel rank.
173+
pp_rank: Pipeline-parallel rank.
174+
moe_mesh: Optional device mesh for MoE checkpointing.
175+
176+
Returns:
177+
Configured :class:`Checkpointer`.
178+
"""
179+
from nemo_automodel.components.checkpoint.checkpointing import Checkpointer
180+
181+
return Checkpointer(config=self, dp_rank=dp_rank, tp_rank=tp_rank, pp_rank=pp_rank, moe_mesh=moe_mesh)
182+
183+
184+
__all__ = ["CheckpointingConfig", "SaveConsolidatedMode"]

0 commit comments

Comments
 (0)