Skip to content

feat(tensilelite): StreamK launch summary diagnostic report - #10036

Merged
jaopaulolc merged 18 commits into
developfrom
users/jolabega/streamk-launch-summary-report
Sep 1, 2026
Merged

jaopaulolc merged 18 commits into
developfrom
users/jolabega/streamk-launch-summary-report

Conversation

@jaopaulolc

@jaopaulolc jaopaulolc commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

JIRA ID : AIHPBLAS-4513

Motivation

When a GEMM runs through the StreamK launch path there is currently no way to see WHICH decisions the launch-parameter logic took for a given problem/solution: the StreamK mode (SK3/SK4/SK5, and how SK5 resolved), the reduction strategy (tree vs data-parallel), the grid actually launched (and whether a fallback shrank it), the tile/split accounting, and the workspace/DP-only path.

These decisions involve several fallbacks — fixed-grid override, tree-fixup 24-bit bounds, the StreamKForceDPOnly cluster-multicast clamp, and the workspace-insufficient DP fallback — that silently rewrite the grid and the reduction strategy. Two different sizes can land on the same kernel and still launch with different grids and different reductions, with nothing in the logs to say why. This change adds an opt-in diagnostic that surfaces those decisions.

Technical Details

Adds an observational StreamK "launch summary" printed as a deeply-indented labeled block to stderr from ContractionSolution::solve().

Gating. Printed only when TENSILE_DB bit 0x200000 is set (Debug::printStreamKLaunchSummary()), sitting alongside the existing 0x100000 StreamK-mode-selection bit. Off by default; the report is inert when the bit is unset.

Field provenance. The report is built from a StreamKDecisions snapshot populated by ContractionSolution::computeStreamKDecisions() and consumed by solve(). A diagnostic that re-derives its own numbers is worse than useless if it drifts from the launch it claims to describe, so every field in the struct is annotated as one of exactly two kinds, and the annotation is part of the struct's documentation:

  • available — read from the value the launch actually uses, or from the same helper/param solve() calls, so the field reflects the launch exactly. These are: streamKMode (sizeMapping.streamK), effectiveDynamic (streamK5EffectiveDynamic()), reduction and finalGrid/skGrid (the exact values solve() wires into StreamKSettings), tiles (problem.getNumTiles()), selectedGrid / skGridPreFallback / fixedGridUsed / treeBoundsFallbackFired / clusterDPGridClamped (getSKGridImpl() return value and out-params), forceDPOnly (sizeMapping.streamKForceDPOnly), streamKDP (Debug::useStreamKDataParrallel()), givenWorkspaceBytes (problem.workspaceSize()), and numQueues (streamKBakedQueueCount()).
  • recomputed — re-derived in computeStreamKDecisions() by mirroring the makeArgs()/solve() sizing and packing logic. These are: isDynamic (mode + effectiveDynamic), skTiles / skSplit / totalItems (the makeArgs() packing), partialsPresent (skTiles > 0), dpOnly (OR of the three DP triggers), workspaceAllocated / requiredWorkspaceBytes / idealWorkspaceBytes (the workspace sizing guard), dynamicPartialsSlots (skTiles * skSplit), and workspaceDPFallbackFired.

Recomputed fields are the ones that can drift, and they are called out as such so a future change to the packing logic has an obvious place to look. Where the mirrored arithmetic already exists as a shared helper it is called rather than copied: the static-split accounting uses streamKStaticSplit(), the same helper the SK3 and SK5-static packers use, instead of a third hand-rolled copy of that arithmetic.

requiredWorkspaceBytes in the snapshot is deliberately not the return value of ContractionSolution::requiredWorkspaceSize(), despite the similar name. That function is the separate, pre-existing, caller-facing implementation the allocator sizes from; it is untouched here. The snapshot instead mirrors the sizing guard inside solve() and evaluates it on the final, post-fallback grid, so the report describes the launch rather than the allocation request. The two encode the same reserve-or-not rule and must stay consistent about whether a workspace is needed; both sites carry a comment saying so.

Reported fields.

  • mode: SK3/SK4/SK5 plus the effective dynamic/static resolution for SK5 (e.g. SK5->dynamic(SK4)), and isDynamic.
  • grid: selected -> final with a changedBy attribution (none / workspaceDP / clusterDPMulticast / treeBounds / fixedGrid), plus the pre-fallback grid when it differs. The ladder is ordered latest-clamp-wins, so the attribution names the clamp that actually produced the final grid.
  • tiles: tiles / skTiles / skSplit / totalItems / partials.
  • workspace: the allocated flag plus required / ideal / given bytes in human units, with unbounded shown for SIZE_MAX.
  • dp-only: with the source distinguished — forceDPOnly(param) (compile-time solution parameter) vs streamKDP(debug) (debug override) vs workspaceDP(runtime) (the runtime workspace-insufficient fallback).
  • work-queue: numQueues(NUM_XCD) and dynamicPartialsSlots, which print NA (work-queues not used) on non-dynamic paths rather than misleading zeros.
  • fallbacks: the flag for each fallback that can rewrite the grid.

Cluster-multicast clamp. getSKGridImpl() has a third post-selection grid clamp, which resets skGrid = tiles for streamK == 3 with streamKForceDPOnly and clusterDim.x * clusterDim.y > 1. It runs after the tree-fixup-bounds clamp. Without explicit handling the report would show selected != final with changedBy = none — misdiagnosing precisely the situation it exists to explain — so the snapshot carries a clusterDPGridClamped field, the changedBy ladder has an entry for it, and the fallbacks: section prints a clusterDPMulticast line.

Ordering. computeStreamKDecisions() runs before solve()'s unsupported-dynamic-queue guard, because the guard consumes isDynamic from the snapshot. This is not a behaviour change: the diagnostics and asserts that this could have reordered all live in the non-dynamic else arm of getSKGridImpl(), which is unreachable whenever the guard fires; getSKReduction() is not called at all on the SK4 / SK5-dynamic path; and the 0x100000 note emitted by streamK5EffectiveDynamic() already ran ahead of the guard.

Observational only. The snapshot's reduction, skGrid and isDynamic are genuine launch inputs, but computing the snapshot and printing the summary change no launch behaviour.

Example output

The report goes to stderr, so redirect it — piping stdout shows nothing:

TENSILE_DB=0x200000 hipblaslt-bench ... 2>&1 1>/dev/null

A bare hipblaslt-bench resolves libhipblaslt.so.1 against the installed ROCm library, not a local build. To exercise a build, point the loader and the library paths at it ($HIPBLASLT_BUILD below is the hipBLASLt build directory, and <arch> the target GPU architecture):

export LD_LIBRARY_PATH=$HIPBLASLT_BUILD/library:$LD_LIBRARY_PATH
export HIPBLASLT_TENSILE_LIBPATH=$HIPBLASLT_BUILD/Tensile/library/<arch>
export HIPBLASLT_EXT_OP_LIBRARY_PATH=$HIPBLASLT_BUILD/device-library/extops

One block is emitted per launch, so -i 1 -j 0 is worth passing: with the bench defaults (-i 10 -j 2) each of the first two scenarios below emits 12 identical blocks. The captures are from gfx950 (MI355X, NUM_XCD = 8). Kernel names are elided mid-string (Cijk_Ailk_Bjlk_..._MT64x64x64_..._WGMXCCn1) for width; the rest of each block is verbatim.

The first two scenarios are a paired contrast: identical kernel, identical problem, only --streamk_tile_scheduling differs, and SK5 resolves the other way. That flag selects the SK5 tile-scheduling sub-path through the HIPBLASLT_MATMUL_DESC_STREAMK_TILE_SCHEDULING_EXT attribute and accepts off|0, on|1, auto|2. Neither needs --algo_method all; both hit their path under default heuristic selection.

Dynamic path. The per-XCD work-queue synchronizer is in use, so the work-queue fields report real counts:

$ TENSILE_DB=0x200000 hipblaslt-bench -f matmul -r bf16_r --compute_type f32_r \
    --transA N --transB T -m 256 -n 256 -k 8192 --streamk_tile_scheduling on -i 1 -j 0

TensileLite::StreamK LAUNCH SUMMARY  kernel='Cijk_Ailk_Bjlk_..._MT64x64x64_..._WGMXCCn1'
  mode:
    mode      = SK5->dynamic(SK4)
    streamK   = 5
    reduction = tree
    isDynamic = yes
  grid:
    selected    = 16
    final       = 16
    changedBy   = none
  tiles:
    tiles      = 16
    skTiles    = 0
    skSplit    = 2
    totalItems = 16
    partials   = no
  workspace:
    allocated = no
    required  = 0 (0 B)
    ideal     = 0 (0 B)
    given     = 0 (0 B)
  dp-only:
    dpOnly      = no
    source      = none
    forceDPOnly = no
    streamKDP   = no
  work-queue:
    numQueues(NUM_XCD)   = 8
    dynamicPartialsSlots = 0
  fallbacks:
    fixedGrid           = no
    workspaceDPFallback = no
    treeBoundsFallback  = no
    clusterDPMulticast  = no

Static / data-parallel path. The same kernel and shape with tile scheduling forced off resolves to SK3 with a parallel (DP) reduction. The grid goes 16 -> 256, a 4 MiB partials workspace is now reserved, and the work-queue fields are meaningless on this path so they print NA (work-queues not used):

$ TENSILE_DB=0x200000 hipblaslt-bench -f matmul -r bf16_r --compute_type f32_r \
    --transA N --transB T -m 256 -n 256 -k 8192 --streamk_tile_scheduling off -i 1 -j 0

TensileLite::StreamK LAUNCH SUMMARY  kernel='Cijk_Ailk_Bjlk_..._MT64x64x64_..._WGMXCCn1'
  mode:
    mode      = SK5->static(SK3)
    streamK   = 5
    reduction = parallel(DP)
    isDynamic = no
  grid:
    selected    = 256
    final       = 256
    changedBy   = none
  tiles:
    tiles      = 16
    skTiles    = 16
    skSplit    = 16
    totalItems = 16
    partials   = yes
  workspace:
    allocated = yes
    required  = 4194304 (4.00 MiB)
    ideal     = 4194304 (4.00 MiB)
    given     = 4194304 (4.00 MiB)
  dp-only:
    dpOnly      = no
    source      = none
    forceDPOnly = no
    streamKDP   = no
  work-queue:
    NA (work-queues not used)
  fallbacks:
    fixedGrid           = no
    workspaceDPFallback = no
    treeBoundsFallback  = no
    clusterDPMulticast  = no

A fallback rewrites the grid. With the workspace pinned to 0, a StreamK-dynamic tree-reduction solution cannot reserve its partials workspace, so the runtime workspace-insufficient DP fallback rewrites the grid: selected = 768 -> final = 1024, changedBy = workspaceDP, and dp-only source = workspaceDP(runtime). Note ideal = 12.00 MiB against given = 0 — the report shows what the launch wanted as well as what it got, which is the whole reason the fallback fired.

This scenario does need --algo_method all to enumerate solutions. It emitted 1670 blocks across all enumerated solutions, of which 461 showed changedBy = workspaceDP; the block below is from the modal (selected, final) = (768, 1024) group and was byte-identical across two independent runs.

$ TENSILE_DB=0x200000 hipblaslt-bench -f matmul -r bf16_r --compute_type f32_r \
    --transA N --transB T -m 2048 -n 2048 -k 16384 --workspace 0 \
    --streamk_tile_scheduling on --algo_method all -i 1 -j 0

TensileLite::StreamK LAUNCH SUMMARY  kernel='Cijk_Ailk_Bjlk_..._MT32x128x32_..._WGMXCC1'
  mode:
    mode      = SK5->dynamic(SK4)
    streamK   = 5
    reduction = tree
    isDynamic = yes
  grid:
    selected    = 768
    final       = 1024
    changedBy   = workspaceDP
  tiles:
    tiles      = 1024
    skTiles    = 0
    skSplit    = 2
    totalItems = 1024
    partials   = no
  workspace:
    allocated = no
    required  = 0 (0 B)
    ideal     = 12583936 (12.00 MiB)
    given     = 0 (0 B)
  dp-only:
    dpOnly      = yes
    source      = workspaceDP(runtime)
    forceDPOnly = no
    streamKDP   = no
  work-queue:
    numQueues(NUM_XCD)   = 8
    dynamicPartialsSlots = 0
  fallbacks:
    fixedGrid           = no
    workspaceDPFallback = yes
    treeBoundsFallback  = no
    clusterDPMulticast  = no

Test Plan

  • 19 new gtests in tensilelite/tests/StreamKLaunchSummary_test.cpp (suite StreamKLaunchSummaryTest, registered in tensilelite/tests/CMakeLists.txt). They cover computeStreamKDecisions() and printStreamKLaunchSummary() across StreamK modes and fallback paths: SK5 resolving dynamic and static, SK3 static partial tiles reserving a workspace, the forceDPOnly and workspace-DP DP-only sources, selected vs final grid attribution, the dynamic workspace-reservation cases (slots/divisibility matrix), the inert snapshot for non-StreamK solutions, the emitted field text, the NA (work-queues not used) non-dynamic behaviour, and four cases for the cluster-multicast clamp — that it clamps the grid to tiles, that it requires SK3 + forceDPOnly + an x*y cluster, that it is reported in the printed block, and that it wins changedBy attribution over fixedGrid.
  • Full tensilelite-tests binary, to confirm no regression elsewhere.
  • Manual capture of the three scenarios above on hardware, to confirm the report matches real launches.

Test Result

Built and run for gfx950 (MI355X, NUM_XCD = 8):

  • StreamKLaunchSummaryTest: 19 of 19 pass.
  • Full tensilelite-tests: 409 ran, 407 passed, 0 failed, 2 skipped. The two skips are pre-existing and unrelated to this change (RangeLibraryTest/RangeLibraryTest.SpecificSizes/0 and FP4ZeroFrequency/MXDataGenFP4Test.ZeroFrequencyWithinBounds/3).
  • Zero compiler warnings in any of the touched files.
  • All three scenarios above reproduce as shown.

Limits of the runtime evidence, stated plainly:

  • No captured block shows clusterDPMulticast = yes; it is no in all 1670 blocks captured. The clamp requires a streamKForceDPOnly solution (kernel token SKFDPO1), and the shipped gfx950 code-object library contains zero such kernels. The hardware runs therefore confirm the field is wired up and printing, but the unit tests are the only coverage of that clamp actually firing.
  • Likewise, no captured block shows treeBoundsFallback = yes or fixedGrid = yes; both are no throughout. Those paths are covered by unit tests only.

Submission Checklist

Risk level

Low. The report is diagnostic and observational only, and is gated off by default behind TENSILE_DB bit 0x200000. It introduces no change to StreamK launch behaviour.

… self-reset (WGMXCC + SKXCC)

The per-XCD StreamK work-queue atomic counter is designed to self-reset to
zero every launch purely by atomic wraparound: each queue q must receive
exactly tiles_q + W_q increments, where W_q = distribute(skGrid, q) assumes
the queue index densely covers [0, skGrid). Deriving the queue index from the
remapped StreamKIdx breaks that invariant, because neither the wgmXCC
CU-count remap (WorkGroupMappingXCC == -1 auto path) nor the StreamKXCCMapping
chiplet remap (SKXCC with WorkGroupMappingXCC > 1) is a
% numQueues-count-preserving permutation when the grid does not block evenly.
The skewed per-queue home-workgroup count leaves the counter drifting off zero
across launches.

Fix: derive the queue index from the raw pre-remap launch workgroup rank,
which is a dense bijection onto [0, skGrid) (== physical XCD rank) and restores
the counting invariant on both the SK4 and SK5 dynamic paths. The raw rank is
snapshotted once in the prologue, before wgmXCC and the SKXCC preLoop XCCMapping
remap rewrite WorkGroup0, into the persistent StreamKTileIdx SGPR -- an
already-allocated slot that is provably dead in the [prologue, queue-read]
window -- and read back masked % numQueues at the queue index computation. This
adds zero additional persistent SGPRs, so it also fits tuned high-register SKXCC
kernels that would overflow the SGPR file with a dedicated queue SGPR. Only
once-per-workgroup setup instructions are added; no steady-state cost.

Count-preserving regimes (fixed non-SKXCC WGMXCC == 1, WorkGroupIdFromTTM
targets, single-queue arches) keep the cheap StreamKIdx % numQueues path.
…ILE_DB 0x200000)

Add an observational diagnostic that reports the StreamK launch DECISIONS made
for a (solution, problem, hardware) triple as an indented labeled block printed
to stderr from ContractionSolution::solve(). It is gated solely by TENSILE_DB
bit 0x200000 (sitting alongside the 0x100000 StreamK-mode-selection bit) and is
off by default, so it changes no launch behaviour.

The report is built from a StreamKDecisions snapshot populated by
computeStreamKDecisions(), which is the single source of truth that solve()
itself consumes to fill StreamKSettings (reduction, finalGrid, tiles). Sourcing
the report from the same values solve() launches with means the summary cannot
drift from the real launch.

Reported fields:
- mode: SK3/SK4/SK5 plus the effective dynamic/static resolution;
- reduction: tree vs parallel (data-parallel);
- grids: selected -> final with changedBy attribution
  (none/fixedGrid/treeBounds/workspaceDP), plus the pre-fallback grid when it
  differs;
- tiles/skTiles/skSplit/totalItems and whether partials are present;
- workspace: allocated flag plus required/ideal/given bytes in human units,
  with "unbounded" shown for SIZE_MAX;
- dp-only: with the source distinguished (forceDPOnly param vs streamKDP debug
  vs runtime workspace-DP fallback);
- work-queue fields: numQueues (NUM_XCD) and dynamicPartialsSlots, which print
  "NA (work-queues not used)" on non-dynamic paths;
- the fallback flags that fired.

Covered by gtests in StreamKLaunchSummary_test.cpp.
@therock-pr-bot

therock-pr-bot Bot commented Jul 27, 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

therock-pr-bot Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

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

@jaopaulolc jaopaulolc added the rocm:gemm algos Label to identify PRs of the GEMM Algorithms team. label Jul 27, 2026
@codecov

codecov Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../hipblaslt/tensilelite/src/ContractionSolution.cpp 90.32% 12 Missing and 12 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #10036      +/-   ##
===========================================
+ Coverage    69.33%   69.36%   +0.03%     
===========================================
  Files         2811     2809       -2     
  Lines       465641   464468    -1173     
  Branches     68698    68556     -142     
===========================================
- Hits        322828   322149     -679     
+ Misses      119254   118811     -443     
+ Partials     23559    23508      -51     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 39.00% <90.32%> (+0.67%) ⬆️ Carriedforward from 6205ace
TensileLite-Unit 76.13% <ø> (ø) Carriedforward from 6205ace
hipBLAS 90.62% <ø> (ø) Carriedforward from 6205ace
hipBLASLt 35.22% <ø> (ø)
hipCUB 82.68% <ø> (ø) Carriedforward from 6205ace
hipDNN 86.85% <ø> (-0.04%) ⬇️ Carriedforward from 6205ace
hipFFT 44.02% <ø> (+0.23%) ⬆️ Carriedforward from 6205ace
hipRAND 76.12% <ø> (ø) Carriedforward from 6205ace
hipSOLVER 69.03% <ø> (+0.11%) ⬆️ Carriedforward from 6205ace
hipSPARSE 86.99% <ø> (ø) Carriedforward from 6205ace
rocBLAS 48.22% <ø> (-0.06%) ⬇️ Carriedforward from 6205ace
rocFFT 46.60% <ø> (ø) Carriedforward from 6205ace
rocRAND 56.91% <ø> (ø) Carriedforward from 6205ace
rocSOLVER 77.28% <ø> (+0.45%) ⬆️ Carriedforward from 6205ace
rocSPARSE 74.62% <ø> (+0.01%) ⬆️ Carriedforward from 6205ace
rocThrust 91.60% <ø> (ø) Carriedforward from 6205ace

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

Files with missing lines Coverage Δ
.../hipblaslt/tensilelite/src/ContractionSolution.cpp 14.27% <90.32%> (+6.30%) ⬆️

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

Replace the general "single source of truth / cannot drift / observational"
prose on StreamKDecisions with precise per-field provenance: each field now
states whether it is available (read from the value the launch uses, or the
same helper/param solve() uses) or recomputed (re-derived by mirroring the
makeArgs/solve() sizing and packing logic).
Conflict resolution:

- Tensile/Components/StreamK.py, Tensile/KernelWriter.py,
  Tensile/KernelWriterAssembly.py and Tensile/Tests/unit/test_streamk_queue_index.py
  are resolved to develop. The branch carried an earlier version of the
  count-preserving per-XCD StreamK work-queue counter fix from when this was a
  stacked PR; an equivalent fix has since landed independently on develop
  (#10008). That work is out of scope for this PR, so develop's landed version
  wins and these files are byte-identical to develop.

- include/Tensile/ContractionSolution.hpp keeps both sides: develop's
  CustomKernel, StreamKStaticSplit / streamKStaticSplit() /
  streamKStaticSplitRowUniform(), UniformSummationOrderError, the four-element
  WGMParamsCache key and the checkUniformSummationOrder() declaration, plus this
  branch's StreamKDecisions struct and the computeStreamKDecisions() /
  printStreamKLaunchSummary() declarations.

Post-merge mirror fixes in src/ContractionSolution.cpp, so the diagnostic
report still describes the launch it observes (report-only; no launch
behaviour changes):

- getSKGridImpl() gained a StreamKForceDPOnly cluster-multicast clamp (#10189)
  that runs after the tree-fixup-bounds fallback and also resets the grid to
  the tile count. The summary had no way to attribute that clamp, so a launch
  taking it reported "changedBy = none". Added a clusterDPGridClamped out-param
  and StreamKDecisions field, reported under "fallbacks" and in the changedBy
  attribution (ordered latest-clamp-wins).

- The static-split mirror now calls the shared streamKStaticSplit() helper
  develop factored out, instead of repeating its skTiles arithmetic inline.
  makeArgs() packs skTiles from that same helper, so the report can no longer
  drift from the kernel arguments.
The ForceDPOnly cluster-multicast clamp in getSKGridImpl() -- SK3 +
streamKForceDPOnly + clusterDim.x*clusterDim.y > 1 resetting the grid to
tiles -- had no unit coverage. Add four cases:

  * the clamp resets the grid to tiles and is reflected in the snapshot,
    with getSKGrid() cross-checked against the reported final grid;
  * the three-way conjunction is a real guard (SK4, non-force-DP SK3, and
    a z-only cluster each leave the grid alone);
  * the printed summary names the clamp, credits it for the
    selected-vs-final grid change, and surfaces preFallback;
  * changedBy credits the clamp over an skFixedGrid override that the
    clamp subsequently overwrote, so the report never names a grid the
    launch did not use.

The clamp reads only sizeMapping, so all four run on the existing mock
AMDGPU with no analytical hardware and no gfx1250 device.

Also tighten a few existing assertions and comments:

  * replace a totalItems assertion that restated the implementation
    character-for-character with the concrete literals the fully
    determined fixture produces (1792);
  * check all four fallback flags in the workspace-DP attribution test
    now that clusterDPGridClamped exists;
  * turn an if(workspaceAllocated)-guarded workspace sizing check into
    ASSERT_TRUE plus an unconditional check so it cannot silently vanish;
  * pin d.tiles and d.skGridPreFallback to literals rather than only to
    the same helpers the implementation calls;
  * correct the ForceDPOnly comment, which claimed the param never resets
    the grid to tiles -- true only because the fixture leaves clusterDim
    at 1;
  * restate the dynamic-path workspace guard in full (parallel OR
    tiles%grid != 0) and note why the fourth combination is not tested;
  * rewrite the comments that read as a diff against an earlier revision,
    and rename NonStreamKProducesEmptySnapshot ->
    NonStreamKProducesInertSnapshot and Sk3StaticPartialTiles ->
    Sk3StaticPartialTilesReserveWorkspace.
The workspace / DP-fallback logic now lives in computeStreamKDecisions()
rather than solve(), but a dozen comments still described themselves as
mirroring solve(). There is nothing left in solve() to mirror: it copies
the reduction, grid and isDynamic predicate straight out of the snapshot
and does no further StreamK sizing.

Point those comments at the real drift peer instead. requiredWorkspaceSize()
is a second, independent implementation of the same reserve-or-not rule and
is what the caller allocates from, yet it already differs: it always uses
getSKReduction() and sizes parallel reduction with requiredWorkspaceSizeGsu()
rather than partialTileSize(). Note explicitly that
StreamKDecisions::requiredWorkspaceBytes is NOT that function's return value
despite the near-identical name.

Other corrections:

  * skTiles/skSplit/totalItems were documented with a formula that is
    false on the parallel and static paths -- the SK3 ABI has no
    TotalItems arg, and the parallel path packs the split factor into the
    skTiles slot. Document all three paths.
  * "It has no side effects" in solve() was wrong: computeStreamKDecisions()
    must precede the dynamic-queue guard because the guard consumes
    isDynamic, and the helpers it calls can write TENSILE_DB diagnostics.
    Record what is and is not reachable on a guard-rejected launch.
  * The SIZE_MAX -> "unbounded" sentinel is ContractionProblem's default
    workspace size (setWorkspaceSize() never called), not something the
    bench client passes.
  * The print-layout comment claimed a format was "preserved verbatim" for
    existing log scrapers; the function is new, so describe the output
    shape and which tokens are load-bearing instead.
  * Restore "Only these three fields are refreshed" in
    reloadDebugBitsForTest()'s doc -- it still refreshes exactly three.
  * Document that effectiveDynamic is SK5-only (false for SK4), that
    isDynamic is a launch input, why finalGrid and skGrid both exist, the
    dpOnlySource ordering, and the (void)problem parameter retention.
@jaopaulolc
jaopaulolc marked this pull request as ready for review August 27, 2026 21:17
@jaopaulolc
jaopaulolc requested review from a team as code owners August 27, 2026 21:17
@jaopaulolc
jaopaulolc enabled auto-merge (squash) August 30, 2026 01:53
@jaopaulolc
jaopaulolc disabled auto-merge August 30, 2026 01:54

@Alex-Vasile Alex-Vasile 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.

Approving CMakeLists changes.

… default

Three consistency fixes around the StreamK launch summary:

- skGridPreFallback documented the workspace-DP fallback as living in
  solve(); it is applied in computeStreamKDecisions(), which is what the
  workspaceDPFallbackFired doc a few lines below already says.

- finalGrid's list of the fallbacks it accounts for omitted the
  ForceDPOnly cluster-multicast clamp. It now names all four clamps in
  application order using the same names the report's "changedBy" ladder
  prints, so the doc and the ladder can be compared directly.

- The static-split path in computeStreamKDecisions() fell back to
  skFullTiles=0 when the AMDGPU downcast yielded null. AMDGPU::skFullTiles
  defaults to 1, and checkUniformSummationOrder() already uses 1 for the
  same fallback. The path is not reachable today (getSKGridImpl asserts
  and dereferences the same pointer first), so this only keeps the
  constant consistent with its two siblings.

No behavioural change.
The StreamK launch-summary tests left four behaviours unasserted. Add five
tests that close them:

- Sk3ParallelReductionReservesPartialsWorkspace: the parallel (split-K DP)
  reduction, which no existing test reached. A 256x4096x4096 SK3 launch on the
  256-CU analytical mock lands in select_reduction's parallel band, so the
  snapshot's packing (skSplit = grid/tiles) and its unconditional partials
  reservation are pinned.
- Sk3ParallelReductionWorkspaceStarvedFallsBackToTree: the same shape with no
  workspace, demoting to tree with grid = tiles while the workspace-DP fallback
  is still credited even though the grid number did not move.
- LaunchSummaryDebugBitIsOffByDefault: the summary is opt-in. Asserts the
  Debug predicate is clear with TENSILE_DB unset, set by 0x200000, not set by
  the neighbouring 0x100000 mode-selection bit, restored to the ambient value
  afterwards, and that building the snapshot writes nothing to stderr.
- TreeBoundsFallbackWinsGridAttribution and FixedGridOverrideWinsGridAttribution:
  the two remaining changedBy attributions, each with a complementary run
  showing the grid is left alone when the clamp does not apply.

Adds a ScopedTensileDb RAII guard that copies the prior TENSILE_DB value
before setenv invalidates it and restores that value on scope exit, since the
Debug singleton is process-wide. The four decision tests assert
TENSILE_STREAMK_DATA_PARALLEL is clear, because it is latched at Debug
construction and would otherwise change the answers silently.

Also correct the StreamKDecisions::requiredWorkspaceBytes comment. It claimed
the snapshot and requiredWorkspaceSize() must stay consistent about whether a
workspace is needed; they do not. Parallel reduction with a grid equal to tiles
gives a k-split of 1, requiredWorkspaceSizeGsu() short-circuits to 0 for a
split of 1, and the snapshot still reserves partialTileSize(tiles). The comment
now describes that case, why it is an over-reservation rather than an
under-allocation, and why the allocate-then-launch loop settles at a
self-consistent fixed point.
@jaopaulolc
jaopaulolc merged commit 5139a61 into develop Sep 1, 2026
79 of 82 checks passed
@jaopaulolc
jaopaulolc deleted the users/jolabega/streamk-launch-summary-report branch September 1, 2026 15:44
jaopaulolc added a commit that referenced this pull request Sep 1, 2026
…eamk-per-tile-extra-iters

Three files conflicted, all from develop-side StreamK work that landed in
this window: #10036 (StreamK launch summary diagnostic), #10951 (multi-stream
Stream-K Synchronizer fix) and #10925 (fused all-to-all epilogue).

Debug.hpp / Debug.cpp: both sides claimed TENSILE_DB bit 0x200000. #10036 is
merged and its bit is documented and exercised by StreamKLaunchSummary_test,
so it keeps 0x200000; the uniform-summation-order no-solution report moves to
0x400000, the next free bit. Both diagnostics remain independently switchable.

ContractionSolution.cpp, getSKGridImpl(): #10036 added out-params recording
which post-selection fallback rewrote the grid, and captures the selected grid
between the CU/override ladder and the tree-fixup bounds clamp -- the same
point the uniform-summation-order F-star snap is inserted at. The snap now runs
first and the capture follows it, because the snap is grid selection under
uniform summation order rather than a fallback: it has no changedBy attribution,
so capturing ahead of it would report selected != final with changedBy = none,
the unattributed rewrite the out-params exist to prevent. #10951's flag-region
clamp stays last, immediately before the return, so the launch summary's grid
ordering is unchanged.

ContractionSolution.cpp, solve(): #10036 extracted the reduction / grid /
workspace-DP-fallback logic into computeStreamKDecisions(); this branch had
extracted the same logic into resolveStreamKSettings(). resolveStreamKSettings()
remains the source of truth for what is launched, since it additionally applies
streamKReconcileReduction() so requiredWorkspaceSize() and the launch agree on
the reduction. computeStreamKDecisions() now applies that same reconciliation at
the same point, before the workspace-fit fallback, so the snapshot cannot report
a fallback the launch did not take; solve() re-asserts the final reduction
alongside the final grid before printing.
caio96 added a commit that referenced this pull request Sep 2, 2026
…exes (#11515)

#10951 gives Stream-K its own flag buffer — a private 2048-int block per
(stream, problem) — and adds the first bound on `skGrid` against that
block. The bound assumes all 2048 entries are addressable as flags, but
`StreamK=4` and the SK4 sub-path of `StreamK=5` place the per-XCD
work-queue counters at the *base* of that same region and start the
ready flags after them. On gfx950 that prefix is 8 XCDs × 128 B = 256
ints, leaving 1792 indexable. A grid between 1793 and 2048 has its last
workgroups write past the end of their own block; blocks are laid out as
adjacent problem slots within a stream, so the overrun lands on the next
problem's flags, or on the next stream's from the last slot of a block.
The prefix is XCD-count × cache-line, so it varies by ISA and cannot be
folded into the constant.

Default grids stay well inside the bound (`skGrid` defaults to the CU
count, 256 on gfx950), but `TENSILE_STREAMK_GRID_MULTIPLIER` scales it
by the CU count with no cap of its own, so the bound is reachable
through a supported tuning knob.

Separately, the host reserved those same queue-region bytes in the
**workspace** — a different buffer. The kernel takes them from
`AddressFlags`, so the reservation was unread.

## Technical Details

**Bound the grid by the entries the kernel can actually index.**
`getSKGrid` subtracts the queue-counter prefix for the dynamic-queue
paths and keeps the full region for the static ones, which index from
offset 0 and would otherwise lose grid they are entitled to. Two
fallbacks keep the full bound: an architecture whose XCD count cannot be
determined, and a prefix that would swallow the region.

**Stop reserving the queue region in the workspace.** `AddressWS`
appears exactly once in codegen (`computeWorkspaceSrd`) and applies only
`MacroTile0*MacroTile1*bpe * partialIdx` — no queue-region base offset —
while every counter address is built from `AddressFlags`
(`StreamK.py:642`, `:3640`, `:4436`). Removing it makes the reported
workspace requirement match what the kernel writes.

**Deduplication and comment fixes.** `streamKQueueRegionBytes()` and
`streamKUsesDynamicQueue()` replace expressions open-coded at three
sites each. The flag-region comments claimed indexing by CTA id, which
holds only for the static paths. The grouped uniform-summation-order
comment named a grid-divisibility check, but the rejection that actually
fires is "resolved StreamK grid is 0". A user-facing error message also
described sizing a workspace region this PR removes.

Rebased over #10036, which moved this logic into
`computeStreamKDecisions()`; the changes now land there and in
`requiredWorkspaceSize()`.

## Test Plan

New unit tests in `tensilelite/tests/CuCount_test.cpp` pin the bound for
each path — SK4, SK3, and both SK5 sub-modes — reusing the existing
`StreamK5AnalyticalEnv` scaffolding. Built for gfx950 and run on MI355X,
plus a client-level Stream-K sweep as a regression guard on solution
selection.

## Test Result

`tensilelite-tests`: `StreamKFlagBound*` 4/4,
`StreamK5WorkspaceRegressionTest*` 3/3, `CuCount*` 7/7; full binary 444
passed, 2 skipped, 0 failed. `hipblaslt-test
--gtest_filter='*StreamK*'`: 33/33, no skips. Both skips are
pre-existing and unrelated.

A GPU probe confirmed the workspace change: a dynamic-queue launch
succeeds at exactly `partialTileSize` and is rejected 1024 B below it,
so the requirement dropped by exactly the queue region and the kernel is
correct at the new minimum.

JIRA ID : AIHPBLAS-4624
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

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