diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e2cec444758..b232c53b424 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,7 @@ Changelog - Add ``--vllm_fakequant_export`` to ``examples/megatron_bridge/export_quantized_megatron_to_hf.py`` to evaluate PTQ / QAD checkpoints in vLLM for formats it has no kernels for (e.g. NVFP4 on Hopper, GGML IQ): it folds the weight quantizers into BF16 weights and saves through Megatron-Bridge at any TP / EP / PP, LLMs and VLMs, with the activation and KV-cache quantizers in sidecars that ``examples/vllm_serve/vllm_serve_fakequant.py`` loads automatically. Also allow ``--ep_size`` > 1 for checkpoints built with ``--no_moe_grouped_gemm``. - GGML GPTQ checkpoints from ``examples/megatron_bridge`` keep their calibrated codes when the Megatron checkpoint is reloaded for QAD or export, and layerwise calibration now runs on Megatron vision-language models. - The Megatron unified HF export (``export_mcore_gpt_to_hf``) now copies the source checkpoint's non-model files (tokenizer, processor, remote code, chat templates, ...) into the export unchanged, from local disk only. The ``examples/megatron_bridge`` scripts make a Hugging Face Hub model local first with ``modelopt.torch.export.ensure_local_checkpoint``. +- Add an end-to-end W4A4 NVFP4 PTQ and QAD tutorial for NVIDIA Nemotron 3.5 Super VL, covering chat-data blend preparation, assistant-only distillation, Hugging Face export and vLLM serving. See `examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md `_ for details. *Misc* diff --git a/examples/megatron_bridge/README.md b/examples/megatron_bridge/README.md index 4d67602186c..81b6f11ff7e 100644 --- a/examples/megatron_bridge/README.md +++ b/examples/megatron_bridge/README.md @@ -18,7 +18,7 @@ This directory contains examples of using Model Optimizer with the [NeMo Megatro This README is the getting-started guide, with each workflow shown on small models (mostly Qwen3-8B). > [!TIP] -> For complete end-to-end workflows on larger models, see the [tutorials](tutorials/README.md): [Nemotron-3-Nano-30B-A3B](tutorials/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16/README.md) pruning + distillation + quantization, [Qwen3.6-35B-A3B](tutorials/Qwen3.6-35B-A3B/README.md) W4A4 NVFP4 + QAD, and [NemotronH omni VL](tutorials/NemotronH-Omni-W4A4-QAD/README.md) W4A4 + QAD. +> For complete end-to-end workflows on larger models, see the [tutorials](tutorials/README.md): [Nemotron-3-Nano-30B-A3B](tutorials/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16/README.md) pruning + distillation + quantization, [Qwen3.6-35B-A3B](tutorials/Qwen3.6-35B-A3B/README.md) W4A4 NVFP4 + QAD, and [NVIDIA Nemotron 3.5 Super VL](tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md) W4A4 + QAD. ## Pre-Requisites diff --git a/examples/megatron_bridge/tutorials/NemotronH-Omni-W4A4-QAD/README.md b/examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md similarity index 79% rename from examples/megatron_bridge/tutorials/NemotronH-Omni-W4A4-QAD/README.md rename to examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md index 461f2e201c9..5419bb145c2 100644 --- a/examples/megatron_bridge/tutorials/NemotronH-Omni-W4A4-QAD/README.md +++ b/examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md @@ -1,6 +1,6 @@ -# NemotronH Omni VL: W4A4 NVFP4 PTQ + QAD + Deployment +# NVIDIA Nemotron 3.5 Super VL: W4A4 NVFP4 PTQ + QAD + Deployment -End-to-end W4A4 quantization of a NemotronH omni vision-language checkpoint (architecture `NemotronH_Omni_Reasoning_V3`), a Mamba-Transformer MoE language model with a vision tower: mixed NVFP4/FP8 post-training quantization → Quantization-Aware Distillation (QAD) → Hugging Face export → vLLM serving. This document covers: +End-to-end W4A4 quantization of [NVIDIA Nemotron 3.5 Super VL](https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16) (architecture `NemotronH_Omni_Reasoning_V3`), a Mamba-Transformer MoE language model with a vision tower: mixed NVFP4/FP8 post-training quantization → Quantization-Aware Distillation (QAD) → Hugging Face export → vLLM serving. This document covers: 1. **[Data Preparation](#1-data-preparation)**: the chat-format QAD blend, from `data_blend.yaml` 2. **[Quantization](#2-quantization)**: W4A4 PTQ with `examples/megatron_bridge/quantize.py` @@ -10,9 +10,11 @@ End-to-end W4A4 quantization of a NemotronH omni vision-language checkpoint (arc Only the language model (including its MTP head) is quantized; the vision tower and projector stay BF16 and are copied from the source checkpoint. +The PTQ recipe is the same as the one used for the released [`nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4`](https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4) checkpoint. The calibration data and the QAD training data are all public Hugging Face datasets. The evaluation results below differ from the official model card scores. + ## Results -All checkpoints were served with the same vLLM settings (see [Section 5](#5-evaluation-and-serving)); the quantized ones with an FP8 KV cache. BF16 is the mean of two runs. +All checkpoints were served with the same vLLM settings (see [Section 5](#5-evaluation-and-serving)), MMLU-ProX at `--max-model-len 32768` for all three; the quantized ones with an FP8 KV cache. | Benchmark | BF16 (teacher) | W4A4 PTQ | W4A4 QAD (600 iters) | | --- | --- | --- | --- | @@ -43,7 +45,8 @@ PTQ and export launch with `torchrun`; QAD runs on 8 nodes with the `sbatch` scr QAD trains on a weighted blend of 11 text-only chat sources, defined in [`data_blend.yaml`](data_blend.yaml) and computing the loss on assistant tokens only. Set `output_dir` in that file, then write the blend as chat JSONL with the [chat JSONL mode of `prepare_megatron_data_blend`](../../../dataset/MEGATRON_DATA_PREP.md#chat-jsonl-blends-for-assistant-only-loss): ```bash -python -m modelopt.torch.utils.plugins.prepare_megatron_data_blend --config data_blend.yaml +python -m modelopt.torch.utils.plugins.prepare_megatron_data_blend \ + --config /opt/Model-Optimizer/examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/data_blend.yaml ``` This writes `train.jsonl` (35,800 conversations) and `validation.jsonl` (1,000) to `output_dir`. Run it in the container: Megatron-Bridge's text-only chat filter selects the rows. @@ -72,14 +75,14 @@ PTQ runs on **one 4-GPU GB300 node** with 512 calibration samples. ```bash torchrun --nproc_per_node 4 /opt/Model-Optimizer/examples/megatron_bridge/quantize.py \ - --hf_model_name_or_path \ + --hf_model_name_or_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ --trust_remote_code \ --recipe models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/ptq/nvfp4-mse \ --tp_size 1 --ep_size 4 --pp_size 1 \ --calib_dataset_name cnn_nemotron_v2_mix \ --calib_num_samples 512 \ --calib_batch_size 1 \ - --export_megatron_path /path/to/nemotron_h_omni_w4a4_megatron \ + --export_megatron_path /path/to/nemotron_3_5_super_vl_w4a4_megatron \ --skip_generate ``` @@ -108,9 +111,9 @@ We used **8 nodes × 4 GB300 (32 GPUs)** with context parallelism 4 and expert p export MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1) MASTER_PORT=29500 srun bash -c 'export RANK=$SLURM_PROCID WORLD_SIZE=$SLURM_NTASKS LOCAL_RANK=$SLURM_LOCALID python -u /opt/Model-Optimizer/examples/megatron_bridge/distill.py \ - --teacher_hf_path \ - --student_hf_path \ - --student_megatron_path /path/to/nemotron_h_omni_w4a4_megatron \ + --teacher_hf_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ + --student_hf_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ + --student_megatron_path /path/to/nemotron_3_5_super_vl_w4a4_megatron \ --trust_remote_code \ --tp_size 1 --pp_size 1 --cp_size 4 --ep_size 32 \ --sft --sft_hf_dataset /path/to/qad_blend/train.jsonl \ @@ -145,10 +148,10 @@ Export the QAD checkpoint to a Hugging Face checkpoint on one 4-GPU node (pipeli ```bash torchrun --nproc_per_node 4 /opt/Model-Optimizer/examples/megatron_bridge/export_quantized_megatron_to_hf.py \ - --hf_model_name_or_path \ + --hf_model_name_or_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ --trust_remote_code \ --megatron_path /path/to/qad_output/checkpoints \ - --export_unified_hf_path /path/to/nemotron_h_omni_w4a4_hf \ + --export_unified_hf_path /path/to/nemotron_3_5_super_vl_w4a4_hf \ --pp_size 4 ``` @@ -162,14 +165,23 @@ torchrun --nproc_per_node 4 /opt/Model-Optimizer/examples/megatron_bridge/export ### 5. Evaluation and Serving -Serve with vLLM on one 4-GPU GB300 node, TP=4 with expert parallelism. Quantized checkpoints use these engine arguments; the BF16 baseline uses `--kv-cache-dtype auto`. +Serve with vLLM on one 4-GPU GB300 node, TP=4 with expert parallelism. The results above used: -| Setting | Value | -| --- | --- | -| `--kv-cache-dtype` | `fp8` | -| `--max-model-len` | 262144 | -| `--mamba-ssm-cache-dtype` | `float16` | -| `--mamba-conv-cache-dtype` | `float32` | -| Mamba cache rounding | stochastic rounding, 5 Philox rounds | -| Reasoning parser | `nemotron_v3` | -| Chat template | the checkpoint's `chat_template.jinja` | +```bash +vllm serve /path/to/nemotron_3_5_super_vl_w4a4_hf \ + --tensor-parallel-size 4 --enable-expert-parallel \ + --trust-remote-code \ + --max-model-len 262144 \ + --kv-cache-dtype fp8 \ + --mamba-ssm-cache-dtype float16 \ + --mamba-cache-dtype float32 \ + --enable-mamba-cache-stochastic-rounding \ + --mamba-cache-philox-rounds 5 \ + --reasoning-parser nemotron_v3 \ + --enable-auto-tool-choice --tool-call-parser qwen3_coder \ + --chat-template /path/to/nemotron_3_5_super_vl_w4a4_hf/chat_template.jinja \ + --enable-prefix-caching --enable-chunked-prefill \ + --gpu-memory-utilization 0.8 +``` + +MMLU-ProX was served with `--max-model-len 32768`. The BF16 baseline uses `--kv-cache-dtype auto`. diff --git a/examples/megatron_bridge/tutorials/NemotronH-Omni-W4A4-QAD/data_blend.yaml b/examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/data_blend.yaml similarity index 100% rename from examples/megatron_bridge/tutorials/NemotronH-Omni-W4A4-QAD/data_blend.yaml rename to examples/megatron_bridge/tutorials/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/data_blend.yaml diff --git a/examples/megatron_bridge/tutorials/README.md b/examples/megatron_bridge/tutorials/README.md index 0552cd075f6..142e1b34e38 100644 --- a/examples/megatron_bridge/tutorials/README.md +++ b/examples/megatron_bridge/tutorials/README.md @@ -8,5 +8,5 @@ Each one walks through a complete workflow using the scripts in [examples/megatr | Tutorial | What it covers | | --- | --- | | [NVIDIA-Nemotron-3-Nano-30B-A3B-BF16](NVIDIA-Nemotron-3-Nano-30B-A3B-BF16/README.md) | End-to-end optimization of the Nemotron-3-Nano-30B-A3B-BF16 (MoE + Mamba-Transformer hybrid) model: Minitron structured pruning (31.6B/A3.6B → 22B/A3.0B) → two-phase knowledge distillation (100B tokens, 8K then 32K seq length) → quantization → vLLM deployment. Includes data-blend preparation, evaluation setup, and detailed pruning / data-blend / long-context ablations. | -| [NemotronH-Omni-W4A4-QAD](NemotronH-Omni-W4A4-QAD/README.md) | **W4A4** quantization of the language model of a NemotronH omni vision-language checkpoint (`NemotronH_Omni_Reasoning_V3`, Mamba-Transformer MoE): mixed NVFP4/FP8 PTQ → KD-only QAD on chat data → Hugging Face export → vLLM serving. | +| [NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16](NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/README.md) | **W4A4** quantization of the language model of NVIDIA Nemotron 3.5 Super VL (`NemotronH_Omni_Reasoning_V3`, Mamba-Transformer MoE): mixed NVFP4/FP8 PTQ → KD-only QAD on chat data → Hugging Face export → vLLM serving. | | [Qwen3.6-35B-A3B](Qwen3.6-35B-A3B/README.md) | End-to-end **W4A4 NVFP4** optimization of the Qwen3.6-35B-A3B (MoE + hybrid linear/full attention VLM) model: NVFP4 W4A4 post-training quantization → quantization-aware distillation (QAD) to recover the accuracy W4A4 costs → evaluation → vLLM deployment. Includes data-format (pre-tokenized vs chat SFT) and SciCode non-termination ablations. | diff --git a/modelopt/torch/utils/plugins/prepare_megatron_data_blend.py b/modelopt/torch/utils/plugins/prepare_megatron_data_blend.py index b2f6bae779e..47f3140a6a0 100644 --- a/modelopt/torch/utils/plugins/prepare_megatron_data_blend.py +++ b/modelopt/torch/utils/plugins/prepare_megatron_data_blend.py @@ -193,7 +193,7 @@ def _to_chat( """ if content_field == "messages": messages = row.get("messages") - if not messages: + if not messages or not isinstance(messages, list): return None messages = prepare_messages_for_chat_template(messages, reasoning_content=reasoning_content) chat: dict[str, Any] = {"messages": messages} @@ -203,8 +203,8 @@ def _to_chat( if row.get(key): chat[key] = row[key] else: - text = row.get("text") or "" - if not text: + text = row.get("text") + if not text or not isinstance(text, str): return None if " max_rejected: break @@ -299,8 +312,11 @@ def _take_chat_rows( if len(rows) < want: gave_up = rejected > max_rejected reason = f"gave up after {rejected} rejected rows" if gave_up else "source exhausted" + if unconvertible: + reason += f"; {unconvertible} rows could not be converted" raise RuntimeError(f"{name}: only {len(rows)}/{want} usable rows ({reason})") - print(f"{name}: {len(rows)} rows ({rejected} rejected)", flush=True) + detail = f", {unconvertible} unconvertible" if unconvertible else "" + print(f"{name}: {len(rows)} rows ({rejected} rejected{detail})", flush=True) return rows diff --git a/tests/gpu_megatron/torch/utils/plugins/test_prepare_megatron_data_blend.py b/tests/gpu_megatron/torch/utils/plugins/test_prepare_megatron_data_blend.py index a037f6f459d..2322f64327c 100644 --- a/tests/gpu_megatron/torch/utils/plugins/test_prepare_megatron_data_blend.py +++ b/tests/gpu_megatron/torch/utils/plugins/test_prepare_megatron_data_blend.py @@ -176,6 +176,7 @@ def test_to_chat_handles_all_row_shapes(): msgs = [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}] assert blend_module._to_chat({"messages": msgs}, "messages") == {"messages": msgs} assert blend_module._to_chat({"messages": []}, "messages") is None + assert blend_module._to_chat({"messages": "hello"}, "messages") is None # Nothing would be trained under assistant-only loss; loss_mode full trains the user turn. assert blend_module._to_chat({"messages": msgs[:1]}, "messages") is None assert blend_module._to_chat({"messages": msgs[:1]}, "messages", loss_mode="full") == { @@ -206,6 +207,7 @@ def test_to_chat_handles_all_row_shapes(): "messages": [{"role": "assistant", "content": "def f(): pass"}] } assert blend_module._to_chat({"text": ""}, "text") is None + assert blend_module._to_chat({"text": ["x"]}, "text") is None # Markers that parse to no turns: dropped, never trained as assistant text. assert blend_module._to_chat({"text": "\nraw body\n"}, "text") is None # A role without a mapping would cut its turn out of the middle: the whole row is dropped. @@ -226,7 +228,9 @@ def test_to_chat_handles_all_row_shapes(): assert blend_module._to_chat({"text": text}, "text") is None, text -def test_take_chat_rows_skips_non_text_rows_and_fails_without_enough(tmp_path: Path, monkeypatch): +def test_take_chat_rows_skips_non_text_rows_and_fails_without_enough( + tmp_path: Path, monkeypatch, capsys +): source = _source("a") # Media is rejected inside message content and as a top-level key the conversion drops. rows = [_chat(0), _image_chat(1), {**_chat(9), "images": ["a.png"]}, _chat(2), _chat(3)] @@ -239,6 +243,22 @@ def test_take_chat_rows_skips_non_text_rows_and_fails_without_enough(tmp_path: P with pytest.raises(RuntimeError, match=r"only 0/2 usable rows \(gave up after 121 rejected"): blend_module._take_chat_rows(source, tmp_path, 2, False, 0, "native", "assistant") + # Unconvertible rows are skipped, but only the first few are warned about. + bad = [{"messages": ["hello"]} for _ in range(20)] + monkeypatch.setattr(blend_module, "_iter_rows", lambda *a: iter([*bad, _chat(0)])) + capsys.readouterr() + assert ( + len(blend_module._take_chat_rows(source, tmp_path, 1, False, 0, "native", "assistant")) == 1 + ) + out = capsys.readouterr().out + assert out.count("cannot be converted") == 5 + assert "1 rows (20 rejected, 20 unconvertible)" in out + # A config error is not a bad row: it fails before any row is skipped. + monkeypatch.setattr(blend_module, "_iter_rows", lambda *a: iter(bad)) + no_field = {k: v for k, v in source.items() if k != "content_field"} + with pytest.raises(KeyError, match="content_field"): + blend_module._take_chat_rows(no_field, tmp_path, 1, False, 0, "native", "assistant") + def test_chat_blend_splits_weighted_rows(tmp_path: Path, chat_blend, monkeypatch): write_config, calls = chat_blend @@ -365,8 +385,10 @@ def test_write_jsonl_is_atomic(tmp_path: Path): def test_chat_blend_reads_files_sources(tmp_path: Path, monkeypatch): plain, packed = tmp_path / "a.jsonl", tmp_path / "b.jsonl.gz" lines = [json.dumps(_chat(i)) for i in range(20)] - # A malformed record is skipped with a warning, as in the tokenizing reader. - plain.write_text("\n".join([*lines[:10], '{"messages": [', *lines[10:]]) + "\n", "utf-8") + # Malformed records, unparseable or unconvertible, are skipped with a warning as in the + # tokenizing reader. + bad = ['{"messages": [', '{"messages": ["hello"]}'] + plain.write_text("\n".join([*lines[:10], *bad, *lines[10:]]) + "\n", "utf-8") with gzip.open(packed, "wt", encoding="utf-8") as f: f.writelines(json.dumps(_chat(i)) + "\n" for i in range(20, 40)) paths = {"data/a.jsonl": str(plain), "data/b.jsonl.gz": str(packed)}