Skip to content

fix(tensilelite): widen StaggerU global-read increments unsigned - #12332

Open
tony-davis wants to merge 5 commits into
developfrom
users/todavis_amdeng/rocm-31230-stagger-incs-unsigned
Open

tony-davis wants to merge 5 commits into
developfrom
users/todavis_amdeng/rocm-31230-stagger-incs-unsigned

Conversation

@tony-davis

Copy link
Copy Markdown
Contributor

JIRA ID : ROCM-31230

Motivation

On gfx1201 an fp32 NN GEMM with a large leading dimension either page-faults or returns a wrong answer while reporting success. hipblasLtMatmul gives HSA_STATUS_ERROR_MEMORY_FAULT at lda = 67,108,864 with K = 32, and at lda = 134,217,727 it returns success with a norm error of 0.911 against a 1e-06 tolerance.

The trigger is a byte count that outgrows a signed 32-bit register. GlobalReadIncs<tc>+unrollIdx is one SGPR holding stride * DepthU * bpeGR, the number of bytes one unroll iteration advances the global-read pointer. calculateStagger widens it to 64 bits before it reaches the global-read buffer descriptor, at two sites, and both used s_mul_i64_i32, a signed widen. Once stride * DepthU * bpeGR reaches 2^31 the register's bit 31 is set, the signed widen sign-extends it to a negative 64-bit value, and incrementSrd moves the read base backwards by about 2 GB instead of forwards. Where that displaced base is unmapped the kernel faults; where it is mapped the kernel reads other memory and returns a wrong result with no error.

Technical Details

Both sites multiply operands that are non-negative by construction: StaggerUIter is an iteration mask and LoopCounter an iteration count, and GlobalReadIncs is a byte stride. They now use s_mul_u64_u32, which the file already provides. WrapU still becomes negative in the subtraction that follows, and the borrowing s_sub_u32 / s_subb_u32 pair there already handles that, so only the intermediate product changes sign treatment.

Emitted instruction count is unchanged. s_mul_hi_i32 becomes s_mul_hi_u32; the low-word s_mul_i32 is identical. There is no register-pressure or scheduling change.

The defect is kernel-specific rather than shape-specific, which is why it survived. Reaching the multiply needs a non-zero StaggerUIter, and two things independently zero it: the loop-count clamp in calculateStagger drops the stagger for larger StaggerUStrideShift at low iteration counts, and StaggerUMapping picks which workgroup dimension feeds the mask, so a problem with a single workgroup in that dimension has no stagger either. Of 14 gfx1201 solutions measured at the boundary, 3 were affected.

Test Plan

Tensile/Tests/unit/characterization/_codegen/test_r3_stagger_incs_unsigned_gfx1201_char.py, CPU-only, in the -m unit lane. It emits gfx1201 kernel assembly and asserts that neither widening site uses a signed high multiply, plus a third assertion pinning that the widened stagger offset still reaches the global-read SRD, so the first two cannot pass vacuously if the offset stops being used.

The signedness is only observable in the emitted instruction, not in the Python source: SMulInt64to32 selects s_mul_hi_i32 when its sign argument is true and s_mul_hi_u32 when false, and both carry the same comment into the listing. The test therefore asserts on generated assembly rather than on source text.

Test Result

The new test fails on both sites before this change and passes after.

Full TensileLite unit lane (pytest Tensile/Tests/unit -m unit): 7746 passed, 32 skipped, 2 xfailed, 1 xpassed, 0 failed, 765 snapshots passed. The _codegen and CodegenResidue characterization suites specifically: 513 passed, 67 snapshots passed, 0 failed. No .ambr golden captures the affected instructions, so no snapshot was re-recorded and no ADR is required.

Hardware confirmation of the mechanism came from gfx1201 before the fix. Holding lda, K, m and the kernel fixed with lda * DepthU * elementBytes on the 2^31 boundary, solution 140189 is correct at n = 64, where dimension 1 has a single workgroup so its WorkGroup1 stagger input is always zero, and faults at n = 128, where a second workgroup makes an odd WorkGroup1 reachable. The same switch reproduces on the reported kernel 140192, which takes its stagger input from WorkGroup0: correct at m = 256, faults at m = 512.

Risk level

Medium. KernelWriterAssembly.py is a high-coupling file and this changes generated code for every kernel that compiles the stagger path. The change is confined to the signedness of two 64-bit widenings of a quantity that cannot be negative, the instruction count is unchanged, and the full unit lane is green. Post-merge verification on gfx1201 hardware with the shapes above is still wanted, because the unit test proves the emitted instruction rather than the runtime result.

Related

Submission Checklist

GlobalReadIncs<tc>+unrollIdx is one SGPR holding stride*DepthU*bpeGR, a byte
count that is non-negative by construction. calculateStagger widened it to 64
bits with s_mul_i64_i32 at two sites: the stagger byte offset
(StaggerUIter * GlobalReadIncs), and the unroll-loop span
(LoopCounter * GlobalReadIncs, which becomes WrapU).

Once that product reaches 2^31 the register's bit 31 is set, the signed widen
sign-extends it to a negative 64-bit value, and incrementSrd moves the
global-read base backwards by about 2 GB. Measured on gfx1201: an fp32 NN GEMM
at lda = 67108864 with DepthU = 8 faults with HSA_STATUS_ERROR_MEMORY_FAULT, and
at lda = 134217727 the displaced base lands in mapped memory and hipblasLtMatmul
returns success with a norm error of 0.911.

Both operands are non-negative at both sites, so widen with s_mul_u64_u32. WrapU
still goes negative in the subtraction below it, which the borrowing
s_sub_u32 / s_subb_u32 pair already handles. Emitted instruction count is
unchanged; s_mul_hi_i32 becomes s_mul_hi_u32.

JIRA ID: ROCM-31230

Co-authored-by: Cursor <cursoragent@cursor.com>
@tony-davis tony-davis changed the title fix(tensilelite): widen StaggerU global-read increments unsigned (ROCM-31230) fix(tensilelite): widen StaggerU global-read increments unsigned Sep 18, 2026
@tony-davis
tony-davis requested a lite review from Copilot September 18, 2026 23:24
@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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Fixes gfx1201 large-leading-dimension GEMM failures by using unsigned widening for StaggerU global-read offsets and adding assembly regression coverage.

Changes:

  • Switches stagger and WrapU products to unsigned 64-bit multiplication.
  • Adds gfx1201 code-generation assertions.
File Description
projects/​hipblaslt/​tensilelite/​Tensile/​Tests/​unit/​characterization/​_codegen/​test_r3_stagger_incs_unsigned_gfx1201_char.py Updated as part of this pull request.
projects/​hipblaslt/​tensilelite/​Tensile/​KernelWriterAssembly.py Updated as part of this pull request.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread projects/hipblaslt/tensilelite/Tensile/KernelWriterAssembly.py Outdated
The previous commit widened GlobalReadIncs unsigned at both stagger sites.
GSU.graIncrements negates that increment when the unroll dimension is in
MirrorDimsA/B, so for a mirrored unroll dimension the operand is genuinely
signed and an unsigned widen would move the global-read descriptor forward
instead of backward.

Select the widen per kernel: signed when the unroll summation index is
mirrored, unsigned otherwise. The mirrored case keeps its existing behavior;
only the non-mirrored case changes, which is where ROCM-31230 lives.

No shipped solution is affected either way: across gfx942, gfx950, gfx1200 and
gfx1201, 0 of 537,486 solutions declare a non-empty MirrorDimsA/B.

JIRA ID: ROCM-31230

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two critical correctness gaps remain in mirrored offsets and sparse TDM metadata handling.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
Resolved since last review (2)

Comment thread projects/hipblaslt/tensilelite/Tensile/KernelWriterAssembly.py Outdated
Comment thread projects/hipblaslt/tensilelite/Tensile/KernelWriterAssembly.py
@tony-davis

Copy link
Copy Markdown
Contributor Author

Hardware verification on gfx1201

Ran this on a gfx1201 R9700 (32 GB) at both commits. Every shape that failed before the fix is now correct, and the shapes that already worked are bit-for-bit unchanged.

Method

Only the Tensile device library needed regenerating, and only for the single logic family holding the affected solutions (gfx1201_Cijk_Ailk_Bljk_S_B_Bias_HA_S_SAV_UserArgs, 24 solutions). Client and host library came from TheRock nightly 35289346844-linux. I ported the hunk onto that nightly's own tensilelite rather than building the branch's tree, so the fix is the only thing differing between the baseline and patched runs; the ported hunk is byte-identical to the branch's. The unpatched baseline library was generated the same way first and reproduces every failure, including norm_error 0.911159 to the digit, so it's a valid control. Every non-faulting run is validated against the CPU reference, because above the boundary this defect sometimes returns a wrong answer instead of faulting.

Solution indices below are local to a single-family build, matched by libraryLogicIndex: 5 is 140192, 8 is 140195, 2 is 140189, 9 is 140196.

Shapes that failed before the fix

solution shape lda*DepthU*4 before after
5 (140192) m=lda=67,108,864, k=32, n=1 2^31 FAULT CORRECT, norm_error 1.0544e-07
5 (140192) m=lda=100,000,000, k=32, n=1 3.2e9 FAULT CORRECT, norm_error 9.92358e-08
5 (140192) m=lda=134,217,727, k=32, n=1 2^32 - 32 WRONG, norm_error 0.911159 CORRECT, norm_error 9.85013e-08
8 (140195) m=lda=22,369,622, k=96, n=1 2^31 + 64 FAULT CORRECT, norm_error 1.68249e-07
2 (140189) m=262,144, lda=16,777,216, k=128, n=128 2^31 FAULT CORRECT, norm_error 1.96929e-07

The third row is the one I care most about. It was returning a wrong answer while reporting success, and it now agrees with the CPU reference to 1e-07.

Regression guards

solution shape before after
5 (140192) m=lda=67,108,863, k=32, n=1 CORRECT 9.93493e-08 CORRECT 9.93493e-08
9 (140196) m=lda=134,217,727, k=32, n=1 CORRECT 0 CORRECT 0
8 (140195) m=lda=22,369,621, k=96, n=1 CORRECT 1.69081e-07 CORRECT 1.69081e-07
2 (140189) m=262,144, lda=16,777,216, k=128, n=64 CORRECT 0 CORRECT 0

Identical norm_error on all four, so nothing that already worked moved.

The two commits are equivalent for shipped kernels

58a790730b claims no shipped solution declares mirrored dims. For this family that's confirmed empirically and not just argued: the code objects generated from ff7d1154f0 (unconditional unsigned) and from 58a790730b (conditional) are byte-identical, both sha256 a66968deae6bbb52, while the baseline is 5435fcf1d21a1370. The conditional always resolves to unsigned here. Both commits also went through the full table above and produced identical results down to the norm_error digits.

Instruction-level check

Unbundled the gfx1201 code object from both libraries and disassembled:

library s_mul_hi_i32 s_mul_hi_u32 disassembly lines
baseline 44 275 163,929
patched 0 319 163,929

Diffing the two disassemblies gives 180 lines of difference: the 44 instruction substitutions plus the objdump filename header, and nothing else. Instruction addresses are unchanged on both sides (the substituted instruction sits at 0x14348 either way), so there's no code motion, no register-pressure change and no scheduling change. That's direct support for the "emitted instruction count is unchanged" claim in the description.

Scope

One logic family on one architecture, covering the three solutions known to be affected plus four controls, so this isn't a library-wide statement. All shapes are fp32 NN with n = 1 except the two 140189 rows, which need n >= 128 before the stagger fires at all. I didn't construct a shape that isolates a WrapU-specific failure, so that site is exercised here only because it shares widenIncs. One thing still open and not for this PR: the sparse-metadata stagger multiply (" stagger byte offset of metadata") is still s_mul_i64_i32 and has the same exposure, though it isn't reachable from these shapes.

… check

calculateStagger is called for the MX scale tensors as well as A and B
(KernelWriter.py, tensorParametersA["MX"] and tensorParametersB["MX"]), and
MirrorDimsMXSA/MXSB are copied from MirrorDimsA/B in Solution.py. The previous
commit's predicate listed only A, B and Metadata, so a mirrored unroll dimension
on an MX scale tensor would have taken the unsigned widen and moved that
descriptor forward instead of backward.

Add MXSA and MXSB to the check. All five MirrorDims keys exist and default to
empty, so the predicate is safe for every tensor char calculateStagger sees.

JIRA ID: ROCM-31230

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

Copy link
Copy Markdown
Contributor Author

Re-verified at 9be4f0e8e31

Same gfx1201 rig as above. The hardware table is unchanged: all five previously failing shapes correct, all four guards bit-for-bit identical to the commit-2 run. For the gfx1201 fp32 family the code object is byte-identical across all three commits (sha256 a66968deae6bbb52), so this commit is a no-op there, which is expected since no MX tensors are involved.

Worth adding, because it's the part this commit is actually about: the MXSA/MXSB path is real and reachable with shipped logic, so commit 2's tuple omitted a live path rather than a hypothetical one. I instrumented a throwaway copy of the generator to log the tensor char reaching the widen site. For gfx1250/Equality/gfx1250_Cijk_Alik_Bljk_F8BS_MXAE8B32_MXBE8B32_BH_UserArgs, calculateStagger is reached with tc = A, B, MXSA and MXSB. For the gfx950 S_MX families it is reached with A and B only, because the MX calls are gated on MXBlockA/MXBlockB (KernelWriter.py:3113-3116) and those solutions don't set them.

It changes no emitted code today. On that MX family all four tensor chars report mirrored=False, and the disassembly is identical between commit 2 and commit 3: 62 s_mul_hi_u32 and 0 s_mul_hi_i32 in both. So this is correctness for a future mirrored MX kernel, not a behaviour change now.

One methodology note in case anyone repeats this: the gfx1250 MX build is not byte-reproducible. Two builds from the same unmodified generator gave different code object hashes, so I compared disassembly rather than sha256 there. The gfx1201 build is reproducible, which is what makes the sha comparison above meaningful.

This also confirms the safety argument in the commit message. All five MirrorDims keys are declared with empty defaults (SolutionStructs/Problem.py:485-489) and MirrorDimsMXSA/MXSB are copied from A/B (SolutionStructs/Solution.py:2830 and 2845). Generation is clean on both the MX and non-MX families.

@tony-davis

Copy link
Copy Markdown
Contributor Author

Confirms the expectation. Commit 3 is a no-op on gfx1201 because all 24 solutions in that family have mxBlockA, mxBlockB and sparse false, so calculateStagger only ever sees tc = A or B there.

The MX instrumentation is the valuable part: MXSA/MXSB are reached by shipped gfx1250 logic, so commit 2's tuple omitted a live path, not a hypothetical one.

Worth recording beyond this PR: gfx1250 MX builds are not byte-reproducible, so sha256 is not a valid before/after technique on that family. Diff disassembly instead.

Still uncovered, both on ROCM-31230: the mirrored path itself, which needs a synthetic config since no shipped solution mirrors, and the deferred sparse-metadata multiply. The gfx1201 coverage Copilot asked for is now satisfied in substance but not in form, since none of it runs in a shared CI lane.

@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop   #12332   +/-   ##
========================================
  Coverage    70.90%   70.90%           
========================================
  Files         2847     2847           
  Lines       466762   466756    -6     
  Branches     68829    68833    +4     
========================================
- Hits        330914   330911    -3     
+ Misses      111922   111916    -6     
- Partials     23926    23929    +3     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 48.78% <ø> (+0.02%) ⬆️
TensileLite-Unit 76.47% <100.00%> (+0.01%) ⬆️
hipBLAS 90.62% <ø> (ø) Carriedforward from 9be4f0e
hipBLASLt 35.28% <ø> (ø) Carriedforward from 9be4f0e
hipCUB 82.68% <ø> (ø) Carriedforward from 9be4f0e
hipDNN 86.98% <ø> (ø) Carriedforward from 9be4f0e
hipFFT 54.81% <ø> (ø) Carriedforward from 9be4f0e
hipRAND 76.12% <ø> (ø) Carriedforward from 9be4f0e
hipSOLVER 68.96% <ø> (ø) Carriedforward from 9be4f0e
hipSPARSE 86.99% <ø> (ø) Carriedforward from 9be4f0e
rocBLAS 48.25% <ø> (ø) Carriedforward from 9be4f0e
rocFFT 48.99% <ø> (-0.06%) ⬇️ Carriedforward from 9be4f0e
rocRAND 57.42% <ø> (ø) Carriedforward from 9be4f0e
rocSOLVER 76.83% <ø> (ø) Carriedforward from 9be4f0e
rocSPARSE 74.61% <ø> (ø) Carriedforward from 9be4f0e
rocThrust 93.15% <ø> (ø) Carriedforward from 9be4f0e

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

Files with missing lines Coverage Δ
...blaslt/tensilelite/Tensile/KernelWriterAssembly.py 70.29% <100.00%> (+<0.01%) ⬆️

... and 4 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.

@tony-davis

Copy link
Copy Markdown
Contributor Author

gfx1201 hardware regression test added

This is the coverage asked for in review. matmul_size_t_stagger_incs_overflow_f32_ROCM31230 in clients/tests/data/matmul_gtest.yaml, with its shape anchor in matmul_common.yaml. fp32 NN, M=2048, N=1, K=32, lda=67108864, integer_exact plus unit_check, gated to gpu_arch: '1201'.

lda * DepthU * bpe is 2^26 * 8 * 4 = 2^31 exactly for the MT256x128x8 kernel the heuristic picks, which is the first failing value. lda = 2^26 - 1 is the ticket's passing case and I confirmed it stays correct at 1.02e-07, so the gate sits one representable step off a verified control.

Before and after on gfx1201

library exit result
baseline, unpatched 1 FAILED, memory fault at testing_matmul.hpp:1057 plus d_vector guard trips
patched 0 [ PASSED ] 1 test, 4.3 s

Three runs each, fully deterministic. Both libraries were generated from the nightly's own tensilelite (shipped copy is byte-identical to 4606f831) differing only by this PR's hunk, which I checked is byte-identical to the branch's. The .dat.zlib metadata came out bit-identical between baseline and patched, so selection and solution indices don't move and only the code objects differ. Disassembly matches the table already in this PR: baseline 44 s_mul_hi_i32 / 275 s_mul_hi_u32, patched 0 / 319. As a regression guard, 506 fp32 NN cases pass identically on both.

I also regenerated hipblaslt_gtest.data from the unmodified branch yaml first and got a byte-identical file to the one shipped in the nightly, which is what makes the harness comparison trustworthy.

Two choices worth flagging

Selection is left to the heuristic rather than pinned with solution_index. Indices aren't stable across library retunes, so pinning one buys a gate that silently rots. The trade is the mirror image: a future retune could move selection off the affected kernel and weaken this gate instead. I think that's the better failure mode, but it's a real trade.

Category is nightly, not pre_checkin. A is 8.6 GiB on both the host and the device and can't be made smaller, since the A span is (K-1)*lda + M whatever M is. The silent-corruption face of this defect (wrong answer, no fault) needs lda near 2^32 and 17 GiB for A, so it won't fit on a 16 GiB part and isn't what's gated here. unit_check with integer_exact is bit-exact, so it would catch that variant too on a larger card.

One behaviour to know about: when this test fails, the process tears down on the poisoned HIP context and never prints gtest's final summary. CI still gets exit code 1 and the per-assertion failures, just not the [ FAILED ] block.

Out of scope, filed separately

Getting this landed turned up two pre-existing CI problems, now tracked in #12337. Short version: category: stress matches no TheRock tier, so the three existing matmul_size_t_* stress cases never run, and an unknown category is silently accepted because match_test_category() is bypassed. Separately, a host-memory shortfall segfaults rather than skipping, because get_available_host_memory() uses freeram and there's no host-side equivalent of CHECK_DEVICE_ALLOCATION. I hit that once on this box during verification. Neither is caused by this PR, but the second one is a flake risk for any large-shape case.

…rflow

The unit test in this PR asserts on emitted assembly, which proves the
instruction changed but not that the kernel is correct at runtime. Add the
hardware reproducer to close that gap.

matmul_size_t_stagger_incs_overflow_f32_ROCM31230 is fp32 NN with
M=2048, N=1, K=32, lda=67108864. For the MT256x128x8 kernel the heuristic
selects, lda * DepthU * bpe is 2^26 * 8 * 4 = 2^31 exactly, the first value
whose bit 31 is set and so the first one the signed widen sign-extends.
lda = 2^26 - 1 is the ticket's passing case and stays correct at 1.02e-07,
so the gate sits one representable step off a verified control.

M only has to be large enough for a second workgroup in dimension 0, since
the selected kernel takes its stagger input from WorkGroup0 and a single
workgroup zeroes it. N = 1 keeps B, C and D negligible. A cannot be made
small: its span is (K-1)*lda + M elements whatever M is, so A is 8.6 GiB on
both the host and the device. That is why the case is nightly rather than
pre_checkin.

Selection is left to the heuristic rather than pinned with solution_index,
because indices are not stable across library retunes.

Verified on a gfx1201 RX 9070 XT against two Tensile libraries generated
from the same generator differing only by this PR's hunk: FAIL pre-fix
(exit 1, memory fault), PASS post-fix (exit 0), three runs each. The
.dat.zlib metadata is bit-identical between the two, so selection does not
move; only the code objects differ, 44 s_mul_hi_i32 becoming 0. 506 fp32 NN
cases pass identically on both.

JIRA ID: ROCM-31230

Co-authored-by: Cursor <cursoragent@cursor.com>
@tony-davis
tony-davis marked this pull request as ready for review September 19, 2026 04:18
@tony-davis
tony-davis requested a review from a team as a code owner September 19, 2026 04:18
@tony-davis
tony-davis requested a lite review from Copilot September 19, 2026 04:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate findings remain in the runtime gate, host-memory handling, and assembly coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Regex does not verify stagger offset reaches SRD

projects/​hipblaslt/​tensilelite/​Tensile/​Tests/​unit/​characterization/​_codegen/​test_r3_stagger_incs_unsigned_gfx1201_char.py:164

This regex does not prove that the newly computed stagger offset reaches the SRD: removeStagger() emits the same s_addc_u32 ... gra SRD += inc(upper) pattern later (KernelWriterAssembly.py:6832-6860). Deleting the calculateStagger increment at line 6638 would still satisfy this assertion. Match the add within the SRDs += (StaggerUIter) ... block so this non-vacuity check covers the production path it is intended to guard.

Comment on lines +3052 to +3054
# Selection is left to the heuristic rather than pinned with solution_index, because
# indices are not stable across library retunes. The trade is that a future retune
# could move selection off the affected kernel and quietly weaken this gate.
Comment on lines +3056 to +3059
# nightly rather than pre_checkin: A is 8.6 GiB on both the host and the device and
# cannot be made smaller, and a host-side shortfall currently crashes the harness
# instead of skipping (see the get_available_host_memory note on ROCM-31230), so this
# is kept off the per-PR tier.
@nakajee

nakajee commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

@tony-davis
This is actually a known issue.
I made a fix in Tensile long time ago.
https://github.com/ROCm/Tensile/pull/1672/changes
In this PR, I made changes in calculateStagger and removeStagger.

In tensilelite, removeStagger still uses signed mul.
Have you confirmed tailloop case?
I am thinking we need similar fix for tensilelite.

@tony-davis

tony-davis commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

@nakajee Thank you, this sent me down the right path. Tensile PR 1672 is the precedent I should have found myself, and your calculateStagger hunk there is the same change this PR makes. Reassuring to have converged on what you already concluded three years ago.

Tail loop: you were right to ask, and it's fixed

My gate uses K=32 with DepthU=8, which divides evenly, so there was no tail loop in it at all. Measured on gfx1201 with solution 140192 forced and lda = 2^26, against two libraries differing only by this PR's hunk:

K tail iterations baseline patched
32 0 FAULT PASS, norm 1.67e-07
33 1 FAULT PASS, norm 1.68e-07
36 4 FAULT PASS, norm 1.58e-07

Controls hold: K=36 just below the boundary and at a small lda pass on either library.

On removeStagger

I think it may already be covered, and the helper name is what makes it look otherwise. Both of its multiplies call s_mul_i64_i32_u32, which reads as signed but computes |src0| * src1 unsigned and negates if src0 was negative, so GlobalReadIncs (src1) is treated as unsigned. The signed operand is (3 or 2) - StaggerUIter, which really can go negative.

It arrived by a different route than yours: commit 9446f2356a3 (December 2024, "memory access fault in tail loop with extreme larger size") rather than your algebraic expansion into two unsigned multiplies. Same property, and it's why the tail rows above pass without a second fix. So my reading is that tensilelite got the removeStagger half of 1672 back then and never got the calculateStagger half, which this PR supplies. Please push back if I'm misreading that helper, since it's the load-bearing claim and you know this code better than I do.

Still open, and I'd value your view: the sparse-metadata pair in calculateStagger (lines 6657 and 6662) is still s_mul_i64_i32. I deferred it because no sparse shape was measured here. After this PR it's the last of the four sites still sign-extending. Tracked on ROCM-31230.

The new regression case ran and passed on the gfx1201 R9700 node in math-ci precheckin (4803 ms), so it's exercised on real hardware in a shared lane.

…fy the SRD assertion

Two review findings on the regression coverage.

The gfx1201 hardware gate selected its kernel through the heuristic, so a future
library retune could pick a solution with StaggerU disabled and report success
without executing either widening. Pinning solution_index is not an option
because indices are not stable across retunes. Use algo_method 1 with
requested_solution_num -1 instead, the pattern matmul_heuristic_all_solutions
already uses, which runs every supported solution. At this shape that is 17
solutions rather than 1, for about 3 extra seconds.

The third assertion in the codegen test searched the whole kernel for
"s_addc_u32 s[sgprSrd{A,B}+1] ... gra SRD += inc(upper)". incrementSrd emits that
from nine call sites, and the ordinary global-read increment among them fires in
every kernel, so the assertion was close to unconditionally true: one kernel
emits 44 such lines, 8 of which match the regex, none from calculateStagger.
Scope it to a single "addr += (StaggerUIter) * GlobalReadIncs<tc>" block and tie
the registers the stagger multiply writes to the ones the SRD increment reads.

Verified by negative control. With calculateStagger's incrementSrd call replaced
by pass, the old assertion still passed while the new one fails on "the A stagger
offset in s16 is no longer added into sgprSrdA+0". Generator restored afterwards;
the _codegen suite is 302 passed, 1 xfailed, 1 xpassed, 67 snapshots, and the
hardware gate still FAILs pre-fix / PASSes post-fix on gfx1201.

JIRA ID: ROCM-31230

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

Copy link
Copy Markdown
Contributor Author

Both actionable findings are addressed in the latest commit. Thanks Copilot, the third one in particular was a good catch.

Kernel selection. Fair point, and pinning solution_index isn't an option since indices move across library retunes. Used algo_method: [1] with requested_solution_num: -1 instead, which is what matmul_heuristic_all_solutions already does: it runs every supported solution rather than the heuristic's pick. At this shape that's 17 solutions instead of 1, for about 3 extra seconds. Still FAILs pre-fix and PASSes post-fix on gfx1201. A retune can't weaken the gate now, because nothing is being selected.

The SRD assertion. Correct, and worse than described. incrementSrd emits that instruction and comment from nine call sites, and the ordinary global-read increment among them fires in every kernel. The kernel this test emits contains 44 gra SRD += inc(upper) lines, 8 of which match the old regex, and none of those 8 come from calculateStagger.

I proved it with a negative control rather than taking it on argument. Replacing calculateStagger's incrementSrd call with pass and re-emitting: the old assertion still passed, matching s_addc_u32 s[sgprSrdA+1], s[sgprSrdA+1], s17 from /* global read inc A loopL */; the new one fails with "the A stagger offset in s16 is no longer added into sgprSrdA+0". So the assertion really did prove nothing, and the PR description's claim that it stopped the first two passing vacuously was wrong. The repair scopes everything to one addr += (StaggerUIter) * GlobalReadIncs<tc> block and ties the registers the stagger multiply writes to the ones the SRD increment reads. It also skips the sparse arm, which reuses the same phrase and has its own increment. Generator restored; _codegen is 302 passed, 1 xfailed, 1 xpassed, 67 snapshots, 0 failed.

Host memory. Agreed, and thanks for the precise site, hA[i] at testing_matmul.hpp:2648-2651 with no host-side counterpart to CHECK_DEVICE_ALLOCATION. This is pre-existing and harness-wide, so it's tracked separately in #12337 rather than fixed here. get_available_host_memory() reads sysinfo freeram rather than available memory, so it also trips spuriously when reclaimable page cache is warm; I hit exactly that during verification on a 125 GB host with 111 GB in cache. Keeping this case in nightly limits the blast radius but doesn't remove it, which is why the issue exists.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two moderate test-configuration findings need resolution, along with a documentation clarification.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 Medium severity

Open (3)

Comment on lines +3069 to +3070
algo_method: [1]
requested_solution_num: -1
@nakajee

nakajee commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Thanks for your comment.
I did not realize that removeStagger was already fixed.

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.

3 participants