Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/Tests/unit/test_SubtileBasedLogicalScheduler.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

4241 lines
180 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for LogicalScheduler — validates each scheduling pass.
Organized by pass:
1. PlaceLRs — MFMA + Local Read placement
2. AssignVgprTiles — VGPR tile ID allocation
3. PlaceGRs — Global Read placement
4. AnnotateDeps — Raw dependency annotation
5. RemoveUnnecessaryGrDeps / RemoveUnnecessaryLrDeps — Dep pruning
6. RemoveCrossDeps — Cross-subIterK dep → preOp conversion
7. InsertGrLrInc — MT iteration increment insertion
8. GroupLrGr — Chain grouping with merged preOps
9. RemoveUnnecessaryWaitLrSync — Sync downgrade
10. Emit — EmittedModule chain generation
11. BuildPreloop / BuildNGLL / BuildNLL — Loop variant construction
12. Integration — Full pipeline with real instructions
"""
import pytest
from Tensile.Components.Subtile.Kernel import (
TileInfo, AB_B8, AB_B16, AB_B16_W32, AB_B4, MXSA_B4, MXSB_B4, CD_F32, CD_F32_W32,
)
from Tensile.Components.Subtile.LogicalScheduler import (
GRPlacementStrategy,
LogicalScheduler,
MFMATileRange,
Pass,
ReadGranularity,
SchedulerConfig,
EmittedModule,
LRPlacement,
GRPlacement,
Dep,
WaitGRCounts,
fmt_mt,
)
from unittest.mock import MagicMock
from rocisa.code import Module
def makeTileInfo(tc, kernel):
"""Compatibility wrapper: select geometry from kernel config and return TileInfo."""
fp4 = kernel["ProblemType"].get("MXBlockA", 0) > 0
wave32 = kernel.get("WavefrontSize", 64) == 32
ab_bf16 = AB_B16_W32 if wave32 else AB_B16
cd_f32 = CD_F32_W32 if wave32 else CD_F32
_geo = {
'A': AB_B4 if fp4 else ab_bf16,
'B': AB_B4 if fp4 else ab_bf16,
'MXSA': MXSA_B4,
'MXSB': MXSB_B4,
'D': cd_f32,
}
# TDM path queries writer.states.subtileLdsSwizzle in TileInfo.__init__.
# Provide a minimal stub so makeTileInfo can be called without a full writer.
writer_stub = None
if kernel.get("enableTDM%s" % tc, False):
from types import SimpleNamespace
writer_stub = SimpleNamespace(states=SimpleNamespace(subtileLdsSwizzle=False))
return TileInfo(_geo[tc], tc, writer_stub, kernel)
# ── Shared fixtures ───────────────────────────────────────────
def _mock_dtype(num_bytes=2):
mock = MagicMock()
mock.numBytes.return_value = num_bytes
mock.numRegisters.return_value = num_bytes / 4
# Treat the default 2-byte mock as BF16 (only consumer is the Subtile tail
# mask path, which dispatches on isBFloat16); 0.5-byte (fp4) returns False.
mock.isBFloat16.return_value = (num_bytes == 2)
# 8-bit float (MXFP8) is exactly the 1-byte mock; fp4 (0.5) and bf16 (2)
# are not. Without this, MagicMock's auto-attribute makes is8bitFloat()
# truthy for every dtype and the data-K-mask 8-bit-float skip misfires.
mock.is8bitFloat.return_value = (num_bytes == 1)
return mock
def create_kernel(MT0=256, MT1=256, fp4=False, depthU=None,
miWaveGroup=None, sourceSwap=False, arch="gfx950"):
mxblock = 32 if fp4 else 0
bpe = 0.5 if fp4 else 2
matrixInstK = 128 if fp4 else 32
if depthU is None:
depthU = 256 if fp4 else 64
if miWaveGroup is None:
miWaveGroup = [2, 2]
is_gfx1250 = (arch == "gfx1250")
waveSize = 32 if is_gfx1250 else 64
dtype = _mock_dtype(bpe)
problemType = {
"DataTypeA": dtype,
"DataTypeB": dtype,
"ComputeDataType": _mock_dtype(4),
}
if fp4:
problemType["MXBlockA"] = mxblock
problemType["MXBlockB"] = mxblock
kernel = {
"DepthU": depthU,
"_DepthUA": depthU,
"_DepthUB": depthU,
"MacroTileA": MT0,
"MacroTileB": MT1,
"MacroTile0": MT0,
"MacroTile1": MT1,
"MatrixInstM": 16,
"MatrixInstN": 16,
"MatrixInstK": matrixInstK,
"MatrixInstB": 1,
"MIInputPerThreadA": matrixInstK // 4,
"MIInputPerThreadB": matrixInstK // 4,
"MIWaveGroup": list(miWaveGroup),
"WavefrontSize": waveSize,
"SourceSwap": sourceSwap,
"MIArchVgpr": is_gfx1250,
"NonTemporalA": 0,
"NonTemporalB": 0,
"NonTemporalMXSA": 0,
"NonTemporalMXSB": 0,
"NoTailLoop": False,
"ProblemType": problemType,
}
if is_gfx1250:
kernel["ISA"] = (12, 5, 0)
kernel["UseSubtileImpl"] = True
kernel["enableTDMA"] = True
kernel["enableTDMB"] = True
if fp4:
kernel["_DepthUMXSA"] = depthU // mxblock
kernel["_DepthUMXSB"] = depthU // mxblock
return kernel
def make_cfg_256x256_fp4(depthU=256, k_gran=1, partSizeM=0, partSizeN=0,
grSA_k_gran=1, grSA_mn_gran=1, grSB_k_gran=1, grSB_mn_gran=1, pgr=2,
miWaveGroup=None):
"""Build FP4 config with scale tensors. k_gran applies to LR A/B.
grSA/SB_k_gran and grSA/SB_mn_gran are multipliers on the tile-info-derived values."""
kernel = create_kernel(256, 256, fp4=True, depthU=depthU, miWaveGroup=miWaveGroup)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
return SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=k_gran),
lrB=ReadGranularity(mn=1, k=k_gran),
grA=grA,
grB=grB,
lrSA=ReadGranularity(mn=2, k=2),
lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0] * grSA_mn_gran,
k=scaleTiA.localMMATileGrid[1] * grSA_k_gran),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0] * grSB_mn_gran,
k=scaleTiB.localMMATileGrid[1] * grSB_k_gran),
partitionSizeM=partSizeM,
partitionSizeN=partSizeN,
pgr=pgr,
)
def make_cfg_bf16(MT0=256, MT1=256, depthU=64, partSizeM=0, partSizeN=0,
miWaveGroup=None, sourceSwap=False, lrA=None, lrB=None):
"""Build BF16 config without scale tensors."""
kernel = create_kernel(MT0, MT1, fp4=False, depthU=depthU,
miWaveGroup=miWaveGroup, sourceSwap=sourceSwap)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
if lrA is None:
lrA = ReadGranularity(mn=1, k=1)
if lrB is None:
lrB = ReadGranularity(mn=1, k=1)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
return SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=lrA,
lrB=lrB,
grA=grA,
grB=grB,
partitionSizeM=partSizeM,
partitionSizeN=partSizeN,
)
def make_cfg_bf16_pgr0(MT0=256, MT1=256, depthU=64):
"""Build BF16 config with pgr=0."""
kernel = create_kernel(MT0, MT1, fp4=False, depthU=depthU)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
return SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
pgr=0,
)
def make_cfg_bf16_pgr1(MT0=256, MT1=256, depthU=128, partSizeM=0, partSizeN=0):
"""Build BF16 config with pgr=1."""
kernel = create_kernel(MT0, MT1, fp4=False, depthU=depthU)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
return SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
partitionSizeM=partSizeM,
partitionSizeN=partSizeN,
pgr=1,
)
def create_kernel_fp8(MT0, MT1, waveGroup, depthU=128):
"""Create a plain FP8 kernel config (bpe=1, matrixInstK=128, no MX scale)."""
dtype = _mock_dtype(1)
return {
"DepthU": depthU,
"_DepthUA": depthU,
"_DepthUB": depthU,
"MacroTileA": MT0,
"MacroTileB": MT1,
"MacroTile0": MT0,
"MacroTile1": MT1,
"MatrixInstM": 16,
"MatrixInstN": 16,
"MatrixInstK": 128,
"MIWaveGroup": list(waveGroup),
"WavefrontSize": 64,
"SourceSwap": False,
"MIArchVgpr": False,
"NonTemporalA": 0,
"NonTemporalB": 0,
"NonTemporalMXSA": 0,
"NonTemporalMXSB": 0,
"ProblemType": {
"DataTypeA": dtype,
"DataTypeB": dtype,
"ComputeDataType": _mock_dtype(4),
},
}
def make_cfg_fp8(MT0, MT1, waveGroup, depthU=128, pgr=2):
"""Build plain FP8 config (AB_B8, matrixInstK=128, no scale tensors).
GR granularity matches production: mn=subtileShape[0]=1, k=subtileShape[1]=1.
With DU=128 and matrixInstK=128: numSubIterK=1, flat_len=numPartitions.
"""
kernel = create_kernel_fp8(MT0, MT1, waveGroup, depthU)
tiA = TileInfo(AB_B8, 'A', None, kernel)
tiB = TileInfo(AB_B8, 'B', None, kernel)
return SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=tiA.subtileShape[0], k=tiA.subtileShape[1]),
grB=ReadGranularity(mn=tiB.subtileShape[0], k=tiB.subtileShape[1]),
pgr=pgr,
)
def make_example_granularities_1():
"""Example Granularities 1 from the design doc: LR A,B=1x1, LR SA,SB=2x2."""
return SchedulerConfig(
numMFMATilesM=2,
numMFMATilesN=2,
numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
lrSA=ReadGranularity(mn=2, k=2),
lrSB=ReadGranularity(mn=2, k=2),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
grSA=ReadGranularity(mn=2, k=2),
grSB=ReadGranularity(mn=2, k=2),
)
def make_writer_and_tileinfos(kernel, fp4=False):
"""Create writer with register pools and TileInfos for integration tests."""
from types import SimpleNamespace
from rocisa import rocIsa
from rocisa.register import RegisterPool
from rocisa.enum import RegisterType
from Tensile.Common.RegisterPool import allocTmpGpr
ri = rocIsa.getInstance()
import shutil
asmpath = shutil.which('amdclang++') or '/usr/bin/amdclang++'
# Pick ISA + wavesize from the kernel — rocisa is a process-wide singleton,
# so re-init each call to override any prior state from other tests.
isa = tuple(kernel.get("ISA", (9, 5, 0)))
waveSize = kernel.get("WavefrontSize", 64)
ri.init(isa, asmpath)
ri.setKernel(isa, waveSize)
is_gfx1250 = (isa == (12, 5, 0))
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel) if fp4 else None
scaleTiB = makeTileInfo('MXSB', kernel) if fp4 else None
writer = SimpleNamespace()
writer.vgprPool = RegisterPool(0, RegisterType.Vgpr, False)
writer.agprPool = RegisterPool(0, RegisterType.Accvgpr, False)
writer.sgprPool = RegisterPool(0, RegisterType.Sgpr, False)
writer.sgprs = {}
writer.states = SimpleNamespace(
regCaps={"MaxSgpr": 106, "MaxVgpr": 256, "PhysicalMaxVgpr": 512},
archCaps={"LDSBankCount": 64, "LDSBankWidth": 4,
"HasWmmaArbStallBit": is_gfx1250},
asmCaps={"HasMFMA": not is_gfx1250,
"HasWMMA_AccImmZero": is_gfx1250},
unrollIdx=0,
laneSGPRCount=1 if is_gfx1250 else 2,
subtileLdsSwizzle=not is_gfx1250,
)
if is_gfx1250:
writer.sgprPool.checkOut(12)
writer.sgprs["StrideA0I"] = 10
writer.sgprs["StrideB1J"] = 11
for tc in ['A', 'B']:
writer.sgprs["tdm%sGroup0" % tc] = writer.sgprPool.checkOutAligned(4, 4, preventOverflow=False)
writer.sgprs["tdm%sGroup1" % tc] = writer.sgprPool.checkOutAligned(8, 4, preventOverflow=False)
writer.sgprs["tdmLdsAddr%s" % tc] = writer.sgprPool.checkOut(1, preventOverflow=False)
writer.sgprs["tdmLdsSwapMask%s" % tc] = writer.sgprPool.checkOut(1, preventOverflow=False)
writer.sgprs["Address%s" % tc] = writer.sgprPool.checkOutAligned(2, 2, preventOverflow=False)
writer.allocTmpSgpr = lambda num, alignment=None, tag=None: allocTmpGpr(
writer.sgprPool, num, writer.states.regCaps["MaxSgpr"], alignment, tag, None)
writer.loopCounterName = lambda kernel, loopIdx: "LoopCounterL"
writer.tailLoopBoundaryDtlLoadAB = lambda *a, **kw: MagicMock()
_label_counters = {}
def _getNameInc(base):
n = _label_counters.get(base, 0)
_label_counters[base] = n + 1
return f"{base}_{n}"
writer.labels = SimpleNamespace(getNameInc=_getNameInc)
dTileInfo = makeTileInfo('D', kernel)
dTileInfo.allocVgprTileRegisters_legacy(writer, kernel)
writer.states.d = SimpleNamespace(tileInfo=dTileInfo)
writer.states.a = SimpleNamespace(tileInfo=tiA)
writer.states.b = SimpleNamespace(tileInfo=tiB)
tiA.allocOffsetRegisters(writer, kernel)
tiB.allocOffsetRegisters(writer, kernel)
if scaleTiA and scaleTiB:
writer.states.mxsa = SimpleNamespace(tileInfo=scaleTiA)
writer.states.mxsb = SimpleNamespace(tileInfo=scaleTiB)
scaleTiA.allocOffsetRegisters(writer, kernel)
scaleTiB.allocOffsetRegisters(writer, kernel)
return writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo
# ── Assertion helpers ─────────────────────────────────────────
def _get_lr(slot, tensor):
"""Get the LR placement for a given tensor in a slot."""
matches = [lr for lr in slot.lrs if lr.tensor == tensor]
assert len(matches) == 1, f"Expected 1 LR for {tensor}, got {len(matches)}"
return matches[0]
def _assert_slot_lrs(slot, expected_tensors):
"""Assert the tensor list of LRs in a slot."""
assert [lr.tensor for lr in slot.lrs] == expected_tensors
def _assert_slot_grs(slot, expected_tensors, msg=""):
"""Assert the tensor list of GRs in a slot."""
assert [gr.tensor for gr in slot.grs] == expected_tensors, msg
def _assert_lr(slot, tensor, mt, k_start, k_end, tile_start, tile_end):
"""Assert an LR placement matches expected values."""
lr = _get_lr(slot, tensor)
assert lr.mtIteration == mt, \
f"LR {tensor}: expected mt={mt}, got {lr.mtIteration}"
assert lr.tiles.subIterK_start == k_start
assert lr.tiles.subIterK_end == k_end
assert lr.tiles.tileId_start == tile_start
assert lr.tiles.tileId_end == tile_end
def _assert_gr(slot, tensor, k_start, k_end, tile_start, tile_end, mt=2, idx=0, uid=None):
"""Assert a GR placement matches expected values."""
if uid is not None:
grs = [gr for gr in slot.grs if gr.tensor == tensor and gr.unrollId == uid]
else:
grs = [gr for gr in slot.grs if gr.tensor == tensor]
assert len(grs) > idx, \
f"Expected at least {idx+1} GR(s) for {tensor} (uid={uid}) in slot {slot.subIterK}, got {len(grs)}"
gr = grs[idx]
assert gr.mtIteration == mt, \
f"GR {tensor}[{idx}] in slot {slot.subIterK}: expected mt={mt}, got {gr.mtIteration}"
assert gr.tiles.subIterK_start == k_start
assert gr.tiles.subIterK_end == k_end
assert gr.tiles.tileId_start == tile_start
assert gr.tiles.tileId_end == tile_end
def _dep_refs(placement):
"""Return list of dep tuples.
GR: (type, tensor, partition, subIterK_slot, mt_offset, unrollId)
LR: (type, tensor, partition, subIterK_slot, mt_offset)
"""
result = []
for dep in placement.deps:
p = dep.ref
if isinstance(p, LRPlacement):
result.append(('LR', p.tensor, p.partition, p.subIterK_slot, dep.mt_offset))
else:
result.append(('GR', p.tensor, p.partition, p.subIterK_slot, dep.mt_offset, p.unrollId))
return result
def _preop_kinds(placement):
"""Return list of (kind, has_sync, wait_gr_counts_dict_or_None) for preOps."""
result = []
for op in placement.preOps:
has_sync = getattr(op, 'has_sync', False)
counts = getattr(op, 'wait_gr_counts', None)
if counts:
result.append((op.kind, has_sync, {'A': counts.A, 'B': counts.B,
'SA': counts.SA, 'SB': counts.SB}))
else:
result.append((op.kind, has_sync, None))
return result
def _preop_inc_tensors(placement, kind):
"""Return list of tensor names for preOps of given kind."""
return [op.tensor for op in placement.preOps if op.kind == kind]
# ══════════════════════════════════════════════════════════════
# Step 1: Place LRs
# ══════════════════════════════════════════════════════════════
class TestPlaceLRs:
def test_1x1_k1_DU256(self):
"""MT=256x256, DU=256, FP4, LR k=1.
numMFMATilesM=8, numMFMATilesN=8, numSubIterK=2.
"""
cfg = make_cfg_256x256_fp4()
assert cfg.numMFMATilesM == 8
assert cfg.numMFMATilesN == 8
assert cfg.numSubIterK == 2
assert cfg.hasScale
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
assert len(slots) == 2
# subIterK=0: LR A, B (k=1, non-wrapping)
s0 = slots[0]
assert s0.mfma.subIterK == 0
assert s0.mfma.tileA.tileId_start == 0
assert s0.mfma.tileA.tileId_end == 8
assert s0.mfma.tileB.tileId_end == 8
_assert_slot_lrs(s0, ['A', 'B'])
_assert_lr(s0, 'A', 0,1, 2, 0, 8)
_assert_lr(s0, 'B', 0,1, 2, 0, 8)
# subIterK=1: LR A, B (wrapping), SA, SB (wrapping)
s1 = slots[1]
assert s1.mfma.subIterK == 1
_assert_slot_lrs(s1, ['A', 'B', 'SA', 'SB'])
_assert_lr(s1, 'A', 1,0, 1, 0, 8)
_assert_lr(s1, 'SA', 1,0, 2, 0, 8)
_assert_lr(s1, 'SB', 1,0, 2, 0, 8)
def test_1x1_k2_DU256(self):
"""MT=256x256, DU=256, FP4, LR A/B with k=2.
With k=2 == numSubIterK, LR A/B load all subIterKs at once.
Split: LR A at subIterK=0, LR B at subIterK=1.
"""
cfg = make_cfg_256x256_fp4(k_gran=2)
assert cfg.numMFMATilesM == 8
assert cfg.numMFMATilesN == 8
assert cfg.numSubIterK == 2
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
assert len(slots) == 2
# subIterK=0: LR A + LR SA (side grouping, all MT n+1)
_assert_slot_lrs(slots[0], ['A', 'SA'])
_assert_lr(slots[0], 'A', 1,0, 2, 0, 8)
_assert_lr(slots[0], 'SA', 1,0, 2, 0, 8)
# subIterK=1: LR B + LR SB
_assert_slot_lrs(slots[1], ['B', 'SB'])
_assert_lr(slots[1], 'B', 1,0, 2, 0, 8)
_assert_lr(slots[1], 'SB', 1,0, 2, 0, 8)
def test_1x1_k1_DU512(self):
"""MT=256x256, DU=512, FP4, LR k=1.
DU=512 → numSubIterK=4. LR SA,SB: k=2 → 2 chunks.
"""
cfg = make_cfg_256x256_fp4(depthU=512)
assert cfg.numMFMATilesM == 8
assert cfg.numMFMATilesN == 8
assert cfg.numSubIterK == 4
assert cfg.hasScale
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
assert len(slots) == 4
# subIterK=0: A, B (k=1) + SA (k=2, A-side)
_assert_slot_lrs(slots[0], ['A', 'B', 'SA'])
assert slots[0].mfma.subIterK == 0
_assert_lr(slots[0], 'A', 0,1, 2, 0, 8)
_assert_lr(slots[0], 'SA', 0,2, 4, 0, 8)
# subIterK=1: A, B + SB (k=2, B-side)
_assert_slot_lrs(slots[1], ['A', 'B', 'SB'])
_assert_lr(slots[1], 'A', 0,2, 3, 0, 8)
_assert_lr(slots[1], 'SB', 0,2, 4, 0, 8)
# subIterK=2: A, B only
_assert_slot_lrs(slots[2], ['A', 'B'])
_assert_lr(slots[2], 'A', 0,3, 4, 0, 8)
# subIterK=3: all wrapping → MT n+1
_assert_slot_lrs(slots[3], ['A', 'B', 'SA', 'SB'])
_assert_lr(slots[3], 'A', 1,0, 1, 0, 8)
_assert_lr(slots[3], 'SA', 1,0, 2, 0, 8)
_assert_lr(slots[3], 'SB', 1,0, 2, 0, 8)
def test_1x1_k2_DU512(self):
"""MT=256x256, DU=512, FP4, LR A/B with k=2.
2 chunks of 2 subIterKs. A/B split by side within each chunk.
"""
cfg = make_cfg_256x256_fp4(depthU=512, k_gran=2)
assert cfg.numSubIterK == 4
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
assert len(slots) == 4
# Chunk 0: A+SA at s0, B+SB at s1 (MT n, loading [2,3])
_assert_slot_lrs(slots[0], ['A', 'SA'])
_assert_lr(slots[0], 'A', 0,2, 4, 0, 8)
_assert_lr(slots[0], 'SA', 0,2, 4, 0, 8)
_assert_slot_lrs(slots[1], ['B', 'SB'])
_assert_lr(slots[1], 'B', 0,2, 4, 0, 8)
_assert_lr(slots[1], 'SB', 0,2, 4, 0, 8)
# Chunk 1: A+SA at s2, B+SB at s3 (MT n+1, loading [0,1])
_assert_slot_lrs(slots[2], ['A', 'SA'])
_assert_lr(slots[2], 'A', 1,0, 2, 0, 8)
_assert_slot_lrs(slots[3], ['B', 'SB'])
_assert_lr(slots[3], 'B', 1,0, 2, 0, 8)
def test_2x2_k1_DU256(self):
"""MT=256x256, DU=256, FP4, k=1, 2x2 partition.
8x8 tiles → 4 partitions of 4x4. Column-major traversal.
"""
cfg = make_cfg_256x256_fp4(partSizeM=4, partSizeN=4)
assert cfg.numPartitions == 4
assert cfg.partitionSizeM == 4
assert cfg.partitionSizeN == 4
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions) == 4
# P0: A[0-3],B[0-3]
p0 = partitions[0]
assert len(p0) == 2
assert p0[0].mfma.tileA.tileId_start == 0
assert p0[0].mfma.tileA.tileId_end == 4
assert p0[0].mfma.tileB.tileId_start == 0
assert p0[0].mfma.tileB.tileId_end == 4
# s0: A, B K-prefetch + SA wrapping for P1 tiles [4-7]
_assert_slot_lrs(p0[0], ['A', 'B', 'SA'])
_assert_lr(p0[0], 'A', 0,1, 2, 0, 4)
_assert_lr(p0[0], 'B', 0,1, 2, 0, 4)
lr_sa0 = _get_lr(p0[0], 'SA')
assert lr_sa0.tiles.tileId_start == 4
assert lr_sa0.tiles.tileId_end == 8
assert lr_sa0.mtIteration == 0
# s1: A wrapping only (B unchanged for P1)
_assert_slot_lrs(p0[1], ['A'])
lr_a1 = _get_lr(p0[1], 'A')
assert lr_a1.tiles.tileId_start == 4
assert lr_a1.tiles.tileId_end == 8
assert lr_a1.mtIteration == 0
# P1: A[4-7],B[0-3] → B wrapping for P2
p1 = partitions[1]
_assert_slot_lrs(p1[0], ['A', 'SB'])
_assert_slot_lrs(p1[1], ['B'])
lr_b1 = _get_lr(p1[1], 'B')
assert lr_b1.tiles.tileId_start == 4
assert lr_b1.tiles.tileId_end == 8
# P2: A[0-3],B[4-7] → no wrapping (both already loaded)
p2 = partitions[2]
_assert_slot_lrs(p2[0], ['B'])
assert len(p2[1].lrs) == 0
# P3: last → load all for MT n+1
p3 = partitions[3]
_assert_slot_lrs(p3[0], ['SA'])
lr_sa3 = _get_lr(p3[0], 'SA')
assert lr_sa3.tiles.tileId_start == 0
assert lr_sa3.tiles.tileId_end == 4
assert lr_sa3.mtIteration == 1
_assert_slot_lrs(p3[1], ['A', 'B', 'SB'])
_assert_lr(p3[1], 'A', 1,0, 1, 0, 4)
def test_2x2_k1_DU512(self):
"""MT=256x256, DU=512, FP4, k=1, 2x2 partition.
numSubIterK=4. 4 partitions × 4 subIterKs = 16 slots.
"""
cfg = make_cfg_256x256_fp4(depthU=512, partSizeM=4, partSizeN=4)
assert cfg.numPartitions == 4
assert cfg.numSubIterK == 4
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions) == 4
# P0: 4 subIterKs with K-prefetch and wrapping
p0 = partitions[0]
assert len(p0) == 4
for s in p0:
assert s.mfma.tileA.tileId_start == 0
assert s.mfma.tileA.tileId_end == 4
_assert_slot_lrs(p0[0], ['A', 'B', 'SA'])
_assert_slot_lrs(p0[1], ['A', 'B', 'SB'])
_assert_slot_lrs(p0[2], ['A', 'B', 'SA'])
_assert_slot_lrs(p0[3], ['A'])
# P3: last partition — last 2 slots load for MT n+1
p3 = partitions[3]
assert len(p3[0].lrs) == 0
assert len(p3[1].lrs) == 0
_assert_slot_lrs(p3[2], ['SA'])
_assert_slot_lrs(p3[3], ['A', 'B', 'SB'])
_assert_lr(p3[3], 'A', 1,0, 1, 0, 4)
_assert_lr(p3[3], 'B', 1,0, 1, 0, 4)
def test_2x2_k2_DU256(self):
"""MT=256x256, DU=256, FP4, k=2, 2x2 partition.
Each partition: A/B loaded per side, wrapping across partitions.
"""
cfg = make_cfg_256x256_fp4(k_gran=2, partSizeM=4, partSizeN=4)
assert cfg.numPartitions == 4
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions) == 4
# P0: A+SA at s0 (A changes for P1)
_assert_slot_lrs(partitions[0][0], ['A', 'SA'])
lr_a = _get_lr(partitions[0][0], 'A')
assert lr_a.tiles.tileId_start == 4
assert lr_a.tiles.tileId_end == 8
assert lr_a.mtIteration == 0
assert len(partitions[0][1].lrs) == 0
# P2: no LRs (both A and B already loaded)
assert len(partitions[2][0].lrs) == 0
assert len(partitions[2][1].lrs) == 0
# P3: last → A+SA, B+SB for MT n+1
_assert_slot_lrs(partitions[3][0], ['A', 'SA'])
_assert_slot_lrs(partitions[3][1], ['B', 'SB'])
_assert_lr(partitions[3][0], 'A', 1,0, 2, 0, 4)
_assert_lr(partitions[3][1], 'B', 1,0, 2, 0, 4)
def test_10x1_k1_bf16(self):
"""MT=320x320, BF16, DU=64, k=1, 10x1 partition. No scale.
B never changes → only A wrapping LRs needed.
"""
cfg = make_cfg_bf16(320, 320, partSizeM=1, partSizeN=10)
assert cfg.numMFMATilesM == 10
assert cfg.numMFMATilesN == 10
assert cfg.numSubIterK == 2
assert not cfg.hasScale
assert cfg.numPartitions == 10
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions) == 10
# P0: A+B K-prefetch, then A wrapping
_assert_slot_lrs(partitions[0][0], ['A', 'B'])
_assert_slot_lrs(partitions[0][1], ['A'])
lr_a0 = _get_lr(partitions[0][0], 'A')
assert lr_a0.tiles.subIterK_start == 1
assert lr_a0.mtIteration == 0
# P1-P8: only A LRs (B deduped)
for pi in range(1, 9):
_assert_slot_lrs(partitions[pi][0], ['A'])
_assert_slot_lrs(partitions[pi][1], ['A'])
# P9: last → A+B for MT n+1
_assert_slot_lrs(partitions[9][0], ['A'])
_assert_slot_lrs(partitions[9][1], ['A', 'B'])
_assert_lr(partitions[9][1], 'A', 1,0, 1, 0, 1)
_assert_lr(partitions[9][1], 'B', 1,0, 1, 0, 10)
def test_asymmetric_mt(self):
"""MT0=384, MT1=128 — asymmetric tile counts."""
cfg = make_cfg_bf16(384, 128)
assert cfg.numMFMATilesM == 12 # 384/16/2
assert cfg.numMFMATilesN == 4 # 128/16/2
assert cfg.numSubIterK == 2
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
assert len(slots) == 2
assert slots[0].mfma.tileA.tileId_end == 12
assert slots[0].mfma.tileB.tileId_end == 4
# ══════════════════════════════════════════════════════════════
# Step 2: Assign VGPR tiles
# ══════════════════════════════════════════════════════════════
class TestAssignVgprTiles:
def _assert_no_conflict_and_unrolling(self, sched, schedule):
"""Validate vgprTileId invariants: no overlap, bounds, alternation, continuity."""
parts = schedule
num_iters = sched.unroll_factor
# 1. No MFMA/LR vgprTileId overlap (B may overlap when compact_b_overlay:
# MFMA runs before LR in the same slot, so tiles are reused in place)
for pi, slots in enumerate(parts):
for slot in slots:
if not slot.mfma:
continue
for lr in slot.lrs:
if lr.tensor not in ('A', 'B') or not lr.vgpr_tile_map:
continue
if lr.tensor == 'B' and getattr(sched, 'compact_b_overlay', False):
continue
mfma_map_list = slot.mfma.vgpr_tile_maps.get(lr.tensor, [])
for ui in range(len(mfma_map_list)):
mfma_vids = set(mfma_map_list[ui].values())
lr_vids = set(lr.vgpr_tile_map[ui].values())
assert mfma_vids.isdisjoint(lr_vids), \
f"P{pi} k={slot.subIterK} iter={ui}: MFMA/LR {lr.tensor} overlap"
# 2. All vgprTileIds within [0, tile_peaks)
for pi, slots in enumerate(parts):
for slot in slots:
if slot.mfma:
for tensor, maps in slot.mfma.vgpr_tile_maps.items():
peak = sched.tile_peaks[tensor]
for ui, tile_map in enumerate(maps):
for tid, vid in tile_map.items():
assert 0 <= vid < peak, \
f"P{pi} k={slot.subIterK} iter={ui}: " \
f"MFMA {tensor} vid={vid} out of [0, {peak})"
for lr in slot.lrs:
if not lr.vgpr_tile_map:
continue
peak = sched.tile_peaks[lr.tensor]
for ui, tile_map in enumerate(lr.vgpr_tile_map):
for tid, vid in tile_map.items():
assert 0 <= vid < peak, \
f"P{pi} k={slot.subIterK} iter={ui}: " \
f"LR {lr.tensor} vid={vid} out of [0, {peak})"
# 3. Double-buffer alternation: MFMAs in different K-groups use different sets.
cfg = sched.config
lr_grans = {'A': cfg.lrA, 'B': cfg.lrB}
if cfg.hasScale:
lr_grans['SA'] = cfg.lrSA
lr_grans['SB'] = cfg.lrSB
for ui in range(num_iters):
for pi, slots in enumerate(parts):
by_k = {}
for slot in slots:
if not slot.mfma:
continue
by_k.setdefault(slot.subIterK, []).append(slot)
sorted_ks = sorted(by_k.keys())
for i in range(len(sorted_ks) - 1):
k0, k1 = sorted_ks[i], sorted_ks[i + 1]
for s0 in by_k[k0]:
for s1 in by_k[k1]:
for tensor in sched.tile_peaks:
gran = lr_grans[tensor]
if k0 // gran.k == k1 // gran.k:
continue
maps0 = s0.mfma.vgpr_tile_maps.get(tensor, [])
maps1 = s1.mfma.vgpr_tile_maps.get(tensor, [])
if ui >= len(maps0) or ui >= len(maps1):
continue
common_tiles = (set(maps0[ui].keys())
& set(maps1[ui].keys()))
for tid in common_tiles:
assert maps0[ui][tid] != maps1[ui][tid], \
f"P{pi} iter={ui}: {tensor} tile {tid} " \
f"same vid at k={k0} and k={k1}"
# 4. Unrolling continuity
if sched.needs_unrolling:
last_ui = num_iters - 1
wrapping_writes = {}
for pi, slots in enumerate(parts):
for slot in slots:
for lr in slot.lrs:
if lr.mtIteration == 0 or lr.tensor not in ('A', 'B'):
continue
if not lr.vgpr_tile_map:
continue
tile_map = lr.vgpr_tile_map[last_ui]
for tileId, vid in tile_map.items():
for lk in lr.tiles.subIterK_list:
wrapping_writes[(lr.tensor, tileId, lk, pi)] = vid
for pi, slots in enumerate(parts):
for slot in slots:
if not slot.mfma:
continue
for tensor, tileRange in [('A', slot.mfma.tileA), ('B', slot.mfma.tileB)]:
mfma_map_0 = slot.mfma.vgpr_tile_maps[tensor][0]
for tileId in tileRange.tileId_list:
key = (tensor, tileId, slot.subIterK, pi)
if key in wrapping_writes:
assert mfma_map_0[tileId] == wrapping_writes[key], \
f"P{pi} k={slot.subIterK}: unrolling continuity broken"
def test_basic_with_scale(self):
"""VgprTile allocation with scale tensors (design doc example)."""
cfg = make_example_granularities_1()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
parts = schedule
s0, s1 = parts[0][0], parts[0][1]
for tensor in ('A', 'B', 'SA', 'SB'):
assert len(s0.mfma.vgpr_tile_maps[tensor]) > 0
for tensor in ('A', 'B'):
map_k0 = s0.mfma.vgpr_tile_maps[tensor][0]
map_k1 = s1.mfma.vgpr_tile_maps[tensor][0]
for tileId in map_k0:
if tileId in map_k1:
assert map_k0[tileId] != map_k1[tileId]
assert sched.tile_peaks['A'] > 0
assert sched.tile_peaks['SA'] > 0
assert sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
def test_no_scale_k1(self):
"""No scales, A/B k_gran=1 → tiles alternate every subIterK."""
cfg = SchedulerConfig(
numMFMATilesM=2, numMFMATilesN=2, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
)
assert not cfg.hasScale
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert 'SA' not in sched.tile_peaks
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
def test_DU512(self):
"""DU=512, FP4. numSubIterK=4, no unrolling needed."""
cfg = make_cfg_256x256_fp4(depthU=512)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
for slot in schedule[0]:
assert len(slot.mfma.vgpr_tile_maps['A']) > 0
assert len(slot.mfma.vgpr_tile_maps['SA']) > 0
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
def test_DU512_partition_2x2(self):
"""DU=512 + 2x2 partition. All partitions have tile maps."""
cfg = make_cfg_256x256_fp4(depthU=512, partSizeM=4, partSizeN=4)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
for pi in range(4):
for slot in schedule[pi]:
assert len(slot.mfma.vgpr_tile_maps['A']) > 0
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 1: BF16 2x2 ──
@pytest.mark.parametrize("depthU", [64, 128])
def test_bf16_2x2(self, depthU):
"""BF16 256x256 with default miWaveGroup=[2,2]."""
cfg = make_cfg_bf16(depthU=depthU)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 16, 'B': 16}
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 2: BF16 1x4 / 4x1 ──
@pytest.mark.parametrize("depthU", [64, 128])
def test_bf16_1x4(self, depthU):
"""BF16 256x256 miWaveGroup=[1,4]: 16 M-tiles, 4 N-tiles."""
cfg = make_cfg_bf16(depthU=depthU, miWaveGroup=[1, 4])
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 32, 'B': 8}
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
@pytest.mark.parametrize("depthU", [64, 128])
def test_bf16_4x1(self, depthU):
"""BF16 256x256 miWaveGroup=[4,1]: 4 M-tiles, 16 N-tiles."""
cfg = make_cfg_bf16(depthU=depthU, miWaveGroup=[4, 1])
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 8, 'B': 32}
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 3: BF16 multi-partition along N ──
@pytest.mark.parametrize("partSizeN,expected_peak_B", [
(4, 8),
(3, 6),
(2, 4),
])
def test_bf16_partition_N(self, partSizeN, expected_peak_B):
"""BF16 256x256 (tilesN=8) with partitions along N."""
cfg = make_cfg_bf16(depthU=64, partSizeN=partSizeN)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks['A'] == 16
assert sched.tile_peaks['B'] == expected_peak_B
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 3b: BF16 multi-partition on asymmetric macro tiles ──
@pytest.mark.parametrize("partSizeN,expected_peak_B,expected_partN", [
(6, 12, [6, 6]),
(4, 8, [4, 4, 4]),
(3, 6, [3, 3, 3, 3]),
(5, 10, [5, 2, 5]),
])
def test_bf16_partition_256x384(self, partSizeN, expected_peak_B, expected_partN):
"""BF16 256x384 (tilesN=12) with partitions along N."""
cfg = make_cfg_bf16(MT1=384, depthU=64, partSizeN=partSizeN)
assert cfg.partitionSizesN == expected_partN
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks['A'] == 16
assert sched.tile_peaks['B'] == expected_peak_B
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
@pytest.mark.parametrize("partSizeN,expected_peak_B,expected_partN", [
(4, 8, [4, 3, 4]),
(3, 6, [3, 2, 3, 3]),
(6, 12, [6, 5]),
])
def test_bf16_partition_256x352(self, partSizeN, expected_peak_B, expected_partN):
"""BF16 256x352 (tilesN=11, odd) with partitions along N."""
cfg = make_cfg_bf16(MT1=352, depthU=64, partSizeN=partSizeN)
assert cfg.partitionSizesN == expected_partN
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks['A'] == 16
assert sched.tile_peaks['B'] == expected_peak_B
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
@pytest.mark.parametrize("partSizeN,expected_peak_B,expected_partN", [
(4, 8, [4, 4, 3, 4, 4, 4]),
(6, 12, [6, 5, 6, 6]),
(8, 16, [8, 7, 8]),
])
def test_bf16_partition_256x368(self, partSizeN, expected_peak_B, expected_partN):
"""BF16 256x368 miWaveGroup=[4,1] (tilesM=4, tilesN=23) with partitions along N."""
cfg = make_cfg_bf16(MT1=368, depthU=64, miWaveGroup=[4, 1],
partSizeN=partSizeN)
assert cfg.partitionSizesN == expected_partN
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks['A'] == 8
assert sched.tile_peaks['B'] == expected_peak_B
assert not sched.needs_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 4: FP4 2x2 ──
@pytest.mark.parametrize("depthU,expect_unrolling", [(256, True), (512, False)])
def test_fp4_2x2(self, depthU, expect_unrolling):
"""FP4 256x256 with default miWaveGroup=[2,2]."""
cfg = make_cfg_256x256_fp4(depthU=depthU)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 16, 'B': 16, 'SA': 8, 'SB': 8}
assert sched.needs_unrolling == expect_unrolling
for pi in range(cfg.numPartitions):
for slot in schedule[pi]:
if slot.mfma:
assert len(slot.mfma.vgpr_tile_maps['SA']) > 0
assert len(slot.mfma.vgpr_tile_maps['SB']) > 0
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 5: FP4 1x4 / 4x1 ──
@pytest.mark.parametrize("depthU,expect_unrolling", [(256, True), (512, False)])
def test_fp4_1x4(self, depthU, expect_unrolling):
"""FP4 256x256 miWaveGroup=[1,4]: 16 M-tiles, 4 N-tiles."""
cfg = make_cfg_256x256_fp4(depthU=depthU, miWaveGroup=[1, 4])
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 32, 'B': 8, 'SA': 16, 'SB': 4}
assert sched.needs_unrolling == expect_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
@pytest.mark.parametrize("depthU,expect_unrolling", [(256, True), (512, False)])
def test_fp4_4x1(self, depthU, expect_unrolling):
"""FP4 256x256 miWaveGroup=[4,1]: 4 M-tiles, 16 N-tiles."""
cfg = make_cfg_256x256_fp4(depthU=depthU, miWaveGroup=[4, 1])
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 8, 'B': 32, 'SA': 4, 'SB': 16}
assert sched.needs_unrolling == expect_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
# ── Group 6: BF16 with lrA/lrB k=2 granularity ──
@pytest.mark.parametrize("depthU,expect_unrolling", [(64, True), (128, False)])
def test_bf16_lr_k2(self, depthU, expect_unrolling):
"""BF16 with LR k=2 granularity — odd k-groups trigger unrolling."""
cfg = make_cfg_bf16(
depthU=depthU,
lrA=ReadGranularity(mn=1, k=2),
lrB=ReadGranularity(mn=1, k=2),
)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 16, 'B': 16}
assert sched.needs_unrolling == expect_unrolling
self._assert_no_conflict_and_unrolling(sched, schedule)
def test_multi_du_unroll2_AB(self):
"""Multi-DU: numSubIterK=4, A/B k_gran=1 → 4 distinct k_chunks.
uid=0 covers k=[0,1], uid=1 covers k=[2,3]. No key collision."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
slots = schedule[0]
assert len(slots) == 4
# All 4 slots have MFMA tile maps for A, B, SA, SB
for slot in slots:
for tensor in ('A', 'B', 'SA', 'SB'):
assert len(slot.mfma.vgpr_tile_maps[tensor]) > 0
# Within each slot, MFMA read and LR write use different VGPRs
# (verified by the generic helper)
self._assert_no_conflict_and_unrolling(sched, schedule)
# Peaks: same as single-DU (no uid collision inflating counts)
assert sched.tile_peaks['A'] == 16
assert sched.tile_peaks['SA'] == 8
# ══════════════════════════════════════════════════════════════
# Step 3: Place GRs
# ══════════════════════════════════════════════════════════════
class TestPlaceGRs:
def test_1x1_k1_DU256(self):
"""256x256, DU256, FP4. GR order: A, B, SA, SB.
Total loads: A(8)+B(8)+SA(1)+SB(1)=18, 9 per slot.
"""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR)
slots = schedule[0]
_assert_slot_grs(slots[0], ['A', 'B'])
_assert_gr(slots[0], 'A', 0, 2, 0, 8)
_assert_gr(slots[0], 'B', 0, 2, 0, 1)
_assert_slot_grs(slots[1], ['B', 'SA', 'SB'])
_assert_gr(slots[1], 'B', 0, 2, 1, 8)
_assert_gr(slots[1], 'SA', 0, 2, 0, 8)
_assert_gr(slots[1], 'SB', 0, 2, 0, 8)
def test_1x1_k1_DU512(self):
"""256x256, DU512, FP4. GR k=2 → two k-chunks. grSA/SB k=4 → full MT.
Spaced-out distribution: B splits across s0/s1, SA migrates to s1, A k=2-4 sits whole at s2.
"""
cfg = make_cfg_256x256_fp4(depthU=512)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR)
slots = schedule[0]
_assert_slot_grs(slots[0], ['A', 'B'])
_assert_gr(slots[0], 'A', 0, 2, 0, 8)
_assert_gr(slots[0], 'B', 0, 2, 0, 1)
_assert_slot_grs(slots[1], ['B', 'SA'])
_assert_gr(slots[1], 'B', 0, 2, 1, 8)
_assert_gr(slots[1], 'SA', 0, 4, 0, 8)
_assert_slot_grs(slots[2], ['SB', 'A'])
_assert_gr(slots[2], 'SB', 0, 4, 0, 8)
_assert_gr(slots[2], 'A', 2, 4, 0, 8)
_assert_slot_grs(slots[3], ['B'])
_assert_gr(slots[3], 'B', 2, 4, 0, 8)
def test_2x2_k1_DU256(self):
"""256x256, DU256, FP4, 2x2 partition. Cross-MT dedup removes n+1 duplicates.
Spaced-out distribution: tensors split into smaller atoms across more slots;
scale GRs migrate to the last slot of the last partition.
"""
cfg = make_cfg_256x256_fp4(partSizeM=4, partSizeN=4)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR)
# P0: A n+1 split, B n+1 starts at s1
_assert_slot_grs(parts[0][0], ['A'], "P0 s0")
_assert_gr(parts[0][0], 'A', 0, 2, 4, 7, mt=1)
_assert_slot_grs(parts[0][1], ['A', 'B'], "P0 s1")
_assert_gr(parts[0][1], 'A', 0, 2, 7, 8, mt=1)
_assert_gr(parts[0][1], 'B', 0, 2, 4, 5, mt=1)
# P1: B n+1 finishes, A n+2 starts at s1
_assert_slot_grs(parts[1][0], ['B'], "P1 s0")
_assert_gr(parts[1][0], 'B', 0, 2, 5, 7, mt=1)
_assert_slot_grs(parts[1][1], ['B', 'A'], "P1 s1")
_assert_gr(parts[1][1], 'B', 0, 2, 7, 8, mt=1)
_assert_gr(parts[1][1], 'A', 0, 2, 0, 1, mt=2)
# P2: A n+2 finishes, B n+2 starts
_assert_slot_grs(parts[2][0], ['A'], "P2 s0")
_assert_gr(parts[2][0], 'A', 0, 2, 1, 4, mt=2)
_assert_slot_grs(parts[2][1], ['B'], "P2 s1")
_assert_gr(parts[2][1], 'B', 0, 2, 0, 2, mt=2)
# P3: B n+2 finishes at s0; SA/SB n+2 at s1 (last slot)
_assert_slot_grs(parts[3][0], ['B'], "P3 s0")
_assert_gr(parts[3][0], 'B', 0, 2, 2, 4, mt=2)
_assert_slot_grs(parts[3][1], ['SA', 'SB'], "P3 s1")
_assert_gr(parts[3][1], 'SA', 0, 2, 0, 8, mt=2)
_assert_gr(parts[3][1], 'SB', 0, 2, 0, 8, mt=2)
def test_2x2_k1_DU512(self):
"""256x256, DU512, FP4, 2x2 partition. GR k=2 × 2 chunks + scale.
Spaced-out distribution: A k=0-2 and k=2-4 chunks split across slots within P0;
B n+1 starts in P0 s3; SA/SB migrate to P3 s3 (last slot of last partition).
"""
cfg = make_cfg_256x256_fp4(depthU=512, partSizeM=4, partSizeN=4)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR)
# P0 s0: A n+1 k=0-2 [4-7]
_assert_slot_grs(parts[0][0], ['A'], "P0 s0")
_assert_gr(parts[0][0], 'A', 0, 2, 4, 7, mt=1)
# P0 s1: A n+1 k=0-2 tail [7-8] + A n+1 k=2-4 head [4-5]
_assert_slot_grs(parts[0][1], ['A', 'A'], "P0 s1")
_assert_gr(parts[0][1], 'A', 0, 2, 7, 8, mt=1, idx=0)
_assert_gr(parts[0][1], 'A', 2, 4, 4, 5, mt=1, idx=1)
# P0 s2: A n+1 k=2-4 [5-7]
_assert_slot_grs(parts[0][2], ['A'], "P0 s2")
_assert_gr(parts[0][2], 'A', 2, 4, 5, 7, mt=1)
# P0 s3: A n+1 k=2-4 tail [7-8] + B n+1 k=0-2 head [4-5]
_assert_slot_grs(parts[0][3], ['A', 'B'], "P0 s3")
_assert_gr(parts[0][3], 'A', 2, 4, 7, 8, mt=1)
_assert_gr(parts[0][3], 'B', 0, 2, 4, 5, mt=1)
# P2 s3: SA+SB n+2 (migrated from P3 s0 to P2 s3)
_assert_slot_grs(parts[2][3], ['SA', 'SB'], "P2 s3")
_assert_gr(parts[2][3], 'SA', 0, 4, 0, 8, mt=2)
_assert_gr(parts[2][3], 'SB', 0, 4, 0, 8, mt=2)
def test_10x1_bf16(self):
"""320x320, BF16, 10x1 partition. No scales."""
cfg = make_cfg_bf16(320, 320, partSizeM=1, partSizeN=10)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR)
# P0..P4: A atoms
for pi in range(4):
_assert_slot_grs(parts[pi][0], ['A'], f"P{pi} s0")
_assert_gr(parts[pi][0], 'A', 0, 2, pi*2+1, pi*2+2, mt=1)
# P5..P9: B atoms
for pi in range(5, 10):
b_idx = (pi - 5) * 2
_assert_slot_grs(parts[pi][0], ['B'], f"P{pi} s0")
_assert_gr(parts[pi][0], 'B', 0, 2, b_idx, b_idx+1, mt=2)
def test_1x19_bf16(self):
"""320x304, BF16, 1x19 partition, wg=[4,1]. High partition count with sourceSwap."""
cfg = make_cfg_bf16(320, 304, partSizeN=1,
miWaveGroup=[4, 1], sourceSwap=True)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR)
# Collect (partition, subIterK) for each tensor/mt.
b_n1 = []
a_n2 = []
for pi in range(19):
for slot in parts[pi]:
for gr in slot.grs:
if gr.tensor == 'B' and gr.mtIteration == 1:
b_n1.append((pi, slot.subIterK))
elif gr.tensor == 'A' and gr.mtIteration == 2:
a_n2.append((pi, slot.subIterK))
# 9 B n+1 atoms (grB.mn=2) spread across P0..P10
assert b_n1 == [
(0,0),(1,0),(2,1),(3,1),(5,0),(6,0),(7,1),(8,1),(10,0)]
# 5 A n+2 atoms spread across P11..P16
assert a_n2 == [(11,0),(12,1),(13,1),(15,0),(16,0)]
# B n+2 at P17
_assert_slot_grs(parts[17][1], ['B'], "P17 s1")
_assert_gr(parts[17][1], 'B', 0, 2, 0, 2, mt=2)
def test_1x1_multi_du_unroll2_AB(self):
"""grSA/SB k_gran=2 → numSubIterK expanded 2→4, numUnroll={A:2, B:2, SA:1, SB:1}.
PGR clamped to 1 → GR at MT1.
uid=0: A/B k=[0,2) only (uid's own k range), SA/SB k=[0,4).
uid=1: A/B k=[2,4) (second data read), SA/SB skipped.
uid=0 is distributed across slots; uid=1 is placed only in the
last slot so GRInc(uid=0) precedes all uid=1 GRs globally.
"""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
assert cfg.numSubIterK == 4
assert cfg.numUnroll == {'A': 2, 'B': 2, 'SA': 1, 'SB': 1}
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR)
slots = schedule[0]
assert len(slots) == 4
# uid=0 distributed across s0..s2
_assert_gr(slots[0], 'A', 0, 2, 0, 5, mt=1, uid=0)
_assert_gr(slots[1], 'A', 0, 2, 5, 8, mt=1, uid=0)
_assert_gr(slots[1], 'B', 0, 2, 0, 1, mt=1, uid=0)
_assert_gr(slots[2], 'B', 0, 2, 1, 8, mt=1, uid=0)
_assert_gr(slots[2], 'SA', 0, 4, 0, 8, mt=1, uid=0)
_assert_gr(slots[2], 'SB', 0, 4, 0, 8, mt=1, uid=0)
# uid=1 at s3 only
_assert_gr(slots[3], 'A', 2, 4, 0, 4, mt=1, uid=1)
_assert_gr(slots[3], 'A', 2, 4, 4, 8, mt=1, uid=1, idx=1)
_assert_gr(slots[3], 'B', 2, 4, 0, 8, mt=1, uid=1)
assert not any(gr.tensor in ('SA', 'SB') and gr.unrollId == 1
for slot in slots for gr in slot.grs)
def test_pgr1_gr_before_corresponding_lr(self):
"""PGR=1: GR(T, mt=X) must be placed strictly before first LR(T, mt=X)."""
cfg = make_cfg_bf16_pgr1()
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR)
numK = cfg.numSubIterK
# Build per-(tensor, mt) earliest LR flat index
lr_first = {}
for pi, slots in enumerate(parts):
for slot in slots:
flat = pi * numK + slot.subIterK
for lr in slot.lrs:
key = (lr.tensor, lr.mtIteration)
if key not in lr_first or flat < lr_first[key]:
lr_first[key] = flat
for pi, slots in enumerate(parts):
for slot in slots:
flat = pi * numK + slot.subIterK
for gr in slot.grs:
key = (gr.tensor, gr.mtIteration)
if key in lr_first:
assert flat < lr_first[key], (
f"GR({gr.tensor}, mt={gr.mtIteration}) at flat={flat} "
f"must be before first LR at flat={lr_first[key]}")
# ══════════════════════════════════════════════════════════════
# Step 4: Annotate deps
# ══════════════════════════════════════════════════════════════
class TestAnnotateDeps:
def test_1x1_DU256(self):
"""256x256, DU256, FP4. MFMA→LR, LR→GR, GR→LR collision deps."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.DEPS)
s0, s1 = parts[0][0], parts[0][1]
# MFMA(k=0): all deps MT-1
mfma0_deps = _dep_refs(s0.mfma)
assert ('LR', 'A', 0, 1, -1) in mfma0_deps
assert ('LR', 'B', 0, 1, -1) in mfma0_deps
assert ('LR', 'SA', 0, 1, -1) in mfma0_deps
assert ('LR', 'SB', 0, 1, -1) in mfma0_deps
assert len(mfma0_deps) == 4
# LR A @s0 → GR A @s0 (MT-2)
assert _dep_refs(_get_lr(s0, 'A')) == [('GR', 'A', 0, 0, -2, 0)]
# LR B @s0 → last GR B @s1 (MT-2)
assert _dep_refs(_get_lr(s0, 'B')) == [('GR', 'B', 0, 1, -2, 0)]
# GR A @s0 → LR A collision (MT 0)
gr_a0 = [gr for gr in s0.grs if gr.tensor == 'A'][0]
assert _dep_refs(gr_a0) == [('LR', 'A', 0, 0, 0)]
# MFMA(k=1): LR A/B @s0 (MT 0), SA/SB @s1 (MT-1)
mfma1_deps = _dep_refs(s1.mfma)
assert ('LR', 'A', 0, 0, 0) in mfma1_deps
assert ('LR', 'SA', 0, 1, -1) in mfma1_deps
assert len(mfma1_deps) == 4
def test_2x2_DU512(self):
"""256x256, DU512, FP4, 2x2 partition. Per-partition deps."""
cfg = make_cfg_256x256_fp4(depthU=512, partSizeM=4, partSizeN=4)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.DEPS)
assert len(parts) == 4
assert len(parts[0]) == 4
# P0 MFMA(k=0): deps from P3
mfma_p0_s0 = _dep_refs(parts[0][0].mfma)
assert ('LR', 'A', 3, 3, -1) in mfma_p0_s0
assert len(mfma_p0_s0) == 4
# P3 MFMA(k=0): deps on LRs that loaded subIterK=0 data for P3 tiles
mfma_p3_s0 = _dep_refs(parts[3][0].mfma)
assert ('LR', 'A', 0, 3, 0) in mfma_p3_s0
assert ('LR', 'SA', 0, 2, 0) in mfma_p3_s0
def test_1x1_multi_du_unroll2_AB(self):
"""Multi-DU: uid=0 and uid=1 GRs get correct collision deps.
PGR=1 → GR at MT1, deps relative to MT1.
uid=0 distributed across slots; uid=1 placed at s3 only."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.DEPS)
slots = schedule[0]
# LR A k=[1] @s0 → GR A uid=0 @s1, MT-1
assert _dep_refs(_get_lr(slots[0], 'A')) == [('GR', 'A', 0, 1, -1, 0)]
# LR B k=[1] @s0 → GR B uid=0 @s2, MT-1
assert _dep_refs(_get_lr(slots[0], 'B')) == [('GR', 'B', 0, 2, -1, 0)]
# LR SA k=[2,3] @s0 → GR SA uid=0 @s2, MT-1
assert _dep_refs(_get_lr(slots[0], 'SA')) == [('GR', 'SA', 0, 2, -1, 0)]
# LR A k=[2] @s1 → GR A uid=1 @s3, MT-1
assert _dep_refs(_get_lr(slots[1], 'A')) == [('GR', 'A', 0, 3, -1, 1)]
# LR B k=[2] @s1 → GR B uid=1 @s3, MT-1
assert _dep_refs(_get_lr(slots[1], 'B')) == [('GR', 'B', 0, 3, -1, 1)]
# LR SB k=[2,3] @s1 → GR SB uid=0 @s2, MT-1
assert _dep_refs(_get_lr(slots[1], 'SB')) == [('GR', 'SB', 0, 2, -1, 0)]
# LR A k=[3] @s2 → GR A uid=1 @s3, MT-1
assert _dep_refs(_get_lr(slots[2], 'A')) == [('GR', 'A', 0, 3, -1, 1)]
# LR B k=[3] @s2 → GR B uid=1 @s3, MT-1
assert _dep_refs(_get_lr(slots[2], 'B')) == [('GR', 'B', 0, 3, -1, 1)]
# LR A k=[0] @s3 → GR A uid=0 @s1, MT 0 (same iter)
assert _dep_refs(_get_lr(slots[3], 'A')) == [('GR', 'A', 0, 1, 0, 0)]
# LR B k=[0] @s3 → GR B uid=0 @s2, MT 0 (same iter)
assert _dep_refs(_get_lr(slots[3], 'B')) == [('GR', 'B', 0, 2, 0, 0)]
# LR SA k=[0,1] @s3 → GR SA uid=0 @s2, MT 0 (same iter)
assert _dep_refs(_get_lr(slots[3], 'SA')) == [('GR', 'SA', 0, 2, 0, 0)]
# LR SB k=[0,1] @s3 → GR SB uid=0 @s2, MT 0 (same iter)
assert _dep_refs(_get_lr(slots[3], 'SB')) == [('GR', 'SB', 0, 2, 0, 0)]
# GR A uid=0 @s0 → collision LR A @s0, MT-1
gr_a_uid0_s0 = [gr for gr in slots[0].grs if gr.tensor == 'A' and gr.unrollId == 0][0]
assert _dep_refs(gr_a_uid0_s0) == [('LR', 'A', 0, 0, -1)]
# GR A uid=1 @s3 (k=[2,4)) → collision LR A @s2, MT-1
gr_a_uid1_s3 = [gr for gr in slots[3].grs if gr.tensor == 'A' and gr.unrollId == 1][0]
assert _dep_refs(gr_a_uid1_s3) == [('LR', 'A', 0, 2, -1)]
# GR B uid=1 @s3 (k=[2,4)) → collision LR B @s2, MT-1
gr_b_uid1_s3 = [gr for gr in slots[3].grs if gr.tensor == 'B' and gr.unrollId == 1][0]
assert _dep_refs(gr_b_uid1_s3) == [('LR', 'B', 0, 2, -1)]
# No uid=1 GRs for SA or SB
all_grs = [gr for slot in slots for gr in slot.grs]
assert not any(gr.tensor in ('SA', 'SB') and gr.unrollId == 1 for gr in all_grs)
# ══════════════════════════════════════════════════════════════
# Step 4b: Remove unnecessary deps (individual passes)
# ══════════════════════════════════════════════════════════════
class TestRemoveUnnecessaryGrDeps:
def test_removes_mt_minus_2_deps(self):
"""MT-2 GR deps are removed when an MT-1 dep from a later LR guarantees the data."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.DEPS)
s0 = schedule[0][0]
# Before: LR A @s0 has dep on GR A @s0 (MT-2)
lr_a_before = _get_lr(s0, 'A')
assert len(lr_a_before.deps) == 1
assert lr_a_before.deps[0].mt_offset == -2
sched.remove_unnecessary_gr_deps()
# After: dep removed (guaranteed by LR A @s1's MT-1 dep)
lr_a_after = _get_lr(s0, 'A')
assert len(lr_a_after.deps) == 0
class TestRemoveUnnecessaryLrDeps:
def test_removes_covered_collision_deps(self):
"""GR→LR collision deps removed when covered by earlier sync."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.DEPS)
sched.remove_unnecessary_gr_deps()
# Before remove_unnecessary_lr_deps: GR B @s1 has collision dep
s1 = schedule[0][1]
gr_b1 = [gr for gr in s1.grs if gr.tensor == 'B'][0]
deps_before = len(gr_b1.deps)
sched.remove_unnecessary_lr_deps()
gr_b1_after = [gr for gr in schedule[0][1].grs if gr.tensor == 'B'][0]
assert len(gr_b1_after.deps) == 0
# ══════════════════════════════════════════════════════════════
# Step 5: Remove cross-subIterK deps
# ══════════════════════════════════════════════════════════════
class TestRemoveCrossDeps:
def test_1x1_DU256(self):
"""256x256, DU256, FP4. Cross deps → preOps, same-subIterK deps preserved."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.REMOVE_DEPS)
s0, s1 = parts[0][0], parts[0][1]
# MFMA(k=0): all cross → wait_lr
assert _preop_kinds(s0.mfma) == [('wait_lr', False, None)]
assert len(s0.mfma.deps) == 0
# LR A @s0: dep removed (guaranteed by prev MT)
assert _preop_kinds(_get_lr(s0, 'A')) == []
assert len(_get_lr(s0, 'A').deps) == 0
# GR A @s0: same-subIterK dep preserved
gr_a0 = [gr for gr in s0.grs if gr.tensor == 'A'][0]
assert _preop_kinds(gr_a0) == [('wait_lr', True, None)]
assert len(gr_a0.deps) == 1
# MFMA(k=1): all cross → wait_lr
assert _preop_kinds(s1.mfma) == [('wait_lr', False, None)]
assert len(s1.mfma.deps) == 0
# LR A @s1: wait_gr_sync with counts
lr_a1 = _get_lr(s1, 'A')
assert _preop_kinds(lr_a1) == [('wait_gr', True, {'A': 8, 'B': 9, 'SA': 1, 'SB': 1})]
# LR B @s1: wait_gr with has_sync
lr_b1 = _get_lr(s1, 'B')
assert _preop_kinds(lr_b1) == [('wait_gr', True, {'A': 8, 'B': 1, 'SA': 1, 'SB': 1})]
# LR SA @s1
lr_sa1 = _get_lr(s1, 'SA')
assert _preop_kinds(lr_sa1) == [('wait_gr', True, {'A': 8, 'B': 1, 'SA': 0, 'SB': 1})]
# LR SB @s1
lr_sb1 = _get_lr(s1, 'SB')
assert _preop_kinds(lr_sb1) == [('wait_gr', True, {'A': 8, 'B': 1, 'SA': 0, 'SB': 0})]
# GR B @s1: dep removed
gr_b1 = [gr for gr in s1.grs if gr.tensor == 'B'][0]
assert _preop_kinds(gr_b1) == []
assert len(gr_b1.deps) == 0
def test_2x2_DU512(self):
"""256x256, DU512, FP4, 2x2 partition. Spot checks."""
cfg = make_cfg_256x256_fp4(depthU=512, partSizeM=4, partSizeN=4)
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.REMOVE_DEPS)
# All P0 MFMAs have wait_lr
assert _preop_kinds(parts[0][0].mfma) == [('wait_lr', False, None)]
# All P3 MFMAs have wait_lr
for slot in parts[3]:
assert _preop_kinds(slot.mfma) == [('wait_lr', False, None)]
# LR A @P3:s3: wait_gr_sync — forward span from re-anchored GR dep
lr_a_p3_s3 = _get_lr(parts[3][3], 'A')
assert lr_a_p3_s3.preOps[0].wait_gr_counts.A == 20
# LR SA @P3:s2: wait_gr_sync with SA=1
lr_sa_p3_s2 = _get_lr(parts[3][2], 'SA')
assert lr_sa_p3_s2.preOps[0].wait_gr_counts.SA == 1
def test_320x256_5part_reanchor_pi1(self):
"""Single-DU 5-partition: wait_gr counts match develop legacy inflight walk."""
kernel = create_kernel(320, 256, fp4=True, depthU=256, miWaveGroup=[2, 2])
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
cfg = SchedulerConfig(
numMFMATilesM=10, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=grA, grB=grB,
lrSA=ReadGranularity(mn=2, k=2), lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
partitionSizeM=2, partitionSizeN=8, pgr=1,
)
assert cfg.numPartitions == 5
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_WAIT_LR_SYNC)
lr_a1 = _get_lr(schedule[1][1], 'A')
wait = next(op for op in lr_a1.preOps if op.kind == 'wait_gr')
c = wait.wait_gr_counts
assert c.A + c.B + c.SA + c.SB <= 10, (
f"pi=1 k=1 wait_gr inflated: A={c.A} B={c.B} SA={c.SA} SB={c.SB}")
assert c.A == 6 and c.B == 4, (
f"expected same-partition sk=0 anchor: A={c.A} B={c.B}")
emitted = sched.build(stop_after=Pass.EMIT)
assert len([e for e in emitted[1][1] if e.opType == 'wait_gr']) == 1
def test_320x256_5part_mt_off_same_partition_sk1(self):
"""Single-DU: same-partition sk=1 dep uses develop ring walk (not multi-DU re-anchor)."""
kernel = create_kernel(320, 256, fp4=True, depthU=256, miWaveGroup=[2, 2])
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
cfg = SchedulerConfig(
numMFMATilesM=10, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=grA, grB=grB,
lrSA=ReadGranularity(mn=2, k=2), lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
partitionSizeM=2, partitionSizeN=8, pgr=1,
)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_WAIT_LR_SYNC)
lr_a1 = _get_lr(schedule[2][1], 'A')
wait = next(op for op in lr_a1.preOps if op.kind == 'wait_gr')
c = wait.wait_gr_counts
assert c.A + c.B + c.SA + c.SB <= 12, (
f"pi=2 k=1 wait_gr inflated: A={c.A} B={c.B} SA={c.SA} SB={c.SB}")
assert c.A == 2 and c.B == 8, (
f"expected same-partition sk=0 walk (not ring wrap): A={c.A} B={c.B}")
emitted = sched.build(stop_after=Pass.EMIT)
assert len([e for e in emitted[2][1] if e.opType == 'wait_gr']) == 1
def test_320x256_5part_mt_off_cross_partition_sk1(self):
"""Single-DU: cross-partition sk=1 has no wait_gr (develop has no re-anchor path)."""
kernel = create_kernel(320, 256, fp4=True, depthU=256, miWaveGroup=[2, 2])
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
cfg = SchedulerConfig(
numMFMATilesM=10, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=grA, grB=grB,
lrSA=ReadGranularity(mn=2, k=2), lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
partitionSizeM=2, partitionSizeN=8, pgr=1,
)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_WAIT_LR_SYNC)
lr_a1 = _get_lr(schedule[3][1], 'A')
assert not any(op.kind == 'wait_gr' for op in lr_a1.preOps), (
f"pi=3 k=1 should not need wait_gr (no ring-wrap inflation): "
f"preOps={_preop_kinds(lr_a1)}")
emitted = sched.build(stop_after=Pass.EMIT)
assert len([e for e in emitted[3][1] if e.opType == 'wait_gr']) == 0
def test_320x256_5part_same_partition_s0_anchor_p4_sk1(self):
"""Single-DU: pi=4 k=1 wait_gr A=8 matches develop emit output."""
kernel = create_kernel(320, 256, fp4=True, depthU=256, miWaveGroup=[2, 2])
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
cfg = SchedulerConfig(
numMFMATilesM=10, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=grA, grB=grB,
lrSA=ReadGranularity(mn=2, k=2), lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
partitionSizeM=2, partitionSizeN=8, pgr=1,
)
sched = LogicalScheduler(cfg)
sched.build()
em = next(e for e in sched._emitted[4][1] if e.opType == 'wait_gr')
c = em.source.wait_gr_counts
assert c.A + c.B + c.SA + c.SB <= 8, (
f"pi=4 k=1 wait_gr inflated: A={c.A} B={c.B} SA={c.SA} SB={c.SB}")
assert c.A == 8, (
f"expected same-partition s0 anchor for LR A: A={c.A} B={c.B}")
def test_256x256_2part_multi_gr_sk1_wrap_drain(self):
"""LR B @ pi=1 k=1 with two sk=0 B GRs must fully drain (not B=7)."""
kernel = create_kernel(256, 256, fp4=True, depthU=256, miWaveGroup=[2, 2])
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
cfg = SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=grA, grB=grB,
lrSA=ReadGranularity(mn=scaleTiA.lrSubtileShape[0],
k=scaleTiA.lrSubtileShape[1]),
lrSB=ReadGranularity(mn=scaleTiB.lrSubtileShape[0],
k=scaleTiB.lrSubtileShape[1]),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
partitionSizeM=8, partitionSizeN=4, pgr=1,
)
sched = LogicalScheduler(cfg)
sched.build()
em = next(e for e in sched._emitted[1][1] if e.opType == 'wait_gr')
c = em.source.wait_gr_counts
assert c.A == 0 and c.B == 0, (
f"expected full drain for multi-GR sk=1 wrap: A={c.A} B={c.B}")
class TestInsertGrLrInc:
def test_1x1_DU256(self):
"""256x256, DU256, FP4. gr_inc at MT transitions, lr_inc on wrap-around."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR_INC)
s0, s1 = parts[0][0], parts[0][1]
# GR A @s0: mt=n+2, A was n → gr_inc(A)
gr_a0 = [gr for gr in s0.grs if gr.tensor == 'A'][0]
assert _preop_inc_tensors(gr_a0, 'gr_inc') == ['A']
gr_b0 = [gr for gr in s0.grs if gr.tensor == 'B'][0]
assert _preop_inc_tensors(gr_b0, 'gr_inc') == ['B']
# LR A @s1: mt=n+1, A was n+2 → lr_inc(A)
assert _preop_inc_tensors(_get_lr(s1, 'A'), 'lr_inc') == ['A']
assert _preop_inc_tensors(_get_lr(s1, 'B'), 'lr_inc') == ['B']
assert _preop_inc_tensors(_get_lr(s1, 'SA'), 'lr_inc') == ['SA']
assert _preop_inc_tensors(_get_lr(s1, 'SB'), 'lr_inc') == ['SB']
# GR B @s1: same mt as GR B @s0 → no duplicate gr_inc
gr_b1 = [gr for gr in s1.grs if gr.tensor == 'B'][0]
assert _preop_inc_tensors(gr_b1, 'gr_inc') == []
# GR SA, SB @s1: mt transition → gr_inc
gr_sa1 = [gr for gr in s1.grs if gr.tensor == 'SA'][0]
assert _preop_inc_tensors(gr_sa1, 'gr_inc') == ['SA']
gr_sb1 = [gr for gr in s1.grs if gr.tensor == 'SB'][0]
assert _preop_inc_tensors(gr_sb1, 'gr_inc') == ['SB']
# Ordering: wait_gr_sync before lr_inc
lr_a1 = _get_lr(s1, 'A')
assert lr_a1.preOps[0].kind == 'wait_gr'
assert lr_a1.preOps[1].kind == 'lr_inc'
def test_multipartition_bf16_gr_inc_only_at_base_id_0(self):
"""128x128, BF16, 2x2 partition. gr_inc only at tileId_start=0."""
cfg = make_cfg_bf16(128, 128, partSizeM=2, partSizeN=2)
assert cfg.numPartitions == 4
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GR_INC)
# P0/P1: GR with tileId_start > 0 → no gr_inc
for pi in [0, 1]:
tensor = 'A' if pi == 0 else 'B'
for slot in parts[pi]:
for gr in slot.grs:
if gr.tensor == tensor:
assert gr.tiles.tileId_start > 0
assert _preop_inc_tensors(gr, 'gr_inc') == []
# P2: GR A with tileId_start=0 → gr_inc(A)
gr_a_p2 = [gr for s in parts[2] for gr in s.grs
if gr.tensor == 'A' and gr.tiles.tileId_start == 0]
assert len(gr_a_p2) == 1
assert _preop_inc_tensors(gr_a_p2[0], 'gr_inc') == ['A']
def test_1x1_multi_du_gr_inc(self):
"""Multi-DU PGR=1: gr_inc as postOps on the globally last GR per (tensor, uid).
With multi-DU, GRInc must fire AFTER all loads for a uid complete
(not before the first load), because uid=0 and uid=1 loads are
interleaved across slots and share the same SRD.
"""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR_INC)
slots = schedule[0]
# No preOp gr_inc on any GR (multi-DU PGR=1 uses postOps)
all_preop_gr_inc = [op for slot in slots for gr in slot.grs
for op in gr.preOps if op.kind == 'gr_inc']
assert len(all_preop_gr_inc) == 0
# postOp gr_inc on the globally last GR of each (tensor, uid)
all_postop_gr_inc = [op for slot in slots for gr in slot.grs
for op in gr.postOps if op.kind == 'gr_inc']
postop_keys = [(op.tensor, op.unrollId) for op in all_postop_gr_inc]
assert ('A', 0) in postop_keys
assert ('A', 1) in postop_keys
assert ('B', 0) in postop_keys
assert ('B', 1) in postop_keys
assert ('SA', 0) in postop_keys
assert ('SB', 0) in postop_keys
# SA/SB have numUnroll=1, no uid=1
assert ('SA', 1) not in postop_keys
assert ('SB', 1) not in postop_keys
def test_1x1_multi_du_lr_inc_at_uid_boundary(self):
"""Multi-DU: lr_inc inserted at uid boundary to match GRIncOp swaps.
numUnroll[A]=numUnroll[B]=2, per_uid_k=2 for A/B, per_uid_k=4 for SA/SB.
LR layout (k values, rotated offset):
s0: LR A k=1 (uid=0), LR B k=1 (uid=0), LR SA k=2 (uid=0)
s1: LR A k=2 (uid=1), LR B k=2 (uid=1), LR SB k=2 (uid=0)
s2: LR A k=3 (uid=1), LR B k=3 (uid=1)
s3: LR A k=0 (uid=0, mt=1), LR B k=0 (uid=0, mt=1),
LR SA k=0 (mt=1), LR SB k=0 (mt=1)
Expected lr_inc:
- s1: LR A/B get lr_inc (uid boundary: k=1→k=2, uid 0→1)
- s3: LR A/B/SA/SB get lr_inc (MT transition: mt=0→mt=1)
No first-LR lr_inc at s0 (MT transition already covers it).
"""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR_INC)
slots = schedule[0]
# s0: no lr_inc (first LRs for A/B, but MT transition will cover them)
lr_a0 = _get_lr(slots[0], 'A')
lr_b0 = _get_lr(slots[0], 'B')
assert _preop_inc_tensors(lr_a0, 'lr_inc') == []
assert _preop_inc_tensors(lr_b0, 'lr_inc') == []
# s1: uid boundary for A and B (k=1→k=2, uid 0→1) → lr_inc
lr_a1 = _get_lr(slots[1], 'A')
lr_b1 = _get_lr(slots[1], 'B')
assert _preop_inc_tensors(lr_a1, 'lr_inc') == ['A']
assert _preop_inc_tensors(lr_b1, 'lr_inc') == ['B']
# s2: no lr_inc (same uid=1 as s1)
lr_a2 = _get_lr(slots[2], 'A')
lr_b2 = _get_lr(slots[2], 'B')
assert _preop_inc_tensors(lr_a2, 'lr_inc') == []
assert _preop_inc_tensors(lr_b2, 'lr_inc') == []
# s3: MT transition (mt=0→mt=1) → lr_inc for all tensors
lr_a3 = _get_lr(slots[3], 'A')
lr_b3 = _get_lr(slots[3], 'B')
lr_sa3 = _get_lr(slots[3], 'SA')
lr_sb3 = _get_lr(slots[3], 'SB')
assert _preop_inc_tensors(lr_a3, 'lr_inc') == ['A']
assert _preop_inc_tensors(lr_b3, 'lr_inc') == ['B']
assert _preop_inc_tensors(lr_sa3, 'lr_inc') == ['SA']
assert _preop_inc_tensors(lr_sb3, 'lr_inc') == ['SB']
# Total lr_inc: 2 per A, 2 per B, 1 per SA, 1 per SB = 6
# Matches gr_inc count (2 per A/B, 1 per SA, 1 per SB = 6)
all_lr_inc = [op for slot in slots for lr in slot.lrs
for op in lr.preOps if op.kind == 'lr_inc']
all_gr_inc = [op for slot in slots for gr in slot.grs
for op in gr.preOps if op.kind == 'gr_inc']
assert len(all_lr_inc) == len(all_gr_inc) == 6
# ══════════════════════════════════════════════════════════════
# Step 7: Compute inflight loads
# ══════════════════════════════════════════════════════════════
class TestComputeInflightLoads:
def test_DU256(self):
"""Validate inflight load counts match remove_cross_deps output."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_DEPS)
s0 = schedule[0][0]
s1 = schedule[0][1]
# LR A @s0: dep removed (guaranteed by prev MT)
assert _preop_kinds(_get_lr(s0, 'A')) == []
# LR B @s1: counts
lr_b1 = _get_lr(s1, 'B')
assert lr_b1.preOps[0].wait_gr_counts.A == 8
assert lr_b1.preOps[0].wait_gr_counts.B == 1
assert lr_b1.preOps[0].wait_gr_counts.SA == 1
# LR SA @s1: counts
lr_sa1 = _get_lr(s1, 'SA')
assert lr_sa1.preOps[0].wait_gr_counts.A == 8
assert lr_sa1.preOps[0].wait_gr_counts.B == 1
# LR A @s1: counts
lr_a1 = _get_lr(s1, 'A')
assert lr_a1.preOps[0].wait_gr_counts.A == 8
assert lr_a1.preOps[0].wait_gr_counts.B == 9
assert lr_a1.preOps[0].wait_gr_counts.SA == 1
assert lr_a1.preOps[0].wait_gr_counts.SB == 1
def test_fp8_DU128_asymmetric_A_lt_B(self):
"""FP8 DU=128, MT=128x192, waveGroup=(2,2), PGR=2.
Regression for _compute_inflight_loads bug (commit 055ecc8):
With flat_len=1 (1 partition × 1 subIterK), wraps_needed=1
(cross-MT dep), consumer_flat==dep_flat==0, the old wrap-counting
code walked the single slot twice instead of once — overcounting all
GRs in the slot as extra inflight.
Correct behavior: walk exactly wraps_needed*flat_len=1 step; on the
final step stop immediately at the dep GR (A), yielding A=0. The B
GRs that were emitted after A (higher sort key) are still counted.
Old (buggy) counts for LR A: A=4, B=12.
New (correct) counts for LR A: A=0, B=6.
"""
cfg = make_cfg_fp8(128, 192, waveGroup=(2, 2))
assert cfg.numSubIterK == 1, "FP8 DU=128 / matrixInstK=128 → single subIterK"
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_DEPS)
s0 = schedule[0][0]
# LR A: dep is GR A (emitted first → last in reverse order).
# All B GRs (emitted after A, encountered first in backward walk) are inflight.
lr_a = _get_lr(s0, 'A')
assert lr_a.preOps[0].wait_gr_counts.A == 0
assert lr_a.preOps[0].wait_gr_counts.B == 6 # 6 B tiles in MT192 / wg2
# LR B: dep is GR B (emitted last → first in reverse order); nothing after it.
lr_b = _get_lr(s0, 'B')
assert lr_b.preOps[0].wait_gr_counts.A == 0
assert lr_b.preOps[0].wait_gr_counts.B == 0
def test_fp8_DU128_asymmetric_A_gt_B(self):
"""FP8 DU=128, MT=448x64, waveGroup=(4,1), PGR=2.
Same _compute_inflight_loads regression as test_fp8_DU128_asymmetric_A_lt_B
but with more A tiles than B tiles (7A vs 4B).
Old (buggy) counts for LR A: A=7, B=8.
New (correct) counts for LR A: A=0, B=4.
"""
cfg = make_cfg_fp8(448, 64, waveGroup=(4, 1))
assert cfg.numSubIterK == 1
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_DEPS)
s0 = schedule[0][0]
lr_a = _get_lr(s0, 'A')
assert lr_a.preOps[0].wait_gr_counts.A == 0
assert lr_a.preOps[0].wait_gr_counts.B == 4 # 4 B tiles in MT64 / wg1
lr_b = _get_lr(s0, 'B')
assert lr_b.preOps[0].wait_gr_counts.A == 0
assert lr_b.preOps[0].wait_gr_counts.B == 0
def test_multi_du_multi_partition_inflight_counts(self):
"""Multi-DU + multi-partition: uid-aware distribution keeps GRs
at their natural positions, so _compute_inflight_loads produces
accurate non-zero counts (no force_drain needed)."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2,
partSizeM=4, partSizeN=4, pgr=1)
assert cfg.numPartitions > 1
assert max(cfg.numUnroll.values()) > 1
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.REMOVE_DEPS)
found_any = False
found_force_drain = False
for pi, slots in enumerate(schedule):
for slot in slots:
for lr in slot.lrs:
if not lr.preOps:
continue
wait = lr.preOps[0]
if not hasattr(wait, 'wait_gr_counts'):
continue
gr_deps = [d for d in lr.deps if isinstance(d.ref, GRPlacement)]
any_cross = any(d.mt_offset != 0 for d in gr_deps)
if lr.mtIteration > 0 or any_cross:
found_any = True
if getattr(wait, 'force_drain', False):
found_force_drain = True
continue
c = wait.wait_gr_counts
# Multi-DU PGR=1 wrap LRs use empty counts (vmcnt drain).
if cfg.pgr == 1 and lr.mtIteration > 0:
continue
has_nonzero = c.A > 0 or c.B > 0 or c.SA > 0 or c.SB > 0
assert has_nonzero, (
f"pi={pi} slot={slot.subIterK} lr={lr.tensor} "
f"mt={lr.mtIteration}: expected non-zero inflight "
f"counts, got {c}")
assert found_any, "Expected at least one cross-iter / next-MT LR dep"
# ══════════════════════════════════════════════════════════════
# Step 8: Group LR/GR
# ══════════════════════════════════════════════════════════════
class TestGroupLrGr:
def test_1x1_DU256(self):
"""256x256, DU256, FP4. Chain grouping with merged preOps."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
parts = sched.build(stop_after=Pass.GROUP_LR_GR)
s0, s1 = parts[0][0], parts[0][1]
# s0: LR chain A←B, no preOps (deps removed)
lr_a0 = _get_lr(s0, 'A')
lr_b0 = _get_lr(s0, 'B')
assert lr_a0.preOps == []
assert lr_b0.deps[0].ref is lr_a0
# s0: GR chain A←B, first GR dep→last LR
gr_a0 = [gr for gr in s0.grs if gr.tensor == 'A'][0]
gr_b0 = [gr for gr in s0.grs if gr.tensor == 'B'][0]
assert gr_a0.preOps[0].kind == 'wait_lr'
assert gr_a0.preOps[1].kind == 'gr_inc'
assert gr_a0.deps[0].ref is lr_b0
assert gr_b0.deps[0].ref is gr_a0
# s1: LR chain A←B←SA←SB, merged preOps on A
lr_a1 = _get_lr(s1, 'A')
lr_b1 = _get_lr(s1, 'B')
lr_sa1 = _get_lr(s1, 'SA')
lr_sb1 = _get_lr(s1, 'SB')
assert lr_a1.preOps[0].kind == 'wait_gr'
assert lr_a1.preOps[0].wait_gr_counts.A == 8
assert len(lr_a1.preOps) == 5 # wait_gr_sync + 4 lr_inc
assert lr_b1.deps[0].ref is lr_a1
assert lr_sa1.deps[0].ref is lr_b1
assert lr_sb1.deps[0].ref is lr_sa1
# s1: GR chain B←SA←SB (collision deps removed — covered by
# MFMA@s0 which already syncs on the same LR SA/SB)
gr_b1 = [gr for gr in s1.grs if gr.tensor == 'B'][0]
gr_sa1 = [gr for gr in s1.grs if gr.tensor == 'SA'][0]
gr_sb1 = [gr for gr in s1.grs if gr.tensor == 'SB'][0]
assert gr_b1.preOps == []
assert gr_sa1.preOps[0].kind == 'gr_inc'
assert gr_sb1.deps[0].ref is gr_sa1
# ══════════════════════════════════════════════════════════════
# Step 9: Remove unnecessary wait_lr_sync
# ══════════════════════════════════════════════════════════════
class TestRemoveUnnecessaryWaitLrSync:
def test_bf16_1x1(self):
"""256x256, BF16, 1x1. Verify wait_lr_sync handling."""
cfg = make_cfg_bf16(256, 256)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GROUP_LR_GR)
# After group_lr_gr, get the preOps state
sched.remove_unnecessary_wait_lr_sync()
parts = schedule
# Verify no crash and GR preOps are present
for slot in parts[0]:
for gr in slot.grs:
for op in gr.preOps:
assert op.kind in ('wait_lr', 'sync', 'gr_inc', 'lr_inc',
'wait_gr', 'wait_gr', 'wait_lr', 'skip')
# ══════════════════════════════════════════════════════════════
# Step 10: Emit
# ══════════════════════════════════════════════════════════════
class TestEmit:
def test_1x1_DU256(self):
"""256x256, DU256, FP4. EmittedModule chains with correct links."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
result = sched.build(stop_after=Pass.EMIT)
assert len(result) == 1 # 1 partition
assert len(result[0]) == 2 # 2 subIterKs
# subIterK=0: mfma + 2 LRs + 2 GRs = 5 primary
em0 = result[0][0]
primary0 = [e for e in em0 if e.opType in ('mfma', 'lr', 'gr')]
assert len(primary0) == 5
assert primary0[0].opType == 'mfma'
# MFMA has wait_lr before-link
assert em0[0].before is not None
assert em0[em0[0].before].opType == 'wait_lr'
# No wait_gr in s0
assert len([e for e in em0 if e.opType == 'wait_gr']) == 0
# subIterK=1: mfma + 4 LRs + 3 GRs = 8 primary
em1 = result[0][1]
primary1 = [e for e in em1 if e.opType in ('mfma', 'lr', 'gr')]
assert len(primary1) == 8
assert len([e for e in em1 if e.opType == 'wait_gr']) == 1
# No self-loops
for em_list in [em0, em1]:
for e in em_list:
if e.before is not None:
assert e.before != e.moduleId
# ══════════════════════════════════════════════════════════════
# Step 11: Build loop variants
# ══════════════════════════════════════════════════════════════
class TestBuildPreloop:
def test_256x256_fp4(self):
"""Preloop contains GR(n), LR, GR(n+1). No GR(n+2) or gr_inc."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.build_preloop()
preloop = sched._preloop_emitted
assert len(preloop) > 0
# Collect all opTypes across all partitions/subIterKs
all_ops = [em.opType for partition in preloop
for group in partition for em in group]
# Should have lr and gr ops
assert 'lr' in all_ops
assert 'gr' in all_ops
# Preloop has gr_inc for the n→n+1 transition but not lr_inc
# (LR data is loaded fresh, not from a previous MT iteration)
assert 'lr_inc' not in all_ops
def test_multi_du_preloop(self):
"""Multi-DU: PGR=1 preloop loads per-uid with GRIncOps between uids."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
sched.build()
sched.build_preloop()
preloop = sched._preloop_emitted
grs = [em.source for partition in preloop
for group in partition for em in group if em.opType == 'gr']
# MT=0 GRs: A uid=0, A uid=1, B uid=0, B uid=1, SA uid=0, SB uid=0
mt0 = [(g.tensor, g.unrollId) for g in grs if g.mtIteration == 0]
assert ('A', 0) in mt0
assert ('A', 1) in mt0
assert ('B', 0) in mt0
assert ('B', 1) in mt0
assert ('SA', 0) in mt0
assert ('SB', 0) in mt0
assert ('SA', 1) not in mt0
assert ('SB', 1) not in mt0
# PGR=1: no MT=1 GRs, no SkipToNGLL
assert not any(g.mtIteration == 1 for g in grs)
# gr_inc between uid groups (SRD advance + LDS buffer swap)
gr_incs = [em.source for partition in preloop
for group in partition for em in group
if em.opType == 'gr_inc']
inc_keys = [(op.tensor, op.unrollId) for op in gr_incs]
assert ('A', 0) in inc_keys
assert ('A', 1) in inc_keys
assert ('B', 0) in inc_keys
assert ('B', 1) in inc_keys
# SA/SB (numUnroll=1) need GR_inc at uid=0 to advance scale SRD
# and swap scale LDS write buffer between DU rounds.
assert ('SA', 0) in inc_keys
assert ('SB', 0) in inc_keys
class TestBuildNGLL:
def test_256x256_fp4(self):
"""NGLL removes GR(n+2). LR, MFMA, and gr_inc preserved (gr_inc
still needed to swap LW for tail entry)."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.build_ngll()
ngll = sched._ngll_emitted
assert len(ngll) > 0
all_ops = [em.opType for partition in ngll
for group in partition for em in group]
assert 'mfma' in all_ops
assert 'lr' in all_ops
# NGLL must drop GR(n+2) loads
for partition in ngll:
for group in partition:
for em in group:
if em.opType == 'gr':
assert em.source.mtIteration != 2, \
"NGLL should not contain GR(n+2)"
def test_multi_du_ngll(self):
"""Multi-DU: PGR=1 — NGLL is empty."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
sched.build()
sched.build_ngll()
ngll = sched._ngll_emitted
assert ngll == [[[]]]
class TestBuildNLL:
def test_256x256_fp4(self):
"""NLL removes GRs, LR(n+1), and (PGR=2) gr_inc. LR(n), MFMA, and
lr_inc remain — lr_inc is required to swap LR base for the next MT."""
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.build_nll()
nll = sched._nll_emitted
assert len(nll) > 0
all_ops = [em.opType for partition in nll
for group in partition for em in group]
assert 'mfma' in all_ops
# NLL drops global loads and (under PGR=2) gr_inc
assert 'gr' not in all_ops
assert 'gr_inc' not in all_ops
# LR(n+1) must be gone — only LR(n) remains
for partition in nll:
for group in partition:
for em in group:
if em.opType == 'lr':
assert em.source.mtIteration == 0, \
"NLL should only contain LR(n)"
def test_multi_du_nll(self):
"""Multi-DU: NLL strips GRs, gr_inc, iteration-boundary lr_inc,
but keeps isUnrollSwap lr_inc (needed to switch LDS read buffer between uids)."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2)
sched = LogicalScheduler(cfg)
sched.build()
sched.build_nll()
nll = sched._nll_emitted
all_ops = [em.opType for partition in nll
for group in partition for em in group]
all_sources = [em.source for partition in nll
for group in partition for em in group]
assert 'mfma' in all_ops
assert 'gr' not in all_ops
assert 'gr_inc' not in all_ops
unroll_swap_lr_incs = [s for op, s in zip(all_ops, all_sources)
if op == 'lr_inc' and s.isUnrollSwap]
non_swap_lr_incs = [s for op, s in zip(all_ops, all_sources)
if op == 'lr_inc' and not s.isUnrollSwap]
assert len(unroll_swap_lr_incs) > 0, "isUnrollSwap lr_inc must be kept in NLL"
assert len(non_swap_lr_incs) == 0, "iteration-boundary lr_inc must be removed in NLL"
# ══════════════════════════════════════════════════════════════
# SchedulerConfig from TileInfo
# ══════════════════════════════════════════════════════════════
class TestFromTileInfo:
def test_64x64_fp4(self):
"""MT=64, FP4 — matches design doc Example Granularities 1 tile counts."""
kernel = create_kernel(64, 64, fp4=True)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
cfg = SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
lrSA=ReadGranularity(mn=2, k=2),
lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0], k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0], k=scaleTiB.localMMATileGrid[1]),
)
assert cfg.numMFMATilesM == 2 # 64/16/2
assert cfg.numMFMATilesN == 2
assert cfg.numSubIterK == 2
assert cfg.hasScale
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions[0]) == 2
assert partitions[0][0].mfma.tileA.tileId_end == 2
def test_256x256_no_scale(self):
"""MT=256, no scale."""
cfg = make_cfg_bf16(256, 256)
assert cfg.numMFMATilesM == 8 # 256/16/2
assert cfg.numMFMATilesN == 8
assert cfg.numSubIterK == 2
assert not cfg.hasScale
class TestInstructionEmitterPerUidK:
"""LR emit uses numSubIterK/numUnroll per uid, not grGran.k (single-DU bug)."""
def test_single_du_per_uid_k_spans_full_numSubIterK(self):
"""Single-DU MX (numUnroll=1): per_uid_k must be numSubIterK, not grGran.k."""
cfg = make_cfg_256x256_fp4()
assert cfg.numSubIterK == 2
assert cfg.numUnroll == {'A': 1, 'B': 1, 'SA': 1, 'SB': 1}
assert cfg.lrA.k < cfg.numSubIterK # LR steps per subIterK; emit spans full range
from Tensile.Components.Subtile.InstructionEmitter import InstructionEmitter
emitter = InstructionEmitter(
writer=MagicMock(), kernel={}, config=cfg,
tileInfoA=MagicMock(subtileShape=[1, 1]),
tileInfoB=MagicMock(subtileShape=[1, 1]),
dtileInfo=MagicMock(), vgprTilesA=[], vgprTilesB=[],
scaleTileInfoA=MagicMock(), scaleTileInfoB=MagicMock(),
)
assert emitter._per_uid_k['A'] == 2
assert emitter._per_uid_k['B'] == 2
def test_multi_du_per_uid_k_matches_gr_k_window(self):
"""Multi-DU: per_uid_k = numSubIterK / numUnroll = grGran.k per uid slice."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2)
assert cfg.numSubIterK == 4
assert cfg.numUnroll == {'A': 2, 'B': 2, 'SA': 1, 'SB': 1}
from Tensile.Components.Subtile.InstructionEmitter import InstructionEmitter
emitter = InstructionEmitter(
writer=MagicMock(), kernel={}, config=cfg,
tileInfoA=MagicMock(subtileShape=[1, 1]),
tileInfoB=MagicMock(subtileShape=[1, 1]),
dtileInfo=MagicMock(), vgprTilesA=[], vgprTilesB=[],
scaleTileInfoA=MagicMock(), scaleTileInfoB=MagicMock(),
)
assert emitter._per_uid_k == {'A': 2, 'B': 2, 'SA': 4, 'SB': 4}
class TestVgprAllocatorSelection:
"""Free-list VGPR path is only for MX multi-DU (numUnroll>1), not numSubIterK>1."""
def test_single_du_mx_uses_deterministic_allocator(self):
cfg = make_cfg_256x256_fp4(pgr=1)
assert cfg.numSubIterK == 2
assert max(cfg.numUnroll.values()) == 1
sched = LogicalScheduler(cfg)
assert not sched._use_free_list_vgpr_allocation()
def test_multi_du_mx_uses_free_list_allocator(self):
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
assert max(cfg.numUnroll.values()) > 1
sched = LogicalScheduler(cfg)
assert sched._use_free_list_vgpr_allocation()
def test_pgr0_mx_multi_du_does_not_use_free_list(self):
"""Gate is off for pgr=0 even when scaled and multi-DU: pgr=0 has no
prefetch double-buffer, so the deterministic allocator is used."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=0)
assert cfg.hasScale
sched = LogicalScheduler(cfg)
assert sched._is_multi_du()
assert not sched._use_free_list_vgpr_allocation()
def test_non_scale_does_not_use_free_list(self):
"""Gate requires scale tensors; plain BF16 (no scale) always uses the
deterministic allocator."""
cfg = make_cfg_bf16()
assert not cfg.hasScale
sched = LogicalScheduler(cfg)
assert not sched._use_free_list_vgpr_allocation()
class TestFreeListVgprAllocation:
"""Non-regression assertions for the MX multi-DU free-list VGPR allocator
(_assign_vgpr_tiles_free_list). Lifetime-based FIFO reuse must keep the
physical id set densely packed at [0, peak) so peak equals the maximum
number of concurrently-live tile groups, not the sum of all allocations.
"""
def _collect_ids(self, sched):
"""Gather every physical vgprTileId assigned per tensor."""
ids = {t: set() for t in sched.tile_peaks}
for slots in sched._partitions:
for slot in slots:
if slot.mfma:
for tensor, maps in slot.mfma.vgpr_tile_maps.items():
for tile_map in maps:
ids[tensor].update(tile_map.values())
for lr in slot.lrs:
for tile_map in lr.vgpr_tile_map:
ids[lr.tensor].update(tile_map.values())
return ids
def test_free_list_ids_dense_and_capped_at_peak(self):
"""FIFO reuse repacks freed ids: every id in [0, peak) is used and
none exceeds peak, so the live-id count never grows past the peak."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
assert sched._use_free_list_vgpr_allocation()
sched.build(stop_after=Pass.VGPR_TILES)
assert sched.tile_peaks == {'A': 16, 'B': 16, 'SA': 8, 'SB': 8}
ids = self._collect_ids(sched)
for tensor, peak in sched.tile_peaks.items():
assert ids[tensor] == set(range(peak)), (
f"{tensor}: ids {sorted(ids[tensor])} not densely packed "
f"into [0, {peak}) — free-list reuse regressed")
def test_free_list_distinct_ids_for_concurrent_groups(self):
"""Within one MFMA slot's per-uid tile map, concurrently-live groups
each get a distinct physical id (no aliasing of live groups)."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
for slots in schedule:
for slot in slots:
if not slot.mfma:
continue
for tensor, maps in slot.mfma.vgpr_tile_maps.items():
for ui, tile_map in enumerate(maps):
vids = list(tile_map.values())
assert len(vids) == len(set(vids)), (
f"{tensor} uid={ui}: two concurrent groups share "
f"one id in {tile_map}")
def test_alloc_vgpr_tiles_list_lengths_match_peaks(self):
"""allocVgprTiles allocates one RegisterTileInfo per tile_peaks entry."""
import math
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, _dTileInfo = make_writer_and_tileinfos(
kernel, fp4=True)
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
def _expected_regs(tileInfo, lrGran):
return int(math.ceil(tileInfo.mmaTileRegCount * lrGran.k * lrGran.mn))
try:
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
assert len(sched.vgprTilesA) == sched.tile_peaks['A']
assert len(sched.vgprTilesB) == sched.tile_peaks['B']
assert len(sched.vgprTilesSA) == sched.tile_peaks['SA']
assert len(sched.vgprTilesSB) == sched.tile_peaks['SB']
for tile in sched.vgprTilesA:
assert len(tile.regList.indices) == _expected_regs(tiA, cfg.lrA)
for tile in sched.vgprTilesB:
assert len(tile.regList.indices) == _expected_regs(tiB, cfg.lrB)
for tile in sched.vgprTilesSA:
assert len(tile.regList.indices) == _expected_regs(scaleTiA, cfg.lrSA)
for tile in sched.vgprTilesSB:
assert len(tile.regList.indices) == _expected_regs(scaleTiB, cfg.lrSB)
finally:
sched.deallocVgprTiles(writer)
def test_free_list_multi_partition_peaks(self):
"""Multi-DU free-list with N partitions: ids stay dense in [0, peak)."""
cfg = make_cfg_256x256_fp4(
partSizeN=4, grSA_k_gran=2, grSB_k_gran=2, pgr=1)
assert cfg.numPartitions > 1
sched = LogicalScheduler(cfg)
assert sched._use_free_list_vgpr_allocation()
sched.build(stop_after=Pass.VGPR_TILES)
ids = self._collect_ids(sched)
for tensor, peak in sched.tile_peaks.items():
assert peak > 0
assert ids[tensor] == set(range(peak)), (
f"{tensor}: ids {sorted(ids[tensor])} not densely packed "
f"into [0, {peak}) with {cfg.numPartitions} partitions")
def test_compact_b_overlay_reuses_ids_in_same_slot(self):
"""Multi-DU free-list sets compact_b_overlay; B LR at subIterK k hands
the same vgprTileIds to B MFMA at k+1 within a partition (in-slot
MFMA-then-LR execution reuses that peak budget instead of doubling)."""
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.VGPR_TILES)
assert sched.compact_b_overlay
assert sched._use_free_list_vgpr_allocation()
assert sched.tile_peaks['B'] == 16
cfg_det = make_cfg_256x256_fp4()
sched_det = LogicalScheduler(cfg_det)
sched_det.build(stop_after=Pass.VGPR_TILES)
assert not sched_det.compact_b_overlay
for slots in sched._partitions:
for k in range(len(slots) - 1):
lr_vids = None
for lr in slots[k].lrs:
if lr.tensor == 'B' and lr.vgpr_tile_map:
lr_vids = set(lr.vgpr_tile_map[0].values())
break
assert lr_vids is not None
mfma_vids = set(slots[k + 1].mfma.vgpr_tile_maps['B'][0].values())
assert lr_vids == mfma_vids, (
f"B overlay: LR k={k} ids {sorted(lr_vids)} must match "
f"MFMA k={k + 1} ids {sorted(mfma_vids)}")
# ══════════════════════════════════════════════════════════════
# get_partition_candidates
# ══════════════════════════════════════════════════════════════
class TestPartitionCandidates:
def test_square(self):
"""M==N: partitions N, full then divUp(N,2) down to 1."""
kernel = create_kernel(256, 256)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
candidates = SchedulerConfig.get_partition_candidates(tiA, tiB)
assert candidates == [(8, 8), (8, 4), (8, 3), (8, 2), (8, 1)]
def test_n_larger(self):
"""N > M: partitions N."""
kernel = create_kernel(64, 256)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
candidates = SchedulerConfig.get_partition_candidates(tiA, tiB)
assert candidates == [(2, 8), (2, 4), (2, 3), (2, 2), (2, 1)]
def test_m_larger(self):
"""M > N: partitions M."""
kernel = create_kernel(256, 64)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
candidates = SchedulerConfig.get_partition_candidates(tiA, tiB)
assert candidates == [(8, 2), (4, 2), (3, 2), (2, 2), (1, 2)]
def test_prime_dim(self):
"""Prime-sized dimension: full then divUp down to 1."""
cfg_tiA = MagicMock()
cfg_tiB = MagicMock()
cfg_tiA.localMMATileGrid = [5, 2]
cfg_tiB.localMMATileGrid = [3, 2]
candidates = SchedulerConfig.get_partition_candidates(cfg_tiA, cfg_tiB)
assert candidates == [(5, 3), (3, 3), (2, 3), (1, 3)]
def test_composite(self):
"""Composite dimension with mixed prime factors."""
cfg_tiA = MagicMock()
cfg_tiB = MagicMock()
cfg_tiA.localMMATileGrid = [2, 2]
cfg_tiB.localMMATileGrid = [6, 2]
candidates = SchedulerConfig.get_partition_candidates(cfg_tiA, cfg_tiB)
assert candidates == [(2, 6), (2, 3), (2, 2), (2, 1)]
# ══════════════════════════════════════════════════════════════
# _normalize_partition_sizes
# ══════════════════════════════════════════════════════════════
class TestNormalizePartitionSizes:
"""Pin the current behavior of SchedulerConfig._normalize_partition_sizes.
These tests exercise the staticmethod directly; they bypass __post_init__
so no full SchedulerConfig is constructed.
"""
norm = staticmethod(SchedulerConfig._normalize_partition_sizes)
# ── single-int spec ──────────────────────────────────────
def test_zero_means_full_dim(self):
assert self.norm(0, 16, 'N') == [16]
def test_spec_equals_total(self):
assert self.norm(16, 16, 'N') == [16]
def test_divides_evenly(self):
assert self.norm(8, 16, 'N') == [8, 8]
assert self.norm(8, 24, 'N') == [8, 8, 8]
assert self.norm(1, 5, 'N') == [1, 1, 1, 1, 1]
def test_single_full_with_remainder(self):
# num_full == 1 → [s, remainder]
assert self.norm(8, 12, 'N') == [8, 4]
assert self.norm(22, 23, 'N') == [22, 1]
def test_remainder_placed_in_middle(self):
"""Single-DU / develop: remainder bracketed in the middle."""
# num_full=2, remainder=4, mid=1 → [s, rem, s]
assert self.norm(8, 20, 'N') == [8, 4, 8]
# num_full=2, remainder=7, mid=1 → [8,7,8]
assert self.norm(8, 23, 'N') == [8, 7, 8]
# num_full=5, remainder=3, mid=2 → [4,4,3,4,4,4]
assert self.norm(4, 23, 'N') == [4, 4, 3, 4, 4, 4]
# num_full=3, remainder=2, mid=1 → [4,2,4,4]
assert self.norm(4, 14, 'N') == [4, 2, 4, 4]
def test_remainder_placed_last(self):
# Multi-DU path: short partition must come LAST.
# num_full=2, remainder=4 → [s, s, rem]
assert self.norm(8, 20, 'N', 1, remainder_last=True) == [8, 8, 4]
# num_full=2, remainder=7 → [8,8,7]
assert self.norm(8, 23, 'N', 1, remainder_last=True) == [8, 8, 7]
# num_full=5, remainder=3 → [4,4,4,4,4,3]
assert self.norm(4, 23, 'N', 1, remainder_last=True) == [4, 4, 4, 4, 4, 3]
# num_full=3, remainder=2 → [4,4,4,2]
assert self.norm(4, 14, 'N', 1, remainder_last=True) == [4, 4, 4, 2]
def test_spec_one(self):
assert self.norm(1, 4, 'N') == [1, 1, 1, 1]
# ── invalid single-int spec ──────────────────────────────
def test_spec_negative_raises(self):
with pytest.raises(AssertionError, match="must be in"):
self.norm(-1, 16, 'N')
def test_spec_larger_than_total_raises(self):
with pytest.raises(AssertionError, match="must be in"):
self.norm(20, 16, 'N')
# ── explicit list spec ───────────────────────────────────
def test_list_passthrough(self):
assert self.norm([8, 8], 16, 'N') == [8, 8]
assert self.norm([22, 1], 23, 'N') == [22, 1]
assert self.norm([4, 4, 3, 4, 4, 4], 23, 'N') == [4, 4, 3, 4, 4, 4]
def test_tuple_spec_returned_as_list(self):
result = self.norm((8, 8), 16, 'N')
assert result == [8, 8]
assert isinstance(result, list)
def test_list_wrong_sum_raises(self):
with pytest.raises(AssertionError, match="must sum to 16"):
self.norm([8, 7], 16, 'N')
def test_list_with_zero_raises(self):
with pytest.raises(AssertionError, match="must be >= 1"):
self.norm([8, 0, 8], 16, 'N')
def test_list_with_negative_raises(self):
with pytest.raises(AssertionError, match="must be >= 1"):
self.norm([10, -2, 8], 16, 'N')
def test_dim_label_in_error(self):
with pytest.raises(AssertionError, match="for M"):
self.norm([1, 2], 16, 'M')
# ── mn-aware behavior ────────────────────────────────────
def test_mn_default_is_one(self):
# Default mn=1: behavior identical to develop (remainder in the middle).
assert self.norm(8, 23, 'N') == [8, 7, 8]
def test_mn_already_aligned_passthrough(self):
assert self.norm(8, 16, 'N', 2) == [8, 8]
assert self.norm(8, 24, 'N', 4) == [8, 8, 8]
def test_mn_snaps_spec_down(self):
# 7 snaps down to 6 (largest mn-multiple <= 7), then split with
# remainder in the middle for single-DU.
assert self.norm(7, 16, 'N', 2) == [6, 4, 6]
def test_mn_snaps_spec_clamped_to_mn(self):
# spec=1 with mn=2 snaps up to mn (the floor of valid sizes).
assert self.norm(1, 16, 'N', 2) == [2] * 8
def test_mn_remainder_stays_aligned(self):
# total=22, s=8 → num_full=2, remainder=6 (even), placed middle → [8,6,8].
assert self.norm(8, 22, 'N', 2) == [8, 6, 8]
# total=20, s=8 → [8,4,8]; remainder 4 is mn-aligned.
assert self.norm(8, 20, 'N', 4) == [8, 4, 8]
def test_mn_zero_spec_uses_full_dim(self):
# spec=0 means "one partition for the whole dim"; if the whole dim
# is mn-aligned the result is [total].
assert self.norm(0, 16, 'N', 2) == [16]
def test_mn_total_not_aligned_falls_back_to_single(self):
# total=23, mn=2: no multi-partition split is mn-valid → [total].
# Caller's vgpr-budget loop sees a single big partition and moves on.
assert self.norm(0, 23, 'N', 2) == [23]
assert self.norm(8, 23, 'N', 2) == [23]
assert self.norm(22, 23, 'N', 2) == [23]
def test_mn_spec_equals_total(self):
assert self.norm(16, 16, 'N', 2) == [16]
# Aligned total but spec snaps down → multi-partition.
assert self.norm(16, 16, 'N', 4) == [16]
def test_mn_one_full_with_aligned_remainder(self):
# num_full=1 path with mn-aligned remainder.
assert self.norm(8, 12, 'N', 4) == [8, 4]
assert self.norm(12, 16, 'N', 4) == [12, 4]
def test_mn_explicit_list_aligned_passes(self):
assert self.norm([8, 4, 8], 20, 'N', 2) == [8, 4, 8]
assert self.norm([4, 4, 4, 4], 16, 'N', 4) == [4, 4, 4, 4]
def test_mn_explicit_list_unaligned_raises(self):
# The [22,1] FP4 case from standalone: 1 is not a multiple of mn=2.
with pytest.raises(AssertionError, match="multiples of mn=2"):
self.norm([22, 1], 23, 'N', 2)
def test_mn_explicit_list_one_unaligned_element_raises(self):
with pytest.raises(AssertionError, match="multiples of mn=4"):
self.norm([8, 6, 8], 22, 'N', 4)
def test_mn_monotonic_candidate_sweep(self):
# Mirrors how Kernel.py iterates get_partition_candidates from large
# to small. Snapping DOWN must keep the post-normalization sequence
# non-increasing in partition size, so the vgpr-budget loop never
# re-tries a size it already rejected.
candidates = [16, 8, 7, 6, 5, 4, 3, 2, 1]
normalized_first = [self.norm(s, 16, 'N', 2)[0] for s in candidates]
# Each step is <= the previous step.
for prev, cur in zip(normalized_first, normalized_first[1:]):
assert cur <= prev, f"non-monotonic: {normalized_first}"
# ══════════════════════════════════════════════════════════════
# getNumVgpr
# ══════════════════════════════════════════════════════════════
class TestGetNumVgpr:
def test_no_scale(self):
"""A+B total without scale."""
kernel = create_kernel(256, 256)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
cfg = make_cfg_bf16(256, 256)
sched = LogicalScheduler(cfg)
sched.build()
total = sched.getNumVgpr(tiA, tiB)
assert total > 0
import math
vgpr_per_A = int(math.ceil(tiA.mmaTileRegCount * cfg.lrA.k * cfg.lrA.mn))
vgpr_per_B = int(math.ceil(tiB.mmaTileRegCount * cfg.lrB.k * cfg.lrB.mn))
expected = sched.tile_peaks['A'] * vgpr_per_A + sched.tile_peaks['B'] * vgpr_per_B
assert total == expected
def test_with_scale(self):
"""Including SA+SB increases total."""
kernel = create_kernel(256, 256, fp4=True)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
total = sched.getNumVgpr(tiA, tiB, scaleTiA, scaleTiB)
total_no_scale = sched.getNumVgpr(tiA, tiB)
assert total > total_no_scale
def test_decreases_with_partitions(self):
"""More partitions → fewer VGPRs."""
kernel = create_kernel(256, 256, fp4=True)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel)
scaleTiB = makeTileInfo('MXSB', kernel)
def _build_and_count(partSizeM, partSizeN):
cfg = make_cfg_256x256_fp4(partSizeM=partSizeM, partSizeN=partSizeN)
sched = LogicalScheduler(cfg)
sched.build()
return sched.getNumVgpr(tiA, tiB, scaleTiA, scaleTiB)
vgpr_1x1 = _build_and_count(0, 0)
vgpr_1x2 = _build_and_count(0, 4)
vgpr_1x4 = _build_and_count(0, 2)
assert vgpr_1x1 >= vgpr_1x2
assert vgpr_1x2 >= vgpr_1x4
# ══════════════════════════════════════════════════════════════
# Integration tests
# ══════════════════════════════════════════════════════════════
class TestIntegration:
def test_populate_instructions_256x256_fp4(self):
"""Full pipeline: emit → populate_instructions → instructionSchedule."""
from Tensile.Components.Subtile.InstructionScheduler import instructionSchedule
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
assert len(sched._emitted_per_unroll) == sched.unroll_factor
assert len(sched._ngll_per_unroll) == sched.unroll_factor
assert len(sched._nll_per_unroll) == sched.unroll_factor
# All modules have instructions
for pi, partition_emitted in enumerate(sched._emitted_per_unroll[0]):
for k, emitted in enumerate(partition_emitted):
for em in emitted:
assert len(em.instructions) > 0, \
f"P{pi} k={k} [{em.moduleId}] {em.opType}: no instructions"
# MFMA/LR VGPR disjoint
for pi, slots in enumerate(sched._partitions):
for slot in slots:
if slot.mfma and slot.lrs:
for lr in slot.lrs:
if lr.vgpr_tile_map and lr.tensor in ('A', 'B'):
if lr.tensor == 'B' and sched.compact_b_overlay:
continue
mfma_map = slot.mfma.vgpr_tile_maps.get(lr.tensor, [])
if mfma_map:
assert set(mfma_map[0].values()).isdisjoint(
set(lr.vgpr_tile_map[0].values()))
# instructionSchedule succeeds
for pi, partition_emitted in enumerate(sched._emitted_per_unroll[0]):
for k, emitted in enumerate(partition_emitted):
scheduled = instructionSchedule(emitted)
assert len(list(scheduled.flatitems())) > 0
finally:
sched.deallocVgprTiles(writer)
def test_emitLoops_256x256_fp4(self):
"""emitMainAndExitLoops + emitTailLoop: label structure and per-unroll VGPR differences."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
uf = sched.unroll_factor
asm = str(sched.emitMainAndExitLoops(writer, kernel)) \
+ str(sched.emitTailLoop(writer, kernel))
assert "LoopBeginL:" in asm
assert "SkipToNGLL:" in asm
if uf > 1:
for ui in range(uf):
assert f"MAINLOOP_C{ui}" in asm
assert f"NGLL_C{ui}" in asm
assert f"NLL_C{ui}" in asm
assert "SkipToEnd:" in asm
assert "SkipToNLL:" in asm
def get_mfma_vgprs(emitted_3d):
vgprs = set()
for partition in emitted_3d:
for group in partition:
for em in group:
if em.opType == 'mfma':
for inst in em.instructions:
vgprs.add(str(inst))
return vgprs
vgprs_0 = get_mfma_vgprs(sched._emitted_per_unroll[0])
vgprs_1 = get_mfma_vgprs(sched._emitted_per_unroll[1])
assert vgprs_0 != vgprs_1, \
"Per-unroll copies should differ in MFMA instructions"
else:
assert "MAINLOOP" in asm
assert "NGLL" in asm
assert "NLL" in asm
finally:
sched.deallocVgprTiles(writer)
def test_initD_emitted_on_tail_only_and_preloop_paths(self):
"""initC is one canonical preloop op reached by every path.
Normal path (K>=DepthU): GR issue -> initC -> rest of preloop.
Tail-only path (K<DepthU): skip the prefetch GR to the same initC,
then jump to the tail loop. No separate/duplicated init block.
"""
kernel = create_kernel(256, 256, fp4=True)
assert not kernel["NoTailLoop"]
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitMainAndExitLoops(writer, kernel))
# initC emitted exactly once across all paths (no duplicate init).
assert asm.count("vgprTiles to zero") == 1, \
"initC must be zeroed exactly once"
assert "InitDForTailOnly" not in asm, "no separate tail-only init block"
# Unified control flow: skip prefetch GR to initC, then jump to tail.
skip_idx = asm.index("SkipPreloopGR:")
zero_idx = asm.index("vgprTiles to zero")
tail_idx = asm.index("K < DepthU: jump to tail loop")
assert skip_idx < zero_idx < tail_idx, \
"expected: SkipPreloopGR: -> initC -> jump to tail loop"
finally:
sched.deallocVgprTiles(writer)
def test_initD_uf1_tail_only_control_flow_invariants(self):
"""uf==1 (make_cfg_bf16): unified initC control-flow invariants."""
import re
kernel = create_kernel(256, 256, fp4=False)
assert not kernel["NoTailLoop"]
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel)
cfg = make_cfg_bf16()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB)
try:
sched.populate_instructions(
writer, kernel, tileInfoA=tiA, tileInfoB=tiB, dtileInfo=dTileInfo)
assert sched.unroll_factor == 1, "fixture must yield uf==1"
asm = str(sched.emitMainAndExitLoops(writer, kernel))
assert "InitDForTailOnly" not in asm, "no separate tail-only init block"
# initC emitted exactly once (single canonical location).
assert asm.count("vgprTiles to zero") == 1, "initC zeroed exactly once"
assert asm.count("label_SkipPreloopGR:") == 1, "SkipPreloopGR label present"
skip_gr = re.findall(r"s_cbranch_scc1\s+label_SkipPreloopGR", asm)
assert len(skip_gr) == 1, "K<DepthU guard must skip prefetch GR to initC"
assert asm.count("label_SkipToEnd:") == 1, "SkipToEnd label must be present"
tail_jump = re.findall(r"s_cbranch_scc1\s+label_SkipToEnd", asm)
assert len(tail_jump) == 1, "K<DepthU must jump to tail loop after initC"
finally:
sched.deallocVgprTiles(writer)
def test_emitMainAndExitLoops_pap_mx_inserts_preloop_skip_and_nll_hooks(self):
"""Subtile PAP is scheduler-owned and applies to MX PRELOOP/NLL paths."""
kernel = create_kernel(256, 256, fp4=True)
kernel.update({
"UseSubtileImpl": True,
"PrefetchAcrossPersistent": 1,
"PrefetchGlobalRead": 2,
"StreamK": 3,
})
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
def _pap_hook(_kernel, _tPA, _tPB, _preloop_gr, skipBarrier=False):
module = Module("Subtile PAP test hook")
module.addComment0("Subtile PAP test hook")
return module
writer.prefetchAcrossPersistentSubtile = MagicMock(side_effect=_pap_hook)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
module = sched.emitMainAndExitLoops(writer, kernel)
asm = str(module)
assert "Subtile PAP: first PRELOOP GR already issued?" in asm
assert "SubtilePAPPreloopFirstGRMerge" in asm
assert "Subtile PAP test hook" in asm
assert writer.prefetchAcrossPersistentSubtile.call_count == sched.unroll_factor
assert all(call.kwargs["skipBarrier"] for call in writer.prefetchAcrossPersistentSubtile.call_args_list)
pap_idx = asm.index("Subtile PAP test hook")
nll_idx = asm.rfind("NLL_C", 0, pap_idx)
assert nll_idx != -1
wait_gr_idx = asm.rfind("Wait GR", nll_idx, pap_idx)
barrier_idx = asm.rfind("Barrier", nll_idx, pap_idx)
assert nll_idx < wait_gr_idx < barrier_idx < pap_idx
mfma_idx = asm.find("MFMA C[", pap_idx)
assert mfma_idx != -1
assert pap_idx < mfma_idx
finally:
sched.deallocVgprTiles(writer)
def test_tailloop_k_mask_256x256_fp4(self):
"""Tail loop must emit per-lane K-mask (v_cmp_lt_i32 + v_cndmask_b32) after wait_lr."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitTailLoop(writer, kernel))
# mask init (preamble): kReg = (Serial % WS) / dividerFortidInK
assert "v_and_b32" in asm or "v_lshrrev_b32" in asm, \
"expected mask-init arithmetic in tail preamble"
# mask body: per-group compare + cndmask -> 0
assert "v_cmp_lt_i32" in asm, \
"tail loop missing per-lane K compare (v_cmp_lt_i32)"
assert "v_cndmask_b32" in asm, \
"tail loop missing v_cndmask_b32 to zero A vgprs"
assert ", 0," in asm, \
"v_cndmask_b32 should zero (src1=0) the masked lanes"
# ordering: compare must come after a wait_lr (lgkmcnt(0)) and
# before the first v_mfma.
first_cmp = asm.find("v_cmp_lt_i32")
first_mfma = asm.find("v_mfma")
last_wait_before_cmp = asm.rfind("lgkmcnt(0)", 0, first_cmp)
assert first_cmp != -1 and first_mfma != -1
assert last_wait_before_cmp != -1, "expected lgkmcnt(0) before mask"
assert first_cmp < first_mfma, "mask must precede first MFMA"
finally:
sched.deallocVgprTiles(writer)
def test_tailloop_k_partial_mask_bf16(self):
"""BF16 tail loop must use V_AND_B32 with a 3-state mask (incl. 0x0000FFFF)
so the K boundary can fall inside a vgpr without losing the valid element."""
kernel = create_kernel(256, 256, fp4=False)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=False)
cfg = make_cfg_bf16(256, 256)
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
tensorParametersA=MagicMock(),
tensorParametersB=MagicMock(),
)
asm = str(sched.emitTailLoop(writer, kernel)).lower()
# half-mask sgpr load
assert "0x0000ffff" in asm, \
"BF16 tail mask must reference the 0x0000FFFF half-mask constant"
# V_AND_B32 applies the per-vgpr mask to A/B tile vgprs
assert "v_and_b32" in asm, \
"BF16 tail must emit V_AND_B32 over A/B tile vgprs"
# Two-stage select uses V_CMP_LT_I32 (diff<2 and diff<1)
assert "v_cmp_lt_i32" in asm, \
"BF16 tail mask uses v_cmp_lt_i32 for the diff<2 / diff<1 select"
# Ordering: the v_and_b32 must follow the wait_lr and precede the v_mfma.
first_and = asm.find("v_and_b32 v") # skip mask-init 'v_and_b32 ...,63,...'
first_mfma = asm.find("v_mfma")
assert first_and != -1 and first_mfma != -1
# The first v_and_b32 we care about is the mask in the body; tolerate
# earlier mask-init operations by checking ordering against MFMA.
last_wait_before_mfma = asm.rfind("lgkmcnt(0)", 0, first_mfma)
assert last_wait_before_mfma != -1, "expected lgkmcnt(0) before MFMA"
assert last_wait_before_mfma < first_mfma
finally:
sched.deallocVgprTiles(writer)
def test_tailloop_data_kmask_emitted_for_single_du_mx_fp4(self):
"""Single-DU MX-scaled (hasScale) FP4 tail loop KEEPS develop's per-vgpr
data K-mask V_AND chain over A/B tile vgprs. The data-K-mask *skip* is
gated to multi-DU only (emit_mask_k: skip iff hasScale AND numUnroll>1),
so single-DU MXFP4 is develop-identical and emits "mask A[" / "mask B[".
Scale-mask reuse (AND on scale vgprs) is also emitted."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4() # hasScale = True, single-DU (numUnroll A/B = 1)
assert cfg.hasScale
sched = LogicalScheduler(cfg)
assert not sched._is_multi_du()
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitTailLoop(writer, kernel))
# Data K-mask V_AND chain emits comments like "mask A[i] (K=...)".
assert "mask A[" in asm or "mask B[" in asm, \
"single-DU MX FP4 tail loop must keep develop's data K-mask V_AND chain"
# Scale-mask reuse fires as well (cndmask result reused against
# scale vgprs even though it's a host-padding no-op).
assert "mask scale vgpr" in asm, \
"MX-scaled tail loop should still apply mask to scale vgprs"
finally:
sched.deallocVgprTiles(writer)
def test_tailloop_no_data_kmask_for_multi_du_mx_scale(self):
"""Multi-DU MX-scaled (hasScale, numUnroll>1, i.e. MXFP8) tail loop must
NOT emit the per-vgpr data K-mask V_AND chain over A/B tile vgprs — the
scale tensor's host zero-padding underflows OOB lanes via v_mfma_scale,
and on gfx950 the cmp→cndmask→AND chain corrupts the v_mfma_scale operand
bypass. The skip is gated to multi-DU only. Scale-mask reuse (AND on
scale vgprs) is still emitted."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
# grSA/SB k_gran=2 → numUnroll A/B = 2 → multi-DU MX (hasScale).
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=1)
assert cfg.hasScale
assert cfg.numUnroll.get('A', 1) > 1 or cfg.numUnroll.get('B', 1) > 1
sched = LogicalScheduler(cfg)
assert sched._is_multi_du()
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitTailLoop(writer, kernel))
assert "mask A[" not in asm, \
"multi-DU MX tail loop must not emit data K-mask over A vgprs"
assert "mask B[" not in asm, \
"multi-DU MX tail loop must not emit data K-mask over B vgprs"
assert "mask scale vgpr" in asm, \
"multi-DU MX tail loop should still apply mask to scale vgprs"
finally:
sched.deallocVgprTiles(writer)
def test_tailloop_data_kmask_emitted_for_no_scale_bf16(self):
"""Plain non-scale (BF16) tail loop must still emit the data K-mask
V_AND chain — the scale-mask gate above only applies to hasScale."""
kernel = create_kernel(256, 256, fp4=False)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=False)
cfg = make_cfg_bf16(256, 256)
assert not cfg.hasScale
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
tensorParametersA=MagicMock(),
tensorParametersB=MagicMock(),
)
asm = str(sched.emitTailLoop(writer, kernel))
assert "mask A[" in asm or "mask B[" in asm, \
"non-scale tail loop must keep data K-mask V_AND chain"
finally:
sched.deallocVgprTiles(writer)
def test_pgr2_no_tail_lw_align_for_single_du_fp4(self):
"""Single-DU PGR=2 FP4 must NOT emit the LW_base parity re-align.
The re-align is gated to multi-DU only: build_nll keeps the
MT-transition lr_inc for single-DU PGR=2 (develop behavior), so the
LW/LR parity invariant already holds and the s_xor_b32 re-align is
neither needed nor emitted. Single-DU MXFP4 is therefore develop-
identical here."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4(pgr=2) # single-DU (numUnroll A/B = 1)
sched = LogicalScheduler(cfg)
assert not sched._is_multi_du()
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitMainAndExitLoops(writer, kernel))
assert "PGR=2 tail entry: re-align LW_base" not in asm, \
"single-DU PGR=2 FP4 must NOT emit the LW_base parity re-align"
assert "PGR=2 tail align" not in asm, \
"single-DU PGR=2 FP4 must NOT emit any PGR=2 align XOR"
finally:
sched.deallocVgprTiles(writer)
def test_pgr2_tail_lw_align_emitted_for_multi_du(self):
"""Multi-DU PGR=2 (numUnroll>1) must emit s_xor_b32 to re-align
LocalWriteBaseAddr{A,B,MXSA,MXSB} parity with the LR vgpr at each NLL
exit. Multi-DU's build_nll drops the MT-transition lr_inc, leaving LW
one swap ahead of the LR vgpr (PRELOOP's unbalanced +1 LW swap), so the
tail entry must restore parity. This re-align is gated to multi-DU
(single-DU keeps lr_inc and needs no fix-up)."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
# grSA/SB k_gran=2 → numUnroll A/B = 2 → multi-DU MX, PGR=2.
cfg = make_cfg_256x256_fp4(grSA_k_gran=2, grSB_k_gran=2, pgr=2)
assert cfg.numUnroll.get('A', 1) > 1 or cfg.numUnroll.get('B', 1) > 1
sched = LogicalScheduler(cfg)
assert sched._is_multi_du()
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitMainAndExitLoops(writer, kernel))
assert "PGR=2 tail entry: re-align LW_base" in asm, \
"multi-DU PGR=2 must emit LW_base parity re-align before tail entry"
# Both A/B and (since hasScale) MX scale tensors must be re-aligned.
for tc in ('A', 'B', 'MXSA', 'MXSB'):
assert f"PGR=2 tail align: parity-swap LW_base for {tc}" in asm, \
f"missing PGR=2 LW_base re-align for {tc}"
# Re-align must happen BEFORE the SBranch to SkipToEnd at NLL_C{last}
align_pos = asm.find("PGR=2 tail align: parity-swap LW_base for A")
skip_branch_pos = asm.find("skip other exit paths", align_pos)
assert skip_branch_pos != -1 and align_pos < skip_branch_pos, \
"PGR=2 LW_base re-align must precede the NLL_C{last} → SkipToEnd branch"
finally:
sched.deallocVgprTiles(writer)
@pytest.mark.parametrize("pgr", [0, 1])
def test_no_pgr2_tail_lw_align_for_pgr_lt_2(self, pgr):
"""PGR=0/1: NLL keeps gr_inc/lr_inc (PGR=1) or has no body (PGR=0),
so no LW_base re-align fix-up is needed at tail entry."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4(pgr=pgr)
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitMainAndExitLoops(writer, kernel))
assert "PGR=2 tail entry: re-align LW_base" not in asm, \
f"PGR={pgr} must NOT emit the PGR=2 LW_base re-align"
assert "PGR=2 tail align" not in asm, \
f"PGR={pgr} must NOT emit any PGR=2 align XOR"
finally:
sched.deallocVgprTiles(writer)
def test_pgr2_no_tail_lw_align_when_no_tail_loop(self):
"""PGR=2 + NoTailLoop: helper short-circuits, no LW_base re-align emitted."""
kernel = create_kernel(256, 256, fp4=True)
kernel["NoTailLoop"] = True
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4(pgr=2)
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
asm = str(sched.emitMainAndExitLoops(writer, kernel))
assert "PGR=2 tail align" not in asm, \
"NoTailLoop must short-circuit the LW_base re-align helper"
finally:
sched.deallocVgprTiles(writer)
@pytest.mark.parametrize("pgr", [1, 2])
def test_nll_scale_vgprs_differ_across_unroll_copies(self, pgr):
"""NLL for each unroll copy must use distinct scale VGPRs matching LR loads."""
from rocisa.instruction import MXMFMAInstruction
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4(pgr=pgr)
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
uf = sched.unroll_factor
if uf < 2:
pytest.skip("unroll_factor < 2, no multi-copy NLL to test")
def get_scale_vgprs(emitted_3d):
sa_vgprs = set()
sb_vgprs = set()
for partition in emitted_3d:
for group in partition:
for em in group:
if em.opType == 'mfma':
for inst in em.instructions:
if isinstance(inst, MXMFMAInstruction):
sa_vgprs.add(str(inst.mxsa))
sb_vgprs.add(str(inst.mxsb))
return sa_vgprs, sb_vgprs
nll_scales = []
for ui in range(uf):
nll_scales.append(get_scale_vgprs(sched._nll_per_unroll[ui]))
for ui in range(uf):
for uj in range(ui + 1, uf):
sa_i, sb_i = nll_scales[ui]
sa_j, sb_j = nll_scales[uj]
assert sa_i != sa_j, \
f"PGR{pgr}: NLL_C{ui} and NLL_C{uj} use same scaleA VGPRs {sa_i}"
assert sb_i != sb_j, \
f"PGR{pgr}: NLL_C{ui} and NLL_C{uj} use same scaleB VGPRs {sb_i}"
finally:
sched.deallocVgprTiles(writer)
# Tool to visualize the scheduling steps on a real kernel configuration. Run with --interactive to step through each phase.
# Also calls the instruction scheduler to verify the emitted modules are valid input and to show the final instruction counts.
# Example usage:
# PYTHONPATH=. python Tensile/Tests/unit/test_SubtileBasedLogicalScheduler.py --mt0 320 --mt1 320 --du 64 --pgr 1 --wg 2x2 --partition-size 10x2
# PYTHONPATH=. python Tensile/Tests/unit/test_SubtileBasedLogicalScheduler.py --mt0 256 --mt1 256 --du 256 --dtype fp4 --pgr 2 --wg 2x2 --partition-size 8x4
if __name__ == "__main__":
import sys
import io
import argparse
parser = argparse.ArgumentParser(
description="Visualize SubtileBased LogicalScheduler steps for a given kernel config.",
)
parser.add_argument("--mt0", type=int, default=256, help="MacroTile0 (default: 256)")
parser.add_argument("--mt1", type=int, default=256, help="MacroTile1 (default: 256)")
parser.add_argument("--du", type=int, default=None,
help="DepthU (default: 64 for bf16, 512 for fp4)")
parser.add_argument("--dtype", choices=["bf16", "fp4"], default="bf16",
help="Data type (default: bf16)")
parser.add_argument("--partition-size", type=str, default="0x0",
help="partitionSize as MxN in MFMA tiles (0 = full dim, default: 0x0)")
parser.add_argument("--wg", type=str, default="2x2",
help="MIWaveGroup as MxN (default: 2x2)")
parser.add_argument("--pgr", type=int, choices=[0, 1, 2], default=1,
help="PrefetchGlobalRead level (default: 1)")
parser.add_argument("--arch", choices=["gfx950", "gfx1250"], default="gfx950",
help="Target arch (default: gfx950). gfx1250 enables wave32 + TDM.")
parser.add_argument("--interactive", "-i", action="store_true",
help="Step through each phase interactively")
args = parser.parse_args()
fp4 = args.dtype == "fp4"
if args.du is None:
args.du = 512 if fp4 else 64
wg_parts = args.wg.lower().split("x")
if len(wg_parts) != 2:
parser.error(f"--wg must be MxN (e.g. 2x2), got: {args.wg}")
waveGroup = (int(wg_parts[0]), int(wg_parts[1]))
ps_parts = args.partition_size.lower().split("x")
if len(ps_parts) != 2:
parser.error(f"--partition-size must be MxN (e.g. 10x2), got: {args.partition_size}")
partSizeM, partSizeN = int(ps_parts[0]), int(ps_parts[1])
kernel = create_kernel(args.mt0, args.mt1, fp4=fp4, depthU=args.du,
miWaveGroup=list(waveGroup), arch=args.arch)
tiA = makeTileInfo('A', kernel)
tiB = makeTileInfo('B', kernel)
scaleTiA = makeTileInfo('MXSA', kernel) if fp4 else None
scaleTiB = makeTileInfo('MXSB', kernel) if fp4 else None
# Mirror Kernel.py mainLoop GR granularity selection:
# - gfx950 (buffer load): mn=subtileShape[0] (doubled when loadRatioGR>1),
# k=subtileShape[1]
# - gfx1250 (TDM): one tensor_load_to_lds covers the full local
# MMA grid -> mn=localMMATileGrid[0], k=[1]
if args.arch == "gfx1250":
grA = ReadGranularity(mn=tiA.localMMATileGrid[0], k=tiA.localMMATileGrid[1])
grB = ReadGranularity(mn=tiB.localMMATileGrid[0], k=tiB.localMMATileGrid[1])
else:
grA = ReadGranularity(mn=1, k=2) if tiA.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
grB = ReadGranularity(mn=1, k=2) if tiB.loadRatioGR <= 1.0 else ReadGranularity(mn=2, k=2)
# gfx1250 enables TDM (tensor_load_to_lds); GR doesn't pressure SIMD issue
# slots, so bunch all GR atoms into partition 0 / subIterK 0. Mirrors
# Kernel.py's grPlacement selection.
grPlacement = (GRPlacementStrategy.BUNCHED if args.arch == "gfx1250"
else GRPlacementStrategy.SPREAD)
cfg_kwargs = dict(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=grA,
grB=grB,
partitionSizeM=partSizeM,
partitionSizeN=partSizeN,
pgr=args.pgr,
grPlacement=grPlacement,
)
if fp4:
cfg_kwargs.update(
lrSA=ReadGranularity(mn=2, k=2),
lrSB=ReadGranularity(mn=2, k=2),
grSA=ReadGranularity(mn=scaleTiA.localMMATileGrid[0],
k=scaleTiA.localMMATileGrid[1]),
grSB=ReadGranularity(mn=scaleTiB.localMMATileGrid[0],
k=scaleTiB.localMMATileGrid[1]),
)
cfg = SchedulerConfig(**cfg_kwargs)
print(f"Config: arch={args.arch}, MT={args.mt0}x{args.mt1}, DU={args.du}, dtype={args.dtype}, "
f"WG={waveGroup[0]}x{waveGroup[1]}, "
f"partitionSize={partSizeM}x{partSizeN}, pgr={args.pgr}")
print(f" numMFMATilesM={cfg.numMFMATilesM}, "
f"numMFMATilesN={cfg.numMFMATilesN}, "
f"numSubIterK={cfg.numSubIterK}, "
f"hasScale={cfg.hasScale}, plr={cfg.plr}")
print(f" loadRatioGR(A,B)=({tiA.loadRatioGR:.3f}, {tiB.loadRatioGR:.3f}) "
f"-> grA=({grA.mn},{grA.k}) grB=({grB.mn},{grB.k})")
print()
sched = LogicalScheduler(cfg)
steps = [
("Place LRs", lambda: (sched.place_LRs(), sched.print_lr())),
("Assign VGPR tiles", lambda: (sched.assign_vgpr_tiles(), sched.print_vgpr())),
("Place GRs", lambda: (sched.place_GRs(), sched.print_gr())),
("Annotate deps", lambda: (sched.annotate_deps(), sched.print_deps())),
("Remove unnecessary GR deps", lambda: (sched.remove_unnecessary_gr_deps(), sched.print_deps())),
("Remove unnecessary LR deps", lambda: (sched.remove_unnecessary_lr_deps(), sched.print_deps())),
("Remove cross deps", lambda: (sched.remove_cross_deps(), sched.print_remove_deps())),
("Insert gr/lr inc", lambda: (sched.insert_gr_lr_inc(), sched.print_group_lr_gr())),
("Group LR/GR", lambda: (sched.group_lr_gr(), sched.print_group_lr_gr())),
("Remove unnecessary wait_lr_sync", lambda: (sched.remove_unnecessary_wait_lr_sync(), sched.print_group_lr_gr())),
("Emit", lambda: (sched.emit(), sched.print_emit())),
("Emit (dependency order)", lambda: (None, sched.print_emit_dep_order())),
]
for i, (title, run) in enumerate(steps):
_, output = run()
print(f"{'=' * 60}")
print(f" {title}")
print(f"{'=' * 60}")
print(output)
if args.interactive and i < len(steps) - 1:
input("Press Enter for next step...")
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=fp4)
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
print(f"{'=' * 60}")
print(f" Build tailloop (PGR0 template)")
print(f"{'=' * 60}")
print(sched.print_emit(sched._tailloop_emitted).replace("MAINLOOP:", "TAILLOOP:"))
if args.interactive:
input("Press Enter for next step...")
def _print_emitLoop(label, emitted_3d, schedule=True):
module = sched._emitLoop(writer, kernel, label, emitted_3d, schedule=schedule)
buf = io.StringIO()
for inst in module.flatitems():
buf.write(f" {str(inst).rstrip()}\n")
return buf.getvalue()
if args.pgr >= 1:
loop_sections = [
("PRELOOP", sched._preloop_emitted, False),
("MAINLOOP", sched._emitted_per_unroll[0]),
("NGLL", sched._ngll_per_unroll[0]),
("NLL", sched._nll_per_unroll[0]),
("TAILLOOP", sched._tailloop_emitted, False),
]
else:
loop_sections = [
("MAINLOOP", sched._emitted_per_unroll[0]),
("TAILLOOP", sched._tailloop_emitted, False),
]
for section in loop_sections:
label, emitted_3d = section[0], section[1]
schedule = section[2] if len(section) > 2 else True
print(f"{'=' * 60}")
print(f" {label} (emitLoop)")
print(f"{'=' * 60}")
print(_print_emitLoop(label, emitted_3d, schedule=schedule))
if args.interactive:
input("Press Enter for next step...")
sched.deallocVgprTiles(writer)
sys.exit(0)
sched.build_preloop()
preloop_output = sched.print_emit(sched._preloop_emitted)
print(f"{'=' * 60}")
print(f" Preloop")
print(f"{'=' * 60}")
print(preloop_output.replace("MAINLOOP:", "PRELOOP:"))
if interactive:
input("Press Enter for next step...")
sched.build_ngll()
ngll_output = sched.print_emit(sched._ngll_emitted)
print(f"{'=' * 60}")
print(f" NGLL")
print(f"{'=' * 60}")
print(ngll_output.replace("MAINLOOP:", "NGLL:"))
if interactive:
input("Press Enter for next step...")
sched.build_nll()
nll_output = sched.print_emit(sched._nll_emitted)
print(f"{'=' * 60}")
print(f" NLL")
print(f"{'=' * 60}")
print(nll_output.replace("MAINLOOP:", "NLL:"))
if interactive:
input("Press Enter for next step...")
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=not use_bf16)
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
def _print_emitLoop(label, emitted_3d, schedule=True):
module = sched._emitLoop(writer, kernel, label, emitted_3d, schedule=schedule)
buf = io.StringIO()
for inst in module.flatitems():
buf.write(f" {str(inst).rstrip()}\n")
return buf.getvalue()
for section in [
("PRELOOP", sched._preloop_emitted, False),
("MAINLOOP", sched._emitted_per_unroll[0]),
("NGLL", sched._ngll_per_unroll[0]),
("NLL", sched._nll_per_unroll[0]),
]:
label, emitted_3d = section[0], section[1]
schedule = section[2] if len(section) > 2 else True
print(f"{'=' * 60}")
print(f" {label} (emitLoop)")
print(f"{'=' * 60}")
print(_print_emitLoop(label, emitted_3d, schedule=schedule))
if interactive:
input("Press Enter for next step...")
sched.deallocVgprTiles(writer)
# ══════════════════════════════════════════════════════════════
# MT iteration integer representation
# ══════════════════════════════════════════════════════════════
class TestFmtMt:
def test_fmt_mt_values(self):
assert fmt_mt(0) == "n"
assert fmt_mt(1) == "n+1"
assert fmt_mt(2) == "n+2"
assert fmt_mt(3) == "n+3"
class TestMtIterationTypes:
def test_mt_iteration_is_int_bf16(self):
cfg = make_cfg_bf16()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR)
for slots in schedule:
for slot in slots:
for lr in slot.lrs:
assert isinstance(lr.mtIteration, int), \
f"LR {lr.tensor} mtIteration is {type(lr.mtIteration)}"
for gr in slot.grs:
assert isinstance(gr.mtIteration, int), \
f"GR {gr.tensor} mtIteration is {type(gr.mtIteration)}"
def test_mt_iteration_is_int_fp4(self):
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
schedule = sched.build(stop_after=Pass.GR)
for slots in schedule:
for slot in slots:
for lr in slot.lrs:
assert isinstance(lr.mtIteration, int), \
f"LR {lr.tensor} mtIteration is {type(lr.mtIteration)}"
for gr in slot.grs:
assert isinstance(gr.mtIteration, int), \
f"GR {gr.tensor} mtIteration is {type(gr.mtIteration)}"
class TestPreloopMtIntegers:
def test_preloop_uses_int_mt(self):
cfg = make_cfg_bf16()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
preloop = sched.build_preloop()
for partition_emitted in preloop:
for emitted in partition_emitted:
for em in emitted:
src = em.source
if isinstance(src, GRPlacement):
assert isinstance(src.mtIteration, int), \
f"Preloop GR {src.tensor} mtIteration is {type(src.mtIteration)}"
elif isinstance(src, LRPlacement):
assert isinstance(src.mtIteration, int), \
f"Preloop LR {src.tensor} mtIteration is {type(src.mtIteration)}"
class TestBuildNll:
def test_nll_removes_expected_ops(self):
cfg = make_cfg_bf16()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
nll = sched.build_nll()
numK = cfg.numSubIterK
for partition_emitted in nll:
for k, emitted in enumerate(partition_emitted):
for em in emitted:
src = em.source
assert em.opType != 'gr', "NLL should have no GR ops"
# PGR=2 (default): gr_inc dropped; lr_inc kept to swap LR
# base for the next MT iteration on tail entry.
assert em.opType != 'gr_inc', "NLL should have no gr_inc ops"
if em.opType == 'lr' and isinstance(src, LRPlacement):
assert src.mtIteration == 0, \
f"NLL should only have LR(n=0), got mt={src.mtIteration}"
if em.opType == 'wait_gr':
from Tensile.Components.Subtile.LogicalScheduler import WaitGROp
if isinstance(src, WaitGROp) and src.wait_gr_counts:
cnts = src.wait_gr_counts
slot_has_lr = any(
e.opType == 'lr' for e in emitted)
preserve = slot_has_lr and k == numK - 1
if not preserve:
assert cnts.A == 0 and cnts.B == 0 \
and cnts.SA == 0 and cnts.SB == 0, \
f"NLL WaitGR should have zeroed counts, got {cnts}"
# ══════════════════════════════════════════════════════════════
# PGR=0 tests
# ══════════════════════════════════════════════════════════════
class TestPGR0Config:
def test_pgr0_requires_single_partition(self):
with pytest.raises(AssertionError, match="pgr=0 requires numPartitions=1"):
SchedulerConfig(
numMFMATilesM=8, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2), grB=ReadGranularity(mn=1, k=2),
pgr=0, partitionSizeN=4,
)
def test_gr_gran_k_must_divide_max(self):
"""grGran[t].k must divide max(grGran.k) — e.g. k=3 vs k=2 is rejected."""
with pytest.raises(AssertionError, match=r"grGran\[A\]\.k=3 must divide max\(grGran\.k\)=4"):
SchedulerConfig(
numMFMATilesM=8, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=3), grB=ReadGranularity(mn=1, k=4),
)
def test_gr_gran_k_divides_max_accepted(self):
"""grGran.k values that divide max(grGran.k) are accepted."""
cfg = SchedulerConfig(
numMFMATilesM=8, numMFMATilesN=8, numSubIterK=2,
lrA=ReadGranularity(mn=1, k=1), lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2), grB=ReadGranularity(mn=1, k=4),
)
assert cfg.numUnroll == {'A': 2, 'B': 1}
class TestPlaceLRs_PLR0:
def test_bf16_plr0_structure(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
assert len(partitions) == 1
slots = partitions[0]
numK = cfg.numSubIterK
for k in range(numK):
slot = slots[k]
assert slot.mfma is not None
assert slot.mfma.subIterK == k
for lr in slot.lrs:
assert lr.mtIteration == 0
assert lr.tiles.subIterK_start == k
assert lr.tiles.subIterK_end == k + 1
def test_bf16_plr0_tensors(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
partitions = sched.place_LRs()
slots = partitions[0]
for k in range(cfg.numSubIterK):
_assert_slot_lrs(slots[k], ['A', 'B'])
def test_bf16_plr0_print_lr(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.place_LRs()
output = sched.print_lr()
assert "MT n," in output
assert "MT n+1" not in output
class TestPlaceGRs_PGR0:
def test_bf16_pgr0_all_in_subIterK0(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.place_LRs()
schedule = sched.build(stop_after=Pass.GR)
slots = schedule[0]
for gr in slots[0].grs:
assert gr.subIterK_slot == 0
assert gr.mtIteration == 0
for k in range(1, cfg.numSubIterK):
assert len(slots[k].grs) == 0
def test_bf16_pgr0_covers_full_k(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.place_LRs()
schedule = sched.build(stop_after=Pass.GR)
slots = schedule[0]
gr_a = [gr for gr in slots[0].grs if gr.tensor == 'A']
all_k = set()
for gr in gr_a:
for k in range(gr.tiles.subIterK_start, gr.tiles.subIterK_end):
all_k.add(k)
assert all_k == set(range(cfg.numSubIterK))
def test_bf16_pgr0_print_gr(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.GR)
output = sched.print_gr()
assert "GR A (MT n," in output
assert "GR B (MT n," in output
assert "MT n+1" not in output
assert "MT n+2" not in output
class TestAnnotateDeps_PGR0:
def _build(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.place_LRs()
sched.build(stop_after=Pass.DEPS)
return cfg, sched
def test_no_crash(self):
self._build()
def test_gr_deps_on_lr_mt_minus2(self):
_, sched = self._build()
slots = sched._partitions[0]
for slot in slots:
for gr in slot.grs:
assert gr.deps, f"GR {gr.tensor} should have collision deps"
for dep in gr.deps:
assert isinstance(dep.ref, LRPlacement)
assert dep.mt_offset == -2
def test_mfma_deps_on_lr_mt0(self):
_, sched = self._build()
slots = sched._partitions[0]
for slot in slots:
if slot.mfma:
for dep in slot.mfma.deps:
assert isinstance(dep.ref, LRPlacement)
assert dep.ref.mtIteration == 0
def test_lr_deps_on_gr_mt0(self):
_, sched = self._build()
slots = sched._partitions[0]
for slot in slots:
for lr in slot.lrs:
for dep in lr.deps:
assert isinstance(dep.ref, GRPlacement)
assert dep.ref.mtIteration == 0
class TestEmit_PGR0:
def test_emit_succeeds(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
assert sched._emitted is not None
assert len(sched._emitted) == 1
assert len(sched._emitted[0]) == cfg.numSubIterK
def test_gr_lr_inc_as_postOps(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
# Each tensor's INC should be on its own placement (not merged)
slot0 = sched._partitions[-1][0] # subIterK=0
slot1 = sched._partitions[-1][-1] # subIterK=1
# lr_inc(A) on LR A, lr_inc(B) on LR B
lr_a = [lr for lr in slot1.lrs if lr.tensor == 'A'][0]
lr_b = [lr for lr in slot1.lrs if lr.tensor == 'B'][0]
assert any(op.kind == 'lr_inc' and op.tensor == 'A' for op in lr_a.postOps)
assert any(op.kind == 'lr_inc' and op.tensor == 'B' for op in lr_b.postOps)
# gr_inc(A) on GR A, gr_inc(B) on GR B
gr_a = [gr for gr in slot0.grs if gr.tensor == 'A'][0]
gr_b = [gr for gr in slot0.grs if gr.tensor == 'B'][0]
assert any(op.kind == 'gr_inc' and op.tensor == 'A' for op in gr_a.postOps)
assert any(op.kind == 'gr_inc' and op.tensor == 'B' for op in gr_b.postOps)
class TestBuildLoopVariants_PGR0:
def test_preloop_has_initD_only(self):
# PGR=0 has no GR/LR preloop, but it must still zero the D
# accumulators before the mainloop's first MFMA. The preloop is
# therefore a single initD op (not empty).
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
preloop = sched.build_preloop()
ops = preloop[0][0]
labels = [getattr(em.source, 'label', None) for em in ops]
assert labels == ['initC_overlap'], \
f"PGR=0 preloop should be exactly one initD op, got {labels}"
def test_ngll_empty(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
ngll = sched.build_ngll()
assert ngll == [[[]]]
def test_nll_empty(self):
cfg = make_cfg_bf16_pgr0()
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
nll = sched.build_nll()
assert nll == [[[]]]
class TestPreloopInitD:
"""initD (accumulator zeroing) placement across pgr values."""
@pytest.mark.parametrize("make_cfg,pgr", [
(make_cfg_bf16_pgr0, 0),
(make_cfg_bf16_pgr1, 1),
(make_cfg_bf16, 2),
], ids=["pgr0", "pgr1", "pgr2"])
def test_preloop_zeros_d_exactly_once(self, make_cfg, pgr):
cfg = make_cfg()
assert cfg.pgr == pgr
sched = LogicalScheduler(cfg)
sched.build(stop_after=Pass.EMIT)
preloop = sched.build_preloop()
initd = [em for em in preloop[0][0]
if getattr(em.source, 'label', None) == 'initC_overlap']
assert len(initd) == 1, \
f"pgr={pgr} preloop should zero D exactly once, got {len(initd)}"
class TestBuildTailloopPGR0:
def test_assembly_fp4_256x256(self):
"""Full pipeline: display generated assembly for FP4 tailloop."""
kernel = create_kernel(256, 256, fp4=True)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=True)
cfg = make_cfg_256x256_fp4()
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
scaleTileInfoA=scaleTiA, scaleTileInfoB=scaleTiB,
)
print("FP4 256x256 tailloop assembly:")
module = sched._emitLoop(writer, kernel, "TAILLOOP",
sched._tailloop_emitted,
schedule=False)
for inst in module.flatitems():
print(f" {str(inst).rstrip()}")
finally:
sched.deallocVgprTiles(writer)
def test_assembly_bf16_320x320_1x5(self):
"""Full pipeline: BF16 320x320 tailloop with 1x5 partitions."""
kernel = create_kernel(320, 320, fp4=False)
writer, tiA, tiB, scaleTiA, scaleTiB, dTileInfo = make_writer_and_tileinfos(kernel, fp4=False)
cfg = SchedulerConfig(
numMFMATilesM=tiA.localMMATileGrid[0],
numMFMATilesN=tiB.localMMATileGrid[0],
numSubIterK=tiA.localMMATileGrid[1],
lrA=ReadGranularity(mn=1, k=1),
lrB=ReadGranularity(mn=1, k=1),
grA=ReadGranularity(mn=1, k=2),
grB=ReadGranularity(mn=1, k=2),
partitionSizeN=2,
pgr=2,
)
sched = LogicalScheduler(cfg)
sched.build()
sched.allocVgprTiles(writer, tiA, tiB)
try:
sched.populate_instructions(
writer, kernel,
tileInfoA=tiA, tileInfoB=tiB,
dtileInfo=dTileInfo,
)
print("BF16 320x320 1x5 tailloop assembly:")
module = sched._emitLoop(writer, kernel, "TAILLOOP",
sched._tailloop_emitted,
schedule=False)
for inst in module.flatitems():
print(f" {str(inst).rstrip()}")
finally:
sched.deallocVgprTiles(writer)
# ══════════════════════════════════════════════════════════════
# InitC D-register self-reuse zeroing
# ══════════════════════════════════════════════════════════════
#
# The initC zeroing uses the last MFMA-sized chunk of the D register range
# itself as the zero source: scalar-zero that chunk, then use its first 2
# registers as the A/B operand to MFMA-zero all preceding chunks. The A/B
# source is addressed in the accumulator's own register file (VGPRs for
# WMMA/VGPR accumulation, AGPRs for AGPR accumulation), so no external scratch
# allocation is needed and 1 MFMA is saved vs zeroing with a separate pair.
class _InitCPool:
"""Minimal vgpr pool mock for initC tests (no checkouts needed)."""
def __init__(self, size=100):
self._size = size
def size(self):
return self._size
def _initc_writer(has_wmma=False, has_wmma_arb_stall=False):
from types import SimpleNamespace
w = SimpleNamespace()
w.states = SimpleNamespace(
asmCaps={"HasWMMA_AccImmZero": has_wmma},
archCaps={"HasWmmaArbStallBit": has_wmma_arb_stall},
regCaps={"MaxVgpr": 256},
)
w.vgprPool = _InitCPool()
w.agprPool = object()
return w
class _InitCRegList:
def __init__(self, base, n, pool=None):
self.indices = [base + k for k in range(n)]
self.pool = pool
class _InitCTile:
def __init__(self, base, n, pool=None):
self.regList = _InitCRegList(base, n, pool)
class _InitCTileInfo:
tc = "D"
def __init__(self, tiles):
self.vgprTiles = tiles
def _zero_range(writer, firstReg, totalRegs, isAgpr):
from Tensile.Components.Subtile.Kernel import _zeroRegRange
module = Module()
_zeroRegRange(module, writer, _InitCTileInfo([]), firstReg, totalRegs, isAgpr)
return str(module)
class TestInitCZeroRegRange:
"""_zeroRegRange: D-register self-reuse zeroing."""
def test_multi_chunk_uses_last_as_source_saves_one_mfma(self):
"""96 regs = 6 MFMA chunks; last chunk scalar-zeroed, 5 MFMAs issued."""
w = _initc_writer()
src = _zero_range(w, 0, 96, isAgpr=True)
assert src.count("initD: [") == 5, f"expected 5 matrix initD ops (last chunk scalar):\n{src[:600]}"
assert "zero-src" in src, "last chunk should be scalar-zeroed as source"
def test_single_chunk_all_scalar(self):
"""16 regs = 1 chunk; no MFMA advantage, all scalar."""
w = _initc_writer()
src = _zero_range(w, 0, 16, isAgpr=True)
assert "initD: [" not in src, "single chunk should be all scalar"
assert src.count("zero-src") == 16, "expected 16 scalar writes"
def test_remainder_regs_scalar(self):
"""96 + 4 remainder = 100 total; 5 MFMAs + 16 src-zero + 4 remainder scalar."""
w = _initc_writer()
src = _zero_range(w, 0, 100, isAgpr=True)
assert src.count("initD: [") == 5, "expected 5 MFMAs for 6 chunks minus last"
assert src.count("zero-src") == 16, "expected 16 scalar writes for last chunk (zero source)"
# 4 remainder regs after all full chunks
remainder_count = src.count("// initD\n")
assert remainder_count == 4, f"expected 4 scalar remainder writes, got {remainder_count}"
def test_no_pool_checkout_needed(self):
"""D-reuse means no pool interaction at all, even at max occupancy."""
w = _initc_writer()
_zero_range(w, 0, 96, isAgpr=True)
assert w.vgprPool.size() == 100, "pool size must not change"
def test_source_uses_last_chunk_base_agpr(self):
"""AGPR accumulation: MFMA A/B source is the zeroed last chunk in the
AGPR file (acc[80:81]), NOT the same-index VGPR (v[80:81]).
Regression guard for the -nan bug: naming vgpr(lastChunkBase) reads an
unzeroed VGPR of a coincidentally-matching index. gfx90a+/CDNA MFMA can
read A/B from AGPRs, so the source must live in the accumulator's file.
"""
w = _initc_writer()
# 96 regs starting at reg0: last chunk base = 0 + 5*16 = 80
src = _zero_range(w, 0, 96, isAgpr=True)
assert "acc[80:81]" in src, f"expected AGPR source acc[80:81]:\n{src[:600]}"
assert "v[80:81]" not in src, f"AGPR source must not be the same-index VGPR:\n{src[:600]}"
def test_source_uses_last_chunk_base_vgpr(self):
"""VGPR accumulation: MFMA A/B source is the zeroed last chunk in VGPRs."""
w = _initc_writer()
src = _zero_range(w, 0, 96, isAgpr=False)
assert "v[80:81]" in src, f"expected VGPR source v[80:81] (last chunk base):\n{src[:600]}"
class TestInitCInitVgprTilesToZero:
"""initVgprTilesToZero: no scratch needed, uses D self-reuse."""
def test_single_range_uses_mfma(self):
from Tensile.Components.Subtile.Kernel import initVgprTilesToZero
w = _initc_writer()
dtile = _InitCTileInfo([_InitCTile(0, 96, pool=w.agprPool)])
src = str(initVgprTilesToZero(w, {}, dtile))
assert src.count("initD: [") == 5, f"expected 5 matrix ops:\n{src[:600]}"
def test_split_pool_groups(self):
"""A range split across agpr + vgpr pools emits MFMAs for both groups."""
from Tensile.Components.Subtile.Kernel import initVgprTilesToZero
w = _initc_writer()
vgpr_pool = object()
dtile = _InitCTileInfo([
_InitCTile(0, 48, pool=w.agprPool),
_InitCTile(48, 48, pool=vgpr_pool),
])
src = str(initVgprTilesToZero(w, {}, dtile))
# Each 48-reg group = 3 chunks -> 2 MFMAs + 1 scalar chunk = 4 MFMAs total
assert src.count("initD: [") == 4, f"expected 4 matrix ops across 2 groups:\n{src[:600]}"
def test_empty_tiles_is_noop(self):
from Tensile.Components.Subtile.Kernel import initVgprTilesToZero
w = _initc_writer()
src = str(initVgprTilesToZero(w, {}, _InitCTileInfo([])))
assert "initD: [" not in src
class TestInitCMakeOpWiring:
"""_make_initC_op end-to-end wiring (scheduler -> zeroing)."""
class _Emitter:
def __init__(self, writer, kernel, dtileInfo):
self.writer = writer
self.kernel = kernel
self.dtileInfo = dtileInfo
def test_emits_mfma_zeroing_with_d_reuse(self):
s = LogicalScheduler.__new__(LogicalScheduler)
op = s._make_initC_op()
assert op.label == "initC_overlap"
w = _initc_writer()
dtile = _InitCTileInfo([_InitCTile(0, 96, pool=w.agprPool)])
src = str(op.build(self._Emitter(w, {"DirectToLds": True}, dtile)))
assert src.count("initD: [") == 5, f"expected 5 MFMAs (D self-reuse):\n{src[:600]}"
assert w.vgprPool.size() == 100, "no pool growth expected"
def test_wmma_reuse_flags_on_gfx1250(self):
"""gfx1250 initC WMMA instructions get reuseA/reuseB on all but the last."""
from rocisa.instruction import MFMAInstruction
s = LogicalScheduler.__new__(LogicalScheduler)
op = s._make_initC_op()
w = _initc_writer(has_wmma=True, has_wmma_arb_stall=True)
dtile = _InitCTileInfo([_InitCTile(0, 32, pool=w.vgprPool)])
module = op.build(self._Emitter(w, {"DirectToLds": True}, dtile))
mfmas = [inst for inst in module.flatitems() if isinstance(inst, MFMAInstruction)]
# 32 regs / 8 per inst = 4 chunks; last chunk scalar-zeroed, first 3 are MFMAs
assert len(mfmas) == 3, f"expected 3 WMMAs, got {len(mfmas)}"
# All but the last MFMA should have reuseA=True, reuseB=True
for mfma in mfmas[:-1]:
assert mfma.reuseA, f"expected reuseA on {mfma}"
assert mfma.reuseB, f"expected reuseB on {mfma}"
# Last MFMA should NOT have reuse (no successor)
assert not mfmas[-1].reuseA
assert not mfmas[-1].reuseB