Repository navigation
feat(hipblaslt): add rocBLAS wvSpltK BF16 TN skinny GEMMs (n ≤ 4) via GFA - #12329
Conversation
✅ All Checks Passed — Ready for Review
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🎉 All checks passed! This PR is ready for review. |
7fe9814 to
e48a882
Compare
|
@nakajee Switched to |
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 Report❌ Patch coverage is
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
*This pull request uses carry forward flags. Click here to find out more.
🚀 New features to boost your workflow:
|
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>
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>
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>
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>
m vs. n: which dimension is the token countThe 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:
The list writes each GEMM as So a true decode is Where "M = 1, 2, 4" came from. The What changed in the PR. The original FP16 NN kernels ( |
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # projects/hipblaslt/tensilelite/include/Tensile/ContractionSolution.hpp # projects/hipblaslt/tensilelite/src/ContractionSolution.cpp
TheRock Submodule Bump ActivityNewest first
|
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>
…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>
…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>
…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>
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
wvSpltKwave-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
nis how PyTorch-based inference calls hipBLASLt. A linear layer computesy[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 withm= H (output features),n= T (tokens) andk= K.torch.mm(x, W.t())on a GLM-5.2 decode shape logs:F.linearon 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 followedhipblaslt-benchruns that fed the GEMM list's row-major M straight into hipBLASLt'sm, a call no framework makes, so they are dropped.Kernels
wvSpltK_bf16_tn_m1,wvSpltK_bf16_tn_m2andwvSpltK_bf16_tn_m4, ported from rocBLAS'sTRANSA=falseinstantiation. The suffix is rocBLAS's M, the token count, which is hipBLASLt'snhere.v_dot2c_f32_bf16, no MFMA. gfx950 only: gfx942 has no BF16 dot instruction..sfiles ship. They are emitted at code object v4, the version hipBLASLt links its library code objects at.wvSpltK_bf16_tn_m1wvSpltK_bf16_tn_m2wvSpltK_bf16_tn_m4CuCountis 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) andTENSILE_PERSISTENT_MAX_CUS. So the persistent stride can never disagree with the grid.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 thanYTILE=3, UNRL=2.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.n == 1/n == 2. m4:0 < n ≤ 4. hipBLASLt quick-returnsm == 0but notn == 0, because grouped GEMM relies on it.Library logic
gfx950/gfx950/Range/gfx950_Cijk_Alik_Bljk_BBS_BH_UserArgs.yamlroutes n = 1, 2 and 3–4 to m1, m2 and m4, form ≥ 9,8 ≤ K ≤ 4096and batch 1.getAllAlgos, which aiter'shipb_findallsolsenumerates) includes them past the cap.gfx950/gfx950/Equality/gfx950_Cijk_Alik_Bljk_BBS_BH_UserArgs.yaml), so both share a placeholder library and tunedEqualitysizes keep priority.K % 8 ≠ 0) falls through to the next library.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).Tensile and host-side changes
CustomGridSize::ComputeUnitsand aComputeUnitsarg 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.AssertSizeEqual,AssertSizeGreaterThan,AssertSizeLessThanandAssertStride{A,B,C,D}Equalkeys throughextractDimPredicateonto the existingSizeEqual,SizeGreaterThan,SizeLessThanandStride*Equalruntime 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.getAllSolutionsalready skipped handwritten custom kernels for general-batched problems, which they cannot run. The heuristic (getSolutions, behindgetBestSolutions/getBestRawSolutions) andisSolutionSupportednow 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 withbatch_count = 1in a skinny range hit an illegal memory access.Which frameworks reach these kernels
torch.mm(x, W.t())andF.linearwithout bias, as above.SGLANG_USE_AITER=1): linear layers go through aiter'stgemm.mm, which reaches hipBLASLt only for shapes missing from aiter's tuned configs (itstorchfallback) or through itshipb_mmbackend.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.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:
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-bench: 21 cases pick the expected kernel and pass CPU verification.ldb > K) and β = 0.5.K % 8 ≠ 0,m ≤ 8, FP16 NN, and general-batched (pointer-array) problems withbatch_count = 1.hipblaslt-test: newpre_checkincasematmul_skinny_bf16_tn, 16 tests, all PASSED. They cover n = 1–4, strided and general-batched, padded leading dimensions, and β 0 and 2.--sm_count_target 64/128andTENSILE_PERSISTENT_MAX_CUS=48, wvSpltK launches on 64 / 128 / 48 workgroups (checked withrocprofv3) and passes CPU verification.CustomKernel_test.cppcovers the grid and kernarg.Perf (MI355X gfx950, hipBLASLt out of the box)
Setup:
hipblaslt-bench --requested_solution 2 -i 100 -j 30 --use_gpu_timer), both timed back to back in one process.--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.
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):
tn_m4loses at very large N with short K: 0.61–0.62× hot and 0.73× streamed at 32768×1024.MI350-tuned sizes: