Skip to content

feat(tensilelite): reusable ClusterLoad component and tri-state Multicast#9599

Open
jaopaulolc wants to merge 7 commits into
developfrom
users/jolabega/clusterload-tristate-multicast
Open

feat(tensilelite): reusable ClusterLoad component and tri-state Multicast#9599
jaopaulolc wants to merge 7 commits into
developfrom
users/jolabega/clusterload-tristate-multicast

Conversation

@jaopaulolc

@jaopaulolc jaopaulolc commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

JIRA ID : AIHPBLAS-3929

Motivation

The TDM multicast ("cluster load") mask machinery was duplicated inline across KernelWriter, KernelWriterAssembly, and Subtile/SubtileGREmit, and Multicast was implicitly coupled to ClusterDim != [1, 1]. That coupling made it impossible to compose barrier-only clustering and cooperative-load clustering independently, or to reuse the mask attach at a new load site. This PR extracts that machinery into a reusable component and decouples the multicast opt-in from ClusterDim via a tri-state parameter.

This is PR (i) of a stacked series ("reusable ClusterLoad component + tri-state Multicast"): a byte-exact foundation with no new StreamK cluster feature. It supersedes the earlier #9570 (recreated on a clean, rebased single-commit branch).

Technical Details

  • Reusable ClusterLoad component (Components/ClusterLoad.py, ClusterLoadTDM; registered via Component.py + Components/__init__.py). It centralizes the mask value computation (computeMasks), the MulticastMask* SGPR declare/undeclare (declareSgprs / undeclareSgprs), the combined-vs-split A/B topology decision (usesCombinedMask), and the descriptor attach at each load site (applyToDescriptor). It is a byte-exact TDM cluster-load extraction: every method emits assembly identical to the previous inline code, and computeMasks receives the exact SGPR operands the caller already holds so register indices are unchanged. Selection is capability-based (HasTDM + TDMInst == 3), like TensorDataMoverLoad. It contains no StreamK-cluster symbols (grep -c StreamKMulticast == 0).
  • KernelWriter / KernelWriterAssembly / SubtileGREmit rewired to route mask declare, undeclare, compute, and descriptor attach through ClusterLoadTDM.find(...).
  • Tri-state Multicast (ValidParameters = [-1, 0, 1], GlobalParameters default -1): -1 = auto (legacy: int(ClusterDim != [1,1] and StreamK == 0)), 0 = force off, 1 = force on. Multicast is now an int end-to-end (derivation emits 0/1). A pre-load coercion (coerceLegacyMulticastType, scoped to the serialized-state loading path) normalizes the legacy serialized bool (false→0 / true→1) for already-shipped library-logic YAMLs, so the strict type gate and emitted msgpack both see an int (avoids a std::bad_cast at C++ deserialization). Default -1 reproduces the historic ClusterDim-coupled derivation exactly, so every existing YAML (which omits Multicast) derives byte-identically.
  • Multicast is excluded from the kernel name (SolutionStructs/Naming.py discards it): it is a derivation/runtime knob that does not alter emitted assembly, so kernel names stay byte-identical to develop (no _M token) and all _codegen assembly goldens are unchanged.

Test Plan

Ran the previously-affected characterization suites, the full _codegen assembly golden suite, and the new unit gates (CPU-only, gfx942/gfx1250 fixtures):
SolutionArms, SolutionClass, ValidParameters, test_PrefetchAcrossPersistent.py, characterization/_codegen, test_cluster_load_component.py, test_multicast_legacy_coercion.py, test_multicast_tristate.py.

Test Result

All green: ≈416 passed, 100 _codegen snapshots byte-identical (kernel names unchanged). Develop-owned goldens/tests touched are purely the intentional new int Multicast param:

  • SolutionArms char asserts (is True/False== 1/0) plus _reset restoring the -1 auto sentinel so re-derivation exercises the legacy path.
  • ValidParameters roster/structure snapshots regenerated (purely +Multicast: [-1, 0, 1]).
  • One test-only mock gap fixed in test_PrefetchAcrossPersistent.py (asmCaps HasTDM / kernel TDMInst on the PAP-TDM writer mock so ClusterLoadTDM.find() matches).

FFM: the component is exercised by non-StreamK cluster-load configs (gemm/gfx12/tdm_multicast_gfx1250.yaml, sparse/gfx1250/spmm_tdm_multicast.yaml) — asm-confirmed multicast masks, clean.

gfx1250: (gemm/gfx12/tdm_multicast_gfx1250.yaml, sparse/gfx1250/spmm_tdm_multicast.yaml pass.

Submission Checklist

Risk level

Low. Behavior-preserving refactor plus an opt-in Multicast parameter that defaults to -1 (legacy auto); Multicast is excluded from the kernel name, so emitted assembly and kernel names are byte-identical to develop for all existing configs.

Update — 64-bit workspace slot-offset fix (applies to the StreamK cluster/multicast path; HW validation pending)

Added the 64-bit workspace slot-offset computation to the shared computeWorkspaceSrd helper so every StreamK path that addresses the partial-sum workspace — including this PR's cluster/tri-state-multicast path and non-cluster StreamK — emits a 64-bit offset instead of a 32-bit one. (This mirrors the now-universal fix landed on the stacked PRs #9611/#9612 and #9603; on this branch the 64-bit hunk did not previously exist, so it is added here, un-guarded.)

Reachability note: although this PR is described as the cluster-load / tri-state-multicast foundation, its Tensile/Components/StreamK.py retains the full StreamK partials path — computeWorkspaceSrd is called from six sites (partials write, deferred write, and owner fixup read), and the branch ships a StreamK cluster designed config + test_streamk_cluster_gfx1250_char.py that emit it. So the 32-bit workspace-address overflow is genuinely reachable here (not pure infra), and the fix is applied rather than skipped.

Motivation — the overflow

computeWorkspaceSrd set SrdWS = AddressWS + offBytes * sPartialIdx with a 32-bit s_mul_i32, where offBytes = MacroTile0*MacroTile1*bpe (per-slot tile stride) and sPartialIdx ∈ [0, skGrid); the partials workspace is partialTileSize = offBytes * skGrid (host ContractionSolution::partialTileSize). For a large SK grid the product exceeds 2³², wraps, and the peer-write / owner-read SRD aliases the wrong workspace slot. It depends only on the tile stride and slot count, not the cluster mode. Concrete example: a 256×256 tile with fp32 partials is 256 KiB/slot, so skGrid ≳ 2³²/262144 = 16384 slots overflows a 32-bit offset (cluster C multiplies the effective slot count).

Technical Details

Compute the high word with s_mul_hi_u32 and fold it (plus the lo-add carry) into SrdWS+1. Emitted for all StreamK paths; adds one SGPR + one s_mul_hi_u32 per workspace-SRD setup.

Test Result (CPU, single-process; reused main worktree venv + editable rocisa .so)

  • Unit + char (streamk / cluster suites): 166 passed, 4 snapshots passed. The StreamK cluster {basename, err} char golden is unchanged and still err == 0 (a {basename, err} digest does not move when only instructions are added), so no snapshot was regenerated.
  • Confirmed the emitted cluster kernel now carries the 64-bit offset (s_mul_hi_u32 … // partials tile offset (high word) for 64-bit SRD folded into SrdWS+1).
  • Config zero-skip enumeration (gfx1250): streamk_cluster.yaml 1/1, streamk.yaml 1/1 kernels → err == 0, 0 DID_NOT_SATISFY_ASSERTS, 64-bit SRD present in every kernel.

FFM/functional-sim is historically green regardless of HW correctness and is treated as a regression check only. Real gfx1250 hardware validation is still pending (owner: user).

@therock-pr-bot

therock-pr-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

✅ All Policy Checks Passed

Check Status Details
🌿 Branch Name ✅ Pass
📝 PR Title/Description ✅ Pass
Forbidden Files ✅ Pass
🧪 Unit Test ✅ Pass
🚫 Draft PR 🔜 To Be Enabled
🚩 Feature Flag 🔜 To Be Enabled
📊 Code Coverage 🔜 To Be Enabled

🎉 All policy checks passed!

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

@jaopaulolc jaopaulolc added the rocm:gemm algos Label to identify PRs of the GEMM Algorithms team. label Jul 20, 2026
@therock-pr-bot

Copy link
Copy Markdown

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

@codecov-commenter

codecov-commenter commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
...lelite/Tensile/Components/Subtile/SubtileGREmit.py 0.00% 3 Missing ⚠️
...blaslt/tensilelite/Tensile/KernelWriterAssembly.py 80.00% 0 Missing and 2 partials ⚠️
...lt/tensilelite/Tensile/SolutionStructs/Solution.py 86.67% 1 Missing and 1 partial ⚠️
...aslt/tensilelite/Tensile/Components/ClusterLoad.py 98.81% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #9599      +/-   ##
===========================================
+ Coverage    69.50%   69.65%   +0.15%     
===========================================
  Files         2770     2765       -5     
  Lines       452498   451197    -1301     
  Branches     66620    66464     -156     
===========================================
- Hits        314492   314270     -222     
+ Misses      117526   116662     -864     
+ Partials     20480    20265     -215     
Flag Coverage Δ *Carryforward flag
TensileLite 34.35% <ø> (+0.04%) ⬆️ Carriedforward from 47df1fb
TensileLite-CPP 38.09% <ø> (ø)
TensileLite-Unit 64.80% <93.39%> (+0.03%) ⬆️
hipBLAS 90.62% <ø> (ø) Carriedforward from 47df1fb
hipBLASLt 34.63% <ø> (ø)
hipCUB 82.68% <ø> (ø) Carriedforward from 47df1fb
hipDNN 86.34% <ø> (-<0.01%) ⬇️ Carriedforward from 47df1fb
hipFFT 48.90% <ø> (+1.56%) ⬆️ Carriedforward from 47df1fb
hipRAND 76.12% <ø> (ø) Carriedforward from 47df1fb
hipSOLVER 69.18% <ø> (ø) Carriedforward from 47df1fb
hipSPARSE 86.27% <ø> (ø) Carriedforward from 47df1fb
rocBLAS 47.95% <ø> (ø) Carriedforward from 47df1fb
rocFFT 48.51% <ø> (+1.95%) ⬆️ Carriedforward from 47df1fb
rocRAND 57.01% <ø> (ø) Carriedforward from 47df1fb
rocSOLVER 76.80% <ø> (-0.12%) ⬇️ Carriedforward from 47df1fb
rocSPARSE 72.49% <ø> (-0.09%) ⬇️ Carriedforward from 47df1fb
rocThrust 91.36% <ø> (ø) Carriedforward from 47df1fb

*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% <ø> (ø)
...aslt/tensilelite/Tensile/Common/ValidParameters.py 100.00% <ø> (ø)
...rojects/hipblaslt/tensilelite/Tensile/Component.py 95.56% <100.00%> (+0.03%) ⬆️
...pblaslt/tensilelite/Tensile/Components/__init__.py 100.00% <ø> (ø)
...ects/hipblaslt/tensilelite/Tensile/KernelWriter.py 82.76% <100.00%> (-0.25%) ⬇️
...aslt/tensilelite/Tensile/SolutionStructs/Naming.py 96.97% <100.00%> (+0.02%) ⬆️
...aslt/tensilelite/Tensile/Components/ClusterLoad.py 98.81% <98.81%> (ø)
...blaslt/tensilelite/Tensile/KernelWriterAssembly.py 80.12% <80.00%> (+1.53%) ⬆️
...lt/tensilelite/Tensile/SolutionStructs/Solution.py 76.86% <86.67%> (+0.06%) ⬆️
...lelite/Tensile/Components/Subtile/SubtileGREmit.py 70.65% <0.00%> (-0.12%) ⬇️

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

…ulticast

JIRA ID: AIHPBLAS-3929

The TDM multicast ("cluster load") mask machinery was duplicated inline across
KernelWriter, KernelWriterAssembly, and Subtile/SubtileGREmit, and Multicast was
implicitly coupled to ClusterDim != [1, 1]. That coupling made it impossible to
compose barrier-only clustering and cooperative-load clustering independently, or
to reuse the mask attach at a new load site.

- New ClusterLoad component (Components/ClusterLoad.py, registered via
  Component.py + Components/__init__.py). ClusterLoadTDM centralizes the mask
  value computation (computeMasks), the MulticastMask* SGPR declare/undeclare
  (declareSgprs / undeclareSgprs), the topology decision (combined vs split
  A/B masks), and the descriptor attach at each load site (applyToDescriptor).
  It is a behavior-preserving extraction: every method emits byte-identical
  assembly to the previous inline code, and computeMasks receives the exact
  SGPR operands the caller already holds so register indices are unchanged.
  Selection is capability-based (HasTDM + TDMInst == 3), like TensorDataMoverLoad.
- KernelWriter / KernelWriterAssembly / SubtileGREmit now route mask declare,
  undeclare, compute, and descriptor attach through ClusterLoadTDM.find(...).
- Tri-state Multicast parameter (ValidParameters + GlobalParameters default -1):
  -1 = auto (legacy: ClusterDim != [1,1] implies Multicast, minus the StreamK
  cluster paths), 0 = force off, 1 = force on. Default -1 reproduces the historic
  derivation exactly, so every existing YAML (which omits Multicast) derives
  byte-identically.
- Solution.py derives Multicast/ClusterBarrier from the tri-state, keying the
  legacy auto coupling and the ClusterBarrier gate on StreamK == 0.
- Common/Utilities.clusterEnabled() helper for the ClusterDim != [1,1] test.

- Unit: test_cluster_load_component.py (component find/declare/compute/attach),
  test_multicast_tristate.py (-1/0/1 derivation, legacy equivalence).

Low. Behavior-preserving refactor; the tri-state defaults to -1 (legacy auto),
so emitted assembly is unchanged for all existing configs.
@jaopaulolc
jaopaulolc force-pushed the users/jolabega/clusterload-tristate-multicast branch from 30bbc1c to 4e3c6bd Compare July 22, 2026 17:28
Condense the verbose module/method docstrings on the reusable ClusterLoad
component to concise summaries. Behavior-preserving: no code change, unit tests
unchanged.
Add unit coverage for the previously-untested enableTDMMetadata path in the
ClusterLoad component: computeMasks Sparse==1 (follows-A) / Sparse==2 (follows-B)
metadata mask emission and undeclareSgprs metadata SGPR free.
The shared computeWorkspaceSrd helper addressed the StreamK partial-sum
workspace with a 32-bit SMulI32 slot*stride product. The per-slot stride
is MacroTile0*MacroTile1*bpe and the addressed slot index ranges over the
StreamK slot count (the partials workspace is partialTileSize ==
tileSize * skGrid, host ContractionSolution::partialTileSize), so for a
large SK grid the product exceeds 2^32, silently wraps, and the peer
write / owner read SRD aliases the wrong workspace slot.

The overflow depends only on the tile stride and the slot count, not on
the cluster mode, so the cluster-multicast [C,1] path -- which emits and
reads this exact workspace via partialsWriteProcedure / fixup -- can
overflow on large problems just like any other StreamK path. Compute the
high word with SMulHIU32 and fold it (plus the lo-add carry) into
SrdWS+1, emitted universally for every StreamK path (multicast,
non-cluster, and any future cluster mode). Adds one SGPR + one
s_mul_hi_u32 per workspace-SRD setup; the {basename, err} char goldens
are unchanged and still emit err==0. Real-HW validation pending
(owner: user).

JIRA ID : N/A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants