mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-29 14:56:06 +00:00
Merge branch 'master' into qwen_mergable
This commit is contained in:
+68
-17
@@ -19,7 +19,7 @@ from tinygrad.runtime.support.hcq import FileIOInterface, HCQBuffer, MMIOInterfa
|
||||
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
|
||||
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
|
||||
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
|
||||
from tinygrad.runtime.support.usb import USB3
|
||||
from tinygrad.runtime.support.usb import USB3, usb_ib, usb_push, usb_arm_bytes, pm_usb_stage, pm_usb_hostio, pm_usb_bufferize
|
||||
from tinygrad.runtime.support.memory import AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.ops_amd import SQTT, SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE, PMC
|
||||
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_EQ, WAIT_REG_MEM_FUNCTION_NEQ, WAIT_REG_MEM_FUNCTION_GEQ
|
||||
@@ -146,11 +146,14 @@ pm_pm4_opsel = PatternMatcher([
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
|
||||
])
|
||||
|
||||
def queue_ptrs(devs, qname:str, q:AMDQueueDesc) -> tuple[UOp, ...]:
|
||||
return tuple(UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"{qname}_{n}")
|
||||
for n, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
|
||||
def pm4_submit(ctx, lin):
|
||||
# ensure compute queues are allocated
|
||||
for d in (devs:=ctx.devs): q = Device[d].compute_queue
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COMPUTE:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
ring, wptr, doorbell, put_ptr = queue_ptrs(devs, "COMPUTE:0", q)
|
||||
|
||||
# the host fence at the start of the batch guarantees the ib is free to reuse
|
||||
size_dw = sum(len(ins.src) for ins in lin.src)
|
||||
@@ -216,8 +219,7 @@ def sdma_submit(cmdbuf, devs):
|
||||
|
||||
# the sdma queue's ring and its host-side ring/write/put pointers
|
||||
for d in devs: q = Device[d].sdma_queue(0)
|
||||
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COPY:0_{name}")
|
||||
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
|
||||
ring, wptr, doorbell, put_ptr = queue_ptrs(devs, "COPY:0", q)
|
||||
|
||||
# sdma needs the cmdbuf contiguous: if it won't fit before the ring end, restart at 0 and zero the tail
|
||||
put_b = put_ptr.index(zero)
|
||||
@@ -244,15 +246,32 @@ def sdma_submit(cmdbuf, devs):
|
||||
pm_sdma_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
|
||||
lambda ctx, lin: sdma_submit(make_cmdbuf(lin, ctx.devs), ctx.devs))])
|
||||
|
||||
# *****************
|
||||
# USB submit
|
||||
|
||||
def amd_usb_submit(ctx, lin):
|
||||
for d in ctx.devs: q = Device[d].compute_queue if (comp:=ctx.qname.startswith("COMPUTE")) else Device[d].sdma_queue(0)
|
||||
|
||||
if nb:=usb_arm_bytes(ctx.pre, Device[ctx.devs[0]].iface.usb_sram):
|
||||
poke = (ctx.sdma.SDMA_OP_WRITE, *data64_le(Device[ctx.devs[0]].iface.cq_buf.va_addr + 12), 0, 0)
|
||||
lin = lin.replace(src=lin.src + (UOp(Ops.INS, arg="poke", src=tuple(UOp.const(x, dtypes.uint32) for x in poke)),))
|
||||
|
||||
ib_host, ib_gpu, pkt_dw = usb_ib(ctx.devs, lin, 32 if comp else 0x100, nb)
|
||||
pkt = (ctx.pm4.PACKET3(ctx.pm4.PACKET3_INDIRECT_BUFFER,2),*data64_le(ib_gpu.getaddr(ctx.devs)),pkt_dw|ctx.pm4.INDIRECT_BUFFER_VALID) if comp else ()
|
||||
return usb_push(ctx.devs, *queue_ptrs(ctx.devs, ctx.qname, q), ib_host, ib_gpu, pkt, 4 if comp else 1)
|
||||
|
||||
pm_usb_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), amd_usb_submit)])
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AMDEncodeCtx: # encode-time constants for one queue: devs (every cmdbuf address resolves into these) + gfx version + packet/ip modules
|
||||
devs: tuple[str, ...]; target: tuple[int, ...]; pm4: Any; sdma: Any; soc: Any # noqa: E702
|
||||
gc: AMDIP; nbio: AMDIP; xccs: int; max_copy_size: int; tmpring_size: Callable # noqa: E702
|
||||
gc: AMDIP; nbio: AMDIP; xccs: int; max_copy_size: int; tmpring_size: Callable; qname: str; pre: UOp # pre: the queue before opsel
|
||||
|
||||
def encode_queue(q:UOp) -> UOp|None:
|
||||
d = Device[(devs:=to_tuple(q.arg[0]))[0]]
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size)
|
||||
opsel, submit = (pm_pm4_opsel, pm_pm4_submit) if q.arg[1].startswith("COMPUTE") else (pm_sdma_opsel, pm_sdma_submit)
|
||||
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size, q.arg[1], q)
|
||||
opsel = pm_pm4_opsel if (comp:=q.arg[1].startswith("COMPUTE")) else pm_sdma_opsel
|
||||
submit = d.pm_submit if d.pm_submit is not None else (pm_pm4_submit if comp else pm_sdma_submit)
|
||||
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"), ctx)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -282,13 +301,14 @@ def amd_build_program(prg:UOp) -> UOp:
|
||||
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
|
||||
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
|
||||
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
|
||||
image = bytes(image).ljust(round_up(len(image), 4), b"\x00") # the program is uploaded as whole dwords
|
||||
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=prg.device).rtag("program")
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, bytes(image))),), arg=(data, prg.arg))
|
||||
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, image)),), arg=(data, prg.arg))
|
||||
return cached
|
||||
|
||||
class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
def __init__(self, dev:AMDDevice):
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb())
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb)
|
||||
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
|
||||
return self.dev.iface.alloc(size, host=options.host, uncached=options.uncached, cpu_access=options.cpu_access or not self.dev.has_copy_queue)
|
||||
@@ -524,8 +544,7 @@ class PCIIface(PCIIfaceBase):
|
||||
cq = d.compute_queue
|
||||
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
|
||||
d.iface.dev_impl.gfx.setup_ring(*cq.params)
|
||||
d.signal('timeline')._buf.cpu_view().mv.cast('Q')[0] = \
|
||||
d.signal('value', 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] - 1
|
||||
d.signal('timeline')._buf.cpu_view().view(fmt='Q')[0] = d.signal('value', 1, device="CPU")._buf.cpu_view().view(fmt='Q')[0] - 1
|
||||
|
||||
def sleep(self, timeout):
|
||||
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
|
||||
@@ -539,6 +558,32 @@ class PCIIface(PCIIfaceBase):
|
||||
|
||||
def device_fini(self): self.dev_impl.fini()
|
||||
|
||||
class USBIface(PCIIface):
|
||||
def __init__(self, dev, dev_id): # pylint: disable=super-init-not-called
|
||||
if dev_id >= len(visible:=hcq_filter_visible_devices(USB3.list_devices(0xADD1, 0x0001) + USB3.list_devices(0x3801, 0x0001), "AMD")):
|
||||
raise RuntimeError(f"AMD:{dev_id} does not exist ({pluralize('device', len(visible))} available)")
|
||||
self.dev, self.pci_dev, self.vram_bar, self.count = dev, USBPCIDevice("AM", *visible[dev_id]), 0, len(visible)
|
||||
self.dev_impl = AMDev(self.pci_dev)
|
||||
self._compute_props()
|
||||
self.sram = self._dma_region(ctrl_addr=0xf000, sys_addr=0x200000, size=0x80000)
|
||||
self.cq_buf = self._dma_region(ctrl_addr=0xb800, sys_addr=0x822000, size=0x1000) # +12 is the dword that releases an armed read
|
||||
self.usb_handle = unwrap(ctypes.cast(self.pci_dev.usb.usb.handle, ctypes.c_void_p).value)
|
||||
|
||||
def _dma_region(self, ctrl_addr, sys_addr, size):
|
||||
region = self.dev_impl.mm.map_range(vaddr:=self.dev_impl.mm.alloc_vaddr(size=size), size, [(sys_addr, size)], aspace=AddrSpace.SYS, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(region, has_cpu_mapping=False), view=self.pci_dev.dma_view(ctrl_addr, size), owner=self.dev)
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
# everything, even host-style signals, lives in vram: gpu writes into the bridge's own memory collide with an armed 0xF2 read stream
|
||||
return super().alloc(size, host=False, uncached=uncached, cpu_access=cpu_access or host, contiguous=contiguous, force_devmem=True, **kwargs)
|
||||
|
||||
def sleep(self, timeout): pass
|
||||
|
||||
# we don't own the sram region, so the buffer never frees it
|
||||
@functools.cached_property
|
||||
def usb_sram(self) -> Buffer:
|
||||
return Buffer(self.dev.device, (b:=self.sram).size, dtypes.uint8, options=BufferSpec(external_ptr=b.va_addr, nolru=True)).allocate(opaque=b)
|
||||
|
||||
def _mock(iface, name=None): return type(name or f"MOCK{iface.__name__}", (iface,), {})
|
||||
|
||||
class AMDDevice(HCQ2Compiled):
|
||||
@@ -549,19 +594,21 @@ class AMDDevice(HCQ2Compiled):
|
||||
# encoding of cmdbuf
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue),
|
||||
])
|
||||
pm_submit: PatternMatcher|None = None
|
||||
|
||||
timestamp_divider = 100.0 # AMD GPU clock: ticks/us
|
||||
max_scratch_psize = 0
|
||||
|
||||
ifaces = [KFDIface, PCIIface, _mock(KFDIface, "MOCKIface"), _mock(KFDIface), _mock(PCIIface)]
|
||||
ifaces = [KFDIface, PCIIface, USBIface, _mock(KFDIface, "MOCKIface"), _mock(KFDIface), _mock(PCIIface), _mock(USBIface)]
|
||||
|
||||
def device_props(self): return self.iface.props
|
||||
|
||||
def is_am(self) -> bool: return isinstance(self.iface, (PCIIface,))
|
||||
def is_usb(self) -> bool: return False
|
||||
|
||||
def __init__(self, device:str=""):
|
||||
self.iface = self._select_iface(device)
|
||||
self.is_usb = isinstance(self.iface, USBIface)
|
||||
if self.is_usb: self.rt_nbytes = 4 << 20
|
||||
|
||||
self.target:tuple[int, ...] = ((trgt:=self.iface.props['gfx_target_version']) // 10000, (trgt // 100) % 100, trgt % 100)
|
||||
self.arch = "gfx%d%x%x" % self.target
|
||||
@@ -586,7 +633,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
|
||||
self.is_aql = getenv("AMD_AQL", int(self.xccs > 1))
|
||||
if self.is_aql:
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb() else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ibs = self.iface.alloc(0x2000 if self.is_usb else (16 << 20), uncached=True, cpu_access=True)
|
||||
self.pm4_ib_alloc = BumpAllocator(self.pm4_ibs.size, wrap=True)
|
||||
|
||||
self.max_copy_size = 0x40000000 if self.iface.ip_versions[am.SDMA0_HWIP][0] >= 5 else 0x400000
|
||||
@@ -599,6 +646,10 @@ class AMDDevice(HCQ2Compiled):
|
||||
self.max_private_segment_size = 0
|
||||
self.pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, tag="scratch", name="b"), lambda ctx, b: ctx[0].scratch_buffer(b.max_numel()))]) + self.pm_bufferize
|
||||
|
||||
if self.is_usb:
|
||||
self.pm_bufferize = pm_usb_bufferize + self.pm_bufferize
|
||||
self.pm_stage_copy, self.pm_host_lower, self.pm_submit = pm_usb_stage, pm_usb_hostio, pm_usb_submit
|
||||
|
||||
self.pmc_enabled:bool = PROFILE > 0 and PMC > 0
|
||||
if self.pmc_enabled:
|
||||
self.iface.require_profile_mode()
|
||||
@@ -659,7 +710,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
wg_data_size = round_up((vgpr_size_per_cu + sgrp_size_per_cu + lds_size_per_cu + hwreg_size_per_cu) * self.cu_cnt, mmap.PAGESIZE)
|
||||
ctl_stack_size = round_up((12 if self.target[0] != 9 else 8) * self.wave_cnt + 8 + 40, mmap.PAGESIZE)
|
||||
return self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL if self.is_aql else kfd.KFD_IOC_QUEUE_TYPE_COMPUTE,
|
||||
0x2000 if self.is_usb() else (16 << 20), eop_buffer_size=0x1000,
|
||||
0x2000 if self.is_usb else (16 << 20), eop_buffer_size=0x1000,
|
||||
ctx_save_restore_size=0 if self.is_am() else wg_data_size + ctl_stack_size, ctl_stack_size=ctl_stack_size,
|
||||
debug_memory_size=round_up(self.wave_cnt * 32, 64))
|
||||
|
||||
@@ -667,7 +718,7 @@ class AMDDevice(HCQ2Compiled):
|
||||
if getenv("AMD_DISABLE_SDMA"): return None
|
||||
if idx in self.sdma_queues: return self.sdma_queues[idx]
|
||||
with contextlib.suppress(OSError):
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x200 if self.is_usb() else (16 << 20), idx=idx)
|
||||
self.sdma_queues[idx] = self.create_queue(kfd.KFD_IOC_QUEUE_TYPE_SDMA, 0x2000 if self.is_usb else (16 << 20), idx=idx)
|
||||
return self.sdma_queues.get(idx, None)
|
||||
|
||||
def tmpring_size(self, private_segment_size):
|
||||
|
||||
@@ -1002,6 +1002,39 @@ class TestBarrier(unittest.TestCase):
|
||||
for tid in range(64):
|
||||
self.assertEqual(st.vgpr[tid][0], tid + 100 + 1000, f"tid={tid}")
|
||||
|
||||
class TestSMaxMinSCCRegressions(unittest.TestCase):
|
||||
"""Regression test: S_MAX sets SCC only on strict inequality (equal operands -> SCC=0)."""
|
||||
|
||||
def test_s_max_i32_equal_scc(self):
|
||||
st = run_program([s_mov_b32(s[4], 64), s_mov_b32(s[5], 64), s_max_i32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 0)
|
||||
self.assertEqual(st.sgpr[6], 64)
|
||||
st = run_program([s_mov_b32(s[4], 65), s_mov_b32(s[5], 64), s_max_i32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 1) # still set when strictly greater
|
||||
|
||||
def test_s_max_u32_equal_scc(self):
|
||||
st = run_program([s_mov_b32(s[4], 64), s_mov_b32(s[5], 64), s_max_u32(s[6], s[4], s[5])], n_lanes=1)
|
||||
self.assertEqual(st.scc, 0)
|
||||
|
||||
class TestAbsdiffOverflowRegressions(unittest.TestCase):
|
||||
"""Regression test: S_ABSDIFF_I32 computes abs on the WRAPPED 32-bit difference (found by random difftest vs hardware)."""
|
||||
|
||||
def test_s_absdiff_wrapped(self):
|
||||
# |45 - (-2147483647)| overflows int32; hardware takes abs of the wrapped 32-bit difference
|
||||
instructions = [s_mov_b32(s[4], 45), s_mov_b32(s[5], 0x80000001), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0x7FFFFFD4)
|
||||
self.assertEqual(st.scc, 1)
|
||||
# INT_MIN - 1 wraps to +2147483647, already positive
|
||||
instructions = [s_mov_b32(s[4], 0x80000000), s_mov_b32(s[5], 1), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0x7FFFFFFF)
|
||||
# equality -> 0 and SCC=0
|
||||
instructions = [s_mov_b32(s[4], 7), s_mov_b32(s[5], 7), s_absdiff_i32(s[6], s[4], s[5])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.sgpr[6], 0)
|
||||
self.assertEqual(st.scc, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1629,5 +1629,66 @@ class TestSwap(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[0][1], 0x55555555)
|
||||
|
||||
|
||||
class TestCvtFrexpRegressions(unittest.TestCase):
|
||||
"""Regression tests for float<->int conversion and FREXP corner cases (found by random difftest vs hardware)."""
|
||||
|
||||
def test_cvt_i32_f32_nan_is_zero(self):
|
||||
"""v_cvt_i32_f32 of NaN is 0, not INT_MIN (x86 cvttss2si returns INT_MIN)."""
|
||||
for nan in (0x7FC00000, 0xFFC00000, 0x7F800001):
|
||||
st = run_program([v_mov_b32_e32(v[0], nan), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0, f"nan=0x{nan:08x}")
|
||||
|
||||
def test_cvt_i32_f32_positive_overflow(self):
|
||||
"""v_cvt_i32_f32 saturates positive overflow/inf to INT_MAX, not INT_MIN."""
|
||||
for bits in (0x7F800000, 0x4F000000, 0x4F800000): # +inf, 2^31, ~2^32
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x7FFFFFFF, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_cvt_i32_f32_negative_overflow(self):
|
||||
"""v_cvt_i32_f32 saturates negative overflow/-inf to INT_MIN."""
|
||||
for bits in (0xFF800000, 0xCF000001): # -inf, below -2^31
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_cvt_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0x80000000, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_cvt_u32_f32_nan_is_zero(self):
|
||||
"""v_cvt_u32_f32 of NaN is 0, not UINT_MAX."""
|
||||
for nan in (0x7FC00000, 0xFFC00000, 0x7F800001):
|
||||
st = run_program([v_mov_b32_e32(v[0], nan), v_cvt_u32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1], 0, f"nan=0x{nan:08x}")
|
||||
|
||||
def test_cvt_i32_f64_nan_and_overflow(self):
|
||||
"""v_cvt_i32_f64: NaN -> 0, positive overflow/+inf -> INT_MAX."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x7FF80000), v_cvt_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x41F00000), v_cvt_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x7FFFFFFF) # 2^32 -> INT_MAX
|
||||
|
||||
def test_frexp_f32_denormal(self):
|
||||
"""v_frexp_exp/mant_f32 of denormal/zero inputs is (0, signed zero) on hardware."""
|
||||
for bits in (0x00000001, 0x007FFFFF, 0x00000000):
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_frexp_exp_i32_f32_e32(v[1], v[0]), v_frexp_mant_f32_e32(v[2], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1] & 0xFFFFFFFF, 0, f"exp bits=0x{bits:08x}")
|
||||
self.assertEqual(st.vgpr[0][2], bits & 0x80000000, f"mant bits=0x{bits:08x}")
|
||||
# negative denormal: mant is -0.0
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_frexp_mant_f32_e32(v[2], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
|
||||
def test_frexp_f64_denormal(self):
|
||||
"""v_frexp_exp_f64 of a denormal returns the normalized exponent (-1073 for min-denormal); zero -> 0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 1), v_mov_b32_e32(v[1], 0), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2] & 0xFFFFFFFF, 0xFFFFFBCF) # -1073
|
||||
st = run_program([v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0)
|
||||
|
||||
def test_frexp_exp_inf_nan(self):
|
||||
"""v_frexp_exp of +/-inf and NaN is 0 on hardware (host frexp gives 129/1024), for both f32 and f64."""
|
||||
for bits in (0x7F800000, 0xFF800000, 0x7FC00000):
|
||||
st = run_program([v_mov_b32_e32(v[0], bits), v_frexp_exp_i32_f32_e32(v[1], v[0])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][1] & 0xFFFFFFFF, 0, f"f32 bits=0x{bits:08x}")
|
||||
for lo, hi in ((0, 0x7FF00000), (0, 0xFFF00000), (0, 0x7FF80000), (1, 0x7FF00000)):
|
||||
st = run_program([v_mov_b32_e32(v[0], lo), v_mov_b32_e32(v[1], hi), v_frexp_exp_i32_f64_e32(v[2], v[0:1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2] & 0xFFFFFFFF, 0, f"f64 bits=0x{hi:08x}{lo:08x}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -989,6 +989,53 @@ class TestCarryOps(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[0][0], 0) # 0xFFFFFFFF + 1 + 0 = 0 (overflow)
|
||||
self.assertEqual(st.vcc, 0xDEADBEEF) # VCC unchanged - carry was discarded
|
||||
|
||||
class TestSelectFlushRegressions(unittest.TestCase):
|
||||
"""Regression tests: f32 MIN/MAX flush denormal inputs to signed zero (select-style ops propagate inputs bitwise)."""
|
||||
|
||||
def test_v_min_f32_denormal_flush(self):
|
||||
"""min(denormal, 1.0) is +0, min(-denormal, -1.0) is -0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x3F800000), v_min_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
# flush(-denormal) = -0.0 > -1.0, so the result is -1.0 (both operand orders)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0xBF800000), v_min_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xBF800000)
|
||||
st = run_program([v_mov_b32_e32(v[1], 0xBF800000), v_mov_b32_e32(v[2], 0x80000001), v_min_f32_e32(v[3], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0xBF800000)
|
||||
|
||||
def test_v_max_f32_denormal_flush(self):
|
||||
"""max(-denormal, -1.0) is -0; max(+denormal, -0) is +0."""
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0xBF800000), v_max_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x80000000), v_max_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
|
||||
|
||||
class TestCarryExecRegressions(unittest.TestCase):
|
||||
"""Regression tests: per-lane VCC writes (carry ops) zero inactive lane bits - VCC = mask & EXEC, never preserved."""
|
||||
|
||||
def test_co_ci_e32_vcc_masked_by_exec(self):
|
||||
"""v_sub_co_ci_u32_e32 with EXEC=0xFFFF0000: hw clears inactive VCC bits instead of preserving them."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0xFFFF0000),
|
||||
s_mov_b32(VCC_LO, 0xFFFFFFFF), # preset all bits
|
||||
v_mov_b32_e32(v[0], 0xFFFFFFFE), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_sub_co_ci_u32_e32(v[2], v[0], v[1]), # active lanes: no borrow
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc, 0x00000000)
|
||||
|
||||
def test_co_ci_e32_vcc_masked_by_exec_ones(self):
|
||||
"""Same with all-ones carry: VCC = borrow_mask & EXEC."""
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x0F0F0F0F),
|
||||
s_mov_b32(VCC_LO, 0),
|
||||
v_mov_b32_e32(v[0], 0xFFFFFFFF), v_mov_b32_e32(v[1], 1),
|
||||
v_add_co_ci_u32_e32(v[2], v[0], v[1]), # all lanes would carry if active
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.vcc, 0x0F0F0F0F)
|
||||
self.assertEqual(st.vgpr[31][2], 0) # 0xFFFFFFFF + 1 wraps to 0 in active lanes
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -4,6 +4,7 @@ Includes: v_fma_f32, v_div_scale_f32, v_div_fmas_f32, v_div_fixup_f32,
|
||||
v_alignbit_b32, v_bfe_i32, v_mad_u64_u32, v_readlane_b32, v_writelane_b32
|
||||
"""
|
||||
import unittest
|
||||
from tinygrad.helpers import OSX
|
||||
from test.amd.hw.helpers import *
|
||||
|
||||
class TestFMA(unittest.TestCase):
|
||||
@@ -3264,6 +3265,23 @@ class TestVOP3ClampMAD(unittest.TestCase):
|
||||
# 0xFFFF * 2 = 0x1FFFE, low 16 bits = 0xFFFE
|
||||
self.assertEqual(st.vgpr[0][3] & 0xFFFF, 0xFFFE, f"expected 0xFFFE, got 0x{st.vgpr[0][3] & 0xFFFF:04x}")
|
||||
|
||||
class TestMadNarrowClampRegressions(unittest.TestCase):
|
||||
"""Regression tests: mad i16/i24 with clamp saturate to narrow output range (found by random difftest vs hardware)."""
|
||||
|
||||
def test_mad_i16_clamp_sat_max(self):
|
||||
# neg/src-floggled 16-bit mul operands are sign-extended after toggling bit15; sum > INT_MAX saturates
|
||||
instructions = [s_mov_b32(s[4], 1232348160), v_mov_b32_e32(v[3], 0x80000000),
|
||||
v_mov_b32_e32(v[1], 0x7F7FFFFF), v_mad_i32_i16(v[0], s[4], v[3], v[1], 0, 3, 5, 1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0x7FFFFFFF)
|
||||
|
||||
def test_mad_i24_clamp_sat_min(self):
|
||||
# sext24(-6344704) * sext24(+4210688) << -2^31 saturates to INT_MIN
|
||||
instructions = [s_mov_b32(s[7], 4290772992), v_mov_b32_e32(v[1], 1077936128),
|
||||
v_mad_i32_i24(v[0], s[7], v[1], v[1], 1, 0, 0, 1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][0], 0x80000000)
|
||||
|
||||
|
||||
class TestCvtPkF16(unittest.TestCase):
|
||||
"""Tests for V_CVT_PK_RTZ_F16_F32 - pack two f32 to f16 with round toward zero."""
|
||||
@@ -3651,6 +3669,80 @@ class TestPermlane(unittest.TestCase):
|
||||
self.assertEqual(st.vgpr[21][1], 5)
|
||||
self.assertEqual(st.vgpr[31][1], 15)
|
||||
|
||||
class TestClampLdExpRegressions(unittest.TestCase):
|
||||
"""Regression tests for f32 clamp (-0 -> +0) and ldexp input passthrough."""
|
||||
|
||||
def test_clamp_negative_zero(self):
|
||||
"""clmp=1 maps -0.0 to +0.0 (found by random difftest vs hardware)."""
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_add_f32_e64(v[2], v[0], v[1], clmp=1), # -0 + -0 = -0, clamp -> +0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
instructions = [
|
||||
v_mov_b32_e32(v[0], 0x3F800000), v_mov_b32_e32(v[1], 0x80000000),
|
||||
v_min_f32_e64(v[2], v[0], v[1], clmp=1), # min(1.0, -0) = -0, clamp -> +0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x00000000)
|
||||
|
||||
def test_ldexp_special_inputs(self):
|
||||
"""v_ldexp_f32 of 0/-0/inf/NaN propagates the input instead of computing val * 2**exp (0*inf = NaN on host)."""
|
||||
# -0.0 * 2^INT_MIN = -0.0 (src1 as integer exponent; huge negative)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 0x80000000), v_ldexp_f32(v[2], v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x80000000)
|
||||
# inf stays inf even with negative exponent
|
||||
instructions = [v_mov_b32_e32(v[0], 0x7F800000), v_mov_b32_e32(v[1], 0xFFFFFF80), v_ldexp_f32(v[2], v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x7F800000)
|
||||
|
||||
def test_ldexp_denormal_flush(self):
|
||||
"""v_ldexp_f32/f64 flush denormal inputs to signed zero (found by random difftest vs hardware)."""
|
||||
# ldexp(+denorm, 1) = +0, ldexp(-denorm, 250) = -0
|
||||
for src, exp_val, want in [(0x00000001, 1, 0x00000000), (0x80000001, 250, 0x80000000)]:
|
||||
st = run_program([v_mov_b32_e32(v[0], src), v_mov_b32_e32(v[1], exp_val), v_ldexp_f32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], want)
|
||||
|
||||
def test_v_mul_neg_modifier_nan_sign(self):
|
||||
"""neg modifier is a pure sign-bit toggle on a NaN operand; result keeps that sign (found by random difftest)."""
|
||||
# mul(normal, NEG(ABS(qNaN))): NaN payload negated in the operand stays negative qNaN
|
||||
instructions = [v_mov_b32_e32(v[0], 0xC96CF47F), v_mov_b32_e32(v[1], 0x7FC00000),
|
||||
v_mul_f32_e64(v[2], v[0], v[1], s[0], 0, 7, 6)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0xFFC00000)
|
||||
# plain neg modifier still applies to non-NaN values: mul(-1.0, NEG(2.0)) = +2.0
|
||||
st = run_program([v_mov_b32_e32(v[0], 0xBF800000), v_mov_b32_e32(v[1], 0x40000000),
|
||||
v_mul_f32_e64(v[2], v[0], v[1], s[0], 0, 2, 0)], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], 0x40000000)
|
||||
|
||||
|
||||
class TestNaNPropagationRegressions(unittest.TestCase):
|
||||
"""Regression tests: float arithmetic propagates a NaN from the FIRST NaN operand, quieted with its own sign/payload."""
|
||||
|
||||
@unittest.skipIf(OSX, "broken on mac, TODO: why?")
|
||||
def test_mul_nan_priority(self):
|
||||
# first NaN operand wins (sign+payload), not x86's second-source propagation
|
||||
for a, b, want in [(0x7FC00001, 0x7F800003, 0x7FC00001), (0xFFC00005, 0x7F800003, 0xFFC00005),
|
||||
(0x7F800001, 0xFFC00005, 0x7FC00001), (0xFF9F1800, 0x7F800001, 0xFFDF1800)]:
|
||||
st = run_program([v_mov_b32_e32(v[0], a), v_mov_b32_e32(v[1], b),
|
||||
v_mul_f32_e32(v[2], v[0], v[1])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][2], want, f"mul({a:#x}, {b:#x})")
|
||||
|
||||
class TestMinMaxFlushE64Regressions(unittest.TestCase):
|
||||
"""Regression tests: f32 min/max/median flush denormal inputs to signed zero (e64 forms)."""
|
||||
|
||||
def test_v_min3_f32_denormal_flush(self):
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x00000001), v_mov_b32_e32(v[1], 0x3F800000), v_mov_b32_e32(v[2], 0x40000000),
|
||||
v_min3_f32(v[3], v[0], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0x00000000) # min(+denorm, 1, 2) = +0
|
||||
|
||||
def test_v_med3_f32_denormal_flush(self):
|
||||
st = run_program([v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x3F800000), v_mov_b32_e32(v[2], 0x40000000),
|
||||
v_med3_f32(v[3], v[0], v[1], v[2])], n_lanes=1)
|
||||
self.assertEqual(st.vgpr[0][3], 0x3F800000) # med(-0, 1, 2) = 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -973,6 +973,71 @@ class TestCmpxPartialWavefront(unittest.TestCase):
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset] & 0xFFFFFFFF, 0x4,
|
||||
"Only lane 2 should be active after v_cmpx_eq_u32_e64")
|
||||
|
||||
class TestClassDenormalRegressions(unittest.TestCase):
|
||||
"""Regression tests: V_CMP_CLASS classifies denormals as DENORMAL (raw bits), not as zero class."""
|
||||
|
||||
def test_class_pos_denormal(self):
|
||||
for bits in (0x00000001, 0x007FFFFF):
|
||||
instructions = [v_mov_b32_e32(v[0], bits), v_mov_b32_e32(v[1], 0x80), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1, f"bits=0x{bits:08x}") # n_lanes=1
|
||||
# ...and it is not the zero class
|
||||
instructions = [v_mov_b32_e32(v[0], bits), v_mov_b32_e32(v[1], 0x40), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0, f"bits=0x{bits:08x}")
|
||||
|
||||
def test_class_neg_denormal(self):
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x10), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1) # n_lanes=1
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 0x20), v_cmp_class_f32_e64(VCC_LO, v[0], v[1])]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0) # not the negative-zero class
|
||||
|
||||
|
||||
class TestIntCmpModRegressions(unittest.TestCase):
|
||||
"""Regression tests: int compares (i32/u32) honor abs/neg as bit-level sign clear/flip (not integer abs/negate)."""
|
||||
|
||||
def test_cmp_i32_abs_neg_bit_level(self):
|
||||
# abs(0x80000001) = 1 -> 1 > 1 is false (integer abs would give 2147483647 > 1)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 1), v_cmp_gt_i32_e64(VCC_LO, v[0], v[1], abs=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0)
|
||||
# neg(0x80000001) flips the sign bit -> 1 > 2 is false (integer negate would give 2147483647 > 2)
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000001), v_mov_b32_e32(v[1], 2), v_cmp_gt_i32_e64(VCC_LO, v[0], v[1], neg=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 0)
|
||||
|
||||
def test_cmp_u32_abs_bit_level(self):
|
||||
# abs(0x80000000) = 0 -> 0 < 1 is true
|
||||
instructions = [v_mov_b32_e32(v[0], 0x80000000), v_mov_b32_e32(v[1], 1), v_cmp_lt_u32_e64(VCC_LO, v[0], v[1], abs=1)]
|
||||
st = run_program(instructions, n_lanes=1)
|
||||
self.assertEqual(st.vcc, 1) # n_lanes=1
|
||||
|
||||
|
||||
class TestCmpxSdstRegressions(unittest.TestCase):
|
||||
"""Regression tests: V_CMPX_*_E64 writes EXEC only, never SDST (hardware verified)."""
|
||||
|
||||
def test_cmpx_e64_no_sdst(self):
|
||||
instructions = [
|
||||
s_mov_b32(VCC_LO, 0), # preset VCC to 0
|
||||
v_mov_b32_e32(v[0], 0x3F800000), v_mov_b32_e32(v[1], 0x40000000),
|
||||
v_cmpx_lt_f32_e64(VCC_LO, v[0], v[1]), # 1.0 < 2.0
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset], 0xFFFFFFFF) # EXEC updated
|
||||
self.assertEqual(st.vcc, 0) # but VCC untouched
|
||||
|
||||
def test_cmpx_e64_partial_exec(self):
|
||||
instructions = [
|
||||
s_mov_b32(EXEC_LO, 0x0F0F0F0F),
|
||||
s_mov_b32(VCC_LO, 0xFFFFFFFF),
|
||||
v_mov_b32_e32(v[0], 0), v_mov_b32_e32(v[1], 0x3F800000),
|
||||
v_cmpx_lt_f32_e64(VCC_LO, v[0], v[1]),
|
||||
]
|
||||
st = run_program(instructions, n_lanes=32)
|
||||
self.assertEqual(st.sgpr[EXEC_LO.offset], 0x0F0F0F0F) # EXEC = computed & old EXEC
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+51
-18
@@ -69,7 +69,7 @@ from tinygrad.runtime.autogen.amd.cdna import ins as irc
|
||||
from tinygrad.renderer.amd.dsl import VCC_LO, EXEC_LO, SCC, ttmp, Inst
|
||||
from tinygrad.runtime.autogen.amd.common import Fmt, OpType
|
||||
from test.amd.helpers import decode_dpp16
|
||||
from test.mockgpu.amd.pcode import parse_pcode, _FUNCS, _set_bits, _to_bool, _to_u32, _val_to_bits
|
||||
from test.mockgpu.amd.pcode import parse_pcode, _FUNCS, _set_bits, _to_bool, _to_u32, _val_to_bits, _ftz_f32
|
||||
|
||||
MASK32 = 0xFFFFFFFF
|
||||
|
||||
@@ -96,7 +96,8 @@ def _apply_src_mods(val: UOp, mod_bit: int, abs_bits: int, neg_bits: int, bits:
|
||||
ut, ft, mask = _SRC_MOD_TYPES[bits]
|
||||
fv = val.cast(ut).bitcast(ft) if bits == 16 else val.bitcast(ft) if val.dtype == ut else val
|
||||
if abs_bits & (1 << mod_bit): fv = (fv.bitcast(ut) & UOp.const(mask, ut)).bitcast(ft)
|
||||
if neg_bits & (1 << mod_bit): fv = fv.neg()
|
||||
# neg modifier is a pure sign-bit toggle (preserves NaN payloads), not an arithmetic negate
|
||||
if neg_bits & (1 << mod_bit): fv = (fv.bitcast(ut) ^ UOp.const((mask + 1) & (1 << (bits - 1)), ut)).bitcast(ft)
|
||||
return fv.bitcast(ut).cast(dtypes.uint32) if bits == 16 else fv.bitcast(ut)
|
||||
|
||||
# Map VOPD ops to VOP2/VOP1 ops for pcode lookup (both RDNA3 and RDNA4 share these targets)
|
||||
@@ -157,6 +158,23 @@ _pcode_fixes = {
|
||||
'V_DIV_FIXUP_F64': ('D0.f64 = sign_out ? -abs(S0.f64) : abs(S0.f64)',
|
||||
'D0.f64 = isNAN(S0.f64) ? (sign_out ? -INF : +INF) : (sign_out ? -abs(S0.f64) : abs(S0.f64))'),
|
||||
'V_TRIG_PREOP_F64': ("result = 64'F((1201'B(2.0 / PI)[1200 : 0] << shift.u32) & 1201'0x1fffffffffffff)", "result = trig_preop_result(shift)"),
|
||||
# exponent() returns 0 for denormals; frexp_exp handles them per hardware (f32: 0, f64: normalized)
|
||||
'V_FREXP_EXP_I32_F32': ('D0.i32 = exponent(S0.f32) - 127 + 1', 'D0.i32 = frexp_exp(S0.f32)'),
|
||||
'V_FREXP_EXP_I32_F64': ('D0.i32 = exponent(S0.f64) - 1023 + 1', 'D0.i32 = frexp_exp(S0.f64)'),
|
||||
# route through ldexp() which propagates 0/inf/NaN inputs instead of computing val * 2**exp (0*inf = NaN on the host)
|
||||
'V_LDEXP_F32': ('D0.f32 = S0.f32 * 2.0F ** S1.i32', 'D0.f32 = ldexp(S0.f32, S1.i32)'),
|
||||
'V_LDEXP_F64': ('D0.f64 = S0.f64 * 2.0 ** S1.i32', 'D0.f64 = ldexp(S0.f64, S1.i32)'),
|
||||
# hardware sets SCC only on STRICT inequality for S_MAX (equal operands -> SCC=0)
|
||||
'S_MAX_I32': ('SCC = S0.i32 >= S1.i32', 'SCC = S0.i32 > S1.i32'),
|
||||
'S_MAX_U32': ('SCC = S0.u32 >= S1.u32', 'SCC = S0.u32 > S1.u32'),
|
||||
# hardware computes abs on the WRAPPED 32-bit difference; the i32 pcode overflows into UB on the host (e.g. |45 - -2147483647|),
|
||||
# so compute in u32 with a UB-free two's-complement negate
|
||||
'S_ABSDIFF_I32': ('D0.i32 = S0.i32 - S1.i32;\nif D0.i32 < 0 then\nD0.i32 = -D0.i32\nendif',
|
||||
'D0.u32 = S0.u32 - S1.u32;\nif D0.i32 < 0 then\nD0.u32 = -D0.u32\nendif'),
|
||||
# CLASS denormal test uses abs(x) > 0.0, which the host's DAZ flushes; use bit-domain test instead
|
||||
'V_CMP_CLASS_F32': ('64\'F(abs(S0.f32)) > 0.0', '(64\'U(S0.u32 & 0x7FFFFFFF) != 0)'),
|
||||
'V_CMP_CLASS_F16': ('64\'F(abs(S0.f16)) > 0.0', '(64\'U(S0.u32 & 0x7FFF) != 0)'),
|
||||
'V_CMP_CLASS_F64': ('64\'F(abs(S0.f64)) > 0.0', '(64\'U(S0.u64 & 0x7FFFFFFFFFFFFFFF) != 0)'),
|
||||
}
|
||||
|
||||
def _get_pcode_dict(op) -> dict:
|
||||
@@ -274,14 +292,24 @@ def _int_clamp(op_name: str, srcs: dict) -> UOp | None:
|
||||
if not isinstance(s0, UOp) or not isinstance(s1, UOp): return None
|
||||
is_signed, is_16bit = '_I' in op_name and '_U' not in op_name, '16' in op_name
|
||||
if any(p in op_name for p in ('_NC_U', '_MAD_U', '_NC_I', '_MAD_I')):
|
||||
if is_16bit and is_signed: return None # skip 16-bit signed ops due to codegen issues
|
||||
narrow_dt = dtypes.uint16 if is_16bit else (dtypes.int32 if is_signed else dtypes.uint32)
|
||||
wide_dt = dtypes.int32 if is_16bit else dtypes.int64
|
||||
narrow_max, narrow_min = (0xFFFF, 0) if is_16bit else ((0x7FFFFFFF, -0x80000000) if is_signed else (0xFFFFFFFF, 0))
|
||||
op_bits = 16 if '16' in op_name else (24 if '24' in op_name else 32)
|
||||
# D0 range: 16 for the *_U16/*_I16 result-narrow ops, else 32 (mad*32* D0 is u32/i32; mul operands have op-fmt width)
|
||||
narrow_dt = dtypes.uint16 if is_16bit and '32' not in op_name else (dtypes.int32 if is_signed else dtypes.uint32)
|
||||
wide_dt = dtypes.int64
|
||||
narrow_max, narrow_min = ((0xFFFF, 0) if narrow_dt == dtypes.uint16 else
|
||||
((0x7FFFFFFF, -0x80000000) if is_signed else (0xFFFFFFFF, 0)))
|
||||
def to_mulin(x: UOp) -> UOp: # mul-source: extract the op-fmt-width suboperand with sext for signed
|
||||
mask = (1 << op_bits) - 1
|
||||
if op_bits == 32: return x.bitcast(narrow_dt) if x.dtype.itemsize == 4 else x.cast(narrow_dt)
|
||||
m = (x & _c(mask)).cast(dtypes.int)
|
||||
if not is_signed: return m.cast(wide_dt)
|
||||
sign = (m >> _c(op_bits - 1)) & _c(1)
|
||||
return sign.ne(_c(0)).where(m - _c(1 << op_bits), m).cast(wide_dt)
|
||||
def to_wide(x: UOp) -> UOp: return (x.bitcast(narrow_dt) if x.dtype.itemsize == narrow_dt.itemsize else x.cast(narrow_dt)).cast(wide_dt)
|
||||
full = (to_wide(s0) * to_wide(s1) + to_wide(s2)) if 'MAD' in op_name and isinstance(s2, UOp) else \
|
||||
(to_wide(s1) - to_wide(s0)) if 'SUBREV' in op_name else \
|
||||
(to_wide(s0) - to_wide(s1)) if 'SUB' in op_name else (to_wide(s0) + to_wide(s1))
|
||||
if isinstance(s2, UOp) and 'MAD' in op_name: full = to_mulin(s0) * to_mulin(s1) + to_wide(s2)
|
||||
elif 'SUBREV' in op_name: full = to_wide(s1) - to_wide(s0)
|
||||
elif 'SUB' in op_name: full = to_wide(s0) - to_wide(s1)
|
||||
else: full = to_wide(s0) + to_wide(s1)
|
||||
return full.clamp(narrow_min, narrow_max).cast(narrow_dt)
|
||||
# V_SUB_U32 / V_ADD_U32 with clamp: unsigned saturate (SUB underflow->0, ADD overflow->0xFFFFFFFF)
|
||||
if any(p in op_name for p in ('_SUB_U32', '_ADD_U32', '_SUB_U16', '_ADD_U16')):
|
||||
@@ -562,6 +590,10 @@ class _Ctx:
|
||||
vcc_reg = sdst_reg if sdst_reg is not None else VCC_LO.offset
|
||||
if 'VCC' not in srcs: srcs['VCC'] = self.rmask(_c(vcc_reg))
|
||||
srcs.update(self.base_srcs(exec_mask, lane), VDST=vdst_reg, MAX_FLOAT_F32=UOp.const(3.4028234663852886e38, dtypes.float32))
|
||||
# f32 min/max/median ops flush denormal inputs to signed zero (select-style ops: results propagate inputs bitwise)
|
||||
# (RDNA4 calls them _NUM_: V_MIN_NUM_F32 etc.)
|
||||
if any(p in op.name for p in ('MIN_F32', 'MAX_F32', 'MIN3_F32', 'MAX3_F32', 'MED3_F32', 'MIN_NUM_F32', 'MAX_NUM_F32')):
|
||||
srcs = {k: _ftz_f32(v) if k in ('S0', 'S1', 'S2') and isinstance(v, UOp) else v for k, v in srcs.items()}
|
||||
_, assigns = parse_pcode(pcode, srcs)
|
||||
|
||||
# For integer ops with clamp, pre-compute the saturated result; floats clamp to [0,1] at write time
|
||||
@@ -586,8 +618,8 @@ class _Ctx:
|
||||
continue
|
||||
if int_saturate is not None: val = int_saturate
|
||||
elif clmp and val.dtype in (dtypes.float32, dtypes.half, dtypes.float64):
|
||||
clamped = val.maximum(UOp.const(0.0, val.dtype)).minimum(UOp.const(1.0, val.dtype))
|
||||
val = _FUNCS['isNAN'](val).where(UOp.const(0.0, val.dtype), clamped)
|
||||
# hardware clamp: -0 becomes +0 and NaN becomes 0 (hardware verified)
|
||||
val = (val > UOp.const(0.0, val.dtype)).where(val.minimum(UOp.const(1.0, val.dtype)), UOp.const(0.0, val.dtype))
|
||||
if val.dtype in (dtypes.uint64, dtypes.int64, dtypes.float64):
|
||||
lo, hi = _split64(val)
|
||||
lane_stores.extend([self.wvgpr_dyn(vdst_reg, lane, lo, exec_mask), self.wvgpr_dyn(vdst_reg + _c(1), lane, hi, exec_mask)])
|
||||
@@ -613,8 +645,9 @@ class _Ctx:
|
||||
stores: list[UOp] = []
|
||||
for mask_val, reg in [(vcc_val, vcc_reg), (exec_val, EXEC_LO.offset)]:
|
||||
if mask_val is None: continue
|
||||
# hardware zeroes the inactive lane bits of per-lane VCC writes (VCC = mask & EXEC), it never preserves them
|
||||
stores.extend(self.wmask(_c(reg), self.unroll_lanes(lambda l, v=mask_val: (_to_u32(v.substitute({lane: l})) & _c(1)).cast(dtypes.uint32),
|
||||
exec_mask, apply_exec=False)))
|
||||
exec_mask, apply_exec=reg != EXEC_LO.offset)))
|
||||
if slice_stores: # merge D0[hi:lo] slices into one read-modify-write of the destination VGPR
|
||||
result = self.rvgpr_dyn(vdst_reg, lane)
|
||||
for lo_bit, width, val_bits in slice_stores: result = _set_bits(result, val_bits, width, lo_bit)
|
||||
@@ -986,6 +1019,9 @@ def _compile_vopc(inst: ir3.VOPC|ir3.VOPC_DPP16|ir3.VOP3|ir4.VOPC|ir4.VOPC_DPP16
|
||||
s1 = _apply_src_mods(s1, 0, 1 if _iattr(inst, 'src1_abs') else 0, 1 if _iattr(inst, 'src1_neg') else 0, bits['s1'])
|
||||
s0 = _apply_src_mods(s0, 0, abs_bits, neg_bits, bits['s0'])
|
||||
s1 = _apply_src_mods(s1, 1, abs_bits, neg_bits, bits['s1'])
|
||||
elif abs_bits or neg_bits: # int compares also honor abs/neg, as bit-level sign clear/flip (not integer abs/negate)
|
||||
s0 = _apply_src_mods(s0, 0, abs_bits, neg_bits, bits['s0'])
|
||||
s1 = _apply_src_mods(s1, 1, abs_bits, neg_bits, bits['s1'])
|
||||
for dest, val in parse_pcode(pcode, {'S0': s0, 'S1': s1, 'laneId': lc, 'D0': UOp.const(0, dtypes.uint64)})[1]:
|
||||
if '[laneId]' in dest and ('D0' in dest or 'EXEC' in dest): return val.cast(dtypes.uint32)
|
||||
return _c(0)
|
||||
@@ -994,12 +1030,9 @@ def _compile_vopc(inst: ir3.VOPC|ir3.VOPC_DPP16|ir3.VOP3|ir4.VOPC|ir4.VOPC_DPP16
|
||||
# Both VOPC and VOP3 clear inactive lane bits (hardware verified)
|
||||
new_result = new_bits & exec_mask
|
||||
|
||||
# CMPX e32: writes EXEC only; CMPX e64: writes both EXEC and SDST; non-CMPX: writes dst only
|
||||
if is_cmpx:
|
||||
stores = ctx.wmask(_c(EXEC_LO.offset), new_result)
|
||||
if not is_vopc: stores.extend(ctx.wmask(dst_off, new_result))
|
||||
else:
|
||||
stores = ctx.wmask(dst_off, new_result) if not is_vopc else ctx.wmask(_c(VCC_LO.offset), new_result)
|
||||
# CMPX writes EXEC only (hardware verified: e64 CMPX does not write SDST); non-CMPX writes SDST/VCC
|
||||
if is_cmpx: stores = ctx.wmask(_c(EXEC_LO.offset), new_result)
|
||||
else: stores = ctx.wmask(dst_off, new_result) if not is_vopc else ctx.wmask(_c(VCC_LO.offset), new_result)
|
||||
return UOp.sink(*stores, *ctx.inc_pc())
|
||||
|
||||
|
||||
|
||||
@@ -200,7 +200,20 @@ def _abs(val: UOp) -> UOp:
|
||||
def _f_to_u(f, dt):
|
||||
clamped = (f < _const(f.dtype, 0.0)).where(_const(f.dtype, 0.0), f)
|
||||
truncated = UOp(Ops.TRUNC, src=(clamped,))
|
||||
return (truncated >= _const(f.dtype, 2**(dt.itemsize*8))).where(_const(dt, dt.max), truncated.cast(dt))
|
||||
res = (truncated >= _const(f.dtype, 2**(dt.itemsize*8))).where(_const(dt, dt.max), truncated.cast(dt))
|
||||
return _isnan(f).where(_const(dt, 0), res) # float->uint conversion of NaN is 0 on hardware
|
||||
|
||||
def _f_to_i32(a: UOp) -> UOp:
|
||||
"""v_cvt_i32_f32/f64: truncate toward zero, saturate to [INT_MIN, INT_MAX], NaN -> 0.
|
||||
(x86 cvttss2si returns 0x80000000 for all of these, which matches hardware only for negative overflow.)"""
|
||||
res = (a >= _const(a.dtype, 2147483648.0)).where(_const(dtypes.int, 0x7FFFFFFF), UOp(Ops.TRUNC, src=(a,)).cast(dtypes.int))
|
||||
return _isnan(a).where(_const(dtypes.int, 0), res)
|
||||
|
||||
def _ftz_f32(v: UOp) -> UOp:
|
||||
"""Flush f32 denormals to signed zero (RDNA default float mode flushes denormal f32 inputs on select-style ops)."""
|
||||
bits = v.bitcast(dtypes.uint32) if v.dtype == dtypes.float32 else v
|
||||
return ((bits & _u32(0x7FFFFFFF)) < _u32(0x00800000)).where((bits & _u32(0x80000000)).bitcast(dtypes.float32),
|
||||
v if v.dtype == dtypes.float32 else v.bitcast(dtypes.float32))
|
||||
|
||||
def _cvt_quiet(val: UOp) -> UOp:
|
||||
bits, _, _, qb, _ = _float_info(val)
|
||||
@@ -245,18 +258,51 @@ def _ldexp(val: UOp, exp: UOp) -> UOp:
|
||||
if val.dtype == dtypes.uint32: val = val.bitcast(dtypes.float32)
|
||||
elif val.dtype == dtypes.uint64: val = val.bitcast(dtypes.float64)
|
||||
if exp.dtype in (dtypes.uint32, dtypes.uint64): exp = exp.cast(dtypes.int if exp.dtype == dtypes.uint32 else dtypes.int64)
|
||||
return val * UOp(Ops.EXP2, src=(exp.cast(val.dtype),))
|
||||
bits = val.bitcast(dtypes.uint32) if val.dtype == dtypes.float32 else val.bitcast(dtypes.uint64)
|
||||
abs_max = _const(bits.dtype, 0x7F800000 if val.dtype == dtypes.float32 else 0x7FF0000000000000)
|
||||
sign_mask = _const(bits.dtype, 0x80000000 if val.dtype == dtypes.float32 else 0x8000000000000000)
|
||||
# hardware flushes denormal inputs to signed zero
|
||||
magn_mask = _const(bits.dtype, 0x7FFFFFFF if val.dtype == dtypes.float32 else 0x7FFFFFFFFFFFFFFF)
|
||||
is_denorm = ((bits & abs_max).eq(_const(bits.dtype, 0))) & ((bits & magn_mask).ne(_const(bits.dtype, 0)))
|
||||
val = is_denorm.where((bits & sign_mask).bitcast(val.dtype), val)
|
||||
# hardware propagates 0/+-inf/NaN unchanged (avoids 0*inf = NaN on the host)
|
||||
res = val * UOp(Ops.EXP2, src=(exp.cast(val.dtype),))
|
||||
is_special = (bits & abs_max).eq(_const(bits.dtype, 0)) | ((bits & abs_max) >= abs_max)
|
||||
return is_special.where(val, res)
|
||||
|
||||
def _frexp_mant(val: UOp) -> UOp:
|
||||
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
|
||||
if val.dtype == dtypes.float32: return ((val.bitcast(dtypes.uint32) & _u32(0x807FFFFF)) | _u32(0x3f000000)).bitcast(dtypes.float32)
|
||||
return ((val.bitcast(dtypes.uint64) & _const(dtypes.uint64, 0x800FFFFFFFFFFFFF)) |
|
||||
_const(dtypes.uint64, 0x3fe0000000000000)).bitcast(dtypes.float64)
|
||||
if val.dtype == dtypes.float32:
|
||||
bits = val.bitcast(dtypes.uint32)
|
||||
# denormal/zero inputs (exponent field == 0) return signed zero on hardware
|
||||
return ((bits & _u32(0x7F800000)).ne(_u32(0))).where(((bits & _u32(0x807FFFFF)) | _u32(0x3F000000)).bitcast(dtypes.float32),
|
||||
(bits & _u32(0x80000000)).bitcast(dtypes.float32))
|
||||
bits = val.bitcast(dtypes.uint64)
|
||||
return ((bits & _const(dtypes.uint64, 0x7FF0000000000000)).ne(_const(dtypes.uint64, 0))).where(
|
||||
((bits & _const(dtypes.uint64, 0x800FFFFFFFFFFFFF)) | _const(dtypes.uint64, 0x3fe0000000000000)).bitcast(dtypes.float64),
|
||||
(bits & _const(dtypes.uint64, 0x8000000000000000)).bitcast(dtypes.float64))
|
||||
|
||||
def _msb(val: UOp, bits: int) -> UOp:
|
||||
"""Index of the highest set bit, or -1 if val == 0."""
|
||||
dt = dtypes.uint64 if bits > 32 else dtypes.uint32
|
||||
val = val.cast(dt) if val.dtype != dt else val
|
||||
result = _const(dtypes.int, -1)
|
||||
for i in range(bits - 1, -1, -1):
|
||||
cond = ((val >> _const(dt, i)) & _const(dt, 1)).ne(_const(dt, 0)) & result.eq(_const(dtypes.int, -1))
|
||||
result = cond.where(_const(dtypes.int, i), result)
|
||||
return result
|
||||
|
||||
def _frexp_exp(val: UOp) -> UOp:
|
||||
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
|
||||
if val.dtype == dtypes.float32: return ((val.bitcast(dtypes.uint32) >> _u32(23)) & _u32(0xFF)).cast(dtypes.int) - _const(dtypes.int, 126)
|
||||
return ((val.bitcast(dtypes.uint64) >> _const(dtypes.uint64, 52)) & _const(dtypes.uint64, 0x7FF)).cast(dtypes.int) - _const(dtypes.int, 1022)
|
||||
if val.dtype == dtypes.float32:
|
||||
e = (val.bitcast(dtypes.uint32) >> _u32(23)) & _u32(0xFF)
|
||||
return e.ne(_u32(0)).where(e.cast(dtypes.int) - _const(dtypes.int, 126), _const(dtypes.int, 0)) # f32 denormals -> 0 (hardware verified)
|
||||
bits = val.bitcast(dtypes.uint64)
|
||||
e = (bits >> _const(dtypes.uint64, 52)) & _const(dtypes.uint64, 0x7FF)
|
||||
mant = bits & _const(dtypes.uint64, 0xFFFFFFFFFFFFF)
|
||||
# f64 denormals: normalized exponent = highest set mantissa bit - 1073, zero -> 0 (hardware verified)
|
||||
denorm = mant.ne(_const(dtypes.uint64, 0)).where(_msb(mant, 52) - _const(dtypes.int, 1073), _const(dtypes.int, 0))
|
||||
return e.ne(_const(dtypes.uint64, 0)).where(e.cast(dtypes.int) - _const(dtypes.int, 1022), denorm)
|
||||
|
||||
TWO_OVER_PI = int(
|
||||
"0145f306dc9c882a53f84eafa3ea69bb81b6c52b3278872083fca2c757bd778ac36e48dc74849ba5c00c925dd413a32439fc3bd"
|
||||
@@ -314,9 +360,9 @@ _FUNCS: dict[str, Callable[..., UOp]] = {
|
||||
'fma': lambda a, b, c: a * b + c,
|
||||
'i32_to_f32': lambda a: a.cast(dtypes.int).cast(dtypes.float32),
|
||||
'u32_to_f32': lambda a: a.cast(dtypes.uint32).cast(dtypes.float32),
|
||||
'f32_to_i32': lambda a: UOp(Ops.TRUNC, src=(a.bitcast(dtypes.float32),)).cast(dtypes.int),
|
||||
'f32_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float32)),
|
||||
'f32_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float32), dtypes.uint32),
|
||||
'f64_to_i32': lambda a: UOp(Ops.TRUNC, src=(a.bitcast(dtypes.float64),)).cast(dtypes.int),
|
||||
'f64_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float64)),
|
||||
'f64_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float64), dtypes.uint32),
|
||||
'f16_to_f32': lambda a: _f16_extract(a).cast(dtypes.float32),
|
||||
'f32_to_f16': lambda a: a.cast(dtypes.half),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from dataclasses import replace
|
||||
from tinygrad.dtype import dtypes, DType, truncate
|
||||
from tinygrad.helpers import flatten, DEBUG, EMULATED_DTYPES, Context, SPEC
|
||||
from tinygrad.helpers import flatten, DEBUG, EMULATED_DTYPES
|
||||
from tinygrad.uop import GroupOp
|
||||
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, graph_rewrite, ParamArg
|
||||
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher, graph_rewrite
|
||||
from tinygrad.renderer import Renderer
|
||||
from tinygrad.codegen.decomp.transcendental import exponent_bias, shl, shr
|
||||
|
||||
@@ -127,17 +127,18 @@ def f2f_clamp(val:UOp, dt:DType, sat=True) -> UOp:
|
||||
return val.ne(val).where(val, (val < -mx).where(-sat, (mx < val).where(sat, val)))
|
||||
|
||||
def f2f_load(x: UOp, fr:DType, to:DType) -> UOp:
|
||||
if (n:=x.max_numel()) == 1: return f2f(x.replace(dtype=f2f_dt[fr]), fr, to)
|
||||
return UOp(Ops.STACK, src=tuple(f2f(x.replace(dtype=f2f_dt[fr], src=(reindex(x.src[0], i, 1),)), fr, to) for i in range(n)))
|
||||
storage_idx = graph_rewrite(x.src[0], pm_float_decomp, ctx=(fr, to), bottom_up=True)
|
||||
if (n:=x.max_numel()) == 1: return f2f(storage_idx.load(*x.src[1:]), fr, to)
|
||||
return UOp(Ops.STACK, src=tuple(f2f(reindex(storage_idx, i, 1).load(*x.src[1:]), fr, to) for i in range(n)))
|
||||
|
||||
def f2f_store(st, idx, val, fr:DType, to:DType):
|
||||
if (n:=val.max_numel()) == 1: return st.replace(src=(idx, f2f(val.bitcast(f2f_dt[to]), to, fr)))
|
||||
return UOp.group(*(st.replace(src=(reindex(idx, i, 1), f2f(val.index(i).bitcast(f2f_dt[to]), to, fr))) for i in range(n)))
|
||||
|
||||
# tag is the 32-bit word this node becomes - (0 for the low word, 1 for the high, the dtype the consumer wants)
|
||||
pm_long_decomp = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
x.replace(dtype=l2i_dt[x.dtype], arg=replace(x.arg, dtype=l2i_dt[x.dtype]), src=(sz*2,)) if x.dtype in l2i_dt else None),
|
||||
pm_long_decomp: PatternMatcher = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, tuple(l2i_dt.keys()), src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
UOp(x.op, src=(sz*2,), arg=replace(x.arg, dtype=l2i_dt[x.dtype]), tag=x.tag)),
|
||||
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
reindex(x, x.tag[0]).replace(tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var('idx', tuple(l2i_dt.keys())), UPat.var('val')), name='st'), lambda st,idx,val:
|
||||
@@ -159,21 +160,24 @@ pm_long_decomp = PatternMatcher([
|
||||
(UPat((*(GroupOp.ALU - GroupOp.Comparison - {Ops.SHL, Ops.SHR, Ops.WHERE}), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda ctx,x:
|
||||
split_l2i(ctx, x.op, l2i_dt[x.dtype], *flatten((a.rtag((0, l2i_dt[x.dtype])), a.rtag((1, l2i_dt[x.dtype]))) for a in x.src))[x.tag[0]]
|
||||
if x.tag is not None else None),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda x,idx:
|
||||
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag[0]).replace(tag=None),), tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda ctx,x,idx:
|
||||
reindex(graph_rewrite(idx, pm_long_decomp, ctx=ctx, bottom_up=True), x.tag[0]).replace(tag=None).load() if x.tag is not None else None),
|
||||
(UPat(Ops.CONST, tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'), lambda x:
|
||||
UOp.const(truncate[x.tag[1]]((x.val >> 32) if x.tag[0] == 1 else (x.val & 0xFFFFFFFF)), x.tag[1]))
|
||||
])
|
||||
|
||||
# float decomposition patterns - ctx is (fr, to) tuple
|
||||
pm_float_decomp = PatternMatcher([
|
||||
(UPat((*GroupOp.Defines, Ops.INDEX, Ops.SHRINK), name="x"), lambda ctx,x:
|
||||
x.replace(dtype=f2f_dt[ctx[0]], arg=replace(x.arg, dtype=f2f_dt[ctx[0]]) if isinstance(x.arg, ParamArg) else x.arg, tag=ctx[0])
|
||||
if x.dtype == ctx[0] and (x.op is not Ops.INDEX or x.src[0].op not in {Ops.LOAD, Ops.STACK}) else None),
|
||||
pm_float_decomp: PatternMatcher = PatternMatcher([
|
||||
(UPat(GroupOp.Defines, name="x"), lambda ctx,x:
|
||||
UOp(x.op, src=x.src, arg=replace(x.arg, dtype=f2f_dt[ctx[0]]), tag=ctx[0]) if x.dtype == ctx[0] else None),
|
||||
# INDEX into a LOAD/STACK selects a lane of an already converted value, the load rules below own those
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat(GroupOp.All-{Ops.LOAD, Ops.STACK}),), allow_any_len=True, name="x"), lambda ctx,x:
|
||||
UOp(x.op, src=(graph_rewrite(x.src[0], pm_float_decomp, ctx=ctx, bottom_up=True), *x.src[1:]), arg=x.arg, tag=ctx[0])
|
||||
if x.dtype == ctx[0] else None),
|
||||
(UPat(Ops.LOAD, dtypes.floats, name="x"), lambda ctx,x: f2f_load(x, *ctx) if x.dtype == ctx[0] else None),
|
||||
# bitcasted load should just replace load
|
||||
(UPat(Ops.BITCAST, src=(UPat(Ops.LOAD, name="ld"),), name="bc"), lambda ctx,bc,ld:
|
||||
ld.replace(dtype=f2f_dt[ctx[0]]).bitcast(bc.dtype) if ld.dtype == ctx[0] else None),
|
||||
graph_rewrite(ld.src[0], pm_float_decomp, ctx=ctx, bottom_up=True).load(*ld.src[1:]).bitcast(bc.dtype) if ld.dtype == ctx[0] else None),
|
||||
# bitcast from
|
||||
(UPat(Ops.BITCAST, src=(UPat.var("x", dtypes.floats),), name="bc"), lambda ctx,bc,x:
|
||||
bc.replace(src=(f2f(x.bitcast(f2f_dt[ctx[1]]), ctx[1], ctx[0]),)) if x.dtype == ctx[1] and bc.dtype.bitsize == ctx[0].bitsize else None),
|
||||
@@ -185,23 +189,20 @@ pm_float_decomp = PatternMatcher([
|
||||
# a CONST has no srcs to cast, it restates its value at the emulating dtype
|
||||
(UPat(Ops.CONST, dtypes.floats, name="x"), lambda ctx,x: UOp.const(x.val, ctx[1]) if x.dtype == ctx[0] else None),
|
||||
(UPat(GroupOp.All-GroupOp.Defines-{Ops.CAST, Ops.BITCAST, Ops.CONST}, dtypes.floats, name="x"), lambda ctx,x:
|
||||
x.replace(dtype=ctx[1], src=tuple(s.cast(ctx[1]) if s.dtype == ctx[0] else s for s in x.src))
|
||||
if x.dtype == ctx[0] else None),
|
||||
UOp(x.op, src=tuple(s.cast(ctx[1]) if s.dtype == ctx[0] else s for s in x.src), arg=x.arg, tag=x.tag) if x.dtype == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx"), UPat(Ops.BITCAST, dtypes.floats, name="val")), name='st'), lambda ctx,st,idx,val:
|
||||
st.replace(src=(idx, val.replace(dtype=f2f_dt[ctx[0]]))) if val.dtype == ctx[0] and idx.tag == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx"), UPat.var("val", dtypes.floats)), name='st'), lambda ctx,st,idx,val:
|
||||
f2f_store(st, idx, val, *ctx) if val.dtype == ctx[1] and (idx:=idx.src[0] if idx.op == Ops.CAST else idx).tag == ctx[0] else None),
|
||||
st.replace(src=(idx, val.src[0].bitcast(f2f_dt[ctx[0]]))) if val.dtype == ctx[0] and idx.tag == ctx[0] else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var("idx").or_casted(), UPat.var("val", dtypes.floats)), name='st'), lambda ctx,st,idx,val:
|
||||
f2f_store(st, idx, val, *ctx) if val.dtype == ctx[1] and idx.tag == ctx[0] else None),
|
||||
])
|
||||
|
||||
def do_dtype_decomps(sink:UOp, ctx:tuple[set[DType], Renderer]) -> UOp:
|
||||
def _should_emulate(dt): return dt in EMULATED_DTYPES.tolist(dtypes) or dt not in ctx[1].supported_dtypes()
|
||||
# NOTE: dtype decomp creates intermediate UOps that don't follow the spec (e.g. half LOAD on ushort BUFFER)
|
||||
with Context(SPEC=min(SPEC.value, 1)):
|
||||
for fr in sorted(filter(_should_emulate, ctx[0])):
|
||||
to = dtypes.int if fr == dtypes.long else dtypes.half if not _should_emulate(dtypes.half) and fr in dtypes.fp8s else dtypes.float
|
||||
if DEBUG >= 2: print(f"emulating {fr} as {to}")
|
||||
pm = pm_float_decomp if fr in dtypes.floats else pm_long_decomp
|
||||
sink = graph_rewrite(sink, pm, name=f"decomp {fr} -> {to}", ctx={} if pm is pm_long_decomp else (fr, to), bottom_up=True)
|
||||
for fr in sorted(filter(_should_emulate, ctx[0])):
|
||||
to = dtypes.int if fr == dtypes.long else dtypes.half if not _should_emulate(dtypes.half) and fr in dtypes.fp8s else dtypes.float
|
||||
if DEBUG >= 2: print(f"emulating {fr} as {to}")
|
||||
pm = pm_float_decomp if fr in dtypes.floats else pm_long_decomp
|
||||
sink = graph_rewrite(sink, pm, name=f"decomp {fr} -> {to}", ctx={} if pm is pm_long_decomp else (fr, to), bottom_up=True)
|
||||
ctx[0].clear()
|
||||
return sink
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import cast, Callable
|
||||
from tinygrad.helpers import to_mv, from_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le, to_tuple
|
||||
from tinygrad.device import Buffer, BufferSpec, TinyELF, Program, Device
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer, MMIOInterface
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, make_cmdbuf, make_signal
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, make_cmdbuf, make_buf
|
||||
from tinygrad.runtime.support.c import DLL
|
||||
from tinygrad.renderer.cstyle import ClangRenderer
|
||||
from tinygrad.renderer.llvmir import CPULLVMRenderer
|
||||
@@ -80,7 +80,7 @@ pm_cpu_opsel = PatternMatcher([
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))),
|
||||
lambda ctx, dst, val: cpu_cmd(ctx, signal_prog, dst.getaddr(ctx), val.cast(dtypes.uint64))),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)),
|
||||
lambda ctx, dst: cpu_cmd(ctx, timestamp_prog, dst.getaddr(ctx), *(() if WIN else (make_signal(ctx, tag="func:clock_gettime").getaddr(ctx),)))),
|
||||
lambda ctx, dst: cpu_cmd(ctx, timestamp_prog, dst.getaddr(ctx), *(() if WIN else (make_buf(ctx, tag="func:clock_gettime").getaddr(ctx),)))),
|
||||
])
|
||||
|
||||
def encode_queue(q:UOp) -> UOp:
|
||||
@@ -91,7 +91,7 @@ def encode_queue(q:UOp) -> UOp:
|
||||
assert cnt < RING_SLOTS, f"submit of {cnt} entries doesn't fit the ring"
|
||||
cmdbuf = make_cmdbuf(lin, devs, buf=UOp.placeholder((cnt*CMD_SIZE,), dtypes.uint64, next(UOp.unique_num), device=devs).rtag("cmdbuf"))
|
||||
ring = UOp.placeholder((ring_words:=RING_SLOTS*CMD_SIZE,), dtypes.uint64, 0, device=devs, volatile=True).rtag(f"{queue}_ring")
|
||||
put, done, sem, sysbuf = (make_signal(devs, tag=f"{queue}_{name}") for name in ("put", "done", "sem", "sys"))
|
||||
put, done, sem, sysbuf = (make_buf(devs, tag=f"{queue}_{name}") for name in ("put", "done", "sem", "sys"))
|
||||
|
||||
# submits are serialized on the submitter, so they can bump put without atomics
|
||||
ran = done.after(l:=UOp.loop(next(UOp.unique_num))).index(0).load()
|
||||
@@ -104,7 +104,7 @@ def encode_queue(q:UOp) -> UOp:
|
||||
if WIN: return sysbuf.after(bumped).index(0).store(put.after(bumped).index(0).load())
|
||||
|
||||
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(bumped,))
|
||||
return make_signal(devs, tag="func:sem_post").after(e).index(0).load().call(sem.after(e).index(0), ret_dtype=dtypes.void).end(e)
|
||||
return make_buf(devs, tag="func:sem_post").after(e).index(0).load().call(sem.after(e).index(0), ret_dtype=dtypes.void).end(e)
|
||||
|
||||
# *****************
|
||||
|
||||
|
||||
@@ -36,13 +36,15 @@ class HCQInfo:
|
||||
|
||||
def all_devices_in(d:Any, c:frozenset[str]) -> bool: return {x.split(":")[0] for x in to_tuple(d)} <= c
|
||||
|
||||
def unwrap_mstack(u):
|
||||
def unwrap_mstack(u:UOp) -> tuple[UOp, ...]:
|
||||
if u.op is Ops.MSTACK: return tuple(x for s in u.src for x in unwrap_mstack(s))
|
||||
return unwrap_mstack(u.src[0]) if u.op is Ops.MSELECT else (u,)
|
||||
|
||||
def unwrap_view(v:UOp) -> tuple[UOp, int]:
|
||||
return unwrap_view(v.src[0]) if v.op is Ops.BITCAST else (v.src[0], v.src[1].val) if v.op is Ops.SHRINK else (v, 0)
|
||||
|
||||
# patches
|
||||
|
||||
def is_value_known_at_link(val:UOp) -> bool:
|
||||
runtime_reads = [u for u in val.toposort() if u.op in (Ops.LOAD, Ops.INDEX)]
|
||||
addressed_bufs = [b for g in val.toposort() if g.op is Ops.GETADDR for b in unwrap_mstack(g.buf_uop)]
|
||||
@@ -51,6 +53,7 @@ def is_value_known_at_link(val:UOp) -> bool:
|
||||
return not val.variables() and not runtime_reads and all(b.op is not Ops.PARAM or b.tag is not None for b in addressed_bufs)
|
||||
|
||||
def make_patches(buf:UOp, patches:Sequence[tuple[sint, UOp]]) -> tuple[UOp, ...]:
|
||||
# group patches into stacks: (tag, type, offset). offset is used for shrink later
|
||||
groups:dict[tuple[str|None, DType, sint], list[tuple[sint, UOp]]] = collections.defaultdict(list)
|
||||
for off, val in patches:
|
||||
tag = "link" if is_value_known_at_link(val) else "inputs" if val.op is Ops.GETADDR else None
|
||||
@@ -63,21 +66,15 @@ def make_patches(buf:UOp, patches:Sequence[tuple[sint, UOp]]) -> tuple[UOp, ...]
|
||||
ret.append(view.index(offs).store(UOp(Ops.STACK, dt, tuple(val for _,val in ps))).rtag(tag))
|
||||
return tuple(ret)
|
||||
|
||||
def make_binary_patch(buf:UOp, blob:bytes) -> UOp:
|
||||
data = UOp(Ops.BINARY, src=(), arg=blob).bitcast(buf.dtype)
|
||||
r = UOp.range(len(blob) // buf.dtype.itemsize, 0, dtype=dtypes.int, src=(buf, data))
|
||||
return buf.index(r).store(data.index(r).load()).end(r).rtag("link")
|
||||
def make_binary_patch(buf:UOp, blob:bytes) -> UOp: return buf.store(UOp(Ops.BINARY, src=(), arg=blob).bitcast(buf.dtype)).rtag("link")
|
||||
|
||||
def make_cmdbuf(lin, devs, buf:UOp|None=None):
|
||||
def make_cmdbuf(lin, devs, buf:UOp|None=None, dep:tuple[UOp, ...]=()):
|
||||
blob, patches = bytearray(), []
|
||||
for s in (s for ins in lin.src for s in ins.src):
|
||||
if s.op is not Ops.CONST: patches.append((len(blob), s))
|
||||
blob.extend(struct.pack(f'<{s.dtype.fmt}', s.val if s.op is Ops.CONST else 0x0))
|
||||
cmdbuf = buf if buf is not None else UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
|
||||
return cmdbuf.after(make_binary_patch(cmdbuf, bytes(blob)), *make_patches(cmdbuf, patches))
|
||||
|
||||
def make_signal(devs, slot:int=0, tag:str="signal") -> UOp:
|
||||
return UOp.placeholder((1,), dtypes.uint64, slot, device=devs, volatile=True).rtag(tag)
|
||||
return cmdbuf.after(*dep, make_binary_patch(cmdbuf, bytes(blob)), *make_patches(cmdbuf, patches))
|
||||
|
||||
def make_submit(*cmds, devs:str|tuple[str, ...], queue:str) -> UOp:
|
||||
return UOp.custom_function("submit_cmdbuf", UOp(Ops.LINEAR, src=tuple(cmds), arg=(to_tuple(devs), queue)))
|
||||
@@ -91,6 +88,8 @@ def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
|
||||
words = [get_call_arg_uops(call)[gi].getaddr(devs) for gi in info.globals] + list(info.vars)
|
||||
return buf.after(*make_patches(buf, list(zip(itertools.accumulate((w.dtype.itemsize for w in words), initial=0), words))))
|
||||
|
||||
def make_buf(devs, slot:int=0, tag:str="signal") -> UOp: return UOp.placeholder((1,), dtypes.uint64, slot, device=devs, volatile=True, tag=tag)
|
||||
|
||||
# *****************
|
||||
# 0.1. prep: replace buffers with params
|
||||
|
||||
@@ -111,6 +110,10 @@ def _staging() -> Buffer: return Buffer("CPU", STAGING_SIZE, dtypes.uint8, preal
|
||||
|
||||
def _need_staging(a, b): return all_devices_in(a.device, HCQ_DEVS - {"CPU"}) and not all_devices_in(b.device, HCQ_DEVS)
|
||||
|
||||
def stage_copy_ext(call:UOp) -> UOp|None:
|
||||
if (d:=next((d for b in call.src[1:] for d in to_tuple(b.device) if not d.startswith("CPU")), None)) is None: return None
|
||||
return pm.rewrite(call) if (pm:=getattr(Device[d], "pm_stage_copy", None)) is not None else None
|
||||
|
||||
def stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
if not (_need_staging(src, dst) or _need_staging(dst, src)): return None
|
||||
|
||||
@@ -126,20 +129,22 @@ def stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
# 1.2. prep: kernel copies
|
||||
|
||||
def _get_enqueue_devs(call:UOp) -> Any|None:
|
||||
if call.src[0].op not in (Ops.PROGRAM, Ops.COPY): return None # only these bodies can be enqueued
|
||||
if not (bufs:=call.src[1:]) or not all(all_devices_in(b.device, HCQ_DEVS) for b in bufs): return None
|
||||
if call.src[0].op is Ops.COPY: bufs = bufs[::-1] # copies push from the src device: p2p writes are faster than reads
|
||||
devs = min(bufs, key=lambda b: to_tuple(b.device)[0].startswith("CPU")).device # prio to enqueue on not CPU device
|
||||
return devs if all_devices_in(devs, HCQ_DEVS) else None
|
||||
|
||||
def kernel_copy(call:UOp, dst:UOp, src:UOp) -> UOp|None:
|
||||
def copy_with_kernel(call:UOp, dst:UOp, src:UOp) -> UOp|None:
|
||||
if (devs:=_get_enqueue_devs(call)) is None or Device[(dev:=to_tuple(devs)[0])].has_copy_queue: return None
|
||||
d, s = (UOp.param(i, dst.dtype, (n:=dst.max_numel(),), device=devs) for i in range(2))
|
||||
ast = d.index(r:=UOp.range(n, 0)).store(s.index(r).load()).end(r).sink(arg=KernelInfo(name="copy"), tag=1)
|
||||
return call.replace(src=(to_program(ast, Device[dev].renderer), dst, src))
|
||||
|
||||
pm_insert_copy_staging = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), stage_copy_ext),
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY), UPat(name="dst"), UPat(name="src"))), stage_copy),
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY), UPat(name="dst"), UPat(name="src")), name="call"), kernel_copy)
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.COPY), UPat(name="dst"), UPat(name="src")), name="call"), copy_with_kernel)
|
||||
])
|
||||
|
||||
# *****************
|
||||
@@ -161,7 +166,7 @@ def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tu
|
||||
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, written, (key, lane))]
|
||||
return dep_lanes
|
||||
|
||||
def _build_wait_cmds(slots:dict[str, int], dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
|
||||
def _build_wait_ins(slots:dict[str, int], dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
|
||||
# opt1: same-queue ops are fifo-ordered
|
||||
if devices[0].split(":")[0] in {"AMD", "QCOM", "CPU"} or queue.startswith("COPY"):
|
||||
dep_lanes = [(dep, dlane, lane) for dep, dlane, lane in dep_lanes if (dep[0][dlane], dep[1]) != (devices[lane], queue)]
|
||||
@@ -174,7 +179,7 @@ def _build_wait_cmds(slots:dict[str, int], dep_lanes:list[tuple[tuple, int, int]
|
||||
waits = []
|
||||
for (ddevs, dqueue, dtag), by_lane in deps.items():
|
||||
for ls in itertools.zip_longest(*(by_lane[lane] for lane in range(len(devices)))):
|
||||
s = UOp.mstack(*[make_signal(d, tag="sentinel_signal") if dl is None else make_signal(ddevs[dl], slots[dqueue]) for dl, d in zip(ls, devices)])
|
||||
s = UOp.mstack(*[make_buf(d, tag="sentinel_signal") if dl is None else make_buf(ddevs[dl], slots[dqueue]) for dl, d in zip(ls, devices)])
|
||||
waits.append(UOp(Ops.INS, arg="wait", src=(s, UOp.const(dtag + 1, dtypes.uint64))))
|
||||
return waits, {dtag for _, _, dtag in deps}
|
||||
|
||||
@@ -192,26 +197,52 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
|
||||
# to finalize the batch, sync all accesses from other devices to buffers that belong to this device
|
||||
fin_deps = [dl for dl in _get_deps(tracker, [list(dev_bufs[d].values()) for d in devs], None, key=(devs, "COMPUTE:0", n)) if dl[0][2] < n]
|
||||
waits, cur_signal_tags = _build_wait_cmds(slots, fin_deps, devs, "COMPUTE:0")
|
||||
waits, cur_signal_tags = _build_wait_ins(slots, fin_deps, devs, "COMPUTE:0")
|
||||
signal_tags |= cur_signal_tags
|
||||
|
||||
# wait the syncs and signal the device epoch, then bump the timeline on the host
|
||||
tl_signal, tl_value = make_signal(devs, tag="timeline_signal"), make_signal(devs, tag="timeline_value")
|
||||
tl_signal, tl_value = make_buf(devs, tag="timeline_signal"), make_buf(devs, tag="timeline_value")
|
||||
fin_submit = make_submit(*waits, UOp(Ops.INS, arg="store", src=(tl_signal, tl_value.index(0))), devs=devs, queue="COMPUTE:0")
|
||||
epoch = (epoch_slot:=tl_value.after(fin_submit).index(0)).load()
|
||||
|
||||
# fence once per device group on this schedule's previous epoch
|
||||
qs = dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])
|
||||
sched_epoch = make_signal(devs, next(UOp.unique_num))
|
||||
sched_epoch = make_buf(devs, next(UOp.unique_num), tag="epoch")
|
||||
|
||||
wait_device_epoch = (done:=tl_signal.after(loop:=UOp.loop(0)).index(0).load()).end(loop, done < sched_epoch.index(0).load())
|
||||
fences.append(make_call("hcq_fence", UOp.sink(wait_device_epoch), HCQInfo(devs)))
|
||||
|
||||
# queues of other groups wait on these signals, so reset them only after every group reached its epoch
|
||||
if qs: resets.append(make_call("hcq_reset", UOp.sink(*[make_signal(devs, slots[q]).index(0).store(0) for q in qs]), HCQInfo(devs)))
|
||||
# queues of other groups wait on these signals, reset them after every group reached its epoch
|
||||
rst = functools.reduce(lambda a,q: a+(make_buf(devs, slots[q]).after(*a[-1:]).index(0).store(0),), qs, cast(tuple[UOp, ...], ()))
|
||||
if rst: resets.append(make_call("hcq_reset", UOp.sink(*rst), HCQInfo(devs)))
|
||||
fins.append(make_call("hcq_finalizer", UOp.sink(epoch_slot.store(epoch + 1), sched_epoch.after(fin_submit).index(0).store(epoch)), HCQInfo(devs)))
|
||||
return fences + resets, fins, signal_tags
|
||||
|
||||
def _merged_hcq_call(calls:list[UOp]) -> UOp: # TODO: simplify?
|
||||
if len(calls) == 1: return calls[0]
|
||||
devs, queue = get_submit(calls[0]).src[0].arg
|
||||
body = make_submit(*[cmd for c in calls for cmd in get_submit(c).src[0].src], devs=devs, queue=queue).sink()
|
||||
return make_call(f"submit {queue} ({len(calls)})", body,
|
||||
replace(calls[0].arg.aux, estimates=sum((c.arg.aux.estimates for c in calls), start=Estimates()).simplify()))
|
||||
|
||||
def _merge_queues(submits:list[UOp]) -> list[UOp]:
|
||||
new_src:list[UOp] = []
|
||||
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of hcq calls, kept in submit order
|
||||
limits:dict[tuple[tuple[str, ...], str], int] = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
|
||||
|
||||
for call in submits:
|
||||
devs, queue = get_submit(call).src[0].arg
|
||||
if (old:=opened_qs.pop(key:=(devs, queue), None)) is not None:
|
||||
if limits[key] and len(old) >= limits[key]: new_src, old, limits[key] = new_src + [_merged_hcq_call(old)], [], limits[key] * 2
|
||||
new_rec = old + [call]
|
||||
else:
|
||||
# no such queue opened: close every open submit on this queue that shares a device, so submit order is kept
|
||||
closing = [k for k in opened_qs if k[1] == queue and set(k[0]) & set(devs)]
|
||||
new_src += [_merged_hcq_call(opened_qs.pop(k)) for k in closing]
|
||||
new_rec = [call]
|
||||
opened_qs[(devs, queue)] = new_rec
|
||||
return new_src + [_merged_hcq_call(c) for c in opened_qs.values()]
|
||||
|
||||
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> list[UOp]:
|
||||
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
|
||||
|
||||
@@ -222,7 +253,7 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> li
|
||||
call_waits:list[list[UOp]] = []
|
||||
for tag, ((call, _), (devices, queue)) in enumerate(zip(batch, batch_info)):
|
||||
deps = _get_deps(deps_tracker, _get_call_bufs_by_lane(call, devices), get_call_outs_ins(call)[0], key=(devices, queue, tag))
|
||||
cmds, cur_signal_tags = _build_wait_cmds(slots, deps, devices, queue)
|
||||
cmds, cur_signal_tags = _build_wait_ins(slots, deps, devices, queue)
|
||||
call_waits.append(cmds)
|
||||
signal_tags |= cur_signal_tags
|
||||
|
||||
@@ -234,24 +265,24 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> li
|
||||
for tag, ((call, _), (devices, queue), q) in enumerate(zip(batch, batch_info, call_waits)):
|
||||
# first queue use, sync prior device work with the device timeline
|
||||
if batch_info.index((devices, queue)) == tag:
|
||||
epoch = make_signal(devices, tag="timeline_value").index(0) - 1
|
||||
q = [UOp(Ops.INS, arg="barrier", src=()), UOp(Ops.INS, arg="wait", src=(make_signal(devices, tag="timeline_signal"), epoch))] + q
|
||||
epoch = make_buf(devices, tag="timeline_value").index(0) - 1
|
||||
q = [UOp(Ops.INS, arg="barrier", src=()), UOp(Ops.INS, arg="wait", src=(make_buf(devices, tag="timeline_signal"), epoch))] + q
|
||||
|
||||
# and make hcq call
|
||||
name, info = get_call_name(call, get_call_arg_uops(call)), HCQInfo(devices, estimate_uop(call))
|
||||
ts_ids = [next(UOp.unique_num) for _ in range(2)] if profile else []
|
||||
kerns.append((devices, make_call(name, call.src[0], info), tuple(ts_ids)))
|
||||
|
||||
ts_ins = [UOp(Ops.INS, arg="timestamp", src=(make_signal(devices, s),)) for s in ts_ids]
|
||||
ts_ins = [UOp(Ops.INS, arg="timestamp", src=(make_buf(devices, s),)) for s in ts_ids]
|
||||
q += ts_ins[:1] + [call.replace(arg=replace(call.arg, aux=info))] + ts_ins[1:]
|
||||
|
||||
# signal the queue if someone waits for us
|
||||
if tag in signal_tags: q += [UOp(Ops.INS, arg="store", src=(make_signal(devices, slots[queue]), UOp.const(tag + 1, dtypes.uint64)))]
|
||||
if tag in signal_tags: q += [UOp(Ops.INS, arg="store", src=(make_buf(devices, slots[queue]), UOp.const(tag + 1, dtypes.uint64)))]
|
||||
src.append(make_call(f"submit {name}", make_submit(*q, devs=devices, queue=queue).sink(), info))
|
||||
|
||||
# append batch timestamps to finalizers
|
||||
fins = [f.replace(arg=replace(f.arg, aux=replace(a:=f.arg.aux, kernels=tuple(x for x in kerns if set(x[0]) & set(a.device))))) for f in fins]
|
||||
return fences + src + fins
|
||||
return fences + _merge_queues(src) + fins
|
||||
|
||||
def sched_hcq_batches(l:UOp, profile:bool) -> UOp:
|
||||
srcs:list[UOp] = []
|
||||
@@ -261,50 +292,23 @@ def sched_hcq_batches(l:UOp, profile:bool) -> UOp:
|
||||
else: srcs, batch = srcs + _finalize_batch(batch, profile) + [call], []
|
||||
return l.replace(src=tuple(srcs + _finalize_batch(batch, profile)))
|
||||
|
||||
# *****************
|
||||
# 3. merge into queues
|
||||
|
||||
def _merged_hcq_call(calls:list[UOp]) -> UOp: # TODO: simplify?
|
||||
if len(calls) == 1: return calls[0]
|
||||
devs, queue = get_submit(calls[0]).src[0].arg
|
||||
body = make_submit(*[cmd for c in calls for cmd in get_submit(c).src[0].src], devs=devs, queue=queue).sink()
|
||||
return make_call(f"submit {queue} ({len(calls)})", body,
|
||||
replace(calls[0].arg.aux, estimates=sum((c.arg.aux.estimates for c in calls), start=Estimates()).simplify()))
|
||||
|
||||
def merge_queues(linear:UOp) -> UOp:
|
||||
new_src:list[UOp] = []
|
||||
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of hcq calls, kept in submit order
|
||||
limits:dict[tuple[tuple[str, ...], str], int] = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
|
||||
|
||||
for call in linear.src:
|
||||
# non-hcq call, fence or finalizer: close all open queues
|
||||
if not isinstance(call.arg.aux, HCQInfo) or (call.arg.name or "").startswith("hcq_"):
|
||||
new_src += [_merged_hcq_call(opened_qs.pop(k)) for k in list(opened_qs)] + [call]
|
||||
continue
|
||||
|
||||
devs, queue = get_submit(call).src[0].arg
|
||||
if (old:=opened_qs.pop(key:=(devs, queue), None)) is not None:
|
||||
if limits[key] and len(old) >= limits[key]: new_src, old, limits[key] = new_src + [_merged_hcq_call(old)], [], limits[key] * 2
|
||||
new_rec = old + [call]
|
||||
else:
|
||||
# no such queue opened: close every open submit on this queue that shares a device, so submit order is kept
|
||||
closing = [k for k in opened_qs if k[1] == queue and set(k[0]) & set(devs)]
|
||||
new_src += [_merged_hcq_call(opened_qs.pop(k)) for k in closing]
|
||||
new_rec = [call]
|
||||
opened_qs[(devs, queue)] = new_rec
|
||||
return linear.replace(src=tuple(new_src + [_merged_hcq_call(c) for c in opened_qs.values()]))
|
||||
|
||||
pm_schedule_and_merge = PatternMatcher([(UPat(Ops.LINEAR, name="l"),
|
||||
lambda ctx, l: merge_queues(sched_hcq_batches(l, ctx[1]).substitute(ctx[0], walk=True, enter_calls=True)))])
|
||||
lambda ctx, l: sched_hcq_batches(l, ctx[1]).substitute(ctx[0], walk=True, enter_calls=True))])
|
||||
|
||||
# *****************
|
||||
# 4.2. hcq lowering: ops to ir
|
||||
|
||||
def encode_host_call(call:UOp) -> UOp|None:
|
||||
if (pm:=getattr(Device[call.arg.aux.device[0]], "pm_host_lower", None)) is None: return None
|
||||
body = graph_rewrite(call.src[0], pm, name="lower host access", enter_calls=True)
|
||||
return None if body is call.src[0] else call.replace(src=(body, *call.src[1:]))
|
||||
|
||||
def encode_cmdbuf(submit:UOp, lin:UOp) -> UOp|None:
|
||||
if (pm:=Device.get_class(lin.arg[0][0]).pm_lower) is None: return None
|
||||
return graph_rewrite(submit, pm, name=f"encode {lin.arg[0]}", enter_calls=True)
|
||||
pm_encode_cmdbufs = PatternMatcher([
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf)])
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf),
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), encode_host_call)])
|
||||
|
||||
# *****************
|
||||
|
||||
@@ -350,20 +354,20 @@ def split_patches(call:UOp) -> UOp|None:
|
||||
lt_patches:list[UOp] = []
|
||||
body = graph_rewrite(call.src[0], pm_trim_link_patches, ctx=(rt_patches, lt_patches), name=f"trim link-time patches ({call.arg.name})")
|
||||
|
||||
# split patches
|
||||
inputs, internals = partition(dedup(g for p in rt_patches for g in get_getaddrs(p)), is_input_addr)
|
||||
# split patches. addresses read in the body go through the tables too
|
||||
inputs, internals = partition(dedup([g for p in rt_patches for g in get_getaddrs(p)] + get_getaddrs(body)), is_input_addr)
|
||||
runtimes, systems = partition(internals, lambda g: any(x.tag in {"program", "kernargs", "cmdbuf"} for x in unwrap_mstack(g.buf_uop)))
|
||||
tables = [make_addr_table(call, gs, n) for gs,n in ((inputs, "inputs"), (runtimes, "runtime"), (systems, "systems"))]
|
||||
reads, fills = {k:v for _,r,_,_ in tables for k,v in r.items()}, [f for t in tables[1:] for f in t[2]] # inputs table is filled by exec
|
||||
|
||||
ipatches = [p for p in rt_patches if p.tag == "inputs" and all(v in tables[0][3] for v in p.src[1].src)] # only getaddrs go to the table
|
||||
gathers = make_gather_loop(ipatches, tables[0][0], tables[0][3], lt_patches) if ipatches else {}
|
||||
body = body.substitute({p:p.substitute(gathers | reads) for p in rt_patches})
|
||||
body = body.substitute({p:p.substitute(gathers | reads) for p in rt_patches}).substitute(reads)
|
||||
|
||||
lt_srcs = collections.defaultdict(list)
|
||||
for p in lt_patches: lt_srcs[p.buf_uop].append(p)
|
||||
return call.replace(src=(body, *call.src[1:], *[b.after(*ps) for b,ps in lt_srcs.items()], *fills),
|
||||
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=((call.arg.aux.device,
|
||||
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=((to_tuple(inputs[0].arg),
|
||||
tuple(sorted(dedup(b.arg.slot for g in inputs for b in unwrap_mstack(g.buf_uop))))),) if inputs else call.arg.aux.input_idxs)))
|
||||
pm_split_patches = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), split_patches)])
|
||||
|
||||
@@ -500,7 +504,7 @@ def push_stack(op, s): return UOp(Ops.STACK,
|
||||
|
||||
def fold_binary(buf:UOp, blob:UOp) -> UOp:
|
||||
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)):
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[:len(blob.arg)] = blob.arg
|
||||
b.ensure_allocated()._buf.cpu_view().view(fmt='B')[:len(blob.arg)] = blob.arg
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
def fold_const_store(view:UOp, off:UOp, val:UOp) -> UOp:
|
||||
@@ -509,7 +513,7 @@ def fold_const_store(view:UOp, off:UOp, val:UOp) -> UOp:
|
||||
for b,v in zip((bs:=mb.bufs if isinstance((mb:=buf.buffer), MultiBuffer) else (mb,)), val.src if val.op is Ops.STACK else (val,)*len(bs)):
|
||||
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype]((v.src[0] if v.op is Ops.CAST else v).val))
|
||||
bo = start*buf.dtype.itemsize + off.val*val.dtype.itemsize
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[bo:bo+len(data)] = data
|
||||
b.ensure_allocated()._buf.cpu_view().view(fmt='B')[bo:bo+len(data)] = data
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
|
||||
@@ -530,8 +534,7 @@ pm_resolve_patches = PatternMatcher([
|
||||
(UPat(Ops.GETADDR, src=(UPat(name="buf"),), name="g"), resolve_getaddr),
|
||||
|
||||
# folders
|
||||
(UPat(name="buf").index(UPat(Ops.RANGE), allow_any_len=True).store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast())
|
||||
.index(UPat(Ops.RANGE), allow_any_len=True).load()).end(UPat(Ops.RANGE)), fold_binary),
|
||||
(UPat(name="buf").store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast())), fold_binary),
|
||||
(UPat((Ops.BITCAST, Ops.SHRINK, Ops.BUFFER, Ops.MSTACK), name="view")
|
||||
.index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
|
||||
])
|
||||
@@ -561,6 +564,7 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
|
||||
class HCQ2Compiled(Compiled):
|
||||
timestamp_divider: float = 1000.0
|
||||
wait_timeout_ms: float = 30000.0
|
||||
rt_nbytes: int = 64 << 20 # scratch that single-run placeholders are carved out of
|
||||
|
||||
def __init__(self, device:str, allocator:HCQAllocator, compilers:list[type[Renderer]], runtime, can_recover:bool=False, arch=None):
|
||||
self.can_recover = can_recover
|
||||
@@ -568,14 +572,15 @@ class HCQ2Compiled(Compiled):
|
||||
self.pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag="sentinel_signal"), lambda ctx: ctx[0].signal("sentinel", (1 << 64) - 1)),
|
||||
(UPat(Ops.PARAM, tag="timeline_signal"), lambda ctx: ctx[0].signal("timeline")),
|
||||
(UPat(Ops.PARAM, tag="timeline_value"), lambda ctx: ctx[0].signal("value", 1)),
|
||||
(UPat(Ops.PARAM, tag="timeline_value"), lambda ctx: ctx[0].signal("value", 1, device="CPU")),
|
||||
(UPat(Ops.PARAM, tag="epoch", name="b"), lambda ctx, b: ctx[0].signal(b.arg.slot, device="CPU")),
|
||||
(UPat(Ops.PARAM, tag="signal", name="b"), lambda ctx, b: ctx[0].signal(b.arg.slot)),
|
||||
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if b.tag is None else ctx[0].new_buffer(b, cache=ctx[1]))
|
||||
])
|
||||
|
||||
super().__init__(device, allocator, compilers, runtime, None, arch=arch)
|
||||
|
||||
self.rt_allocator = BumpAllocator(64 << 20)
|
||||
self.rt_allocator = BumpAllocator(self.rt_nbytes)
|
||||
self.prof_ents:dict[int, ProfileGraphEntry] = {}
|
||||
|
||||
def collect_prof(self):
|
||||
@@ -609,12 +614,12 @@ class HCQ2Compiled(Compiled):
|
||||
self.rt_allocator.alloc(b.max_numel() * b.dtype.itemsize, alignment=128))
|
||||
|
||||
@functools.cache
|
||||
def signal(self, name:str|int, init_value:int=0) -> Buffer:
|
||||
buf = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
|
||||
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
|
||||
def signal(self, name:str|int, init_value:int=0, device:str|None=None) -> Buffer:
|
||||
buf = Buffer(device or self.device, 1, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
|
||||
buf._buf.cpu_view().view(fmt='Q')[0] = init_value
|
||||
return buf
|
||||
|
||||
def _wait_signal(self, sig:memoryview, value:int, timeout:int|None=None):
|
||||
def _wait_signal(self, sig:MMIOInterface|memoryview, value:int, timeout:int|None=None):
|
||||
timeout = timeout if timeout is not None and self.can_recover else None
|
||||
st, done = time.perf_counter(), sig[0]
|
||||
while done < value:
|
||||
@@ -624,8 +629,8 @@ class HCQ2Compiled(Compiled):
|
||||
def synchronize(self, timeout:int|None=None):
|
||||
if HCQ_RUNTIME_DEV.value != self.device: Device[HCQ_RUNTIME_DEV.value].synchronize()
|
||||
|
||||
sig = self.signal("timeline").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
tl = self.signal("value", 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
sig = self.signal("timeline")._buf.cpu_view().view(fmt='Q')
|
||||
tl = self.signal("value", 1, device="CPU")._buf.cpu_view().view(fmt='Q')
|
||||
self._wait_signal(sig, tl[0] - 1, timeout)
|
||||
if self.prof_ents: self.collect_prof()
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import ctypes, struct, time, functools, itertools
|
||||
from typing import Any, cast
|
||||
from tinygrad.runtime.autogen import libusb
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, from_mv, round_up, ceildiv
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, from_mv, round_up, ceildiv, unwrap, dedup, to_tuple
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher
|
||||
from tinygrad.device import Buffer, BufferSpec, Device
|
||||
from tinygrad.runtime.support.hcq2 import HCQInfo, make_buf, make_cmdbuf, make_submit, HCQ_RUNTIME_DEV
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support import c
|
||||
|
||||
@@ -220,6 +225,7 @@ class USBMMIOInterface(MMIOInterface):
|
||||
return (index * self.el_sz, self.el_sz)
|
||||
|
||||
def __getitem__(self, index):
|
||||
Device[HCQ_RUNTIME_DEV.value].synchronize() # one driver on the link: drain the compiled submits before python touches it
|
||||
off, sz = self._off_from_index(index)
|
||||
if self.pcimem:
|
||||
assert sz % 4 == 0 and off % 4 == 0, f"pcie_mem_read requires 4-byte aligned access, got off={off}, sz={sz}"
|
||||
@@ -228,12 +234,114 @@ class USBMMIOInterface(MMIOInterface):
|
||||
return data if isinstance(index, slice) else int.from_bytes(data, "little")
|
||||
|
||||
def __setitem__(self, index, data):
|
||||
Device[HCQ_RUNTIME_DEV.value].synchronize()
|
||||
off, _ = self._off_from_index(index)
|
||||
data = struct.pack(self.fmt, data) if isinstance(data, int) else bytes(data)
|
||||
if not self.pcimem: self.usb.scsi_write(data) if self.addr == 0xf000 else self.usb.write(self.addr + off, data)
|
||||
else: self.usb.pcie_mem_write(self.addr+off, data)
|
||||
else:
|
||||
# writes are whole dwords
|
||||
assert len(data) % 4 == 0 and off % 4 == 0, f"pcie_mem_write requires 4-byte aligned access, got off={off}, sz={len(data)}"
|
||||
self.usb.pcie_mem_write(self.addr+off, data)
|
||||
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None):
|
||||
return USBMMIOInterface(self.usb, self.addr+offset, self.nbytes-offset if size is None else size, fmt=fmt or self.fmt, pcimem=self.pcimem)
|
||||
|
||||
# *****************
|
||||
|
||||
def _libusb(devs, dep:tuple[UOp, ...], fn:str, *args) -> UOp:
|
||||
return make_buf(devs, tag=f"func:{fn}").after(*dep).index(0).load().call(make_buf(devs, tag="usb_handle").index(0).load(),
|
||||
*[UOp.const(a, dtypes.int) if isinstance(a, int) else a for a in args], ret_dtype=dtypes.void)
|
||||
|
||||
def usb_bulk(devs, dep, endpoint:int, data:UOp, length, timeout:int=1000) -> UOp: # NULL actual_length out param
|
||||
return _libusb(devs, dep, "libusb_bulk_transfer", endpoint, data, length, UOp.const(0, dtypes.uint64), timeout)
|
||||
|
||||
def usb_stream(devs, dep:tuple[UOp, ...], addr:UOp, data:UOp, nbytes:int, write:bool) -> UOp:
|
||||
hdr = UOp.placeholder((2,), dtypes.uint64, device=devs, tag="usb_scratch").after(*dep)
|
||||
arm = _libusb(devs, (hdr.index(0).store(addr), hdr.index(1).store(UOp.const(nbytes // 4, dtypes.uint64))), "libusb_control_transfer",
|
||||
0x40, 0xF0, (0x60 if write else 0x20) | (0x0F << 8), 1 if write else 2, hdr.index(0), 12, 5000)
|
||||
return usb_bulk(devs, (arm,), 0x02 if write else 0x81, data, nbytes)
|
||||
|
||||
def usb_writes(devs, ws:list[tuple[UOp, UOp, int]]) -> tuple[UOp, ...]:
|
||||
return functools.reduce(lambda dep, w: (usb_stream(devs, dep, w[0], w[1], w[2], True),), ws, ())
|
||||
|
||||
def usb_load(b:UOp, idx:UOp, dt) -> UOp:
|
||||
got = UOp.placeholder((1,), dt, device=(devs:=to_tuple(b.device)), tag="usb_scratch")
|
||||
addr = b.getaddr((HCQ_RUNTIME_DEV.value,)) + (idx*dt.itemsize).cast(dtypes.uint64)
|
||||
return got.after(usb_stream(devs, b.src[1:] if b.op is Ops.AFTER else (), addr, got.index(0), dt.itemsize, False)).index(0).load()
|
||||
|
||||
def usb_write(b:UOp, idx:UOp, v:UOp) -> UOp:
|
||||
val = (s:=UOp.placeholder((1,), v.dtype, device=(devs:=to_tuple(b.device)), tag="usb_scratch")).after(s.index(0).store(v))
|
||||
addr = b.getaddr((HCQ_RUNTIME_DEV.value,)) + (idx*v.dtype.itemsize).cast(dtypes.uint64)
|
||||
return usb_stream(devs, b.src[1:] if b.op is Ops.AFTER else (), addr, val.index(0), v.dtype.itemsize, True)
|
||||
|
||||
def usb_idle(devs) -> UOp:
|
||||
v = usb_load(make_buf(devs, tag="timeline_signal").after(loop:=UOp.loop(0)), UOp.const(0, dtypes.int), dtypes.uint64)
|
||||
return v.end(loop, v + 1 < make_buf(devs, tag="timeline_value").index(0).load())
|
||||
|
||||
def usb_scsi(devs, read:bool, nbytes:int) -> UOp:
|
||||
return _libusb(devs, (usb_idle(devs),), "libusb_control_transfer", 0x40, 0xF2, ceildiv(nbytes, 512) | (0x8000 if read else 0),
|
||||
(ceildiv(nbytes, 0x4000) & 0xFF) << 8, UOp.const(0, dtypes.uint64), 0, 1000)
|
||||
|
||||
def usb_stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
if (cin:=to_tuple(src.device)[0].startswith("CPU")) == to_tuple(dst.device)[0].startswith("CPU"): return None
|
||||
|
||||
total, ops, win = dst.nbytes(), [], cast(Any, Device[(devs:=to_tuple((dst if cin else src).device))[0]]).iface.usb_sram
|
||||
for off in range(0, total, win.size): # off and nb are bytes, the two ends of the copy can have different dtypes
|
||||
sram = UOp.from_buffer(win)[0:(nb:=min(win.size, total - off))]
|
||||
s, d = src[off // src.dtype.itemsize:(off + nb) // src.dtype.itemsize], dst[off // dst.dtype.itemsize:(off + nb) // dst.dtype.itemsize]
|
||||
if cin:
|
||||
push = usb_bulk(devs, (usb_scsi(devs, False, nb),), 0x02, s.getaddr((HCQ_RUNTIME_DEV.value,)), round_up(nb, 512), 10000)
|
||||
ops += [UOp.custom_function("hcq", push.sink()).call(sram, s, name="hcq_copyin", aux=HCQInfo(devs)),
|
||||
sram.copy_to_device(d.device).call(d, sram)]
|
||||
else:
|
||||
pad = UOp.new_buffer("CPU", round_up(nb, 512), dtypes.uint8)[0:nb]
|
||||
submit = make_submit(UOp(Ops.CALL, dtypes.void, (UOp(Ops.COPY, dtypes.void, ()), sram, s)), devs=devs, queue="COPY:0")
|
||||
pull = usb_bulk(devs, (submit,), 0x81, pad.getaddr((HCQ_RUNTIME_DEV.value,)), round_up(nb, 512), 10000)
|
||||
ops += [UOp.custom_function("hcq", pull.sink()).call(pad, sram, s, name="hcq_copyout", aux=HCQInfo(devs)),
|
||||
pad.copy_to_device("CPU").call(d, pad)]
|
||||
return UOp(Ops.LINEAR, src=tuple(ops))
|
||||
pm_usb_stage = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.COPY), UPat(name="dst"), UPat(name="src"))), usb_stage_copy)])
|
||||
|
||||
def usb_arm_bytes(lin:UOp, sram:Buffer) -> int:
|
||||
dsts = [c.src[1] for c in lin.src if c.op is Ops.CALL and c.src[0].op is Ops.COPY] # the rest of a linear is INS, some with no srcs
|
||||
return next((d.nbytes() for d in dsts if d.base.op is Ops.BUFFER and d.base.buffer is sram), 0)
|
||||
|
||||
def usb_ib(devs, lin:UOp, align:int, arm:int=0) -> tuple[UOp, UOp, int]:
|
||||
pkt_dw = sum(s.dtype.itemsize for ins in lin.src for s in ins.src) // 4 # by bytes: sdma packs 64-bit addresses as single srcs
|
||||
kargs = dedup([b for b in lin.toposort() if b.op is Ops.PARAM and b.tag == "kernargs"])
|
||||
offs, up_dw = {}, round_up(pkt_dw, align)
|
||||
for k in kargs: offs[k], up_dw = up_dw, round_up(up_dw + k.max_numel(), 32)
|
||||
ib_gpu = UOp.placeholder((up_dw,), dtypes.uint32, device=devs, tag="cmdbuf")
|
||||
ib_host = UOp.placeholder((up_dw,), dtypes.uint32, device=devs, tag="usb_scratch")
|
||||
gsubs = {g: g.replace(src=(d if a.op is not Ops.AFTER else d.after(*a.src[1:]),)) for g in lin.toposort() if g.op is Ops.GETADDR
|
||||
for a in [g.src[0]] if (k:=a.src[0] if a.op is Ops.AFTER else a) in offs for d in [ib_gpu[offs[k]:offs[k] + k.max_numel()]]}
|
||||
lin = lin.substitute(gsubs, walk=True).substitute({k: ib_host[offs[k]:offs[k] + k.max_numel()] for k in kargs}, walk=True)
|
||||
return make_cmdbuf(lin, devs, buf=ib_host, dep=(usb_scsi(devs, True, arm),) if arm else ()), ib_gpu, pkt_dw
|
||||
|
||||
def usb_push(devs, ring:UOp, wptr:UOp, doorbell:UOp, put_ptr:UOp, ib_host:UOp, ib_gpu:UOp, pkt:tuple, unit:int) -> UOp:
|
||||
stage = UOp.placeholder(((n:=round_up(len(pkt), 4)) + 2,), dtypes.uint32, device=devs, tag="usb_scratch")
|
||||
put, step = put_ptr.index(zero:=UOp.const(0, dtypes.int)), (n * 4 if pkt else ib_host.nbytes()) // unit
|
||||
st = stage.after(*[stage.index(i).store(UOp.const(v, dtypes.uint32)) for i, v in enumerate(pkt)],
|
||||
*[stage.index(n + i).store((((put + step) >> (32 * i)) & 0xffffffff).cast(dtypes.uint32)) for i in (0, 1)])
|
||||
|
||||
writes = [(ib_gpu.getaddr((HCQ_RUNTIME_DEV.value,)), ib_host.index(zero), ib_gpu.nbytes())] if pkt else []
|
||||
writes += [(ring.getaddr((HCQ_RUNTIME_DEV.value,)) + ((put % (ring.nbytes() // unit)) * unit).cast(dtypes.uint64),
|
||||
(st if pkt else ib_host).index(zero), step * unit)]
|
||||
writes += [(p.getaddr((HCQ_RUNTIME_DEV.value,)), st.index(n), 8) for p in (wptr, doorbell)]
|
||||
return put_ptr.after(*usb_writes(devs, writes)).index(zero).store(put + step)
|
||||
|
||||
USB_HOST_TAGS = {"signal", "timeline_signal"}
|
||||
pm_usb_hostio = PatternMatcher([
|
||||
(UPat(Ops.LOAD, src=(UPat(Ops.INDEX, src=(UPat(Ops.PARAM, tag=USB_HOST_TAGS).or_after(name="b"), UPat(name="idx"))),),
|
||||
name="ld"), lambda b, idx, ld: usb_load(b, idx, ld.dtype)),
|
||||
(UPat(Ops.STORE, src=(UPat(Ops.INDEX, src=(UPat(Ops.PARAM, tag=USB_HOST_TAGS).or_after(name="b"), UPat(name="idx"))), UPat(name="v"))), usb_write)])
|
||||
|
||||
pm_usb_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag={"systems", "runtime", "inputs", "usb_scratch"}, name="b"),
|
||||
lambda ctx, b: Buffer("CPU", b.max_numel(), b.dtype, options=BufferSpec(nolru=True), preallocate=True)),
|
||||
(UPat(Ops.PARAM, tag="usb_handle", name="b"), lambda ctx, b: ctx[0].signal(b.tag, ctx[0].iface.usb_handle, device="CPU")),
|
||||
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if not isinstance(b.tag, str) or not b.tag.startswith("func:") else
|
||||
ctx[0].signal(b.tag, unwrap(ctypes.cast(getattr(libusb.dll, b.tag[5:]), ctypes.c_void_p).value), device="CPU")),
|
||||
])
|
||||
|
||||
if DEV.interface.startswith("MOCK"): from test.mockgpu.usb import MockUSB3 as USB3 # type: ignore # noqa: F811
|
||||
|
||||
+3
-1
@@ -1135,14 +1135,16 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# *** uop high level syntactic sugar ***
|
||||
|
||||
@staticmethod
|
||||
def placeholder(shape:tuple[int, ...], dtype:DType, slot:int, addrspace=AddrSpace.GLOBAL, device=None, volatile=False):
|
||||
def placeholder(shape:tuple[int, ...], dtype:DType, slot:int|None=None, addrspace=AddrSpace.GLOBAL, device=None, volatile=False, tag=None):
|
||||
dtype = strong_dtype(dtype) # storage is never weak: a placeholder commits the width of what's put in it
|
||||
if slot is None: slot = next(UOp.unique_num)
|
||||
if addrspace is AddrSpace.GLOBAL:
|
||||
ret = UOp(Ops.PARAM, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace, device=device,volatile=volatile))
|
||||
else:
|
||||
assert addrspace in (AddrSpace.LOCAL, AddrSpace.REG)
|
||||
assert device is None, "LOCAL and REG placeholders cannot have a device"
|
||||
ret = UOp(Ops.BUFFER, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace))
|
||||
if tag is not None: ret = ret.rtag(tag)
|
||||
if len(shape) > 1: ret = ret.reshape(shape)
|
||||
return ret
|
||||
def placeholder_like(self, slot:int, addrspace=AddrSpace.GLOBAL):
|
||||
|
||||
Reference in New Issue
Block a user