[Quantization][ModelOpt] W4A16 NVFP4 fused MoE + --override-activation-dtype flag - #42428
juhi10071998 wants to merge 3 commits into
Conversation
Extends ModelOptNvFp4FusedMoE to honor W4A16_NVFP4 checkpoints. When the on-disk quant_algo is W4A16_NVFP4, the MoE class passes activation_key=None to select_nvfp4_moe_backend. W4A4 backends reject the scheme (their _supports_quant_scheme requires (kNvfp4Static, kNvfp4Dynamic) exactly); Marlin survives (it only checks weight_key). Marlin's MoE prep already nulls activation scales in convert_to_nvfp4_moe_kernel_format and routes through nvfp4_w4a16_moe_quant_config — no other change needed. Follow-up to PR vllm-project#41769 (dense Linear W4A16). Signed-off-by: Juhi Mittal <juhim@nvidia.com> Co-authored-by: Claude
Engine-level override that loads a W4A4 NVFP4 ckpt via the W4A16 path without editing the ckpt's JSON. Mirrors --override-attention-dtype in shape (single-subsystem dtype override on ModelConfig). - ModelConfig.override_activation_dtype: Literal["auto","bfloat16","float16"] - weight_utils._maybe_inject_engine_overrides() injects a private dict key into hf_quant_config (inject-and-leave, matches total_num_heads precedent). - ModelOptQuantConfigBase.from_config reads it once, propagates as a kwarg to every subclass's _from_config. - ModelOptNvFp4Config honors it (forces W4A16 LinearMethod); the other three ModelOpt Configs absorb-and-discard via **kwargs. - ModelOptNvFp4FusedMoE extends use_a16 to OR on the override. Signed-off-by: Juhi Mittal <juhim@nvidia.com> Co-authored-by: Claude
…e flag Two new parametrized tests appended to tests/quantization/test_modelopt.py: - test_modelopt_nvfp4_moe_dispatches_to_marlin_when_w4a16 (3 cases) — ModelOptNvFp4FusedMoE must pass activation_key=None to select_nvfp4_moe_backend for both routes that should trigger W4A16 (on-disk quant_algo W4A16_NVFP4, or --override-activation-dtype set). Mocks the oracle to capture call args; verifies use_a16 and activation_key without needing a GPU or real ckpt. - test_modelopt_nvfp4_override_activation_dtype_pipeline (4 cases) — End-to-end dict -> ModelOptNvFp4Config.from_config -> correct LinearMethodCls. Exercises base read of __override_activation_dtype__, kwarg propagation, and the priority rule (override > on-disk algo). Both run in ~2s on CPU; all 7 cases pass locally. Signed-off-by: Juhi Mittal <juhim@nvidia.com> Co-authored-by: Claude
There was a problem hiding this comment.
Code Review
This pull request introduces the --override-activation-dtype engine-level flag, allowing users to force W4A16 execution (via Marlin) for ModelOpt NVFP4 checkpoints even if they were originally serialized as W4A4. The changes include updates to the engine arguments, model configuration, and the ModelOpt quantization logic to propagate and honor this override. However, a critical issue was identified where this override is not correctly propagated in mixed-precision configurations (ModelOptMixedPrecisionConfig), which would cause the flag to be silently ignored for those models.
| # Engine-level overrides flow in via private dict keys injected | ||
| # by weight_utils._maybe_inject_engine_overrides. Read them here | ||
| # once and propagate to every subclass's _from_config via kwargs. | ||
| # Subclasses that honor a knob name the kwarg in their signature | ||
| # (e.g. ModelOptNvFp4Config); others absorb-and-discard via | ||
| # **kwargs. The keys stay in original_config (no pop) — matches | ||
| # the total_num_heads precedent in compressed_tensors. | ||
| override_activation_dtype = config.get("__override_activation_dtype__", "auto") | ||
|
|
||
| return cls._from_config( | ||
| quant_method=quant_method, | ||
| kv_cache_quant_method=kv_cache_quant_method, | ||
| exclude_modules=exclude_modules, | ||
| group_size=group_size, | ||
| original_config=config, | ||
| override_activation_dtype=override_activation_dtype, | ||
| ) |
There was a problem hiding this comment.
The logic for propagating the override_activation_dtype is incomplete for mixed-precision models. The comment on lines 362-364 mentions that some subclasses will "absorb-and-discard" the kwarg, but ModelOptMixedPrecisionConfig should not do this. It contains an nvfp4_config and needs to propagate the override to it.
Currently, ModelOptMixedPrecisionConfig._from_config does not pass the override_activation_dtype to the ModelOptNvFp4Config it creates. This will cause the --override-activation-dtype flag to be silently ignored for mixed-precision models that include NVFP4 layers, which is a critical bug.
To fix this, ModelOptMixedPrecisionConfig._from_config should be updated to pass override_activation_dtype to its ModelOptNvFp4Config instance.
There appears to be a related pre-existing issue where ModelOptMixedPrecisionConfig.get_quant_method hardcodes ModelOptNvFp4LinearMethod instead of using the dynamically selected self.nvfp4_config.LinearMethodCls. This would also prevent the override from working correctly for linear layers in mixed-precision models.
|
Actually I would say #41566 is a duplicate since it is my general approach on activation overrides. It is just starting with MXFP4/MXFP8 since we have kernels for gpt-oss |
Thanks @mgoin , I will review and evaluate if we can use your approach for our ModelOpt path to ingest nvfp4 ckpts (and route to w4a16). cc- @pavanimajety |
|
This pull request has merge conflicts that must be resolved before it can be |
Summary
Adds ModelOpt NVFP4 W4A16 support for the fused-MoE path and an engine-level
--override-activation-dtypeflag that lets a user load a W4A4 NVFP4 checkpoint via the W4A16 path without editing the checkpoint's JSON.Follow-up to #41769 (phase-1, merged), which added
ModelOptNvFp4W4A16LinearMethodfor dense Linear. This PR completes W4A16 NVFP4 support across the model: MoE + a user-facing override.Tested: 7 unit tests + end-to-end smoke on
nvidia/Qwen3.6-35B-A3B-NVFP4(Qwen3.5-MoE-VL hybrid), with the flag (Marlin path) and without (FlashInferCutlass W4A4 path, regression check). See Test plan.What's in this PR
Three commits, each scoped to one logical change:
1.
[Quantization][ModelOpt] Add W4A16 NVFP4 support to fused MoEExtends
ModelOptNvFp4FusedMoE.__init__to honorW4A16_NVFP4on-disk checkpoints. Whenquant_config.quant_method == "W4A16_NVFP4", the MoE class passesactivation_key=Nonetoselect_nvfp4_moe_backend.Every W4A4 backend's
_supports_quant_schemerequires(kNvfp4Static, kNvfp4Dynamic)exactly, so they all reject themselves; Marlin's_supports_quant_scheme(fused_marlin_moe.py:607) only checksweight_key, so it accepts. Marlin's MoE prep already nulls activation scales inconvert_to_nvfp4_moe_kernel_format(oracle/nvfp4.py:362-363) and routes throughnvfp4_w4a16_moe_quant_config(oracle/nvfp4.py:433) — no other change needed.2.
[Quantization][ModelOpt] Add --override-activation-dtype CLI flag(4 files, +80/−8)Adds an engine-level dtype override that mirrors the existing
--override-attention-dtypeprecedent (vllm/config/model.py:315). When set tobfloat16orfloat16, a W4A4 NVFP4 checkpoint is routed through the W4A16 LinearMethod and Marlin MoE backend regardless of the on-diskquant_algo.Pipeline:
ModelConfig.override_activation_dtype: Literal["auto", "bfloat16", "float16"](inline at field site soget_kwargs(ModelConfig)auto-derives argparsechoices=).weight_utils._maybe_inject_engine_overrides()injects a__override_activation_dtype__private key intohf_quant_configat the two ModelOpt code paths. Inject-and-leave, matching thetotal_num_headsprecedent atcompressed_tensors.py:250-251.ModelOptQuantConfigBase.from_configreads the key once, propagates as a kwarg to every subclass's_from_config.ModelOptNvFp4Configpulls it viakwargs.get(...); the other three ModelOpt Configs absorb-and-discard via their existing**kwargs: Any.ModelOptNvFp4FusedMoE'suse_a16extends to OR onquant_config.override_activation_dtype in ("bfloat16", "float16").3.
[Quantization][ModelOpt] Add unit tests(+99 LOC intests/quantization/test_modelopt.py)Two parametrized tests, 7 effective cases:
Why a new flag rather than
--hf-overrides?--hf-overridesis the natural alternative for "patch the on-disk quant config." It has three paths to the quant parser and none are usable here:--hf-overrides '{"quantization_config": {...}}') —ModelConfig._apply_dict_overridesatvllm/config/model.py:454does wholesale replace on plain dict attributes. To change one field, the user has to type the entirequantization_configblock verbatim.hf_overrides.quantization_config_file) and Path C (hf_overrides.quantization_config_dict_json) atweight_utils.py:338,354are both gated byif hf_quant_config is None— unreachable when the ckpt already carries an on-disk quant config.--override-activation-dtypeis one token, semantically specific, and mirrors the existing single-subsystem override pattern. Full design + alternatives discussion in commit-2's added comments + the PR diff.Test plan
Done
Unit tests — 7/7 pass on CPU, no GPU / no ckpt needed
pytest tests/quantization/test_modelopt.py::test_modelopt_nvfp4_moe_dispatches_to_marlin_when_w4a16 \ tests/quantization/test_modelopt.py::test_modelopt_nvfp4_override_activation_dtype_pipeline -v # 7 passed in 2.07sEnd-to-end smoke —
nvidia/Qwen3.6-35B-A3B-NVFP4Qwen3.5-MoE-VL hybrid (gated-delta linear attention + conv1d + experts, ~35B). One GPU, eager mode.
With override (
LLM(override_activation_dtype="bfloat16", ...)) — exercises this PR's new path:The three log signals together prove the full pipeline wired up: on-disk
quant_algo=NVFP4(no JSON edit), override INFO line fires, MoE oracle picks Marlin (only backend that acceptsactivation_key=None).Without the flag — regression check (same ckpt, plain
LLM(...)):W4A4 path stays intact; the override INFO log does not fire; FlashInferCutlass NVFP4 MoE backend selected; coherent generation. Confirms zero behavioral drift when the flag is unset.
Duplicate-work check
QuantSpec(weight, activation)rework ofOnlineQuantScheme; this PR targets NVFP4 on ModelOpt via a newModelConfig.override_activation_dtypefield. Different quant method, different mechanism, no file overlap.modelopt nvfp4 moe w4a16oroverride-activation-dtype.