mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
174 lines
8.3 KiB
Python
174 lines
8.3 KiB
Python
import os, sys, time, struct, functools, unittest
|
|
from dataclasses import replace
|
|
from typing import Any, Callable
|
|
import numpy as np
|
|
from tinygrad import Tensor, dtypes, Device
|
|
from tinygrad.device import Buffer
|
|
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
|
from tinygrad.tensor import _to_np_dtype
|
|
from tinygrad.codegen import to_program
|
|
from tinygrad.dtype import DType, truncate, AddrSpace
|
|
from tinygrad.nn.state import get_parameters
|
|
from tinygrad.helpers import T, Target, DEV, DEBUG, Context, GlobalCounters
|
|
from tinygrad.renderer import Renderer
|
|
from tinygrad.codegen import full_rewrite_to_sink, line_rewrite, pm_linearize_cleanups
|
|
from tinygrad.codegen.late.linearizer import linearize
|
|
from tinygrad.engine.realize import compile_linear
|
|
|
|
# decorator to skip slow tests by default, run with RUN_SLOW=1 to include them
|
|
slow = unittest.skipUnless(os.getenv("RUN_SLOW"), "slow test, set RUN_SLOW=1 to run")
|
|
from tinygrad.runtime.ops_python import PythonRenderer
|
|
|
|
def full_rewrite(sink:UOp, ren:Renderer|None=None) -> UOp:
|
|
if ren is None: ren = Renderer(Target())
|
|
if sink.arg is None: sink = sink.replace(arg=KernelInfo())
|
|
return full_rewrite_to_sink(sink, ren, optimize=sink.tag is None)
|
|
|
|
def get_uops(sink:UOp, ren:Renderer|None=None) -> list[UOp]:
|
|
"""Extract linearized UOps from a sink. Test helper that only does linearization (no render)."""
|
|
full_sink = full_rewrite(sink, ren)
|
|
return line_rewrite(linearize(full_sink), pm_linearize_cleanups)
|
|
|
|
def replace_opts(ast:UOp, opts:list) -> UOp: return ast.replace(arg=replace(ast.arg, opts_to_apply=tuple(opts)))
|
|
|
|
def buffer_uops(ast:UOp, bufs:list[Buffer]) -> list[UOp]:
|
|
params = {p.arg.slot:p.dtype for p in ast.toposort() if p.op is Ops.PARAM and p.addrspace is not AddrSpace.ALU}
|
|
return [UOp.from_buffer(b, params[i]) for i, b in enumerate(bufs)]
|
|
|
|
def derandomize_model(model):
|
|
for p in get_parameters(model):
|
|
p.replace(Tensor.empty(p.shape, device=p.device, dtype=p.dtype))
|
|
p.realize()
|
|
|
|
class KernelCountException(Exception):
|
|
def __init__(self, expected:int, got:int):
|
|
self.expected, self.got = expected, got
|
|
super().__init__(f"expected {expected}, got {got}")
|
|
|
|
def check_schedule(t:Tensor|list[Tensor]|UOp, allowed:int, to_prerealize:list[Tensor]|None=None, filter_sink=True):
|
|
if to_prerealize:
|
|
with Context(DEBUG=0, TRACK_MATCH_STATS=0): Tensor.realize(*to_prerealize)
|
|
if isinstance(t, Tensor): linear, var_vals = t.linear_with_vars()
|
|
elif isinstance(t, list) and isinstance(t[0], Tensor): linear, var_vals = Tensor.linear_with_vars(*t)
|
|
else:
|
|
assert isinstance(t, UOp), f"can't schedule {t}"
|
|
linear, var_vals = Tensor(t).linear_with_vars()
|
|
# test compiling the linear
|
|
compile_linear(linear)
|
|
kernel_cnt = sum((len(call.device) if isinstance(call.device, tuple) else 1)
|
|
for call in linear.src if call.src[0].op is Ops.SINK or not filter_sink)
|
|
if kernel_cnt != allowed:
|
|
print(f"SCHEDULE ISSUE, expecting {allowed} got {kernel_cnt}")
|
|
if DEBUG >= 3:
|
|
for i,call in enumerate(linear.src):
|
|
print("kernel", i+1)
|
|
print(call.src[0])
|
|
raise KernelCountException(allowed, kernel_cnt)
|
|
return linear, var_vals
|
|
|
|
def assert_kernel_count(expected:int):
|
|
got = GlobalCounters.kernel_count
|
|
if got != expected: raise KernelCountException(expected, got)
|
|
|
|
def is_hcq2_device() -> bool: # an hcq2 device stages every copy from the host through a pinned buffer: such a copy is two calls, not one
|
|
from tinygrad.runtime.support.hcq2 import HCQ_DEVS
|
|
return Device.DEFAULT.split(":")[0] in HCQ_DEVS
|
|
|
|
def call_is_hcq(call:UOp) -> bool: # an hcq2 batch: a compiled body whose aux lists the kernels it submits
|
|
from tinygrad.runtime.support.hcq2 import HCQInfo
|
|
return isinstance(getattr(call.without_after.arg, "aux", None), HCQInfo)
|
|
|
|
def jit_cache_count(linear:UOp) -> int:
|
|
return sum(len(call.without_after.arg.aux.kernels) if call_is_hcq(call) else 1 for call in linear.src)
|
|
|
|
def assert_jit_cache_len(fxn, expected_len):
|
|
linear = fxn.captured.linear if fxn.captured is not None else None
|
|
if linear is None or not linear.src:
|
|
if expected_len != 0: raise KernelCountException(expected_len, 0)
|
|
return
|
|
if expected_len and any(call_is_hcq(call) for call in linear.src): # HCQ2: kernels batch into submits, the finalizers carry the batch's kernels
|
|
count = sum(len(call.without_after.arg.aux.kernels) if call_is_hcq(call) else 1 for call in linear.src)
|
|
if count != expected_len: raise KernelCountException(expected_len, count)
|
|
return
|
|
if len(linear.src) != expected_len: raise KernelCountException(expected_len, len(linear.src))
|
|
|
|
def prepare_test_op(low, high, shps, vals, forward_only=False):
|
|
import torch
|
|
if shps is None:
|
|
ts = [torch.tensor(x, requires_grad=(not forward_only)) for x in vals]
|
|
else:
|
|
np.random.seed(0)
|
|
np_data = [np.random.uniform(low=low, high=high, size=size).astype(_to_np_dtype(dtypes.default_float)) for size in shps]
|
|
ts = [torch.tensor(data, requires_grad=(not forward_only)) for data in np_data]
|
|
for i in range(len(ts)):
|
|
# NOTE: torch default int64 for python ints input
|
|
if ts[i].dtype == torch.int64: ts[i] = ts[i].type(torch.int32)
|
|
tst = [Tensor(x.detach().cpu().numpy()) for x in ts]
|
|
return ts, tst
|
|
|
|
class TensorTestCase(unittest.TestCase):
|
|
def helper_test_exception(self, shps, torch_fxn, tinygrad_fxn=None, expected=None, forward_only=False, exact=False, vals=None, low=-1.5, high=1.5):
|
|
if DEV.interface.startswith("MOCK") and Device.DEFAULT == "NV": self.skipTest('helper_test_exception fails in CI CUDA')
|
|
ts, tst = prepare_test_op(low, high, shps, vals, forward_only)
|
|
if tinygrad_fxn is None:
|
|
tinygrad_fxn = torch_fxn
|
|
with self.assertRaises(expected) as torch_cm:
|
|
torch_fxn(*ts)
|
|
with self.assertRaises(expected) as tinygrad_cm:
|
|
tinygrad_fxn(*tst)
|
|
if exact: self.assertEqual(str(torch_cm.exception), str(tinygrad_cm.exception))
|
|
if sys.stdout.isatty(): print("\ntesting %40r torch/tinygrad exception: %s / %s" % (shps, torch_cm.exception, tinygrad_cm.exception), end="")
|
|
|
|
def min_normal(dt:DType) -> float: return 2.0 ** (2 - (1 << (dtypes.finfo(dt)[0] - 1)))
|
|
|
|
def rand_for_dtype(dt:DType, size:int, allow_subnormal=True):
|
|
if dtypes.is_unsigned(dt):
|
|
return np.random.randint(0, 100, size=size, dtype=_to_np_dtype(dt))
|
|
elif dtypes.is_int(dt):
|
|
return np.random.randint(-100, 100, size=size, dtype=_to_np_dtype(dt))
|
|
elif dt == dtypes.bool:
|
|
return np.random.choice([True, False], size=size)
|
|
ret = np.random.uniform(-10, 10, size=size).astype(_to_np_dtype(dt))
|
|
if dt == dtypes.bfloat16 or dt in dtypes.fp8s: ret = np.array([truncate[dt](x) for x in ret], dtype=ret.dtype)
|
|
if not allow_subnormal: ret = np.where(np.abs(ret) < min_normal(dt), 0, ret)
|
|
return ret
|
|
|
|
def timeit(fxn:Callable[..., T], *args, **kwargs) -> tuple[T, float]:
|
|
st = time.perf_counter_ns()
|
|
ret = fxn(*args, **kwargs)
|
|
return ret, (time.perf_counter_ns()-st)*1e-6
|
|
|
|
def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None, vals:tuple[int, ...]=()):
|
|
dev = Device['PYTHON']
|
|
allocator = dev.allocator
|
|
bufs = []
|
|
for buf_dt, data in inputs or []:
|
|
bufs.append(buf:=allocator.alloc(len(data) * buf_dt.itemsize))
|
|
allocator._copyin(buf.buf, memoryview(struct.pack(str(len(data)) + (buf_dt.fmt or ""), *data)))
|
|
g = UOp.param(0, uop.dtype, 1)
|
|
prg = to_program(UOp.store(g.index(UOp.const(0)), uop).sink(arg=KernelInfo()), PythonRenderer(Target("PYTHON")))
|
|
prog = dev.runtime(prg.to_elf())
|
|
out_buf = Buffer("PYTHON", uop.dtype.itemsize, preallocate=True)
|
|
prog(out_buf._buf, *[b.buf for b in bufs], vals=vals)
|
|
return out_buf.as_memoryview().cast(uop.dtype.fmt or "").tolist()[0]
|
|
|
|
def to_uops_list(u:list[UOp], ren=None) -> list[UOp]:
|
|
sink = UOp.sink(*u)
|
|
for r in sink.ranges: sink = sink.end(r)
|
|
ret = get_uops(sink.sink(arg=KernelInfo(opts_to_apply=())), ren)
|
|
assert ret[-1].op is Ops.SINK
|
|
return ret
|
|
|
|
def not_support_multi_device():
|
|
# CL and CUDA don't support multi device if in CI
|
|
return (Device.DEFAULT == "CL" and Device[Device.DEFAULT].count() < 2) or (Device.DEFAULT == "CUDA" and DEV.interface.startswith("MOCK"))
|
|
|
|
def needs_second_gpu(fn):
|
|
@functools.wraps(fn)
|
|
def wrapper(self, *args, **kwargs):
|
|
# check if there's a second GPU, if not, skip multi tests
|
|
try: Tensor.zeros(10, device=f"{Device.DEFAULT}:1").contiguous().realize()
|
|
except Exception as e: self.skipTest(f"second device not available: {e}")
|
|
return fn(self, *args, **kwargs)
|
|
return wrapper
|