Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,20 @@ jobs:
asm.generate_binary('/tmp/smolvlm2.asm', '/tmp/smolvlm2.mem')
print('SmolVLM2 assembled OK')
"

matrix-view-tests:
name: Matrix SRAM view and L_TILE tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install test dependencies
run: pip install pytest pyyaml
- name: Test encoding, layouts, ownership and lowering
run: |
PYTHONPATH=. python3 -m pytest -q \
assembler/tests/test_matrix_view.py \
aten/tests/test_matrix_view.py \
aten/tests/test_lcompute.py
133 changes: 131 additions & 2 deletions assembler/assembly_to_binary.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from utils.load_config import load_svh_settings

from compiler.aten.plena.isa_matrix_view import (
LTilePrimitive,
MatrixViewAxis,
encode_matrix_view_dma_word,
encode_l_tile_exec,
encode_l_tile_cfg,
validate_matrix_view_dominance,
)

from .parser import load_isa_definitions, parse_asm_file


Expand All @@ -23,6 +32,16 @@
"V_TOPK",
}
)
_PSEUDO_OPCODE_ALIASES = {
"L_TILE_CFG": "L_TILE",
"L_TILE_EXEC": "L_TILE",
"V_ADD_VV.MV": "V_ADD_VV",
"V_SUB_VV.MV": "V_SUB_VV",
"V_MUL_VV.MV": "V_MUL_VV",
"H_PREFETCH_V.MV": "H_PREFETCH_V",
"H_STORE_V.MV": "H_STORE_V",
}

_IMM_RS1_RD_OPS = frozenset(
{
"S_ADDI_INT",
Expand Down Expand Up @@ -89,7 +108,8 @@ def _convert_to_binary(self, instruction):
:return: Binary representation of the instruction
"""
# Example conversion logic (to be replaced with actual logic)
opcode = self.isa_definitions[instruction.opcode]
mnemonic = instruction.opcode
opcode = self.isa_definitions[_PSEUDO_OPCODE_ALIASES.get(mnemonic, mnemonic)]
rd = instruction.rd
rs1 = instruction.rs1
rs2 = instruction.rs2
Expand All @@ -105,7 +125,114 @@ def _convert_to_binary(self, instruction):
# Treat omitted rmask deterministically as "mask disabled" instead of crashing on None << ...
rmask = 0

if instruction.opcode in _IMM_RS1_RD_OPS:
if mnemonic == "L_TILE_CFG":
# Text: L_TILE_CFG slot, gp_shape, gp_map.
slot = rd
shape_register = rs1
map_register = rs2
if slot is None or shape_register is None or map_register is None:
raise ValueError("L_TILE_CFG requires slot, shape register, and map register")
if not 0 <= slot < 4:
raise ValueError(f"L_TILE_CFG slot must be in [0, 4), got {slot}")
binary_instruction = encode_l_tile_cfg(
slot=slot,
shape_register=shape_register,
map_register=map_register,
)
elif mnemonic == "L_TILE_EXEC":
# Text: L_TILE_EXEC gp_dst, gp_src1, gp_scale, primitive[, axis_mask].
# axis_mask[0] selects source columns and axis_mask[1] selects scale
# columns. Omission is the byte-compatible row/row form.
if rd is None or rs1 is None or rs2 is None or rstride is None:
raise ValueError(
"L_TILE_EXEC requires destination/source base registers and primitive"
)
try:
primitive = LTilePrimitive(rstride)
except ValueError as error:
raise ValueError(f"reserved L_TILE primitive {rstride}") from error
axis_mask = 0 if funct1 is None else funct1
if not isinstance(axis_mask, int) or not 0 <= axis_mask <= 0b11:
raise ValueError("L_TILE_EXEC axis mask must be in [0, 3]")
binary_instruction = encode_l_tile_exec(
dst_register=rd,
src1_register=rs1,
src2_register=rs2,
primitive=primitive,
source_axis=MatrixViewAxis((axis_mask >> 0) & 1),
scale_axis=MatrixViewAxis((axis_mask >> 1) & 1),
)
elif mnemonic in {"H_PREFETCH_V.MV", "H_STORE_V.MV"}:
# Existing vector DMA with an explicit Matrix-view destination or
# source: rd, rs1, rs2, rstride, precision, view_slot.
if None in (rd, rs1, rs2, rstride, funct1, instruction.funct2):
raise ValueError(f"{mnemonic} requires all six operands")
if not isinstance(funct1, int) or not 0 <= funct1 <= 2:
raise ValueError(f"{mnemonic}: precision must be in [0, 2]")
slot = instruction.funct2
if not isinstance(slot, int) or not 0 <= slot < 4:
raise ValueError(f"{mnemonic}: Matrix view slot must be in [0, 4)")
legacy_word = (
(funct1 << (opw + 4 * ow))
+ (rstride << (opw + 3 * ow))
+ (rs2 << (opw + 2 * ow))
+ (rs1 << (opw + ow))
+ (rd << opw)
+ opcode
)
binary_instruction = encode_matrix_view_dma_word(legacy_word, slot=slot)
elif mnemonic == "M_MM_WO" and rstride is not None:
# View-qualified existing writeback. The 18-bit immediate uses its
# top bit as an explicit view marker and the next two bits as the
# slot. Legacy three-operand words retain marker=0 byte-for-byte.
if rd is None or rs1 is None or imm is None:
raise ValueError("M_MM_WO requires base, stride register, and offset")
if not 0 <= rstride < 4:
raise ValueError(f"M_MM_WO Matrix view slot must be in [0, 4), got {rstride}")
if not 0 <= imm < (1 << 15):
raise ValueError(
f"view-qualified M_MM_WO offset must fit 15 bits, got {imm}"
)
encoded_imm = (1 << 17) | (rstride << 15) | imm
binary_instruction = (
(encoded_imm << (opw + 2 * ow))
+ (rs1 << (opw + ow))
+ (rd << opw)
+ opcode
)
elif mnemonic in _RS2_RS1_RD_OPS and mnemonic.startswith("M_") and rstride is not None:
# A fourth Matrix operand is an explicit view slot. funct1=0 keeps
# the legacy no-view word; codes 1..4 select slots 0..3.
if not 0 <= rstride < 4:
raise ValueError(f"{mnemonic}: Matrix view slot must be in [0, 4), got {rstride}")
binary_instruction = (
((rstride + 1) << (opw + 4 * ow))
+ (rs2 << (opw + 2 * ow))
+ (rs1 << (opw + ow))
+ (rd << opw)
+ opcode
)
elif mnemonic in {"V_ADD_VV.MV", "V_SUB_VV.MV", "V_MUL_VV.MV"}:
if not isinstance(funct1, int) or isinstance(funct1, bool):
raise TypeError(f"{mnemonic}: Matrix-view operand mask must be an int")
if funct1 == 0:
raise ValueError(f"{mnemonic}: Matrix-view operand mask cannot be zero")
if not 1 <= funct1 <= 7:
raise ValueError(f"{mnemonic}: Matrix-view operand mask must be in [1, 7]")
binary_instruction = (
((funct1 | 0x8) << (opw + 4 * ow))
+ ((rmask or 0) << (opw + 3 * ow))
+ ((rs2 or 0) << (opw + 2 * ow))
+ ((rs1 or 0) << (opw + ow))
+ ((rd or 0) << opw)
+ opcode
)
elif instruction.opcode in _IMM_RS1_RD_OPS:
if mnemonic == "M_MM_WO" and not 0 <= imm < (1 << 17):
raise ValueError(
"legacy M_MM_WO offset must fit 17 bits; bit 17 is the "
f"Matrix-view marker, got {imm}"
)
binary_instruction = (imm << (opw + 2 * ow)) + (rs1 << (opw + ow)) + (rd << opw) + opcode
elif instruction.opcode in _IMM_RD_OPS:
binary_instruction = (imm << (opw + ow)) + (rd << opw) + opcode
Expand Down Expand Up @@ -153,6 +280,8 @@ def generate_binary(self, asm_file: str, output_file: str):
"""
Generate binary instructions from the assembled instructions.
"""
with open(asm_file) as source:
validate_matrix_view_dominance(source.read())
instructions = parse_asm_file(asm_file)
binary_instructions = []
for instruction in instructions:
Expand Down
166 changes: 166 additions & 0 deletions assembler/tests/test_matrix_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from pathlib import Path

import pytest

from assembler.assembly_to_binary import AssemblyToBinary
from assembler.parser import Instruction
from compiler.aten.plena.isa_matrix_view import LTilePrimitive


ROOT = Path(__file__).resolve().parents[2]


def _assembler() -> AssemblyToBinary:
return AssemblyToBinary(
str(ROOT / "doc" / "operation.svh"),
str(ROOT / "doc" / "configuration.svh"),
)


def test_config_uses_0x3f_and_the_config_funct() -> None:
word = _assembler()._convert_to_binary(
Instruction("L_TILE_CFG", 2, 7, 9, None, None, None)
)
assert word & 0x3F == 0x3F
assert (word >> 22) & 0xF == 1


@pytest.mark.parametrize("primitive", list(LTilePrimitive))
def test_exec_uses_0x3f_and_a_distinct_funct(
primitive: LTilePrimitive,
) -> None:
word = _assembler()._convert_to_binary(
Instruction("L_TILE_EXEC", 4, 5, 6, int(primitive), None, None)
)
assert word & 0x3F == 0x3F
assert (word >> 22) & 0xF == 3
assert (word >> 18) & 0xF == int(primitive)


def test_exec_rejects_a_reserved_primitive() -> None:
with pytest.raises(ValueError, match="reserved L_TILE primitive"):
_assembler()._convert_to_binary(
Instruction("L_TILE_EXEC", 4, 5, 6, 3, None, None)
)


def test_exec_encodes_optional_operand_axes_without_spending_an_opcode() -> None:
word = _assembler()._convert_to_binary(
Instruction("L_TILE_EXEC", 4, 5, 6, 1, 0b10, None)
)
assert word & 0x3F == 0x3F
assert (word >> 22) & 0xF == 3
assert (word >> 26) & 0b11 == 0b10


def test_exec_rejects_reserved_operand_axes() -> None:
with pytest.raises(ValueError, match="axis mask"):
_assembler()._convert_to_binary(
Instruction("L_TILE_EXEC", 4, 5, 6, 1, 4, None)
)


def test_matrix_fourth_operand_is_an_explicit_view_slot() -> None:
assembler = _assembler()
# M_BMV architecturally consumes rd as its matrix offset, so a non-zero rd
# locks every register field without violating M_MV's rd=0 convention.
plain = assembler._convert_to_binary(Instruction("M_BMV", 5, 1, 2, None, None, None))
viewed = assembler._convert_to_binary(Instruction("M_BMV", 5, 1, 2, 3, None, None))
assert plain == 0x09 | (5 << 6) | (1 << 10) | (2 << 14)
assert viewed & ((1 << 22) - 1) == plain
assert (viewed >> 22) & 0xF == 4


def test_view_slot_out_of_range_is_rejected() -> None:
with pytest.raises(ValueError, match="view slot"):
_assembler()._convert_to_binary(Instruction("M_TMM", 0, 1, 2, 4, None, None))


def test_matrix_view_vector_alias_keeps_arithmetic_opcode_and_marks_operands() -> None:
assembler = _assembler()
plain = assembler._convert_to_binary(
Instruction("V_ADD_VV", 4, 5, 6, 0, 0, None)
)
viewed = assembler._convert_to_binary(
Instruction("V_ADD_VV.MV", 4, 5, 6, 0, 0b110, None)
)
assert plain & 0x3F == viewed & 0x3F == 0x0D
assert plain >> 22 == 0
assert (viewed >> 22) & 0xF == 0x8 | 0b110


def test_matrix_view_vector_alias_rejects_an_empty_operand_mask() -> None:
with pytest.raises(ValueError, match="cannot be zero"):
_assembler()._convert_to_binary(
Instruction("V_MUL_VV.MV", 1, 1, 2, 0, 0, None)
)


def test_accumulator_writeback_uses_existing_m_mm_wo_with_explicit_view() -> None:
assembler = _assembler()
legacy = assembler._convert_to_binary(
Instruction("M_MM_WO", 4, 0, None, None, None, None, imm=5)
)
viewed = assembler._convert_to_binary(
Instruction("M_MM_WO", 4, 0, None, 2, None, None, imm=5)
)
assert legacy & 0x3F == viewed & 0x3F == 0x06
legacy_imm = legacy >> 14
viewed_imm = viewed >> 14
assert legacy_imm == 5
assert viewed_imm == (1 << 17) | (2 << 15) | 5


@pytest.mark.parametrize(
("mnemonic", "opcode"),
[("H_PREFETCH_V.MV", 0x29), ("H_STORE_V.MV", 0x2A)],
)
def test_vector_dma_matrix_view_form_reuses_opcode_and_names_slot(
mnemonic: str, opcode: int
) -> None:
word = _assembler()._convert_to_binary(
Instruction(mnemonic, 1, 2, 3, 1, 2, 3)
)
assert word & 0x3F == opcode
assert word >> 31 == 1
assert (word >> 29) & 0b11 == 3
assert (word >> 26) & 0b111 == 0
assert (word >> 22) & 0xF == 2
assert word & ((1 << 26) - 1) == (
opcode | (1 << 6) | (2 << 10) | (3 << 14) | (1 << 18) | (2 << 22)
)


@pytest.mark.parametrize("precision", [0, 1, 2])
def test_vector_dma_matrix_view_accepts_all_canonical_precisions(
precision: int,
) -> None:
word = _assembler()._convert_to_binary(
Instruction("H_PREFETCH_V.MV", 1, 2, 3, 1, precision, 0)
)
assert (word >> 22) & 0xF == precision


def test_vector_dma_matrix_view_form_rejects_reserved_slot() -> None:
with pytest.raises(ValueError, match="view slot"):
_assembler()._convert_to_binary(
Instruction("H_PREFETCH_V.MV", 1, 2, 3, 1, 2, 4)
)


def test_legacy_m_mm_wo_cannot_alias_the_view_marker() -> None:
with pytest.raises(ValueError, match="bit 17"):
_assembler()._convert_to_binary(
Instruction("M_MM_WO", 4, 0, None, None, None, None, imm=1 << 17)
)


def test_generate_binary_enforces_matrix_view_dominance(tmp_path: Path) -> None:
source = tmp_path / "bad.asm"
output = tmp_path / "bad.mem"
source.write_text("M_TMM gp0, gp1, gp2, 2\n")

with pytest.raises(ValueError, match="before a dominating configuration"):
_assembler().generate_binary(str(source), str(output))

assert not output.exists()
Loading
Loading