Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/Components/Subtile/InstructionScheduler.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

547 lines
22 KiB
Python

# Copyright Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Instruction scheduler for subtile-based mainloop.
Interleaves non-MFMA instructions between MFMAs using a slot-based placer
with pluggable scheduling rules.
"""
from typing import List, Tuple, Optional
from rocisa.code import Module
from rocisa.instruction import SWaitCnt, SBarrier, MFMAInstruction, MXMFMAInstruction, \
LocalReadInstruction, GlobalReadInstruction, CommonInstruction
class _SlotPlacer:
"""Generic slot placement engine for interleaving instructions between MFMAs.
Each interval (pair of adjacent MFMAs) has 2 placement slots.
Rules are injected via callbacks:
- validators: (placer, pos, inst) -> bool — reject invalid slots
- adjusters: (placer, limit, inst) -> limit — shift search start
- onPlace: (placer, pos, inst) -> None — update rule state after placement
"""
def __init__(self, intervals: int, numModules: int,
pathOrders: List[List[int]],
validators=None, adjusters=None, onPlace=None):
self.totalSlots = intervals * 2
self._n = numModules
self._prevInPath: List[int] = [-1] * numModules
self._nextInPath: List[int] = [-1] * numModules
for order in pathOrders:
for a, b in zip(order, order[1:]):
self._prevInPath[b] = a
self._nextInPath[a] = b
self._validators = validators or []
self._adjusters = adjusters or []
self._onPlace = onPlace
self._placed: List[List[Tuple[int, object]]] = [[] for _ in range(self.totalSlots)]
self._firstPos: List[Optional[int]] = [None] * numModules
self._lastPos: List[Optional[int]] = [None] * numModules
self.leftovers: List[Tuple[int, object]] = []
# ── Placement ──
def _canPlace(self, pos: int, inst) -> bool:
if pos < 0 or pos >= self.totalSlots or len(self._placed[pos]) >= 2:
return False
return all(v(self, pos, inst) for v in self._validators)
def adjustLimit(self, limit: int, inst) -> int:
for adj in self._adjusters:
limit = adj(self, limit, inst)
return limit
def bounds(self, mid: int) -> Tuple[int, int]:
lo = 0
pred = self._prevInPath[mid]
if 0 <= pred < self._n and self._lastPos[pred] is not None:
lo = self._lastPos[pred] + 1
hi = self.totalSlots - 1
succ = self._nextInPath[mid]
if 0 <= succ < self._n and self._firstPos[succ] is not None:
hi = self._firstPos[succ] - 1
return lo, hi
def findSlot(self, mid: int, inst, limit: int, reverse: bool = False) -> Optional[int]:
lo, hi = self.bounds(mid)
if reverse:
hi = min(hi, limit)
else:
lo = max(lo, limit)
if hi < lo:
return None
for pos in (range(hi, lo - 1, -1) if reverse else range(lo, hi + 1)):
if self._canPlace(pos, inst):
return pos
return None
def _forceSlot(self, mid: int, limit: int, reverse: bool) -> int:
"""Find the closest valid slot respecting dependencies, allowing >2 items per slot."""
lo, hi = self.bounds(mid)
if reverse:
hi = min(hi, limit)
lo = max(lo, 0)
if hi < lo:
hi = lo
return hi
else:
lo = max(lo, limit)
hi = min(hi, self.totalSlots - 1)
if lo > hi:
lo = hi
return lo
def place(self, pos: int, item: Tuple[int, object], reverse: bool = False):
mid = item[0]
if reverse:
self._placed[pos].insert(0, item)
else:
self._placed[pos].append(item)
if self._firstPos[mid] is None or pos < self._firstPos[mid]:
self._firstPos[mid] = pos
if self._lastPos[mid] is None or pos > self._lastPos[mid]:
self._lastPos[mid] = pos
if self._onPlace:
self._onPlace(self, pos, item[1])
def placePath(self, pathInsts: List[Tuple[int, object]], reverse: bool = False,
multiDU: bool = False):
"""Place a sequence of (moduleId, instruction) items into slots.
Walks pathInsts in order, applying adjusters (forward only) and
finding valid slots. When no empty slot is found, force-places at
the closest valid position respecting dependencies (allowing >2
items per slot).
When multiDU is True, wait_gr immediately followed by barrier is
placed as an atomic pair in the same slot so other paths cannot
insert m0 setup between them.
"""
limit = (self.totalSlots - 1) if reverse else 0
if not multiDU:
for item in pathInsts:
mid, inst = item
if not reverse:
limit = self.adjustLimit(limit, inst)
pos = self.findSlot(mid, inst, limit, reverse=reverse)
if pos is None:
pos = self._forceSlot(mid, limit, reverse)
self.place(pos, item, reverse=reverse)
limit = (pos - 1) if reverse else (pos + 1)
return
idx = 0
while idx < len(pathInsts):
item = pathInsts[idx]
mid, inst = item
if (not reverse and idx + 1 < len(pathInsts)
and _isWaitGr(inst) and _isBarrier(pathInsts[idx + 1][1])):
if not reverse:
limit = self.adjustLimit(limit, inst)
pos = self.findSlot(mid, inst, limit, reverse=reverse)
if pos is None:
pos = self._forceSlot(mid, limit, reverse)
self.place(pos, item, reverse=reverse)
sync_item = pathInsts[idx + 1]
if self._canPlace(pos, sync_item[1]):
self.place(pos, sync_item, reverse=reverse)
else:
sync_pos = self.findSlot(sync_item[0], sync_item[1],
pos + 1, reverse=reverse)
if sync_pos is None:
sync_pos = self._forceSlot(sync_item[0], pos + 1, reverse)
self.place(sync_pos, sync_item, reverse=reverse)
pos = sync_pos
limit = (pos - 1) if reverse else (pos + 1)
idx += 2
continue
if not reverse:
limit = self.adjustLimit(limit, inst)
pos = self.findSlot(mid, inst, limit, reverse=reverse)
if pos is None:
pos = self._forceSlot(mid, limit, reverse)
self.place(pos, item, reverse=reverse)
limit = (pos - 1) if reverse else (pos + 1)
idx += 1
# ── Assembly ──
def assemble(self, mfmas) -> Module:
intervals = len(mfmas) - 1
result = Module()
result.add(mfmas[0])
for i in range(intervals):
for slot in (2 * i, 2 * i + 1):
for item in self._placed[slot]:
result.add(item[1])
result.add(mfmas[i + 1])
for _, inst in self.leftovers:
result.add(inst)
return result
# ── Scheduling rules ──
# Hardcoded gap to hide ds_read latency. TODO: compute this more accurately.
# gfx1250 needs a larger gap (8); other archs use 4.
_MIN_MFMA_GAP_DS_READ_TO_WAIT_DEFAULT = 4
_MIN_MFMA_GAP_DS_READ_TO_WAIT_GFX1250 = 8
_isDsRead = lambda x: isinstance(x, LocalReadInstruction)
_isBufferLoad = lambda x: isinstance(x, GlobalReadInstruction)
_isWaitCnt = lambda x: isinstance(x, SWaitCnt)
_isM0Update = lambda x: isinstance(x, CommonInstruction) and hasattr(x, 'dst') and hasattr(x.dst, 'regType') and x.dst.regType == 'm'
# Typed op detection (see SWaitCntEx.isWaitGr / SBarrier) rather than matching
# substrings in the instruction comment, which is brittle to comment edits.
_isWaitGr = lambda x: _isWaitCnt(x) and getattr(x, "isWaitGr", False)
_isBarrier = lambda x: isinstance(x, SBarrier)
class _SchedulingRules:
"""Scheduling rules for slot placement: validators, adjusters, and placement hooks.
Owns all rule state (ds_read/waitcnt tracking, buffer-load spreading).
Bound methods are passed as callbacks to _SlotPlacer.
"""
def __init__(self, totalSlots: int, minGapDsReadToWait: int = _MIN_MFMA_GAP_DS_READ_TO_WAIT_DEFAULT):
# Cross-path state
self.lastDsReadPos = -1
self.earliestWaitCntPos = totalSlots
self.waitGrBarrierPos: Optional[int] = None
self.maxDsReadAfterWaitGr = -1
self.minGap = minGapDsReadToWait
# Per-path state
self._resetPath()
def _resetPath(self):
self.firstBufLoadPos: Optional[int] = None
self.bufLoadIdx = 0
self.bufLoadMaxSlot = 0
self.numBufLoads = 0
# ── Validators: (placer, pos, inst) -> bool ──
def oneDsReadPerInterval(self, placer, pos, inst):
"""At most one ds_read per interval (pair of slots) to avoid same SIMD pair stalls as we have a single codepath"""
if not _isDsRead(inst):
return True
peer = pos ^ 1
return not (0 <= peer < placer.totalSlots
and any(_isDsRead(item[1]) for item in placer._placed[peer]))
def minGapDsReadBeforeWait(self, placer, pos, inst):
"""Reject ds_read too close to an already-placed waitcnt ahead."""
if not _isDsRead(inst):
return True
gap = self.minGap * 2
return self.earliestWaitCntPos - pos >= gap
def minGapDsReadToWait(self, placer, pos, inst):
"""Reject waitcnt too close to the last placed ds_read."""
if not _isWaitCnt(inst) or self.lastDsReadPos < 0:
return True
gap = self.minGap * 2
return pos - self.lastDsReadPos >= gap
def noM0WithBufferLoad(self, placer, pos, inst):
"""Avoid placing M0 updates and buffer_loads in the same MFMA interval."""
if not _isM0Update(inst) and not _isBufferLoad(inst):
return True
peer = pos ^ 1
slots = [pos]
if 0 <= peer < placer.totalSlots:
slots.append(peer)
if _isM0Update(inst):
return not any(_isBufferLoad(item[1]) for s in slots for item in placer._placed[s])
return not any(_isM0Update(item[1]) for s in slots for item in placer._placed[s])
def grAfterWaitGrDsReads(self, placer, pos, inst):
"""Keep GR (m0 / buffer_load) out of the window after wait_gr+barrier until ds_reads finish."""
if not (_isBufferLoad(inst) or _isM0Update(inst)):
return True
if self.waitGrBarrierPos is None or self.maxDsReadAfterWaitGr < 0:
return True
if pos <= self.waitGrBarrierPos:
return True
return pos > self.maxDsReadAfterWaitGr
# ── Adjusters: (placer, limit, inst) -> limit ──
def deferGrAfterWaitGrDsReads(self, placer, limit, inst):
"""Once past wait_gr, start searching for GR only after post-barrier ds_reads."""
if not (_isBufferLoad(inst) or _isM0Update(inst)):
return limit
if self.waitGrBarrierPos is None or self.maxDsReadAfterWaitGr < 0:
return limit
if limit <= self.waitGrBarrierPos:
return limit
return max(limit, self.maxDsReadAfterWaitGr + 1)
def spreadBufferLoads(self, placer, limit, inst):
"""Spread buffer_load instructions evenly across available range."""
if not _isBufferLoad(inst) or self.bufLoadMaxSlot <= 0:
return limit
if self.firstBufLoadPos is not None:
stride = max(1, (self.bufLoadMaxSlot - self.firstBufLoadPos)
// self.numBufLoads)
limit = max(limit, self.firstBufLoadPos
+ self.bufLoadIdx * stride)
self.bufLoadIdx += 1
return limit
# ── Placement hook: (placer, pos, inst) -> None ──
def trackPlacement(self, placer, pos, inst):
"""Update rule state after a successful placement."""
if _isDsRead(inst):
self.lastDsReadPos = max(self.lastDsReadPos, pos)
if _isWaitCnt(inst):
self.earliestWaitCntPos = min(self.earliestWaitCntPos, pos)
if _isBufferLoad(inst) and self.firstBufLoadPos is None:
self.firstBufLoadPos = pos
# ── Per-path setup ──
def resetPath(self):
self._resetPath()
def setupBufLoadSpreading(self, placer, pathInsts, order):
"""Compute buffer-load spreading bounds for a forward path.
Reserves tail slots for non-buffer-load instructions in modules that
follow the last GR module (e.g. GR_INC SRD updates, LDS buffer swaps).
"""
self.numBufLoads = sum(1 for _, inst in pathInsts if _isBufferLoad(inst))
if self.numBufLoads > 1:
_, rawMax = placer.bounds(pathInsts[-1][0])
grModuleIds = {mid for mid, inst in pathInsts if _isBufferLoad(inst)}
lastGrIdx = max(order.index(m) for m in grModuleIds if m in order)
tailModuleIds = set(order[lastGrIdx + 1:])
numTailInsts = sum(1 for mid, _ in pathInsts if mid in tailModuleIds)
# this is an approximation as we don't know exactly how many slots will be use by modules after the GR yet (in this codepath)
self.bufLoadMaxSlot = max(0, rawMax - numTailInsts)
def setPostWaitGrDsReadFence(self, placer) -> None:
"""After the wait_gr path is placed, record ds_read coverage for GR deferral."""
wg_pos = None
for pos, slot in enumerate(placer._placed):
for _, inst in slot:
if _isWaitGr(inst):
wg_pos = pos if wg_pos is None else min(wg_pos, pos)
if wg_pos is None:
self.waitGrBarrierPos = None
self.maxDsReadAfterWaitGr = -1
return
max_ds = -1
for pos, slot in enumerate(placer._placed):
if pos <= wg_pos:
continue
for _, inst in slot:
if _isDsRead(inst):
max_ds = max(max_ds, pos)
self.waitGrBarrierPos = wg_pos
self.maxDsReadAfterWaitGr = max_ds
def _classifyPaths(pathOrders, emittedModules):
"""Classify paths by wait_gr presence, sorted: wait_gr first, then by index."""
paths = []
for order in pathOrders:
hasWaitGR = any(emittedModules[i].opType == "wait_gr" for i in order)
paths.append((order, hasWaitGR))
paths.sort(key=lambda p: (0 if p[1] else 1, p[0][0] if p[0] else 10**9))
return paths
def _flattenPath(order, emittedModules, reverse=False):
"""Flatten a path of module indices into (moduleId, instruction) pairs."""
pathInsts = [(mid, inst) for mid in order for inst in emittedModules[mid].instructions]
if reverse:
pathInsts.reverse()
return pathInsts
def extractPathsFromBeforeDeps(emittedModules) -> Tuple[int, List[List[int]], List[List[int]]]:
"""Extract non-MFMA dependency paths using only EmittedModule.before links.
Returns:
(mfmaIdx, paths, preMfmaPaths)
- mfmaIdx: index of the MFMA emitted module in emittedModules
- paths: list of non-MFMA module-index paths to interleave between MFMAs
- preMfmaPaths: paths that must be emitted before the first MFMA
(reachable from the MFMA's before link)
"""
idToIdx = {em.moduleId: i for i, em in enumerate(emittedModules)}
n = len(emittedModules)
mfmaModuleIds = [i for i, em in enumerate(emittedModules) if em.opType == "mfma"]
assert len(mfmaModuleIds) == 1, "extractPathsFromBeforeDeps expects exactly one MFMA emitted module"
mfmaIdx = mfmaModuleIds[0]
nonMfmaIds = [i for i in range(n) if i != mfmaIdx]
nonMfmaSet = set(nonMfmaIds)
# Identify the non-MFMA module the MFMA depends on (if any).
mfmaBefore = emittedModules[mfmaIdx].before
preMfmaTarget = None
if mfmaBefore is not None:
bi = idToIdx.get(mfmaBefore)
if bi is not None and bi in nonMfmaSet:
preMfmaTarget = bi
# Each non-MFMA module has at most one predecessor, and each predecessor
# has at most one child, so paths are simple chains.
pred: List[int] = [-1 for _ in range(n)]
child: List[int] = [-1 for _ in range(n)]
for i in nonMfmaIds:
parent = -1
b = emittedModules[i].before
if b is not None:
bi = idToIdx.get(b)
if bi is not None and bi != i and bi in nonMfmaSet:
parent = bi
pred[i] = parent
if parent != -1:
assert child[parent] == -1, \
f"extractPathsFromBeforeDeps expects unique child per predecessor, got {child[parent]} and {i} for {parent}"
child[parent] = i
def _findHead(mid: int) -> int:
cur = mid
seen = [False for _ in range(n)]
while pred[cur] != -1 and not seen[cur]:
seen[cur] = True
cur = pred[cur]
return cur
def _walkFromHead(head: int, used: List[bool]) -> List[int]:
order: List[int] = []
localSeen = [False for _ in range(n)]
cur = head
while cur != -1 and not used[cur] and not localSeen[cur]:
order.append(cur)
localSeen[cur] = True
cur = child[cur]
return order
used = [False for _ in range(n)]
paths: List[List[int]] = []
for mid in nonMfmaIds:
if used[mid]:
continue
head = _findHead(mid)
order = _walkFromHead(head, used)
assert order, f"extractPathsFromBeforeDeps produced empty path for module {mid}"
for i in order:
used[i] = True
paths.append(order)
# Separate paths that the MFMA depends on (must go before first MFMA).
preMfmaPaths: List[List[int]] = []
regularPaths: List[List[int]] = []
for path in paths:
if preMfmaTarget is not None and preMfmaTarget in path:
preMfmaPaths.append(path)
else:
regularPaths.append(path)
return mfmaIdx, regularPaths, preMfmaPaths
def instructionSchedule(emittedModules, multiDU: bool = False,
minGapDsReadToWait=_MIN_MFMA_GAP_DS_READ_TO_WAIT_DEFAULT):
"""Interleave non-MFMA instructions between MFMAs using 2 slots/interval.
Rules (shared):
- MFMA order is preserved.
- Between two adjacent MFMAs there are 2 placement slots.
- At most one ds_read (LocalReadInstruction) per interval.
- Before dependencies are respected at module order level.
- Minimm distance between ds_read and it waitcnt (hardcoded for now)
- Module-internal instruction order is preserved.
- GR path is spread as much as possible across remaining valid slots.
When multiDU is False (single-DU / legacy path):
- LR path containing a WAIT_GR is packed from the end backwards.
When multiDU is True (MX multi-DU path):
- LR path containing a WAIT_GR is placed forward (wait_gr → sync → LR insts).
- wait_gr+barrier stay contiguous; GR m0/buffer_load defer until that path's
post-barrier ds_reads complete.
TODO : To be tested on multi-partition setup.
"""
if not emittedModules:
return Module()
isMFMA = lambda x: isinstance(x, (MFMAInstruction, MXMFMAInstruction))
n = len(emittedModules)
mfmaIdx, pathOrders, preMfmaOrders = extractPathsFromBeforeDeps(emittedModules)
mfmas = [x for x in emittedModules[mfmaIdx].instructions if isMFMA(x)]
def _emitPreMfma(result):
for order in preMfmaOrders:
for mid in order:
for inst in emittedModules[mid].instructions:
result.add(inst)
# Single MFMA: no slots to interleave into — emit preMfma, MFMA, then paths.
if len(mfmas) < 2:
result = Module()
_emitPreMfma(result)
for m in mfmas:
result.add(m)
for order in pathOrders:
for mid in order:
for inst in emittedModules[mid].instructions:
result.add(inst)
return result
paths = _classifyPaths(pathOrders, emittedModules)
rules = _SchedulingRules(totalSlots=(len(mfmas) - 1) * 2,
minGapDsReadToWait=minGapDsReadToWait)
base_validators = [rules.oneDsReadPerInterval, rules.minGapDsReadBeforeWait,
rules.minGapDsReadToWait, rules.noM0WithBufferLoad]
base_adjusters = [rules.spreadBufferLoads]
if multiDU:
base_validators.append(rules.grAfterWaitGrDsReads)
base_adjusters.insert(0, rules.deferGrAfterWaitGrDsReads)
placer = _SlotPlacer(
len(mfmas) - 1, n, pathOrders,
validators=base_validators,
adjusters=base_adjusters,
onPlace=rules.trackPlacement)
for order, hasWaitGR in paths:
if not order:
continue
reverse = (not multiDU) and hasWaitGR
pathInsts = _flattenPath(order, emittedModules, reverse=reverse)
rules.resetPath()
if not hasWaitGR:
rules.setupBufLoadSpreading(placer, pathInsts, order)
placer.placePath(pathInsts, reverse=reverse, multiDU=multiDU)
if multiDU and hasWaitGR:
rules.setPostWaitGrDsReadFence(placer)
scheduled = Module()
_emitPreMfma(scheduled)
scheduled.add(placer.assemble(mfmas))
# Post-pass: adjust vmcnt of any SWaitCnt to account for buffer_loads
# that the scheduler placed before it within this subIterK.
bufLoadCount = 0
for inst in scheduled.flatitems():
if _isBufferLoad(inst):
bufLoadCount += 1
elif _isWaitCnt(inst) and inst.vlcnt >= 0:
if getattr(inst, 'adjustVmcnt', True):
inst.vlcnt += bufLoadCount
return scheduled