From 4c5a1d4327bbe003c1135791056f4be1f7de711e Mon Sep 17 00:00:00 2001 From: arham766 Date: Sun, 5 Jul 2026 18:47:48 -0700 Subject: [PATCH] fix: config loader no longer discards the body of a null-header multi-document file An explicit null/~ first YAML document (or an empty explicit document via a leading '---' pair) tripped the empty-file early return before the two-document branch, so 'null / --- / ' silently loaded as {}. A null first document is now treated as an empty header: dict bodies load as themselves, list bodies take the existing snippet path with no imports, a lone null still loads as {}, and the >2-document error now applies to null-first files too (previously they silently returned {}). Part of the findings in #1902 (wave 2). Signed-off-by: arham766 --- modelopt/torch/opt/config_loader.py | 6 +++++- tests/unit/recipe/test_loader.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/modelopt/torch/opt/config_loader.py b/modelopt/torch/opt/config_loader.py index 80864523e52..d3a263b48ac 100644 --- a/modelopt/torch/opt/config_loader.py +++ b/modelopt/torch/opt/config_loader.py @@ -193,7 +193,7 @@ def _load_raw_config_with_schema(config_file: str | Path | Traversable) -> _RawC schema = _parse_modelopt_schema(text, config_path) docs = list(yaml.safe_load_all(text)) - if len(docs) == 0 or docs[0] is None: + if len(docs) == 0 or (len(docs) == 1 and docs[0] is None): return _RawConfig({}, schema=schema, path=config_path) if len(docs) == 1: _raw = docs[0] @@ -201,6 +201,10 @@ def _load_raw_config_with_schema(config_file: str | Path | Traversable) -> _RawC # Multi-document: first doc is imports/metadata, second is content. # Merge the imports into the content for downstream resolution. header, content = docs[0], docs[1] + if header is None: + # An explicit null/empty first document is an empty header, not an + # empty file — the second document still carries the real body. + header = {} if not isinstance(header, dict): raise ValueError( f"Config file {config_path}: first YAML document must be a mapping, " diff --git a/tests/unit/recipe/test_loader.py b/tests/unit/recipe/test_loader.py index 3aaacaa3e0e..0d5eab37c80 100644 --- a/tests/unit/recipe/test_loader.py +++ b/tests/unit/recipe/test_loader.py @@ -1611,6 +1611,38 @@ def test_load_config_multi_doc_null_content(tmp_path): assert data == {"key": "value"} +def test_load_config_multi_doc_null_header_list_body(tmp_path): + """Multi-document YAML with a null first doc and a list body keeps the list.""" + cfg_file = tmp_path / "null_header_list.yaml" + cfg_file.write_text("null\n---\n- item1\n- item2\n") + data = load_config(cfg_file) + assert data == ["item1", "item2"] + + +def test_load_config_multi_doc_empty_header_list_body(tmp_path): + """Multi-document YAML with an empty explicit first doc and a list body keeps the list.""" + cfg_file = tmp_path / "empty_header_list.yaml" + cfg_file.write_text("---\n---\n- item1\n") + data = load_config(cfg_file) + assert data == ["item1"] + + +def test_load_config_multi_doc_null_header_dict_body(tmp_path): + """Multi-document YAML with a null first doc and a dict body keeps the dict.""" + cfg_file = tmp_path / "null_header_dict.yaml" + cfg_file.write_text("~\n---\nkey: value\n") + data = load_config(cfg_file) + assert data == {"key": "value"} + + +def test_load_config_lone_null_doc(tmp_path): + """A single explicit null document still loads as an empty dict.""" + cfg_file = tmp_path / "lone_null.yaml" + cfg_file.write_text("null\n") + data = load_config(cfg_file) + assert data == {} + + def test_load_config_multi_doc_first_not_dict_raises(tmp_path): """Multi-document YAML with non-dict first document raises ValueError.""" cfg_file = tmp_path / "bad_multi.yaml"