Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/Components/Subtile/InstructionEmitter.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

691 lines
34 KiB
Python

# Copyright Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Instruction emitter for LogicalScheduler.
Converts the logical schedule (EmittedModule chains) into concrete GPU
instructions by dispatching each opType to its emit method.
"""
from __future__ import annotations
from Tensile.Components.Subtile.Kernel import emitMfmaInstruction
from Tensile.Components.Subtile.SubtileGREmit import (
emitSingleBufferLoad, globalReadPtrUpdates, globalReadLDSBufferSwap,
)
from Tensile.Components.Subtile.SubtileLREmit import (
emitSingleDsRead, localReadLDSBufferSwap,
)
from Tensile.Components.Subtile.SubtileScaleEmit import (
globalReadDoScaleSubtile, globalReadScalePtrUpdates,
)
from rocisa.code import Module
from rocisa.instruction import (
SWaitCnt, SBarrier, DSLoadB32, SCmpEQU32, SCmpLeU32,
SCBranchSCC1, SMovB32, VAddU32, VAndB32, VCmpGEI32, VCmpGTI32, VCmpLeI32,
VCmpLtI32, VCndMaskB32, VLShiftLeftB32, VLShiftRightB32, VMovB32, VSubI32,
)
from rocisa.instruction import SWaitTensorcnt
from rocisa.container import vgpr, sgpr, DSModifiers, ContinuousRegister
from rocisa.code import Label
class SWaitCntEx(SWaitCnt):
"""SWaitCnt with adjustVmcnt flag for the instruction scheduler post-pass.
isWaitGr is a typed marker the instruction scheduler uses to identify the
wait_gr SWaitCnt (instead of substring-matching the instruction comment).
"""
def __init__(self, adjustVmcnt=True, isWaitGr=True, **kwargs):
super().__init__(**kwargs)
self._adjustVmcnt = adjustVmcnt
self._isWaitGr = isWaitGr
@property
def adjustVmcnt(self):
return self._adjustVmcnt
@property
def isWaitGr(self):
return self._isWaitGr
def __deepcopy__(self, memo):
return SWaitCntEx(
adjustVmcnt=self._adjustVmcnt,
isWaitGr=self._isWaitGr,
vlcnt=self.vlcnt, vscnt=self.vscnt,
dscnt=self.dscnt, kmcnt=self.kmcnt,
comment=self.comment)
def _zigzag_order(rows, cols):
"""Return (row, col) pairs in boustrophedon (zigzag/snake) order.
Even rows traverse left-to-right, odd rows right-to-left.
Row transitions share the column coordinate with the last element
of the previous row, so every consecutive pair shares exactly one
coordinate -- guaranteeing a VGPR source-cache hit at every step.
"""
result = []
for r in range(rows):
if r % 2 == 0:
for c in range(cols):
result.append((r, c))
else:
for c in range(cols - 1, -1, -1):
result.append((r, c))
return result
class InstructionEmitter:
"""Emits GPU instructions for each opType in the LogicalScheduler output.
VGPR tile indexing uses placement-level tile maps (tileId → vgprTileId)
set by assign_vgpr_tiles(). Per-tensor VGPR tile lists are indexed by
vgprTileId. All tensors (A, B, SA, SB) use the same tile-map approach.
"""
def __init__(self, writer, kernel, config,
tileInfoA, tileInfoB, dtileInfo,
vgprTilesA, vgprTilesB,
scaleTileInfoA=None, scaleTileInfoB=None,
vgprTilesSA=None, vgprTilesSB=None,
tensorParametersA=None, tensorParametersB=None):
self.writer = writer
self.kernel = kernel
self.config = config
self.tileInfoA = tileInfoA
self.tileInfoB = tileInfoB
self.dtileInfo = dtileInfo
self.vgprTilesA = vgprTilesA
self.vgprTilesB = vgprTilesB
self.vgprTilesSA = vgprTilesSA or []
self.vgprTilesSB = vgprTilesSB or []
self.tensorParametersMap = {}
if tensorParametersA is not None:
self.tensorParametersMap['A'] = tensorParametersA
if tensorParametersB is not None:
self.tensorParametersMap['B'] = tensorParametersB
# Derived state
self.hasScale = scaleTileInfoA is not None and scaleTileInfoB is not None
self.subtileShapeK = tileInfoA.subtileShape[1]
self.tileInfoMap = {'A': tileInfoA, 'B': tileInfoB}
if self.hasScale:
self.tileInfoMap['SA'] = scaleTileInfoA
self.tileInfoMap['SB'] = scaleTileInfoB
# Per-uid K-window: numSubIterK / numUnroll[t]. Single-DU configs
# (numUnroll=1) span the full numSubIterK range; multi-DU slices it.
self._per_uid_k = {
t: config.numSubIterK // config.numUnroll.get(t, 1)
for t in ('A', 'B')
}
if self.hasScale:
self._per_uid_k['SA'] = config.numSubIterK // config.numUnroll.get('SA', 1)
self._per_uid_k['SB'] = config.numSubIterK // config.numUnroll.get('SB', 1)
# Dispatch table — unroll_iter is passed for mfma/lr
self._dispatch = {
'mfma': lambda em, ui: self.emit_mfma(em.source, ui),
'lr': lambda em, ui: self.emit_lr(em.source, ui),
'gr': lambda em, ui: self.emit_gr(em.source),
'wait_gr': lambda em, ui: self.emit_wait_gr(em.source),
'wait_lr': lambda em, ui: self.emit_wait_lr(),
'sync': lambda em, ui: self.emit_sync(),
'lr_inc': lambda em, ui: self.emit_lr_inc(em.source),
'gr_inc': lambda em, ui: self.emit_gr_inc(em.source),
'skip': lambda em, ui: self.emit_skip(em.source),
'mask_k': lambda em, ui: self.emit_mask_k(em.source),
'inline': lambda em, ui: self.emit_inline(em.source),
'gl2_prefetch': lambda em, ui: self.emit_gl2_prefetch(),
'gl2_prefetch_inc': lambda em, ui: self.emit_gl2_prefetch_inc(),
}
# Sentinel for the long-lived per-lane diff vgpr. Set by
# emit_mask_k_init, consumed by every emit_mask_k call in the tail body.
self._tail_vDiff = None
def emit_mfma(self, placement, unroll_iter=0):
"""Emit MFMA instructions from MFMAPlacement."""
module = Module()
subIterK = placement.subIterK
tile_maps = {t: placement.vgpr_tile_maps[t][unroll_iter]
for t in placement.vgpr_tile_maps}
aTiles = placement.tileA.tileId_list
bTiles = placement.tileB.tileId_list
hasWmmaSourceCache = self.writer.states.asmCaps.get("HasWMMA_V3", False)
if hasWmmaSourceCache and len(aTiles) > 1 and len(bTiles) > 1:
# Zigzag ordering: traverse the AxB tile grid in snake order so
# every consecutive WMMA pair shares exactly one operand (A or B),
# guaranteeing a VGPR source-cache hit at every step.
# Always sweep along the longer tile dimension for maximum reuse.
if len(aTiles) >= len(bTiles):
abPairs = [(aTiles[c], bTiles[r]) for r, c in _zigzag_order(len(bTiles), len(aTiles))]
else:
abPairs = [(aTiles[r], bTiles[c]) for r, c in _zigzag_order(len(aTiles), len(bTiles))]
else:
abPairs = [(a, b) for a in aTiles for b in bTiles]
for a, b in abPairs:
groupA = (a // self.config.lrA.mn) * self.config.lrA.mn
groupB = (b // self.config.lrB.mn) * self.config.lrB.mn
aTile = self.vgprTilesA[tile_maps['A'][groupA]]
bTile = self.vgprTilesB[tile_maps['B'][groupB]]
dTile = self.dtileInfo.vgprTiles[a + b * self.dtileInfo.localMMATileGrid[0]]
if self.hasScale:
scaleGroupA = (a // self.config.lrSA.mn) * self.config.lrSA.mn
scaleGroupB = (b // self.config.lrSB.mn) * self.config.lrSB.mn
scaleATile = self.vgprTilesSA[tile_maps['SA'][scaleGroupA]]
scaleBTile = self.vgprTilesSB[tile_maps['SB'][scaleGroupB]]
scaleAVgpr = next(iter(scaleATile))
scaleBVgpr = next(iter(scaleBTile))
mShapeA = self.tileInfoMap['SA'].lrSubtileShape[0]
mShapeB = self.tileInfoMap['SB'].lrSubtileShape[0]
kShapeA = self.tileInfoMap['SA'].lrSubtileShape[1]
kShapeB = self.tileInfoMap['SB'].lrSubtileShape[1]
sAsel = (a % mShapeA) + mShapeA * (subIterK % kShapeA)
sBsel = (b % mShapeB) + mShapeB * (subIterK % kShapeB)
else:
scaleAVgpr = scaleBVgpr = -1
sAsel = sBsel = 0
module.add(emitMfmaInstruction(
self.writer, self.kernel, aTile, bTile, dTile, dTile,
scaleAVgpr=scaleAVgpr, scaleBVgpr=scaleBVgpr,
scaleAsel=sAsel, scaleBsel=sBsel,
comment=f"MFMA C[{a},{b}] += A[{a},K={subIterK}] * B[{b},K={subIterK}]"))
return list(module.flatitems())
def emit_lr(self, placement, unroll_iter=0):
"""Emit LR (ds_read) instructions from LRPlacement."""
module = Module()
tensor = placement.tensor
tile_map = placement.vgpr_tile_map[unroll_iter] if placement.vgpr_tile_map else {}
if tensor in ('A', 'B'):
ti = self.tileInfoMap[tensor]
vgprTiles = self.vgprTilesA if tensor == 'A' else self.vgprTilesB
lrGran = self.config.lrA if tensor == 'A' else self.config.lrB
# `per_uid_k` (= numSubIterK / numUnroll) folds k into a
# uid-local subtile coordinate so multi-DU (numUnroll > 1)
# uids each iterate [0, per_uid_k). Under single-DU
# (numUnroll == 1) per_uid_k == numSubIterK and the modulo
# is a no-op for in-window k, but it ALIASES wrap-LRs that
# use k >= numSubIterK to address the alternate LDS half
# (PGR>=1 prefetch target) back to k=0 — losing the half
# offset. Skip the modulo in that case.
nUnroll = self.config.numUnroll.get(tensor, 1)
per_uid_k = self._per_uid_k[tensor] if nUnroll > 1 else None
for tileId in range(placement.tiles.tileId_start, placement.tiles.tileId_end, lrGran.mn):
for k in range(placement.tiles.subIterK_start, placement.tiles.subIterK_end, lrGran.k):
local_k = (k % per_uid_k) if per_uid_k is not None else k
subtileK = local_k // self.subtileShapeK
subIterK_within = local_k % self.subtileShapeK
dstTile = vgprTiles[tile_map[tileId]]
swizzled = self.writer.states.subtileLdsSwizzle
module.add(emitSingleDsRead(
ti, tileId, subtileK, subIterK_within, dstTile, swizzled=swizzled))
elif tensor in ('SA', 'SB'):
tc = 'MXSA' if tensor == 'SA' else 'MXSB'
ti = self.tileInfoMap[tensor]
lrGran = self.config.lrSA if tensor == 'SA' else self.config.lrSB
vgprTilesScale = self.vgprTilesSA if tensor == 'SA' else self.vgprTilesSB
for tileId in range(placement.tiles.tileId_start, placement.tiles.tileId_end, lrGran.mn):
scaleGroupIdx = tileId // lrGran.mn
groupKey = scaleGroupIdx * lrGran.mn
kGroupIdx = placement.tiles.subIterK_start // ti.lrSubtileShape[1]
numKGroups = ti.lrLocalSubtileGrid[1]
dsOffset = int(ti.lrSubtileSize) * (scaleGroupIdx * numKGroups + kGroupIdx)
vdst = next(iter(vgprTilesScale[tile_map[groupKey]]))
module.add(DSLoadB32(
dst=vgpr(vdst),
src=vgpr(ti.sharedVgprLROffset[0]),
ds=DSModifiers(offset=dsOffset),
comment=f"scale{tc}[group{scaleGroupIdx},K={placement.tiles.subIterK_start}]: load 4B from LDS"))
return list(module.flatitems())
def emit_gr(self, placement):
"""Emit GR (buffer_load) instructions from GRPlacement."""
module = Module()
tensor = placement.tensor
if tensor in ('A', 'B'):
ti = self.tileInfoMap[tensor]
grGran = self.config.grA if tensor == 'A' else self.config.grB
uid_k_base = placement.unrollId * grGran.k
for tileId in range(placement.tiles.tileId_start, placement.tiles.tileId_end, grGran.mn):
for k in range(placement.tiles.subIterK_start, placement.tiles.subIterK_end, grGran.k):
subtileK = (k - uid_k_base) // self.subtileShapeK
module.add(emitSingleBufferLoad(ti, self.kernel, tileId, subtileK))
elif tensor in ('SA', 'SB'):
tc = 'MXSA' if tensor == 'SA' else 'MXSB'
module.add(globalReadDoScaleSubtile(tc, self.writer, self.kernel))
return list(module.flatitems())
def emit_wait_gr(self, source):
"""Emit SWaitCnt for wait_gr from BaseOp with wait_gr_counts."""
counts = source.wait_gr_counts
if counts is None:
return []
# force_drain means "wait for every outstanding global read to retire"
# (full vmcnt(0) drain); the per-tensor counts are kept only for the
# diagnostic comment. See WaitGROp.force_drain in LogicalScheduler.py.
force_drain = getattr(source, 'force_drain', False)
if self.kernel.get("enableTDMA", False) and self.kernel.get("enableTDMB", False):
tdmCnt = 0 if force_drain else (counts.A + counts.B + counts.SA + counts.SB)
label = "full drain" if force_drain else "tensor_load_to_lds"
return [SWaitTensorcnt(tensorcnt=tdmCnt,
comment=f"Wait TDM ({label}): A={counts.A} B={counts.B} SA={counts.SA} SB={counts.SB}")]
# TODO. Hardcoded for now, but we should just get this from atomic emit codes (emitSingleBufferLoad, ...)
grMap = {'A': max(1,int(1.0/self.tileInfoA.loadRatioGR)),
'B': max(1,int(1.0/self.tileInfoB.loadRatioGR)),
'SA': 1,
'SB': 1}
if force_drain:
grCnt = 0
label = "full drain"
else:
grCnt = (counts.A * grMap['A'] +
counts.B * grMap['B'] +
counts.SA * grMap['SA'] +
counts.SB * grMap['SB'])
label = "per-subIterK"
swait = SWaitCntEx(vlcnt=grCnt, vscnt=-1,
adjustVmcnt=source.adjustVmcnt,
comment=f"Wait GR ({label}): A={counts.A} B={counts.B} SA={counts.SA} SB={counts.SB}")
return [swait]
def emit_wait_lr(self):
return [SWaitCnt(dscnt=0, vlcnt=-1, vscnt=-1,
comment="Wait for LR to complete")]
def emit_sync(self):
return [SBarrier(comment="Barrier")]
def emit_inline(self, source):
"""Emit a writer-built Module supplied by an InlineModuleOp callback."""
if source.build is None:
return []
mod = source.build(self)
return list(mod.flatitems()) if mod is not None else []
def emit_lr_inc(self, source):
"""Emit localReadLDSBufferSwap for a single tensor."""
tensor = source.tensor
tc = {'A': 'A', 'B': 'B', 'SA': 'MXSA', 'SB': 'MXSB'}.get(tensor, tensor)
module = Module()
module.add(localReadLDSBufferSwap(tc, self.writer, self.kernel))
return list(module.flatitems())
def emit_gr_inc(self, source):
"""Emit globalReadPtrUpdates + globalReadLDSBufferSwap for a single tensor."""
tensor = source.tensor
tc = {'A': 'A', 'B': 'B', 'SA': 'MXSA', 'SB': 'MXSB'}.get(tensor, tensor)
module = Module()
if tensor in ('SA', 'SB'):
module.add(globalReadScalePtrUpdates(tc, self.writer, self.kernel))
else:
module.add(globalReadPtrUpdates(tc, self.writer, self.kernel))
module.add(globalReadLDSBufferSwap(tc, self.writer, self.kernel))
return list(module.flatitems())
def emit_gl2_prefetch(self):
"""Emit GL2 prefetch loads (global_prefetch_b8) for all tensors."""
writer = self.writer
kernel = self.kernel
tPA = self.tensorParametersMap['A']
tPB = self.tensorParametersMap['B']
mod = writer.gl2PrefetchIssueLoad(kernel, tPA, tPB)
return list(mod.flatitems())
def emit_gl2_prefetch_inc(self):
"""Emit GL2 prefetch address increment with end-of-K guard.
Mirrors the SIA path: when LoopCounterL <= PGR + PrefetchGL2,
zero the increment SGPRs so prefetch stops advancing past K.
Then advance all GL2 prefetch addresses by the (possibly zeroed) increment.
"""
writer = self.writer
kernel = self.kernel
tPA = self.tensorParametersMap['A']
tPB = self.tensorParametersMap['B']
from rocisa.code import Module
from rocisa.instruction import SCmpLeU32, SCMovB32
from rocisa.container import sgpr
mod = Module("GL2 Prefetch Increment")
loopCounter = writer.loopCounter(kernel, writer.states.unrollIdx)
pgl = kernel["PrefetchGL2"]
pgr = kernel["PrefetchGlobalRead"]
mod.add(SCmpLeU32(src0=loopCounter, src1=pgr + pgl,
comment=f"counterL <= PGR({pgr})+PGL({pgl})?"))
mod.add(SCMovB32(dst=sgpr("GL2PrefetchIncA"), src=0))
mod.add(SCMovB32(dst=sgpr("GL2PrefetchIncB"), src=0))
if kernel["ProblemType"].get("MXBlockA", 0):
mod.add(SCMovB32(dst=sgpr("GL2PrefetchIncMXSA"), src=0))
if kernel["ProblemType"].get("MXBlockB", 0):
mod.add(SCMovB32(dst=sgpr("GL2PrefetchIncMXSB"), src=0))
mod.add(writer.gl2PrefetchIncrementAddr(kernel, tPA, tPB))
return list(mod.flatitems())
def emit_skip(self, source):
"""Emit skip guard: compare LoopCounterL and branch."""
labelName = source.target if source.rawLabel else f"SkipTo{source.target}"
skipLabel = Label(labelName, "")
cmpMap = {"EQ": SCmpEQU32, "LE": SCmpLeU32}
cmpCls = cmpMap[source.compare]
module = Module()
# SCMP inline range is -16..64 (signed); stage non-inline via scratch sgpr.
if -16 <= source.value <= 64:
module.add(cmpCls(
src0=sgpr("LoopCounterL"), src1=source.value,
comment=f"LoopCounter {source.compare} {source.value}?"))
else:
with self.writer.allocTmpSgpr(1, tag="InstructionEmitter_skip_tmpSgpr") as litSgprInfo:
litSgpr = litSgprInfo.idx
module.add(SMovB32(
dst=sgpr(litSgpr), src=hex(source.value),
comment=f"stage literal {source.value} (non-inline) for cmp src1"))
module.add(cmpCls(
src0=sgpr("LoopCounterL"), src1=sgpr(litSgpr),
comment=f"LoopCounter {source.compare} {source.value}?"))
module.add(SCBranchSCC1(
labelName=skipLabel.getLabelName(),
comment=source.branchComment or f"skip to {source.target}"))
return list(module.flatitems())
def _mfma_K_constants(self):
"""Constants used by both mask emitters.
Returns (numMIInUnroll, dividerFortidInK):
* numMIInUnroll = MI_M * MI_K / WavefrontSize — per-lane K chunk
consumed by one MFMA call (contiguous K-elements held by each lane).
* dividerFortidInK = MI_M — lanes with the same
(Serial % WavefrontSize) / MI_M share the same K-position;
WavefrontSize / MI_M = number of K-groups in the wave.
"""
kernel = self.kernel
MI_M = kernel["MatrixInstM"]
MI_K = kernel["MatrixInstK"]
waveSize = kernel["WavefrontSize"]
numMIInUnroll = MI_M * MI_K // waveSize
dividerFortidInK = MI_M
return numMIInUnroll, dividerFortidInK
def emit_mask_k_init(self):
"""Stage the per-subIterK invariants for emit_mask_k.
Only two vgprs survive past this routine and into emit_mask_k:
* self._tail_vDiff — rem - laneK_0 (for subIterK=n the
effective diff is diff - n*MatrixInstK, folded into cmp constants)
* self._tail_boundaryMask[4] — BF16 only; precomputed boundary masks
indexed by vgpr i, derived from d = rem % 8
"""
numMIInUnroll, dividerFortidInK = self._mfma_K_constants()
writer = self.writer
module = Module()
# Compute K per lane (numMIInUnroll, dividerFortidInK are power of 2)
waveSize = self.kernel["WavefrontSize"]
divLog2 = (dividerFortidInK).bit_length() - 1
mulLog2 = (numMIInUnroll).bit_length() - 1
kReg = writer.vgprPool.checkOut(1, "tail_kReg")
module.add(VAndB32(
dst=vgpr(kReg), src0=waveSize - 1, src1=vgpr("Serial"),
comment=f"tail_kReg = Serial & {waveSize - 1} (Serial % {waveSize})"))
module.add(VLShiftRightB32(
dst=vgpr(kReg), shiftHex=divLog2, src=vgpr(kReg),
comment=f"tail_kReg >>= {divLog2} (tail_kReg / {dividerFortidInK})"))
# laneK_0 and diff = rem - laneK_0 are shared across all subIterK.
workKVgpr = writer.vgprPool.checkOut(1, "tail_workK")
self._tail_vDiff = writer.vgprPool.checkOut(1, "tail_vDiff")
loopCounterName = writer.loopCounterName(
self.kernel, writer.states.unrollIdx)
module.add(VLShiftLeftB32(
dst=vgpr(workKVgpr), shiftHex=mulLog2, src=vgpr(kReg),
comment=f"laneK_0 = tail_kReg << {mulLog2} (tail_kReg * {numMIInUnroll})"))
writer.vgprPool.checkIn(kReg)
module.add(VSubI32(
dst=vgpr(self._tail_vDiff),
src0=sgpr(loopCounterName), src1=vgpr(workKVgpr),
comment="diff = rem - laneK_0 (shared across all subIterK)"))
writer.vgprPool.checkIn(workKVgpr)
self._tail_boundaryMask = None
laneSGPRCount = writer.states.laneSGPRCount
if self.kernel["ProblemType"]["DataTypeA"].isBFloat16():
halfMaskVgpr = writer.vgprPool.checkOut(1, "tail_halfMask")
module.add(VMovB32(
dst=vgpr(halfMaskVgpr), src="0x0000FFFF",
comment="BF16 half-mask: keep K0 (low 16b), zero K1 (high 16b)"))
# Precompute the boundary masks from d = rem % numMIInUnroll.
# The boundary-mask pattern (which vgprs are full/half/zero)
# depends only on rem % numMIInUnroll: laneK_0 is always a
# multiple of numMIInUnroll, and (assuming MatrixInstK is a
# multiple of numMIInUnroll) every "boundary" lane in every
# subIterK has effective_diff ≡ rem (mod numMIInUnroll). So all
# boundary lanes share the same mask pattern (one per vgpr held
# per lane: numMIInUnroll // kStride masks).
kStride = 2 # BF16: 2 K-elements packed per 32-bit vgpr
assert numMIInUnroll % kStride == 0, \
f"numMIInUnroll ({numMIInUnroll}) must be a multiple of kStride ({kStride})"
numBoundaryMasks = numMIInUnroll // kStride
vDLaneRem = writer.vgprPool.checkOut(1, "tail_vDLaneRem")
module.add(VAndB32(
dst=vgpr(vDLaneRem),
src0=sgpr(loopCounterName), src1=numMIInUnroll - 1,
comment=f"d = rem % {numMIInUnroll} (boundary-mask pattern depends only on this)"))
self._tail_boundaryMask = [
writer.vgprPool.checkOut(1, f"tail_boundaryMask{i}")
for i in range(numBoundaryMasks)
]
with writer.allocTmpSgpr(laneSGPRCount, alignment=laneSGPRCount, tag="InstructionEmitter_mask_k_init_tmpSgpr") as tmpSgprInfo:
maskSgpr = tmpSgprInfo.idx
for i in range(numBoundaryMasks):
bm = self._tail_boundaryMask[i]
hiBound = i * kStride + kStride # d < hiBound → half-keep
loBound = i * kStride + 1 # d < loBound → zero
module.add(VCmpLtI32(
dst=sgpr(maskSgpr, laneSGPRCount),
src0=vgpr(vDLaneRem), src1=hiBound,
comment=f"boundary[{i}]: d < {hiBound} ? halfKeep : full"))
module.add(VCndMaskB32(
dst=vgpr(bm),
src0=-1,
src1=vgpr(halfMaskVgpr),
src2=sgpr(maskSgpr, laneSGPRCount),
comment=f"boundaryMask[{i}] = (d<{hiBound}) ? halfKeep : full"))
module.add(VCmpLtI32(
dst=sgpr(maskSgpr, laneSGPRCount),
src0=vgpr(vDLaneRem), src1=loBound,
comment=f"boundary[{i}]: d < {loBound} ? 0 : prev"))
module.add(VCndMaskB32(
dst=vgpr(bm), src0=vgpr(bm), src1=0,
src2=sgpr(maskSgpr, laneSGPRCount),
comment=f"boundaryMask[{i}] = (d<{loBound}) ? 0 : prev"))
writer.vgprPool.checkIn(halfMaskVgpr)
writer.vgprPool.checkIn(vDLaneRem)
return list(module.flatitems())
def emit_mask_k(self, source):
"""Per-lane K-mask for one subIterK, applied to A/B tiles via V_AND_B32.
Uses diff = rem - laneK_0 from emit_mask_k_init. For subIterK=n the
effective per-lane diff is diff - n*MatrixInstK, folded into the
cmp immediates (no per-call sub).
BF16 (kStride=2 K positions per vgpr) builds a per-vgpr 3-state mask
from the precomputed boundary masks (full / boundary[i] / zero).
Non-BF16 (e.g. FP4) builds a single 2-state mask shared across all
vgprs, assuming rem aligns to the per-lane K stride (true for
rem=32 with FP4 MIK=128). A boundary inside a non-BF16 vgpr would
need per-byte/nibble handling.
"""
assert self._tail_vDiff is not None, \
"emit_mask_k_init must run before emit_mask_k"
writer = self.writer
kernel = self.kernel
subIterK = source.subIterK
kBaseConst = subIterK * kernel["MatrixInstK"]
laneSGPRCount = writer.states.laneSGPRCount
isBF16 = kernel["ProblemType"]["DataTypeA"].isBFloat16()
kStride = 2 # BF16: 2 elements packed per 32-bit vgpr (low=K0, high=K1)
module = Module()
def _unique_ids(key):
m = source.vgpr_tile_map.get(key, [{}])[0]
return sorted(set(m.values()))
aIds, bIds = _unique_ids('A'), _unique_ids('B')
# All A/B tiles share the same vgprs-per-lane count and K layout,
# so the i-th vgpr of every tile gets the same mask.
refTiles = self.vgprTilesA if aIds else self.vgprTilesB
refIds = aIds or bIds
vgprPerInUnroll = len(list(refTiles[refIds[0]])) if refIds else 0
with writer.allocTmpSgpr(laneSGPRCount, alignment=laneSGPRCount, tag="InstructionEmitter_mask_k_tmpSgpr") as tmpSgprInfo:
maskSgpr = tmpSgprInfo.idx
def _emit_cmp(cmpCls, literal, comment):
# VOPC inline range is -16..64; stage out-of-range via scratch sgpr
# (e.g. BF16 MI_K=32 subIterK>=2, or FP4 MI_K=128 subIterK>=1).
if -16 <= literal <= 64:
module.add(cmpCls(
dst=sgpr(maskSgpr, laneSGPRCount),
src0=vgpr(self._tail_vDiff), src1=literal,
comment=comment))
else:
with writer.allocTmpSgpr(1, tag="InstructionEmitter_mask_k_litSgprInfo") as litSgprInfo:
litSgpr = litSgprInfo.idx
module.add(SMovB32(
dst=sgpr(litSgpr), src=hex(literal),
comment=f"stage literal {literal} (non-inline)"))
module.add(cmpCls(
dst=sgpr(maskSgpr, laneSGPRCount),
src0=vgpr(self._tail_vDiff), src1=sgpr(litSgpr),
comment=comment))
if isBF16:
# 3-way per (lane, subIterK), 2 cmps shared across all i:
# sFull = effective_diff_n >= numMIInUnroll → -1
# sZero = effective_diff_n <= 0 → 0
# else → boundary[i]
numMIInUnroll = vgprPerInUnroll * kStride
fullLit = kBaseConst + numMIInUnroll - 1
zeroLit = kBaseConst
maskVgprs = [writer.vgprPool.checkOut(1, f"mask_k_msk{i}_k{subIterK}")
for i in range(vgprPerInUnroll)]
_emit_cmp(VCmpGTI32, fullLit,
f"sFull: diff > {fullLit} (effective_diff_{subIterK} >= {numMIInUnroll})")
for i in range(vgprPerInUnroll):
module.add(VCndMaskB32(
dst=vgpr(maskVgprs[i]),
src0=vgpr(self._tail_boundaryMask[i]), src1=-1,
src2=sgpr(maskSgpr, laneSGPRCount),
comment=f"mask[{i}] = sFull ? full : boundary[{i}]"))
_emit_cmp(VCmpLeI32, zeroLit,
f"sZero: diff <= {zeroLit} (effective_diff_{subIterK} <= 0)")
for i in range(vgprPerInUnroll):
module.add(VCndMaskB32(
dst=vgpr(maskVgprs[i]), src0=vgpr(maskVgprs[i]), src1=0,
src2=sgpr(maskSgpr, laneSGPRCount),
comment=f"mask[{i}] = sZero ? 0 : prev"))
else:
# 2-state shared mask: diff < kBaseConst+1 → 0, else -1.
literal = kBaseConst + 1
sharedMask = writer.vgprPool.checkOut(1, f"mask_k_msk_k{subIterK}")
maskVgprs = [sharedMask] * vgprPerInUnroll
_emit_cmp(VCmpLtI32, literal,
f"mask: diff < {literal} (laneK_{subIterK} >= rem)")
module.add(VCndMaskB32(
dst=vgpr(sharedMask), src0=-1, src1=0,
src2=sgpr(maskSgpr, laneSGPRCount),
comment=f"mask = (diff < {literal}) ? 0 : -1"))
# MX-scaled paths apply the K mask via the scale lanes (host
# zero-pads OOB scale bytes to 0x00, which v_mfma_scale treats
# as ~2^-127 and underflows the contribution to zero). Emitting
# a second data mask here is redundant and, in practice on
# gfx950, the per-lane V_AND against the cndmask-derived mask
# corrupts the v_mfma_scale operand bypass and double-zeros
# valid residual lanes (~0.5% wrong outputs at K%MIK!=0).
#
# Skip the data K-mask for the MX paths that the gfx950 corruption
# actually hits: all multi-DU MX kernels, and all 8-bit-float MX
# (MXFP8) kernels regardless of unroll depth. GPU-validated on
# gfx950 (full-element, K%MIK!=0 tails): single-DU MXFP8 fails with
# the mask and passes without it, while MXFP4 (4-bit) passes either
# way -- so single-DU MXFP4 keeps the mask and stays develop-
# identical, but single-DU MXFP8 must skip it like multi-DU does.
_emit_mask_multi_du = (self.config.numUnroll.get('A', 1) > 1
or self.config.numUnroll.get('B', 1) > 1)
_data_is_8bit_float = kernel["ProblemType"]["DataTypeA"].is8bitFloat()
_skip_data_kmask = self.hasScale and (_emit_mask_multi_du
or _data_is_8bit_float)
if not _skip_data_kmask:
for label, ids, tilesDict in (("A", aIds, self.vgprTilesA),
("B", bIds, self.vgprTilesB)):
for tid in ids:
for i, v in enumerate(list(tilesDict[tid])):
module.add(VAndB32(
dst=vgpr(v), src0=vgpr(v), src1=vgpr(maskVgprs[i]),
comment=f"mask {label}[{i}] (K=[{i*kStride},{i*kStride+kStride-1}])"))
# Scale-mask reuse (non-BF16 hasScale only): AND the scale
# vgprs with the data mask we just built. Redundant when
# host zero-padding leaves OOB scale bytes at 0x00 (0x00
# AND 0 = 0x00) but kept to stay explicit under any input
# pattern.
scaleStride = self.config.lrSA.k if self.hasScale else 0
if (not isBF16) and self.hasScale \
and (self.vgprTilesSA or self.vgprTilesSB) \
and scaleStride > 0 and (subIterK % scaleStride == 0):
for tensor, tilesList in (('SA', self.vgprTilesSA),
('SB', self.vgprTilesSB)):
liveIds = sorted(set(
source.vgpr_tile_map.get(tensor, [{}])[0].values()))
for tid in liveIds:
for v in list(tilesList[tid]):
module.add(VAndB32(
dst=vgpr(v), src0=vgpr(v),
src1=vgpr(maskVgprs[0]),
comment=f"mask scale vgpr (reuse A/B mask, subIterK={subIterK})"))
for m in set(maskVgprs):
writer.vgprPool.checkIn(m)
return list(module.flatitems())
def emit_mask_k_done(self):
"""Release the long-lived tail-loop vgprs (scratch ones are released
at the end of emit_mask_k_init)."""
if getattr(self, "_tail_vDiff", None) is not None:
self.writer.vgprPool.checkIn(self._tail_vDiff)
self._tail_vDiff = None
if getattr(self, "_tail_boundaryMask", None) is not None:
for bm in self._tail_boundaryMask:
self.writer.vgprPool.checkIn(bm)
self._tail_boundaryMask = None
return []
def populate(self, emitted, unroll_iter=0):
"""Walk emitted partitions and fill em.instructions."""
for partition_emitted in emitted:
for emitted_group in partition_emitted:
for em in emitted_group:
handler = self._dispatch.get(em.opType)
if handler:
em.instructions = handler(em, unroll_iter)