Skip to content

feat(hipblaslt): add rocBLAS wvSpltK BF16 TN skinny GEMMs (n ≤ 4) via GFA - #12329

Merged
jinchen62 merged 33 commits into
ROCm:developfrom
jinchen62:gemv_kernel
Oct 8, 2026
Merged

jinchen62 merged 33 commits into
ROCm:developfrom
jinchen62:gemv_kernel

Conversation

@jinchen62

@jinchen62 jinchen62 commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

JIRA ID: AIHPBLAS-4726

Reopen #12250 because GemmFromAnywhere was merged to develop and the branch was deleted.

Summary

hipBLASLt loses on decode GEMMs with very few tokens. This ports the rocBLAS-internal wvSpltK wave-split-K kernels into hipBLASLt as GFA CustomKernels for BF16 TN with n ≤ 4 tokens on gfx950. It also routes those problems to them through library logic, so hipBLASLt picks them out of the box.

TN with a skinny n is how PyTorch-based inference calls hipBLASLt. A linear layer computes y[T, H] = x[T, K] · W[H, K]ᵀ in row-major terms. Column-major hipBLASLt computes the transpose, yᵀ = W · xᵀ, that is (H, K) · (K, T) = (H, T): TN with m = H (output features), n = T (tokens) and k = K. torch.mm(x, W.t()) on a GLM-5.2 decode shape logs:

hipblaslt-bench --api_method c -m 2624 -n 1 -k 6144 --lda 6144 --ldb 6144 --ldc 2624 --ldd 2624 ... --transA T --transB N --a_type bf16_r --b_type bf16_r --c_type bf16_r --d_type bf16_r ... --compute_type f32_r

F.linear on a sliced activation (qkv[:, :6144] of a 6200-wide tensor) reaches it with --ldb 6200, so the kernels take every leading dimension as a kernarg.

Earlier revisions also carried FP16 NN kernels with m = tokens. Those followed hipblaslt-bench runs that fed the GEMM list's row-major M straight into hipBLASLt's m, a call no framework makes, so they are dropped.

Kernels

wvSpltK_bf16_tn_m1, wvSpltK_bf16_tn_m2 and wvSpltK_bf16_tn_m4, ported from rocBLAS's TRANSA=false instantiation. The suffix is rocBLAS's M, the token count, which is hipBLASLt's n here.

  • 64-lane K-split, 16 waves/WG and persistent WGs (grid = CU count). Tokens are staged in 128 KB of LDS, and any past that are read from global memory, so the kernels handle any K.
  • v_dot2c_f32_bf16, no MFMA. gfx950 only: gfx942 has no BF16 dot instruction.
  • Only the .s files ship. They are emitted at code object v4, the version hipBLASLt links its library code objects at.
Kernel n YTILE UNRL LDS VGPRs
wvSpltK_bf16_tn_m1 1 2 2 128 KB 44
wvSpltK_bf16_tn_m2 2 2 2 128 KB 58
wvSpltK_bf16_tn_m4 0 < n ≤ 4 1 4 128 KB 112
  • CuCount is a kernarg, not the hardcoded 80 (MI308). It and the launch grid are one value: the device's CU count, capped like Tensile's persistent grids by the caller's SM-count target (HIPBLASLT_MATMUL_DESC_SM_COUNT_TARGET) and TENSILE_PERSISTENT_MAX_CUS. So the persistent stride can never disagree with the grid.
  • C and D are separate pointers. rocBLAS called these kernels in place and read the beta term through the output pointer. hipBLASLt also dispatches out-of-place, where that would read uninitialized D.
  • The m4 tile (YTILE=1, UNRL=4) is tuned for gfx950's 256 CUs. On the GLM-5.2 decode shapes a wider tile leaves too few waves to hide memory latency; with weights streamed from HBM, this tile is 1.2× faster than YTILE=3, UNRL=2.
  • The kernels take 72 bytes of kernargs. They order everything the staging and K loop read first and preload those 14 dwords into SGPRs, so only the epilogue's arguments are fetched with s_load.

Predicates

Enforced in custom.config:

  • batch == 1. There are no batch strides in the kernarg list, so a strided-batched problem would leave every batch past the first unwritten.
  • m > 8 (below one wave tile the tail fixup underflows), K % 8 == 0, and unit strides on all four tensors.
  • m1 / m2: n == 1 / n == 2. m4: 0 < n ≤ 4. hipBLASLt quick-returns m == 0 but not n == 0, because grouped GEMM relies on it.

Library logic

gfx950/gfx950/Range/gfx950_Cijk_Alik_Bljk_BBS_BH_UserArgs.yaml routes n = 1, 2 and 3–4 to m1, m2 and m4, for m ≥ 9, 8 ≤ K ≤ 4096 and batch 1.

  • The ranges stop at K = 4096, short of the kernels' own bound. Past that, hipBLASLt's own kernels match or beat wvSpltK on MI355X (see Perf).
  • Tuning tools still see the kernels at any K: hipBLASLt's all-solutions list (getAllAlgos, which aiter's hipb_findallsols enumerates) includes them past the cap.
  • The file carries the plain-GEMM problem type. A custom kernel takes its logic file's type, and these kernels support no bias, activation or scale vector, so those problems keep their current kernels.
  • It reuses the header of the shipped plain-GEMM logic (gfx950/gfx950/Equality/gfx950_Cijk_Alik_Bljk_BBS_BH_UserArgs.yaml), so both share a placeholder library and tuned Equality sizes keep priority.
  • A matched range whose kernel fails its own predicates (for example K % 8 ≠ 0) falls through to the next library.
  • General-batched (pointer-array) problems never use these kernels: hipBLASLt skips custom kernels for that batch mode in every selection path (see below).
  • The file uses the MI350 (0x75a0) header. MI355X searches its own tuned logic (gfx950_id75a3) first, so its tuned sizes are unaffected. On MI350, the ranges come before 19 skinny sizes tuned in its Bias library (see Perf).
  • The prediction-library and dynamic stream-K modes skip Equality and Range rows, so those modes don't use these kernels.

Tensile and host-side changes

  • CustomGridSize::ComputeUnits and a ComputeUnits arg semantic, so a persistent kernel can take the CU count as both grid and kernarg. The count honors the same CU budget as Tensile's persistent grids.
  • Wired the classic Tensile AssertSizeEqual, AssertSizeGreaterThan, AssertSizeLessThan and AssertStride{A,B,C,D}Equal keys through extractDimPredicate onto the existing SizeEqual, SizeGreaterThan, SizeLessThan and Stride*Equal runtime predicates. The C++ side and its serialization already existed; only the Python mapping was missing. One registry drives validation and predicate emission, with a test that fails if a key is validated but not wired.
  • Custom kernels in pointer-array mode: getAllSolutions already skipped handwritten custom kernels for general-batched problems, which they cannot run. The heuristic (getSolutions, behind getBestSolutions / getBestRawSolutions) and isSolutionSupported now do too. The heuristic widens its search by the number it drops, so callers still get their requested count. Before this, a pointer-array GEMM with batch_count = 1 in a skinny range hit an illegal memory access.

Which frameworks reach these kernels

  • PyTorch: BF16 torch.mm(x, W.t()) and F.linear without bias, as above.
  • SGLang on ROCm (SGLANG_USE_AITER=1): linear layers go through aiter's tgemm.mm, which reaches hipBLASLt only for shapes missing from aiter's tuned configs (its torch fallback) or through its hipb_mm backend.
    • In the current SGLang ROCm image (v0.5.20-rocm724-mi35x), aiter has tuned flydsl/triton kernels for 6 of the 7 GLM-5.2 decode shapes at ≤ 4 tokens. On those, its kernels are about 10% faster than wvSpltK with weights streamed from HBM.
    • The seventh, 6144×16384, falls back to hipBLASLt. It is past the K cap and keeps hipBLASLt's existing kernel, so GLM-5.2 decode under SGLang+aiter is unchanged.
  • vLLM on ROCm: decode with ≤ 5 tokens runs vLLM's own wvSplitK (VLLM_ROCM_USE_SKINNY_GEMM, on by default), so it doesn't reach hipBLASLt.

Test plan

All on MI355X gfx950 with the shipped kernels:

  • Tensile full-element validation of custom_rocblas_gemv_bf16_tn_m{1,2,4}.yaml (8, 8 and 11 sizes) at β=0, and at α=2/β=2 with random out-of-place C: all PASSED. The sizes include padded leading dimensions and K past the LDS stage (73728, 36864 and 24576).
  • hipBLASLt out of the box through hipblaslt-bench: 21 cases pick the expected kernel and pass CPU verification.
    • Picks wvSpltK: n = 1–4 up to the K = 4096 range edge, m up to 32768, sliced activations (ldb > K) and β = 0.5.
    • Falls back: K past the cap (4104, 6144 and 16384), n = 5, bias, GELU, batch 2, K % 8 ≠ 0, m ≤ 8, FP16 NN, and general-batched (pointer-array) problems with batch_count = 1.
  • hipblaslt-test: new pre_checkin case matmul_skinny_bf16_tn, 16 tests, all PASSED. They cover n = 1–4, strided and general-batched, padded leading dimensions, and β 0 and 2.
  • CU budget: with --sm_count_target 64 / 128 and TENSILE_PERSISTENT_MAX_CUS=48, wvSpltK launches on 64 / 128 / 48 workgroups (checked with rocprofv3) and passes CPU verification. CustomKernel_test.cpp covers the grid and kernarg.
  • Direct HIP vs CPU FP32 reference, on an earlier build of the same kernels: 156/156, plus 79/79 with α ≠ 1. These cover padded and misaligned leading dimensions, β ≠ 0 and the global-memory fallback. D is poisoned, so a stray store fails loudly.
  • Unit suites: custom-kernel, library-logic corpus and ValidParameters (1220 passed); StaggerU census (417 passed). The logic-file test skips in TheRock's installed-artifact layout, where the logic tree is absent.

Perf (MI355X gfx950, hipBLASLt out of the box)

Setup:

  • Compares hipBLASLt's pick with vs. without the new Range rows (hipblaslt-bench --requested_solution 2 -i 100 -j 30 --use_gpu_timer), both timed back to back in one process.
  • Numbers are wvSpltK's speedup over hipBLASLt's previous pick, as weights hot / weights streamed from HBM (--rotating 512). Shapes outside a range run hipBLASLt's own kernel and count as 1.00×.

GLM-5.2 decode shapes (N × K: 2624×6144, 2048×2048, 6144×2048, 4096×2048, 6144×4096, 16384×2048, 6144×16384), geomean over the seven. 2624×6144 and 6144×16384 are past the cap.

tokens 1 2 3 4
BF16 TN 1.24× / 1.16× 1.17× / 1.11× 1.01× / 1.04× 1.01× / 1.03×

In range, n = 3–4 still lose on 6144×2048, 6144×4096 and 16384×2048 with weights hot (0.86–0.99×), and on 16384×2048 streamed (0.89–0.90×).

N × K sweep (N 1024–32768, K 1024–16384; 30 shapes per row, 29 for n = 4 because 1024×1024 is tuned):

n shapes in range in range whole sweep worst in range
1 18/30 1.33× / 1.25× 1.19× / 1.14× 1.03× / 0.99×
2 18/30 1.21× / 1.16× 1.12× / 1.09× 0.97× / 0.95×
3 18/30 1.06× / 1.10× 1.04× / 1.06× 0.62× / 0.73×
4 17/29 1.03× / 1.08× 1.02× / 1.04× 0.61× / 0.73×
  • Why the cap: measured without it, wvSpltK at K ≥ 8192 was 1.03× / 1.00× at n = 1 and 0.89–0.94× at n ≥ 2. The GLM-5.2 shape 6144×16384 fell to 0.65× / 0.74× at n = 4.
  • Inside the ranges, tn_m4 loses at very large N with short K: 0.61–0.62× hot and 0.73× streamed at 32768×1024.

MI350-tuned sizes:

  • On MI350, the ranges take priority over 19 BF16 TN sizes tuned in the MI350 Bias library.
  • Timed on MI355X as a stand-in, against the kernel MI355X's own tuning picks for each size, wvSpltK is 1.19× / 1.23× geomean.
  • 2 (hot) and 3 (streamed) of the 19 are more than 5% slower. The worst are 0.83× hot (n = 4, 2880×4096) and 0.74× streamed (n = 1, 18432×3072).

@therock-pr-bot

therock-pr-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

@jinchen62

Copy link
Copy Markdown
Contributor Author

@nakajee Switched to AssertSizeEqual: [{0: 1}] as suggested.

Keep AssertSizeEqual off defaultSolution so ordinary kernels do not pick up a new name token, and cover every map-validator reject branch so ValidParameters.py stays at the 100% floor.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.30159% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rary/src/amd_detail/rocblaslt/src/tensile_host.cpp 70.37% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #12329      +/-   ##
===========================================
+ Coverage    71.93%   72.13%   +0.20%     
===========================================
  Files         2863     2863              
  Lines       472708   472459     -249     
  Branches     69829    69885      +56     
===========================================
+ Hits        340007   340782     +775     
+ Misses      109118   107910    -1208     
- Partials     23583    23767     +184     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 49.33% <ø> (ø)
TensileLite-Unit 83.29% <100.00%> (+0.01%) ⬆️
hipBLAS 90.62% <ø> (ø) Carriedforward from 9e24e6d
hipBLASLt 38.96% <70.37%> (+0.04%) ⬆️
hipCUB 82.68% <ø> (ø) Carriedforward from 9e24e6d
hipDNN 86.64% <ø> (+0.01%) ⬆️ Carriedforward from 9e24e6d
hipFFT 54.32% <ø> (+10.89%) ⬆️ Carriedforward from 9e24e6d
hipRAND 76.12% <ø> (ø) Carriedforward from 9e24e6d
hipSOLVER 68.96% <ø> (ø) Carriedforward from 9e24e6d
hipSPARSE 86.99% <ø> (ø) Carriedforward from 9e24e6d
rocBLAS 48.25% <ø> (ø) Carriedforward from 9e24e6d
rocFFT 48.78% <ø> (+1.16%) ⬆️ Carriedforward from 9e24e6d
rocRAND 57.45% <ø> (ø) Carriedforward from 9e24e6d
rocSOLVER 76.83% <ø> (ø) Carriedforward from 9e24e6d
rocSPARSE 74.75% <ø> (+0.12%) ⬆️ Carriedforward from 9e24e6d
rocThrust 93.15% <ø> (ø) Carriedforward from 9e24e6d

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...s/hipblaslt/tensilelite/Tensile/AddCustomConfig.py 97.13% <100.00%> (+0.01%) ⬆️
...aslt/tensilelite/Tensile/Common/ValidParameters.py 100.00% <100.00%> (ø)
...ects/hipblaslt/tensilelite/Tensile/Contractions.py 88.03% <100.00%> (+0.29%) ⬆️
...cts/hipblaslt/tensilelite/Tensile/CustomKernels.py 97.42% <ø> (ø)
...rary/src/amd_detail/rocblaslt/src/tensile_host.cpp 43.65% <70.37%> (+0.17%) ⬆️

... and 81 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jinchen62 jinchen62 changed the title feat(hipblaslt): add rocBLAS wvSpltK M=1 decode GEMV via GFA feat(hipblaslt): add rocBLAS wvSpltK skinny GEMMs (M=1/2/4) via GFA Sep 21, 2026
jinchen62 and others added 8 commits September 22, 2026 19:15
wvSpltK_hf_m4 now reads M as a kernarg and is predicated on M <= 4 instead
of M == 4, so M=3 runs on it too. Tile rows past M read the last real row of
A and are never stored. With a runtime M, lda == ldc == ldd == M is no longer
a constant a stride predicate can pin, so the kernel takes all four leading
dimensions as kernargs and only unit strides stay predicated.

The staging walks A in memory order with a compile-time M per case, the
epilogue stays unrolled, and the kernargs are ordered so the 14 dwords the
staging and K loop read are preloaded into SGPRs. On MI355X, over the PR's 20
M=4 decode shapes plus 6 GLM-5.2 shapes, this is 1.03x the previous kernel
timed back to back (worst 0.96x, at N=4096, K=2048).

Co-authored-by: Cursor <cursoragent@cursor.com>
wvSpltK_bf16_tn_m1/m2/m4 cover hipBLASLt TN with a skinny n, the layout
torch.mm(x, w.t()) and F.linear reach hipBLASLt with (m = output features,
n = tokens), ported from the rocBLAS TRANSA=false instantiation. All three
build from wvSpltK_bf16_tn.cpp and use v_dot2c_f32_bf16, so they are
gfx950-only.

Every leading dimension is a kernarg, since an activation sliced out of a
fused projection reaches hipBLASLt with ldb > K. m4 also takes n as a kernarg
and is predicated on n <= 4; m1 and m2 pin n. Tokens are staged in 128 KB of
LDS and any past that are read from global memory, so K needs no bound. m4
uses YTILE=1 / UNRL=4, tuned for gfx950's 256 CUs.

On MI355X, GLM-5.2 decode shapes, against hipBLASLt BF16 TN heuristic top-1
with both timed back to back and weights streamed from HBM (512 MB rotation):
n=1 1.29x (7/7 faster), n=2 1.19x (7/7), n=3 1.07x (6/7), n=4 1.05x (6/7).
With a hot cache: 1.42x, 1.22x, 0.97x, 0.96x.

Co-authored-by: Cursor <cursoragent@cursor.com>
jinchen62 and others added 3 commits September 30, 2026 14:58
TheRock runs Tensile/Tests/unit from the installed TensileLite test
artifacts, which do not include library/src/.../Logic, so
test_wvspltk_range_logic failed with FileNotFoundError in the gfx94X
tensilelite job. Skip it when the logic file is absent, as the StaggerU
census does for its logic-backed tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jinchen62
jinchen62 requested a review from babakpst October 5, 2026 14:12
jinchen62 and others added 7 commits October 5, 2026 09:20
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	projects/hipblaslt/tensilelite/Tensile/AddCustomConfig.py
#	projects/hipblaslt/tensilelite/Tensile/CustomKernels.py
#	projects/hipblaslt/tensilelite/include/Tensile/ContractionSolution.hpp
wvSpltK_hf_m1 and wvSpltK_hf_m2 indexed B with K, so they assumed
ldb == K, which no predicate can express. With library logic routing
every FP16 NN problem with m = 1 or 2 to them, a padded or sliced B
silently produced wrong results (norm_error 1.3 and 1.7 at K = 4096,
ldb = 4160). Both now take ldb as a trailing kernarg; m4 already did.
Their kernargs grow from 52 to 56 bytes, and GLM-5.2 decode timings are
unchanged within noise.

Rename the three kernels to wvSpltK_f16_nn_m{1,2,4}, and their Tensile
configs to custom_rocblas_gemv_f16_nn_m{1,2,4}.yaml, to match
wvSpltK_bf16_tn_m{1,2,4}. m4's instruction stream is unchanged. The m1
and m2 configs gain a padded-ldb size, and m1 a K past the LDS stage;
all three pass full-element validation on MI355X at beta = 0 and at
alpha = 2, beta = 2 with random C. The FP16 NN Range logic is
regenerated from those runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
getAllSolutions already skipped handwritten custom kernels for
general-batched (pointer-array) problems, which they cannot run: they
take plain A/B/C/D addresses. The heuristic path did not, and with
library logic now ranking the wvSpltK kernels first, a pointer-array
GEMM with batch_count 1 in their range launched one on the pointer
arrays: an illegal memory access, then a hang.

Share the check, apply it in getSolutions (behind getBestSolutions and
getBestRawSolutions) and in isSolutionSupported. getSolutions widens
the search by the number it drops, so callers still get up to the
requested count of solutions that can run the problem.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add pre_checkin hipblaslt-test cases for FP16 NN with m = 1..4 and BF16
TN with n = 1..4, at beta 0 and 2, both strided and general-batched with
batch_count 1. They cover padded leading dimensions the kernels must
honor, and the pointer-array problems they must fall back from. On
gfx942/gfx950 these route to the wvSpltK custom kernels; elsewhere they
are ordinary GEMMs.

Co-authored-by: Cursor <cursoragent@cursor.com>
After the Oct 5 develop merge, hipBLASLt's own kernels match or beat
wvSpltK past K = 4096 for every group but FP16 NN m = 1. Over an N x K
sweep on MI355X (N 1024-32768, K 1024-16384), wvSpltK at K >= 8192 was
0.88x-1.05x geomean for FP16 NN m >= 2 and BF16 TN, worst 0.55x (FP16 NN
m = 2, 1024 x 16384). FP16 NN m = 1 stays 1.31x-1.39x there.

So every Range row but FP16 NN m = 1 now stops at K = 4096, on gfx950
and, from the same data, gfx942. Inside the ranges the sweep is
1.04x-1.47x geomean, and GLM-5.2 decode shapes are 1.02x-1.38x.

Co-authored-by: Cursor <cursoragent@cursor.com>
PyTorch-based inference (torch.mm(x, w.t()), F.linear, aiter's hipb_mm)
reaches hipBLASLt as TN with m = output features and n = tokens, which the
BF16 TN kernels serve. Nothing in that stack issues NN with m = tokens, the
layout the FP16 NN kernels were written for, so remove them along with their
gfx942 and gfx950 Range logic, Tensile tests and gtests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jinchen62 jinchen62 changed the title feat(hipblaslt): add rocBLAS wvSpltK skinny GEMMs (M=1/2/4) via GFA feat(hipblaslt): add rocBLAS wvSpltK BF16 TN skinny GEMMs (n ≤ 4) via GFA Oct 5, 2026
jinchen62 and others added 2 commits October 5, 2026 13:40
Tensile's persistent kernels size their grid within the caller's SM-count
target (HIPBLASLT_MATMUL_DESC_SM_COUNT_TARGET) and TENSILE_PERSISTENT_MAX_CUS,
and so do the Stream-K kernels the wvSpltK ranges displace. The ComputeUnits
grid and kernarg always used the device's full CU count, so a caller that
limits CUs, for example to leave room for a communication kernel, lost that
limit on every skinny problem routed to wvSpltK.

Compute one count, the device's capped by both, and use it for the grid and
the kernarg, which a persistent kernel strides by. On MI355X, hipblaslt-bench
with --sm_count_target 64 / 128 and TENSILE_PERSISTENT_MAX_CUS=48 launches
wvSpltK on 64 / 128 / 48 workgroups, and CPU verification passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
The code-object-version requirement for kernels that library logic
references applies to every external kernel, so state it under "Adding a
new external kernel" and restore the Triton section's own advice. Also drop
extractDimPredicate's unused key argument.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jinchen62

Copy link
Copy Markdown
Contributor Author

m vs. n: which dimension is the token count

The skinny shapes in the GLM-5.2 decode GEMM list we were given are written in PyTorch's row-major naming, and the first revision of this PR used them with the wrong mapping. The two naming conventions map like this:

GEMM list / PyTorch (row-major) hipBLASLt (column-major)
tokens M n
output features (H) N m
input features K k

The list writes each GEMM as C[M, N] = A[M, K] @ B[K, N] and times it as torch.mm(A, B.t()), where B is the [N, K] weight. hipBLASLt is column-major, so it computes the transpose: Cᵀ = W · Aᵀ, i.e. (H, K) · (K, T) = (H, T). That is a TN GEMM with m = H, n = tokens and k = K. The (H, T) column-major output has the same memory layout as the (T, H) row-major output PyTorch expects. hipBLASLt's call log (HIPBLASLT_LOG_MASK=32) for torch.mm on the 2624×6144 shape with one token shows exactly that:

hipblaslt-bench --api_method c -m 2624 -n 1 -k 6144 --lda 6144 --ldb 6144 --ldc 2624 --ldd 2624 ... --transA T --transB N --a_type bf16_r --b_type bf16_r --c_type bf16_r --d_type bf16_r ... --compute_type f32_r

So a true decode is n = 1 in hipBLASLt terms. F.linear on a sliced activation (qkv[:, :6144] of a 6200-wide tensor) arrives the same way, with --ldb 6200.

Where "M = 1, 2, 4" came from. The hipblaslt-bench runs that came with the list fed its M straight into hipBLASLt's m, in an NN problem; their kernels are Cijk_Ailk_Bljk_…_MT1x4x256, an NN tile one row tall. With one token the two layouts happen to touch the same memory. With two or more tokens, NN with m = tokens expects transposed activations and output. Either way, hipBLASLt treats NN and TN as different problem types, and torch.mm never issues NN with m = tokens. So those benchmark numbers, and any kernels tuned from them, don't apply to the PyTorch call.

What changed in the PR. The original FP16 NN kernels (m = tokens) followed that NN mapping, and the list is BF16 anyway, so they are dropped. The PR now ships BF16 TN kernels for n ≤ 4 on gfx950, in the TN BF16 logic. That is the exact problem torch.mm(x, W.t()), F.linear and aiter's hipb_mm produce, and it's verified end to end through hipBLASLt on that call, including sliced activations. The kernel names keep rocBLAS's suffix, wvSpltK_bf16_tn_m1/m2/m4, where rocBLAS's M is the token count, i.e. hipBLASLt's n. The description's "Which frameworks reach these kernels" section covers when SGLang (with aiter) and vLLM actually send these GEMMs to hipBLASLt.

jinchen62 and others added 2 commits October 7, 2026 12:52
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	projects/hipblaslt/tensilelite/include/Tensile/ContractionSolution.hpp
#	projects/hipblaslt/tensilelite/src/ContractionSolution.cpp
@talumbau
talumbau requested a review from msujon-AMD October 7, 2026 17:37
@jinchen62
jinchen62 merged commit 0f6c343 into ROCm:develop Oct 8, 2026
243 of 251 checks passed
@jinchen62
jinchen62 deleted the gemv_kernel branch October 8, 2026 18:54
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

archana-ramalingam added a commit that referenced this pull request Oct 12, 2026
JIRA ID : NA

## Motivation
- The gfx1250-strict HW job (#12983) found 12 tests on develop that
can't run on gfx1250-strict: 4 TensileLite configs and 8 hipBLASLt MX
rows. None of them ran on gfx1250-strict before, because no per-PR CI
covers it.
- Stacked on #12983 so the HW job runs on this PR; retarget to `develop`
once #12983 merges.

## Technical Details
- TensileLite: add `skip-gfx1250-strict` to configs that already skip
`gfx1250`. A base `skip-gfx1250` mark doesn't fire on gfx1250-strict by
design (`test_config_helpers.py`), so these ran there:
- `custom_rocblas_gemv_bf16_tn_m{1,2,4}.yaml` (#12329): gfx950
hand-written assembly (`v_dot2c_f32_bf16`, `s_waitcnt`) that doesn't
assemble for gfx12.
- `streamk/sk_queue_selfclean.yaml` (#10451): SGEMM with a 32x32x2 MFMA
tile, rejected as an invalid WMMA configuration (0 solutions).

- hipBLASLt `matmul_gtest.yaml` (#13031): `matmul_fast_check_mx_fp8` and
`_large_k` run every fp8/bf8 pair on `'(950|1250)'`, which also matches
gfx1250-strict. gfx1250 and gfx1250-strict ship transposed-A MX kernels
for f8 x f8 (and f8 x f4, f4 x f4) only, so the bf8 pairs fail with `NO
solution found!`. The four rows with bf8 inputs get `gpu_arch: '950'`
(rows override their entry), so f8 x f8 stays on `'(950|1250)'` and the
bf8 pairs run on gfx950 only, like the other MX tests. Entry names and
gfx950 test IDs are unchanged.

## Test Plan
- 4 non gfx1250 tensilelite tests are skipped on gfx1250 test.
- 8 failing hipblaslt tests pass

## Test Result
- [ ] gfx1250-strict HW test:
   - 4 non gfx1250 tensilelite tests are skipped
   - 8 failing hipblaslt tests pass

## Submission Checklist
- [x] Look over the contributing guidelines at
https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests.

## Risk level
- Low: 4 skip marks and an arch filter on 8 rows. No library code
changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
jinchen62 added a commit that referenced this pull request Oct 30, 2026
…via GFA (#13489)

JIRA ID: AIHPBLAS-5126

## Summary

Ports vLLM's ROCm wave-split-K skinny GEMM kernels (`wvSplitK`,
`csrc/rocm/skinny_gemms.cu` at `9163190dda00`) to hipBLASLt as GFA
custom kernels, and routes BF16 and FP16 TN GEMMs with `n` = 1 to 5
tokens to them on gfx950. Follow-up to #12329.

On MI355X, these GEMMs get a **+22%** (BF16) and **+26%** (FP16) perf
lift over develop on average, over ~590 shapes per type, each with the
weights in cache and streamed from HBM. That includes random shapes the
logic was not fitted to.

## Kernels


`tensilelite/CustomKernels/vllm/wvSplitK_vllm_{bf16,f16}_tn_n<N>_y<YTILE>u<UNRL>.s`:
27 kernels, one per type, `n` and tile.

| Type | n | Tiles (YTILE, UNRL) |
| ---- | - | ------------------- |
| BF16 | 1, 3, 4 | (1, 4), (1, 2), (3, 2) |
| BF16 | 2 | (1, 4), (1, 2) |
| BF16 | 5 | (1, 2) |
| FP16 | 1 to 5 | (1, 4), (1, 2), (3, 2) |

The main loop, the DPP/MFMA reductions, the tail handling and the
chunked staging are vLLM's `wvSplitK_hf_sml_` and `wvSplitK_hf_big_`.
Each kernel picks between them at run time: all `n * K` token elements
staged once when they fit the 160 KB of LDS, K walked in LDS-sized
chunks otherwise. vLLM's third variant (`wvSplitK_hf_`, global-memory
fallback) is slower than `big` on MI355X and is not used. BF16
accumulates with `v_mfma_f32_4x4x4_16b_bf16`, FP16 with
`v_dot2c_f32_f16`.

Changes from vLLM, all needed for hipBLASLt's interface:

- alpha/beta epilogue with separate C and D and their own leading
dimensions (C is not read when `beta == 0`);
- tokens staged packed at stride K and only within bounds, so a padded
`ldb` neither overflows LDS nor reads past the last token. LDS DMA is
used only for 16-byte-aligned token rows. vLLM's `big` variant gives
wrong results with an odd `ldb`;
- 64-bit row offsets;
- vLLM's host-side wave balancing (`mindiv`) computed on the device in
closed form, after the token loads are issued;
- `sml` handles `m` that is not a multiple of `YTILE`;
- no bias.

Arguments and kernarg preload are the same as the rocBLAS wvSpltK
kernels from #12329. Predicates: `batch == 1`, `n == N`, `m > 8`, `K % 8
== 0`, unit strides; K is unbounded. The kernels follow the CU budget
(`ComputeUnits`).

Only the `.s` ship, like the other external kernels; provenance is in
each kernel's `custom.config` and in `CustomKernels/vllm/README.md`.

## Library logic

- `gfx950/gfx950/Range/gfx950_Cijk_Alik_Bljk_BBS_BH_UserArgs.yaml`
(BF16) is rewritten, with 76 rows. The rocBLAS `wvSpltK` kernels stay
where they still win: mostly K ≤ 2560 for m1 and m2, and K ≤ 448 for m4.
- `gfx950/gfx950/Range/gfx950_Cijk_Alik_Bljk_HHS_BH_UserArgs.yaml`
(FP16) is new, with 68 rows. gfx950 ships no plain-GEMM FP16 TN logic,
so this file forms its own placeholder (same header; the BF16 plain-GEMM
problem type with FP16 data types). hipBLASLt searches it before the
BiasSH/SAV libraries; problems with bias, batch > 1, `K % 8 != 0`, `m ≤
8` or `n > 5` fall through to them as before.

How the rows were chosen: for each `n` and each cell of m and K,
measured over 590 shapes per type (model decode shapes, a power-of-two
sweep, 250 random shapes), each with the weights in cache and streamed
from HBM. A cell keeps what hipBLASLt picked before unless a new tile is
at least 3% faster (6% for BF16 n = 5). K below 449 gets no new-kernel
rows (most lanes idle). Unmeasured cells keep the previous choice unless
both neighbours agree. The method was validated on 150 random shapes
held out of the fit (+5% to +22% perf lift by `n`), then refitted with
them included. Three cells where hipBLASLt runs with the built library
showed no gain keep the previous kernels. Under sustained load the vLLM
kernels lose more clock than hipBLASLt's own on the largest shapes (for
example FP16 201088×2880, n = 4: 184 µs at first, settling near 195 µs),
which standalone timing misses. Details in
`CustomKernels/vllm/README.md`.

## Performance (MI355X, perf lift over develop)

Perf lift = develop's kernel time / this branch's kernel time − 1, as a
geometric mean over the shapes. Kernel times are hipBLASLt's top-1 pick,
measured with `hipblaslt-bench` under `rocprofv3`, develop and this
branch back to back on the same GPU. "hot" = weights reused across
iterations, "cold" = `--rotating 512`. Measured before commit c22a9e0,
which returns a few cells to hipBLASLt's previous picks; shapes in those
cells are counted at develop's time, since they now run develop's kernel
(checked with `--print_kernel_info`).

| Type | Shapes | hot | cold | n=1 | n=2 | n=3 | n=4 | n=5 |
| ---- | ------ | --- | ---- | --- | --- | --- | --- | --- |
| BF16 | model decode shapes (190) | +19% | +20% | +16% | +20% | +27% |
+18% | +18% |
| BF16 | power-of-two sweep (150) | +25% | +26% | +21% | +26% | +33% |
+28% | +20% |
| BF16 | random (250) | +21% | +21% | +16% | +15% | +42% | +31% | +6% |
| FP16 | model decode shapes (190) | +29% | +35% | +46% | +41% | +33% |
+26% | +17% |
| FP16 | power-of-two sweep (150) | +30% | +38% | +42% | +41% | +38% |
+29% | +21% |
| FP16 | random (250) | +17% | +19% | +24% | +20% | +21% | +18% | +8% |

Overall: **+22%** for BF16 and **+26%** for FP16, over 1,180
measurements each.

The model shapes are the QKV/O/MLP/LM-head GEMMs of GLM-5.2,
Llama-3.1-8B/70B, GPT-OSS-120B and DeepSeek-V3 (full and TP8). Some
sizes are tuned in hipBLASLt's Equality logic and keep their kernels.

Regressions: every result with a perf lift below −5% was re-measured 5
times, alternating develop and this branch on one GPU. Most were noise
in 3–6 µs kernels (including identical kernels in both builds). The
confirmed ones that remain are within −9%, except a few n = 5 shapes
(48640×4544 down to −21%, 27648×2816 down to −12%) and a few shapes with
the weights in cache (down to −18%, BF16 2048×1024 n = 2).

## Validation

- Tensile, full-element: each kernel on 11 sizes (tails, padded and
misaligned leading dimensions, `n * K` past the LDS stage, m = 9, K =
8), at alpha = 1/beta = 0 and alpha = 2/beta = 2 with random C. All
pass.
- Standalone harness against an FP32 reference: 35 edge cases × every
tile and variant, both types, both alpha/beta settings; D padding
checked untouched.
- `hipblaslt-bench --verify`: 90 selection checks (both types, n = 1–6,
bias, batch 2, `K % 8 != 0`, m = 8 negatives). Each picks the expected
kernel and passes.
- gtests: `matmul_skinny_{bf16,f16}_tn` (64 cases, incl. pointer-array
fall-back).
- Unit tests: metadata, Range-logic checks (rows inside each kernel's
predicates, no overlaps), census, buffer-bound scan.

## Follow-ups

Bias epilogue, FP8 (`wvSplitKQ`), gfx942, gfx12, and `wvSplitKrc` for 10
≤ n ≤ 128.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
jinchen62 added a commit that referenced this pull request Nov 4, 2026
…ation order (#14740)

JIRA ID: AIHPBLAS-5126

## Summary

Since #14010, uniform summation order is on by default, and in that mode
hipBLASLt refuses every handwritten custom kernel. That stopped the
gfx950 skinny TN GEMM kernels from #13489 (vLLM wvSplitK) and #12329
(rocBLAS wvSpltK) from being selected by default: those GEMMs went back
to Tensile kernels, and the speedups the two PRs measured only appear
when the caller turns the mode off. Nothing failed; this is a
performance regression.

These kernels already keep the guarantee. Each row of D is reduced by
one wave with the same split of K over its lanes, the same loop and the
same reduction, and they use no StaggerU. This PR lets a handwritten
kernel declare that, and declares it for those 30 kernels.

## Changes

- `CustomKernel.uniformSummationOrder` (default `false`) in a kernel's
`custom.config`, carried into the solution library (Python
`Contractions.CustomKernel`, the C++ `CustomKernel` struct and its
serialization). `ValidateMetadata` rejects a value that is not a
boolean.
- Uniform summation order refuses a handwritten kernel only when it does
not declare the key. A declaring kernel still goes through the remaining
checks, including the compiled-in StaggerU check that was kept for this
case.
- The 27 vLLM and 3 rocBLAS skinny kernels declare it: one line in each
`custom.config`, regenerated with `AddCustomConfig` from their test
YAMLs.
- Test YAMLs: the 10 vLLM ones, where #14772 had turned the mode off,
and the 3 rocBLAS ones now run under the default mode.
- `row_uniformity_gtest.cpp`: the harness takes TN problems with D in
A's type. Four `SkinnyTn` cases check on the device that the default
picks keep every row of D bitwise identical and, on gfx950, that they
are the skinny kernels: BF16 n = 1; FP16 n = 5 with a YTILE = 3 tail;
BF16 with K staged in LDS chunks; the rocBLAS m4 kernel. A 16-bit D
rounds away the order differences of the harness's FP32 fill, so these
cases use a fill whose sum depends on the order by far more than D's
precision.
- `CustomKernel_test.cpp`: the declaration survives deserialization.
- `CustomKernels/README.md`: the declaration must hold for every problem
the kernel's predicates admit, and a declaring kernel needs a case like
the `SkinnyTn` ones.

## Validation (MI355X)

Develop is `29e5b0a059e`, the commit merged here. Both are
gfx950-filtered builds.

Selection: the 90 skinny selection checks (both types, n = 1 to 6,
padded leading dimensions, alpha/beta, and cases that must not reach
these kernels):

| | develop, default mode | develop, mode off | this PR, default mode |
this PR, mode off |
| --- | --- | --- | --- | --- |
| wvSplitK or wvSpltK picks | 0 of 90 | 60 of 90 | 60 of 90 | 60 of 90 |
| `--verify` passes | 90 of 90 | 90 of 90 | 90 of 90 | 90 of 90 |

With this PR, the default-mode pick equals the mode-off pick, and
develop's mode-off pick, in all 90 cases.

- Row uniformity, run on each kernel's code object: with every row of A
identical, all 30 kernels write bitwise-identical rows of D on three
shapes each (a full and a partial YTILE = 3 tile, and K = 28672 with
`ldb` = 28680 for the LDS-chunked path and a padded `ldb`): 93 of 93
runs.
- Tensile full-element validation under the default mode: each of the 30
kernels as committed and with alpha = 2, beta = 2 and random C, 60 of 60
runs passing every problem.
- gtests: the four `SkinnyTn` cases pass. Against develop's library all
four fail, for example "The default pick is
Cijk_Alik_Bljk_BBS_BH_Bias_HA_S_SAV_UserArgs_MT16x16x256..., not a
wvSplitK_vllm_bf16_tn_n1_ kernel". The `RowUniformityStreamKRejection`
cases and the 64 skinny matmul gtests pass. In the filtered builds,
`ClampRepaired_6144x5120x8192` and
`HybridDynamicStagger_1536x1536x2048_SmCount160` fail the same way on
develop, because they need FP32 logic.
- TensileLite: `CustomKernelTest.UniformSummationOrderDeserializes`
passes, and fails with the new `mapOptional` line removed.
- Unit tests: `test_CustomKernelMetadata` (with a new validation test)
and `test_custom_kernel_host` (the key is in the Python `CustomKernel`
state the library is written from) pass, the rest with the same results
as develop; `ValidateMetadata --strict` passes; the StaggerU census
passed before develop was merged in.
- CI: Math CI precheckin ran the 64 skinny matmul gtests on an MI355X
([gfx950 test
log](https://math-ci.amd.com/job/rocm-libraries/job/precheckin/job/hipblaslt/job/PR-14740/1/execution/node/1166/log/)).
From this push on it also runs the `SkinnyTn` cases, which assert the
pick. TheRock's gfx950 test jobs were skipped, and I found no CI lane
that runs the custom Tensile YAMLs on gfx950.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
cycheng pushed a commit that referenced this pull request Nov 6, 2026
…ation order (#14740)

JIRA ID: AIHPBLAS-5126

## Summary

Since #14010, uniform summation order is on by default, and in that mode
hipBLASLt refuses every handwritten custom kernel. That stopped the
gfx950 skinny TN GEMM kernels from #13489 (vLLM wvSplitK) and #12329
(rocBLAS wvSpltK) from being selected by default: those GEMMs went back
to Tensile kernels, and the speedups the two PRs measured only appear
when the caller turns the mode off. Nothing failed; this is a
performance regression.

These kernels already keep the guarantee. Each row of D is reduced by
one wave with the same split of K over its lanes, the same loop and the
same reduction, and they use no StaggerU. This PR lets a handwritten
kernel declare that, and declares it for those 30 kernels.

## Changes

- `CustomKernel.uniformSummationOrder` (default `false`) in a kernel's
`custom.config`, carried into the solution library (Python
`Contractions.CustomKernel`, the C++ `CustomKernel` struct and its
serialization). `ValidateMetadata` rejects a value that is not a
boolean.
- Uniform summation order refuses a handwritten kernel only when it does
not declare the key. A declaring kernel still goes through the remaining
checks, including the compiled-in StaggerU check that was kept for this
case.
- The 27 vLLM and 3 rocBLAS skinny kernels declare it: one line in each
`custom.config`, regenerated with `AddCustomConfig` from their test
YAMLs.
- Test YAMLs: the 10 vLLM ones, where #14772 had turned the mode off,
and the 3 rocBLAS ones now run under the default mode.
- `row_uniformity_gtest.cpp`: the harness takes TN problems with D in
A's type. Four `SkinnyTn` cases check on the device that the default
picks keep every row of D bitwise identical and, on gfx950, that they
are the skinny kernels: BF16 n = 1; FP16 n = 5 with a YTILE = 3 tail;
BF16 with K staged in LDS chunks; the rocBLAS m4 kernel. A 16-bit D
rounds away the order differences of the harness's FP32 fill, so these
cases use a fill whose sum depends on the order by far more than D's
precision.
- `CustomKernel_test.cpp`: the declaration survives deserialization.
- `CustomKernels/README.md`: the declaration must hold for every problem
the kernel's predicates admit, and a declaring kernel needs a case like
the `SkinnyTn` ones.

## Validation (MI355X)

Develop is `29e5b0a059e`, the commit merged here. Both are
gfx950-filtered builds.

Selection: the 90 skinny selection checks (both types, n = 1 to 6,
padded leading dimensions, alpha/beta, and cases that must not reach
these kernels):

| | develop, default mode | develop, mode off | this PR, default mode |
this PR, mode off |
| --- | --- | --- | --- | --- |
| wvSplitK or wvSpltK picks | 0 of 90 | 60 of 90 | 60 of 90 | 60 of 90 |
| `--verify` passes | 90 of 90 | 90 of 90 | 90 of 90 | 90 of 90 |

With this PR, the default-mode pick equals the mode-off pick, and
develop's mode-off pick, in all 90 cases.

- Row uniformity, run on each kernel's code object: with every row of A
identical, all 30 kernels write bitwise-identical rows of D on three
shapes each (a full and a partial YTILE = 3 tile, and K = 28672 with
`ldb` = 28680 for the LDS-chunked path and a padded `ldb`): 93 of 93
runs.
- Tensile full-element validation under the default mode: each of the 30
kernels as committed and with alpha = 2, beta = 2 and random C, 60 of 60
runs passing every problem.
- gtests: the four `SkinnyTn` cases pass. Against develop's library all
four fail, for example "The default pick is
Cijk_Alik_Bljk_BBS_BH_Bias_HA_S_SAV_UserArgs_MT16x16x256..., not a
wvSplitK_vllm_bf16_tn_n1_ kernel". The `RowUniformityStreamKRejection`
cases and the 64 skinny matmul gtests pass. In the filtered builds,
`ClampRepaired_6144x5120x8192` and
`HybridDynamicStagger_1536x1536x2048_SmCount160` fail the same way on
develop, because they need FP32 logic.
- TensileLite: `CustomKernelTest.UniformSummationOrderDeserializes`
passes, and fails with the new `mapOptional` line removed.
- Unit tests: `test_CustomKernelMetadata` (with a new validation test)
and `test_custom_kernel_host` (the key is in the Python `CustomKernel`
state the library is written from) pass, the rest with the same results
as develop; `ValidateMetadata --strict` passes; the StaggerU census
passed before develop was merged in.
- CI: Math CI precheckin ran the 64 skinny matmul gtests on an MI355X
([gfx950 test
log](https://math-ci.amd.com/job/rocm-libraries/job/precheckin/job/hipblaslt/job/PR-14740/1/execution/node/1166/log/)).
From this push on it also runs the `SkinnyTn` cases, which assert the
pick. TheRock's gfx950 test jobs were skipped, and I found no CI lane
that runs the custom Tensile YAMLs on gfx950.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants