Add sm121 (GB10) tier to the 4-bit GEMM dispatch heuristic - #2030
Add sm121 (GB10) tier to the 4-bit GEMM dispatch heuristic
#2030yashb98 wants to merge 1 commit into
Conversation
| # GB10 (DGX Spark): unified LPDDR5X, far less bandwidth than the sm89 GDDR6X | ||
| # parts this used to fall through to, so dequant+F.linear stays expensive | ||
| # much further up the M range. Calibrated on GB10 at >=1 wave only; below | ||
| # one wave the crossover is strongly K-dependent, so those shapes keep using | ||
| # the shared tiers below (which already branch on tall-K). |
There was a problem hiding this comment.
Some of this comment seems a bit unnecessary. We don't need to really talk about how it /used to/ fall through to sm89 or how it compares.
|
Thanks for the PR. In general it looks good. I would expect the custom kernels to do well considering the lower memory bandwidth on GB10 so the result makes sense in general. I'm wondering if you can share your benchmark figures and exact methodology? The other part I've wondered here, is there's an additional layer of dispatch decisions that I was not able to test on the real hardware. Besides deciding to use one of our fused kernels, there's also decisions on the C++ side on which kernel to use: SIMT, or MMA with different configurations. It's possible we're not selecting the best and leaving some performance on the table since this also hasn't been calibrated for GB10 and there are arch-specific decisions in there. But this doesn't really block merging this if we're confident there's no significant performance regressions being introduced with it. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Thanks for the quick look. Happy to share everything, and I also went ahead and measured the SIMT/MMA layer you mentioned, since I have the hardware. Methodology and figures first, then that data. MethodologyHarness: for each (shape, M, dtype) cell I time the two paths directly, bypassing dispatch entirely, so the measurement is the kernels rather than the heuristic:
The two are timed interleaved in the same loop (custom, fallback, custom, fallback, ...) so clock drift and thermal state hit both sides equally. 8 warmup iterations, 25 timed iterations per side, per cell. Reported as median with min/max. Setup: nf4, blocksize 64, bf16 and fp16 activations, 14 (N, K) shapes taken from real projection dimensions (Llama2-7B, Llama3-8B, 13B-class, Qwen2.5-1.5B), M swept 1 to 1536. bitsandbytes 0.50.0, torch 2.13.0+cu130, driver 580.142, GB10 (48 SMs). Five independent sessions over two days (four full sweeps plus one confirmation run). Numerical agreement between the two paths: max abs diff 0.0625 across all cells, i.e. bf16 rounding, with relative mean diff ~0. FiguresThe region this PR changes (>= 1 wave, M in [48, 256], both dtypes, 120 cells):
Per-shape medians over that region (bf16):
Why the cap stayed at 256: at M=384 the custom kernel still wins 18 of 20 wave-eligible cells (median 1.34x), and at M=512 only 15 of 20 (median 1.07x). The consistent loser is the smallest tier shape, 3072x3072 at M=384: 0.86x, 0.93x, 0.93x, 0.94x, 0.93x across the five sessions. So there is real headroom above 256, but it is shape-dependent, and I would rather a second GB10 confirm that before widening
The SIMT / MMA layerI measured this too, same rig. Built 1. On this arch the dispatcher picks the slower kernel in 32 of 260 cells (12%). 2. The largest losses are at M=4 on large weights, where stock takes MMA but SIMT wins:
(fp16 rows sit within a few percent of bf16 throughout.) The root cause looks like exactly what you suspected: at M=4 the SIMT route needs 3. The reverse direction exists but is smaller: on 2048x2048 at M=6-8 the 4. Everywhere else, 228 of 260 cells (88%), the current choice is the measured winner, including the whole M>=48 region this PR touches. And to be explicit about the regression question: all 32 mispicked cells sit at M<=16, where this PR changes no routing at all, so they behave identically on main with or without this change. Raw JSONs (all 780 cells, min/med/max per cell) and the harness are available if useful; happy to rerun any cell or grid shape under different conditions, and happy to look at what calibrated sm121 rules would look like as a follow-up if there is appetite. |



What this does
Adds an
sm121branch to_gemm_4bit_use_custom_cudaso GB10 (DGX Spark) stops falling through to the sm89 tiers, and updates the two comments that say sm121 is unvalidated.Right now sm121 is explicitly routed to the sm89 fallback with a comment saying "uses sm89 fallback below until validated". This is that validation, for the part of the space I could measure reliably.
Why sm121 needs its own tier
GB10 is unified LPDDR5X, so it has far less bandwidth than the GDDR6X parts sm89 was calibrated on. Dequant plus
F.linearstays expensive much further up the M range, which means the custom kernel keeps winning past the sm89 caps.The change is one branch:
Everything below one wave keeps using the shared tiers, because there the crossover is strongly K-dependent and I did not have clean enough data to justify a separate rule.
Safety
This cannot change behaviour on any other architecture, and that is checkable rather than argued. I enumerated the dispatch decisions of the stock and patched functions over a cross product of 10 architecture profiles (sm75/sm80/sm86/sm89/sm90/sm100/sm120/sm121 plus an unrecognized arch), 3 dtypes, and 23 x 11 x 8 M x N x K shapes:
All 216 changes land on the sm121 profile, at M between 48 and 256. No other arch is touched anywhere in the grid. Since it is exhaustive over the enumerated space rather than sampled, it is a proof of no-regression for those shapes, not evidence of one.
Tests
Run against the released 0.50.0 wheel with only this function transplanted, since the source tree ships no compiled library. The function source is byte-identical between the wheel and main (verified by direct comparison), so the substitution is faithful.
test_functional.py -k 4bittest_linear4bit.py+test_autograd.pyThe one failure is
test_fsdp_state_dict_save_4bit, and it fails on stock too, so it is pre-existing on this machine and not caused by this change.Honest limits
Three things I want to be upfront about, because they bound what this PR claims.
The cap is conservative, not exact. I originally thought M=256 was the crossover. It is not: across five measurement sessions, M=384 still favours the custom kernel on 10 of 14 shapes (cell median about 1.2x) and M=512 on 7 to 8 of 14. Within the at-or-above-one-wave region the only M=384 loser is the smallest tier-A shape, (3072, 3072), at 0.86x to 0.94x across five sessions. So M=256 leaves real headroom, and I chose it deliberately rather than pushing to the measured edge on one shape.
Only the at-or-above-one-wave region is calibrated. I had a wider three-tier version of this patch. I threw it away: enumerating it the same way showed it changing decisions at N=2560 in the tall-K shapes, a region where my own measurements do not support a clean rule (the custom kernel swings from about 3x ahead at low M to 0.75x behind at M=384, across four seeds). Two of its three tiers rested on a single measured shape each. This PR is what survived that.
No real model forward was run. All timings are synthetic stacks of real projection shapes, not an actual HuggingFace model, and the machine had an unrelated job resident throughout, so absolute microseconds are upper bounds. The dispatch decisions above are exact; the performance motivation behind them is measured but not production-validated.
Environment: GB10 (DGX Spark), sm_121, aarch64, driver 580.142, torch 2.13.0+cu130, CUDA 13.0.
Happy to add a regression test for the dispatch table, or to widen the cap if someone with a second GB10 can confirm the M=384 numbers.