forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0798119
This commit is contained in:
1
tinygrad_repo/test/amd/hw/__init__.py
Normal file
1
tinygrad_repo/test/amd/hw/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Hardware-validated emulator tests for RDNA3 instructions."""
|
||||
287
tinygrad_repo/test/amd/hw/helpers.py
Normal file
287
tinygrad_repo/test/amd/hw/helpers.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""Test infrastructure for hardware-validated RDNA3 emulator tests.
|
||||
|
||||
Uses run_asm() with memory output, so tests can run on both emulator and real hardware.
|
||||
Set USE_HW=1 to run on both emulator and hardware, comparing results.
|
||||
"""
|
||||
import ctypes, math, os, struct
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
from tinygrad.renderer.amd.dsl import NULL, SCC, VCC_LO, VCC_HI, EXEC_LO, EXEC_HI, M0
|
||||
|
||||
def _i32(f: float) -> int: return struct.unpack('<I', struct.pack('<f', f))[0]
|
||||
def _f32(i: int) -> float: return struct.unpack('<f', struct.pack('<I', i & 0xFFFFFFFF))[0]
|
||||
|
||||
# f16 conversion helpers
|
||||
def f16(i: int) -> float: return struct.unpack('<e', struct.pack('<H', i & 0xFFFF))[0]
|
||||
def f32_to_f16(f: float) -> int:
|
||||
f = float(f)
|
||||
if math.isnan(f): return 0x7e00
|
||||
if math.isinf(f): return 0x7c00 if f > 0 else 0xfc00
|
||||
try: return struct.unpack('<H', struct.pack('<e', f))[0]
|
||||
except OverflowError: return 0x7c00 if f > 0 else 0xfc00
|
||||
|
||||
# For backwards compatibility with tests using SrcEnum.NULL etc.
|
||||
class SrcEnum:
|
||||
NULL = NULL
|
||||
VCC_LO = VCC_LO
|
||||
VCC_HI = VCC_HI
|
||||
EXEC_LO = EXEC_LO
|
||||
EXEC_HI = EXEC_HI
|
||||
SCC = SCC
|
||||
M0 = M0
|
||||
POS_HALF = 0.5
|
||||
NEG_HALF = -0.5
|
||||
POS_ONE = 1.0
|
||||
NEG_ONE = -1.0
|
||||
POS_TWO = 2.0
|
||||
NEG_TWO = -2.0
|
||||
POS_FOUR = 4.0
|
||||
NEG_FOUR = -4.0
|
||||
|
||||
VCC = VCC_LO # For VOP3SD sdst field (VCC_LO is exported from dsl)
|
||||
USE_HW = os.environ.get("USE_HW", "0") == "1"
|
||||
FLOAT_TOLERANCE = 1e-5
|
||||
|
||||
def get_gpu_target() -> tuple[int, int, int]:
|
||||
"""Get the GPU target as (major, minor, stepping) tuple."""
|
||||
if not USE_HW: return (0, 0, 0)
|
||||
from tinygrad.device import Device
|
||||
return Device["AMD"].target # type: ignore[attr-defined]
|
||||
|
||||
def skip_unless_gfx(min_major: int, min_minor: int = 0, reason: str = ""):
|
||||
"""Skip test if GPU target is below the minimum required version."""
|
||||
import unittest
|
||||
def decorator(test_func):
|
||||
if not USE_HW: return test_func
|
||||
target = get_gpu_target()
|
||||
if target[0] < min_major or (target[0] == min_major and target[1] < min_minor):
|
||||
return unittest.skip(reason or f"requires gfx{min_major}{min_minor}0+")(test_func)
|
||||
return test_func
|
||||
return decorator
|
||||
|
||||
# Output buffer layout: vgpr[N_VGPRS][n_lanes], sgpr[N_SGPRS], vcc, scc, exec
|
||||
N_VGPRS, N_SGPRS, WAVE_SIZE = 16, 16, 32
|
||||
SGPR_BYTES = N_SGPRS * 4 # 16 regs * 4 bytes = 64
|
||||
_VGPR_REGION = N_VGPRS * WAVE_SIZE * 4 # minimum vgpr region size (tests may use as scratch)
|
||||
def _out_bytes(n_lanes: int) -> int: return max(N_VGPRS * n_lanes * 4, _VGPR_REGION) + SGPR_BYTES + 12
|
||||
OUT_BYTES = _out_bytes(WAVE_SIZE) # default for single-wave (backward compat)
|
||||
|
||||
# Float conversion helpers
|
||||
def f2i(f: float) -> int: return _i32(f)
|
||||
def i2f(i: int) -> float: return _f32(i)
|
||||
def f2i64(f: float) -> int: return struct.unpack('<Q', struct.pack('<d', f))[0]
|
||||
def i642f(i: int) -> float: return struct.unpack('<d', struct.pack('<Q', i))[0]
|
||||
|
||||
def assemble(instructions: list) -> bytes:
|
||||
return b''.join(inst.to_bytes() for inst in instructions)
|
||||
|
||||
# Simple WaveState class for test output parsing (mirrors test/mockgpu/amd/emu.py interface for tests)
|
||||
class WaveState:
|
||||
def __init__(self, n_lanes: int = 32):
|
||||
self.vgpr = [[0] * 256 for _ in range(n_lanes)] # vgpr[lane][reg]
|
||||
self.sgpr = [0] * 128
|
||||
self.vcc = 0
|
||||
self.scc = 0
|
||||
|
||||
def get_prologue_epilogue(n_lanes: int) -> tuple[list, list]:
|
||||
"""Generate prologue and epilogue instructions for state capture."""
|
||||
prologue = [
|
||||
s_mov_b32(s[80], s[0]),
|
||||
s_mov_b32(s[81], s[1]),
|
||||
v_mov_b32_e32(v[255], v[0]),
|
||||
]
|
||||
for i in range(N_VGPRS):
|
||||
prologue.append(v_mov_b32_e32(v[i], 0))
|
||||
for i in range(N_SGPRS):
|
||||
prologue.append(s_mov_b32(s[i], 0))
|
||||
prologue.append(s_mov_b32(VCC_LO, 0))
|
||||
|
||||
epilogue = [
|
||||
s_mov_b32(s[90], VCC_LO),
|
||||
s_cselect_b32(s[91], 1, 0),
|
||||
# Save EXEC early (before we modify it for VGPR stores)
|
||||
s_mov_b32(s[95], EXEC_LO),
|
||||
# Restore EXEC to all active lanes for VGPR stores (test may have modified EXEC)
|
||||
s_mov_b32(EXEC_LO, (1 << min(n_lanes, WAVE_SIZE)) - 1),
|
||||
s_load_b64(s[92:93], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt(0), # simm16=0 waits for all
|
||||
v_lshlrev_b32_e32(v[240], 2, v[255]),
|
||||
]
|
||||
vgpr_bytes = N_VGPRS * n_lanes * 4
|
||||
for i in range(N_VGPRS):
|
||||
epilogue.append(global_store_b32(addr=v[240], data=v[i], saddr=s[92:93], offset=i * n_lanes * 4))
|
||||
epilogue.append(v_mov_b32_e32(v[241], 0))
|
||||
epilogue.append(v_cmp_eq_u32_e32(v[255], v[241]))
|
||||
epilogue.append(s_and_saveexec_b32(s[94], VCC_LO))
|
||||
# Scalar stores: only thread 0. Use v[240]=vgpr_bytes as base offset so immediate offsets stay small.
|
||||
epilogue.append(v_mov_b32_e32(v[240], vgpr_bytes))
|
||||
for i in range(N_SGPRS):
|
||||
epilogue.append(v_mov_b32_e32(v[243], s[i]))
|
||||
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=i * 4))
|
||||
epilogue.append(v_mov_b32_e32(v[243], s[90]))
|
||||
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=SGPR_BYTES))
|
||||
epilogue.append(v_mov_b32_e32(v[243], s[91]))
|
||||
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=SGPR_BYTES + 4))
|
||||
# Store EXEC (saved earlier in s[95])
|
||||
epilogue.append(v_mov_b32_e32(v[243], s[95]))
|
||||
epilogue.append(global_store_b32(addr=v[240], data=v[243], saddr=s[92:93], offset=SGPR_BYTES + 8))
|
||||
epilogue.append(s_mov_b32(EXEC_LO, s[94]))
|
||||
epilogue.append(s_endpgm())
|
||||
return prologue, epilogue
|
||||
|
||||
def parse_output(out_buf: bytes, n_lanes: int) -> WaveState:
|
||||
"""Parse output buffer into WaveState."""
|
||||
vgpr_bytes = N_VGPRS * n_lanes * 4
|
||||
st = WaveState(n_lanes)
|
||||
for i in range(N_VGPRS):
|
||||
for lane in range(n_lanes):
|
||||
off = i * n_lanes * 4 + lane * 4
|
||||
st.vgpr[lane][i] = struct.unpack_from('<I', out_buf, off)[0]
|
||||
for i in range(N_SGPRS):
|
||||
st.sgpr[i] = struct.unpack_from('<I', out_buf, vgpr_bytes + i * 4)[0]
|
||||
st.vcc = struct.unpack_from('<I', out_buf, vgpr_bytes + SGPR_BYTES)[0]
|
||||
st.scc = struct.unpack_from('<I', out_buf, vgpr_bytes + SGPR_BYTES + 4)[0]
|
||||
# Store EXEC in its proper location (index 126)
|
||||
st.sgpr[EXEC_LO.offset] = struct.unpack_from('<I', out_buf, vgpr_bytes + SGPR_BYTES + 8)[0]
|
||||
return st
|
||||
|
||||
def run_program_emu(instructions: list, n_lanes: int = 1) -> WaveState:
|
||||
"""Run instructions via emulator run_asm, dump state to memory, return WaveState."""
|
||||
buf_sz = _out_bytes(n_lanes)
|
||||
out_buf = (ctypes.c_uint8 * buf_sz)(*([0] * buf_sz))
|
||||
out_addr = ctypes.addressof(out_buf)
|
||||
|
||||
prologue, epilogue = get_prologue_epilogue(n_lanes)
|
||||
code = assemble(prologue + instructions + epilogue)
|
||||
|
||||
args = (ctypes.c_uint64 * 1)(out_addr)
|
||||
args_ptr = ctypes.addressof(args)
|
||||
kernel_buf = (ctypes.c_char * len(code)).from_buffer_copy(code)
|
||||
lib_ptr = ctypes.addressof(kernel_buf)
|
||||
|
||||
# rsrc2: USER_SGPR_COUNT=2, ENABLE_SGPR_WORKGROUP_ID_X/Y/Z=1, LDS_SIZE=128 (64KB)
|
||||
rsrc2 = 0x19c | (128 << 15)
|
||||
scratch_size = 0x10000 # 64KB per lane, matches .amdhsa_private_segment_fixed_size in run_program_hw
|
||||
result = run_asm(lib_ptr, len(code), 1, 1, 1, n_lanes, 1, 1, args_ptr, rsrc2, scratch_size)
|
||||
assert result == 0, f"run_asm failed with {result}"
|
||||
|
||||
return parse_output(bytes(out_buf), n_lanes)
|
||||
|
||||
def run_program_hw(instructions: list, n_lanes: int = 1) -> WaveState:
|
||||
"""Run instructions on real AMD hardware via HIPCompiler and AMDProgram."""
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
from tinygrad.helpers import flat_mv
|
||||
|
||||
dev = Device["AMD"]
|
||||
compiler = HIPCompiler(dev.arch) # type: ignore[attr-defined]
|
||||
|
||||
prologue, epilogue = get_prologue_epilogue(n_lanes)
|
||||
code = assemble(prologue + instructions + epilogue)
|
||||
|
||||
byte_str = ', '.join(f'0x{b:02x}' for b in code)
|
||||
asm_src = f""".text
|
||||
.globl test
|
||||
.p2align 8
|
||||
.type test,@function
|
||||
test:
|
||||
.byte {byte_str}
|
||||
|
||||
.rodata
|
||||
.p2align 6
|
||||
.amdhsa_kernel test
|
||||
.amdhsa_next_free_vgpr 256
|
||||
.amdhsa_next_free_sgpr 96
|
||||
.amdhsa_wavefront_size32 1
|
||||
.amdhsa_user_sgpr_kernarg_segment_ptr 1
|
||||
.amdhsa_kernarg_size 8
|
||||
.amdhsa_group_segment_fixed_size 65536
|
||||
.amdhsa_private_segment_fixed_size 65536
|
||||
.amdhsa_enable_private_segment 1
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 0
|
||||
amdhsa.kernels:
|
||||
- .name: test
|
||||
.symbol: test.kd
|
||||
.kernarg_segment_size: 8
|
||||
.group_segment_fixed_size: 65536
|
||||
.private_segment_fixed_size: 65536
|
||||
.kernarg_segment_align: 8
|
||||
.wavefront_size: 32
|
||||
.sgpr_count: 96
|
||||
.vgpr_count: 256
|
||||
.max_flat_workgroup_size: 1024
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
|
||||
|
||||
buf_sz = _out_bytes(n_lanes)
|
||||
out_gpu = dev.allocator.alloc(buf_sz)
|
||||
assert out_gpu.va_addr % 16 == 0, f"buffer not 16-byte aligned: 0x{out_gpu.va_addr:x}"
|
||||
prg(out_gpu, global_size=(1, 1, 1), local_size=(n_lanes, 1, 1), wait=True)
|
||||
|
||||
out_buf = bytearray(buf_sz)
|
||||
dev.allocator._copyout(flat_mv(memoryview(out_buf)), out_gpu)
|
||||
|
||||
return parse_output(bytes(out_buf), n_lanes)
|
||||
|
||||
def compare_wave_states(emu_st: WaveState, hw_st: WaveState, n_lanes: int, n_vgprs: int = N_VGPRS, ulp_tolerance: int = 0) -> list[str]:
|
||||
"""Compare two WaveStates and return list of differences.
|
||||
|
||||
Args:
|
||||
ulp_tolerance: Allow up to this many ULPs difference for float comparisons (0 = exact match required)
|
||||
"""
|
||||
import math
|
||||
diffs = []
|
||||
for i in range(n_vgprs):
|
||||
for lane in range(n_lanes):
|
||||
emu_val = emu_st.vgpr[lane][i]
|
||||
hw_val = hw_st.vgpr[lane][i]
|
||||
if emu_val != hw_val:
|
||||
emu_f, hw_f = _f32(emu_val), _f32(hw_val)
|
||||
if math.isnan(emu_f) and math.isnan(hw_f):
|
||||
continue
|
||||
# Check ULP difference for floats (only for same-sign values)
|
||||
if ulp_tolerance > 0 and (emu_val < 0x80000000) == (hw_val < 0x80000000):
|
||||
ulp_diff = abs(int(emu_val) - int(hw_val))
|
||||
if ulp_diff <= ulp_tolerance:
|
||||
continue
|
||||
diffs.append(f"v[{i}] lane {lane}: emu=0x{emu_val:08x} ({emu_f:.6g}) hw=0x{hw_val:08x} ({hw_f:.6g})")
|
||||
for i in range(N_SGPRS):
|
||||
emu_val = emu_st.sgpr[i]
|
||||
hw_val = hw_st.sgpr[i]
|
||||
if emu_val != hw_val:
|
||||
diffs.append(f"s[{i}]: emu=0x{emu_val:08x} hw=0x{hw_val:08x}")
|
||||
if emu_st.vcc != hw_st.vcc:
|
||||
diffs.append(f"vcc: emu=0x{emu_st.vcc:08x} hw=0x{hw_st.vcc:08x}")
|
||||
if emu_st.scc != hw_st.scc:
|
||||
diffs.append(f"scc: emu={emu_st.scc} hw={hw_st.scc}")
|
||||
return diffs
|
||||
|
||||
def run_program(instructions: list, n_lanes: int = 1, ulp_tolerance: int = 0) -> WaveState:
|
||||
"""Run instructions and return WaveState.
|
||||
|
||||
If USE_HW=1, runs on both emulator and hardware, compares results, and raises if they differ.
|
||||
Otherwise, runs only on emulator.
|
||||
|
||||
Args:
|
||||
ulp_tolerance: Allow up to this many ULPs difference for float comparisons (0 = exact match required)
|
||||
"""
|
||||
emu_st = run_program_emu(instructions, n_lanes)
|
||||
if USE_HW:
|
||||
hw_st = run_program_hw(instructions, n_lanes)
|
||||
diffs = compare_wave_states(emu_st, hw_st, n_lanes, ulp_tolerance=ulp_tolerance)
|
||||
if diffs:
|
||||
raise AssertionError("Emulator vs Hardware mismatch:\n" + "\n".join(diffs))
|
||||
return hw_st
|
||||
return emu_st
|
||||
20
tinygrad_repo/test/amd/hw/test_cdna_sdwa.py
Normal file
20
tinygrad_repo/test/amd/hw/test_cdna_sdwa.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
import tinygrad.runtime.autogen.amd.cdna.ins as cdna
|
||||
from test.amd.hw.test_cdna_vop3 import run_cdna
|
||||
|
||||
class TestCDNASDWA(unittest.TestCase):
|
||||
def test_v_add_co_u32_e32_writes_vcc(self):
|
||||
out = run_cdna([
|
||||
cdna.s_mov_b32(cdna.s[0], 0xffffffff),
|
||||
cdna.v_mov_b32_e32(cdna.v[0], cdna.s[0]),
|
||||
cdna.v_mov_b32_e32(cdna.v[13], 1),
|
||||
cdna.v_add_co_u32_e32(cdna.v[0], cdna.SDWA, cdna.v[13], vsrc0=cdna.v[0], dst_sel=6, src0_sel=6),
|
||||
cdna.v_mov_b32_e32(cdna.v[2], cdna.VCC_LO),
|
||||
cdna.v_lshlrev_b32_e32(cdna.v[2], 31, cdna.v[2]),
|
||||
cdna.v_or_b32_e32(cdna.v[2], cdna.v[2], cdna.v[0]),
|
||||
])
|
||||
self.assertEqual(out, 0x80000000)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
130
tinygrad_repo/test/amd/hw/test_cdna_vop3.py
Normal file
130
tinygrad_repo/test/amd/hw/test_cdna_vop3.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""CDNA VOP3 instruction coverage.
|
||||
|
||||
Exercises generated CDNA pcode end-to-end in the emulator and compares against
|
||||
gfx950 hardware when USE_HW=1.
|
||||
"""
|
||||
import ctypes, struct, unittest
|
||||
import tinygrad.runtime.autogen.amd.cdna.ins as cdna
|
||||
from tinygrad.helpers import flat_mv
|
||||
from tinygrad.renderer.amd.dsl import NULL
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
|
||||
LANES = 1
|
||||
|
||||
def _code(instructions: list, out_reg: int = 2, out_addr: int | None = None) -> bytes:
|
||||
load_out_addr = [
|
||||
cdna.s_mov_b32(cdna.s[92], out_addr & 0xffffffff),
|
||||
cdna.s_mov_b32(cdna.s[93], out_addr >> 32),
|
||||
] if out_addr is not None else [
|
||||
cdna.s_load_dwordx2(cdna.s[92:93], cdna.s[80:81], 0, soffset=NULL),
|
||||
cdna.s_waitcnt(0),
|
||||
]
|
||||
return assemble([
|
||||
cdna.s_mov_b32(cdna.s[80], cdna.s[0]),
|
||||
cdna.s_mov_b32(cdna.s[81], cdna.s[1]),
|
||||
cdna.v_mov_b32_e32(cdna.v[255], cdna.v[0]),
|
||||
*instructions,
|
||||
*load_out_addr,
|
||||
cdna.v_lshlrev_b32_e32(cdna.v[240], 2, cdna.v[255]),
|
||||
cdna.global_store_dword(addr=cdna.v[240], data=cdna.v[out_reg], saddr=cdna.s[92:93], offset=0),
|
||||
cdna.s_endpgm(),
|
||||
])
|
||||
|
||||
def _run_emu(instructions: list, out_reg: int = 2) -> int:
|
||||
out_buf = (ctypes.c_uint32 * LANES)(*([0] * LANES))
|
||||
args = (ctypes.c_uint64 * 1)(ctypes.addressof(out_buf))
|
||||
code = _code(instructions, out_reg)
|
||||
kernel_buf = (ctypes.c_char * len(code)).from_buffer_copy(code)
|
||||
result = run_asm(ctypes.addressof(kernel_buf), len(code), 1, 1, 1, LANES, 1, 1, ctypes.addressof(args),
|
||||
0x19c | (128 << 15), 0x10000, arch="cdna")
|
||||
assert result == 0, f"run_asm failed with {result}"
|
||||
return out_buf[0]
|
||||
|
||||
def _run_hw(instructions: list, out_reg: int = 2) -> int:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device["AMD"]
|
||||
if dev.arch != "gfx950": raise unittest.SkipTest("requires gfx950 hardware")
|
||||
out_gpu = dev.allocator.alloc(LANES * 4)
|
||||
code = _code(instructions, out_reg, out_gpu.va_addr)
|
||||
byte_str = ", ".join(f"0x{b:02x}" for b in code)
|
||||
asm_src = f""".text
|
||||
.globl test
|
||||
.p2align 8
|
||||
.type test,@function
|
||||
test:
|
||||
.byte {byte_str}
|
||||
|
||||
.rodata
|
||||
.p2align 6
|
||||
.amdhsa_kernel test
|
||||
.amdhsa_next_free_vgpr 256
|
||||
.amdhsa_next_free_sgpr 96
|
||||
.amdhsa_accum_offset 256
|
||||
.amdhsa_kernarg_size 0
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 0
|
||||
amdhsa.kernels:
|
||||
- .name: test
|
||||
.symbol: test.kd
|
||||
.kernarg_segment_size: 0
|
||||
.group_segment_fixed_size: 0
|
||||
.private_segment_fixed_size: 0
|
||||
.kernarg_segment_align: 8
|
||||
.wavefront_size: 64
|
||||
.sgpr_count: 96
|
||||
.vgpr_count: 256
|
||||
.max_flat_workgroup_size: 1024
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
prg = AMDProgram(dev, "test", HIPCompiler(dev.arch).compile(asm_src))
|
||||
prg(global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
|
||||
out = bytearray(LANES * 4)
|
||||
dev.allocator._copyout(flat_mv(memoryview(out)), out_gpu)
|
||||
return struct.unpack("<I", out)[0]
|
||||
|
||||
def run_cdna(instructions: list, out_reg: int = 2) -> int:
|
||||
emu = _run_emu(instructions, out_reg)
|
||||
if not USE_HW: return emu
|
||||
hw = _run_hw(instructions, out_reg)
|
||||
if emu != hw: raise AssertionError(f"Emulator vs Hardware mismatch: emu=0x{emu:08x} hw=0x{hw:08x}")
|
||||
return hw
|
||||
|
||||
class TestCDNAVOP3(unittest.TestCase):
|
||||
def test_cvt_pk_fp8_f32_preserves_upper_half(self):
|
||||
"""V_CVT_PK_FP8_F32 with OPSEL[3]=0 writes only D[15:0]."""
|
||||
out = run_cdna([
|
||||
cdna.s_mov_b32(cdna.s[0], 0xdeadbeef),
|
||||
cdna.v_mov_b32_e32(cdna.v[2], cdna.s[0]),
|
||||
cdna.v_mov_b32_e32(cdna.v[0], 1.0),
|
||||
cdna.v_mov_b32_e32(cdna.v[1], 2.0),
|
||||
cdna.v_cvt_pk_fp8_f32(cdna.v[2], cdna.v[0], cdna.v[1]),
|
||||
])
|
||||
self.assertEqual(out, 0xdead4038)
|
||||
|
||||
def test_cvt_pk_bf8_f32_overflow_and_inf(self):
|
||||
"""V_CVT_PK_BF8_F32 converts finite overflow and infinities to E5M2 infinities."""
|
||||
for name, bits, expected in [
|
||||
("finite_overflow", 0x47700000, 0x7c),
|
||||
("pos_inf", 0x7f800000, 0x7c),
|
||||
("neg_inf", 0xff800000, 0xfc),
|
||||
]:
|
||||
with self.subTest(name=name):
|
||||
out = run_cdna([
|
||||
cdna.s_mov_b32(cdna.s[0], 0xdeadbeef),
|
||||
cdna.v_mov_b32_e32(cdna.v[2], cdna.s[0]),
|
||||
cdna.s_mov_b32(cdna.s[0], bits),
|
||||
cdna.v_mov_b32_e32(cdna.v[0], cdna.s[0]),
|
||||
cdna.v_mov_b32_e32(cdna.v[1], 1.0),
|
||||
cdna.v_cvt_pk_bf8_f32(cdna.v[2], cdna.v[0], cdna.v[1]),
|
||||
])
|
||||
self.assertEqual(out, 0xdead3c00 | expected)
|
||||
187
tinygrad_repo/test/amd/hw/test_dpp.py
Normal file
187
tinygrad_repo/test/amd/hw/test_dpp.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Tests for DPP16 source swizzles.
|
||||
|
||||
These instructions trap in the default wave32 hw helper, so this file uses a
|
||||
minimal wave64 lane-store harness and compares emulator vs hardware directly
|
||||
when USE_HW=1.
|
||||
"""
|
||||
import ctypes, unittest
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import *
|
||||
from tinygrad.helpers import flat_mv
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
|
||||
WAVE64 = 64
|
||||
|
||||
def _wave64_code(instructions: list, out_reg: int = 1) -> bytes:
|
||||
return assemble([
|
||||
s_mov_b32(s[80], s[0]),
|
||||
s_mov_b32(s[81], s[1]),
|
||||
v_mov_b32_e32(v[255], v[0]),
|
||||
*instructions,
|
||||
s_load_b64(s[92:93], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt(0),
|
||||
v_lshlrev_b32_e32(v[240], 2, v[255]),
|
||||
global_store_b32(addr=v[240], data=v[out_reg], saddr=s[92:93], offset=0),
|
||||
s_endpgm(),
|
||||
])
|
||||
|
||||
def _run_wave64_emu(instructions: list, out_reg: int = 1) -> list[int]:
|
||||
out_buf = (ctypes.c_uint32 * WAVE64)(*([0] * WAVE64))
|
||||
args = (ctypes.c_uint64 * 1)(ctypes.addressof(out_buf))
|
||||
code = _wave64_code(instructions, out_reg)
|
||||
kernel_buf = (ctypes.c_char * len(code)).from_buffer_copy(code)
|
||||
rsrc2 = 0x19c | (128 << 15)
|
||||
scratch_size = 0x10000
|
||||
result = run_asm(ctypes.addressof(kernel_buf), len(code), 1, 1, 1, WAVE64, 1, 1, ctypes.addressof(args), rsrc2, scratch_size)
|
||||
assert result == 0, f"run_asm failed with {result}"
|
||||
return list(out_buf)
|
||||
|
||||
def _run_wave64_hw(instructions: list, out_reg: int = 1) -> list[int]:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device["AMD"]
|
||||
compiler = HIPCompiler(dev.arch) # type: ignore[attr-defined]
|
||||
code = _wave64_code(instructions, out_reg)
|
||||
byte_str = ', '.join(f'0x{b:02x}' for b in code)
|
||||
asm_src = f""".text
|
||||
.globl test
|
||||
.p2align 8
|
||||
.type test,@function
|
||||
test:
|
||||
.byte {byte_str}
|
||||
|
||||
.rodata
|
||||
.p2align 6
|
||||
.amdhsa_kernel test
|
||||
.amdhsa_next_free_vgpr 256
|
||||
.amdhsa_next_free_sgpr 96
|
||||
.amdhsa_user_sgpr_kernarg_segment_ptr 1
|
||||
.amdhsa_kernarg_size 8
|
||||
.amdhsa_group_segment_fixed_size 65536
|
||||
.amdhsa_private_segment_fixed_size 65536
|
||||
.amdhsa_enable_private_segment 1
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 0
|
||||
amdhsa.kernels:
|
||||
- .name: test
|
||||
.symbol: test.kd
|
||||
.kernarg_segment_size: 8
|
||||
.group_segment_fixed_size: 65536
|
||||
.private_segment_fixed_size: 65536
|
||||
.kernarg_segment_align: 8
|
||||
.wavefront_size: 64
|
||||
.sgpr_count: 96
|
||||
.vgpr_count: 256
|
||||
.max_flat_workgroup_size: 1024
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
|
||||
out_gpu = dev.allocator.alloc(WAVE64 * 4)
|
||||
prg(out_gpu, global_size=(1, 1, 1), local_size=(WAVE64, 1, 1), wait=True)
|
||||
out = bytearray(WAVE64 * 4)
|
||||
dev.allocator._copyout(flat_mv(memoryview(out)), out_gpu)
|
||||
return [int.from_bytes(out[i*4:(i+1)*4], 'little') for i in range(WAVE64)]
|
||||
|
||||
def run_wave64(instructions: list, out_reg: int = 1) -> list[int]:
|
||||
emu = _run_wave64_emu(instructions, out_reg)
|
||||
if not USE_HW: return emu
|
||||
hw = _run_wave64_hw(instructions, out_reg)
|
||||
if emu != hw:
|
||||
diffs = [f"lane {i}: emu=0x{e:08x} hw=0x{h:08x}" for i, (e, h) in enumerate(zip(emu, hw)) if e != h]
|
||||
raise AssertionError("Emulator vs Hardware mismatch:\n" + '\n'.join(diffs[:16]))
|
||||
return hw
|
||||
|
||||
class TestDPP16(unittest.TestCase):
|
||||
def _run_copy(self, dpp: int, *, row_mask: int = 0xf, bank_mask: int = 0xf, bc: int = 1, dst_seed: int | None = None) -> list[int]:
|
||||
instructions = [
|
||||
v_mul_u32_u24_e32(v[0], 10, v[255]),
|
||||
v_add_nc_u32_e32(v[0], 3, v[0]),
|
||||
]
|
||||
if dst_seed is not None: instructions.append(v_mov_b32_e32(v[1], dst_seed))
|
||||
instructions += [v_mov_b32_e32(v[2], 0), v_or_b32_e32(v[1], DPP, v[2], vsrc0=v[0], dpp=dpp, row_mask=row_mask, bank_mask=bank_mask, bc=bc)]
|
||||
return run_wave64(instructions)
|
||||
|
||||
def test_quad_perm_reverse(self):
|
||||
out = self._run_copy(0x1b)
|
||||
self.assertEqual(out[0], 33)
|
||||
self.assertEqual(out[1], 23)
|
||||
self.assertEqual(out[2], 13)
|
||||
self.assertEqual(out[3], 3)
|
||||
self.assertEqual(out[4], 73)
|
||||
|
||||
def test_row_shl(self):
|
||||
out = self._run_copy(0x101)
|
||||
self.assertEqual(out[0], 13)
|
||||
self.assertEqual(out[7], 83)
|
||||
self.assertEqual(out[14], 153)
|
||||
self.assertEqual(out[15], 0)
|
||||
self.assertEqual(out[16], 173)
|
||||
|
||||
def test_row_shr(self):
|
||||
out = self._run_copy(0x111)
|
||||
self.assertEqual(out[0], 0)
|
||||
self.assertEqual(out[1], 3)
|
||||
self.assertEqual(out[8], 73)
|
||||
self.assertEqual(out[15], 143)
|
||||
self.assertEqual(out[16], 0)
|
||||
self.assertEqual(out[17], 163)
|
||||
|
||||
def test_row_ror(self):
|
||||
out = self._run_copy(0x121)
|
||||
self.assertEqual(out[0], 153)
|
||||
self.assertEqual(out[1], 3)
|
||||
self.assertEqual(out[15], 143)
|
||||
self.assertEqual(out[16], 313)
|
||||
|
||||
def test_row_mirror(self):
|
||||
out = self._run_copy(0x140)
|
||||
self.assertEqual(out[0], 153)
|
||||
self.assertEqual(out[5], 103)
|
||||
self.assertEqual(out[8], 73)
|
||||
self.assertEqual(out[16], 313)
|
||||
|
||||
def test_row_half_mirror(self):
|
||||
out = self._run_copy(0x141)
|
||||
self.assertEqual(out[0], 73)
|
||||
self.assertEqual(out[7], 3)
|
||||
self.assertEqual(out[8], 153)
|
||||
self.assertEqual(out[15], 83)
|
||||
self.assertEqual(out[16], 233)
|
||||
|
||||
def test_row_mask(self):
|
||||
out = self._run_copy(0x101, row_mask=0x5, dst_seed=0xDEADBEEF)
|
||||
self.assertEqual(out[0], 13)
|
||||
self.assertEqual(out[15], 0)
|
||||
self.assertEqual(out[16], 0xDEADBEEF)
|
||||
self.assertEqual(out[32], 333)
|
||||
self.assertEqual(out[47], 0)
|
||||
self.assertEqual(out[48], 0xDEADBEEF)
|
||||
|
||||
def test_bank_mask(self):
|
||||
out = self._run_copy(0x101, bank_mask=0x5, dst_seed=0xDEADBEEF)
|
||||
self.assertEqual(out[0], 13)
|
||||
self.assertEqual(out[3], 43)
|
||||
self.assertEqual(out[4], 0xDEADBEEF)
|
||||
self.assertEqual(out[8], 93)
|
||||
self.assertEqual(out[12], 0xDEADBEEF)
|
||||
|
||||
class TestVOPCDPP16(unittest.TestCase):
|
||||
def test_row_bcast15_materializes_vcc(self):
|
||||
out = run_wave64([
|
||||
v_mov_b32_e32(v[0], v[255]),
|
||||
v_cmp_eq_u32_e32(DPP, v[0], vsrc0=v[0], dpp=0x142, row_mask=0xf, bank_mask=0xf, bc=1),
|
||||
v_mov_b32_e32(v[2], 0),
|
||||
v_mov_b32_e32(v[3], 1),
|
||||
v_cndmask_b32_e32(v[1], v[2], v[3]),
|
||||
])
|
||||
for lane in (0, 16, 32, 48): self.assertEqual(out[lane], 1)
|
||||
for lane in (1, 15, 31, 47, 63): self.assertEqual(out[lane], 0)
|
||||
960
tinygrad_repo/test/amd/hw/test_ds.py
Normal file
960
tinygrad_repo/test/amd/hw/test_ds.py
Normal file
@@ -0,0 +1,960 @@
|
||||
"""Tests for DS instructions - data share (LDS) operations.
|
||||
|
||||
Includes: ds_store_b32, ds_load_b32, ds_store_2addr_*, ds_load_2addr_*,
|
||||
ds_add_*, ds_max_*, ds_min_*, ds_and_*, ds_or_*, ds_xor_*,
|
||||
ds_inc_*, ds_dec_*, ds_cmpstore_*, ds_storexchg_*
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestDS2Addr(unittest.TestCase):
|
||||
"""Tests for DS_*_2ADDR instructions."""
|
||||
|
||||
def test_ds_store_load_2addr_b32(self):
|
||||
"""DS_STORE_2ADDR_B32 and DS_LOAD_2ADDR_B32 with offset * 4."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB)
|
||||
|
||||
def test_ds_store_load_2addr_b64(self):
|
||||
"""DS_STORE_2ADDR_B64 and DS_LOAD_2ADDR_B64."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0x12345678),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x9ABCDEF0),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_B64, addr=v[10], data0=v[0:1], data1=v[2:3], vdst=v[0], offset0=0, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4:7], offset0=0, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][5], 0xCAFEBABE)
|
||||
self.assertEqual(st.vgpr[0][6], 0x12345678)
|
||||
self.assertEqual(st.vgpr[0][7], 0x9ABCDEF0)
|
||||
|
||||
|
||||
class TestDS2AddrMore(unittest.TestCase):
|
||||
"""Additional DS_*_2ADDR tests."""
|
||||
|
||||
def test_ds_store_load_2addr_b32_nonzero_offsets(self):
|
||||
"""DS_STORE_2ADDR_B32 with non-zero offsets (offset*4 scaling)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=2, offset1=5),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=2, offset1=5),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x11111111, "v2 should have value from offset 8 (2*4)")
|
||||
self.assertEqual(st.vgpr[0][3], 0x22222222, "v3 should have value from offset 20 (5*4)")
|
||||
|
||||
def test_ds_2addr_b64_no_overlap(self):
|
||||
"""DS_LOAD_2ADDR_B64 with adjacent offsets should not overlap."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_mov_b32(s[2], 0x22222222),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=4),
|
||||
s_mov_b32(s[2], 0x33333333),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=8),
|
||||
s_mov_b32(s[2], 0x44444444),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=12),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[10], vdst=v[4:7], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should be 0x11111111")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should be 0x22222222")
|
||||
self.assertEqual(st.vgpr[0][6], 0x33333333, "v6 should be 0x33333333")
|
||||
self.assertEqual(st.vgpr[0][7], 0x44444444, "v7 should be 0x44444444")
|
||||
|
||||
def test_ds_load_2addr_b32_no_overwrite(self):
|
||||
"""DS_LOAD_2ADDR_B32 should only write 2 VGPRs."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[4], s[2]), # Sentinel
|
||||
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[2:3], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF, "v4 should be untouched")
|
||||
|
||||
def test_ds_load_2addr_b64_addr_overlaps_vdst(self):
|
||||
"""DS_LOAD_2ADDR_B64 where addr register overlaps vdst range.
|
||||
|
||||
Hardware reads the address before writing any results, so addr=v[4]
|
||||
with vdst=v[4:7] must load all 4 dwords using the original v[4] value.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_mov_b32(s[2], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=4),
|
||||
s_mov_b32(s[2], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=8),
|
||||
s_mov_b32(s[2], 0xDDDDDDDD),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=12),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# addr=v[4] overlaps vdst=v[4:7]
|
||||
v_mov_b32_e32(v[4], 0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B64, addr=v[4], vdst=v[4:7], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA, "v4 = LDS[0:4]")
|
||||
self.assertEqual(st.vgpr[0][5], 0xBBBBBBBB, "v5 = LDS[4:8]")
|
||||
self.assertEqual(st.vgpr[0][6], 0xCCCCCCCC, "v6 = LDS[8:12]")
|
||||
self.assertEqual(st.vgpr[0][7], 0xDDDDDDDD, "v7 = LDS[12:16]")
|
||||
|
||||
def test_ds_load_2addr_b32_addr_overlaps_vdst(self):
|
||||
"""DS_LOAD_2ADDR_B32 where addr register overlaps vdst range."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_mov_b32(s[2], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=4),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# addr=v[2] overlaps vdst=v[2:3]
|
||||
v_mov_b32_e32(v[2], 0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[2], vdst=v[2:3], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA, "v2 = LDS[0:4]")
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB, "v3 = LDS[4:8]")
|
||||
|
||||
def test_ds_load_b64_no_overwrite(self):
|
||||
"""DS_LOAD_B64 should only write 2 VGPRs."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_store_b64(addr=v[10], data0=v[0:1], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0x12345678),
|
||||
v_mov_b32_e32(v[4], s[2]), # Sentinel
|
||||
ds_load_b64(addr=v[10], vdst=v[2:3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][3], 0xCAFEBABE)
|
||||
self.assertEqual(st.vgpr[0][4], 0x12345678, "v4 should be untouched")
|
||||
|
||||
|
||||
class TestDSB96(unittest.TestCase):
|
||||
"""Tests for DS_STORE_B96 and DS_LOAD_B96 (96-bit / 3 dwords)."""
|
||||
|
||||
def test_ds_store_load_b96(self):
|
||||
"""DS_STORE_B96 stores 3 VGPRs, DS_LOAD_B96 loads them back."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0x33333333),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
ds_store_b96(addr=v[10], data0=v[0:2]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b96(addr=v[10], vdst=v[4:6]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should have first dword")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should have second dword")
|
||||
self.assertEqual(st.vgpr[0][6], 0x33333333, "v6 should have third dword")
|
||||
|
||||
def test_ds_store_b96_with_offset(self):
|
||||
"""DS_STORE_B96 with non-zero offset."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
DS(DSOp.DS_STORE_B96, addr=v[10], data0=v[0:2], offset0=12),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_B96, addr=v[10], vdst=v[4:6], offset0=12),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][5], 0xBBBBBBBB)
|
||||
self.assertEqual(st.vgpr[0][6], 0xCCCCCCCC)
|
||||
|
||||
|
||||
class TestDSB128(unittest.TestCase):
|
||||
"""Tests for DS_STORE_B128 and DS_LOAD_B128 (128-bit / 4 dwords)."""
|
||||
|
||||
def test_ds_store_load_b128(self):
|
||||
"""DS_STORE_B128 stores 4 VGPRs, DS_LOAD_B128 loads them back."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0x33333333),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x44444444),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
ds_store_b128(addr=v[10], data0=v[0:3]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b128(addr=v[10], vdst=v[4:7]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should have first dword")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should have second dword")
|
||||
self.assertEqual(st.vgpr[0][6], 0x33333333, "v6 should have third dword")
|
||||
self.assertEqual(st.vgpr[0][7], 0x44444444, "v7 should have fourth dword")
|
||||
|
||||
def test_ds_store_b128_with_offset(self):
|
||||
"""DS_STORE_B128 with non-zero offset."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xDDDDDDDD),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
DS(DSOp.DS_STORE_B128, addr=v[10], data0=v[0:3], offset0=16),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_B128, addr=v[10], vdst=v[4:7], offset0=16),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][5], 0xBBBBBBBB)
|
||||
self.assertEqual(st.vgpr[0][6], 0xCCCCCCCC)
|
||||
self.assertEqual(st.vgpr[0][7], 0xDDDDDDDD)
|
||||
|
||||
|
||||
class TestDSAtomic(unittest.TestCase):
|
||||
"""Tests for DS atomic operations."""
|
||||
|
||||
def test_ds_max_rtn_u32(self):
|
||||
"""DS_MAX_RTN_U32: atomically store max and return old value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 200),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_max_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 100, "v2 should have old value (100)")
|
||||
self.assertEqual(st.vgpr[0][3], 200, "v3 should have max(100, 200) = 200")
|
||||
|
||||
def test_ds_min_rtn_u32(self):
|
||||
"""DS_MIN_RTN_U32: atomically store min and return old value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 200),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_min_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 200)
|
||||
self.assertEqual(st.vgpr[0][3], 100)
|
||||
|
||||
def test_ds_and_rtn_b32(self):
|
||||
"""DS_AND_RTN_B32: atomically AND and return old value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xFF00FF00),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0xFFFF0000),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_and_rtn_b32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xFF00FF00)
|
||||
self.assertEqual(st.vgpr[0][3], 0xFF000000)
|
||||
|
||||
def test_ds_or_rtn_b32(self):
|
||||
"""DS_OR_RTN_B32: atomically OR and return old value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0x00FF0000),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0x000000FF),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_or_rtn_b32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00FF0000)
|
||||
self.assertEqual(st.vgpr[0][3], 0x00FF00FF)
|
||||
|
||||
def test_ds_xor_rtn_b32(self):
|
||||
"""DS_XOR_RTN_B32: atomically XOR and return old value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_xor_rtn_b32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][3], 0x55555555)
|
||||
|
||||
def test_ds_inc_rtn_u32(self):
|
||||
"""DS_INC_RTN_U32: increment with wrap."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 5),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 10), # limit
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_inc_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 5)
|
||||
self.assertEqual(st.vgpr[0][3], 6)
|
||||
|
||||
def test_ds_dec_rtn_u32(self):
|
||||
"""DS_DEC_RTN_U32: decrement with wrap."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 5),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 10), # limit
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_dec_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 5)
|
||||
self.assertEqual(st.vgpr[0][3], 4)
|
||||
|
||||
def test_ds_cmpstore_b32_match(self):
|
||||
"""DS_CMPSTORE_B32: conditional store when compare matches."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 200),
|
||||
v_mov_b32_e32(v[1], s[2]), # new value
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[2], s[2]), # compare = 100 (matches)
|
||||
ds_cmpstore_b32(addr=v[10], data0=v[1], data1=v[2], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[4], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 200)
|
||||
|
||||
def test_ds_cmpstore_b32_no_match(self):
|
||||
"""DS_CMPSTORE_B32: no store when compare doesn't match."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 200),
|
||||
v_mov_b32_e32(v[1], s[2]), # new value
|
||||
s_mov_b32(s[2], 50),
|
||||
v_mov_b32_e32(v[2], s[2]), # compare = 50 (doesn't match)
|
||||
ds_cmpstore_b32(addr=v[10], data0=v[1], data1=v[2], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[4], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 100)
|
||||
|
||||
def test_ds_max_u32_no_rtn(self):
|
||||
"""DS_MAX_U32 (no RTN): atomically store max, no return value."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 200),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_max_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 200, "v3 should have max(100, 200) = 200")
|
||||
|
||||
def test_ds_add_u32_no_rtn_preserves_vdst(self):
|
||||
"""DS_ADD_U32 (no RTN) should NOT write to vdst."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[2]), # sentinel
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 50),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_add_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xDEADBEEF, "v2 should preserve sentinel")
|
||||
self.assertEqual(st.vgpr[0][3], 150, "v3 should have 100 + 50 = 150")
|
||||
|
||||
def test_ds_add_rtn_u32_writes_vdst(self):
|
||||
"""DS_ADD_RTN_U32 should write old value to vdst."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[2]), # sentinel
|
||||
s_mov_b32(s[2], 100),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 50),
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_add_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 100, "v2 should have old value (100)")
|
||||
self.assertEqual(st.vgpr[0][3], 150, "v3 should have 100 + 50 = 150")
|
||||
|
||||
def test_ds_dec_rtn_u32_wrap(self):
|
||||
"""DS_DEC_RTN_U32: decrement wraps when value is 0 or > limit."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[2], 0), # Start at 0
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 10), # limit
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
ds_dec_rtn_u32(addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0, "v2 should have old value (0)")
|
||||
# When mem == 0 or mem > limit, result = limit
|
||||
self.assertEqual(st.vgpr[0][3], 10, "v3 should wrap to limit (10)")
|
||||
|
||||
|
||||
class TestDSStorexchg(unittest.TestCase):
|
||||
"""Tests for DS_STOREXCHG instructions."""
|
||||
|
||||
def test_ds_storexchg_rtn_b32(self):
|
||||
"""DS_STOREXCHG_RTN_B32: exchange value and return old."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STOREXCHG_RTN_B32, addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB)
|
||||
|
||||
|
||||
class TestDSRegisterWidth(unittest.TestCase):
|
||||
"""Regression tests: DS loads should only write correct number of VGPRs."""
|
||||
|
||||
def test_ds_load_b32_no_overwrite(self):
|
||||
"""DS_LOAD_B32 should only write 1 VGPR."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[0]), # sentinel
|
||||
ds_store_b32(addr=v[0], data0=v[1], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[0], vdst=v[1], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][2], 0x11111111, "v2 should be untouched")
|
||||
|
||||
|
||||
class TestDS2AddrStride64(unittest.TestCase):
|
||||
"""Tests for DS_*_2ADDR_STRIDE64 (offset * 256 for B32, offset * 512 for B64)."""
|
||||
|
||||
def test_ds_store_load_2addr_stride64_b32(self):
|
||||
"""DS_STORE_2ADDR_STRIDE64_B32: stores at ADDR + offset*256."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[2:3], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA, "v2 from addr 256")
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB, "v3 from addr 512")
|
||||
|
||||
def test_ds_store_load_2addr_stride64_b64(self):
|
||||
"""DS_STORE_2ADDR_STRIDE64_B64: stores at ADDR + offset*512."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
s_mov_b32(s[0], 0x12345678),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x9ABCDEF0),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[2:3], vdst=v[0], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[4:7], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][5], 0xCAFEBABE)
|
||||
self.assertEqual(st.vgpr[0][6], 0x12345678)
|
||||
self.assertEqual(st.vgpr[0][7], 0x9ABCDEF0)
|
||||
|
||||
def test_ds_storexchg_2addr_rtn_b32(self):
|
||||
"""DS_STOREXCHG_2ADDR_RTN_B32: exchange at two addresses."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
DS(DSOp.DS_STOREXCHG_2ADDR_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4:5], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_B32, addr=v[10], vdst=v[6:7], offset0=0, offset1=1),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "old val 0")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "old val 1")
|
||||
self.assertEqual(st.vgpr[0][6], 0xAAAAAAAA, "new val 0")
|
||||
self.assertEqual(st.vgpr[0][7], 0xBBBBBBBB, "new val 1")
|
||||
|
||||
def test_ds_storexchg_rtn_b64(self):
|
||||
"""DS_STOREXCHG_RTN_B64: exchange 64-bit value and return old."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[0], s[0]), # initial low
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[1], s[0]), # initial high
|
||||
DS(DSOp.DS_STORE_B64, addr=v[10], data0=v[0:1], vdst=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0x12345678),
|
||||
v_mov_b32_e32(v[2], s[0]), # new low
|
||||
s_mov_b32(s[0], 0x9ABCDEF0),
|
||||
v_mov_b32_e32(v[3], s[0]), # new high
|
||||
DS(DSOp.DS_STOREXCHG_RTN_B64, addr=v[10], data0=v[2:3], vdst=v[4:5], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_B64, addr=v[10], vdst=v[6:7], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF, "v4 should have old low dword")
|
||||
self.assertEqual(st.vgpr[0][5], 0xCAFEBABE, "v5 should have old high dword")
|
||||
self.assertEqual(st.vgpr[0][6], 0x12345678, "v6 should have new low dword")
|
||||
self.assertEqual(st.vgpr[0][7], 0x9ABCDEF0, "v7 should have new high dword")
|
||||
|
||||
def test_ds_store_load_2addr_stride64_b64_roundtrip(self):
|
||||
"""DS_STORE_2ADDR_STRIDE64_B64 followed by DS_LOAD_2ADDR_STRIDE64_B64 works correctly."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[0:1], vdst=v[0], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B64, addr=v[10], vdst=v[2:5], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x11111111, "v2 should have val1 low")
|
||||
self.assertEqual(st.vgpr[0][3], 0x22222222, "v3 should have val1 high")
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should have val2 low")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should have val2 high")
|
||||
|
||||
def test_ds_storexchg_2addr_stride64_rtn_b32(self):
|
||||
"""DS_STOREXCHG_2ADDR_STRIDE64_RTN_B32: exchange at two addresses (offset*256)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B32, addr=v[10], data0=v[0], data1=v[1], vdst=v[0], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B32, addr=v[10], data0=v[2], data1=v[3], vdst=v[4:5], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_2ADDR_STRIDE64_B32, addr=v[10], vdst=v[6:7], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0x11111111, "v4 should have old value")
|
||||
self.assertEqual(st.vgpr[0][5], 0x22222222, "v5 should have old value")
|
||||
self.assertEqual(st.vgpr[0][6], 0xAAAAAAAA, "v6 should have new value")
|
||||
self.assertEqual(st.vgpr[0][7], 0xBBBBBBBB, "v7 should have new value")
|
||||
|
||||
def test_ds_storexchg_2addr_stride64_rtn_b64_returns_old(self):
|
||||
"""DS_STOREXCHG_2ADDR_STRIDE64_RTN_B64: returns old values correctly."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
DS(DSOp.DS_STORE_2ADDR_STRIDE64_B64, addr=v[10], data0=v[0:1], data1=v[0:1], vdst=v[0], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[6], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[7], s[0]),
|
||||
DS(DSOp.DS_STOREXCHG_2ADDR_STRIDE64_RTN_B64, addr=v[10], data0=v[6:7], data1=v[6:7], vdst=v[8:11], offset0=1, offset1=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][8], 0x11111111, "v8 should have old val1 low")
|
||||
self.assertEqual(st.vgpr[0][9], 0x22222222, "v9 should have old val1 high")
|
||||
self.assertEqual(st.vgpr[0][10], 0x11111111, "v10 should have old val2 low")
|
||||
self.assertEqual(st.vgpr[0][11], 0x22222222, "v11 should have old val2 high")
|
||||
|
||||
|
||||
class TestAtomicOrdering(unittest.TestCase):
|
||||
"""Tests for atomic operation return values and ordering."""
|
||||
|
||||
def test_ds_add_rtn_sequence(self):
|
||||
"""DS_ADD_RTN returns correct old values in sequence."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
v_mov_b32_e32(v[0], 100),
|
||||
DS(DSOp.DS_STORE_B32, addr=v[10], data0=v[0], vdst=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[1], 25),
|
||||
DS(DSOp.DS_ADD_RTN_U32, addr=v[10], data0=v[1], vdst=v[2], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_ADD_RTN_U32, addr=v[10], data0=v[1], vdst=v[3], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
DS(DSOp.DS_LOAD_B32, addr=v[10], vdst=v[4], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 100, "First add should return 100")
|
||||
self.assertEqual(st.vgpr[0][3], 125, "Second add should return 125")
|
||||
self.assertEqual(st.vgpr[0][4], 150, "Final value should be 150")
|
||||
|
||||
|
||||
class TestDsPermute(unittest.TestCase):
|
||||
"""Tests for DS_PERMUTE_B32 and DS_BPERMUTE_B32 instructions."""
|
||||
|
||||
def test_ds_permute_b32_identity(self):
|
||||
"""DS_PERMUTE_B32 with identity permutation (lane 0 sends to lane 0)."""
|
||||
# For simplicity, test with single lane
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # addr = 0 (lane 0)
|
||||
v_mov_b32_e32(v[1], 0xDEADBEEF), # data
|
||||
ds_permute_b32(v[2], v[0], v[1]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Lane 0 sends to lane 0, so lane 0 gets 0xDEADBEEF
|
||||
self.assertEqual(st.vgpr[0][2], 0xDEADBEEF)
|
||||
|
||||
def test_ds_bpermute_b32_identity(self):
|
||||
"""DS_BPERMUTE_B32 with identity permutation (each lane reads from itself)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # addr = 0 (read from lane 0)
|
||||
v_mov_b32_e32(v[1], 0xCAFEBABE), # data in lane 0
|
||||
ds_bpermute_b32(v[2], v[0], v[1]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Lane 0 reads from lane 0's v[1]
|
||||
self.assertEqual(st.vgpr[0][2], 0xCAFEBABE)
|
||||
|
||||
def test_ds_permute_b32_broadcast(self):
|
||||
"""DS_PERMUTE_B32 broadcast - all lanes send to lane 0."""
|
||||
# With 4 lanes, all sending to lane 0, highest lane wins
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # All lanes send to addr 0 (lane 0)
|
||||
v_mov_b32_e32(v[1], 0x11111111), # All lanes send same data
|
||||
ds_permute_b32(v[2], v[0], v[1]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# Lane 0 receives data (highest numbered active lane wins)
|
||||
self.assertEqual(st.vgpr[0][2], 0x11111111)
|
||||
|
||||
def test_ds_bpermute_b32_xor_swap(self):
|
||||
"""DS_BPERMUTE_B32 with XOR-1 pattern — each lane reads from lane^1.
|
||||
|
||||
This is the pattern used by warp_shfl_xor in flash attention for reduce_max/reduce_sum.
|
||||
Each lane has a unique value (lane_id + 100), and reads from the adjacent lane.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = (lane_id ^ 1) * 4 (byte offset for bpermute)
|
||||
v_xor_b32_e32(v[0], 1, v[255]),
|
||||
v_lshlrev_b32_e32(v[0], 2, v[0]),
|
||||
# v[1] = lane_id + 100 (unique per-lane value)
|
||||
s_mov_b32(s[0], 100),
|
||||
v_add_nc_u32_e32(v[1], s[0], v[255]),
|
||||
# ds_bpermute: v[2] = v[1] from lane (lane_id ^ 1)
|
||||
ds_bpermute_b32(vdst=v[2], addr=v[0], data0=v[1]),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
for lane in range(32):
|
||||
src_lane = lane ^ 1
|
||||
expected = src_lane + 100
|
||||
self.assertEqual(st.vgpr[lane][2], expected, f"lane {lane}: expected v[1] from lane {src_lane} = {expected}, got {st.vgpr[lane][2]}")
|
||||
class TestDSSubDword(unittest.TestCase):
|
||||
"""Tests for sub-dword DS operations (ds_store_b16, ds_store_b16_d16_hi)."""
|
||||
|
||||
def test_ds_store_b16_and_d16_hi(self):
|
||||
"""DS_STORE_B16 stores low 16 bits, DS_STORE_B16_D16_HI stores high 16 bits to adjacent LDS half-words."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0xBEEF1234),
|
||||
DS(DSOp.DS_STORE_B16, addr=v[0], data0=v[1], offset0=0),
|
||||
DS(DSOp.DS_STORE_B16_D16_HI, addr=v[0], data0=v[1], offset0=2),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(vdst=v[2], addr=v[0], offset0=0),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xBEEF1234, "lo=0x1234 at byte 0, hi=0xBEEF at byte 2")
|
||||
|
||||
|
||||
class TestDSLargeOffset(unittest.TestCase):
|
||||
"""Tests for DS instructions with offsets > 255 (offset1 > 0).
|
||||
|
||||
The DS offset is a 16-bit value encoded as (offset1 << 8) | offset0.
|
||||
These tests verify that offset1 is used correctly, not just offset0.
|
||||
"""
|
||||
|
||||
def test_ds_store_load_b32_offset_256(self):
|
||||
"""DS_STORE_B32/DS_LOAD_B32 with offset=256 (offset0=0, offset1=1)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0, offset1=1), # offset = 256
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[1], offset0=0, offset1=1), # offset = 256
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0xDEADBEEF)
|
||||
|
||||
def test_ds_store_load_b32_offset_300(self):
|
||||
"""DS_STORE_B32/DS_LOAD_B32 with offset=300 (offset0=44, offset1=1)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=44, offset1=1), # offset = 300
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[1], offset0=44, offset1=1), # offset = 300
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0xCAFEBABE)
|
||||
|
||||
def test_ds_store_load_b64_offset_512(self):
|
||||
"""DS_STORE_B64/DS_LOAD_B64 with offset=512 (offset0=0, offset1=2)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
ds_store_b64(addr=v[10], data0=v[0:1], offset0=0, offset1=2), # offset = 512
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b64(addr=v[10], vdst=v[2:3], offset0=0, offset1=2), # offset = 512
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x11111111)
|
||||
self.assertEqual(st.vgpr[0][3], 0x22222222)
|
||||
|
||||
def test_ds_large_offset_distinct_from_small(self):
|
||||
"""Verify offset=256 and offset=0 address different LDS locations."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
# Store 0xAAAAAAAA at offset=0, 0xBBBBBBBB at offset=256
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=0, offset1=0), # offset = 0
|
||||
ds_store_b32(addr=v[10], data0=v[1], offset0=0, offset1=1), # offset = 256
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Read back both
|
||||
ds_load_b32(addr=v[10], vdst=v[2], offset0=0, offset1=0), # offset = 0
|
||||
ds_load_b32(addr=v[10], vdst=v[3], offset0=0, offset1=1), # offset = 256
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAAAAAAAA, "offset=0 should read 0xAAAAAAAA")
|
||||
self.assertEqual(st.vgpr[0][3], 0xBBBBBBBB, "offset=256 should read 0xBBBBBBBB")
|
||||
|
||||
def test_ds_store_load_b32_offset_448(self):
|
||||
"""DS_STORE_B32/DS_LOAD_B32 with offset=448 (offset0=192, offset1=1) - matches matmul B tile."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0x12345678),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
ds_store_b32(addr=v[10], data0=v[0], offset0=192, offset1=1), # offset = 448
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b32(addr=v[10], vdst=v[1], offset0=192, offset1=1), # offset = 448
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x12345678)
|
||||
|
||||
def test_ds_load_b64_offset_392(self):
|
||||
"""DS_LOAD_B64 with offset=392 (offset0=136, offset1=1) - matches matmul B tile load."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], 0),
|
||||
s_mov_b32(s[0], 0xAABBCCDD),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[0], 0x11223344),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
ds_store_b64(addr=v[10], data0=v[0:1], offset0=136, offset1=1), # offset = 392
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
ds_load_b64(addr=v[10], vdst=v[2:3], offset0=136, offset1=1), # offset = 392
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xAABBCCDD)
|
||||
self.assertEqual(st.vgpr[0][3], 0x11223344)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
363
tinygrad_repo/test/amd/hw/test_flat.py
Normal file
363
tinygrad_repo/test/amd/hw/test_flat.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""Tests for FLAT instructions - flat memory operations.
|
||||
|
||||
Includes: flat_load_*, flat_store_*, flat_atomic_*
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestFlatAtomic(unittest.TestCase):
|
||||
"""Tests for FLAT atomic instructions."""
|
||||
|
||||
def _make_test(self, setup_instrs, atomic_instr, check_fn, test_offset=2000):
|
||||
"""Helper to create atomic test instructions."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
] + setup_instrs + [atomic_instr, s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
check_fn(st)
|
||||
|
||||
def test_flat_atomic_add_u32(self):
|
||||
"""FLAT_ATOMIC_ADD_U32 adds to memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 50),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 100)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_swap_b32(self):
|
||||
"""FLAT_ATOMIC_SWAP_B32 swaps memory value and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_and_b32(self):
|
||||
"""FLAT_ATOMIC_AND_B32 ANDs with memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0xFF00FF00),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xFFFF0000),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_AND_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 0xFF00FF00)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_or_b32(self):
|
||||
"""FLAT_ATOMIC_OR_B32 ORs with memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0x00FF0000),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0x0000FF00),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_OR_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 0x00FF0000)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_inc_u32(self):
|
||||
"""FLAT_ATOMIC_INC_U32 increments and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 10),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 100), # threshold
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 10)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_dec_u32(self):
|
||||
"""FLAT_ATOMIC_DEC_U32 decrements and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 10),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_DEC_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 10)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_sub_u32(self):
|
||||
"""FLAT_ATOMIC_SUB_U32 subtracts from memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 30),
|
||||
v_mov_b32_e32(v[3], s[0]), # sub 30
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_SUB_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 100, "v4 should have old value (100)")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_xor_b32(self):
|
||||
"""FLAT_ATOMIC_XOR_B32 XORs with memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[3], s[0]), # XOR mask
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_XOR_B32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 0xAAAAAAAA, "v4 should have old value")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_min_u32(self):
|
||||
"""FLAT_ATOMIC_MIN_U32 stores min and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 50),
|
||||
v_mov_b32_e32(v[3], s[0]), # compare value (smaller)
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_MIN_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 100, "v4 should have old value (100)")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_max_u32(self):
|
||||
"""FLAT_ATOMIC_MAX_U32 stores max and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 50),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[3], s[0]), # compare value (larger)
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_MAX_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 50, "v4 should have old value (50)")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_inc_u64_returns_old_value(self):
|
||||
"""FLAT_ATOMIC_INC_U64 should return full 64-bit old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
# Store initial 64-bit value: 0xCAFEBABE_DEADBEEF
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Threshold: 0xFFFFFFFF_FFFFFFFF
|
||||
s_mov_b32(s[0], 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_INC_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][6], 0xDEADBEEF, "v6 should have old value low dword")
|
||||
self.assertEqual(st.vgpr[0][7], 0xCAFEBABE, "v7 should have old value high dword")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_add_u64(self):
|
||||
"""FLAT_ATOMIC_ADD_U64 adds 64-bit value and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0x00000001), # add 1
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
s_mov_b32(s[0], 0x00000000),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_ADD_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][6], 0x11111111, "v6 should have old value low")
|
||||
self.assertEqual(st.vgpr[0][7], 0x22222222, "v7 should have old value high")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_flat_atomic_swap_b64(self):
|
||||
"""FLAT_ATOMIC_SWAP_B64 swaps 64-bit value and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
s_mov_b32(s[0], 0xDDDDDDDD),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
]
|
||||
atomic = FLAT(FLATOp.FLAT_ATOMIC_SWAP_B64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][6], 0xAAAAAAAA, "v6 should have old value low")
|
||||
self.assertEqual(st.vgpr[0][7], 0xBBBBBBBB, "v7 should have old value high")
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
|
||||
class TestFlatLoad(unittest.TestCase):
|
||||
"""Tests for FLAT load instructions."""
|
||||
|
||||
def test_flat_load_b32(self):
|
||||
"""FLAT_LOAD_B32 loads 32-bit value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
FLAT(FLATOp.FLAT_LOAD_B32, addr=v[0:1], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF)
|
||||
|
||||
def test_flat_load_b64(self):
|
||||
"""FLAT_LOAD_B64 loads 64-bit value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
FLAT(FLATOp.FLAT_LOAD_B64, addr=v[0:1], vdst=v[4:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][4], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][5], 0xCAFEBABE)
|
||||
|
||||
def test_flat_load_b96(self):
|
||||
"""FLAT_LOAD_B96 loads 96-bit (3 dword) value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
s_mov_b32(s[0], 0x33333333),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
global_store_b96(addr=v[0:1], data=v[2:4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
FLAT(FLATOp.FLAT_LOAD_B96, addr=v[0:1], vdst=v[5:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][5], 0x11111111)
|
||||
self.assertEqual(st.vgpr[0][6], 0x22222222)
|
||||
self.assertEqual(st.vgpr[0][7], 0x33333333)
|
||||
|
||||
def test_flat_load_b128(self):
|
||||
"""FLAT_LOAD_B128 loads 128-bit value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
s_mov_b32(s[0], 0x33333333),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
s_mov_b32(s[0], 0x44444444),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
global_store_b128(addr=v[0:1], data=v[2:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
FLAT(FLATOp.FLAT_LOAD_B128, addr=v[0:1], vdst=v[6:9], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][6], 0x11111111)
|
||||
self.assertEqual(st.vgpr[0][7], 0x22222222)
|
||||
self.assertEqual(st.vgpr[0][8], 0x33333333)
|
||||
self.assertEqual(st.vgpr[0][9], 0x44444444)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
679
tinygrad_repo/test/amd/hw/test_global.py
Normal file
679
tinygrad_repo/test/amd/hw/test_global.py
Normal file
@@ -0,0 +1,679 @@
|
||||
"""Tests for GLOBAL instructions - global memory operations.
|
||||
|
||||
Includes: global_load_*, global_store_*, global_atomic_*, global_load_d16_*
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestGlobalAtomic(unittest.TestCase):
|
||||
"""Tests for GLOBAL atomic instructions."""
|
||||
|
||||
def _make_test(self, setup_instrs, atomic_instr, check_fn, test_offset=2000):
|
||||
"""Helper to create atomic test instructions."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
] + setup_instrs + [atomic_instr, s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
check_fn(st)
|
||||
|
||||
def test_global_atomic_add_u32(self):
|
||||
"""GLOBAL_ATOMIC_ADD_U32 adds to memory and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 100),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 50),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
]
|
||||
atomic = GLOBAL(GLOBALOp.GLOBAL_ATOMIC_ADD_U32, addr=v[0:1], data=v[3], vdst=v[4], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][4], 100)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
def test_global_atomic_add_u64(self):
|
||||
"""GLOBAL_ATOMIC_ADD_U64 adds 64-bit value and returns old value."""
|
||||
TEST_OFFSET = 2000
|
||||
setup = [
|
||||
s_mov_b32(s[0], 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0x00000000),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b64(addr=v[0:1], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[0], 0x00000001),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
s_mov_b32(s[0], 0x00000000),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
]
|
||||
atomic = GLOBAL(GLOBALOp.GLOBAL_ATOMIC_ADD_U64, addr=v[0:1], data=v[4:5], vdst=v[6:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET, glc=1)
|
||||
def check(st):
|
||||
self.assertEqual(st.vgpr[0][6], 0xFFFFFFFF)
|
||||
self.assertEqual(st.vgpr[0][7], 0x00000000)
|
||||
self._make_test(setup, atomic, check, TEST_OFFSET)
|
||||
|
||||
|
||||
class TestGlobalLoad(unittest.TestCase):
|
||||
"""Tests for GLOBAL load instructions."""
|
||||
|
||||
def test_global_load_b96(self):
|
||||
"""GLOBAL_LOAD_B96 loads 96-bit value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
s_mov_b32(s[0], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
global_store_b96(addr=v[0:1], data=v[2:4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B96, addr=v[0:1], vdst=v[5:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][5], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][6], 0xBBBBBBBB)
|
||||
self.assertEqual(st.vgpr[0][7], 0xCCCCCCCC)
|
||||
|
||||
def test_global_load_b128(self):
|
||||
"""GLOBAL_LOAD_B128 loads 128-bit value correctly."""
|
||||
TEST_OFFSET = 2000
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[0], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
s_mov_b32(s[0], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
s_mov_b32(s[0], 0x12345678),
|
||||
v_mov_b32_e32(v[4], s[0]),
|
||||
s_mov_b32(s[0], 0x9ABCDEF0),
|
||||
v_mov_b32_e32(v[5], s[0]),
|
||||
global_store_b128(addr=v[0:1], data=v[2:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B128, addr=v[0:1], vdst=v[6:9], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][6], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][7], 0xCAFEBABE)
|
||||
self.assertEqual(st.vgpr[0][8], 0x12345678)
|
||||
self.assertEqual(st.vgpr[0][9], 0x9ABCDEF0)
|
||||
|
||||
|
||||
class TestGlobalStore(unittest.TestCase):
|
||||
"""Tests for GLOBAL store instructions."""
|
||||
|
||||
def test_global_store_b8_basic(self):
|
||||
"""GLOBAL_STORE_B8 stores a single byte from VDATA[7:0]."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# First store 0xDEADBEEF to memory
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Now store single byte 0x42 to same address (should only change byte 0)
|
||||
v_mov_b32_e32(v[2], 0x42),
|
||||
global_store_b8(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Read back and check
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Only byte 0 should change from 0xEF to 0x42
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBE42, "Only byte 0 should be modified")
|
||||
|
||||
def test_global_store_b8_byte1(self):
|
||||
"""GLOBAL_STORE_B8 at offset+1 stores to byte 1."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[2], 0x42),
|
||||
global_store_b8(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+1),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEAD42EF, "Only byte 1 should be modified")
|
||||
|
||||
def test_global_store_b16_basic(self):
|
||||
"""GLOBAL_STORE_B16 stores a 16-bit value from VDATA[15:0]."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADCAFE, "Only lower 16 bits should be modified")
|
||||
|
||||
def test_global_store_b16_high_half(self):
|
||||
"""GLOBAL_STORE_B16 at offset+2 stores to high 16 bits."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+2),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xCAFEBEEF, "Only upper 16 bits should be modified")
|
||||
|
||||
def test_global_store_b16_byte_offset_1(self):
|
||||
"""GLOBAL_STORE_B16 at byte offset 1 stores bytes 1-2 within the same word."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDDCCBBAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Store 0xBEEF at byte offset 1 (bytes 1-2)
|
||||
s_mov_b32(s[4], 0xBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+1),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Bytes 1-2 should be 0xBEEF (0xEF at byte 1, 0xBE at byte 2)
|
||||
# Original: 0xDDCCBBAA -> bytes [AA, BB, CC, DD]
|
||||
# After: 0xDDBEEFAA -> bytes [AA, EF, BE, DD]
|
||||
self.assertEqual(st.vgpr[0][0], 0xDDBEEFAA, "Bytes 1-2 should be 0xBEEF")
|
||||
|
||||
def test_global_store_b16_cross_word_boundary(self):
|
||||
"""GLOBAL_STORE_B16 at byte offset 3 crosses word boundary (byte 3 of word N, byte 0 of word N+1)."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Initialize two consecutive words
|
||||
s_mov_b32(s[4], 0xDDCCBBAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_mov_b32(s[4], 0x44332211),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Store 0xBEEF at byte offset 3 (crosses word boundary)
|
||||
# Low byte (0xEF) goes to byte 3 of first word
|
||||
# High byte (0xBE) goes to byte 0 of second word
|
||||
s_mov_b32(s[4], 0xBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b16(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+3),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back both words
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[4], data=v[4], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
v_mov_b32_e32(v[1], v[4]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# First word: 0xDDCCBBAA -> 0xEFCCBBAA (byte 3 becomes 0xEF)
|
||||
# Second word: 0x44332211 -> 0x443322BE (byte 0 becomes 0xBE)
|
||||
self.assertEqual(st.vgpr[0][0], 0xEFCCBBAA, "Byte 3 of first word should be 0xEF")
|
||||
self.assertEqual(st.vgpr[0][1], 0x443322BE, "Byte 0 of second word should be 0xBE")
|
||||
|
||||
def test_global_store_b64_basic(self):
|
||||
"""GLOBAL_STORE_B64 stores 8 bytes from v[n:n+1] to memory."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
s_mov_b32(s[5], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], s[5]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b64(addr=v[0], data=v[2:3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B64, addr=v[0], vdst=v[4:5], data=v[4:5], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[4]),
|
||||
v_mov_b32_e32(v[1], v[5]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][1], 0xCAFEBABE)
|
||||
|
||||
|
||||
class TestD16HiLoads(unittest.TestCase):
|
||||
"""Tests for D16_HI load instructions that load into high 16 bits."""
|
||||
|
||||
def test_global_load_d16_hi_b16_preserves_low_bits(self):
|
||||
"""GLOBAL_LOAD_D16_HI_B16 must preserve low 16 bits of destination."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b16(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x0000BEEF),
|
||||
v_mov_b32_e32(v[3], s[4]),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[0:1], vdst=v[3], data=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
self.assertEqual(result, 0xCAFEBEEF, f"Expected 0xCAFEBEEF, got 0x{result:08x}")
|
||||
|
||||
def test_global_load_d16_hi_b16_data_differs_from_vdst(self):
|
||||
"""GLOBAL_LOAD_D16_HI_B16 where data field differs from vdst."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
global_store_b16(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x0000DEAD),
|
||||
v_mov_b32_e32(v[0], s[4]), # data field - should NOT affect result
|
||||
v_mov_b32_e32(v[1], 0), # vdst - low bits should be preserved
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[1]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
self.assertEqual(result, 0xCAFE0000, f"Expected 0xCAFE0000, got 0x{result:08x}")
|
||||
|
||||
def test_global_load_d16_hi_u8_data_differs_from_vdst(self):
|
||||
"""GLOBAL_LOAD_D16_HI_U8 where data field differs from vdst."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xAB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
global_store_b8(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x0000DEAD),
|
||||
v_mov_b32_e32(v[4], s[4]), # data field
|
||||
s_mov_b32(s[4], 0x0000BEEF),
|
||||
v_mov_b32_e32(v[5], s[4]), # vdst
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_U8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[5]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
self.assertEqual(result, 0x00ABBEEF, f"Expected 0x00ABBEEF, got 0x{result:08x}")
|
||||
|
||||
def test_global_load_d16_hi_b16_same_addr_and_dst_zero_addr(self):
|
||||
"""GLOBAL_LOAD_D16_HI_B16 with same register for addr and vdst, addr value=0."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
global_store_b16(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[1]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
self.assertEqual(result, 0xCAFE0000, f"Expected 0xCAFE0000, got 0x{result:08x}")
|
||||
|
||||
def test_global_load_d16_hi_b16_tril_exact_pattern(self):
|
||||
"""Exact pattern from tril() failure: data=v0 differs from vdst=v1."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x01010101),
|
||||
v_mov_b32_e32(v[10], s[4]),
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
global_store_b32(addr=v[3], data=v[10], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[3], data=v[10], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Set v[0] to 0x0101 (simulating prior u16 load result)
|
||||
s_mov_b32(s[4], 0x0101),
|
||||
v_mov_b32_e32(v[0], s[4]),
|
||||
# Set v[1] to 0
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
# Load using v[1] as addr AND vdst, but v[0] as data
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[0], saddr=s[2:3], offset=TEST_OFFSET+6),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[1]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
# Expected: hi=0x0101 (loaded), lo=0x0000 (from v1) -> 0x01010000
|
||||
self.assertEqual(result, 0x01010000, f"Expected 0x01010000, got 0x{result:08x}")
|
||||
|
||||
def test_global_load_d16_hi_i8_data_differs_from_vdst(self):
|
||||
"""GLOBAL_LOAD_D16_HI_I8 where data field differs from vdst."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x80), # negative signed byte = -128
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
global_store_b8(addr=v[3], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x0000DEAD),
|
||||
v_mov_b32_e32(v[4], s[4]), # data field
|
||||
s_mov_b32(s[4], 0x0000BEEF),
|
||||
v_mov_b32_e32(v[5], s[4]), # vdst
|
||||
v_mov_b32_e32(v[3], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_I8, addr=v[3], vdst=v[5], data=v[4], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[5]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][0]
|
||||
# 0x80 sign-extended = 0xFF80, lo=0xBEEF -> 0xFF80BEEF
|
||||
self.assertEqual(result, 0xFF80BEEF, f"Expected 0xFF80BEEF, got 0x{result:08x}")
|
||||
|
||||
def test_global_store_b64_tril_pattern(self):
|
||||
"""Test the exact pattern from tril() kernel that was failing."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0x01010101),
|
||||
v_mov_b32_e32(v[10], s[4]),
|
||||
v_mov_b32_e32(v[11], s[4]),
|
||||
s_mov_b32(s[4], 0x01),
|
||||
v_mov_b32_e32(v[12], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b64(addr=v[0], data=v[10:11], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b8(addr=v[0], data=v[12], saddr=s[2:3], offset=TEST_OFFSET+8),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
|
||||
v_mov_b32_e32(v[2], 0),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_U16, addr=v[2], vdst=v[0], data=v[0], saddr=s[2:3], offset=TEST_OFFSET+3),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_D16_HI_B16, addr=v[1], vdst=v[1], data=v[1], saddr=s[2:3], offset=TEST_OFFSET+6),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[3], data=v[3], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_U8, addr=v[2], vdst=v[4], data=v[4], saddr=s[2:3], offset=TEST_OFFSET+8),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
|
||||
v_and_b32_e32(v[5], 0xffff, v[0]),
|
||||
v_lshlrev_b32_e32(v[0], 24, v[0]),
|
||||
v_lshrrev_b32_e32(v[5], 8, v[5]),
|
||||
v_or_b32_e32(v[0], v[3], v[0]),
|
||||
v_or_b32_e32(v[1], v[5], v[1]),
|
||||
|
||||
global_store_b64(addr=v[2], data=v[0:1], saddr=s[2:3], offset=TEST_OFFSET+16),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B64, addr=v[2], vdst=v[6:7], data=v[6:7], saddr=s[2:3], offset=TEST_OFFSET+16),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[6]),
|
||||
v_mov_b32_e32(v[1], v[7]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
|
||||
v0 = st.vgpr[0][0]
|
||||
v1 = st.vgpr[0][1]
|
||||
self.assertEqual(v0, 0x01000001, f"v0: expected 0x01000001, got 0x{v0:08x}")
|
||||
self.assertEqual(v1, 0x01010001, f"v1: expected 0x01010001, got 0x{v1:08x}")
|
||||
|
||||
byte5 = (v1 >> 8) & 0xff
|
||||
self.assertEqual(byte5, 0x00, f"byte5: expected 0x00, got 0x{byte5:02x}")
|
||||
|
||||
|
||||
class TestGlobalOffset(unittest.TestCase):
|
||||
"""Tests for GLOBAL instructions with different offsets.
|
||||
|
||||
These tests verify that instruction deduplication correctly handles different offset values.
|
||||
If offset is made dynamic incorrectly, instructions with different offsets may load/store wrong data.
|
||||
"""
|
||||
|
||||
def test_global_load_different_offsets(self):
|
||||
"""Load from two different offsets and verify correct values."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
# Store 0xAAAAAAAA at offset 100
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=100),
|
||||
# Store 0xBBBBBBBB at offset 200
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load from offset 100 -> should get 0xAAAAAAAA
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0:1], vdst=v[3], saddr=SrcEnum.NULL, offset=100),
|
||||
# Load from offset 200 -> should get 0xBBBBBBBB
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0:1], vdst=v[4], saddr=SrcEnum.NULL, offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
v_mov_b32_e32(v[1], v[4]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAAAAAAAA, f"offset 100: expected 0xAAAAAAAA, got 0x{st.vgpr[0][0]:08x}")
|
||||
self.assertEqual(st.vgpr[0][1], 0xBBBBBBBB, f"offset 200: expected 0xBBBBBBBB, got 0x{st.vgpr[0][1]:08x}")
|
||||
|
||||
def test_global_store_different_offsets(self):
|
||||
"""Store to two different offsets and verify correct values."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
# Store 0x11111111 at offset 300
|
||||
s_mov_b32(s[0], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=300),
|
||||
# Store 0x22222222 at offset 400
|
||||
s_mov_b32(s[0], 0x22222222),
|
||||
v_mov_b32_e32(v[3], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[3], saddr=SrcEnum.NULL, offset=400),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back to verify
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0:1], vdst=v[4], saddr=SrcEnum.NULL, offset=300),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0:1], vdst=v[5], saddr=SrcEnum.NULL, offset=400),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[4]),
|
||||
v_mov_b32_e32(v[1], v[5]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0x11111111, f"offset 300: expected 0x11111111, got 0x{st.vgpr[0][0]:08x}")
|
||||
self.assertEqual(st.vgpr[0][1], 0x22222222, f"offset 400: expected 0x22222222, got 0x{st.vgpr[0][1]:08x}")
|
||||
|
||||
def test_global_negative_offset_no_saddr(self):
|
||||
"""Test negative offset without saddr (VGPR pair for address).
|
||||
Store 0xAAAA at offset 100, 0xBBBB at offset 200.
|
||||
Load with offset -100 from vaddr pointing to base+200 -> should get 0xAAAA (at 100).
|
||||
Load with offset -100 from vaddr pointing to base+300 -> should get 0xBBBB (at 200)."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
v_mov_b32_e32(v[1], s[3]),
|
||||
# Store 0xAAAAAAAA at offset 100, 0xBBBBBBBB at offset 200
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=100),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0:1], data=v[2], saddr=SrcEnum.NULL, offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# vaddr = base+200, load with offset -100 -> should get value at 100
|
||||
s_add_u32(s[4], s[2], 200),
|
||||
s_addc_u32(s[5], s[3], 0),
|
||||
v_mov_b32_e32(v[4], s[4]),
|
||||
v_mov_b32_e32(v[5], s[5]),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[4:5], vdst=v[6], saddr=SrcEnum.NULL, offset=-100),
|
||||
# vaddr = base+300, load with offset -100 -> should get value at 200
|
||||
s_add_u32(s[4], s[2], 300),
|
||||
s_addc_u32(s[5], s[3], 0),
|
||||
v_mov_b32_e32(v[4], s[4]),
|
||||
v_mov_b32_e32(v[5], s[5]),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[4:5], vdst=v[7], saddr=SrcEnum.NULL, offset=-100),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[6]),
|
||||
v_mov_b32_e32(v[1], v[7]),
|
||||
v_mov_b32_e32(v[4], 0),
|
||||
v_mov_b32_e32(v[5], 0),
|
||||
v_mov_b32_e32(v[6], 0),
|
||||
v_mov_b32_e32(v[7], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
s_mov_b32(s[4], 0),
|
||||
s_mov_b32(s[5], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAAAAAAAA, f"offset 200-100=100: expected 0xAAAAAAAA, got 0x{st.vgpr[0][0]:08x}")
|
||||
self.assertEqual(st.vgpr[0][1], 0xBBBBBBBB, f"offset 300-100=200: expected 0xBBBBBBBB, got 0x{st.vgpr[0][1]:08x}")
|
||||
|
||||
def test_global_negative_offset_with_saddr(self):
|
||||
"""Test negative offset with saddr (SGPR pair for base address).
|
||||
Store 0xAAAA at offset 100, 0xBBBB at offset 200.
|
||||
Load with offset -100 from saddr pointing to base+200 -> should get 0xAAAA (at 100).
|
||||
Load with offset -100 from saddr pointing to base+300 -> should get 0xBBBB (at 200)."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store 0xAAAAAAAA at offset 100, 0xBBBBBBBB at offset 200
|
||||
s_mov_b32(s[0], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=100),
|
||||
s_mov_b32(s[0], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[0]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# saddr = base+200, load with offset -100 -> should get value at 100
|
||||
s_add_u32(s[4], s[2], 200),
|
||||
s_addc_u32(s[5], s[3], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[6], saddr=s[4:5], offset=-100),
|
||||
# saddr = base+300, load with offset -100 -> should get value at 200
|
||||
s_add_u32(s[4], s[2], 300),
|
||||
s_addc_u32(s[5], s[3], 0),
|
||||
GLOBAL(GLOBALOp.GLOBAL_LOAD_B32, addr=v[0], vdst=v[7], saddr=s[4:5], offset=-100),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[6]),
|
||||
v_mov_b32_e32(v[1], v[7]),
|
||||
v_mov_b32_e32(v[6], 0),
|
||||
v_mov_b32_e32(v[7], 0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
s_mov_b32(s[4], 0),
|
||||
s_mov_b32(s[5], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAAAAAAAA, f"offset 200-100=100: expected 0xAAAAAAAA, got 0x{st.vgpr[0][0]:08x}")
|
||||
self.assertEqual(st.vgpr[0][1], 0xBBBBBBBB, f"offset 300-100=200: expected 0xBBBBBBBB, got 0x{st.vgpr[0][1]:08x}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
129
tinygrad_repo/test/amd/hw/test_rdna4_permlane_var.py
Normal file
129
tinygrad_repo/test/amd/hw/test_rdna4_permlane_var.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""RDNA4 V_PERMLANE16_VAR_B32 / V_PERMLANEX16_VAR_B32 coverage.
|
||||
|
||||
Exercises the generated pcode path end-to-end in the emulator and compares against
|
||||
real RDNA4 hardware when USE_HW=1.
|
||||
"""
|
||||
import ctypes, unittest
|
||||
import tinygrad.runtime.autogen.amd.rdna4.ins as r4
|
||||
from tinygrad.helpers import flat_mv
|
||||
from tinygrad.renderer.amd.dsl import NULL
|
||||
from test.amd.hw.helpers import USE_HW, assemble
|
||||
from test.mockgpu.amd.emu import run_asm
|
||||
|
||||
LANES = 32
|
||||
|
||||
def _code(instructions: list, out_reg: int = 2) -> bytes:
|
||||
return assemble([
|
||||
r4.s_mov_b32(r4.s[80], r4.s[0]),
|
||||
r4.s_mov_b32(r4.s[81], r4.s[1]),
|
||||
r4.v_mov_b32_e32(r4.v[255], r4.v[0]),
|
||||
*instructions,
|
||||
r4.s_load_b64(r4.s[92:93], r4.s[80:81], soffset=NULL),
|
||||
r4.s_wait_kmcnt(simm16=0),
|
||||
r4.v_lshlrev_b32_e32(r4.v[240], 2, r4.v[255]),
|
||||
r4.v_mov_b32_e32(r4.v[241], 0),
|
||||
r4.global_store_b32(vaddr=r4.v[240:241], saddr=r4.s[92:93], vsrc=r4.v[out_reg]),
|
||||
r4.s_endpgm(),
|
||||
])
|
||||
|
||||
def _run_emu(instructions: list, out_reg: int = 2) -> list[int]:
|
||||
out_buf = (ctypes.c_uint32 * LANES)(*([0] * LANES))
|
||||
args = (ctypes.c_uint64 * 1)(ctypes.addressof(out_buf))
|
||||
code = _code(instructions, out_reg)
|
||||
kernel_buf = (ctypes.c_char * len(code)).from_buffer_copy(code)
|
||||
result = run_asm(ctypes.addressof(kernel_buf), len(code), 1, 1, 1, LANES, 1, 1, ctypes.addressof(args), arch='rdna4')
|
||||
assert result == 0, f"run_asm failed with {result}"
|
||||
return list(out_buf)
|
||||
|
||||
def _run_hw(instructions: list, out_reg: int = 2) -> list[int]:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.runtime.ops_amd import AMDProgram
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCompiler
|
||||
|
||||
dev = Device['AMD']
|
||||
if not dev.arch.startswith('gfx12'): raise unittest.SkipTest('requires RDNA4 hardware')
|
||||
compiler = HIPCompiler(dev.arch)
|
||||
code = _code(instructions, out_reg)
|
||||
byte_str = ', '.join(f'0x{b:02x}' for b in code)
|
||||
asm_src = f""".text
|
||||
.globl test
|
||||
.p2align 8
|
||||
.type test,@function
|
||||
test:
|
||||
.byte {byte_str}
|
||||
|
||||
.rodata
|
||||
.p2align 6
|
||||
.amdhsa_kernel test
|
||||
.amdhsa_next_free_vgpr 256
|
||||
.amdhsa_next_free_sgpr 96
|
||||
.amdhsa_wavefront_size32 1
|
||||
.amdhsa_user_sgpr_kernarg_segment_ptr 1
|
||||
.amdhsa_kernarg_size 8
|
||||
.amdhsa_group_segment_fixed_size 65536
|
||||
.amdhsa_private_segment_fixed_size 65536
|
||||
.amdhsa_enable_private_segment 1
|
||||
.end_amdhsa_kernel
|
||||
|
||||
.amdgpu_metadata
|
||||
---
|
||||
amdhsa.version:
|
||||
- 1
|
||||
- 0
|
||||
amdhsa.kernels:
|
||||
- .name: test
|
||||
.symbol: test.kd
|
||||
.kernarg_segment_size: 8
|
||||
.group_segment_fixed_size: 65536
|
||||
.private_segment_fixed_size: 65536
|
||||
.kernarg_segment_align: 8
|
||||
.wavefront_size: 32
|
||||
.sgpr_count: 96
|
||||
.vgpr_count: 256
|
||||
.max_flat_workgroup_size: 1024
|
||||
...
|
||||
.end_amdgpu_metadata
|
||||
"""
|
||||
lib = compiler.compile(asm_src)
|
||||
prg = AMDProgram(dev, 'test', lib)
|
||||
out_gpu = dev.allocator.alloc(LANES * 4)
|
||||
prg(out_gpu, global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
|
||||
out = bytearray(LANES * 4)
|
||||
dev.allocator._copyout(flat_mv(memoryview(out)), out_gpu)
|
||||
return [int.from_bytes(out[i*4:(i+1)*4], 'little') for i in range(LANES)]
|
||||
|
||||
def run_rdna4(instructions: list, out_reg: int = 2) -> list[int]:
|
||||
emu = _run_emu(instructions, out_reg)
|
||||
if not USE_HW: return emu
|
||||
hw = _run_hw(instructions, out_reg)
|
||||
if emu != hw:
|
||||
diffs = [f"lane {i}: emu=0x{e:08x} hw=0x{h:08x}" for i, (e, h) in enumerate(zip(emu, hw)) if e != h]
|
||||
raise AssertionError("Emulator vs Hardware mismatch:\n" + '\n'.join(diffs[:16]))
|
||||
return hw
|
||||
|
||||
class TestPermlaneVarRDNA4(unittest.TestCase):
|
||||
def test_v_permlane16_var_b32_reverse(self):
|
||||
out = run_rdna4([
|
||||
r4.v_mov_b32_e32(r4.v[0], r4.v[255]),
|
||||
r4.v_xor_b32_e32(r4.v[1], 15, r4.v[255]),
|
||||
r4.v_permlane16_var_b32(r4.v[2], r4.v[0], r4.v[1]),
|
||||
])
|
||||
self.assertEqual(out[0], 15)
|
||||
self.assertEqual(out[5], 10)
|
||||
self.assertEqual(out[15], 0)
|
||||
self.assertEqual(out[16], 31)
|
||||
self.assertEqual(out[21], 26)
|
||||
self.assertEqual(out[31], 16)
|
||||
|
||||
def test_v_permlanex16_var_b32_cross_row(self):
|
||||
out = run_rdna4([
|
||||
r4.v_mov_b32_e32(r4.v[0], r4.v[255]),
|
||||
r4.v_mov_b32_e32(r4.v[1], r4.v[255]),
|
||||
r4.v_permlanex16_var_b32(r4.v[2], r4.v[0], r4.v[1]),
|
||||
])
|
||||
self.assertEqual(out[0], 16)
|
||||
self.assertEqual(out[5], 21)
|
||||
self.assertEqual(out[15], 31)
|
||||
self.assertEqual(out[16], 0)
|
||||
self.assertEqual(out[21], 5)
|
||||
self.assertEqual(out[31], 15)
|
||||
355
tinygrad_repo/test/amd/hw/test_scratch.py
Normal file
355
tinygrad_repo/test/amd/hw/test_scratch.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""Tests for SCRATCH instructions - scratch (private) memory operations.
|
||||
|
||||
Includes: scratch_load_*, scratch_store_*
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestScratchStore(unittest.TestCase):
|
||||
"""Tests for SCRATCH store instructions."""
|
||||
|
||||
def test_scratch_store_b32_basic(self):
|
||||
"""SCRATCH_STORE_B32 stores 32-bit value to scratch memory."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store via scratch
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back via scratch
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBEEF)
|
||||
|
||||
def test_scratch_store_b64_basic(self):
|
||||
"""SCRATCH_STORE_B64 stores 64-bit value to scratch memory."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
s_mov_b32(s[5], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], s[5]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
scratch_store_b64(addr=v[0], data=v[2:3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_b64(addr=v[0], vdst=v[4:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[4]),
|
||||
v_mov_b32_e32(v[1], v[5]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][1], 0xCAFEBABE)
|
||||
|
||||
def test_scratch_store_b8_basic(self):
|
||||
"""SCRATCH_STORE_B8 stores single byte to scratch memory."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# First store full word
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Store single byte
|
||||
v_mov_b32_e32(v[2], 0x42),
|
||||
scratch_store_b8(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Only byte 0 should change from 0xEF to 0x42
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBE42)
|
||||
|
||||
def test_scratch_store_b16_basic(self):
|
||||
"""SCRATCH_STORE_B16 stores 16-bit value to scratch memory."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[4], 0xCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b16(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADCAFE)
|
||||
|
||||
|
||||
class TestScratchLoad(unittest.TestCase):
|
||||
"""Tests for SCRATCH load instructions."""
|
||||
|
||||
def test_scratch_load_b96(self):
|
||||
"""SCRATCH_LOAD_B96 loads 96-bit value correctly."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
s_mov_b32(s[4], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[3], s[4]),
|
||||
s_mov_b32(s[4], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[4], s[4]),
|
||||
scratch_store_b96(addr=v[0], data=v[2:4], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_b96(addr=v[0], vdst=v[5:7], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[5]),
|
||||
v_mov_b32_e32(v[1], v[6]),
|
||||
v_mov_b32_e32(v[2], v[7]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAAAAAAAA)
|
||||
self.assertEqual(st.vgpr[0][1], 0xBBBBBBBB)
|
||||
self.assertEqual(st.vgpr[0][2], 0xCCCCCCCC)
|
||||
|
||||
def test_scratch_load_b128(self):
|
||||
"""SCRATCH_LOAD_B128 loads 128-bit value correctly."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
s_mov_b32(s[4], 0xCAFEBABE),
|
||||
v_mov_b32_e32(v[3], s[4]),
|
||||
s_mov_b32(s[4], 0x12345678),
|
||||
v_mov_b32_e32(v[4], s[4]),
|
||||
s_mov_b32(s[4], 0x9ABCDEF0),
|
||||
v_mov_b32_e32(v[5], s[4]),
|
||||
scratch_store_b128(addr=v[0], data=v[2:5], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_b128(addr=v[0], vdst=v[6:9], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[6]),
|
||||
v_mov_b32_e32(v[1], v[7]),
|
||||
v_mov_b32_e32(v[2], v[8]),
|
||||
v_mov_b32_e32(v[3], v[9]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xDEADBEEF)
|
||||
self.assertEqual(st.vgpr[0][1], 0xCAFEBABE)
|
||||
self.assertEqual(st.vgpr[0][2], 0x12345678)
|
||||
self.assertEqual(st.vgpr[0][3], 0x9ABCDEF0)
|
||||
|
||||
def test_scratch_load_u8(self):
|
||||
"""SCRATCH_LOAD_U8 loads unsigned byte with zero extension."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xDEADBEAB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_u8(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAB)
|
||||
|
||||
def test_scratch_load_i8(self):
|
||||
"""SCRATCH_LOAD_I8 loads signed byte with sign extension."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0x80), # -128 as signed byte
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b8(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_i8(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xFFFFFF80)
|
||||
|
||||
def test_scratch_load_u16(self):
|
||||
"""SCRATCH_LOAD_U16 loads unsigned 16-bit with zero extension."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xDEADCAFE),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_u16(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xCAFE)
|
||||
|
||||
def test_scratch_load_i16(self):
|
||||
"""SCRATCH_LOAD_I16 loads signed 16-bit with sign extension."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0x8000), # -32768 as signed 16-bit
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b16(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
scratch_load_i16(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xFFFF8000)
|
||||
|
||||
|
||||
class TestScratchSVE(unittest.TestCase):
|
||||
"""Tests for SCRATCH SVE (Scratch VGPR Enable) bit behavior."""
|
||||
|
||||
def test_scratch_sve_zero_ignores_vaddr(self):
|
||||
"""With SVE=0, VADDR should be ignored in address calculation."""
|
||||
TEST_OFFSET = 256
|
||||
# Store a marker value at offset 256 (where SVE=0 should go)
|
||||
# Then set v[0] to a non-zero value (100) and store via scratch with SVE=0
|
||||
# If SVE=0 is handled correctly, the VADDR (100) should be IGNORED,
|
||||
# and the store should go to offset 256, not 256+100=356
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# First, store 0xAAAAAAAA at offset 256 with v[0]=0
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=0),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Now set v[0] to 100 (non-zero) and store 0xBBBBBBBB with SVE=0
|
||||
# With SVE=0, v[0] should be IGNORED, so this should overwrite offset 256
|
||||
v_mov_b32_e32(v[0], 100),
|
||||
s_mov_b32(s[4], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=0),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back from offset 256 (with v[0]=0) - should get 0xBBBBBBBB
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=0),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# If SVE=0 works correctly, v[0] should be 0xBBBBBBBB (the second store overwrote the first)
|
||||
# If SVE=0 is wrong (VADDR used), v[0] would be 0xAAAAAAAA (stores went to different locations)
|
||||
self.assertEqual(st.vgpr[0][0], 0xBBBBBBBB, "SVE=0 should ignore VADDR, both stores should go to same location")
|
||||
|
||||
def test_scratch_sve_one_uses_vaddr(self):
|
||||
"""With SVE=1, VADDR should be used as offset in address calculation."""
|
||||
TEST_OFFSET = 256
|
||||
# Store at offset 256 with v[0]=0, then store at offset 256 with v[0]=100 and SVE=1
|
||||
# With SVE=1, the second store should go to 256+100=356, not 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# First, store 0xAAAAAAAA at offset 256 with v[0]=0
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=1),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Now set v[0] to 100 and store 0xBBBBBBBB with SVE=1
|
||||
# With SVE=1, v[0] IS used, so this should go to offset 256+100=356
|
||||
v_mov_b32_e32(v[0], 100),
|
||||
s_mov_b32(s[4], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=1),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back from offset 256 (with v[0]=0) - should still be 0xAAAAAAAA
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET, sve=1),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# If SVE=1 works correctly, v[0] should be 0xAAAAAAAA (stores went to different locations)
|
||||
self.assertEqual(st.vgpr[0][0], 0xAAAAAAAA, "SVE=1 should use VADDR, stores should go to different locations")
|
||||
|
||||
|
||||
class TestScratchMultiLane(unittest.TestCase):
|
||||
"""Tests for SCRATCH operations with multiple lanes."""
|
||||
|
||||
def test_scratch_store_load_multi_lane(self):
|
||||
"""SCRATCH store/load works correctly with multiple lanes (private per-lane memory)."""
|
||||
TEST_OFFSET = 256
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=SrcEnum.NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Each lane stores its lane ID
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
v_mov_b32_e32(v[2], v[255]), # v[255] has packed workitem IDs, low 10 bits = x
|
||||
v_and_b32_e32(v[2], 0x3FF, v[2]), # extract lane ID
|
||||
scratch_store_b32(addr=v[0], data=v[2], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
# Load back
|
||||
scratch_load_b32(addr=v[0], vdst=v[3], saddr=SrcEnum.NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], v[3]),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# Each lane should have loaded its own lane ID
|
||||
for lane in range(4):
|
||||
self.assertEqual(st.vgpr[lane][0], lane, f"Lane {lane} should have value {lane}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
448
tinygrad_repo/test/amd/hw/test_smem.py
Normal file
448
tinygrad_repo/test/amd/hw/test_smem.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""Tests for SMEM instructions - scalar memory operations.
|
||||
|
||||
Includes: s_load_b32, s_load_b64, s_load_b128, s_load_b256, s_load_b512
|
||||
Tests both immediate and register offset addressing modes.
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
# Use offset into output buffer for test data (output buffer is 2124 bytes)
|
||||
TEST_OFFSET = 2000
|
||||
|
||||
# Cache invalidation sequence for scalar loads after vector stores
|
||||
# s_wait_idle waits for all outstanding memory operations including cache flushes
|
||||
CACHE_INV = [s_gl1_inv(), s_dcache_inv(), s_wait_idle()]
|
||||
|
||||
class TestSLoadRegisterOffset(unittest.TestCase):
|
||||
"""Tests for s_load with register offset (soffset field).
|
||||
|
||||
Bug: s_load_b32(s[dst], s[base:base+1], s[off]) ignores the register offset
|
||||
and only uses the immediate offset field. This causes incorrect memory loads
|
||||
when the offset comes from a register.
|
||||
"""
|
||||
|
||||
def test_s_load_b32_register_offset_basic(self):
|
||||
"""s_load_b32 with register offset should load from base + reg_offset."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values to output buffer: 0xAAAAAAAA at offset, 0xBBBBBBBB at offset+4
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
s_mov_b32(s[5], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], s[5]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[0], data=v[3], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Now test s_load with register offset
|
||||
# Put offset value in s[4]: offset = 4 bytes (1 dword)
|
||||
s_mov_b32(s[4], 4),
|
||||
# Load from out_ptr + TEST_OFFSET + s[4] (should load 0xBBBBBBBB)
|
||||
s_load_b32(s[5], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[5], 0xBBBBBBBB,
|
||||
f"s_load with reg offset 4 should load 0xBBBBBBBB: s[5]=0x{st.sgpr[5]:08x}")
|
||||
|
||||
def test_s_load_b32_register_offset_different_from_immediate(self):
|
||||
"""s_load_b32 with register offset loads different data than immediate offset 0."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values: 0xAAAAAAAA at offset, 0xBBBBBBBB at offset+4
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
s_mov_b32(s[5], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
v_mov_b32_e32(v[3], s[5]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[0], data=v[3], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load with immediate offset 0
|
||||
s_load_b32(s[5], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Load with register offset 4
|
||||
s_mov_b32(s[4], 4),
|
||||
s_load_b32(s[6], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# s[5] has dword at offset 0 (0xAAAAAAAA), s[6] has dword at offset 4 (0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[5], 0xAAAAAAAA)
|
||||
self.assertEqual(st.sgpr[6], 0xBBBBBBBB)
|
||||
self.assertNotEqual(st.sgpr[5], st.sgpr[6],
|
||||
f"s_load with reg offset 4 should load different value than offset 0: "
|
||||
f"s[5]=0x{st.sgpr[5]:08x}, s[6]=0x{st.sgpr[6]:08x}")
|
||||
|
||||
def test_s_load_b32_register_offset_same_as_dst(self):
|
||||
"""s_load_b32 where soffset register is same as destination.
|
||||
|
||||
This is the exact pattern that exposes the bug:
|
||||
s_load_b32(s[8], s[2:3], s[8])
|
||||
The offset should be read BEFORE the destination is overwritten.
|
||||
"""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values: 0xAAAAAAAA at offset, 0xBBBBBBBB at offset+4
|
||||
s_mov_b32(s[6], 0xAAAAAAAA),
|
||||
s_mov_b32(s[7], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[6]),
|
||||
v_mov_b32_e32(v[3], s[7]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[0], data=v[3], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Set up s[4] = 4 (offset in bytes)
|
||||
s_mov_b32(s[4], 4),
|
||||
# Load using s[4] as both offset and destination
|
||||
# Should load from base + 4, then store result in s[4]
|
||||
s_load_b32(s[4], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Also load with immediate offset 4 for comparison
|
||||
s_load_b32(s[5], s[2:3], NULL, offset=TEST_OFFSET+4),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# s[4] and s[5] should have the same value (both loaded from offset 4 = 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[4], 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[4], st.sgpr[5],
|
||||
f"s_load with reg offset s[4]=4 should match immediate offset=4: "
|
||||
f"s[4]=0x{st.sgpr[4]:08x}, s[5]=0x{st.sgpr[5]:08x}")
|
||||
|
||||
def test_s_load_b32_register_offset_zero(self):
|
||||
"""s_load_b32 with register offset = 0 should be same as immediate offset 0."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test value: 0xDEADBEEF at offset
|
||||
s_mov_b32(s[7], 0xDEADBEEF),
|
||||
v_mov_b32_e32(v[2], s[7]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load with register offset 0
|
||||
s_mov_b32(s[4], 0),
|
||||
s_load_b32(s[5], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Load with immediate offset 0
|
||||
s_load_b32(s[6], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[5], 0xDEADBEEF)
|
||||
self.assertEqual(st.sgpr[5], st.sgpr[6],
|
||||
f"s_load with reg offset 0 should match immediate offset 0: "
|
||||
f"s[5]=0x{st.sgpr[5]:08x}, s[6]=0x{st.sgpr[6]:08x}")
|
||||
|
||||
def test_s_load_b32_register_plus_immediate_offset(self):
|
||||
"""s_load_b32 with both register and immediate offset should add them."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values: 0xAAAAAAAA at offset, 0xBBBBBBBB at offset+4
|
||||
s_mov_b32(s[8], 0xAAAAAAAA),
|
||||
s_mov_b32(s[9], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[8]),
|
||||
v_mov_b32_e32(v[3], s[9]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[0], data=v[3], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# reg offset = 4, imm offset = 0 -> total offset = 4
|
||||
s_mov_b32(s[4], 4),
|
||||
s_load_b32(s[5], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# reg offset = 0, imm offset = 4 -> total offset = 4
|
||||
s_mov_b32(s[6], 0),
|
||||
s_load_b32(s[7], s[2:3], s[6], offset=TEST_OFFSET+4),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# Both should load from offset 4 (0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[5], 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[7], 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[5], st.sgpr[7],
|
||||
f"reg_off=4 + imm_off=0 should equal reg_off=0 + imm_off=4: "
|
||||
f"s[5]=0x{st.sgpr[5]:08x}, s[7]=0x{st.sgpr[7]:08x}")
|
||||
|
||||
|
||||
class TestSLoadMultiDword(unittest.TestCase):
|
||||
"""Tests for multi-dword s_load with register offset."""
|
||||
|
||||
def test_s_load_b64_register_offset(self):
|
||||
"""s_load_b64 with register offset should load 2 dwords from base + reg_offset."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values: 0xAAAAAAAA, 0xBBBBBBBB at offset
|
||||
s_mov_b32(s[10], 0xAAAAAAAA),
|
||||
s_mov_b32(s[11], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[10]),
|
||||
v_mov_b32_e32(v[3], s[11]),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
global_store_b32(addr=v[0], data=v[3], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load with register offset 0
|
||||
s_mov_b32(s[4], 0),
|
||||
s_load_b64(s[6:7], s[2:3], s[4], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Compare with immediate offset
|
||||
s_load_b64(s[8:9], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0xAAAAAAAA)
|
||||
self.assertEqual(st.sgpr[7], 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[6], st.sgpr[8])
|
||||
self.assertEqual(st.sgpr[7], st.sgpr[9])
|
||||
|
||||
def test_s_load_b128_register_offset(self):
|
||||
"""s_load_b128 with register offset should load 4 dwords from base + reg_offset."""
|
||||
instructions = [
|
||||
# Load output buffer pointer from args
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Store test values: 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD at offset
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[14], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[14]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_mov_b32(s[14], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[14]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_mov_b32(s[14], 0xCCCCCCCC),
|
||||
v_mov_b32_e32(v[2], s[14]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+8),
|
||||
s_mov_b32(s[14], 0xDDDDDDDD),
|
||||
v_mov_b32_e32(v[2], s[14]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+12),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load with register offset 0 (s_load_b128 requires 4-aligned dest: s[4], s[8], s[12], ...)
|
||||
s_mov_b32(s[15], 0),
|
||||
s_load_b128(s[4:7], s[2:3], s[15], offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Compare with immediate offset
|
||||
s_load_b128(s[8:11], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt(0),
|
||||
# Zero out pointer regs (different addresses in emu vs hw)
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0xAAAAAAAA)
|
||||
self.assertEqual(st.sgpr[5], 0xBBBBBBBB)
|
||||
self.assertEqual(st.sgpr[6], 0xCCCCCCCC)
|
||||
self.assertEqual(st.sgpr[7], 0xDDDDDDDD)
|
||||
self.assertEqual(st.sgpr[4], st.sgpr[8])
|
||||
self.assertEqual(st.sgpr[5], st.sgpr[9])
|
||||
|
||||
|
||||
class TestSLoadLarge(unittest.TestCase):
|
||||
"""Tests for large s_load operations (s_load_b256, s_load_b512)."""
|
||||
|
||||
def test_s_load_b256_basic(self):
|
||||
"""s_load_b256 loads 8 consecutive dwords."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store 8 test values
|
||||
s_mov_b32(s[20], 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET),
|
||||
s_mov_b32(s[20], 0x22222222),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+4),
|
||||
s_mov_b32(s[20], 0x33333333),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+8),
|
||||
s_mov_b32(s[20], 0x44444444),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+12),
|
||||
s_mov_b32(s[20], 0x55555555),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+16),
|
||||
s_mov_b32(s[20], 0x66666666),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+20),
|
||||
s_mov_b32(s[20], 0x77777777),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+24),
|
||||
s_mov_b32(s[20], 0x88888888),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET+28),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load all 8 dwords with s_load_b256
|
||||
s_load_b256(s[4:11], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0x11111111)
|
||||
self.assertEqual(st.sgpr[5], 0x22222222)
|
||||
self.assertEqual(st.sgpr[6], 0x33333333)
|
||||
self.assertEqual(st.sgpr[7], 0x44444444)
|
||||
self.assertEqual(st.sgpr[8], 0x55555555)
|
||||
self.assertEqual(st.sgpr[9], 0x66666666)
|
||||
self.assertEqual(st.sgpr[10], 0x77777777)
|
||||
self.assertEqual(st.sgpr[11], 0x88888888)
|
||||
|
||||
def test_s_load_b512_basic(self):
|
||||
"""s_load_b512 loads 16 consecutive dwords."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store 16 test values (use a pattern: 0x10, 0x20, ..., 0x100)
|
||||
*[instr for i in range(16) for instr in [
|
||||
s_mov_b32(s[20], (i + 1) * 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET + i * 4),
|
||||
]],
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load all 16 dwords with s_load_b512
|
||||
s_load_b512(s[64:79], s[2:3], NULL, offset=TEST_OFFSET),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
# Copy results to lower regs for verification (since st.sgpr only has 16 regs in test)
|
||||
s_mov_b32(s[4], s[64]),
|
||||
s_mov_b32(s[5], s[65]),
|
||||
s_mov_b32(s[6], s[78]),
|
||||
s_mov_b32(s[7], s[79]),
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0x11111111, "first dword")
|
||||
self.assertEqual(st.sgpr[5], 0x22222222, "second dword")
|
||||
self.assertEqual(st.sgpr[6], 0xFFFFFFFF & (15 * 0x11111111), "15th dword")
|
||||
self.assertEqual(st.sgpr[7], 0xFFFFFFFF & (16 * 0x11111111), "16th dword")
|
||||
|
||||
def test_s_load_b256_with_register_offset(self):
|
||||
"""s_load_b256 with register offset should add reg offset to address."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store pattern at TEST_OFFSET+8: skip first 2 dwords
|
||||
*[instr for i in range(8) for instr in [
|
||||
s_mov_b32(s[20], (i + 1) * 0x11111111),
|
||||
v_mov_b32_e32(v[2], s[20]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=TEST_OFFSET + 8 + i * 4),
|
||||
]],
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load with register offset 8
|
||||
s_mov_b32(s[20], 8),
|
||||
s_load_b256(s[4:11], s[2:3], s[20], offset=TEST_OFFSET),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0x11111111, "first dword at offset+8")
|
||||
self.assertEqual(st.sgpr[5], 0x22222222, "second dword at offset+8")
|
||||
self.assertEqual(st.sgpr[11], 0x88888888, "last dword at offset+8")
|
||||
|
||||
|
||||
class TestSLoadOffset(unittest.TestCase):
|
||||
"""Tests for s_load with different immediate offsets.
|
||||
|
||||
These tests verify that instruction deduplication correctly handles different offset values.
|
||||
If offset is made dynamic incorrectly, instructions with different offsets may load wrong data.
|
||||
"""
|
||||
|
||||
def test_s_load_different_offsets(self):
|
||||
"""Load from two different offsets and verify correct values."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store 0xAAAAAAAA at offset 100
|
||||
s_mov_b32(s[4], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=100),
|
||||
# Store 0xBBBBBBBB at offset 200
|
||||
s_mov_b32(s[4], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[4]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# Load from offset 100 -> should get 0xAAAAAAAA
|
||||
s_load_b32(s[4], s[2:3], NULL, offset=100),
|
||||
# Load from offset 200 -> should get 0xBBBBBBBB
|
||||
s_load_b32(s[5], s[2:3], NULL, offset=200),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0), s_mov_b32(s[3], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0xAAAAAAAA, f"offset 100: expected 0xAAAAAAAA, got 0x{st.sgpr[4]:08x}")
|
||||
self.assertEqual(st.sgpr[5], 0xBBBBBBBB, f"offset 200: expected 0xBBBBBBBB, got 0x{st.sgpr[5]:08x}")
|
||||
|
||||
def test_s_load_negative_offset(self):
|
||||
"""Test negative offset (21-bit signed).
|
||||
Store 0xAAAA at offset 100, 0xBBBB at offset 200.
|
||||
Load with offset -100 from base+200 -> should get 0xAAAA.
|
||||
Load with offset -100 from base+300 -> should get 0xBBBB."""
|
||||
instructions = [
|
||||
s_load_b64(s[2:3], s[80:81], 0, soffset=NULL),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
# Store 0xAAAAAAAA at offset 100, 0xBBBBBBBB at offset 200
|
||||
s_mov_b32(s[8], 0xAAAAAAAA),
|
||||
v_mov_b32_e32(v[2], s[8]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=100),
|
||||
s_mov_b32(s[8], 0xBBBBBBBB),
|
||||
v_mov_b32_e32(v[2], s[8]),
|
||||
global_store_b32(addr=v[0], data=v[2], saddr=s[2:3], offset=200),
|
||||
s_waitcnt_vmcnt(sdst=NULL, simm16=0),
|
||||
*CACHE_INV,
|
||||
# base+200, load with offset -100 -> should get value at 100
|
||||
s_add_u32(s[6], s[2], 200),
|
||||
s_addc_u32(s[7], s[3], 0),
|
||||
s_load_b32(s[4], s[6:7], NULL, offset=-100),
|
||||
# base+300, load with offset -100 -> should get value at 200
|
||||
s_add_u32(s[6], s[2], 300),
|
||||
s_addc_u32(s[7], s[3], 0),
|
||||
s_load_b32(s[5], s[6:7], NULL, offset=-100),
|
||||
s_waitcnt_lgkmcnt(sdst=NULL, simm16=0),
|
||||
s_mov_b32(s[2], 0),
|
||||
s_mov_b32(s[3], 0),
|
||||
s_mov_b32(s[6], 0),
|
||||
s_mov_b32(s[7], 0),
|
||||
s_mov_b32(s[8], 0),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[4], 0xAAAAAAAA, f"offset 200-100=100: expected 0xAAAAAAAA, got 0x{st.sgpr[4]:08x}")
|
||||
self.assertEqual(st.sgpr[5], 0xBBBBBBBB, f"offset 300-100=200: expected 0xBBBBBBBB, got 0x{st.sgpr[5]:08x}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
1007
tinygrad_repo/test/amd/hw/test_sop.py
Normal file
1007
tinygrad_repo/test/amd/hw/test_sop.py
Normal file
File diff suppressed because it is too large
Load Diff
35
tinygrad_repo/test/amd/hw/test_vinterp.py
Normal file
35
tinygrad_repo/test/amd/hw/test_vinterp.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Tests for VINTERP instructions."""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestVInterp(unittest.TestCase):
|
||||
def test_v_interp_p10_f32(self):
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], v[255]),
|
||||
v_cvt_f32_u32_e32(v[1], v[10]),
|
||||
s_mov_b32(s[0], f2i(100.0)),
|
||||
v_add_f32_e32(v[1], s[0], v[1]),
|
||||
v_cvt_f32_u32_e32(v[3], v[10]),
|
||||
s_mov_b32(s[1], f2i(10.0)),
|
||||
v_add_f32_e32(v[3], s[1], v[3]),
|
||||
s_mov_b32(s[2], f2i(2.0)),
|
||||
v_interp_p10_f32(v[4], v[1], s[2], v[3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=8)
|
||||
for lane in range(4): self.assertAlmostEqual(i2f(st.vgpr[lane][4]), 212.0, places=5)
|
||||
for lane in range(4, 8): self.assertAlmostEqual(i2f(st.vgpr[lane][4]), 224.0, places=5)
|
||||
|
||||
def test_v_interp_p10_f16_f32(self):
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[10], v[255]),
|
||||
v_cvt_f32_u32_e32(v[11], v[10]),
|
||||
v_cvt_f16_f32_e32(v[1], v[11]),
|
||||
s_mov_b32(s[0], f2i(10.0)),
|
||||
v_add_f32_e32(v[12], s[0], v[11]),
|
||||
v_cvt_f16_f32_e32(v[3], v[12]),
|
||||
s_mov_b32(s[1], f2i(2.0)),
|
||||
v_interp_p10_f16_f32(v[4], v[1], s[1], v[3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=8)
|
||||
for lane in range(4): self.assertAlmostEqual(i2f(st.vgpr[lane][4]), 12.0, places=5)
|
||||
for lane in range(4, 8): self.assertAlmostEqual(i2f(st.vgpr[lane][4]), 24.0, places=5)
|
||||
1633
tinygrad_repo/test/amd/hw/test_vop1.py
Normal file
1633
tinygrad_repo/test/amd/hw/test_vop1.py
Normal file
File diff suppressed because it is too large
Load Diff
994
tinygrad_repo/test/amd/hw/test_vop2.py
Normal file
994
tinygrad_repo/test/amd/hw/test_vop2.py
Normal file
@@ -0,0 +1,994 @@
|
||||
"""Tests for VOP2 instructions - two operand vector operations.
|
||||
|
||||
Includes: v_add_f32, v_mul_f32, v_and_b32, v_or_b32, v_xor_b32,
|
||||
v_lshrrev_b32, v_lshlrev_b32, v_fmac_f32, v_fmaak_f32, v_fmamk_f32,
|
||||
v_add_nc_u32, v_cndmask_b32, v_add_f16, v_mul_f16
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestBasicArithmetic(unittest.TestCase):
|
||||
"""Tests for basic arithmetic VOP2 instructions."""
|
||||
|
||||
def test_v_add_f32(self):
|
||||
"""V_ADD_F32 adds two floats."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 1.0),
|
||||
v_mov_b32_e32(v[1], 2.0),
|
||||
v_add_f32_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 3.0, places=5)
|
||||
|
||||
def test_v_mul_f32(self):
|
||||
"""V_MUL_F32 multiplies two floats."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 2.0),
|
||||
v_mov_b32_e32(v[1], 4.0),
|
||||
v_mul_f32_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 8.0, places=5)
|
||||
|
||||
def test_v_add_f32_dpp_row_shl(self):
|
||||
"""V_ADD_F32 DPP row_shl swizzles src0 before the add."""
|
||||
instructions = [
|
||||
v_cvt_f32_u32_e32(v[0], v[255]),
|
||||
v_add_f32_e32(v[1], DPP, v[0], vsrc0=v[0], dpp=0x101, row_mask=0xf, bank_mask=0xf, bc=1),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=16)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][1]), 1.0, places=5)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[1][1]), 3.0, places=5)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[14][1]), 29.0, places=5)
|
||||
|
||||
def test_v_fmac_f32(self):
|
||||
"""V_FMAC_F32: d = d + a*b using inline constants."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 2.0),
|
||||
v_mov_b32_e32(v[1], 4.0),
|
||||
v_mov_b32_e32(v[2], 1.0),
|
||||
v_fmac_f32_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 9.0, places=5)
|
||||
|
||||
def test_v_fmaak_f32(self):
|
||||
"""V_FMAAK_F32: d = a * b + K using inline constants."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 2.0),
|
||||
v_mov_b32_e32(v[1], 4.0),
|
||||
v_fmaak_f32_e32(v[2], v[0], v[1], literal=0x3f800000),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 9.0, places=5)
|
||||
|
||||
def test_v_fmamk_f32_basic(self):
|
||||
"""V_FMAMK_F32: d = a * K + b."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 2.0),
|
||||
v_mov_b32_e32(v[1], 1.0),
|
||||
v_fmamk_f32_e32(v[2], v[0], v[1], literal=0x40800000),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 9.0, places=5)
|
||||
|
||||
def test_v_fmamk_f32_small_constant(self):
|
||||
"""V_FMAMK_F32 with small constant."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 4.0),
|
||||
v_mov_b32_e32(v[1], 1.0),
|
||||
v_fmamk_f32_e32(v[2], v[0], v[1], literal=f2i(0.5)),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 3.0, places=5)
|
||||
|
||||
|
||||
class TestBitManipulation(unittest.TestCase):
|
||||
"""Tests for bit manipulation VOP2 instructions."""
|
||||
|
||||
def test_v_and_b32(self):
|
||||
"""V_AND_B32 bitwise and."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xff),
|
||||
s_mov_b32(s[1], 0x0f),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_and_b32_e32(v[1], s[1], v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x0f)
|
||||
|
||||
def test_v_and_b32_quadrant(self):
|
||||
"""V_AND_B32 for quadrant extraction (n & 3)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 15915),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_and_b32_e32(v[1], 3, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 15915 & 3)
|
||||
|
||||
def test_v_lshrrev_b32(self):
|
||||
"""V_LSHRREV_B32 logical shift right."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xff00),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_lshrrev_b32_e32(v[1], 8, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0xff)
|
||||
|
||||
def test_v_lshlrev_b32(self):
|
||||
"""V_LSHLREV_B32 logical shift left."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xff),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_lshlrev_b32_e32(v[1], 8, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0xff00)
|
||||
|
||||
def test_v_xor_b32(self):
|
||||
"""V_XOR_B32 bitwise xor (used in sin for sign)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x80000000),
|
||||
s_mov_b32(s[1], f2i(1.0)),
|
||||
v_mov_b32_e32(v[0], s[1]),
|
||||
v_xor_b32_e32(v[1], s[0], v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][1]), -1.0, places=5)
|
||||
|
||||
def test_v_xor_b32_sign_flip(self):
|
||||
"""V_XOR_B32 for sign flip pattern."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x80000000),
|
||||
v_mov_b32_e32(v[0], -2.0),
|
||||
v_xor_b32_e32(v[1], s[0], v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][1]), 2.0, places=5)
|
||||
|
||||
|
||||
class TestSpecialValues(unittest.TestCase):
|
||||
"""Tests for special float values - inf, nan, zero handling."""
|
||||
|
||||
def test_v_mul_f32_zero_times_inf(self):
|
||||
"""V_MUL_F32: 0 * inf = NaN."""
|
||||
import math
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0),
|
||||
s_mov_b32(s[0], 0x7f800000),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
v_mul_f32_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertTrue(math.isnan(i2f(st.vgpr[0][2])))
|
||||
|
||||
def test_v_add_f32_inf_minus_inf(self):
|
||||
"""V_ADD_F32: inf + (-inf) = NaN."""
|
||||
import math
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7f800000),
|
||||
s_mov_b32(s[1], 0xff800000),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_add_f32_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertTrue(math.isnan(i2f(st.vgpr[0][2])))
|
||||
|
||||
|
||||
class TestF16Ops(unittest.TestCase):
|
||||
"""Tests for 16-bit VOP2 operations."""
|
||||
|
||||
def test_v_add_f16_basic(self):
|
||||
"""V_ADD_F16 adds two f16 values."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_add_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4200, f"Expected 0x4200 (f16 3.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_add_f16_negative(self):
|
||||
"""V_ADD_F16 with negative values."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0xc000), # f16 -2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_add_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0xbc00, f"Expected 0xbc00 (f16 -1.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_basic(self):
|
||||
"""V_MUL_F16 multiplies two f16 values."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4000), # f16 2.0
|
||||
s_mov_b32(s[1], 0x4200), # f16 3.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_mul_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4600, f"Expected 0x4600 (f16 6.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_by_zero(self):
|
||||
"""V_MUL_F16 by zero."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
v_mul_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x0000, f"Expected 0x0000 (f16 0.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_fmac_f16_basic(self):
|
||||
"""V_FMAC_F16: d = d + a*b."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4000), # f16 2.0
|
||||
s_mov_b32(s[1], 0x4200), # f16 3.0
|
||||
s_mov_b32(s[2], 0x3c00), # f16 1.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
v_fmac_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
# 2.0 * 3.0 + 1.0 = 7.0, f16 7.0 = 0x4700
|
||||
self.assertEqual(result, 0x4700, f"Expected 0x4700 (f16 7.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_max_f16_basic(self):
|
||||
"""V_MAX_F16 returns the maximum of two f16 values."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_max_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4000, f"Expected 0x4000 (f16 2.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_min_f16_basic(self):
|
||||
"""V_MIN_F16 returns the minimum of two f16 values."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_min_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x3c00, f"Expected 0x3c00 (f16 1.0), got 0x{result:04x}")
|
||||
|
||||
def test_v_fmaak_f16_basic(self):
|
||||
"""V_FMAAK_F16: d = a * b + K."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4000), # f16 2.0
|
||||
s_mov_b32(s[1], 0x4200), # f16 3.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_fmaak_f16_e32(v[2], v[0], v[1], literal=0x3c00), # + f16 1.0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
# 2.0 * 3.0 + 1.0 = 7.0, f16 7.0 = 0x4700
|
||||
self.assertEqual(result, 0x4700, f"Expected 0x4700 (f16 7.0), got 0x{result:04x}")
|
||||
|
||||
|
||||
class TestHiHalfOps(unittest.TestCase):
|
||||
"""Tests for VOP2 16-bit operations with hi-half operands."""
|
||||
|
||||
def test_v_add_f16_src0_hi_fold(self):
|
||||
"""V_ADD_F16 with src0 hi-half fold (same register, different halves)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x40003c00), # lo=f16(1.0), hi=f16(2.0)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
VOP3(VOP3Op.V_ADD_F16, vdst=v[1], src0=v[0], src1=v[0], opsel=0b0001),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xffff
|
||||
self.assertEqual(result, 0x4200, f"Expected f16(3.0)=0x4200, got 0x{result:04x}")
|
||||
|
||||
def test_v_add_f16_src0_hi_different_reg(self):
|
||||
"""V_ADD_F16 with src0 hi-half from different register."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x40000000), # hi=f16(2.0), lo=0
|
||||
s_mov_b32(s[1], 0x00003c00), # hi=0, lo=f16(1.0)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
VOP3(VOP3Op.V_ADD_F16, vdst=v[2], src0=v[0], src1=v[1], opsel=0b0001),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4200, f"Expected f16(3.0)=0x4200, got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_src0_hi(self):
|
||||
"""V_MUL_F16 with src0 from high half."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x40000000), # hi=f16(2.0), lo=0
|
||||
s_mov_b32(s[1], 0x00004200), # hi=0, lo=f16(3.0)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
VOP3(VOP3Op.V_MUL_F16, vdst=v[2], src0=v[0], src1=v[1], opsel=0b0001),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_hi_half(self):
|
||||
"""V_MUL_F16 reading from high half."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x40003c00), # lo=1.0, hi=2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
VOP3(VOP3Op.V_MUL_F16, vdst=v[1], src0=v[0], src1=v[0], opsel=0b0011),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xffff
|
||||
self.assertEqual(result, 0x4400, f"Expected f16(4.0)=0x4400, got 0x{result:04x}")
|
||||
|
||||
def test_v_fma_f16_hi_dest(self):
|
||||
"""V_FMA_F16 writing to high half with opsel.
|
||||
|
||||
Uses V_FMA_F16 (not V_FMAC_F16) because it has explicit src2 operand
|
||||
which makes opsel handling clearer.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c000000), # hi=f16(1.0), lo=0
|
||||
s_mov_b32(s[1], 0x4000), # f16(2.0) in lo
|
||||
s_mov_b32(s[2], 0x4200), # f16(3.0) in lo
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
# V_FMA_F16: dst = src0 * src1 + src2
|
||||
# opsel=0b1100: bit2=src2 hi, bit3=dst hi
|
||||
# So: v[0].hi = v[1].lo * v[2].lo + v[0].hi = 2.0 * 3.0 + 1.0 = 7.0
|
||||
VOP3(VOP3Op.V_FMA_F16, vdst=v[0], src0=v[1], src1=v[2], src2=v[0], opsel=0b1100),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][0] >> 16) & 0xffff
|
||||
# 2.0 * 3.0 + 1.0 = 7.0, f16 7.0 = 0x4700
|
||||
self.assertEqual(hi, 0x4700, f"Expected f16(7.0)=0x4700 in hi, got 0x{hi:04x}")
|
||||
|
||||
def test_v_add_f16_multilane(self):
|
||||
"""V_ADD_F16 with multiple lanes."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_add_f16_e32(v[2], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
for lane in range(4):
|
||||
result = st.vgpr[lane][2] & 0xffff
|
||||
self.assertEqual(result, 0x4200, f"Lane {lane}: expected 0x4200, got 0x{result:04x}")
|
||||
|
||||
|
||||
class TestVop2F16HiHalf(unittest.TestCase):
|
||||
"""Regression tests for VOP2 f16 hi-half operand handling.
|
||||
|
||||
These test the bugs where:
|
||||
1. VOP2 vsrc1 >= 384 (v[128]+) wasn't extracting hi 16 bits
|
||||
2. VOP2 vdst >= 384 (v[128]+) wasn't preserving lo 16 bits
|
||||
"""
|
||||
|
||||
def test_v_add_f16_e32_vsrc1_hi_half(self):
|
||||
"""V_ADD_F16_E32 with vsrc1 from hi-half (v[128]+).
|
||||
|
||||
When vsrc1 >= 384 (representing v[128]+), the hardware reads from the hi 16 bits
|
||||
of v[vsrc1-128]. The emulator must extract bits [31:16] from the actual VGPR.
|
||||
|
||||
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
|
||||
s_mov_b32(s[0], 0x40003c00),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v_add_f16_e32 v[1], v[0], v[128] (vsrc1=v[128] reads hi of v[0])
|
||||
# In VOP2 encoding, vsrc1=384 means v[128], which maps to v[0].hi
|
||||
# v[1] = v[0].lo + v[0].hi = 1.0 + 2.0 = 3.0
|
||||
VOP2(VOP2Op.V_ADD_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xffff
|
||||
# 1.0 + 2.0 = 3.0, f16 3.0 = 0x4200
|
||||
self.assertEqual(result, 0x4200, f"Expected f16(3.0)=0x4200, got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_e32_vsrc1_hi_half(self):
|
||||
"""V_MUL_F16_E32 with vsrc1 from hi-half.
|
||||
|
||||
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4200_4000: hi=f16(3.0), lo=f16(2.0)
|
||||
s_mov_b32(s[0], 0x42004000),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v_mul_f16_e32 v[1], v[0], v[128] (vsrc1=v[128] reads hi of v[0])
|
||||
# v[1] = v[0].lo * v[0].hi = 2.0 * 3.0 = 6.0
|
||||
VOP2(VOP2Op.V_MUL_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xffff
|
||||
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
|
||||
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
|
||||
|
||||
def test_v_add_f16_e32_vdst_hi_half(self):
|
||||
"""V_ADD_F16_E32 writing to hi-half destination (v[128]+).
|
||||
|
||||
When vdst >= 384 (representing v[128]+), the hardware writes to bits [31:16]
|
||||
of v[vdst-128] while preserving bits [15:0]. The emulator must merge the result.
|
||||
|
||||
Regression test for: VOP2 f16 vdst hi-half write bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x0000_BEEF: lo has marker value
|
||||
s_mov_b32(s[0], 0x0000BEEF),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(1.0), v[2] = f16(2.0)
|
||||
s_mov_b32(s[1], 0x3c00),
|
||||
s_mov_b32(s[2], 0x4000),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
# v_add_f16_e32 v[128], v[1], v[2] (vdst=v[128] writes hi of v[0])
|
||||
# v[0].hi = 1.0 + 2.0 = 3.0, v[0].lo preserved = 0xBEEF
|
||||
VOP2(VOP2Op.V_ADD_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][0] >> 16) & 0xffff
|
||||
lo = st.vgpr[0][0] & 0xffff
|
||||
# hi = 3.0 = 0x4200, lo preserved = 0xBEEF
|
||||
self.assertEqual(hi, 0x4200, f"Expected hi=f16(3.0)=0x4200, got 0x{hi:04x}")
|
||||
self.assertEqual(lo, 0xBEEF, f"Expected lo preserved=0xBEEF, got 0x{lo:04x}")
|
||||
|
||||
def test_v_mul_f16_e32_vdst_hi_half(self):
|
||||
"""V_MUL_F16_E32 writing to hi-half destination.
|
||||
|
||||
Regression test for: VOP2 f16 vdst hi-half write bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x0000_DEAD: lo has marker value
|
||||
s_mov_b32(s[0], 0x0000DEAD),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(2.0), v[2] = f16(4.0)
|
||||
s_mov_b32(s[1], 0x4000),
|
||||
s_mov_b32(s[2], 0x4400),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
# v_mul_f16_e32 v[128], v[1], v[2] (vdst=v[128] writes hi of v[0])
|
||||
# v[0].hi = 2.0 * 4.0 = 8.0, v[0].lo preserved = 0xDEAD
|
||||
VOP2(VOP2Op.V_MUL_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][0] >> 16) & 0xffff
|
||||
lo = st.vgpr[0][0] & 0xffff
|
||||
# hi = 8.0 = 0x4800, lo preserved = 0xDEAD
|
||||
self.assertEqual(hi, 0x4800, f"Expected hi=f16(8.0)=0x4800, got 0x{hi:04x}")
|
||||
self.assertEqual(lo, 0xDEAD, f"Expected lo preserved=0xDEAD, got 0x{lo:04x}")
|
||||
|
||||
def test_v_add_f16_e32_both_hi_half(self):
|
||||
"""V_ADD_F16_E32 with both vsrc1 and vdst as hi-half (different underlying regs).
|
||||
|
||||
Tests the combination of both fixes: reading vsrc1 from hi-half AND
|
||||
writing result to hi-half destination, using different underlying VGPRs.
|
||||
|
||||
Regression test for: VOP2 f16 hi-half bugs (combined).
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4000_xxxx: hi=f16(2.0) for vsrc1
|
||||
s_mov_b32(s[0], 0x40000000),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = 0x0000_3c00: lo=f16(1.0) for src0
|
||||
s_mov_b32(s[1], 0x00003c00),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
# v[2] = 0x0000_CAFE: lo=marker for vdst preservation
|
||||
s_mov_b32(s[2], 0x0000CAFE),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
# v_add_f16_e32 v[130], v[1], v[128]
|
||||
# src0 = v[1].lo = 1.0
|
||||
# vsrc1 = v[128] reads v[0].hi = 2.0
|
||||
# result = 1.0 + 2.0 = 3.0
|
||||
# vdst = v[130] writes to v[2].hi, preserving v[2].lo
|
||||
VOP2(VOP2Op.V_ADD_F16, vdst=v[130], src0=v[1], vsrc1=v[128]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][2] >> 16) & 0xffff
|
||||
lo = st.vgpr[0][2] & 0xffff
|
||||
# hi = 3.0 = 0x4200, lo preserved = 0xCAFE
|
||||
self.assertEqual(hi, 0x4200, f"Expected hi=f16(3.0)=0x4200, got 0x{hi:04x}")
|
||||
self.assertEqual(lo, 0xCAFE, f"Expected lo preserved=0xCAFE, got 0x{lo:04x}")
|
||||
|
||||
def test_v_fmac_f16_e32_vsrc1_hi_half(self):
|
||||
"""V_FMAC_F16_E32 with vsrc1 from hi-half.
|
||||
|
||||
V_FMAC_F16: vdst = vdst + src0 * vsrc1
|
||||
|
||||
Regression test for: VOP2 f16 vsrc1 hi-half extraction bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
|
||||
s_mov_b32(s[0], 0x40003c00),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(3.0) = 0x4200
|
||||
s_mov_b32(s[1], 0x4200),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
# v_fmac_f16_e32 v[1], v[0], v[128]
|
||||
# vdst = v[1] = 3.0 + v[0].lo * v[0].hi = 3.0 + 1.0 * 2.0 = 5.0
|
||||
VOP2(VOP2Op.V_FMAC_F16, vdst=v[1], src0=v[0], vsrc1=v[128]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xffff
|
||||
# 3.0 + 1.0 * 2.0 = 5.0, f16 5.0 = 0x4500
|
||||
self.assertEqual(result, 0x4500, f"Expected f16(5.0)=0x4500, got 0x{result:04x}")
|
||||
|
||||
def test_v_fmac_f16_e32_vdst_hi_half(self):
|
||||
"""V_FMAC_F16_E32 writing to hi-half destination.
|
||||
|
||||
V_FMAC_F16: vdst.h = vdst.h + src0 * vsrc1
|
||||
|
||||
When vdst is v[128]+, the accumulator D0 must also read from the hi-half.
|
||||
This tests the bug where D0 was read from lo-half instead of hi-half.
|
||||
|
||||
Regression test for: VOP2 FMAC hi-half D0 accumulator read bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x3800_DEAD: hi=f16(0.5), lo=marker (0xDEAD)
|
||||
s_mov_b32(s[0], 0x3800DEAD),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(2.0) = 0x4000
|
||||
s_mov_b32(s[1], 0x4000),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
# v[2] = f16(3.0) = 0x4200
|
||||
s_mov_b32(s[2], 0x4200),
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
# v_fmac_f16_e32 v[128], v[1], v[2]
|
||||
# vdst = v[128] means v[0].hi
|
||||
# D0 = v[0].hi = 0.5
|
||||
# result = D0 + src0 * vsrc1 = 0.5 + 2.0 * 3.0 = 6.5
|
||||
# v[0].hi = 6.5, v[0].lo preserved = 0xDEAD
|
||||
VOP2(VOP2Op.V_FMAC_F16, vdst=v[128], src0=v[1], vsrc1=v[2]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][0] >> 16) & 0xffff
|
||||
lo = st.vgpr[0][0] & 0xffff
|
||||
# hi = 6.5 = 0x4680, lo preserved = 0xDEAD
|
||||
self.assertEqual(hi, 0x4680, f"Expected hi=f16(6.5)=0x4680, got 0x{hi:04x}")
|
||||
self.assertEqual(lo, 0xDEAD, f"Expected lo preserved=0xDEAD, got 0x{lo:04x}")
|
||||
|
||||
def test_v_mul_f16_e32_src0_hi_half(self):
|
||||
"""V_MUL_F16_E32 with src0 from hi-half (src0 >= v[128]).
|
||||
|
||||
When src0 >= 384 (representing v[128]+), the hardware reads from the hi 16 bits
|
||||
of v[src0-128]. The emulator must extract bits [31:16] from the actual VGPR.
|
||||
|
||||
Regression test for: VOP2 f16 src0 hi-half extraction bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
|
||||
s_mov_b32(s[0], 0x40003c00),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(3.0) = 0x4200
|
||||
s_mov_b32(s[1], 0x4200),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
# v_mul_f16_e32 v[2], v[128], v[1]
|
||||
# src0 = v[128] reads from v[0].hi = 2.0
|
||||
# result = 2.0 * 3.0 = 6.0
|
||||
VOP2(VOP2Op.V_MUL_F16, vdst=v[2], src0=v[128], vsrc1=v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
|
||||
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
|
||||
|
||||
def test_v_add_f16_e32_src0_hi_half(self):
|
||||
"""V_ADD_F16_E32 with src0 from hi-half (src0 >= v[128]).
|
||||
|
||||
Regression test for: VOP2 f16 src0 hi-half extraction bug.
|
||||
"""
|
||||
instructions = [
|
||||
# v[0] = 0x4000_3c00: hi=f16(2.0), lo=f16(1.0)
|
||||
s_mov_b32(s[0], 0x40003c00),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v[1] = f16(5.0) = 0x4500
|
||||
s_mov_b32(s[1], 0x4500),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
# v_add_f16_e32 v[2], v[128], v[1]
|
||||
# src0 = v[128] reads from v[0].hi = 2.0
|
||||
# result = 2.0 + 5.0 = 7.0
|
||||
VOP2(VOP2Op.V_ADD_F16, vdst=v[2], src0=v[128], vsrc1=v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
# 2.0 + 5.0 = 7.0, f16 7.0 = 0x4700
|
||||
self.assertEqual(result, 0x4700, f"Expected f16(7.0)=0x4700, got 0x{result:04x}")
|
||||
|
||||
|
||||
class TestF16InlineConstants(unittest.TestCase):
|
||||
"""Regression tests for VOP2 F16 inline float constants.
|
||||
|
||||
For 16-bit VOP2 operations (v_add_f16, v_mul_f16, etc.), inline float constants
|
||||
like 1.0, 2.0 must use F16 encoding (0x3c00, 0x4000) not F32 encoding (0x3f800000).
|
||||
|
||||
The emulator's rsrc() function needs bits=16 to select F16_INLINE constants.
|
||||
|
||||
Regression test for: VOP2 16-bit inline constant using F32 instead of F16.
|
||||
"""
|
||||
|
||||
def test_v_add_f16_inline_constant_1_0(self):
|
||||
"""V_ADD_F16_E32 with inline constant 1.0 should use F16 encoding."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
# v_add_f16_e32 v[1], 1.0, v[0] -- 1.0 must be F16 0x3c00, not F32 0x3f800000
|
||||
v_add_f16_e32(v[1], 1.0, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xFFFF
|
||||
# 1.0 + 1.0 = 2.0, f16 2.0 = 0x4000
|
||||
self.assertEqual(result, 0x4000, f"Expected f16(2.0)=0x4000, got 0x{result:04x}")
|
||||
|
||||
def test_v_add_f16_inline_constant_2_0(self):
|
||||
"""V_ADD_F16_E32 with inline constant 2.0."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4200), # f16 3.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_add_f16_e32(v[1], 2.0, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xFFFF
|
||||
# 2.0 + 3.0 = 5.0, f16 5.0 = 0x4500
|
||||
self.assertEqual(result, 0x4500, f"Expected f16(5.0)=0x4500, got 0x{result:04x}")
|
||||
|
||||
def test_v_mul_f16_inline_constant(self):
|
||||
"""V_MUL_F16_E32 with inline constant 2.0."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x4200), # f16 3.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mul_f16_e32(v[1], 2.0, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][1] & 0xFFFF
|
||||
# 2.0 * 3.0 = 6.0, f16 6.0 = 0x4600
|
||||
self.assertEqual(result, 0x4600, f"Expected f16(6.0)=0x4600, got 0x{result:04x}")
|
||||
|
||||
|
||||
class TestCndmask(unittest.TestCase):
|
||||
"""Tests for V_CNDMASK_B32 and V_CNDMASK_B16."""
|
||||
|
||||
def test_v_cndmask_b16_select_src0(self):
|
||||
"""V_CNDMASK_B16 selects src0 when VCC bit is 0."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # VCC = 0
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cndmask_b16(v[2], v[0], v[1], VCC),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x3c00, f"Expected src0=0x3c00, got 0x{result:04x}")
|
||||
|
||||
def test_v_cndmask_b16_select_src1(self):
|
||||
"""V_CNDMASK_B16 selects src1 when VCC bit is 1."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 1), # VCC = 1
|
||||
s_mov_b32(s[0], 0x3c00), # f16 1.0
|
||||
s_mov_b32(s[1], 0x4000), # f16 2.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cndmask_b16(v[2], v[0], v[1], VCC),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
result = st.vgpr[0][2] & 0xffff
|
||||
self.assertEqual(result, 0x4000, f"Expected src1=0x4000, got 0x{result:04x}")
|
||||
|
||||
def test_v_cndmask_b16_write_hi(self):
|
||||
"""V_CNDMASK_B16 can write to high 16 bits with opsel."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c003800), # src0: hi=1.0, lo=0.5
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], 0x4000c000), # src1: hi=2.0, lo=-2.0
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
s_mov_b32(s[2], 0xDEAD0000), # v2 initial: hi=0xDEAD, lo=0
|
||||
v_mov_b32_e32(v[2], s[2]),
|
||||
s_mov_b32(VCC_LO, 0), # vcc = 0, select src0
|
||||
# opsel=0b1011: bit0=src0 hi, bit1=src1 hi, bit3=dst hi
|
||||
VOP3(VOP3Op.V_CNDMASK_B16, vdst=v[2], src0=v[0], src1=v[1], src2=SrcEnum.VCC_LO, opsel=0b1011),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
hi = (st.vgpr[0][2] >> 16) & 0xffff
|
||||
lo = st.vgpr[0][2] & 0xffff
|
||||
# vcc=0 selects src0.h = 1.0 = 0x3c00, writes to hi
|
||||
self.assertEqual(hi, 0x3c00, f"Expected hi=0x3c00 (1.0), got 0x{hi:04x}")
|
||||
self.assertEqual(lo, 0x0000, f"Expected lo preserved as 0, got 0x{lo:04x}")
|
||||
|
||||
|
||||
class TestSpecialFloatValues(unittest.TestCase):
|
||||
"""Tests for special float value handling in VOP2 instructions."""
|
||||
|
||||
def test_neg_zero_add(self):
|
||||
"""-0.0 + 0.0 = +0.0 (IEEE 754)."""
|
||||
neg_zero = 0x80000000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], neg_zero),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_add_f32_e32(v[1], 0.0, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x00000000, "Should be +0.0")
|
||||
|
||||
def test_neg_zero_mul(self):
|
||||
"""-0.0 * -1.0 = +0.0."""
|
||||
neg_zero = 0x80000000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], neg_zero),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mul_f32_e32(v[1], -1.0, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x00000000, "Should be +0.0")
|
||||
|
||||
def test_inf_minus_inf(self):
|
||||
"""+inf - inf = NaN."""
|
||||
import math
|
||||
pos_inf = 0x7f800000
|
||||
neg_inf = 0xff800000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], pos_inf),
|
||||
s_mov_b32(s[1], neg_inf),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_sub_f32_e32(v[2], v[0], v[1]), # inf - (-inf) = inf
|
||||
v_add_f32_e32(v[3], v[0], v[1]), # inf + (-inf) = NaN
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], pos_inf, "inf - (-inf) = inf")
|
||||
self.assertTrue(math.isnan(i2f(st.vgpr[0][3])), "inf + (-inf) = NaN")
|
||||
|
||||
def test_denormal_f32_mul_ftz(self):
|
||||
"""Denormal * normal - RDNA3 flushes denormals to zero (FTZ mode)."""
|
||||
smallest_denorm = 0x00000001 # Smallest positive denormal
|
||||
instructions = [
|
||||
s_mov_b32(s[0], smallest_denorm),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mul_f32_e32(v[1], 2.0, v[0]), # Denormal input gets flushed to 0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x00000000)
|
||||
|
||||
|
||||
class TestCarryOps(unittest.TestCase):
|
||||
"""Tests for VOP2 carry instructions (v_add_co_ci_u32, v_sub_co_ci_u32, v_subrev_co_ci_u32)."""
|
||||
|
||||
def test_v_subrev_co_ci_u32_no_borrow(self):
|
||||
"""V_SUBREV_CO_CI_U32: D0 = S1 - S0 - VCC_IN, when VCC_IN=0."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # VCC = 0 (no borrow in)
|
||||
v_mov_b32_e32(v[0], 5), # S0 = 5
|
||||
v_mov_b32_e32(v[1], 10), # S1 = 10
|
||||
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 0 = 5
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 5)
|
||||
self.assertEqual(st.vcc, 0) # No borrow out
|
||||
|
||||
def test_v_subrev_co_ci_u32_with_borrow(self):
|
||||
"""V_SUBREV_CO_CI_U32: D0 = S1 - S0 - VCC_IN, when VCC_IN=1."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 1), # VCC = 1 (borrow in)
|
||||
v_mov_b32_e32(v[0], 5), # S0 = 5
|
||||
v_mov_b32_e32(v[1], 10), # S1 = 10
|
||||
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 1 = 4
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 4)
|
||||
self.assertEqual(st.vcc, 0) # No borrow out
|
||||
|
||||
def test_v_subrev_co_ci_u32_generates_borrow(self):
|
||||
"""V_SUBREV_CO_CI_U32: generates borrow when S0 + VCC_IN > S1."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # VCC = 0
|
||||
v_mov_b32_e32(v[0], 10), # S0 = 10
|
||||
v_mov_b32_e32(v[1], 5), # S1 = 5
|
||||
v_subrev_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 - 10 - 0 = -5 (underflow)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xFFFFFFFB) # -5 as unsigned
|
||||
self.assertEqual(st.vcc, 1) # Borrow out
|
||||
|
||||
def test_v_add_co_ci_u32_no_carry(self):
|
||||
"""V_ADD_CO_CI_U32: D0 = S0 + S1 + VCC_IN, when VCC_IN=0."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # VCC = 0 (no carry in)
|
||||
v_mov_b32_e32(v[0], 5), # S0 = 5
|
||||
v_mov_b32_e32(v[1], 10), # S1 = 10
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 + 10 + 0 = 15
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 15)
|
||||
self.assertEqual(st.vcc, 0) # No carry out
|
||||
|
||||
def test_v_add_co_ci_u32_with_carry(self):
|
||||
"""V_ADD_CO_CI_U32: D0 = S0 + S1 + VCC_IN, when VCC_IN=1."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 1), # VCC = 1 (carry in)
|
||||
v_mov_b32_e32(v[0], 5), # S0 = 5
|
||||
v_mov_b32_e32(v[1], 10), # S1 = 10
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 5 + 10 + 1 = 16
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 16)
|
||||
self.assertEqual(st.vcc, 0) # No carry out
|
||||
|
||||
def test_v_add_co_ci_u32_generates_carry(self):
|
||||
"""V_ADD_CO_CI_U32: generates carry when overflow occurs."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 1), # VCC = 1 (carry in)
|
||||
s_mov_b32(s[0], 0xFFFFFFFF), # max u32
|
||||
v_mov_b32_e32(v[0], s[0]), # S0 = 0xFFFFFFFF
|
||||
v_mov_b32_e32(v[1], 0), # S1 = 0
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 0xFFFFFFFF + 0 + 1 = 0 (overflow)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0) # Overflowed to 0
|
||||
self.assertEqual(st.vcc, 1) # Carry out
|
||||
|
||||
def test_v_add_co_ci_u32_clears_carry(self):
|
||||
"""V_ADD_CO_CI_U32: VCC must be updated even when no carry is generated.
|
||||
|
||||
This tests the case where VCC=1 going in (carry-in consumed) but the addition
|
||||
does not overflow, so VCC must be cleared to 0.
|
||||
|
||||
Regression test for: VCC not being written by v_add_co_ci_u32_e32.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 1), # VCC = 1 (carry in)
|
||||
v_mov_b32_e32(v[0], 1), # S0 = 1
|
||||
v_mov_b32_e32(v[1], 1), # S1 = 1
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 1 + 1 + 1 = 3 (no overflow)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 3) # 1 + 1 + 1 = 3
|
||||
self.assertEqual(st.vcc, 0) # No carry out - VCC must be cleared
|
||||
|
||||
def test_v_add_co_ci_u32_multilane_clears_vcc(self):
|
||||
"""V_ADD_CO_CI_U32 with multiple lanes: VCC bits must be updated per-lane.
|
||||
|
||||
When VCC has multiple bits set (one per active lane), and the addition doesn't
|
||||
overflow for any lane, all VCC bits must be cleared.
|
||||
|
||||
Regression test for: VCC not being written by v_add_co_ci_u32_e32 in multi-lane case.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0b11), # VCC = 0b11 (lanes 0,1 have carry-in)
|
||||
v_mov_b32_e32(v[0], 1), # S0 = 1 for all lanes
|
||||
v_mov_b32_e32(v[1], 1), # S1 = 1 for all lanes
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 1 + 1 + 1 = 3 (no overflow)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=2)
|
||||
self.assertEqual(st.vgpr[0][2], 3) # lane 0: 1 + 1 + 1 = 3
|
||||
self.assertEqual(st.vgpr[1][2], 3) # lane 1: 1 + 1 + 1 = 3
|
||||
self.assertEqual(st.vcc, 0) # No carry out for any lane - all VCC bits must be cleared
|
||||
|
||||
def test_v_add_co_ci_u32_preserves_inactive_vcc_bits(self):
|
||||
"""V_ADD_CO_CI_U32: VCC carry-out overwrites entire VCC register.
|
||||
|
||||
VOP2 carry instructions write ALL VCC bits based on carry-out, clearing
|
||||
bits for lanes that don't overflow regardless of EXEC mask.
|
||||
|
||||
Note: This differs from VOPC which only writes active lane bits.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0x00010000), # VCC bit 16 set
|
||||
v_mov_b32_e32(v[0], 1), # S0 = 1
|
||||
v_mov_b32_e32(v[1], 1), # S1 = 1
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 1 + 1 + 0 = 2 (no carry)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
self.assertEqual(st.vgpr[0][2], 2) # lane 0: 1 + 1 + 0 = 2
|
||||
# VCC should be completely cleared (all lanes have no carry-out)
|
||||
self.assertEqual(st.vcc, 0)
|
||||
|
||||
def test_v_add_co_ci_u32_all_lanes_same_result(self):
|
||||
"""V_ADD_CO_CI_U32: all active lanes should produce the same result.
|
||||
|
||||
When the same constant inputs are used across all lanes, each lane should
|
||||
compute the same result and write to its own VGPR slot.
|
||||
|
||||
Regression test for: VGPR writes not happening for all lanes.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # No carry-in
|
||||
v_mov_b32_e32(v[0], 3), # inline constant 3
|
||||
v_mov_b32_e32(v[1], 5), # value 5
|
||||
v_add_co_ci_u32_e32(v[1], 3, v[1]), # v[1] = 3 + v[1] + 0 = 3 + 5 = 8
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# All 4 lanes should have v[1] = 8
|
||||
for lane in range(4):
|
||||
self.assertEqual(st.vgpr[lane][1], 8, f"lane {lane} should have v[1]=8")
|
||||
|
||||
def test_v_sub_co_ci_u32_no_borrow(self):
|
||||
"""V_SUB_CO_CI_U32: D0 = S0 - S1 - VCC_IN, when VCC_IN=0."""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # VCC = 0 (no borrow in)
|
||||
v_mov_b32_e32(v[0], 10), # S0 = 10
|
||||
v_mov_b32_e32(v[1], 5), # S1 = 5
|
||||
v_sub_co_ci_u32_e32(v[2], v[0], v[1]), # D0 = 10 - 5 - 0 = 5
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 5)
|
||||
self.assertEqual(st.vcc, 0) # No borrow out
|
||||
|
||||
def test_v_sub_co_ci_u32_vop3sd_separate_carry_regs(self):
|
||||
"""VOP3SD V_SUB_CO_CI_U32: carry-in from src2, carry-out to sdst (separate registers).
|
||||
|
||||
This tests the VOP3SD encoding where src2 specifies the carry-in register
|
||||
independently from sdst (carry-out). The bug was reading carry-in from sdst
|
||||
instead of src2.
|
||||
|
||||
Computation: D0 = S0 - S1 - carry_in = 0 - 0 - 1 = -1 = 0xFFFFFFFF
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(s[6], 1), # carry-in = 1 (in s[6])
|
||||
s_mov_b32(s[10], 0), # carry-out dest = 0 initially (in s[10])
|
||||
# VOP3SD: v_sub_co_ci_u32(vdst, sdst, src0, src1, src2)
|
||||
# src2 is carry-in (s[6]=1), sdst is carry-out (s[10])
|
||||
v_sub_co_ci_u32(v[0], s[10], 0, 0, s[6]), # D0 = 0 - 0 - 1 = -1
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0xFFFFFFFF) # -1 as unsigned
|
||||
self.assertEqual(st.sgpr[10], 1) # Borrow out to s[10]
|
||||
|
||||
def test_v_add_co_ci_u32_vop3sd_separate_carry_regs(self):
|
||||
"""VOP3SD V_ADD_CO_CI_U32: carry-in from src2, carry-out to sdst (separate registers).
|
||||
|
||||
This tests the VOP3SD encoding where src2 specifies the carry-in register
|
||||
independently from sdst (carry-out).
|
||||
|
||||
Computation: D0 = S0 + S1 + carry_in = 5 + 10 + 1 = 16
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(s[6], 1), # carry-in = 1 (in s[6])
|
||||
s_mov_b32(s[10], 0), # carry-out dest = 0 initially (in s[10])
|
||||
# VOP3SD: v_add_co_ci_u32(vdst, sdst, src0, src1, src2)
|
||||
v_add_co_ci_u32(v[0], s[10], 5, 10, s[6]), # D0 = 5 + 10 + 1 = 16
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 16)
|
||||
self.assertEqual(st.sgpr[10], 0) # No carry out
|
||||
|
||||
def test_v_add_co_ci_u32_vop3sd_null_sdst(self):
|
||||
"""VOP3SD V_ADD_CO_CI_U32 with sdst=NULL: carry output is discarded.
|
||||
|
||||
When sdst=NULL (register 124), the carry-out should NOT be written anywhere.
|
||||
We verify this by checking that VCC (which we set to a sentinel value) is unchanged.
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0xDEADBEEF), # Sentinel value in VCC
|
||||
s_mov_b32(s[6], 0), # carry-in = 0
|
||||
# VOP3SD with NULL sdst: carry-out should be discarded
|
||||
# Uses 0xFFFFFFFF + 1 + 0 = 0 with carry-out=1, but carry should not be written
|
||||
v_add_co_ci_u32(v[0], NULL, 0xFFFFFFFF, 1, s[6]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0) # 0xFFFFFFFF + 1 + 0 = 0 (overflow)
|
||||
self.assertEqual(st.vcc, 0xDEADBEEF) # VCC unchanged - carry was discarded
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
3656
tinygrad_repo/test/amd/hw/test_vop3.py
Normal file
3656
tinygrad_repo/test/amd/hw/test_vop3.py
Normal file
File diff suppressed because it is too large
Load Diff
1036
tinygrad_repo/test/amd/hw/test_vop3p.py
Normal file
1036
tinygrad_repo/test/amd/hw/test_vop3p.py
Normal file
File diff suppressed because it is too large
Load Diff
964
tinygrad_repo/test/amd/hw/test_vopc.py
Normal file
964
tinygrad_repo/test/amd/hw/test_vopc.py
Normal file
@@ -0,0 +1,964 @@
|
||||
"""Tests for VOPC instructions - vector compare operations.
|
||||
|
||||
Includes: v_cmp_class_f32, v_cmp_class_f16, v_cmp_eq_*, v_cmp_lt_*, v_cmp_gt_*
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
VCC = 106 # SGPR index for VCC_LO
|
||||
|
||||
class TestCmpClass(unittest.TestCase):
|
||||
"""Tests for V_CMP_CLASS_F32 float classification."""
|
||||
|
||||
def test_cmp_class_quiet_nan(self):
|
||||
"""V_CMP_CLASS_F32 detects quiet NaN."""
|
||||
quiet_nan = 0x7fc00000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0b0000000010), # bit 1 = quiet NaN
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect quiet NaN")
|
||||
|
||||
def test_cmp_class_signaling_nan(self):
|
||||
"""V_CMP_CLASS_F32 detects signaling NaN."""
|
||||
signal_nan = 0x7f800001
|
||||
instructions = [
|
||||
s_mov_b32(s[0], signal_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0b0000000001), # bit 0 = signaling NaN
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect signaling NaN")
|
||||
|
||||
def test_cmp_class_positive_inf(self):
|
||||
"""V_CMP_CLASS_F32 detects +inf."""
|
||||
pos_inf = 0x7f800000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], pos_inf),
|
||||
s_mov_b32(s[1], 0b1000000000), # bit 9 = +inf
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect +inf")
|
||||
|
||||
def test_cmp_class_negative_inf(self):
|
||||
"""V_CMP_CLASS_F32 detects -inf."""
|
||||
neg_inf = 0xff800000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], neg_inf),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0b0000000100), # bit 2 = -inf
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect -inf")
|
||||
|
||||
def test_cmp_class_normal_positive(self):
|
||||
"""V_CMP_CLASS_F32 detects positive normal."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 1.0),
|
||||
s_mov_b32(s[1], 0b0100000000), # bit 8 = positive normal
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect positive normal")
|
||||
|
||||
def test_cmp_class_normal_negative(self):
|
||||
"""V_CMP_CLASS_F32 detects negative normal."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], -1.0),
|
||||
v_mov_b32_e32(v[1], 0b0000001000), # bit 3 = negative normal
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect negative normal")
|
||||
|
||||
def test_cmp_class_quiet_nan_not_signaling(self):
|
||||
"""Quiet NaN does not match signaling NaN mask."""
|
||||
quiet_nan = 0x7fc00000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0b0000000001), # bit 0 = signaling NaN only
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "Quiet NaN should not match signaling mask")
|
||||
|
||||
def test_cmp_class_signaling_nan_not_quiet(self):
|
||||
"""Signaling NaN does not match quiet NaN mask."""
|
||||
signal_nan = 0x7f800001
|
||||
instructions = [
|
||||
s_mov_b32(s[0], signal_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0b0000000010), # bit 1 = quiet NaN only
|
||||
v_cmp_class_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "Signaling NaN should not match quiet mask")
|
||||
|
||||
def test_v_cmp_lg_f32_nan(self):
|
||||
"""v_cmp_lg_f32 is ordered not-equal (<>): NaN <> x should be False per IEEE 754."""
|
||||
quiet_nan = 0x7fc00000
|
||||
one_f32 = 0x3f800000 # 1.0f
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], one_f32),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_lg_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "v_cmp_lg_f32(NaN, 1.0) should be 0")
|
||||
|
||||
def test_v_cmp_neq_f32_nan(self):
|
||||
"""v_cmp_neq_f32 is unordered not-equal (!=): NaN != x should be True per IEEE 754."""
|
||||
quiet_nan = 0x7fc00000
|
||||
one_f32 = 0x3f800000 # 1.0f
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], one_f32),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_neq_f32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "v_cmp_neq_f32(NaN, 1.0) should be 1")
|
||||
|
||||
def test_v_cmp_sets_vcc_bits(self):
|
||||
"""V_CMP_EQ sets VCC bits based on per-lane comparison."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 5),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
v_cmp_eq_u32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
self.assertEqual(st.vcc & 0xf, 0xf, "All lanes should match")
|
||||
|
||||
|
||||
class TestCmpClassF16(unittest.TestCase):
|
||||
"""Tests for V_CMP_CLASS_F16 float classification.
|
||||
|
||||
Class bit mapping:
|
||||
bit 0 = signaling NaN
|
||||
bit 1 = quiet NaN
|
||||
bit 2 = -infinity
|
||||
bit 3 = -normal
|
||||
bit 4 = -denormal
|
||||
bit 5 = -zero
|
||||
bit 6 = +zero
|
||||
bit 7 = +denormal
|
||||
bit 8 = +normal
|
||||
bit 9 = +infinity
|
||||
"""
|
||||
|
||||
def test_cmp_class_f16_positive_zero(self):
|
||||
"""V_CMP_CLASS_F16: +zero matches bit 6."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x0000), # f16 +0.0
|
||||
v_mov_b32_e32(v[1], 0x40), # bit 6 = +zero
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect positive zero")
|
||||
|
||||
def test_cmp_class_f16_negative_zero(self):
|
||||
"""V_CMP_CLASS_F16: -zero matches bit 5."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x8000), # f16 -0.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x20), # bit 5 = -zero
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect negative zero")
|
||||
|
||||
def test_cmp_class_f16_positive_normal(self):
|
||||
"""V_CMP_CLASS_F16: +1.0 (normal) matches bit 8."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 +1.0
|
||||
s_mov_b32(s[1], 0x100), # bit 8 = +normal
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect positive normal")
|
||||
|
||||
def test_cmp_class_f16_negative_normal(self):
|
||||
"""V_CMP_CLASS_F16: -1.0 (normal) matches bit 3."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xbc00), # f16 -1.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x08), # bit 3 = -normal
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect negative normal")
|
||||
|
||||
def test_cmp_class_f16_positive_infinity(self):
|
||||
"""V_CMP_CLASS_F16: +inf matches bit 9."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7c00), # f16 +inf
|
||||
s_mov_b32(s[1], 0x200), # bit 9 = +inf
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect positive infinity")
|
||||
|
||||
def test_cmp_class_f16_negative_infinity(self):
|
||||
"""V_CMP_CLASS_F16: -inf matches bit 2."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xfc00), # f16 -inf
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x04), # bit 2 = -inf
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect negative infinity")
|
||||
|
||||
def test_cmp_class_f16_quiet_nan(self):
|
||||
"""V_CMP_CLASS_F16: quiet NaN matches bit 1."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7e00), # f16 quiet NaN
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x02), # bit 1 = quiet NaN
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect quiet NaN")
|
||||
|
||||
def test_cmp_class_f16_signaling_nan(self):
|
||||
"""V_CMP_CLASS_F16: signaling NaN matches bit 0."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7c01), # f16 signaling NaN
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x01), # bit 0 = signaling NaN
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect signaling NaN")
|
||||
|
||||
def test_cmp_class_f16_positive_denormal(self):
|
||||
"""V_CMP_CLASS_F16: positive denormal matches bit 7."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 1), # f16 +denormal (0x0001)
|
||||
v_mov_b32_e32(v[1], 0x80), # bit 7 = +denormal
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect positive denormal")
|
||||
|
||||
def test_cmp_class_f16_negative_denormal(self):
|
||||
"""V_CMP_CLASS_F16: negative denormal matches bit 4."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x8001), # f16 -denormal
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], 0x10), # bit 4 = -denormal
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Should detect negative denormal")
|
||||
|
||||
def test_cmp_class_f16_combined_mask_zeros(self):
|
||||
"""V_CMP_CLASS_F16: mask 0x60 covers both +zero and -zero."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # f16 +0.0
|
||||
v_mov_b32_e32(v[1], 0x60), # bits 5 and 6 (+-zero)
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +zero with mask 0x60")
|
||||
|
||||
def test_cmp_class_f16_combined_mask_1f8(self):
|
||||
"""V_CMP_CLASS_F16: mask 0x1f8 covers -normal,-denorm,-zero,+zero,+denorm,+normal.
|
||||
|
||||
This is the exact mask used in the f16 sin kernel at PC=46.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # f16 +0.0
|
||||
s_mov_b32(s[0], 0x1f8),
|
||||
v_mov_b32_e32(v[1], s[0]), # mask 0x1f8
|
||||
v_cmp_class_f16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +zero with mask 0x1f8")
|
||||
|
||||
def test_cmp_class_f16_vop3_encoding(self):
|
||||
"""V_CMP_CLASS_F16 in VOP3 encoding (v_cmp_class_f16_e64)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0), # f16 +0.0
|
||||
s_mov_b32(s[0], 0x1f8), # class mask
|
||||
v_cmp_class_f16_e64(VCC_LO, v[0], s[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +zero with VOP3 encoding")
|
||||
|
||||
def test_cmp_class_f16_vop3_normal_positive(self):
|
||||
"""V_CMP_CLASS_F16 VOP3 encoding with +1.0 (normal)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x3c00), # f16 +1.0
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], 0x1f8), # class mask
|
||||
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "VCC should be 1 for +1.0 (normal) with mask 0x1f8")
|
||||
|
||||
def test_cmp_class_f16_vop3_nan_fails_mask(self):
|
||||
"""V_CMP_CLASS_F16 VOP3: NaN should NOT match mask 0x1f8 (no NaN bits set)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7e00), # f16 quiet NaN
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], 0x1f8), # class mask
|
||||
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "VCC should be 0 for NaN with mask 0x1f8 (no NaN bits)")
|
||||
|
||||
def test_cmp_class_f16_vop3_inf_fails_mask(self):
|
||||
"""V_CMP_CLASS_F16 VOP3: +inf should NOT match mask 0x1f8 (no inf bits set)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x7c00), # f16 +inf
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
s_mov_b32(s[1], 0x1f8), # class mask
|
||||
v_cmp_class_f16_e64(VCC_LO, v[0], s[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "VCC should be 0 for +inf with mask 0x1f8 (no inf bits)")
|
||||
|
||||
|
||||
class TestCmpInt(unittest.TestCase):
|
||||
"""Tests for integer comparison operations."""
|
||||
|
||||
def test_v_cmp_eq_u32(self):
|
||||
"""V_CMP_EQ_U32 sets VCC bits based on per-lane comparison."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 5),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
v_cmp_eq_u32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
self.assertEqual(st.vcc & 0xf, 0xf, "All lanes should match")
|
||||
|
||||
def test_v_cmp_ne_u32_with_zero(self):
|
||||
"""V_CMP_NE_U32: compare with zero, used for int->bool cast."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[1], 0),
|
||||
v_cmp_eq_u32_e32(1, v[255]), # vcc = (lane == 1)
|
||||
v_cndmask_b32_e64(v[1], v[1], 1, VCC_LO), # v1[lane1] = 1
|
||||
v_cmp_ne_u32_e32(0, v[1]), # vcc = (0 != v1)
|
||||
v_cndmask_b32_e64(v[0], 0, 1, VCC_LO), # v0 = vcc ? 1 : 0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=2)
|
||||
self.assertEqual(st.vgpr[0][0], 0, "lane 0: 0 != 0 should be false")
|
||||
self.assertEqual(st.vgpr[1][0], 1, "lane 1: 0 != 1 should be true")
|
||||
self.assertEqual(st.vcc & 0x3, 0x2, "VCC should be 0b10")
|
||||
|
||||
def test_v_cmp_ne_u32_all_nonzero(self):
|
||||
"""V_CMP_NE_U32: all lanes have nonzero values."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[1], 5),
|
||||
v_cmp_ne_u32_e32(0, v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
self.assertEqual(st.vcc & 0xf, 0xf, "All lanes should be != 0")
|
||||
|
||||
def test_cmp_eq_u16_opsel_lo_lo(self):
|
||||
"""V_CMP_EQ_U16 comparing lo halves."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x12340005), # lo=5, hi=0x1234
|
||||
s_mov_b32(s[1], 0xABCD0005), # lo=5, hi=0xABCD
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[1]),
|
||||
v_cmp_eq_u16_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Lo halves should be equal")
|
||||
|
||||
def test_cmp_eq_u16_opsel_hi_hi(self):
|
||||
"""V_CMP_EQ_U16 comparing hi halves with VOP3 opsel."""
|
||||
instructions = [
|
||||
s_mov_b32(s[2], 0x00051234), # hi=5, lo=0x1234
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x0005ABCD), # hi=5, lo=0xABCD
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
v_cmp_eq_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[0] & 1, 1, "Hi halves should be equal: 5==5")
|
||||
|
||||
def test_cmp_eq_u16_opsel_hi_hi_equal(self):
|
||||
"""V_CMP_EQ_U16 VOP3 with opsel=3 compares hi halves (equal case)."""
|
||||
instructions = [
|
||||
s_mov_b32(s[2], 0x12340005), # lo=5, hi=0x1234
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x12340009), # lo=9, hi=0x1234
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
v_cmp_eq_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[0] & 1, 1, "hi==hi should be true: 0x1234==0x1234")
|
||||
|
||||
def test_cmp_gt_u16_opsel_hi(self):
|
||||
"""V_CMP_GT_U16 VOP3 with opsel=3 compares hi halves."""
|
||||
instructions = [
|
||||
s_mov_b32(s[2], 0x99990005), # lo=5, hi=0x9999
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x12340005), # lo=5, hi=0x1234
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
v_cmp_gt_u16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[0] & 1, 1, "hi>hi should be true: 0x9999>0x1234")
|
||||
|
||||
|
||||
class TestCmpFloat(unittest.TestCase):
|
||||
"""Tests for float comparison operations."""
|
||||
|
||||
def test_v_cmp_lt_f16_vsrc1_hi(self):
|
||||
"""V_CMP_LT_F16 with both operands from high half using VOP3 opsel."""
|
||||
instructions = [
|
||||
s_mov_b32(s[2], 0x3c000000), # hi=1.0 (f16), lo=0
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x40000000), # hi=2.0 (f16), lo=0
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
v_cmp_lt_f16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[0] & 1, 1, "1.0 < 2.0 should be true")
|
||||
|
||||
def test_v_cmp_gt_f16_vsrc1_hi(self):
|
||||
"""V_CMP_GT_F16 with both operands from high half using VOP3 opsel."""
|
||||
instructions = [
|
||||
s_mov_b32(s[2], 0x40000000), # hi=2.0 (f16), lo=0
|
||||
v_mov_b32_e32(v[0], s[2]),
|
||||
s_mov_b32(s[2], 0x3c000000), # hi=1.0 (f16), lo=0
|
||||
v_mov_b32_e32(v[1], s[2]),
|
||||
v_cmp_gt_f16_e64(vdst=s[0], src0=v[0], src1=v[1], opsel=3),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[0] & 1, 1, "2.0 > 1.0 should be true")
|
||||
|
||||
def test_v_cmp_eq_f16_vsrc1_hi_equal(self):
|
||||
"""v_cmp_eq_f16 with equal low and high halves."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x42004200), # hi=3.0 (0x4200), lo=3.0 (0x4200)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_cmp_eq_f16_e32(v[0], v[0].h),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Expected vcc=1 (3.0 == 3.0)")
|
||||
|
||||
def test_v_cmp_neq_f16_vsrc1_hi(self):
|
||||
"""v_cmp_neq_f16 with different low and high halves."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0x40003c00), # hi=2.0 (0x4000), lo=1.0 (0x3c00)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_cmp_lg_f16_e32(v[0], v[0].h),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "Expected vcc=1 (1.0 != 2.0)")
|
||||
|
||||
def test_v_cmp_nge_f16_inf_self(self):
|
||||
"""v_cmp_nge_f16 comparing -inf with itself (unordered less than).
|
||||
|
||||
Regression test: -inf < -inf should be false (IEEE 754).
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0xFC00FC00), # both halves = -inf (0xFC00)
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_cmp_nge_f16_e32(v[0], v[0].h),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "Expected vcc=0 (-inf >= -inf)")
|
||||
|
||||
def test_v_cmp_f16_multilane(self):
|
||||
"""v_cmp_lt_f16 with vsrc1=v128 across multiple lanes."""
|
||||
instructions = [
|
||||
# Lane 0: v0 = 0x40003c00 (hi=2.0, lo=1.0) -> 1.0 < 2.0 = true
|
||||
# Lane 1: v0 = 0x3c004000 (hi=1.0, lo=2.0) -> 2.0 < 1.0 = false
|
||||
v_mov_b32_e32(v[0], 0x40003c00), # default
|
||||
v_cmp_eq_u32_e32(1, v[255]), # vcc = (lane == 1)
|
||||
v_cndmask_b32_e64(v[0], v[0], 0x3c004000, SrcEnum.VCC_LO),
|
||||
v_cmp_lt_f16_e32(v[0], v[0].h),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=2)
|
||||
self.assertEqual(st.vcc & 1, 1, "Lane 0: expected vcc=1 (1.0 < 2.0)")
|
||||
self.assertEqual((st.vcc >> 1) & 1, 0, "Lane 1: expected vcc=0 (2.0 < 1.0)")
|
||||
|
||||
|
||||
class TestVOP3VOPCModifiers(unittest.TestCase):
|
||||
"""Tests for VOP3 VOPC with abs/neg modifiers."""
|
||||
|
||||
def test_v_cmp_ge_f32_abs_both(self):
|
||||
"""v_cmp_ge_f32 with abs on both sources: abs(0.0) >= abs(-1.0) = false.
|
||||
|
||||
Regression test: int16 mod operation uses v_cmp_ge_f32 with abs modifiers.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0.0),
|
||||
v_mov_b32_e32(v[1], -1.0),
|
||||
# abs=0b11 means abs(src0) and abs(src1)
|
||||
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "abs(0.0) >= abs(-1.0) should be false")
|
||||
|
||||
def test_v_cmp_ge_f32_abs_negative_divisor(self):
|
||||
"""v_cmp_ge_f32 with abs: remainder check for negative divisor.
|
||||
|
||||
Tests the exact comparison used in int16 mod: abs(rem_f) >= abs(div_f).
|
||||
For 1 % -1: rem_f = 0.0, div_f = -1.0, so abs(0.0) >= abs(-1.0) = false.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0.0), # remainder as float
|
||||
v_mov_b32_e32(v[1], -1.0), # divisor as float
|
||||
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "abs(0.0) >= abs(-1.0) should be false")
|
||||
|
||||
def test_v_cmp_ge_f32_abs_small_remainder(self):
|
||||
"""v_cmp_ge_f32 with abs: abs(-0.5) >= abs(-3.0) = false."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], -0.5),
|
||||
v_mov_b32_e32(v[1], -3.0),
|
||||
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "abs(-0.5) >= abs(-3.0) should be false")
|
||||
|
||||
def test_v_cmp_ge_f32_abs_equal(self):
|
||||
"""v_cmp_ge_f32 with abs: abs(-1.0) >= abs(1.0) = true."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], -1.0),
|
||||
v_mov_b32_e32(v[1], 1.0),
|
||||
v_cmp_ge_f32_e64(VCC_LO, v[0], v[1], abs=0b11),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "abs(-1.0) >= abs(1.0) should be true")
|
||||
|
||||
|
||||
class TestVOP3VOPC64Bit(unittest.TestCase):
|
||||
"""Tests for VOP3 VOPC with 64-bit operands."""
|
||||
|
||||
def test_v_cmp_lt_f64_basic(self):
|
||||
"""v_cmp_lt_f64: 0.0 < 1.0 = true."""
|
||||
zero_f64 = f2i64(0.0)
|
||||
one_f64 = f2i64(1.0)
|
||||
instructions = [
|
||||
s_mov_b32(s[0], zero_f64 & 0xffffffff),
|
||||
s_mov_b32(s[1], zero_f64 >> 32),
|
||||
s_mov_b32(s[2], one_f64 & 0xffffffff),
|
||||
s_mov_b32(s[3], one_f64 >> 32),
|
||||
v_cmp_lt_f64_e64(VCC_LO, s[0:1], s[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "0.0 < 1.0 should be true")
|
||||
|
||||
def test_v_cmp_lt_f64_negative(self):
|
||||
"""v_cmp_lt_f64: -1.0 < 0.0 = true."""
|
||||
neg_one_f64 = f2i64(-1.0)
|
||||
zero_f64 = f2i64(0.0)
|
||||
instructions = [
|
||||
s_mov_b32(s[0], neg_one_f64 & 0xffffffff),
|
||||
s_mov_b32(s[1], neg_one_f64 >> 32),
|
||||
s_mov_b32(s[2], zero_f64 & 0xffffffff),
|
||||
s_mov_b32(s[3], zero_f64 >> 32),
|
||||
v_cmp_lt_f64_e64(VCC_LO, s[0:1], s[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "-1.0 < 0.0 should be true")
|
||||
|
||||
def test_v_cmp_lt_i64_signed(self):
|
||||
"""v_cmp_lt_i64: 0 < -1 (signed) = false."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0),
|
||||
s_mov_b32(s[1], 0), # s[0:1] = 0
|
||||
s_mov_b32(s[2], 0xffffffff),
|
||||
s_mov_b32(s[3], 0xffffffff), # s[2:3] = -1
|
||||
v_cmp_lt_i64_e64(VCC_LO, s[0:1], s[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "0 < -1 (signed) should be false")
|
||||
|
||||
def test_v_cmp_lt_u64_unsigned(self):
|
||||
"""v_cmp_lt_u64: 0 < 0xFFFFFFFFFFFFFFFF (unsigned) = true."""
|
||||
instructions = [
|
||||
s_mov_b32(s[0], 0),
|
||||
s_mov_b32(s[1], 0), # s[0:1] = 0
|
||||
s_mov_b32(s[2], 0xffffffff),
|
||||
s_mov_b32(s[3], 0xffffffff), # s[2:3] = max uint64
|
||||
v_cmp_lt_u64_e64(VCC_LO, s[0:1], s[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "0 < max_uint64 should be true")
|
||||
|
||||
|
||||
class TestVOPCF64(unittest.TestCase):
|
||||
"""Tests for VOPC (E32 encoding) with 64-bit float operands. Regression test for f64 compare bug."""
|
||||
|
||||
def test_v_cmp_lt_f64_e32_true(self):
|
||||
"""v_cmp_lt_f64_e32: 2.0 < 3.0 = true."""
|
||||
lo0, hi0 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
|
||||
lo1, hi1 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
|
||||
instructions = [
|
||||
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
|
||||
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
|
||||
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
|
||||
v_cmp_lt_f64_e32(v[0:1], v[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "2.0 < 3.0 should be true")
|
||||
|
||||
def test_v_cmp_lt_f64_e32_false(self):
|
||||
"""v_cmp_lt_f64_e32: 3.0 < 2.0 = false."""
|
||||
lo0, hi0 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
|
||||
lo1, hi1 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
|
||||
instructions = [
|
||||
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
|
||||
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
|
||||
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
|
||||
v_cmp_lt_f64_e32(v[0:1], v[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "3.0 < 2.0 should be false")
|
||||
|
||||
def test_v_cmp_nlt_f64_e32_true(self):
|
||||
"""v_cmp_nlt_f64_e32: !(3.0 < 2.0) = true."""
|
||||
lo0, hi0 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
|
||||
lo1, hi1 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
|
||||
instructions = [
|
||||
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
|
||||
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
|
||||
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
|
||||
v_cmp_nlt_f64_e32(v[0:1], v[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(3.0 < 2.0) should be true")
|
||||
|
||||
def test_v_cmp_nlt_f64_e32_false(self):
|
||||
"""v_cmp_nlt_f64_e32: !(2.0 < 3.0) = false."""
|
||||
lo0, hi0 = f2i64(2.0) & 0xffffffff, f2i64(2.0) >> 32
|
||||
lo1, hi1 = f2i64(3.0) & 0xffffffff, f2i64(3.0) >> 32
|
||||
instructions = [
|
||||
s_mov_b32(s[0], lo0), s_mov_b32(s[1], hi0),
|
||||
s_mov_b32(s[2], lo1), s_mov_b32(s[3], hi1),
|
||||
v_mov_b32_e32(v[0], s[0]), v_mov_b32_e32(v[1], s[1]),
|
||||
v_mov_b32_e32(v[2], s[2]), v_mov_b32_e32(v[3], s[3]),
|
||||
v_cmp_nlt_f64_e32(v[0:1], v[2:3]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "!(2.0 < 3.0) should be false")
|
||||
|
||||
|
||||
class TestCmpxExec(unittest.TestCase):
|
||||
"""Tests for V_CMPX instructions that modify EXEC mask."""
|
||||
|
||||
def test_v_cmpx_ngt_f32_e64_all_true(self):
|
||||
"""V_CMPX_NGT_F32_E64: all lanes pass (literal <= all values)."""
|
||||
# 131072.0 = 0x48000000
|
||||
# All values > 131072, so !(131072 > val) = true for all
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
|
||||
v_mov_b32_e32(v[0], f2i(200000.0)), # lane 0
|
||||
v_cmp_eq_u32_e32(1, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[0], f2i(300000.0), VCC_LO), # lane 1
|
||||
v_cmp_eq_u32_e32(2, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[1], f2i(400000.0), VCC_LO), # lane 2
|
||||
# Now v[1] has: lane0=200000, lane1=300000, lane2=400000
|
||||
# Compare: !(131072.0 > v[1]) i.e., 131072.0 <= v[1]
|
||||
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# All values > 131072, so all lanes should remain active
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x7, "All 3 lanes should remain active")
|
||||
|
||||
def test_v_cmpx_ngt_f32_e64_some_false(self):
|
||||
"""V_CMPX_NGT_F32_E64: some lanes fail (literal > some values)."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
|
||||
v_mov_b32_e32(v[0], f2i(100000.0)), # lane 0: 131072 > 100000 = true, so !(true) = false
|
||||
v_cmp_eq_u32_e32(1, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[0], f2i(200000.0), VCC_LO), # lane 1: 131072 > 200000 = false, so !(false) = true
|
||||
v_cmp_eq_u32_e32(2, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[1], f2i(150000.0), VCC_LO), # lane 2: 131072 > 150000 = false, so !(false) = true
|
||||
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# lane 0: fail (100000 < 131072), lanes 1,2: pass
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x6, "Lanes 1,2 should be active, lane 0 inactive")
|
||||
|
||||
def test_v_cmpx_ngt_f32_e64_all_false(self):
|
||||
"""V_CMPX_NGT_F32_E64: all lanes fail (literal > all values)."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
|
||||
v_mov_b32_e32(v[0], f2i(100.0)), # all lanes have 100.0
|
||||
# 131072 > 100 = true, so !(true) = false for all
|
||||
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x0, "All lanes should be inactive")
|
||||
|
||||
def test_v_cmpx_ngt_f32_e64_large_values(self):
|
||||
"""V_CMPX_NGT_F32_E64: test with values that trigger Payne-Hanek in sin().
|
||||
|
||||
This is a regression test for the sin(859240.0) bug.
|
||||
Values 859240, 1000000, 100594688 should all pass !(131072 > val).
|
||||
"""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x7), # 3 lanes active
|
||||
v_mov_b32_e32(v[0], f2i(859240.0)), # lane 0
|
||||
v_cmp_eq_u32_e32(1, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[0], f2i(1000000.0), VCC_LO), # lane 1
|
||||
v_cmp_eq_u32_e32(2, v[255]),
|
||||
v_cndmask_b32_e64(v[1], v[1], f2i(100594688.0), VCC_LO), # lane 2
|
||||
v_cmpx_ngt_f32_e64(EXEC_LO, f2i(131072.0), v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# All values > 131072, so !(131072 > val) = true for all
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0x7, 0x7, "All 3 lanes should remain active")
|
||||
|
||||
|
||||
class TestVCCBehavior(unittest.TestCase):
|
||||
"""Tests for VCC condition code behavior."""
|
||||
|
||||
def test_vcc_all_lanes_true(self):
|
||||
"""VCC should have all bits set when all lanes compare true."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 5),
|
||||
v_mov_b32_e32(v[1], 5),
|
||||
v_cmp_eq_u32_e32(v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc, 0xFFFFFFFF, "All 32 lanes should be true")
|
||||
|
||||
def test_vcc_lane_dependent(self):
|
||||
"""VCC should differ per lane based on lane_id comparison."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 16),
|
||||
v_cmp_lt_u32_e32(v[255], v[0]), # lanes 0-15 are < 16
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc & 0xFFFF, 0xFFFF, "Lanes 0-15 should be true")
|
||||
self.assertEqual(st.vcc >> 16, 0x0000, "Lanes 16-31 should be false")
|
||||
|
||||
|
||||
class TestCmpNge(unittest.TestCase):
|
||||
"""Tests for V_CMP_NGE (not-greater-or-equal) with NaN semantics.
|
||||
|
||||
NGE = !(a >= b). With NaN inputs:
|
||||
- If either input is NaN, a >= b is false, so !(false) = true
|
||||
- This differs from a < b which returns false for NaN inputs
|
||||
"""
|
||||
|
||||
def test_v_cmp_nge_f32_normal_values(self):
|
||||
"""v_cmp_nge_f32: basic comparison with normal floats."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], f2i(1.0)),
|
||||
v_mov_b32_e32(v[1], f2i(2.0)),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(1.0 >= 2.0) = !(false) = true
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(1.0 >= 2.0) should be true")
|
||||
|
||||
def test_v_cmp_nge_f32_equal_values(self):
|
||||
"""v_cmp_nge_f32: equal values should return false."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], f2i(1.0)),
|
||||
v_mov_b32_e32(v[1], f2i(1.0)),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(1.0 >= 1.0) = !(true) = false
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "!(1.0 >= 1.0) should be false")
|
||||
|
||||
def test_v_cmp_nge_f32_greater_value(self):
|
||||
"""v_cmp_nge_f32: greater value should return false."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], f2i(2.0)),
|
||||
v_mov_b32_e32(v[1], f2i(1.0)),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(2.0 >= 1.0) = !(true) = false
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 0, "!(2.0 >= 1.0) should be false")
|
||||
|
||||
def test_v_cmp_nge_f32_neg_inf(self):
|
||||
"""v_cmp_nge_f32: -inf compared to normal value."""
|
||||
neg_inf = 0xff800000 # -inf
|
||||
instructions = [
|
||||
s_mov_b32(s[0], neg_inf),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], f2i(1.0)),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(-inf >= 1.0) = !(false) = true
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(-inf >= 1.0) should be true")
|
||||
|
||||
def test_v_cmp_nge_f32_clears_inactive_vcc_bits(self):
|
||||
"""v_cmp_nge_f32 with partial EXEC clears inactive VCC bits (hardware behavior)."""
|
||||
neg_inf = 0xff800000 # -inf
|
||||
instructions = [
|
||||
# Set VCC to all 1s first
|
||||
s_mov_b32(VCC_LO, 0xFFFFFFFF),
|
||||
# Set EXEC to only lane 0
|
||||
s_mov_b32(EXEC_LO, 0x00000001),
|
||||
# v0 = 1.0 for lane 0
|
||||
v_mov_b32_e32(v[0], f2i(1.0)),
|
||||
# Compare: !(-inf >= 1.0) = true for lane 0
|
||||
v_cmp_nge_f32_e32(neg_inf, v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=16)
|
||||
# Hardware clears inactive lane bits, only active lane results remain
|
||||
# Lane 0 result = 1 (true), lanes 1-15 = 0 (cleared)
|
||||
self.assertEqual(st.vcc, 0x00000001, "VCC should only have active lane results")
|
||||
|
||||
def test_v_cmp_nge_f32_nan_src0(self):
|
||||
"""v_cmp_nge_f32: NaN in src0 should return true (NaN >= x is false)."""
|
||||
quiet_nan = 0x7fc00000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], f2i(1.0)),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(NaN >= 1.0) = !(false) = true
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(NaN >= 1.0) should be true")
|
||||
|
||||
def test_v_cmp_nge_f32_nan_src1(self):
|
||||
"""v_cmp_nge_f32: NaN in src1 should return true (x >= NaN is false)."""
|
||||
quiet_nan = 0x7fc00000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], f2i(1.0)),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(1.0 >= NaN) = !(false) = true
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(1.0 >= NaN) should be true")
|
||||
|
||||
def test_v_cmp_nge_f32_both_nan(self):
|
||||
"""v_cmp_nge_f32: both NaN should return true."""
|
||||
quiet_nan = 0x7fc00000
|
||||
instructions = [
|
||||
s_mov_b32(s[0], quiet_nan),
|
||||
v_mov_b32_e32(v[0], s[0]),
|
||||
v_mov_b32_e32(v[1], s[0]),
|
||||
v_cmp_nge_f32_e32(v[0], v[1]), # !(NaN >= NaN) = !(false) = true
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc & 1, 1, "!(NaN >= NaN) should be true")
|
||||
|
||||
|
||||
class TestCmpxPartialWavefront(unittest.TestCase):
|
||||
"""Tests for V_CMPX with partial wavefronts (fewer than 32 active lanes).
|
||||
|
||||
Regression tests for bug where v_cmpx incorrectly set EXEC bits for inactive
|
||||
lanes when the wavefront had fewer than 32 lanes. This caused garbage data
|
||||
from uninitialized lanes to corrupt memory writes.
|
||||
"""
|
||||
|
||||
def test_v_cmpx_eq_u32_partial_wave_3_lanes(self):
|
||||
"""V_CMPX_EQ_U32 with 3 active lanes should only affect those 3 lanes.
|
||||
|
||||
With n_lanes=3, initial EXEC=0x7. After v_cmpx comparing lane_id == 1,
|
||||
only lane 1 should pass, so EXEC should become 0x2 (not have bits 3-31 set).
|
||||
"""
|
||||
instructions = [
|
||||
v_cmpx_eq_u32_e32(1, v[255]), # EXEC = lanes where lane_id == 1
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# Only lane 1 should be active (bit 1 set)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x2,
|
||||
"Only lane 1 should be active after v_cmpx_eq_u32 with 3 lanes")
|
||||
|
||||
def test_v_cmpx_eq_u32_partial_wave_5_lanes(self):
|
||||
"""V_CMPX_EQ_U32 with 5 active lanes."""
|
||||
instructions = [
|
||||
v_cmpx_eq_u32_e32(3, v[255]), # EXEC = lanes where lane_id == 3
|
||||
]
|
||||
st = run_program(instructions, n_lanes=5)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x8,
|
||||
"Only lane 3 should be active after v_cmpx_eq_u32 with 5 lanes")
|
||||
|
||||
def test_v_cmpx_lt_u32_partial_wave(self):
|
||||
"""V_CMPX_LT_U32 with partial wavefront."""
|
||||
# VOPC: src0 < vsrc1, so we need v_cmpx_gt_u32 to get lane_id < 2
|
||||
instructions = [
|
||||
v_cmpx_gt_u32_e32(2, v[255]), # EXEC = lanes where 2 > lane_id (i.e., lane_id < 2)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# Lanes 0,1 should be active (bits 0,1 set = 0x3)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x3,
|
||||
"Only lanes 0,1 should be active after v_cmpx_gt_u32(2, lane_id) with 4 lanes")
|
||||
|
||||
def test_v_cmpx_ge_u32_partial_wave(self):
|
||||
"""V_CMPX_GE_U32 with partial wavefront."""
|
||||
# VOPC: src0 >= vsrc1, so v_cmpx_le_u32(1, lane_id) gives lane_id >= 2? No.
|
||||
# v_cmpx_le_u32(src0, vsrc1) = src0 <= vsrc1 = 1 <= lane_id
|
||||
instructions = [
|
||||
v_cmpx_le_u32_e32(2, v[255]), # EXEC = lanes where 2 <= lane_id (i.e., lane_id >= 2)
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# Lanes 2,3 should be active (bits 2,3 set = 0xC)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0xC,
|
||||
"Only lanes 2,3 should be active after v_cmpx_le_u32(2, lane_id) with 4 lanes")
|
||||
|
||||
def test_v_cmpx_ne_u32_partial_wave_all_pass(self):
|
||||
"""V_CMPX_NE_U32 where all active lanes pass."""
|
||||
instructions = [
|
||||
v_cmpx_ne_u32_e32(99, v[255]), # EXEC = lanes where lane_id != 99
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# All 3 lanes should remain active (bits 0,1,2 set = 0x7)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x7,
|
||||
"All 3 lanes should remain active when all pass")
|
||||
|
||||
def test_v_cmpx_eq_u32_partial_wave_none_pass(self):
|
||||
"""V_CMPX_EQ_U32 where no active lanes pass."""
|
||||
instructions = [
|
||||
v_cmpx_eq_u32_e32(99, v[255]), # EXEC = lanes where lane_id == 99
|
||||
]
|
||||
st = run_program(instructions, n_lanes=3)
|
||||
# No lanes should be active
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x0,
|
||||
"No lanes should be active when none pass")
|
||||
|
||||
def test_v_cmpx_f32_partial_wave(self):
|
||||
"""V_CMPX_GT_F32 with partial wavefront - float comparison."""
|
||||
instructions = [
|
||||
v_cvt_f32_u32_e32(v[0], v[255]), # v[0] = float(lane_id)
|
||||
v_mov_b32_e32(v[1], f2i(0.5)), # v[1] = 0.5
|
||||
v_cmpx_gt_f32_e32(v[0], v[1]), # EXEC = lanes where v[0] > 0.5
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
# Lanes 1,2,3 have values > 0.5, lane 0 has 0.0
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0xE,
|
||||
"Lanes 1,2,3 should be active (float > 0.5)")
|
||||
|
||||
def test_v_cmpx_e64_partial_wave(self):
|
||||
"""V_CMPX_EQ_U32_E64 (VOP3 encoding) with partial wavefront."""
|
||||
instructions = [
|
||||
v_cmpx_eq_u32_e64(EXEC_LO, v[255], 2), # EXEC = lanes where lane_id == 2
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x4,
|
||||
"Only lane 2 should be active after v_cmpx_eq_u32_e64")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
201
tinygrad_repo/test/amd/hw/test_vopd.py
Normal file
201
tinygrad_repo/test/amd/hw/test_vopd.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""Tests for VOPD instructions - dual-issue vector operations.
|
||||
|
||||
VOPD executes two operations simultaneously. Key behavior:
|
||||
- Both ops read their sources BEFORE either writes (dual-issue semantics)
|
||||
- This means if X writes to a register that Y reads, Y sees the OLD value
|
||||
- Op X can use ops 0-15 (FMAC, MUL, ADD, MOV, etc.)
|
||||
- Op Y can use ops 0-18 (includes ADD_NC_U32, LSHLREV, AND)
|
||||
"""
|
||||
import unittest
|
||||
from test.amd.hw.helpers import run_program, v, v_mov_b32_e32
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import VOPD, VOPD_LIT, VOPDOp
|
||||
|
||||
class TestVOPDBasic(unittest.TestCase):
|
||||
"""Basic VOPD functionality tests."""
|
||||
|
||||
def test_vopd_dual_mov(self):
|
||||
"""VOPD with two MOV operations to different registers."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x12345678),
|
||||
v_mov_b32_e32(v[1], 0xDEADBEEF),
|
||||
# X: v[2] = v[0], Y: v[3] = v[1]
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[1], v[0], v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x12345678)
|
||||
self.assertEqual(st.vgpr[0][3], 0xDEADBEEF)
|
||||
|
||||
def test_vopd_mov_and_add(self):
|
||||
"""VOPD with MOV (X) and ADD_NC_U32 (Y) - ADD_NC_U32 can only be Y op."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 10),
|
||||
v_mov_b32_e32(v[1], 5),
|
||||
# X: v[2] = 100 (literal), Y: v[3] = v[0] + v[1] = 15
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[3], 100, v[0], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 100)
|
||||
self.assertEqual(st.vgpr[0][3], 15)
|
||||
|
||||
|
||||
class TestVOPDReadBeforeWrite(unittest.TestCase):
|
||||
"""Tests for VOPD dual-issue read-before-write semantics.
|
||||
|
||||
In VOPD, both X and Y operations read their sources BEFORE either writes.
|
||||
This is critical when X's destination is Y's source.
|
||||
"""
|
||||
|
||||
def test_vopd_x_writes_y_reads_same_reg(self):
|
||||
"""VOPD where X writes to a register that Y reads.
|
||||
|
||||
X: v[2] = 0 (overwrites v[2])
|
||||
Y: v[1] = v[2] + v[0] (srcy0=v[2], vsrcy1=v[0])
|
||||
|
||||
If reads happen before writes: v[1] = OLD_v[2] + v[0] = 0xFFFFFFFF + 1 = 0
|
||||
If writes happen before reads: v[1] = 0 + v[0] = 0 + 1 = 1
|
||||
|
||||
Hardware does reads-before-writes, so v[1] should be 0.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 1), # v[0] = 1
|
||||
v_mov_b32_e32(v[1], 0x99999999), # v[1] = placeholder (will be overwritten)
|
||||
v_mov_b32_e32(v[2], 0xFFFFFFFF), # v[2] = 0xFFFFFFFF
|
||||
# X: v[2] = 0 (literal), srcx0=0, vsrcx1=v[0] (unused for MOV)
|
||||
# Y: v[1] = srcy0 + vsrcy1 = v[2] + v[0] (should read OLD v[2] = 0xFFFFFFFF)
|
||||
# vdsty encoding: (vdsty << 1) | ((vdstx & 1) ^ 1) where vdsty field = 0, vdstx = v[2]
|
||||
# So vdsty_reg = (0 << 1) | ((2 & 1) ^ 1) = 0 | 1 = 1 = v[1]
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 0, v[2], v[0], v[0]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# X should have written 0 to v[2]
|
||||
self.assertEqual(st.vgpr[0][2], 0, "X should write 0 to v[2]")
|
||||
# Y should have read OLD v[2] (0xFFFFFFFF) and added v[0] (1)
|
||||
# 0xFFFFFFFF + 1 = 0 (wrap around)
|
||||
self.assertEqual(st.vgpr[0][1], 0, "Y should read OLD v[2]=0xFFFFFFFF, compute 0xFFFFFFFF+1=0")
|
||||
|
||||
def test_vopd_x_writes_y_reads_same_reg_v2(self):
|
||||
"""VOPD where X writes to a register that Y reads - cleaner test case.
|
||||
|
||||
X: v[2] = 0 (MOV)
|
||||
Y: v[1] = v[2] + v[2] (ADD_NC_U32 with both sources from v[2])
|
||||
|
||||
If reads happen before writes: v[1] = OLD_v[2] + OLD_v[2] = 100 + 100 = 200
|
||||
If writes happen before reads: v[1] = 0 + 0 = 0
|
||||
|
||||
Hardware does reads-before-writes, so v[1] should be 200.
|
||||
"""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x88888888), # v[0] = unused placeholder
|
||||
v_mov_b32_e32(v[1], 0x99999999), # v[1] = placeholder (will be overwritten)
|
||||
v_mov_b32_e32(v[2], 100), # v[2] = 100
|
||||
# X: v[2] = 0 (literal)
|
||||
# Y: v[1] = srcy0 + vsrcy1 = v[2] + v[2] (should read OLD v[2] = 100)
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 0, v[2], v[0], v[2]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
# X should have written 0 to v[2]
|
||||
self.assertEqual(st.vgpr[0][2], 0, "X should write 0 to v[2]")
|
||||
# Y should have read OLD v[2] (100) twice and added them
|
||||
self.assertEqual(st.vgpr[0][1], 200, "Y should read OLD v[2]=100 twice, compute 100+100=200")
|
||||
|
||||
|
||||
class TestVOPDLiterals(unittest.TestCase):
|
||||
"""Tests for VOPD instructions that use SIMM32 literals (FMAAK, FMAMK)."""
|
||||
|
||||
def test_vopd_fmaak_f32(self):
|
||||
"""VOPD V_DUAL_FMAAK_F32: D = S0 * S1 + SIMM32 (literal addend).
|
||||
|
||||
Tests that the 32-bit literal (SIMM32) is correctly passed to the instruction.
|
||||
fma(2.0, 3.0, 10.0) = 2*3 + 10 = 16.0
|
||||
"""
|
||||
from test.amd.hw.helpers import f2i, i2f
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], f2i(2.0)), # v[0] = 2.0
|
||||
v_mov_b32_e32(v[1], f2i(3.0)), # v[1] = 3.0
|
||||
# VOPD args: opx, opy, vdstx, vdsty, srcx0, srcy0, vsrcx1, vsrcy1
|
||||
# X: v[2] = fma(srcx0, vsrcx1, SIMM32) = v[0]*v[1]+10.0 = 2*3+10 = 16
|
||||
# Y: v[3] = srcy0 (MOV) = v[0] = 2.0
|
||||
VOPD_LIT(VOPDOp.V_DUAL_FMAAK_F32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[0], v[1], v[0], literal=f2i(10.0)),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 16.0, places=5, msg="fma(2.0, 3.0, 10.0) should be 16.0")
|
||||
|
||||
def test_vopd_fmamk_f32(self):
|
||||
"""VOPD V_DUAL_FMAMK_F32: D = S0 * SIMM32 + S1 (literal multiplier).
|
||||
|
||||
Tests that the 32-bit literal (SIMM32) is correctly used as the multiplier.
|
||||
fma(2.0, 5.0, 3.0) = 2*5 + 3 = 13.0
|
||||
"""
|
||||
from test.amd.hw.helpers import f2i, i2f
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], f2i(2.0)), # v[0] = 2.0
|
||||
v_mov_b32_e32(v[1], f2i(3.0)), # v[1] = 3.0
|
||||
# X: v[2] = fma(srcx0, SIMM32, vsrcx1) = v[0]*5.0+v[1] = 2*5+3 = 13
|
||||
# Y: v[3] = srcy0 (MOV) = v[0] = 2.0
|
||||
VOPD_LIT(VOPDOp.V_DUAL_FMAMK_F32, VOPDOp.V_DUAL_MOV_B32, v[2], v[3], v[0], v[0], v[1], v[0], literal=f2i(5.0)),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][2]), 13.0, places=5, msg="fma(2.0, 5.0, 3.0) should be 13.0")
|
||||
|
||||
|
||||
class TestVOPDDot2Acc(unittest.TestCase):
|
||||
"""Tests for V_DUAL_DOT2ACC_F32_F16 - packed f16 dot product accumulate."""
|
||||
|
||||
def test_vopd_dot2acc_f32_f16_basic(self):
|
||||
"""V_DUAL_DOT2ACC_F32_F16: D += lo(S0)*lo(S1) + hi(S0)*hi(S1).
|
||||
|
||||
S0 = pack(1.0h, 2.0h), S1 = pack(3.0h, 4.0h), D = 10.0f
|
||||
result = 10.0 + 1.0*3.0 + 2.0*4.0 = 10.0 + 3.0 + 8.0 = 21.0
|
||||
"""
|
||||
from test.amd.hw.helpers import f2i, i2f, f32_to_f16
|
||||
pk_s0 = f32_to_f16(1.0) | (f32_to_f16(2.0) << 16) # lo=1.0h, hi=2.0h
|
||||
pk_s1 = f32_to_f16(3.0) | (f32_to_f16(4.0) << 16) # lo=3.0h, hi=4.0h
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], pk_s0),
|
||||
v_mov_b32_e32(v[1], pk_s1),
|
||||
v_mov_b32_e32(v[3], f2i(10.0)), # accumulator in v[3] (vdsty with vdstx=v[4])
|
||||
# X: v[4] = MOV v[0] (don't care), Y: v[3] += dot2(v[0], v[1])
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_DOT2ACC_F32_F16, v[4], v[3], v[0], v[0], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][3]), 21.0, places=2, msg="10.0 + 1.0*3.0 + 2.0*4.0 = 21.0")
|
||||
|
||||
def test_vopd_dot2acc_f32_f16_zero_accum(self):
|
||||
"""V_DUAL_DOT2ACC_F32_F16 with zero accumulator — pure dot product.
|
||||
|
||||
S0 = pack(0.5h, -1.0h), S1 = pack(2.0h, 3.0h), D = 0.0f
|
||||
result = 0.0 + 0.5*2.0 + (-1.0)*3.0 = 1.0 - 3.0 = -2.0
|
||||
"""
|
||||
from test.amd.hw.helpers import f2i, i2f, f32_to_f16
|
||||
pk_s0 = f32_to_f16(0.5) | (f32_to_f16(-1.0) << 16)
|
||||
pk_s1 = f32_to_f16(2.0) | (f32_to_f16(3.0) << 16)
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], pk_s0),
|
||||
v_mov_b32_e32(v[1], pk_s1),
|
||||
v_mov_b32_e32(v[3], f2i(0.0)), # zero accumulator in v[3]
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_DOT2ACC_F32_F16, v[4], v[3], v[0], v[0], v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertAlmostEqual(i2f(st.vgpr[0][3]), -2.0, places=2, msg="0.5*2.0 + (-1.0)*3.0 = -2.0")
|
||||
|
||||
|
||||
class TestVOPDMultilane(unittest.TestCase):
|
||||
"""Tests for VOPD with multiple lanes."""
|
||||
|
||||
def test_vopd_multilane_mov_add(self):
|
||||
"""VOPD MOV and ADD with multiple active lanes - no register conflict."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 5),
|
||||
v_mov_b32_e32(v[1], 10),
|
||||
# X: v[2] = 100 (constant), Y: v[1] = v[0] + v[1] = 5 + 10 = 15
|
||||
# vdsty_reg = (vdsty << 1) | ((vdstx.offset & 1) ^ 1) = (0 << 1) | ((258 & 1) ^ 1) = 0 | 1 = 1
|
||||
VOPD(VOPDOp.V_DUAL_MOV_B32, VOPDOp.V_DUAL_ADD_NC_U32, v[2], v[0], 100, v[0], v[2], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=4)
|
||||
for lane in range(4):
|
||||
self.assertEqual(st.vgpr[lane][2], 100, f"Lane {lane}: v[2] should be 100")
|
||||
self.assertEqual(st.vgpr[lane][1], 15, f"Lane {lane}: v[1] should be 15 (5+10)")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user