Compare commits

..
Author SHA1 Message Date
geohot 303c5d3259 we don't float anymore 2026-07-21 18:33:34 -07:00
geohot 048f510b51 cleanups 2026-07-21 18:29:13 -07:00
geohot cacba3f4d5 cleanups 2026-07-21 18:11:20 -07:00
geohot 01c6f396b1 upd 2026-07-21 17:36:05 -07:00
geohot d51003bb61 LOOP is srcless RANGE (kimi) 2026-07-21 17:18:17 -07:00
58 changed files with 563 additions and 895 deletions
+1 -1
View File
@@ -291,7 +291,7 @@ jobs:
llvm: 'true'
- name: Test openpilot model kernel count and gate usage
run: |
ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1361 ALLOWED_GATED_READ_IMAGE=54 FLOAT16=1 DEV="CL::IMAGE_PITCH_ALIGNMENT=64" IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1391 ALLOWED_GATED_READ_IMAGE=58 FLOAT16=1 DEV="CL::IMAGE_PITCH_ALIGNMENT=64" IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
# IMAGE_PITCH_ALIGNMENT=64 matches adreno 630
- name: Test openpilot CL compile fp32 (test correctness)
run: |
+22 -48
View File
@@ -10,7 +10,7 @@ if __name__ == "__main__":
from tinygrad import Tensor, nn, function, getenv, dtypes, TinyJit
from tinygrad.helpers import Timing, colored, GlobalCounters, profile_marker
from tinygrad.uop.ops import Ops, UOp
from extra.models.llama import apply_rotary_emb
from extra.models.llama import apply_rotary_emb, precompute_freqs_cis
from extra.llama_kernels.rmsnorm import rmsnorm
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8
@@ -70,11 +70,6 @@ def swiglu(x:Tensor, limit:float=7.0, alpha:float=1.702) -> Tensor:
x_linear = x_linear.clamp(-limit, limit)
return (x_glu * (alpha * x_glu).sigmoid()) * (x_linear + 1)
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0) -> Tensor:
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2, dtype=dtypes.float32)[:(dim // 2)] / dim))
freqs = Tensor.arange(end, dtype=dtypes.float32).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
return Tensor.stack(freqs.cos(), freqs.sin(), dim=-1).cast(dtypes.default_float).reshape(1, end, 1, dim//2, 2)
class GPTOSS:
def __init__(self, dim:int, n_layers:int, n_heads:int, n_kv_heads:int, head_dim:int, n_experts:int, experts_per_tok:int,
intermediate_size:int, vocab_size:int, norm_eps:float=1e-5, rope_theta:int=150000, sliding_window:int=128,
@@ -117,30 +112,14 @@ class GPTOSS:
w_q, w_e8, _ = quantize_mxfp8(w)
return w_q, w_e8.is_param_(False)
def _attn_mask(self, seqlen:int, dtype) -> Tensor:
def _attn_mask(self, seqlen:int, sliding:bool, dtype) -> Tensor:
i, j = Tensor.arange(seqlen).reshape(seqlen, 1), Tensor.arange(seqlen).reshape(1, seqlen)
return (j <= i).where(0.0, -1e30).cast(dtype).contiguous()
allowed = j <= i
if sliding: allowed = allowed & (i - j < self.sliding_window)
return allowed.where(0.0, -1e30).cast(dtype).contiguous()
def _sliding_attention(self, xq:Tensor, xk:Tensor, xv:Tensor, sinks:Tensor) -> Tensor:
bsz, seqlen, H, hd = xq.shape
KV, R, W = self.n_kv_heads, self.n_rep, self.sliding_window
assert seqlen % W == 0, f"seqlen {seqlen} must be a multiple of sliding_window {W} for banded attention"
nb = seqlen // W
q = xq.reshape(bsz, seqlen, KV, R, hd).permute(0, 2, 3, 1, 4).reshape(bsz, KV, R, nb, W, hd).float()
k, v = (x.permute(0, 2, 1, 3).reshape(bsz, KV, 1, nb, W, hd).float() for x in (xk, xv))
kk, vv = (x.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb].cat(x, dim=-2) for x in (k, v))
sc = (q @ kk.transpose(-1, -2)) * self.sm_scale # (B,KV,R,nb,W,2W)
i, j, pv = Tensor.arange(W).reshape(W, 1), Tensor.arange(2 * W).reshape(1, 2 * W), Tensor.arange(nb).reshape(nb, 1, 1) >= 1
sc = ((j > i) & (j <= i + W) & (pv | (j >= W))).where(sc, -float("inf"))
sink = sinks.reshape(1, KV, R, 1, 1, 1).float()
m = sc.max(-1, keepdim=True).maximum(sink)
e = (sc - m).exp()
p = (e / (e.sum(-1, keepdim=True) + (sink - m).exp())).cast(dtypes.bfloat16)
attn = p @ vv.cast(dtypes.bfloat16)
return attn.reshape(bsz, KV, R, seqlen, hd).permute(0, 3, 1, 2, 4).reshape(bsz, seqlen, H * hd)
def attention(self, x:Tensor, freqs_cis:Tensor, mask:Tensor, sliding:bool, *, attention_norm:Tensor, wqkv:Tensor,
wqkv_scale:Tensor, wqkv_bias:Tensor, wo:Tensor, wo_scale:Tensor, wo_bias:Tensor, sinks:Tensor):
def attention(self, x:Tensor, freqs_cis:Tensor, mask:Tensor, *, attention_norm:Tensor, wqkv:Tensor, wqkv_scale:Tensor,
wqkv_bias:Tensor, wo:Tensor, wo_scale:Tensor, wo_bias:Tensor, sinks:Tensor):
bsz, seqlen, _ = x.shape
x_normed, rrms = rmsnorm(x, self.norm_eps)
qkv = matmul_mx(x_normed * attention_norm, wqkv, wqkv_scale) + wqkv_bias
@@ -148,23 +127,16 @@ class GPTOSS:
xq = qkv[:, :, :, :self.n_rep].reshape(bsz, seqlen, self.n_heads, self.head_dim)
xk, xv = qkv[:, :, :, self.n_rep], qkv[:, :, :, self.n_rep + 1]
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
xq, xk, xv = xq.cast(dtypes.bfloat16), xk.cast(dtypes.bfloat16), xv.cast(dtypes.bfloat16) # (B,N,H,D)/(B,N,KV,D)
if sliding:
attn = self._sliding_attention(xq, xk, xv, sinks)
elif getenv("HK_FLASH_ATTENTION"):
from extra.thunder.amd.fa import flash_attention
attn, *_ = flash_attention(xq, xk, xv, is_causal=True, write_flat=True, sinks=sinks)
attn = attn.reshape(bsz, seqlen, self.n_heads * self.head_dim)
else:
xqm = xq.reshape(bsz, seqlen, self.n_kv_heads, self.n_rep, self.head_dim).permute(0, 2, 3, 1, 4)
xkm, xvm = xk.permute(0, 2, 1, 3).unsqueeze(2), xv.permute(0, 2, 1, 3).unsqueeze(2)
scores = (xqm @ xkm.transpose(-2, -1)).float() * self.sm_scale + mask
sink = sinks.reshape(1, self.n_kv_heads, self.n_rep, 1, 1).float()
m = scores.max(-1, keepdim=True).maximum(sink)
e = (scores - m).exp()
w = (e / (e.sum(-1, keepdim=True) + (sink - m).exp())).cast(dtypes.bfloat16)
attn = (w @ xvm).permute(0, 3, 1, 2, 4).reshape(bsz, seqlen, self.n_heads * self.head_dim)
xq = xq.cast(dtypes.bfloat16).reshape(bsz, seqlen, self.n_kv_heads, self.n_rep, self.head_dim).permute(0, 2, 3, 1, 4)
xk = xk.cast(dtypes.bfloat16).permute(0, 2, 1, 3).unsqueeze(2)
xv = xv.cast(dtypes.bfloat16).permute(0, 2, 1, 3).unsqueeze(2)
scores = (xq @ xk.transpose(-2, -1)).float() * self.sm_scale + mask
sink = sinks.reshape(1, self.n_kv_heads, self.n_rep, 1, 1).float()
m = scores.max(-1, keepdim=True).maximum(sink)
e = (scores - m).exp()
w = (e / (e.sum(-1, keepdim=True) + (sink - m).exp())).cast(dtypes.bfloat16)
attn = (w @ xv).permute(0, 3, 1, 2, 4).reshape(bsz, seqlen, self.n_heads * self.head_dim)
out = matmul_mx(attn, wo, wo_scale) + wo_bias
return out, [x_normed, rrms, attn]
@@ -188,8 +160,8 @@ class GPTOSS:
return out, [x_normed, rrms]
@function(precompile=True, precompile_backward=True)
def run_layer(self, x:Tensor, freqs_cis:Tensor, mask:Tensor, sliding:bool, attn_kwargs:dict, ffn_kwargs:dict, save:bool=True):
attn, attn_saves = self.attention(x, freqs_cis, mask, sliding, **attn_kwargs)
def run_layer(self, x:Tensor, freqs_cis:Tensor, mask:Tensor, attn_kwargs:dict, ffn_kwargs:dict, save:bool=True):
attn, attn_saves = self.attention(x, freqs_cis, mask, **attn_kwargs)
h = x + attn
ffn, ffn_saves = self.feed_forward(h, **ffn_kwargs)
h = h + ffn
@@ -206,7 +178,8 @@ class GPTOSS:
h = self.tok_embeddings(tokens)
bsz, seqlen = tokens.shape
freqs_cis = self.freqs_cis.cast(h.dtype)[:, :seqlen, :, :, :]
mask_full = None if getenv("HK_FLASH_ATTENTION") else self._attn_mask(seqlen, dtypes.float32)
mask_full = self._attn_mask(seqlen, False, dtypes.float32)
mask_sliding = self._attn_mask(seqlen, True, dtypes.float32)
for i in range(self.n_layers):
attn_kwargs = dict(attention_norm=self.attention_norm[i], wqkv=self.wqkv[i], wqkv_scale=self.wqkv_scale[i],
wqkv_bias=self.wqkv_bias[i], wo=self.wo[i], wo_scale=self.wo_scale[i], wo_bias=self.wo_bias[i],
@@ -214,7 +187,8 @@ class GPTOSS:
ffn_kwargs = dict(ffn_norm=self.ffn_norm[i], gate=self.gate[i], gate_bias=self.gate_bias[i],
w_gate_up=self.w_gate_up[i], w_gate_up_scale=self.w_gate_up_scale[i], w_gate_up_bias=self.w_gate_up_bias[i],
w_down=self.w_down[i], w_down_scale=self.w_down_scale[i], w_down_bias=self.w_down_bias[i])
h, *_ = self.run_layer(h, freqs_cis, mask_full, i % 2 == 0, attn_kwargs, ffn_kwargs, save=save)
mask = mask_sliding if i % 2 == 0 else mask_full
h, *_ = self.run_layer(h, freqs_cis, mask, attn_kwargs, ffn_kwargs, save=save)
logits = self.norm(h) @ self.output.T
return logits
+21 -15
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import cast, Callable, TypeVar, Generic, Any
import struct, functools, time, collections, itertools
from dataclasses import replace, dataclass
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE
from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, ContextVar
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, track_rewrites, GroupOp
@@ -410,14 +410,13 @@ def push_stack(op, s): return UOp(Ops.STACK, op.dtype.scalar(),
tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
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
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)): b.ensure_allocated()._buf.cpu_view().view(fmt='B')[:len(blob.arg)] = blob.arg
return UOp(Ops.NOOP)
def fold_const_store(buf: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.arg))
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(byte_off:=off.arg*buf.dtype.itemsize):byte_off+len(data)] = data
b.ensure_allocated()._buf.cpu_view().view(offset=off.arg * buf.dtype.itemsize, size=len(data), fmt='B')[:] = data
return UOp(Ops.NOOP)
def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
@@ -493,19 +492,19 @@ class HCQ2Compiled(Compiled):
@functools.cache
def timeline_signal(self, queue:str|None=None, 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
buf._buf.cpu_view().mv.cast('Q')[0] = init_value
return buf
@functools.cache
def timeline_value(self, queue:str|None=None, init_value:int=1) -> Buffer:
buf = Buffer("CPU", 1, dtypes.uint64, preallocate=True)
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
buf.as_memoryview(force_zero_copy=True).cast('Q')[0] = init_value
return buf
def synchronize(self, timeout:int|None=None):
if not hasattr(self, 'iface'): return
sig = self.timeline_signal().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
tl = self.timeline_value().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
sig = self.timeline_signal()._buf.cpu_view().mv.cast('Q')
tl = self.timeline_value().as_memoryview(force_zero_copy=True).cast('Q')
st = time.perf_counter()
while sig[0] < tl[0] - 1:
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
@@ -533,18 +532,25 @@ class HCQ2Compiled(Compiled):
# if the device has an interface, call device_fini to clean up resources
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
@dataclass
class HCQ2Buffer:
va_addr:sint
meta:Any=None
view:MMIOInterface|None=None
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQ2Buffer|None=None, view:MMIOInterface|None=None, owner:HCQ2Compiled|None=None):
self.va_addr, self.size, self.meta, self._base, self.view, self.owner = va_addr, size, meta, _base, view, owner
def offset(self, offset:int, size:int) -> HCQ2Buffer:
return HCQ2Buffer(self.va_addr+offset, meta=self.meta, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
def offset(self, offset:int=0, size:int|None=None) -> HCQ2Buffer:
return HCQ2Buffer(self.va_addr+offset, size or (self.size - offset), owner=self.owner, meta=self.meta,
_base=self._base or self, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
def cpu_view(self) -> MMIOInterface:
assert self.view is not None, "buffer has no cpu_view"
return self.view
@property
def base(self) -> HCQ2Buffer: return self._base or self
class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
def _as_buffer(self, buf:HCQ2Buffer) -> memoryview:
return unwrap(buf.view).mv
self.dev.synchronize()
return buf.cpu_view().mv
def _map(self, buf:HCQ2Buffer) -> HCQ2Buffer:
if not hasattr(self, '_do_map'): raise NotImplementedError("map failed: no method implemented")
+1 -2
View File
@@ -121,8 +121,7 @@ class TestTinygradKernelRoundtrip(unittest.TestCase):
arch = self.arch
from tinygrad.runtime.support.elf import elf_loader
from tinygrad.runtime.support.compiler_amd import HIPCompiler
from tinygrad.runtime.support.compiler_llvm import AMDLLVMCompiler
from tinygrad.runtime.support.compiler_amd import HIPCompiler, AMDLLVMCompiler
from tinygrad.helpers import DEV
kernels, _, _ = get_kernels_from_tinygrad(op_fn)
-39
View File
@@ -1,39 +0,0 @@
import unittest, ctypes
from tinygrad import Tensor, UOp
from tinygrad.device import Device
from tinygrad.dtype import dtypes
from tinygrad.renderer.cstyle import CStyleLanguage
from tinygrad.uop.ops import KernelInfo
def call_out_kernel(F:UOp, C:UOp) -> UOp:
call = F[0].load().call(UOp.const(dtypes.int, 3), C[0], ret_dtype=dtypes.void)
return C.after(call)[1].store(C.after(call)[0].load() + 1).sink(arg=KernelInfo(name="call_out"))
def call_ret_kernel(F:UOp, C:UOp) -> UOp:
val = F[0].load().call(UOp.const(dtypes.int, 21), ret_dtype=dtypes.int)
return C[0].store(val * 2).sink(arg=KernelInfo(name="call_ret"))
@unittest.skipUnless(isinstance(Device["CPU"].renderer, CStyleLanguage), "TODO: CALL is rendered in C style only")
class TestCall(unittest.TestCase):
def test_call_out_param(self):
called = []
@ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
def fxn(n, out):
called.append(n)
out[0] = n * 2
f = Tensor([ctypes.cast(fxn, ctypes.c_void_p).value], dtype=dtypes.uint64, device="CPU")
c = Tensor.empty(2, dtype=dtypes.int, device="CPU")
c = Tensor.custom_kernel(f, c, fxn=call_out_kernel)[1]
self.assertEqual(c.tolist(), [6, 7])
self.assertEqual(called, [3])
def test_call_ret(self):
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
def fxn(n): return n + 1
f = Tensor([ctypes.cast(fxn, ctypes.c_void_p).value], dtype=dtypes.uint64, device="CPU")
c = Tensor.empty(1, dtype=dtypes.int, device="CPU")
c = Tensor.custom_kernel(f, c, fxn=call_ret_kernel)[1]
c.realize()
self.assertEqual(c.item(), 44)
if __name__ == "__main__": unittest.main()
+14 -6
View File
@@ -199,7 +199,8 @@ class TestCustomKernel(unittest.TestCase):
c = Tensor.empty(N, N)
tst = Tensor.custom_kernel(c, a, b, fxn=custom_gemm)[0]
self.assertTrue(tst.allclose(a@b, atol=1e-3).item())
err = (tst - (a@b)).square().max()
self.assertLess(err.item(), 1e-6)
def test_gemm_multi(self):
devs = ("CPU:0", "CPU:1")
@@ -208,7 +209,8 @@ class TestCustomKernel(unittest.TestCase):
b = Tensor.randn(N, N).to(devs)
c = Tensor(Tensor.empty(N//2, N, device=devs).uop.multi(0), device=devs)
tst = Tensor.custom_kernel(c, a, b, fxn=custom_gemm)[0]
self.assertTrue(tst.allclose(a@b, atol=1e-3).item())
err = (tst - (a@b)).square().max()
self.assertLess(err.item(), 1e-6)
def test_gemm_backward_custom(self): self.test_gemm_backward(True)
# NOTE: grad_fxn doesn't work with pyrender
@@ -231,9 +233,14 @@ class TestCustomKernel(unittest.TestCase):
real_grad_a, real_grad_b = a.grad, b.grad
Tensor.realize(ref, real_grad_a, real_grad_b)
self.assertTrue(tst.allclose(ref, atol=1e-3).item())
self.assertTrue(grad_a.allclose(real_grad_a, atol=1e-3).item())
self.assertTrue(grad_b.allclose(real_grad_b, atol=1e-3).item())
err = (tst - ref).square().max()
self.assertLess(err.item(), 1e-6)
err = (grad_a - real_grad_a).square().max()
self.assertLess(err.item(), 1e-6)
err = (grad_b - real_grad_b).square().max()
self.assertLess(err.item(), 1e-6)
def test_simple_qkv(self):
N, d = 8, 4
@@ -246,7 +253,8 @@ class TestCustomKernel(unittest.TestCase):
O_ref = ((Q @ K.T) / (d ** 0.5)) @ V
Tensor.realize(O_custom, O_ref)
self.assertTrue(O_custom.allclose(O_ref, atol=1e-3).item())
err = (O_custom - O_ref).square().max()
self.assertLess(err.item(), 1e-6)
def test_gemm_qkv(self):
B, N, K_DIM, H_KV, REP, D = 2, 7, 6, 2, 2, 6
-3
View File
@@ -59,7 +59,6 @@ class TestLinearizer(unittest.TestCase):
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 0)
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, PTXRenderer), "broken on ptx")
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
def test_late_bias_load(self):
img = Tensor.empty(1, 3, 16, 16)
w = Tensor.empty(16, 3, 3, 3)
@@ -239,7 +238,6 @@ class TestLinearizer(unittest.TestCase):
helper_arg_acc_dtype(d.conv2d(w, dtype=acc_dtype), expected_dtype)
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_float4, "test requires float4")
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
def test_simple_unroll_no_between_phi_dependencies(self):
x, y = Tensor.empty(64, 64), Tensor.empty(64, 64)
r = (x@y).relu()
@@ -301,7 +299,6 @@ class TestLinearizer(unittest.TestCase):
program = to_program(replace_opts(linear.src[-1].src[0], []), renderer=Device[Device.DEFAULT].renderer)
assert not any(u.op == Ops.WHERE for u in tuple(program.src[1].src)), "found where where where should be folded"
@unittest.skipUnless(Device[Device.DEFAULT].renderer.supports_ranges, "test inspects ranges, which are rewritten to loops on this renderer")
def test_phi_simplification(self):
def helper(t, max_ops=0):
ast = helper_linearizer_opt(t)
+1 -1
View File
@@ -450,7 +450,7 @@ class TestMultiTransformer(unittest.TestCase):
else: v.shard_(device, axis=None)
last_tok = 0
for i in range(5):
for i in range(10):
real_tok = real_model(Tensor([[last_tok]], device=Device.DEFAULT), i).item()
shard_tok = shard_model(Tensor([[last_tok]], device=device), i).item()
+40 -30
View File
@@ -243,6 +243,7 @@ class TestOps(unittest.TestCase):
self.helper_test_exception([(8,)], lambda x: x.unfold(0, 9, 3), expected=RuntimeError)
self.helper_test_exception([(8,)], lambda x: x.unfold(1, 8, 3), expected=IndexError)
self.helper_test_exception([(8,)], lambda x: x.unfold(0, 9, 3), expected=RuntimeError)
self.helper_test_exception([(8,)], lambda x: x.unfold(0, 1, -1), expected=RuntimeError)
def test_meshgrid(self):
@@ -283,8 +284,6 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: torch.arange(-128, 128, dtype=torch.int8), lambda: Tensor.arange(-128, 128, dtype=dtypes.int8), forward_only=True)
helper_test_op([], lambda: torch.arange(127, -129, -1, dtype=torch.int8),
lambda: Tensor.arange(127, -129, -1, dtype=dtypes.int8), forward_only=True)
# an int range too large for default_int picks int64
self.assertEqual(Tensor.arange(2**31, 2**31+3).dtype, dtypes.int64)
# overflow: tinygrad raises (torch silently wraps)
with self.assertRaises(OverflowError): Tensor.arange(2**33, dtype=dtypes.int)
with self.assertRaises(OverflowError): Tensor.arange(129, dtype=dtypes.int8) # last=128 overflows
@@ -792,6 +791,8 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: tor^0x1337, lambda: ten^0x1337, forward_only=True)
helper_test_op([], lambda: 0x1337^tor, lambda: 0x1337^ten, forward_only=True)
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_xor(y), expected=RuntimeError)
def test_and(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -807,6 +808,8 @@ class TestOps(unittest.TestCase):
helper_test_op(None, lambda x: (1 < x) & (x < 2), forward_only=True, vals=[[1.2, 1.2, 1.2, 3.2]])
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_and(y), expected=RuntimeError)
def test_or(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -820,6 +823,8 @@ class TestOps(unittest.TestCase):
ten0, ten1 = Tensor(data[0], dtype=dtypes.bool), Tensor(data[1], dtype=dtypes.bool)
helper_test_op([], lambda: tor0|tor1, lambda: ten0|ten1, forward_only=True)
self.helper_test_exception([(4), (4)], lambda x,y: x.bitwise_or(y), expected=RuntimeError)
def test_bitwise_not(self):
data = [[1,-8,1],[32,1,6]]
tor = torch.tensor(data, dtype=torch.int)
@@ -833,6 +838,8 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: tor.bitwise_not(), lambda: ten.bitwise_not(), forward_only=True)
helper_test_op([], lambda: ~tor, lambda: ~ten, forward_only=True)
self.helper_test_exception([(4)], lambda x: x.bitwise_not(), expected=RuntimeError)
def test_lshift(self):
data = [[0,1,2],[1<<8,1<<16,1<<31-1]]
tor = torch.tensor(data, dtype=torch.int)
@@ -845,9 +852,6 @@ class TestOps(unittest.TestCase):
lambda: (ten << Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
helper_test_op([], lambda: tor.__lshift__(2), lambda: ten.__lshift__(2).cast(dtypes.int32), forward_only=True)
helper_test_op([], lambda: tor.bitwise_left_shift(2), lambda: ten.lshift(2).cast(dtypes.int32), forward_only=True)
self.helper_test_exception([], lambda: torch.tensor([1.0]) << 2, lambda: Tensor([1.0]) << 2, expected=RuntimeError)
self.helper_test_exception([], lambda: tor << torch.tensor([1.0]), lambda: ten << Tensor([1.0]), expected=RuntimeError)
self.helper_test_exception([], lambda: tor << 1.0, lambda: ten << 1.0, expected=RuntimeError)
def test_rshift(self):
data = [[0,1,2],[1<<8,1<<16,1<<31-1]]
@@ -861,8 +865,6 @@ class TestOps(unittest.TestCase):
lambda: (ten >> Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
helper_test_op([], lambda: tor.__rshift__(2), lambda: ten.__rshift__(2).cast(dtypes.int32), forward_only=True)
helper_test_op([], lambda: tor.bitwise_right_shift(2), lambda: ten.rshift(2).cast(dtypes.int32), forward_only=True)
self.helper_test_exception([], lambda: torch.tensor([4.0]) >> 1, lambda: Tensor([4.0]) >> 1, expected=RuntimeError)
self.helper_test_exception([], lambda: tor >> torch.tensor([1.0]), lambda: ten >> Tensor([1.0]), expected=RuntimeError)
def test_lshift_signed(self):
data = [[-1, -3, 1, 7], [0, -2147483648, 2147483647, -1]]
@@ -1046,8 +1048,8 @@ class TestOps(unittest.TestCase):
helper_test_op([(45,65)], torch.nn.functional.hardsigmoid, Tensor.hardsigmoid)
helper_test_op([()], torch.nn.functional.hardsigmoid, Tensor.hardsigmoid)
def test_hardsigmoid_extreme(self):
helper_test_op([(45,65)], torch.nn.functional.hardsigmoid, Tensor.hardsigmoid, low=300, high=400)
helper_test_op([(45,65)], torch.nn.functional.hardsigmoid, Tensor.hardsigmoid, low=-400, high=-300)
helper_test_op([(45,65)], torch.sigmoid, Tensor.sigmoid, low=300, high=400)
helper_test_op([(45,65)], torch.sigmoid, Tensor.sigmoid, low=-400, high=-300)
def test_softplus(self):
helper_test_op([(45,65)], torch.nn.functional.softplus, Tensor.softplus, grad_atol=1e-6)
helper_test_op([(45,65)], lambda t: torch.nn.functional.softplus(t, beta=3), lambda t: Tensor.softplus(t, beta=3), grad_atol=1e-6)
@@ -1263,20 +1265,23 @@ class TestOps(unittest.TestCase):
lambda x: x.sort(descending=True)[1], forward_only=True, vals=[[0, 1] * 9])
def test_argsort(self):
helper_test_op([(8,8,6)], lambda x: torch.argsort(x, dim=1, descending=True, stable=True).type(torch.int32),
lambda x: x.argsort(1, True), forward_only=True)
for dim in [-1, 0, 1]:
for descending in [True, False]:
helper_test_op([(8,8,6)], lambda x: torch.argsort(x, dim=dim, descending=descending, stable=True).type(torch.int32),
lambda x: x.argsort(dim, descending), forward_only=True)
def test_topk(self):
helper_test_op([(8)], lambda x: x.topk(3).values, lambda x: x.topk(3)[0], forward_only=True)
helper_test_op([(8)], lambda x: x.topk(3).indices.type(torch.int32), lambda x: x.topk(3)[1], forward_only=True)
for dim, largest in [(0, True), (1, False)]:
for sorted_ in [True]: # TODO support False
helper_test_op([(5,5,4)],
lambda x: x.topk(4, dim, largest, sorted_).values,
lambda x: x.topk(4, dim, largest, sorted_)[0], forward_only=True)
helper_test_op([(5,5,4)],
lambda x: x.topk(4, dim, largest, sorted_).indices.type(torch.int32),
lambda x: x.topk(4, dim, largest, sorted_)[1], forward_only=True)
for dim in [0, 1, -1]:
for largest in [True, False]:
for sorted_ in [True]: # TODO support False
helper_test_op([(5,5,4)],
lambda x: x.topk(4, dim, largest, sorted_).values,
lambda x: x.topk(4, dim, largest, sorted_)[0], forward_only=True)
helper_test_op([(5,5,4)],
lambda x: x.topk(4, dim, largest, sorted_).indices.type(torch.int32),
lambda x: x.topk(4, dim, largest, sorted_)[1], forward_only=True)
# repeated values
if not COMPILE_ONLY:
value, indices = Tensor([1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0]).topk(3)
@@ -1899,6 +1904,9 @@ class TestOps(unittest.TestCase):
helper_test_op([(3,3,3)], lambda x: x[-2:2])
helper_test_op([(3,3,3)], lambda x: x[-2:-5])
def test_slice_empty(self):
helper_test_op([(10,10)], lambda x: x[1:1])
def test_slice_zero_in_shape(self):
helper_test_op([(10,10)], lambda x: x[1:1]) # x.shape = (0, 10)
helper_test_op([(3,3,3)], lambda x: x[-2:-5]) # x.shape = (0, 3, 3)
@@ -2091,6 +2099,7 @@ class TestOps(unittest.TestCase):
helper_test_op([(4,3,1,6)], lambda x: x.squeeze(1))
helper_test_op([(4,3,6,6)], lambda x: x.squeeze(3))
self.helper_test_exception([(4,3,6,6)], lambda x: x.squeeze(50), expected=IndexError)
self.helper_test_exception([(4,3,6,6)], lambda x: x.squeeze(50), expected=IndexError)
helper_test_op([(4,3,6,1)], lambda x: x.squeeze(-1))
helper_test_op([(4,3,6,6)], lambda x: x.squeeze())
helper_test_op([(1,3,6,6)], lambda x: x.squeeze())
@@ -2363,10 +2372,9 @@ class TestOps(unittest.TestCase):
lambda x,w: Tensor.conv2d(x,w,groups=groups), grad_rtol=1e-5)
def test_conv2d(self): self._test_conv2d(bs=1, cin=3)
@slow_test
@unittest.skip("redundant: bs/cout are loop dims, kernel×cin sweep covered by test_conv2d")
def test_conv2d_bs_4_cin_3(self): self._test_conv2d(bs=4, cin=3, cout=2)
def test_conv2d_bs_1_cin_1(self): self._test_conv2d(bs=1, cin=1)
@unittest.skip("redundant: cin=1 covered by test_conv2d_bs_1_cin_1")
@slow_test
def test_conv2d_bs_4_cin_1(self): self._test_conv2d(bs=4, cin=1)
def test_conv2d_errors(self):
@@ -2486,6 +2494,9 @@ class TestOps(unittest.TestCase):
helper_test_op([(1,1,n,n), (1,1,k,k)],
lambda x,w: torch.nn.functional.conv2d(torch.nn.functional.pad(x, p),w),
lambda x,w: Tensor.conv2d(x,w,padding=p))
helper_test_op([(1,1,n,n), (1,1,k,k)],
lambda x,w: torch.nn.functional.conv2d(torch.nn.functional.pad(x, p),w),
lambda x,w: Tensor.conv2d(x,w,padding=p))
def test_padded_conv2d_p21(self):
bs,cin,H,W,padding = 4, 3, 3, 3, (2,1)
@@ -2534,7 +2545,7 @@ class TestOps(unittest.TestCase):
@slow_test
def test_max_pool2d(self):
for ksz in [2, (3,3), (3,2), (5,5), (5,1)]:
for ksz in [(2,2), (3,3), 2, 3, (3,2), (5,5), (5,1)]:
with self.subTest(kernel_size=ksz):
helper_test_op([(32,2,11,28)],
lambda x: torch.nn.functional.max_pool2d(x, kernel_size=ksz),
@@ -2542,7 +2553,7 @@ class TestOps(unittest.TestCase):
@slow_test
def test_max_pool2d_padding(self):
for ksz in [(3,3), 2, (3,2)]:
for ksz in [(2,2), (3,3), 2, 3, (3,2)]:
for p in [1, (1,0), (0,1)]:
with self.subTest(kernel_size=ksz, padding=p):
helper_test_op([(4,2,11,28)],
@@ -2605,7 +2616,7 @@ class TestOps(unittest.TestCase):
def test_max_pool2d_ceil_mode(self):
shape = (1,1,6,6)
for ksz in [(3,3), (3,2), 4]:
for ksz in [(3,3), 3, (3,2), 4]:
with self.subTest(kernel_size=ksz):
helper_test_op([shape],
lambda x: torch.nn.functional.max_pool2d(x, kernel_size=ksz, padding=1, stride=3, ceil_mode=True),
@@ -2685,7 +2696,7 @@ class TestOps(unittest.TestCase):
@slow_test
def test_avg_pool2d(self):
shape = (32,2,11,28)
for ksz in [2, (3,3), (3,2), (5,5), (5,1)]:
for ksz in [(2,2), (3,3), (3,2), (5,5), (5,1)]:
with self.subTest(kernel_size=ksz):
helper_test_op([shape],
lambda x: torch.nn.functional.avg_pool2d(x, kernel_size=ksz),
@@ -2699,7 +2710,7 @@ class TestOps(unittest.TestCase):
@slow_test
def test_avg_pool2d_padding(self):
shape = (32,2,11,28)
for ksz in [2, (3,3), (3,2)]:
for ksz in [(2,2), (3,3), 2, 3, (3,2)]:
for p in [1, (1,0), (0,1)]:
with self.subTest(kernel_size=ksz, padding=p):
helper_test_op([shape],
@@ -2721,7 +2732,7 @@ class TestOps(unittest.TestCase):
@slow_test
def test_avg_pool2d_padding_not_counted(self):
shape = (32,2,11,28)
for ksz in [(3,3), 2, (3,2)]:
for ksz in [(2,2), (3,3), 2, 3, (3,2)]:
with self.subTest(kernel_size=ksz):
helper_test_op([shape],
lambda x: torch.nn.functional.avg_pool2d(x, kernel_size=ksz, padding=1, count_include_pad=False),
@@ -2729,7 +2740,7 @@ class TestOps(unittest.TestCase):
def test_avg_pool2d_ceil_mode(self):
shape = (1,1,6,6)
for ksz in [(3,3), (3,2), 4]:
for ksz in [(3,3), 3, (3,2), 4]:
with self.subTest(kernel_size=ksz):
helper_test_op([shape],
lambda x: torch.nn.functional.avg_pool2d(x, kernel_size=ksz, padding=1, stride=3, ceil_mode=True),
@@ -2737,7 +2748,7 @@ class TestOps(unittest.TestCase):
def test_avg_pool2d_ceil_mode_padding_not_counted(self):
shape = (1,1,6,6)
for ksz in [(3,3), (3,2), 4]:
for ksz in [(3,3), 3, (3,2), 4]:
with self.subTest(kernel_size=ksz):
helper_test_op([shape],
lambda x: torch.nn.functional.avg_pool2d(x, kernel_size=ksz, padding=1, stride=3, ceil_mode=True, count_include_pad=False),
@@ -3324,7 +3335,6 @@ class TestOps(unittest.TestCase):
@unittest.skipIf((DEV.interface.startswith("MOCK") or Device.DEFAULT == "PYTHON"), "very slow on MOCKGPU because reduce does not fold")
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "webgpu runtime issue")
@unittest.skipIf(Device.DEFAULT == "QCOM", "QCOM fails with: Resource deadlock avoided")
@unittest.skipIf(Device.DEFAULT == "CPU" and DEV.renderer == "LVP", "extremely slow with LVP")
def test_masked_select(self):
helper_test_op([(32, 10)], lambda x: x.masked_select(x>0.5), lambda x: x.masked_select(x>0.5), forward_only=True)
helper_test_op([(32, 10)], lambda x: x.masked_select(torch.tensor(True)), lambda x: x.masked_select(Tensor(True)), forward_only=True)
+5 -31
View File
@@ -1,10 +1,10 @@
import unittest, threading
import unittest
from tinygrad import Tensor, UOp
from tinygrad.device import Device, Buffer, BufferSpec
from tinygrad.device import Device
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.engine.realize import run_linear
from tinygrad.renderer.nir import NIRRenderer
from tinygrad.renderer.isa.x86 import X86Renderer
from tinygrad.uop.ops import Ops, KernelInfo
from tinygrad.uop.ops import KernelInfo
def wait_loop_kernel(C:UOp) -> UOp:
N = 10
@@ -43,13 +43,6 @@ def nested_loop_kernel(C:UOp) -> UOp:
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="nested_loop", opts_to_apply=()))
def wait_ext_kernel() -> UOp:
sig = UOp.param(0, dtypes.int, (1,), volatile=True)
l = UOp.loop(0)
v = sig.after(l)[0].load()
e = v.end(l, v < 1)
return e.sink(arg=KernelInfo(name="wait_ext"))
def two_loops_kernel(C:UOp) -> UOp:
# two sequential loops on the same counter: ++ until 10, then ++ until 25
l1, l2 = UOp.loop(0), UOp.loop(1)
@@ -82,7 +75,7 @@ def loop_in_loop_kernel(C:UOp) -> UOp:
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="loop_in_loop", opts_to_apply=()))
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, (NIRRenderer, X86Renderer)), "loops are not supported in LVP and X86")
class TestWaitLoop(unittest.TestCase):
def test_wait_loop(self):
c = Tensor.empty(1, dtype=dtypes.int)
@@ -108,23 +101,4 @@ class TestWaitLoop(unittest.TestCase):
c.realize()
self.assertEqual(c.item(), 12)
@unittest.skipUnless(Device.DEFAULT in ("CPU", "AMD", "NV"), "need proper uncached=True handling")
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
class TestVolatileLoops(unittest.TestCase):
def test_async_wait_ext(self):
sig_buf = Buffer(Device.DEFAULT, 1, dtypes.int, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
try: sig_view = sig_buf.as_memoryview(force_zero_copy=True).cast('i')
except (AssertionError, NotImplementedError): self.skipTest(f"{Device.DEFAULT} does not support host-visible buffers")
sig_view[0] = 0
def set_signal():
threading.Event().wait(0.3)
sig_view[0] = 1
sync = threading.Thread(target=set_signal, daemon=True)
sync.start()
run_linear(UOp(Ops.LINEAR, src=(wait_ext_kernel().call(UOp.from_buffer(sig_buf)),)), wait=True)
sync.join(timeout=3)
if __name__ == "__main__": unittest.main()
+1 -1
View File
@@ -3,7 +3,7 @@ from tinygrad import Device
from tinygrad.device import CompileError
if Device.DEFAULT == "AMD":
# NOTE: if you don't gate this, LVP fails on Mac
from tinygrad.runtime.support.compiler_llvm import AMDLLVMCompiler
from tinygrad.runtime.support.compiler_amd import AMDLLVMCompiler
@unittest.skipUnless(Device.DEFAULT == "AMD", "Runs only on AMD")
class TestAMDLLVM(unittest.TestCase):
+1 -1
View File
@@ -50,7 +50,7 @@ class TestWeakConstFolding(unittest.TestCase):
self.assertEqual((out.op, out.dtype, out.arg), (Ops.CONST, dtypes.weakfloat, 3.75))
def test_invalid_poison(self):
self.assertIs(UOp.invalid().alu(Ops.CDIV, UOp.const(dtypes.weakint, 0)).simplify().arg, Invalid)
self.assertIs(UOp.const(dtypes.weakint, Invalid).alu(Ops.CDIV, UOp.const(dtypes.weakint, 0)).simplify().arg, Invalid)
class TestBinaryOpsConstFolding(unittest.TestCase):
def test_add_literal_zero(self):
+6 -11
View File
@@ -69,13 +69,11 @@ class TestDevice(unittest.TestCase):
@unittest.skipIf(WIN, "skipping windows test") # TODO: subprocess causes memory violation?
def test_env_overwrite_default_compiler(self):
if Device.DEFAULT == "CPU":
from tinygrad.runtime.support.compiler_cpu import ClangCompiler
from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler, ClangCompiler
try: _, _ = CPULLVMCompiler(), ClangCompiler()
except Exception as e: self.skipTest(f"skipping compiler test: not all compilers: {e}")
imports = ("from tinygrad import Device; from tinygrad.runtime.support.compiler_cpu import ClangCompiler; "
"from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler")
imports = "from tinygrad import Device; from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler, ClangCompiler"
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, CPULLVMCompiler)"'],
shell=True, check=True, env={**os.environ, "DEV": "CPU:LLVM"})
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, ClangCompiler)"'],
@@ -83,13 +81,11 @@ class TestDevice(unittest.TestCase):
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, ClangCompiler)"'],
shell=True, check=True, env={**os.environ, "DEV": "CPU:CLANG"})
elif Device.DEFAULT == "AMD":
from tinygrad.runtime.support.compiler_amd import HIPCompiler
from tinygrad.runtime.support.compiler_llvm import AMDLLVMCompiler
from tinygrad.runtime.support.compiler_amd import HIPCompiler, AMDLLVMCompiler
try: _, _ = HIPCompiler(Device[Device.DEFAULT].arch), AMDLLVMCompiler(Device[Device.DEFAULT].arch)
except Exception as e: self.skipTest(f"skipping compiler test: not all compilers: {e}")
imports = ("from tinygrad import Device; from tinygrad.runtime.support.compiler_amd import HIPCompiler; "
"from tinygrad.runtime.support.compiler_amd import AMDLLVMCompiler")
imports = "from tinygrad import Device; from tinygrad.runtime.support.compiler_amd import HIPCompiler, AMDLLVMCompiler"
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, AMDLLVMCompiler)"'],
shell=True, check=True, env={**os.environ, "DEV": "AMD:LLVM"})
subprocess.run([f'python3 -c "{imports}; assert isinstance(Device[Device.DEFAULT].compiler, HIPCompiler)"'],
@@ -100,8 +96,7 @@ class TestDevice(unittest.TestCase):
@unittest.skipIf(WIN, "skipping windows test")
def test_env_online(self):
from tinygrad.runtime.support.compiler_cpu import ClangCompiler
from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler, ClangCompiler
try: _, _ = CPULLVMCompiler(), ClangCompiler()
except Exception as e: self.skipTest(f"skipping compiler test: not all compilers: {e}")
@@ -116,7 +111,7 @@ class TestDevice(unittest.TestCase):
@unittest.skipIf(Device.DEFAULT != "CPU", "only run on CPU")
def test_compiler_autodetect_fallback(self):
from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler
try: CPULLVMCompiler()
except Exception as e: self.skipTest(f"skipping: LLVM not available: {e}")
+3 -3
View File
@@ -409,7 +409,7 @@ class TestImageSimplification(unittest.TestCase):
alu1 = ((idx2*1536)+(ridx4*768)+ridx3+(idx1*24)+(ridx5*3)+-771)//768
valid = (((idx2+ridx4)<1)!=1)&(((idx1+ridx5)<1)!=1)
load = get_load_image_uop((128, 768, 4), valid, (alu0, alu1))
self.check(load, None, "((((idx1*24)+(r5*3))+r3)+-3)", "(((idx2*2)+r4)+-1)")
self.check(load, None, "((((idx1*24)+r3)+(r5*3))+-3)", "(((idx2*2)+r4)+-1)")
def test_simplify7(self):
# DEBUG=2 ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1397 ALLOWED_GATED_READ_IMAGE=94 FLOAT16=1 CL=1 IMAGE=2 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916 # noqa: E501
@@ -568,7 +568,7 @@ class TestRangeShrink(unittest.TestCase):
from tinygrad.dtype import Invalid
r = Range(0, 204)
x = (r < 4).where(UOp.const(dtypes.float, 1), Invalid)
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r < 4).where(x, Invalid)).sink())
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r < 4).where(x, 0)).sink())
self.assertEqual(len(ranges), 1)
self.assertEqual(ranges[0].src[0].arg, 4)
@@ -577,7 +577,7 @@ class TestRangeShrink(unittest.TestCase):
from tinygrad.dtype import Invalid
r = Range(0, 204)
x = (r < 4).where(UOp.const(dtypes.float, 1), Invalid)
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r >= 4).where(Invalid, x)).sink())
ranges = self.get_ranges(UOp.param(0, dtypes.float, (204,)).index(r).store((r < 4).where(0, x)).sink())
self.assertEqual(len(ranges), 1)
self.assertEqual(ranges[0].src[0].arg, 4)
+33 -98
View File
@@ -333,7 +333,7 @@ class TestSymbolic(unittest.TestCase):
def test_mod_mod_wrong_sign(self):
v1=Variable("v1", 0, 128)
v3=Variable("v3", 0, 7)
self.helper_test_variable((((((v1%2)*2)+((v3+-1)%5))+-2)%5), 0, 4, "((v3+v1%2*2+2)%5)")
self.helper_test_variable((((((v1%2)*2)+((v3+-1)%5))+-2)%5), 0, 4, "((v3+v1%2*2+-3)%5)")
def test_mod_mod_wrong_sign2(self):
v2=Variable("v2", 0, 8)
@@ -365,7 +365,7 @@ class TestSymbolic(unittest.TestCase):
def test_div_const_div_wrong_sign_divisor(self):
a = Variable("a", 0, 124)
self.helper_test_variable(((a+10)//-2+10)//-4, -2, 14, "((a//-2+-3)//-4+-2)")
self.helper_test_variable(((a+10)//-2+10)//-4, -2, 14, "(((a+10)//-2+10)//-4)")
def test_nested_div_negative_divisor(self):
# (x//c1)//c2 -> x//(c1*c2) only when c2>0
@@ -437,7 +437,7 @@ class TestSymbolic(unittest.TestCase):
def test_masked_shr_fold(self):
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32)
self.helper_test_variable((x & -4) >> 2, 0, 63, "(x>>2)")
self.helper_test_variable((x & -4) >> 2, 0, 63, "(x>>2)", test_z3=False)
def test_bool_or_not_tautology(self):
a = Variable("a", 0, 10)
@@ -450,15 +450,8 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(c & c.logical_not(), False, False, "False")
def test_mod_factor_negative(self):
self.helper_test_variable(usum([uconst(-29), Variable("a", 0, 10), Variable("b", 0, 10)*28]) % 28, 0, 27, "((a+27)%28)")
self.helper_test_variable(usum([uconst(-29), Variable("a", 0, 100), Variable("b", 0, 10)*28]) % 28, 0, 27, "((a+27)%28)")
def test_mod_const_reduction_negative_offset(self):
# (x+c)%d -> (x+c%d)%d holds for any sign of x+c and d
x = Variable("x", 0, 100)
self.helper_test_variable((x-50)%3, 0, 2, "((x+1)%3)")
self.helper_test_variable((x-50)%-3, -2, 0, "((x+-2)%-3)")
self.helper_test_variable((x+7)%-13, -12, 0, "((x+-6)%-13)")
self.helper_test_variable(usum([uconst(-29), Variable("a", 0, 10), Variable("b", 0, 10)*28]) % 28, 0, 27, "((a+b*28+-29)%28)")
self.helper_test_variable(usum([uconst(-29), Variable("a", 0, 100), Variable("b", 0, 10)*28]) % 28, 0, 27, "((a+b*28+-29)%28)")
def test_sum_combine_num(self):
self.helper_test_variable(usum([uconst(29), Variable("a", 0, 10), uconst(-23)]), 6, 16, "(a+6)")
@@ -586,9 +579,9 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(x%12//4*4 + x%4 + x//12*12, 0, 23, "x")
def test_div_neg_cancel(self):
self.helper_test_variable((-Variable("idx", 0, 100)+199)//-4 + 50, 0, 25, "((idx*-1+-1)//-4)")
self.helper_test_variable((-Variable("idx", 0, 100)+200)//-4 + 50, 0, 25, "(idx*-1//-4)")
self.helper_test_variable((-Variable("idx", 0, 100)+201)//-4 + 50, -1, 24, "((idx*-1+-3)//-4+-1)")
self.helper_test_variable((-Variable("idx", 0, 100)+199)//-4 + 50, 0, 25, "((idx*-1+199)//-4+50)")
self.helper_test_variable((-Variable("idx", 0, 100)+200)//-4 + 50, 0, 25, "((idx*-1+200)//-4+50)")
self.helper_test_variable((-Variable("idx", 0, 100)+201)//-4 + 50, -1, 24, "((idx*-1+201)//-4+50)")
self.helper_test_variable((-Variable("idx", 0, 100))//2, -50, 0, "(idx*-1//2)")
self.helper_test_variable(Variable("idx", 0, 100)//-2, -50, 0, "(idx//-2)")
@@ -665,20 +658,20 @@ class TestSymbolic(unittest.TestCase):
def test_div_neg_all_range(self):
gidx = Variable("gidx", 0, 124)
lidx = Variable("lidx", 0, 7)
self.helper_test_variable((-gidx*8-lidx+999)//-4 + 250, 0, 250, "((lidx*-1+gidx*-8+-1)//-4)")
self.helper_test_variable((-gidx*8-lidx+1000)//-4 + 250, 0, 249, "((lidx*-1+gidx*-8)//-4)")
self.helper_test_variable((-gidx*8-lidx+1001)//-4 + 250, -1, 249, "((lidx*-1+gidx*-8+-3)//-4+-1)")
self.helper_test_variable((-gidx*8-lidx+1002)//-4 + 250, -1, 249, "((lidx*-1+gidx*-8+-2)//-4+-1)")
self.helper_test_variable((-gidx*8-lidx+999)//-4 + 250, 0, 250, "((gidx*-8+lidx*-1+999)//-4+250)")
self.helper_test_variable((-gidx*8-lidx+1000)//-4 + 250, 0, 249, "((gidx*-8+lidx*-1+1000)//-4+250)")
self.helper_test_variable((-gidx*8-lidx+1001)//-4 + 250, -1, 249, "((gidx*-8+lidx*-1+1001)//-4+250)")
self.helper_test_variable((-gidx*8-lidx+1002)//-4 + 250, -1, 249, "((gidx*-8+lidx*-1+1002)//-4+250)")
def test_div_neg_then_neg(self):
# taken from arange opts
lidx0 = Variable("lidx0", 0, 7)
lidx1 = Variable("lidx1", 0, 7)
alu2 = -lidx0-lidx1
self.helper_test_variable((((alu2+14)//(-32))+4), 3, 4, "((lidx0*-1+lidx1*-1+-18)//-32+3)")
self.helper_test_variable(-(((alu2+14)//(-32))+4), -4, -3, "((lidx0*-1+lidx1*-1+-18)//-32*-1+-3)")
self.helper_test_variable((((alu2+134)//(-32))+4), -1, 0, "((lidx0*-1+lidx1*-1+-26)//-32+-1)")
self.helper_test_variable((((alu2+142)//(-32))+4), -1, 0, "((lidx0*-1+lidx1*-1+-18)//-32+-1)")
self.helper_test_variable((((alu2+14)//(-32))+4), 3, 4, "((lidx0*-1+lidx1*-1+14)//-32+4)")
self.helper_test_variable(-(((alu2+14)//(-32))+4), -4, -3, "((lidx0*-1+lidx1*-1+14)//-32*-1+-4)")
self.helper_test_variable((((alu2+134)//(-32))+4), -1, 0, "((lidx0*-1+lidx1*-1+134)//-32+4)")
self.helper_test_variable((((alu2+142)//(-32))+4), -1, 0, "((lidx0*-1+lidx1*-1+142)//-32+4)")
self.helper_test_variable((((alu2+150)//(-32))+4), -1, -1, "-1")
self.helper_test_variable((((alu2+158)//(-32))+4), -1, -1, "-1")
@@ -844,12 +837,12 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(x%(-3) + ((x//(-3))%5)*(-3), -14, 0, "x%-15")
def test_div_mod_recombine_shifted_quotient(self):
# const reduction stores mod/div const-shifted: (x-50)%3 -> (x+1)%3, (x-50)//3 -> (x+1)//3 - 17.
# when vmin<0 blocks const reduction on the mod side, the quotient is stored const-shifted: (x-50)//3 -> (x+1)//3 - 17.
# recombine only needs a quotient of some b congruent to base mod div, so the shift folds into the result
x = Variable("x", 0, 100)
y = Variable("y", 0, 99)
self.helper_test_variable((x-50)%3 + ((x-50)//3)*3, -50, 50, "(x+-50)") # shifted literal quotient
self.helper_test_variable((x-50)%3 + (((x-50)//3)%5)*3, 0, 14, "((x+10)%15)") # shift inside the partial's mod
self.helper_test_variable((x-50)%3 + (((x-50)//3)%5)*3, 0, 14, "((x+-50)%15)") # shift inside the partial's mod
self.helper_test_variable(((y-50)//5)%4 + ((y-50)//20)*4, -10, 9, "(y//5+-10)") # merged and shifted
def test_div_mod_recombine_in_additive_sum(self):
@@ -879,16 +872,12 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable((idx<4).where(idx//4, idx.const_like(-1)), -1, 6, "(idx<4).where((idx//4), -1)")
def test_floordiv_lt(self):
# x//d<c <=> x<c*d for d>0, and <=> c*d<x for d<0
# x//d<c <=> x<c*d for d>0
idx = Variable("idx", 0, 24)
self.helper_test_variable((idx//4<3), 0, 1, "(idx<12)")
self.helper_test_variable(((idx-20)//4<-3), 0, 1, "(idx<8)")
self.helper_test_variable(((idx-10)//4<0), 0, 1, "(idx<10)")
self.helper_test_variable((idx//-4<-3), 0, 1, "(12<idx)")
self.helper_test_variable((idx//-4<-5), 0, 1, "(20<idx)")
self.helper_test_variable((idx//-4<-6), 0, 0, "False")
self.helper_test_variable(((idx-10)//-4<0), 0, 1, "(8<(idx+-2))")
self.helper_test_variable(((idx-20)//-4<2), 0, 1, "(12<idx)")
self.helper_test_variable((idx//-4<-3), 0, 1, "((idx//-4)<-3)")
def test_nested_div_mod_negative_inner_divisor(self):
# (x % (k*c)) // c -> (x // c) % k requires k>0; (x % (k*c)) % c -> x % c is unconditional for c>0
@@ -967,28 +956,6 @@ class TestSymbolic(unittest.TestCase):
# not combining # TODO: can combine if one is identity element const
self.helper_test_variable(aa+ab, 0, 6, "((x<2).where(a, b)+(x<2).where(a, 0))")
def test_where_combine_cross_zero(self):
cond = Variable("x", 0, 3) < 2
a = Variable("a", 0, 3)
b = Variable("b", 0, 3)
self.helper_test_variable(cond.where(a, a.ufix(0)) + cond.where(b.ufix(0), b), 0, 3, "(x<2).where(a, b)")
self.helper_test_variable(cond.where(a, a.ufix(0)) + cond.where(a.ufix(0), a), 0, 3, "a")
def test_where_or_dual(self):
m1 = Variable("x", 0, 3) < 2
m2 = Variable("y", 0, 3) < 2
a = Variable("a", 0, 3)
b = Variable("b", 0, 3)
self.helper_test_variable(m1.where(a, m2.where(a, b)), 0, 3, "((x<2)|(y<2)).where(a, b)")
def test_bool_ne_false(self):
cond = Variable("x", 0, 3) < 2
self.helper_test_variable(cond.ne(False), 0, 1, "(x<2)")
def test_bitcast_chain(self):
a = Variable("a", 0, 3)
self.assertIs(graph_rewrite(a.bitcast(dtypes.float32).bitcast(a.dtype), sym), a)
def test_negation_in_where(self):
cond = Variable("x", 0, 3) < 2
a = Variable("a", 0, 3)
@@ -1034,6 +1001,7 @@ class TestSymbolic(unittest.TestCase):
# (a if ((s<5)&(s<6)) else b) -> (a if (s<5) else b)
self.helper_test_variable(expr, 0, 3, "(s<5).where(a, b)")
@unittest.expectedFailure
def test_where_closure_folding(self):
# cond.where(t, f) where f contains cond.where(a, b) should fold the inner where to b in false branch
x = Variable("x", 0, 10)
@@ -1043,41 +1011,6 @@ class TestSymbolic(unittest.TestCase):
# the inner where should be folded: true branch gets -x, false branch gets x
self.helper_test_variable(outer, -20, 11, "(x<5).where((x*-2), (x+1))")
def test_where_closure_folding_deep(self):
x = Variable("x", 0, 10)
cond = x < 5
w1 = cond.where(-x, x)
w2 = cond.where(w1*2, w1+1)
self.helper_test_variable(cond.where(w2*3, w2+7), -60, 18, "(x<5).where((x*-6), (x+8))")
def test_where_closure_folding_different_cond(self):
# a nested where on a different condition is not folded
x = Variable("x", 0, 10)
a = Variable("a", 0, 3)
b = Variable("b", 0, 3)
expr = (x<5).where((x<7).where(a, b), (x<7).where(b, a))
self.helper_test_variable(expr, 0, 3, "(x<5).where((x<7).where(a, b), (x<7).where(b, a))")
def test_where_closure_folding_derived_cond(self):
# cond is a value inside the branch: (!cond).where(a, b) is b in the true branch
x = Variable("x", 0, 10)
a = Variable("a", 0, 3)
b = Variable("b", 0, 3)
c = Variable("c", 0, 3)
expr = (x<5).where((x<5).logical_not().where(a, b)*2, c)
self.helper_test_variable(expr, 0, 6, "(x<5).where((b*2), c)")
def test_where_closure_folding_valid(self):
# a valid gate on the same cond folds in the true branch, the live else value is kept
x = Variable("x", 0, 10)
a = Variable("a", 0, 3)
cond = x < 5
expr = cond.where(a.valid(cond), Variable("c", 0, 3))
self.assertIs(graph_rewrite(expr, sym), cond.where(a, Variable("c", 0, 3)))
# a same-cond valid gate in the false branch is Invalid there
expr = cond.where(Variable("t", 0, 3), a.valid(cond))
self.assertIs(graph_rewrite(expr, sym), cond.where(Variable("t", 0, 3), UOp.invalid()))
def test_symbolic_div(self):
# from symbolic arange
a = Variable("a", 1, 10)
@@ -1110,8 +1043,8 @@ class TestSymbolic(unittest.TestCase):
def test_nested_mod_negative_range(self):
# (x%(k*c))%c = x%c for positive c
x = Variable("x", 0, 1575)
self.helper_test_variable(((x + (-1064)) % 512) % 4, 0, 3, "(x%4)")
self.helper_test_variable(((x + (-1064)) % 512) % 128, 0, 127, "((x+88)%128)")
self.helper_test_variable(((x + (-1064)) % 512) % 4, 0, 3, "((x+-1064)%4)")
self.helper_test_variable(((x + (-1064)) % 512) % 128, 0, 127, "((x+-1064)%128)")
class TestSymbolicNumeric(unittest.TestCase):
def helper_test_numeric(self, f):
@@ -1321,21 +1254,23 @@ class TestSymbolicSymbolicOps(unittest.TestCase):
"""
class TestInvalidIndex(unittest.TestCase):
def test_invalid_lift_keeps_live_else(self):
ridx = Variable("ridx", 0, 10)
cond = ridx < 5
expr = cond.where(cond.where(ridx, UOp.invalid()), ridx+100)
self.assertIs(expr.simplify(), cond.where(ridx, ridx+100))
def test_invalid_times_0(self):
ridx = Variable("ridx", 0, 10)
idx = (ridx<5).where(ridx, UOp.invalid())*0
self.assertIs(idx.simplify(), (ridx<5).where(0, UOp.invalid()), "multiplying an index by 0 should preserve the invalid")
def test_invalid_comparison_drops_invalid(self):
# comparisons return a bool, and bools can't be invalid
ridx = Variable("ridx", 0, 10)
idx = (ridx<5).where(ridx, UOp.invalid())<3
self.assertIs(idx.simplify(), (ridx<3), "comparison of index should drop the invalid")
self.assertIs(idx.where(UOp.const(dtypes.int, 1), 0).simplify(), (ridx<3).where(UOp.const(dtypes.int, 1), 0),
"comparison of index should drop the invalid")
def test_alu_moves_inside_invalid(self):
ridx = Variable("ridx", 0, 10)
self.assertIs((10*(ridx<5).where(ridx, UOp.invalid())).simplify(), (ridx<5).where(ridx*10, UOp.invalid()),
"Invalid should poison either binary operand position")
idx = (ridx<5).where(ridx, UOp.invalid())*10
self.assertIs(idx.simplify(), (ridx<5).where(ridx*10, UOp.invalid()), "multiplying an index by 0 should preserve the invalid")
def test_merge_invalid_conditions(self):
ridx0 = Variable("ridx0", 0, 10)
+2 -21
View File
@@ -3,9 +3,9 @@ import unittest
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.helpers import Timing, Context, cdiv
from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
from tinygrad.dtype import dtypes, ConstFloat, Invalid # noqa: F401
from tinygrad.device import Device
from tinygrad.uop.ops import Ops, ParamArg, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite, pm_lower_index_dtype # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
from tinygrad.uop.ops import Ops, ParamArg, UOp, UPat, dtype_from_uop, exec_alu # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
from tinygrad.uop.symbolic import sym
from test.helpers import eval_uop, to_uops_list
@@ -45,25 +45,6 @@ class TestDTypeFromUOp(unittest.TestCase):
with self.assertRaises(RuntimeError): type_verify(UOp.const(weak, value).sink(), spec_program)
type_verify(UOp.const(concrete, value).sink(), spec_program)
class TestLowerIndexDtype(unittest.TestCase):
def test_gated_shrink_lowers_to_selected_width(self):
# coalesce builds gated SHRINKs for masked vectorized loads; lowering must resolve them at the
# width the offset bounds select (this one needs long)
buf = UOp.param(0, dtypes.float, (2**31+64,))
i = UOp.variable("i", 0, 2**28)
shrink = UOp(Ops.SHRINK, src=(buf, (i*24).valid(i < 2**28), UOp.const(dtypes.weakint, 4)))
lowered = graph_rewrite(shrink.sink(), pm_lower_index_dtype)
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
sh = next(u for u in lowered.backward_slice_with_self if u.op is Ops.SHRINK)
self.assertEqual(sh.src[1].dtype, dtypes.long)
def test_reg_buffer_size_lowers(self):
reg = UOp.placeholder((4,), dtypes.float, 0, addrspace=AddrSpace.REG)
self.assertEqual(reg.src[0].dtype, dtypes.weakint)
lowered = graph_rewrite(reg.sink(), pm_lower_index_dtype)
self.assertTrue(all(u.dtype != dtypes.weakint for u in lowered.backward_slice_with_self), "lowering must resolve all weakint")
self.assertEqual(next(u for u in lowered.backward_slice_with_self if u.op is Ops.BUFFER).src[0].dtype, dtypes.int)
class TestSafeCast(unittest.TestCase):
def test_cast_folds(self):
a = UOp.variable("a", 1, 10, dtype=dtypes.int32)
-8
View File
@@ -123,14 +123,6 @@ class TestUOpsStats(unittest.TestCase):
# NOTE; ops also include indexing ops
assert expected_ops <= ops and ops <= expected_ops * 2
def test_cat_equal_pieces(self):
# concatenating equal-size pieces lowers to STACK: pure data movement, no arithmetic
equal = [Tensor.empty(256, 128) for _ in range(4)]
self.assertEqual(get_stats(Tensor.cat(*equal, dim=1))[0], 0)
# a mismatched piece falls back to pad+usum, which sums N zero-padded copies and pays their adds
unequal = equal[:3] + [Tensor.empty(256, 129)]
self.assertGreater(get_stats(Tensor.cat(*unequal, dim=1))[0], 0)
def test_simple_matmul(self, M=1024, N=1024, K=1024):
a = Tensor.empty(M,N)
b = Tensor.empty(N,K)
-7
View File
@@ -90,13 +90,6 @@ class TestValidateOOB(unittest.TestCase):
to_uops_list([buf.index(r & 15).load(dtype=dtypes.int)]) # 0..15 valid
with self.assertRaises(RuntimeError):
to_uops_list([buf.index(r & 31).load(dtype=dtypes.int)]) # 0..31 oob
# align masks round down to a multiple of 2^k
to_uops_list([buf.index((r & -4).valid(r < 16)).load(dtype=dtypes.int)]) # 0..12 valid
with self.assertRaises(RuntimeError):
to_uops_list([buf.index(r & -2).load(dtype=dtypes.int)]) # 0..100 oob
# other masks can't be modeled as mod
with self.assertRaisesRegex(RuntimeError, "z3 int AND only supports"):
to_uops_list([buf.index(r & 21).load(dtype=dtypes.int)])
def test_max(self):
with Context(CHECK_OOB=1, SPEC=2):
+6 -16
View File
@@ -15,22 +15,12 @@ class TestWinograd(unittest.TestCase):
out = Tensor.conv2d(x,w)
self.assertEqual(len(out.schedule_linear().src), 4)
def test_backward_counters(self):
# contiguous_backward on the pooled input keeps the input-transform adjoint out of the overlap accumulation, so
# winograd backward runs in a fraction of the direct-conv flops; NOOPT=1 keeps the raw flop ratio from drifting with the optimizer
IC, OC, H = 64, 64, 28
x,w = Tensor.empty(1,IC,H,H,device="NULL").realize(), Tensor.empty(OC,IC,3,3,device="NULL").realize()
x.requires_grad = w.requires_grad = True
def backward_ops(wino):
x.grad = w.grad = None
GlobalCounters.reset()
with Context(NOOPT=1, WINO=wino):
Tensor.conv2d(x,w,padding=1).mean().backward()
Tensor.realize(x.grad, w.grad)
return GlobalCounters.global_ops
ops_wino, ops_normal = backward_ops(1), backward_ops(0)
print(f"backward ops: normal {ops_normal} wino {ops_wino} ratio {ops_wino/ops_normal:.2f}")
self.assertLess(ops_wino/ops_normal, 0.35)
def test_backward_kernels(self):
x,w = Tensor.empty(1,4,9,9).realize(), Tensor.empty(4,4,3,3).realize()
out = Tensor.conv2d(x,w, padding=1)
out.mean().backward()
backward_schedule = x.grad.schedule_linear(w.grad)
self.assertEqual(len(backward_schedule.src), 4)
def test_counters(self):
IC, OC, H = 64, 64, 28
+3 -3
View File
@@ -141,9 +141,9 @@ class TestTensorCores(unittest.TestCase):
if tc.dtype_in is dtypes.bfloat16: continue # <-- broken with numpy
# this will be a M=G16, N=G32, M=G16, M=G16, K=R16, K=R16, K=R16 with 9 choices of TC MNK axes
golden_result = None
a = Tensor.rand(16, 16, 29, 29, dtype=tc.dtype_in).realize()
b = Tensor.rand(32, 16, 16, 16, dtype=tc.dtype_in).realize()
for axis in range(9):
a = Tensor.rand(16, 16, 29, 29, dtype=tc.dtype_in).realize()
b = Tensor.rand(32, 16, 16, 16, dtype=tc.dtype_in).realize()
c = a.conv2d(b, padding=1, dtype=tc.dtype_out)
realized_ast, real_bufs = helper_realized_ast(c)
@@ -160,7 +160,7 @@ class TestTensorCores(unittest.TestCase):
result = np.frombuffer(real_bufs[0].as_memoryview(), _to_np_dtype(real_bufs[0].dtype))
# ensure the results for each choice of axis matches
if golden_result is None: golden_result = result.copy()
if golden_result is None: golden_result = np.frombuffer(real_bufs[0].as_memoryview(), _to_np_dtype(real_bufs[0].dtype))
np.testing.assert_allclose(result, golden_result, atol=0.1, rtol=0.2)
@Context(ALLOW_TF32=1)
+8
View File
@@ -46,6 +46,14 @@ class TestConv(unittest.TestCase):
out = x.conv2d(w, padding=(1,1))
np.testing.assert_allclose(out.relu().numpy(), np.maximum(out.numpy(), 0), atol=1e-6)
def test_two_binops_no_rerun(self):
x = Tensor.randn(1,12,16,32)
w = Tensor.randn(32,12,3,3)
out = x.conv2d(w, stride=(2,2), padding=(1,1))
r1, r2 = out.relu(), (out-1)
np.testing.assert_allclose(r1.numpy(), np.maximum(out.numpy(), 0), atol=1e-5)
np.testing.assert_allclose(r2.numpy(), out.numpy() - 1, atol=1e-5)
def test_two_overlapping_binops_no_rerun(self):
x = Tensor.randn(1,12,16,32)
w = Tensor.randn(32,12,3,3)
+1 -16
View File
@@ -1,9 +1,7 @@
import tempfile, unittest
from tinygrad import Tensor, dtypes
from tinygrad.helpers import Context
from tinygrad.uop.ops import UOp, Ops
from tinygrad.uop.spec import spec_shared, type_verify
from tinygrad.uop.ops import UOp
class TestWeakPromotion(unittest.TestCase):
@@ -55,19 +53,6 @@ class TestWeakPromotion(unittest.TestCase):
weak = Tensor([True, False]).where(Tensor(1), 2)
self.assertEqual(weak.dot(Tensor([1, 1], dtype=dtypes.int8)).dtype, dtypes.int8)
def test_weak_int_binop(self):
v = UOp.variable("i", 0, 10, dtypes.weakint)
self.assertEqual((v << 1).dtype, dtypes.weakint)
self.assertEqual((v & 3).dtype, dtypes.weakint)
with self.assertRaises(RuntimeError): Tensor.const(dtypes.weakfloat, 1.0) << Tensor.const(dtypes.weakfloat, 1.0)
with self.assertRaises(RuntimeError): UOp.const(dtypes.int32, 1).alu(Ops.SHL, UOp.const(dtypes.float64, 1))
# float bitwise/shift builds, the spec rejects it
with Context(SPEC=1):
f32, wf = UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.weakfloat, 1.0)
for bad in (f32.alu(Ops.AND, f32), f32.alu(Ops.SHL, UOp.const(dtypes.int32, 1)),
UOp(Ops.AND, dtypes.float32, (f32, f32)), UOp(Ops.AND, dtypes.int32, (wf, wf))):
with self.assertRaises(RuntimeError): type_verify([bad], spec_shared)
def test_integer_values(self):
x = Tensor.full((1,), 1, dtype=dtypes.int64, device="CPU")
self.assertEqual((x + 2**40).item(), 2**40 + 1)
+1 -1
View File
@@ -593,7 +593,7 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
if dtype not in Device[Device.DEFAULT].renderer.supported_dtypes(): return
t = Tensor.arange(64).reshape(8, 8).clone().realize()
t.shard_([f"{Device.DEFAULT}:{i}" for i in range(4)], axis=0)
for i in range(2):
for i in range(4):
print(f"{i=}")
a = t.shrink(((0+2*i,2+2*i),None))
b = Tensor(t.numpy()[0+2*i:2+2*i])
+5 -5
View File
@@ -137,8 +137,8 @@ class TestRandomness(unittest.TestCase):
@TinyJit
def sample_one(): return Tensor(w).multinomial(1, replacement=False).realize()
tiny_samples = [sample_one().item() for _ in range(200)]
torch_samples = [torch.tensor(w).multinomial(1, replacement=False).item() for _ in range(200)]
tiny_samples = [sample_one().item() for _ in range(400)]
torch_samples = [torch.tensor(w).multinomial(1, replacement=False).item() for _ in range(400)]
self.assertTrue(equal_distribution(lambda *_: Tensor(tiny_samples), lambda _: torch.tensor(torch_samples)))
w = list(range(32))
@@ -153,8 +153,8 @@ class TestRandomness(unittest.TestCase):
@TinyJit
def sample_three(): return Tensor(w).multinomial(3, replacement=False).realize()
tiny_draws = np.array([sample_three().numpy() for _ in range(200)])
torch_draws = np.array([torch.tensor(w).multinomial(3, replacement=False).numpy() for _ in range(200)])
tiny_draws = np.array([sample_three().numpy() for _ in range(400)])
torch_draws = np.array([torch.tensor(w).multinomial(3, replacement=False).numpy() for _ in range(400)])
for pos in range(3):
self.assertTrue(equal_distribution(lambda *_: Tensor(tiny_draws[:, pos]), lambda _: torch.tensor(torch_draws[:, pos])))
@@ -167,7 +167,7 @@ class TestRandomness(unittest.TestCase):
self.assertFalse(equal_distribution(lambda *_: tiny_res, lambda _: torch_res))
def test_conv2d_init(self):
params = (32, 64, (3,3))
params = (128, 256, (3,3))
assert equal_distribution(lambda *_: nn.Conv2d(*params).weight, lambda _: torch.nn.Conv2d(*params).weight.detach())
assert equal_distribution(lambda *_: nn.Conv2d(*params).bias, lambda _: torch.nn.Conv2d(*params).bias.detach())
+1 -1
View File
@@ -151,7 +151,7 @@ pm_early_transform_tensor_graph = PatternMatcher([
# add CONTIGUOUS to tagged UOps
(UPat(GroupOp.All-{Ops.CONTIGUOUS, Ops.AFTER, Ops.STORE}, name="x"),
lambda x: None if x.tag is None else x.rtag(None).contiguous(tag=x.tag) if x.tag else x.replace(tag=None)),
lambda x: x.rtag(None).contiguous(tag=x.tag) if x.tag else x.replace(tag=None)),
# remove extra CONTIGUOUS on AFTER (only when target is contiguous)
(UPat(Ops.CONTIGUOUS, src=(UPat(Ops.AFTER, name="a"),), name="c"),
lambda a,c: a.replace(tag=(a.tag or ())+(c.tag or ())) if a.src[0].has_buffer_identity() else None),
+11 -9
View File
@@ -22,7 +22,7 @@ from tinygrad.codegen.opt.postrange import apply_opts
from tinygrad.codegen.late.gater import pm_move_gates_from_index
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
from tinygrad.schedule.rangeify import pm_mops
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize, ranges_to_loops
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
from tinygrad.helpers import all_same, flatten, argsort, partition
@@ -38,6 +38,11 @@ pm_number_params = PatternMatcher([
(UPat(Ops.PARAM, name="x"), do_number_param),
])
pm_no_index = PatternMatcher([
(UPat(GroupOp.ALU.union({Ops.CONST}), dtype=dtypes.weakint, name="x"), lambda x: x.replace(dtype=dtypes.int)),
(UPat(Ops.CAST, dtype=dtypes.weakint, src=(UPat.var("x"),)), lambda x: x.cast(dtypes.int)),
])
def build_range_map(sink:UOp) -> dict[int, int]:
ctx: dict[int, int] = {}
for x in sink.toposort():
@@ -322,7 +327,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# lower index dtype
# NOTE: we need indexing_simplify to remove the cast to long using the Invalid
sink = graph_rewrite(sink, pm_lower_index_dtype+indexing_simplify, ctx={}, name="lower all index dtypes")
sink = graph_rewrite(sink, pm_lower_index_dtype+indexing_simplify, name="lower all index dtypes")
# final symbolic before decomp
sink = graph_rewrite(sink, symbolic, name="final symbolic")
@@ -346,15 +351,9 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# final rules for the renderer (without sym)
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else PatternMatcher([])
pm_final_rewrite = pm_decomp+extra_matcher+pm_split_ends
pm_final_rewrite = pm_decomp+extra_matcher+pm_split_ends+pm_no_index
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
if SPEC: type_verify(sink, spec_program)
# rewrite bounded ranges to loops for renderers without range support, after validation like instruction selection
if not ren.supports_ranges: sink = ranges_to_loops(sink)
# this was the linearizer
sink = graph_rewrite(sink, pm_add_control_flow, ctx=CFGContext(sink), name="add control flow", bottom_up=True)
@@ -362,6 +361,9 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
num_params = len([x for x in sink.toposort() if x.op is Ops.PARAM and x.arg.slot != -1])
sink = graph_rewrite(sink, pm_number_params, ctx=[num_params], name="number params with -1", walk=True)
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
if SPEC: type_verify(sink, spec_program)
# return the rewritten sink
return sink
+3 -5
View File
@@ -1,6 +1,6 @@
from typing import Callable
import functools
from tinygrad.dtype import dtypes
from tinygrad.dtype import dtypes, promo_lattice
from tinygrad.uop.ops import UOp, UPat, Ops, PatternMatcher
from tinygrad.renderer import Renderer
@@ -35,10 +35,8 @@ def fast_idiv(ren: Renderer, x: UOp, d: int, dont_cast=False) -> UOp|None:
if (ret:=fast_idiv(ren, x.alu(Ops.CDIV, x.const_like(largest_factor_of_two_in_d)),
d//largest_factor_of_two_in_d, dont_cast=True)) is not None: return ret
if dont_cast: return None
# the next integer width that holds x*m
widen = {dtypes.int8:dtypes.int16, dtypes.int16:dtypes.int32, dtypes.int32:dtypes.int64, dtypes.int64:dtypes.uint64,
dtypes.uint8:dtypes.uint16, dtypes.uint16:dtypes.uint32, dtypes.uint32:dtypes.uint64}
if (next_dtype := widen.get(x.dtype)) is not None and next_dtype in ren.supported_dtypes():
# promo_lattice needs to return an unsigned type if the type is unsigned
if dtypes.is_int(next_dtype := promo_lattice[x.dtype][-1]) and next_dtype in ren.supported_dtypes():
if m*vmin >= next_dtype.min and m*vmax <= next_dtype.max:
return ((x.cast(next_dtype)*m) >> s).cast(x.dtype) if is_unsigned else ((x.cast(next_dtype)*m) >> s).cast(x.dtype) + (x<0).where(x.ufix(1), 0)
return None
+1 -40
View File
@@ -1,7 +1,7 @@
import heapq
from typing import Any
from collections import defaultdict
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat, multirange_str, ParamArg, AxisType
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat, multirange_str
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.helpers import prod, getenv, TUPLE_ORDER
@@ -93,42 +93,3 @@ pm_split_ends = PatternMatcher([
# split the ends
(UPat(Ops.END, name="e"), do_split_ends),
])
def ranges_to_loops(sink:UOp) -> UOp:
# rewrite bounded ranges to bound-less loops with a register counter: i = 0; loop { body; i += 1; loop again while i < bound }
slot = max((u.arg.slot for u in sink.toposort() if u.op is Ops.BUFFER and u.addrspace == AddrSpace.REG), default=-1) + 1
ends = [u for u in sink.toposort() if u.op is Ops.END and any(x.op is Ops.RANGE and x.dtype is not dtypes.void for x in u.src[1:])]
# e.ranges over-approximates nesting (it flows ranges through ordering deps), so compute true nesting from the body slices
# NOTE: uop identity is not stable (the uop cache is weak), all lookups are by uop key
end_for_range = {r.key: e for e in ends for r in e.src[1:] if r.op is Ops.RANGE and r.dtype is not dtypes.void}
body_ends = {e.key: {u.key for u in e.src[0].toposort()} for e in ends}
repl: dict[UOp, UOp] = {}
range_to_loop: dict[bytes, UOp] = {}
for e in ends:
# the counter init is placed after the enclosing loops so it resets every outer iteration, the loop header depends on it so it runs first
enclosing = tuple(r for r in e.ranges if (er:=end_for_range.get(r.key)) is not None and e.key in body_ends[er.key])
e = e.substitute(repl)
assert len(e.src) == 2, f"expected a split END with one range, got {len(e.src)-1} ranges"
r = e.src[1]
i = UOp(Ops.BUFFER, src=(UOp.const(dtypes.int, 1),), arg=ParamArg(slot, r.dtype, addrspace=AddrSpace.REG))
slot += 1
z = UOp.const(dtypes.int, 0)
init = i.after(*enclosing).index(z).store(UOp.const(r.dtype, 0))
i = i.after(init)
# a do-while can't skip its first iteration, so a range with a possibly zero bound gets a one-time entry guard on the loop header
guard = () if r.src[0].vmin >= 1 else (UOp.const(r.dtype, 0) < r.src[0],)
l = range_to_loop[r.key] = UOp(Ops.RANGE, dtypes.void, src=(init,)+guard, arg=(r.arg[0], AxisType.LOOP))
iv = i.after(l).index(z).load()
inc = iv + UOp.const(r.dtype, 1)
body = e.src[0].substitute({r: iv})
# the counter store is part of the loop body, an AFTER body can't be in a GROUP so sequence it with a dep instead
ret = body.after(i.index(z).store(inc)) if body.op is Ops.AFTER else UOp.group(body, i.index(z).store(inc))
repl[e] = ret.end(l, inc < r.src[0])
# keep the tracked loop headers up to date: their init deps on enclosing ranges get rewritten by the same substitution
for k in range_to_loop: range_to_loop[k] = range_to_loop[k].substitute({r: iv})
if not len(repl): return sink
out = sink.substitute(repl)
# ordering deps on the old ranges (scope AFTERs outside the loop bodies) point at the loop headers
fix = {a: a.replace(src=(a.src[0],) + tuple(range_to_loop[s.key] if s.key in range_to_loop else s for s in a.src[1:]))
for a in out.toposort() if a.op is Ops.AFTER and any(s.key in range_to_loop for s in a.src[1:])}
return out.substitute(fix) if len(fix) else out
+3 -4
View File
@@ -51,10 +51,9 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
if IMAGE:
for buf_index,buf in enumerate(k.bufs):
if image_valid_dims(buf.src[0].dtype, buf.src[0].max_numel(), k.ren.target.arch):
idx = k.bufs[buf_index].src[1]
# IMAGE upcasts require one validity shared by all four unit-stride lanes so memory_coalescing can combine them into one vector read.
unit_stride_axes_mul_4 = [k.rngs.index(c) for c in idx.get_idx().split_uop(Ops.ADD) if
c.op is Ops.RANGE and (c.vmax+1)%4 == 0 and c not in idx.get_valid().backward_slice]
# part of is_expanded
unit_stride_axes_mul_4 = [k.rngs.index(c) for c in k.bufs[buf_index].src[1].get_idx().split_uop(Ops.ADD) if
c.op is Ops.RANGE and (c.vmax+1)%4 == 0]
if len(unit_stride_axes_mul_4):
if (axis:=unit_stride_axes_mul_4[0]) in k.upcastable_dims:
k.apply_opt(Opt(OptOps.UPCAST, axis, 4))
+2 -4
View File
@@ -191,11 +191,9 @@ class Buffer:
def __repr__(self):
return f"<buf real:{self.is_allocated()} device:{self.device} size:{self.size} dtype:{self.dtype}" + \
(f" offset:{self.offset}" if self._base is not None else "") + (f" {self.options=}" if self.options is not None else "") + ">"
def as_memoryview(self, allow_zero_copy=False, force_zero_copy=False, no_sync=False) -> memoryview:
def as_memoryview(self, allow_zero_copy=False, force_zero_copy=False) -> memoryview:
# zero copy with as_memoryview (disabled by default due to use after free)
if (force_zero_copy or allow_zero_copy) and hasattr(self.allocator, '_as_buffer'):
if not no_sync: self.allocator.dev.synchronize()
return self.allocator._as_buffer(self._buf)
if (force_zero_copy or allow_zero_copy) and hasattr(self.allocator, '_as_buffer'): return self.allocator._as_buffer(self._buf)
assert not force_zero_copy, "force zero copy was passed, but copy is required"
Buffer("PYTHON", self.size, self.dtype, opaque=(mv:=memoryview(bytearray(self.nbytes)))).copy_from(self)
return mv
+3 -5
View File
@@ -89,13 +89,11 @@ def optimize_local_size(call:UOp, prg:UOp) -> UOp|None:
if prg.arg.local_size is not None or not Device[device].renderer.has_local or not all_int(prg.arg.global_size): return None
if (local_size:=local_size_cache.get(prg.key)) is None:
# reuse one loaded runtime across candidates, only launch dims vary
bufs, runtime = [b.allocate() for b in bufs_from_ast(prg.src[0], device)], get_runtime(device, prg, cache=False)
bufs = [UOp.from_buffer(b.allocate()) for b in bufs_from_ast(prg.src[0], device)]
def try_exec(local_size):
try:
new_gs = tuple(g//l if g%l == 0 else g/l for g,l in zip(prg.arg.global_size, local_size))
return runtime(*[bufs[i].get_buf(device) for i in prg.arg.globals], global_size=new_gs, local_size=(*local_size,),
vals=prg.arg.vals({}), wait=True)
return time_call(prg.replace(arg=replace(prg.arg, global_size=new_gs, local_size=tuple(local_size))).call(*bufs))
except Exception: return float('inf')
MAX_WORKGROUP = 1024
@@ -168,7 +166,7 @@ def exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
elif src.device.startswith("DISK") and getattr(src.allocator.dev, 'fd', None) is not None \
and hasattr(dest.allocator, 'copy_from_disk') and src.nbytes >= 4096 and dest.allocator.supports_copy_from_disk:
dest.allocator.copy_from_disk(dest._buf, src._buf, src.nbytes)
elif hasattr(dest.allocator, '_as_buffer'): src.allocator._copyout(dest.as_memoryview(force_zero_copy=True), src._buf)
elif hasattr(dest.allocator, '_as_buffer'): src.allocator._copyout(dest.allocator._as_buffer(dest._buf), src._buf)
else: dest.allocator._copyin(dest._buf, src.as_memoryview(allow_zero_copy=True))
return None
+1 -12
View File
@@ -274,6 +274,7 @@ DISALLOW_BROADCAST = ContextVar("DISALLOW_BROADCAST", 0)
@dataclass(frozen=True)
class Metadata:
name: str
caller: str
backward: bool = False
def __hash__(self): return hash(self.name)
def __str__(self): return self.name + (" bw" if self.backward else "")
@@ -511,18 +512,6 @@ def capstone_flatdump(lib: bytes, arch:str):
print(f"{instr.address:#08x}: {instr.mnemonic}\t{instr.op_str}")
sys.stdout.flush()
def _find_llvm_objdump():
if OSX: return '/opt/homebrew/opt/llvm/bin/llvm-objdump'
# Try ROCm path first, then versioned, then unversioned
for p in ['/opt/rocm/llvm/bin/llvm-objdump', 'llvm-objdump-21', 'llvm-objdump-20', 'llvm-objdump']:
if shutil.which(p): return p
raise FileNotFoundError("llvm-objdump not found")
def amdgpu_disassemble(lib:bytes):
asm = system(f"{_find_llvm_objdump()} -d -", input=lib).splitlines()
while asm and ("s_nop 0" in asm[-1] or "s_code_end" in asm[-1]): asm.pop()
print("\n".join(asm))
def wait_cond(cb, *args, value=True, timeout_ms=10000, msg="") -> bool:
start_time = int(time.perf_counter() * 1000)
while int(time.perf_counter() * 1000) - start_time < timeout_ms:
+4 -3
View File
@@ -37,7 +37,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
def q_to_uint8(t: Tensor, b: int) -> Tensor:
# TODO: rewrite with arange?
shift_tensor, bitmask = Tensor.const(t.dtype, tuple(2**(i*b) for i in range(8//b))), 0xff >> (8 - b)
shift_tensor, bitmask = Tensor.stack(*[ Tensor(2**(i*b), device=t.device, dtype=t.dtype) for i in range(8//b) ]), 0xff >> (8 - b)
return t.unsqueeze(-1).expand((*t.shape,8//b)).div(shift_tensor, rounding_mode="trunc").bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
if (nelements_nbytes := _GGML_QUANT.get(ggml_type)) is not None:
@@ -74,7 +74,8 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
scale_words = blocks[:, 66:98].bitcast(dtypes.uint32)
db = d * (scale_words.rshift(28).cast(dtypes.float32) + 0.5).reshape((-1, 8, 1, 1)) * 0.5
sign_idx = scale_words.unsqueeze(-1).rshift(Tensor.const(dtypes.uint32, (0, 7, 14, 21))).bitwise_and(0x7F).reshape((-1, 32)).cast(dtypes.int32)
sign_idx = scale_words.unsqueeze(-1).rshift(
Tensor([0, 7, 14, 21], device=t.device, dtype=dtypes.uint32)).bitwise_and(0x7F).reshape((-1, 32)).cast(dtypes.int32)
even_signs = Tensor([i | (0x80 if i.bit_count() % 2 else 0) for i in range(128)], dtype=dtypes.uint8, device=t.device)
signs = (q_to_uint8(even_signs[sign_idx].reshape((-1, 32, 1)), 1) == 0).where(1.0, -1.0).reshape((-1, 8, 4, 8))
grid = _ggml_iq_grid(t.device, _ggml.iq3xxs_grid, (256, 4))[blocks[:, 2:66]].reshape((-1, 8, 4, 8))
@@ -95,7 +96,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
return (db * _ggml_iq_grid(t.device, _ggml.iq2s_grid, (1024, 8))[q].reshape((-1, 16, 2, 8)) * signs).flatten(-3)
if ggml_type == 23:
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1))
scale_shifts = Tensor.const(dtypes.uint16, (0, 2, 4, 6, 8, 10, 12, 14))
scale_shifts = Tensor([0, 2, 4, 6, 8, 10, 12, 14], device=t.device, dtype=dtypes.uint16)
iq4_xs_lut = Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)
scales_l = Tensor.stack((sl:=blocks[:, 4:8]).bitwise_and(0xF), sl.rshift(4), dim=2).reshape((-1, 8))
scales_h = blocks[:, 2:4].bitcast(dtypes.uint16).unsqueeze(-1).rshift(scale_shifts).bitwise_and(0x03).reshape((-1, 8)).cast(dtypes.uint8)
+8
View File
@@ -72,6 +72,10 @@ class ElementwiseMixin(CreationMixin):
"""
return self.logical_not() if self.dtype == dtypes.bool else self * (-1)
def _check_dtype(self) -> None:
if not (dtypes.is_bool(self.dtype) or dtypes.is_int(self.dtype)):
raise RuntimeError(f"{self.dtype} is not supported")
def add(self, x: Self | ConstType, reverse: bool = False) -> Self:
"""
Adds `self` and `x`.
@@ -143,6 +147,7 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([True, False]).bitwise_not().numpy())
```
"""
self._check_dtype()
if self.dtype == dtypes.bool: return self.logical_not()
return (self ^ self.dtype.max) if dtypes.is_unsigned(self.dtype) else (self ^ -1)
@@ -158,6 +163,7 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([True, True, False, False]).bitwise_and(Tensor([True, False, True, False])).numpy())
```
"""
self._check_dtype()
return self._binop(Ops.AND, x, reverse)
def bitwise_or(self, x: Self | ConstType, reverse: bool = False) -> Self:
@@ -172,6 +178,7 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([True, True, False, False]).bitwise_or(Tensor([True, False, True, False])).numpy())
```
"""
self._check_dtype()
return self._binop(Ops.OR, x, reverse)
def bitwise_xor(self, x: Self | ConstType, reverse: bool = False) -> Self:
@@ -187,6 +194,7 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([True, True, False, False]).bitwise_xor(Tensor([True, False, True, False])).numpy())
```
"""
self._check_dtype()
return self._binop(Ops.XOR, x, reverse)
def mod(self, x: Self | ConstType, reverse: bool = False) -> Self:
+22 -18
View File
@@ -184,11 +184,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
```
"""
if stop is None: stop, start = start, 0
if dtype is None: dtype = dtypes.default_float if any(isinstance(x, float) for x in (start, stop, step)) else dtypes.default_int
lo, hi = (start, stop-step) if step > 0 else (stop-step, start)
if dtype is None:
dtype = dtypes.default_float if any(isinstance(x, float) for x in (start, stop, step)) else dtypes.default_int
# an int range too large for default_int picks int64
if dtype is dtypes.default_int and (lo < dtype.min or dtype.max < hi): dtype = dtypes.int64
if lo < (dt:=to_dtype(dtype)).min or dt.max < hi: raise OverflowError(f"arange [{start}, {stop}) is not representable in dtype {dtype}")
# NOTE: this matches numpy, torch raises RuntimeError if stop-start and step have different signs
if (output_len:=ceildiv(stop-start, step)) <= 0: return cls.full((0,), 0, dtype=dtype, buffer=False)
@@ -719,7 +716,6 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
dim = self._resolve_dim(dim)
for arg in args: assert arg.ndim==self.ndim and all(ti==ai for i,(ti,ai) in enumerate(zip(self.shape, arg.shape)) if i!=dim)
tensors = [self, *args]
if all(t.shape[dim] == self.shape[dim] for t in args): return self.stack(*args, dim=dim).flatten(dim, dim+1)
dim_cumsum = list(itertools.accumulate([t.shape[dim] for t in tensors], initial=0))
padded = [t.pad(tuple((dim_cumsum[i], dim_cumsum[-1]-dim_cumsum[i+1]) if j==dim else None for j in range(t.ndim))) for i,t in enumerate(tensors)]
return padded[0].usum(*padded[1:])
@@ -980,9 +976,11 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
# helper function commonly used for indexing
def _one_hot_along_dim(self, num_classes:sint, dim:int=-1) -> Self:
from tinygrad.uop.ops import sint_to_uop
if not dtypes.is_int(self.dtype): raise RuntimeError(f"_one_hot_along_dim expects int index tensor, getting {self.dtype}")
offset = self.ndim - self._resolve_dim(dim) - 1
return self.eq(type(self).arange(num_classes).reshape((num_classes,) + (1,) * offset))
dt = dtypes.int64 if sint_to_uop(num_classes).overflows(dtypes.int32) else dtypes.int32
return self.eq(type(self).arange(num_classes, dtype=dt).reshape((num_classes,) + (1,) * offset))
def one_hot(self, num_classes:int) -> Self:
"""
@@ -1384,17 +1382,22 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
ret = (indices.reshape(bs,c,1,-1)._one_hot_along_dim(prod(output_size), 2).where(self.reshape(bs,c,1,-1), 0)).sum(3)
return ret.reshape(bs,c,*output_size)
@classmethod
def _get_winograd_matcols(cls, mat, dims:int, shp:tuple[sint, ...], dtype:DType) -> list[list[Self]]:
return [[cls.cat(*[cls.full(shp[:dim] + (1,) + shp[dim+1:], float(m[k]), dtype=dtype, buffer=False) for m in mat], dim=dim)
for k in range(len(mat[0]))] for dim in range(dims)]
# winograd conv 3 kernel f(4x4,3x3) see: http://arxiv.org/abs/1509.09308
def _apply_winograd_matrix(self, mat, dims:int) -> Self:
# apply mat along each of the first `dims` axes: the separable transform kron(mat, ..., mat) @ self
# column k of mat is a stacked-CONST vector that folds into the arithmetic, so no constant is materialized
ret = self
for dim in range(dims):
ret = ret.transpose(0, dim)
ret = sum(type(self).const(ret.dtype, tuple(float(m[k]) for m in mat)).reshape((len(mat),)+(1,)*(ret.ndim-1)) * ret[k]
for k in range(len(mat[0])))
assert not isinstance(ret, int), "sum over empty winograd matrix"
ret = ret.transpose(0, dim)
# multiply mat_1 @ mat_2 @ t with foldable constants, where mat_i acts on vector t along dimension i; roughly kron(mat, mat) @ t
# due to realize-before-expand rule in lazy.py, we must operate in this order: reshape -> expand -> arithmetic
t_ = self.reshape(self.shape[:dims] + (1,) * dims + self.shape[dims:]).expand(
self.shape[:dims] + (len(mat),) * dims + self.shape[dims:]) # add output dims
# precalculate mat columns for each dim; prod(itertools.product(matcols)) gives the columns of kron(mat, mat, ...)
matcols = type(self)._get_winograd_matcols(mat, dims, t_.shape[dims:], t_.dtype)
# multiply each element of t_ by the corresponding stacked column of kron(mat, mat), producing only one view for each element of t
ret = sum(prod(col[idx] for col, idx in zip(matcols, mat_is)) * t_[mat_is] for mat_is in itertools.product(range(len(mat[0])), repeat=dims))
assert not isinstance(ret, int), "sum over empty winograd matrix"
return ret
# TODO: winograd can be a rewrite rule like split_reduceop
@@ -1414,8 +1417,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
# (bs, cin_, tyx, HWI)
pads = [(pB, pA + (-(s + pB + pA - 2) % 4)) for (pB, pA), s in zip(flat_to_grouped(padding_), self.shape[-len(HW):])]
d = self.pad(flatten(reversed(pads)))._pool(HWI, HWO)
# move HW to the front: # (HWI, bs, cin_, tyx); contiguous_backward keeps the input transform's adjoint out of the overlap accumulation
d = d.permute(*range(len(d.shape)-len(HW),len(d.shape)), *range(len(d.shape)-len(HW))).contiguous_backward()
# move HW to the front: # (HWI, bs, cin_, tyx)
d = d.permute(*range(len(d.shape)-len(HW),len(d.shape)), *range(len(d.shape)-len(HW)))
tyx = d.shape[-len(HWI):] # dim of tiling
g = weight.permute(*range(len(weight.shape)-len(HW),len(weight.shape)), *range(len(weight.shape)-len(HW))) # move HW to the front
@@ -1878,7 +1881,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
# https://keccak.team/keccak_specs_summary.html
def ctensor(l: Sequence[PyConst], dtype: DType = dtypes.uint64):
return type(self).const(dtype, tuple(l))
# TODO: contiguous is here for compile speed
return type(self).stack(*(type(self).const(dtype, v) for v in l)).contiguous()
rot_offsets = [44, 43, 21, 14, 28, 20, 3, 45, 61, 1, 6, 25, 8, 18, 27, 36, 10, 15, 56, 62, 55, 39, 41, 2]
rot_offsets_v0, rot_offsets_v1 = ctensor([0] + [1 << v for v in rot_offsets]), ctensor([1] + [1 << (64 - v) for v in rot_offsets])
-2
View File
@@ -73,8 +73,6 @@ class Renderer:
tensor_cores: list[TensorCore] = []
extra_matcher: PatternMatcher|None = None
code_for_op: dict[Ops, Callable] = {}
# renderers without range support get all bounded ranges rewritten to loops in codegen
supports_ranges: bool = True
compiler: Compiler = Compiler()
+2 -10
View File
@@ -65,11 +65,6 @@ base_rewrite = PatternMatcher([
(UPat(GroupOp.ALU, name="x"), lambda ctx,x: ctx.code_for_op[x.op](
*([strip_parens(ctx[v]) if v.op == x.op and x.op in {Ops.ADD, Ops.MUL, Ops.XOR, Ops.OR, Ops.AND} else ctx[v] for v in x.src]), x.dtype)),
# call an external function
(UPat(Ops.CALL, src=(UPat(),), allow_any_len=True, name="x"), lambda ctx,x:
f"((({ctx.abi}{ctx.render_dtype(x.dtype)}(*)({', '.join(ctx.render_type(y) for y in x.src[1:])}))({ctx[x.src[0]]}))" +
f"({', '.join(f'({ctx.render_type(y)})({ctx[y]})' for y in x.src[1:])}))" + (";" if x.dtype is dtypes.void else "")),
# custom passes through with format
(UPat((Ops.CUSTOM, Ops.CUSTOMI), name="x"), lambda ctx,x: x.arg.format(*[ctx[y] for y in x.src])),
])
@@ -116,7 +111,6 @@ def wmma_args(uops:list[UOp]):
for uop in uops if uop.op is Ops.WMMA)
class CStyleLanguage(Renderer):
abi: str = ""
kernel_typedef: str = "void"
buffer_prefix: str = ""
buffer_suffix: str = ""
@@ -149,8 +143,7 @@ class CStyleLanguage(Renderer):
tmp = ""
if any(is_image_shape(u._shape) for _,(u,_) in bufs):
tmp = "const sampler_t smp = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP | CLK_FILTER_NEAREST;\n"
buftypes = [(name, ("volatile " if u.arg.volatile else "")+
self._render_dtype(u.dtype, sz=1, addrspace=u.addrspace, mutable=mutable, shape=u._shape)+self.buffer_suffix \
buftypes = [(name, self._render_dtype(u.dtype, sz=1, addrspace=u.addrspace, mutable=mutable, shape=u._shape)+self.buffer_suffix \
if u.addrspace == AddrSpace.GLOBAL else self.arg_int_prefix if u.dtype == dtypes.int else None) for name,(u,mutable) in bufs]
local_dims = [u.src[0] for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]
launch_bounds = prod([d.vmax for d in local_dims])
@@ -278,8 +271,7 @@ class ClangRenderer(CStyleLanguage):
+ create_non_native_float_pats((dtypes.bfloat16,)) + pm_manual_bf16_cast
if sys.platform == 'win32':
abi = "__attribute__((ms_abi)) "
kernel_typedef = abi + "void"
kernel_typedef = "__attribute__((ms_abi)) void"
def render_vector_prefix(self, dt:DType, count:int) -> str:
# round (down) to power of two (this is actually the default clang behavior)
alignment = 2**int(math.log2(dt.itemsize * count)) if getenv("ALIGNED", 1) and not dtypes.is_bool(dt) else 1
+25 -16
View File
@@ -3,12 +3,10 @@ from tinygrad.codegen.opt import tc
from tinygrad.renderer import Renderer
from tinygrad.renderer.cstyle import HIPRenderer, create_non_native_float_pats, pm_manual_bf16_cast
from tinygrad.codegen.decomp.transcendental import xexp2, xlog2
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, GroupOp
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, GroupOp, range_str
from tinygrad.dtype import dtypes, float_to_fp8, DType, truncate, AddrSpace
from tinygrad.helpers import prod, Target, CPU_COUNT, getenv, OSX
def is_volatile(u:UOp) -> bool: return (buf:=u.buf_uop).op is Ops.PARAM and buf.arg.volatile
def ldt(dt:DType, count=1, ptr=False):
if ptr: return ldt(dt, count) + "*"
if count > 1: return f"<{count} x {ldt(dt, 1, ptr)}>"
@@ -74,16 +72,13 @@ base_rewrite = PatternMatcher([
lambda ctx,x,idx,alt,mask:
f" br label {ctx[x]}_entry\n{ctx[x][1:]}_entry:\n"
f" br i1 {ctx[mask]}, label {ctx[x]}_load, label {ctx[x]}_exit\n{ctx[x][1:]}_load:\n"
f" {ctx[x]}_yes = load {'volatile ' if is_volatile(idx) else ''}{ldt(idx.dtype, idx.max_numel())}, "
f"{ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}\n"
f" {ctx[x]}_yes = load {ldt(idx.dtype, idx.max_numel())}, {ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}\n"
f" br label {ctx[x]}_exit\n{ctx[x][1:]}_exit:\n"
f" {ctx[x]} = phi {ldt(x.dtype, x.max_numel())} [{ctx[x]}_yes, {ctx[x]}_load], [{ctx[alt]}, {ctx[x]}_entry]"),
(UPat.var('idx').load(name="x"), lambda ctx,x,idx:
f" {ctx[x]} = load {'volatile ' if is_volatile(idx) else ''}{ldt(idx.dtype, idx.max_numel())}, "
f"{ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}"),
f" {ctx[x]} = load {ldt(idx.dtype, idx.max_numel())}, {ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}"),
(UPat.var('idx').store(UPat.var("var")), lambda ctx,idx,var:
f" store {'volatile ' if is_volatile(idx) else ''}{ldt(var.dtype, idx.max_numel())} {ctx[var]}, "
f"{ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}"),
f" store {ldt(var.dtype, idx.max_numel())} {ctx[var]}, {ldt(idx.dtype, idx.max_numel(), True)} {ctx[idx]}"),
# GEP/VECTORIZE/CAST for float4 support
(UPat(Ops.STACK, name="x"), lambda ctx,x:
@@ -101,13 +96,28 @@ base_rewrite = PatternMatcher([
(UPat(Ops.WHERE, name="x"), lambda ctx,x:
f" {ctx[x]} = select {ldt(x.src[0].dtype)} {ctx[x.src[0]]}, {ldt(x.src[1].dtype)} {ctx[x.src[1]]}, {ldt(x.src[2].dtype)} {ctx[x.src[2]]}"),
# loop (ranges are rewritten to loops in codegen), a bool src is a one-time entry guard for possibly zero trip counts
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx,l:
f" br i1 {ctx[g]}, label %loop_{ctx[l][1:]}, label %loop_exit_{ctx[l][1:]}\nloop_{ctx[l][1:]}:" \
if (g:=next((s for s in l.src if s.dtype is dtypes.bool), None)) is not None else f" br label %loop_{ctx[l][1:]}\nloop_{ctx[l][1:]}:"),
# loop (a RANGE with no src is an unbounded loop header)
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx,l: f" br label %loop_{ctx[l][1:]}\nloop_{ctx[l][1:]}:"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx,l,c:
f" br i1 {ctx[c]}, label %loop_{ctx[l][1:]}, label %loop_exit_{ctx[l][1:]}\nloop_exit_{ctx[l][1:]}:"),
# range
(UPat(Ops.RANGE, name="r"), lambda ctx,r:
f" br label %loop_entry_{range_str(r)}\n"
f"loop_entry_{range_str(r)}:\n"
f" br label %loop_latch_{range_str(r)}\n"
f"loop_latch_{range_str(r)}:\n"
f" {ctx[r]} = phi {ldt(r.dtype)} [ 0, %loop_entry_{range_str(r)} ], [ {ctx[r]}phi, %loop_footer_{range_str(r)} ]\n"
f" {ctx[r]}phi = add {ldt(r.dtype)} {ctx[r]}, 1\n"
f" {ctx[r]}cmp = icmp ult {ldt(r.dtype)} {ctx[r]}, {ctx[r.src[0]]}\n"
f" br i1 {ctx[r]}cmp, label %loop_body_{range_str(r)}, label %loop_exit_{range_str(r)}\n"
f"loop_body_{range_str(r)}:"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, name="r"))), lambda r:
f" br label %loop_footer_{range_str(r)}\n"
f"loop_footer_{range_str(r)}:\n"
f" br label %loop_latch_{range_str(r)}\n"
f"loop_exit_{range_str(r)}:"),
# if
(UPat(Ops.IF, name="x"), lambda ctx,x: f" br i1 {ctx[x.src[0]]}, label %ifbody_{ctx[x][1:]}, label %ifskip_{ctx[x][1:]}\nifbody_{ctx[x][1:]}:"),
(UPat(Ops.ENDIF, name="x"), lambda ctx,x: f" br label %ifskip_{ctx[x.src[0]][1:]}\nifskip_{ctx[x.src[0]][1:]}:"),
@@ -117,7 +127,6 @@ base_rewrite = PatternMatcher([
class LLVMRenderer(Renderer):
supports_float4 = True
supports_ranges = False
abi: str | None
string_rewrite: PatternMatcher
code_for_op = {k:lambda:None for v in lop.values() for k in v.keys()}
@@ -183,7 +192,7 @@ class CPULLVMRenderer(LLVMRenderer):
def _render_footer(self, uops: list[UOp]) -> str: return 'attributes #0 = { alwaysinline nounwind "no-builtins" "no-trapping-math"="true" }'
def __init__(self, target:Target):
super().__init__(target)
from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler
self.compiler = CPULLVMCompiler(target.arch.split(","))
# FIXME: fp16 works on non-osx, but only if the cpu supports it
@@ -244,7 +253,7 @@ exit: %packed = phi i32 [%packed_bf8, %do_bf8], [%packed_fp8, %do_fp8]\n %trunc
return 'attributes #0 = { ' + ' '.join(attributes) + ' }'
def __init__(self, target:Target):
super().__init__(target)
from tinygrad.runtime.support.compiler_llvm import AMDLLVMCompiler
from tinygrad.runtime.support.compiler_amd import AMDLLVMCompiler
self.compiler, self.tensor_cores, self.is_cdna = AMDLLVMCompiler(target.arch), tc.get_amd(target.arch), HIPRenderer.is_cdna(target.arch)
self.string_rewrite += PatternMatcher([(UPat(Ops.WMMA, name="wmma"), lambda ctx, wmma, cdna=self.is_cdna: render_wmma_amd(ctx, wmma, cdna))])
if self.is_cdna:
+11 -11
View File
@@ -3,7 +3,7 @@ from tinygrad.dtype import AddrSpace, DType, dtypes, truncate
from tinygrad.helpers import DEBUG, OSX, unwrap, fromimport, Target, is_image_shape
from tinygrad.renderer import Renderer
from tinygrad.renderer.cstyle import CUDARenderer, OpenCLRenderer
from tinygrad.uop.ops import GroupOp, Ops, UOp, PatternMatcher, UPat
from tinygrad.uop.ops import GroupOp, Ops, UOp, PatternMatcher, UPat, range_str
from tinygrad.runtime.autogen import mesa, libc
from tinygrad.runtime.support.c import POINTER
import base64, ctypes, struct, functools, inspect, itertools
@@ -117,7 +117,6 @@ def nidx(b:mesa.nir_builder, buf, off, space, itemsize, gate=None) -> mesa.nir_d
class NIRRenderer(Renderer):
suffix = "NIR"
nir_options: bytes
supports_ranges = False
global_max, local_max, shared_max = CUDARenderer.global_max, CUDARenderer.local_max, CUDARenderer.shared_max
code_for_op = {**{k:lambda:None for k in u_aop.keys()}, **{k:lambda:None for k in s_aop.keys()}, **{k:lambda:None for k in f_aop.keys()}}
@@ -188,7 +187,7 @@ class NIRRenderer(Renderer):
self.prerender(uops)
for u in [u for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]: self.b.shader.contents.info.workgroup_size[int(u.arg[-1])] = u.src[0].arg
self.r: dict[UOp, Any] = {}
self.param_idx, loop_ifs = 0, []
self.param_idx, ranges = 0, []
for u in uops:
if u.op in {Ops.NOOP, Ops.GROUP} or (u.op is Ops.STACK and len(u.src) == 0): pass
@@ -204,17 +203,18 @@ class NIRRenderer(Renderer):
self.r[u] = nimm(self.b, self.b.shader.contents.info.shared_size, dtypes.long)
self.b.shader.contents.info.shared_size += u.max_numel()*u.dtype.itemsize
elif u.op == Ops.RANGE:
# ranges are rewritten to loops in codegen: just open the loop, the END adds the conditional backedge
# a bool src is a one-time entry guard for possibly zero trip counts
assert u.dtype == dtypes.void, "NIRRenderer does not support ranges"
guard = next((s for s in u.src if s.dtype is dtypes.bool), None)
loop_ifs.append(mesa.nir_push_if(self.b, self.r[guard]) if guard is not None else None)
ranges.append(i:=deref_var(self.b, mesa.nir_local_variable_create(self.b.impl, glsl_type(u.dtype), f"idx{range_str(u)}".encode()).contents))
nstore(self.b, AddrSpace.REG, i, nimm(self.b, 0, u.dtype))
mesa.nir_push_loop(self.b)
self.r[u] = nload(self.b, AddrSpace.REG, i, u)
nif(self.b, nalu(self.b, "ilt", self.r[u], self.r[u.src[0]]), lambda: None, lambda: njump(self.b, mesa.nir_jump_break))
elif u.op == Ops.END:
# loop again while the condition is true
nif(self.b, self.r[u.src[2]], lambda: None, lambda: njump(self.b, mesa.nir_jump_break))
r = u.src[1]
next_i = nalu(self.b, "iadd", self.r[r], nimm(self.b, 1, r.dtype))
# TODO: this nif should be removable ... but TestMultiTensor.test_double_matmul_shard_W_0 segfaults with it gone
nif(self.b, nalu(self.b, "ilt", next_i, self.r[r.src[0]]), lambda: None, lambda: njump(self.b, mesa.nir_jump_break))
nstore(self.b, AddrSpace.REG, ranges.pop(), next_i),
mesa.nir_pop_loop(self.b, None)
if (nif_ref:=loop_ifs.pop()) is not None: mesa.nir_pop_if(self.b, nif_ref)
else:
d: mesa.nir_def|None = self.def_rewrite.rewrite(u, ctx=self)
if d is None: raise RuntimeError(f"failed to render {u.op} srcs {[x.dtype for x in u.src]}")
+11 -7
View File
@@ -116,13 +116,18 @@ string_rewrite = PatternMatcher([
# simple
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
f".shared .align 16 .b8 local{x.arg.slot}[{x.max_numel()*x.dtype.itemsize}];", f"mov.u64 {ctx.r[x]}, local{x.arg.slot}[0];"]),
# loop (ranges are rewritten to loops in codegen), a bool src is a one-time entry guard for possibly zero trip counts
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx, l:
[f"@!{ctx.r[g]} bra WAITLOOP_EXIT_{ctx.uops.index(l)};", f"WAITLOOP_{ctx.uops.index(l)}:"] \
if (g:=next((s for s in l.src if s.dtype is dtypes.bool), None)) is not None else f"WAITLOOP_{ctx.uops.index(l)}:"),
(UPat(Ops.RANGE, dtypes.void, name="l"), lambda ctx, l: f"WAITLOOP_{ctx.uops.index(l)}:"),
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void, name="l"), UPat(name="c"))), lambda ctx, l, c:
[f"@{ctx.r[c]} bra WAITLOOP_{ctx.uops.index(l)};"] +
([f"WAITLOOP_EXIT_{ctx.uops.index(l)}:"] if any(s.dtype is dtypes.bool for s in l.src) else [])),
f"@{ctx.r[c]} bra WAITLOOP_{ctx.uops.index(l)};"),
(UPat(Ops.RANGE, name="r"), lambda ctx, r: [
f"mov.u32 {ctx.r[r]}, -1;",
f"bra END_{ctx.r[r][1:]};",
"LOOP_" + f"{ctx.r[r][1:]}:"]),
(UPat(Ops.END, name="x", src=(UPat(), UPat(Ops.RANGE, name="r"))), lambda ctx, x, r: [
"END_" + f"{ctx.r[r][1:]}:",
ctx.code_for_op[Ops.ADD](ctx.r[r], ctx.r[r], "1", dtypes.int, ctx.types[dtypes.int]),
ctx.code_for_op[Ops.CMPLT](ctx.r[x], ctx.r[r], ctx.r[r.src[0]], dtypes.int, ctx.types[dtypes.int]),
f"@{ctx.r[x]} bra LOOP_{ctx.r[r][1:]};"]),
(UPat(Ops.IF, name="x"), lambda ctx, x: f"@!{ctx.r[x.src[0]]} bra IF_{ctx.r[x.src[0]][1:]}_{ctx.uops.index(x)};"),
(UPat(Ops.ENDIF, name="x"), lambda ctx, x: f"IF_{ctx.r[x.src[0].src[0]][1:]}_{ctx.uops.index(x.src[0])}:"),
(UPat(Ops.WMMA, name="x"), lambda ctx, x: list(render_wmma(ctx, x))),
@@ -131,7 +136,6 @@ string_rewrite = PatternMatcher([
class PTXRenderer(Renderer):
suffix = "PTX"
supports_ranges = False
global_max, local_max, shared_max = CUDARenderer.global_max, CUDARenderer.local_max, CUDARenderer.shared_max
tc_sm80 = [x for x in tc.cuda_sm80 if x.dtype_in in [dtypes.half, dtypes.float]]
code_for_op = asm_for_op
+1 -2
View File
@@ -53,8 +53,7 @@ def __getattr__(nm):
match nm:
case "libc":
return load("libc", lambda: ([i for i in system("dpkg -L libc6-dev").split() if 'sys/mman.h' in i or 'bits/mman-shared.h' in i] +
["/usr/include/string.h", "/usr/include/elf.h", "/usr/include/unistd.h", "/usr/include/stdio.h", "/usr/include/semaphore.h",
"/usr/include/asm-generic/mman-common.h"]),
["/usr/include/string.h", "/usr/include/elf.h", "/usr/include/unistd.h", "/usr/include/stdio.h", "/usr/include/asm-generic/mman-common.h"]),
args=["-D__USE_GNU", "-D_GNU_SOURCE"], dll="'c'", errno=True, recsym=True, rules=[(r'([a-z]+) = \1', '')]) # removes stdin = stdin
case "avcodec": return load("avcodec", ["{}/libavcodec/hevc/hevc.h", "{}/libavcodec/cbs_h265.h"], srcs=ffmpeg_src)
case "opencl": return load("opencl", ["{}/CL/cl.h"], dll="'OpenCL'", args=["-I{}"], srcs=opencl_src)
-60
View File
@@ -1194,65 +1194,6 @@ def funlockfile(__stream:c.POINTER[FILE]) -> None: ...
def __uflow(_0:c.POINTER[FILE]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[FILE], ctypes.c_int32)
def __overflow(_0:c.POINTER[FILE], _1:int) -> int: ...
@c.record
class fd_set(c.Struct):
SIZE = 128
fds_bits: c.Array[ctypes.c_int64, Literal[16]]
__fd_mask: TypeAlias = ctypes.c_int64
fd_set.register_fields([('fds_bits', c.Array[ctypes.c_int64, Literal[16]], 0)])
@c.record
class struct_timeval(c.Struct):
SIZE = 16
tv_sec: int
tv_usec: int
__time_t: TypeAlias = ctypes.c_int64
__suseconds_t: TypeAlias = ctypes.c_int64
struct_timeval.register_fields([('tv_sec', ctypes.c_int64, 0), ('tv_usec', ctypes.c_int64, 8)])
@dll.bind(ctypes.c_int32, ctypes.c_int32, c.POINTER[fd_set], c.POINTER[fd_set], c.POINTER[fd_set], c.POINTER[struct_timeval])
def select(__nfds:int, __readfds:c.POINTER[fd_set], __writefds:c.POINTER[fd_set], __exceptfds:c.POINTER[fd_set], __timeout:c.POINTER[struct_timeval]) -> int: ...
@c.record
class struct_timespec(c.Struct):
SIZE = 16
tv_sec: int
tv_nsec: int
__syscall_slong_t: TypeAlias = ctypes.c_int64
struct_timespec.register_fields([('tv_sec', ctypes.c_int64, 0), ('tv_nsec', ctypes.c_int64, 8)])
@c.record
class __sigset_t(c.Struct):
SIZE = 128
__val: c.Array[ctypes.c_uint64, Literal[16]]
__sigset_t.register_fields([('__val', c.Array[ctypes.c_uint64, Literal[16]], 0)])
@dll.bind(ctypes.c_int32, ctypes.c_int32, c.POINTER[fd_set], c.POINTER[fd_set], c.POINTER[fd_set], c.POINTER[struct_timespec], c.POINTER[__sigset_t])
def pselect(__nfds:int, __readfds:c.POINTER[fd_set], __writefds:c.POINTER[fd_set], __exceptfds:c.POINTER[fd_set], __timeout:c.POINTER[struct_timespec], __sigmask:c.POINTER[__sigset_t]) -> int: ...
@c.record
class sem_t(c.Struct):
SIZE = 32
__size: c.Array[ctypes.c_char, Literal[32]]
__align: int
sem_t.register_fields([('__size', c.Array[ctypes.c_char, Literal[32]], 0), ('__align', ctypes.c_int64, 0)])
@dll.bind(ctypes.c_int32, c.POINTER[sem_t], ctypes.c_int32, ctypes.c_uint32)
def sem_init(__sem:c.POINTER[sem_t], __pshared:int, __value:int) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t])
def sem_destroy(__sem:c.POINTER[sem_t]) -> int: ...
@dll.bind(c.POINTER[sem_t], c.POINTER[ctypes.c_char], ctypes.c_int32)
def sem_open(__name:c.POINTER[ctypes.c_char], __oflag:int) -> c.POINTER[sem_t]: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t])
def sem_close(__sem:c.POINTER[sem_t]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[ctypes.c_char])
def sem_unlink(__name:c.POINTER[ctypes.c_char]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t])
def sem_wait(__sem:c.POINTER[sem_t]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t], c.POINTER[struct_timespec])
def sem_timedwait(__sem:c.POINTER[sem_t], __abstime:c.POINTER[struct_timespec]) -> int: ...
clockid_t: TypeAlias = ctypes.c_int32
@dll.bind(ctypes.c_int32, c.POINTER[sem_t], clockid_t, c.POINTER[struct_timespec])
def sem_clockwait(__sem:c.POINTER[sem_t], clock:clockid_t, __abstime:c.POINTER[struct_timespec]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t])
def sem_trywait(__sem:c.POINTER[sem_t]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t])
def sem_post(__sem:c.POINTER[sem_t]) -> int: ...
@dll.bind(ctypes.c_int32, c.POINTER[sem_t], c.POINTER[ctypes.c_int32])
def sem_getvalue(__sem:c.POINTER[sem_t], __sval:c.POINTER[ctypes.c_int32]) -> int: ...
MREMAP_MAYMOVE = 1
MREMAP_FIXED = 2
MREMAP_DONTUNMAP = 4
@@ -4405,7 +4346,6 @@ _PRINTF_NAN_LEN_MAX = 4
RENAME_NOREPLACE = (1 << 0)
RENAME_EXCHANGE = (1 << 1)
RENAME_WHITEOUT = (1 << 2)
_SEMAPHORE_H = 1
PROT_READ = 0x1
PROT_WRITE = 0x2
PROT_EXEC = 0x4
+68 -108
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
import platform, sys, os, ctypes, functools, mmap, threading, array
from tinygrad.helpers import to_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le
import platform, sys, ctypes, functools, time, mmap, threading, queue
from tinygrad.helpers import to_mv, OSX, WIN, mv_address, suppress_finalizing, unwrap, data64_le
from tinygrad.device import BufferSpec
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, HCQArgsState, HCQSignal, HCQProgram, MMIOInterface
from tinygrad.runtime.support.hcq import CLikeArgsState
@@ -9,74 +9,62 @@ from tinygrad.renderer.llvmir import CPULLVMRenderer
from tinygrad.renderer.nir import LVPRenderer
from tinygrad.renderer.isa.x86 import X86Renderer
from tinygrad.runtime.support.elf import jit_loader
from tinygrad.runtime.autogen import libc
from tinygrad.codegen import do_to_program
from tinygrad import UOp, dtypes
from tinygrad.dtype import AddrSpace
from tinygrad.uop.ops import sint, KernelInfo
from tinygrad.uop.ops import sint
MAX_ARGS, CMD_SIZE, RING_SLOTS = 31, 32, (16 << 10)
class CPUSignal(HCQSignal):
def _sleep(self, time_spent_since_last_sleep_ms:int):
if self.is_timeline and self.owner is not None:
self.owner.tasks.join()
if self.owner.error_state is not None: raise self.owner.error_state
def signal_prog():
val = UOp.param(1, dtypes.int, (), vmin_vmax=(0, dtypes.int.max), name="value", addrspace=AddrSpace.ALU)
return UOp.param(0, dtypes.uint32, (1,))[0].store(val.cast(dtypes.uint32))
class CPUWorker(threading.Thread):
def __init__(self, dev, tasks, thread_id):
super().__init__()
self.dev, self.tasks, self.thread_id, self.pool, self.daemon = dev, tasks, thread_id, [], True
def wait_prog():
val = UOp.param(1, dtypes.int, (), vmin_vmax=(0, dtypes.int.max), name="value", addrspace=AddrSpace.ALU)
return (v:=UOp.param(0, dtypes.uint32, (1,), volatile=True).after(l:=UOp.loop(0))[0].load()).end(l, v < val.cast(dtypes.uint32))
def push_task(self, tid, cmd, args):
if len(self.pool) <= tid:
self.pool.append(queue.Queue())
CPUWorker(self, self.pool[tid], thread_id=tid+1).start()
self.pool[tid].put([cmd, 1, len(args)] + args)
def timestamp_prog():
if WIN: val = UOp.const(dtypes.uint64, 0)
else:
fn, ts = UOp.param(1, dtypes.uint64, (1,)), UOp.placeholder((2,), dtypes.uint64, slot=0, addrspace=AddrSpace.REG)
call = fn[0].load().call(UOp.const(dtypes.int, 6 if OSX else 1), ts[0], ret_dtype=dtypes.void) # clock_gettime(CLOCK_MONOTONIC, &ts)
val = ts.after(call)[0].load() * 1_000_000_000 + ts.after(call)[1].load()
return UOp.param(0, dtypes.uint64, (1,))[0].store(val)
def quit_prog():
fn = UOp.param(0, dtypes.uint64, (1 if WIN else 3,))
if WIN: return fn[0].load().call(UOp.const(dtypes.uint64, 0), ret_dtype=dtypes.void) # ExitThread(0)
sem = UOp.param(1, dtypes.uint64, (1,))
close = fn[2].load().call(sem[0], ret_dtype=dtypes.void) # sem_close(sem)
return fn.after(close)[0].load().call(UOp.const(dtypes.uint64, 0), ret_dtype=dtypes.void) # pthread_exit(0)
def worker_prog():
ring = UOp.param(0, dtypes.uint64, (RING_SLOTS * CMD_SIZE,), volatile=True)
wait, sem = UOp.param(1, dtypes.uint64, (1,), volatile=True), UOp.param(2, dtypes.uint64, (1,))
cur = UOp.range(2**64-1, 0, dtype=dtypes.uint64)
# spin on windows, sem_wait to sleep on posix
if WIN: ready = (v:=wait.after(lw:=UOp.loop(1), cur)[0].load()).end(lw, v <= cur)
else: ready = wait.after(cur)[0].load().call(sem.after(cur)[0], ret_dtype=dtypes.void)
entry = [ring.after(ready).index((cur % RING_SLOTS) * CMD_SIZE + i).load() for i in range(CMD_SIZE)]
return entry[0].call(*entry[1:], ret_dtype=dtypes.void).end(cur)
def run(self):
while True:
cmd_iter = iter(self.tasks.get())
try:
for cmd in cmd_iter:
threads, args_cnt = next(cmd_iter), next(cmd_iter)
args = [next(cmd_iter) for _ in range(args_cnt)]
for th in range(threads - 1): self.push_task(th, cmd, args)
cmd(self.thread_id, *args)
for th in range(threads - 1): self.pool[th].join()
except Exception as e: self.dev.error_state = e
finally: self.tasks.task_done()
class CPUComputeQueue(HWQueue):
def __init__(self, dev):
super().__init__()
self.dev = dev
def _cmd(self, prog, args=(), vals=()): return self.exec(prg:=self.dev.prgs[prog], prg.fill_kernargs(args, vals), None, None)
def _exec(self, tid, prg, bufs, *args):
vals = list(args[bufs:])
if 'core_id' in prg.runtimevars: vals[prg.runtimevars['core_id']] = tid
prg.fxn(*map(ctypes.c_uint64, args[:bufs]), *map(ctypes.c_int64 if platform.machine().lower() == "arm64" else ctypes.c_int32, vals))
def _signal(self, tid, signal_addr, value): to_mv(signal_addr, 4).cast('I')[0] = value
def _wait(self, tid, tmpl_sig, signal_addr, value):
tmpl_sig.base_buf = HCQBuffer(signal_addr, 16, view=MMIOInterface(signal_addr, 16))
tmpl_sig.wait(value)
def _timestamp(self, tid, timestamp_addr): to_mv(timestamp_addr, 8).cast('Q')[0] = time.perf_counter_ns()
def cmd(self, cmd, *args, threads=1):
self.q(cmd, threads, len(args), *args)
return self
def memory_barrier(self): return self
def exec(self, prg:CPUProgram, args_state:HCQArgsState, global_size, local_size):
if (lvp:=isinstance(args_state, LVPArgsState)): self.bind_args_state(args_state)
args:list[sint|None] = [args_state.buf.va_addr] if lvp else [*[x.va_addr for x in args_state.bufs], *args_state.vals]
assert len(args) <= MAX_ARGS, f"CPU programs support at most {MAX_ARGS} arguments, got {len(args)}"
for tid in range(1 if lvp else (global_size or (1,))[0]):
if not lvp and 'core_id' in prg.runtimevars: args[len(args_state.bufs)+prg.runtimevars['core_id']] = tid
self.q(prg, *[unwrap(x) for x in args], *([0] * (MAX_ARGS - len(args))))
return self
def wait(self, signal, value=0): return self._cmd(wait_prog, (signal.base_buf,), (value,))
def timestamp(self, signal): return self._cmd(timestamp_prog, (signal.base_buf.offset(8, 8), self.dev.func_table.offset(0, 8)))
def signal(self, signal, value:sint=0): return self._cmd(signal_prog, (signal.base_buf,), (value,))
def _submit(self, dev):
for off in range(0, len(self._q), CMD_SIZE):
entry = [self._q[off].addr, *self._q[off+1:off+CMD_SIZE]]
dev.ring_view[(base:=(dev.ring_pos % RING_SLOTS) * CMD_SIZE):base+CMD_SIZE] = array.array('Q', (int(x) & ((1<<64)-1) for x in entry))
dev.ring_pos += 1
if WIN: dev.sys_view[0] = dev.ring_pos
else: assert libc.sem_post(dev.sem) == 0
if isinstance(args_state, LVPArgsState):
self.bind_args_state(args_state)
return self.cmd(self._exec, prg, 1, args_state.buf.va_addr)
return self.cmd(self._exec, prg, len(args_state.bufs), *[x.va_addr for x in args_state.bufs], *args_state.vals, threads=(global_size or (1,))[0])
def wait(self, signal, value=0): return self.cmd(self._wait, type(signal)(signal.base_buf, owner=signal.owner, virt=True), signal.value_addr, value)
def timestamp(self, signal): return self.cmd(self._timestamp, signal.timestamp_addr)
def signal(self, signal, value:sint=0): return self.cmd(self._signal, signal.value_addr, value)
def _submit(self, dev): dev.tasks.put(self._q[:])
class LVPArgsState(CLikeArgsState):
def __init__(self, buf, prg, bufs, vals=()): super().__init__(buf, prg, bufs, vals, [*data64_le(buf.va_addr + 12), (len(bufs) + len(vals)) * 2])
@@ -89,24 +77,23 @@ class CPUProgram(HCQProgram):
try: rt_lib = ctypes.CDLL(ctypes.util.find_library('System' if OSX else 'kernel32') if OSX or WIN else 'libgcc_s.so.1')
except OSError: pass
def __init__(self, dev, name:str, lib:bytes, runtimevars:dict[str, int]|None=None, native=False, **kwargs):
def __init__(self, dev, name:str, lib:bytes, runtimevars:dict[str, int]|None=None, **kwargs):
self.runtimevars = runtimevars or {}
LVP = isinstance(dev.renderer, LVPRenderer) and not native
LVP = isinstance(dev.renderer, LVPRenderer)
if sys.platform == "win32": # mypy doesn't understand when WIN is used here
PAGE_EXECUTE_READWRITE, MEM_COMMIT, MEM_RESERVE = 0x40, 0x1000, 0x2000
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
self.addr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_void_p(0), ctypes.c_size_t(len(lib)), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
ctypes.memmove(self.addr, lib, len(lib))
self.mem = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_void_p(0), ctypes.c_size_t(len(lib)), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
ctypes.memmove(self.mem, lib, len(lib))
ctypes.windll.kernel32.GetCurrentProcess.restype = ctypes.c_void_p
proc = ctypes.windll.kernel32.GetCurrentProcess()
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.addr), ctypes.c_size_t(len(lib)))
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.mem), ctypes.c_size_t(len(lib)))
self.fxn = ctypes.CFUNCTYPE(None)(self.mem)
else:
# On apple silicon with SPRR enabled (it always is in macos) RWX pages are unrepresentable: https://blog.svenpeter.dev/posts/m1_sprr_gxf/
# MAP_JIT allows us to easily flip pages from RW- to R-X and vice versa. It is a noop on intel cpus. (man pthread_jit_write_protect_np)
self.mem = mmap.mmap(-1, len(lib), mmap.MAP_ANON|mmap.MAP_PRIVATE|(MAP_JIT if OSX else 0), mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC)
self.addr = mv_address(self.mem)
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(False)
if LVP: lib = jit_loader(lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m'])
@@ -117,18 +104,20 @@ class CPUProgram(HCQProgram):
# libgcc_s comes as shared library but compiler-rt is only a bunch of static library archives which we can't directly load, but fortunately
# it somehow found its way into libSystem on macos (likely because it used __builtin_clear_cache) and libgcc_s is ~always present on linux
# Using ["name"] instead of .name because otherwise name is getting mangled: https://docs.python.org/3.12/reference/expressions.html#index-5
if CPUProgram.rt_lib is not None: CPUProgram.rt_lib["__clear_cache"](ctypes.c_void_p(self.addr), ctypes.c_void_p(self.addr + len(lib)))
if CPUProgram.rt_lib is not None:
CPUProgram.rt_lib["__clear_cache"](ctypes.c_void_p(mv_address(self.mem)), ctypes.c_void_p(mv_address(self.mem) + len(lib)))
else:
# msync should be a universal POSIX way to do this
libc.msync(ctypes.c_void_p(self.addr), len(lib), libc.MS_SYNC | libc.MS_INVALIDATE)
from tinygrad.runtime.autogen import libc
libc.msync(ctypes.c_void_p(mv_address(self.mem)), len(lib), libc.MS_SYNC | libc.MS_INVALIDATE)
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
self.fxn = ctypes.CFUNCTYPE(None)(mv_address(self.mem))
super().__init__(LVPArgsState if LVP else HCQArgsState, dev, name, kernargs_alloc_size=12+256 if LVP else 0)
@suppress_finalizing
def __del__(self):
if sys.platform == 'win32': ctypes.windll.kernel32.VirtualFree(ctypes.c_void_p(self.addr), ctypes.c_size_t(0), 0x8000) #0x8000 - MEM_RELEASE
if sys.platform == 'win32': ctypes.windll.kernel32.VirtualFree(ctypes.c_void_p(self.mem), ctypes.c_size_t(0), 0x8000) #0x8000 - MEM_RELEASE
class CPUAllocator(HCQAllocator):
def __init__(self, dev:CPUDevice): super().__init__(dev, supports_copy_from_disk=False, supports_transfer=False)
@@ -137,7 +126,9 @@ class CPUAllocator(HCQAllocator):
elif WIN: addr = mv_address(buf:=mmap.mmap(-1, size, access=mmap.ACCESS_WRITE))
else: addr = mv_address(buf:=mmap.mmap(-1, size, mmap.MAP_ANON | mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE))
return HCQBuffer(va:=addr, sz:=size, meta=buf, view=MMIOInterface(va, sz, fmt='B'), owner=self.dev)
def _as_buffer(self, src) -> memoryview: return to_mv(src.va_addr, src.size)
def _as_buffer(self, src) -> memoryview:
self.dev.synchronize()
return to_mv(src.va_addr, src.size)
def _do_map(self, buf:HCQBuffer):
if buf.view is None or not isinstance(buf.view, MMIOInterface): raise RuntimeError("Cannot map buffer without view to cpu")
return HCQBuffer(buf.view.addr, buf.size, view=buf.view, owner=buf.owner)
@@ -145,38 +136,7 @@ class CPUAllocator(HCQAllocator):
class CPUDevice(HCQCompiled):
def __init__(self, device:str=""):
self.tasks:queue.Queue = queue.Queue()
CPUWorker(self, self.tasks, thread_id=0).start()
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], functools.partial(CPUProgram, self),
HCQSignal, functools.partial(CPUComputeQueue, self), arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
self.ring = self.allocator.alloc(RING_SLOTS * CMD_SIZE * 8, BufferSpec())
self.ring_view, self.ring_pos = self.ring.cpu_view().view(fmt='Q'), 0
# posix uses sem to put cpus into sleep
if WIN:
self.sys = self.allocator.alloc(8, BufferSpec())
self.sys_view, sem_addr = self.sys.cpu_view().view(fmt='Q'), 0
else:
self.sem = libc.sem_open(sem_name:=f"/tinygrad-{os.getpid()}-{id(self):x}".encode(), os.O_CREAT|os.O_EXCL, 0o600, 0) # type: ignore[call-arg]
if (sem_addr:=unwrap(ctypes.cast(self.sem, ctypes.c_void_p).value)) == ctypes.c_void_p(-1).value or libc.sem_unlink(sem_name):
raise OSError(ctypes.get_errno(), "semaphore")
self.sem_buf = HCQBuffer(sem_addr, 1, owner=self)
# TODO: move to hcq2 infra
self.func_table = self.allocator.alloc(32, BufferSpec())
fns = ([0, ctypes.windll.kernel32.ExitThread, 0, 0] if WIN else # type: ignore[attr-defined]
[libc.dll.clock_gettime, libc.dll.pthread_exit, libc.dll.sem_wait, libc.dll.sem_close])
self.func_table.cpu_view().view(fmt='Q')[:] = array.array('Q', [unwrap(ctypes.cast(f, ctypes.c_void_p).value) if f else 0 for f in fns])
# TODO: move to hcq2
with Context(EMULATED_DTYPES="", TRACK_MATCH_STATS=0):
prgs = {f: f().sink(arg=KernelInfo(f.__name__), tag=1) for f in (signal_prog, wait_prog, timestamp_prog, quit_prog, worker_prog)}
self.prgs = {f: self.runtime(f.__name__, do_to_program(v, ClangRenderer(self.renderer.target)).src[3].arg, native=True) for f,v in prgs.items()}
self.worker:threading.Thread|None = threading.Thread(target=self.prgs[worker_prog].fxn, args=(ctypes.c_uint64(self.ring.va_addr),
ctypes.c_uint64(self.sys.va_addr if WIN else self.func_table.va_addr+16), ctypes.c_uint64(sem_addr)), daemon=True)
self.worker.start()
def finalize(self):
if self.worker is None: return
CPUComputeQueue(self)._cmd(quit_prog, (self.func_table.offset(8, 8),) if WIN else (self.func_table.offset(8, 24), self.sem_buf)).submit(self)
self.worker = None
CPUSignal, CPUComputeQueue, arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
+3 -2
View File
@@ -183,9 +183,10 @@ class MetalAllocator(LRUAllocator[MetalDevice]):
# There is no real metal multidevice support for now, so transfer is used only for tests.
src_dev.synchronize()
def _cp_mv(self, dst, src, prof_desc):
self.dev.synchronize()
with cpu_profile(prof_desc, f"{self.dev.device}:COPY"): dst[:] = src
def _as_buffer(self, src:MetalBuffer) -> memoryview: return to_mv(src.buf.contents(), src.size + src.offset)[src.offset:]
def _as_buffer(self, src:MetalBuffer) -> memoryview:
self.dev.synchronize()
return to_mv(src.buf.contents(), src.size + src.offset)[src.offset:]
def _copyin(self, dest:MetalBuffer, src:memoryview): self._cp_mv(self._as_buffer(dest), src, "TINY -> METAL")
def _copyout(self, dest:memoryview, src:MetalBuffer): self._cp_mv(dest, self._as_buffer(src), "METAL -> TINY")
def _offset(self, buf:MetalBuffer, size:int, offset:int): return MetalBuffer(buf.buf, size, offset)
+3 -1
View File
@@ -332,7 +332,9 @@ class QCOMAllocator(HCQAllocatorBase):
def _copyin(self, dest:HCQBuffer, src:memoryview): self._do_copy(mv_address(src), dest.cpu_view().addr, src.nbytes, f"TINY -> {self.dev.device}")
def _copyout(self, dest:memoryview, src:HCQBuffer): self._do_copy(src.cpu_view().addr, mv_address(dest), src.size, f"{self.dev.device} -> TINY")
def _as_buffer(self, src:HCQBuffer) -> memoryview: return to_mv(src.cpu_view().addr, src.size)
def _as_buffer(self, src:HCQBuffer) -> memoryview:
self.dev.synchronize()
return to_mv(src.cpu_view().addr, src.size)
def _do_free(self, opaque, options:BufferSpec): self.dev._gpu_free(opaque)
+29 -3
View File
@@ -1,5 +1,5 @@
import ctypes, hashlib, tempfile, subprocess, pathlib
from tinygrad.helpers import amdgpu_disassemble, getenv
import ctypes, hashlib, tempfile, subprocess, pathlib, shutil
from tinygrad.helpers import system, getenv
from tinygrad.runtime.autogen import comgr
try:
comgr.amd_comgr_get_version(ctypes.byref(major:=ctypes.c_uint64()), ctypes.byref(minor:=ctypes.c_uint64()))
@@ -9,8 +9,21 @@ try:
assert comgr.AMD_COMGR_LANGUAGE_HIP == 3
except AttributeError: pass # ignore if ROCm isn't installed
from tinygrad.device import Compiler, CompileError
from tinygrad.runtime.support.compiler_cpu import LLVMCompiler
from tinygrad.runtime.support import c
from tinygrad.helpers import to_char_p_p
from tinygrad.helpers import OSX, to_char_p_p
def _find_llvm_objdump():
if OSX: return '/opt/homebrew/opt/llvm/bin/llvm-objdump'
# Try ROCm path first, then versioned, then unversioned
for p in ['/opt/rocm/llvm/bin/llvm-objdump', 'llvm-objdump-21', 'llvm-objdump-20', 'llvm-objdump']:
if shutil.which(p): return p
raise FileNotFoundError("llvm-objdump not found")
def amdgpu_disassemble(lib:bytes):
asm = system(f"{_find_llvm_objdump()} -d -", input=lib).splitlines()
while asm and ("s_nop 0" in asm[-1] or "s_code_end" in asm[-1]): asm.pop()
print("\n".join(asm))
def check(status):
if status != 0:
@@ -105,3 +118,16 @@ class HIPCCCompiler(Compiler):
return pathlib.Path(libf.name).read_bytes()
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
class AMDLLVMCompiler(LLVMCompiler):
jit = False
def __init__(self, arch: str):
self.arch = arch
super().__init__("AMDGPU", self.arch, "+cumode")
def __reduce__(self): return (AMDLLVMCompiler, (self.arch,))
def compile(self, src:str) -> bytes:
try: return super().compile(src)
except RuntimeError as e:
if "undefined value '@llvm.amdgcn." in str(e): raise CompileError(str(e) + "AMD with LLVM backend requires LLVM >= 18") from e
raise CompileError(e) from e
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
+78 -2
View File
@@ -1,7 +1,8 @@
import subprocess
import ctypes, subprocess
from tinygrad.device import Compiler
from tinygrad.helpers import getenv, capstone_flatdump
from tinygrad.helpers import getenv, capstone_flatdump, DEBUG, unwrap
from tinygrad.runtime.support.elf import jit_loader
from tinygrad.runtime.autogen import llvm
class ClangCompiler(Compiler):
def __init__(self, arch:list[str], cachekey="compile_clang_jit"):
@@ -26,6 +27,81 @@ class ClangCompiler(Compiler):
def disassemble(self, lib:bytes): return capstone_flatdump(lib, self.arch)
def cerr(): return ctypes.pointer(ctypes.pointer(ctypes.c_char()))
def expect(x, err, ret=None):
if x: raise RuntimeError(unwrap(ctypes.cast(err.contents, ctypes.c_char_p).value).decode() if not isinstance(err, str) else err)
return ret
class LLVMCompiler(Compiler):
jit = True
def __init__(self, arch:str, processor:str, feats:str, cache_key=None):
for component in ['Target', 'TargetInfo', 'TargetMC', 'AsmParser', 'AsmPrinter']:
getattr(llvm, "LLVMInitialize" + {'arm64': 'AArch64', 'x86_64': 'X86', 'riscv64': 'riscv64'}.get(arch, "AMDGPU") + component)()
triple = {'arm64': b'aarch64-none-unknown-elf', 'x86_64': b'x86_64-none-unknown-elf', 'AMDGPU': b'amdgcn-amd-amdhsa'}[arch]
target = expect(llvm.LLVMGetTargetFromTriple(triple, ctypes.pointer(tgt:=llvm.LLVMTargetRef()), err:=cerr()), err, tgt)
if DEBUG >= 3: print(f"LLVM init for {processor!r} with {feats!r}")
self.target_machine = llvm.LLVMCreateTargetMachine(target, triple, processor.encode(), feats.encode(),
llvm.LLVMCodeGenLevelDefault, llvm.LLVMRelocPIC, llvm.LLVMCodeModelDefault)
self.pbo = llvm.LLVMCreatePassBuilderOptions()
if (opt:=bool(getenv("LLVMOPT", "1"))):
self.passes = b'default<O2>'
llvm.LLVMPassBuilderOptionsSetLoopUnrolling(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetLoopVectorization(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetSLPVectorization(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetVerifyEach(self.pbo, True)
else:
self.passes = b'default<O0>'
# Create a per-instance context instead of using the global context to avoid shared state between parallel test processes
self.context = llvm.LLVMContextCreate()
self.diag_msgs: list[str] = []
@llvm.LLVMDiagnosticHandler
def handle_diag(diag_ref, _arg):
severity = llvm.LLVMGetDiagInfoSeverity(diag_ref)
msg = ctypes.string_at(llvm.LLVMGetDiagInfoDescription(diag_ref)).decode()
if severity == llvm.LLVMDSError:
self.diag_msgs.append(msg)
self.handle_diag = handle_diag
llvm.LLVMContextSetDiagnosticHandler(self.context, handle_diag, None)
super().__init__(cache_key or f"compile_llvm_{processor}_{feats}{'_jit' if self.jit else ''}{'_opt' if opt else ''}")
def __del__(self):
if hasattr(self, 'pbo'): llvm.LLVMDisposePassBuilderOptions(self.pbo)
if hasattr(self, 'context'): llvm.LLVMContextDispose(self.context)
def compile_to_obj(self, src:str) -> bytes:
self.diag_msgs.clear()
src_buf = llvm.LLVMCreateMemoryBufferWithMemoryRangeCopy(ctypes.create_string_buffer(src_bytes:=src.encode()), len(src_bytes), b'src')
mod = expect(llvm.LLVMParseIRInContext(self.context, src_buf, ctypes.pointer(m:=llvm.LLVMModuleRef()), err:=cerr()), err, m)
expect(llvm.LLVMVerifyModule(mod, llvm.LLVMReturnStatusAction, err:=cerr()), err)
expect(llvm.LLVMRunPasses(mod, self.passes, self.target_machine, self.pbo), 'failed to run passes')
if DEBUG >= 7: print(ctypes.string_at(llvm.LLVMPrintModuleToString(mod)).decode())
obj_buf = expect(llvm.LLVMTargetMachineEmitToMemoryBuffer(self.target_machine, mod, llvm.LLVMObjectFile, err:=cerr(),
buf:=llvm.LLVMMemoryBufferRef()), err, buf)
llvm.LLVMDisposeModule(mod)
obj = ctypes.string_at(llvm.LLVMGetBufferStart(obj_buf), llvm.LLVMGetBufferSize(obj_buf))
llvm.LLVMDisposeMemoryBuffer(obj_buf)
if self.diag_msgs: raise RuntimeError("llvm diagnostic: " + "\n".join(self.diag_msgs))
return obj
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src)) if self.jit else self.compile_to_obj(src)
class CPULLVMCompiler(LLVMCompiler):
def __init__(self, arch:list[str], cache_key=None):
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
self.arch, cpu, *feats = arch
featstr = ','.join(f if f.startswith('-') else '+'+f for f in feats)
if cpu == "native":
cpu = ctypes.string_at(llvm.LLVMGetHostCPUName()).decode()
featstr = (featstr + "," if featstr else "") + ctypes.string_at(llvm.LLVMGetHostCPUFeatures()).decode()
# +reserve-x18 here does the same thing as -ffixed-x18 in ClangCompiler, see comments there for why it's needed on arm osx
super().__init__(self.arch, cpu, ('+reserve-x18,' if self.arch == "arm64" else '') + featstr, cache_key)
def disassemble(self, lib:bytes): capstone_flatdump(lib, self.arch)
class X86Compiler(Compiler):
def __init__(self): super().__init__(None)
-94
View File
@@ -1,94 +0,0 @@
import ctypes
from tinygrad.device import Compiler, CompileError
from tinygrad.helpers import getenv, capstone_flatdump, amdgpu_disassemble, unwrap, DEBUG
from tinygrad.runtime.support.elf import jit_loader
from tinygrad.runtime.autogen import llvm
def cerr(): return ctypes.pointer(ctypes.pointer(ctypes.c_char()))
def expect(x, err, ret=None):
if x: raise RuntimeError(unwrap(ctypes.cast(err.contents, ctypes.c_char_p).value).decode() if not isinstance(err, str) else err)
return ret
class LLVMCompiler(Compiler):
jit = True
def __init__(self, arch:str, processor:str, feats:str, cache_key=None):
for component in ['Target', 'TargetInfo', 'TargetMC', 'AsmParser', 'AsmPrinter']:
getattr(llvm, "LLVMInitialize" + {'arm64': 'AArch64', 'x86_64': 'X86', 'riscv64': 'riscv64'}.get(arch, "AMDGPU") + component)()
triple = {'arm64': b'aarch64-none-unknown-elf', 'x86_64': b'x86_64-none-unknown-elf', 'AMDGPU': b'amdgcn-amd-amdhsa'}[arch]
target = expect(llvm.LLVMGetTargetFromTriple(triple, ctypes.pointer(tgt:=llvm.LLVMTargetRef()), err:=cerr()), err, tgt)
if DEBUG >= 3: print(f"LLVM init for {processor!r} with {feats!r}")
self.target_machine = llvm.LLVMCreateTargetMachine(target, triple, processor.encode(), feats.encode(),
llvm.LLVMCodeGenLevelDefault, llvm.LLVMRelocPIC, llvm.LLVMCodeModelDefault)
self.pbo = llvm.LLVMCreatePassBuilderOptions()
if (opt:=bool(getenv("LLVMOPT", "1"))):
self.passes = b'default<O2>'
llvm.LLVMPassBuilderOptionsSetLoopUnrolling(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetLoopVectorization(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetSLPVectorization(self.pbo, True)
llvm.LLVMPassBuilderOptionsSetVerifyEach(self.pbo, True)
else:
self.passes = b'default<O0>'
# Create a per-instance context instead of using the global context to avoid shared state between parallel test processes
self.context = llvm.LLVMContextCreate()
self.diag_msgs: list[str] = []
@llvm.LLVMDiagnosticHandler
def handle_diag(diag_ref, _arg):
severity = llvm.LLVMGetDiagInfoSeverity(diag_ref)
msg = ctypes.string_at(llvm.LLVMGetDiagInfoDescription(diag_ref)).decode()
if severity == llvm.LLVMDSError:
self.diag_msgs.append(msg)
self.handle_diag = handle_diag
llvm.LLVMContextSetDiagnosticHandler(self.context, handle_diag, None)
super().__init__(cache_key or f"compile_llvm_{processor}_{feats}{'_jit' if self.jit else ''}{'_opt' if opt else ''}")
def __del__(self):
if hasattr(self, 'pbo'): llvm.LLVMDisposePassBuilderOptions(self.pbo)
if hasattr(self, 'context'): llvm.LLVMContextDispose(self.context)
def compile_to_obj(self, src:str) -> bytes:
self.diag_msgs.clear()
src_buf = llvm.LLVMCreateMemoryBufferWithMemoryRangeCopy(ctypes.create_string_buffer(src_bytes:=src.encode()), len(src_bytes), b'src')
mod = expect(llvm.LLVMParseIRInContext(self.context, src_buf, ctypes.pointer(m:=llvm.LLVMModuleRef()), err:=cerr()), err, m)
expect(llvm.LLVMVerifyModule(mod, llvm.LLVMReturnStatusAction, err:=cerr()), err)
expect(llvm.LLVMRunPasses(mod, self.passes, self.target_machine, self.pbo), 'failed to run passes')
if DEBUG >= 7: print(ctypes.string_at(llvm.LLVMPrintModuleToString(mod)).decode())
obj_buf = expect(llvm.LLVMTargetMachineEmitToMemoryBuffer(self.target_machine, mod, llvm.LLVMObjectFile, err:=cerr(),
buf:=llvm.LLVMMemoryBufferRef()), err, buf)
llvm.LLVMDisposeModule(mod)
obj = ctypes.string_at(llvm.LLVMGetBufferStart(obj_buf), llvm.LLVMGetBufferSize(obj_buf))
llvm.LLVMDisposeMemoryBuffer(obj_buf)
if self.diag_msgs: raise RuntimeError("llvm diagnostic: " + "\n".join(self.diag_msgs))
return obj
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src)) if self.jit else self.compile_to_obj(src)
class CPULLVMCompiler(LLVMCompiler):
def __init__(self, arch:list[str], cache_key=None):
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
self.arch, cpu, *feats = arch
featstr = ','.join(f if f.startswith('-') else '+'+f for f in feats)
if cpu == "native":
cpu = ctypes.string_at(llvm.LLVMGetHostCPUName()).decode()
featstr = (featstr + "," if featstr else "") + ctypes.string_at(llvm.LLVMGetHostCPUFeatures()).decode()
# +reserve-x18 here does the same thing as -ffixed-x18 in ClangCompiler, see comments there for why it's needed on arm osx
super().__init__(self.arch, cpu, ('+reserve-x18,' if self.arch == "arm64" else '') + featstr, cache_key)
def disassemble(self, lib:bytes): capstone_flatdump(lib, self.arch)
class AMDLLVMCompiler(LLVMCompiler):
jit = False
def __init__(self, arch: str):
self.arch = arch
super().__init__("AMDGPU", self.arch, "+cumode")
def __reduce__(self): return (AMDLLVMCompiler, (self.arch,))
def compile(self, src:str) -> bytes:
try: return super().compile(src)
except RuntimeError as e:
if "undefined value '@llvm.amdgcn." in str(e): raise CompileError(str(e) + "AMD with LLVM backend requires LLVM >= 18") from e
raise CompileError(e) from e
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
+1 -1
View File
@@ -2,7 +2,7 @@ import base64, ctypes, pathlib, tempfile, hashlib
from tinygrad.device import Compiler
from tinygrad.helpers import cpu_objdump, system, data64
from tinygrad.runtime.autogen import mesa, llvm, libc
from tinygrad.runtime.support.compiler_llvm import CPULLVMCompiler, expect, cerr
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler, expect, cerr
# NB: compilers assume mesa's glsl type cache is managed externally with mesa.glsl_type_singleton_init_or_ref() and mesa.glsl_type_singleton_decref()
+2 -2
View File
@@ -60,9 +60,9 @@ pm_mops = PatternMatcher([
# 0. do some cleanup rewrites, mostly copied from the old stuff
def fix_store_hazard(target:UOp, src:UOp):
if (base:=target.base) not in src.backward_slice_with_self: return None
# PERMUTE and FLIP reorder indices, SHRINK can have overlapping regions when dest is also shrunk
unsafe = {Ops.PERMUTE, Ops.FLIP} | ({Ops.SHRINK} if target.op_in_backward_slice_with_self(Ops.SHRINK) else set())
base = target.base
reaches_base: dict[UOp, bool] = {}
for s in src.toposort(gate=lambda s: s.op is not Ops.CONTIGUOUS):
reaches_base[s] = s is base or any(reaches_base.get(c) for c in s.src)
@@ -323,7 +323,7 @@ pm_remove_bufferize = PatternMatcher([
(UPat(Ops.END, src=(UPat(Ops.NOOP, name="x"),), allow_any_len=True), lambda x: x),
])
DEVICE_MAX_BUFS = {"METAL": 31, "WEBGPU": 8, "CPU": 31} # TODO: get from device?
DEVICE_MAX_BUFS = {"METAL": 31, "WEBGPU": 8} # TODO: get from device?
def limit_bufs(ctx:IndexingContext, root:UOp):
if (device:=root.device) is None: return None # no device, index related calculations
device = device if isinstance(device, str) else device[0].split(":")[0]
+26 -3
View File
@@ -6,7 +6,7 @@ if TYPE_CHECKING: import numpy
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtype, to_dtype, strong_dtype, _from_np_dtype, _to_np_dtype, PyConst
from tinygrad.helpers import all_int, getenv, fully_flatten, fetch, Metadata, TRACEMETA, TracingKey
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable, ConstLike
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable
from tinygrad.mixin.rand import RandMixin
from tinygrad.schedule import create_linear_with_vars
from tinygrad.device import Buffer, canonicalize_device
@@ -125,7 +125,7 @@ class Tensor(RandMixin):
@classmethod
def _wrap_uop(cls, u:UOp) -> Tensor: return cls(u)
@staticmethod
def const(dtype:DType, b:ConstLike) -> Tensor: return Tensor(UOp.const(dtype, b))
def const(dtype:DType, b:ConstType|UOp) -> Tensor: return Tensor(UOp.const(dtype, b))
def is_param_(self, is_param:bool=True) -> Tensor:
self.is_param = is_param
@@ -537,7 +537,30 @@ _METADATA: _ContextVar[Metadata|None] = _ContextVar(default=None)
def _metadata_wrapper(fn: Callable[P, T]) -> Callable[P, T]:
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
if TRACEMETA < 1 or _METADATA.get() is not None: return fn(*args, **kwargs)
token = _METADATA.set(Metadata(name=fn.__name__))
if TRACEMETA >= 2:
caller_frame = sys._getframe(frame := 1)
caller_module = caller_frame.f_globals.get("__name__", None)
caller_func = caller_frame.f_code.co_name
if caller_module is None: return fn(*args, **kwargs)
# if its called from nn we want to step up frames until we are out of nn
while caller_module.startswith("tinygrad.nn") and "optim" not in caller_module:
caller_frame = sys._getframe(frame := frame + 1)
caller_module = caller_frame.f_globals.get("__name__", None)
if caller_module is None: return fn(*args, **kwargs)
# if its called from a lambda in tinygrad we want to look two more frames up
if caller_module.startswith("tinygrad") and caller_func == "<lambda>": caller_frame = sys._getframe(frame := frame + 2)
caller_module = caller_frame.f_globals.get("__name__", None)
if caller_module is None: return fn(*args, **kwargs)
caller_func = caller_frame.f_code.co_name
caller_lineno = caller_frame.f_lineno
caller = f"{caller_module}:{caller_lineno}::{caller_func}"
else: caller = ""
token = _METADATA.set(Metadata(name=fn.__name__, caller=caller))
with cpu_profile(TracingKey(fn.__name__), "USER"):
ret = fn(*args, **kwargs)
_METADATA.set(token)
+3 -4
View File
@@ -99,10 +99,9 @@ div_and_mod_symbolic = PatternMatcher([
# ** 1. Fast Inline Rules **
# (x//c+a)//d -> (x+a*c)//(c*d) for c>0, d>0
((UPat.var("x")//UPat.cvar("c") + UPat.cvar("a"))//UPat.cvar("d"), lambda x,c,a,d: (x+a*c)//(c*d) if d.vmin>0 else None),
# (x+c)//d -> (x+c%d)//d + c//d ; (x+c)%d -> (x+c%d)%d (split the multiple of d out of the const, holds for any d!=0)
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), src=(UPat.var("x", dtypes.weakint)+UPat.cvar("c"), UPat.cvar("d")), name="n"),
lambda n,x,c,d: None if d.arg==0 or c.arg%d.arg==c.arg else
(x+c.arg%d.arg)//d + c.arg//d.arg if n.op is Ops.FLOORDIV else (x+c.arg%d.arg)%d),
# (x+c)//d -> (x+c%d)//d + c//d for d>0 (split out the multiple of d in the constant)
((UPat.var("x", dtypes.weakint)+UPat.cvar("c"))//UPat.cvar("d"),
lambda x,c,d: (x+c.arg%d.arg)//d + c.arg//d.arg if c.arg%d.arg!=c.arg and d.arg>0 else None),
# ** 2. Slow Rules **
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), dtypes.weakint, name="d"), lambda d: fold_divmod_general(d)),
+29 -54
View File
@@ -29,10 +29,8 @@ class ParamArg:
addrspace: AddrSpace|None = AddrSpace.GLOBAL
axis: int|None = None
device: str|tuple[str, ...]|None = None
volatile: bool = False
def __repr__(self):
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None),
("volatile", False))
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None))
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
return f"ParamArg({', '.join(args)})"
axis_letters = {AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L", AxisType.UPCAST: "u",
@@ -113,14 +111,11 @@ def promo_dtype(src:tuple[UOp,...]) -> DType:
def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
# here are the dtype production rules, eventually this will go in UOp as a recursive property
match op:
case Ops.STORE | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
case Ops.STORE | Ops.CALL | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | \
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.REWRITE_ERROR:
# always void
return dtypes.void
case Ops.CALL:
# a CALL of an opaque body is void, a CALL of an address can return a value
return dtypes.void if src[0].dtype is dtypes.void else None
case Ops.CUSTOM | Ops.CUSTOMI | Ops.INS | Ops.PYLITERAL:
return dtypes.void
case Ops.NOOP:
@@ -154,7 +149,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
case Ops.GETADDR:
return dtypes.uint64
case Ops.SHL | Ops.SHR:
if not dtypes.is_int(src[1].dtype): raise RuntimeError(f"shift distance must be int, got {src[1].dtype}")
assert dtypes.is_int(src[1].dtype), "shift distance must be int"
return src[0].dtype
case Ops.BUFFER | Ops.PARAM:
assert isinstance(arg, ParamArg), "BUFFER/PARAM must have ParamArg"
@@ -274,12 +269,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# Check self first, then iterate backward_slice (avoids creating intermediate dict)
return self.op in ops or any(x.op in ops for x in self.backward_slice)
@recursive_property
def _bool_slice(self) -> frozenset[UOp]: return frozenset().union(*[s.bool_slice for s in self.src])
# NOTE: self is added outside the cache, a cached self-reference is a cycle the refcounter can't free
@property
def bool_slice(self) -> frozenset[UOp]: return self._bool_slice | {self} if self.dtype is dtypes.bool else self._bool_slice
def toposort(self, gate:Callable|None=None, enter_calls=True) -> dict[UOp, None]:
cache: dict[UOp, None] = {}
stack: list[tuple[UOp, bool]] = [(self, False)] # each stack entry is (node, visited_flag)
@@ -317,13 +306,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
match self.op:
# late ops don't have shape
case Ops.IF | Ops.BARRIER | Ops.SINK | Ops.REWRITE_ERROR | Ops.ENDIF | Ops.GROUP | \
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.TUPLE | Ops.FUNCTION:
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.TUPLE | Ops.CALL | Ops.FUNCTION:
return None
# a void CALL has no shape, the return value of a CALL has the shape of its dtype
case Ops.CALL:
return None if self.dtype is dtypes.void else ()
# INS shape is always scalar, vector width is in the instruction encoding
case Ops.INS:
if self.dtype is dtypes.void: return None
@@ -475,8 +460,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
@functools.cached_property
def ended_ranges(self) -> tuple[UOp, ...]:
# an END only ends ranges, the loop backedge condition is not an ended range
if self.op is Ops.END: return tuple(x for x in self.src[1:] if x.op is Ops.RANGE)
if self.op in range_start: return self.src[range_start[self.op]:]
if self.op is Ops.AFTER: return tuple(flatten([x.ended_ranges for x in self.src[1:]]))
return ()
@@ -1106,12 +1089,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# TODO: this should replace placeholder
@staticmethod
def param(slot:int, dtype:DType, shape:tuple[sint, ...]|None=None, device=None, vmin_vmax:tuple[PyConst, PyConst]|None=None,
multiple_of:int|None=None, name=None, addrspace=AddrSpace.GLOBAL, axis:int|None=None, volatile:bool=False):
multiple_of:int|None=None, name=None, addrspace=AddrSpace.GLOBAL, axis:int|None=None):
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create param for weak dtype {dtype}")
if shape is not None and axis is not None and isinstance(device, tuple):
shape = tuple(s*len(device) if i == axis else s for i,s in enumerate(shape))
src: tuple[UOp, ...] = (UOp(Ops.NOOP) if shape is None else shape_to_shape_arg(shape),)
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, multiple_of, name, addrspace, axis, device, volatile))
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, multiple_of, name, addrspace, axis, device))
def param_like(self, slot:int):
addrspace = self.addrspace if self.addrspace is not None else AddrSpace.GLOBAL
if self.op is Ops.BIND: return self.src[0].replace(arg=replace(self.src[0].arg, slot=slot, addrspace=addrspace))
@@ -1122,9 +1105,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# opaque bodies stay as Ops.CALL; value-producing bodies become Ops.FUNCTION (wrapped in TUPLE)
_OPAQUE_CALL_BODIES = {Ops.SINK, Ops.PROGRAM, Ops.LINEAR, Ops.COPY, Ops.SLICE, Ops.CUSTOM_FUNCTION}
def call(self, *srcs:UOp, ret_dtype:DType|None=None, grad_fxn:Callable|None=None,
def call(self, *srcs:UOp, grad_fxn:Callable|None=None,
name:str|None=None, precompile:bool=False, precompile_backward:bool=False, aux:Any=None) -> UOp:
if ret_dtype is not None: return UOp(Ops.CALL, ret_dtype, src=(self,)+srcs)
assert len(self.ranges) == 0, f"ranges {self.ranges} are leaking out of the call in {self.pyrender()}"
if self.op in UOp._OPAQUE_CALL_BODIES:
return UOp(Ops.CALL, src=(self,)+srcs, arg=CallInfo(grad_fxn, name, precompile, precompile_backward, aux))
@@ -1292,6 +1274,9 @@ class UPat(OpMixin):
@property
def dtype(self) -> DType: return self.match_dtype[0] if self.match_dtype is not None else dtypes.void
def _check_dtype(self) -> None: pass
def _ensure_float(self) -> UPat: return self
def __reduce__(self):
return UPat, (self.op, self.match_dtype, self._in_src, self.arg, self.name, not self.strict_length, self.custom_early_reject, self.location,
self.is_any, self.match_tag)
@@ -1604,8 +1589,7 @@ class RewriteContext:
continue
# no rewrite, process children then come back to rebuild
stack.append((n, True))
if not self.enter_calls and (n.op is Ops.FUNCTION or (n.op is Ops.CALL and n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
self.replace[n.src[0]] = n.src[0]
if not self.enter_calls and n.op in {Ops.CALL, Ops.FUNCTION}: self.replace[n.src[0]] = n.src[0]
for x in reversed(n.src):
if x not in self.replace: stack.append((x, False))
else:
@@ -1645,9 +1629,7 @@ class RewriteContext:
# NOTE: CALL/FUNCTION are handled as a special case.
# The function that is called is not included in the graph_rewrite.
# If you want to graph_rewrite a call, you can
# A CALL of an address is not a body, its srcs are regular dataflow
if not self.enter_calls and (new_n.op is Ops.FUNCTION or (new_n.op is Ops.CALL and new_n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
self.replace[new_n.src[0]] = new_n.src[0]
if not self.enter_calls and new_n.op in {Ops.CALL, Ops.FUNCTION}: self.replace[new_n.src[0]] = new_n.src[0]
for x in reversed(new_n.src):
if x in on_stack: continue
stack.append((x, 0, x))
@@ -1698,7 +1680,7 @@ def select_dtype(u:UOp):
def lower_alu_dtype(u:UOp, x:UOp, y:UOp, dt:DType) -> UOp:
src = u.src[:-2]+(x.cast(dt), y.cast(dt))
return src[0].alu(u.op, *src[1:]).cast(u.dtype)
pm_lower_weakint = PatternMatcher([
pm_lower_index_dtype = PatternMatcher([
# There are no Unary ops at this point in symbolic, those are introduced later
(UPat(Ops.CONST, dtype=dtypes.weakint, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype) if u.arg!=Invalid else None),
# Binary can widen the dtype, WHERE cannot
@@ -1706,9 +1688,6 @@ pm_lower_weakint = PatternMatcher([
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(select_dtype(u), x.dtype, y.dtype))),
(UPat(Ops.WHERE, dtypes.weakint, src=(UPat(), UPat.var("x").cast(dtypes.weakint), UPat.var("y").cast(dtypes.weakint)), name="u"),
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(x.dtype, y.dtype))),
# in a weakint WHERE, an Invalid branch takes the dtype of the other branch
(UPat.var("gate").where(UPat.var("idx", dtypes.ints).cast(dtypes.weakint), UPat(Ops.CONST, arg=Invalid)),
lambda gate,idx: idx.valid(gate).cast(dtypes.weakint)),
(UPat(Ops.RANGE, src=(UPat.var("end").cast(dtypes.weakint)), name="r"), lambda r,end: r.replace(dtype=end.dtype, src=(end,)).cast(dtypes.weakint)),
(UPat(Ops.STACK, src=UPat().cast(dtypes.weakint), name="v"),
lambda v: v.replace(dtype=(dt:=select_dtype(v)), src=tuple(s.src[0].cast(dt) for s in v.src)).cast(dtypes.weakint)),
@@ -1719,26 +1698,22 @@ pm_lower_weakint = PatternMatcher([
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.weakint), UPat.cvar("val").cast(dtypes.weakint))),
lambda var,val: var.bind(val).cast(dtypes.weakint)),
])
def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
if ctx is None: ctx = {}
def lower(s:UOp) -> UOp:
if (r:=ctx.get(s)) is None:
r = graph_rewrite(s, pm_lower_weakint)
# the consumer absorbs the cast on its own edge
ctx[s] = r = r.src[0] if r.op is Ops.CAST and r.dtype == dtypes.weakint else r
return r
# a comparison demands a common operand width: lower it whole so the Binary rule unifies its operands
ret = lower(u) if u.op in GroupOp.Comparison else u.replace(src=tuple(lower(s) if s.dtype == dtypes.weakint else s for s in u.src))
return None if ret is u else ret
pm_lower_index_dtype = PatternMatcher([
(UPat(GroupOp.All, name="u"),
lambda ctx,u: lower_weak_srcs(ctx, u) if u.dtype != dtypes.weakint and any(s.dtype == dtypes.weakint for s in u.src) else None),
# a valid index into an n-element buffer lives in [0,n): a gated long index narrows when n-1 fits int32 (out-of-gate wraps, discarded)
# TODO: more generic
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"), UPat.var("gate").where(UPat.var("idx", dtypes.long), UPat(Ops.CONST, arg=Invalid))),
allow_any_len=True, name="u"),
lambda u,buf,gate,idx: u.replace(src=(buf, idx.cast(dtypes.int).valid(gate))+u.src[2:]) if buf.max_numel()-1 <= dtypes.int32.max else None),
# remove hanging casts
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast()),), lambda buf,idx: buf.index(idx)),
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast(), UPat.var("slen", dtypes.ints).cast(),), name="shrink"),
lambda shrink,buf,idx,slen: shrink.replace(src=(buf,idx,slen))),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("gate").where(UPat.var("idx", dtypes.ints).cast(), UPat(Ops.CONST, arg=Invalid)))),
lambda buf,idx,gate: buf.index(idx.valid(gate))),
# remove hanging casts for images
(UPat(Ops.PARAM, src=(UPat.var("shape").cast(),), name="p"), lambda p,shape: p.replace(src=(shape,))),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx_y", dtypes.ints).cast(), UPat.var("idx_x", dtypes.ints).cast()),),
lambda buf,idx_x,idx_y: buf.index(idx_y, idx_x, dtype=dtypes.float)),
(UPat(Ops.INDEX, src=(UPat.var("buf"),
UPat.var("gate").where(UPat.var("idx_y", dtypes.ints).cast(), UPat(Ops.CONST, arg=Invalid)),
UPat.var("gate").where(UPat.var("idx_x", dtypes.ints).cast(), UPat(Ops.CONST, arg=Invalid)))),
lambda buf,idx_x,idx_y,gate: buf.index(idx_y.valid(gate), idx_x.valid(gate), dtype=dtypes.float)),
(UPat((Ops.SINK, Ops.NOOP, Ops.END), name="n"),
lambda n: n.replace(src=tuple(s.src[0] if s.op is Ops.CAST and s.dtype == dtypes.weakint else s for s in n.src))),
])
def _index_to_concrete_int(u:UOp) -> UOp: return graph_rewrite(u.sink(), pm_lower_index_dtype).src[0]
+1 -1
View File
@@ -144,7 +144,7 @@ def pyrender(ast:UOp) -> str:
for s in u.src: to_render.add(s)
if u.op is Ops.STORE: to_render.add(u.src[1])
if u.op is Ops.REDUCE: to_render.add(u.src[0])
if u.op is Ops.FUNCTION or (u.op is Ops.CALL and u.src[0].dtype is dtypes.void): raise NotImplementedError("call can't be pyrendered")
if u.op in {Ops.CALL, Ops.FUNCTION}: raise NotImplementedError("call can't be pyrendered")
if u.op in not_rendered: continue
# checking the consumers is not enough, you have to make sure it's not used twice by the one consumer
if len(cmap[u]) == 1 and len([x for x in list(cmap[u].keys())[0].src if x is u]) == 1 and u.op not in always_rendered: continue
-5
View File
@@ -67,7 +67,6 @@ spec_shared = PatternMatcher([
lambda w: all(s.dtype == w.dtype or s.dtype in dtypes.weaks for s in w.src[1:])),
(UPat(GroupOp.Comparison, dtype=dtypes.bool, src=(UPat.var("x"), UPat.var("y"))),
lambda x,y: x.dtype == y.dtype or x.dtype in dtypes.weaks or y.dtype in dtypes.weaks),
(UPat((Ops.AND, Ops.OR, Ops.XOR, Ops.SHL, Ops.SHR), name="x"), lambda x: False if any(dtypes.is_float(s.dtype) for s in x.src) else None),
(UPat((Ops.SHL, Ops.SHR), src=(UPat.var("x"), UPat(dtype=dtypes.uint)), name="a"), lambda a,x: a.dtype == x.dtype or None),
(UPat((Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD), name="x"), lambda x: None if dtypes.is_int(x.dtype) else False),
(UPat(GroupOp.ALU, name="x"), lambda x: all(y.dtype == x.dtype or y.dtype in dtypes.weaks for y in x.src)),
@@ -101,10 +100,6 @@ spec_shared = PatternMatcher([
# CUSTOM (inline and non inline)
(UPat((Ops.CUSTOMI, Ops.CUSTOM)), lambda: True),
# CALL of an external function
(UPat(Ops.CALL, src=(UPat(),), allow_any_len=True, name="x"),
lambda x: x.src[0].dtype is dtypes.uint64 if x.src[0].dtype is not dtypes.void else None),
# pattern compiler IR ops (not in tensor/program graphs, but spec-compliant)
(UPat(Ops.PYLITERAL), lambda: True),
+34 -29
View File
@@ -64,14 +64,22 @@ def fold_add_divmod_recombine(x:UOp) -> UOp|None:
return ((b % (div*d))*mul).usum(*rest)
return None
# Invalid poisons the value: ops move inside the gate so the Invalid reaches the LOAD/STORE and folds there.
# an invalid index is cond.where(idx, Invalid) in index. the consumer reads cond back off the WHERE with UOp.get_valid,
# so casts and comparisons of a gated index can drop the gate: when the index is invalid the result is never used
invalid_idx_gate = UPat().where(UPat.var("x"), UPat(Ops.CONST, dtypes.weakint, arg=Invalid))
pm_index_invalid = PatternMatcher([
(invalid_idx_gate.cast(name="cast"), lambda x,cast: x.cast(cast.dtype)),
(UPat(GroupOp.Comparison, src=(invalid_idx_gate, UPat.var("y")), name="alu"), lambda x,y,alu: x.alu(alu.op,y)),
(UPat(GroupOp.Comparison, src=(UPat.var("y"), invalid_idx_gate), name="alu"), lambda x,y,alu: y.alu(alu.op,x)),
])
# everywhere else Invalid poisons the value: ops move inside the gate so the Invalid reaches the LOAD/STORE and folds there.
# this needs to be before symbolic so that 0*something_that_might_be_invalid doesnt become 0
invalid_pat = UPat(Ops.CONST, arg=Invalid, name="i")
invalid_gate = UPat.var("cond").where(UPat.var("x"), invalid_pat)
pm_data_invalid = PatternMatcher([
(UPat(GroupOp.Unary|{Ops.BITCAST}, src=(invalid_pat,), name="op"), lambda i,op: i.cast(op.dtype)),
(UPat(GroupOp.Unary|{Ops.CAST, Ops.BITCAST}, src=(invalid_gate,), name="op"),
lambda cond,x,op,i: cond.where(op.replace(src=(x,)), i.cast(op.dtype))),
(UPat(GroupOp.Unary|{Ops.BITCAST}, src=(invalid_gate,), name="op"), lambda cond,x,op,i: cond.where(op.replace(src=(x,)), i.cast(op.dtype))),
# binary ops move inside the gate, with Invalid cast to the result dtype (bool for comparisons)
(UPat(GroupOp.Binary, src=(invalid_gate, UPat.var("y")), name="alu"), lambda cond,x,y,alu,i: cond.where(x.alu(alu.op,y), i.cast(alu.dtype))),
(UPat(GroupOp.Binary, src=(UPat.var("y"), invalid_gate), name="alu"), lambda cond,x,y,alu,i: cond.where(y.alu(alu.op,x), i.cast(alu.dtype))),
@@ -82,8 +90,9 @@ pm_data_invalid = PatternMatcher([
# normalize where(cond, Invalid, val) -> where(~cond, val, Invalid)
(UPat.var("cond").where(invalid_pat, UPat.var("val")), lambda cond, i, val: cond.logical_not().where(val, i) if val.arg != Invalid else i),
# lift Invalid out: a.where(cond.where(x, Invalid), c) -> (~a|cond).where(a.where(x, c), Invalid)
# when a is cond, ~a|cond is True and would drop the Invalid gate (losing the valid), so keep cond as the gate
(UPat.var("a").where(invalid_gate, UPat.var("c")), lambda cond,i,x,a,c:
(a.logical_not()|cond).where(a.where(x,c), i) if c.arg != Invalid else None),
(cond if a is cond else (a.logical_not()|cond)).where(a.where(x,c), i) if c.arg != Invalid else None),
(UPat.var("a").where(UPat.var("b"), invalid_gate), lambda cond,i,x,a,b: (a|cond).where(a.where(b, x), i) if b.arg != Invalid else None),
# fold gated LOAD/STORE
(UPat(Ops.STORE, src=(UPat(Ops.INDEX, src=(UPat(), invalid_pat), allow_any_len=True).or_casted(), UPat())), lambda i: UOp(Ops.NOOP)),
@@ -91,11 +100,13 @@ pm_data_invalid = PatternMatcher([
lambda x,i: x.src[1] if len(x.src) > 1 else x.const_like(0)),
])
propagate_invalid = pm_index_invalid + pm_data_invalid
pm_remove_invalid = PatternMatcher([
(invalid_pat, lambda i: i.const_like(0)),
])
symbolic_simple = pm_data_invalid + PatternMatcher([
symbolic_simple = propagate_invalid + PatternMatcher([
# ** self folding **
(UPat.var("x") + 0, lambda x: x), # x+0 -> x
(UPat.var("x") * 1, lambda x: x), # x*1 -> x
@@ -109,7 +120,6 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
(UPat(Ops.ADD, dtype=dtypes.weakint, name="x"), fold_add_divmod_recombine),
(UPat.var("x", dtype=dtypes.bool) & UPat.cvar("c"), lambda x,c: x if c.arg else c),
(UPat.var("x", dtype=dtypes.bool) | UPat.cvar("c"), lambda x,c: c if c.arg else x),
(UPat.var("x", dtype=dtypes.bool) != UPat.const(dtypes.bool, False), lambda x: x), # x != False -> x
(UPat(GroupOp.Idempotent, src=(UPat.var("x"), UPat.var("x"))), lambda x: x),
(UPat.var("x", dtype=dtypes.bool).logical_not().logical_not(), lambda x: x),
(UPat.var("x", dtype=dtypes.bool).where(UPat.const(dtypes.bool, True), UPat.const(dtypes.bool, False)), lambda x: x),
@@ -157,8 +167,6 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
(UPat(Ops.BITCAST, name="root", src=(UPat.cvar("c"),)), fold_bitcast),
# b.cast(a).cast(b) -> b if a preserves all values in b
(UPat.var('x').cast(name="a").cast(name="b"), lambda x,a,b: x if x.dtype == b.dtype and can_lossless_cast(b.dtype, a.dtype) else None),
# bitcast twice
(UPat(Ops.BITCAST, name="b", src=(UPat.var('x').bitcast(),)), lambda x,b: x.bitcast(b.dtype)),
(UPat.var("x").cast(dtypes.bool), lambda x: x != 0),
# ** pow **
(UPat.var("x").alu(Ops.POW, UPat.cvar("c")), simplify_pow),
@@ -177,8 +185,6 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
(UPat.cvar("gate").where(UPat.var("c0"), UPat.var("c1")), lambda gate, c0, c1: c0 if gate.arg else c1),
# a.where(b.where(c, d), d) -> (a & b).where(c, d)
(UPat.var("a").where(UPat.var("b").where(UPat.var("c"), UPat.var("d")), UPat.var("d")), lambda a,b,c,d: (a&b).where(c,d)),
# a.where(c, b.where(c, d)) -> (a | b).where(c, d)
(UPat.var("a").where(UPat.var("c"), UPat.var("b").where(UPat.var("c"), UPat.var("d"))), lambda a,b,c,d: (a|b).where(c,d)),
])+mop_cleanup
# ******** phase 2 builds on phase 1, it includes the old "symbolic", rules that match deeper ********
@@ -209,13 +215,6 @@ commutative = PatternMatcher([
x.replace(src=x.src[::-1]) if x.src[1].tuplize < x.src[0].tuplize and not x.src[0].tuplize < x.src[1].tuplize else None),
])
def fold_where_closure(cond:UOp, t:UOp, f:UOp) -> UOp|None:
"""in cond.where(t, f), cond is True within t and False within f"""
if cond not in t.bool_slice and cond not in f.bool_slice: return None
# INDEX gates are owned by the valid/store-coalescing machinery, leave them alone
if any(u.op_in_backward_slice_with_self(Ops.INDEX) for u in (cond, t, f)): return None
return cond.where(t.substitute({cond: cond.const_like(True)}), f.substitute({cond: cond.const_like(False)}))
symbolic = symbolic_simple+commutative+PatternMatcher([
# ** boolean algebra **
# TODO: make a more general or folder like simplify_valid
@@ -234,16 +233,12 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
# ** where folding **
(UPat.var("cond", dtype=dtypes.bool).logical_not().where(UPat.var("t"), UPat.var("f")),
lambda cond, t, f: cond.where(f,t) if f.arg is not Invalid else None),
# in cond.where(t, f), uses of cond fold to True within t and False within f
(UPat.var("cond", dtype=dtypes.bool).where(UPat.var("t"), UPat.var("f")), fold_where_closure),
# alu of two where with same conds can combine, only do if true branch or false branch is const
(UPat(GroupOp.Binary, name="alu", src=(UPat.var("c").where(UPat.var("t"), UPat.var("f")), UPat.var("c").where(UPat.var("tt"), UPat.var("ff")))), \
lambda alu,c,t,tt,f,ff: c.where(t.alu(alu.op, tt), f.alu(alu.op, ff)) if t.op == tt.op == Ops.CONST or f.op == ff.op == Ops.CONST else None),
# if its a plus we add the associative variation too
((UPat.var("y")+UPat.var("c").where(UPat.var("t"), UPat.var("f"))) + UPat.var("c").where(UPat.var("tt"), UPat.var("ff")), \
lambda y,c,t,tt,f,ff: y+c.where(t+tt, f+ff) if t.op == tt.op == Ops.CONST or f.op == ff.op == Ops.CONST else None),
# complementary zero branches under the same condition select directly
(UPat.var("c").where(UPat.var("t"), 0) + UPat.var("c").where(0, UPat.var("f")), lambda c,t,f: c.where(t, f)),
# ALU/variable min==max -> CONST
(UPat({Ops.CMPLT, Ops.CMPNE, Ops.FLOORDIV, Ops.FLOORMOD, Ops.PARAM, Ops.BIND, Ops.SPECIAL}, name="x"),
lambda x: x.const_like(x.vmin) if x.vmin == x.vmax else None),
@@ -265,12 +260,12 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
# c0*x<c1 for negative int c0 and non-positive c1
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
lambda x,c0,c1: (-x)<(-(math.floor(-c1.arg/-c0.arg))) if c0.arg < 0 and c0.arg != -1 and c1.arg <= 0 else None),
# x//d<c -> x<c*d for d>0, and -> c*d<x for d<0
# x//d<c -> x<c*d for d>0
((UPat.var("x", dtype=dtypes.weakint)//UPat.cvar("d"))<UPat.cvar("c"),
lambda x,d,c: (x<c.arg*d.arg) if d.arg > 0 else (x>c.arg*d.arg) if d.arg < 0 else None),
lambda x,d,c: x<(c.arg*d.arg) if d.arg > 0 else None),
# ** move add/mul consts to end (NOTE: this is still happening before constant folding) **
((UPat.var("x") + UPat.cvar("c1")) + UPat.var("y"), lambda x,c1,y: (x+y)+c1 if y.op is not Ops.CONST else None),
((UPat.var("x") * UPat.cvar("c1")) * UPat.var("y"), lambda x,c1,y: (x*y)*c1 if y.op is not Ops.CONST else None),
((UPat.var("x") + UPat.cvar("c1")) + UPat.var("y"), lambda x,c1,y: (x+y)+c1),
((UPat.var("x") * UPat.cvar("c1")) * UPat.var("y"), lambda x,c1,y: (x*y)*c1),
# *** rules from symbolic ***
# generic lt folding
(UPat.var("x", dtypes.weakint)<UPat.cvar("c"), lambda x,c: lt_folding(x, c.arg) if 0 < c.arg else None),
@@ -307,8 +302,6 @@ def parse_valid(v:UOp) -> tuple[UOp, bool, int]|None:
# (X < c).ne(True) -> X >= c
return s0.src[0], False, int(s0.src[1].vmin)
if v.op is Ops.CMPLT and dtypes.is_int(v.src[0].dtype):
# c < X -> X >= c+1 (a const on the left is a lower bound on the right)
if v.src[0].op is Ops.CONST: return v.src[1], False, int(v.src[0].arg)+1
# X < c -> X <= c-1
return v.src[0], True, int((v.src[1]).vmax)-1
return None
@@ -339,8 +332,9 @@ def uop_given_valid(valid:UOp, uop:UOp, try_simplex=True) -> UOp:
for candidate in candidates:
# if every branch in candidate gives the same simplified uop, we can rewrite the uop
if any(X not in uop.backward_slice_with_self for X,_ in candidate): continue # skip if a branch var isn't in uop
newuops = [uop.substitute({X:newX}).simplify().substitute({newX:X}).simplify() for X,newX in candidate]
newuops = [uop.substitute({X:newX}) for X,newX in candidate]
if any(u is uop for u in newuops): continue # if any branch doesnt appear in uop, skip
newuops = [u.simplify().substitute({newX:X}).simplify() for (X,newX),u in zip(candidate,newuops)]
if all_same(newuops): uop = newuops[0]
elif uop.op is Ops.STACK and len(uop.src) == 2:
if all_same([uops.src[0] for uops in newuops]): uop = uop.replace(src=(newuops[0].src[0], uop.src[1]))
@@ -410,6 +404,15 @@ def gated_given_valid(cond:UOp, x:UOp, i:UOp) -> UOp|None:
if IMAGE.value > 0 and x.op_in_backward_slice_with_self(Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD): return None
return cond.where(uop_given_valid(cond, x, try_simplex=False), i)
# TODO: this is O(number of WHERE * number of node)
# def fold_where_closure(cond:UOp, t:UOp, f:UOp) -> UOp|None:
# """In cond.where(t, f), fold nested cond.where(a, b) -> a in t, -> b in f"""
# def is_valid_where(u:UOp) -> bool: return u.op is Ops.WHERE and u.src[0] is cond and Invalid not in (u.src[1].arg, u.src[2].arg)
# t_subs, f_subs = {u: u.src[1] for u in t.toposort() if is_valid_where(u)}, {u: u.src[2] for u in f.toposort() if is_valid_where(u)}
# if not t_subs and not f_subs: return None
# new_t, new_f = t.substitute(t_subs).simplify() if t_subs else t, f.substitute(f_subs).simplify() if f_subs else f
# return None if new_t is t and new_f is f else cond.where(new_t, new_f)
pm_simplify_valid = PatternMatcher([
# simplify valid
(UPat(Ops.AND, name="valid"), simplify_valid),
@@ -431,6 +434,8 @@ sym = symbolic+pm_simplify_valid+PatternMatcher([
(UPat(GroupOp.ALU, src=(UPat(Ops.STACK, src=UPat(name='x')), UPat(Ops.STACK, src=UPat(name='y'))), name='alu'),
lambda x,y,alu: UOp(Ops.STACK, src=(UOp(alu.op, src=(x,y)),))),
# ** where **
# # fold nested where with same condition: in cond.where(t,f), cond.where(a,b)->a in t, ->b in f
# (UPat.var("cond").where(UPat.var("t"), UPat.var("f")), fold_where_closure),
# push cast to branches
(UPat.var("s").where(UPat.var("a"), UPat.var("b")).cast().named("cast"), lambda s,a,b,cast: s.where(a.cast(cast.dtype), b.cast(cast.dtype))),
# ** pow **
+1 -9
View File
@@ -17,18 +17,10 @@ def z3_xor(a:z3.ExprRef, b:z3.ExprRef) -> z3.ExprRef:
if isinstance(b, z3.IntNumRef) and b.as_long() == -1: return -(a+1)
if isinstance(a, z3.IntNumRef) and a.as_long() == -1: return -(b+1)
raise RuntimeError(f"z3 int XOR only supports XOR with -1, got {a=} {b=}")
def z3_and(a:z3.ExprRef, b:z3.ExprRef) -> z3.ExprRef:
if isinstance(a, z3.BoolRef): return a&b
if isinstance(a, z3.IntNumRef): a, b = b, a
if isinstance(b, z3.IntNumRef):
# x & (2^k-1) = x % 2^k and x & -(2^k) = x - x % 2^k for any x in two's complement
if (m:=b.as_long()+1) > 0 and m&(m-1) == 0: return a%m
if (m:=-b.as_long()) > 0 and m&(m-1) == 0: return a - a%m
raise RuntimeError(f"z3 int AND only supports 2**k-1 and -2**k masks, got {a=} {b=}")
z3_alu: dict[Ops, Callable[..., z3.ExprRef]] = python_alu | {Ops.CMOD: lambda a,b: a-z3_cdiv(a,b)*b, Ops.CDIV: z3_cdiv, Ops.FLOORDIV: z3_floordiv,
Ops.FLOORMOD: lambda a,b: a-z3_floordiv(a,b)*b,
Ops.SHR: lambda a,b: a/(2**b.as_long()), Ops.SHL: lambda a,b: a*(2**b.as_long()),
Ops.AND: z3_and, Ops.WHERE: z3.If, Ops.XOR: z3_xor, Ops.MAX: lambda a,b: z3.If(a<b, b, a),}
Ops.AND: lambda a,b: a%(b+1) if isinstance(b, z3.ArithRef) else a&b, Ops.WHERE: z3.If, Ops.XOR: z3_xor, Ops.MAX: lambda a,b: z3.If(a<b, b, a),}
def create_bounded(name:str, vmin:int, vmax:int, z3ctx:z3.Context) -> tuple[z3.ArithRef, z3.BoolRef]:
return (s:=z3.Int(name, ctx=z3ctx)), (vmin <= s)&(s <= vmax)
+1 -1
View File
@@ -341,7 +341,7 @@ def load_amd_counters(data:VizData, profile:list) -> None:
counter_events.setdefault((e.kern, e.exec_tag), {}).setdefault(type(e).__name__, []).append(e)
if isinstance(e, ProfileRangeEvent) and e.device.startswith("AMD") and e.en is not None:
durations.setdefault(str(e.name), []).append(float(e.en-e.st))
if isinstance(e, ProfileProgramEvent) and e.device.startswith("AMD") and e.tag is not None: prg_events[e.tag] = e
if isinstance(e, ProfileProgramEvent) and e.tag is not None: prg_events[e.tag] = e
if isinstance(e, ProfileDeviceEvent) and e.device.startswith("AMD"): arch = f"gfx{unwrap(e.props)['gfx_target_version']//1000}"
if len(counter_events) == 0: return None
data.ctxs.append({"name":"All Counters", "steps":[create_step("PMC", ("/all-pmc", len(data.ctxs), 0), (durations, all_counters:={}))]})