[Performance] Add Triton kernel for Gemma3n sparse GELU - #48498
Conversation
|
@BWAAEEEK how does it fair with various concurrencies e.g. 1, 64, 128? Is this always faster even when the number of tokens are large? |
|
This pull request has merge conflicts that must be resolved before it can be |
0b69b7d to
38ce151
Compare
|
Thanks @tjtanaa. I rebased the PR onto the latest The short answer is: the end-to-end path is not always faster on every GPU/concurrency, but the updated implementation no longer shows a statistically significant regression in the tested cases. It is performance-neutral on A100 and improves the higher-concurrency B200 cases. Gemma3n E2B, BF16, CUDA Graph enabled, prefix caching disabled, 14 input / 128 output tokens, 20 warmups + 30 measured runs:
The A100 differences are all statistically neutral. On B200, C1 is neutral while C64 and C128 improve significantly. I also rechecked the shorter 14-input / 16-output C64 case:
Neither short-case difference is statistically significant. The A100 short-run output tokens matched exactly. The reason for the wrapper change was visible in the A100 Nsight comparison. Relative Triton-vs-native deltas changed from:
Both providers still have the same 52,875 total kernels, 48,330 graph kernels, and 45 graph launches, so the compiler-visible wrapper does not add graph partitions or launches. Post-rebase validation:
Long greedy generations were not bitwise deterministic even across repeated runs of the same provider, so I am not treating cross-provider long-output differences as a kernel regression. Numerical correctness/opcheck passed, and the short 16-token outputs matched exactly. |
|
This pull request has merge conflicts that must be resolved before it can be |
Fuse Gemma3n sparse GELU statistics, thresholding, activation, and gating in a dedicated IR provider. Add standalone AOT, CUDA Graph, correctness, and benchmark coverage. Assisted-by: OpenAI Codex Signed-off-by: BWAAEEEK <jooho414@gmail.com>
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: JooHo Lee <jooho414@gmail.com> Signed-off-by: BWAAEEEK <jooho414@gmail.com>
38ce151 to
69b86fe
Compare
|
@BadrBasowid PTAL as well |
BadrBasowid
left a comment
There was a problem hiding this comment.
looks good, I added a few comments
| d=d, | ||
| std_multiplier=std_multiplier, | ||
| BLOCK_SIZE=triton.next_power_of_2(d), | ||
| num_warps=8, |
There was a problem hiding this comment.
Is fixed num_warps=8 optimal across the supported CUDA architectures and all intermediate sizes? The kernel covers d=1 through 32768, so eight warps feels a bit too high, at least to cover all CUDA archs. If this was tuned for one specific GPU, should the optimized implementation be restricted to that hardware until broader results are available?
There was a problem hiding this comment.
I also say this because i am worried about spills on archs that haven't been tested
There was a problem hiding this comment.
Thanks for raising this. I replaced the fixed num_warps=8 launch with a reduction-width heuristic:
block_size = triton.next_power_of_2(d)
num_warps = min(max(block_size // 256, 1), 8)This keeps eight warps for the production Gemma dimensions (d >= 2048) while avoiding over-provisioning for small reductions.
I validated the selected configurations on both A100 (SM80) and B200 (SM100). Relative to fixed eight warps, the small-dimension geometric mean improved by 1.022x on A100 and 1.134x on B200. On A100, the current kernel was faster than compiled native in all 18 tested production shapes (1.143x-4.209x).
At d=32768, A100 reports a 48-byte spill, but eight warps is still the fastest tested configuration. The B200 resource sweep leads to the same selection. Based on the SM80 and SM100 correctness, resource, and performance results, I do not think the provider needs to be restricted to one architecture.
There was a problem hiding this comment.
I see, i think delaying the up load might help resolve the spills in some cases, but i don't want to block the PR for that reason, especially since you are seeing perf gain despite the spills. Perhaps you can just add a TODO to let others, or yourself, know about the spills and to try and take a stab at it at a later time.
There was a problem hiding this comment.
Do note that delaying the up loads might reduce spills, but it also might hurt perf in some cases, i just mentioned it as one of many possible solutions. So if you do attempt it in this PR, make sure you do not introduce perf regression for smaller D sizes. But its also fine if you want to work on it later, or let others attempt to do it! @tjtanaa you can also let us know what you think.
There was a problem hiding this comment.
@BadrBasowid Thanks for the suggestion. I explored several up load placements across the full d=1..32768 range on B200.
Loading up immediately after the mean reduction preserved bitwise correctness and introduced no regressions above 2%, but it did not meaningfully reduce register/spill pressure. Delaying it until after the variance or near the final multiplication substantially reduced the SM80 stack frame for d=32768 (1312B to 592B in offline SM80 compilation), but caused approximately 2–3% regressions in some B200 shapes.
I also tried delaying only the FP32 conversion, but Triton generated effectively the same resource usage and performance as the current implementation.
Since there is no universally better placement and the current kernel remains faster despite the A100 spill, I don’t think adding an architecture- or shape-specific heuristic here would be justified. I’ll keep the current implementation for now, leave the spill optimization as a TODO, and continue exploring it separately as a follow-up.
There was a problem hiding this comment.
Thanks for verifying, yes just add a TODO
| cutoff = (mean + scaled_std).to(input_dtype).to(tl.float32) | ||
| sparse_gate = (gate - cutoff).to(input_dtype).to(tl.float32) | ||
| sparse_gate = tl.where(sparse_gate < 0.0, 0.0, sparse_gate) | ||
|
|
There was a problem hiding this comment.
Are the dtype roundtrips necessary?
There was a problem hiding this comment.
Yes. The roundtrips reproduce the input-dtype rounding performed at native PyTorch operation boundaries; keeping the intermediates in FP32 changes both the values and the sparsity decision.
On A100, removing the roundtrips caused 14/24 FP16 and 21/24 BF16 cases to exceed tolerance, with 281 and 2,700 zero-mask mismatches respectively. The same ablation on B200 produced 25 failing cases and 3,292 zero-mask mismatches. I added a short comment to make this intent explicit in the kernel.
|
|
||
| expected = gelu_and_mul_sparse_native(*clone_args(args)) | ||
| actual = impl.impl_fn(*clone_args(args)) | ||
| assert_close(ir.ops.gelu_and_mul_sparse, actual, expected) |
There was a problem hiding this comment.
can you report tolerances separately for FP32, FP16, and BF16.
There was a problem hiding this comment.
The per-dtype tolerances and observed maximum absolute errors on both A100 and B200 are:
| dtype | atol | rtol | max abs error |
|---|---|---|---|
| FP32 | 1e-5 |
1.3e-6 |
1.43e-6 |
| FP16 | 1e-3 |
1e-3 |
0 |
| BF16 | 1e-3 |
1.6e-2 |
0 |
All tested cases passed their dtype-specific tolerance, and there were no sparse zero-mask mismatches.
| dtype=torch.bfloat16, | ||
| ) | ||
|
|
||
| actual = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn(*clone_args(args)) |
There was a problem hiding this comment.
what about fp16 and fp32?
There was a problem hiding this comment.
Added. The small-size test is now parameterized over FP16, BF16, and FP32 for d = 1, 31, 128, and it also requires exact agreement of the sparse zero mask.
The updated activation suite passes 74/74 tests on both A100 and B200; IR lowering and standalone AOT add another 2/2 passing tests on each GPU.
Select the launch warp count from the reduction width and extend small-size coverage across supported dtypes and sparse masks. Assisted-by: AI coding assistant Signed-off-by: BWAAEEEK <jooho414@gmail.com>
e35fbbb to
4ef92c6
Compare
|
@BadrBasowid Thanks for the review. I have addressed the four inline comments and updated the PR description with the additional A100 and B200 validation results. The fixed eight-warp launch now uses a reduction-width heuristic, the dtype roundtrips are supported by ablation results on both architectures, the per-dtype tolerances are reported separately, and the small-size tests now cover FP16, BF16, and FP32 with exact sparse-mask checks. When you have a chance, could you take another look at the updates? Thanks! |
|
/ci run |
|
✅ Triggered Buildkite CI #87881 for commit |
|
/ci retry |
|
✅ Triggered Buildkite CI #88040 for commit |
|
/ci run |
|
✅ Triggered Buildkite CI #88043 for commit |
|
/ci retry |
|
✅ Queued 1 failed job(s) for retry in Buildkite CI #88043. |
|
/ci run |
|
✅ Triggered Buildkite CI #88303 for commit |
…#48498) Signed-off-by: BWAAEEEK <jooho414@gmail.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
…#48498) Signed-off-by: BWAAEEEK <jooho414@gmail.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
Purpose
This PR adds a dedicated Triton provider for Gemma3n's
gelu_and_mul_sparseactivation. The kernel fuses per-row mean and variance, Gaussian thresholding, tanh GELU, and gated multiplication, avoiding the multi-kernel native/Inductor path.It also:
gelu_and_mul_sparsewith vLLM IR and routesGeluAndMulSparsethrough IR dispatch;tritonbeforenativeon CUDA when the dtype, layout, approximation mode, and intermediate size are supported;torch.library.triton_opandwrap_triton, allowingtorch.compileto trace the launch while preserving standalone AOT dependencies;Related to #32676 and #41903.
Why this is not a duplicate of #41903
#41903 is a broad IR migration for four activation families and provides only the native implementation for
gelu_and_mul_sparse. This PR overlaps with its IR registration for that one op, but is materially performance-focused: it adds a dedicated Triton provider, CUDA priority/configuration, an AOT dependency fix, cross-architecture benchmarks, and model-level validation. I recorded the overlap in #41903 and requested that it retain the other activation migrations while excludinggelu_and_mul_sparse.AI assistance was used and I reviewed every changed line and ran the tests and evaluations reported below.
Test Plan
Tested on:
Commands:
Model validation used Gemma3n E2B with greedy decoding, prefix caching disabled, and CUDA Graph enabled. GSM8K used 1,319 questions, 5-shot prompting,
max_tokens=256, temperature 0, and seed 42.Test Result
Correctness and integration
git diff --check: passedThe standalone AOT regression specifically guards against dropping the upstream projection or Triton write before the downstream projection.
Cross-architecture warp and dtype validation
Following review, the fixed eight-warp launch was replaced with a reduction-width heuristic (
1/2/4/8warps, capped at eight). Production Gemma dimensions (d >= 2048) retain eight warps.d=32768, A100 reports a 48-byte spill, but eight warps remains the fastest tested configuration. No architecture restriction is needed based on the measured SM80 and SM100 results.Correctness was rechecked separately for each supported dtype:
1e-51.3e-61.43e-61e-31e-31e-31.6e-2The input-dtype roundtrips are required to reproduce native PyTorch rounding at operation boundaries. Removing them on A100 caused 14/24 FP16 and 21/24 BF16 cases to exceed tolerance, with 281 and 2,700 sparse-mask mismatches respectively.
BF16 kernel performance
Each of the 42 shapes was checked against the native implementation before timing.
On A100, Triton was also 4.85x faster than eager native by geometric mean.
A100 Gemma3n E2B end-to-end
Workload: 4 prompts, 32 output tokens each, 128 output tokens total, warmup followed by 30 measurements.
-7.53 msto+4.59 ms.p=0.627.The result is performance parity with no statistically significant A100 end-to-end regression. Three of four prompts matched token-for-token in the first comparison. The remaining prompt diverged at a newline but retained the same meaning and final answer; both providers also showed within-provider token variation on repeated runs.
B200 Gemma3n E2B end-to-end and evaluation
172.79 msnative vs172.92 msTriton).150.14 msto147.81 ms(1.58%); all greedy tokens and selected logprobs matched exactly.0.5262native vs0.5307Triton.0.00227native vs0.00152Triton.13.24 snative vs12.75 sTriton.11,322.2 tok/snative vs11,757.0 tok/sTriton.The model evaluation shows no output-quality regression.
Essential Elements of an Effective PR Description Checklist