mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
289 lines
16 KiB
Python
289 lines
16 KiB
Python
import unittest, gc, struct, ctypes, threading, numpy as np
|
|
from tinygrad import Device, Tensor, TinyJit, Variable, dtypes, GlobalCounters
|
|
from tinygrad.device import Buffer, BufferSpec
|
|
from tinygrad.dtype import AddrSpace
|
|
from tinygrad.helpers import Context
|
|
from tinygrad.uop.ops import Ops, UOp, uopfunc
|
|
from tinygrad.engine.realize import compile_linear, link_linear, lower_and_compile, run_linear
|
|
from tinygrad.renderer.cstyle import CStyleLanguage
|
|
from tinygrad.renderer.llvmir import LLVMRenderer
|
|
from tinygrad.runtime.autogen import libc
|
|
from tinygrad.runtime.support.c import init_c_struct_t
|
|
import tinygrad.runtime.support.hcq2 as hcq2
|
|
from tinygrad.runtime.support.hcq2 import HCQ_DEVS, all_devices_in, hcq_compile_cache
|
|
from test.null.test_hcq2 import chain, chain_input, compiled_chain, lower_hcq
|
|
|
|
def cpu_buf(size:int=1, dtype=dtypes.uint8, tag=None, **kwargs) -> UOp: return UOp.alloc((size,), dtype, device="CPU", **kwargs).rtag(tag)
|
|
|
|
@unittest.skipUnless(all_devices_in(Device.DEFAULT, HCQ_DEVS) and not Device.DEFAULT.startswith("NULL"), "hcq2 device required")
|
|
class TestHCQ2Schedule(unittest.TestCase):
|
|
def test_compile_and_link_are_idempotent(self):
|
|
for jit in (False, True):
|
|
with self.subTest(jit=jit):
|
|
out, compiled, inputs = compiled_chain(2, jit=jit, device=Device.DEFAULT)
|
|
linked = link_linear(compiled, input_uops=inputs, allow_cache=not jit)
|
|
before = tuple(inputs)
|
|
for linear in (compiled, linked): self.assertIs(compile_linear(linear, input_uops=inputs, cache=not jit), linear)
|
|
self.assertIs(link_linear(linked, input_uops=inputs, allow_cache=not jit), linked)
|
|
self.assertEqual(tuple(inputs), before)
|
|
run_linear(linked, input_uops=inputs, jit=True, wait=True)
|
|
self.assertEqual(out.tolist(), [4] * 4)
|
|
|
|
def test_jit_new_inputs_each_call(self):
|
|
@TinyJit
|
|
def f(a, b): return (a * b + a).contiguous().realize()
|
|
ins = [(Tensor.full((23,), float(i)).contiguous().realize(), Tensor.full((23,), 2.0).contiguous().realize()) for i in range(6)]
|
|
for a, b in ins[:3]: f(a, b).tolist() # warm the jit and the copyout
|
|
|
|
before = len(hcq_compile_cache)
|
|
for i, (a, b) in enumerate(ins[3:], 3): self.assertEqual(f(a, b).tolist(), [i * 3.0] * 23)
|
|
self.assertEqual(len(hcq_compile_cache), before)
|
|
|
|
def test_jit_symbolic(self):
|
|
@TinyJit
|
|
def f(a): return (a + 1).sum().contiguous().realize()
|
|
a = Tensor.rand(3, 10).contiguous().realize()
|
|
for i in range(1, 5):
|
|
vi = Variable("i", 1, 10).bind(i)
|
|
np.testing.assert_allclose(f(a[:, :vi]).item(), (a[:, :i] + 1).sum().item(), atol=1e-5, rtol=1e-5)
|
|
|
|
def test_repeated_copy(self):
|
|
vram, host, new = [Buffer(d, 4096, preallocate=True) for d in (Device.DEFAULT, "CPU", "CPU")]
|
|
new.host[:] = bytes(range(256)) * 16
|
|
hu, vu, nu = [UOp.from_buffer(b, dtypes.uint8) for b in (host, vram, new)]
|
|
copyout, copyin = hu.store_call(vu), vu.store_call(nu)
|
|
run_linear(UOp(Ops.LINEAR, src=(copyout, copyin, copyout)), wait=True)
|
|
self.assertEqual(bytes(host.host[:]), bytes(new.host[:]))
|
|
|
|
@unittest.skipIf(Device.DEFAULT == "METAL", "unified memory: METAL copies on the host and maps nothing")
|
|
def test_map_cpu_buffer_preserves_contents(self):
|
|
src = Buffer("CPU", 16, preallocate=True)
|
|
data = bytes(range(16))
|
|
src.host[:] = data
|
|
src.get_buf(Device.DEFAULT)
|
|
self.assertEqual(bytes(src.as_memoryview()), data)
|
|
|
|
def test_caches_hold_no_buffers(self):
|
|
# an eager template caches without its buffers and the jit's linear compiles once uncached: freeing the tensors frees the device memory
|
|
def step(i):
|
|
buf = Buffer("NPY", 4096, initial_value=struct.pack("f", i) * 1024)
|
|
x = Tensor(UOp.from_buffer(buf, dtypes.float32)).to(Device.DEFAULT).realize()
|
|
@TinyJit
|
|
def f(a): return (a * 2 + 1).contiguous().realize()
|
|
for _ in range(3): out = f(x)
|
|
self.assertEqual(out.to("CPU").tolist(), [2.0 * i + 1] * 1024)
|
|
step(1) # warms the programs, templates and rings
|
|
gc.collect()
|
|
used = GlobalCounters.mem_used
|
|
for i in range(2, 5): step(i)
|
|
gc.collect()
|
|
self.assertEqual(GlobalCounters.mem_used, used)
|
|
|
|
def test_jit_has_no_rt_buffers(self):
|
|
# a one shot link borrows ring slots, a jit's link owns its buffers: nothing it keeps may come from the ring
|
|
dev = Device[Device.DEFAULT]
|
|
specs = (BufferSpec(cpu_access=True), BufferSpec(host=True, uncached=True, cpu_access=True)) # device data, signals
|
|
ranges = [((b:=dev.rt_buffer(spec))._buf, b._buf + b.nbytes) for spec in specs]
|
|
x, f = chain_input(device=dev.device), TinyJit(lambda a: chain(a, 2).realize())
|
|
for _ in range(2): f(x)
|
|
for u in f.captured.linear.toposort(enter_calls=True):
|
|
if u.op is Ops.BUFFER and u.addrspace is AddrSpace.GLOBAL and (buf:=u.buffer).device == dev.device:
|
|
self.assertFalse(any(buf._buf < end and start < buf._buf + buf.nbytes for start, end in ranges))
|
|
|
|
# the fence and the ffi run on the CPU runtime, which is always available
|
|
@unittest.skipUnless(isinstance(Device["CPU"].renderer, (CStyleLanguage, LLVMRenderer)), "CALL is rendered in C style and LLVM only")
|
|
class TestHCQ2Fence(unittest.TestCase):
|
|
def setUp(self):
|
|
self.enterContext(Context(HCQ_RUNTIME_DEV="CPU"))
|
|
self.tl = Device["CPU"].timeline.host.view(fmt='Q')
|
|
self.addCleanup(lambda: self.tl.__setitem__(0, self.tl[1]))
|
|
|
|
def test_a_schedule_waits_for_its_previous_run(self):
|
|
slots = UOp.alloc((4,), dtypes.uint64, device="CPU").rtag("slots")
|
|
program = lower_and_compile(lower_hcq(UOp.custom_function("hcq_fence").call(slots[0:2], slots[2:4])))
|
|
linked = hcq2.hcq_link(program, allow_cache=False)
|
|
(i,) = [i for i, p in enumerate(program.src[0].without_after.src[1:]) if p.without_after.tag == "slots"]
|
|
slots_mv = linked.src[0].without_after.src[1 + i].buffer.host.view(fmt='Q')
|
|
slots_mv[2], base = 7, self.tl[1]
|
|
|
|
run_linear(linked, jit=True)
|
|
self.assertEqual((self.tl[1], slots_mv[0], slots_mv[2]), (base + 1, base + 1, 0), "the run is announced, recorded, the signal re-armed")
|
|
|
|
t = threading.Thread(target=run_linear, args=(linked,), kwargs={"jit": True}, daemon=True)
|
|
t.start()
|
|
t.join(0.2)
|
|
self.assertTrue(t.is_alive(), "the second run must wait for the first to finish")
|
|
self.tl[0] = base + 1
|
|
t.join(5)
|
|
self.assertFalse(t.is_alive())
|
|
self.assertEqual(self.tl[1], base + 2)
|
|
|
|
@unittest.skipUnless(isinstance(Device["CPU"].renderer, (CStyleLanguage, LLVMRenderer)), "CALL is rendered in C style and LLVM only")
|
|
class TestHCQ2FFI(unittest.TestCase):
|
|
@staticmethod
|
|
def _run(body:UOp) -> list[UOp]:
|
|
linear = hcq2.hcq_link(lower_and_compile(lower_hcq(body)), allow_cache=False)
|
|
assert hcq2.hcq_link(linear, allow_cache=False) is linear, "a linked linear links to itself, with the refs its call keeps"
|
|
run_linear(linear, jit=True)
|
|
return [u for u in linear.src[0].without_after.src[1:] if u.op is Ops.BUFFER]
|
|
|
|
def test_ffi_ccall(self):
|
|
with Context(HCQ_RUNTIME_DEV="CPU"):
|
|
out = cpu_buf(dtype=dtypes.int32, slot=1, tag="ffi_result")
|
|
bufs = self._run(out.index(0).store(hcq2.ccall(libc.dll.ffs, 0x10)))
|
|
self.assertEqual(next(b for b in bufs if b.dtype is dtypes.int).buffer.host.view(fmt='i')[0], 5)
|
|
|
|
def test_nested_ffi_call(self, host="CPU"): # a function calls a C function: no pointer to pass, the symbol links
|
|
@uopfunc
|
|
def copy(dst:UOp, src:UOp): return hcq2.ccall(libc.memcpy, dst.index(0), src.index(0), 4).sink()
|
|
@uopfunc
|
|
def copy_pair(dst:UOp, src:UOp): return copy(dst.after(copy(dst, src)).index(1), src).sink()
|
|
|
|
with Context(HCQ_RUNTIME_DEV=host):
|
|
src = hcq2.cstruct(init_c_struct_t(4, (("value", ctypes.c_uint32, 0),)), value=42)
|
|
out = cpu_buf(2, dtypes.uint32, tag="ffi_result")
|
|
bufs = self._run(copy_pair(out, src.bitcast(dtypes.uint32)))
|
|
self.assertEqual(list(next(b for b in bufs if b.dtype is dtypes.uint32).buffer.host.view(fmt='I')), [42, 42])
|
|
def test_nested_ffi_call_python(self): self.test_nested_ffi_call("PYTHON")
|
|
|
|
def test_ffi_cstruct(self):
|
|
struct_t = init_c_struct_t(16, (("u8", ctypes.c_uint8, 0), ("u16", ctypes.c_uint16, 2),
|
|
("u32", ctypes.c_uint32, 4), ("u64", ctypes.c_uint64, 8)))
|
|
cpu_buf() # reserve slot zero for device-owned placeholders
|
|
with Context(HCQ_RUNTIME_DEV="CPU"):
|
|
s = hcq2.cstruct(struct_t, u8=0x12, u16=UOp.const(0x3456, dtypes.uint16), u32=0x789ABCDE, u64=0xFEDCBA9876543210)
|
|
bufs = self._run(s.index(0).load())
|
|
got = struct_t.from_buffer_copy(bytes(next(b for b in bufs if b.nbytes() == ctypes.sizeof(struct_t)).buffer.host.view(fmt='B')))
|
|
self.assertEqual((got.u8, got.u16, got.u32, got.u64), (0x12, 0x3456, 0x789ABCDE, 0xFEDCBA9876543210))
|
|
|
|
def test_nested_cstruct_patches(self):
|
|
with Context(HCQ_RUNTIME_DEV="CPU"):
|
|
inner = hcq2.cstruct(init_c_struct_t(8, (("pad", ctypes.c_uint32, 0), ("value", ctypes.c_uint32, 4))), value=42)
|
|
outer = hcq2.cstruct(init_c_struct_t(8, (("ptr", ctypes.c_uint64, 0),)), ptr=inner[4:8].getaddr("CPU"))
|
|
out = cpu_buf(dtype=dtypes.uint32, tag="result")
|
|
copied = hcq2.ccall(libc.memcpy, out.index(0), outer.bitcast(dtypes.uint64).index(0).load(), 4)
|
|
bufs = self._run(out.after(copied).index(0).load())
|
|
self.assertEqual(next(b for b in bufs if b.dtype is dtypes.uint32).buffer.host.view(fmt='I')[0], 42)
|
|
|
|
@uopfunc
|
|
def addr_of(o:UOp, b:UOp): return o.index(0).store(b.getaddr("CPU")).sink() # o[0] = &b
|
|
|
|
# host functions in a batch
|
|
@unittest.skipUnless(isinstance(Device["CPU"].renderer, (CStyleLanguage, LLVMRenderer)), "CALL is rendered in C style and LLVM only")
|
|
class TestHostCalls(unittest.TestCase):
|
|
def setUp(self): self.enterContext(Context(HCQ_RUNTIME_DEV="CPU"))
|
|
|
|
@staticmethod
|
|
def _buf(n:int, dtype=dtypes.uint64) -> UOp:
|
|
return UOp.from_buffer(Buffer("CPU", n * dtype.itemsize, initial_value=bytes(n * dtype.itemsize)), dtype)
|
|
@staticmethod
|
|
def _run(fxn, *bufs:UOp, **var_vals:int) -> list: # first buffer is the output
|
|
run_linear(hcq2.hcq_link(lower_and_compile(lower_hcq(fxn(*bufs))), allow_cache=False), var_vals, jit=True)
|
|
return bufs[0].buffer.host.view(fmt=bufs[0].dtype.fmt)[:]
|
|
|
|
def test_no_addrs_no_placeholders(self):
|
|
@uopfunc
|
|
def inc(o:UOp, a:UOp): return o.index(0).store(a.index(0).load() + 1).sink()
|
|
a = UOp.from_buffer(Buffer("CPU", 8, initial_value=struct.pack("Q", 41)), dtypes.uint64)
|
|
self.assertEqual(self._run(inc, self._buf(1), a), [42])
|
|
|
|
def test_addr_of_arg(self):
|
|
b = self._buf(16, dtypes.uint8)
|
|
self.assertEqual(self._run(addr_of, self._buf(1), b), [b.buffer.get_buf("CPU")])
|
|
|
|
def test_addr_of_view(self):
|
|
@uopfunc
|
|
def addr(o:UOp, b:UOp): return o.index(0).store(b[4:8].getaddr("CPU")).sink()
|
|
b = self._buf(16, dtypes.uint8)
|
|
self.assertEqual(self._run(addr, self._buf(1), b), [b.buffer.get_buf("CPU") + 4])
|
|
|
|
def test_sink_calls_function(self): # a bare sink with args calls a function that writes one arg and takes the address of the other
|
|
def sink(o:UOp, b:UOp): return addr_of(UOp.param(0, dtypes.uint64, 1), UOp.param(1, dtypes.uint8, 16)).sink().call(o, b, name="sink")
|
|
outs, b = [self._buf(1) for _ in range(2)], self._buf(16, dtypes.uint8)
|
|
for out in outs: self.assertEqual(self._run(sink, out, b), [b.buffer.get_buf("CPU")]) # the second run reuses the program
|
|
|
|
def test_nested_functions(self): # the address is taken two calls deep
|
|
@uopfunc
|
|
def mid(o:UOp, b:UOp): return addr_of(o, b).sink()
|
|
@uopfunc
|
|
def top(o:UOp, b:UOp): return mid(o, b).sink()
|
|
b = self._buf(16, dtypes.uint8)
|
|
self.assertEqual(self._run(top, self._buf(1), b), [b.buffer.get_buf("CPU")])
|
|
|
|
def test_call_sites(self): # one function, called on different args
|
|
@uopfunc
|
|
def both(o:UOp, a:UOp, b:UOp): return UOp.sink(addr_of(o[0:1], a), addr_of(o[1:2], b))
|
|
a, b = self._buf(16, dtypes.uint8), self._buf(16, dtypes.uint8)
|
|
self.assertEqual(self._run(both, self._buf(2), a, b), [a.buffer.get_buf("CPU"), b.buffer.get_buf("CPU")])
|
|
|
|
def test_nested_placeholders(self): # storage a function keeps for itself
|
|
@uopfunc
|
|
def keep(o:UOp):
|
|
tmps = [cpu_buf(dtype=dtypes.uint64, tag="tmp") for _ in range(2)]
|
|
return UOp.sink(*[o.index(i).store(t.after(t.index(0).store(7 + i)).index(0).load()) for i, t in enumerate(tmps)])
|
|
@uopfunc
|
|
def top(o:UOp): return keep(o).sink()
|
|
self.assertEqual(self._run(top, self._buf(2)), [7, 8])
|
|
|
|
def test_addr_of_placeholder(self): # the address is the one of the buffer the placeholder links to
|
|
@uopfunc
|
|
def keep(o:UOp):
|
|
tmp = cpu_buf(dtype=dtypes.uint64, tag="tmp")
|
|
return UOp.sink(tmp.index(0).store(7), o.index(0).store(tmp.getaddr("CPU")))
|
|
self.assertEqual(ctypes.c_uint64.from_address(self._run(keep, self._buf(1))[0]).value, 7)
|
|
|
|
def test_cstruct(self): # a patched placeholder inside a function
|
|
@uopfunc
|
|
def read(o:UOp):
|
|
s = hcq2.cstruct(init_c_struct_t(8, (("pad", ctypes.c_uint32, 0), ("value", ctypes.c_uint32, 4))), value=42)
|
|
return o.index(0).store(s.bitcast(dtypes.uint32).index(1).load().cast(dtypes.uint64)).sink()
|
|
self.assertEqual(self._run(read, self._buf(1)), [42])
|
|
|
|
def test_variable_in_function(self): # a variable is passed to a function, the program binds it by name
|
|
@uopfunc
|
|
def scale(o:UOp, a:UOp, k:UOp): return o.index(0).store(a.index(0).load() * k).sink()
|
|
@uopfunc
|
|
def top(o:UOp, a:UOp): return scale(o, a, UOp.variable("k", 0, 10, dtypes.uint64)).sink()
|
|
a = UOp.from_buffer(Buffer("CPU", 8, initial_value=struct.pack("Q", 7)), dtypes.uint64)
|
|
self.assertEqual(self._run(top, self._buf(1), a, k=6), [42])
|
|
|
|
def test_weak_variable_in_function(self): # a weak variable reached in a function
|
|
@uopfunc
|
|
def put(o:UOp): return o.index(0).store(UOp.variable("n", 1, 10).cast(dtypes.uint64)).sink()
|
|
self.assertEqual(self._run(put, self._buf(1), n=7), [7])
|
|
|
|
def test_weak_variable_as_arg(self): # a weak arg commits its dtype, like lift does
|
|
@uopfunc
|
|
def put(o:UOp, v:UOp): return o.index(0).store(v.cast(dtypes.uint64)).sink()
|
|
@uopfunc
|
|
def top(o:UOp): return put(o, UOp.variable("n", 1, 10)).sink()
|
|
self.assertEqual(self._run(top, self._buf(1), n=7), [7])
|
|
|
|
def test_addr_of_arg_after_a_write(self): # the arg contains the param it replaces
|
|
@uopfunc
|
|
def top(o:UOp, b:UOp): return addr_of(o, b.after(b.index(0).store(2))).sink()
|
|
b = self._buf(1)
|
|
self.assertEqual(self._run(top, self._buf(1), b), [b.buffer.get_buf("CPU")])
|
|
|
|
def test_inputs_out_of_slot_order(self): # inputs reached out of slot order
|
|
p = [UOp.param(i, dtypes.uint64, 1, "CPU") for i in range(2)]
|
|
self.assertEqual(lower_hcq(p[1].index(0).load(), p[0].index(0).load()).src[0].without_after.src[1:], (p[1], p[0]))
|
|
|
|
def test_one_function_for_any_placeholder(self): # a function names its params: what it is called on does not make another function
|
|
@uopfunc
|
|
def put(out:UOp, v:UOp): return out.index(0).store(v).sink()
|
|
a, b = [cpu_buf(dtype=dtypes.uint64, tag=t) for t in ("cb", "enc")]
|
|
lowered = lower_hcq(put(a, UOp.const(1, dtypes.uint64)), put(b, UOp.const(2, dtypes.uint64)))
|
|
self.assertEqual(len({c.body for c in lowered.toposort(enter_calls=True) if c.op is Ops.CALL and c.arg.name == "put"}), 1)
|
|
|
|
def test_one_function_with_registers(self): # a body numbers its own registers and loops: two traces are one function
|
|
@uopfunc
|
|
def put(out:UOp, v:UOp):
|
|
r = UOp.placeholder((1,), dtypes.uint64, addrspace=AddrSpace.REG)
|
|
return out.index(0).store(r.after(r.index(0).store(v)).index(0).load()).sink()
|
|
calls = [put(cpu_buf(dtype=dtypes.uint64), UOp.const(1, dtypes.uint64)) for _ in range(2)]
|
|
self.assertIs(calls[0].body, calls[1].body)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|