Force NVFP4 W4A8 path for NVFP4_W4A16 layers on Blackwell, where NVFP4 normally uses the native W4A4 path. - #24364
Conversation
| mul_mat_q_case<GGML_TYPE_NVFP4, true>(ctx, args, stream); | ||
| break; | ||
| } | ||
| #endif // GGML_CUDA_HAS_BLACKWELL_TARGET |
There was a problem hiding this comment.
How do we opt out from this? A drop from 5486.02 to 4492.20 is very severe.
If you want a higher precision, there's a variety of Q4 quants that are just as small and even more precise (see #23572 for detailed comparisons).
There was a problem hiding this comment.
There is no need to opt out from this, if you want to run W4A4 take a checkpoint with W4A4.
The intention of this PR is if a checkpoint has W4A16 layers it should have activations in higher precision.
This PR doesn't cause regression on pure W4A4_NVFP4 checkpoints.
You can confirm there is no regression by testing llama-bench with this PR on below checkpoints:
- Pure W4A4_NVFP4: https://huggingface.co/RedHatAI/Qwen3.6-35B-A3B-NVFP4
- W4A16_NVFP4: https://huggingface.co/nvidia/Qwen3.6-35B-A3B-NVFP4
There was a problem hiding this comment.
There may be a case that there aren't other checkpoints or somebody likes one's calibration over another. I think many prefer increased speed and that is why they pick NVFP4. Usually Q4_K~ will always be better precision than NVFP4 if that is what they are going for and then it may end up faster than skipping the native FP4. We're blending checkpoints with Q_K quants and NVFP4 combined that can compensate for ppl loss. I think some selection control by the user would be a good balance to let them decide.
There was a problem hiding this comment.
ModelOpt, Redhat came up with W4A16_NVFP4 after through investigations on performance and accuracy. Checkout this weight-only-quantization-schemes.
We should honor the intention behind creating a recipe/checkpoint which was specifically designed for W4A16_NVFP4.
If someone intends to use W4A4 on Blackwell they should use a W4A4 checkpoint.
Few PR on vLLM for reference, that were added by ModelOpt to support W4A16_NVFP4 :
There was a problem hiding this comment.
I think some selection control by the user would be a good balance to let them decide.
I mean we can in principle add such a knob in the CUDA backend to give the ability to override model builder intents encoded in the GGUF, but the default should be what is encoded in the GGUF
There was a problem hiding this comment.
Added the knob for user controlled W4A4_NVFP4 fast path on Blackwell, even if the checkpoint specifies W4A16_NVFP4 layers
am17an
left a comment
There was a problem hiding this comment.
I didn't look into the PR in detail, but does using 8-bit activation disable the fp4 tensor core?
Yes. Basically you can think of this as a step towards the support of weight-only-quantization-schemes in llama.cpp. |
b72a8c9 to
18f1df3
Compare
ORippler
left a comment
There was a problem hiding this comment.
@ggerganov @CISC thoughts on this approach and the required granularity for W4A4 vs. W4A16?
| if (CMAKE_CUDA_ARCHITECTURES MATCHES "(^|;)12[0-9]a(-real|-virtual)?($|;)") | ||
| add_compile_definitions(GGML_CUDA_HAS_BLACKWELL_TARGET) | ||
| endif() |
There was a problem hiding this comment.
Just FYI this compile definition will be visible for all archs in CMAKE_CUDA_ARCHITECTURES (per arch specialization requires constructing nvcc commands by hand)
| if self._is_nvfp4: | ||
| for tensor_name, entry in quant_layers.items(): | ||
| if not isinstance(entry, dict) or entry.get("quant_algo") != "W4A16_NVFP4": | ||
| continue | ||
| if "lm_head" in tensor_name or "output" in tensor_name: | ||
| self._nvfp4_w4a16_output = True | ||
| continue | ||
| bid_m = re.search(r'\.layers\.(\d+)\.', tensor_name) | ||
| if bid_m: | ||
| self._nvfp4_w4a16_blocks.add(int(bid_m.group(1))) |
There was a problem hiding this comment.
https://huggingface.co/nvidia/Qwen3.6-35B-A3B-NVFP4/blob/main/config.json#L455-L470
huggingface/modelopt seems to offer storing this information on a per-weight level, as opposed to per-transformer-block we parse here. Later on, we set the marker also on a per-weight/op level. I feel this is inconsistent and we should adapt the conversion script to support the finer level of granularity also
ggerganov
left a comment
There was a problem hiding this comment.
If I read correctly, this introduces a path to do w4a8 where we normally do w4a4. And the reason is that w4a8 has higher accuracy?
Is there a case where we would want to use w4a4? If no, we can probably simplify a lot of the logic.
Yet, if there is specific tensor hardware for that, it's probably useful.
| // NVFP4 W4A16: per-layer + LM head flag, true where NVFP4 weights skip activation quantization. | ||
| std::array<bool, LLAMA_MAX_LAYERS> nvfp4_w4a16_layer_arr = {}; | ||
| bool nvfp4_w4a16_output = false; | ||
|
|
There was a problem hiding this comment.
This does not feel like it belongs to hparams. It's too low-level, backend-specific information.
There was a problem hiding this comment.
Should we make this as a per-tensor attribute in llama-model at load time? This will also help acknowledge @ORippler's comment as well to have more flexibility for per-tensor information.
https://huggingface.co/nvidia/Qwen3.6-35B-A3B-NVFP4/blob/main/config.json#L455-L470
huggingface/modelopt seems to offer storing this information on a per-weight level, as opposed to per-transformer-block we parse here. Later on, we set the marker also on a per-weight/op level. I feel this is inconsistent and we should adapt the conversion script to support the finer level of granularity also
There was a problem hiding this comment.
Yes, we should formulate this information to be per tensor. Also, it should abstract away the NVFP4 and make it more generic. For example, "allow 4-bit activations (bool)" seems generic enough.
We can represent this information with a map: tensor name -> bool. The values are false by default. The map is optional. To represent the map in GGUF, you'll need 2 arrays - the first one with the tensor names and the second one with the same size and the bool values.
There was a problem hiding this comment.
"allow 4-bit activations (bool)"
I intuitively think about this PR/feature as "enabling Weight-only-Quantization schemes", so naming should signal this intent if you agree -> "weight-only quantization (bool)" / "allow activation quantization (bool)".
W4A16 is effectively weight-only, as LLMs are trained in BF16 precision typically
There was a problem hiding this comment.
In some backends, we already quantize the activations by default (to 8-bits). So I'm not sure if it is not going to be a bit misleading to call it "weight-only quantization (bool)" / "allow activation quantization (bool)".
There was a problem hiding this comment.
Updated the change so allow-activation-quant is stored per-tensor. Since it is currently NVFP4-specific, activation quantization is enabled by default for all layers. For layers that use W4A16_NVFP4 as the quantization method, the activation-quant array stores the tensor name paired with false. Please review.
Yes on both. The motivation is that doing W4A16 (or weight-only-quantization in general) may allow one to quantize more layers than when one quantizes both activations and weights. This reduces memory-footprint of the model, and furthermore increases decode throughput - decode is effectively weight-streaming in local inference with small BS setting) |
|
Correct me if I am wrong, but I think in the CUDA backend we already do w4a8 for existing types like Q4 - we do it by default. If that's the case, adding support for w4a8+nvfp4 would make sense only if it is better quality-wise than the existing w4a8+q4 methods. |
I think that's going to be difficult to prove generally, though I can add the single model we have validated so far:
|
|
For my understanding, the nvidia/Qwen3.6-35B-A3B-NVFP4 readme says that the model was quantized with the Model Optimizer. Does the quantization process involve some form of quantization aware training? My understanding is that if a model is trained natively in NVFP4 format, the best thing to do is keep the format intact during inference. I.e. it's not beneficial to use Q4 quantizations in such case on hardware that supports NVFP4. But I would assume that if the model was trained in NVFP4 in the first place, then the recommended way to run would be If the model was not trained in NVFP4, and instead was quantized to NVFP4, then we have to justify and demonstrate in which cases it is worth it compared to the Q4 formats. One simple justification is that It might be worth taking a look at the KLD between these quantizations and a reference BF16 model. For example, let's take a look at KLD and speed for: |
No, training was involved, the checkpoint is yielded by PTQ only. QAT/QAD would be disclosed in the model card like this https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4#training-methodology (this is a W4A4 checkpoint).
As outlined above, the proposed benefit of weight-only quantization is to allow the quantization of weights more aggressive at iso-quality. We will try to collect & present some numbers. In general, the idea would be to go for NVFP4 with W4A16, the W4A8 path is just a transient step. |
18f1df3 to
2c7052a
Compare
For my understanding, is the expectation that W4A16 would be faster compared to W4A8 if implemented efficiently? |
| #### GGML_CUDA_FORCE_W4A4 | ||
|
|
||
| NVFP4 models that carry W4A16 layers request higher-precision activations, so on Blackwell those layers run through the W4A8 path instead of the native W4A4 path. Set `GGML_CUDA_DISABLE_FORCE_W4A8=1` to ignore that request and keep the native W4A4 path for faster prompt processing at the cost of accuracy. | ||
| NVFP4 models that carry W4A16 layers request higher-precision activations (W4A8), so on Blackwell those layers run through the W4A8 path instead of the native W4A4 path. Set `GGML_CUDA_FORCE_W4A4=1` to override that request and keep the native W4A4 path for faster prompt processing at the cost of accuracy. |
There was a problem hiding this comment.
At some point we can remove this compile-time option and toggle this functionality at runtime through a libllama argument. It will simply skip setting the quantization hints for the matrix multiplications (i.e. override the activation policy). The idea is to avoid "communicating" directly with backend.
| @@ -443,7 +443,7 @@ extern "C" { | |||
| enum ggml_op_hint { | |||
| GGML_HINT_NONE = 0, | |||
| GGML_HINT_SRC0_IS_HADAMARD = 1, | |||
| GGML_HINT_NO_QUANT_SRC1 = 2, // W4A16_NVFP4: keep activations higher precision. | |||
| GGML_HINT_NO_QUANT_SRC1 = 2, // keep src1 at higher precision (skip 4-bit activation quant). | |||
There was a problem hiding this comment.
If we agree on the "allow 4-bit activations", then we should consistently name where relevant. For example here: GGML_HINT_SRC1_ALLOW_4BIT.
| # Per-tensor activation precision policy (tensor name -> allow 4-bit activations). | ||
| ALLOW_4BIT_ACT_TENSOR = "general.allow_4bit_act.tensor" | ||
| ALLOW_4BIT_ACT_VALUE = "general.allow_4bit_act.value" |
There was a problem hiding this comment.
This can become more generic and future-proof to allow setting additional per-tensor options in the future. To do that, the array with the tensor names should be called something like "general.tensor_extra.name". And the "allow 4-bit activations" values should be stored in the bool array named "general.tensor_extra.allow_4bit_act".
| if (no_quant_src1_for_weight(act_policy, w)) { | ||
| ggml_mul_mat_set_hint(res, GGML_HINT_NO_QUANT_SRC1); | ||
| } | ||
|
|
There was a problem hiding this comment.
Should be:
| if (no_quant_src1_for_weight(act_policy, w)) { | |
| ggml_mul_mat_set_hint(res, GGML_HINT_NO_QUANT_SRC1); | |
| } | |
| if (llama_act_policy_allow_4bit(act_policy, w)) { | |
| ggml_mul_mat_set_hint(res, GGML_HINT_SRC1_ALLOW_4BIT); | |
| } | |
| @@ -18,6 +18,7 @@ struct ggml_tensor; | |||
|
|
|||
| struct llama_cparams; | |||
| struct llama_layer; | |||
| struct llama_weight_act_policy; | |||
There was a problem hiding this comment.
| struct llama_weight_act_policy; | |
| struct llama_act_policy; |
|
My opinion is that we should solve this differently at a ggml level. As of right now we have this for "precision": // precision
enum ggml_prec {
GGML_PREC_DEFAULT = 0, // stored as ggml_tensor.op_params, 0 by default
GGML_PREC_F32 = 10,
};I am interpreting "default" to mean that we don't really care about the precision and that the backends should just optimize for speed/memory use. So on Blackwell that would mean W4A4. But we can add something like To be clear: I think that the way we currently define "precision" in ggml is poorly defined and needs more maintainer attention. My opinion is that |
This makes sense to me. It would reduce the ambiguity created by having multiple precision-related signals and hints. |
|
I agree it would be nice to have more clear instructions to the backend about what precision to use, but who makes that choice and how? And I think we need to be more clear about weight/activation precision vs accumulator precision/range. |
0a1701e to
a470fa6
Compare
In the current design for the gguf carries the information for activation precision per-tensor and backend honors it. |
There is no straightforward answer to this, as WXAY with X/Y specifying bits does not tell whether its FP or INT. Generally, W4A16 is the way to go for weight-only-quantization as (I) it's the way that the checkpoint are validated during PTQ in weight-only-quantization settings, and (II) will be accelerated equally across HW stacks. I stress again to think about this PR as "adding support for weight-only-quantization schemes" rather than "add this one niche-case for ModelOpt + NVFP4"
+1 on this, as I think precision in its current form is only used to signal accumulator precision/range.
|
a470fa6 to
5de3322
Compare
|
Trying to move this forward - prepared a ggml-level proposal concerning |
Signed-off-by: ynankani <ynankani@nvidia.com>
822a5b7 to
d47e561
Compare
Overview
This PR adds support to force W4A8 path for W4A16_NVFP4 HF model layers on Blackwell, where NVFP4 normally uses the native W4A4 path.
This PR includes the below:
GGML_HINT_NO_QUANT_SRC1hint for NVFP4_W4A16 layersmul_mat_idas wellmul_matfor dense andmul_mat_idMoE modelsAdditional information
Observed a quality improvement for W4A8 compared to W4A4 and are able to meet stricter quality threshold of the test_mul_mat cases used by ggml.
Also it is mentioned that "W4A4 sometimes difficult to achieve for small LLMs" in this paper URL
Tested on : nvidia/Qwen3.6-35B-A3B-NVFP4
Force W4A8 for W4A16_NVFP4 layers:
Master Baseline:
Perplexity Improvement:
Requirements