Compare commits

..
Author SHA1 Message Date
geohot c80cac1f26 reshape shrink 2026-08-18 20:38:48 -07:00
geohot 356d107c62 180 tests 2026-08-18 18:49:52 -07:00
geohot 6340e234bd lil fixes (185) 2026-08-18 18:34:40 -07:00
geohot 93df4d7e77 201 failures 2026-08-18 18:23:22 -07:00
geohot a7bde46d07 mstack buffer identity 2026-08-18 18:16:22 -07:00
geohot 18d687d70b some fixes 2026-08-18 18:07:17 -07:00
geohot b6f9095edb flatten params for kernels 2026-08-18 16:44:00 -07:00
George HotzandGitHub 4fd3f9dd15 Merge branch 'master' into rewrite_rangeify2 2026-08-18 16:06:21 -07:00
George HotzandGitHub c31038ff37 use KernelCountException when kernel count is being compared (#17584) 2026-08-18 16:06:03 -07:00
geohot a6bfc8ef50 flatten calls 2026-08-18 15:53:39 -07:00
geohot 0189ee2885 allreduce 2026-08-18 14:56:45 -07:00
geohot 72045c38d7 fix walk in pm_split_graph 2026-08-18 14:38:10 -07:00
George HotzandGitHub 12058587df Merge branch 'master' into rewrite_rangeify2 2026-08-18 14:35:59 -07:00
geohot 2d07d18906 reshape mselect/mstack 2026-08-17 16:53:27 -07:00
geohot a296411588 mselect + mstack 2026-08-17 16:42:30 -07:00
geohot 60eacffe0a fix 0s 2026-08-17 16:27:12 -07:00
geohot 849d785c9d minor cleanups 2026-08-17 16:15:48 -07:00
geohot bd0f1d8920 fix call/function/tuple 2026-08-17 15:54:49 -07:00
geohot 64483543d1 contig fix 2026-08-17 15:46:56 -07:00
geohot c15d1b41dd fix bitcast 2026-08-17 13:51:45 -07:00
geohot 069e1ff21e fix test_symbolic 2026-08-17 11:33:00 -07:00
geohot b092163c82 stale 2026-08-17 08:14:16 -07:00
geohot 357c598fcd Merge remote-tracking branch 'origin/master' into rewrite_rangeify2 2026-08-17 08:13:11 -07:00
George HotzandGitHub 7a83fec3ad Merge branch 'master' into rewrite_rangeify2 2026-08-15 12:06:41 -07:00
geohot 292c93a93a no function in bmnist 2026-08-11 17:47:48 -07:00
George HotzandGitHub 5114d1e234 Merge branch 'master' into rewrite_rangeify2 2026-08-11 16:06:51 -07:00
geohot 8b8c4df66e weakint issue for symbolic 2026-08-11 14:51:12 -07:00
geohot cddd0f8083 test tiny 2026-08-11 14:39:31 -07:00
geohot 92954b9baf don't recompute 2026-08-11 13:43:21 -07:00
geohot 3b3bb20a91 consumers 2026-08-11 12:10:27 -07:00
geohot cddc4dcfc0 split kernels 2026-08-11 10:58:28 -07:00
geohot e9dd5792e8 clean slate rangeify rewrite 2026-08-11 10:47:42 -07:00
14 changed files with 382 additions and 45 deletions
+6 -14
View File
@@ -42,11 +42,7 @@ inputs:
required: false
default: 'false'
qemu:
description: "Install qemu?"
required: false
default: 'false'
ninja:
description: "Install ninja?"
description: "Install qemu"
required: false
default: 'false'
runs:
@@ -134,7 +130,7 @@ runs:
# ******************* apt *******************
- name: Setup apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
shell: bash
run: |
sudo mkdir -p /var/cache/apt/archives
@@ -162,7 +158,7 @@ runs:
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
- name: Compute Package List + Hash
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
id: apt-pkgs
shell: bash
run: |
@@ -187,29 +183,25 @@ runs:
if [[ "${{ inputs.qemu }}" == "true" ]]; then
pkgs+=" qemu-user-static"
fi
# **** ninja ****
if [[ "${{ inputs.ninja }}" == "true" ]]; then
pkgs+=" ninja-build"
fi
echo "pkgs=$pkgs" >> "$GITHUB_OUTPUT"
echo "hash=$(echo -n "$pkgs" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Cache apt (PR)
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name == 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name == 'pull_request'
uses: actions/cache/restore@v5
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Cache apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name != 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name != 'pull_request'
uses: actions/cache@v5
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Run apt Update + Install
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
shell: bash
run: |
sudo apt -qq update || true
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
key: 'autogen'
amd: 'true'
llvm: 'true'
deps: 'autogen'
pydeps: 'pyyaml mako'
- name: Install autogen support packages
run: sudo apt-get install -y --no-install-recommends libclang-20-dev llvm-20-dev hip-dev libusb-1.0-0-dev libdrm-dev liburing-dev
- name: Regenerate autogen files
+11 -3
View File
@@ -31,7 +31,8 @@ jobs:
- name: Setup Environment
uses: ./.github/actions/setup-tinygrad
with:
deps: "docs testing_minimal"
deps: docs
pydeps: "capstone torch"
- name: Build wheel and show size
run: |
uv build --wheel
@@ -72,7 +73,10 @@ jobs:
deps: testing_unit
pydeps: "pillow torchvision expecttest"
llvm: 'true'
ninja: 'true'
- name: Install ninja
run: |
sudo apt update || true
sudo apt install -y --no-install-recommends ninja-build
- name: Test ResNet-18
run: DEBUG=2 python3 extra/torch_backend/example.py
- name: Test one op in torch tests
@@ -94,8 +98,12 @@ jobs:
with:
key: torch-backend-pillow-torchvision-et-pt
deps: testing_unit
pydeps: "pillow torchvision expecttest"
llvm: 'true'
ninja: 'true'
- name: Install ninja
run: |
sudo apt update || true
sudo apt install -y --no-install-recommends ninja-build
- name: Test beautiful_mnist in torch with TINY_BACKEND
run: STEPS=20 DEV=CPU TARGET_EVAL_ACC_PCT=90.0 MAX_BUFFER_SIZE=0 TINY_BACKEND=1 python3 examples/other_mnist/beautiful_mnist_torch.py
+1 -2
View File
@@ -1,6 +1,6 @@
# model based off https://medium.com/data-science/going-beyond-99-mnist-handwritten-digits-recognition-cfff96337392
from typing import Callable
from tinygrad import Tensor, TinyJit, nn, GlobalCounters, function, Context
from tinygrad import Tensor, TinyJit, nn, GlobalCounters, Context
from tinygrad.helpers import getenv, colored, trange
from tinygrad.nn.datasets import mnist
@@ -15,7 +15,6 @@ class Model:
nn.BatchNorm(64), Tensor.max_pool2d,
lambda x: x.flatten(1), nn.Linear(576, 10)]
@function
def __call__(self, x:Tensor) -> Tensor: return x.sequential(self.layers)
@TinyJit
-4
View File
@@ -111,10 +111,6 @@ docs = [
"numpy",
]
mesa = ["tinymesa==25.2.7.2"]
autogen = [
"pyyaml",
"mako",
]
[tool.mutmut]
+3 -3
View File
@@ -4,7 +4,7 @@ import numpy as np
from tinygrad.dtype import AddrSpace, dtypes, Invalid
from tinygrad.uop.ops import KernelInfo, AxisType, Ops
from tinygrad.renderer.ptx import PTXRenderer
from test.helpers import assert_kernel_count
from test.helpers import assert_kernel_count, KernelCountException
# **** kernels ****
@@ -474,7 +474,7 @@ class TestCustomKernelInput(unittest.TestCase):
y.realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), x.add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
# same test with @function, input is PARAM
from tinygrad import function
x0 = Tensor.arange(32).clone("CPU").realize()
@@ -487,7 +487,7 @@ class TestCustomKernelInput(unittest.TestCase):
y = run(x0).realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), mop_fxn(x0).add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
if kernel_count > max_kernels: raise KernelCountException(max_kernels, kernel_count)
def test_reshape(self): self._test_mop(lambda x: x.reshape(16, 2), max_kernels=2)
def test_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T, max_kernels=3)
+2 -2
View File
@@ -6,7 +6,7 @@ from tinygrad.nn.state import get_parameters
from tinygrad.engine.realize import run_linear, compile_linear, pm_beam, pm_compile
import numpy as np
from hypothesis import given, strategies as strat, settings
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count, KernelCountException
settings.register_profile("my_profile", max_examples=200, deadline=None, derandomize=getenv("DERANDOMIZE_CI", False))
settings.load_profile("my_profile")
@@ -395,7 +395,7 @@ class TestMultiBufferView(unittest.TestCase):
linear, var_vals = b_multi.linear_with_vars()
if all(not d.startswith(("WEBGPU", "CL")) for d in b_multi.device):
compiled = [call for call in linear.src if call.src[0].op is Ops.SINK]
self.assertEqual(len(compiled), 0, f"expected zero compiled kernels, got {len(compiled)}")
if len(compiled) != 0: raise KernelCountException(0, len(compiled))
run_linear(linear, var_vals)
np.testing.assert_equal(b_multi.numpy(), b_ref.numpy())
+2 -1
View File
@@ -3,6 +3,7 @@ import numpy as np
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
from tinygrad.uop.ops import PatternMatcher, UPat, UOp, deconstruct_function
from test.helpers import KernelCountException
class TestPickle(unittest.TestCase):
def test_pickle_code_object(self):
@@ -41,7 +42,7 @@ class TestPickle(unittest.TestCase):
t2:Tensor = pickle.loads(st)
np.testing.assert_equal(t_values, t2.numpy())
# expect at most one COPY kernel
self.assertLessEqual(GlobalCounters.kernel_count, 1)
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count)
def test_pickle_realized_tensor_alt(self):
print("** init")
+2 -1
View File
@@ -1,6 +1,7 @@
import unittest
from tinygrad.helpers import GlobalCounters
from tinygrad.nn.datasets import mnist
from test.helpers import KernelCountException
class TestDataset(unittest.TestCase):
def test_dataset_is_realized(self):
@@ -8,7 +9,7 @@ class TestDataset(unittest.TestCase):
X_train[0].contiguous().realize()
GlobalCounters.reset()
X_train[0].contiguous().realize()
self.assertLessEqual(GlobalCounters.kernel_count, 1) # 0 if SLICE (zero-copy), 1 otherwise
if GlobalCounters.kernel_count > 1: raise KernelCountException(1, GlobalCounters.kernel_count) # 0 if SLICE (zero-copy), 1 otherwise
if __name__ == '__main__':
unittest.main()
+6 -5
View File
@@ -2,6 +2,7 @@ import unittest
from tinygrad import Tensor, UOp, dtypes
from tinygrad.helpers import Context
from tinygrad.uop.ops import Ops
from test.helpers import KernelCountException
class TestRingAllReduce(unittest.TestCase):
def test_schedule_ring(self):
@@ -13,7 +14,7 @@ class TestRingAllReduce(unittest.TestCase):
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
# N*(N-1) scatter reduce, and N*(N-1) allgather
self.assertEqual(len(pairs), N*(N-1)*2)
if len(pairs) != N*(N-1)*2: raise KernelCountException(N*(N-1)*2, len(pairs))
# copy topology forms a ring
self.assertEqual(len(set(pairs)), N)
@@ -25,8 +26,8 @@ class TestRingAllReduce(unittest.TestCase):
linear = t.sum(0).mul(2.0).contiguous().linear_with_vars()[0]
copies = [si for si in linear.src if si.src[0].op is Ops.COPY]
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
self.assertEqual(len(copies), 24)
self.assertEqual(len(sinks), 26)
if len(copies) != 24: raise KernelCountException(24, len(copies))
if len(sinks) != 26: raise KernelCountException(26, len(sinks))
@Context(RING=0, ALL2ALL=0)
def test_schedule_naive(self):
@@ -39,8 +40,8 @@ class TestRingAllReduce(unittest.TestCase):
sinks = [si for si in linear.src if si.src[0].op is Ops.SINK]
pairs = [(c.src[1].buffer.device, c.src[2].buffer.device) for c in copies]
self.assertEqual(len(pairs), N*(N-1))
self.assertEqual(len(sinks), 2)
if len(pairs) != N*(N-1): raise KernelCountException(N*(N-1), len(pairs))
if len(sinks) != 2: raise KernelCountException(2, len(sinks))
self.assertTrue(all(dst != src for dst, src in pairs))
def test_symbolic_shape(self):
+2 -2
View File
@@ -4,7 +4,7 @@ from tinygrad.function import function
from tinygrad import Tensor, GlobalCounters, Device
from tinygrad.dtype import Invalid
from tinygrad.uop.ops import UOp, Ops, KernelInfo, ProgramInfo
from test.helpers import assert_kernel_count
from test.helpers import assert_kernel_count, KernelCountException
class TestFunction(unittest.TestCase):
def test_simple(self):
@@ -516,7 +516,7 @@ class TestFunctionTuple(unittest.TestCase):
Tensor.realize(a)
c = f(a)
self.assertEqual(count_kernels(c), 1)
if count_kernels(c) != 1: raise KernelCountException(1, count_kernels(c))
c.sum().backward()
Tensor.realize(a.grad)
+2 -1
View File
@@ -80,7 +80,8 @@ def create_schedule(sched_sink:UOp) -> UOp:
from tinygrad.schedule.memory import memory_plan_rewrite
from tinygrad.engine.realize import capturing, pm_flatten_linear
from tinygrad.schedule.rangeify import get_kernel_graph
#from tinygrad.schedule.rangeify import get_kernel_graph
from tinygrad.schedule.rangeify2 import get_kernel_graph
from tinygrad.helpers import CAPTURING
from tinygrad.uop.ops import PatternMatcher, UPat, ParamArg
from tinygrad.dtype import AddrSpace
+337
View File
@@ -0,0 +1,337 @@
from dataclasses import dataclass, field
import itertools
from tinygrad.dtype import AddrSpace, Invalid, to_dtype
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, GroupOp, KernelInfo, ParamArg, shape_to_shape_arg
from tinygrad.uop.ops import graph_rewrite, AxisType, rewrite_group, identity_element, remove_all_tags, resolve
from tinygrad.helpers import all_int, VIZ, SPEC, Context, panic
from tinygrad.schedule.indexing import BufferizeOpts, apply_movement_op
from tinygrad.schedule.multi import multi_pm
from tinygrad.schedule.allreduce import create_allreduce_function
def walk_mop(u:UOp):
if u.op in GroupOp.Movement or u.op in {Ops.INDEX, Ops.UNSHARD}: return walk_mop(u.src[0])
return u
fix_mselect_mstack = PatternMatcher([
# move RESHAPEs through MSELECT/MSTACK
(UPat((Ops.MSELECT, Ops.MSTACK), src=UPat(Ops.RESHAPE), name="m"),
lambda m: m.replace(src=tuple([x.src[0].base for x in m.src])).reshape(m.shape)),
])
# *** preparation ***
from tinygrad.helpers import all_same
from tinygrad.uop.ops import _broadcast_shape
def expand_broadcast(x:UOp):
shapes = [u._shape for u in x.src]
if any(s is None for s in shapes) or all_same(shapes): return None
shape = _broadcast_shape(*shapes)
return x.replace(src=tuple([u.expand(shape) for u in x.src]))
# shape-changing bitcast
def expand_bitcast(bc:UOp) -> UOp|None:
x = bc.src[0]
if (ns:=bc.dtype.itemsize) == (os:=x.dtype.itemsize) or (isinstance(x.device, str) and x.device.startswith(("DISK", "TINYFS"))): return None
new_uint, tmp = to_dtype(f"uint{8*ns}"), x.bitcast(to_dtype(f"uint{8*os}"))
if ns > os:
tmp = tmp.reshape(x.shape[:-1] + (x.shape[-1]//(rate := ns//os), rate))
parts = [tmp.shrink((None,)*(len(tmp.shape)-1) + ((i, i+1),)).cast(new_uint)<<8*i*os for i in range(rate)]
return parts[0].usum(*parts[1:]).squeeze(-1).bitcast(bc.dtype)
parts = [tmp>>8*i*ns for i in range(os//ns)]
return parts[0].stack(*parts[1:], dim=-1).flatten(-2).cast(new_uint).bitcast(bc.dtype)
pm_gather_params = PatternMatcher([ (UPat(Ops.PARAM, name="p"), lambda ctx, p: ctx.append(p) if p.arg.slot >= 0 else None), ])
def resolve_function(c:UOp, allow_param_mismatch=True) -> UOp|None:
if c.arg.precompile: return None
params: list[UOp] = []
graph_rewrite(c.src[0], pm_gather_params, bottom_up=True, ctx=params, name="gather params")
params = sorted(params, key=lambda x: x.arg.slot)
args = c.src[1:]
# NOTE: this isn't really needed. it's okay if there's unused args in the function
if not allow_param_mismatch:
if [x.arg.slot for x in params] != list(range(len(params))): raise RuntimeError(f"params not in order: {[x.arg.slot for x in params]}")
if len(params) != len(args): raise TypeError(f"expected {len(params)} args, got {len(args)}")
dict_map = {x:args[x.arg.slot] for x in params}
for i, (p, a) in enumerate(dict_map.items()):
if p.axis != a.axis: raise TypeError(f"arg {i} axis mismatch: expected {p.axis}, got {a.axis}")
if p.max_shape != a.max_shape: raise TypeError(f"arg {i} shape mismatch: expected {p.shape}, got {a.shape}")
if p.dtype != a.dtype: raise TypeError(f"arg {i} dtype mismatch: expected {p.dtype}, got {a.dtype}")
return c.src[0].substitute(dict_map, walk=True)
def fix_store_hazard(target:UOp, src:UOp):
if (base:=target.base) not in src.toposort(enter_calls=False): 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())
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)
if reaches_base[s] and s.op in unsafe and not (s is target and s.op is Ops.SHRINK): return target.store(src.contiguous())
pm_prepare_graph = PatternMatcher([
# CALL inputs need buffer identity (and to be flat)
(UPat(Ops.CALL, name="c"),
lambda c: c.replace(src=c.src[0:1]+tuple(x.contiguous() if not x.has_buffer_identity(after_ok=True) else x for x in c.src[1:]))),
# MSTACK inputs need buffer identity
(UPat(Ops.MSTACK, name="c"),
lambda c: c.replace(src=tuple(x.contiguous() if not x.has_buffer_identity(after_ok=True) else x for x in c.src))),
# resolve FUNCTION calls (inline the body)
(UPat(Ops.FUNCTION, name="c"), resolve_function),
# resolve allreduce (must be bottom up)
(UPat(Ops.ALLREDUCE, src=(UPat.var("buf"),), name="red"), create_allreduce_function),
# resolve TUPLE+GETTUPLE
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
# expand broadcasts first
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), expand_broadcast),
# also expand bitcasts
(UPat(Ops.BITCAST, name="bc"), expand_bitcast),
# move movement ops and INDEX after AFTER
(UPat(GroupOp.Movement|{Ops.INDEX}, name="r").after(name="a", allow_any_len=True),
lambda r,a: UOp(r.op, src=(a.replace(src=(r.src[0],)+a.src[1:]),)+r.src[1:], arg=r.arg)),
# remove movement ops from SINK/AFTER. TODO: should be generic
(UPat(Ops.SINK, name="s"), lambda s: s.replace(src=tuple(walk_mop(u) for u in s.src if u.op is not Ops.NOOP))),
(UPat(Ops.AFTER, name="s"), lambda s: s.replace(src=(s.src[0],)+tuple(walk_mop(u) for u in s.src[1:] if u.op is not Ops.NOOP))),
# reduce of size 0 is the identity element
(UPat(Ops.REDUCE, name="reduce", src=(UPat.var("x"),)),
lambda reduce,x: reduce.const_like(identity_element(reduce.arg[0], reduce.dtype)) if 0 in x.shape and 0 not in reduce.shape else None),
# STORE to () is reshaped to (1,)
(UPat(Ops.STORE, name="s"), lambda s: s.src[0].reshape((1,)).store(s.src[1].reshape((1,))) if s.shape == () else None),
# size 0 STORE is NOOP
(UPat(Ops.STORE, name="s"), lambda s: UOp(Ops.NOOP) if 0 in s.shape else None),
# fix store hazard (dest is in used in src) by adding contiguous: TestAssign.test_post_flipped_assignment
(UPat(Ops.STORE, src=(UPat(name="target"), UPat(name="src"))), fix_store_hazard),
])
def convert_copy_to_store(ctx, copy:UOp, existing_buf:UOp|None=None):
input_src = copy.src[0]
# if it's a COPY, we need to give the input buffer identity
if not input_src.has_buffer_identity(after_ok=True) and copy.op is Ops.COPY: input_src = input_src.contiguous()
input_src = input_src.flatten()
if existing_buf is not None:
# if the existing buffer is not a full buffer, we can't use it
if not existing_buf.has_buffer_identity(after_ok=True): return None
# if there's already a buffer, we just use it
return existing_buf.flatten().store(input_src)
# create the output buffer
buf = UOp(Ops.BUFFER, src=(shape_to_shape_arg(input_src.max_shape),), arg=ParamArg(next(ctx), copy.dtype, device=copy.device))
# reshape back to input
return buf.after(buf.store(input_src)).reshape(copy.max_shape).shrink_to(copy.shape)
pm_copy_to_store = PatternMatcher([
(UPat(name="existing_buf").store(UPat(Ops.COPY, name="copy")), convert_copy_to_store),
(UPat((Ops.COPY, Ops.CONTIGUOUS), name="copy"), convert_copy_to_store),
])+fix_mselect_mstack
# *** RANGE creation ***
def rangeify_on_reduce(ctx, inp:UOp, red:UOp, idx:UOp|None=None):
if red.arg[1] == 0: return None
if idx is None and len(red.shape) > 0: return None
# TODO: is AxisType.REDUCE a real thing?
rngs = [UOp.range(s, next(ctx), AxisType.REDUCE) for s in inp.shape[:red.arg[1]]]
return inp.index(*rngs, *(idx.src[1:] if idx is not None else ())).reduce(*rngs, arg=(red.arg[0], 0))
def rangeify_on_store(ctx, x:UOp):
if x.shape == (): return None
rngs = [UOp.range(s, next(ctx)) for s in x.shape]
return x.src[0].index(*rngs).store(x.src[1].index(*rngs)).end(*rngs)
def rangeify_on_stage(ctx, x:UOp):
if x.src[0].shape == (): return None
# size 1 dims don't get ranges, they are reshaped out and back in
if all_int(x.shape) and 0 < len(sq := tuple(s for s in x.shape if s != 1)) < len(x.shape):
return rangeify_on_stage(ctx, x.src[0].reshape(sq).bufferize(arg=x.arg)).reshape(x.shape)
rngs = [UOp.range(s, next(ctx)) for s in x.shape]
return x.replace(src=(x.src[0].index(*rngs), *rngs))
pm_range_creation = PatternMatcher([
# reduce/store are what creates ranges
(UPat(Ops.REDUCE, src=(UPat.var('inp'),), name="red").index(name="idx", allow_any_len=True), rangeify_on_reduce),
(UPat(Ops.REDUCE, src=(UPat.var('inp'),), name="red"), rangeify_on_reduce),
(UPat(Ops.STORE, name="x"), rangeify_on_store),
(UPat(Ops.STAGE, name="x"), rangeify_on_stage),
])
# *** RANGE migration ***
# movement op on INDEX as a PatternMatcher
def _mop_index(r:UOp, idx:UOp):
idxs = idx.src[1:]
if len(idxs) == len(r.shape):
ret = r.src[0].index(*apply_movement_op(r.op, r.src[0].shape, r.marg, idxs), dtype=idx.dtype, arg=idx.arg)
if r.op is Ops.PAD:
# insert 0 for PAD with where
# TODO: does this need simplify to ensure the Invalids are at the base?
a = UOp.const(True)
for s in ret.src[1:]:
if s.op is Ops.WHERE and s.src[2].op is Ops.CONST and s.src[2].arg == Invalid: a = a & s.src[0]
ret = a.where(ret, ret.const_like(0))
return ret
if r.op is Ops.RESHAPE:
src_prefix = len(r.src[0].shape) - len(r.shape[len(idxs):])
if src_prefix >= 0 and r.src[0].shape[src_prefix:] == r.shape[len(idxs):]:
if src_prefix == 0: return r.src[0] if r.src[0].dtype == idx.dtype else None
ret = r.src[0].index(*apply_movement_op(r.op, r.src[0].shape[:src_prefix], r.shape[:len(idxs)], idxs), dtype=idx.dtype, arg=idx.arg)
return ret if ret.shape == idx.shape else None
# TODO: this should be in _mop_index
def index_on_stack(stack:UOp, idx:UOp):
srcs = [s.index(*idx.src[2:]) for s in stack.src]
r0 = idx.src[1]
ret = srcs[-1]
for k in range(len(srcs)-2, -1, -1): ret = r0.eq(k).where(srcs[k], ret)
return ret
pm_range_migration = PatternMatcher([
# STAGE on shape () is nothing
(UPat(Ops.STAGE, src=(UPat.var('x'),)), lambda x: x if x.shape == () else None),
# if INDEX is on STAGE with the same ranges, remove the pair
(UPat(Ops.STAGE, allow_any_len=True, name="s").index(allow_any_len=True, name="i"),
lambda s,i: s.src[0] if s.src[1:] == i.src[1:] else None),
# reshape of a single element shaped value to scalar is an index
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(0) if x.marg == () and x.src[0].shape == (1,) else None),
# handle movement ops on INDEX
(UPat(GroupOp.Movement, name="r").index(name="idx", allow_any_len=True), _mop_index),
(UPat(Ops.STACK, name="stack").index(name="idx", allow_any_len=True), index_on_stack),
# move movement ops and INDEX after AFTER
(UPat(GroupOp.Movement|{Ops.INDEX}, name="r").after(name="a", allow_any_len=True),
lambda r,a: UOp(r.op, src=(a.replace(src=(r.src[0],)+a.src[1:]),)+r.src[1:], arg=r.arg)),
# block bitcast that changes shape
(UPat(Ops.BITCAST, name="b").index(allow_any_len=True),
lambda b: panic(RuntimeError, "shape changing bitcast not allowed in rangeify") if b.src[0].shape != b.shape else None),
# pass index through elementwise
(UPat(GroupOp.Elementwise, name="b").index(name="idx", allow_any_len=True),
lambda b,idx: b.replace(src=tuple(s.index(*idx.src[1:]) for s in b.src))),
# INDEX without src is nothing (must be at the bottom)
(UPat(Ops.INDEX, src=(UPat.var('x'),)), lambda x: x),
])
# *** split into kernels ***
@dataclass
class SplitCtx:
call_args:list = field(default_factory=list)
range_number:int = -1
addrspace:AddrSpace = AddrSpace.GLOBAL
def _split_graph(ctx:SplitCtx, u:UOp) -> UOp|None:
if u.tag is not None: return None
if u.addrspace != ctx.addrspace: return None
us = u.flatten() if u.addrspace == AddrSpace.GLOBAL else u
ctx.call_args.append(us)
return us.param_like(len(ctx.call_args)-1).rtag().reshape(u.shape)
def _renumber_range(ctx:SplitCtx, u:UOp) -> UOp|None:
if u.tag is not None: return None
ctx.range_number += 1
return u.replace(arg=(ctx.range_number, u.arg[-1])).rtag()
pm_split_graph = pm_range_migration+PatternMatcher([
(UPat((Ops.PARAM, Ops.AFTER, Ops.BUFFER, Ops.MSELECT, Ops.MSTACK), name="u"), _split_graph),
(UPat(Ops.RANGE, name="u"), _renumber_range),
])
def split_store(x:UOp) -> UOp:
ret = graph_rewrite(x, pm_split_graph, ctx:=SplitCtx(), name="split kernel", bottom_up=True)
# TODO: params and args should be able to be in any order
ctx.addrspace = AddrSpace.ALU
ret = graph_rewrite(ret, pm_split_graph, ctx, name="split kernel (vars)", bottom_up=True)
ret = graph_rewrite(ret, remove_all_tags, name="remove split tags", bottom_up=True)
return ret.sink(arg=KernelInfo()).call(*ctx.call_args)
split_kernels = PatternMatcher([
(UPat((Ops.STORE, Ops.END), name="x"), split_store),
])
# cleanups
def strip_zero_offset_shrink(x:UOp) -> UOp:
return x.src[0] if x.op is Ops.SHRINK and all(resolve(start == 0, False) for start,_ in x.marg) else x
def no_indexing_calls(u:UOp):
new_srcs = []
for x in u.src:
if x.op is Ops.INDEX:
# sometimes if call srcs have children the call will get an INDEX. we remove it here.
# TODO: we should add safety checks here for contiguous
new_srcs.append(x.src[0])
elif x.op is Ops.SHRINK:
# SHRINK with offset 0 is fine
new_srcs.append(strip_zero_offset_shrink(x))
elif x.op is Ops.MSTACK:
new_srcs.append(x.replace(src=tuple(strip_zero_offset_shrink(s) for s in x.src)))
else:
# everything else we pass through
new_srcs.append(x)
return u.replace(src=tuple(new_srcs))
pm_no_indexing_calls = PatternMatcher([
(UPat(Ops.CALL, name="u"), no_indexing_calls),
])
# *** main rangeify ***
debug_tag_factor = PatternMatcher([
(UPat(GroupOp.All, name="x"), lambda ctx,x: x.rtag(ctx[0][x] if x not in ctx[1] else 'REAL') if x.tag is None else None),
])
def remove_stage(ctx, x:UOp) -> UOp:
buf = UOp.new_buffer(x.arg.device, x.max_numel(), x.dtype, num=next(ctx))
return buf.after(buf.reshape(x.shape).index(*x.src[1:]).store(x.src[0]).end(*x.src[1:])).reshape(x.shape)
pm_remove_stage = PatternMatcher([
(UPat(Ops.STAGE, name="x"), remove_stage),
])+fix_mselect_mstack
@rewrite_group(new_ctx=False)
def get_kernel_graph(sink:UOp) -> UOp:
# TODO: multi should just be part of rangeify
tsink = graph_rewrite(sink, multi_pm, name="multi_pm")
# prepare
tsink = graph_rewrite(tsink, pm_prepare_graph, bottom_up=True, name="prepare graph")
tsink = graph_rewrite(tsink, pm_copy_to_store, ctx=itertools.count(0), bottom_up=True, name="convert copy to store")
# add safe STAGEs to never duplicate compute
# we compute the number of times a buffer is consumed. if > 1, we realize
realize = {}
consumes = {tsink:0}
for u in reversed(tsink.toposort()):
assert u in consumes, f"{u.op} not in consumes"
if (u.op in GroupOp.ALU or u.op is Ops.REDUCE) and consumes[u] > 1 and u.device is not None:
# TODO: rename to stage
realize[u] = u.rtag(1).bufferize(arg=BufferizeOpts(device=u.device))
consumes[u] = 1
if u.op is Ops.STORE: consumes[u] = 1
if u.op is Ops.EXPAND: consumes[u] *= u.max_numel() // u.src[0].max_numel()
for i,s in enumerate(u.src):
if s not in consumes: consumes[s] = 0
if u.op is not Ops.STORE or i > 0:
consumes[s] += consumes[u]
if VIZ:
with Context(TRACK_MATCH_STATS=0): ctags = graph_rewrite(tsink, debug_tag_factor, ctx=(consumes, realize), bottom_up=True)
graph_rewrite(ctags, PatternMatcher([]), name="View Consumes")
# add stages
tsink = graph_rewrite(tsink.substitute(realize), remove_all_tags, name="untag")
# simple rangeify
tsink = graph_rewrite(tsink, pm_range_creation+pm_range_migration, ctx=itertools.count(0), bottom_up=True, name="simple rangeify")
# TODO: merging and splitting algorithm
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Rangeify")
tsink = graph_rewrite(tsink, pm_remove_stage, ctx=itertools.count(0), bottom_up=True, name="remove stage")
tsink = graph_rewrite(tsink, split_kernels, bottom_up=True, name="split kernels")
tsink = graph_rewrite(tsink, pm_no_indexing_calls, name="remove indexing from call args")
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Kernel Graph")
if SPEC:
# validate the kernel graph
from tinygrad.uop.spec import type_verify, spec_kernel_graph
type_verify(tsink, spec_kernel_graph, enter_calls=False)
return tsink
+7 -6
View File
@@ -1163,12 +1163,13 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
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))
def param_like(self, slot:int):
# Variables become ALU params in the call body; the stored value (if bound) stays in the call args
if self.is_bound_var or self.is_variable:
b = self.src[0] if self.op is Ops.AFTER else self
return UOp(Ops.PARAM, src=b.src, arg=replace(b.arg, slot=slot, name=f"p{slot}"))
addrspace = self.addrspace if self.addrspace is not None else AddrSpace.GLOBAL
return UOp.param(slot, self.dtype, self.shard_shape if self.axis is not None else self._shape, self.device, addrspace=addrspace, axis=self.axis)
# if it's a PARAM or BUFFER, we just replace the slot
buf = self
while buf.op is Ops.AFTER: buf = buf.src[0]
if buf.op in {Ops.PARAM, Ops.BUFFER}: return UOp(Ops.PARAM, src=buf.src, arg=replace(buf.arg, slot=slot))
# otherwise we create a new param
addrspace = buf.addrspace if buf.addrspace is not None else AddrSpace.GLOBAL
return UOp.param(slot, buf.dtype, buf.shard_shape if buf.axis is not None else buf._shape, self.device, addrspace=addrspace, axis=buf.axis)
@staticmethod
def custom_function(name:str, *src:UOp) -> UOp: return UOp(Ops.CUSTOM_FUNCTION, src=src, arg=name)