forked from tinygrad/tinygrad
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02fbfa41a2 | ||
|
|
64ccbde3bb | ||
|
|
6ea665ed66 | ||
|
|
0725acc392 | ||
|
|
4a1f32977c | ||
|
|
13c381b0c0 | ||
|
|
ac7067ac60 | ||
|
|
80169c6758 | ||
|
|
adacaa3e17 | ||
|
|
89ab344c42 | ||
|
|
25c3bd027b | ||
|
|
6b35220622 | ||
|
|
b1859805b1 | ||
|
|
faba071b1d | ||
|
|
81dc8ec232 | ||
|
|
95ca5081fe |
@@ -94,6 +94,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -148,6 +149,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -200,6 +202,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -249,6 +252,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export DEVICE_IN_FUNCTION_BUG=1
|
||||
export DEBUG=${DEBUG:-2}
|
||||
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
|
||||
export ASM_GEMM=${ASM_GEMM:-1}
|
||||
export GROUPED_MOE=${GROUPED_MOE:-1}
|
||||
export ALL2ALL=${ALL2ALL:-1}
|
||||
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
|
||||
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export DEVICE_IN_FUNCTION_BUG=1
|
||||
export DEBUG=${DEBUG:-0}
|
||||
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
|
||||
export ASM_GEMM=${ASM_GEMM:-1}
|
||||
export GROUPED_MOE=${GROUPED_MOE:-1}
|
||||
export ALL2ALL=${ALL2ALL:-1}
|
||||
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
|
||||
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
|
||||
|
||||
@@ -35,7 +35,7 @@ def compile_net(linear:UOp, output_bufs:List[Buffer]) -> Tuple[Dict[str,str], Li
|
||||
return name
|
||||
|
||||
for call in iter_kernel_calls(linear):
|
||||
arg_uops = [b for b in call.src[1:] if b.op is not Ops.BIND]
|
||||
arg_uops = [b for b in call.src[1:] if not b.is_bound_var]
|
||||
prg = to_program(call.src[0], Device[arg_uops[0].device].renderer)
|
||||
info = prg.arg
|
||||
functions[info.function_name] = prg.src[2].arg
|
||||
|
||||
@@ -79,7 +79,7 @@ if __name__ == "__main__":
|
||||
linear, var_vals = C.linear_with_vars()
|
||||
last_call = linear.src[-1]
|
||||
ast = last_call.src[0]
|
||||
bufs = [s.buffer for s in last_call.src[1:] if s.op is not Ops.BIND]
|
||||
bufs = [s.buffer for s in last_call.src[1:] if not s.is_bound_var]
|
||||
|
||||
src = compiled.asm["ptx"]
|
||||
# specify the shared memory here so we don't need to do it dynamically
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import cast, Any, Callable
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, encode_kernargs_clike, make_cmdbuf
|
||||
from tinygrad.runtime.support.hcq2 import make_binary_patch
|
||||
from tinygrad.uop.ops import sint, UOp
|
||||
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
|
||||
@@ -290,14 +290,14 @@ class AMDAllocator(HCQAllocator['AMDDevice']):
|
||||
def __init__(self, dev:AMDDevice):
|
||||
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb())
|
||||
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQ2Buffer:
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
|
||||
return self.dev.iface.alloc(size, host=options.host, uncached=options.uncached, cpu_access=options.cpu_access or not self.dev.has_copy_queue)
|
||||
|
||||
def _do_free(self, opaque, options:BufferSpec): self.dev.iface.free(opaque)
|
||||
|
||||
def _do_map(self, buf:HCQ2Buffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
|
||||
def _do_map(self, buf:HCQBuffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
|
||||
|
||||
def _do_unmap(self, buf:HCQ2Buffer): self.dev.iface.unmap(buf)
|
||||
def _do_unmap(self, buf:HCQBuffer): self.dev.iface.unmap(buf)
|
||||
|
||||
@dataclass
|
||||
class AMDQueueDesc:
|
||||
|
||||
@@ -16,8 +16,9 @@ apt-get install -y python3-pip python3-venv git tmux rclone clang
|
||||
|
||||
### 1.2 Install Python deps
|
||||
```bash
|
||||
python3 -m pip install --break-system-packages numpy tqdm wandb tiktoken sentencepiece
|
||||
python3 -m pip install --break-system-packages --ignore-installed typing-extensions numpy tqdm wandb tiktoken sentencepiece
|
||||
```
|
||||
Note: `--ignore-installed typing-extensions` is needed because the base image ships typing-extensions 4.10.0 without a RECORD file, so pip cannot uninstall it.
|
||||
|
||||
### 1.3 Install ROCm dev headers
|
||||
The base image has ROCm runtime but NOT the HIP dev headers. Need:
|
||||
@@ -85,36 +86,45 @@ Files downloaded (~85GB total, ~6 minutes):
|
||||
- `c4-validation-91205-samples.en_text_document.idx` (1.8 MB)
|
||||
- `LICENSE.txt`, `NOTICE.txt`
|
||||
|
||||
**Wait for rclone to fully complete before starting training.** Starting training while the dataset is still downloading will read a truncated .bin file, causing `ValueError: all input arrays must have the same shape` in the dataloader. The stale `.index_cache` and `.blend_cache` files must also be deleted if this happens:
|
||||
```bash
|
||||
rm -f /raid/datasets/c4-8b/*.index_cache /raid/datasets/c4-8b/*.blend_cache
|
||||
```
|
||||
|
||||
## Phase 4: wandb Login
|
||||
```bash
|
||||
wandb login
|
||||
```
|
||||
Enter API key from https://wandb.ai/authorize
|
||||
|
||||
Alternatively, pass the key directly:
|
||||
```bash
|
||||
wandb login <API_KEY>
|
||||
```
|
||||
|
||||
## Phase 5: Run Training
|
||||
|
||||
Run training in tmux so it survives SSH disconnects:
|
||||
```bash
|
||||
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
|
||||
```
|
||||
Attach with `tmux attach -t train`.
|
||||
|
||||
### 5.1 Smoke test (beam search, 2 layers, real data)
|
||||
Always run beam first to validate the pipeline:
|
||||
```bash
|
||||
cd /root/tinygrad
|
||||
COMGR_PATH=/opt/rocm/lib/libamd_comgr.so \
|
||||
COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so \
|
||||
CC=/opt/rocm/core-7.14/lib/llvm/bin/clang \
|
||||
DEV=AMD:HIP \
|
||||
ROCM_PATH=/opt/rocm \
|
||||
bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh
|
||||
tmux new-session -d -s beam 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh 2>&1 | tee /root/beam.log'
|
||||
```
|
||||
|
||||
The beam test runs 10 training steps with 2 layers. Expected results:
|
||||
- ~0.29s per step after warmup
|
||||
- ~700K GFLOPS, ~7% MFU (low because only 2 layers)
|
||||
- ~380 GB VRAM used
|
||||
- Loss stable at ~12.55 with random init
|
||||
|
||||
### 5.2 Full training run
|
||||
```bash
|
||||
cd /root/tinygrad
|
||||
COMGR_PATH=/opt/rocm/lib/libamd_comgr.so \
|
||||
COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so \
|
||||
CC=/opt/rocm/core-7.14/lib/llvm/bin/clang \
|
||||
DEV=AMD:HIP \
|
||||
ROCM_PATH=/opt/rocm \
|
||||
WANDB=1 \
|
||||
bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh
|
||||
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
|
||||
```
|
||||
|
||||
## Environment Variable Reference
|
||||
@@ -180,13 +190,18 @@ $ lspci -nn | grep AMD
|
||||
```
|
||||
CPU flags include `hypervisor`. `dmesg` shows `Hypervisor detected: KVM`.
|
||||
|
||||
### PCI device ID
|
||||
`lspci -v` shows device ID `0x75b0` and subsystem ID `0x75a0`:
|
||||
```
|
||||
83:00.0 Processing accelerators: ... Device 75b0
|
||||
Subsystem: ... Device 75a0
|
||||
```
|
||||
tinygrad's `PCIIface` in `ops_amd.py` and `hive_reset.py` did not list `0x75b0`, so the GPU was not found. Adding `0x75b0` to the device ID list in both files fixes the detection.
|
||||
### Working path: amdgpu driver (KFDIface)
|
||||
The amdgpu driver loads on boot and binds to all 8 GPUs, creating `/dev/kfd` and 64 renderD nodes (`/dev/dri/renderD128` through `/dev/dri/renderD191`). tinygrad's `KFDIface` enumerates GPUs through `/sys/devices/virtual/kfd/kfd/topology/nodes` and uses `/dev/kfd` for ioctl. No PCI device ID patching is needed — the KFD path does not use `PCIIface` or `AMDev._run_discovery()`.
|
||||
|
||||
This is the working configuration. No code changes to tinygrad are required.
|
||||
|
||||
### PCIIface path (does not work on this VM)
|
||||
For reference, the `PCIIface` path was also explored but does not work in this KVM guest:
|
||||
|
||||
- `PCIIface` in `ops_amd.py` does not list device ID `0x75b0`. Adding it allows PCI detection but `AMDev._run_discovery()` fails because the VRAM BAR reads all `0xFF`.
|
||||
- This was observed with the GPU unbound from any driver, after PCI reset, and with VFIO bound.
|
||||
- VFIO binding (`vfio-pci` with `enable_unsafe_noiommu_mode=1`) succeeded but VRAM BAR still reads all `0xFF`.
|
||||
- No IOMMU in guest — `dmesg` has no `AMD-Vi` entries, PCI devices have no `iommu_group` symlink.
|
||||
|
||||
### amdgpu driver behavior
|
||||
On first boot, amdgpu loaded and bound to all 8 GPUs. On one boot it failed to initialize:
|
||||
@@ -198,24 +213,5 @@ On first boot, amdgpu loaded and bound to all 8 GPUs. On one boot it failed to i
|
||||
```
|
||||
On a subsequent boot, amdgpu initialized successfully (SMU initialized, VRAM ready). After unbinding all 8 GPUs from amdgpu, `rmmod amdgpu` wedged the module (stuck in "Unloading" state in `/proc/modules`), requiring a full VM reboot.
|
||||
|
||||
### `/dev/kfd`
|
||||
`/dev/kfd` exists when amdgpu is loaded. Opening it returns `OSError: [Errno 22] Invalid argument`.
|
||||
|
||||
### VRAM BAR reads all 0xFF
|
||||
After amdgpu initializes the GPU and is then unbound, reading the VRAM BAR (via `/sys/bus/pci/devices/0000:83:00.0/resource0`) returns all `0xFF` at all offsets — including the discovery table at `vram_size - 64KB`. tinygrad's `AMDev._run_discovery()` fails with `AssertionError: discovery signatures mismatch`.
|
||||
|
||||
A PCI reset (`echo 1 > /sys/bus/pci/devices/0000:83:00.0/reset`) did not change the VRAM contents — still all `0xFF`.
|
||||
|
||||
VRAM was also all `0xFF` when read via `/dev/mem` at the BAR physical address (`0xa0000000000`).
|
||||
|
||||
### VFIO attempt
|
||||
Bound the GPU to `vfio-pci` with `enable_unsafe_noiommu_mode=1`. The GPU bound successfully and `/dev/vfio/noiommu-0` appeared. Running tinygrad with `VFIO=1` still failed with the same `discovery signatures mismatch` — VRAM BAR still reads all `0xFF`.
|
||||
|
||||
### No IOMMU in guest
|
||||
`dmesg` has no `AMD-Vi` entries. PCI devices have no `iommu_group` symlink.
|
||||
|
||||
### No fan control
|
||||
No `fan*` or `pwm*` hwmon entries exist. Only `temp*`, `power*`, `freq*` are exposed. GPU temps read 56-63°C, power ~265W per GPU.
|
||||
|
||||
### Current status: NOT WORKING
|
||||
tinygrad's `PCIIface` finds the GPU (after adding `0x75b0`) but `AMDev._run_discovery()` fails because the VRAM discovery table reads all `0xFF`. This was observed with the GPU unbound from any driver, after PCI reset, and with VFIO bound.
|
||||
|
||||
@@ -360,7 +360,7 @@ class TestJitGraphSplit(unittest.TestCase):
|
||||
self.expect(f, inp, inp_cpu,
|
||||
graph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()],
|
||||
multigraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()],
|
||||
hcqgraph=[self.ji_graph(4)])
|
||||
hcqgraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()]) # cpu is hcq2 now, it does not join hcq graphs
|
||||
|
||||
def test_jit_cpu_several(self):
|
||||
if Device.DEFAULT == "CPU": raise unittest.SkipTest("CPU is not a valid default device for this test")
|
||||
@@ -377,9 +377,9 @@ class TestJitGraphSplit(unittest.TestCase):
|
||||
inp = Tensor.randn(10, 10, device=Device.DEFAULT).realize()
|
||||
inp_cpu = Tensor.randn(10, 10, device="CPU").realize()
|
||||
self.expect(f, inp, inp_cpu,
|
||||
graph=[self.ji_graph(2), self.ji_graph(2), self.ji_comp()],
|
||||
multigraph=[self.ji_graph(2), self.ji_graph(2), self.ji_comp()],
|
||||
hcqgraph=[self.ji_graph(5)])
|
||||
graph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()],
|
||||
multigraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()],
|
||||
hcqgraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()])
|
||||
|
||||
def test_jit_multidev(self):
|
||||
if Device.DEFAULT == "CPU": raise unittest.SkipTest("CPU is not a valid default device for this test")
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestLinearizer(unittest.TestCase):
|
||||
c = ((a.shrink(((0, 2),)) - a.shrink(((2, 4),))) - (b.shrink(((0, 2),)) - b.shrink(((2, 4),))))
|
||||
linear = c.schedule_linear()
|
||||
run_linear(linear)
|
||||
rawbufs = [s.buffer for s in linear.src[-1].src[1:] if s.op is not Ops.BIND]
|
||||
rawbufs = [s.buffer for s in linear.src[-1].src[1:] if not s.is_bound_var]
|
||||
assert len(rawbufs) == 3 and set(rawbufs[1:]) == {a.uop.base.realized, b.uop.base.realized}
|
||||
np_c = (np_a[:2] - np_a[2:]) - (np_b[:2] - np_b[2:])
|
||||
np.testing.assert_allclose(np_c, c.numpy(), atol=1e-4, rtol=1e-4)
|
||||
@@ -411,7 +411,7 @@ def helper_realized_ast(r:Tensor|list[Tensor]) -> tuple[UOp, list[Buffer]]:
|
||||
last_call = linear.src[-1]
|
||||
ast = last_call.src[0]
|
||||
assert ast.op is Ops.SINK, f"helper_realized_ast expects a SINK {last_call}"
|
||||
last_bufs = [s.buffer for s in last_call.src[1:] if s.op is not Ops.BIND]
|
||||
last_bufs = [s.buffer for s in last_call.src[1:] if not s.is_bound_var]
|
||||
# now all input buffers in last_call should be realized
|
||||
# create fresh buffers for the outputs
|
||||
bufs = [Buffer(x.device, x.size, x.dtype).allocate() if i < len(ast.src) else x for i,x in enumerate(last_bufs)]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import unittest, random
|
||||
from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variable
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
|
||||
from tinygrad.helpers import getenv, prod, Context
|
||||
from tinygrad.nn.state import get_parameters
|
||||
from tinygrad.engine.realize import run_linear, compile_linear
|
||||
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
|
||||
@@ -79,9 +79,9 @@ class TestMultiTensor(unittest.TestCase):
|
||||
def test_shard_beam(self):
|
||||
cpu_2 = ("CPU:1", "CPU:2")
|
||||
src = Tensor.ones(16).shard(cpu_2, 0).realize()
|
||||
pad = src.to(cpu_2[::-1]).schedule_linear().src[0]
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): prg = compile_linear(UOp(Ops.LINEAR, src=(pad,))).src[0].src[0]
|
||||
self.assertNotEqual(prg.src[0].arg.applied_opts, ())
|
||||
lin = UOp(Ops.LINEAR, src=(src.to(cpu_2[::-1]).schedule_linear().src[0],))
|
||||
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = graph_rewrite(graph_rewrite(lin, pm_beam, ctx=1, walk=True), pm_compile, walk=True).src[0]
|
||||
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
|
||||
|
||||
def test_shard_same_device(self):
|
||||
X = Tensor.ones(256).contiguous().realize()
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest, pickle, types, tracemalloc
|
||||
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
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, UOp, deconstruct_function
|
||||
|
||||
class TestPickle(unittest.TestCase):
|
||||
def test_pickle_code_object(self):
|
||||
@@ -11,6 +11,11 @@ class TestPickle(unittest.TestCase):
|
||||
fxn = types.FunctionType(pickle.loads(code_str), globals())
|
||||
self.assertEqual(fxn(2), 4)
|
||||
|
||||
def test_deconstruct_function_nested_comprehension(self):
|
||||
# pre PEP 709, each comprehension is its own code object, so dtypes here is referenced two code objects deep
|
||||
def fxn(): return [[dtypes.int for _ in range(2)] for _ in range(2)]
|
||||
self.assertEqual(types.FunctionType(*deconstruct_function(fxn))(), fxn())
|
||||
|
||||
def test_pickle_pattern_matcher(self):
|
||||
pm = PatternMatcher([(UPat.cvar('x'), lambda x: x*2)])
|
||||
sink = UOp.const(2)
|
||||
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
import unittest, time
|
||||
from tinygrad import Tensor
|
||||
import unittest, time, itertools
|
||||
from tinygrad import Tensor, Context
|
||||
|
||||
class TestScheduleScaling(unittest.TestCase):
|
||||
"""Test that .schedule() scales linearly with graph size (no O(n^2) behavior)."""
|
||||
@@ -130,5 +130,18 @@ class TestScheduleScaling(unittest.TestCase):
|
||||
return parts[0].cat(*parts[1:])
|
||||
self._assert_linear(concat_chain)
|
||||
|
||||
@Context(DEV="NULL:HIP:gfx1100")
|
||||
def test_custom_kernel_assign_scaling(self):
|
||||
from tinygrad.uop.ops import UOp, Ops, KernelInfo
|
||||
from tinygrad.runtime.autogen.amd.rdna3.ins import s_nop
|
||||
count = itertools.count(0)
|
||||
def custom_kernel_assign(n):
|
||||
def custom_asm(out):
|
||||
return UOp(Ops.PROGRAM, src=(UOp.sink(out, arg=KernelInfo(f"fxn_{next(count)}")),
|
||||
UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=s_nop(i)) for i in range(n*8)))))
|
||||
call = Tensor.custom_kernel(Tensor.empty(1), fxn=custom_asm)[0]
|
||||
return Tensor.cat(*[Tensor.empty(1).assign(call+i) for i in range(n)])
|
||||
self._assert_linear(custom_kernel_assign, n_small=50, n_large=500)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ def assert_jit_cache_len(fxn, expected_len):
|
||||
if linear is None or not linear.src:
|
||||
if expected_len != 0: raise KernelCountException(expected_len, 0)
|
||||
return
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 3 # HCQ2: merged same-queue calls + finalizer + bumps
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 4 # HCQ2: fence + reset + merged same-queue calls + finalizer
|
||||
if call_is_graph(linear.src[0]):
|
||||
if len(linear.src) != 1: raise KernelCountException(1, len(linear.src))
|
||||
inner = linear.src[0].src[0].src[0] # LINEAR UOp inside CUSTOM_FUNCTION
|
||||
|
||||
+16
-20
@@ -260,19 +260,6 @@ def _cond(cond, if_true, if_false):
|
||||
def _cond_hi16(cond, val: UOp) -> UOp: return _cond(cond, _hi16(val), val)
|
||||
def _apply_opsel(val: UOp, sel_bit: int, opsel: int) -> UOp: return _hi16(val) if opsel & (1 << sel_bit) else val
|
||||
|
||||
def _set_lane_bit(old: UOp, lane: UOp, val: UOp, exec_mask: UOp) -> UOp:
|
||||
"""Set/clear a single bit in a mask based on lane index, respecting exec mask."""
|
||||
if old.dtype in (dtypes.uint64, dtypes.int64):
|
||||
dt = dtypes.uint64
|
||||
mask = UOp.const(1, dt) << lane.cast(dt)
|
||||
new_bit = _to_u32(val).cast(dt) << lane.cast(dt)
|
||||
cleared = old.cast(dt) & (mask ^ UOp.const(0xFFFFFFFFFFFFFFFF, dt))
|
||||
return _lane_active(exec_mask, lane).where(cleared | new_bit, old.cast(dt))
|
||||
mask = _c(1) << lane.cast(dtypes.uint32)
|
||||
new_bit = _to_u32(val) << lane.cast(dtypes.uint32)
|
||||
cleared = old & (mask ^ _c(MASK32))
|
||||
return _lane_active(exec_mask, lane).where(cleared | new_bit, old)
|
||||
|
||||
def _val_to_u32(val: UOp) -> UOp:
|
||||
"""Convert any value to uint32 for storage (bitcast floats, cast ints)."""
|
||||
if val.dtype == dtypes.uint32: return val
|
||||
@@ -532,6 +519,19 @@ class _Ctx:
|
||||
return [self.wsgpr_dyn(reg, lo), self.wsgpr_dyn(reg + _c(1), hi)]
|
||||
return [self.wsgpr_dyn(reg, val)]
|
||||
|
||||
def wmask_lane_bit(self, reg: UOp, lane: UOp, val: UOp, exec_mask: UOp) -> list[UOp]:
|
||||
"""Set/clear bit `lane` of the mask at `reg` from val for exec-active lanes, preserving memory for inactive lanes"""
|
||||
active, bit = _lane_active(exec_mask, lane), _to_u32(val)
|
||||
if self.wave_size <= 32:
|
||||
old = self.rsgpr_dyn(reg)
|
||||
mask = _c(1) << lane.cast(dtypes.uint32)
|
||||
return [self.wsgpr_dyn(reg, active.where((old & (mask ^ _c(MASK32))) | (bit << lane.cast(dtypes.uint32)), old))]
|
||||
off = (lane & _c(31, dtypes.int)).cast(dtypes.uint32)
|
||||
mask = _c(1) << off
|
||||
def half(old: UOp, sel: UOp) -> UOp: return sel.where(active.where((old & (mask ^ _c(MASK32))) | (bit << off), old), old)
|
||||
return [self.wsgpr_dyn(reg, half(self.rsgpr_dyn(reg), lane < _c(32, dtypes.int))),
|
||||
self.wsgpr_dyn(reg + _c(1), half(self.rsgpr_dyn(reg + _c(1)), _c(32, dtypes.int) <= lane))]
|
||||
|
||||
def rmask(self, reg: UOp) -> UOp:
|
||||
"""Read a lane mask (VCC/EXEC). Combines lo/hi for wave64."""
|
||||
if self.wave_size > 32: return _u64(self.rsgpr_dyn(reg), self.rsgpr_dyn(reg + _c(1)))
|
||||
@@ -718,9 +718,7 @@ class _Ctx:
|
||||
raw_stores.append(('vgpr_direct', self.vgpr.index(val[0].valid(active)).store(new_val)))
|
||||
continue
|
||||
if 'D0' in dest and '[laneId]' in dest:
|
||||
old_vcc = self.rmask(_c(VCC_LO.offset))
|
||||
new_vcc = _set_lane_bit(old_vcc, lane, val, exec_mask)
|
||||
raw_stores.extend([('vcc', s) for s in self.wmask(_c(VCC_LO.offset), new_vcc)])
|
||||
raw_stores.extend([('vcc', s) for s in self.wmask_lane_bit(_c(VCC_LO.offset), lane, val, exec_mask)])
|
||||
elif dest.startswith('D0'):
|
||||
dest_suffix = re.match(r'D0\.(\w+)', dest)
|
||||
if dest_suffix is not None:
|
||||
@@ -1039,13 +1037,11 @@ def _compile_sdwa(inst: irc.VOP1_SDWA | irc.VOP2_SDWA | irc.VOP2_SDWA_SDST | irc
|
||||
result = _sdwa_write(old, result, dst_sel, dst_unused)
|
||||
stores.append(ctx.wvgpr_dyn(vdst_reg, lane, result, exec_mask))
|
||||
elif dest.startswith('VCC'):
|
||||
old_vcc = ctx.rmask(_c(VCC_LO.offset))
|
||||
stores.extend(ctx.wmask(_c(VCC_LO.offset), _set_lane_bit(old_vcc, lane, val, exec_mask)))
|
||||
stores.extend(ctx.wmask_lane_bit(_c(VCC_LO.offset), lane, val, exec_mask))
|
||||
if vcc_val is not None:
|
||||
# Initialize sdst to 0 before lane loop (old value may be unrelated data), then set lane bits in loop
|
||||
init_stores = [ctx.wsgpr_dyn(sdst_off, _c(0)), ctx.wsgpr_dyn(sdst_off + _c(1), _c(0))]
|
||||
old_sdst = ctx.rmask(sdst_off)
|
||||
stores.extend(ctx.wmask(sdst_off, _set_lane_bit(old_sdst, lane, vcc_val, exec_mask)))
|
||||
stores.extend(ctx.wmask_lane_bit(sdst_off, lane, vcc_val, exec_mask))
|
||||
if stores:
|
||||
return UOp.sink(*init_stores, UOp.sink(*stores).end(lane), *ctx.inc_pc())
|
||||
return UOp.sink(*init_stores, *ctx.inc_pc())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest, itertools, math
|
||||
from tinygrad import Tensor, dtypes, Context
|
||||
from tinygrad.dtype import DType, ConstType, truncate
|
||||
from tinygrad.dtype import DType, ConstType
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
from test.helpers import full_rewrite
|
||||
import numpy as np
|
||||
@@ -51,16 +51,13 @@ class TestWeakConstFolding(unittest.TestCase):
|
||||
def test_invalid_poison(self):
|
||||
self.assertTrue(UOp.invalid().alu(Ops.CDIV, UOp.const(0)).simplify().is_invalid)
|
||||
|
||||
def test_cast_commits_to_dtype_grid(self):
|
||||
# committing a weak const to a stated width puts the value on that width's grid, same as storage packing and native compilers
|
||||
v = 1/123008 # not representable in float16
|
||||
out = UOp.const(v).cast(dtypes.half).simplify()
|
||||
self.assertEqual((out.op, out.dtype, out.val), (Ops.CONST, dtypes.half, truncate[dtypes.half](v)))
|
||||
self.assertNotEqual(out.val, v)
|
||||
# the grid commit preserves the sign of zero
|
||||
self.assertEqual(math.copysign(1, UOp.const(-0.0).cast(dtypes.half).simplify().val), -1)
|
||||
# observable at tensor level: the const-folded comparison agrees with the committed value
|
||||
self.assertTrue((Tensor(-3.2).cast(dtypes.float32) <= truncate[dtypes.float32](-3.2)).item())
|
||||
def test_single_rounding_log10_backward(self):
|
||||
# log10 backward folds log10(2)/log(2) = 1/log(10) in one rounding, not the double-rounded 1/float32(log(10))
|
||||
x = Tensor([1.0, 2.0, 3.0])
|
||||
ast = next(s.src[0] for s in x.log10().sum().gradient(x)[0].schedule_linear().src if s.src[0].op is Ops.SINK)
|
||||
const = next(u.arg for u in full_rewrite(ast).toposort() if u.op is Ops.CONST and u.dtype is dtypes.float32)
|
||||
# correctly rounded: within half a float32 ulp of the exact value (folding at float32 lands 0.66 ulp off)
|
||||
self.assertLess(abs(const - 1/math.log(10)), 2**-26)
|
||||
|
||||
class TestBinaryOpsConstFolding(unittest.TestCase):
|
||||
def test_add_literal_zero(self):
|
||||
|
||||
@@ -143,13 +143,13 @@ class TestModuloAndDivisionFolding(unittest.TestCase):
|
||||
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.weakint) % 10).render(simplify=False), x_var_uop.render(simplify=False))
|
||||
|
||||
def test_full_graph_rewrite_division_with_remainder(self):
|
||||
x_var_uop = UOp.variable('x', 7, 9)
|
||||
x_var_uop = UOp.variable('x', 7, 9, param=True)
|
||||
optimized_sink = apply_rewrite(x_var_uop // 2)
|
||||
for x_value in range(7, 10):
|
||||
self.assertEqual(x_value // 2, evaluate_uop(optimized_sink, {'x': x_value}))
|
||||
|
||||
def test_full_graph_rewrite_complex_mod_div_expression(self):
|
||||
x_var_uop = UOp.variable('x', 1, 10)
|
||||
x_var_uop = UOp.variable('x', 1, 10, param=True)
|
||||
optimized_sink = apply_rewrite(((x_var_uop * 5) % 3) // 2)
|
||||
for x_value in range(1, 11):
|
||||
original_result = ((x_value * 5) % 3) // 2
|
||||
|
||||
@@ -25,7 +25,7 @@ def get_load_image_uop(image_shape:tuple[int, ...], valid:UOp, idx:tuple[UOp, UO
|
||||
))
|
||||
|
||||
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(nmax),), arg=expr)
|
||||
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax)
|
||||
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax, param=True)
|
||||
def Range(n, nmax): return UOp.range(nmax, n)
|
||||
|
||||
class TestValidIdxSimplification(unittest.TestCase):
|
||||
|
||||
@@ -157,7 +157,7 @@ class TestGraphRewrite(unittest.TestCase):
|
||||
self.assertEqual(nout.val, 3.0)
|
||||
|
||||
def test_depth_2_fold(self):
|
||||
v = UOp.variable("v", 0, 1, dtypes.float)
|
||||
v = UOp.variable("v", 0, 1, dtypes.float, param=True)
|
||||
c1 = UOp.const(1.0)
|
||||
c2 = UOp.const(2.0)
|
||||
nout = graph_rewrite(v+c1+c2, simple_pm)
|
||||
@@ -339,7 +339,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 1)
|
||||
|
||||
def test_depth_2_const_fold(self):
|
||||
v = UOp.variable("tmp", 0, 1, dtypes.int)
|
||||
v = UOp.variable("tmp", 0, 1, dtypes.int, param=True)
|
||||
c2 = UOp.const(2, dtypes.int)
|
||||
c4 = UOp.const(4, dtypes.int)
|
||||
vc = v+c2
|
||||
|
||||
@@ -3,7 +3,6 @@ import unittest, pickle, functools, math
|
||||
import z3
|
||||
|
||||
from tinygrad.dtype import dtypes, ConstType, DType, Invalid
|
||||
from test.helpers import get_uops
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, sym_infer
|
||||
from tinygrad.uop.spec import spec_shared, type_verify
|
||||
from tinygrad.uop.symbolic import sym, pm_fold_cast_const, commutative, pm_simplify_valid, pm_move_where_on_load
|
||||
@@ -16,7 +15,8 @@ def check_uop_against_string(self, v:UOp, s:str):
|
||||
s_eval = graph_rewrite(s_eval, commutative, name="cannonicalize eval")
|
||||
self.assertIs(s_eval, v, f"eval did not match simplified: {s_eval} != {v.render()} for {s}")
|
||||
|
||||
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint): return UOp.variable(name,min_val,max_val,dtype)
|
||||
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint):
|
||||
return UOp.variable(name, min_val, max_val, dtype, param=True)
|
||||
def uconst(val): return UOp.const(val)
|
||||
def usum(ops): return functools.reduce(lambda x,y: x+y, ops)
|
||||
def uand(ops): return functools.reduce(lambda x,y: x*y, ops)
|
||||
@@ -442,7 +442,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.helper_test_variable(uand([uconst(1), Variable("a", 0, 1)]), 0, 1, "a")
|
||||
|
||||
def test_masked_shr_fold(self):
|
||||
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32)
|
||||
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32, param=True)
|
||||
self.helper_test_variable((x & -4) >> 2, 0, 63, "(x>>2)")
|
||||
|
||||
def test_bool_or_not_tautology(self):
|
||||
@@ -483,12 +483,12 @@ class TestSymbolic(unittest.TestCase):
|
||||
|
||||
def test_div_drop_small_terms(self):
|
||||
# from openpilot, shouldnt simplify
|
||||
gidx0 = UOp.variable("gidx0", 0, 10)
|
||||
gidx1 = UOp.variable("gidx1", 0, 10)
|
||||
lidx0 = UOp.variable("lidx0", 0, 1)
|
||||
lidx1 = UOp.variable("lidx1", 0, 1)
|
||||
ridx1005 = UOp.variable("ridx1005", 0, 2)
|
||||
ridx1006 = UOp.variable("ridx1006", 0, 2)
|
||||
gidx0 = UOp.variable("gidx0", 0, 10, param=True)
|
||||
gidx1 = UOp.variable("gidx1", 0, 10, param=True)
|
||||
lidx0 = UOp.variable("lidx0", 0, 1, param=True)
|
||||
lidx1 = UOp.variable("lidx1", 0, 1, param=True)
|
||||
ridx1005 = UOp.variable("ridx1005", 0, 2, param=True)
|
||||
ridx1006 = UOp.variable("ridx1006", 0, 2, param=True)
|
||||
self.helper_test_variable((lidx1+((gidx1*18)+(ridx1005*18)+(lidx0*162))+(gidx0*2)+(ridx1006*2)+-40)//18, -3, 20,
|
||||
"(gidx1+ridx1005+lidx0*9+(gidx0+ridx1006+7)//9+-3)")
|
||||
|
||||
@@ -997,7 +997,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.helper_test_variable(cond.ne(False), 0, 1, "(x<2)")
|
||||
|
||||
def test_bitcast_chain(self):
|
||||
a = UOp.variable("a", 0, 3, dtype=dtypes.int32)
|
||||
a = UOp.variable("a", 0, 3, dtype=dtypes.int32, param=True)
|
||||
self.assertIs(graph_rewrite(a.bitcast(dtypes.float32).bitcast(a.dtype), sym), a)
|
||||
|
||||
def test_negation_in_where(self):
|
||||
@@ -1012,22 +1012,6 @@ class TestSymbolic(unittest.TestCase):
|
||||
b = Variable("b", 0, 3)
|
||||
self.helper_test_variable(-a<-b, False, True, "(b<a)")
|
||||
|
||||
def test_where_cast(self):
|
||||
s = Variable("s", 0, 3, dtypes.int)
|
||||
cond = s < 2
|
||||
a = Variable("a", 0, 3, dtypes.int)
|
||||
b = Variable("b", 0, 3, dtypes.int)
|
||||
expr = cond.where(a, b).cast(dtypes.half)
|
||||
|
||||
# TODO: copied from render, render does not support cast
|
||||
glbl = UOp.param(0, dtypes.int, (1,))
|
||||
uops = get_uops(UOp(Ops.STORE, src=(glbl.index(UOp.const(0, dtypes.int)), expr)).sink())
|
||||
rewritten_uop = [uop for uop in uops if uop.op is Ops.STORE][0].src[1]
|
||||
|
||||
# the vars are now scalar PARAMs
|
||||
pvar = {u.expr: u for u in rewritten_uop.toposort() if u.op is Ops.PARAM}
|
||||
self.assertEqual(rewritten_uop, (pvar['s']<UOp.const(2, dtypes.int)).where(pvar['a'].cast(dtypes.half), pvar['b'].cast(dtypes.half)))
|
||||
|
||||
def test_where_merge_branches(self):
|
||||
cond1 = Variable("s", 0, 10) < 6
|
||||
cond2 = Variable("s", 0, 10) > 2
|
||||
@@ -1180,7 +1164,7 @@ class TestSymbolicVariables(unittest.TestCase):
|
||||
assert (a//4 + a//6).variables() == [a]
|
||||
|
||||
def test_variable_min_eq_max_bind_folds(self):
|
||||
b = Variable("x", 1, 1).bind(1)
|
||||
b = UOp.variable("x", 1, 1).bind(1)
|
||||
s = b.simplify()
|
||||
self.assertEqual(s.op, Ops.CONST)
|
||||
self.assertEqual(s.val, 1)
|
||||
@@ -1374,6 +1358,16 @@ class TestInvalidIndex(unittest.TestCase):
|
||||
c2 = UOp.const((1, Invalid, 1, 1))
|
||||
self.assertIs((c1+c2).simplify(), UOp.const((2, Invalid, Invalid, Invalid)))
|
||||
|
||||
def test_gated_load_keeps_index_valid(self):
|
||||
# the load executes even on gated-off iterations: gated_given_valid must not erase its mask (PADTO OOB shape)
|
||||
buf = UOp.param(0, dtypes.bool, (17,))
|
||||
ridx = Variable("ridx", 0, 31)
|
||||
cond = ridx < 17
|
||||
load = buf.index(ridx.valid(cond))
|
||||
out = graph_rewrite(cond.where(load.where(uconst(2), uconst(0)), UOp.invalid()), sym)
|
||||
idx = next(u for u in out.toposort() if u.op is Ops.INDEX)
|
||||
self.assertIs(idx.src[1].get_valid(), cond.simplify())
|
||||
|
||||
class TestStoreLoadFolding(unittest.TestCase):
|
||||
"""Tests for store(index, load(index)) -> NOOP rule. This rule matches patterns that EMERGE during simplification."""
|
||||
def test_store_load_folding(self):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import unittest
|
||||
from tinygrad import dtypes, Variable
|
||||
from tinygrad import dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.uop.ops import Ops, UOp, AxisType
|
||||
from test.helpers import to_uops_list
|
||||
|
||||
def Variable(name, nmin, nmax): return UOp.variable(name, nmin, nmax, param=True)
|
||||
|
||||
class TestValidateOOB(unittest.TestCase):
|
||||
"""Test z3 validation of index bounds for different ALU ops and patterns."""
|
||||
|
||||
|
||||
@@ -305,10 +305,10 @@ class TestVizTree(unittest.TestCase):
|
||||
|
||||
def test_tree_view(self):
|
||||
with save_viz() as viz:
|
||||
a = UOp.variable("a",0,10)
|
||||
b = UOp.variable("b",0,10)
|
||||
c = UOp.variable("c",0,10)
|
||||
d = UOp.variable("d",0,10)
|
||||
a = UOp.variable("a",0,10,param=True)
|
||||
b = UOp.variable("b",0,10,param=True)
|
||||
c = UOp.variable("c",0,10,param=True)
|
||||
d = UOp.variable("d",0,10,param=True)
|
||||
sink = UOp.sink(a+b, c+d)
|
||||
def tree_rewrite(): return graph_rewrite(sink, root, name="root")
|
||||
tree_rewrite()
|
||||
|
||||
@@ -540,6 +540,10 @@ class TestAssign(unittest.TestCase):
|
||||
c = Tensor([1.0, 2.0, 3.0, 4.0], dtype=dtypes.float32).realize()
|
||||
c[0:2].bitcast(dtypes.uint32).assign(Tensor([0x40800000, 0x40400000], dtype=dtypes.uint32)).realize()
|
||||
np.testing.assert_allclose(c.numpy(), [4.0, 3.0, 3.0, 4.0])
|
||||
# without .realize()
|
||||
a = Tensor([1.0, 2.0, 3.0, 4.0], dtype=dtypes.float32).realize()
|
||||
a.bitcast(dtypes.uint32).assign(Tensor([0x40800000, 0x40400000, 0x40000000, 0x3f800000], dtype=dtypes.uint32))
|
||||
np.testing.assert_allclose(a.numpy(), [4.0, 3.0, 2.0, 1.0])
|
||||
|
||||
def test_assign_bitcast_different_size(self):
|
||||
# assign to a shape-changing bitcast view (only works on DISK currently)
|
||||
|
||||
@@ -25,10 +25,10 @@ class TestHCQUnit(unittest.TestCase):
|
||||
cpu_call = UOp(Ops.PROGRAM, src=(UOp.sink(),)).call(UOp.new_buffer("CPU", 1, dtypes.float))
|
||||
gpu_devs = [d0]
|
||||
|
||||
# local MMIO: GPU works alone and with CPU in batch (cpu_support=True)
|
||||
# CPU uses HCQ2 and is no longer batched into legacy HCQ graphs.
|
||||
assert HCQGraph.supports_uop(gpu_devs, gpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is True
|
||||
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is False
|
||||
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is False
|
||||
|
||||
# USB MMIO: GPU-only still works, but CPU batching must be rejected (cpu_support=False)
|
||||
orig_view = d0.timeline_signal.base_buf.view
|
||||
|
||||
@@ -301,7 +301,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
sink = graph_rewrite(sink, pm_split_ranges+pm_flatten_range, ctx={}, name="split ranges")
|
||||
|
||||
# symbolic (NOTE: this is a requirement for pm_simplify_ranges to be correct)
|
||||
sink = graph_rewrite(sink, sym+pm_fold_cast_const+pm_flatten_range, name="initial symbolic")
|
||||
sink = graph_rewrite(sink, sym+pm_flatten_range, name="initial symbolic")
|
||||
|
||||
# optimize (schedule) the AST
|
||||
sink = graph_rewrite(sink, pm_flatten_range+pm_simplify_ranges, ctx={}, name="simplify ranges")
|
||||
@@ -357,7 +357,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
|
||||
# floordiv+mod / dtype decomp (early)
|
||||
supported_ops = tuple(ren.code_for_op.keys())
|
||||
pm_decomp = symbolic_simple+pm_fold_cast_const+get_simplifying_rewrite_patterns(supported_ops)
|
||||
pm_decomp = symbolic_simple+get_simplifying_rewrite_patterns(supported_ops)
|
||||
sink = graph_rewrite(sink, pm_decomp, name="early decompositions")
|
||||
|
||||
# late decomps + move gates from unrenderable INVALID where
|
||||
|
||||
@@ -57,7 +57,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
|
||||
# get the idxs
|
||||
ki: KernelInfo = s.arg
|
||||
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.weakint)]
|
||||
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int, param=True).cast(dtypes.weakint)]
|
||||
elif ki.dont_use_locals:
|
||||
assert not local_dims, "can't use locals if there's no local dims"
|
||||
idxs = get_grouped_dims("idx", global_shape, ctx.global_max, reverse=True)
|
||||
@@ -89,7 +89,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
|
||||
pm_device_to_var = PatternMatcher([
|
||||
# the DEVICE axis is not a program axis, it's bound per device at launch. lower it to the _device_num variable (like SPECIAL for devices)
|
||||
(UPat(Ops.RANGE, name="r"), lambda r: UOp.variable("_device_num", 0, r.vmax, dtype=r.dtype) if r.arg[-1] is AxisType.DEVICE else None),
|
||||
(UPat(Ops.RANGE, name="r"), lambda r: UOp.variable("_device_num", 0, r.vmax, dtype=r.dtype, param=True) if r.arg[-1] is AxisType.DEVICE else None),
|
||||
# ENDs that closed a DEVICE range no longer close it
|
||||
(UPat(Ops.END, name="e"), lambda e: e.replace(src=(e.src[0],)+tuple(s for s in e.src[1:] if s.op is not Ops.PARAM))
|
||||
if any(s.op is Ops.PARAM and s.arg.name == '_device_num' for s in e.src[1:]) else None),
|
||||
|
||||
@@ -26,7 +26,7 @@ def _drop_valid_stmts(valid:UOp, idx:UOp, height:int, width:int) -> list[UOp]:
|
||||
# check if idx is out of bound when X is on the wrong side of the bound: X in [c+1, vmax] or [vmin, c-1]
|
||||
lo, hi = (c + 1, X.vmax) if is_upper_bound else (X.vmin, c - 1)
|
||||
if lo <= hi:
|
||||
fake = UOp.variable(f"fake{i}", lo, hi, X.dtype)
|
||||
fake = UOp.variable(f"fake{i}", lo, hi, X.dtype, param=True)
|
||||
subs = [{X: fake}]
|
||||
# idx may not have X itself, so also substitute a term of X: v -> fake - (X - v)
|
||||
terms = list(X.split_uop(Ops.ADD))
|
||||
|
||||
@@ -3,7 +3,7 @@ from tinygrad.uop.ops import PatternMatcher, UPat, Ops
|
||||
from tinygrad.dtype import Invalid, dtypes
|
||||
|
||||
def move_where_load(gate, l, a, w):
|
||||
return l.replace(src=(l.src[0], l.vconst_like(0) if a.is_invalid else
|
||||
return l.replace(src=(l.src[0], l.vconst_like(0) if a.is_invalid else l.const_like(a.val) if a.op is Ops.CONST else
|
||||
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(w.dtype)
|
||||
|
||||
pm_move_gates_from_index = PatternMatcher([
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import itertools
|
||||
from typing import Callable
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, graph_rewrite, _substitute, range_start, AxisType
|
||||
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const, invalid_gate
|
||||
from tinygrad.uop.symbolic import symbolic, invalid_gate
|
||||
from tinygrad.helpers import partition
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
@@ -32,7 +32,7 @@ def simplify_merge_adjacent(u:UOp) -> UOp|None:
|
||||
s0, s1 = r0.src[0], r1.src[0]
|
||||
# do the merge
|
||||
new_range = r0.replace(src=(s0*s1,))
|
||||
nidx = graph_rewrite(u, _substitute+symbolic+pm_fold_cast_const+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1},
|
||||
nidx = graph_rewrite(u, _substitute+symbolic+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1},
|
||||
name=f"check_merge_{r0.arg[0]}_{r1.arg[0]}")
|
||||
|
||||
# check if it simplifies
|
||||
@@ -137,7 +137,7 @@ def reduce_collapse(red:UOp, u:UOp, pm:PatternMatcher=pm_reduce_collapse) -> UOp
|
||||
for u in included:
|
||||
for s in u.src:
|
||||
if s in included or s in replaces or s.op in {Ops.CONST, Ops.PARAM, Ops.BUFFER}: continue
|
||||
replaces[s] = UOp.variable(f'in{len(replaces)}', s.vmin, s.vmax, s.dtype)
|
||||
replaces[s] = UOp.variable(f'in{len(replaces)}', s.vmin, s.vmax, s.dtype, param=True)
|
||||
collapse_fxn = u.substitute(replaces).reduce(r, arg=Ops.ADD)
|
||||
sink = graph_rewrite(collapse_fxn, pm, name="reduce_collapse")
|
||||
if not no_range(sink): return None
|
||||
|
||||
@@ -44,7 +44,7 @@ def graph_split_rewrite(linear:UOp, max_batch_size:int=0) -> UOp:
|
||||
current_batch, current_batch_devs = [], []
|
||||
|
||||
for si in linear.src:
|
||||
devs = dedup([Device[x] for b in si.src[1:] if b.op is not Ops.BIND for x in (b.device if isinstance(b.device, tuple) else (b.device,))])
|
||||
devs = dedup([Device[x] for b in si.src[1:] if not b.is_bound_var for x in (b.device if isinstance(b.device, tuple) else (b.device,))])
|
||||
graph_t = graph_class(devs[0]) if devs[0].graph is not None else None
|
||||
|
||||
can_graph = graph_t is not None and graph_t.supports_uop(devs, si)
|
||||
|
||||
@@ -12,7 +12,7 @@ from tinygrad.codegen.opt.postrange import args_from_ast
|
||||
|
||||
# **************** Helpers ****************
|
||||
|
||||
def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call.src[1:] if s.op is not Ops.BIND)
|
||||
def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call.src[1:] if not s.is_bound_var)
|
||||
|
||||
def get_call_outs_ins(call:UOp) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||
ast = call.src[0]
|
||||
|
||||
@@ -12,7 +12,7 @@ def add_to_ctx(ctx, x:UOp):
|
||||
return ret
|
||||
|
||||
pm_ctx = PatternMatcher([
|
||||
(UPat((Ops.BUFFER, Ops.BIND), name="x"), add_to_ctx),
|
||||
(UPat(Ops.BUFFER, name="x"), add_to_ctx),
|
||||
(UPat((Ops.AFTER, Ops.CONTIGUOUS), name="x"),
|
||||
lambda ctx,x: add_to_ctx(ctx,x) if not x.op_in_backward_slice_with_self(Ops.PARAM) and x.op_in_backward_slice_with_self(Ops.BUFFER) else None),
|
||||
])
|
||||
|
||||
@@ -264,6 +264,7 @@ class ClangRenderer(CStyleLanguage):
|
||||
nan = '__builtin_nanf("")'
|
||||
|
||||
# language options
|
||||
barrier = "__atomic_thread_fence(__ATOMIC_SEQ_CST);"
|
||||
buffer_suffix = " restrict"
|
||||
type_map = {dtypes.bool:"_Bool", dtypes.half:"__fp16"}
|
||||
code_for_op = {**({k:v for k,v in CStyleLanguage.code_for_op.items() if k not in [Ops.EXP2, Ops.SIN, Ops.LOG2, Ops.TRUNC, Ops.RECIPROCAL]}),
|
||||
|
||||
@@ -142,7 +142,7 @@ base_rewrite = PatternMatcher([
|
||||
(UPat(Ops.IF, name="x"), lambda ctx,x: f" br i1 {ctx[x.src[0]]}, label %ifbody_{ctx[x][1:]}, label %ifskip_{ctx[x][1:]}\nifbody_{ctx[x][1:]}:"),
|
||||
(UPat(Ops.ENDIF, name="x"), lambda ctx,x: f" br label %ifskip_{ctx[x.src[0]][1:]}\nifskip_{ctx[x.src[0]][1:]}:"),
|
||||
|
||||
(UPat(Ops.BARRIER), lambda ctx: "")
|
||||
(UPat(Ops.BARRIER), lambda ctx: " fence seq_cst")
|
||||
])
|
||||
|
||||
class LLVMRenderer(Renderer):
|
||||
@@ -238,8 +238,8 @@ class AMDLLVMRenderer(LLVMRenderer):
|
||||
(UPat(Ops.CAST, dtypes.fp8s, (UPat(dtype=dtypes.float),), name="x",), lambda ctx,x:
|
||||
f" {ctx[x]} = call i8 @f32_to_fp8({ldt(x.src[0].dtype)} {ctx[x.src[0]]}, i1 {'1' if x.dtype == dtypes.fp8e5m2 else '0'})"),
|
||||
(UPat(Ops.CAST, dtypes.float, (UPat.var("y", dtypes.fp8s),), name="x",), lambda ctx,x,y:
|
||||
f" {ctx[x.src[0]]}_i32 = zext i8 {ctx[x.src[0]]} to i32\n"
|
||||
f" {ctx[x]} = call float @llvm.amdgcn.cvt.f32.{'bf8' if y.dtype == dtypes.fp8e5m2 else 'fp8'}(i32 {ctx[x.src[0]]}_i32, i32 0)"),
|
||||
f" {ctx[x]}_i32 = zext i8 {ctx[x.src[0]]} to i32\n"
|
||||
f" {ctx[x]} = call float @llvm.amdgcn.cvt.f32.{'bf8' if y.dtype == dtypes.fp8e5m2 else 'fp8'}(i32 {ctx[x]}_i32, i32 0)"),
|
||||
]) + base_rewrite
|
||||
extra_matcher = LLVMRenderer.extra_matcher + create_non_native_float_pats(dtypes.fp8s) + PatternMatcher([
|
||||
# amd llvm intrinsics llvm.log2/llvm.exp2 don't support double
|
||||
|
||||
@@ -137,7 +137,8 @@ class NIRRenderer(Renderer):
|
||||
(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. NOTE: this doesn't apply to image indexing cause it's 1-D
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), 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.addrspace != AddrSpace.REG and not is_image_shape(buf._shape) else None),
|
||||
src=(buf,UOp.const(off.val, dtypes.long) if off.op is Ops.CONST else off.cast(dtypes.long))+x.src[2:])
|
||||
if buf.addrspace != AddrSpace.REG and not is_image_shape(buf._shape) else None),
|
||||
# images need index to be int for nir (coordinates only: the INDEX keeps its access dtype)
|
||||
(UPat.var("buf").index(UPat.var("idx_y"), UPat.var("idx_x"), name="x"),
|
||||
lambda x,buf,idx_y,idx_x: x.replace(src=(buf, idx_y.cast(dtypes.int), idx_x.cast(dtypes.int)))),
|
||||
|
||||
@@ -790,14 +790,10 @@ class KFDIface:
|
||||
|
||||
def create_queue(self, queue_type, ring, gart, rptr, wptr, eop_buffer=None, cwsr_buffer=None, ctl_stack_size=0, ctx_save_restore_size=0,
|
||||
xcc_id=0, idx=0):
|
||||
if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA and idx and self.kfd_ver >= (1,17):
|
||||
queue_type = kfd.KFD_IOC_QUEUE_TYPE_SDMA_BY_ENG_ID
|
||||
sdma_engine_id = idx % (self.props['num_sdma_engines'] + self.props.get('num_sdma_xgmi_engines', 0))
|
||||
else: sdma_engine_id = 0
|
||||
queue = kfd.AMDKFD_IOC_CREATE_QUEUE(KFDIface.kfd, ring_base_address=ring.va_addr, ring_size=ring.size, gpu_id=self.gpu_id,
|
||||
queue_type=queue_type, queue_percentage=kfd.KFD_MAX_QUEUE_PERCENTAGE|(xcc_id<<8), queue_priority=getenv("AMD_KFD_QUEUE_PRIORITY", 7),
|
||||
eop_buffer_address=eop_buffer.va_addr if eop_buffer else 0, eop_buffer_size=eop_buffer.size if eop_buffer else 0, ctl_stack_size=ctl_stack_size,
|
||||
ctx_save_restore_address=cwsr_buffer.va_addr if cwsr_buffer else 0, ctx_save_restore_size=ctx_save_restore_size, sdma_engine_id=sdma_engine_id,
|
||||
ctx_save_restore_address=cwsr_buffer.va_addr if cwsr_buffer else 0, ctx_save_restore_size=ctx_save_restore_size,
|
||||
write_pointer_address=gart.va_addr+wptr, read_pointer_address=gart.va_addr+rptr+8*xcc_id)
|
||||
|
||||
if not hasattr(self, 'doorbells'):
|
||||
|
||||
+125
-112
@@ -1,11 +1,11 @@
|
||||
from __future__ import annotations
|
||||
import platform, sys, os, ctypes, ctypes.util, functools, mmap, threading, array, itertools
|
||||
from dataclasses import replace
|
||||
from typing import cast
|
||||
from tinygrad.helpers import to_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le, partition
|
||||
from tinygrad.device import Buffer, BufferSpec, TinyELF
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, HCQArgsState, HCQSignal, HCQProgram, MMIOInterface
|
||||
from tinygrad.runtime.support.hcq import CLikeArgsState
|
||||
import platform, sys, os, ctypes, functools, mmap, threading, array, struct, time
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import cast, Callable
|
||||
from tinygrad.helpers import to_mv, from_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le, to_tuple
|
||||
from tinygrad.device import Buffer, BufferSpec, TinyELF, Program, Device
|
||||
from tinygrad.runtime.support.hcq import HCQBuffer, MMIOInterface
|
||||
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, make_cmdbuf, make_signal
|
||||
from tinygrad.renderer.cstyle import ClangRenderer
|
||||
from tinygrad.renderer.llvmir import CPULLVMRenderer
|
||||
from tinygrad.renderer.nir import LVPRenderer
|
||||
@@ -13,11 +13,15 @@ from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
from tinygrad.runtime.support.elf import jit_loader
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from tinygrad.codegen import do_to_program
|
||||
from tinygrad.engine.realize import pm_flatten_linear, get_call_arg_uops, get_runtime
|
||||
from tinygrad import UOp, dtypes
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.uop.ops import sint, KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
|
||||
from tinygrad.uop.ops import KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
|
||||
|
||||
MAX_ARGS, CMD_SIZE, RING_SLOTS = 63, 64, (16 << 10)
|
||||
MAX_ARGS, CMD_SIZE, RING_SLOTS, FUNCS = 63, 64, (16 << 10), (() if WIN else ('clock_gettime', 'sem_wait', 'sem_post'))
|
||||
|
||||
# *****************
|
||||
# 1. workers
|
||||
|
||||
def signal_prog():
|
||||
val = UOp.param(1, dtypes.int, (), vmin_vmax=(0, dtypes.int.max), name="value", addrspace=AddrSpace.ALU)
|
||||
@@ -35,79 +39,86 @@ def timestamp_prog():
|
||||
val = ts.after(call)[0].load() * 1_000_000_000 + ts.after(call)[1].load()
|
||||
return UOp.param(0, dtypes.uint64, (1,))[0].store(val)
|
||||
|
||||
def quit_prog():
|
||||
fn = UOp.param(0, dtypes.uint64, (1 if WIN else 3,))
|
||||
if WIN: return fn[0].load().call(UOp.const(0, dtypes.uint64), ret_dtype=dtypes.void) # ExitThread(0)
|
||||
sem = UOp.param(1, dtypes.uint64, (1,))
|
||||
|
||||
close = fn[2].load().call(sem[0], ret_dtype=dtypes.void) # sem_close(sem)
|
||||
return fn.after(close)[0].load().call(UOp.const(0, dtypes.uint64), ret_dtype=dtypes.void) # pthread_exit(0)
|
||||
|
||||
def worker_prog():
|
||||
ring = UOp.param(0, dtypes.uint64, (RING_SLOTS * CMD_SIZE,), volatile=True)
|
||||
wait, sem = UOp.param(1, dtypes.uint64, (1,), volatile=True), UOp.param(2, dtypes.uint64, (1,))
|
||||
cur = UOp.range(2**64-1, 0, dtype=dtypes.uint64)
|
||||
wait, done = UOp.param(1, dtypes.uint64, (1,), volatile=True), UOp.param(2, dtypes.uint64, (1,), volatile=True)
|
||||
sem, cur = UOp.param(3, dtypes.uint64, (1,)), UOp.range(2**64-1, 0, dtype=dtypes.uint64) # sem is unused on windows, it has to come last
|
||||
|
||||
# spin on windows, sem_wait to sleep on posix
|
||||
if WIN: ready = (v:=wait.after(lw:=UOp.loop(1), cur)[0].load()).end(lw, v <= cur)
|
||||
else: ready = (rv:=wait.after(lw:=UOp.loop(1), cur)[0].load().call(sem.after(cur)[0], ret_dtype=dtypes.int)).end(lw, rv != 0)
|
||||
|
||||
entry = [ring.after(ready).index((cur % RING_SLOTS) * CMD_SIZE + i).load() for i in range(CMD_SIZE)]
|
||||
return entry[0].call(*entry[1:], ret_dtype=dtypes.void).end(cur)
|
||||
return done.after(entry[0].call(*entry[1:], ret_dtype=dtypes.void)).index(0).store(cur + 1).end(cur)
|
||||
|
||||
def host_wait(ctx, dst:UOp, val:UOp) -> UOp:
|
||||
return (cur:=dst.after(loop:=UOp.loop(next(ctx))).index(UOp.const(0, dtypes.int)).load()).end(loop, cur < val)
|
||||
@dataclass
|
||||
class CPUWorker: ring:Buffer; put:Buffer; sem:Buffer; sys:Buffer; done:Buffer; thread:threading.Thread # noqa: E702
|
||||
|
||||
pm_host_opsel = PatternMatcher([(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), host_wait)])
|
||||
# *****************
|
||||
# 2. queue encoders
|
||||
|
||||
def encode_host_queue(q:UOp) -> UOp:
|
||||
# TODO: subset of hcq2 for now
|
||||
spins, (store,) = partition(graph_rewrite(q, pm_host_opsel, ctx=itertools.count(), walk=True, name="host opsel").src, lambda u: u.op is Ops.END)
|
||||
assert store.op is Ops.INS and store.arg == "store", f"host queue cannot encode {store.op} {store.arg}"
|
||||
return store.src[0].after(*spins).index(UOp.const(0, dtypes.int)).store(store.src[1])
|
||||
def cpu_cmd(devs:tuple[str, ...], prog, *args:UOp) -> UOp:
|
||||
progs = [get_runtime(d, prog) if isinstance(prog, UOp) else cast(CPUDevice, Device[d]).prgs[prog] for d in devs]
|
||||
addrs = tuple(UOp.const(p.addr, dtypes.uint64) for p in progs)
|
||||
words = ((addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, dtypes.uint64, addrs)),) + args
|
||||
return UOp(Ops.INS, dtypes.void, words + (UOp.const(0, dtypes.uint64),) * (CMD_SIZE - len(words)), arg="cmd")
|
||||
|
||||
class CPUComputeQueue(HWQueue):
|
||||
def __init__(self, dev):
|
||||
super().__init__()
|
||||
self.dev = dev
|
||||
def _cmd(self, prog, args=(), vals=()): return self.exec(prg:=self.dev.prgs[prog], prg.fill_kernargs(args, vals), None, None)
|
||||
def memory_barrier(self): return self
|
||||
def exec(self, prg:CPUProgram, args_state:HCQArgsState, global_size, local_size):
|
||||
if (lvp:=isinstance(args_state, LVPArgsState)): self.bind_args_state(args_state)
|
||||
args:list[sint|None] = [args_state.buf.va_addr] if lvp else [*[x.va_addr for x in args_state.bufs], *args_state.vals]
|
||||
assert len(args) <= MAX_ARGS, f"CPU programs support at most {MAX_ARGS} arguments, got {len(args)}"
|
||||
for tid in range(1 if lvp else (global_size or (1,))[0]):
|
||||
if not lvp and 'core_id' in prg.runtimevars: args[prg.runtimevars['core_id']] = tid
|
||||
self.q(prg, *[unwrap(x) for x in args], *([0] * (MAX_ARGS - len(args))))
|
||||
return self
|
||||
def wait(self, signal, value=0): return self._cmd(wait_prog, (signal.base_buf,), (value,))
|
||||
def timestamp(self, signal): return self._cmd(timestamp_prog, (signal.base_buf.offset(8, 8), self.dev.func_table._buf.offset(0, 8)))
|
||||
def signal(self, signal, value:sint=0): return self._cmd(signal_prog, (signal.base_buf,), (value,))
|
||||
def _submit(self, dev):
|
||||
dev.ensure_worker()
|
||||
ring_view = dev.ring.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
for off in range(0, len(self._q), CMD_SIZE):
|
||||
entry = [self._q[off].addr, *self._q[off+1:off+CMD_SIZE]]
|
||||
ring_view[(base:=(dev.ring_pos % RING_SLOTS) * CMD_SIZE):base+CMD_SIZE] = array.array('Q', (int(x) & ((1<<64)-1) for x in entry))
|
||||
dev.ring_pos += 1
|
||||
if WIN: dev.sys.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = dev.ring_pos
|
||||
else: assert libc.sem_post(dev.sem) == 0
|
||||
def cpu_exec(ctx:tuple[str, ...], call:UOp, prg:UOp) -> UOp:
|
||||
args = [get_call_arg_uops(call)[i].getaddr(ctx) for i in prg.arg.globals] + [v.cast(dtypes.uint64) for v in prg.arg.vars]
|
||||
if (core:=prg.arg.runtimevars.get('core_id')) is None: return cpu_cmd(ctx, prg, *args)
|
||||
|
||||
class LVPArgsState(CLikeArgsState):
|
||||
def __init__(self, buf, prg, bufs, vals=()): super().__init__(buf, prg, bufs, vals, [*data64_le(buf.va_addr + 12), (len(bufs) + len(vals)) * 2])
|
||||
la = [cpu_cmd(ctx,prg,*args[:(cid:=(len(prg.arg.globals)+core))],UOp.const(t, dtypes.uint64),*args[cid+1:]) for t in range(prg.arg.global_size[0])]
|
||||
return UOp(Ops.LINEAR, dtypes.void, tuple(la))
|
||||
|
||||
pm_cpu_opsel = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), cpu_exec),
|
||||
|
||||
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
|
||||
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))),
|
||||
lambda ctx, dst, val: cpu_cmd(ctx, wait_prog, dst.getaddr(ctx), val.cast(dtypes.uint64))),
|
||||
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))),
|
||||
lambda ctx, dst, val: cpu_cmd(ctx, signal_prog, dst.getaddr(ctx), val.cast(dtypes.uint64))),
|
||||
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)),
|
||||
lambda ctx, dst: cpu_cmd(ctx, timestamp_prog, dst.getaddr(ctx), *(() if WIN else (make_signal(ctx, tag="func:clock_gettime").getaddr(ctx),)))),
|
||||
])
|
||||
|
||||
def encode_queue(q:UOp) -> UOp:
|
||||
devs, queue = to_tuple(q.arg[0]), q.arg[1]
|
||||
lin = graph_rewrite(q, pm_cpu_opsel+pm_flatten_linear, ctx=devs, walk=True, name=f"{queue} opsel")
|
||||
|
||||
cnt = sum(len(ins.src) for ins in lin.src) // CMD_SIZE
|
||||
assert cnt < RING_SLOTS, f"submit of {cnt} entries doesn't fit the ring"
|
||||
cmdbuf = make_cmdbuf(lin, devs, buf=UOp.placeholder((cnt*CMD_SIZE,), dtypes.uint64, next(UOp.unique_num), device=devs).rtag("cmdbuf"))
|
||||
ring = UOp.placeholder((ring_words:=RING_SLOTS*CMD_SIZE,), dtypes.uint64, 0, device=devs, volatile=True).rtag(f"{queue}_ring")
|
||||
put, done, sem, sysbuf = (make_signal(devs, tag=f"{queue}_{name}") for name in ("put", "done", "sem", "sys"))
|
||||
|
||||
# submits are serialized on the submitter, so they can bump put without atomics
|
||||
ran = done.after(l:=UOp.loop(next(UOp.unique_num))).index(0).load()
|
||||
room = ran.end(l, put.index(0).load() - ran > RING_SLOTS - cnt) # wait until cnt entries fit in the ring
|
||||
base = ((put.after(room).index(0).load() % RING_SLOTS) * CMD_SIZE).cast(dtypes.int)
|
||||
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(cmdbuf, ring))
|
||||
copy = UOp.group(*[ring.index((base + e*CMD_SIZE + w) % ring_words).store(cmdbuf.index(e*CMD_SIZE + w).load()) for w in range(CMD_SIZE)])
|
||||
|
||||
# wake the worker after each entry, keeping the post with the stores stops it from hoisting out of the loop
|
||||
wake = copy.end(e) if WIN else make_signal(devs, tag="func:sem_post").after(copy).index(0).load().call(sem.index(0), ret_dtype=dtypes.void).end(e)
|
||||
bumped = put.after(wake).index(0).store(put.index(0).load() + cnt)
|
||||
return sysbuf.after(bumped).index(0).store(put.index(0).load() + cnt) if WIN else bumped
|
||||
|
||||
# *****************
|
||||
|
||||
# NOTE: MAP_JIT is added to mmap module in python 3.13
|
||||
MAP_JIT = 0x0800
|
||||
|
||||
class CPUProgram(HCQProgram['CPUDevice']):
|
||||
class CPUProgram(Program['CPUDevice']):
|
||||
rt_lib = None
|
||||
try: rt_lib = ctypes.CDLL(ctypes.util.find_library('System' if OSX else 'kernel32') if OSX or WIN else 'libgcc_s.so.1')
|
||||
except OSError: pass
|
||||
|
||||
def __init__(self, dev:CPUDevice, obj:TinyELF):
|
||||
self.signature, self.runtimevars = obj.signature, {name:slot for name,slot,*_ in obj.signature if name == 'core_id'}
|
||||
self.dev, self.name, self.signature = dev, obj.name, obj.signature
|
||||
self.runtimevars = {name:slot for name,slot,*_ in obj.signature if name == 'core_id'}
|
||||
self.lvp = obj.target.renderer == "LVP"
|
||||
|
||||
LVP = obj.target.renderer == "LVP"
|
||||
if sys.platform == "win32": # mypy doesn't understand when WIN is used here
|
||||
PAGE_EXECUTE_READWRITE, MEM_COMMIT, MEM_RESERVE = 0x40, 0x1000, 0x2000
|
||||
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
|
||||
@@ -117,7 +128,7 @@ class CPUProgram(HCQProgram['CPUDevice']):
|
||||
ctypes.windll.kernel32.GetCurrentProcess.restype = ctypes.c_void_p
|
||||
proc = ctypes.windll.kernel32.GetCurrentProcess()
|
||||
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.addr), ctypes.c_size_t(len(obj.lib)))
|
||||
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
|
||||
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
|
||||
else:
|
||||
# On apple silicon with SPRR enabled (it always is in macos) RWX pages are unrepresentable: https://blog.svenpeter.dev/posts/m1_sprr_gxf/
|
||||
# MAP_JIT allows us to easily flip pages from RW- to R-X and vice versa. It is a noop on intel cpus. (man pthread_jit_write_protect_np)
|
||||
@@ -125,7 +136,7 @@ class CPUProgram(HCQProgram['CPUDevice']):
|
||||
self.addr = mv_address(self.mem)
|
||||
|
||||
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(False)
|
||||
lib = jit_loader(obj.lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m']) if LVP else obj.lib
|
||||
lib = jit_loader(obj.lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m']) if self.lvp else obj.lib
|
||||
self.mem.write(lib)
|
||||
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(True)
|
||||
|
||||
@@ -138,15 +149,30 @@ class CPUProgram(HCQProgram['CPUDevice']):
|
||||
# msync should be a universal POSIX way to do this
|
||||
libc.msync(ctypes.c_void_p(self.addr), len(lib), libc.MS_SYNC | libc.MS_INVALIDATE)
|
||||
|
||||
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
|
||||
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
|
||||
|
||||
super().__init__(LVPArgsState if LVP else HCQArgsState, dev, obj, kernargs_alloc_size=12+256 if LVP else 0)
|
||||
def __call__(self, *bufs:HCQBuffer, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1),
|
||||
vals:tuple[int|None, ...]=(), wait:bool=False, timeout:int|None=None) -> float|None:
|
||||
st = time.perf_counter()
|
||||
if self.lvp:
|
||||
lvp_args = bytearray(12 + (len(bufs) + len(vals)) * 8)
|
||||
addr = mv_address(lvp_args)
|
||||
struct.pack_into(f'<3I{len(bufs)}Q', lvp_args, 0, *data64_le(addr+12), (len(bufs)+len(vals))*2, *[b.va_addr for b in bufs])
|
||||
for v,(off,dt) in zip(vals, TinyELF.iter_sig(self.signature[-len(vals):], len(bufs)*8)): struct.pack_into(f'<{dt.fmt}', lvp_args, 12+off, v)
|
||||
self.fxn(addr)
|
||||
else:
|
||||
args = [*[cast(int, b.va_addr) for b in bufs], *cast(tuple[int, ...], vals)]
|
||||
assert len(args) <= MAX_ARGS, f"CPU programs support at most {MAX_ARGS} arguments, got {len(args)}"
|
||||
for tid in range(global_size[0]):
|
||||
if 'core_id' in self.runtimevars: args[self.runtimevars['core_id']] = tid
|
||||
self.fxn(*[ctypes.c_uint64(x) for x in args])
|
||||
return time.perf_counter() - st if wait else None
|
||||
|
||||
@suppress_finalizing
|
||||
def __del__(self):
|
||||
if sys.platform == 'win32': ctypes.windll.kernel32.VirtualFree(ctypes.c_void_p(self.addr), ctypes.c_size_t(0), 0x8000) #0x8000 - MEM_RELEASE
|
||||
|
||||
class CPUAllocator(HCQAllocator):
|
||||
class CPUAllocator(HCQAllocator['CPUDevice']):
|
||||
def __init__(self, dev:CPUDevice): super().__init__(dev, supports_copy_from_disk=False, supports_transfer=False)
|
||||
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
|
||||
if options.external_ptr is not None: addr, buf = options.external_ptr, None
|
||||
@@ -154,68 +180,55 @@ class CPUAllocator(HCQAllocator):
|
||||
else: addr = mv_address(buf:=mmap.mmap(-1, size, mmap.MAP_ANON | mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE))
|
||||
return HCQBuffer(va:=addr, sz:=size, meta=buf, view=MMIOInterface(va, sz, fmt='B'), owner=self.dev)
|
||||
def _as_buffer(self, src) -> memoryview: return to_mv(src.va_addr, src.size)
|
||||
def _copyin(self, dest:HCQBuffer, src:memoryview):
|
||||
self.dev.synchronize()
|
||||
ctypes.memmove(int(dest.va_addr), from_mv(src), len(src))
|
||||
def _copyout(self, dest:memoryview, src:HCQBuffer):
|
||||
self.dev.synchronize()
|
||||
ctypes.memmove(from_mv(dest), int(src.va_addr), len(dest))
|
||||
def _do_map(self, buf:HCQBuffer):
|
||||
if buf.view is None or not isinstance(buf.view, MMIOInterface): raise RuntimeError("Cannot map buffer without view to cpu")
|
||||
return HCQBuffer(buf.view.addr, buf.size, view=buf.view, owner=buf.owner)
|
||||
def _unmap(self, mb): pass # CPU _do_map returns a view wrapper, nothing to release
|
||||
|
||||
class CPUDevice(HCQCompiled):
|
||||
pm_lower = PatternMatcher([
|
||||
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_host_queue)])
|
||||
|
||||
pm_bufferize = PatternMatcher([
|
||||
(UPat(Ops.PARAM, tag="sentinel_signal"), lambda ctx: ctx[0].signal("sentinel", (1 << 64) - 1)),
|
||||
(UPat(Ops.PARAM, tag="timeline_signal"), lambda ctx: ctx[0].signal("timeline")),
|
||||
(UPat(Ops.PARAM, tag="timeline_value"), lambda ctx: ctx[0].signal("value", 1)),
|
||||
(UPat(Ops.PARAM, tag="signal", name="b"), lambda ctx, b: ctx[0].signal(b.arg.slot)),
|
||||
])
|
||||
|
||||
@functools.cache
|
||||
def signal(self, name:str|int, init_value:int=0) -> Buffer:
|
||||
(buf:=Buffer(self.device, 1, dtypes.uint64, preallocate=True)).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
|
||||
return buf
|
||||
class CPUDevice(HCQ2Compiled):
|
||||
wait_timeout_ms, has_copy_queue = 30000, False
|
||||
pm_lower = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue)])
|
||||
|
||||
def __init__(self, device:str=""):
|
||||
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], CPUProgram, HCQSignal,
|
||||
functools.partial(CPUComputeQueue, self), arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
|
||||
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], CPUProgram,
|
||||
arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
|
||||
|
||||
self.ring_pos = 0
|
||||
self.pm_bufferize = PatternMatcher(
|
||||
[(UPat(Ops.PARAM, tag=f"COMPUTE:0_{n}"), lambda ctx, n=n: getattr(ctx[0].worker, n)) for n in ("ring", "put", "sem", "sys", "done")] +
|
||||
[(UPat(Ops.PARAM, tag=f"func:{f}"), lambda ctx, f=f: ctx[0].func_ptr(f)) for f in FUNCS]) + self.pm_bufferize
|
||||
|
||||
# posix uses sem to put cpus into sleep
|
||||
self.sem_addr = 0
|
||||
if not WIN:
|
||||
self.sem = libc.sem_open(sem_name:=f"/tinygrad-{os.getpid()}-{id(self):x}".encode(), os.O_CREAT|os.O_EXCL, 0o600, 0) # type: ignore[call-arg]
|
||||
self.sem_addr = unwrap(ctypes.cast(self.sem, ctypes.c_void_p).value)
|
||||
if self.sem_addr == ctypes.c_void_p(-1).value or libc.sem_unlink(sem_name): raise OSError(ctypes.get_errno(), "semaphore")
|
||||
|
||||
# TODO: move to hcq2
|
||||
with Context(EMULATED_DTYPES="", TRACK_MATCH_STATS=0):
|
||||
prgs = {f: f().sink(arg=KernelInfo(f.__name__), tag=1) for f in (signal_prog, wait_prog, timestamp_prog, quit_prog, worker_prog)}
|
||||
self.prgs = {f: self.runtime(do_to_program(v, ClangRenderer(replace(self.renderer.target, renderer="CLANG"))).to_elf()) for f,v in prgs.items()}
|
||||
clang = ClangRenderer(replace(self.renderer.target, renderer="CLANG"))
|
||||
self.prgs:dict[Callable, CPUProgram] = {f: CPUProgram(self, do_to_program(f().sink(arg=KernelInfo(f.__name__), tag=1), clang).to_elf())
|
||||
for f in (signal_prog, wait_prog, timestamp_prog, worker_prog)}
|
||||
|
||||
@functools.cached_property
|
||||
def ring(self) -> Buffer: return Buffer(self.device, RING_SLOTS * CMD_SIZE, dtypes.uint64, preallocate=True)
|
||||
@functools.cached_property
|
||||
def sys(self) -> Buffer: return Buffer(self.device, 1, dtypes.uint64, preallocate=True)
|
||||
@functools.cached_property
|
||||
def sem_buf(self) -> Buffer: return Buffer(self.device, 1, dtypes.uint8, options=BufferSpec(external_ptr=self.sem_addr), preallocate=True)
|
||||
def func_ptr(self, name:str) -> Buffer: return self.func_table.view(1, dtypes.uint64, FUNCS.index(name)*8).ensure_allocated()
|
||||
|
||||
# TODO: move to hcq2 infra
|
||||
@functools.cached_property
|
||||
def func_table(self) -> Buffer:
|
||||
fns = ([0, ctypes.windll.kernel32.ExitThread, 0, 0] if WIN else # type: ignore[attr-defined]
|
||||
[libc.dll.clock_gettime, libc.dll.pthread_exit, libc.dll.sem_wait, libc.dll.sem_close])
|
||||
addrs = array.array('Q', [unwrap(ctypes.cast(f, ctypes.c_void_p).value) if f else 0 for f in fns])
|
||||
(ft:=Buffer(self.device, len(fns), dtypes.uint64, preallocate=True)).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[:] = addrs
|
||||
lib = ctypes.windll.kernel32 if sys.platform == "win32" else libc.dll # type: ignore[attr-defined]
|
||||
(ft:=Buffer(self.device, len(FUNCS), dtypes.uint64, preallocate=True))._buf.cpu_view().view(fmt='Q')[:] = \
|
||||
array.array('Q', [unwrap(ctypes.cast(getattr(lib, f), ctypes.c_void_p).value) for f in FUNCS])
|
||||
return ft
|
||||
|
||||
@functools.cache
|
||||
def ensure_worker(self):
|
||||
threading.Thread(target=cast(CPUProgram, self.prgs[worker_prog]).fxn, daemon=True, args=[ctypes.c_uint64(x) for x in
|
||||
[self.ring._buf.va_addr, self.sys._buf.va_addr if WIN else self.func_table._buf.va_addr+16, self.sem_addr]]).start()
|
||||
@functools.cached_property
|
||||
def worker(self) -> CPUWorker:
|
||||
ring, put, sysbuf, done = (Buffer(self.device, sz, dtypes.uint64, preallocate=True) for sz in (RING_SLOTS*CMD_SIZE, 1, 1, 1))
|
||||
addr, hsem = 0, None
|
||||
|
||||
def finalize(self):
|
||||
if self.ring_pos == 0: return # the worker starts with the first submit
|
||||
ft = self.func_table._buf
|
||||
CPUComputeQueue(self)._cmd(quit_prog, (ft.offset(8, 8),) if WIN else (ft.offset(8, 24), self.sem_buf._buf)).submit(self)
|
||||
self.ring_pos = 0
|
||||
# sem are posix-only
|
||||
if not WIN:
|
||||
hsem = libc.sem_open(nm:=f"/tinygrad-{os.getpid()}-{id(ring):x}".encode(), os.O_CREAT|os.O_EXCL, 0o600, 0) # type: ignore[call-arg]
|
||||
if (addr:=unwrap(ctypes.cast(hsem, ctypes.c_void_p).value)) == ctypes.c_void_p(-1).value or libc.sem_unlink(nm):
|
||||
raise OSError(ctypes.get_errno(), "semaphore")
|
||||
sem = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(external_ptr=addr), preallocate=True)
|
||||
|
||||
worker_args = [ring._buf.va_addr, sysbuf._buf.va_addr if WIN else self.func_ptr('sem_wait')._buf.va_addr, done._buf.va_addr, addr]
|
||||
(worker:=threading.Thread(target=self.prgs[worker_prog].fxn, daemon=True, args=[ctypes.c_uint64(x) for x in worker_args])).start()
|
||||
return CPUWorker(ring, put, sem, sysbuf, done, worker)
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# works to test the tensor cores, and all the uops in general
|
||||
# this is the (living) definition of uops
|
||||
from typing import Any, TYPE_CHECKING
|
||||
import pickle, base64, itertools, time, sys, functools
|
||||
import pickle, base64, itertools, time, sys, functools, ctypes
|
||||
from dataclasses import replace
|
||||
from tinygrad.dtype import bitcast, DType, dtypes, AddrSpace, truncate, storage_fmt_for_dtype, to_storage_scalar, from_storage_scalar
|
||||
from tinygrad.helpers import all_same, getenv, flatten, Target, IMAGE, is_image_shape, cpu_profile
|
||||
from tinygrad.helpers import all_same, getenv, flatten, Target, IMAGE, is_image_shape, cpu_profile, mv_address
|
||||
from tinygrad.device import Buffer, Compiled, Compiler, Allocator, Program, TinyELF
|
||||
from tinygrad.codegen.opt import tc
|
||||
from tinygrad.uop.ops import exec_alu, python_alu, Ops, UOp, GroupOp
|
||||
@@ -134,6 +134,13 @@ class PythonProgram(Program['PythonDevice']):
|
||||
for k in range(len(src_values))], j, u.dtype) for j in range(load_sz)]
|
||||
else:
|
||||
values[u] = load(src_values, 0, u.dtype)
|
||||
elif u.op is Ops.CALL:
|
||||
assert u.dtype is dtypes.void
|
||||
cfunc = ctypes.CFUNCTYPE(None, *[ctypes.c_uint64] * (len(src_values)-1))
|
||||
values[u] = []
|
||||
for args,gate in zip(zip(*src_values), exec_masks[-1]):
|
||||
call_args = [(mv_address(x[0]) + x[1]*dt.itemsize) if isinstance(x, tuple) else x for x,dt in zip(args, src_dtypes)]
|
||||
values[u].append(cfunc(call_args[0])(*call_args[1:]) if gate else None)
|
||||
elif u.op is Ops.WMMA:
|
||||
first_src_dtype = u.src[0].dtype
|
||||
assert isinstance(first_src_dtype, DType) # mypy
|
||||
|
||||
@@ -517,8 +517,7 @@ class HCQCompiled(Compiled, Generic[SignalType]):
|
||||
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
|
||||
|
||||
class HCQBuffer:
|
||||
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQBuffer|None=None, view:MMIOInterface|None=None,
|
||||
owner:HCQCompiled|None=None):
|
||||
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQBuffer|None=None, view:MMIOInterface|None=None, owner:Any=None):
|
||||
self.va_addr, self.size, self.meta, self._base, self.view = va_addr, size, meta, _base, view
|
||||
self._devs, self.owner = ([owner] if owner is not None else []), owner
|
||||
self._mappings:dict[HCQCompiled, HCQBuffer] = {} # mapping to the other devices
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Callable, TypeVar, Generic, Any, Sequence
|
||||
from typing import cast, Callable, TypeVar, Generic, Any, Sequence, Iterable
|
||||
import struct, functools, time, collections, itertools, decimal, statistics
|
||||
from dataclasses import replace, dataclass
|
||||
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap, PROFILE
|
||||
@@ -7,9 +7,9 @@ from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, Co
|
||||
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer, DepsTracker
|
||||
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEntry, ProfileGraphEvent
|
||||
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, rewrite_group, GroupOp
|
||||
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.dtype import dtypes, truncate
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface, HCQBuffer
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop
|
||||
@@ -22,7 +22,7 @@ HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
|
||||
|
||||
HCQ_RUNTIME_DEV = ContextVar("HCQ_RUNTIME_DEV", "CPU")
|
||||
|
||||
HCQ_DEVS = frozenset(("AMD",))
|
||||
HCQ_DEVS = frozenset(("AMD", "CPU"))
|
||||
HCQ_P2P_DEVS = HCQ_DEVS | frozenset(("CPU",))
|
||||
HCQ_CACHE_TAGS = frozenset(("program", "systems", "template"))
|
||||
|
||||
@@ -87,16 +87,19 @@ def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
|
||||
def replace_call_buffers(ctx:tuple[list[UOp], dict[UOp, int]], call:UOp) -> UOp|None:
|
||||
bufs, slots = ctx
|
||||
for s in call.src[1:]:
|
||||
if s.op not in (Ops.PARAM, Ops.BIND) and slots.setdefault(s, len(bufs)) == len(bufs): bufs.append(s)
|
||||
return call.replace(src=call.src[:1] + tuple(s if s.op in (Ops.PARAM, Ops.BIND) else s.param_like(slots[s]) for s in call.src[1:]))
|
||||
if s.op is not Ops.PARAM and not s.is_bound_var and slots.setdefault(s, len(bufs)) == len(bufs): bufs.append(s)
|
||||
return call.replace(src=call.src[:1] + tuple(s if s.op is Ops.PARAM or s.is_bound_var else s.param_like(slots[s]) for s in call.src[1:]))
|
||||
pm_replace_buffers = PatternMatcher([(UPat(Ops.CALL, name="call"), replace_call_buffers)])
|
||||
|
||||
# *****************
|
||||
# 1.1. prep: staging copies
|
||||
|
||||
def _need_staging(a, b): return all_devices_in(a.device, HCQ_DEVS) and not all_devices_in(b.device, HCQ_P2P_DEVS)
|
||||
def _need_staging(a, b): return all_devices_in(a.device, HCQ_DEVS - {"CPU"}) and not all_devices_in(b.device, HCQ_P2P_DEVS)
|
||||
|
||||
def hcq_call_devs(call:UOp) -> Any|None: return next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)
|
||||
def _get_enqueue_devs(call:UOp) -> Any|None:
|
||||
if not (bufs:=call.src[1:]) or not all(all_devices_in(b.device, HCQ_P2P_DEVS) for b in bufs): return None
|
||||
devs = min(bufs, key=lambda b: to_tuple(b.device)[0].startswith("CPU")).device # prio to enqueue on not CPU device
|
||||
return devs if all_devices_in(devs, HCQ_DEVS) else None
|
||||
|
||||
def stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
if not (_need_staging(src, dst) or _need_staging(dst, src)): return None
|
||||
@@ -105,7 +108,7 @@ def stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
return UOp(Ops.LINEAR, src=(src.copy_to_device("CPU").call(stage, src), stage.copy_to_device(dst.device).call(dst, stage)))
|
||||
|
||||
def kernel_copy(call:UOp, dst:UOp, src:UOp) -> UOp|None:
|
||||
if (devs:=hcq_call_devs(call)) is None or Device[(dev:=to_tuple(devs)[0])].has_copy_queue: return None
|
||||
if (devs:=_get_enqueue_devs(call)) is None or Device[(dev:=to_tuple(devs)[0])].has_copy_queue: return None
|
||||
d, s = (UOp.param(i, dst.dtype, (n:=dst.max_numel(),), device=devs) for i in range(2))
|
||||
ast = d.index(r:=UOp.range(n, 0)).store(s.index(r).load()).end(r).sink(arg=KernelInfo(name="copy"), tag=1)
|
||||
return call.replace(src=(to_program(ast, Device[dev].renderer), dst, src))
|
||||
@@ -136,7 +139,7 @@ def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tu
|
||||
|
||||
def _build_wait_cmds(slots:dict[str, int], dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
|
||||
# opt1: same-queue ops are fifo-ordered
|
||||
if devices[0].split(":")[0] in {"AMD", "QCOM"} or queue.startswith("COPY"):
|
||||
if devices[0].split(":")[0] in {"AMD", "QCOM", "CPU"} or queue.startswith("COPY"):
|
||||
dep_lanes = [(dep, dlane, lane) for dep, dlane, lane in dep_lanes if (dep[0][dlane], dep[1]) != (devices[lane], queue)]
|
||||
|
||||
# opt2: keep latest dep per (dep device, queue, cur lane)
|
||||
@@ -159,7 +162,7 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
for b in itertools.chain.from_iterable(_get_call_bufs_by_lane(call, devices)):
|
||||
for bd in to_tuple(b.device): dev_bufs[bd][id(b)] = b
|
||||
|
||||
n, fences, fins, signal_tags = len(batch_info), [], [], set()
|
||||
n, fences, resets, fins, signal_tags = len(batch_info), [], [], [], set()
|
||||
for _, devgroup in itertools.groupby(sorted(dev_bufs), key=lambda d: d.split(":")[0]):
|
||||
devs = tuple(devgroup)
|
||||
|
||||
@@ -173,16 +176,17 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
|
||||
fin_submit = make_submit(*waits, UOp(Ops.INS, arg="store", src=(tl_signal, tl_value.index(0))), devs=devs, queue="COMPUTE:0")
|
||||
epoch = (epoch_slot:=tl_value.after(fin_submit).index(0)).load()
|
||||
|
||||
# fence once per device group on this schedule's previous epoch, then reset any queue signals used by the group
|
||||
# fence once per device group on this schedule's previous epoch
|
||||
qs = dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])
|
||||
sched_epoch = make_signal(devs, next(UOp.unique_num))
|
||||
|
||||
wait_device_epoch = (done:=tl_signal.after(loop:=UOp.loop(0)).index(0).load()).end(loop, done < sched_epoch.index(0).load())
|
||||
resets = [make_signal(devs, slots[q]).after(wait_device_epoch).index(0).store(0) for q in qs]
|
||||
fences.append(make_call("hcq_fence", UOp.sink(wait_device_epoch), HCQInfo(devs)))
|
||||
|
||||
fences.append(make_call("hcq_fence", UOp.sink(*(resets or [wait_device_epoch])), HCQInfo(devs)))
|
||||
# queues of other groups wait on these signals, so reset them only after every group reached its epoch
|
||||
if qs: resets.append(make_call("hcq_reset", UOp.sink(*[make_signal(devs, slots[q]).index(0).store(0) for q in qs]), HCQInfo(devs)))
|
||||
fins.append(make_call("hcq_finalizer", UOp.sink(epoch_slot.store(epoch + 1), sched_epoch.after(fin_submit).index(0).store(epoch)), HCQInfo(devs)))
|
||||
return fences, fins, signal_tags
|
||||
return fences + resets, fins, signal_tags
|
||||
|
||||
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> list[UOp]:
|
||||
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
|
||||
@@ -229,7 +233,7 @@ def sched_hcq_batches(l:UOp, profile:bool) -> UOp:
|
||||
srcs:list[UOp] = []
|
||||
batch:list[tuple[UOp, tuple[str, ...]]] = []
|
||||
for call in l.src:
|
||||
if (devs:=hcq_call_devs(call)) is not None: batch.append((call, to_tuple(devs)))
|
||||
if (devs:=_get_enqueue_devs(call)) is not None: batch.append((call, to_tuple(devs)))
|
||||
else: srcs, batch = srcs + _finalize_batch(batch, profile) + [call], []
|
||||
return l.replace(src=tuple(srcs + _finalize_batch(batch, profile)))
|
||||
|
||||
@@ -354,9 +358,13 @@ pm_split_patches = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION
|
||||
|
||||
# *****************
|
||||
|
||||
def _rank_ranges(uops:Iterable[UOp]) -> dict[UOp, UOp]:
|
||||
return {r: r.replace(arg=(i,)+r.arg[1:]) for i,r in enumerate(sorted([u for u in uops if u.op is Ops.RANGE], key=lambda r: r.arg))}
|
||||
|
||||
def replace_params(call:UOp) -> UOp|None:
|
||||
body, variables, param_ops = call.src[0], call.src[0].variables(), {Ops.PARAM, Ops.MSTACK}
|
||||
args = dedup([s for u in body.toposort(gate=lambda u: u.op not in param_ops) for s in u.src if s.op in param_ops and s not in variables])
|
||||
tops = body.toposort(gate=lambda u: u.op not in param_ops)
|
||||
args = dedup([s for u in tops for s in u.src if s.op in param_ops and s not in variables])
|
||||
|
||||
patched, refhold = partition(call.src[1:], lambda x: x.src[0] in args)
|
||||
by_root = {p.src[0]: p for p in patched}
|
||||
@@ -368,7 +376,7 @@ def replace_params(call:UOp) -> UOp|None:
|
||||
refhold += [a for a in addrs if a not in held and all(b.op is not Ops.PARAM or b.tag is not None for b in unwrap_mstack(a))]
|
||||
|
||||
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=HCQ_RUNTIME_DEV.value, volatile=b.op is Ops.PARAM and b.arg.volatile)
|
||||
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM}
|
||||
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM} | _rank_ranges(tops)
|
||||
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args) if u.without_after.tag == "inputs"), None))
|
||||
return call.replace(src=(body.substitute(sub).replace(arg="hcq_args"), *c_args, *refhold),
|
||||
arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
|
||||
@@ -437,7 +445,7 @@ def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
|
||||
linear = graph_rewrite(linear, pm_encode_cmdbufs+pm_pack_placeholders, walk=True, name="encode and pack", enter_calls=True)
|
||||
|
||||
# patches and runtime uops
|
||||
linear = graph_rewrite(linear, pm_early_simplify+symbolic+pm_fold_cast_const, bottom_up=False, name="simplify patches", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_early_simplify+symbolic, bottom_up=False, name="simplify patches", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_split_patches, walk=True, name="split patches")
|
||||
|
||||
# and compile it
|
||||
@@ -469,7 +477,7 @@ def fold_binary(buf:UOp, blob:UOp) -> UOp:
|
||||
def fold_const_store(buf:UOp, off:UOp, val:UOp) -> UOp:
|
||||
for off,val in zip(off.src, val.src):
|
||||
for b,v in zip((bs:=mb.bufs if isinstance((mb:=buf.buffer), MultiBuffer) else (mb,)), val.src if val.op is Ops.STACK else (val,)*len(bs)):
|
||||
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype](v.val))
|
||||
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype]((v.src[0] if v.op is Ops.CAST else v).val))
|
||||
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(bo:=off.val*buf.dtype.itemsize):bo+len(data)] = data
|
||||
return UOp(Ops.NOOP)
|
||||
|
||||
@@ -484,7 +492,7 @@ def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
|
||||
|
||||
pm_resolve_patches = PatternMatcher([
|
||||
# multi
|
||||
(UPat(GroupOp.ALU, src=[UPat(Ops.STACK, name="s"), UPat(Ops.CONST)], name="op"), push_stack),
|
||||
(UPat(GroupOp.ALU, src=[UPat(Ops.STACK, name="s"), UPat.any(UPat(Ops.CONST), UPat(Ops.CAST, src=(UPat(Ops.CONST),)))], name="op"), push_stack),
|
||||
(UPat(Ops.CAST, src=(UPat(Ops.STACK, name="s"),), name="op"), push_stack),
|
||||
|
||||
# getaddr
|
||||
@@ -510,7 +518,7 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
|
||||
bufs = {(j,i):a for j,c in enumerate(linear.src) for i,a in enumerate(c.src[1:], 1)
|
||||
if a.op is Ops.AFTER and unwrap_mstack(a.src[0])[0].tag in HCQ_CACHE_TAGS}
|
||||
linear = linear.substitute({x:link_buf_cache[k] for a in bufs.values() if (k:=link_buf_key(a)) in link_buf_cache for x in (a, a.src[0])}, walk=True)
|
||||
linear = graph_rewrite(linear, pm_resolve_patches+symbolic+pm_fold_cast_const+pm_assert_no_afters, bpm=pm_bufferize, ctx=cache, bottom_up=False,
|
||||
linear = graph_rewrite(linear, pm_resolve_patches+symbolic+pm_assert_no_afters, bpm=pm_bufferize, ctx=cache, bottom_up=False,
|
||||
name="resolve patches")
|
||||
for (j,i),a in bufs.items(): link_buf_cache.setdefault(link_buf_key(a), linear.src[j].src[i])
|
||||
if cache: link_linear_cache[linear_key] = linear
|
||||
@@ -521,6 +529,7 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
|
||||
|
||||
class HCQ2Compiled(Compiled):
|
||||
timestamp_divider: float = 1000.0
|
||||
wait_timeout_ms: float = 30000.0
|
||||
|
||||
def __init__(self, device:str, allocator:HCQAllocator, compilers:list[type[Renderer]], runtime, can_recover:bool=False, arch=None):
|
||||
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
|
||||
@@ -577,9 +586,10 @@ class HCQ2Compiled(Compiled):
|
||||
sig = self.signal("timeline").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
tl = self.signal("value", 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
|
||||
timeout = timeout if timeout is not None and self.can_recover else None
|
||||
st = time.perf_counter()
|
||||
while sig[0] < tl[0] - 1:
|
||||
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
|
||||
st, done = time.perf_counter(), sig[0]
|
||||
while done < tl[0] - 1:
|
||||
if done != (done:=sig[0]): st = time.perf_counter()
|
||||
elif time.perf_counter() - st > (timeout or self.wait_timeout_ms) / 1000: self.on_device_hang()
|
||||
if self.prof_ents: self.collect_prof()
|
||||
|
||||
def on_device_hang(self): raise RuntimeError(f"{self.device} hang detected")
|
||||
@@ -617,17 +627,17 @@ class HCQ2Buffer:
|
||||
return HCQ2Buffer(self.va_addr+offset, meta=self.meta, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
|
||||
|
||||
class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
|
||||
def _as_buffer(self, buf:HCQ2Buffer) -> memoryview:
|
||||
def _as_buffer(self, buf:HCQBuffer) -> memoryview:
|
||||
return unwrap(buf.view).mv
|
||||
|
||||
def _map(self, buf:HCQ2Buffer) -> HCQ2Buffer:
|
||||
def _map(self, buf:HCQBuffer) -> HCQBuffer:
|
||||
if not hasattr(self, '_do_map'): raise NotImplementedError("map failed: no method implemented")
|
||||
return self._do_map(buf)
|
||||
|
||||
def _do_unmap(self, mb): self.dev.iface.free(mb)
|
||||
|
||||
@suppress_finalizing
|
||||
def _free(self, buf:HCQ2Buffer, options:BufferSpec|None=None):
|
||||
def _free(self, buf:HCQBuffer, options:BufferSpec|None=None):
|
||||
if options is not None and options.external_ptr is not None: return
|
||||
self.dev.synchronize()
|
||||
if hasattr(self, '_do_free'): self._do_free(buf, options)
|
||||
@@ -636,4 +646,4 @@ class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
|
||||
self.dev.synchronize()
|
||||
self._do_unmap(mb)
|
||||
|
||||
def _offset(self, buf, size:int, offset:int) -> HCQ2Buffer: return buf.offset(offset=offset, size=size)
|
||||
def _offset(self, buf, size:int, offset:int) -> HCQBuffer: return buf.offset(offset=offset, size=size)
|
||||
|
||||
@@ -8,14 +8,13 @@ from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SC
|
||||
|
||||
# unwrap VIEW/CAST/etc to find the actual data source (kernel output, buffer, or multi-device op)
|
||||
def _unwrap_src(s: UOp) -> UOp:
|
||||
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK, Ops.BIND}: s = s.src[0]
|
||||
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK}: s = s.src[0]
|
||||
return s
|
||||
|
||||
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states, BIND is not a buffer dependency
|
||||
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states
|
||||
def _states(s: UOp) -> list[UOp]:
|
||||
s = _unwrap_src(s)
|
||||
if s.op in {Ops.MSELECT, Ops.MSTACK}: return [st for ss in s.src for st in _states(ss)]
|
||||
if s.op is Ops.BIND: return []
|
||||
assert s.op in {Ops.AFTER, Ops.BUFFER, Ops.PARAM}, f"input to kernel must resolve to a buffer state, not {s.op}"
|
||||
return [s]
|
||||
|
||||
@@ -71,7 +70,7 @@ def create_schedule(sched_sink:UOp) -> UOp:
|
||||
else:
|
||||
k = rk.src[0] if rk.op is Ops.END else rk
|
||||
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if s.op is not Ops.BIND)
|
||||
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
|
||||
linearized.append(k.src[0].call(*buf_uops))
|
||||
for x in children.get(rk, []):
|
||||
in_degree[x] -= 1
|
||||
@@ -100,7 +99,8 @@ pm_post_sched_cache = PatternMatcher([
|
||||
|
||||
def resolve_linear_call(linear_call:UOp):
|
||||
linear = graph_rewrite(linear_call.src[0], pm_post_sched_cache, ctx=({}, linear_call.src[1:]), walk=True, name="params to buffers")
|
||||
binds = {f"p{i}":x.src[0] for i,x in enumerate(linear_call.src[1:]) if x.op is Ops.BIND}
|
||||
# map the call body params back to the original Variables stored in the call args
|
||||
binds = {f"p{i}":x.src[0].replace(op=Ops.PARAM) for i,x in enumerate(linear_call.src[1:]) if x.is_bound_var}
|
||||
return linear.substitute({v:binds[v.expr] for v in linear.variables() if v.expr in binds}, enter_calls=True, name="resolve scalar params")
|
||||
|
||||
pm_resolve_linear_call = PatternMatcher([
|
||||
@@ -184,13 +184,13 @@ def create_linear_with_vars(big_sink:UOp) -> tuple[UOp, dict[str, int]]:
|
||||
|
||||
# vars used in the schedule
|
||||
used_vars = set().union(*[{v.expr for v in si.src[0].variables()} for si in linear.src])
|
||||
# get var_vals
|
||||
# get var_vals from the bound Variables in the call args
|
||||
var_vals: dict[str, int] = {}
|
||||
for b in big_sink.src[1:]:
|
||||
if b.op is Ops.BIND:
|
||||
nm = b.src[0].expr
|
||||
if b.is_bound_var:
|
||||
v, val = b.unbind()
|
||||
nm = v.expr
|
||||
if nm not in used_vars: continue
|
||||
val = b.src[1].val
|
||||
if var_vals.get(nm, val) != val: raise RuntimeError(f"bind mismatch on {nm}, {var_vals[nm]} != {val}")
|
||||
var_vals[nm] = val
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class IndexingContext:
|
||||
|
||||
|
||||
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.BUFFER,
|
||||
Ops.CONST, Ops.BIND, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
|
||||
Ops.CONST, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
|
||||
Ops.LOAD, Ops.CALL, Ops.FUNCTION}
|
||||
|
||||
def realize(ctx:IndexingContext, tr:UOp) -> None: ctx.realize_map[tr] = None
|
||||
@@ -35,7 +35,7 @@ def realize_srcs(ctx:IndexingContext, rb:UOp) -> None:
|
||||
|
||||
def realize_store_after_src(ctx:IndexingContext, dest:UOp, src:UOp):
|
||||
# you don't usually have to do this for assign unless there's a WAR hazard like TestAssign.test_assign_double_diamond_reduce
|
||||
if dest.base in src.backward_slice_with_self: ctx.realize_map[src] = None
|
||||
if dest.base in src.toposort(enter_calls=False): ctx.realize_map[src] = None
|
||||
|
||||
def realize_custom_kernel_srcs(ctx:IndexingContext, c:UOp) -> None:
|
||||
for s in c.src[1:]:
|
||||
@@ -69,7 +69,9 @@ def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
|
||||
|
||||
# TODO: srcs contain (real data srcs, something else, ranges) and the boundary is confusing. see range_start
|
||||
def data_srcs(op:Ops, src:tuple[UOp, ...]) -> tuple[UOp, ...]:
|
||||
if op in {Ops.PARAM, Ops.BUFFER, Ops.RANGE, Ops.SPECIAL, Ops.BIND}: return ()
|
||||
if op in {Ops.PARAM, Ops.BUFFER, Ops.RANGE, Ops.SPECIAL}: return ()
|
||||
# the store of a bound Variable only carries the input value, it has no data srcs
|
||||
if op is Ops.STORE and src[0].is_variable: return ()
|
||||
if op in GroupOp.Movement|{Ops.INDEX, Ops.STAGE, Ops.REDUCE, Ops.AFTER, Ops.END}: return src[:1]
|
||||
return src
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ pm_mops = PatternMatcher([
|
||||
# 0. do some cleanup rewrites, mostly copied from the old stuff
|
||||
|
||||
def fix_store_hazard(target:UOp, src:UOp):
|
||||
if (base:=target.base) not in src.backward_slice_with_self: return None
|
||||
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] = {}
|
||||
@@ -461,11 +461,12 @@ pm_add_buffers = pm_mops+pm_flatten_bufferize+PatternMatcher([
|
||||
class LocalAddBufferContext:
|
||||
dg:int = 0
|
||||
map:dict = field(default_factory=dict)
|
||||
vars:dict = field(default_factory=dict)
|
||||
range:int = 0
|
||||
opts:tuple|None = None
|
||||
|
||||
def debuf(ctx:LocalAddBufferContext, buf:UOp):
|
||||
# Variables (ALU buffers with a value range) are scalar symbolic values, not real buffers: they become ALU params with no slot
|
||||
if buf.is_variable: return buf.replace(op=Ops.PARAM)
|
||||
param = UOp(Ops.PARAM, src=(UOp.const(prod(buf.max_shape)),),
|
||||
arg=ParamArg(ctx.dg, buf.dtype, addrspace=buf.addrspace, device=buf.device))
|
||||
ret = param.reshape(buf.max_shape)
|
||||
@@ -475,10 +476,6 @@ def debuf(ctx:LocalAddBufferContext, buf:UOp):
|
||||
ctx.dg += 1
|
||||
return ret
|
||||
|
||||
def unbind_kernel(ctx:LocalAddBufferContext, b:UOp):
|
||||
ctx.vars[b] = None
|
||||
return b.src[0]
|
||||
|
||||
def handle_after(ctx:LocalAddBufferContext, after:UOp):
|
||||
if after.addrspace == AddrSpace.LOCAL: return None
|
||||
buf = after.buf_uop
|
||||
@@ -502,8 +499,7 @@ to_define_global = PatternMatcher([
|
||||
(UPat(Ops.STORE, name="x"), find_bufs),
|
||||
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
|
||||
(UPat(Ops.PARAM, name="v"), lambda v:
|
||||
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype, multiple_of=v.arg.multiple_of)
|
||||
if v.arg.name is not None and v.arg.vmin_vmax is not None else None),
|
||||
v.replace(arg=replace(v.arg, slot=-1)) if v.arg.name is not None and v.arg.vmin_vmax is not None and v.arg.slot != -1 else None),
|
||||
|
||||
# this renumbers the params
|
||||
(UPat(Ops.PARAM, name="buf"), lambda ctx, buf:
|
||||
@@ -512,7 +508,8 @@ to_define_global = PatternMatcher([
|
||||
# ALU params are scalar symbolic values, not buffers.
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.PARAM, name="v"),)), lambda v: v if v.addrspace == AddrSpace.ALU else None),
|
||||
|
||||
(UPat(Ops.BIND, name="b"), unbind_kernel),
|
||||
# bound Variables are stores into Variable buffers: strip the store, the buffer becomes an ALU param via debuf
|
||||
(UPat(Ops.AFTER, name="b"), lambda b: b.src[0] if b.is_bound_var else None),
|
||||
(UPat(Ops.AFTER, name="after"), handle_after),
|
||||
|
||||
# remove device from local BUFFERIZE
|
||||
@@ -541,13 +538,16 @@ pm_add_param_range_tags = PatternMatcher([
|
||||
def split_store(x:UOp) -> UOp|None:
|
||||
# if we have any open ranges here, we don't split. open DEVICE ranges are fine, they are bound per device at launch
|
||||
if any(r.arg[-1] is not AxisType.DEVICE for r in x.ranges): return None
|
||||
# the store of a bound Variable is an input value, not a kernel
|
||||
st = x.src[0] if x.op is Ops.END else x
|
||||
if st.op is Ops.STORE and st.src[0].is_variable: return None
|
||||
|
||||
# local kernel rewrite
|
||||
lctx = LocalAddBufferContext()
|
||||
ret = graph_rewrite(x, to_define_global+pm_flatten_range+rangeify_codegen, ctx=lctx, name="kernel split", bottom_up=True)
|
||||
|
||||
# create the Kernel. NOTE: buffers can be on different devices here now, they are compiled to SDMA copies later by schedule
|
||||
return ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts)).call(*lctx.map.values(), *lctx.vars.keys())
|
||||
return ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts)).call(*lctx.map.values())
|
||||
|
||||
split_kernels = PatternMatcher([
|
||||
(UPat((Ops.STORE, Ops.END), name="x"), split_store),
|
||||
|
||||
+11
-10
@@ -109,7 +109,7 @@ def _precompiled_output_redirect(s:UOp, t:UOp) -> UOp|None:
|
||||
def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
if not c.arg.precompile: return None
|
||||
assert c.src[0].op is Ops.TUPLE, f"expected TUPLE body for precompiled FUNCTION, got {c.src[0].op}"
|
||||
input_buffers = tuple(x.contiguous() if x.op not in {Ops.AFTER, Ops.BIND} else x for x in c.src[1:])
|
||||
input_buffers = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in c.src[1:])
|
||||
|
||||
# add the outputs to the call
|
||||
srcs = c.src[0].src
|
||||
@@ -176,6 +176,8 @@ pm_early_transform_tensor_graph = PatternMatcher([
|
||||
])
|
||||
|
||||
def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
# bound Variables are call inputs, not assigns: they stay in the graph and pm_replace_buf turns them into call args
|
||||
if x.is_bound_var: return None
|
||||
# untagged: record as an assign for the call body
|
||||
if x.tag is None:
|
||||
ctx.assigns.append(x)
|
||||
@@ -196,7 +198,7 @@ def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
if b.op is Ops.BIND: return b.param_like(len(ctx.replacements)-1)
|
||||
if b.is_bound_var or b.is_variable: return b.param_like(len(ctx.replacements)-1)
|
||||
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
|
||||
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
|
||||
|
||||
@@ -214,8 +216,8 @@ pm_replace_buf = PatternMatcher([
|
||||
# replace SHRINK with PARAM
|
||||
(UPat(Ops.SHRINK, src=(UPat(Ops.BUFFER),), name="b", allow_any_len=True), replace_input_view),
|
||||
(UPat(Ops.BITCAST, src=(UPat.any(UPat(Ops.SHRINK, src=(UPat(Ops.BUFFER),), allow_any_len=True), UPat(Ops.BUFFER)),), name="b"), replace_input_view),
|
||||
# strip value from BIND for cache key normalization, so different values hit same cache
|
||||
(UPat(Ops.BIND, src=(UPat(Ops.PARAM), UPat(Ops.CONST)), name="b"), replace_input_buffer),
|
||||
# strip the stored value from bound Variables for cache key normalization, so different values hit same cache
|
||||
(UPat(Ops.AFTER, name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b.is_bound_var else None),
|
||||
])
|
||||
|
||||
@rewrite_group(lambda _,ret: f"Callify {pluralize('Buffer', len(ret[1]))}")
|
||||
@@ -451,12 +453,11 @@ class Tensor(RandMixin):
|
||||
return self
|
||||
# STORE+AFTER: STORE is the write effect (void), AFTER wraps the view for correct shape/ranging
|
||||
assign = self.uop.after(self.uop.store(x.uop))
|
||||
if (base := self.uop.base).op in {Ops.BUFFER, Ops.AFTER} and self.uop is not base and not self.uop.has_buffer_identity():
|
||||
ib = self.uop
|
||||
while not ib.has_buffer_identity() and ib.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH}: ib = ib.src[0]
|
||||
if ib is not self.uop and ib.has_buffer_identity(after_ok=True):
|
||||
# view assign: replace at the buffer-identity level (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
|
||||
ib = self.uop
|
||||
while not ib.has_buffer_identity() and ib is not base: ib = ib.src[0]
|
||||
assigned_ib = ib.after(assign)
|
||||
_apply_map_to_tensors({ib: assigned_ib}, name="Embed View Assign")
|
||||
_apply_map_to_tensors({ib: ib.after(assign)}, name="Embed View Assign")
|
||||
else:
|
||||
# simple assign
|
||||
self.uop = assign
|
||||
@@ -741,7 +742,7 @@ class Tensor(RandMixin):
|
||||
the reference frames (`ref_frames`).
|
||||
"""
|
||||
ref_frames = [x.contiguous() for x in ref_frames or []]
|
||||
assert frame_pos.op is Ops.BIND, "frame_pos must be a bound Variable"
|
||||
assert frame_pos.is_bound_var, "frame_pos must be a bound Variable"
|
||||
srcs = (out:=Tensor.empty(*shape, device=self.device, dtype=self.dtype), self.contiguous(), state.contiguous(), *ref_frames)
|
||||
fn = UOp(Ops.CUSTOM_FUNCTION, src=(frame_pos.src[0], *[UOp.const(s, dtypes.int) for s in shape]), arg="encdec")
|
||||
return Tensor(out.uop.after(fn.call(*[s.uop for s in srcs], frame_pos)))
|
||||
|
||||
@@ -13,9 +13,6 @@ class FastEnum(IntEnum):
|
||||
class Ops(FastEnum):
|
||||
# ** 1 -- defines/special **
|
||||
|
||||
# BIND pairs a symbolic PARAM with a concrete value
|
||||
BIND = auto()
|
||||
|
||||
# this is a RANGE for GPU dimensions, similar to symbolic shapes but not exactly
|
||||
SPECIAL = auto()
|
||||
|
||||
|
||||
+46
-33
@@ -150,9 +150,6 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
case Ops.STACK:
|
||||
if len(src) == 0: return dtypes.void
|
||||
return promo_dtype(src)
|
||||
case Ops.BIND:
|
||||
assert src[0].dtype == src[1].dtype, f"bind dtype mismatch {src[0].dtype} != {src[1].dtype}"
|
||||
return src[0].dtype
|
||||
case Ops.WMMA:
|
||||
# WMMA output dtype is the accumulator dtype (src[2])
|
||||
return src[2].dtype
|
||||
@@ -377,7 +374,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
# some ops init the shape
|
||||
case Ops.GETADDR: return ()
|
||||
case Ops.BIND | Ops.RANGE | Ops.SPECIAL: return ()
|
||||
case Ops.RANGE | Ops.SPECIAL: return ()
|
||||
case Ops.BINARY: return (len(self.arg),)
|
||||
case Ops.BUFFER:
|
||||
if len(self.src): return self.src[0].as_shape
|
||||
@@ -520,10 +517,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.CONST: return self
|
||||
if self.op is Ops.SINK and all(s.op is Ops.CONST or (s.op is Ops.STACK and len(s.src) == 0) for s in self.src): return self
|
||||
# late import!
|
||||
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
with Context(TRACK_MATCH_STATS=0 if not tracked else TRACK_MATCH_STATS.value):
|
||||
return graph_rewrite(self, symbolic+pm_fold_cast_const, name="simplify")
|
||||
def ssimplify(self) -> UOp|ConstType: return ret.val if (ret:=self.simplify()).op is Ops.CONST else ret
|
||||
return graph_rewrite(self, symbolic, name="simplify")
|
||||
def ssimplify(self) -> UOp|ConstType:
|
||||
if (ret := self.simplify()).op is Ops.CAST and ret.src[0].op is Ops.CONST: return ret.dtype.const(ret.src[0].val)
|
||||
return ret.val if ret.op is Ops.CONST else ret
|
||||
def _eval(self, dtype, expected_type:Type[T]) -> T:
|
||||
assert self.dtype in dtype, f"eval with wrong dtype {self}"
|
||||
vmin, vmax = (simple_self:=self.simplify())._min_max
|
||||
@@ -799,11 +798,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
case Ops.PAD | Ops.SHRINK: src_args = list(zip(*arg))
|
||||
case Ops.PERMUTE | Ops.FLIP: src_args = []
|
||||
case Ops.STACK:
|
||||
# arg is the other srcs; all are cast to the promoted dtype, spec requires STACK srcs to match its dtype
|
||||
srcs = (self,)+tuple(arg)
|
||||
dtype = cast(DType, dtype_from_uop(Ops.STACK, srcs, None))
|
||||
# TODO: why cast here?
|
||||
return UOp(Ops.STACK, dtype, tuple(u if u.base.is_invalid else u.cast(dtype) for u in srcs))
|
||||
return UOp(Ops.STACK, dtype, tuple(u if u.base.is_invalid else UOp.const(u.val, dtype) if u.op is Ops.CONST else u.cast(dtype) for u in srcs))
|
||||
case _: raise RuntimeError(f"{op} is not a MovementOp")
|
||||
usrcs = [shape_to_shape_arg(arg) for arg in src_args]
|
||||
if len(usrcs) == 0: return UOp(op, src=(self,), arg=arg)
|
||||
@@ -960,8 +957,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.UNSHARD: return self.src[0].realized
|
||||
# only these can be realized
|
||||
if self.op not in (Ops.BUFFER, Ops.MSTACK): return None
|
||||
# LOCAL/REG scratch buffers are never realized
|
||||
if self.op is Ops.BUFFER and self.addrspace in (AddrSpace.LOCAL, AddrSpace.REG): return None
|
||||
# LOCAL/REG scratch buffers are never realized, and Variables (ALU) have no real storage
|
||||
if self.op is Ops.BUFFER and self.addrspace in (AddrSpace.LOCAL, AddrSpace.REG, AddrSpace.ALU): return None
|
||||
# an unbacked intermediate BUFFER (directly or as an MSTACK source) is not realized
|
||||
if any(b.op is Ops.BUFFER and buffers.get(b) is None for b in self.backward_slice_with_self): return None
|
||||
# NOTE: this is used by the JIT to determine which inputs we capture
|
||||
@@ -972,29 +969,41 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# *** uop Variable stuff ***
|
||||
|
||||
@staticmethod
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.weakint, multiple_of:int=1) -> UOp:
|
||||
return UOp(Ops.PARAM, src=(shape_to_shape_arg(()),),
|
||||
arg=ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU))
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.weakint, multiple_of:int=1, param:bool=False) -> UOp:
|
||||
# a Variable is a 0-d BUFFER in the ALU addrspace; binding it is storing a CONST into it
|
||||
# param=True creates the kernel-side form directly: an ALU PARAM (what the BUFFER becomes inside kernels)
|
||||
arg = ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU)
|
||||
return UOp(Ops.PARAM if param else Ops.BUFFER, src=(shape_to_shape_arg(()),), arg=arg)
|
||||
@property
|
||||
def is_variable(self) -> bool:
|
||||
# a Variable is a 0-d BUFFER in the ALU addrspace that carries a value range (it becomes a PARAM inside kernels)
|
||||
return self.op is Ops.BUFFER and isinstance(self.arg, ParamArg) and \
|
||||
self.arg.vmin_vmax is not None and self.arg.addrspace is AddrSpace.ALU and self._shape == ()
|
||||
@property
|
||||
def is_bound_var(self) -> bool:
|
||||
# a bound Variable is bind()'s AFTER(var, STORE(var, CONST))
|
||||
return self.op is Ops.AFTER and self.src[0].is_variable and self.src[1].op is Ops.STORE and \
|
||||
self.src[1].src[0] is self.src[0] and self.src[1].src[1].op is Ops.CONST and len(self.src) == 2
|
||||
@property
|
||||
def expr(self) -> str:
|
||||
assert self.op is Ops.PARAM
|
||||
assert self.op in {Ops.PARAM, Ops.BUFFER}
|
||||
return unwrap(self.arg.name)
|
||||
def bind(self, val:int|UOp):
|
||||
assert self.op is Ops.PARAM and self.addrspace is AddrSpace.ALU, f"op is {self.op}, need PARAM"
|
||||
assert self.is_variable, f"op is {self.op}, need Variable"
|
||||
uval = self.const_like(val) if isinstance(val, int) else val
|
||||
assert self.vmin <= uval.vmin and uval.vmax <= self.vmax, f"bind {val} not in range [{self.vmin}, {self.vmax}]"
|
||||
assert uval.divides(self.arg.multiple_of) is not None, f"bind {val} not divisible by {self.arg.multiple_of}"
|
||||
return UOp(Ops.BIND, src=(self, uval))
|
||||
return self.after(self.store(uval))
|
||||
def unbind(self) -> tuple[Variable, int]:
|
||||
assert self.op is Ops.BIND and self.src[0].op is Ops.PARAM and self.src[1].op is Ops.CONST, f"can't unbind {self}"
|
||||
return self.src[0], self.src[1].val
|
||||
assert self.is_bound_var, f"can't unbind {self}"
|
||||
return self.src[0], self.src[1].src[1].val
|
||||
def unbind_all(self) -> tuple[UOp, dict[Variable, int]]:
|
||||
ret:dict[Variable, int] = {}
|
||||
return graph_rewrite(self, pm_unbind, ctx=ret), ret
|
||||
def variables(self) -> list[Variable]:
|
||||
return sorted({x if x.op is Ops.PARAM else UOp.variable("_device_num", 0, x.vmax, dtype=x.dtype)
|
||||
for x in self.backward_slice_with_self if (x.op is Ops.RANGE and x.arg[-1] is AxisType.DEVICE) or x.op is Ops.PARAM
|
||||
and x.arg.addrspace is AddrSpace.ALU}, key=lambda v: v.expr)
|
||||
return sorted({x if x.op in {Ops.PARAM, Ops.BUFFER} else UOp.variable("_device_num", 0, x.vmax, dtype=x.dtype, param=True)
|
||||
for x in self.backward_slice_with_self if (x.op is Ops.RANGE and x.arg[-1] is AxisType.DEVICE) or
|
||||
(x.op is Ops.PARAM and x.arg.addrspace is AddrSpace.ALU) or x.is_variable}, key=lambda v: v.expr)
|
||||
|
||||
# *** uop symbolic stuff ***
|
||||
|
||||
@@ -1005,7 +1014,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.STACK: return math.gcd(*[x.const_factor() for x in self.src])
|
||||
if self.op is Ops.ADD: return math.gcd(self.src[0].const_factor(), self.src[1].const_factor())
|
||||
if self.op is Ops.MUL: return self.src[0].val if self.src[0].op is Ops.CONST else self.src[1].val if self.src[1].op is Ops.CONST else 1
|
||||
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self.arg.multiple_of
|
||||
if self.op in (Ops.PARAM, Ops.BUFFER) and isinstance(self.arg, ParamArg) and self.arg.multiple_of is not None: return self.arg.multiple_of
|
||||
return 1
|
||||
def divides(self, v:int) -> UOp|None:
|
||||
if v==1: return self
|
||||
@@ -1017,7 +1026,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.MUL:
|
||||
if (d0:=self.src[0].divides(v)) is not None: return d0 * self.src[1]
|
||||
if (d1:=self.src[1].divides(v)) is not None: return self.src[0] * d1
|
||||
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self // v if self.arg.multiple_of%v == 0 else None
|
||||
if self.op in (Ops.PARAM, Ops.BUFFER) and isinstance(self.arg, ParamArg) and self.arg.multiple_of is not None:
|
||||
return self // v if self.arg.multiple_of%v == 0 else None
|
||||
return None # generic None if we aren't sure
|
||||
def pop_const(self, op=Ops.ADD) -> tuple[UOp, PyConst]: # NOTE: assume Invalid ALU is resolved
|
||||
return (self.src[0], self.src[1].val) if self.op is op and self.src[1].op is Ops.CONST else (self, identity_element(op, self.dtype))
|
||||
@@ -1082,9 +1092,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# float has NAN issue and we use explicit NAN in transcendental
|
||||
if self.op is Ops.WHERE and dtypes.is_int(self.dtype): return min(self.src[1].vmin, self.src[2].vmin), max(self.src[1].vmax, self.src[2].vmax)
|
||||
# NOTE: returned UOp is assumed to be CONST
|
||||
if self.op is Ops.PARAM and self.arg.vmin_vmax is not None: return self.arg.vmin_vmax
|
||||
if self.op in (Ops.PARAM, Ops.BUFFER) and isinstance(self.arg, ParamArg) and self.arg.vmin_vmax is not None: return self.arg.vmin_vmax
|
||||
if self.op in (Ops.RANGE, Ops.SPECIAL) and self.dtype is not dtypes.void: return 0, (self.src[0]-1).vmax
|
||||
if self.op is Ops.BIND: return self.src[0]._min_max # ignore the bound value
|
||||
if self.op is Ops.AFTER: return self.src[0]._min_max
|
||||
if self.op is Ops.STACK: return min(x.vmin for x in self.src), max(x.vmax for x in self.src)
|
||||
if self.op is Ops.CONST and self.val is not Invalid: return self.val, self.val
|
||||
if self.op is Ops.INDEX: return self.src[0]._min_max
|
||||
@@ -1101,7 +1111,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
def _sym_fxn(self):
|
||||
from tinygrad.uop.render import _render_with_splits, renderer_infer
|
||||
sself = self.simplify()
|
||||
varnames = tuple(dedup(x.expr for x in sself.toposort() if x.op is Ops.PARAM and x.arg.addrspace == AddrSpace.ALU))
|
||||
varnames = tuple(dedup(x.expr for x in sself.toposort() if (x.op is Ops.PARAM and x.arg.addrspace == AddrSpace.ALU) or x.is_variable))
|
||||
# TODO: sanitize varnames, or don't use naked eval while staying fast
|
||||
ret = _render_with_splits(list(sself.toposort()), renderer_infer, {sself})
|
||||
lines = [f" {k}={v}" for k,v in ret.items() if k != "ast"] + [f" return {ret['ast']}"]
|
||||
@@ -1156,7 +1166,10 @@ 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):
|
||||
if self.op is Ops.BIND: return self.src[0].replace(arg=replace(self.src[0].arg, slot=slot, name=f"p{slot}"))
|
||||
# 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)
|
||||
|
||||
@@ -1410,9 +1423,9 @@ class UPat(OpMixin):
|
||||
return res
|
||||
|
||||
def deconstruct_function(fxn:Callable) -> tuple:
|
||||
new_globals = {k:v for k,v in fxn.__globals__.items() if k in fxn.__code__.co_names}
|
||||
for co in fxn.__code__.co_consts:
|
||||
if isinstance(co, types.CodeType): new_globals.update({k:v for k,v in fxn.__globals__.items() if k in co.co_names})
|
||||
# globals can be referenced from arbitrarily nested code objects (comprehensions/lambdas, pre PEP 709)
|
||||
def names(co:types.CodeType) -> set: return set(co.co_names).union(*(names(c) for c in co.co_consts if isinstance(c, types.CodeType)))
|
||||
new_globals = {k:v for k,v in fxn.__globals__.items() if k in names(fxn.__code__)}
|
||||
# NOTE: optional round trip through pickle!
|
||||
assert fxn.__closure__ is None, "closures are not supported in pattern matchers"
|
||||
ret = fxn.__code__, new_globals, fxn.__name__, fxn.__defaults__
|
||||
@@ -1754,7 +1767,7 @@ def do_unbind(ctx:dict[Variable, int], x:UOp):
|
||||
v,i = x.unbind()
|
||||
ctx[v] = i
|
||||
return v
|
||||
pm_unbind = PatternMatcher([(UPat(Ops.BIND, name="x"), do_unbind)])
|
||||
pm_unbind = PatternMatcher([(UPat(Ops.AFTER, name="x"), lambda ctx,x: do_unbind(ctx,x) if x.is_bound_var else None)])
|
||||
|
||||
# ctx is source UOp for which we are finding a contiguous view for. used in contiguous_view_offset
|
||||
pm_contiguous_view_offset = PatternMatcher([
|
||||
|
||||
@@ -33,12 +33,13 @@ def strip_binary_parens(x:UOp, left:str, right:str, code_for_op) -> str:
|
||||
|
||||
renderer = PatternMatcher([
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: x.arg.name if x.arg.name is not None else f"p{x.arg.slot}"),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda x: x.arg.name if isinstance(x.arg, ParamArg) and x.arg.name is not None else f"b{x.arg.slot}"),
|
||||
(UPat(Ops.AFTER, name="x"), lambda ctx,x: ctx[x.src[0]]),
|
||||
(UPat((Ops.SPECIAL), name="x"), lambda x: x.arg),
|
||||
(UPat(Ops.RANGE, dtypes.void, name="x"), lambda x: f"loop{x.arg[0]}"),
|
||||
(UPat(Ops.RANGE, name="x"), lambda x: f"r{range_str(x)}"),
|
||||
(UPat(Ops.CONST, name="x"), lambda x: str(x.val)),
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({str(x.dtype)[7:]})({ctx[x.src[0]]})"),
|
||||
(UPat(Ops.BIND, name="x"), lambda ctx,x: ctx[x.src[0]]),
|
||||
(UPat(Ops.NEG, name="x"), lambda ctx,x: f"(-{ctx[x.src[0]]})"),
|
||||
(UPat(Ops.RECIPROCAL, name="x"), lambda ctx,x: f"(1/{ctx[x.src[0]]})"),
|
||||
(UPat(Ops.MAX, name="x"), lambda ctx,x: f"max({ctx[x.src[0]]}, {ctx[x.src[1]]})"),
|
||||
|
||||
+7
-12
@@ -141,9 +141,8 @@ spec_tensor = PatternMatcher([
|
||||
(isinstance(buf.dtype, DType) and matches_dtype(buf.src[0], dtypes.weakint) and is_device(buf.arg.device))
|
||||
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
|
||||
|
||||
# Tensor variable bindings
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.long, dtypes.weakint,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.long,dtypes.weakint,))), arg=None),
|
||||
lambda: True),
|
||||
# a Variable is a 0-d ALU BUFFER with a value range and no device
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="buf"), lambda buf: buf.arg.device is None if buf.is_variable else None),
|
||||
|
||||
# custom function
|
||||
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
|
||||
@@ -241,9 +240,6 @@ spec_full = PatternMatcher([
|
||||
|
||||
# all loads/stores
|
||||
(UPat((Ops.LOAD, Ops.STORE)), lambda: True),
|
||||
|
||||
# while BIND is being casted
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint), (UPat(), UPat()), arg=None), lambda: True),
|
||||
])+spec_tensor+spec_program+spec_hcq
|
||||
|
||||
# ***** kernel graph spec *****
|
||||
@@ -251,17 +247,16 @@ spec_full = PatternMatcher([
|
||||
spec_kernel_graph = PatternMatcher([
|
||||
# sink
|
||||
(UPat(Ops.SINK, dtypes.void), lambda: True),
|
||||
# bind
|
||||
(UPat(Ops.BIND), lambda: True),
|
||||
# const + stack to make vconsts
|
||||
# the store of a bound Variable binds it: AFTER(BUFFER, STORE(BUFFER, CONST)) in call args
|
||||
(UPat(Ops.STORE, dtypes.void, (UPat(Ops.BUFFER, name="b"), UPat(Ops.CONST))), lambda b: b.is_variable),
|
||||
# const + stack to make vconsts and shape args
|
||||
(UPat(Ops.CONST, src=()), lambda: True),
|
||||
(UPat(Ops.STACK, src=()), lambda: True),
|
||||
(UPat(Ops.STACK, src=UPat((Ops.CONST, Ops.BIND, Ops.PARAM))), lambda: True),
|
||||
(UPat(Ops.STACK, name="s"), lambda s: all(x.op in (Ops.CONST, Ops.PARAM) or x.is_variable or x.is_bound_var for x in s.src) or None),
|
||||
# linear for more kernels (TODO: we should enter non sink calls)
|
||||
#(UPat(Ops.LINEAR), lambda: True),
|
||||
# param is outside buffer, buffer is local buffer
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda x: isinstance(x.arg, ParamArg) and x.addrspace == AddrSpace.GLOBAL),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda x: isinstance(x.arg, ParamArg) and x.addrspace in (AddrSpace.GLOBAL, AddrSpace.ALU)),
|
||||
# RESHAPE/BITCAST are NOOPs in the kernel graph (do we need them?)
|
||||
(UPat((Ops.RESHAPE, Ops.BITCAST)), lambda: True),
|
||||
# mstack/mselect
|
||||
|
||||
+12
-10
@@ -23,8 +23,12 @@ def fold_bitcast(root:UOp, c:UOp) -> UOp|None:
|
||||
if c.dtype.fmt is None or root.dtype.fmt is None or c.dtype.itemsize != root.dtype.itemsize: return None
|
||||
return root.const_like(bitcast(c.val, c.dtype, root.dtype))
|
||||
|
||||
# const folding works for CONST, STACK, and casted CONST
|
||||
const_folding_pat = UPat.any(UPat((Ops.CONST, Ops.STACK)), UPat(Ops.CAST, src=(UPat(Ops.CONST),)))
|
||||
|
||||
def const_arg(u:UOp) -> ConstType|tuple[ConstType, ...]|None:
|
||||
if u.op is Ops.CONST: return u.val
|
||||
if u.op is Ops.CAST and u.src[0].op is Ops.CONST: return u.dtype.const(u.src[0].val)
|
||||
if u.op is Ops.STACK and all(s.op is Ops.CONST for s in u.src): return tuple(s.val for s in u.src)
|
||||
return None
|
||||
|
||||
@@ -136,10 +140,10 @@ symbolic_simple = pm_data_invalid + PatternMatcher([
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) != UPat.var("x"),
|
||||
lambda x: x.const_like(False, dtypes.bool)), # x != x -> False (only ints)
|
||||
# ** constant folding **
|
||||
(UPat(GroupOp.Unary, src=(UPat((Ops.CONST, Ops.STACK)),), name="a"), fold_const_alu),
|
||||
(UPat(GroupOp.Unary, src=(const_folding_pat,), name="a"), fold_const_alu),
|
||||
# NOTE: THREEFRY(const,const) folds via its decomposition
|
||||
(UPat(GroupOp.Binary-{Ops.THREEFRY}, src=(UPat((Ops.CONST, Ops.STACK)),)*2, name="a"), fold_const_alu),
|
||||
(UPat(GroupOp.Ternary, src=(UPat((Ops.CONST, Ops.STACK)),)*3, name="a"), fold_const_alu),
|
||||
(UPat(GroupOp.Binary-{Ops.THREEFRY}, src=(const_folding_pat,)*2, name="a"), fold_const_alu),
|
||||
(UPat(GroupOp.Ternary, src=(const_folding_pat,)*3, name="a"), fold_const_alu),
|
||||
# bool MUL is AND, ADD/MAX is OR. prevents other rules to rewrite bool ADD/MUL incorrectly
|
||||
(UPat.var('x', dtype=dtypes.bool) * UPat.var('y', dtype=dtypes.bool), lambda x,y: x&y),
|
||||
(UPat.var('x', dtype=dtypes.bool) + UPat.var('y', dtype=dtypes.bool), lambda x,y: x|y),
|
||||
@@ -245,7 +249,7 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
|
||||
# complementary zero branches under the same condition select directly
|
||||
(UPat.var("c").where(UPat.var("t"), 0) + UPat.var("c").where(0, UPat.var("f")), lambda c,t,f: c.where(t, f)),
|
||||
# ALU/variable min==max -> CONST
|
||||
(UPat({Ops.CMPLT, Ops.CMPNE, Ops.FLOORDIV, Ops.FLOORMOD, Ops.PARAM, Ops.BIND, Ops.SPECIAL}, name="x"),
|
||||
(UPat({Ops.CMPLT, Ops.CMPNE, Ops.FLOORDIV, Ops.FLOORMOD, Ops.PARAM, Ops.AFTER, Ops.SPECIAL}, name="x"),
|
||||
lambda x: x.const_like(x.vmin) if x.vmin == x.vmax else None),
|
||||
(UPat(Ops.RANGE, src=(UPat(Ops.CONST,)), name="x"), lambda x: x.const_like(x.vmin) if x.vmin == x.vmax else None),
|
||||
# max folding
|
||||
@@ -330,14 +334,14 @@ def uop_given_valid(valid:UOp, uop:UOp, try_simplex=True) -> UOp:
|
||||
for i,(expr,v) in enumerate(bounds.items()):
|
||||
v0, v1 = (expr.vmin if v[0] is None else v[0], expr.vmax if v[1] is None else v[1])
|
||||
# try checking the whole clause
|
||||
all_candidates.append((expr, UOp.variable(f"fake{i}", v0, v1, expr.dtype)))
|
||||
all_candidates.append((expr, UOp.variable(f"fake{i}", v0, v1, expr.dtype, param=True)))
|
||||
|
||||
if try_simplex:
|
||||
# every candidate is a set of constrained UOp based on valid, and if every item in a set simplifies the uop into a same output, we rewrite uop
|
||||
candidates = [[all_candidates[-1]]]
|
||||
if expr.op is Ops.ADD and v0 == 1 and all(u.op in GroupOp.Irreducible for u in expr.split_uop(Ops.ADD)):
|
||||
# if the constraint is a simplex: X0 + X1 + ... > 0, we can check if all Xi > 0 simplify into the same output
|
||||
candidates.append([(Xi, UOp.variable(f"fake{i}", 1, Xi.vmax, Xi.dtype)) for Xi in expr.split_uop(Ops.ADD)])
|
||||
candidates.append([(Xi, UOp.variable(f"fake{i}", 1, Xi.vmax, Xi.dtype, param=True)) for Xi in expr.split_uop(Ops.ADD)])
|
||||
|
||||
for candidate in candidates:
|
||||
# if every branch in candidate gives the same simplified uop, we can rewrite the uop
|
||||
@@ -407,7 +411,8 @@ pm_move_where_on_load = PatternMatcher([
|
||||
])
|
||||
|
||||
def gated_given_valid(cond:UOp, x:UOp, i:UOp) -> UOp|None:
|
||||
if x.dtype is not dtypes.weakint: return None
|
||||
# pure index math only: a LOAD in x executes even where cond is false, so its INDEX valid must survive the assumption
|
||||
if x.dtype is not dtypes.weakint or x.op_in_backward_slice_with_self(Ops.INDEX): return None
|
||||
# Skip if x contains DIV/MOD AND IMAGE mode is enabled -> image index e.g. openpilot
|
||||
if IMAGE.value > 0 and x.op_in_backward_slice_with_self(Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD): return None
|
||||
return cond.where(uop_given_valid(cond, x, try_simplex=False), i)
|
||||
@@ -432,9 +437,6 @@ sym = symbolic+pm_simplify_valid+PatternMatcher([
|
||||
# reorder ALU/VECTORIZE
|
||||
(UPat(GroupOp.ALU, src=(UPat(Ops.STACK, src=UPat(name='x')), UPat(Ops.STACK, src=UPat(name='y'))), name='alu'),
|
||||
lambda x,y,alu: UOp(Ops.STACK, src=(UOp(alu.op, src=(x,y)),))),
|
||||
# ** where **
|
||||
# push cast to branches
|
||||
(UPat.var("s").where(UPat.var("a"), UPat.var("b")).cast().named("cast"), lambda s,a,b,cast: s.where(a.cast(cast.dtype), b.cast(cast.dtype))),
|
||||
# ** pow **
|
||||
((UPat(Ops.POW, name="p"), lambda p: xpow(*p.src))),
|
||||
# ** load/store folding **
|
||||
|
||||
@@ -37,6 +37,7 @@ z3_renderer = PatternMatcher([
|
||||
# variables
|
||||
(UPat(Ops.SPECIAL, name="x"), lambda x,ctx: create_bounded(x.arg, 0, ctx[1][x.src[0]]-1, ctx[0])),
|
||||
(UPat(Ops.PARAM, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0])),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0]) if x.is_variable else None),
|
||||
(UPat(Ops.RANGE, name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
|
||||
# loads are variables bounded by the min/max of the dtype. non-pointer INDEX is also a LOAD
|
||||
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx:
|
||||
@@ -60,7 +61,7 @@ z3_renderer = PatternMatcher([
|
||||
|
||||
def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
|
||||
# gate on upstream memory addressing, but keep INDEX as an unknown LOAD
|
||||
lst = list(UOp.sink(*uops).toposort(gate=lambda x: x.op not in {Ops.AFTER, Ops.BUFFER, Ops.SHRINK} and \
|
||||
lst = list(UOp.sink(*uops).toposort(gate=lambda x: x.op not in {Ops.AFTER, Ops.SHRINK} and (x.op is not Ops.BUFFER or x.is_variable) and \
|
||||
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.weakint) or x.op is Ops.SINK)))[:-1]
|
||||
z3map: dict[UOp, z3.ExprRef] = {}
|
||||
for u in lst:
|
||||
|
||||
@@ -12,7 +12,7 @@ def lower_weak_node(u:UOp) -> UOp|None:
|
||||
if src == u.src or any(s.dtype in dtypes.weaks for s in src[start:]): return None
|
||||
dt = strong_dtype(least_upper_dtype(select_dtype(u), *(s.dtype for s in src)) if u.op in GroupOp.Binary
|
||||
else unwrap(dtype_from_uop(u.op, src, u.arg)))
|
||||
return u.replace(dtype=None, src=src[:start]+tuple(s if s.base.is_invalid else s.cast(dt) for s in src[start:])).cast(u.dtype)
|
||||
return u.replace(dtype=None, src=src[:start]+tuple(s if s.base.is_invalid else commit_weak(s, dt) for s in src[start:])).cast(u.dtype)
|
||||
|
||||
pm_lower_weak = PatternMatcher([
|
||||
(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"), lambda u: UOp.const(u.val, select_dtype(u)).cast(u.dtype)),
|
||||
@@ -23,7 +23,7 @@ pm_lower_weak = PatternMatcher([
|
||||
# Binary can widen from the bounds, all other nodes derive from the lowered sources.
|
||||
# a weakfloat Unary (sin/exp2/...) must resolve here, before the transcendental decomposition
|
||||
(UPat(GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}, name="u"), lower_weak_node),
|
||||
(UPat(Ops.PARAM, dtype=dtypes.weakint, name="u"),
|
||||
(UPat((Ops.PARAM, Ops.BUFFER), dtype=dtypes.weakint, name="u"),
|
||||
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=select_dtype(u))).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
|
||||
])
|
||||
|
||||
@@ -40,7 +40,7 @@ def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
|
||||
return None if ret is u else ret
|
||||
|
||||
def commit_weak(s:UOp, dt:DType) -> UOp:
|
||||
# a bare weak CONST commits directly (the value stays mathematical, emission truncates), a weak non-const src takes the demand cast
|
||||
# a CONST commits directly at dt (the value stays mathematical, emission truncates), a non-const src takes the cast
|
||||
return UOp.const(s.val, dt) if s.op is Ops.CONST else s.cast(dt)
|
||||
|
||||
def commit_weak_srcs(u:UOp) -> UOp|None:
|
||||
@@ -65,6 +65,7 @@ def cast_weak_srcs(c:UOp, u:UOp) -> UOp|None:
|
||||
|
||||
pm_cast_weak = PatternMatcher([
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(GroupOp.ALU, dtype=dtypes.weaks, name="u"),)), cast_weak_srcs),
|
||||
(UPat(Ops.CAST, name="c", src=(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"),)), lambda c,u: commit_weak(u, c.dtype)),
|
||||
])
|
||||
|
||||
pm_lower_index_dtype = pm_commit_weak+pm_cast_weak+PatternMatcher([
|
||||
|
||||
Reference in New Issue
Block a user