forked from tinygrad/tinygrad
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80cac1f26 | ||
|
|
356d107c62 | ||
|
|
6340e234bd | ||
|
|
93df4d7e77 | ||
|
|
a7bde46d07 | ||
|
|
18d687d70b | ||
|
|
b6f9095edb | ||
|
|
4fd3f9dd15 | ||
|
|
c31038ff37 | ||
|
|
a6bfc8ef50 | ||
|
|
0189ee2885 | ||
|
|
72045c38d7 | ||
|
|
12058587df | ||
|
|
2d07d18906 | ||
|
|
a296411588 | ||
|
|
60eacffe0a | ||
|
|
849d785c9d | ||
|
|
bd0f1d8920 | ||
|
|
64483543d1 | ||
|
|
c15d1b41dd | ||
|
|
069e1ff21e | ||
|
|
b092163c82 | ||
|
|
357c598fcd | ||
|
|
7a83fec3ad | ||
|
|
292c93a93a | ||
|
|
5114d1e234 | ||
|
|
8b8c4df66e | ||
|
|
cddd0f8083 | ||
|
|
92954b9baf | ||
|
|
3b3bb20a91 | ||
|
|
cddc4dcfc0 | ||
|
|
e9dd5792e8 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -111,10 +111,6 @@ docs = [
|
||||
"numpy",
|
||||
]
|
||||
mesa = ["tinymesa==25.2.7.2"]
|
||||
autogen = [
|
||||
"pyyaml",
|
||||
"mako",
|
||||
]
|
||||
|
||||
|
||||
[tool.mutmut]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user