mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-29 17:36:07 +00:00
look ahead
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the RDNA assembly renderer"""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from tinygrad import Tensor, dtypes, Device
|
||||
from tinygrad.helpers import getenv
|
||||
|
||||
# Skip tests if not on AMD RDNA device
|
||||
AMD_RDNA = getenv("AMD_RDNA", 0)
|
||||
|
||||
@unittest.skipUnless(AMD_RDNA, "AMD_RDNA=1 required")
|
||||
class TestRDNABasic(unittest.TestCase):
|
||||
"""Basic functionality tests"""
|
||||
|
||||
def test_basic_half_load(self):
|
||||
"""Test basic half-precision load and sum"""
|
||||
a = Tensor(np.arange(16, dtype=np.float16).reshape(1, 16))
|
||||
b = a.sum()
|
||||
b.realize()
|
||||
np.testing.assert_allclose(b.numpy(), np.arange(16).sum(), rtol=1e-2)
|
||||
|
||||
def test_basic_float_load(self):
|
||||
"""Test basic float load and sum"""
|
||||
a = Tensor(np.arange(16, dtype=np.float32).reshape(1, 16))
|
||||
b = a.sum()
|
||||
b.realize()
|
||||
np.testing.assert_allclose(b.numpy(), np.arange(16).sum(), rtol=1e-5)
|
||||
|
||||
def test_elementwise_add(self):
|
||||
"""Test elementwise addition"""
|
||||
a = Tensor([1.0, 2.0, 3.0, 4.0])
|
||||
b = Tensor([5.0, 6.0, 7.0, 8.0])
|
||||
c = a + b
|
||||
c.realize()
|
||||
np.testing.assert_allclose(c.numpy(), [6.0, 8.0, 10.0, 12.0])
|
||||
|
||||
def test_elementwise_mul(self):
|
||||
"""Test elementwise multiplication"""
|
||||
a = Tensor([1.0, 2.0, 3.0, 4.0])
|
||||
b = Tensor([2.0, 2.0, 2.0, 2.0])
|
||||
c = a * b
|
||||
c.realize()
|
||||
np.testing.assert_allclose(c.numpy(), [2.0, 4.0, 6.0, 8.0])
|
||||
|
||||
@unittest.skipUnless(AMD_RDNA, "AMD_RDNA=1 required")
|
||||
class TestRDNAWMMA(unittest.TestCase):
|
||||
"""WMMA tensor core tests"""
|
||||
|
||||
def test_wmma_16x16_correctness(self):
|
||||
"""Test 16x16 WMMA correctness"""
|
||||
a = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
b = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
c_np = a.numpy().astype(np.float32) @ b.numpy().astype(np.float32)
|
||||
np.testing.assert_allclose(c.numpy(), c_np, rtol=1e-2, atol=1e-2)
|
||||
|
||||
def test_wmma_32x32_correctness(self):
|
||||
"""Test 32x32 WMMA correctness"""
|
||||
a = Tensor(np.random.randn(32, 32).astype(np.float16))
|
||||
b = Tensor(np.random.randn(32, 32).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
c_np = a.numpy().astype(np.float32) @ b.numpy().astype(np.float32)
|
||||
np.testing.assert_allclose(c.numpy(), c_np, rtol=1e-2, atol=1e-2)
|
||||
|
||||
@unittest.skipUnless(AMD_RDNA, "AMD_RDNA=1 required")
|
||||
class TestRDNAKernelGeneration(unittest.TestCase):
|
||||
"""Tests for kernel code generation"""
|
||||
|
||||
def test_wmma_kernel_runs(self):
|
||||
"""Test that WMMA kernel runs without error"""
|
||||
# If tensor cores are used, this matmul should run successfully
|
||||
a = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
b = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
c.realize()
|
||||
# Check result is reasonable
|
||||
self.assertEqual(c.shape, (16, 16))
|
||||
self.assertFalse(np.any(np.isnan(c.numpy())))
|
||||
|
||||
def test_larger_wmma_kernel_runs(self):
|
||||
"""Test that larger WMMA kernel (multiple tiles) runs"""
|
||||
a = Tensor(np.random.randn(32, 32).astype(np.float16))
|
||||
b = Tensor(np.random.randn(32, 32).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
c.realize()
|
||||
self.assertEqual(c.shape, (32, 32))
|
||||
self.assertFalse(np.any(np.isnan(c.numpy())))
|
||||
|
||||
@unittest.skipUnless(AMD_RDNA, "AMD_RDNA=1 required")
|
||||
class TestRDNAVGPRLimits(unittest.TestCase):
|
||||
"""Tests documenting VGPR limits"""
|
||||
|
||||
@unittest.skip("64x64 WMMA exceeds 256 VGPR limit - needs further register allocator improvements")
|
||||
def test_wmma_64x64_vgpr_limit(self):
|
||||
"""64x64 WMMA currently exceeds VGPR limit.
|
||||
Look-ahead packing helps but additional optimizations needed for larger matrices.
|
||||
"""
|
||||
a = Tensor(np.random.randn(64, 64).astype(np.float16))
|
||||
b = Tensor(np.random.randn(64, 64).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
c.realize()
|
||||
|
||||
class TestLookAheadPacking(unittest.TestCase):
|
||||
"""Tests for the look-ahead packing optimization - these run without device"""
|
||||
|
||||
def test_half16_const_packing(self):
|
||||
"""Test that half16 VECTORIZE with constants generates pack instructions"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Build a simple test: 16 scalar half constants -> half16 VECTORIZE
|
||||
# This tests the basic VECTORIZE packing logic (not look-ahead, but verifies packing works)
|
||||
half_vals = [UOp.const(dtypes.half, float(i)) for i in range(16)]
|
||||
vec = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(half_vals))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec,))
|
||||
|
||||
# Render and check for v_pack_b32_f16 instructions
|
||||
uops = list(sink.toposort())
|
||||
asm = renderer.render(uops)
|
||||
|
||||
# Should have v_pack_b32_f16 instructions for packing pairs of halfs
|
||||
pack_count = asm.count('v_pack_b32_f16')
|
||||
self.assertGreater(pack_count, 0, "Should have v_pack_b32_f16 instructions for half16 packing")
|
||||
# Should have 8 pack instructions (16 halfs / 2 per pack)
|
||||
self.assertEqual(pack_count, 8, f"Should have exactly 8 pack instructions, got {pack_count}")
|
||||
|
||||
def test_half16_load_packing_with_index(self):
|
||||
"""Test that half16 VECTORIZE with LOADs generates pack instructions and uses look-ahead"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Create a minimal kernel with half LOADs feeding half16 VECTORIZE
|
||||
# This simulates what WMMA needs for input data
|
||||
|
||||
# Create buffer argument using .ptr() method
|
||||
buf = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=0)
|
||||
|
||||
# Create index (workitem ID)
|
||||
ridx = UOp(Ops.SPECIAL, dtypes.int, (), ("ridx0", 32))
|
||||
|
||||
# Create 16 LOADs at different offsets
|
||||
loads = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i)
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf.dtype, (buf, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads.append(load)
|
||||
|
||||
# Create half16 VECTORIZE
|
||||
vec = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec,))
|
||||
|
||||
uops = list(sink.toposort())
|
||||
asm = renderer.render(uops)
|
||||
|
||||
# Should have v_pack_b32_f16 instructions
|
||||
pack_count = asm.count('v_pack_b32_f16')
|
||||
self.assertGreater(pack_count, 0, "Should have v_pack_b32_f16 instructions")
|
||||
|
||||
# Should have 8 pack instructions total (16 halfs / 2 per pack)
|
||||
self.assertEqual(pack_count, 8, f"Should have exactly 8 pack instructions, got {pack_count}")
|
||||
|
||||
# Check that LOADs are generated (global_load_u16 for half precision)
|
||||
load_count = asm.count('global_load_u16')
|
||||
self.assertGreater(load_count, 0, "Should have global_load_u16 instructions")
|
||||
# Should have 16 loads (one per half element)
|
||||
self.assertEqual(load_count, 16, f"Should have 16 load instructions, got {load_count}")
|
||||
|
||||
def test_look_ahead_packing_pre_scan(self):
|
||||
"""Test that the pre-scan correctly identifies half16 VECTORIZE sources"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
# Create a minimal kernel with half LOADs feeding half16 VECTORIZE
|
||||
buf = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=0)
|
||||
ridx = UOp(Ops.SPECIAL, dtypes.int, (), ("ridx0", 32))
|
||||
|
||||
# Create 16 LOADs
|
||||
loads = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i)
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf.dtype, (buf, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads.append(load)
|
||||
|
||||
vec = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec,))
|
||||
|
||||
uops = list(sink.toposort())
|
||||
|
||||
# Simulate the pre-scan logic from the renderer
|
||||
half16_vectorize_sources = {}
|
||||
for u in uops:
|
||||
if u.op is Ops.VECTORIZE and u.dtype.scalar() == dtypes.half and u.dtype.count == 16:
|
||||
for pos, src in enumerate(u.src):
|
||||
half16_vectorize_sources[src] = (u, pos)
|
||||
|
||||
# All 16 LOADs should be identified as half16 sources
|
||||
load_count = sum(1 for u in uops if u.op is Ops.LOAD and u in half16_vectorize_sources)
|
||||
self.assertEqual(load_count, 16, f"All 16 LOADs should be identified as half16 sources, got {load_count}")
|
||||
|
||||
def test_look_ahead_packing_is_interleaved(self):
|
||||
"""Test that pack instructions are interleaved with loads (not all at the end)"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Create a minimal kernel with half LOADs feeding half16 VECTORIZE
|
||||
buf = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=0)
|
||||
ridx = UOp(Ops.SPECIAL, dtypes.int, (), ("ridx0", 32))
|
||||
|
||||
# Create 16 LOADs
|
||||
loads = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i)
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf.dtype, (buf, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads.append(load)
|
||||
|
||||
vec = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec,))
|
||||
|
||||
uops = list(sink.toposort())
|
||||
asm = renderer.render(uops)
|
||||
lines = asm.split('\n')
|
||||
|
||||
# Find line numbers of loads and packs
|
||||
load_lines = [i for i, l in enumerate(lines) if 'global_load_u16' in l]
|
||||
pack_lines = [i for i, l in enumerate(lines) if 'v_pack_b32_f16' in l]
|
||||
|
||||
self.assertGreater(len(load_lines), 0, "Should have load instructions")
|
||||
self.assertGreater(len(pack_lines), 0, "Should have pack instructions")
|
||||
|
||||
# The first pack should occur BEFORE the last load (interleaving)
|
||||
first_pack = min(pack_lines)
|
||||
last_load = max(load_lines)
|
||||
self.assertLess(first_pack, last_load,
|
||||
f"First pack (line {first_pack}) should be before last load (line {last_load}) for interleaving")
|
||||
|
||||
def test_vgpr_reuse_in_look_ahead_packing(self):
|
||||
"""Test that temp VGPRs are reused after packing (reducing register pressure)"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.dtype import dtypes
|
||||
import re
|
||||
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Create a minimal kernel with half LOADs feeding half16 VECTORIZE
|
||||
buf = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=0)
|
||||
ridx = UOp(Ops.SPECIAL, dtypes.int, (), ("ridx0", 32))
|
||||
|
||||
# Create 16 LOADs
|
||||
loads = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i)
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf.dtype, (buf, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads.append(load)
|
||||
|
||||
vec = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec,))
|
||||
|
||||
uops = list(sink.toposort())
|
||||
asm = renderer.render(uops)
|
||||
|
||||
# Extract VGPR count from metadata
|
||||
vgpr_match = re.search(r'\.amdhsa_next_free_vgpr (\d+)', asm)
|
||||
self.assertIsNotNone(vgpr_match, "Should have .amdhsa_next_free_vgpr in metadata")
|
||||
vgpr_count = int(vgpr_match.group(1))
|
||||
|
||||
# With look-ahead packing and VGPR reuse:
|
||||
# - 8 VGPRs for the half16 destination range
|
||||
# - A few temp VGPRs for loading (reused)
|
||||
# - A few VGPRs for address computation
|
||||
# Without reuse, we'd need 8 + 16 = 24 VGPRs minimum
|
||||
# With reuse, we should need significantly fewer
|
||||
# The generated code shows temp VGPRs v6 and v16 are reused
|
||||
self.assertLess(vgpr_count, 24, f"VGPR count should be < 24 with reuse, got {vgpr_count}")
|
||||
|
||||
def test_multiple_half16_vectorizes(self):
|
||||
"""Test look-ahead packing with multiple half16 VECTORIZEs (like WMMA A and B inputs)"""
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.dtype import dtypes
|
||||
import re
|
||||
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Create two buffers (like A and B matrices)
|
||||
buf_a = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=0)
|
||||
buf_b = UOp(Ops.DEFINE_GLOBAL, dtypes.half.ptr(), arg=1)
|
||||
ridx = UOp(Ops.SPECIAL, dtypes.int, (), ("ridx0", 32))
|
||||
|
||||
# Create 16 LOADs from buf_a
|
||||
loads_a = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i)
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf_a.dtype, (buf_a, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads_a.append(load)
|
||||
|
||||
# Create 16 LOADs from buf_b
|
||||
loads_b = []
|
||||
for i in range(16):
|
||||
offset = UOp.const(dtypes.int, i + 100) # Different offsets
|
||||
idx = UOp(Ops.ADD, dtypes.int, (ridx, offset))
|
||||
index = UOp(Ops.INDEX, buf_b.dtype, (buf_b, idx))
|
||||
load = UOp(Ops.LOAD, dtypes.half, (index,))
|
||||
loads_b.append(load)
|
||||
|
||||
# Create two half16 VECTORIZEs
|
||||
vec_a = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads_a))
|
||||
vec_b = UOp(Ops.VECTORIZE, dtypes.half.vec(16), tuple(loads_b))
|
||||
sink = UOp(Ops.SINK, dtypes.void, (vec_a, vec_b))
|
||||
|
||||
uops = list(sink.toposort())
|
||||
asm = renderer.render(uops)
|
||||
|
||||
# Should have 16 pack instructions (8 for each half16)
|
||||
pack_count = asm.count('v_pack_b32_f16')
|
||||
self.assertEqual(pack_count, 16, f"Should have 16 pack instructions, got {pack_count}")
|
||||
|
||||
# Should have 32 loads total
|
||||
load_count = asm.count('global_load_u16')
|
||||
self.assertEqual(load_count, 32, f"Should have 32 load instructions, got {load_count}")
|
||||
|
||||
# Extract VGPR count
|
||||
vgpr_match = re.search(r'\.amdhsa_next_free_vgpr (\d+)', asm)
|
||||
self.assertIsNotNone(vgpr_match, "Should have .amdhsa_next_free_vgpr in metadata")
|
||||
vgpr_count = int(vgpr_match.group(1))
|
||||
|
||||
# With look-ahead packing and VGPR reuse:
|
||||
# - 16 VGPRs for the two half16 destination ranges (8 each)
|
||||
# - A few temp VGPRs for loading (reused)
|
||||
# Without reuse, we'd need 16 + 32 = 48 VGPRs minimum
|
||||
self.assertLess(vgpr_count, 48, f"VGPR count should be < 48 with reuse, got {vgpr_count}")
|
||||
|
||||
@unittest.skipUnless(AMD_RDNA, "AMD_RDNA=1 required")
|
||||
class TestLookAheadPackingOnDevice(unittest.TestCase):
|
||||
"""Tests for look-ahead packing that require actual device execution"""
|
||||
|
||||
def test_wmma_generates_pack_instructions(self):
|
||||
"""Test that WMMA kernel generates v_pack_b32_f16 instructions"""
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.uop.ops import Ops
|
||||
|
||||
# Create matmul that uses WMMA
|
||||
a = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
b = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
|
||||
sched = c.schedule()
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Find the WMMA kernel
|
||||
for item in sched:
|
||||
if hasattr(item, 'ast') and item.ast is not None:
|
||||
uops = list(item.ast.toposort())
|
||||
has_wmma = any(u.op == Ops.WMMA for u in uops)
|
||||
if has_wmma:
|
||||
asm = renderer.render(uops)
|
||||
pack_count = asm.count('v_pack_b32_f16')
|
||||
self.assertGreater(pack_count, 0, f"WMMA kernel should have v_pack_b32_f16 instructions")
|
||||
return
|
||||
|
||||
self.fail("Should find WMMA kernel in schedule")
|
||||
|
||||
def test_look_ahead_packing_reduces_temp_regs(self):
|
||||
"""Test that look-ahead packing packs halfs immediately after load"""
|
||||
from tinygrad.renderer.rdna import RDNARenderer
|
||||
from tinygrad.uop.ops import Ops
|
||||
|
||||
# Create matmul that uses WMMA
|
||||
a = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
b = Tensor(np.random.randn(16, 16).astype(np.float16))
|
||||
c = a.matmul(b, dtype=dtypes.float)
|
||||
|
||||
sched = c.schedule()
|
||||
renderer = RDNARenderer("gfx1100")
|
||||
|
||||
# Find and render the WMMA kernel
|
||||
for item in sched:
|
||||
if hasattr(item, 'ast') and item.ast is not None:
|
||||
uops = list(item.ast.toposort())
|
||||
has_wmma = any(u.op == Ops.WMMA for u in uops)
|
||||
if has_wmma:
|
||||
asm = renderer.render(uops)
|
||||
lines = asm.split('\n')
|
||||
|
||||
# Check that pack instructions appear after loads (look-ahead packing)
|
||||
# The pattern should be: global_load_d16_b16 ... then v_pack_b32_f16
|
||||
load_lines = [i for i, l in enumerate(lines) if 'global_load_d16_b16' in l]
|
||||
pack_lines = [i for i, l in enumerate(lines) if 'v_pack_b32_f16' in l]
|
||||
|
||||
# Some pack instructions should appear interleaved with loads
|
||||
# (i.e., packing happens as soon as pairs are loaded, not all at the end)
|
||||
if load_lines and pack_lines:
|
||||
# Find first pack after a load
|
||||
first_pack = min(pack_lines)
|
||||
last_load = max(load_lines)
|
||||
# Pack should happen before all loads are done (interleaved)
|
||||
self.assertLess(first_pack, last_load,
|
||||
"Look-ahead packing should interleave pack instructions with loads")
|
||||
return
|
||||
|
||||
self.fail("Should find WMMA kernel in schedule")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+92
-10
@@ -909,6 +909,28 @@ class RDNARenderer(Renderer):
|
||||
name = "test"
|
||||
pending_waits = set() # track which ops need waits
|
||||
|
||||
# === LOOK-AHEAD PACKING FOR HALF16 VECTORIZE ===
|
||||
# Pre-scan to find scalar half LOADs that will be packed into half16 VECTORIZE
|
||||
# This allows us to pack halfs immediately when loaded, reducing register pressure
|
||||
half16_vectorize_sources: dict[UOp, tuple[UOp, int]] = {} # source -> (vectorize_uop, position)
|
||||
half16_vectorize_ranges: dict[UOp, str] = {} # vectorize_uop -> allocated v[base:end] range (lazy allocated)
|
||||
half16_packed: dict[UOp, set[int]] = {} # vectorize_uop -> set of packed VGPR indices (0-7)
|
||||
half16_temp_regs: dict[UOp, str] = {} # source_uop -> temp VGPR holding the loaded value
|
||||
|
||||
for u in uops:
|
||||
if u.op is Ops.VECTORIZE and u.dtype.scalar() == dtypes.half and u.dtype.count == 16:
|
||||
# This is a half16 VECTORIZE for WMMA input
|
||||
# Map each source to its position
|
||||
for pos, src in enumerate(u.src):
|
||||
half16_vectorize_sources[src] = (u, pos)
|
||||
half16_packed[u] = set()
|
||||
|
||||
def get_half16_range(vec_uop: UOp) -> str:
|
||||
"""Lazily allocate the 8-VGPR range for a half16 VECTORIZE"""
|
||||
if vec_uop not in half16_vectorize_ranges:
|
||||
half16_vectorize_ranges[vec_uop] = alloc_vgpr_range(vec_uop, 8)
|
||||
return half16_vectorize_ranges[vec_uop]
|
||||
|
||||
for i, u in enumerate(uops):
|
||||
# Free registers that are no longer needed
|
||||
free_dead_regs(i)
|
||||
@@ -929,16 +951,27 @@ class RDNARenderer(Renderer):
|
||||
# For WMMA inputs (half16), we need contiguous packed VGPRs
|
||||
# half16 = 16 halfs = 8 VGPRs (2 halfs per 32-bit VGPR)
|
||||
if u.dtype.scalar() == dtypes.half and u.dtype.count == 16:
|
||||
# Allocate 8 contiguous VGPRs for packed half16 - use range allocator for reuse
|
||||
r[u] = alloc_vgpr_range(u, 8)
|
||||
base = int(r[u][2:r[u].index(':')])
|
||||
# Pack the source halfs into the destination VGPRs
|
||||
# Each VGPR holds 2 halfs: low 16 bits and high 16 bits
|
||||
for j in range(8):
|
||||
src_lo = r[u.src[j*2]]
|
||||
src_hi = r[u.src[j*2+1]]
|
||||
# Pack two halfs into one VGPR using v_pack_b32_f16
|
||||
kernel.append(f"v_pack_b32_f16 v{base+j}, {src_lo}, {src_hi}")
|
||||
# Check if we used look-ahead packing (range already allocated)
|
||||
if u in half16_vectorize_ranges:
|
||||
r[u] = half16_vectorize_ranges[u]
|
||||
base = int(r[u][2:r[u].index(':')])
|
||||
# Only pack VGPRs that weren't already packed by look-ahead
|
||||
for j in range(8):
|
||||
if j not in half16_packed.get(u, set()):
|
||||
src_lo = r[u.src[j*2]]
|
||||
src_hi = r[u.src[j*2+1]]
|
||||
kernel.append(f"v_pack_b32_f16 v{base+j}, {src_lo}, {src_hi}")
|
||||
else:
|
||||
# No look-ahead - allocate and pack as before
|
||||
r[u] = alloc_vgpr_range(u, 8)
|
||||
base = int(r[u][2:r[u].index(':')])
|
||||
# Pack the source halfs into the destination VGPRs
|
||||
# Each VGPR holds 2 halfs: low 16 bits and high 16 bits
|
||||
for j in range(8):
|
||||
src_lo = r[u.src[j*2]]
|
||||
src_hi = r[u.src[j*2+1]]
|
||||
# Pack two halfs into one VGPR using v_pack_b32_f16
|
||||
kernel.append(f"v_pack_b32_f16 v{base+j}, {src_lo}, {src_hi}")
|
||||
# For float8 (WMMA accumulator), check if sources are contiguous
|
||||
elif u.dtype.scalar() == dtypes.float and u.dtype.count == 8:
|
||||
# Check if all sources are from contiguous registers
|
||||
@@ -1050,6 +1083,11 @@ class RDNARenderer(Renderer):
|
||||
# Calculate number of 32-bit VGPRs needed
|
||||
vgpr_count = (u.dtype.itemsize + 3) // 4 # Round up to 32-bit chunks
|
||||
r[u] = alloc_vgpr_range(u, vgpr_count)
|
||||
elif u in half16_vectorize_sources:
|
||||
# Scalar half LOAD destined for half16 VECTORIZE - use look-ahead packing
|
||||
# Allocate a temp VGPR for the load, will pack immediately after
|
||||
r[u] = alloc_vgpr(u)
|
||||
half16_temp_regs[u] = r[u]
|
||||
else:
|
||||
r[u] = alloc_vgpr_pair(u) if needs_vgpr_pair(u.dtype) else alloc_vgpr(u)
|
||||
pending_waits.add(u)
|
||||
@@ -1182,6 +1220,50 @@ class RDNARenderer(Renderer):
|
||||
else:
|
||||
kernel.extend(l)
|
||||
|
||||
# === LOOK-AHEAD PACKING: pack halfs immediately after load ===
|
||||
if u.op is Ops.LOAD and u in half16_vectorize_sources:
|
||||
vec_uop, pos = half16_vectorize_sources[u]
|
||||
vgpr_idx = pos // 2 # Which VGPR in the 8-VGPR range (0-7)
|
||||
is_high_half = pos % 2 == 1 # Even positions are low half, odd are high half
|
||||
range_str = get_half16_range(vec_uop) # Lazy allocate if needed
|
||||
base = int(range_str[2:range_str.index(':')])
|
||||
dst_vgpr = f"v{base + vgpr_idx}"
|
||||
|
||||
# Find the pair (the other half that shares this destination VGPR)
|
||||
pair_pos = pos ^ 1 # XOR to toggle between even/odd
|
||||
pair_src = vec_uop.src[pair_pos]
|
||||
|
||||
# Check if pair is already loaded (its temp reg exists in half16_temp_regs and it's been processed)
|
||||
pair_loaded = pair_src in half16_temp_regs and pair_src in r
|
||||
|
||||
if pair_loaded:
|
||||
# Both halves ready - wait for loads and pack immediately
|
||||
kernel.append("s_waitcnt vmcnt(0)")
|
||||
pending_waits.discard(u)
|
||||
pending_waits.discard(pair_src)
|
||||
|
||||
# Determine which is low and which is high
|
||||
if is_high_half:
|
||||
lo_reg = half16_temp_regs[pair_src]
|
||||
hi_reg = half16_temp_regs[u]
|
||||
else:
|
||||
lo_reg = half16_temp_regs[u]
|
||||
hi_reg = half16_temp_regs[pair_src]
|
||||
|
||||
# Pack two halfs into destination VGPR
|
||||
kernel.append(f"v_pack_b32_f16 {dst_vgpr}, {lo_reg}, {hi_reg}")
|
||||
half16_packed[vec_uop].add(vgpr_idx)
|
||||
|
||||
# Immediately free the temp VGPRs since values are now in packed destination
|
||||
# This is critical for reducing register pressure
|
||||
for temp_src in [u, pair_src]:
|
||||
temp_reg = half16_temp_regs[temp_src]
|
||||
if temp_reg.startswith('v') and '[' not in temp_reg:
|
||||
reg_num = int(temp_reg[1:])
|
||||
if reg_num in vgpr_owner:
|
||||
del vgpr_owner[reg_num]
|
||||
free_vgprs.append(reg_num)
|
||||
|
||||
# Add wait after loads
|
||||
if u.op is Ops.DEFINE_GLOBAL:
|
||||
kernel.append("s_waitcnt lgkmcnt(0)")
|
||||
|
||||
Reference in New Issue
Block a user