Add Triton BF16xINT4 rowwise GEMM for ROCm gfx942/gfx950 - #420
Conversation
|
@apicciau can you rebase this branch? thanks. |
fc334d5 to
b9b1473
Compare
|
@cthi, I don't think the failures are related to this PR...the job fails before reaching any of the code I've edited. Can we rerun the CI? |
|
I think there is some existing test failure, let me try to resolve it. |
|
@cthi has imported this pull request. If you are a Meta employee, you can view this in D112191149. |
|
Hi @cthi! Do you have any news on this? I have a follow-up PR that sits on top of this change. Let me know how to help |
cthi
left a comment
There was a problem hiding this comment.
We only want to support cuda graphable APIs largely in MSLK. E.g. we should not have any D2H/H2D sync in the API.
b9b1473 to
f681970
Compare
|
Hi @cthi, I think the new failures are ROCm 7.0-specific, because that image ship an older Triton. I'm adding a workaround for that case. |
Implements bf16i4bf16_shuffled, bf16i4bf16_shuffled_grouped, and
bf16i4bf16_shuffled_batched on AMD via Triton, with the following
kernel design and optimisations:
* Row-major INT4 layout: replace CUTLASS preshuffle with activations
pre-split into x_even/x_odd [M, K//2] outside the kernel via
optimised HIP strided copies, avoiding LDS bank conflicts
* Fused x_even/x_odd dot via tl.cat along K: halves LDS staging passes
(ds_write_b128 + s_barrier) for the A operand — single wider dot is
mathematically equivalent to two separate dots
* Split-K without atomics: partial sums written to float32 workspace
[SPLIT_K, M, N] via direct stores; separate _bf16i4_splitk_reduce
kernel sums slices before casting to bf16 — CUDA-graph safe
* XCD remapping: spreads output tiles evenly across 8 chiplets before
L2-friendly M-grouping, reducing cross-chiplet traffic on MI300X/MI350X
* EVEN_MN / EVEN_K heuristics: skip boundary masking on the common case
where M, N are tile-aligned and K2 is cleanly divisible
* GROUP_SIZE_M: autotuned L2 tile grouping parameter
* Pruned config sweep: early_config_prune removes infeasible configs
(group_size alignment, tile-vs-problem-size, warp/stage heuristics),
reducing autotuning time ~3x vs exhaustive sweep
* Native fused grouped kernel (_bf16i4_grouped_kernel): single launch
for all G groups, eliminates Python-loop overhead for small M-per-group
(< 256); routes to Python loop for large M where bandwidth dominates
* bf16i4bf16_* op schemas moved outside #ifdef to remove duplication
between ROCm and CUDA builds in gemm_ops.cpp
* TritonBF16Int4GroupedShuffled inherits CutlassFP8Int4Rowwise to reuse
_int4_row_quantize and _pack_int4 helpers
Performance on MI350X (gfx950), warmup=25 rep=200:
bf16i4bf16_shuffled:
M=128, N=4096, K=4096: 90 → 111 TFLOPS (+23%, 4.0% peak)
M=128, N=4096, K=11008: 122 → 153 TFLOPS (+25%, 5.5% peak)
M=2048, N=4096, K=11008: 530 → 566 TFLOPS (+7%, 20.5% peak)
M=4096, N=4096, K=11008: 593 → 632 TFLOPS (+7%, 22.9% peak)
bf16i4bf16_shuffled_grouped (new op, fused for M<256, loop for M≥256):
G=4, M=128, K=4096: 115 TFLOPS (4.2% peak)
G=4, M=2048, K=11008: 544 TFLOPS (19.7% peak)
All 9 BF16xINT4 accuracy tests pass (rowwise, batched, grouped).
f681970 to
3306e0c
Compare
cthi
left a comment
There was a problem hiding this comment.
Thanks, left few more comments.
* Class overrides every method from CutlassFP8Int4Rowwise, so inheriting from GemmOpBase directly is correct and honest * No behaviour change — only inherited defaults (benchmark, name, supported, output_bytes_per_element) come from GemmOpBase anyway
* M_sizes must be int64 and on GPU, not silently converted * w_scale_group and w_zero_group must be float32 and contiguous * Surfaces caller bugs at the call site instead of hiding them
* Remove method-level import of matmul_bf16i4_rowwise_grouped * Call torch.ops.mslk.bf16i4bf16_shuffled_grouped to test the full registration chain
* Derive num_warps from tile area instead of sweeping (<=2048 elems -> 4, >=16384 -> 8, else both) * Fix GROUP_SIZE_M=8 and num_stages=2 — noise dimensions with no inner-loop effect * Rowwise configs: 1536 -> 276, grouped configs: 144 -> 54 * Simplify _prune_configs and _prune_grouped_configs: remove rules now guaranteed by construction
|
Hi @cthi, let me know if there is any issue with this code |
|
@apicciau could you rebase onto latest main to resolve the merge conflict? Thanks |
Summary
Adds Triton implementations of
bf16i4bf16_shuffled,bf16i4bf16_shuffled_grouped,and
bf16i4bf16_shuffled_batchedfor AMD MI300X/MI350X. These ops exist on CUDA viaCUTLASS with a preshuffle weight layout; on ROCm the preshuffle layout is not
available, so this PR implements them using a row-major INT4 layout. The activation
matrix is split into even and odd K columns in Python (via optimised HIP strided
copies) before kernel launch — doing this split inside the kernel would require
stride-2 global memory loads, which halve effective memory bandwidth as adjacent
threads in a wavefront load non-adjacent addresses.
Context
bf16i4bf16_rowwise(already merged)Implementation
Fused A-operand dot. The straightforward port issues two
tl.dotcalls perK-iteration — one against the lo-nibble weights with even-K activations, one against
the hi-nibble weights with odd-K activations. On gfx950 each dot requires a
ds_write_b128 → s_barrier → ds_read_b128LDS round-trip to convert the A operandfrom
#blockedto#dot_opregister layout. Concatenating both activation tilesand both weight tiles along the K dimension via
tl.catbefore the dot halves thenumber of LDS staging passes. This is mathematically equivalent:
[x_even | x_odd] @ [w_lo | w_hi]ᵀ = x_even @ w_lo.T + x_odd @ w_hi.T.Split-K without atomics. When
SPLIT_K > 1, partial sums are written into afloat32 workspace tensor
[SPLIT_K, M, N]using direct stores — notl.atomic_add. A separate_bf16i4_splitk_reducekernel sums the slices andcasts to bf16. This avoids atomic contention and is CUDA-graph safe.
The prune function restricts
SPLIT_K > 1toM < 512, so it has zero cost forlarge-batch (prefill) shapes. For decode shapes the autotuner selects freely:
benchmarks on MI350X show the autotuner consistently picks
SK=8atM ≤ 128,SK=4atM=128for large NK products, andSK=1atM ≥ 2048— matching theoccupancy argument (at
M=1,N=8192,BLOCK_M=32,BLOCK_N=32a non-split gridhas only 256 tiles on a 304-CU GPU;
SK=8raises this to 2048 tiles).XCD remapping. Output tiles are redistributed across the 8 XCDs of MI300X/MI350X
before the L2-friendly M-grouping step, avoiding hot-spot cross-chiplet traffic when
the tile count is not a multiple of 8.
Pruned autotuner config sweep. The config space is structured to avoid redundant
combinations:
num_warpsis derived from tile area (≤2048 elems → 4 warps,≥16384 → 8 warps, otherwise both),GROUP_SIZE_Mis fixed at 8 (grid schedulinghint, no inner-loop effect), and
num_stagesis fixed at 2 (the kernel doesexplicit prefetching). This reduces the total config count from 1536 to 276 with no
loss of autotuning quality, cutting per-shape autotuning time by ~5×.
Native fused grouped kernel. The grouped op dispatches all G groups in a single
kernel launch (
_bf16i4_grouped_kernel) using a 3D grid(G, ceil(M_total/BLOCK_M), ceil(N/BLOCK_N)). The grid is over-provisioned usingM_total(a host int from tensor metadata) — tiles that exceed their group's rowcount early-exit in the kernel.
M_startsis computed on-device viaM_sizes.cumsum(0) - M_sizes, so there are no device-to-host synchronisations andthe wrapper captures cleanly inside a CUDA graph.
Schema deduplication. The
bf16i4bf16_*op schemas andpreshuffle_i4wereduplicated inside both
#ifdef USE_ROCMand#elseingemm_ops.cpp. They arenow registered once, above the
#ifdef, since the schemas are shared between CUDAand ROCm builds.
TritonBF16Int4GroupedShuffledingemm_ops.pyinherits fromGemmOpBasedirectly — the previousCutlassFP8Int4Rowwisebase was a copy-pasteartefact; the class overrides every method so no code was shared.
Testing
All 9 BF16×INT4 accuracy tests pass:
Tests cover
test_rowwise_accuracy,test_rowwise_batched_accuracy, andtest_torch_op_dispatch, three shapes each. The grouped op is tested via the Tritonkernel directly (the
bf16i4bf16_shuffled_groupedtorch op schema requires the C++build; the kernel itself is validated by the Python-level tests in
BF16Int4TritonROCmGroupedTests).Performance
Hardware: AMD Instinct MI350X (gfx950), ROCm 7.1.1, Triton 3.5.1+rocm7.1.1.
triton.testing.do_bench_cudagraph, rep=200, return_mode=mean.Peak: 2300 TFLOPS BF16 matrix, 8000 GB/s HBM.
Ridge point: 287.5 FLOP/byte — shapes below are memory-bandwidth bound (report
GB/s, % HBM), shapes above are compute bound (report TFLOPS, % BF16 peak).
bf16i4bf16_shuffled — decode shapes (memory-bandwidth bound)
Model-attributed shapes from the MSLK registered shape sets. "Before" is the
bf16i4bf16_rowwisekernel on main (no tl.cat fusion, no XCD remapping,no split-K).
The autotuner selects SK>1 only where it helps occupancy (M≤128) and SK=1
for prefill shapes where the overhead would exceed the benefit.
bf16i4bf16_shuffled — prefill shapes (compute bound)
bf16i4bf16_shuffled_grouped — new op on ROCm
Single fused launch for all G groups; CUDA-graph safe. M/g = rows per group.
bf16i4bf16_shuffled_batched — new op on ROCm