Skip to content

feat(custom_recipes): complete MXFP4 support and dispatch custom recipes in grouped GEMMs - #3824

Open
NolanHo wants to merge 6 commits into
NVIDIA:mainfrom
NolanHo:feat/mxfp4-custom-recipe-grouped
Open

NolanHo wants to merge 6 commits into
NVIDIA:mainfrom
NolanHo:feat/mxfp4-custom-recipe-grouped

Conversation

@NolanHo

@NolanHo NolanHo commented Nov 5, 2026

Copy link
Copy Markdown

What this changes

Makes CustomRecipe usable beyond plain Linear, and completes the MXFP4 (block-32 / E8M0) path in
the reference implementation. Without these, a custom quantizer can never be used for MoE experts and
the reference MXFP4 branch cannot run at all.

general_gemm already dispatches to a custom quantizer's qgemm; neither grouped GEMM entry does,
so GroupedLinear — the MoE expert path — goes straight to nvte_grouped_gemm with native tensors.

grep -c 'is_custom|custom_gemm|qgemm'  linear.py          -> 2
                                       grouped_linear.py -> 0

Changes, each tied to an observed failure

pytorch/custom_recipes/reference_nvfp4.py

  1. The pow_2_scales (MXFP4) branch is unfinished. It validates the configuration and then falls
    through to the common tail, which raises:
    UnboundLocalError: cannot access local variable 'row_input' where it is not associated with a value
    The source's own # TODO(etsykunov): Fix bug where global_amax_row and global_amax_col are not defined points at the same hole. This prepares row_input / col_input and both amax values on
    that branch.
  2. _split_quantization can hand this a 0-element split, where torch.max() without a reduction
    dim raises RuntimeError: max(): Expected reduction dim to be specified for input.numel() == 0.
    Guarded.
  3. size(*args, **kwargs) ignores its argument and always returns the full logical shape, so
    x.size(0) yields a torch.Size. GroupedLinear._validate_or_alloc_output does
    torch.empty((rows, cols)) with rows = x.size(0) and dies with
    TypeError: 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 is
    unchanged.
  4. update_usage refuses a placeholder. A tensor with neither data nor data_t populated has
    nothing to generate from, yet TE raises
    RuntimeError: Cannot generate FP4 data, even from FP4 data transpose. Treated as a no-op instead.
  5. No quantize_(). module/base.py::quantize_weight calls workspace.quantize_(...) when the
    workspace exposes it and otherwise falls back to tex.quantize(...), a C++ entry that cannot drive
    a Python reference quantizer — the workspace stays empty and the GEMM then fails on data is None.
    Implemented in Python.

pytorch/cpp_extensions/gemm.py

  1. Custom-recipe dispatch for both grouped GEMM entry points.
    • general_grouped_gemm (list form): per expert, the activation rows are B[i][0:m_splits[i]], the
      weight is A[i], and the result goes to out[0][offset : offset+m_splits[i]]. The operand roles
      are 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 (GroupedTensorStorage form): groups are sliced from
      offsets / scale_inv_offsets / rowwise_data / rowwise_scale_inv.

Both refuse contracts they cannot honour explicitly (GELU outputs, bias gradients, D_dtype, output
quantisation, 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 through CustomRecipe.qgemm:

  • the custom qgemm matches this reference implementation bitwise on real quantised operands
    (max|Δ| = 0.0, reference magnitude ≈ 12.6) for the fprop direction;
  • each change above is a fix for a failure reproduced end-to-end; the error strings quoted here are
    the observed messages.

Not exercised by this PR: grad/dgrad/wgrad for the grouped paths (explicitly refused), and
row_scaled_nvfp4 in the MXFP4 configuration (rejected upstream already).

@NolanHo
NolanHo requested a review from ksivaman as a code owner November 5, 2026 21:11
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Nov 5, 2026
@greptile-apps

greptile-apps Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 1/5

[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

  1. P1 Existing grouped calls now raise ▶
  2. P1 NVFP4 grouped forward still fails ▶
  3. P1 Grouped outputs lose their bias ▶
  4. P1 Backward runs the forward calculation ▶
  5. P2 Skipped updates still quantize weights ▶

Summary

The PR completes the reference MXFP4 quantization branch and adds custom-recipe calls to both grouped GEMM entries.

  • Per-expert grouped GEMMs now call custom quantizers.
  • Grouped-tensor forward GEMMs now call custom quantizers.
  • MXFP4 reference quantization now prepares both input directions.
  • Reference NVFP4 tensors support in-place updates and dimension sizing.
  • Custom quantizers now quantize weights without reusing cached workspaces.

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):

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.

P1 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.

Comment on lines +1126 to +1128
result = quantizer.qgemm(
qx, qw, m_params, out_dtype, sx, sw, None, gemm_type=GEMMType.FPROP,
)

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.

P1 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,

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.

P1 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.

Comment on lines +1071 to +1072
if grad and use_bias:
raise NotImplementedError("custom grouped GEMM does not define bias gradients")

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.

P1 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.

Comment on lines +403 to +404
del noop_flag
fresh = self._quantizer.quantize(tensor)

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.

P2 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.

@ptrendx

ptrendx commented Nov 6, 2026

Copy link
Copy Markdown
Member

@NolanHo Please sign every commit in this PR. The DCO check is failing because commits are missing a Signed-off-by trailer. See the contribution guide.

@NolanHo
NolanHo force-pushed the feat/mxfp4-custom-recipe-grouped branch from 2e8bf88 to 6658007 Compare November 6, 2026 19:40
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>
@NolanHo
NolanHo force-pushed the feat/mxfp4-custom-recipe-grouped branch from 6658007 to 4d83b57 Compare November 6, 2026 19:42
… 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>
@NolanHo
NolanHo force-pushed the feat/mxfp4-custom-recipe-grouped branch from 54d5879 to 9d083a9 Compare November 7, 2026 12:20

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants