Repository navigation
Conversation
[Medium impact] The PR extends custom quantizer behavior across grouped forward calls and reference NVFP4 use. The PR is not ready to merge because grouped calls can still raise, omit bias, or compute the wrong backward result. Findings
SummaryThe PR completes the reference MXFP4 quantization branch and adds custom-recipe calls to both grouped GEMM entries.
Reviews (7) · Last reviewed commit: "docs(custom_recipes): record the per-gro..." · Reviewed by Greptile |
| The caller must ensure that GroupedTensor metadata is already compatible with the | ||
| underlying GEMM implementation (e.g., aligned offsets and output metadata layout). | ||
| """ | ||
| if is_custom(A) or is_custom(B) or is_custom(out): |
There was a problem hiding this comment.
Existing grouped calls now raise
is_custom rejects plain GroupedTensorStorage objects and lists with an AssertionError. The existing grouped forward passes a plain GroupedTensorStorage as its output, so even non-custom grouped GEMMs now fail before reaching the native implementation. Check grouped objects through their quantizers and inspect list members instead of passing these containers to is_custom.
| result = quantizer.qgemm( | ||
| qx, qw, m_params, out_dtype, sx, sw, None, gemm_type=GEMMType.FPROP, | ||
| ) |
There was a problem hiding this comment.
NVFP4 grouped forward still fails
This call omits qresult_x and qresult_w. NVFP4QuantizerRef.qgemm requires both for non-pow2 NVFP4 because they carry the global scales. A grouped forward using the supplied NVFP4 reference recipe therefore raises instead of computing its output. Pass act and w through these arguments, as custom_gemm does.
| sx = sx[:rows] | ||
|
|
||
| result = quantizer.qgemm( | ||
| qx, qw, m_params, out_dtype, sx, sw, None, gemm_type=GEMMType.FPROP, |
There was a problem hiding this comment.
Grouped outputs lose their bias
The helper accepts use_bias=True for forward, but passes None as the bias and never adds bias[i] afterward. GroupedLinear relies on this call to add its biases, so custom recipes that otherwise run return silently wrong outputs. Pass each bias to qgemm, apply it afterward, or explicitly reject this option.
| if grad and use_bias: | ||
| raise NotImplementedError("custom grouped GEMM does not define bias gradients") |
There was a problem hiding this comment.
Backward runs the forward calculation
This guard rejects only grad and use_bias, but the helper always runs GEMMType.FPROP on rowwise weights. GroupedLinear requests input gradients with grad=True, layout="NN", and no bias. For square weights, this can pass every shape check while computing the gradient with the wrong weight direction. Reject grad=True and non-TN layouts until those paths are implemented.
| del noop_flag | ||
| fresh = self._quantizer.quantize(tensor) |
There was a problem hiding this comment.
Skipped updates still quantize weights
quantize_ discards noop_flag and always quantizes. The weight-cache caller passes this flag to skip updates on later microbatches during CUDA graph replay. Ignoring it repeats weight quantization on every replay and removes that cache benefit. Honor the flag without adding a CPU read that would prevent graph capture.
|
@NolanHo Please sign every commit in this PR. The DCO check is failing because commits are missing a |
2e8bf88 to
6658007
Compare
CustomRecipe 目前只能用于普通 Linear:`general_gemm` 有自定义分流,而 `general_grouped_gemm` / `general_grouped_gemm_for_grouped_tensor` 没有,因此 MoE 专家(走 `GroupedLinear`)永远无法到达 quantizer 的 `qgemm`。同时参考实现里 block-32 / E8M0(即 MXFP4)那条分支未写完,即使能走到也会中途报错。 本补丁按实测失败逐项修复(每项都有可复现的报错原文): reference_nvfp4.py 1. pow_2(MXFP4)分支只做了配置校验,从未准备 `row_input` / `col_input` / `global_amax_row` / `global_amax_col`,公共尾部因此抛 `UnboundLocalError: cannot access local variable 'row_input'`。 补上赋值,并对 `_split_quantization` 可能产生的 0 元素分片加保护 (`torch.max()` 无参调用在空张量上会抛 `RuntimeError: max(): Expected reduction dim to be specified`)。 2. `size(*args, **kwargs)` 忽略参数、始终返回整个逻辑形状,导致调用方 `GroupedLinear._validate_or_alloc_output` 的 `rows = x.size(0)` 得到 `torch.Size`, `torch.empty((rows, cols))` 抛 `TypeError: empty(): argument 'size' failed to unpack the object at pos 2`。 改为 `size(dim=None)`,传入 dim 时返回该维长度。 3. `update_usage` 在两个方向都没有数据(占位张量)时直接抛 `Cannot generate FP4 data, even from FP4 data transpose`;占位符没有任何可推导的 数据,应视为 no-op 而不是错误。 4. 补 `quantize_()`。`module/base.py::quantize_weight` 在 workspace 暴露 `quantize_` 时走它,否则回落 `tex.quantize(...)`——后者无法驱动 Python 参考 量化器,workspace 会保持为空,随后 GEMM 拿到 `data is None`。 cpp_extensions/gemm.py 5. 给 `general_grouped_gemm` 增加自定义分流(列表形态:A 为权重列表、B 为激活列表、 out 为单张量、`m_splits` 为每个专家的有效激活行数),逐组调用 `qgemm`。 6. 给 `general_grouped_gemm_for_grouped_tensor` 增加自定义分流 (`GroupedTensorStorage` 形态,按 `offsets` / `scale_inv_offsets` 切组)。 两处分流都对不支持的契约显式拒绝(GELU 输出、bias 梯度、D_dtype、输出量化、 grad),而不是静默产生错误结果。 实测背景:在 B300(sm_103,EP8+CP8)上,MXFP4 blockscaled fprop GEMM 已用 CUTLASS `KernelTmaWarpSpecialized1SmBlockScaledMxNvf4UltraVs32Sm103` 实现并与参考实现 逐位一致;上述缺口是它无法接入训练的原因。 Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
for more information, see https://pre-commit.ci Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
…ed custom GEMM TEGroupedLinear passes one output tensor per expert rather than a single concatenated buffer, so refusing single_output=False made the MoE expert path unusable even with the dispatch in place. Both layouts are now handled: with a single buffer each expert writes rows [offset : offset+m_splits[i]], and with a list each expert writes its own tensor. Verified on a TEGroupedLinear with four experts: the dispatch is reached and every group is dispatched to qgemm. Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
for more information, see https://pre-commit.ci Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
A custom (reference) quantizer cannot be cached safely. The cached entry is a TensorSpec whose storage
is driven by update_usage, and a gradient-checkpoint replay can reach the GEMM after that storage has
been released, which surfaces as
FPROP GEMM: quantized weight data (B.data) is None
in a model whose single un-replayed forward is fine. Measured on DSV4.1-Flash (40 layers, EP8/CP8) with
an MXFP4 CustomRecipe: identical weights and identical forward, and the failure appears only on the
replay path (checkpoint.py recompute_fn -> transformer_layer._run_mlp), where the operand handed to
the GEMM is a payload-less reference tensor of the right shape while the module's own parameter is a
healthy bf16 Parameter.
Skipping the workspace path for custom quantizers re-quantises per call instead. With this in place the
same run completes a full training step (loss 3.667, grad_norm 1.823) with gradient checkpointing left
ON, which is what keeps activation memory within budget.
Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
6658007 to
4d83b57
Compare
… custom GEMM The grouped entry point issues one single-group GEMM per expert group. On a real training step that is ~40,000 kernel launches for ~1,000 GEMMs of work. The per-launch cost is small enough that it never surfaces in a per-phase profile, but the sum dominates the custom-recipe path's remaining overhead versus the native FP4 path. Recorded at the loop rather than fixed: collapsing it requires a grouped GEMM kernel, which is a separate change with its own numerical acceptance test. Signed-off-by: Nolan Ho <nolanho@users.noreply.github.com>
54d5879 to
9d083a9
Compare
What this changes
Makes
CustomRecipeusable beyond plainLinear, and completes the MXFP4 (block-32 / E8M0) path inthe reference implementation. Without these, a custom quantizer can never be used for MoE experts and
the reference MXFP4 branch cannot run at all.
general_gemmalready dispatches to a custom quantizer'sqgemm; neither grouped GEMM entry does,so
GroupedLinear— the MoE expert path — goes straight tonvte_grouped_gemmwith native tensors.Changes, each tied to an observed failure
pytorch/custom_recipes/reference_nvfp4.pypow_2_scales(MXFP4) branch is unfinished. It validates the configuration and then fallsthrough to the common tail, which raises:
UnboundLocalError: cannot access local variable 'row_input' where it is not associated with a valueThe source's own
# TODO(etsykunov): Fix bug where global_amax_row and global_amax_col are not definedpoints at the same hole. This preparesrow_input/col_inputand both amax values onthat branch.
_split_quantizationcan hand this a 0-element split, wheretorch.max()without a reductiondim raises
RuntimeError: max(): Expected reduction dim to be specified for input.numel() == 0.Guarded.
size(*args, **kwargs)ignores its argument and always returns the full logical shape, sox.size(0)yields atorch.Size.GroupedLinear._validate_or_alloc_outputdoestorch.empty((rows, cols))withrows = x.size(0)and dies withTypeError: empty(): argument 'size' failed to unpack the object at pos 2 with error "type must be tuple of ints, but got torch.Size".Changed to
size(dim=None), returning that dimension when asked. The no-argument behaviour isunchanged.
update_usagerefuses a placeholder. A tensor with neitherdatanordata_tpopulated hasnothing to generate from, yet TE raises
RuntimeError: Cannot generate FP4 data, even from FP4 data transpose. Treated as a no-op instead.quantize_().module/base.py::quantize_weightcallsworkspace.quantize_(...)when theworkspace exposes it and otherwise falls back to
tex.quantize(...), a C++ entry that cannot drivea Python reference quantizer — the workspace stays empty and the GEMM then fails on
data is None.Implemented in Python.
pytorch/cpp_extensions/gemm.pygeneral_grouped_gemm(list form): per expert, the activation rows areB[i][0:m_splits[i]], theweight is
A[i], and the result goes toout[0][offset : offset+m_splits[i]]. The operand rolesare taken from
GroupedLinear's own call site (grouped_linear.py), which passes(weights_fp8, inputmats, [out], single_output=True, m_splits=...).general_grouped_gemm_for_grouped_tensor(GroupedTensorStorageform): groups are sliced fromoffsets/scale_inv_offsets/rowwise_data/rowwise_scale_inv.Both refuse contracts they cannot honour explicitly (GELU outputs, bias gradients,
D_dtype, outputquantisation,
grad) rather than producing a silently wrong result.Testing
Measured on 8× sm_103 (B300), EP8 + CP8, with an E2M1/block-32/E8M0 CUTLASS kernel
(
KernelTmaWarpSpecialized1SmBlockScaledMxNvf4UltraVs32Sm103) bound throughCustomRecipe.qgemm:qgemmmatches this reference implementation bitwise on real quantised operands(
max|Δ| = 0.0, reference magnitude ≈ 12.6) for the fprop direction;the observed messages.
Not exercised by this PR:
grad/dgrad/wgrad for the grouped paths (explicitly refused), androw_scaled_nvfp4in the MXFP4 configuration (rejected upstream already).