Skip to content

Commit a8db72f

Browse files
committed
refactor: the epilogue is the solver's answer, not a second
opinion accumulator_gathers walked the lanes, asked layouts.position where each element sat, grouped by region and checked that one XOR carried every lane of it. bitlayout.moves is that question asked of two layouts, and patch 15 showed the two agree on 1888 regions -- so keeping both was keeping a second implementation of one thing, which is how they drift. It reads the solver now. The plan is identical for every entry and every column: 756 answers, 692 of them plans, compared field by field against the implementation it replaces. moves takes a base on each side. Which one carries the group offset depends on which of the two the wave is divided over -- the epilogue gathers into the nest, so it is the destination, while the transpose check has it on the source -- and stating both is what lets one function serve both directions. Move.lanes are the destination lanes, because that is the region a caller has to mask when it merges. AI-generated. Model: Opus 5
1 parent 3a047c4 commit a8db72f

4 files changed

Lines changed: 72 additions & 21 deletions

File tree

‎src/tensorforge/backend/instructions/compute/bitlayout.py‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ class Move:
211211

212212

213213
def moves(have: BitLayout, want: BitLayout, indices,
214-
base: Position = Position()) -> Optional[Tuple[Move, ...]]:
214+
base_have: Position = Position(),
215+
base_want: Position = Position()) -> Optional[Tuple[Move, ...]]:
215216
"""How to get from one distribution to the other, or `None`.
216217
217218
The `swap` family does not permute bits: `swap<B>` reads the lane `B` away,
@@ -221,10 +222,14 @@ def moves(have: BitLayout, want: BitLayout, indices,
221222
lane and a slot, which is a transpose and `relayout.find_relayout`'s
222223
business.
223224
224-
`base` is a constant added to the source, which is what a caller holding
225-
one of several groups of a wave contributes: the group index is an offset
226-
on the lane and not a bit of any index the two layouts share, so it cannot
227-
be an axis of either.
225+
The two bases are constants added to each side, which is what a caller
226+
holding one of several groups of a wave contributes: the group index is an
227+
offset on the lane and not a bit of any index the two layouts share, so it
228+
cannot be an axis of either. Which side carries it depends on which of
229+
them the wave is divided over, so both are stated rather than one assumed.
230+
231+
`Move.lanes` are the *destination* lanes -- the region of `want`'s register
232+
this writes -- because that is what a caller has to mask when it merges.
228233
229234
`None` where some pair of slots needs more than one toggle. Not a
230235
limitation to route around: that is the case where no sequence of reads
@@ -239,7 +244,8 @@ def moves(have: BitLayout, want: BitLayout, indices,
239244
return None
240245
regions = {}
241246
for index in indices:
242-
here, there = base + have.locate(*index), want.locate(*index)
247+
here = base_have + have.locate(*index)
248+
there = base_want + want.locate(*index)
243249
if here.element or there.element:
244250
# A vector element is neither a lane nor a slot, so no lane toggle
245251
# reaches it; a packed operand needs the element bits accounted

‎src/tensorforge/backend/instructions/compute/primitives/amd/reorder.py‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787

8888
from . import layouts
8989

90+
from ... import bitlayout
91+
9092
#: `quad_perm:[0,1,2,3]`. The DPP control that moves nothing, so that
9193
#: `dppUpdate` is a masked merge and not also a shuffle.
9294
IDENTITY_DPP = 0xE4
@@ -400,21 +402,32 @@ def accumulator_gathers(op, column: int) -> Optional[Tuple[Gather, ...]]:
400402
span = op.n * op.blocks
401403
if op.wave % span:
402404
return None
405+
fragment = layouts.fragment_layout(op, 'D')
406+
if fragment is None:
407+
return None
403408

404-
regions = {}
405-
for lane in range(op.wave):
406-
group, local = divmod(lane, span)
407-
block, index = divmod(local, op.n)
408-
slot, source = layouts.position(op, 'D', column, index, block)
409-
regions.setdefault((group, slot), []).append((lane, source))
409+
# The nest's own register: lane `l` holds leading-dimension element `l`,
410+
# and nothing varies with the column -- each one is a separate store.
411+
nest = bitlayout.BitLayout((
412+
tuple(bitlayout.Bit(bitlayout.Place.LANE, op.n << bit)
413+
for bit in range((op.blocks - 1).bit_length())),
414+
(),
415+
tuple(bitlayout.Bit(bitlayout.Place.LANE, 1 << bit)
416+
for bit in range((op.n - 1).bit_length())),
417+
))
418+
indices = [(block, column, index)
419+
for block in range(op.blocks) for index in range(op.n)]
410420

411421
gathers = []
412-
for (group, slot), pairs in sorted(regions.items()):
413-
masks = {lane ^ source for lane, source in pairs}
414-
if len(masks) != 1:
422+
for group in range(op.wave // span):
423+
found = bitlayout.moves(
424+
fragment, nest, indices,
425+
base_want=bitlayout.Position(lane=group * span))
426+
if found is None:
415427
return None
416-
gathers.append(Gather(group, slot, _swaps_for(masks.pop()),
417-
Select.of((lane for lane, _ in pairs), op.wave)))
428+
gathers += [Gather(group, move.source, _swaps_for(move.xor),
429+
Select.of(move.lanes, op.wave))
430+
for move in found]
418431
return tuple(gathers)
419432

420433

‎tests/test_bitlayout.py‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def test_the_solver_finds_the_swaps_the_plan_states(name):
278278
for b in range(op.blocks) for i in range(op.n)]
279279
for group in range(op.wave // span):
280280
found = bitlayout.moves(have, fragment, indices,
281-
base=Position(lane=group * span))
281+
base_have=Position(lane=group * span))
282282
assert found is not None, (name, column, group)
283283
for move in found:
284284
assert stated[(group, move.target)] == reorder._swaps_for(
@@ -415,3 +415,31 @@ def test_a_packed_operand_is_not_reached_by_a_transpose():
415415
))
416416
assert relayout.transposes_between(
417417
packed, relayout.transposed(4, 64), 4) is None
418+
419+
420+
def test_the_gathers_are_the_solver_s_answer():
421+
"""`accumulator_gathers` reads the solver now instead of walking lanes.
422+
423+
The plan it returns is the same for every entry and every column -- 756
424+
answers, 692 of them plans -- which is what makes reading it from two
425+
layouts a refactor rather than a second opinion.
426+
"""
427+
from tensorforge.backend.instructions.compute.primitives.amd import reorder
428+
planned = sum(1 for op in MATRIX_OPS for column in range(op.m)
429+
if reorder.accumulator_gathers(op, column) is not None)
430+
assert planned == 692
431+
432+
433+
def test_a_base_on_each_side():
434+
"""Which side carries the group offset depends on which of the two the
435+
wave is divided over: the epilogue gathers *into* the nest, so the offset
436+
is on the destination, and the transpose check has it on the source."""
437+
layout = BitLayout(((Bit(Place.LANE, 1),),))
438+
indices = [(0,), (1,)]
439+
forward = bitlayout.moves(layout, layout, indices,
440+
base_have=Position(lane=8))
441+
backward = bitlayout.moves(layout, layout, indices,
442+
base_want=Position(lane=8))
443+
assert forward is not None and backward is not None
444+
assert {m.xor for m in forward} == {8}
445+
assert {m.xor for m in backward} == {8}

‎tools/mutation_check.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,10 +421,14 @@ def make():
421421
sub(Path('src/tensorforge/backend/instructions/compute/bitlayout.py'),
422422
' regions.setdefault((here.slot, there.slot), []).append(',
423423
' regions.setdefault((0, there.slot), []).append(')),
424-
('the group offset dropped from the source',
424+
('the group offset dropped from the destination',
425425
sub(Path('src/tensorforge/backend/instructions/compute/bitlayout.py'),
426-
' here, there = base + have.locate(*index), want.locate(*index)',
427-
' here, there = have.locate(*index), want.locate(*index)')),
426+
' there = base_want + want.locate(*index)',
427+
' there = want.locate(*index)')),
428+
('the destination lanes reported as the source lanes',
429+
sub(Path('src/tensorforge/backend/instructions/compute/bitlayout.py'),
430+
'tuple(sorted({there for _, there in pairs}))',
431+
'tuple(sorted({here for here, _ in pairs}))')),
428432
('a slot weight read as a lane weight',
429433
sub(Path('src/tensorforge/backend/instructions/compute/bitlayout.py'),
430434
'Bit(Place.LANE if weight > 0 else Place.SLOT, abs(weight))',

0 commit comments

Comments
 (0)