Skip to content

Commit cc35fd3

Browse files
authored
perf(hipblaslt): Split subtile cluster barrier signal/wait (gfx1250) (ROCm#8805)
ISSUE ID : AIHPBLAS-3984 ### Motivation The subtile cluster barrier exposed its cross-CU latency as a stall, and loop-control / cluster branches were issued without regard to the surrounding WMMAs, costing branch latency in the mainloop on gfx1250. ### Technical Details Added features: - Split the cluster barrier into separate signal and wait halves - Wave-0 election replacing the prior isfirst-barrier scheme; only wave 0 branches into the cluster signal. - Post-schedule splicing (insertClusterBarrier) — the signal is spliced right after the mainloop's existing workgroup barrier (reusing that sync rather than emitting a second one); - Branch-after-WMMA placement to hide branching latency: - 16-byte alignment on branch targets (LoopBeginL, skipCBPreSignal). - gfx1250 gating via HasWmmaArbStallBit, with an assert that ClusterBarrier is gfx1250-only. ### Test Result - test_SubtileBasedLogicalScheduler.py — 131 passed. - The three subtile bf16 gfx1250 gemm common tests pass: - Tensile/Tests/common/gemm/gfx12/subtile_bf16_gfx1250.yaml - Tensile/Tests/common/gemm/gfx12/subtile_bf16_gfx1250_dp.yaml - Tensile/Tests/common/gemm/gfx12/subtile_bf16_gfx1250_cluster.yaml
1 parent 946e8e0 commit cc35fd3

3 files changed

Lines changed: 372 additions & 36 deletions

File tree

projects/hipblaslt/tensilelite/Tensile/Components/Subtile/ClusterBarrier.py

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,130 @@
33

44
"""Cluster-scope barrier handshake for the subtile mainloop.
55
6-
Subtile-specific equivalent of the StinkyTofu InsertClusterBarrierPass (which
7-
does not run at OptLevel 0 / ScheduleIterAlg=3). Free functions take the writer
8-
explicitly to keep cluster logic in Subtile/ rather than KernelWriterAssembly.
6+
The handshake is split into a *signal* half and a *wait* half so the wait can be
7+
moved away from the signal, hiding the cluster barrier's cross-CU latency behind
8+
the WMMAs that issue in between instead of exposing it as a stall.
99
"""
1010

1111
from __future__ import annotations
1212

13-
from rocisa.code import Label, Module, TextBlock
14-
from rocisa.instruction import SBarrier, SBarrierSignalIsFirst, SCBranchSCC0
13+
from rocisa.code import Label, Module
14+
from rocisa.container import sgpr
15+
from rocisa.instruction import (SBarrier, SCBranchSCC0, SCmpEQU32,
16+
MFMAInstruction, MXMFMAInstruction)
1517

18+
_isWgBarrier = lambda x: isinstance(x, SBarrier) and "s_barrier_wait -1" in str(x)
1619

17-
def subtileClusterBarrier(writer, kernel, label="") -> Module:
18-
# Balanced workgroup barrier + cluster signal/wait -3 per occurrence.
19-
mod = Module("subtile_cluster_barrier")
20-
# Workgroup barrier via isfirst: the first wave to arrive gets SCC=1 and so
21-
# is the single wave that signals the cluster barrier (one arrival per WG).
22-
skipPreSignal = Label(writer.labels.getUniqueNamePrefix("skipCBPreSignal"), "")
23-
mod.add(SBarrierSignalIsFirst(False, "workgroup barrier signal (isfirst)"))
24-
mod.add(SBarrier(True, True, False, "workgroup barrier wait"))
25-
mod.add(SCBranchSCC0(skipPreSignal.getLabelName(), "only the first-arriving wave signals the cluster"))
20+
21+
def _findNextMFMA(items, start):
22+
"""Index of the first MFMA at/after ``start``, or ``None`` if none follows."""
23+
for j in range(start, len(items)):
24+
if isinstance(items[j], (MFMAInstruction, MXMFMAInstruction)):
25+
return j
26+
return None
27+
28+
29+
def subtileClusterBarrierSignal(writer, kernel) -> Module:
30+
"""Wave-0-only cluster_barrier signal.
31+
32+
Wave 0 alone issues the cluster_barrier signal; all other waves branch over
33+
it. Ends at the ``skipPreSignal`` label so all waves fall through to whatever
34+
work follows; the matching wait is emitted later by ``subtileClusterBarrierWait``.
35+
"""
36+
mod = Module("subtile_cluster_barrier_signal")
37+
skipPreSignal = Label(writer.labels.getUniqueNamePrefix("skipCBPreSignal"), "", 16)
38+
# Elect wave 0 to issue the single cluster_barrier signal.
39+
mod.add(SCmpEQU32(sgpr("WaveIdx"), 0, "wave 0?"))
40+
mod.add(SCBranchSCC0(skipPreSignal.getLabelName(), "only wave 0 signals the cluster"))
2641
mod.add(SBarrier(True, False, True, "cluster_barrier signal"))
2742
mod.add(skipPreSignal)
43+
return mod
44+
45+
46+
def subtileClusterBarrierWait(writer, kernel) -> Module:
47+
"""The all-waves cluster_barrier wait that closes the handshake."""
48+
mod = Module("subtile_cluster_barrier_wait")
2849
mod.add(SBarrier(True, True, True, "cluster_barrier wait"))
29-
# SQTT trace marker tagging which loop section this barrier belongs to.
30-
base = {"PRELOOP": 0x00, "MAINLOOP": 0x10, "NGLL": 0x20,
31-
"NLL": 0x30, "TAILLOOP": 0x40}
32-
kind, _, suffix = label.partition("_C")
33-
loopId = (base.get(kind, 0xf0) + (int(suffix) if suffix.isdigit() else 0)) & 0xff
34-
mod.add(TextBlock(f"s_ttracedata_imm {0xc100 | loopId:#06x}\n"))
3550
return mod
51+
52+
53+
def insertClusterBarrier(module, writer, kernel):
54+
"""Splice the cluster-scope barrier handshake into the post-schedule order.
55+
56+
No-op unless ``ClusterBarrier`` is enabled. The signal is spliced in right
57+
after the mainloop's existing workgroup barrier (reusing that sync instead of
58+
emitting a second one); the wait is appended at the end of the section, so the
59+
barrier's cross-CU latency overlaps the whole macro tile's WMMAs before the
60+
handshake is closed.
61+
62+
If no workgroup barrier is found in this section, the signal is prepended at
63+
the start so the handshake is still opened (correctness over reuse).
64+
65+
Returns a rebuilt Module; the input is left untouched.
66+
"""
67+
if not kernel.get("ClusterBarrier"):
68+
return module
69+
70+
signalItems = subtileClusterBarrierSignal(writer, kernel).flatitems()
71+
waitItems = subtileClusterBarrierWait(writer, kernel).flatitems()
72+
73+
# ClusterBarrier is only supported on gfx1250.
74+
assert writer.states.asmCaps.get("HasClusterBarrier", False), \
75+
"ClusterBarrier requires the HasClusterBarrier asm capability"
76+
77+
# Place the wave-0-election branch right after a WMMA to hide branching
78+
# latency: keep s_cmp before the next scheduled MFMA and emit the branch
79+
# after it.
80+
81+
items = module.flatitems()
82+
result = Module(module.name)
83+
done = False
84+
skip = set()
85+
for i, inst in enumerate(items):
86+
if i in skip:
87+
continue
88+
result.add(inst)
89+
if not done and _isWgBarrier(inst):
90+
done = True
91+
mfmaIdx = _findNextMFMA(items, i + 1)
92+
if mfmaIdx is None:
93+
# No following MFMA to pin the branch to: emit the block intact
94+
# (best-effort).
95+
for s in signalItems:
96+
result.add(s)
97+
else:
98+
# Split the signal block at the wave-0 election branch. The
99+
# block is authored with exactly one conditional branch; assert
100+
# it so a future change that adds another fails loudly here.
101+
brIdxs = [k for k, s in enumerate(signalItems)
102+
if isinstance(s, SCBranchSCC0)]
103+
assert len(brIdxs) == 1, \
104+
"signal block must contain exactly one wave-0 election branch"
105+
brIdx = brIdxs[0]
106+
pre, post = signalItems[:brIdx], signalItems[brIdx:]
107+
# Everything up to the MFMA (incl. its s_set_vgpr_msb primer)
108+
# keeps its order, then s_cmp, the MFMA, and the branch. SCC
109+
# survives the MFMA and vgpr-msb is a persistent mode, so the
110+
# intervening compare disturbs neither.
111+
for k in range(i + 1, mfmaIdx):
112+
result.add(items[k])
113+
skip.add(k)
114+
for s in pre:
115+
result.add(s)
116+
result.add(items[mfmaIdx])
117+
skip.add(mfmaIdx)
118+
for s in post:
119+
result.add(s)
120+
if not done: # no workgroup barrier: open the handshake at the start
121+
head = Module(module.name)
122+
for s in signalItems:
123+
head.add(s)
124+
for inst in result.flatitems():
125+
head.add(inst)
126+
result = head
127+
128+
# Wait: append at the end of the section so cluster latency hides behind the
129+
# whole macro tile's WMMAs before the handshake is closed.
130+
for w in waitItems:
131+
result.add(w)
132+
return result

projects/hipblaslt/tensilelite/Tensile/Components/Subtile/LogicalScheduler.py

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,62 @@ def _checkin_tile(tile):
6262
tile.regList.pool.checkIn(tile.regList.indices[0])
6363

6464

65+
def _hoistSetupBeforeLastMFMA(loopBody, setupInsts):
66+
"""Move loop-control setup (dec + compare) above the body's last MFMA.
67+
68+
Place the loop-control branch immediately after the last MFMA to hide
69+
branching latency behind it. The decrement+compare that feed the branch's
70+
SCC are normally emitted right after the body (MFMA; s_sub; s_cmp;
71+
s_cbranch); hoisting them above the final MFMA produces (s_sub; s_cmp;
72+
MFMA; s_cbranch) so the branch directly follows the MFMA.
73+
74+
The hoist is dependency-safe: SCC set by s_cmp survives the MFMA (which
75+
never touches SCC), and the MFMA does not read LoopCounterL.
76+
77+
Falls back to appending the setup when the body has no MFMA (e.g. a skip
78+
branch with no preceding compute), leaving behavior unchanged there.
79+
Returns a rebuilt Module; the input is left untouched.
80+
"""
81+
from rocisa.instruction import MFMAInstruction, MXMFMAInstruction
82+
83+
items = loopBody.flatitems()
84+
lastMfma = -1
85+
for idx, it in enumerate(items):
86+
if isinstance(it, (MFMAInstruction, MXMFMAInstruction)):
87+
lastMfma = idx
88+
89+
rebuilt = Module(loopBody.name)
90+
if lastMfma < 0:
91+
for it in items:
92+
rebuilt.add(it)
93+
for s in setupInsts:
94+
rebuilt.add(s)
95+
return rebuilt
96+
97+
for idx, it in enumerate(items):
98+
if idx == lastMfma:
99+
for s in setupInsts:
100+
rebuilt.add(s)
101+
rebuilt.add(it)
102+
return rebuilt
103+
104+
105+
def _emitBodySetupBranch(module, loopBody, setupInsts, branch, hoist):
106+
"""Append a loop body, its dec+compare setup, and the trailing control branch.
107+
108+
When ``hoist`` is set, the setup is moved above the body's last MFMA (via
109+
_hoistSetupBeforeLastMFMA) so the branch lands right after that MFMA to hide
110+
branching latency. Otherwise the setup and branch simply follow the body.
111+
"""
112+
if hoist:
113+
module.add(_hoistSetupBeforeLastMFMA(loopBody, setupInsts))
114+
else:
115+
module.add(loopBody)
116+
for s in setupInsts:
117+
module.add(s)
118+
module.add(branch)
119+
120+
65121
class Pass(IntEnum):
66122
"""Scheduler passes in dependency order.
67123
@@ -3315,10 +3371,6 @@ def _emitLoop(self, writer, kernel, label, emitted_3d, schedule=True):
33153371

33163372
module = Module(label)
33173373
module.addComment0(f"{label} start")
3318-
if kernel.get("ClusterBarrier"):
3319-
from Tensile.Components.Subtile.ClusterBarrier import subtileClusterBarrier
3320-
for inst in subtileClusterBarrier(writer, kernel, label=label).flatitems():
3321-
module.add(inst)
33223374
use_pap_preloop_skip = (
33233375
label == "PRELOOP"
33243376
and kernel.get("UseSubtileImpl")
@@ -3372,6 +3424,11 @@ def _emitLoop(self, writer, kernel, label, emitted_3d, schedule=True):
33723424
# them (swap hoisted ahead, dscnt drain between), so no WAR can form.
33733425
if self.config.pgr == 0 and label.startswith("MAINLOOP"):
33743426
module = insertLRSwapWarWaitAlu(module, writer, kernel)
3427+
# Cluster barrier: splice both halves against the final post-schedule order.
3428+
# Signal goes right after the mainloop's existing workgroup barrier (reusing
3429+
# that sync); the wait is appended at the end to hide its cross-CU latency.
3430+
from Tensile.Components.Subtile.ClusterBarrier import insertClusterBarrier
3431+
module = insertClusterBarrier(module, writer, kernel)
33753432
return module
33763433

33773434
def _emit_pgr2_tail_lw_align(self, kernel):
@@ -3433,6 +3490,10 @@ def emitMainAndExitLoops(self, writer, kernel, tensorParametersA=None, tensorPar
34333490
module = Module("MainAndExitLoops")
34343491
uf = self.unroll_factor
34353492

3493+
# gfx1250 requires a loop-back/exit s_cbranch to be alone/first after an
3494+
# MFMA, so hoist the per-copy dec+compare above the body's last MFMA.
3495+
isGfx1250 = writer.states.archCaps.get("HasWmmaArbStallBit", False)
3496+
34363497
def make_subtile_pap_module(skip_barrier=False):
34373498
if (kernel.get("UseSubtileImpl")
34383499
and kernel.get("PrefetchAcrossPersistent")
@@ -3502,7 +3563,7 @@ def insert_after_drain(em_list):
35023563

35033564
# ── Mainloop ──
35043565
module.addComment0("MAINLOOP")
3505-
loopBegin = Label("LoopBeginL", "")
3566+
loopBegin = Label("LoopBeginL", "", alignment=16)
35063567

35073568
exitValue = self.config.pgr
35083569

@@ -3524,21 +3585,25 @@ def insert_after_drain(em_list):
35243585
module.add(_SMovB32(dst=mgpr(0), src=sgpr("LoopCounterL"),
35253586
comment="trace: M0 = LoopCounterL"))
35263587
module.add(_STtraceData(comment="trace: emit M0 to SQTT"))
3527-
module.add(self._emitLoop(writer, kernel, f"MAINLOOP_C{ui}",
3528-
self._emitted_per_unroll[ui]))
3529-
module.add(SSubU32(dst=sgpr("LoopCounterL"),
3530-
src0=sgpr("LoopCounterL"), src1=1,
3531-
comment=f"dec counterL (copy {ui})"))
3532-
module.add(SCmpEQU32(src0=sgpr("LoopCounterL"), src1=exitValue,
3533-
comment=f"counterL == {exitValue}? (copy {ui} exit)"))
3588+
loopBody = self._emitLoop(writer, kernel, f"MAINLOOP_C{ui}",
3589+
self._emitted_per_unroll[ui])
3590+
# dec + compare that produce the SCC the loop-control branch reads.
3591+
setupInsts = [
3592+
SSubU32(dst=sgpr("LoopCounterL"),
3593+
src0=sgpr("LoopCounterL"), src1=1,
3594+
comment=f"dec counterL (copy {ui})"),
3595+
SCmpEQU32(src0=sgpr("LoopCounterL"), src1=exitValue,
3596+
comment=f"counterL == {exitValue}? (copy {ui} exit)"),
3597+
]
35343598
if ui < uf - 1:
3535-
module.add(SCBranchSCC1(
3599+
branch = SCBranchSCC1(
35363600
labelName=exitLabels[ui].getLabelName(),
3537-
comment=f"copy {ui} exit → NGLL_C{ui}"))
3601+
comment=f"copy {ui} exit → NGLL_C{ui}")
35383602
else:
3539-
module.add(SCBranchSCC0(
3603+
branch = SCBranchSCC0(
35403604
labelName=loopBegin.getLabelName(),
3541-
comment="restart mainloop"))
3605+
comment="restart mainloop")
3606+
_emitBodySetupBranch(module, loopBody, setupInsts, branch, hoist=isGfx1250)
35423607

35433608
# ── NGLL + NLL exit paths ──
35443609
hasNGLL = self.config.pgr >= 2

0 commit comments

Comments
 (0)