mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-18 02:18:27 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42c1f4d5b6 | ||
|
|
e36288d047 | ||
|
|
faf7fb7513 | ||
|
|
7d0c5ab689 | ||
|
|
41c14d3558 | ||
|
|
eaa362f49e | ||
|
|
32138c2418 | ||
|
|
ebf45b4f34 | ||
|
|
69e1f3b551 | ||
|
|
2172363be5 | ||
|
|
420a08c6d1 | ||
|
|
c6a82fe927 | ||
|
|
3844a31f87 | ||
|
|
316607f004 | ||
|
|
bdcdf1f1a1 | ||
|
|
a613bcfc6d | ||
|
|
7c3e3fa154 | ||
|
|
da3b7e89a4 | ||
|
|
25583f6dc1 | ||
|
|
64c81dfd24 |
@@ -49,6 +49,10 @@ inputs:
|
||||
description: "Install tinydreno"
|
||||
required: false
|
||||
default: 'false'
|
||||
qemu:
|
||||
description: "Install qemu"
|
||||
required: false
|
||||
default: 'false'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -129,7 +133,7 @@ runs:
|
||||
|
||||
# ******************* apt *******************
|
||||
- name: Setup apt
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
shell: bash
|
||||
run: |
|
||||
sudo chown -R $USER:$USER /var/cache/apt/archives
|
||||
@@ -161,7 +165,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.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
id: apt-pkgs
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -177,10 +181,10 @@ runs:
|
||||
if [[ "${{ inputs.amd }}" == "true" ]]; then
|
||||
pkgs+=" hsa-rocr comgr hsa-rocr-dev liburing-dev libibverbs-dev libc6-dev"
|
||||
fi
|
||||
# **** CUDA ****
|
||||
if [[ "${{ inputs.cuda }}" == "true" ]]; then
|
||||
# **** ocelot (dependencies) ****
|
||||
if [[ "${{ inputs.ocelot }}" == "true" ]]; then
|
||||
pkgs+=" git g++ cmake ninja-build llvm-15-dev zlib1g-dev libglew-dev \
|
||||
flex bison libfl-dev libboost-thread-dev libboost-filesystem-dev nvidia-cuda-toolkit-gcc libzstd-dev"
|
||||
flex bison libfl-dev libboost-thread-dev libboost-filesystem-dev libzstd-dev"
|
||||
fi
|
||||
# **** WebGPU (dependencies for software-based vulkan) ****
|
||||
if [[ "${{ inputs.webgpu }}" == "true" ]]; then
|
||||
@@ -190,25 +194,29 @@ runs:
|
||||
if [[ "${{ inputs.llvm }}" == "true" ]]; then
|
||||
pkgs+=" libllvm20 clang-20 lld-20"
|
||||
fi
|
||||
# **** QEMU ****
|
||||
if [[ "${{ inputs.qemu }}" == "true" ]]; then
|
||||
pkgs+=" qemu-user-static"
|
||||
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.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true') && github.event_name == 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name == 'pull_request'
|
||||
uses: actions/cache/restore@v4
|
||||
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.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true') && github.event_name != 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == '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.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt -qq update || true
|
||||
@@ -239,6 +247,17 @@ runs:
|
||||
jq -r '.assets[] | select(.name == "libamd_comgr.dylib").browser_download_url' | \
|
||||
sudo xargs curl -fL -o /usr/local/lib/libamd_comgr.dylib
|
||||
|
||||
# **** CUDA ****
|
||||
- name: Install CUDA
|
||||
if: inputs.cuda == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /usr/local/cuda/targets/x86_64-linux
|
||||
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/linux-x86_64/cuda_nvrtc-linux-x86_64-11.5.119-archive.tar.xz \
|
||||
| sudo tar -xJ -C /usr/local/cuda/targets/x86_64-linux --strip-components=1
|
||||
echo /usr/local/cuda/targets/x86_64-linux/lib | sudo tee /etc/ld.so.conf.d/cuda-nvrtc.conf
|
||||
sudo ldconfig
|
||||
|
||||
# **** gpuocelot ****
|
||||
|
||||
- name: Install gpuocelot dependencies (MacOS)
|
||||
@@ -286,6 +305,11 @@ runs:
|
||||
if [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
CMAKE_ARGS="$CMAKE_ARGS -DBoost_INCLUDE_DIR=$(brew --prefix boost)/include -DBoost_LIBRARY_DIR=$(brew --prefix boost)/lib"
|
||||
else
|
||||
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvcc/linux-x86_64/cuda_nvcc-linux-x86_64-11.5.119-archive.tar.xz \
|
||||
| sudo tar -xJ -C /usr/ --strip-components=1
|
||||
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-11.5.117-archive.tar.xz \
|
||||
| sudo tar -xJ -C /usr/ --strip-components=1
|
||||
fi
|
||||
|
||||
cmake .. $CMAKE_ARGS
|
||||
|
||||
@@ -594,17 +594,7 @@ jobs:
|
||||
deps: testing_unit
|
||||
pydeps: "onnx==1.18.0 onnxruntime ml_dtypes"
|
||||
llvm: "true"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Build QEMU Docker with cache
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
file: extra/dsp/Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: qemu-hexagon:latest
|
||||
cache-from: type=gha
|
||||
cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=min' || '' }}
|
||||
qemu: "true"
|
||||
- name: Set MOCKDSP env
|
||||
run: printf "MOCKDSP=1" >> $GITHUB_ENV
|
||||
- name: Run test_tiny on DSP
|
||||
@@ -835,7 +825,6 @@ jobs:
|
||||
deps: testing
|
||||
python-version: '3.12'
|
||||
amd: 'true'
|
||||
cuda: 'true'
|
||||
ocelot: 'true'
|
||||
llvm: 'true'
|
||||
- name: Run unit tests
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
install_loc="$HOME/.local/bin"
|
||||
docker build -t qemu-hexagon-static:latest - <<'EOF'
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends qemu-user-static ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
EOF
|
||||
|
||||
mkdir -p "$install_loc"
|
||||
tee "$install_loc/qemu-hexagon-static" >/dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
exec docker run --rm -i \
|
||||
-v /var/folders:/var/folders -v "$HOME":"$HOME" \
|
||||
qemu-hexagon-static:latest qemu-hexagon-static "$@"
|
||||
EOF
|
||||
chmod +x "$install_loc/qemu-hexagon-static"
|
||||
@@ -341,7 +341,6 @@ class TestCustomKernel(unittest.TestCase):
|
||||
self.assertEqual(y.tolist(), [1, 2, 3, 4])
|
||||
|
||||
@Context(DEV="CPU")
|
||||
@unittest.expectedFailure
|
||||
def test_simple_from_source(self):
|
||||
a = Tensor([0., 1., 2.]).realize()
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ class TestEmptyTensorEdgeCases(unittest.TestCase):
|
||||
with self.assertRaises(RuntimeError):
|
||||
Tensor([]).argmax()
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_masked_select_empty(self):
|
||||
# Masked select on empty tensors should return an empty tensor.
|
||||
torch_out = torch.tensor([], dtype=torch.float32).masked_select(torch.tensor([], dtype=torch.bool))
|
||||
|
||||
@@ -333,6 +333,25 @@ class TestJitFootguns(unittest.TestCase):
|
||||
with self.assertRaises(JitError):
|
||||
f(Tensor([1, 2, 3, 4]), Tensor([True, False, True, False])) # capture - .item() raises
|
||||
|
||||
def test_masked_select_static_size_jittable(self):
|
||||
@TinyJit
|
||||
def f(x, mask): return x.masked_select(mask, size=4, fill_value=-1).realize()
|
||||
|
||||
for _ in range(3):
|
||||
np.testing.assert_equal(f(Tensor([1, 2, 3, 4]), Tensor([True, False, True, False])).numpy(), [1, 3, -1, -1])
|
||||
np.testing.assert_equal(f(Tensor([5, 6, 7, 8]), Tensor([False, True, True, True])).numpy(), [6, 7, 8, -1])
|
||||
np.testing.assert_equal(f(Tensor([9, 8, 7, 6]), Tensor([True, True, True, True])).numpy(), [9, 8, 7, 6])
|
||||
np.testing.assert_equal(f(Tensor([1, 1, 1, 1]), Tensor([False, False, False, False])).numpy(), [-1, -1, -1, -1])
|
||||
|
||||
def test_nonzero_static_size_jittable(self):
|
||||
@TinyJit
|
||||
def f(x): return x.nonzero(size=3, fill_value=-1).realize()
|
||||
|
||||
for _ in range(3):
|
||||
np.testing.assert_equal(f(Tensor([1, 0, 2, 0, 3])).numpy(), [[0], [2], [4]])
|
||||
np.testing.assert_equal(f(Tensor([0, 0, 5, 0, 0])).numpy(), [[2], [-1], [-1]])
|
||||
np.testing.assert_equal(f(Tensor([0, 0, 0, 0, 0])).numpy(), [[-1], [-1], [-1]])
|
||||
|
||||
def test_tolist_bakes_in_values(self):
|
||||
""".tolist() raises error during JIT capture (would bake in values)."""
|
||||
@TinyJit
|
||||
|
||||
@@ -3330,10 +3330,33 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([(32, 10)], lambda x: x.masked_select(x>0.5), lambda x: x.masked_select(x>0.5), forward_only=True)
|
||||
helper_test_op([(32, 10)], lambda x: x.masked_select(torch.tensor(True)), lambda x: x.masked_select(Tensor(True)), forward_only=True)
|
||||
|
||||
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
|
||||
def test_masked_select_size(self):
|
||||
t = Tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
mask = Tensor([True, False, True, False, True, False, False, False, True])
|
||||
np.testing.assert_equal(t.masked_select(mask, size=4).numpy(), [0, 2, 4, 8])
|
||||
np.testing.assert_equal(t.masked_select(mask, size=6, fill_value=-1).numpy(), [0, 2, 4, 8, -1, -1])
|
||||
np.testing.assert_equal(t.masked_select(mask, size=2).numpy(), [0, 2])
|
||||
np.testing.assert_equal(Tensor([], dtype=dtypes.int32).masked_select(Tensor([], dtype=dtypes.bool), size=2, fill_value=-1).numpy(), [-1, -1])
|
||||
# fill_value must not alter output dtype
|
||||
self.assertEqual(Tensor([1.0, 2.0]).masked_select(Tensor([True, False]), size=3, fill_value=-1).dtype, dtypes.default_float)
|
||||
|
||||
def test_nonzero(self):
|
||||
helper_test_op([(32, 10)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
|
||||
helper_test_op([(20,)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
|
||||
helper_test_op([(10, 5, 3)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
|
||||
for v in (0, 1, 0.0, 2.5, True, False):
|
||||
helper_test_op(None, lambda x: x.nonzero().int(), lambda x: x.nonzero(), vals=[v], forward_only=True)
|
||||
|
||||
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
|
||||
def test_nonzero_size(self):
|
||||
np.testing.assert_equal(Tensor([1, 0, 2, 0, 3]).nonzero(size=3).numpy(), [[0], [2], [4]])
|
||||
np.testing.assert_equal(Tensor([1, 0, 2, 0, 3]).nonzero(size=5, fill_value=-1).numpy(), [[0], [2], [4], [-1], [-1]])
|
||||
np.testing.assert_equal(Tensor([[1, 0], [0, 2]]).nonzero(size=2).numpy(), [[0, 0], [1, 1]])
|
||||
self.assertEqual(Tensor(5).nonzero(size=4).shape, (4, 0))
|
||||
np.testing.assert_equal(Tensor([], dtype=dtypes.int32).nonzero(size=3, fill_value=-1).numpy(), [[-1], [-1], [-1]])
|
||||
# fill_value must not promote dtype to float
|
||||
self.assertEqual(Tensor([1, 0]).nonzero(size=3, fill_value=-1.5).dtype, dtypes.default_int)
|
||||
|
||||
def test_cast(self):
|
||||
helper_test_op([(3, 3)], lambda x: x.float())
|
||||
|
||||
@@ -363,6 +363,11 @@ class TestAutoCastType(unittest.TestCase):
|
||||
assert (Tensor([0, 1], dtype=dtypes.float32)).cumsum(0).dtype == dtypes.float32
|
||||
assert (Tensor([0, 1], dtype=dtypes.float64)).cumsum(0).dtype == dtypes.float64
|
||||
|
||||
def test_cumsum_empty(self):
|
||||
# empty cumsum dtype must match non-empty
|
||||
for d in (dtypes.bool, dtypes.int8, dtypes.uint8, dtypes.float16, dtypes.float32):
|
||||
self.assertEqual(Tensor([], dtype=d).cumsum(0).dtype, Tensor([0, 1], dtype=d).cumsum(0).dtype)
|
||||
|
||||
@given(strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes))
|
||||
def test_matmul(self, dt1, dt2, acc_dt):
|
||||
t1 = Tensor([0, 1], dtype=dt1)
|
||||
|
||||
@@ -328,6 +328,32 @@ class TestTensorUOpSoftmax(unittest.TestCase):
|
||||
def test_log_softmax_default(self): _check(self, _t(2, 3).float(), lambda x: x.log_softmax())
|
||||
def test_log_softmax_axis0(self): _check(self, _t(2, 3).float(), lambda x: x.log_softmax(axis=0))
|
||||
|
||||
class TestTensorUOpQR(unittest.TestCase):
|
||||
def _check(self, t):
|
||||
qt, rt = t.qr()
|
||||
qu, ru = t.uop.qr()
|
||||
self.assertIs(_strip_unique(qt.uop), _strip_unique(qu))
|
||||
self.assertIs(_strip_unique(rt.uop), _strip_unique(ru))
|
||||
def test_qr_square(self): self._check(_t(3, 3).float())
|
||||
def test_qr_tall(self): self._check(_t(4, 3).float())
|
||||
def test_qr_wide(self): self._check(_t(3, 4).float())
|
||||
def test_qr_zero_col(self): self._check(Tensor([[0.0, 1.0], [0.0, 2.0]]))
|
||||
def test_qr_batched(self): self._check(_t(2, 3, 3).float())
|
||||
|
||||
class TestTensorUOpSVD(unittest.TestCase):
|
||||
def _check(self, t, **kw):
|
||||
ut, st, vt = t.svd(**kw)
|
||||
uu, su, vu = t.uop.svd(**kw)
|
||||
self.assertIs(_strip_unique(ut.uop), _strip_unique(uu))
|
||||
self.assertIs(_strip_unique(st.uop), _strip_unique(su))
|
||||
self.assertIs(_strip_unique(vt.uop), _strip_unique(vu))
|
||||
def test_svd_square(self): self._check(_t(2, 2).float())
|
||||
def test_svd_tall(self): self._check(_t(3, 2).float())
|
||||
def test_svd_wide(self): self._check(_t(2, 3).float())
|
||||
def test_svd_odd_num(self): self._check(_t(3, 3).float()) # exercises odd-num runoff path
|
||||
def test_svd_batched(self): self._check(_t(2, 2, 2).float())
|
||||
def test_svd_nonfull(self): self._check(_t(3, 2).float(), full_matrices=False)
|
||||
|
||||
# UOp.empty / UOp.empty_like are the canonical buffer allocators; Tensor.empty / Tensor.empty_like just forward.
|
||||
class TestUOpEmpty(unittest.TestCase):
|
||||
def test_empty_dtype_string(self):
|
||||
|
||||
@@ -495,6 +495,20 @@ class TestFunctionTuple(unittest.TestCase):
|
||||
Tensor.realize(a.grad)
|
||||
np.testing.assert_allclose(a.grad.numpy(), [2., 2., 2., 2.])
|
||||
|
||||
def test_custom_kernel_precompile_further_compute(self):
|
||||
def my_kernel(C:UOp, A:UOp) -> UOp:
|
||||
i = UOp.range(A.shape[0], 0)
|
||||
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="my_kernel"))
|
||||
|
||||
@function(precompile=True)
|
||||
def f(a:Tensor):
|
||||
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
|
||||
c = Tensor.custom_kernel(c, a, fxn=my_kernel)[0]
|
||||
return c + 1
|
||||
|
||||
a = Tensor([1., 2., 3., 4.]).contiguous().realize()
|
||||
np.testing.assert_allclose(f(a).numpy(), [3., 5., 7., 9.])
|
||||
|
||||
class TestFunctionGrad(unittest.TestCase):
|
||||
def test_function_grad_ops(self, precompile=False, precompile_backward=False):
|
||||
N = 64
|
||||
|
||||
@@ -113,8 +113,8 @@ class TestMultiOutputGradient(unittest.TestCase):
|
||||
Tensor.realize(a, b)
|
||||
c, d, _, _ = Tensor.custom_kernel(Tensor.empty(4, 4), Tensor.empty(4, 4), a, b, fxn=self.addmul_kernel, grad_fxn=self.backward_addmul)
|
||||
(c * d).sum().backward()
|
||||
np.testing.assert_allclose(a.grad.numpy(), a_ref.grad.numpy(), rtol=1e-5)
|
||||
np.testing.assert_allclose(b.grad.numpy(), b_ref.grad.numpy(), rtol=1e-5)
|
||||
np.testing.assert_allclose(a.grad.numpy(), a_ref.grad.numpy(), rtol=1e-5, atol=1e-7)
|
||||
np.testing.assert_allclose(b.grad.numpy(), b_ref.grad.numpy(), rtol=1e-5, atol=1e-7)
|
||||
|
||||
def test_custom_kernel_three_output_backward(self):
|
||||
def addmulsub_kernel(C:UOp, D:UOp, E:UOp, A:UOp, B:UOp) -> UOp:
|
||||
|
||||
@@ -43,6 +43,7 @@ class TestLinAlg(unittest.TestCase):
|
||||
def test_svd_nonfull_5_3(self): self._test_svd_nonfull((5,3))
|
||||
def test_svd_nonfull_3_5(self): self._test_svd_nonfull((3,5))
|
||||
def test_svd_nonfull_2_2_2_2_3(self): self._test_svd_nonfull((2,2,2,2,3))
|
||||
def test_svd_nonfull_5_5(self): self._test_svd_nonfull((5,5))
|
||||
|
||||
@unittest.skip("very big. recommend wrapping with TinyJit around inner function")
|
||||
def test_svd_large(self):
|
||||
|
||||
@@ -80,6 +80,9 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
|
||||
# lower the index dtype to a concrete int
|
||||
sink = graph_rewrite(sink, pm_lower_index_dtype+load_store_indexing+gep_pushing, name="lower all index dtypes")
|
||||
# move gates from unrenderable INVALID where
|
||||
sink = graph_rewrite(sink, pm_move_gates_from_index, name="move gates from index")
|
||||
# final symbolic
|
||||
sink = graph_rewrite(sink, symbolic, name="post index symbolic")
|
||||
|
||||
# optional pre matcher
|
||||
@@ -93,9 +96,6 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
sink = graph_rewrite(sink, pm_dtype_decomps, ctx=(set(), ren.target), name="decomp dtypes")
|
||||
sink = graph_rewrite(sink, pm_transcendental, name="transcendental")
|
||||
|
||||
# move gates from unrenderable INVALID where
|
||||
sink = graph_rewrite(sink, pm_move_gates_from_index, name="move gates from index")
|
||||
|
||||
# final rules for the renderer (without sym)
|
||||
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else PatternMatcher([])
|
||||
pm_final_rewrite = pm_decomp+pm_render+extra_matcher+pm_split_ends
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# this is a temporary intermediate step while we remove this index style
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops
|
||||
from tinygrad.dtype import Invalid, dtypes
|
||||
|
||||
pm_move_gates_from_index = PatternMatcher([
|
||||
@@ -10,12 +10,12 @@ pm_move_gates_from_index = PatternMatcher([
|
||||
lambda buf,gate,idx,cast,data: buf.index(idx, ptr=True).cast(cast.dtype).store(data, gate)),
|
||||
|
||||
# Where after gated load becomes alt value
|
||||
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate"), name="l").or_casted(), UPat.var("a")), lambda gate,l,a:
|
||||
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate", dtype=dtypes.bool), name="l").or_casted(), UPat.var("a")), lambda gate,l,a:
|
||||
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
|
||||
(UPat.var("gate").where(UPat.var("a"), UPat().load(UPat(), ~UPat.var("gate", dtype=dtypes.bool), name="l").or_casted()), lambda gate,l,a:
|
||||
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
|
||||
|
||||
# vectorized indexes (ie. images) must be int
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.STACK, dtypes.long, name="vec")), allow_any_len=True, name="idx"),
|
||||
lambda idx,vec: idx.replace(src=(idx.src[0], UOp.vectorize(*(u.cast(dtypes.int) for u in vec.src)), *idx.src[2:])))
|
||||
# images use 2D INDEX now (y,x)
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat((Ops.CONST, Ops.VCONST, Ops.STACK), name="vec")), name="idx"),
|
||||
lambda idx,vec: idx.replace(src=(idx.src[0], vec.gep(1).cast(dtypes.int), vec.gep(0).cast(dtypes.int))) if vec.dtype.count == 2 else None),
|
||||
])
|
||||
|
||||
@@ -644,7 +644,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
|
||||
def _split_cumalu(self, axis:int, op:Ops) -> Self:
|
||||
axis = self._resolve_dim(axis)
|
||||
if self.ndim == 0 or 0 in self.shape: return self
|
||||
if self.ndim == 0 or 0 in self.shape: return self.cast(self.sum().dtype) if op is Ops.ADD else self
|
||||
# TODO: someday the optimizer will find this on its own
|
||||
# for now this is a two stage cumsum
|
||||
SPLIT = 256
|
||||
@@ -1441,6 +1441,80 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
|
||||
# ***** matrix ops *****
|
||||
|
||||
def qr(self) -> tuple[Self, Self]:
|
||||
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
|
||||
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
|
||||
R, Q = self, type(self).eye(m, dtype=self.dtype, device=self.device).expand(b_shape + (m, m))
|
||||
idx = type(self).arange(m, device=self.device)
|
||||
for i in range(min(m, n)):
|
||||
# full-length Householder reflector v with zeros above row i; w = tau*v is the rank-1 update factor
|
||||
at_i, x = idx.eq(i), (idx >= i).where(R[..., :, i], 0)
|
||||
norm = x.square().sum(-1, keepdim=True).sqrt()
|
||||
x0 = at_i.where(x, 0).sum(-1, keepdim=True)
|
||||
sgn, active = x0.ne(0).where(x0.sign(), 1), norm.ne(0)
|
||||
u0 = x0 + sgn * norm
|
||||
v = (at_i.where(u0, x) / active.where(u0, 1)).unsqueeze(-1)
|
||||
w = active.where(sgn * u0 / active.where(norm, 1), 0).unsqueeze(-1) * v
|
||||
R = R - w @ (v.transpose(-2, -1) @ R)
|
||||
Q = Q - (Q @ v) @ w.transpose(-2, -1)
|
||||
return Q, R
|
||||
|
||||
def svd(self, full_matrices = True) -> tuple[Self, Self, Self]:
|
||||
#partial implementation of https://www.netlib.org/lapack/lawnspdf/lawn169.pdf , pg 26
|
||||
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
|
||||
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
|
||||
#preprocess the matrix
|
||||
Q, R = (self if m >= n else self.transpose(-2, -1)).qr()
|
||||
num, q_num = min(m, n), max(m, n)
|
||||
# TODO: codegen infinite loop without contiguous
|
||||
U = R[..., :num, :num].contiguous()
|
||||
V = type(self).eye(num, dtype=self.dtype, device=self.device).expand(b_shape + (num, num)).contiguous()
|
||||
#prepare round robin pairing: identity on first half, reversed on second half
|
||||
permute = type(self).arange(num//2, dtype=dtypes.int, device=self.device).cat(
|
||||
type(self).arange(num//2, num, dtype=dtypes.int, device=self.device).flip(0))
|
||||
cols = type(self).arange(num, dtype=dtypes.int, device=self.device)
|
||||
def one_round_jacobi(U, V, permute):
|
||||
# permutation matrix P such that X @ P == X[..., permute]; X @ P.T applies the inverse permutation
|
||||
P = cols.unsqueeze(1).eq(permute.unsqueeze(0)).cast(U.dtype)
|
||||
#pair all the columns
|
||||
U_perm, V_perm = U @ P, V @ P
|
||||
U_permuted, runoff_U = U_perm.split(num - 1, -1) if num % 2 == 1 else (U_perm, None)
|
||||
V_permuted, runoff_V = V_perm.split(num - 1, -1) if num % 2 == 1 else (V_perm, None)
|
||||
U_left, U_right = U_permuted.split(num//2, -1)
|
||||
V_left, V_right = V_permuted.split(num//2, -1)
|
||||
#compute the jacobi rotations for each pairing
|
||||
gamma = (U_left * U_right).sum(-2).reshape(b_shape + (1, num//2))
|
||||
alpha, beta = U_permuted.square().sum(-2).unsqueeze(-2).split(num//2, -1)
|
||||
rot = gamma.ne(0)
|
||||
tau = (beta - alpha) / (2 * rot.where(gamma, 1))
|
||||
t = tau.ne(0).where(tau.sign(), 1) / (tau.abs() + (1 + tau.square()).sqrt())
|
||||
t = rot.where(t, 0)
|
||||
c = 1 / (1 + t.square()).sqrt()
|
||||
s = c * t
|
||||
#apply the rotations and unpermute via P.T
|
||||
U_left, U_right = c * U_left - s * U_right, s * U_left + c * U_right
|
||||
U = U_left.cat(U_right.cat(runoff_U, dim=-1) if num % 2 == 1 else U_right, dim=-1) @ P.transpose(-2, -1)
|
||||
V_left, V_right = c * V_left - s * V_right, s * V_left + c * V_right
|
||||
V = V_left.cat(V_right.cat(runoff_V, dim=-1) if num % 2 == 1 else V_right, dim=-1) @ P.transpose(-2, -1)
|
||||
#prepare the next round robin pairings
|
||||
if num % 2 == 1: permute = (permute - 1) % num
|
||||
else: permute = permute[0].reshape(1).cat(((permute[1:num] - 2) % (num - 1)) + 1)
|
||||
return U, V, permute
|
||||
# classical Jacobi converges in ~4 sweeps; one full sweep is (num-1) rounds for even num
|
||||
for _ in range(4 * num): U, V, permute = one_round_jacobi(U, V, permute)
|
||||
#extract singular values and sort. construct U from Q
|
||||
S, indices = U.square().sum(-2).sqrt().sort(dim=-1, descending=True)
|
||||
new_indices = indices.unsqueeze(-2).expand(b_shape + (num, num))
|
||||
U = U.gather(-1, new_indices) / S.ne(0).where(S, 1).unsqueeze(-2)
|
||||
V = V.gather(-1, new_indices)
|
||||
# place U into the top-left num×num block of a q_num×q_num identity matrix
|
||||
pad_arg = (None,) * len(b_shape) + ((0, q_num - num), (0, q_num - num))
|
||||
eye_q = type(self).eye(q_num, dtype=U.dtype, device=U.device).expand(b_shape + (q_num, q_num))
|
||||
eye_n = type(self).eye(num, dtype=U.dtype, device=U.device).expand(b_shape + (num, num)).pad(pad_arg)
|
||||
U = Q @ (U.pad(pad_arg) + eye_q - eye_n)
|
||||
if not full_matrices: U = U[..., 0:num]
|
||||
return (U, S, V.transpose(-2, -1)) if m >= n else (V, S, U.transpose(-2, -1))
|
||||
|
||||
def newton_schulz(self, steps:int, params:tuple[int, ...], eps:float=1.0e-7) -> Self:
|
||||
"""
|
||||
Performs the newton-schulz algorithm for odd polynomials. The degree of the odd polynomial depends on the number of params.
|
||||
|
||||
@@ -44,8 +44,7 @@ base_rewrite = PatternMatcher([
|
||||
# default const render
|
||||
(UPat(Ops.CONST, name="x"), lambda ctx,x: str(x.arg)),
|
||||
# new load/store
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var('idx'))),
|
||||
lambda ctx,buf,idx: f"({ctx[buf]}+{strip_parens(ctx[idx]) if idx.arg == Ops.ADD else ctx[idx]})"),
|
||||
(UPat.var("buf").index(UPat.var('idx')), lambda ctx,buf,idx: f"({ctx[buf]}+{strip_parens(ctx[idx]) if idx.arg == Ops.ADD else ctx[idx]})"),
|
||||
(UPat(Ops.LOAD, src=(UPat.var('bidx'),)), lambda ctx,bidx: f"(*{ctx[bidx]})"),
|
||||
(UPat(Ops.LOAD, src=(UPat.var("bidx"), UPat.var("var"), UPat.var("gate"))), lambda ctx,bidx,var,gate: f"({ctx[gate]}?*{ctx[bidx]}:{ctx[var]})"),
|
||||
(UPat(Ops.STORE, src=(UPat.var('bidx'), UPat.var("var")), allow_any_len=True), lambda ctx,bidx,var: f"*{ctx[bidx]} = {ctx[var]};"),
|
||||
@@ -301,13 +300,14 @@ class OpenCLRenderer(CStyleLanguage):
|
||||
(UPat(Ops.CONST, dtypes.bfloat16, name="x"),
|
||||
lambda ctx,x: f"{(struct.unpack('I', struct.pack('f', float_to_bf16(x.arg)))[0] >> 16)}u"),
|
||||
# load/store image (OpenCL)
|
||||
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))), UPat.var("var"), UPat.var("gate"))),
|
||||
lambda ctx,buf,idx,var,gate: f"({ctx[gate]}?read_imagef({ctx[buf]}, smp, {ctx[idx]}):{ctx[var]})"),
|
||||
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))),)),
|
||||
lambda ctx,buf,idx: f"read_imagef({ctx[buf]}, smp, {ctx[idx]})"),
|
||||
(UPat(Ops.STORE, src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))),
|
||||
(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), lambda ctx,buf,idx_y,idx_x: f"IMAGE<{ctx[buf]}, {ctx[idx_y]}, {ctx[idx_x]}>"),
|
||||
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("var"), UPat.var("gate"))),
|
||||
lambda ctx,buf,idx_y,idx_x,var,gate: f"({ctx[gate]}?read_imagef({ctx[buf]}, smp, (int2)({ctx[idx_x]},{ctx[idx_y]})):{ctx[var]})"),
|
||||
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')),)),
|
||||
lambda ctx,buf,idx_y,idx_x: f"read_imagef({ctx[buf]}, smp, (int2)({ctx[idx_x]},{ctx[idx_y]}))"),
|
||||
(UPat(Ops.STORE, src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')),
|
||||
UPat.var("var", dtypes.float.vec(4))), allow_any_len=True),
|
||||
lambda ctx,buf,idx,var: f"write_imagef({ctx[buf]}, {ctx[idx]}, {ctx[var]});"),
|
||||
lambda ctx,buf,idx_y,idx_x,var: f"write_imagef({ctx[buf]}, (int2)({ctx[idx_x]},{ctx[idx_y]}), {ctx[var]});"),
|
||||
]) + base_rewrite
|
||||
|
||||
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None) -> str:
|
||||
|
||||
+16
-16
@@ -135,9 +135,9 @@ class NIRRenderer(Renderer):
|
||||
# OpConvertFToU is undefined if Result Type is not wide enough, cast through int32
|
||||
# ref: https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpConvertFToU
|
||||
(UPat(Ops.CAST, (dtypes.uchar, dtypes.ushort), src=(UPat.var("x", dtypes.floats),), name="c"), lambda x,c: x.cast(dtypes.int32).cast(c.dtype)),
|
||||
# load/store use pointer arithmetic, and the cast does nothing
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("off")), allow_any_len=True, name="x"), lambda x,buf,off: x.replace(
|
||||
src=(buf,off.cast(dtypes.long))+x.src[2:]) if buf.dtype.addrspace != AddrSpace.REG and off.op not in (Ops.CAST, Ops.STACK) else None),
|
||||
# load/store use pointer arithmetic, and the cast does nothing. NOTE: this doesn't apply to image indexing cause it's 1-D
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("off")), name="x"), lambda x,buf,off: x.replace(
|
||||
src=(buf,off.cast(dtypes.long))) if buf.dtype.addrspace != AddrSpace.REG and off.op not in (Ops.CAST, Ops.STACK) else None),
|
||||
(UPat(Ops.CAST, name="x"), lambda x: x.src[0] if isinstance(x.dtype, PtrDType) or x.src[0].dtype == dtypes.void else None),
|
||||
])
|
||||
|
||||
@@ -248,31 +248,31 @@ class LVPRenderer(NIRRenderer):
|
||||
super().prerender(uops)
|
||||
self.param_sz = sum([8 if u.op == Ops.PARAM else u.dtype.itemsize for u in uops if u.op in (Ops.PARAM, Ops.DEFINE_VAR)])
|
||||
|
||||
# FIXME: this should be a rewrite rule
|
||||
def tovec(b, coord): return nalu(b, "vec4", nchannel(b, coord, 0), nchannel(b, coord, 1), nundef(b, dtypes.int), nundef(b, dtypes.int))
|
||||
def tovec(b, idx_y, idx_x): return nalu(b, "vec4", idx_x, idx_y, nundef(b, dtypes.int), nundef(b, dtypes.int))
|
||||
def nfloat(dtype): return mesa.nir_type_float16 if dtype == dtypes.half else mesa.nir_type_float32
|
||||
nstore_img = nir_instr(has_def=False, df=lambda img:img, num_components=lambda val:val.num_components,
|
||||
intrins=lambda dtype:{'IMAGE_DIM':mesa.GLSL_SAMPLER_DIM_2D, 'ACCESS':mesa.ACCESS_CAN_REORDER, 'SRC_TYPE':nfloat(dtype)},
|
||||
srcs=lambda b,img,coord,val:[nsrc(x) for x in [img, tovec(b, coord), nundef(b, dtypes.int), val, nimm(b, 0, dtypes.int)]])(
|
||||
lambda b,img,coord,val,dtype:mesa.nir_intrinsic_instr_create(b.shader,g("nir_intrinsic_image_store")))
|
||||
srcs=lambda b,img,idx_y,idx_x,val:[nsrc(x) for x in [img, tovec(b, idx_y, idx_x), nundef(b, dtypes.int), val, nimm(b, 0, dtypes.int)]])(
|
||||
lambda b,img,idx_y,idx_x,val,dtype:mesa.nir_intrinsic_instr_create(b.shader,g("nir_intrinsic_image_store")))
|
||||
|
||||
_nload_img = nir_instr(intrins=lambda dtype:{'IMAGE_DIM':mesa.GLSL_SAMPLER_DIM_2D, 'ACCESS':mesa.ACCESS_CAN_REORDER, 'DEST_TYPE':nfloat(dtype)},
|
||||
nc=4, bs=32, num_components=4, srcs=lambda b,img,coord:[nsrc(x) for x in [img, tovec(b, coord), nundef(b, dtypes.int), nimm(b, 0, dtypes.int)]])(
|
||||
lambda b,img,coord,dtype: mesa.nir_intrinsic_instr_create(b.shader, g("nir_intrinsic_image_load")))
|
||||
nc=4, bs=32, num_components=4,
|
||||
srcs=lambda b,img,idx_y,idx_x:[nsrc(x) for x in [img, tovec(b, idx_y, idx_x), nundef(b, dtypes.int), nimm(b, 0, dtypes.int)]])(
|
||||
lambda b,img,idx_y,idx_x,dtype: mesa.nir_intrinsic_instr_create(b.shader, g("nir_intrinsic_image_load")))
|
||||
|
||||
class IR3Renderer(NIRRenderer, OpenCLRenderer):
|
||||
has_aux = True
|
||||
|
||||
def nload_img(ctx,img,coord):
|
||||
def nload_img(ctx,img,idx_y,idx_x):
|
||||
ctx.texs.add(img)
|
||||
return _nload_img(ctx.b, ctx.r[img], ctx.r[coord], img.dtype)
|
||||
return _nload_img(ctx.b, ctx.r[img], ctx.r[idx_y], ctx.r[idx_x], img.dtype)
|
||||
|
||||
def_rewrite = PatternMatcher([
|
||||
(UPat(Ops.STORE, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))), UPat.var("val")), allow_any_len=True),
|
||||
lambda ctx,img,coord,val: nstore_img(ctx.b, ctx.r[img], ctx.r[coord], ctx.r[val], val.dtype)),
|
||||
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))), UPat.var("alt"), UPat.var("gate"))),
|
||||
lambda ctx,img,coord,alt,gate: if_phi(ctx.b, ctx.r[gate], lambda: ctx.nload_img(img, coord), lambda: ctx.r[alt])),
|
||||
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))),)), nload_img),
|
||||
(UPat(Ops.STORE, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("val")), allow_any_len=True),
|
||||
lambda ctx,img,idx_y,idx_x,val: nstore_img(ctx.b, ctx.r[img], ctx.r[idx_y], ctx.r[idx_x], ctx.r[val], val.dtype)),
|
||||
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("alt"), UPat.var("gate"))),
|
||||
lambda ctx,img,idx_y,idx_x,alt,gate: if_phi(ctx.b, ctx.r[gate], lambda: ctx.nload_img(img, idx_y, idx_x), lambda: ctx.r[alt])),
|
||||
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')),)), nload_img),
|
||||
]) + NIRRenderer.def_rewrite
|
||||
|
||||
_param = LVPRenderer.param
|
||||
|
||||
@@ -297,9 +297,7 @@ class MockDSPProgram:
|
||||
dsp_lib.write(self.lib)
|
||||
dsp_lib.flush()
|
||||
os.chmod(dsp_lib.name, 0o0777)
|
||||
# NOTE: this timing includes a docker launch
|
||||
proc = subprocess.run(["docker", "run", "--rm", "-i", "-v", f"{os.path.abspath(os.path.dirname(dsp_lib.name))}:/work", "-w", "/work",
|
||||
"qemu-hexagon", "-c", f"qemu-hexagon {'-strace' if DEBUG >= 5 else ''} /work/"+os.path.basename(dsp_lib.name)],
|
||||
proc = subprocess.run(["qemu-hexagon-static", *(['-strace'] if DEBUG >= 5 else []), dsp_lib.name],
|
||||
input=b''.join([bytes(to_mv(x.va_addr, x.size)) for x in bufs] + [struct.pack("I", x) for x in vals]), stdout=subprocess.PIPE, check=True)
|
||||
offset = 4
|
||||
for x in bufs:
|
||||
|
||||
@@ -92,14 +92,15 @@ class PythonProgram:
|
||||
elif arg[0] == 'l': values[i] = [x[2-int(arg[-1])] for x in warp]
|
||||
elif uop is Ops.CONST: values[i] = [arg] * warp_size
|
||||
elif uop is Ops.INDEX:
|
||||
if len(src_values) != 2: raise RuntimeError("gates must be on LOAD/STORE, not INDEX")
|
||||
ret:list = []
|
||||
if isinstance(src_dtypes[0], ImageDType):
|
||||
for m,ox,oy in zip(src_values[0], src_values[1][0], src_values[1][1]):
|
||||
assert len(src_values) == 3, "image index must be 3 srcs"
|
||||
for m,oy,ox in zip(*src_values):
|
||||
if ox < 0 or ox >= src_dtypes[0].shape[1] or oy < 0 or oy >= src_dtypes[0].shape[0]: ret.append((m, None))
|
||||
else: ret.append((m, ox*4 + oy*src_dtypes[0].shape[1]*4))
|
||||
else:
|
||||
for m,o in zip(src_values[0], src_values[1]): ret.append((m,o))
|
||||
assert len(src_values) == 2, "non-image index must be 2 srcs"
|
||||
for m,o in zip(*src_values): ret.append((m,o))
|
||||
values[i] = ret
|
||||
elif uop is Ops.CAST and isinstance(dtype, PtrDType):
|
||||
values[i] = src_values[0]
|
||||
|
||||
+22
-78
@@ -1057,10 +1057,13 @@ class Tensor(OpMixin):
|
||||
def __delitem__(self, indices) -> None:
|
||||
raise TypeError("Tensor does not support deleting items")
|
||||
|
||||
def masked_select(self, mask):
|
||||
def masked_select(self, mask, size:int|None=None, fill_value:ConstType=0):
|
||||
"""
|
||||
Selects elements from `self` based on the boolean `mask`.
|
||||
|
||||
With `size=None` (default), output length equals the number of `True` values (not jittable).
|
||||
With `size=N`, output length is `N`, padded with `fill_value` or truncated (jittable).
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
|
||||
mask = Tensor([[True, False, True], [False, True, False], [False, False, True]])
|
||||
@@ -1070,19 +1073,25 @@ class Tensor(OpMixin):
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.masked_select(mask).numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.masked_select(mask, size=6, fill_value=-1).numpy())
|
||||
```
|
||||
"""
|
||||
if not dtypes.is_bool(mask.dtype): raise RuntimeError(f"masked_select expects bool mask tensor, got {mask.dtype}")
|
||||
x, mask = self.flatten(), mask._broadcast_to(self.shape).flatten()
|
||||
mask_cumsum = mask.cumsum()
|
||||
counts = Tensor.zeros(mask_cumsum[-1].item(), dtype=dtypes.int32, device=self.device)
|
||||
idxs = counts.scatter(0, mask_cumsum, 1, reduce='add').cumsum()
|
||||
return x[idxs]
|
||||
if size is None:
|
||||
counts = Tensor.zeros(mask_cumsum[-1].item() if mask.numel() else 0, dtype=dtypes.int32, device=self.device)
|
||||
return x[counts.scatter(0, mask_cumsum, 1, reduce='add').cumsum()]
|
||||
counts = Tensor.zeros(size, dtype=dtypes.int32, device=self.device).scatter(0, mask_cumsum, 1, reduce='add')
|
||||
return (Tensor.arange(size, device=self.device) < mask.sum()).where(x[counts.cumsum()], fill_value).cast(self.dtype)
|
||||
|
||||
def nonzero(self) -> Tensor:
|
||||
def nonzero(self, size:int|None=None, fill_value:ConstType=0) -> Tensor:
|
||||
"""
|
||||
Returns the indices of the elements that are non-zero.
|
||||
|
||||
Returns a 2D tensor where each row is the index of a non-zero element.
|
||||
With `size=None` (default), output shape is `(n_nonzero, ndim)` (not jittable).
|
||||
With `size=N`, output shape is `(N, ndim)`, padded with `fill_value` or truncated (jittable).
|
||||
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
t = Tensor([1, 0, 2, 0, 3])
|
||||
@@ -1098,11 +1107,17 @@ class Tensor(OpMixin):
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.nonzero().numpy())
|
||||
```
|
||||
```python exec="true" source="above" session="tensor" result="python"
|
||||
print(t.nonzero(size=3, fill_value=-1).numpy())
|
||||
```
|
||||
"""
|
||||
if self.ndim == 0:
|
||||
return Tensor.zeros(size if size is not None else int((self != 0).item()), 0, dtype=dtypes.int32, device=self.device)
|
||||
mask = (self != 0).flatten()
|
||||
indices = Tensor.stack(*[Tensor.arange(s, device=self.device).reshape(*[1]*i, s, *[1]*(self.ndim-i-1)).expand(self.shape).flatten()
|
||||
for i, s in enumerate(self.shape)], dim=-1)
|
||||
return indices.masked_select(mask.unsqueeze(-1).expand(*mask.shape, self.ndim)).reshape(-1, self.ndim)
|
||||
return indices.masked_select(mask.unsqueeze(-1).expand(*mask.shape, self.ndim),
|
||||
size=size*self.ndim if size is not None else None, fill_value=fill_value).reshape(-1, self.ndim)
|
||||
|
||||
# ***** reduce ops *****
|
||||
|
||||
@@ -1393,77 +1408,6 @@ class Tensor(OpMixin):
|
||||
qk = qk + attn_mask
|
||||
return qk.cast(self.dtype).softmax(-1).dropout(dropout_p) @ value
|
||||
|
||||
def qr(self) -> tuple[Tensor, Tensor]:
|
||||
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
|
||||
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
|
||||
R = self.clone()
|
||||
Q = Tensor.eye(m, dtype=self.dtype, device=self.device).expand(b_shape + (m, m))
|
||||
for i in range(min(m, n)):
|
||||
x = R[..., i:m, i]
|
||||
norm = x.square().sum(-1).sqrt()
|
||||
mask = norm != 0
|
||||
s = (x[..., 0] != 0).where(-x[..., 0].sign(), -1)
|
||||
u1 = x[..., 0] - s * norm
|
||||
w = x.unsqueeze(-1) / mask.where(u1, 1)[..., None, None]
|
||||
w[..., 0, 0] = 1
|
||||
tau = (-s * u1 / mask.where(norm, 1))[..., None, None]
|
||||
tau = mask[..., None, None].where(tau, 0)
|
||||
R[..., i:m, :] = R[..., i:m, :] - (w * tau) @ (w.transpose(-2, -1) @ R[..., i:m, :])
|
||||
Q[..., :, i:m] = Q[..., :, i:m] - (Q[..., :, i:m] @ w) @ (tau * w).transpose(-2, -1)
|
||||
return Q, R
|
||||
|
||||
def svd(self, full_matrices = True) -> tuple[Tensor, Tensor, Tensor]:
|
||||
#partial implementation of https://www.netlib.org/lapack/lawnspdf/lawn169.pdf , pg 26
|
||||
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
|
||||
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
|
||||
#preprocess the matrix
|
||||
Q, R = (self if m >= n else self.transpose(-2, -1)).qr()
|
||||
num, q_num = min(m, n), max(m, n)
|
||||
# TODO: codegen infinite loop without contiguous
|
||||
U = R[..., :num, :num].contiguous()
|
||||
V = Tensor.eye(num, dtype=self.dtype, device=self.device).expand(b_shape + (num, num)).contiguous()
|
||||
#prepare round robin pairing
|
||||
permute, inverse_permute = Tensor.arange(0, num, dtype=dtypes.int, device=self.device), Tensor.zeros(num, dtype=dtypes.int, device=self.device)
|
||||
permute[num//2:num] = permute[num//2:num].flip(0)
|
||||
inverse_permute[permute] = Tensor.arange(num, dtype=dtypes.int, device=self.device)
|
||||
def one_round_jacobi(U, V, permute, inverse_permute):
|
||||
#pair all the columns
|
||||
V_permuted, runoff_V = (V[..., permute].split(num - 1, -1)) if num % 2 == 1 else (V[..., permute], None)
|
||||
V_left, V_right = V_permuted.split(num//2, -1)
|
||||
U_permuted, runoff_U = (U[..., permute].split(num - 1, -1)) if num % 2 == 1 else (U[..., permute], None)
|
||||
U_left, U_right = U_permuted.split(num//2, -1)
|
||||
#compute the jacobi rotations for each pairing
|
||||
gamma = (U_left * U_right).sum(-2).reshape(b_shape + (1, num//2))
|
||||
alpha, beta = U_permuted.square().sum(-2).unsqueeze(-2).split(num//2, -1)
|
||||
rot = gamma != 0
|
||||
tau = (beta - alpha) / (2 * rot.where(gamma, 1))
|
||||
t = (tau != 0).where(tau.sign(), 1) / (tau.abs() + (1 + tau.square()).sqrt())
|
||||
t = rot.where(t, 0)
|
||||
c = 1 / (1 + t.square()).sqrt()
|
||||
s = c * t
|
||||
#apply the rotations
|
||||
U_left, U_right = c * U_left - s * U_right, s * U_left + c * U_right
|
||||
U = U_left.cat(U_right.cat(runoff_U, dim=-1) if num % 2 == 1 else U_right, dim=-1)[..., inverse_permute]
|
||||
V_left, V_right = c * V_left - s * V_right, s * V_left + c * V_right
|
||||
V = V_left.cat(V_right.cat(runoff_V, dim=-1) if num % 2 == 1 else V_right, dim=-1)[..., inverse_permute]
|
||||
#prepare the next round robin pairings
|
||||
if num % 2 == 1: permute = (permute - 1) % num
|
||||
else: permute = permute[0].reshape(1).cat(((permute[1:num] - 2) % (num - 1)) + 1)
|
||||
inverse_permute = inverse_permute.scatter(0, permute, Tensor.arange(num, dtype=dtypes.int32, device=self.device))
|
||||
return U, V, permute, inverse_permute
|
||||
#sorta heuristic, most use num*log2(num)
|
||||
for _ in range(int(num * math.log2(num) * 2 + 2)): U, V, permute, inverse_permute = one_round_jacobi(U, V, permute, inverse_permute)
|
||||
#extract singular values and sort. construct U from Q
|
||||
S, indices = U.square().sum(-2).sqrt().sort(dim=-1, descending=True)
|
||||
new_indices = indices.unsqueeze(-2).expand(b_shape + (num, num))
|
||||
U = U.gather(-1, new_indices) / (S != 0).where(S, 1).unsqueeze(-2)
|
||||
V = V.gather(-1, new_indices)
|
||||
padded_u = Tensor.eye(q_num, dtype=U.dtype, device=U.device).expand(b_shape + (q_num, q_num))
|
||||
padded_u[..., 0:num, 0:num] = U
|
||||
U = Q @ padded_u
|
||||
if not full_matrices: U = U[..., 0:num]
|
||||
return (U, S, V.transpose(-2, -1)) if m >= n else (V, S, U.transpose(-2, -1))
|
||||
|
||||
# ***** cast ops *****
|
||||
|
||||
def cast(self, dtype:DTypeLike) -> Tensor:
|
||||
|
||||
@@ -419,8 +419,10 @@ def f2f_clamp(val:UOp, dt:DType) -> UOp:
|
||||
return val.ne(val).where(val, (val < -mx).where(-sat, (mx < val).where(sat, val)))
|
||||
|
||||
def f2f_load(x: UOp, fr:DType, to:DType) -> UOp:
|
||||
if (n:=x.dtype.count) == 1: return f2f(x.replace(dtype=f2f_dt[fr]), fr, to)
|
||||
return UOp.vectorize(*(f2f(x.replace(dtype=f2f_dt[fr], src=(reindex(x.src[0].src[0], i, 1),)), fr, to) for i in range(n)))
|
||||
if (n:=x.dtype.count) == 1:
|
||||
return f2f(x.replace(dtype=f2f_dt[fr], src=(x.src[0], x.src[1].cast(f2f_dt[fr]), x.src[2]) if len(x.src) > 1 else x.src), fr, to)
|
||||
return UOp.vectorize(*(f2f(x.replace(dtype=f2f_dt[fr], src=(reindex(x.src[0].src[0], i, 1),)+
|
||||
((x.src[1].cast(f2f_dt[fr]), x.src[2]) if len(x.src) > 1 else ())), fr, to) for i in range(n)))
|
||||
|
||||
def f2f_store(st, idx, val, fr:DType, to:DType):
|
||||
if (n:=val.dtype.count) == 1: return st.replace(src=(idx, f2f(val.bitcast(f2f_dt[to]), to, fr)))
|
||||
@@ -522,7 +524,9 @@ pm_long_decomp = PatternMatcher([
|
||||
(UPat((*(GroupOp.ALU - GroupOp.Comparison), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda x:
|
||||
l2i(x.op, l2i_dt[x.dtype], *flatten((a.rtag(0).cast(dt:=l2i_dt[x.src[-1].dtype]), a.rtag(1).cast(dt))
|
||||
if a.dtype in l2i_dt else (a,) for a in x.src))[x.tag] if x.tag is not None else None),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda x,idx: x.replace(dtype=l2i_dt[x.dtype],src=(reindex(idx, x.tag),))),
|
||||
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), allow_any_len=True, name='x'),
|
||||
lambda x,idx: x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag),)+
|
||||
(x.src[1].cast(l2i_dt[x.dtype]), x.src[2]) if len(x.src) > 1 else ())),
|
||||
(UPat(Ops.CONST, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
UOp.const(dt:=l2i_dt[x.dtype], truncate[dt]((x.arg >> 32) if x.tag == 1 else (x.arg & 0xFFFFFFFF))))
|
||||
])
|
||||
|
||||
+17
-16
@@ -7,7 +7,9 @@ from tinygrad.helpers import DEBUG, Context, prod, SPEC, Metadata, panic, CHECK_
|
||||
|
||||
# ***** uop helpers *****
|
||||
|
||||
def validate_index(buf:UOp, idx:UOp, gate:UOp|None=None):
|
||||
def validate_index(uidx:UOp, gate:UOp|None=None):
|
||||
if len(uidx.src) != 2: return True # skip for non final index. TODO: check more complex index with shape
|
||||
buf,idx = uidx.src
|
||||
if idx.op is Ops.CONST and idx.arg is Invalid: return True
|
||||
if gate is None: gate = UOp.const(dtypes.bool, True)
|
||||
# TODO: check for overflow
|
||||
@@ -79,8 +81,9 @@ spec_shared = PatternMatcher([
|
||||
(UPat(Ops.DEFINE_LOCAL, name="x"), lambda x: isinstance(x.dtype, PtrDType) and x.dtype.addrspace == AddrSpace.LOCAL),
|
||||
(UPat(Ops.DEFINE_REG, src=(), name="x"), lambda x: isinstance(x.arg, int)),
|
||||
|
||||
# AFTER on Movement Op, PARAM, BUFFER, or another AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.DEFINE_REG, Ops.DEFINE_LOCAL, Ops.AFTER, Ops.MULTI, Ops.BITCAST})),),
|
||||
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, or another AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.DEFINE_REG, Ops.DEFINE_LOCAL, Ops.AFTER, Ops.MULTI,
|
||||
Ops.BITCAST})),),
|
||||
allow_any_len=True), lambda: True),
|
||||
|
||||
# CUSTOM (inline and non inline)
|
||||
@@ -96,11 +99,11 @@ spec_shared = PatternMatcher([
|
||||
(UPat(Ops.INS), lambda: True),
|
||||
|
||||
# LOAD(idx) / STORE(idx, val) with gates on the LOAD/STORE
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().load(), validate_index),
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().load(UPat.var("alt"), UPat.var("gate", dtype=dtypes.bool), name="load"),
|
||||
lambda buf,idx,gate,alt,load: validate_index(buf, idx, gate) if alt.dtype == load.dtype else False),
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().store(UPat()), validate_index),
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().store(UPat(), UPat.var("gate", dtype=dtypes.bool)), validate_index),
|
||||
(UPat(Ops.INDEX, name="uidx").or_casted().load(), validate_index),
|
||||
(UPat(Ops.INDEX, name="uidx").or_casted().load(UPat.var("alt"), UPat.var("gate", dtype=dtypes.bool), name="load"),
|
||||
lambda uidx,gate,alt,load: validate_index(uidx, gate) if alt.dtype == load.dtype else False),
|
||||
(UPat(Ops.INDEX, name="uidx").or_casted().store(UPat()), validate_index),
|
||||
(UPat(Ops.INDEX, name="uidx").or_casted().store(UPat(), UPat.var("gate", dtype=dtypes.bool)), validate_index),
|
||||
|
||||
# STORE in tensor graph: store a value into a target
|
||||
(UPat(Ops.STORE, dtypes.void, (UPat(name="x"), UPat())), lambda x: True),
|
||||
@@ -179,9 +182,14 @@ spec_tensor = PatternMatcher([
|
||||
# TODO: this should not be here. STAGE is transformed to DEFINE_LOCAL later
|
||||
(UPat(Ops.STAGE, src=(UPat(),), allow_any_len=True), lambda: True),
|
||||
|
||||
# LINEAR
|
||||
# codegen: PROGRAM with progressive sources through the pipeline (SINK, DEVICE, LINEAR?, SOURCE?, BINARY?)
|
||||
(UPat(Ops.LINEAR, dtypes.void), lambda: True),
|
||||
(UPat(Ops.SOURCE, dtypes.void, src=()), lambda: True),
|
||||
(UPat(Ops.BINARY, dtypes.void, src=()), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE))), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE), UPat(Ops.LINEAR))), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE), UPat(Ops.LINEAR), UPat(Ops.SOURCE))), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE), UPat(Ops.LINEAR), UPat(Ops.SOURCE), UPat(Ops.BINARY))), lambda: True),
|
||||
|
||||
# UNROLL/CONTRACT is used here for WMMA
|
||||
(UPat(Ops.CONTRACT, name="x"), lambda x: x.dtype.count == prod(y[1] for y in x.arg)),
|
||||
@@ -211,13 +219,6 @@ spec_full = PatternMatcher([
|
||||
# codegen may end ranges after gpudims has replaced RANGE with SPECIAL.
|
||||
(UPat(Ops.END, src=(UPat(), UPat()), allow_any_len=True), lambda: True),
|
||||
|
||||
# codegen: PROGRAM with progressive sources through the pipeline (SINK, DEVICE, LINEAR?, SOURCE?, BINARY?)
|
||||
(UPat(Ops.SOURCE, dtypes.void, src=()), lambda: True),
|
||||
(UPat(Ops.BINARY, dtypes.void, src=()), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE))), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE), UPat(Ops.LINEAR), UPat(Ops.SOURCE))), lambda: True),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat(Ops.SINK), UPat(Ops.DEVICE), UPat(Ops.LINEAR), UPat(Ops.SOURCE), UPat(Ops.BINARY))), lambda: True),
|
||||
|
||||
# allow any AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(),), allow_any_len=True), lambda: True),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user