Files
Model-Optimizer/.github/workflows/example_tests.yml
T
Keval MorabiaandClaude Opus 5 d73278808b Bump nemo container requirement to 26.08 for MBridge examples (#2257)
### What does this PR do?

Type of change: Bug fix

Bumps the Megatron-Bridge examples, tests and launcher configs to
`nemo:26.08` and removes the version-gated fallbacks they carried, plus
the fixes needed to make the suites green on that container.

**26.08 bump and shim removal**

- Examples, CI workflows, `noxfile.py` and the `mbridge_*` launcher
configs move to `nemo:26.08`.
- `examples/megatron_bridge/_distillation_provider.py` is deleted —
26.08's Megatron-Bridge ships `convert_to_distillation_provider(...,
distill_submodule=...)` natively, so `distill.py` imports it directly.
- `prune_minitron.py` drops the `AutoBridge.from_hf_config` /
config-only-export probing; `--no_moe_grouped_gemm` is no longer needed
in the MoE pruning tests, and the Qwen3.5-MoE `skipif` is gone (native
MoE expert mappings are in 26.08).
- `_DynamicMambaMixer` targets only the raw `conv1d_weight` /
`conv1d_bias` parameters that replaced the `conv1d` module in
Megatron-Core.

**MambaModel / MambaModelProvider removal**

Megatron-Core has shipped `HybridModel` since 26.06 and `MambaModel` is
a deprecated subclass that shares its `forward`, so `DMRegistry`
resolves those instances to the `HybridModel` registration and the
separate entry is redundant. Same for `MambaModelProvider` vs
`HybridModelProvider` on the bridge side. `MambaMixer` / `MambaLayer` /
`ExtendedRMSNorm` are untouched — the layers still exist. The deprecated
`get_te_mamba_stack_spec` is removed; use `get_te_hybrid_stack_spec`.

**Bug fix: compressed output_layer extra state**

`mtq.compress` converts even a *disabled* `output_layer` into a
`RealQuantLinear` (its weight is left uncompressed, since
`pack_real_quantize_weight` skips disabled quantizers). The guard added
in #2112 excluded `RealQuantLinear` by class, so the layer emitted extra
state and every worker died in `GPTModel.sharded_state_dict`:

```
RuntimeError: Boolean value of Tensor with more than one value is ambiguous
  megatron/core/models/gpt/gpt_model.py:896, in sharded_state_dict
    output_extra_state and output_extra_state.data
```

The guard now keys off whether the weight was actually compressed
(`QTensorWrapper`) instead of the class. This took out all 12
`test_homogeneous_compressed_sharded_state_dict` params, and the crashed
workers poisoned the pool, which surfaced as unrelated timeouts and NCCL
errors in `test_layer_sync_moe_local_experts_amax`,
`test_kv_cache_quant`, `test_kv_cache_amax_sync`,
`test_convert_mcore_te_gpt_model` and
`test_homogeneous_sharded_state_dict_te_spec` — 21 tests in total. The
e2e coverage is `skip_flaky_on_blackwell`, so CI never ran it;
`test_output_layer_extra_state_empty_when_nothing_quantized` now asserts
the contract directly and is not skipped.

**Checkpoint import entry point**

26.08 replaced `examples/conversion/convert_checkpoints.py` with
`scripts/conversion/convert.sh`, so
`tools/launcher/common/megatron_bridge/import/import.sh` and the three
README snippets are retargeted. `import.sh` uses the distributed GPU
backend with `GPUS_PER_NODE` / `TP` / `PP` / `EP` knobs.

**Megatron-LM on nemo:26.06** keeps working: `_get_mamba_conv1d` still
dispatches between the `conv1d` module (26.06 and earlier) and the raw
parameters (26.08+), so `import_mcore_gpt_from_hf` /
`export_mcore_gpt_to_hf` handle NemotronH on both. Only the
Megatron-Bridge examples and Minitron pruning of Mamba/hybrid models
require 26.08.

**Test consolidation**

`test_export_distilled_megatron_to_hf.py` is merged into
`test_distill.py`: `test_distill_llm` becomes
`test_distill_llm_hf_export` and covers the standalone
`--export_iterations all` run on the checkpoints it already produces,
saving one full distillation (~185 s of CI time). The two mamba-named
gpu test files are renamed to `hybrid`.

### Usage

```bash
# HF -> Megatron import, via Megatron-Bridge's 26.08 conversion entry point
bash /opt/Megatron-Bridge/scripts/conversion/convert.sh import \
    --executor local \
    --device gpu \
    --gpus-per-node 8 \
    --hf-model Qwen/Qwen3-8B \
    --megatron-path /tmp/Qwen3-8B-megatron
```

### Testing

All on `nvcr.io/nvidia/nemo:26.08`, 2x RTX 6000 Ada, no timeout
overrides:

- `tests/examples/megatron_bridge`: 16 passed, 1 skipped (28m14s). The
skip is the `gemma3vl` QAD param, now `@pytest.mark.manual` since
`qwen3_5_moe_vl` covers the VLM QAD path.
- `tests/gpu_megatron` (`_extensions`, `distill`, `export`, `opt`,
`peft`, `sparsity`, `speculative`, `utils`): 61 passed, 5 xpassed.
- `tests/gpu_megatron/torch/export` re-run after the conv1d dispatch
change: 27 passed.
- The 21 previously failing/hanging quantization tests: 21 passed (12 +
9).
- `tests/gpu_megatron/torch/{nas,prune}`: verified separately.

`import.sh` equivalence on a toy `qwen3_moe`, comparing all 12 weight
tensors after flattening each dist checkpoint with `dcp_to_torch_save` —
the GPU backend at 1 GPU, `--tp 2`, `--pp 2`, `--ep 2`, and `import.sh`
end-to-end (`GPUS_PER_NODE=2 EP=2`) are all byte-identical to `--device
cpu`.

`nemo:26.06` compatibility was checked directly in that image:
`megatron.core.models.hybrid.HybridModel`, the modelopt hybrid spec and
`hybrid_layer_pattern` are all present, while
`megatron.bridge.models.hybrid` and the bridge's `distill_submodule` are
not. The NemotronH round-trip test failed there before the conv1d
dispatch was restored and the dispatch is back in place; per project
convention the suites themselves only run on 26.08.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ⚠️ Megatron-Bridge examples plus
Minitron pruning of Mamba/hybrid models now require `nemo:26.08`.
Megatron-LM quantization and checkpoint export still run on
`nemo:26.06`.
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅ —
`test_output_layer_extra_state_empty_when_nothing_quantized` for the
compress fix; existing tests extended for the merged export coverage.
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
✅
- Did you get Claude approval on this PR?: ❌

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added guidance for importing Hugging Face checkpoints into Megatron
distributed format.
* Expanded distillation workflows to export selected or all checkpoint
iterations.

* **Improvements**
  * Expanded Hybrid model support across Megatron workflows.
  * Updated distributed import tooling with GPU and parallelism options.
  * Updated supported environments and examples to NVIDIA NeMo 26.08.

* **Bug Fixes**
* Corrected output-layer quantization state handling when quantization
is disabled.

* **Documentation**
  * Added compatibility guidance for current and legacy NeMo containers.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 00:31:41 +05:30

203 lines
8.5 KiB
YAML

name: Example tests
on:
push:
branches: ["pull-request/[0-9]+"]
# NOTE: paths cannot be used since push happens to copied PR and only latest commit to PR is used
schedule:
- cron: "0 0 * * *" # Nightly
workflow_dispatch:
# On-demand
concurrency:
# Cancel previous runs if new commit is pushed to the same PR
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/heads/pull-request/') && github.ref || github.sha }}
cancel-in-progress: true
# Each job's `allow_failure` reads the repo variable ALLOW_FAILURE_EXAMPLE_TESTS:
# a comma-separated list of example names whose test failures should be non-blocking, e.g. "torch_trt,llm_qat"
jobs:
# One changed-files pass decides which lanes run. Lane granularity, not per-example: a job's
# `if` cannot read `matrix`, so gating an individual example would need a job per example.
pr-gate:
runs-on: ubuntu-latest
permissions:
checks: read
contents: read
outputs:
any_changed: ${{ steps.lanes.outputs.any_changed }}
torch: ${{ steps.lanes.outputs.torch }}
trtllm: ${{ steps.lanes.outputs.trtllm }}
megatron: ${{ steps.lanes.outputs.megatron }}
onnx: ${{ steps.lanes.outputs.onnx }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- if: startsWith(github.ref, 'refs/heads/pull-request/')
id: base
uses: ./.github/actions/pr-merge-base
- if: startsWith(github.ref, 'refs/heads/pull-request/')
id: changed
uses: step-security/changed-files@v47.0.5
with:
base_sha: ${{ steps.base.outputs.merge_base }}
sha: ${{ steps.base.outputs.head_sha }}
fail_on_initial_diff_error: true
# `common` runs every lane. Each lane lists the example directories it covers, plus any
# extra directory those tests reach into (e.g. hf_ptq's script runs lm_eval from
# ../llm_eval, and the speculative_decoding test drives hf_ptq).
files_yaml: |
common:
- .github/actions/**
- .github/workflows/_example_tests_runner.yml
- .github/workflows/example_tests.yml
- modelopt/**
- modelopt_recipes/**
- pyproject.toml
- tests/_test_utils/**
- tests/conftest.py
- tests/examples/conftest.py
- "!**/*.{md,rst,png,ipynb}"
torch:
- "{examples,tests/examples}/{llm_distill,llm_qat,llm_sparsity,specdec_bench,speculative_decoding}/**"
- examples/dataset/** # data prep for llm_qat and speculative_decoding
- examples/hf_ptq/** # the speculative_decoding test drives hf_ptq
- "!**/*.{md,rst,png,ipynb}"
trtllm:
- "{examples,tests/examples}/{gpt-oss,hf_ptq,llm_eval}/**"
- examples/dataset/** # data prep for gpt-oss
- "!**/*.{md,rst,png,ipynb}"
megatron:
- "{examples,tests/examples}/megatron_bridge/**"
- "!**/*.{md,rst,png,ipynb}"
onnx:
- "{examples,tests/examples}/{diffusers,torch_onnx,torch_trt}/**"
- examples/onnx_ptq/** # torch_trt reuses onnx_ptq
- "!**/*.{md,rst,png,ipynb}"
- id: lanes
env:
# Nightly and on-demand runs have no diff to inspect, so they run everything.
NON_PR: ${{ !startsWith(github.ref, 'refs/heads/pull-request/') }}
COMMON: ${{ steps.changed.outputs.common_any_modified }}
TORCH: ${{ steps.changed.outputs.torch_any_modified }}
TRTLLM: ${{ steps.changed.outputs.trtllm_any_modified }}
MEGATRON: ${{ steps.changed.outputs.megatron_any_modified }}
ONNX: ${{ steps.changed.outputs.onnx_any_modified }}
run: |
any=false
for lane in TORCH TRTLLM MEGATRON ONNX; do
if [[ "$NON_PR" == "true" || "$COMMON" == "true" || "${!lane}" == "true" ]]; then
run=true
any=true
else
run=false
fi
echo "${lane,,}=$run" | tee --append "$GITHUB_OUTPUT"
done
echo "any_changed=$any" | tee --append "$GITHUB_OUTPUT"
- name: Wait for unit tests before spending GPU runners
if: ${{ steps.lanes.outputs.any_changed == 'true' && startsWith(github.ref, 'refs/heads/pull-request/') }}
uses: poseidon/wait-for-status-checks@v0.7.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
match_pattern: "^linux$" # Wait for Unit tests / linux
delay: 300s
##### PyTorch Example Tests #####
torch:
needs: [pr-gate]
if: needs.pr-gate.outputs.torch == 'true'
strategy:
fail-fast: false
matrix:
example: [llm_distill, llm_qat, llm_sparsity, specdec_bench, speculative_decoding]
uses: ./.github/workflows/_example_tests_runner.yml
permissions:
contents: read
secrets: inherit
with:
docker_image: "nvcr.io/nvidia/pytorch:26.07-py3"
example: ${{ matrix.example }}
timeout_minutes: 30
pip_install_extras: "[hf,dev-test]"
runner: ${{ startsWith(github.ref, 'refs/heads/pull-request/') && 'linux-amd64-gpu-rtxpro6000-latest-1' || 'linux-amd64-gpu-rtxpro6000-latest-2' }}
allow_failure: ${{ contains(format(',{0},', vars.ALLOW_FAILURE_EXAMPLE_TESTS), format(',{0},', matrix.example)) }}
##### TensorRT-LLM Example Tests #####
trtllm:
needs: [pr-gate]
if: needs.pr-gate.outputs.trtllm == 'true'
strategy:
fail-fast: false
matrix:
example: [gpt-oss, hf_ptq, llm_eval]
uses: ./.github/workflows/_example_tests_runner.yml
permissions:
contents: read
secrets: inherit
with:
docker_image: "nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc20"
example: ${{ matrix.example }}
pip_install_extras: "[hf,dev-test]"
runner: ${{ startsWith(github.ref, 'refs/heads/pull-request/') && 'linux-amd64-gpu-rtxpro6000-latest-1' || 'linux-amd64-gpu-rtxpro6000-latest-2' }}
allow_failure: ${{ contains(format(',{0},', vars.ALLOW_FAILURE_EXAMPLE_TESTS), format(',{0},', matrix.example)) }}
##### Megatron Example Tests #####
megatron:
needs: [pr-gate]
if: needs.pr-gate.outputs.megatron == 'true'
uses: ./.github/workflows/_example_tests_runner.yml
permissions:
contents: read
secrets: inherit
with:
docker_image: "nvcr.io/nvidia/nemo:26.08"
example: megatron_bridge
timeout_minutes: 60
pip_install_extras: "[hf,puzzletron,dev-test]"
runner: ${{ startsWith(github.ref, 'refs/heads/pull-request/') && 'linux-amd64-gpu-rtxpro6000-latest-1' || 'linux-amd64-gpu-rtxpro6000-latest-2' }}
allow_failure: ${{ contains(format(',{0},', vars.ALLOW_FAILURE_EXAMPLE_TESTS), ',megatron_bridge,') }}
##### ONNX/TensorRT Example Tests #####
onnx:
needs: [pr-gate]
if: needs.pr-gate.outputs.onnx == 'true'
strategy:
fail-fast: false
matrix:
example: [diffusers, torch_onnx, torch_trt]
uses: ./.github/workflows/_example_tests_runner.yml
permissions:
contents: read
secrets: inherit
with:
# Pinned to 26.05 (TensorRT 10): torch-tensorrt is capped at <2.13 (== 2.12.1),
# which needs libnvinfer.so.10; newer tensorrt containers drop it. Bump only once
# a torch-tensorrt build for the newer TensorRT is available.
docker_image: "nvcr.io/nvidia/tensorrt:26.05-py3"
example: ${{ matrix.example }}
timeout_minutes: 45
pip_install_extras: "[onnx,hf,dev-test]"
runner: ${{ startsWith(github.ref, 'refs/heads/pull-request/') && 'linux-amd64-gpu-rtxpro6000-latest-1' || 'linux-amd64-gpu-rtxpro6000-latest-2' }}
allow_failure: ${{ contains(format(',{0},', vars.ALLOW_FAILURE_EXAMPLE_TESTS), format(',{0},', matrix.example)) }}
##### Required Check for PR #####
example-pr-required-check:
# Run even if example tests are skipped
if: ${{ startsWith(github.ref, 'refs/heads/pull-request/') && always() }}
needs: [pr-gate, torch, trtllm, megatron, onnx]
runs-on: ubuntu-latest
steps:
- name: Required example tests did not succeed
if: |
needs.pr-gate.result != 'success' ||
(needs.pr-gate.outputs.any_changed == 'true' && (
!contains(fromJSON('["success", "skipped"]'), needs.torch.result) ||
!contains(fromJSON('["success", "skipped"]'), needs.trtllm.result) ||
!contains(fromJSON('["success", "skipped"]'), needs.megatron.result) ||
!contains(fromJSON('["success", "skipped"]'), needs.onnx.result)
))
run: exit 1