Compare commits

...
Author SHA1 Message Date
geohot a29986f074 small changes and test fixes from kernel is call 2026-02-06 16:08:02 +08:00
George HotzandGitHub 3c26ce29b2 make disk tensor tests process safe (#14584) 2026-02-06 15:39:55 +08:00
qazalandGitHub cf73d7e2a7 hotfix: disable slower asm gemm shape from llama seqlen 8192 (#14582) 2026-02-06 15:05:19 +09:00
qazalandGitHub be77873974 llama: contig backward for wk / wv matmul backward (#14581) 2026-02-06 14:54:00 +09:00
chenyuandGitHub 15d3344d9e use int inputs in test_assign (#14580)
int is less flaky
2026-02-06 00:07:31 -05:00
qazalandGitHub 50a166a5fa viz: cleanup amdgpu target mapping (#14579)
* viz: cleanup amdgpu target mapping

* linter

* unwraps
2026-02-06 13:51:51 +09:00
chenyuandGitHub b09dc646f5 revert some late_buffer_view change (#14578)
revert #14478 which breaks tinyfs
2026-02-05 22:51:40 -05:00
chenyuandGitHub d41836f135 remove KERNEL special case in realize_assign [pr] (#14573) 2026-02-05 21:55:44 -05:00
George HotzandGitHub 6cbcf98627 KernelInfo is required on get_program (#14571)
* rangeify always adds KernelInfo

* fix tests

* skip flaky test
2026-02-06 10:49:27 +08:00
George HotzandGitHub 28c56a783c add CallInfo and viz call toggle (#14570) 2026-02-06 09:30:58 +08:00
23 changed files with 209 additions and 165 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ GEMM_ARGS = {
(8192, 4096, 4096): (256, 64, 32768),
(8192, 14336, 4096): (256, 64, 114688),
(8192, 4096, 14336): (256, 224, 114688),
(8192, 128256, 4096): (16032, 64, 1026048),
# TODO: get a fast gemm for this shape
#(8192, 128256, 4096): (16032, 64, 1026048),
(8192, 8192, 8192): (256, 128, 131072),
(4096, 4096, 4096): (256, 64, 16384),
(4096, 14336, 4096): (256, 64, 57344),
+1 -1
View File
@@ -55,7 +55,7 @@ class Attention:
xqkv = x @ self.wqkv.T
xq, xk, xv = xqkv.split([self.wq.weight.shape[0], self.wk.weight.shape[0], self.wv.weight.shape[0]], dim=2)
else:
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
xq, xk, xv = self.wq(x), self.wk(x.contiguous_backward()), self.wv(x)
if self.q_norm is not None and self.k_norm is not None:
xq = self.q_norm(xq)
+1 -1
View File
@@ -62,7 +62,7 @@ def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None):
bufs.append(buf:=allocator.alloc(len(data) * buf_dt.itemsize))
allocator._copyin(buf, memoryview(struct.pack(str(len(data)) + (buf_dt.fmt or ""), *data)))
g = UOp(Ops.PARAM, uop.dtype.ptr(), arg=0, src=())
prg = get_program(UOp.store(g.index(UOp.const(dtypes.int, 0)), uop).sink(), PythonRenderer())
prg = get_program(UOp.store(g.index(UOp.const(dtypes.int, 0)), uop).sink(arg=KernelInfo()), PythonRenderer())
prog = PythonProgram("run", PythonCompiler().compile(prg.src))
prog(out_buf:=allocator.alloc(uop.dtype.itemsize), *bufs)
return out_buf.cast(uop.dtype.fmt or "").tolist()[0]
+2 -2
View File
@@ -1,6 +1,6 @@
# ruff: noqa: E501
import unittest
from tinygrad.uop.ops import UOp, Ops, AxisType
from tinygrad.uop.ops import UOp, Ops, AxisType, KernelInfo
from tinygrad.dtype import dtypes
from tinygrad.engine.realize import get_program
from tinygrad.device import Device
@@ -18,7 +18,7 @@ class TestLinearizerFailures(unittest.TestCase):
c8 = c7.index(c3)
c9 = ((((c6+(c8*UOp.const(dtypes.float, -1.0)))*(c6+(c8*UOp.const(dtypes.float, -1.0)))).reduce(c5, arg=Ops.ADD)*UOp.const(dtypes.float, 0.000390625))+UOp.const(dtypes.float, 1e-05)).sqrt().reciprocal()
c10 = c0.index(c3).store(c9).end(c1, c2)
ast = c10.sink()
ast = c10.sink(arg=KernelInfo())
get_program(ast, renderer=Device[Device.DEFAULT].renderer)
if __name__ == '__main__':
+1 -1
View File
@@ -35,7 +35,7 @@ class TestLinearizerRewrite(unittest.TestCase):
prg = get_program(ast, Device["CPU"].renderer)
assert prg.applied_opts == (), f"expected no opts, got {prg}"
prg = get_program(ast.replace(arg=None), Device["CPU"].renderer)
prg = get_program(ast.replace(arg=KernelInfo()), Device["CPU"].renderer)
assert prg.applied_opts != (), f"expected opts to apply, got {prg.applied_opts}"
prg = get_program(ast.replace(arg=KernelInfo(name="custom")), Device["CPU"].renderer)
+4
View File
@@ -5,18 +5,22 @@ class TestLoadStore(unittest.TestCase):
def test_load_shape(self):
t = Tensor(bytes(16)).fs_load(1024)
assert t.shape == (1024,), t.shape
t.schedule()
def test_store_shape(self):
t = Tensor.zeros(1024).fs_store()
assert t.shape == (16,), t.shape
t.schedule()
def test_load_large_shape(self):
t = Tensor(bytes(16)).fs_load(10_000_000)
assert t.shape == (10_000_000,), t.shape
t.schedule()
def test_store_large_shape(self):
t = Tensor.zeros(10_000_000).fs_store()
assert t.shape == (16,), t.shape
t.schedule()
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -52,7 +52,7 @@ def flip_contract_kernel(dest:UOp, src:UOp):
j = UOp.range(dest.shape[1], 1, AxisType.UPCAST)
vec = src[i, j].contract(j)
store = UOp.group(*[dest[i, k].store(vec.gep(3-k)) for k in range(4)])
return store.end(i).sink(arg=KernelInfo(name=f"flip_contract_{dest.size}", opts_to_apply=()))
return store.end(i, j).sink(arg=KernelInfo(name=f"flip_contract_{dest.size}", opts_to_apply=()))
def slice_sum_kernel(dest:UOp, src:UOp):
G = UOp.range(src.shape[0], 0)
@@ -104,7 +104,7 @@ def backward_gemm_custom(gradient:UOp, kernel:UOp) -> tuple[UOp, UOp]:
class TestCustomKernel(unittest.TestCase):
def test_empty(self):
a = Tensor.empty(1)
a = Tensor.custom_kernel(a, fxn=lambda _: UOp.sink())[0]
a = Tensor.custom_kernel(a, fxn=lambda _: UOp.sink(arg=KernelInfo()))[0]
a.realize()
def test_simple(self):
+1
View File
@@ -178,6 +178,7 @@ class TestProfiler(unittest.TestCase):
print("pairwise clock jitter matrix (us):\n" + '\n'.join([''.join([f'{float(item):8.3f}' for item in row]) for row in jitter_matrix]))
@unittest.skip("this test is flaky")
def test_cpu_profile(self):
def test_fxn(err=False):
time.sleep(0.1)
+5 -5
View File
@@ -9,7 +9,7 @@ from tinygrad.renderer.cstyle import CStyleLanguage
from tinygrad.renderer.ptx import PTXRenderer
from tinygrad.renderer.wgsl import WGSLRenderer
from tinygrad.runtime.ops_python import PythonRenderer
from tinygrad.uop.ops import UOp, Ops, python_alu
from tinygrad.uop.ops import UOp, Ops, KernelInfo, python_alu
from tinygrad.tensor import Tensor, _to_np_dtype
def _test_uop_result(inputs:list[Tensor], prg, local_size=None):
@@ -32,7 +32,7 @@ def _setup_and_test_alu(alu_op:Ops, input_val:ConstType, *alu_src_uops:UOp):
ld = b.index(idx)
alu = ld.alu(alu_op, *alu_src_uops)
store = UOp.store(a.index(idx), alu)
sink = UOp(Ops.SINK, dtypes.void, (store,))
sink = UOp(Ops.SINK, dtypes.void, (store,), arg=KernelInfo())
prg = get_program(sink, Device[Device.DEFAULT].renderer)
return _test_uop_result([Tensor([input_val])], prg)[0]
@@ -42,7 +42,7 @@ class TestRendererFailures(unittest.TestCase):
a = UOp(Ops.PARAM, dtypes.int.ptr(), (), 0)
gate_alu = (lidx0:=UOp(Ops.SPECIAL, dtypes.int, (UOp.const(dtypes.int, 4),), 'lidx0')).ne(0)
gated_alu_store = UOp(Ops.STORE, dtypes.void, (a.index(lidx0.valid(gate_alu)), UOp.const(dtypes.int, 1)))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,), arg=KernelInfo())
prg = get_program(sink, Device[Device.DEFAULT].renderer)
ret = _test_uop_result([], prg, local_size=[4, 1, 1])[0]
np.testing.assert_equal(ret, [0, 1, 1, 1])
@@ -53,7 +53,7 @@ class TestRendererFailures(unittest.TestCase):
gate_alu_0 = (lidx0:=UOp(Ops.SPECIAL, dtypes.int, (UOp.const(dtypes.int, 4),), 'lidx0')).ne(0)
gate_alu_1 = (lidx1:=UOp(Ops.SPECIAL, dtypes.int, (UOp.const(dtypes.int, 2),), 'lidx1')).ne(0)
gated_alu_store = UOp(Ops.STORE, dtypes.void, (a.index((lidx0+lidx1*4).valid(gate_alu_0&gate_alu_1)), UOp.const(dtypes.int, 1)))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,), arg=KernelInfo())
prg = get_program(sink, Device[Device.DEFAULT].renderer)
ret = _test_uop_result([], prg, local_size=[4, 2, 1])[0]
np.testing.assert_equal(ret, [0, 0, 0, 0, 0, 1, 1, 1])
@@ -99,7 +99,7 @@ class TestPTXFailures(unittest.TestCase):
val = UOp.const(dtypes.int, 1)
if_uop = UOp(Ops.IF, dtypes.void, (gate_alu,))
gated_alu_store = UOp(Ops.STORE, dtypes.void, (a.index(lidx0, if_uop), val))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,))
sink = UOp(Ops.SINK, dtypes.void, (gated_alu_store,), arg=KernelInfo())
prg = get_program(sink, Device[Device.DEFAULT].renderer)
ret = _test_uop_result([], prg, local_size=[4, 1, 1])[0]
np.testing.assert_equal(ret, [0, 1, 1, 1])
+1 -2
View File
@@ -10,9 +10,8 @@ from hypothesis import assume, given, settings, strategies as strat
from tinygrad import nn, dtypes, Device, Tensor, Variable
from tinygrad.device import is_dtype_supported
from tinygrad.dtype import DType, ImageDType
from tinygrad.uop.ops import UOp, Ops, GroupOp, UPat
from tinygrad.uop.ops import UOp, Ops, GroupOp, UPat, Kernel
from tinygrad.helpers import CI, DEBUG, SPLIT_REDUCEOP, GlobalCounters, Context, getenv, all_same, temp
from tinygrad.schedule.rangeify import Kernel
from tinygrad.engine.realize import CompiledRunner, run_schedule
class KernelCountException(Exception): pass
+2 -2
View File
@@ -23,7 +23,7 @@ def to_uops_list(u:list[UOp], ren=None) -> list[UOp]:
return ret
def _uops_to_prg(uops_list):
prg = get_program(UOp.sink(*uops_list), Device[Device.DEFAULT].renderer)
prg = get_program(UOp.sink(*uops_list, arg=KernelInfo()), Device[Device.DEFAULT].renderer)
return CompiledRunner(replace(prg, device=Device.DEFAULT))
def uop(uops:list[UOp], op:Ops, dtype:Optional[DType], src:tuple[UOp, ...], arg:Any=None) -> UOp:
@@ -281,7 +281,7 @@ class TestUOpPrograms(unittest.TestCase):
ptr = UOp.placeholder(out.shape, out.dtype, slot=0)
i, j = UOp.range(10, axis_id=0), UOp.range(10, axis_id=1)
prog = ptr[i,j].set(42).end(i,j)
self._run(prog.sink(), out)
self._run(prog.sink(arg=KernelInfo()), out)
with Context(DEBUG=0): self.assertTrue((out == 42).all().item())
+4
View File
@@ -3,6 +3,7 @@ from tinygrad import Tensor, Device, dtypes, Context
from tinygrad.device import is_dtype_supported
from tinygrad.helpers import getenv, CI
from extra.gemm.asm.cdna.gemm import asm_gemm
from test.helpers import needs_second_gpu
def verify_asm_gemm(batch:int, M:int, N:int, K:int, dtype=dtypes.float16, gpus:int=1) -> None:
Tensor.manual_seed(0)
@@ -37,6 +38,7 @@ class TestGemm(unittest.TestCase):
def test_simple(self): verify_asm_gemm(1, N:=(getenv("N", 4096)//SCALE), N, N, dtype=dtypes.half)
def test_gemm(self): verify_asm_gemm(1, 8192//SCALE, 4096//SCALE, 14336//SCALE)
def test_gemm_batched(self): verify_asm_gemm(2, 8192//SCALE, 4096//SCALE, 4096//SCALE)
@needs_second_gpu
def test_gemm_multi(self): verify_asm_gemm(2, 8192//SCALE, 4096//SCALE, 4096//SCALE, gpus=2)
class TestGemmLarge(unittest.TestCase):
@@ -45,11 +47,13 @@ class TestGemmLarge(unittest.TestCase):
self.skipTest("very slow on non mi350x")
def test_gemm1(self): verify_asm_gemm(8, 8192, 4096, 14336, dtype=dtypes.bfloat16, gpus=8)
@unittest.skip("disabled, asm in this shape is slower than tinygrad")
def test_gemm2(self): verify_asm_gemm(8, 8192, 128256, 4096, dtype=dtypes.bfloat16, gpus=8)
def test_gemm3(self): verify_asm_gemm(8, 8192, 14336, 4096, dtype=dtypes.bfloat16, gpus=8)
def test_gemm4(self): verify_asm_gemm(8, 4096, 14336, 4096, dtype=dtypes.bfloat16, gpus=8)
def test_gemm5(self): verify_asm_gemm(8, 4096, 4096, 14336, dtype=dtypes.bfloat16, gpus=8)
def test_gemm6(self): verify_asm_gemm(16, 4096, 4096, 14336, dtype=dtypes.bfloat16, gpus=8)
@unittest.skip("disabled, asm in this shape is slower than tinygrad")
def test_gemm7(self): verify_asm_gemm(1, 8192, 128256, 4096)
def test_gemm_unsupported(self):
with self.assertRaisesRegex(AssertionError, "shape not supported"):
+11 -10
View File
@@ -259,15 +259,15 @@ class TestAssign(unittest.TestCase):
np.testing.assert_allclose(out, [1.,1.,1.,1.,1.,1.,0.,0.,1.,1.,1.,1.,1.,1.,0.,0.])
def test_assign_contiguous(self):
b = Tensor.rand(4,4).realize()
a = (Tensor.rand(4,4).realize() + 1)
b = Tensor.arange(16).reshape(4,4).contiguous().realize()
a = (Tensor.arange(16).reshape(4,4).contiguous().realize() + 1)
kc = GlobalCounters.kernel_count
b.assign(a.contiguous()).realize()
assert GlobalCounters.kernel_count - kc == 2
def test_assign_contiguous_permute(self):
b = Tensor.rand(4,4).realize()
a = (Tensor.rand(4,4).realize() + 1).permute((1,0))
b = Tensor.arange(16).reshape(4,4).contiguous().realize()
a = (Tensor.arange(16).reshape(4,4).contiguous().realize() + 1).permute((1,0))
kc = GlobalCounters.kernel_count
b.assign(a.contiguous()).realize()
assert GlobalCounters.kernel_count - kc == 2
@@ -333,7 +333,7 @@ class TestAssign(unittest.TestCase):
@unittest.skip("multi output not supported anymore")
def test_simple_assignment_multioutput(self):
a = Tensor.randn(32, 32).realize()
a = Tensor.arange(32*32).reshape(32, 32).contiguous().realize()
b = Tensor.full((32, ), 1.).contiguous().realize()
c = Tensor.full((32, ), 2.).contiguous().realize()
d = Tensor.full((32, ), 3.).contiguous().realize()
@@ -361,16 +361,16 @@ class TestAssign(unittest.TestCase):
np.testing.assert_equal(a.numpy(), np.arange(4 * 4).reshape(4, 4).transpose(1, 0) + np.arange(4 * 4).reshape(4, 4))
def test_permuted_reduceop_child_dual_use(self):
a = Tensor.randn(32, 32, 32).realize()
b = Tensor.full((32, 32), 1.).contiguous().realize()
a = Tensor.arange(32*32*32).reshape(32, 32, 32).contiguous().realize()
b = Tensor.ones(32, 32, dtype=dtypes.int).contiguous().realize()
r = a.sum(axis=1)
b.assign(r + b.permute(1, 0))
b.realize()
np.testing.assert_allclose(b.numpy(), a.numpy().sum(axis=1)+np.ones((32, 32)).transpose(1, 0), atol=1e-6, rtol=1e-3)
np.testing.assert_equal(b.numpy(), a.numpy().sum(axis=1)+np.ones((32, 32), dtype=np.int32).transpose(1, 0))
@unittest.skip("multi output not supported anymore")
def test_permuted_reduceop_multioutput_dual_use(self):
a = Tensor.randn(32, 32, 32).realize()
a = Tensor.arange(32*32*32).reshape(32, 32, 32).contiguous().realize()
b = Tensor.full((32, 32), 1.).contiguous().realize()
c = Tensor.full((32, 32), 2.).contiguous().realize()
@@ -383,7 +383,7 @@ class TestAssign(unittest.TestCase):
@unittest.skip("multi output not supported anymore")
def test_permuted_reduceop_multioutput_dual_use_possible(self):
a = Tensor.randn(32, 32, 32, dtype=dtypes.int).realize()
a = Tensor.arange(32*32*32).reshape(32, 32, 32).contiguous().realize()
b = Tensor.arange(32 * 32).reshape(32, 32).realize()
c = Tensor.arange(32 * 32).reshape(32, 32).realize()
@@ -529,6 +529,7 @@ class TestAssign(unittest.TestCase):
a = Tensor.empty(5, device=f"disk:{temp('disk_assignment')}").assign(Tensor.ones(5)).numpy()
np.testing.assert_equal(a, np.ones(5))
@unittest.skip("this test is crashing!")
def test_assign_slice_then_read(self):
"""Assign to slice then read from buffer - read should see the assigned values.
This is the KV cache pattern from llm.py.
+82 -100
View File
@@ -4,9 +4,17 @@ from tinygrad import Tensor, Device, dtypes
from tinygrad.device import is_dtype_supported
from tinygrad.dtype import DType, DTYPES_DICT
from tinygrad.nn.state import safe_load, safe_save, get_state_dict, torch_load
from tinygrad.helpers import Timing, fetch, temp, OSX
from tinygrad.helpers import Timing, fetch, OSX, dedup
from test.helpers import slow
class TempDirTestCase(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
def tearDown(self):
self.temp_dir.cleanup()
def tmp(self, name:str) -> str:
return (pathlib.Path(self.temp_dir.name) / name).as_posix()
def compare_weights_both(url):
import torch
fn = fetch(url)
@@ -84,7 +92,7 @@ class TestRawDiskBuffer(unittest.TestCase):
pathlib.Path(tmp).unlink()
@unittest.skipUnless(is_dtype_supported(dtypes.uint8), "need uint8")
class TestSafetensors(unittest.TestCase):
class TestSafetensors(TempDirTestCase):
def test_real_safetensors(self):
import torch
from safetensors.torch import save_file
@@ -95,19 +103,19 @@ class TestSafetensors(unittest.TestCase):
"weight3": torch.arange(0, 17, dtype=torch.int32).reshape(17,1,1),
"weight4": torch.arange(0, 2, dtype=torch.uint8),
}
save_file(tensors, temp("real.safetensors"))
save_file(tensors, self.tmp("real.safetensors"))
ret = safe_load(temp("real.safetensors"))
ret = safe_load(self.tmp("real.safetensors"))
for k,v in tensors.items(): np.testing.assert_array_equal(ret[k].numpy(), v.numpy())
safe_save(ret, temp("real.safetensors_alt"))
with open(temp("real.safetensors"), "rb") as f:
with open(temp("real.safetensors_alt"), "rb") as g:
safe_save(ret, self.tmp("real.safetensors_alt"))
with open(self.tmp("real.safetensors"), "rb") as f:
with open(self.tmp("real.safetensors_alt"), "rb") as g:
assert f.read() == g.read()
ret2 = safe_load(temp("real.safetensors_alt"))
ret2 = safe_load(self.tmp("real.safetensors_alt"))
for k,v in tensors.items(): np.testing.assert_array_equal(ret2[k].numpy(), v.numpy())
def test_real_safetensors_open(self):
fn = temp("real_safe")
fn = self.tmp("real_safe")
state_dict = {"tmp": Tensor.rand(10,10)}
safe_save(state_dict, fn)
import os
@@ -123,15 +131,15 @@ class TestSafetensors(unittest.TestCase):
from extra.models.efficientnet import EfficientNet
model = EfficientNet(0)
state_dict = get_state_dict(model)
safe_save(state_dict, temp("eff0"))
state_dict_loaded = safe_load(temp("eff0"))
safe_save(state_dict, self.tmp("eff0"))
state_dict_loaded = safe_load(self.tmp("eff0"))
assert sorted(state_dict_loaded.keys()) == sorted(state_dict.keys())
for k,v in state_dict.items():
np.testing.assert_array_equal(v.numpy(), state_dict_loaded[k].numpy())
# load with the real safetensors
from safetensors import safe_open
with safe_open(temp("eff0"), framework="pt", device="cpu") as f:
with safe_open(self.tmp("eff0"), framework="pt", device="cpu") as f:
assert sorted(f.keys()) == sorted(state_dict.keys())
for k in f.keys():
np.testing.assert_array_equal(f.get_tensor(k).numpy(), state_dict[k].numpy())
@@ -155,19 +163,19 @@ class TestSafetensors(unittest.TestCase):
def test_metadata(self):
metadata = {"hello": "world"}
safe_save({}, temp('metadata.safetensors'), metadata)
safe_save({}, self.tmp('metadata.safetensors'), metadata)
import struct
with open(temp('metadata.safetensors'), 'rb') as f:
with open(self.tmp('metadata.safetensors'), 'rb') as f:
dat = f.read()
sz = struct.unpack(">Q", dat[0:8])[0]
import json
assert json.loads(dat[8:8+sz])['__metadata__']['hello'] == 'world'
def test_save_all_dtypes(self):
for dtype in DTYPES_DICT.values():
for dtype in dedup(DTYPES_DICT.values()):
if dtype in [dtypes.bfloat16]: continue # not supported in numpy
if not is_dtype_supported(dtype): continue
path = temp(f"ones.{dtype}.safetensors")
path = self.tmp(f"ones.{dtype}.safetensors")
ones = Tensor(np.random.rand(10,10), dtype=dtype)
safe_save(get_state_dict(ones), path)
np.testing.assert_equal(ones.numpy(), list(safe_load(path).values())[0].numpy())
@@ -189,9 +197,9 @@ class TestSafetensors(unittest.TestCase):
"weight_I16": torch.tensor([127, 64], dtype=torch.short),
"weight_BF16": torch.randn((2, 2), dtype=torch.bfloat16),
}
save_file(tensors, temp("dtypes.safetensors"))
save_file(tensors, self.tmp("dtypes.safetensors"))
loaded = safe_load(temp("dtypes.safetensors"))
loaded = safe_load(self.tmp("dtypes.safetensors"))
for k,v in loaded.items():
if v.dtype != dtypes.bfloat16:
assert v.numpy().dtype == tensors[k].numpy().dtype
@@ -203,57 +211,52 @@ class TestSafetensors(unittest.TestCase):
"weight_U32": np.array([1, 2, 3], dtype=np.uint32),
"weight_U64": np.array([1, 2, 3], dtype=np.uint64),
}
np_save_file(tensors, temp("dtypes.safetensors"))
np_save_file(tensors, self.tmp("dtypes.safetensors"))
loaded = safe_load(temp("dtypes.safetensors"))
loaded = safe_load(self.tmp("dtypes.safetensors"))
for k,v in loaded.items():
assert v.numpy().dtype == tensors[k].dtype
np.testing.assert_allclose(v.numpy(), tensors[k])
def helper_test_disk_tensor(fn, data, np_fxn, tinygrad_fxn=None):
def helper_test_disk_tensor(tmp, fn, data, np_fxn, tinygrad_fxn=None):
if tinygrad_fxn is None: tinygrad_fxn = np_fxn
pathlib.Path(temp(fn)).unlink(missing_ok=True)
tinygrad_tensor = Tensor(data, device="CPU").to(f"disk:{temp(fn)}")
pathlib.Path(tmp(fn)).unlink(missing_ok=True)
tinygrad_tensor = Tensor(data, device="CPU").to(f"disk:{tmp(fn)}")
numpy_arr = np.array(data)
tinygrad_fxn(tinygrad_tensor)
np_fxn(numpy_arr)
np.testing.assert_allclose(tinygrad_tensor.numpy(), numpy_arr)
class TestDiskTensor(unittest.TestCase):
class TestDiskTensor(TempDirTestCase):
def test_empty(self):
pathlib.Path(temp("dt_empty")).unlink(missing_ok=True)
Tensor.empty(100, 100, device=f"disk:{temp('dt_empty')}")
Tensor.empty(100, 100, device=f"disk:{self.tmp('dt_empty')}")
def test_simple_read(self):
fn = pathlib.Path(temp("dt_simple_read"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_simple_read"))
fn.write_bytes(bytes(range(256)))
t = Tensor.empty(16, 16, device=f"disk:{temp('dt_simple_read')}", dtype=dtypes.uint8)
t = Tensor.empty(16, 16, device=f"disk:{self.tmp('dt_simple_read')}", dtype=dtypes.uint8)
out = t[1].to(Device.DEFAULT).tolist()
assert out == list(range(16, 32))
def test_simple_read_bitcast(self):
fn = pathlib.Path(temp("dt_simple_read_bitcast"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_simple_read_bitcast"))
fn.write_bytes(bytes(range(256))*2)
t = Tensor.empty(16, 16*2, device=f"disk:{temp('dt_simple_read_bitcast')}", dtype=dtypes.uint8)
t = Tensor.empty(16, 16*2, device=f"disk:{self.tmp('dt_simple_read_bitcast')}", dtype=dtypes.uint8)
out = t[1].bitcast(dtypes.uint16).to(Device.DEFAULT).tolist()
tout = [(x//256, x%256) for x in out]
assert tout == list([(x+1,x) for x in range(32,64,2)])
def test_simple_read_bitcast_alt(self):
fn = pathlib.Path(temp("dt_simple_read_bitcast_alt"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_simple_read_bitcast_alt"))
fn.write_bytes(bytes(range(256))*2)
t = Tensor.empty(16, 16*2, device=f"disk:{temp('dt_simple_read_bitcast_alt')}", dtype=dtypes.uint8)
t = Tensor.empty(16, 16*2, device=f"disk:{self.tmp('dt_simple_read_bitcast_alt')}", dtype=dtypes.uint8)
out = t.bitcast(dtypes.uint16)[1].to(Device.DEFAULT).tolist()
tout = [(x//256, x%256) for x in out]
assert tout == list([(x+1,x) for x in range(32,64,2)])
def test_strided_read(self):
# test non-contiguous (strided) read - should read elements at indices 0, 2, 4
pathlib.Path(temp(fn:="dt_strided_read")).unlink(missing_ok=True)
dt = Tensor([0, 1, 2, 3, 4, 5]).to(f"disk:{temp(fn)}")
dt = Tensor([0, 1, 2, 3, 4, 5]).to(f"disk:{self.tmp('dt_strided_read')}")
result = dt[::2].tolist()
# TODO: dt[::2] selects indices 0, 2, 4, so result should be [0, 2, 4]
# self.assertEqual(result, [0, 2, 4])
@@ -261,43 +264,38 @@ class TestDiskTensor(unittest.TestCase):
def test_permuted_read(self):
# test non-contiguous (permuted) read - should read transposed
pathlib.Path(temp(fn:="dt_permuted_read")).unlink(missing_ok=True)
dt = Tensor([[0, 1, 2], [3, 4, 5]]).to(f"disk:{temp(fn)}")
dt = Tensor([[0, 1, 2], [3, 4, 5]]).to(f"disk:{self.tmp('dt_permuted_read')}")
result = dt.T.tolist()
# TODO: transpose should give [[0, 3], [1, 4], [2, 5]]
# self.assertEqual(result, [[0, 3], [1, 4], [2, 5]])
self.assertEqual(result, [[0, 1], [2, 3], [4, 5]]) # wrong!
def test_write_ones(self):
pathlib.Path(temp("dt_write_ones")).unlink(missing_ok=True)
out = Tensor.ones(10, 10, device="CPU").contiguous()
outdisk = out.to(f"disk:{temp('dt_write_ones')}")
outdisk = out.to(f"disk:{self.tmp('dt_write_ones')}")
print(outdisk)
outdisk.realize()
del out, outdisk
import struct
# test file
with open(temp("dt_write_ones"), "rb") as f:
with open(self.tmp("dt_write_ones"), "rb") as f:
assert f.read() == struct.pack('<f', 1.0) * 100 == b"\x00\x00\x80\x3F" * 100
# test load alt
reloaded = Tensor.empty(10, 10, device=f"disk:{temp('dt_write_ones')}")
reloaded = Tensor.empty(10, 10, device=f"disk:{self.tmp('dt_write_ones')}")
np.testing.assert_almost_equal(reloaded.numpy(), np.ones((10, 10)))
def test_simple_setitem(self):
pathlib.Path(temp(fn:="dt_simple_setitem")).unlink(missing_ok=True)
data = [[1],[2]]
src = Tensor(data)
dt = src.to(f"disk:{temp(fn)}")
dt = src.to(f"disk:{self.tmp('dt_simple_setitem')}")
dt[1] = [3]
self.assertEqual(dt.tolist(), [[1], [3]])
def test_strided_setitem(self):
# test non-contiguous (strided) setitem - should set elements at indices 0, 2, 4
pathlib.Path(temp(fn:="dt_strided_setitem")).unlink(missing_ok=True)
dt = Tensor([1, 2, 3, 4, 5, 6]).to(f"disk:{temp(fn)}")
dt = Tensor([1, 2, 3, 4, 5, 6]).to(f"disk:{self.tmp('dt_strided_setitem')}")
dt[::2] = Tensor([10, 20, 30])
# TODO: dt[::2] selects indices 0, 2, 4, so result should be [10, 2, 20, 4, 30, 6]
# self.assertEqual(dt.tolist(), [10, 2, 20, 4, 30, 6])
@@ -305,39 +303,35 @@ class TestDiskTensor(unittest.TestCase):
def test_assign_const_to_disk(self):
# assign from CONST (Tensor.full) to disk - source has no buffer, needs contiguous first
pathlib.Path(temp(fn:="dt_assign_const")).unlink(missing_ok=True)
dt = Tensor.empty(4, device=f"disk:{temp(fn)}", dtype=dtypes.int32)
dt = Tensor.empty(4, device=f"disk:{self.tmp('dt_assign_const')}", dtype=dtypes.int32)
dt.assign(Tensor.full((4,), 42, dtype=dtypes.int32)).realize()
np.testing.assert_array_equal(dt.numpy(), [42, 42, 42, 42])
def test_assign_slice_from_const(self):
# slice assign from CONST to disk - tests size calculation when no RANGE ops
pathlib.Path(temp(fn:="dt_slice_const")).unlink(missing_ok=True)
dt = Tensor([0, 1, 2, 3], dtype=dtypes.int32).to(f"disk:{temp(fn)}")
dt = Tensor([0, 1, 2, 3], dtype=dtypes.int32).to(f"disk:{self.tmp('dt_slice_const')}")
dt[1:3].assign(Tensor.full((2,), 99, dtype=dtypes.int32)).realize()
np.testing.assert_array_equal(dt.numpy(), [0, 99, 99, 3])
def test_disk_to_disk_copy(self):
# disk-to-disk copy needs to go through CPU
pathlib.Path(temp(fn1:="dt_d2d_src")).unlink(missing_ok=True)
pathlib.Path(temp(fn2:="dt_d2d_dst")).unlink(missing_ok=True)
src = Tensor([1, 2, 3, 4], dtype=dtypes.int32).to(f"disk:{temp(fn1)}")
dst = Tensor.empty(4, device=f"disk:{temp(fn2)}", dtype=dtypes.int32)
src = Tensor([1, 2, 3, 4], dtype=dtypes.int32).to(f"disk:{self.tmp('dt_d2d_src')}")
dst = Tensor.empty(4, device=f"disk:{self.tmp('dt_d2d_dst')}", dtype=dtypes.int32)
dst.assign(src.to("CPU")).realize()
np.testing.assert_array_equal(dst.numpy(), [1, 2, 3, 4])
def test_assign_slice(self):
def assign(x,s,y): x[s] = y
helper_test_disk_tensor("dt_assign_slice_1", [0,1,2,3], lambda x: assign(x, slice(0,2), [13, 12]))
helper_test_disk_tensor("dt_assign_slice_2", [[0,1,2,3],[4,5,6,7]], lambda x: assign(x, slice(0,1), [[13, 12, 11, 10]]))
helper_test_disk_tensor(self.tmp, "dt_assign_slice_1", [0,1,2,3], lambda x: assign(x, slice(0,2), [13, 12]))
helper_test_disk_tensor(self.tmp, "dt_assign_slice_2", [[0,1,2,3],[4,5,6,7]], lambda x: assign(x, slice(0,1), [[13, 12, 11, 10]]))
def test_reshape(self):
helper_test_disk_tensor("dt_reshape_1", [1,2,3,4,5], lambda x: x.reshape((1,5)))
helper_test_disk_tensor("dt_reshape_2", [1,2,3,4], lambda x: x.reshape((2,2)))
helper_test_disk_tensor(self.tmp, "dt_reshape_1", [1,2,3,4,5], lambda x: x.reshape((1,5)))
helper_test_disk_tensor(self.tmp, "dt_reshape_2", [1,2,3,4], lambda x: x.reshape((2,2)))
def test_assign_to_different_dtype(self):
# NOTE: this is similar to Y_train in fetch_cifar
t = Tensor.empty(10, device=f'disk:{temp("dt_assign_to_different_dtype")}', dtype=dtypes.int64)
t = Tensor.empty(10, device=f'disk:{self.tmp("dt_assign_to_different_dtype")}', dtype=dtypes.int64)
for i in range(5):
data = np.array([3, 3])
@@ -349,8 +343,7 @@ class TestDiskTensor(unittest.TestCase):
def test_assign_with_bitcast(self):
# bitcast assign is used in safe_save for writing header length
# bitcast on source side works, bitcast on target side raises
pathlib.Path(temp(fn:="dt_assign_bitcast")).unlink(missing_ok=True)
t = Tensor.empty(16, device=f"disk:{temp(fn)}", dtype=dtypes.uint8)
t = Tensor.empty(16, device=f"disk:{self.tmp('dt_assign_bitcast')}", dtype=dtypes.uint8)
# correct way: bitcast the source to match target dtype
t[0:8].assign(Tensor([12345], dtype=dtypes.int64, device="CPU").bitcast(dtypes.uint8))
val = int.from_bytes(t[0:8].data(), 'little')
@@ -361,8 +354,7 @@ class TestDiskTensor(unittest.TestCase):
def test_assign_to_bitcast_view(self):
# assign float values to a float32 view of a uint8 disk buffer (used by safe_save)
pathlib.Path(temp(fn:="dt_bitcast_view_assign")).unlink(missing_ok=True)
t = Tensor.empty(32, device=f"disk:{temp(fn)}", dtype=dtypes.uint8)
t = Tensor.empty(32, device=f"disk:{self.tmp('dt_bitcast_view_assign')}", dtype=dtypes.uint8)
# create float32 view of bytes 8-24 (4 floats)
float_view = t[8:24].bitcast(dtypes.float32)
float_view.assign(Tensor([1.0, 2.0, 3.0, 4.0], dtype=dtypes.float32, device="CPU"))
@@ -370,21 +362,20 @@ class TestDiskTensor(unittest.TestCase):
def test_assign_cross_device(self):
# disk assign allows cross-device (source on GPU/CPU, target on disk)
pathlib.Path(temp(fn:="dt_assign_cross")).unlink(missing_ok=True)
t = Tensor.empty(4, device=f"disk:{temp(fn)}", dtype=dtypes.float32)
t = Tensor.empty(4, device=f"disk:{self.tmp('dt_assign_cross')}", dtype=dtypes.float32)
src = Tensor([1.0, 2.0, 3.0, 4.0]) # on default device
t.assign(src)
np.testing.assert_array_equal(t.numpy(), [1.0, 2.0, 3.0, 4.0])
def test_bitcast(self):
with open(temp('dt_bitcast'), "wb") as f: f.write(bytes(range(10,20)))
t = Tensor.empty(5, dtype=dtypes.int16, device=f"disk:{temp('dt_bitcast')}")
with open(self.tmp('dt_bitcast'), "wb") as f: f.write(bytes(range(10,20)))
t = Tensor.empty(5, dtype=dtypes.int16, device=f"disk:{self.tmp('dt_bitcast')}")
ret = t.to("CPU").bitcast(dtypes.uint16) + 1
assert ret.tolist() == [2827, 3341, 3855, 4369, 4883]
def test_bitcast_view(self):
with open(temp('dt_bitcast_view'), "wb") as f: f.write(bytes(range(10, 24)))
t = Tensor.empty(3, dtype=dtypes.uint, device=f"disk:{temp('dt_bitcast_view')}").shrink([(0, 2)])
with open(self.tmp('dt_bitcast_view'), "wb") as f: f.write(bytes(range(10, 24)))
t = Tensor.empty(3, dtype=dtypes.uint, device=f"disk:{self.tmp('dt_bitcast_view')}").shrink([(0, 2)])
ret = t.bitcast(dtypes.uint16).to("CPU") + 1
assert ret.tolist() == [2827, 3341, 3855, 4369]
@@ -392,59 +383,55 @@ class TestDiskTensor(unittest.TestCase):
@unittest.skipUnless(is_dtype_supported(dtypes.bfloat16), "bfloat16 not supported")
def test_bf16_disk_write_read(self):
t = Tensor([10000, -1, -1000, -10000, 20], dtype=dtypes.float32)
t.to(f"disk:{temp('dt_bf16_disk_write_read_f32')}").realize()
t.to(f"disk:{self.tmp('dt_bf16_disk_write_read_f32')}").realize()
# hack to "cast" f32 -> bf16
with open(temp('dt_bf16_disk_write_read_f32'), "rb") as f: dat = f.read()
with open(self.tmp('dt_bf16_disk_write_read_f32'), "rb") as f: dat = f.read()
adat = b''.join([dat[i+2:i+4] for i in range(0, len(dat), 4)])
with open(temp('dt_bf16_disk_write_read_bf16'), "wb") as f: f.write(adat)
with open(self.tmp('dt_bf16_disk_write_read_bf16'), "wb") as f: f.write(adat)
t = Tensor.empty(5, dtype=dtypes.bfloat16, device=f"disk:{temp('dt_bf16_disk_write_read_bf16')}")
t = Tensor.empty(5, dtype=dtypes.bfloat16, device=f"disk:{self.tmp('dt_bf16_disk_write_read_bf16')}")
ct = t.to(Device.DEFAULT).cast(dtypes.float)
assert ct.numpy().tolist() == [9984., -1, -1000, -9984, 20]
def test_copy_from_disk(self):
fn = pathlib.Path(temp("dt_copy_from_disk"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_copy_from_disk"))
fn.write_bytes(bytes(range(256))*1024)
t = Tensor.empty(256*1024, device=f"disk:{temp('dt_copy_from_disk')}", dtype=dtypes.uint8)
t = Tensor.empty(256*1024, device=f"disk:{self.tmp('dt_copy_from_disk')}", dtype=dtypes.uint8)
on_dev = t.to(Device.DEFAULT).realize()
np.testing.assert_equal(on_dev.numpy(), t.numpy())
def test_copy_from_disk_offset(self):
fn = pathlib.Path(temp("dt_copy_from_disk_offset"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_copy_from_disk_offset"))
fn.write_bytes(bytes(range(256))*1024)
for off in [314, 991, 2048, 4096]:
t = Tensor.empty(256*1024, device=f"disk:{temp('dt_copy_from_disk_offset')}", dtype=dtypes.uint8)[off:]
t = Tensor.empty(256*1024, device=f"disk:{self.tmp('dt_copy_from_disk_offset')}", dtype=dtypes.uint8)[off:]
on_dev = t.to(Device.DEFAULT).realize()
np.testing.assert_equal(on_dev.numpy(), t.numpy())
@slow
def test_copy_from_disk_huge(self):
fn = pathlib.Path(temp("dt_copy_from_disk_huge"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_copy_from_disk_huge"))
fn.write_bytes(bytes(range(256))*1024*256)
for off in [0, 551]:
t = Tensor.empty(256*1024*256, device=f"disk:{temp('dt_copy_from_disk_huge')}", dtype=dtypes.uint8)[off:]
t = Tensor.empty(256*1024*256, device=f"disk:{self.tmp('dt_copy_from_disk_huge')}", dtype=dtypes.uint8)[off:]
on_dev = t.to(Device.DEFAULT).realize()
np.testing.assert_equal(on_dev.numpy(), t.numpy())
@unittest.skip("this allocates a lot of RAM")
@unittest.skipUnless(OSX, "seems to only be an issue on macOS with file size >2 GiB")
def test_copy_to_cpu_not_truncated(self):
with open((fn:=temp("dt_copy_to_cpu_not_truncated")), "wb") as f: f.write(b'\x01' * (size := int(2 * 1024**3)) + (test := b"test"))
fn = self.tmp("dt_copy_to_cpu_not_truncated")
with open(fn, "wb") as f: f.write(b'\x01' * (size := int(2 * 1024**3)) + (test := b"test"))
x = Tensor.empty(size + len(test), dtype=dtypes.uint8, device=f"disk:{fn}").to("CPU").realize()
assert x[size:].data().tobytes() == test
def test_disk_device_reuse(self):
from tinygrad.runtime.ops_disk import DiskDevice
fn = pathlib.Path(temp("dt_device_reuse"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_device_reuse"))
fn.write_bytes(bytes(range(256)))
# create first tensor and realize it
t1 = Tensor.empty(128, device=f"disk:{fn}", dtype=dtypes.uint8)
@@ -466,8 +453,7 @@ class TestDiskTensor(unittest.TestCase):
def test_disk_open_failure_state(self):
from tinygrad.runtime.ops_disk import DiskDevice
fn = pathlib.Path(temp("dt_open_failure"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_open_failure"))
fn.write_bytes(bytes(range(256)))
os.chmod(fn, 0o000)
try:
@@ -486,8 +472,7 @@ class TestDiskTensor(unittest.TestCase):
assert disk_device.size == 200
def test_disk_permission_error(self):
fn = pathlib.Path(temp("dt_permission"))
fn.unlink(missing_ok=True)
fn = pathlib.Path(self.tmp("dt_permission"))
fn.write_bytes(bytes(range(256)))
os.chmod(fn, 0o000)
try:
@@ -496,17 +481,14 @@ class TestDiskTensor(unittest.TestCase):
finally:
os.chmod(fn, 0o644)
class TestPathTensor(unittest.TestCase):
class TestPathTensor(TempDirTestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
super().setUp()
self.test_file = pathlib.Path(self.temp_dir.name) / "test_file.bin"
self.test_data = np.arange(100, dtype=np.uint8).tobytes()
with open(self.test_file, "wb") as f:
f.write(self.test_data)
def tearDown(self):
self.temp_dir.cleanup()
def test_path_tensor_no_device(self):
t = Tensor(self.test_file)
self.assertEqual(t.shape, (100,))
@@ -557,10 +539,10 @@ class TestPathTensor(unittest.TestCase):
os.chmod(test_file, 0o644)
assert Tensor(pathlib.Path(test_file)).tolist(), list(range(10))
class TestDiskTensorMovement(unittest.TestCase):
class TestDiskTensorMovement(TempDirTestCase):
def setUp(self):
self.fn = pathlib.Path(temp("custom_disk_range"))
self.fn.unlink(missing_ok=True)
super().setUp()
self.fn = pathlib.Path(self.tmp("custom_disk_range"))
Tensor.arange(100, dtype=dtypes.uint8).to(f"disk:{str(self.fn)}").realize()
def test_simple_read(self):
+11 -8
View File
@@ -1,4 +1,5 @@
from typing import cast
from dataclasses import replace
import itertools
from tinygrad.helpers import DISABLE_FAST_IDIV, EMULATED_DTYPES, DEVECTORIZE, TRANSCENDENTAL, SPEC, DEBUG, getenv, TracingKey, Context
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, pm_lower_index_dtype, Ops, UPat, track_rewrites, KernelInfo, pyrender
@@ -164,17 +165,19 @@ def get_program(ast:UOp, renderer:Renderer, opts:list[Opt]|None=None) -> Program
The ProgramSpec of the program.
"""
# fix up KernelInfo
if opts is not None:
assert ast.arg is None, "can't apply opts if sink has an arg"
ast = ast.replace(arg=KernelInfo(opts_to_apply=tuple(opts)))
if ast.arg is None and ast.op is Ops.SINK: ast = ast.replace(arg=KernelInfo())
# rewrite to prg
if ast.op is Ops.PROGRAM: prg = ast
else:
elif ast.op is Ops.SINK:
# rewrite to prg
assert isinstance(ast.arg, KernelInfo), "requires KernelInfo on arg to get_program"
if opts is not None:
# TODO: should this be here?
assert ast.arg.opts_to_apply is None, "can't apply opts if there's already opts to apply"
ast = ast.replace(arg=replace(ast.arg, opts_to_apply=tuple(opts)))
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None)
prg = UOp(Ops.PROGRAM, src=(full_sink, UOp(Ops.DEVICE, arg=renderer.device)))
else:
raise RuntimeError(f"can't call get_program on {ast.op}")
prg = graph_rewrite(prg, pm_to_program, ctx=renderer, name="linearize/render")
# create the ProgramSpec
+1 -1
View File
@@ -14,7 +14,7 @@ def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
if op == Ops.MUL: return (broadcast_to_input(ctx * ret) / ret.src[0],)
def call_gradient(ctx:UOp, k:UOp):
if k.arg is not None: return (None,) + k.arg(ctx, k)
if k.arg.grad_fxn is not None: return (None,) + k.arg.grad_fxn(ctx, k)
# auto-differentiate the function
fxn, args = k.src[0], k.src[1:]
params = sorted([x for x in fxn.toposort() if x.op == Ops.PARAM], key=lambda x: x.arg)
+2 -3
View File
@@ -19,8 +19,7 @@ def realize_srcs(ctx:dict[UOp, None], rb:UOp) -> None:
def realize_assign(ctx:dict[UOp, None], a:UOp) -> None:
if a.src[1].op not in ALWAYS_CONTIGUOUS: ctx[a.src[1]] = None
# if it's a kernel, we don't realize it
if a.src[1].op is not Ops.KERNEL: ctx[a] = None
ctx[a] = None
pm_generate_realize_map = PatternMatcher([
# always realize SINK src
@@ -162,7 +161,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
rctx = IndexingContext()
# get ops to realize
graph_rewrite(tsink, pm_generate_realize_map, ctx=rctx.realize_map, name="get realize")
graph_rewrite(tsink, pm_generate_realize_map, ctx=rctx.realize_map, bottom_up=True, name="get realize")
# get the consumer map
with cpu_profile("consumer map in rangeify", "TINY"):
+12 -10
View File
@@ -68,7 +68,10 @@ def resolve_custom_kernel(ck:UOp) -> UOp:
placeholders = [UOp.placeholder_like(s, slot=i) for i,s in enumerate(ck.src)]
return UOp(Ops.KERNEL, src=ck.src, arg=Kernel(ck.arg.fxn(*placeholders)))
def resolve_call(c:UOp) -> UOp:
def resolve_call(c:UOp) -> UOp|None:
# don't resolve real kernel calls, sink or program
if c.src[0].op is Ops.SINK and isinstance(c.src[0].arg, KernelInfo): return None
if c.src[0].op is Ops.PROGRAM: return None
params = sorted([x for x in c.src[0].toposort() if x.op == Ops.PARAM], key=lambda x: x.arg)
args = c.src[1:]
# TODO: this check belongs in spec, not here
@@ -271,15 +274,14 @@ def late_buffer_view(t:UOp, b:UOp):
size = prod(shape)
# walk up for the INDEX
# NOTE: even though we allow RESHAPE and SHRINK, they can combine to form non-contiguous access patterns (e.g. t[::2])
x = t
while x.op is not Ops.INDEX:
assert x.op in {Ops.BITCAST, Ops.CONTIGUOUS, Ops.SHRINK, Ops.RESHAPE}, f"unexpected op {x.op} in buffer view walk"
while not any(u.op is Ops.INDEX for u in x.src):
assert x.op not in GroupOp.Elementwise, "can't buffer view elementwise"
x = x.src[0]
x = next(u for u in x.src if u.op is Ops.INDEX)
if len(shape) == 0: offset = x.src[1].arg
else: offset = sum(idx.vmin for idx in x.src[1:])
if offset < 0: raise RuntimeError(f"negative offset {offset} in buffer view")
else: offset = max(sum(idx.vmin for idx in x.src[1:]), 0)
return b.replace(src=(UOp(Ops.BUFFER_VIEW, t.dtype, (x.base,), (size, offset), tag=t.tag), b.src[1]))
@@ -514,10 +516,10 @@ def split_store(ctx:list[UOp], x:UOp) -> UOp|None:
elif ret.op is Ops.END and ret.src[0].op is Ops.STORE: stored = ret.src[0].src[1]
else: raise RuntimeError(f"unknown kernel type {ret.op}")
if stored.op in {Ops.COPY, Ops.BUFFER_VIEW, Ops.ENCDEC}: ret = stored
else:
ret = ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts) if lctx.opts is not None else None)
else: ret = ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts))
kernel_arg = Kernel(ret,tuple(dedup(flatten([x for x in metadatas if x is not None])))[::-1])
metadata = tuple(dedup(flatten([x for x in metadatas if x is not None])))[::-1]
kernel_arg = Kernel(ret, metadata)
kernel = UOp(Ops.KERNEL, src=tuple(lctx.map.values())+tuple(lctx.vars.keys()), arg=kernel_arg)
if ret.op is Ops.SINK and not all_same([x.device for x in kernel.src if x.op is not Ops.BIND]):
raise RuntimeError(f"all buffers must be on the same device: {tuple(b.buf_uop for b in kernel.src)}")
@@ -581,7 +583,7 @@ def get_rangeify_map(sink:UOp) -> dict[UOp, UOp]:
# bufferize -> store
lunique_start: int = max([-1]+[x.arg for x in tsink.toposort() if x.op is Ops.LUNIQUE]) + 1
tsink = graph_rewrite(tsink, pm_add_buffers+pm_add_range_tags, ctx=itertools.count(lunique_start), bottom_up=True, name="bufferize to store")
tsink = graph_rewrite(tsink, split_kernels, ctx=uop_list, name="split kernels")
tsink = graph_rewrite(tsink, split_kernels, ctx=uop_list, bottom_up=True, name="split kernels")
# if a kernel depends on a buffer, and that buffer is later assigned to, make the assign depend on the kernel's assign
kernel_assign: dict[UOp, UOp] = {}
+1 -1
View File
@@ -240,7 +240,7 @@ class Tensor(OpMixin):
param = UOp.param(slot, self.dtype, self.shape, self.device)
return Tensor(param, device=self.device)
def call(self, *lst:Tensor, fxn:Tensor|UOp, grad_fxn:Callable|None=None) -> Tensor:
return Tensor(UOp.call(*[t.uop for t in (self,)+lst], fxn=fxn.uop if isinstance(fxn, Tensor) else fxn, arg=grad_fxn), device=self.device)
return Tensor((fxn.uop if isinstance(fxn, Tensor) else fxn).call(*[t.uop for t in (self,)+lst], grad_fxn=grad_fxn), device=self.device)
def custom_kernel(self, *lst:Tensor, fxn:Callable, grad_fxn:Callable|None=None) -> list[Tensor]:
"""
+15 -2
View File
@@ -67,7 +67,8 @@ def consumer_map_from_toposort(lst:Iterable[UOp]):
ret: dict[UOp, dict[UOp, None]] = {}
for u in lst:
ret[u] = {}
for s in u.src: ret[s][u] = None
for s in u.src:
if s in ret: ret[s][u] = None
return ret
def pretty_print(x:UOp, cache=None, d=0)->str:
@@ -310,6 +311,8 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
def ended_ranges(self):
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:]]))
# TODO: copy isn't using range properly and isn't ending the range it uses, remove this
if self.op in {Ops.COPY, Ops.BUFFER_VIEW}: return self.src[0].ranges
return ()
# determine what ranges this is in
@@ -818,7 +821,9 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
src = (UOp(Ops.NOOP) if shape is None else shape_to_shape_arg(shape),) + (() if device is None else (UOp(Ops.DEVICE, arg=device),))
return UOp(Ops.PARAM, dtype, src, arg=slot)
def call(*srcs:UOp, fxn:UOp, arg:Any|None) -> UOp: return UOp(Ops.CALL, fxn.dtype, (fxn,)+srcs, arg)
def call(self, *srcs:UOp, grad_fxn:Callable|None=None, metadata:tuple[Metadata, ...]=()) -> UOp:
assert len(self.ranges) == 0, f"ranges {self.ranges} are leaking out of the call"
return UOp(Ops.CALL, self.dtype, (self,)+srcs, CallInfo(grad_fxn, metadata))
def custom_kernel(*srcs:UOp, fxn:Callable, grad_fxn:Callable|None=None) -> list[UOp]:
contig_srcs = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in srcs)
kernel = UOp(Ops.CUSTOM_KERNEL, src=contig_srcs, arg=CustomKernel(fxn=fxn, grad_fxn=grad_fxn))
@@ -843,6 +848,14 @@ class CustomKernel:
def __reduce__(self): return (CustomKernel, (panic,))
def __repr__(self): return f"CustomKernel({id(self.fxn)})"
@dataclass(frozen=True)
class CallInfo:
grad_fxn: Callable|None = None
metadata: tuple[Metadata, ...] = ()
# grad_fxn can't be pickled, but metadata can
def __reduce__(self): return (CallInfo, (None, self.metadata))
def __repr__(self): return f"CallInfo({id(self.grad_fxn) if self.grad_fxn else None}, {self.metadata})"
@dataclass(frozen=True)
class Kernel:
ast: UOp
+11 -6
View File
@@ -738,12 +738,13 @@ window.addEventListener("popstate", (e) => {
});
const createToggle = (id, text) => {
const label = d3.create("label").text(text).node();
const label = d3.create("label").style("display", "block").text(text).node();
const toggle = d3.create("input").attr("type", "checkbox").attr("id", id).property("checked", true).node();
label.prepend(toggle);
return { toggle, label };
}
const { toggle, label:toggleLabel } = createToggle("show-indexing", "Show indexing (r)");
const showIndexing = createToggle("show-indexing", "Show indexing (r)");
const showCallSrc = createToggle("show-call-src", "Show CALL src (c)");
const showGraph = createToggle("show-graph", "Show graph (g)");
showGraph.toggle.onchange = () => displaySelection(rect("#graph").width > 0 ? "#custom" : "#graph");
@@ -893,11 +894,13 @@ async function main() {
// ** center graph
const data = ret[currentRewrite];
const render = (opts) => renderDag({ data, opts }, { recenter:currentRewrite === 0 });
render({ showIndexing:toggle.checked });
toggle.onchange = (e) => render({ showIndexing:e.target.checked });
const getOpts = () => ({ showIndexing:showIndexing.toggle.checked, showCallSrc:showCallSrc.toggle.checked });
render(getOpts());
showIndexing.toggle.onchange = () => render(getOpts());
showCallSrc.toggle.onchange = () => render(getOpts());
// ** right sidebar metadata
metadata.innerHTML = "";
if (ckey.includes("rewrites")) metadata.appendChild(toggleLabel);
if (ckey.includes("rewrites")) metadata.append(showIndexing.label, showCallSrc.label);
if (step.code_line != null) metadata.appendChild(codeBlock(step.code_line, "python", { loc:step.loc, wrap:true }));
if (step.trace) {
const trace = d3.create("pre").append("code").classed("hljs", true);
@@ -1025,7 +1028,9 @@ document.addEventListener("keydown", (event) => {
document.getElementById("zoom-to-fit-btn").click();
}
// r key toggles indexing
if (event.key === "r") toggle.click();
if (event.key === "r") showIndexing.toggle.click();
// c key toggles CALL src
if (event.key === "c") showCallSrc.toggle.click();
// g key toggles graph
if (event.key === "g") showGraph.toggle.click();
});
+30 -1
View File
@@ -55,13 +55,42 @@ const layoutUOp = (g, { graph, change }, opts) => {
for (const [port, s] of src) g.setEdge(s, k, { label: edgeCounts[s] > 1 ? {type:"tag", text:edgeCounts[s]} : {type:"port", text:port}});
if (change?.includes(parseInt(k))) g.setParent(k, "overlay");
}
// optionally hide nodes from the layuot
// optionally hide nodes from the layout
if (!opts.showIndexing) {
for (const n of g.nodes()) {
const node = g.node(n);
if (node.label.includes("dtypes.index")) g.removeNode(n);
}
}
if (!opts.showCallSrc) {
// remove edges from src[0] to CALL nodes, track affected nodes
const disconnected = new Set();
for (const n of g.nodes()) {
const node = g.node(n);
if (node?.label?.startsWith("CALL\n") || node?.label === "CALL") {
for (const pred of (g.predecessors(n) || [])) {
const edge = g.edge(pred, n);
if (edge?.label?.text === 0) {
g.removeEdge(pred, n);
disconnected.add(pred);
}
}
}
}
// remove nodes that are now disconnected (no successors), only from affected subtree
let changed = true;
while (changed) {
changed = false;
for (const n of disconnected) {
if (!g.hasNode(n)) continue;
if ((g.successors(n) || []).length === 0) {
for (const pred of (g.predecessors(n) || [])) disconnected.add(pred);
g.removeNode(n);
changed = true;
}
}
}
}
dagre.layout(g);
// remove overlay node if it's empty
if (!g.node("overlay")?.width) g.removeNode("overlay");
+7 -6
View File
@@ -171,7 +171,7 @@ def option(s:int|None) -> int: return 0 if s is None else s+1
device_ts_diffs:dict[str, tuple[Decimal, Decimal]] = {}
def cpu_ts_diff(device:str, thread=0) -> Decimal: return device_ts_diffs.get(device, (Decimal(0),))[thread]
device_props:dict[str, dict] = {}
amdgpu_targets:dict[str, int] = {}
DevEvent = ProfileRangeEvent|ProfileGraphEntry|ProfilePointEvent
def flatten_events(profile:list[ProfileEvent]) -> Generator[tuple[Decimal, Decimal, DevEvent], None, None]:
@@ -308,7 +308,7 @@ def load_counters(profile:list[ProfileEvent]) -> None:
if (sqtt:=v.get(ProfileSQTTEvent)):
for e in sqtt:
if e.itrace: steps.append(create_step(f"PKTS SE:{e.se}", (f"/prg-pkts-{e.se}", len(ctxs), len(steps)),
data=(e.blob, prg_events[k].lib, device_props[e.device]["gfx_target_version"])))
data=(e.blob, prg_events[k].lib, amdgpu_targets[e.device])))
steps.append(create_step("SQTT", ("/prg-sqtt", len(ctxs), len(steps)), ((k, tag), sqtt, prg_events[k])))
ctxs.append({"name":f"Exec {name}"+(f" n{run_number[k]}" if run_number[k] > 1 else ""), "steps":steps})
@@ -348,7 +348,7 @@ def unpack_sqtt(key:tuple[str, int], data:list, p:ProfileProgramEvent) -> tuple[
# * init decoder
from extra.sqtt.roc import decode
base = unwrap(p.base)
addr_table = amd_decode(unwrap(p.lib), device_props[p.device]["gfx_target_version"])
addr_table = amd_decode(unwrap(p.lib), amdgpu_targets[p.device])
disasm:dict[int, tuple[str, int]] = {addr+base:(inst.disasm(), inst.size()) for addr, inst in addr_table.items()}
rctx = decode(data, {p.tag:disasm})
cu_events:dict[str, list[ProfileEvent]] = {}
@@ -385,8 +385,9 @@ def get_profile(profile:list[ProfileEvent], sort_fn:Callable[[str], Any]=device_
for ev in profile:
if isinstance(ev, ProfileDeviceEvent):
device_ts_diffs[ev.device] = (ev.comp_tdiff,ev.copy_tdiff if ev.copy_tdiff is not None else ev.comp_tdiff)
if ev.props is not None: device_props[ev.device] = ev.props
if (d:=ev.device.split(":")[0]) == "AMD": device_decoders[d] = load_counters
if (d:=ev.device.split(":")[0]) == "AMD":
device_decoders[d] = load_counters
amdgpu_targets[d] = unwrap(ev.props)["gfx_target_version"]
# load device specific counters
for fxn in device_decoders.values(): fxn(profile)
# map events per device
@@ -510,7 +511,7 @@ def get_render(query:str) -> dict:
if fmt == "asm":
ret:dict = {"metadata":[]}
if data.device.startswith("AMD") and data.lib is not None:
with soft_err(lambda err: ret.update(err)): ret.update(amdgpu_cfg(data.lib, device_props[data.device]["gfx_target_version"]))
with soft_err(lambda err: ret.update(err)): ret.update(amdgpu_cfg(data.lib, amdgpu_targets[data.device]))
with soft_err(lambda err: ret["metadata"].append(err)): ret["metadata"].append(amd_readelf(data.lib))
else: ret["src"] = get_stdout(lambda: (compiler:=Device[data.device].compiler).disassemble(compiler.compile(data.src)))
return ret