[DSV4] hc-prenorm: fuse the combine step into a Triton kernel - #35118
Conversation
The torch hc-prenorm path combines the sinkhorn weights with the flattened
activations as
(pre.unsqueeze(-1) * x_flat.view(M, HC, H)).sum(dim=1)
which materialises an [M, HC, H] intermediate and then reduces it. `hc_combine`
accumulates over HC in registers in fp32 and writes [M, H] once.
Speedup over the previous expression, RTX 6000D, bf16, HC=4, H=7168:
M before after speedup
1 11.2 us 9.7 us 1.16x
8 10.4 us 9.3 us 1.12x
64 10.7 us 9.3 us 1.15x
1024 133.8 us 12.6 us 10.60x
4096 647.7 us 231.3 us 2.80x
16384 2533.1 us 920.3 us 2.75x
Max relative error against the previous expression, over the same reduction:
m=7 hc=4 h=128 bf16 4.61e-03
m=1024 hc=4 h=512 bf16 4.18e-03
m=33 hc=8 h=256 fp16 6.53e-04
m=2048 hc=4 h=1024 bf16 3.52e-03
m=1 hc=4 h=64 bf16 3.42e-03
Within bf16 rounding; the fused kernel accumulates in fp32, the previous
expression accumulated in the input dtype.
Split out of sgl-project#29927: this is arch-independent and does not belong in an SM120
enablement PR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fridge003 asked for smaller PRs instead of one large one carrying several independent optimizations. Two of them are not SM120 work at all and now live on their own: sgl-project#35116 allocate the page-split buffer outside inference mode (correctness fix) sgl-project#35118 fuse the hc-prenorm combine step into a Triton kernel (2.75-10.6x) Both are reverted here, so this PR keeps only the SM120 enablement. The triton imports in mhc.py went with hc_combine; the remaining SM120 change in that file is the TileLang warp-specialization workaround. This PR now depends on sgl-project#35116: without it, the page-split buffer is allocated under inference mode during autotune and CUDA graph capture cannot write to it. Will rebase once that lands. A third candidate, vectorizing the page-split copy in u64 lanes, measured within noise on RTX 6000D (119.3 -> 121.1, 429.8 -> 433.4, 541.6 -> 540.1 GB/s at 2048/ 8192/16384 pages), so it is not worth a PR of its own and stays here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
31 cases (m in {1, 7, 64, 1024, 2048} x (hc, h) in {(4,128), (4,512), (8,256)} x
{bf16, fp16}), registered for base-b-kernel-unit.
The reference is computed in fp32 rather than in the input dtype: hc_combine
accumulates over HC in fp32 while the previous expression accumulated in the
input dtype, so pinning the kernel to the old rounding would be pinning the
wrong thing. Both are asserted to agree with the fp32 result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review feedback on sgl-project#35118: pass stride information instead of requiring a contiguous `pre`. The call site's `pre.squeeze(1)` was already contiguous (`hc_split_sinkhorn` allocates it fresh with s == 1), so the `.contiguous()` was a no-op, but it made contiguity an unwritten contract on the caller. Device time is unchanged at M = 1 / 64 / 1024 / 4096 / 16384 (HC=4, H=7168), and the strided result matches the contiguous copy bit-for-bit. The unit test named `..._non_contiguous_pre_...` called `.contiguous()` on its input and so never exercised a strided `pre`; it now passes a real strided view. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
/rerun-test test/registered/kernels/ops/layernorm/test_hc_combine.py test/registered/models_e2e/test_deepseek_v4_flash_fp4_b200.py |
|
|
/rerun-test test/registered/kernels/ops/layernorm/test_hc_combine.py test/registered/models_e2e/test_deepseek_v4_flash_fp4_b200.py |
|
Results for 🚀 🚀 |
Hi @Fridge003, looks like the newly-added test file breaks pytest suite. Could you please give it a check? update: bugfix #37406 |
Purpose
The torch hc-prenorm path in DeepSeek-V4 combines the sinkhorn weights with the
flattened activations as
which materialises an
[M, HC, H]intermediate and then reduces it.hc_combineaccumulates over
HCin registers in fp32 and writes[M, H]once.Measurements
RTX 6000D, bf16,
HC=4,H=7168:Numerical agreement with the previous expression, max relative error over the
reduction:
Within bf16 rounding. The fused kernel accumulates in fp32; the previous expression
accumulated in the input dtype, so the fused result is the more accurate of the two.
Context
Split out of #29927. That PR is SM120 enablement; this change is arch-independent and
was flagged in review as the kind of unrelated optimization that should be its own PR.
Measured on SM120 only — the datacenter part I had access to is no longer available. The
kernel itself has no arch-specific code, and correctness is pinned by the numerical
comparison above, but I have not measured the speedup on SM90/SM100. Happy to add those
numbers if someone can run the microbenchmark, or to hold the PR until I can.
CI States
Latest PR Test (Base): ❌ Run #33482048236
Latest PR Test (Extra): ❌ Run #33482048167
Latest PR Test (AMD ROCm 7.2): ❌ Run #33482048264