Skip to content

Add a general composable $import system for YAML configs, and use it to implement composable recipes#1253

Open
shengliangxu wants to merge 32 commits intomainfrom
shengliangx/composable-recipes
Open

Add a general composable $import system for YAML configs, and use it to implement composable recipes#1253
shengliangxu wants to merge 32 commits intomainfrom
shengliangx/composable-recipes

Conversation

@shengliangxu
Copy link
Copy Markdown
Collaborator

@shengliangxu shengliangxu commented Apr 14, 2026

What does this PR do?

Type of change: New feature

Add a composable $import system for recipe YAML configs, plus reorganize config snippets and begin migrating hardcoded *_CFG dicts to YAML presets.

Problem

  1. The 6 built-in PTQ recipes duplicated the same numeric format definitions and 14-entry exclusion list across every file
  2. The hardcoded *_CFG dicts in config.py (e.g., FP8_DEFAULT_CFG) had no connection to the YAML recipe system
  3. The config loading infrastructure lived in modelopt.recipe, causing circular imports when modelopt.torch.quantization.config needed to load YAML configs

Solution

Composable $import system

Recipes declare an imports section mapping names to config snippet files. The {$import: name} marker is explicitly resolved at load time. For example, a recipe can import shared FP8 attributes and standard exclusions rather than duplicating them inline.

$import semantics:

  • Dict value: replaced with snippet content. Supports multiple imports ($import: [a, b]) and inline override keys with ordered precedence (later imports override earlier, inline keys override all)
  • List element: snippet (must be a YAML list) is spliced into the surrounding list
  • Multi-document YAML: list-valued snippets that need imports use --- separator (first doc = imports, second doc = list content)
  • Recursive: snippets can themselves have imports. Circular imports detected
  • Scoped: each file's import names are independent

Config snippet library (modelopt_recipes/configs/):

  • numerics/all numeric format definitions live here (fp8, nvfp4, nvfp4_static). Any new numeric format (e.g., int8, mxfp8) should be added to this directory as the single source of truth for quantizer attributes
  • ptq/units/ — reusable quant_cfg entries (base_disable_all, default_disabled, fp8_kv, w8a8_fp8_fp8, w4a4_nvfp4_nvfp4)
  • ptq/presets/model/ — complete configs replacing hardcoded *_CFG dicts
  • ptq/presets/kv/ — KV cache config presets

Hardcoded config migration:

  • FP8_DEFAULT_CFG and FP8_KV_CFG now load from YAML presets via load_config()
  • Config loading infrastructure moved from modelopt.recipe._config_loader to modelopt.torch.opt.config_loader to eliminate circular imports

All 6 built-in PTQ recipes converted to use imports — each reduced by ~30 lines.

Changes by area

Config loading infrastructure:

  • modelopt/torch/opt/config_loader.py — new home for YAML loading + $import resolution (zero modelopt imports, no circular dependency risk)
  • modelopt/recipe/_config_loader.py — thin re-export from torch.opt.config_loader
  • modelopt/recipe/loader.py — uses shared _resolve_imports from config_loader

Quantization config:

  • modelopt/torch/quantization/config.pyFP8_DEFAULT_CFG and FP8_KV_CFG loaded from YAML presets

Recipe YAML files:

  • 6 PTQ recipes converted to $import style
  • 8 new config snippet files + 2 preset files

Pre-commit:

  • .pre-commit-config.yaml — recipe validator excludes configs/ directory
  • tools/precommit/check_modelopt_recipes.py — recognizes $import entries

Documentation:

  • docs/source/guides/10_recipes.rst — full $import specification with examples, inline/import style comparison, multi-document snippets, override precedence

Tests:

  • tests/unit/recipe/test_loader.py — 20+ new tests covering all import features

Testing

All new and existing recipe loader tests pass. Built-in recipe smoke tests pass with converted recipes.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ (recipes without imports load unchanged; FP8_DEFAULT_CFG / FP8_KV_CFG produce identical dicts)
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅

Summary by CodeRabbit

  • New Features

    • Composable YAML imports for recipes ($import); new FP8/NVFP4 numerics, units, and PTQ presets for modular quantization.
  • Documentation

    • Expanded recipe guide covering inline vs import styles, composable imports, multi-document snippets, repo layout and examples.
  • Tests

    • Extensive unit tests for import resolution, list splicing, recursive/circular imports, and directory-format recipes.
  • Chores

    • Pre-commit hook updated to skip the configs/ subtree during recipe validation.

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot bot commented Apr 14, 2026

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3f9c5e0-a142-4dee-b92a-c32789fb8481

📥 Commits

Reviewing files that changed from the base of the PR and between c7ce455 and 33af932.

📒 Files selected for processing (1)
  • modelopt_recipes/configs/ptq/presets/README.md
✅ Files skipped from review due to trivial changes (1)
  • modelopt_recipes/configs/ptq/presets/README.md

📝 Walkthrough

Walkthrough

Adds a composable YAML imports system (imports + $import) for recipe/config snippets, centralizes config loading in modelopt.torch.opt.config_loader, updates recipe loading to raw-load + resolve imports, refactors many PTQ recipes to use shared snippets, adds numeric/unit presets, expands tests, and updates pre-commit to exclude modelopt_recipes/configs/.

Changes

Cohort / File(s) Summary
Config loader core
modelopt/torch/opt/config_loader.py, modelopt/recipe/_config_loader.py, modelopt/recipe/loader.py
New YAML loader with raw load + $import resolution, multi-document support, ExMy string parsing for numeric keys; old loader replaced by re-exports and recipe loader updated to call raw load then resolve imports.
Quantization defaults integration
modelopt/torch/quantization/config.py
Replaced inline FP8/kv default dicts with load_config(...) calls to YAML presets and added dict[str, Any] annotations.
Reusable numerics & units
modelopt_recipes/configs/numerics/*.yaml, modelopt_recipes/configs/ptq/units/*, modelopt_recipes/configs/ptq/units/README.md
Added numeric snippets (fp8, nvfp4, nvfp4_static) and PTQ unit fragments (base_disable_all, default_disabled_quantizers, fp8_kv, w8a8_fp8_fp8, w4a4_nvfp4_nvfp4) designed for composition via $import.
Presets & recipes
modelopt_recipes/configs/ptq/presets/*, modelopt_recipes/general/ptq/*.yaml
New preset fragments and refactored PTQ recipes to use imports + $import directives, replacing many inline quantizer entries with imported units.
Documentation & tooling
docs/source/guides/10_recipes.rst, CHANGELOG.rst, .pre-commit-config.yaml, modelopt_recipes/configs/ptq/presets/README.md
Expanded recipe docs describing imports/$import, added changelog entry, and updated pre-commit hook to exclude modelopt_recipes/configs/.
Tests & validator
tests/unit/recipe/test_loader.py, tools/precommit/check_modelopt_recipes.py
Extensive unit tests for import resolution (list/dict cases, recursive/circular imports, multi-document snippets, scoped names); pre-commit checker updated to accept and validate "$import" entries in quant_cfg.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Loader as modelopt.recipe.loader
    participant Raw as _load_raw_config
    participant Resolver as _resolve_imports
    participant FS as Files / Builtins

    User->>Loader: load_recipe(path)
    Loader->>Raw: _load_raw_config(path)
    Raw->>FS: read YAML (file or builtin)
    FS-->>Raw: raw documents
    Raw-->>Loader: parsed raw config (may include imports)
    alt imports present
        Loader->>Resolver: _resolve_imports(parsed_config)
        Resolver->>Raw: _load_raw_config(import_path)
        Raw->>FS: read imported snippet
        FS-->>Raw: snippet content
        Raw-->>Resolver: snippet (dict/list / _list_content)
        Resolver->>Resolver: merge / splice / detect circular refs
        Resolver-->>Loader: resolved config
    end
    Loader-->>User: final resolved recipe
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a composable $import system for YAML configs and using it to implement composable recipes. It is specific, concise, and directly reflects the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 95.35% which is sufficient. The required threshold is 80.00%.
Security Anti-Patterns ✅ Passed The PR introduces no security anti-patterns according to SECURITY.md guidelines. Safe YAML parsing, no eval/exec, no hardcoded trust_remote_code=True, and no security bypasses detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shengliangx/composable-recipes

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-1253/

Built to branch gh-pages at 2026-04-16 17:36 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Commit description:

Introduce an import mechanism that lets recipe YAML files reference reusable
config snippets by name, reducing duplication across recipes.

Syntax:
  imports:
    fp8: configs/numerics/fp8
    base_disable_all: configs/ptq/base_disable_all

  quant_cfg:
    - base_disable_all # string entry → replaced with imported dict or spliced
list
    - quantizer_name: '*weight_quantizer'
      cfg: fp8 # string cfg → replaced with imported dict

Features:
- Dict-based imports (keys are names, values are config paths) — no name conflicts
- Three resolution modes: string cfg value, string list entry (dict), string list entry (list
 splice)
- Recursive resolution with circular import detection
- Path resolution via load_config (built-in library first, then filesystem)
- Works with both single-file and directory recipe formats

New reusable config snippets (modelopt_recipes/configs/):
- numerics/fp8.yml, nvfp4_dynamic.yml, nvfp4_static.yml
- ptq/base_disable_all.yaml, default_disabled_quantizers.yaml

All 6 built-in PTQ recipes converted to use imports, reducing each by ~30 lines.

Pre-commit hook updated to skip configs/ directory and allow string entries in
quant_cfg. load_config() now accepts YAML lists for list-valued snippets.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu shengliangxu force-pushed the shengliangx/composable-recipes branch from ea910ae to 82d5a12 Compare April 15, 2026 21:10
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu shengliangxu changed the title Add composable $import system for recipe YAML configs Add a general composable $import system for YAML configs, and refactor some flat YAML config files into composable format Apr 15, 2026
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu shengliangxu marked this pull request as ready for review April 15, 2026 23:14
@shengliangxu shengliangxu requested review from a team as code owners April 15, 2026 23:14
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modelopt/recipe/loader.py (1)

126-165: ⚠️ Potential issue | 🟠 Major

Resolve directory recipes from one import pass.

_load_recipe_from_dir() still reads metadata from unresolved recipe.yml, and it resolves quantize.yml before recipe-level aliases are available. That breaks valid directory recipes that use $import inside metadata, and it also fails when quantize.yml has its own imports plus references names declared in recipe.yml. Please build the combined directory recipe first, then resolve imports once before reading metadata / quantize.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/recipe/loader.py` around lines 126 - 165, The loader currently reads
metadata from the unresolved recipe and resolves quantize.yml before
recipe-level imports, breaking imports that span both files; change
_load_recipe_from_dir so it first loads raw recipe_data via _load_raw_config
and—if recipe_type == RecipeType.PTQ—loads quantize_data via _load_raw_config,
then build a single combined dict that contains the recipe fields plus a
"quantize" key set to quantize_data and a merged "imports" entry (include both
recipe_data.get("imports") and quantize_data.get("imports") as appropriate),
call _resolve_imports once on that combined dict, and only then read metadata
and construct ModelOptPTQRecipe (i.e., use resolved combined["quantize"] and
resolved metadata) so that _resolve_imports is applied consistently for
_load_recipe_from_dir, _resolve_imports, RecipeType.PTQ, and ModelOptPTQRecipe.
🧹 Nitpick comments (1)
modelopt/torch/opt/config_loader.py (1)

177-187: Cycle detection should use canonical resolved identities, not raw import strings.

At Line 180-Line 187, _loading tracks the raw config_path value. Equivalent aliases (e.g., a, a.yaml, ./a.yaml) can bypass the circular guard and recurse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/torch/opt/config_loader.py` around lines 177 - 187, The cycle
detection currently compares raw config_path strings in _loading, which allows
equivalent paths (aliases like "a", "a.yaml", "./a.yaml") to bypass the guard;
update _resolve_imports (and the loop over imports_dict) to canonicalize/resolve
each config_path before checking or inserting into _loading (use the same
resolution used by _load_raw_config or a new _canonicalize/resolve function), so
replace checks against raw config_path with checks against the canonical
identity and pass the canonical identity into recursive calls and into the
_loading set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/source/guides/10_recipes.rst`:
- Around line 598-606: Update the repository tree snippet so the directory paths
match the rest of the guide: change the entries under "configs/ptq/" to
"configs/ptq/units/" (or alternatively adjust examples elsewhere to remove
"units/"); specifically ensure the listed folders (e.g., base_disable_all.yaml,
default_disabled_quantizers.yaml) appear under configs/ptq/units/ in the tree
snippet so it matches the examples and table references throughout the guide.

In `@modelopt_recipes/configs/numerics/nvfp4.yaml`:
- Around line 18-21: The YAML has mistaken nesting: `type` and `scale_bits` are
indented under `block_sizes` and must be sibling keys per the NVFP4 quantizer
schema; fix by dedenting `type` and `scale_bits` so `block_sizes:`, `type:`, and
`scale_bits:` are all at the same indentation level (ensure `block_sizes`
retains its mapping with `-1: 16` and `type: dynamic` and `scale_bits: e4m3` are
siblings).

In `@modelopt_recipes/configs/ptq/presets/README.md`:
- Around line 7-10: Update the README.md text to distinguish full model presets
from partial KV-only fragments: change the paragraph that claims "Each preset is
a complete, self-contained config..." to explain there are two kinds — full
presets (e.g., configs/ptq/presets/model/*) that include an algorithm and
quant_cfg and are ready to pass to mtq.quantize(), and merge-only KV fragments
(e.g., configs/ptq/presets/kv/fp8.yaml) that lack an algorithm and must be
imported/merged into a full preset; reference the $import system and the two
directories (model/ vs kv/) and note that KV files are partial fragments not
suitable alone for mtq.quantize().

In `@modelopt/torch/opt/config_loader.py`:
- Around line 250-260: The current logic only resolves list-splice imports for
keys named "quant_cfg" and "_list_content", leaving other list locations
unresolved; modify the code that currently iterates over containers and calls
_resolve_list to instead recursively walk the full config tree (visiting all
dicts and lists within the top-level data) and call _resolve_list for any list
value that may contain $import markers (not only "quant_cfg" or
"_list_content"); reuse the existing _resolve_list helper and ensure you update
in-place entries discovered during the traversal so other keys/paths besides
quant_cfg are correctly resolved.
- Around line 72-91: The code currently appends local filesystem paths to
paths_to_check before BUILTIN_CONFIG_ROOT, causing local files to shadow
built-ins; update the ordering so BUILTIN_CONFIG_ROOT candidates are inserted
before local filesystem candidates. Specifically: in the str branch, when
config_file lacks .yml/.yaml, append
BUILTIN_CONFIG_ROOT.joinpath(f"{config_file}.yml") and
BUILTIN_CONFIG_ROOT.joinpath(f"{config_file}.yaml") before
Path(f"{config_file}.yml") and Path(f"{config_file}.yaml"); when config_file
already endswith .yml/.yaml, append BUILTIN_CONFIG_ROOT.joinpath(config_file)
before Path(config_file). In the Path branch, when config_file.suffix is
.yml/.yaml and not config_file.is_absolute(), prepend
BUILTIN_CONFIG_ROOT.joinpath(str(config_file)) ahead of config_file; when no
suffix and not absolute, append
BUILTIN_CONFIG_ROOT.joinpath(f"{config_file}.yml") and
BUILTIN_CONFIG_ROOT.joinpath(f"{config_file}.yaml") before the corresponding
Path(f"{config_file}.yml") and Path(f"{config_file}.yaml"). Ensure references to
config_file, paths_to_check, and BUILTIN_CONFIG_ROOT are used to locate these
changes.

In `@tools/precommit/check_modelopt_recipes.py`:
- Around line 58-60: The conditional that skips entries with "$import" should
first validate the payload shape: in the block where you check `if "$import" in
entry:` (in tools/precommit/check_modelopt_recipes.py), verify that
`entry["$import"]` is either a str or a list of str (all elements are str)
before doing `continue`; if the value is not the expected shape, do not skip
validation—raise or report a structural error instead so malformed payloads like
`{"$import": 123}` or `{"$import": ["ok", 1]}` are caught.

---

Outside diff comments:
In `@modelopt/recipe/loader.py`:
- Around line 126-165: The loader currently reads metadata from the unresolved
recipe and resolves quantize.yml before recipe-level imports, breaking imports
that span both files; change _load_recipe_from_dir so it first loads raw
recipe_data via _load_raw_config and—if recipe_type == RecipeType.PTQ—loads
quantize_data via _load_raw_config, then build a single combined dict that
contains the recipe fields plus a "quantize" key set to quantize_data and a
merged "imports" entry (include both recipe_data.get("imports") and
quantize_data.get("imports") as appropriate), call _resolve_imports once on that
combined dict, and only then read metadata and construct ModelOptPTQRecipe
(i.e., use resolved combined["quantize"] and resolved metadata) so that
_resolve_imports is applied consistently for _load_recipe_from_dir,
_resolve_imports, RecipeType.PTQ, and ModelOptPTQRecipe.

---

Nitpick comments:
In `@modelopt/torch/opt/config_loader.py`:
- Around line 177-187: The cycle detection currently compares raw config_path
strings in _loading, which allows equivalent paths (aliases like "a", "a.yaml",
"./a.yaml") to bypass the guard; update _resolve_imports (and the loop over
imports_dict) to canonicalize/resolve each config_path before checking or
inserting into _loading (use the same resolution used by _load_raw_config or a
new _canonicalize/resolve function), so replace checks against raw config_path
with checks against the canonical identity and pass the canonical identity into
recursive calls and into the _loading set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b6ed87d-c72b-42a3-b9b3-6b7a312c52aa

📥 Commits

Reviewing files that changed from the base of the PR and between 361f7e3 and 1127f32.

📒 Files selected for processing (27)
  • .pre-commit-config.yaml
  • CHANGELOG.rst
  • docs/source/guides/10_recipes.rst
  • modelopt/recipe/_config_loader.py
  • modelopt/recipe/loader.py
  • modelopt/torch/opt/config_loader.py
  • modelopt/torch/quantization/config.py
  • modelopt_recipes/configs/numerics/fp8.yaml
  • modelopt_recipes/configs/numerics/nvfp4.yaml
  • modelopt_recipes/configs/numerics/nvfp4_static.yaml
  • modelopt_recipes/configs/ptq/presets/README.md
  • modelopt_recipes/configs/ptq/presets/kv/fp8.yaml
  • modelopt_recipes/configs/ptq/presets/model/fp8.yaml
  • modelopt_recipes/configs/ptq/units/README.md
  • modelopt_recipes/configs/ptq/units/base_disable_all.yaml
  • modelopt_recipes/configs/ptq/units/default_disabled_quantizers.yaml
  • modelopt_recipes/configs/ptq/units/fp8_kv.yaml
  • modelopt_recipes/configs/ptq/units/w4a4_nvfp4_nvfp4.yaml
  • modelopt_recipes/configs/ptq/units/w8a8_fp8_fp8.yaml
  • modelopt_recipes/general/ptq/fp8_default-fp8_kv.yaml
  • modelopt_recipes/general/ptq/nvfp4_default-fp8_kv.yaml
  • modelopt_recipes/general/ptq/nvfp4_default-none_kv_gptq.yaml
  • modelopt_recipes/general/ptq/nvfp4_experts_only-fp8_kv.yaml
  • modelopt_recipes/general/ptq/nvfp4_mlp_only-fp8_kv.yaml
  • modelopt_recipes/general/ptq/nvfp4_omlp_only-fp8_kv.yaml
  • tests/unit/recipe/test_loader.py
  • tools/precommit/check_modelopt_recipes.py

Comment thread docs/source/guides/10_recipes.rst
Comment thread modelopt_recipes/configs/numerics/nvfp4.yaml
Comment thread modelopt_recipes/configs/ptq/presets/README.md Outdated
Comment thread modelopt/torch/opt/config_loader.py
Comment thread modelopt/torch/opt/config_loader.py Outdated
Comment thread tools/precommit/check_modelopt_recipes.py
@shengliangxu shengliangxu changed the title Add a general composable $import system for YAML configs, and refactor some flat YAML config files into composable format Add a general composable $import system for YAML configs, and use it to implement composable recipes Apr 15, 2026
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
modelopt/torch/opt/config_loader.py (2)

179-189: Potential false negative in circular import detection with path variants.

The circular import check compares raw config_path strings. If the same file is referenced via different path representations (e.g., "configs/foo" vs "./configs/foo" or absolute vs relative paths), the check may not detect the cycle.

Consider normalizing paths before adding to _loading:

🔧 Suggested improvement
     for name, config_path in imports_dict.items():
         if not config_path:
             raise ValueError(f"Import {name!r} has an empty config path.")
+        # Normalize for consistent cycle detection
+        norm_path = str(config_path)
-        if config_path in _loading:
+        if norm_path in _loading:
             raise ValueError(
                 f"Circular import detected: {config_path!r} is already being loaded. "
                 f"Import chain: {sorted(_loading)}"
             )
         snippet = _load_raw_config(config_path)
         if isinstance(snippet, dict) and "imports" in snippet:
-            snippet = _resolve_imports(snippet, _loading | {config_path})
+            snippet = _resolve_imports(snippet, _loading | {norm_path})

For more robust detection, consider resolving to absolute paths or using the resolved config_path from _load_raw_config.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/torch/opt/config_loader.py` around lines 179 - 189, The
circular-import detection is comparing raw config_path strings which can differ
in representation; update _resolve_imports to normalize/resolve each config path
(e.g., via pathlib.Path(...).resolve()) before checking membership in or adding
to _loading and before calling _load_raw_config, and use the resolved path
consistently for the error message and when recursing (referencing
_resolve_imports, _load_raw_config, _loading, imports_dict, and snippet) so
equivalent paths (relative vs absolute or with ./) are treated identically.

252-262: Note: $import list-splice resolution is scoped to specific keys.

The resolution only processes quant_cfg and _list_content lists, which matches current recipe usage. However, if future configs need $import in other list-valued fields, this will require extending the resolution logic. Consider documenting this limitation in the docstring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/torch/opt/config_loader.py` around lines 252 - 262, The current
list-splice resolution only runs _resolve_list for quant_cfg (either at
top-level or under the quantize key) and for _list_content, so other list-valued
fields won't have $import resolution; update the docstring of the surrounding
function (referencing data, container, _resolve_list, quant_cfg, and
_list_content) to state this scoping limitation and that any future list fields
requiring $import must be added to the resolution logic (or call _resolve_list
explicitly), so reviewers and future maintainers know to extend the loop when
adding new list keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@modelopt/torch/opt/config_loader.py`:
- Around line 179-189: The circular-import detection is comparing raw
config_path strings which can differ in representation; update _resolve_imports
to normalize/resolve each config path (e.g., via pathlib.Path(...).resolve())
before checking membership in or adding to _loading and before calling
_load_raw_config, and use the resolved path consistently for the error message
and when recursing (referencing _resolve_imports, _load_raw_config, _loading,
imports_dict, and snippet) so equivalent paths (relative vs absolute or with ./)
are treated identically.
- Around line 252-262: The current list-splice resolution only runs
_resolve_list for quant_cfg (either at top-level or under the quantize key) and
for _list_content, so other list-valued fields won't have $import resolution;
update the docstring of the surrounding function (referencing data, container,
_resolve_list, quant_cfg, and _list_content) to state this scoping limitation
and that any future list fields requiring $import must be added to the
resolution logic (or call _resolve_list explicitly), so reviewers and future
maintainers know to extend the loop when adding new list keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 073f02cb-4603-407c-92af-400f4092f280

📥 Commits

Reviewing files that changed from the base of the PR and between 1127f32 and 5e0cc8a.

📒 Files selected for processing (4)
  • docs/source/guides/10_recipes.rst
  • modelopt/torch/opt/config_loader.py
  • modelopt/torch/quantization/config.py
  • tools/precommit/check_modelopt_recipes.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/source/guides/10_recipes.rst
  • modelopt/torch/quantization/config.py

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
modelopt/torch/opt/config_loader.py (1)

109-110: Consider defensive limits for file size/import depth/expanded node count.

Current loading/import expansion is unbounded. A deeply nested or very large YAML import graph can still cause resource exhaustion even with safe parsing and cycle checks.

I’d recommend adding configurable caps (e.g., max file bytes, max recursive depth, max expanded elements).

As per coding guidelines: "Treat configs/snippets as untrusted inputs: validate shapes/types and enforce limits to avoid crashes or resource exhaustion from malformed/large YAML."

Also applies to: 156-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/torch/opt/config_loader.py` around lines 109 - 110, Before parsing
untrusted YAML, enforce configurable defensive limits: check the byte size of
config_path.read_text() against MAX_FILE_BYTES (reject if exceeded) before
assigning text, then when calling yaml.safe_load_all(text) stop after a
configurable MAX_DOCS and raise a clear error if more documents are present;
additionally, add a max_import_depth counter to the import/expansion logic (the
code that iterates over docs and resolves imports around the later
import-expansion block) to prevent unbounded recursion and track a
MAX_EXPANDED_NODES counter for total expanded elements/nodes (increment as you
expand/import and raise if exceeded). Make all limits configurable via
parameters or environment, surface explicit errors, and document which constants
control yaml.safe_load_all, text, docs, and the import/expansion logic so
callers can tune them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modelopt/torch/opt/config_loader.py`:
- Around line 214-221: Validate the types of the $import reference(s) before
calling _lookup: ensure ref is either a string or a list of strings, and if it's
a list verify every element is a string (no mixed or non-string items); if
validation fails, raise a clear ValueError describing the bad $import value.
Update the logic around _IMPORT_KEY/ref/ref_names and the loop that calls
_lookup (the block using ref_names, snippet, and merged) as well as the same
pattern in the later resolution path (lines handling list resolution around
235-243) to perform these checks prior to any call to _lookup.
- Around line 182-189: The cycle check should use a canonical identity for the
config source instead of the raw config_path string: compute a
normalized/canonical source key (e.g., resolve to an absolute Path via
Path(config_path).resolve() or a canonical resource key) before checking
`_loading`, use that canonical key in the membership test and error message,
call `_load_raw_config` with the resolved/canonical path (or map the original to
the canonical key), and pass the canonical key into `_resolve_imports` by adding
it to `_loading` (replace uses of `config_path` with the canonical key in the
circular-detection and recursive `_resolve_imports` call).

---

Nitpick comments:
In `@modelopt/torch/opt/config_loader.py`:
- Around line 109-110: Before parsing untrusted YAML, enforce configurable
defensive limits: check the byte size of config_path.read_text() against
MAX_FILE_BYTES (reject if exceeded) before assigning text, then when calling
yaml.safe_load_all(text) stop after a configurable MAX_DOCS and raise a clear
error if more documents are present; additionally, add a max_import_depth
counter to the import/expansion logic (the code that iterates over docs and
resolves imports around the later import-expansion block) to prevent unbounded
recursion and track a MAX_EXPANDED_NODES counter for total expanded
elements/nodes (increment as you expand/import and raise if exceeded). Make all
limits configurable via parameters or environment, surface explicit errors, and
document which constants control yaml.safe_load_all, text, docs, and the
import/expansion logic so callers can tune them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 202821f4-c1b8-43d1-bfc9-013e8c78d2b0

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0cc8a and c7ce455.

📒 Files selected for processing (2)
  • modelopt/torch/opt/config_loader.py
  • tests/unit/recipe/test_loader.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/recipe/test_loader.py

Comment thread modelopt/torch/opt/config_loader.py
Comment thread modelopt/torch/opt/config_loader.py
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 91.03448% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.56%. Comparing base (f238d93) to head (569c424).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/opt/config_loader.py 90.00% 12 Missing ⚠️
modelopt/recipe/loader.py 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1253      +/-   ##
==========================================
- Coverage   75.58%   72.56%   -3.03%     
==========================================
  Files         459      460       +1     
  Lines       48613    48697      +84     
==========================================
- Hits        36745    35336    -1409     
- Misses      11868    13361    +1493     
Flag Coverage Δ
examples 41.42% <76.55%> (+11.58%) ⬆️
gpu 51.81% <76.55%> (-8.69%) ⬇️
unit 52.29% <91.03%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant