Skip to content

feat(hipblaslt): wire the fused GEMM + all-to-all epilogue into the launch path - #11413

Open
DDEle wants to merge 9 commits into
developfrom
users/yiding12/gemm-a2a-api
Open

DDEle wants to merge 9 commits into
developfrom
users/yiding12/gemm-a2a-api

Conversation

@DDEle

@DDEle DDEle commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

JIRA ID : ROCM-27524

Depends on #11342.

Motivation

The public host API and the kernels for a fused GEMM + all-to-all epilogue are both in place.
This PR joins them: it carries the A2A attributes from the C-API layer down to the launch path,
builds the per-rank kernarg groups the kernel reads, marks the Tensile problem so that A2A
solutions survive selection, and opens validate_fused_a2a_launch so a validated request
reaches the heuristic.

Measuring that path takes more than one process. hipblaslt-bench drives a single device, and
the collective exists only across a group of ranks. This PR therefore also adds
hipblaslt-bench-a2a, a benchmark that runs the fused path across such a group, and ships the
gfx950 kernel-selection entry it resolves against.

Technical Details

Descriptor. The fused-epilogue descriptor is resolved into a POD at the C-API layer, where
its fields are visible, and carried on RocblasltContractionProblem together with the handle's
communicator view. The checks that can run before the heuristic live in
rocblaslt_fused_a2a_validate.hpp.

Kernarg groups. buildFusedA2APeerFields turns the resolved epilogue into the per-rank
groups — flag, recv, and the four SDMA queue words per peer — indexed by Tensile's
FusedA2APeerSlot, so that slot order stays a single definition. The launch path fills rank,
drain, and the counter, which comes from the Synchronizer the handle already owns.

Selection. ConstructTensileProblem and updateTensileProblem set fusedGemmA2A and the
world size. What the opened placeholder stood for is enforced in two places: the
shape-independent half — MacroTile0/1 in {128, 256}, data-parallel only, no split-K, a BF16 D,
gfx94x/95x — as a generation-time solution rejection in Solution.py, and the
problem-dependent half as the FusedA2ATileDivisible predicate, which requires
extent % (MacroTile0 * world) == 0 and a non-zero world. Both also filter the
size-independent getAllSolutions fallback.

Repeat launches. The kernel re-arms the counter and flag blocks it consumed before it
retires, so the next launch on the same handle starts from zero without extra dispatches on the
caller's stream. The work-group already elected for the DRAIN barrier does the clears: receive
flags after the receive poll, the outbound counter after the send poll, SDMA cursors on the way
out. The cursor clear writes through (glc); s_store otherwise lands in the per-CU scalar
cache and the next launch raises its cursors off a stale value.

Completion. HIPBLASLT_A2A_COMPLETION_IN_KERNEL_FULL carries the number the kernel's drain
mask already uses for both directions, so the kernel does not retire until this rank's receive
buffer is populated and its engines have finished reading D. Synchronizing the caller's own
stream is therefore enough to reuse D. It is not enough to reuse a communicator channel
(HIPBLASLT_FUSED_EPILOGUE_COMM_CHANNEL) or the receive buffers: both need every rank's
previous operation on that channel to have retired, which no rank can establish from its own
stream.

Build gate. All of the above sits behind HIPBLASLT_ENABLE_GEMM_A2A_FUSION (default OFF),
matching the gate introduced in #11342.

Gates. A request naming no SDMA queue set is reported as no match, since every fused A2A
solution in the tree emits SDMA packets unconditionally. The heuristic reports it as zero algos
rather than an error, matching what that function already does when selection yields nothing,
and hipblasLtMatmul applies the same gate for a caller holding an algo from elsewhere.

Scope. hipblaslt_ext::Gemm rejects the A2A stage. That path has no launch-time reset and
no place to check the per-rank arrays against the world size, so it is left for a follow-up.

Benchmark. hipblaslt-bench-a2a measures the fused path across a group of ranks started by
an external launcher. Rank identity and the coordinating rank's address come from the RANK,
WORLD_SIZE, LOCAL_RANK, MASTER_ADDR and MASTER_PORT variables that PyTorch's torchrun
sets; with none of them set the run is single-rank. Ranks exchange inter-process memory handles
and receive-buffer addresses over a TCP rendezvous rather than MPI, so the benchmark needs
nothing the library does not already need. Latency is reported as the slowest rank's.

--verify is off by default. When it is on, it runs its own pass before the timed batch, so the
device-to-host copy and the comparison stay out of the measurement. It covers the transport
rather than the GEMM: the reference is the source rank's own D, which each rank reads back out
of every peer's receive buffer.

Launcher. hipblaslt-bench-a2a-launch starts the group. It sets all five variables for
every rank, so a stale WORLD_SIZE in the caller's environment cannot turn a single-rank run
into a failed rendezvous that still exits zero. A rank exiting non-zero terminates the others,
and each failing rank is reported with its log path. CMake copies the script next to the
benchmark in the build tree and installs it into bin/, so it finds the executable beside
itself in either layout.

Shipped kernel selection. TensileLite, the kernel generator, picks a kernel per problem size
from selection files shipped with the library. The gfx950 set gains the fused GEMM + A2A entry
the benchmark resolves against, so a build with the fusion gate on needs no hand-built device
library. The entry lands in the base gfx950 tree and declares the default fallback chip
identifier, which every gfx950 part maps onto, rather than a per-part directory that would cover
one. A build without the gate drops the entry and reports the filtered count. Three problem
sizes are covered and any other size still reports no match, and the kernel recorded for each
was chosen from synthetic data rather than from a tuning run on hardware.

Two supporting changes. norm.hpp and allclose.hpp define six non-template functions
without inline, so two translation units that include either header emit the same symbols and
the link fails. Nothing hit this because each binary pulled the headers into exactly one
translation unit, and the benchmark is the first to break that. Both headers sit in
clients/common/include/, shared with hipblaslt-bench and hipblaslt-test. Separately,
FindNUMA.cmake and HsakmtLinkInterface.cmake move from tensilelite/cmake/ to the project's
cmake/, which the root CMakeLists.txt already exports, so the samples and the benchmark
resolve them without each caller extending the module path.

Test Plan

  • Host gtests — the FusedA2A* descriptor and attribute rejection matrix and the kernarg slot
    layout, plus the benchmark's host-side pieces: the rendezvous, the collective agreement, the
    Arguments layout, and the single-rank path. Runs in CI, and gives the same answer whether or
    not an A2A device library is present.
  • TensileLite characterization goldens, for the generator-side changes. Runs in CI.
  • 4-GPU end-to-end over the public API, the only layer that exercises the collective itself.
    One sample at the customer shape, parameterized over processes and ranks-per-process, so the
    same source covers a process per card, one process with a thread per card, and the mixed
    deployment that also exercises cross-process peer resolution. It checks every rank's recv
    buffer against every peer's D. hipblaslt-bench-a2a --verify makes the same comparison from
    its own launcher. Run manually, since CI has no 4-GPU runner.
  • Both settings of HIPBLASLT_ENABLE_GEMM_A2A_FUSION, since the default-OFF build is what CI
    compiles. The library and its gtest are built at every commit with the gate OFF, so the
    branch bisects. Three of the benchmark's gtests build only with the gate ON, so no lane runs
    them today.

Test Result

  • Host gtests: pass, identically with and without an A2A device library.
  • Characterization goldens: pass; emitted assembly unchanged.
  • Builds clean with the gate both OFF and ON; the library and its gtest build at every commit
    with the gate OFF.
  • 4-GPU end-to-end on gfx950: pass at 2 x 2, 4 x 1 and 1 x 4, zero mismatches on every rank, and
    over 50 back-to-back launches repeated across five runs.
  • hipblaslt-bench-a2a on gfx950 against the shipped selection entry, with no device-library
    path set: pass at 1 and 2 ranks, --verify reporting no mismatch. Larger rank counts are
    untested.

Submission Checklist

@therock-pr-bot

therock-pr-bot Bot commented Aug 28, 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.

@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #11413      +/-   ##
===========================================
+ Coverage    70.78%   70.80%   +0.02%     
===========================================
  Files         2842     2842              
  Lines       465020   465412     +392     
  Branches     68499    68572      +73     
===========================================
+ Hits        329152   329527     +375     
+ Misses      112174   112161      -13     
- Partials     23694    23724      +30     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 46.40% <ø> (-<0.01%) ⬇️
TensileLite-Unit 76.22% <100.00%> (+0.12%) ⬆️
hipBLAS 90.62% <ø> (ø) Carriedforward from 4e0b054
hipBLASLt 35.24% <ø> (ø) Carriedforward from 4e0b054
hipCUB 82.68% <ø> (ø) Carriedforward from 4e0b054
hipDNN 87.01% <ø> (ø) Carriedforward from 4e0b054
hipFFT 43.25% <ø> (ø) Carriedforward from 4e0b054
hipRAND 76.12% <ø> (ø) Carriedforward from 4e0b054
hipSOLVER 68.96% <ø> (ø) Carriedforward from 4e0b054
hipSPARSE 86.99% <ø> (ø) Carriedforward from 4e0b054
rocBLAS 48.31% <ø> (ø) Carriedforward from 4e0b054
rocFFT 51.59% <ø> (ø) Carriedforward from 4e0b054
rocRAND 57.42% <ø> (ø) Carriedforward from 4e0b054
rocSOLVER 76.83% <ø> (ø) Carriedforward from 4e0b054
rocSPARSE 74.61% <ø> (ø) Carriedforward from 4e0b054
rocThrust 93.15% <ø> (ø) Carriedforward from 4e0b054

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

Files with missing lines Coverage Δ
...slt/clients/common/include/hipblaslt_arguments.hpp 29.04% <ø> (ø)
...pblaslt/clients/common/src/hipblaslt_arguments.cpp 22.67% <ø> (ø)
...ts/hipblaslt/library/include/hipblaslt/hipblaslt.h 25.00% <ø> (ø)
...cts/hipblaslt/library/src/amd_detail/hipblaslt.cpp 63.50% <ø> (ø)
...c/amd_detail/rocblaslt/src/rocblaslt_auxiliary.cpp 68.40% <ø> (ø)
...ary/src/amd_detail/rocblaslt/src/rocblaslt_mat.cpp 83.49% <ø> (ø)
...rary/src/amd_detail/rocblaslt/src/tensile_host.cpp 41.84% <ø> (ø)
...tensilelite/Tensile/Components/GlobalWriteBatch.py 69.54% <100.00%> (+0.12%) ⬆️
...ipblaslt/tensilelite/client/src/FusedA2AClient.cpp 0.00% <ø> (ø)

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

@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from 8b62f1f to 51031ea Compare August 31, 2026 09:30
Comment thread projects/hipblaslt/library/src/amd_detail/rocblaslt/src/tensile_host.cpp Outdated
Comment on lines +2338 to +2347
{
RocblasltFusedEpilogueInfo fusedInfo;
if(rocblaslt_resolve_fused_epilogue(prob.fused_epilogue, fusedInfo)
&& fusedInfo.hasA2APrefix)
{
tensileProblem.setFusedGemmA2A(true);
tensileProblem.setFusedA2AExtent(fusedInfo.a2aExtent);
tensileProblem.setFusedA2AWorld(prob.fused_a2a_world);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Same lines in updateTensileProblem as above? Worth writing a helper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These two functions already duplicate the whole translation verbatim, so extracting just the A2A block would leave it the one factored piece in an otherwise duplicated section.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure I followed your response.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ConstructTensileProblem (lines 1827-2140) and updateTensileProblem (lines 2142-2427) are already near-copies of each other: 221 of the 314 lines in the first appear verbatim in the second, about 70%. The only structural difference is that one builds a fresh ContractionProblemGemm while the other overwrites an existing one. The fused all-to-all block is copied the same way, which follows the current style of other setters.

@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from 51031ea to c2cc716 Compare September 2, 2026 08:35
@DDEle
DDEle changed the base branch from develop to users/vivizhan/fused-epilogue-a2a September 2, 2026 08:38
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch 2 times, most recently from 0b4a779 to a0e4d53 Compare September 3, 2026 03:28
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch 2 times, most recently from 49595f5 to 2ccdbe9 Compare September 7, 2026 09:39
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from 2ccdbe9 to 55c7e0b Compare September 9, 2026 08:56
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from 55c7e0b to b458981 Compare September 10, 2026 08:25
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from b458981 to f8129ca Compare September 15, 2026 09:12
Base automatically changed from users/vivizhan/fused-epilogue-a2a to develop September 16, 2026 16:08
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from b538dc7 to 5d2021a Compare September 17, 2026 02:09
The mode documented that ordinary stream semantics cover the collective,
but its name and its value only spoke for receive completion. A caller
that synchronized its stream and then overwrote D raced this card's SDMA
engines, which may still be reading it.

Rename the value to _FULL and give it the number the kernel's drain mask
already uses for both directions, so the name and the value both say what
the mode guarantees. The tensilelite client's drain-send default follows:
leaving it off contradicted the mode it benchmarks.

COMM_CHANNEL had the same gap in the other direction: reusing a channel
needs every rank's previous operation on it to have retired, which no rank
can establish from its own stream, and the caller's recv buffers carry the
same requirement.
This is what is left of the commit that used to add the public API and the device communicator here. #11342 now provides hipblasLtSetDeviceComm, the A2A_PREFIX stage and every attribute, so the header additions, the communicator implementation and its peer-kind bookkeeping are all dropped; what remains is the rocblaslt-level plumbing that PR does not reach.

Everything about such a request that is checkable before the heuristic runs goes in a header of its own; handle.h stays limited to the handle definitions and the helpers that resolve an effective value out of handle/desc/pref.

JIRA ID : ROCM-27524
buildFusedA2APeerFields turns the resolved epilogue into the per-rank kernarg groups the kernel expects -- flag, recv and the four SDMA queue words per peer, indexed by Tensile's FusedA2APeerSlot so that order stays a single definition -- and the launch path fills rank, drain and the counter, which comes from the Synchronizer the handle already owns rather than a second allocation. FusedA2APeerFields pins that layout: the slot literals in the test are spelled out, so reordering or widening the enum fails there rather than silently downstream.

ConstructTensileProblem and updateTensileProblem set fusedGemmA2A from the resolver. Without it the FusedGemmA2A predicate filters every A2A solution out of selection, and FusedA2ATileDivisible rejects a world of zero, so the world has to be carried too.

An A2A launch consumes a counter block and a flag block that both have to start at zero, and nothing puts them back, so a second launch would find every flag already READY, wait on nothing and transfer nothing -- silently. A training loop hits that on its second call. The kernel re-arms them itself: it already elects one work-group per launch for the DRAIN barrier, and once that work-group clears a drain poll, the data that poll covers is finished with. The clears go there -- the receive flags after the receive poll, the outbound counter after the send poll, and the SDMA cursors on the way out. counter1/2/3 already wrap back to zero on their last increment and the cursors are the only part of the Synchronizer slice that does not, so the slice still returns to the pool zeroed. Re-arming in the kernel rather than with two hipMemsetAsync calls enqueued after launchKernels keeps two extra dispatches off the caller's stream.

The cursor clear needs glc. s_store lands in the per-CU scalar cache, and without writing through, the next launch raised its cursors off a stale value and the SDMA ring deadlocked. No waitcnt is needed: s_endpgm implicitly executes S_WAITCNT 0.

A request naming no SDMA queue set is now reported as no match instead of reaching the kernel: every fused A2A solution in the tree emits SDMA packets unconditionally, so null queue handles used to reach the kernarg groups and fault. hipblasLtMatmul rejects it as well, since a caller holding an algo from elsewhere would otherwise still get there. The heuristic reports it as zero algos rather than an error, matching what that function already does when selection yields nothing; both gates restore the dummy bias pointer they may have to return through.

hipblaslt_ext::Gemm rejects the stage outright. That path has no reset after its launch and no place to check the per-rank arrays against the world size, so admitting an all-to-all there would answer a second run with stale flags and index past the descriptor's vectors. Leaving fused_a2a_world unset also keeps FusedA2ATileDivisible from matching, should a request reach selection another way.

Measured on four gfx950 cards: 50 back-to-back launches verify (250 over five runs).

JIRA ID : ROCM-27524
One sample over the public API at the customer shape (18432 features,
2048 tokens, extent 10240), parameterized over the deployment: kProcesses
worker processes of kRanksPerProcess ranks each, with kWorld derived from
them. That reaches the shapes callers spread over -- torchrun gives a
process per card, a single-process launcher gives one process with a
thread per card -- and static_asserts hold the pair to a world that
divides the extent and fits the eight rank slots the fused-A2A ABI
carries. At the default 2 x 2 one communicator carries a self peer, a
same-process peer and two cross-process peers, and each kind of recv
pointer resolves a different way. The sample checks every rank's recv
buffer against every peer's D.

The parent only coordinates: it forks one worker per process and holds a
socket to each, so a star is the only topology the collectives need.
Every collective has the same shape -- a worker contributes its
kRanksPerProcess entries behind a byte-stride header, the parent
concatenates them in rank order and hands the whole array back -- so the
recv IPC handles, the registration allgather and the verdict share one
pair of primitives.

No A2A solution ships in the logic tree, so the sample needs a device
library built for it. Producing one from the gfx950 A2A config would mean
giving that config a LibraryLogic section, and the fused path skips the
result reporters, so the stage gets an empty benchmark CSV and fails in
LogicAnalyzer. Threading the fused path through the listener chain is
AIHPBLAS-4534; until then the sample states the prerequisite and skips
when the loaded library does not meet it.

Each rank releases the barrier it shares on the way out, so one rank
failing between the two rendezvous ends the run instead of leaving the
others blocked; the descriptors and device buffers are owned rather than
leaked, since a sample gets copied into code that loops. A worker that
skips closes its socket before the first collective, so the coordinator's
own exchange failing is only an error once every worker has claimed to
have run; the child exit statuses are consulted first.

COMM_CHANNEL selects the flag region. It was validated against nChannels
and then dropped -- every channel aliased to the first block, so the
guarantee that a channel covers its own flag region did not hold. The
channel is a host-side pointer bias of channel * FLAG_BLOCK_BYTES; the
kernel is unchanged.

The per-rank attributes are passed as the arrays themselves, sized in
whole entries, which is what hipblasLtFusedEpilogueSetAttribute expects;
a pointer to the array sizes out to less than one entry and is rejected.

JIRA ID : ROCM-27524
#11342 closed validate_fused_a2a_launch with an unconditional NOT_SUPPORTED, standing in for the kernels that did not exist yet. Those kernels land on this branch, so the placeholder rejected every request before rocblaslt was reached and both SDMA samples failed at hipblasLtMatmulAlgoGetHeuristic with it.

What the function defers is still enforced, in two places rather than one. The shape-independent half -- MacroTile0/1 in {128, 256}, data-parallel only, no split-K, a BF16 D, gfx94x/95x -- is a generation-time solution rejection in Solution.py. The half that depends on the problem is the FusedA2ATileDivisible predicate, which requires extent % (MacroTile0 * world) == 0 and rejects a world of zero, and so runs at selection. Both also filter the size-independent getAllSolutions fallback, which is what keeps a shape with no Equality logic entry from picking up an algo that does not fit it.

WellFormedRequestReportsMissingCapability asserted that placeholder. A replacement would have had three different correct answers - SUCCESS with an A2A device library, NOT_SUPPORTED with an ordinary one, and INVALID_VALUE with none - so it is dropped rather than pinned to one environment, and the file's header note is updated to match. Samples 30 and 31 cover that path end to end; the twelve rejection tests around it are untouched.

JIRA ID : ROCM-27524
The sample already runs the customer shape, which makes it the natural
place to hand users a reproducible latency number they can check on their
own machines.

The timing pass runs after the correctness check instead of reusing its
launch. The closed form that check compares against requires A to be
near-zero, which is not representative of real operand traffic, so the
timing pass re-fills A and B from the same (x%7)-3 alphabet the
tensilelite client draws from. That keeps the two harnesses comparable.

IN_KERNEL_FULL completion compares each peer's accumulated arrival count
for exact equality, so a rank that ran ahead could push a straggler's slot
past tokenTiles and stall its DRAIN poll for good. The loop alternates the
communicator's two flag regions, which rules that out structurally:
reaching round N+2 requires completing N+1, which requires every peer to
have sent its N+1 data, and hence to have finished N -- so the region a
run-ahead rank writes is never one a straggler is still polling. Nothing
on the host side has to coordinate the iterations, which also keeps socket
wake-ups out of the measured window.

Latency is reported as the max across ranks per iteration, matching what
the tensilelite client reports. It is the one collective that stops at the
parent, which reduces it to the single number the parent prints. The
kernel re-arms its own A2A state before it retires, so that cost falls
inside the measured window, which is correct because every caller pays it.

The check now validates the last launch rather than the first. The timing
loop leaves its own operands in place and never copies back, so hD and
hRecv still held the opening launch and a counter block that went bad
mid-loop would have gone unnoticed. The pass now re-fills the near-zero
operands the closed form needs and lands a final launch into the buffers
checkRank reads.

Verified on a four-card hold at 2 x 2, 4 x 1 and 1 x 4, each checking
every rank's recv buffer against every peer's D. 8 x 1 compiles but has
not been run; it needs a node with eight free cards.
@DDEle
DDEle force-pushed the users/yiding12/gemm-a2a-api branch from 5d2021a to 4e0b054 Compare September 17, 2026 07:28
FindNUMA.cmake and HsakmtLinkInterface.cmake exist to repair ROCm 7.x's
hsakmt package config. Neither is specific to TensileLite, and both now
have consumers on the hipBLASLt side. Moving them to the project's cmake/
directory puts them on the module path the root CMakeLists already
exports, so find_package(NUMA) and include(HsakmtLinkInterface) resolve
from any subdirectory without each caller appending to CMAKE_MODULE_PATH.
That also retires the ordering constraint FindNUMA carried: it had to
reach the module path before find_package(hsakmt), and nothing enforced
it.

hipblaslt_sanitize_hsakmt_link_interface now takes no arguments. It reads
and rewrites hsakmt::hsakmt's link interface itself and resolves ROCM_PATH
internally, so the get/if/call/set block both callers repeated verbatim
collapses to one line and the ROCM_PATH fallback lives in one place rather
than three. The function returns untouched when the target is absent or
carries no link interface.

The SDMA sample drops its hand-written libnuma search, which duplicated
FindNUMA and looked for numa.h under include/rocm_sysdeps where ROCm does
not install it.
`norm.hpp` and `allclose.hpp` define six non-template functions in the header
without `inline`: the `void*` plus `hipDataType` dispatch overloads of
`norm_check_general` and `allclose_check_general`, `norm_tolerance`, and the two
`norm_check` type helpers. Two translation units that include either header emit
the same symbols and the link fails.

Nothing hit this because each binary pulled the headers into exactly one
translation unit - `client.cpp` for the benchmark, `matmul_gtest.cpp` for the
tests.
`hipblaslt-bench-a2a` measures the fused all-to-all GEMM path across a rank
group started by an external launcher. Rank identity and rendezvous come from
the `RANK` / `WORLD_SIZE` / `LOCAL_RANK` / `MASTER_ADDR` / `MASTER_PORT`
variables torchrun sets; with none of them set the run is single-rank.

The shipped gfx950 logic corpus gains the fused GEMM+A2A entry the benchmark
resolves against, so a `--gemm-a2a-fusion` build needs no hand-built device
library. It lands in the base `gfx950` tree and declares the default fallback
chip ID, which `chipIdFallbacks` maps every gfx950 SKU onto, rather than a
`gfx950_id<chip>` directory that would cover one SKU. A build without the
fusion gate drops the entry in `generateLogicDataAndSolutions` and reports the
filtered count.

`hipblaslt-bench-a2a-launch` starts the rank group. It sets all five launcher
variables for every rank, so a stale `WORLD_SIZE` in the caller's environment
cannot turn a single-rank run into a failed rendezvous that still exits zero.
A rank exiting non-zero terminates the survivors, which would otherwise stay
blocked in the rendezvous, and each failing rank is reported with its log path
instead of the run standing or falling on rank 0's code alone. CMake copies the
script next to the benchmark in the build tree and installs it into bin/, so it
finds the executable beside itself in either layout.

- a TCP rendezvous exchanges IPC handles and recv pointers without MPI
- `--adaptive` self-sizes the sample count, and a collective agreement keeps
  every rank on the same iteration count
- latency is reported as the slowest rank, and the derived rate goes through
  `rate_per_second` so a non-positive time prints `-1` rather than dividing
  into `inf`
- a rank's device resources are released on every path out

`--verify` is off by default. When it is on, it runs `--iters` launches as its
own pass before the timed batch instead of checking inside the timed callable,
so the device-to-host copy and the comparison stay out of the reported latency.
Every rank runs the full count even after a mismatch: a short run would leave
that rank's launch counter behind the group, the ranks would then pick different
flag regions, and the run would deadlock.

Operands are HPL-random, which is what `hipblaslt-bench` initializes with by
default, so the two clients' numbers are comparable - flip rate drives power
draw, and on a power-limited part that reaches the reported latency through
clock throttling. The seed is derived from the rank; identical operands would
compare equal no matter which source's block landed, so deriving it is what
makes a misrouted block visible.

`--verify` therefore covers the transport and not the GEMM: the gold is the
source rank's own `D`. Each rank reads back the block it exported into every
peer's receive buffer and compares it against the rows of its local `D` that
block should hold. An all-zero `D` fails outright, since the two sides would
otherwise agree trivially. `HIPBLASLT_A2A_COMPLETION_IN_KERNEL_FULL` retires a
kernel once that rank's receive buffer is full, which says nothing about this
rank's outbound data having landed, so the check takes a rendezvous barrier
first.

The fixed-count path warms up for `--cold_iters` before the timed batch, so the
first launch's lazy code-object load stays out of the sample; a cold load off a
network filesystem costs seconds and, divided across the batch, silently
multiplies the reported latency. `--adaptive` sizes its own warmup and skips
that step, but a `--verify` pass still precedes it.

Successive launches alternate over two of the communicator's flag regions, and
two is the whole range the benchmark can need. Reaching round N+2 requires
completing N+1, which requires every peer to have sent its N+1 data and hence
finished N, so ranks stay within one round of each other and the region a
run-ahead rank writes is never one a straggler is still polling.

Every agreement is one allgather over a caller-supplied reduction, so doubles
and flags share one code path instead of one typed callback each. A failed
allgather throws rather than answering with the local value, which would leave
ranks on different iteration counts. Ranks agree on the default-path launch
outcome only after the kernel has been dispatched and completed; a failure
before dispatch still hangs, because a peer blocked in `hipStreamSynchronize`
never reaches the allgather.

`read_launcher_env` clamps `WORLD_SIZE` to at least 1. A zero or non-numeric
value used to reach `same_host_group()`, which sizes its result buffer by the
world size and then copies a hostname into the empty buffer.

`peers_reachable` rejects a multi-rank run whose `LOCAL_RANK` differs from its
`RANK`. The peer pre-check indexes devices by `LOCAL_RANK` while the
recv-pointer exchange indexes by `RANK`, so a launcher that leaves `LOCAL_RANK`
unset puts every rank on device 0 and the two disagree with nothing to catch it
unless `--verify` is on.

Two places do not reuse an existing client helper:

- `parse_a2a_args` handles its ten value options directly rather than pulling in
  the `boost::program_options` emulation in `bench/include/program_options.hpp`
- `RankResources` allocates with `hipMalloc` instead of `device_vector`, because
  the receive buffer has to export an IPC handle

`check_recv` goes through `norm_check_general` and `allclose_check_general`, the
same two measures `hipblaslt-bench --verify` turns on, but decides pass and fail
itself: `client.cpp` clears `norm_check_assert` and discards the `allclose`
result, so consuming them the way it does would leave `--verify` printing
diagnostics without gating anything. Requiring a zero norm error is exact rather
than approximate, since both sides hold the same bf16 values.
`unit_check_general` and `near_check_general` stay out because they expand to
nothing unless `GOOGLE_TEST` is defined, which would check nothing here.

The A2A SDMA sample leaves timing to the benchmark and picks up the hsakmt,
hsa-runtime64 and libnuma targets from the enclosing clients scope, which the
benchmark's dependency block resolves before the samples subdirectory is added.

gtest coverage: the rendezvous over loopback sockets, including a payload past
the socket buffer so the segmented send and receive loops run; the collective
agreement's reduction and its failure path; the `Arguments` layout for the added
fields; the single-rank benchmark path; and the `LOCAL_RANK` rejection. The
suites carry the `smoke` category token so ctest's quick tier and the
pull-request gate's `*smoke*` filter select them. The three tests that exercise
the fusion build only with `HIPBLASLT_ENABLE_GEMM_A2A_FUSION=ON`, which defaults
off and which no lane sets, so those do not run in a shared lane yet.
`collective_rendezvous_gtest.cpp` uses POSIX sockets and is excluded on Windows.
@DDEle
DDEle marked this pull request as ready for review September 18, 2026 09:44
@DDEle
DDEle requested review from a team as code owners September 18, 2026 09:44

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

See comments below.

for(uint32_t j = 0; j < arg.a2a_world; ++j)
{
res.ownedQueues.push_back(std::make_unique<TensileLite::Client::SdmaQueue>(
srcNode, TensileLite::Client::sdmaSelectEngine(srcNode, srcNode)));

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.

This selects the engine for srcNode -> srcNode for every peer. The public attribute defines entry j as the queue targeting rank j, and the sample resolves a separate destination node before calling sdmaSelectEngine. On a topology whose peer link recommends a different engine, this benchmark can use the wrong copy engine. Please resolve dstNode from j, call sdmaSelectEngine(srcNode, dstNode), and cover the multi-rank case.

{
return [&res, &heur, &launchCount, &lastStatus](int64_t) {
const float alpha = 1.0f, beta = 0.0f;
const uint32_t channel = launchCount++ % kCommChannels;

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.

The benchmark has two channels, but run_measurement can enqueue the default batch of 10 launches without a group-wide barrier. This reuses channel 0 on launch 3 before every rank is known to have retired launch 1. The public header explicitly says that local stream synchronization does not establish safe channel reuse. Please prevent reuse until all ranks agree that the earlier launch retired, or provide a distinct channel for every in-flight launch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Two channels are enough here because the alternation is interlocked across ranks, rather than relying on a stream synchronization.

Under HIPBLASLT_A2A_COMPLETION_IN_KERNEL_FULL a rank's kernel does not retire until its own receive buffer is full, and every rank issues its launches on a single stream. So for rank i to start launch k+2, launch k+1 must have retired on i; that requires every peer to have pushed its k+1 shard, which requires every peer to have started k+1, which by stream order requires every peer to have finished k. Launch k and launch k+2 are the pair that share a channel, so by the time i returns to that channel every rank has retired its previous operation on it.

Rebuilding the benchmark with kCommChannels = 1 deadlocks at 8 ranks under --iters 50 --verify 1, with no rank producing output before the run is killed; the two-channel build completes the same run with every launch verified.

if(rocblaslt_resolve_fused_epilogue(prob.fused_epilogue, fusedInfo)
&& fusedInfo.hasA2APrefix)
{
inputs.fusedA2ACounter = prob.Synchronizer;

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.

Every fused-A2A launch receives the same handle-wide Synchronizer address. commChannel biases the peer flag blocks, but it does not separate the SDMA cursors or counter1/2/3 storage. Two operations on different streams can therefore race even when they use distinct channels and queue sets, contrary to the documented concurrency rule. Please allocate or derive counter storage per channel or stream, or reject that concurrent use explicitly.

// kernel in this release.
log_error(__func__, "no fused all-to-all implementation for the selected device");
return HIPBLAS_STATUS_NOT_SUPPORTED;
return HIPBLAS_STATUS_SUCCESS;

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.

Before this returns success, can we revalidate every receive pointer and all four fields of every SDMA queue? The matmul descriptor references the fused descriptor rather than copying it, and SetAttribute can replace these arrays after attachment. The current launch check verifies only their lengths, so a post-attachment null entry reaches kernel argument construction and can fault on the device.

)
endif()

if(HIPBLASLT_ENABLE_GEMM_A2A_FUSION AND NOT WIN32)

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.

These tests are compiled only when the feature is enabled, while the option defaults to OFF and the PR says no shared automated lane enables it. This change adds public launch behavior and device-side state resets, so the enabled path needs a shared gfx950 run; manual results alone do not satisfy the hipBLASLt regression-test requirement.

comment="some peer incomplete -> spin (poll again)"))

# Still one lane per slot, and vPollOff still holds lane j's byte offset.
module.add(VMovB32(dst=vgpr(vPollVal), src=0, comment="fused-A2A: zero for the flag clear"))

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.

This changes emitted synchronization behavior by clearing state for the next launch, but the diff adds no focused TensileLite unit test or saved expected-result test for the generated flag, outbound-counter, and cursor clears. Please add a test that fails when these instructions or their ordering are removed, then run it in the shared feature-enabled gfx950 lane.


if(HIPBLASLT_ENABLE_GEMM_A2A_FUSION AND NOT WIN32)
rocm_install(
PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/clients/scripts/a2a/hipblaslt-bench-a2a-launch"

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.

This installs the launcher, but hipblaslt-bench-a2a is absent from the rocm_install(TARGETS ...) list above. In an installed tree the script therefore cannot find the executable beside itself as the PR description promises. Please install the benchmark target under the same condition and add an installed-tree smoke check.

return 1;

hipblasLtMatmulHeuristicResult_t heur{};
const uint8_t found = select_algo(res, heur) ? 1 : 0;

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.

select_algo returns false both when the heuristic succeeds with zero algorithms and when hipblasLtMatmulAlgoGetHeuristic returns an error. The block below converts both cases into a successful skipped exit. Please preserve the API status separately so an invalid request or internal failure returns non-zero instead of being reported as an unsupported configuration.

@@ -0,0 +1,497 @@
MinimumRequiredVersion: 5.0.0

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.

new YAML files under projects/hipblaslt must start with the short copyright and SPDX-License-Identifier: MIT header required by projects/hipblaslt/AGENTS.md. Please add it above MinimumRequiredVersion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These files are generated rather than hand-written: none of the 2,797 logic YAML files under Tensile/Logic/ carries the header.

@@ -0,0 +1,237 @@
// Copyright Advanced Micro Devices, Inc., or its affiliates.

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.

I have the same critique of this one-off script as for the previous PR: #10925 (comment)

@DDEle DDEle Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This design did take your earlier comment into account, and the benchmark reuses the common client layer directly: hipblaslt_init_hpl for operand setup (a2a_bench.hpp:202), norm_check_general and allclose_check_general for the host comparison (:264, :269), run_measurement for the timing loop (client_a2a.cpp:157, the same function client_sequence.cpp:658 uses), and the shared Arguments struct throughout. What a2a_bench.hpp adds on top is the part with no counterpart in the common layer: process rendezvous, peer reachability, receive-pointer exchange, and per-rank SDMA queue setup.

A separate executable is also the existing shape here. clients/CMakeLists.txt already builds seven specialized benchmarks from clients/bench/src/hipblaslt-api-overhead, hipblaslt-bench-groupedgemm-fixed-mk, the four extop ones, and hipblaslt-sequence — each an add_executable linking hipblaslt-clients-common. hipblaslt-bench-a2a is the eighth. Further fused-A2A benchmarking (e.g. a2a then gemm) is meant to extend this binary rather than add another.

It is not a flag on hipblaslt-bench because this benchmark needs one process per GPU and a launcher to start them, which does not fit that client's single-process problem loop.

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