Skip to content

[Port][Quant] MXFP8 / NVFP4 low-precision export, converters and in-loop NVFP4 vLLM patch (miles F08) - #503

Draft
zhou9402 wants to merge 7 commits into
vllm-project:mainfrom
zhou9402:miles-port/f08-low-precision
Draft

zhou9402 wants to merge 7 commits into
vllm-project:mainfrom
zhou9402:miles-port/f08-low-precision

Conversation

@zhou9402

Copy link
Copy Markdown

Ports miles' low-precision path: pure-torch quantization oracle, MXFP8 trainer-side export quantizer + --extra-high-precision-layers, offline HF→MXFP8 converter, NVFP4 primitives + QDQ fallback + fake-QAT TE hook, NVFP4 Megatron→HF export (modelopt_fp4 layout), offline HF→NVFP4 converter, and vllm-nvfp4-inloop.patch for in-loop NVFP4 weight updates.

Source: radixark/miles@ba28991834. Port is additive (new modules + new add_*_arguments() groups); slime-origin files are not restructured so future [Sync] Mirror Slime PRs stay clean. One commit per sub-PR; review each commit separately.

Commits:

  • 2b405c2 Add pure-torch low-precision quantization reference oracle
  • a644f74 Add MXFP8 trainer-side export quantizer and --extra-high-precision-layers flags
  • a333ffb Add offline HF->MXFP8 checkpoint converter and metadata-only rollout schema
  • 4e4d08c Add NVFP4 primitives, pure-torch QDQ fallback, and fake-QAT TE hook
  • 49d62ef Add NVFP4 Megatron->HF export quantizer (modelopt_fp4 layout)
  • ab377b7 Add offline HF->NVFP4 checkpoint converter
  • e66e155 Add vLLM patch for in-loop NVFP4 (modelopt_fp4) weight updates

Verification: every quantizer is checked against the pure-torch oracle on CPU; tests/test_mxfp8_te_vs_oracle.py (real TE path) needs Blackwell. Patch applies in Dockerfile order; docker build pending. On rebase, drop this branch's copy of vime/utils/hf_utils/{weight_mapping,config}.py in favor of F04's.

Merge order of the miles-port series: F10 → F02 → F06 → F11a → F11b → F05 → F09 → F04 → F03 → F07 → F08 → F01. train.py / vime/utils/arguments.py are touched by several branches; later ones are rebased after each merge.

🤖 Generated with Claude Code

zhou and others added 7 commits October 27, 2026 18:41
Port of miles@ba28991834 miles/utils/mxfp8.py, miles/utils/nvfp4.py (numerics contract).

Adds vime/utils/quant_ref.py with deterministic CPU reference implementations
(MXFP8/UE8M0, NVFP4 e2m1+E4M3 block scale, block-FP8 dequant) plus golden-vector
and roundtrip error-bound tests; the oracle gates the MXFP8/NVFP4 port PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yers flags

Port of miles@ba28991834 miles/utils/mxfp8.py, miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_mxfp8.py, --extra-high-precision-layers-{hf,megatron} from miles/utils/args/configs/train.py.

Divergences from miles: scale tensor named 'weight_scale' (vLLM
ModelOptMxFp8Config contract, not SGLang's 'weight_scale_inv'); Megatron args
read flat via getattr (no args.backend); SGLang-loader-specific indexer wk
exclusion dropped. Dispatcher now raises for nvfp4/modelopt_fp4 instead of
silently passing BF16 through until F08-3/F08-4 land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…schema

Port of miles@ba28991834 tools/convert_hf_to_mxfp8.py, miles/utils/hf_rollout_schema.py, miles/utils/hf_utils/weight_mapping.py.

Divergences from miles: sglang block_quant_dequant replaced by
vime.utils.quant_ref.block_fp8_dequant; emitted scale tensors named
'weight_scale' (vLLM ModelOptMxFp8Config, not 'weight_scale_inv'); written
config adds 'ignored_layers' (vLLM flat form) alongside
'modules_to_not_convert'; 'expert_dtype' dropped (vLLM reads it only for
DeepSeek-V4); SGLang-native DeepseekV4 checkpoint remap raises
NotImplementedError (no vLLM equivalent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of miles@ba28991834 miles/utils/nvfp4.py, miles/utils/nvfp4_fake_qat.py; miles/kernels/quant/nvfp4_qdq.py intentionally NOT ported (SM10x-only CuTe DSL).

The CuTe kernel is replaced by a portable pure-torch fused QDQ with STE
(nvfp4_qdq_torch.py), validated against the quant_ref oracle incl. M%16!=0
edges; same NVTE_* env config surface, 4over6 approximated by per-block
candidate selection. TE hook added to install_transformer_engine_extensions
(env OPEN_TRAINING_NVFP4_FAKE_QAT_FLAG=1), mirroring the INT4 QAT pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of miles@ba28991834 miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_nvfp4.py.

Emits vLLM modelopt_fp4 on-disk layout (weight uint8 [M,K/2], weight_scale
e4m3 [M,K/16], weight_scale_2 fp32) for routed experts only, with gate/up
shared-amax pairing; miles' layout matches vLLM's contract verbatim (verified
against vllm@43b4aaea modelopt.py), no name remap needed. Adaptations: flat
getattr args instead of args.backend; first/last BF16 layers keyed off
--num-layers-at-{start,end}-in-bf16; dispatcher routes
quant_algo==NVFP4 / quant_method in {modelopt_fp4, nvfp4}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of miles@ba28991834 tools/convert_hf_to_nvfp4.py.

Quantizes only MoE routed-expert GEMMs in the main language decoder (gate/up
paired with a shared global amax, incl. pairs split across shards); writes
config.json quantization_config (quant_algo=NVFP4, quant_method=modelopt,
group_size=16) and hf_quant_config.json matching vLLM ModelOptNvFp4Config.
No sglang imports in miles' file; only cleanup adaptations (with-open JSON
reads/writes, parse_args).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of miles@ba28991834 NVFP4 rollout enablement (vLLM-side piece; miles used SGLang FLASHINFER CuTeDSL flags instead).

vLLM's layerwise reload restores layers in post-processed kernel format
(swizzled/padded scales, dropped input_scale/weight_scale_2), so
checkpoint-format NVFP4 tensors have no parameter to load into. The patch adds
an opt-in recreate_checkpoint_weights_on_reload hook: ModelOptNvFp4FusedMoE
stashes its create_weights arguments and recreates checkpoint-format
parameters inside initialize_layerwise_reload (mirrors the attention path's
create_weights re-run). 88-line patch, 2 files.

Verified on a fresh worktree of vllm@43b4aaea with the FULL Dockerfile patch
sequence plus this patch: for each p in the Dockerfile order, git apply --check
&& git apply, then git apply --check vllm-nvfp4-inloop.patch -> APPLY_CHECK_OK.
CPU verifier: tests/test_nvfp4_inloop_recreate.py (passed against the patched
worktree; skips on runtimes without the reload machinery).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@read-the-docs-community

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive support for low-precision quantization formats (MXFP8 and NVFP4) optimized for Blackwell architectures, including offline conversion tools, pure-torch reference implementations, fake QAT fallbacks, and integration with the Megatron-to-HF conversion pipeline. It also adds a vLLM patch to support in-loop NVFP4 weight updates during training. The review feedback highlights a discrepancy in default environment variable parsing between the fake QAT config and reference utilities that could cause numerical divergence, and recommends using safe dictionary access on quantization_config to prevent potential KeyError exceptions.

Comment on lines +48 to +65
def _env_flag(name: str) -> bool:
return os.getenv(name, "").strip().lower() in ("1", "true", "yes", "weights", "all")


@lru_cache(maxsize=1)
def current_nvfp4_qdq_config() -> NVFP4QDQConfig:
"""QDQ config resolved once per process from NVTE_* env vars.

Mirrors the INT4 path, which reads its env once at hook install time.
Tests must call ``current_nvfp4_qdq_config.cache_clear()`` after changing
the env (see reset_nvfp4_qdq_config_for_tests).
"""
if _env_flag("NVTE_USE_FAST_MATH"):
raise RuntimeError("NVFP4 fake QAT requires NVTE_USE_FAST_MATH=0 (exact RN numerics).")
use_4over6 = _env_flag("NVTE_NVFP4_4OVER6")
e4m3_max = 448
if use_4over6 and _env_flag("NVTE_NVFP4_4OVER6_E4M3_USE_256"):
e4m3_max = 256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

There is a discrepancy in the default value of NVTE_NVFP4_4OVER6_E4M3_USE_256 when it is not set in the environment. In vime/utils/nvfp4.py, it defaults to "all" (which resolves to 256 if 4over6 is enabled), whereas in nvfp4_qdq_torch.py, _env_flag defaults to "" (which resolves to 448). This inconsistency can cause numerical divergence between the training-time fake QAT and the offline/live quantizers.

We should update _env_flag to accept a default value so that NVTE_NVFP4_4OVER6_E4M3_USE_256 can default to "all" consistently.

Suggested change
def _env_flag(name: str) -> bool:
return os.getenv(name, "").strip().lower() in ("1", "true", "yes", "weights", "all")
@lru_cache(maxsize=1)
def current_nvfp4_qdq_config() -> NVFP4QDQConfig:
"""QDQ config resolved once per process from NVTE_* env vars.
Mirrors the INT4 path, which reads its env once at hook install time.
Tests must call ``current_nvfp4_qdq_config.cache_clear()`` after changing
the env (see reset_nvfp4_qdq_config_for_tests).
"""
if _env_flag("NVTE_USE_FAST_MATH"):
raise RuntimeError("NVFP4 fake QAT requires NVTE_USE_FAST_MATH=0 (exact RN numerics).")
use_4over6 = _env_flag("NVTE_NVFP4_4OVER6")
e4m3_max = 448
if use_4over6 and _env_flag("NVTE_NVFP4_4OVER6_E4M3_USE_256"):
e4m3_max = 256
def _env_flag(name: str, default: str = "") -> bool:
return os.getenv(name, default).strip().lower() in ("1", "true", "yes", "weights", "all")
@lru_cache(maxsize=1)
def current_nvfp4_qdq_config() -> NVFP4QDQConfig:
"""QDQ config resolved once per process from NVTE_* env vars.
Mirrors the INT4 path, which reads its env once at hook install time.
Tests must call ``current_nvfp4_qdq_config.cache_clear()`` after changing
the env (see reset_nvfp4_qdq_config_for_tests).
"""
if _env_flag("NVTE_USE_FAST_MATH"):
raise RuntimeError("NVFP4 fake QAT requires NVTE_USE_FAST_MATH=0 (exact RN numerics).")
use_4over6 = _env_flag("NVTE_NVFP4_4OVER6")
e4m3_max = 448
if use_4over6 and _env_flag("NVTE_NVFP4_4OVER6_E4M3_USE_256", "all"):
e4m3_max = 256

Comment on lines +21 to +34
if quantization_config["quant_method"] == "mxfp8":
from .quantizer_mxfp8 import quantize_params_mxfp8

return quantize_params_mxfp8(args, megatron_name, converted_named_params, quantization_config)

# NVFP4 checkpoints: quant_algo "NVFP4" with quant_method "modelopt" /
# "modelopt_fp4", or a bare "nvfp4" quant_method.
if (
quantization_config.get("quant_algo") == "NVFP4"
or quantization_config["quant_method"] in ("nvfp4", "NVFP4", "modelopt_fp4")
):
from .quantizer_nvfp4 import quantize_params_nvfp4

return quantize_params_nvfp4(args, megatron_name, converted_named_params, quantization_config)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Accessing quantization_config["quant_method"] directly can raise a KeyError if the key is missing. It is safer to use .get("quant_method") to handle missing keys gracefully.

Suggested change
if quantization_config["quant_method"] == "mxfp8":
from .quantizer_mxfp8 import quantize_params_mxfp8
return quantize_params_mxfp8(args, megatron_name, converted_named_params, quantization_config)
# NVFP4 checkpoints: quant_algo "NVFP4" with quant_method "modelopt" /
# "modelopt_fp4", or a bare "nvfp4" quant_method.
if (
quantization_config.get("quant_algo") == "NVFP4"
or quantization_config["quant_method"] in ("nvfp4", "NVFP4", "modelopt_fp4")
):
from .quantizer_nvfp4 import quantize_params_nvfp4
return quantize_params_nvfp4(args, megatron_name, converted_named_params, quantization_config)
if quantization_config.get("quant_method") == "mxfp8":
from .quantizer_mxfp8 import quantize_params_mxfp8
return quantize_params_mxfp8(args, megatron_name, converted_named_params, quantization_config)
# NVFP4 checkpoints: quant_algo "NVFP4" with quant_method "modelopt" /
# "modelopt_fp4", or a bare "nvfp4" quant_method.
if (
quantization_config.get("quant_algo") == "NVFP4"
or quantization_config.get("quant_method") in ("nvfp4", "NVFP4", "modelopt_fp4")
):
from .quantizer_nvfp4 import quantize_params_nvfp4
return quantize_params_nvfp4(args, megatron_name, converted_named_params, quantization_config)

@zhou9402
zhou9402 marked this pull request as draft October 28, 2026 02:03
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