diff --git a/README.md b/README.md index b5baf34678..56cab7d6c3 100644 --- a/README.md +++ b/README.md @@ -233,18 +233,19 @@ Megatron Bridge provides out-of-the-box bridges and training recipes for a wide | [**DeepSeek**](docs/models/deepseek/index.md) | DeepSeek V2 / V2 Lite (deprecated), DeepSeek V3, DeepSeek V4 / V4 Flash | | [**Diffusion**](https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/src/megatron/bridge/diffusion/models) | FLUX, LLaDA 1.5, Nemotron-Labs Diffusion, WAN | | **Ernie** | [Ernie 4.5 MoE](https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/src/megatron/bridge/models/ernie), [Ernie 4.5 VL MoE](https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/src/megatron/bridge/models/ernie_vl) | +| [**EXAONE**](docs/models/exaone/exaone.md) | EXAONE 4.0, EXAONE 4.5 VL, K-EXAONE MoE | | [**Falcon**](docs/models/falcon/index.md) | Falcon H1 | | [**Gemma**](docs/models/gemma/index.md) | Gemma / Gemma 2 (deprecated), Gemma 3, Gemma 3-VL, Gemma 4 (26B-A4B MoE / 31B dense), Gemma 4-VL (26B-A4B MoE) | | [**GLM**](docs/models/glm/index.md) | GLM-4.5 / GLM-4.7 / GLM-4.7-Flash, GLM-4.5V, GLM-5 / GLM-5.1 / GLM-5.2 | | [**GPT-OSS**](docs/models/gpt_oss/index.md) | GPT-oss | | [**HY V3**](https://huggingface.co/tencent/Hy3-preview-Base) | Hy3 preview-Base (HF → Megatron checkpoint conversion) | -| [**Kimi**](docs/models/kimi/index.md) | Kimi K2, Kimi-K2.5-VL | +| [**Kimi**](docs/models/kimi/index.md) | Kimi K2, Kimi-K2.5-VL, Kimi K3 | | [**Llama**](docs/models/llama/index.md) | Llama 2 (deprecated), Llama 3 / 3.1 / 3.2 / 3.3 | -| [**MiniMax**](docs/models/minimax/index.md) | MiniMax-M2 / M2.5 / M2.7 | +| [**MiniMax**](docs/models/minimax/index.md) | MiniMax-M2 / M2.5 / M2.7, MiniMax-M3 | | [**Mistral**](docs/models/mistral/index.md) | Mistral 7B / Small 3 24B (deprecated), Ministral 3 (3B/8B/14B) | | [**Xiaomi-MiMo**](docs/models/mimo/index.md) | Xiaomi-MiMo, MiMo-V2-Flash | | [**Moonlight**](docs/models/moonlight/index.md) | Moonlight | -| [**Nemotron**](docs/models/nemotron/index.md) | Nemotron H v1 (deprecated), Nemotron Nano v2 (deprecated), Nemotron-3 Nano, Nemotron-3 Super, Llama Nemotron (deprecated), Nemotron Nano v2 VL (deprecated), Nemotron-3 Nano Omni | +| [**Nemotron**](docs/models/nemotron/index.md) | Nemotron H v1 (deprecated), Nemotron Nano v2 (deprecated), Nemotron-3 Nano, Nemotron-3 Super, Nemotron-3 Ultra, Llama Nemotron (deprecated), Nemotron Nano v2 VL (deprecated), Nemotron-3 Nano Omni | | [**OLMoE**](docs/models/olmoe/index.md) | OLMoE | | [**Qwen**](docs/models/qwen/index.md) | Qwen2 / Qwen2.5, Qwen3, Qwen3-MoE, Qwen3 Next, Qwen3.5 (dense/MoE), Qwen2.5-VL, Qwen3-VL, Qwen3.5-VL, Qwen3.6-VL, Qwen2 Audio, Qwen2.5-Omni, Qwen3-Omni, Qwen3-ASR | | [**Sarvam**](docs/models/sarvam/index.md) | Sarvam | diff --git a/tests/unit_tests/doc_consistency/test_readme_consistency.py b/tests/unit_tests/doc_consistency/test_readme_consistency.py index afb12b15bc..84a24ac7c6 100644 --- a/tests/unit_tests/doc_consistency/test_readme_consistency.py +++ b/tests/unit_tests/doc_consistency/test_readme_consistency.py @@ -28,6 +28,9 @@ REPO_ROOT = Path(__file__).resolve().parents[3] +README = REPO_ROOT / "README.md" +DOCS_MODELS = REPO_ROOT / "docs" / "models" +SUPPORTED_MODELS_HEADING = "## Supported Models" RECIPES_DIR = REPO_ROOT / "src" / "megatron" / "bridge" / "recipes" PERF_RECIPES_DIR = REPO_ROOT / "src" / "megatron" / "bridge" / "perf_recipes" TRAINING_README = REPO_ROOT / "scripts" / "training" / "README.md" @@ -830,6 +833,81 @@ def test_qwen3_recipe_examples_match_source_signatures_and_nested_owners(): assert expected_topologies <= set(qwen3_docs.splitlines()) +def _normalize(text: str) -> str: + """Lowercase and drop every non-alphanumeric character, so separators and case do not matter.""" + return re.sub(r"[^a-z0-9]", "", text.lower()) + + +def _supported_models_rows() -> list[tuple[str, str]]: + """`(family_cell, variants_cell)` for every data row of the README Supported Models table.""" + text = _read(README) + assert SUPPORTED_MODELS_HEADING in text, f"README.md has no {SUPPORTED_MODELS_HEADING!r} section" + body = text.split(SUPPORTED_MODELS_HEADING, 1)[1] + following = re.search(r"^## ", body, flags=re.MULTILINE) + table = body[: following.start()] if following else body + + rows: list[tuple[str, str]] = [] + for line in table.splitlines(): + line = line.strip() + if not line.startswith("|") or "---" in line or line.count("|") < 3: + continue + cells = [cell.strip() for cell in line.strip("|").split("|")] + if not cells[0].lower().startswith("family"): + rows.append((cells[0], cells[1] if len(cells) > 1 else "")) + return rows + + +def _row_for_family(rows: list[tuple[str, str]], family: str) -> tuple[str, str] | None: + """The row claiming `family`, matched by its docs link or by name, or None.""" + for cell, variants in rows: + if f"docs/models/{family}/" in cell: + return cell, variants + for cell, variants in rows: + if _normalize(family) in _normalize(cell): + return cell, variants + return None + + +def test_readme_supported_models_table_lists_every_shipped_model_family(): + """Every documented model family with code in the tree has a Supported Models row.""" + rows = _supported_models_rows() + assert len(rows) >= 15, f"only {len(rows)} Supported Models rows parsed — the table layout has changed" + + missing = [] + for family_dir in sorted(path for path in DOCS_MODELS.iterdir() if path.is_dir()): + name = family_dir.name + shipped = (REPO_ROOT / "examples" / "models" / name).is_dir() or ( + REPO_ROOT / "src" / "megatron" / "bridge" / "models" / name + ).is_dir() + if shipped and _row_for_family(rows, name) is None: + missing.append(f"docs/models/{name}/") + assert not missing, f"documented, shipped model families have no Supported Models row: {missing}" + + +def test_readme_supported_models_rows_name_every_shipped_model_variant(): + """Every documented variant page with runnable examples is named in its family's row.""" + rows = _supported_models_rows() + checked, missing = [], [] + for page in sorted(DOCS_MODELS.glob("*/*.md")): + if page.name == "index.md": + continue + examples = re.findall(r"(examples/[A-Za-z0-9_./-]+)", _read(page)) + if not any((REPO_ROOT / path.rstrip("/.")).exists() for path in examples): + continue # nothing runnable behind this page yet, so the table cannot be expected to name it + row = _row_for_family(rows, page.parent.name) + if row is None: + continue # already reported at family granularity + checked.append(page.name) + named = _normalize(" ".join(row)) + tokens = [token for token in re.split(r"[-_]", page.stem) if token] + if _normalize(page.stem) in named or all(_normalize(token) in named for token in tokens): + continue + missing.append(f"docs/models/{page.parent.name}/{page.name}") + + assert checked, "no documented variant page cleared the shipped gate — the docs layout has changed" + assert not missing, f"documented, shipped model variants are not named in their README row: {missing}" + + def test_sphinx_docs_link_out_of_tree_tutorials_as_urls(): """Sphinx must not treat repository-root tutorials as source documents.""" for path in SPHINX_TUTORIAL_LINK_DOCS: