feat: add blackwell sm120 forward support - #21
Conversation
|
Thank you for your contribution! Using chunk_size=32 to address the shared memory limitation is a truly brilliant design. |
ab0954b to
afdc5be
Compare
SummaryThis PR rebases onto the latest main (previously built on library v0.1.10), resolves merge conflicts, and introduces a shared memory optimization for prepare_h on the SM120 (Blackwell consumer) architecture. SMEM Optimization for prepare_h To reduce shared memory usage, h_shared is reused to replace both m_shared_L and m_shared_R. Motivation
Why This Optimization Does Not Apply to HopperWhen attempting the same approach on H20 with tilelang v0.1.8, using indexed slicing in T.gemm(A, B[0:DK//2]) fails — the wgmma instruction does not support indexed addressing. On SM120 (consumer-grade Blackwell), the Code Changesh_shared[:, 0:DK//2] replaces m_shared_L, and h_shared[:, DK//2:] replaces m_shared_R. Hopper path (unchanged): SM120 path (new): A T.barrier_wait(bar_2, ...) is added because the last producer warp group performs store_h — we must wait for store_h to complete before reusing h_shared. Correspondingly, T.barrier_arrive(bar_2) is emitted after
The same pattern applies when replacing m_shared_R. TestingBenchmarkpython benchmark/bench_gated_delta_rule.py Forward-pass benchmark results on NVIDIA GeForce RTX 5090 are available in benchmark/benchmark_results_5090.txt. FlashQLA achieves up to ~2x speedup over FLA and up to ~2x over FlashInfer across Qwen3.5 family model Unit Testspython -m pytest tests/test_gdr_unit.py -k "fwd and not mixed_cp" -v All 56 forward-only tests pass. Mixed CP tests are skipped because they include backward pass, which is not yet implemented for SM120. Notes
cc @Starmys |
| ): | ||
| g = chunk_local_cumsum(g, chunk_size=64, cu_seqlens=cu_seqlens) | ||
| # todo add chunk_size | ||
| g = chunk_local_cumsum(g, chunk_size=32, cu_seqlens=cu_seqlens) |
There was a problem hiding this comment.
Can we remove the chunk_size here and apply the default chunk_size for different devices?
There was a problem hiding this comment.
I’ve removed the hard‑coded chunk_size values (64 and 32) from the forward passes, and added a helper _get_default_chunk_size() that returns a device‑dependent default:
For SM120 (compute version "12.0") → 32
For all other supported devices (SM90, SM100) → 64
There was a problem hiding this comment.
To protect users from calling an unsupported backward path, we’d suggest dropping the SM120 backward kernels and mark backward pass as not implemented for SM120
There was a problem hiding this comment.
Summary
Drop unsupported SM120 backward kernels and mark backward pass as not implemented
for SM120. This prevents users from silently running incorrect backward
computations on Blackwell SM120 (compute capability 12.0) GPUs.
Changes
Backward disabled for SM120
| File | Change |
|---|---|
chunk/__init__.py |
SM120 branch: removed fused_gdr_bwd / fused_gdr_dh imports, set both to None. Added NotImplementedError guard in chunk_gated_delta_rule_bwd. |
chunk/cp_context.py |
SM120 branch: removed cp_bwd import, fused_gdr_dh = None. Added guard in intra_card_cp_preprocess_bwd. |
blackwell_sm120/__init__.py |
Removed fused_gdr_bwd from imports and __all__. |
blackwell_sm120/fused_bwd.py |
Deleted — unsupported backward kernel. |
blackwell_sm120/cp_bwd.py |
Deleted — unsupported CP backward kernel. |
Supporting changes
tests/conftest.py: Added SM120 arch detection andrequires_sm120marker.pytest.ini: Registered thesm120marker.
Note
test_bwd_works_on_non_sm120 was not run on real SM90 or SM100 hardware due to
environment constraints. Before merging, please confirm that the existing
backward tests in test_gdr_unit.py still pass on both architectures.

SM120 (Blackwell) Forward Pass Support for FlashQLA
It implements forward-pass inference for FlashQLA on SM120 GPUs (RTX 5090 / RTX Pro 6000). issue #2 The primary challenge is that SM120 provides only ~100 KB of usable shared memory, compared to
228 KB on SM90. This PR targets a fixed configuration of chunk_size=32 and block_DV=64, and includes a new kkt_solve kernel specialized for chunk_size=32.
✅ Completed
💬 Discussion
The bottleneck is prepare_h. We observed that m_shared_L and m_shared_R can potentially reuse the memory of **h_shared**: h_shared finishes computation at barrier 1, while m_shared_L/R are only needed after
barrier 2. On SM90 this was infeasible because T.gemm(A, B[0:DK]) with sliced indexing triggered compilation errors. However, on SM120 this pattern is supported. Once this reuse is implemented, shared memory
usage should drop below 100 KB.
q_shared ↔ tmp_shared_2_1 reuse (validated on H20 SM90):
By moving T.copy(q_shared, odot_fragment_2) (line 662 of fused_bwd.py) to before the write of dV' into tmp_shared_2_1, we can let tmp_shared_2_1 take over q_shared's role for both prefetching and
producer-side reads. This eliminates the standalone q_shared allocation. At fp16 + block_S=64, this saves ~16 KB of shared memory (block_S × DK × 2B / 1024).
k_shared / do_shared reuse — difficult:
Unlike q_shared, there is no intermediate fragment (odot_fragment_2) that can recover the values of k and do after their shared memory is overwritten. The conflict arises from the usage of W and U, making
straightforward reuse infeasible.
Remaining memory budget (with block_S=32):
Even with successful q_shared elimination, the total shared memory footprint still exceeds 100 KB:
┌───────────────────────────────────────────┬────────┐
│ Variable │ Size │
├───────────────────────────────────────────┼────────┤
│ h_shared + tmp_shared_4_1 (128×128, fp32) │ 64 KB │
├───────────────────────────────────────────┼────────┤
│ tmp_shared_2_x (3 × block_S × DK × 2B) │ 24 KB │
├───────────────────────────────────────────┼────────┤
│ tmp_shared_1_x (3 × block_S × block_S) │ 6 KB │
├───────────────────────────────────────────┼────────┤
│ q, k, v, do (4 × block_S × DK × 2B) │ 32 KB │
├───────────────────────────────────────────┼────────┤
│ dqkv_shared │ 8 KB │
├───────────────────────────────────────────┼────────┤
│ a_shared │ 2 KB │
├───────────────────────────────────────────┼────────┤
│ Total │ 136 KB │
└───────────────────────────────────────────┴────────┘
Reducing block_S to 16 would bring it under 100 KB, but breaks correctness in kkt_solve (16×16 matrix is too small for the block-inversion algorithm). The remaining options are further precision adjustments
or deeper buffer reuse.