Skip to content

feat(tensilelite): StreamK single-hop work stealing#8442

Merged
bnemanich merged 53 commits into
developfrom
users/jolabega/dyn-streamk-queue-work-stealing-rebased
Jul 24, 2026
Merged

feat(tensilelite): StreamK single-hop work stealing#8442
bnemanich merged 53 commits into
developfrom
users/jolabega/dyn-streamk-queue-work-stealing-rebased

Conversation

@jaopaulolc

@jaopaulolc jaopaulolc commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

JIRA ID: AIHPBLAS-3646

Motivation

In the dynamic-queue StreamK schedulers, a workgroup whose home queue drains early sits idle while sibling queues still have structural-extra tiles to process. This load imbalance is most visible on tile counts that do not divide evenly across the queues. This PR lets a drained workgroup steal a single neighboring tile so the imbalance is smoothed out.

Technical Details

Adds single-hop next-neighbor work stealing to the dynamic-queue StreamK fetch, controlled by a new codegen-time Tensile solution parameter StreamKWorkStealing (∈ [0, 1], default 0). It applies to both SK4 (StreamKDynamic) and SK5 (StreamKHybrid's dynamic sub-path).

When a workgroup's home queue (1 of 8) drains, it makes exactly one atomic attempt to claim the structural-extra tile of queue (queueIdx+1) & 0x7, gated so it only fires when a tile remainder exists and the neighbor owns an extra the home does not. Home-queue atomic auto-reset is disabled while stealing (to keep tile ownership unique under races); the last workgroup restores the queue + completion counters in kernelEnd.

Test Plan

  • Unit (-m unit, TensileLite): full Tensile/Tests/unit/ suite, including the characterization .ambr goldens and the strict-corpus gate (test_input_yaml_corpus_is_strict_clean), plus test_streamk_work_stealing.py (asserts the parameter exists, the steal / no-reset / kernelEnd-reset assembly is emitted by the new helpers, and Solution validation rejects the parameter outside StreamK ∈ {4,5} and with StreamKAtomic).
  • GPU (gfx950 / MI355X) with NumElementsToValidate=-1 (full-element validation):
    • sk_dynamic_work_stealing.yaml (SK4): fork StreamKWorkStealing[0,1].
    • sk_hybrid_work_stealing.yaml (SK5): fork StreamKWorkStealing[0,1] × StreamKHybridMode[0,1].
    • Shapes include unaligned 640x640 (25 tiles → steal fires) and batched 384x384x3, with repeated launches to exercise the kernelEnd counter reset.
    • Broader StreamK GPU config family run as a merge-sync sanity check.

Test Result

Revalidated on top of the latest develop (post-merge):

  • Unit suite: 5244 passed, 19 skipped, 0 failed (includes the regenerated characterization goldens; .ambr byte-stable on re-run).
  • GPU (gfx950 / MI355X): sk_dynamic_work_stealing.yaml and sk_hybrid_work_stealing.yaml both PASSED (clientExit=0), all StreamKWorkStealing on/off × StreamKHybridMode on/off variants validating with full-element checking; broader StreamK GPU family 22 passed / 27 skipped (non-gfx950 archs) / 0 failed.

Submission Checklist

Risk level

Low — the entire feature is behind a codegen-time toggle that defaults to off, so generated kernels and runtime behavior are unchanged unless StreamKWorkStealing=1 is explicitly selected.

@jaopaulolc
jaopaulolc force-pushed the users/jolabega/dyn-streamk-queue-work-stealing-rebased branch from 3ba0a69 to 350d1de Compare June 15, 2026 17:48
@jaopaulolc jaopaulolc changed the title [hipblaslt][tensilelite] Single-hop next-neighbor StreamK work stealing (SK4/SK5) [hipblaslt][tensilelite] Single-hop next-neighbor StreamK work stealing Jun 15, 2026
@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.52475% with 106 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../hipblaslt/tensilelite/src/ContractionSolution.cpp 28.05% 53 Missing and 6 partials ⚠️
...ipblaslt/tensilelite/Tensile/Components/StreamK.py 67.31% 26 Missing and 8 partials ⚠️
...lt/tensilelite/Tensile/SolutionStructs/Solution.py 10.00% 8 Missing and 1 partial ⚠️
...ects/hipblaslt/tensilelite/Tensile/KernelWriter.py 33.33% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #8442      +/-   ##
===========================================
+ Coverage    69.50%   69.57%   +0.07%     
===========================================
  Files         2770     2770              
  Lines       452498   452684     +186     
  Branches     66620    66649      +29     
===========================================
+ Hits        314492   314929     +437     
+ Misses      117526   117388     -138     
+ Partials     20480    20367     -113     
Flag Coverage Δ *Carryforward flag
TensileLite 34.32% <ø> (+0.01%) ⬆️ Carriedforward from 33718a5
TensileLite-CPP 38.06% <28.05%> (-0.03%) ⬇️
TensileLite-Unit 64.67% <47.52%> (-0.09%) ⬇️
hipBLAS 90.62% <ø> (ø) Carriedforward from 33718a5
hipBLASLt 34.63% <ø> (ø)
hipCUB 82.68% <ø> (ø) Carriedforward from 33718a5
hipDNN 86.35% <ø> (ø) Carriedforward from 33718a5
hipFFT 47.34% <ø> (ø) Carriedforward from 33718a5
hipRAND 76.12% <ø> (ø) Carriedforward from 33718a5
hipSOLVER 69.18% <ø> (ø) Carriedforward from 33718a5
hipSPARSE 86.27% <ø> (ø) Carriedforward from 33718a5
rocBLAS 47.95% <ø> (ø) Carriedforward from 33718a5
rocFFT 47.38% <ø> (+0.83%) ⬆️ Carriedforward from 33718a5
rocRAND 57.01% <ø> (ø) Carriedforward from 33718a5
rocSOLVER 76.92% <ø> (ø) Carriedforward from 33718a5
rocSPARSE 72.58% <ø> (ø) Carriedforward from 33718a5
rocThrust 91.36% <ø> (ø) Carriedforward from 33718a5

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

Files with missing lines Coverage Δ
...slt/tensilelite/Tensile/Common/GlobalParameters.py 98.43% <ø> (ø)
...t/tensilelite/Tensile/Common/RequiredParameters.py 100.00% <ø> (ø)
...aslt/tensilelite/Tensile/Common/ValidParameters.py 100.00% <ø> (ø)
...ects/hipblaslt/tensilelite/Tensile/KernelWriter.py 83.06% <33.33%> (+0.05%) ⬆️
...lt/tensilelite/Tensile/SolutionStructs/Solution.py 76.61% <10.00%> (-0.19%) ⬇️
...ipblaslt/tensilelite/Tensile/Components/StreamK.py 86.15% <67.31%> (+4.08%) ⬆️
.../hipblaslt/tensilelite/src/ContractionSolution.cpp 7.85% <28.05%> (+0.48%) ⬆️

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

Add StreamKWorkStealing, a codegen-time solution parameter (not a kernel
argument) that enables single-hop next-neighbor work stealing in the
dynamic-queue StreamK fetch, for both SK4 (StreamKDynamic) and the dynamic
sub-path of SK5 (StreamKHybrid). When off (default) the emitted assembly is
identical to baseline.

When a workgroup's home queue (one of 8 hardcoded queues) is drained, it makes
exactly one atomic attempt to claim the structural-extra tile of the next
queue ((queueIdx+1) & 0x7), gated so it only fires when there is a tile
remainder and the neighbor owns an extra the home does not. To keep tile
ownership unique while stealing, the home queue's atomic auto-reset is disabled
whenever a remainder exists; the last workgroup restores the 8 queue counters
plus a completion counter (placed at synchronizer offset 0x80, clear of both
the 256B-strided queue counters and the partials/fixup flag region at 0x800)
in kernelEnd. The host already zeroes the whole Synchronizer buffer once at
handle creation, so no host-side workspace changes are needed. A lost steal
race leaves the index out of range and is absorbed by the existing
valid-work-item check.

Shared helpers on the StreamK base are reused by SK4 and SK5 (parameterized on
the skGrid/SKGrid SGPR). SK5's kernelEnd reset is gated on the hybrid mode bit
so it is a no-op in the static (SK3) sub-path. StreamKWorkStealing is rejected
unless StreamK in {4,5} and is incompatible with StreamKAtomic.
Unit tests (test_streamk_work_stealing.py): assert the StreamKWorkStealing
solution parameter exists, that the steal/no-reset/kernelEnd-reset assembly is
emitted by the new StreamK helpers, and that Solution validation rejects the
parameter outside StreamK in {4,5} and with StreamKAtomic.

GPU YAML tests (sk_dynamic_work_stealing.yaml for SK4, sk_hybrid_work_stealing.yaml
for SK5): fork StreamKWorkStealing over [0,1] (and StreamKHybridMode over [0,1]
for SK5) with NumElementsToValidate=-1 over aligned and unaligned/batched shapes
(640x640, 384x384x3) so the steal path and the kernelEnd counter-reset across
repeated launches are exercised. Validated on gfx950: all off/on and hybrid-mode
variants PASSED.
@jaopaulolc
jaopaulolc force-pushed the users/jolabega/dyn-streamk-queue-work-stealing-rebased branch from 8be6664 to 948a5a1 Compare June 18, 2026 01:08
@jaopaulolc
jaopaulolc marked this pull request as ready for review June 18, 2026 03:04
@jaopaulolc
jaopaulolc requested a review from a team as a code owner June 18, 2026 03:04
@jaopaulolc jaopaulolc added the rocm:gemm algos Label to identify PRs of the GEMM Algorithms team. label Jun 18, 2026
jaopaulolc and others added 14 commits June 19, 2026 09:30
Correct bool/int type mismatches flagged by the strict corpus-clean
validator: BoundsCheck and PrefetchLocalRead must be int, MIArchVgpr
must be bool. Resolves the 2 corpus-clean failures for the new
sk_dynamic_work_stealing / sk_hybrid_work_stealing tests.
…ealing

Adding StreamKWorkStealing to the required (min naming) parameter set
inserts the SKWS0 token into the verbose solution name and perturbs the
kernel identity-hash basenames, consistent with the rest of the StreamK
family (SKA/SKFTR/SKFDPO/SKXCCM). Regenerate the syrupy golden snapshots
to match. Diff is pure naming/identity churn -- no err-code, instruction
count, or emitted-assembly changes.
…kernel end

StreamK work-stealing (WS) kernels coordinate through device-global work-queue
and completion counters. On the deferred-store / K-split epilogue path the
kernel ended via SEndpgm without emitting the StreamK end-of-kernel reset
(only the non-deferred functionEnd path emitted it). The counters therefore
kept their drained terminal value across launches, so the first launch did the
real work and launches 2..N found the queue exhausted and exited as no-ops.
This produced physically impossible benchmark numbers (per-launch time
collapsing below the arithmetic floor, us < min_us, super-peak Gflops) and
would silently no-op repeated real-world calls until the buffer was re-zeroed.

- Fix 1 (root cause, KernelWriter.py): emit skComponent.kernelEnd() in the
  hasAnyDeferred epilogue branch before SEndpgm, mirroring functionEnd. WS
  gating lives inside kernelEnd, so non-WS kernels are unaffected.
- Fix 2 (defense-in-depth, tensile_host.cpp, default OFF): optional per-launch
  hipMemsetAsync of the Synchronizer for StreamK-WS solutions, gated by env
  TENSILE_STREAMK_WS_RESET_SYNCHRONIZER (off by default; Fix 1 is sufficient).

Validated on gfx950/MI355X with a --streamk-ws-mode only library: per-launch
time is now flat at the physical ~4.56 ms across iters (was collapsing toward
sub-us), adaptive timing is sub-peak with us >= min_us, and --verify at iters=10
passes (norm_error ~1e-5) on WS-selecting shapes.

(cherry picked from commit 58f9489)
(cherry picked from commit 2c5a52c)
(cherry picked from commit 65c65bb)
…anch

The cherry-picked WS counter-reset fix included an optional host-side
per-launch Synchronizer memset gated on solution->sizeMapping.streamKWorkStealing.
That SizeMapping field does not exist on this branch (work-stealing is not
exposed via the hipBLASLt API / to clients here), so the host block failed to
compile (no member named 'streamKWorkStealing' in 'TensileLite::SizeMapping').

Remove the host-side block entirely. The kernel-side reset (KernelWriter.py,
emitting the StreamK kernel-end reset on the deferred-store epilogue path) is
the actual fix and is retained; it is sufficient on its own.
…-streamk-queue-work-stealing-rebased

# Conflicts:
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_emit_bigfiles_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_emit_gfx1250_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_r3_datamover_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_r3_streamk_gfx1250_char.ambr
…count

Add a NumXCD arch capability (populated in rocisa initArchCaps) mirroring
origami get_default_num_xcds. StreamK codegen now derives the work-queue
count from archCaps["NumXCD"] instead of a hardcoded 8 table; the value is
a compile-time constant and asserted to be a power of two. The host derives
the same count via origami and rejects at runtime when NUM_XCD is
non-power-of-two or does not match the arch's XCD count; the work-stealing
workspace is sized from it.

Generated assembly is byte-identical for gfx942/gfx950.
… cache-line log2

Trim over-verbose and stale StreamK work-stealing comments and docstrings;
fix the stale _WS_NUM_QUEUES_BY_ISA reference in ContractionSolution.cpp to
point at _wsQueueConstants / archCaps["NumXCD"]; clarify that gfx942 covers
both MI300X (8 XCDs) and MI300A (6 XCDs) with MI300A excluded at runtime by
the host guard; compute _WS_CACHE_LINE_LOG2 from a named 256B constant.

No logic change; emitted asm byte-identical (gfx942 and gfx950, SK4 and SK5,
StreamKWorkStealing 0 and 1).
…line size

Derive the per-queue counter stride from the hardware cache-line size (128 B
for gfx942/gfx950) via a new origami accessor
(hardware_t::get_default_cache_line_bytes), mirrored into a rocisa
CacheLineBytes arch-cap. Stride == cache-line size so each per-XCD atomic
counter occupies its own line (no false sharing), replacing the hardcoded
256 B over-alignment. Halves the per-queue stride/workspace; queue-index math
(count/mask/log2 from NumXCD) is unchanged.
@jaopaulolc
jaopaulolc requested a review from a team as a code owner July 14, 2026 14:21
… dead code

Behavior-neutral cleanup from a multi-model code review; no host logic
changes and emitted kernel assembly is byte-identical for gfx942/gfx950.

- collapse dead CacheLineBytes ternary (rocisa hardware_caps.hpp) and the
  all-128 origami get_default_cache_line_bytes switch to a plain return 128
- correct the origami hardware.hpp and ValidParameters StreamKWorkStealing
  docstrings to match the shipped single-hop next-neighbor implementation
- trim the over-verbose StreamK banner/helper docstrings, the kernelEnd
  reset comments, the Solution DebugStreamK reject comment, and DECISIONS D17
- remove session/history spill from StreamK comments and the streamk
  work-stealing YAML headers (no test params/problem sizes changed)
- delete two ghost-guarding unit tests that asserted absence of symbols from
  an abandoned in-PR design; de-session-spill remaining test docstrings
…wn XCD count and gfx1250

(1) Treat unknown/null NUM_XCD (missing analyticalHardware) as unsupported for
the dynamic-queue path and fail early rather than under-allocating the counter
workspace. streamKDynamicQueueUnsupported now returns true for unknown hardware
so the softwarePredicate excludes the dynamic-queue solution and a
non-dynamic-queue solution serves the GEMM; solve() throws an explicit error
early if the baked queue count / NUM_XCD is unknown (0) before workspace sizing.

(2) Reject StreamKWorkStealing on gfx1250, which lacks scalar atomics
(HasSAtomic=false) for the steal path (streamKWorkStealingSteal emits
s_atomic_inc with no vector fallback).

gtest (CuCount_test) and unit test (test_streamk_work_stealing) updated. No
codegen change for gfx942/gfx950 (asm byte-identical).
…-streamk-queue-work-stealing-rebased

# Conflicts:
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_emit_bigfiles_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_emit_gfx1250_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_r3_datamover_char.ambr
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/_codegen/__snapshots__/test_r3_streamk_gfx1250_char.ambr
This _codegen golden did not textually conflict during the merge (only
develop touched it, via #9227/#9217), so it auto-merged to develop's
content. But the branch's StreamKWorkStealing -> required-parameter
promotion (D17) also churns this kernel's basename hash, leaving the
golden stale. Diff is basename-hash-only; err:0 unchanged, no
instruction-count or emitted-assembly changes.
@jaopaulolc
jaopaulolc enabled auto-merge (squash) July 20, 2026 16:29
@jaopaulolc jaopaulolc changed the title feat(tensilelite): single-hop StreamK work stealing feat(tensilelite): StreamK single-hop work stealing Jul 20, 2026
…-streamk-queue-work-stealing-rebased

# Conflicts:
#	projects/hipblaslt/tensilelite/Tensile/Tests/unit/characterization/SolutionClass/__snapshots__/test_solution_class_char.ambr
@bnemanich
bnemanich disabled auto-merge July 24, 2026 20:23
@bnemanich
bnemanich merged commit f80df0c into develop Jul 24, 2026
73 of 96 checks passed
@bnemanich
bnemanich deleted the users/jolabega/dyn-streamk-queue-work-stealing-rebased branch July 24, 2026 20:24
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.

6 participants