Repository navigation
Conversation
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>
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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) |
There was a problem hiding this comment.
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.
| 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) |
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, andvllm-nvfp4-inloop.patchfor in-loop NVFP4 weight updates.Source:
radixark/miles@ba28991834. Port is additive (new modules + newadd_*_arguments()groups); slime-origin files are not restructured so future[Sync] Mirror SlimePRs stay clean. One commit per sub-PR; review each commit separately.Commits:
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 ofvime/utils/hf_utils/{weight_mapping,config}.pyin 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.pyare touched by several branches; later ones are rebased after each merge.🤖 Generated with Claude Code