[PyTorch] NVFP4: emit GEMM-swizzled scales in the non-RHT 2D quantize kernel#3190
Conversation
Greptile SummaryThis PR extends the NVFP4 2D quantize kernel to emit GEMM-swizzled scale factors directly (no standalone swizzle pass) for 128-aligned shapes on the non-RHT path, fixing CUDA graph capture issues when NVFP4 weight caching is enabled. It also corrects a latent bug where the backward-path fallback kernel hardcoded compact-scale output even when the tensor was pre-allocated expecting swizzled scales.
Confidence Score: 5/5This PR is safe to merge. The swizzle index formula is mathematically identical to the existing mxfp8 reference and the Python test utility, buffer sizes are unchanged for the gated 128-aligned shapes, and NVTE_CHECKs correctly constrain the new path to the 2D kernel on 128-aligned dims only. The swizzle formula in the new swizzle.cuh expands to the same expression as mxfp8::swizzle::gemm_swizzled_scale_idx and the Python reference used by the fidelity tests. Namespace resolution is correct (function is in transformer_engine::dispatch::nvfp4::swizzle, accessed from the nvfp4 namespace as swizzle::). Buffer sizes for compact and swizzled layouts are equal for 128-aligned shapes, so no allocation size change is required. The fallback path fix in quantize.cuh (removing the hardcoded false for the backward path) is consistent with the forward path and with how the fallback kernel already supports swizzled scales. The two new test files provide direct fidelity, pointer-stability, and CUDA-graph-replay coverage for the fix. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["quantizer(x) called\noptimize_for_gemm=True\nwith_2d_quantization=True"] --> B["nvfp4_emits_gemm_swizzled_scales()"]
B --> C{with_rht?}
C -- Yes --> D["is_eligible_for_rht_cast_fusion(shape)"]
C -- No --> E{with_2d_quantization\n&& !row_scaled\n&& !4over6?}
E -- No --> F["with_gemm_swizzled_scales = false\n compact scales only"]
E -- Yes --> G["is_eligible_for_2d_swizzle_fusion(shape)\n(rows%128==0 && cols%128==0 && sm100-110)"]
D -- eligible --> H["with_gemm_swizzled_scales = true"]
D -- ineligible --> F
G -- eligible --> H
G -- ineligible --> F
H --> I["create_tensor allocates scale buffer"]
F --> I
I --> J["quantize_fwd_helper / quantize_bwd_helper"]
J --> K{use_optimized_kernel?}
K -- Yes --> L["nvfp4::quantize_transpose (2D kernel)"]
K -- No --> M["quantize_transpose_vector_blockwise_fp4\nswizzled_scale=with_gemm_swizzled_scales"]
L --> N{WITH_GEMM_SWIZZLED_SCALES?}
N -- true --> O["gemm_swizzled_scale_idx()\nwrite SF at swizzled offset in-kernel"]
N -- false --> P["compact SF write + post-quantize swizzle pass"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["quantizer(x) called\noptimize_for_gemm=True\nwith_2d_quantization=True"] --> B["nvfp4_emits_gemm_swizzled_scales()"]
B --> C{with_rht?}
C -- Yes --> D["is_eligible_for_rht_cast_fusion(shape)"]
C -- No --> E{with_2d_quantization\n&& !row_scaled\n&& !4over6?}
E -- No --> F["with_gemm_swizzled_scales = false\n compact scales only"]
E -- Yes --> G["is_eligible_for_2d_swizzle_fusion(shape)\n(rows%128==0 && cols%128==0 && sm100-110)"]
D -- eligible --> H["with_gemm_swizzled_scales = true"]
D -- ineligible --> F
G -- eligible --> H
G -- ineligible --> F
H --> I["create_tensor allocates scale buffer"]
F --> I
I --> J["quantize_fwd_helper / quantize_bwd_helper"]
J --> K{use_optimized_kernel?}
K -- Yes --> L["nvfp4::quantize_transpose (2D kernel)"]
K -- No --> M["quantize_transpose_vector_blockwise_fp4\nswizzled_scale=with_gemm_swizzled_scales"]
L --> N{WITH_GEMM_SWIZZLED_SCALES?}
N -- true --> O["gemm_swizzled_scale_idx()\nwrite SF at swizzled offset in-kernel"]
N -- false --> P["compact SF write + post-quantize swizzle pass"]
Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/nvfp4-weigh..." | Re-trigger Greptile |
… kernel Fixes a CUDA graph hang/corruption with NVFP4 weight caching. Scope: the non-RHT 2D quantize path on 128-aligned shapes, which is the default cached-weight case. Other paths (1D weight scaling, non-128-aligned, etc) keep the compact + post-quantize swizzle fallback and are left for follow-ups. - swizzle.cuh: NVFP4 128x4 GEMM-swizzled scale-index helper (byte-compatible with nvte_swizzle_scaling_factors). - quantize_transpose_nvfp4_2D_kernel: add WITH_GEMM_SWIZZLED_SCALES; write rowwise and columnwise scales at swizzled offsets; relax the compact-only guard to allow the 2D swizzled path on 128-aligned dims. - dispatch: thread with_gemm_swizzled_scales through the blockwise fallback (which already implements kSwizzledScale) instead of hardcoding false. - quantizer: is_eligible_for_2d_swizzle_fusion + shared gate so the non-RHT 2D path enables swizzled SF on 128-aligned shapes; the post-quantize swizzle fallback auto-skips when the flag is set. - tests: byte-equal SF vs nvte_swizzle_scaling_factors (rowwise/columnwise/ both, multiple shapes) + shape gate; cached-weight scale-pointer stability and CUDA graph capture/replay regression. Signed-off-by: Cael Ling <caell@nvidia.com>
2ebed7b to
00b8d30
Compare
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
LGTM. Thanks for the PR @cael-ling. Great work!
For more context, we enabled weight preswizzling in the previous PR by setting optimize_for_gemm in the weight quantizer to be True for unquantized primary weights.
However when weight caching + cuda graph is enabled, this led to a crash with nvfp4 recipe. Previously, NVFP4 quantization with weight preswizzling first quantizes the weights and then modifies the scales to point to a new swizzled buffer. This didnt work, since cuda graph capture on the cached weights wont be aware of the scale buffer being pointed to a new swizzled location.
Now, the preswizzling being done as part of the quantize kernel, means the quantizer kernel directly writes into the cached weight's swizzled buffer and the scale buffer is never modified
|
/te-ci |
Description
Support the CUDA graph capture when enabling NVFP4 weight caching in #3093
Scope: the non-RHT 2D quantize path on 128-aligned shapes, which is the default cached-weight case. Other paths (1D weight scaling, non-128-aligned, etc) keep the compact + post-quantize swizzle fallback and are left for follow-ups.
Type of change
Changes
Please list the changes introduced in this PR:
Checklist: