Compare commits

...
Author SHA1 Message Date
geohot 1e10302055 match kernel 2026-07-28 22:59:28 -07:00
geohot 599245fc64 fix smu reset for kernel >7 2026-07-28 22:56:14 -07:00
qazalandGitHub e684fcc689 pm_reduce_collapse fix for re enabling stack for cat of same shape (codex) (#17251)
* change

* stack cat

* remove nop

* change
2026-07-29 14:47:28 +09:00
George HotzandGitHub 451120c6e1 make .barrier implicit (kimi) (#17275)
* make .barrier implicit (kimi)

* simplier

* lil

* remove tinygrad stock barriers

* readable

* lil
2026-07-28 22:34:57 -07:00
chenyuandGitHub 2f8f2d2d37 remove no-op explicit dtype= or cast [PR] (#17276) 2026-07-29 00:47:35 -04:00
George HotzandGitHub dd16d5aead apply shrink bugfix for 3.11 (#17271) 2026-07-28 19:03:06 -07:00
chenyuandGitHub 291ee435bb qwen3.6 for 27b and 35b-a3b (#17268) 2026-07-28 21:11:49 -04:00
George HotzandGitHub a17387d6a0 add UNSHARD to spec (#17269) 2026-07-28 17:00:21 -07:00
George HotzandGitHub 57ae1bc7a7 rename MULTI to UNSHARD (#17267)
* rename MULTI to UNSHARD

* comment updates (glm)

* rename method to unshard
2026-07-28 16:51:41 -07:00
George HotzandGitHub 1757067fa6 add device range as src[1] to multi (kimi) (#17264)
* add device range as src[1] to multi (kimi)

* cleanup

* fix tests
2026-07-28 16:39:34 -07:00
chenyuandGitHub f11f884f49 update dtypes.md (#17266) 2026-07-28 19:30:13 -04:00
chenyuandGitHub 23e9e76e8c DEFAULT_FLOAT/DEFAULT_INT ContextVar [pr] (#17265) 2026-07-28 19:08:18 -04:00
755dfb243b rename CPU_COUNT to NUM_CPU_THREADS with cgroup awareness (#17263)
Rename CPU_COUNT to NUM_CPU_THREADS so it can be overridden via env var.
Default uses _get_cpu_count() which respects cgroup limits:
  - os.process_cpu_count() on Python 3.13+
  - /sys/fs/cgroup/cpu.max on cgroup v2
  - /sys/fs/cgroup/cpu/cpu.cfs_quota_us on cgroup v1
  - os.sched_getaffinity(0) fallback

Use NUM_CPU_THREADS.value in the dataloader instead of cpu_count(),
and update export_model.py and all renderer references.

Co-authored-by: teeny-runner <runner@teeny>
2026-07-28 15:33:57 -07:00
George HotzandGitHub 0cdddf3495 remove unneeded default args in renderers (#17262) 2026-07-28 14:53:04 -07:00
chenyuandGitHub fde3a8faff CAPTURE_PROCESS_REPLAY=0 default chaging test [PR] (#17261)
* CAPTURE_PROCESS_REPLAY=0 default chaging test [PR]

quick patch before making default dtype context vars, which is quite annoying

* few more
2026-07-28 17:32:35 -04:00
George HotzandGitHub 9ce65b71c0 delete dsp_pm_late (#17260) 2026-07-28 14:05:41 -07:00
George HotzandGitHub 7d48926293 switch _device_num to AxisType.DEVICE range (kimi) (#17252)
* switch _device_num to AxisType.DEVICE range (kimi)

* improve

* don't exclude device for ranges

* current multi ends the range
2026-07-28 13:52:47 -07:00
drkandGitHub d1c3ae0f91 preserve typed ranges through reshape (#17259) 2026-07-28 16:51:06 -04:00
chenyuandGitHub 749e002286 remove some dtype= when construct UOp [pr] (#17257)
prep for removing the dtype field
2026-07-28 16:05:35 -04:00
chenyuandGitHub c9e11544df delete explicit casts [pr] (#17255)
handled by broadcast
2026-07-28 13:57:13 -04:00
George HotzandGitHub a9ad08064e make the github actions runners generic for gitea (#17254) 2026-07-28 10:48:49 -07:00
chenyuandGitHub 37cf15942b UOp.const(dtype=None) infers from from_py [pr] (#17253)
eventually we drop the dtype arg once everything const dtype is inferred
2026-07-28 12:06:55 -04:00
qazalandGitHub 05e4727feb arange stack regression test (#17250)
* simple failing test

* commend out stack

* a little smaller

* work
2026-07-28 20:00:22 +09:00
qazalandGitHub e3b3eea1e2 test for extra copy in allreduce_cast (#16740)
* test for extra copy in allreduce_cast

* simpler

* cleanup
2026-07-28 19:02:54 +09:00
chenyuandGitHub 1380d6cc5a pm_long_decomp doesn't depend on operand dtype [pr] (#17249)
* pm_long_decomp doesn't depend on operand dtype [pr]

the const the rules returned won't have strong dtype

* fix
2026-07-28 01:15:04 -04:00
chenyuandGitHub 4b7022e8f4 Revert "64-bit UOp.variable support (#17246)" (#17248)
This reverts commit ab8fb191b2.
2026-07-28 00:00:26 -04:00
sirhcmandGitHub ab8fb191b2 64-bit UOp.variable support (#17246) 2026-07-27 23:56:09 -04:00
chenyuandGitHub f837ca3587 don't match const dtype in UPat [pr] (#17247) 2026-07-27 22:50:08 -04:00
chenyuandGitHub 654d475d37 update pm_long_decomp [pr] (#17245)
* update pm_long_decomp [pr]

instead of reading dtype from UOp (which might be const that's going to be weak), just pass output dtype in tag

* cleanup tag
2026-07-27 22:28:50 -04:00
chenyuandGitHub 550225f603 clean up pm_lower_weak [pr] (#17243) 2026-07-27 19:11:39 -04:00
chenyuandGitHub 0bb36c9989 make Tensor(None) weakfloat (#17241)
match other float consts
2026-07-27 16:45:28 -04:00
chenyuandGitHub 896afad9bf don't match strong typed const in UPat [pr] (#17240) 2026-07-27 16:19:01 -04:00
nimlgenandGitHub 8b9ef157d1 run_linear in external_test_gpu_crash (#17239) 2026-07-27 22:00:11 +03:00
b1tgandGitHub bdbb1d702f fix shard axis through symbolic reshape (#17238)
* fix shard axis through symbolic reshape

* bind
2026-07-27 11:32:15 -04:00
nimlgenandGitHub 8eaeede96d hcq2: do not cache beam (#17234) 2026-07-27 17:16:24 +03:00
chenyuandGitHub 818a892ebc flip from_py to use weak dtypes [pr] (#17229) 2026-07-27 10:07:00 -04:00
qazalandGitHub a3ca1e55a5 polish the viz readme (#17236)
* polish the viz readme

* style

* no epilog=
2026-07-27 22:03:13 +09:00
wozeparrotandGitHub 056974468e gptoss: split no-wd params (#17233) 2026-07-27 02:58:43 -07:00
qazalandGitHub 19c4d736f2 validate json output of viz.cli in CI (#17232)
* validate viz.cli --json always prints valid JSON

* highest debug level

* jq empty we don't need a print

* gate that import
2026-07-27 15:44:24 +09:00
chenyuandGitHub f45fc4c566 test updates from weak flip (#17231) 2026-07-27 01:27:42 -04:00
qazalandGitHub 95e3b0066f webgpu failing test for duplicate PARAM in CALL [pr] (#17230)
* webgpu failing test for duplicate PARAM in CALL [pr]

* typo
2026-07-27 14:06:06 +09:00
chenyuandGitHub 0f98212e80 skip test_float_to_fp8e4m3_extreme_values (#17228)
fp8 overflow behavior changed in torch 2.13.0, skipped the test for now
2026-07-26 21:01:51 -04:00
chenyuandGitHub 456b5b5060 fix python_alu inf (#17227) 2026-07-26 20:04:12 -04:00
chenyuandGitHub afb25a624c simpler minimum and copysign [PR] (#17226) 2026-07-26 19:54:22 -04:00
nimlgenandGitHub 165f0626f8 speedy hcq2 (#17225)
* faster

* x
2026-07-27 01:31:56 +03:00
nimlgenandGitHub 97a2265362 hcq2: amd indirect (#17220)
* ind

* mock
2026-07-26 21:36:01 +03:00
chenyuandGitHub 4b6760539b fix _prepare_jit_inputs for weak [pr] (#17222) 2026-07-26 14:03:27 -04:00
chenyuandGitHub 94dad3d261 clean up mixin cos and exp [PR] (#17221) 2026-07-26 13:13:05 -04:00
chenyuandGitHub a8d51097dc realize weak is no-op [pr] (#17219)
None device and weak dtype are both virtual
2026-07-26 11:52:56 -04:00
79c07a334c fix ValueError in UOp.axis for shard reshape crossing boundary (#16547)
Co-authored-by: George Hotz <[email protected]>
2026-07-26 08:38:10 -07:00
C TandGitHub d70a134845 fix nvrtc_check helper used for jitlink call (#16362) 2026-07-26 08:20:39 -07:00
George HotzandGitHub 960430a5e5 Revert "nv: set lower interleave level to reduce GPU hogging (ai slop) (#15518)" (#17218)
This reverts commit ac12914506.
2026-07-26 08:15:16 -07:00
ac12914506 nv: set lower interleave level to reduce GPU hogging (ai slop) (#15518)
Fixes #10773. The NV backend makes desktop systems unusable (cursor lag,
video drops) because the channel group runs at the default HIGH interleave
level, monopolizing the GPU and starving the display compositor.

Sets tsgInterleaveLevel to LOW (0) by default so the GPU scheduler can
preempt compute work for display refresh. Dedicated compute machines can
restore full priority with NV_INTERLEAVE=2.

Also adds SET_INTERLEAVE_LEVEL to the mock GPU driver's pass-through list.

Co-authored-by: Yasko C <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-07-26 08:12:05 -07:00
chenyuandGitHub 5d1aa84901 SHR/SHL are Broadcastable [pr] (#17216) 2026-07-26 02:11:03 -04:00
chenyuandGitHub acc2374b6f minor normalize cleanup [pr] (#17215)
and add a few no-op clones in test_ops to prep for weak flip
2026-07-26 00:39:53 -04:00
chenyuandGitHub a96974e70c slight weak behavior tweak and cleanups [pr] (#17214)
* slight weak behavior tweak and cleanups [pr]

* ruff
2026-07-25 22:51:24 -04:00
chenyuandGitHub eb889053bf weak frompy prerequisite [PR] (#17213) 2026-07-25 22:35:45 -04:00
chenyuandGitHub a60b5f77ac fix torch backend out= into a view (#17210) 2026-07-25 21:04:42 -04:00
nimlgenandGitHub 3946df787d hcq2, cpu is hcq2-ish (#17197)
* m

* i

* x

* x

* Df

* x

* x
2026-07-26 02:38:15 +03:00
chenyuandGitHub 076b37e1ae failing batch norm test (#17206)
* failing batch norm test

running stats does not schedule in training now since there's no reader

* not that
2026-07-25 18:46:49 -04:00
104 changed files with 978 additions and 689 deletions
@@ -137,6 +137,7 @@ runs:
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
shell: bash
run: |
sudo mkdir -p /var/cache/apt/archives
sudo chown -R $USER:$USER /var/cache/apt/archives
echo 'Acquire::GzipIndexes "true";' | sudo tee /etc/apt/apt.conf.d/gzip
@@ -214,6 +215,7 @@ runs:
sudo apt-get -y --allow-unauthenticated --no-install-recommends install ${{ steps.apt-pkgs.outputs.pkgs }}
fi
sudo mkdir -p /var/cache/apt/archives
sudo chown -R $USER:$USER /var/cache/apt/archives/
- name: Add clang to PATH (Linux)
+3 -3
View File
@@ -117,10 +117,10 @@ jobs:
run: python3 test/external/process_replay/reset.py
- name: Run llama3.2
run: BENCHMARK_LOG=llama32_3b-f16 JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m llama3.2:3b-f16 --benchmark --warmup
- name: Run qwen3.5
# qwen3.5:35b-a3b doesn't fit on mac
- name: Run qwen3.6
# qwen3.6:35b-a3b doesn't fit on mac
if: ${{ matrix.dev != 'METAL' }}
run: BENCHMARK_LOG=qwen35_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.5:35b-a3b --benchmark --warmup
run: BENCHMARK_LOG=qwen36_35b-a3b JITBEAM=2 IGNORE_BEAM_CACHE=1 python3 -m tinygrad.llm -m qwen3.6:35b-a3b --benchmark --warmup
- name: Run olmoe
# just metal for now
if: ${{ matrix.dev == 'METAL' }}
+8 -5
View File
@@ -167,6 +167,7 @@ jobs:
uses: ./.github/actions/setup-tinygrad
with:
key: unittest-13
python-version: '3.11'
deps: testing_unit
llvm: 'true'
amd: 'true'
@@ -176,13 +177,14 @@ jobs:
run: |
DEV=NULL python3 -m unittest test.backend.test_multitensor.TestMultiTensor.test_data_parallel_resnet_train_step
DEV=NULL VIZ=1 python3 -m pytest -n=auto test/null/test_viz.py
DEBUG=7 python -m tinygrad.viz.cli --json | jq empty
# TODO: too slow
# - name: Run SDXL on NULL backend
# run: DEV=NULL DEBUG=1 python3 examples/sdxl.py --seed 0 --noshow --timing --fakeweights
- name: Run Clip tests for SD MLPerf on NULL backend
run: DEV=NULL python -m pytest -n=auto test/external/mlperf_stable_diffusion/external_test_models.py::TestOpenClip --durations=20
- name: Run AMD emulated BERT training on NULL backend
run: DEV=NULL::gfx1201 NULL_ALLOW_COPYOUT=1 CAPTURE_PROCESS_REPLAY=0 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
run: DEV=NULL::gfx1201 NULL_ALLOW_COPYOUT=1 DEFAULT_FLOAT=HALF BENCHMARK=10 BS=66 GPUS=1 BERT_LAYERS=2 MODEL=bert python3 examples/mlperf/model_train.py
# TODO: support fake weights
#- name: Run LLaMA 7B on 4 fake devices
# run: DEV=NULL python3 examples/llama.py --gen 1 --size 7B --shard 4 --prompt "Hello." --count 3 --temperature 0 --timing
@@ -199,6 +201,7 @@ jobs:
uses: ./.github/actions/setup-tinygrad
with:
key: unittest-13
python-version: '3.11'
pydeps: "pre-commit"
deps: testing_unit
llvm: 'true'
@@ -478,7 +481,7 @@ jobs:
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update
sudo apt-get install llvm-21 llvm-21-tools cloc
sudo apt-get install -y llvm-21 llvm-21-tools cloc
- name: Install rocprof-trace-decoder
run: sudo PYTHONPATH="." ./extra/sqtt/install_rocprof_decoder.py
- name: Run AMD renderer tests
@@ -628,7 +631,7 @@ jobs:
unittestmacos:
name: MacOS (unit)
runs-on: &macos macos-26
runs-on: macos-26
timeout-minutes: 20
steps:
- name: Checkout Code
@@ -677,7 +680,7 @@ jobs:
matrix:
group: [1, 2]
name: MacOS (DEV=METAL) (${{ matrix.group }})
runs-on: *macos
runs-on: macos-26
timeout-minutes: 20
env:
DEV: METAL
@@ -709,7 +712,7 @@ jobs:
- 'WEBGPU'
name: MacOS (DEV=${{ matrix.dev }})
runs-on: *macos
runs-on: macos-26
timeout-minutes: 20
steps:
- name: Checkout Code
+1 -1
View File
@@ -3,4 +3,4 @@
- Run tests with `-n12` for speed (e.g. `python -m pytest test/null/test_dtype.py -x -q -n12`)
- Run `python -m mypy tinygrad/` to typecheck
- Run `python -m ruff check .` to lint
- Read `./tinygrad/viz/README` for profiling
- Read `./tinygrad/viz/README.md` for profiling and debugging rewrite rules
+1 -1
View File
@@ -88,7 +88,7 @@ def example_3_custom_uop(a:Tensor, correct):
# store all the per lane accumulators to LOCAL
local_accs = UOp.placeholder((LCLS,), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
local_accs = local_accs.after(local_accs[lane].store(acc[0]).barrier())
local_accs = local_accs.after(local_accs[lane].store(acc[0]))
# accumulate LOCALs into a single per CU accumulator
late_reduce_loop = UOp.range(LCLS, 3, AxisType.REDUCE)
+2 -1
View File
@@ -1,7 +1,8 @@
::: tinygrad.dtype.DType
::: tinygrad.dtype.dtypes
::: tinygrad.dtype.DTypes
options:
heading: dtypes
members: true
members_order: source
show_labels: false
+1 -2
View File
@@ -9,8 +9,7 @@ from extra.lr_scheduler import OneCycleLR
GPUS = [f'{Device.DEFAULT}:{i}' for i in range(getenv("GPUS", 1))]
# override tinygrad defaults
dtypes.default_float = dtypes.half
Context(FUSE_OPTIM=1).__enter__()
Context(DEFAULT_FLOAT=dtypes.half, FUSE_OPTIM=1).__enter__()
# from https://github.com/tysam-code/hlb-CIFAR10/blob/main/main.py
batchsize = getenv("BS", 1024)
+6 -6
View File
@@ -1,11 +1,11 @@
import os, random, pickle, queue, struct, math, functools, hashlib, time
from typing import List
from pathlib import Path
from multiprocessing import Queue, Process, shared_memory, connection, Lock, cpu_count
from multiprocessing import Queue, Process, shared_memory, connection, Lock
import numpy as np
from tinygrad import dtypes, Tensor
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX
from tinygrad.helpers import getenv, prod, Context, round_up, tqdm, OSX, NUM_CPU_THREADS
from tinygrad.nn.state import TensorIO
### ResNet
@@ -131,7 +131,7 @@ def batch_load_resnet(batch_size=64, val=False, shuffle=True, seed=None, pad_fir
else: X = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name}")
Y = [None] * (batch_size*BATCH_COUNT)
for _ in range(cpu_count()):
for _ in range(NUM_CPU_THREADS.value):
p = Process(target=loader_process, args=(q_in, q_out, X, seed))
p.daemon = True
p.start()
@@ -212,7 +212,7 @@ def batch_load_train_bert(BS:int, seed:int|None=None):
rng.shuffle(fs)
train_files.append(fs.pop(0))
cycle_length = min(getenv("NUM_CPU_THREADS", min(os.cpu_count(), 8)), len(train_files))
cycle_length = min(NUM_CPU_THREADS.value, len(train_files))
assert cycle_length > 0, "cycle_length must be greater than 0"
dataset = InterleavedDataset(train_files, cycle_length)
@@ -301,7 +301,7 @@ def batch_load_unet3d(preprocessed_dataset_dir:Path, batch_size:int=6, val:bool=
X = Tensor.empty(*sz, dtype=dtypes.float32, device=f"disk:/dev/shm/{shm_name_x}")
Y = Tensor.empty(*sz, dtype=dtypes.uint8, device=f"disk:/dev/shm/{shm_name_y}")
for _ in range(cpu_count()):
for _ in range(NUM_CPU_THREADS.value):
proc = Process(target=load_unet3d_data, args=(preprocessed_dataset_dir, seed, queue_in, queue_out, X, Y))
proc.daemon = True
proc.start()
@@ -437,7 +437,7 @@ def batch_load_retinanet(dataset, val:bool, base_dir:Path, batch_size:int=32, sh
dataset_iter = iter(image_ids)
try:
for _ in range(cpu_count()):
for _ in range(NUM_CPU_THREADS.value):
proc = Process(
target=load_retinanet_data,
args=(base_dir, val, queue_in, queue_out, imgs, boxes, labels),
+7 -2
View File
@@ -1668,7 +1668,7 @@ def train_llama3():
def train_gptoss():
from examples.mlperf.models.gpt_oss import GPTOSS, GPT_OSS_20B, apply_grad, FP8_DTYPE
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
from examples.mlperf.optim import GradAccClipAdamW, clip_grads
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, clip_grads
BENCHMARK = getenv("BENCHMARK")
@@ -1734,7 +1734,12 @@ def train_gptoss():
is_offload_optim = bool(getenv("OFFLOAD_OPTIM"))
is_fake_offload = Device.DEFAULT == "NULL"
optim_device = ("CPU" if not is_fake_offload else "NULL:99") if is_offload_optim else None
optim = GradAccClipAdamW(params, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=grad_acc, device=optim_device)
params_wd = [p for p in params if p.ndim >= 3]
params_no_wd = [p for p in params if p.ndim < 3]
optim = GradAccClipAdamWGroup(
GradAccClipAdamW(params_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=opt_adamw_weight_decay, grad_acc=grad_acc, device=optim_device),
GradAccClipAdamW(params_no_wd, lr=0.0, b1=opt_adamw_beta_1, b2=opt_adamw_beta_2, eps=opt_adamw_epsilon, weight_decay=0.0, grad_acc=grad_acc, device=optim_device),
)
for p in optim.params:
grad_dtype = dtypes.bfloat16 if p.dtype == FP8_DTYPE else p.dtype
+20 -2
View File
@@ -1,6 +1,6 @@
from tinygrad.tensor import Tensor
from tinygrad.dtype import dtypes
from tinygrad.nn.optim import Optimizer
from tinygrad.nn.optim import Optimizer, OptimizerGroup
from tinygrad.helpers import FUSE_OPTIM, getenv
from tinygrad.uop.ops import UOp, Ops
@@ -43,7 +43,7 @@ class GradAccClipAdamW(Optimizer):
def _zero_shard(self, t:Tensor) -> Tensor:
if not self.zero or (t.shape[0] % len(self.device)) != 0: return t
return Tensor(t.uop._shard(0, len(self.device)).multi(0)).clone()
return Tensor(t.uop._shard(0, len(self.device)).unshard(0)).clone()
def _zero_gather(self, t:Tensor) -> Tensor:
if not isinstance(t.device, tuple) or t.uop.axis != 0: return t
@@ -121,3 +121,21 @@ class GradAccClipAdamW(Optimizer):
return ret.shard_like(t) if offloaded else ret
out = new_w.cast(t.dtype)
return out.shard_like(t) if offloaded else out
class GradAccClipAdamWGroup(OptimizerGroup):
def fstep(self, grads:list[Tensor], grad_norm:Tensor|None=None):
offset = 0
to_realize = []
for o in self.optimizers:
n = len(o.params)
to_realize += o.fschedule_step(grads[offset:offset+n])
offset += n
Tensor.realize(*to_realize, *([grad_norm] if grad_norm is not None else []))
@property
def lr(self): return self.optimizers[0].lr
@property
def device(self): return self.optimizers[0].device
@property
def master_params(self):
mp = [mp for o in self.optimizers for mp in (o.master_params or [])]
return mp if mp else None
+2 -2
View File
@@ -241,8 +241,8 @@ export default {model_name};
def export_model(model, target:str, *inputs, model_name: Optional[str] = "model", stream_weights=False):
assert Device.DEFAULT in EXPORT_SUPPORTED_DEVICE, f"only {', '.join(EXPORT_SUPPORTED_DEVICE)} are supported"
# NOTE: CPU_COUNT=1, since export does not support threading
with Context(JIT=2, CPU_COUNT=1): linear, output_bufs = jit_model(model, *inputs)
# NOTE: NUM_CPU_THREADS=1, since export does not support threading
with Context(JIT=2, NUM_CPU_THREADS=1): linear, output_bufs = jit_model(model, *inputs)
functions, statements, bufs, bufs_to_save = compile_net(linear, output_bufs)
state = get_state_dict(model)
weight_names = {(id(b), b.offset, b.size, b.dtype): name for name, x in state.items() if (b:=x.uop.base.realized) is not None}
+1 -1
View File
@@ -53,7 +53,7 @@ class FP8Linear:
x_fp8, x_scale = quantize_to_fp8(x)
GPUS = self.weight.device
if isinstance(GPUS, tuple) and len(GPUS) > 1:
y = Tensor(Tensor.empty((batch//len(GPUS), seq, self.weight.shape[0]), dtype=dtypes.float, device=GPUS).uop.multi(0), device=GPUS)
y = Tensor(Tensor.empty((batch//len(GPUS), seq, self.weight.shape[0]), dtype=dtypes.float, device=GPUS).uop.unshard(0), device=GPUS)
else:
y = Tensor.empty((batch, seq, self.weight.shape[0]), dtype=dtypes.float)
y = Tensor.custom_kernel(y, x_fp8, w_fp8, fxn=custom_matmul, grad_fxn=custom_matmul_backward)[0]
+4 -4
View File
@@ -58,8 +58,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
B_copy = B_local.permute((1,0)) if use_wmma else B_local
A_store = A_copy.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(a[k_tile].reshape(-1, THREADS_PER_BLOCK)[:, tid])
B_store = B_copy.reshape(-1, THREADS_PER_BLOCK)[:, tid].store(b[k_tile].reshape(-1, THREADS_PER_BLOCK)[:, tid])
barrier = UOp.barrier(A_store, B_store)
A_local, B_local = A_local.after(barrier), B_local.after(barrier)
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
A_local, B_local = A_local.after(A_store, B_store), B_local.after(A_store, B_store)
# -- COMPUTE --
lane_m, lane_n = lane // LANES_PER_WAVE_N, lane % LANES_PER_WAVE_N
@@ -96,8 +96,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
b_frag = b_frag.reshape(1, TN).expand(TM, TN)
acc_store = acc.store(acc.after(k) + (a_frag * b_frag))
# store accumulator and loop
acc = acc.after(acc_store.end(k).barrier().end(k_tile))
# store accumulator and loop (the barrier at the end of the loop is implied by the LOCAL buffers stored and loaded in the loop)
acc = acc.after(acc_store.end(k).end(k_tile))
# store accumulator to output (unified)
c = c.reshape(WAVES_M, TM//UNROLL_M, LANES_PER_WAVE_M, UNROLL_M,
+7 -7
View File
@@ -84,9 +84,9 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
q.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
K_store = KV_lds.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
k[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
qk_load_barrier = UOp.barrier(UOp.group(Q_store, K_store))
Q_lds = Q_lds.after(qk_load_barrier)
KV_lds_k = KV_lds.after(qk_load_barrier)
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
Q_lds = Q_lds.after(UOp.group(Q_store, K_store))
KV_lds_k = KV_lds.after(UOp.group(Q_store, K_store))
# -- S = Q @ K^T via WMMA (re-init each n_tile) --
S_reg = UOp.placeholder((TM, TN), dtypes.float, slot=6, addrspace=AddrSpace.REG)
@@ -147,9 +147,9 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
# load V into KV_lds (must wait for QK WMMA to finish reading K from KV_lds)
V_store = KV_lds.after(qk_done).reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
v[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
pv_barrier = UOp.barrier(UOp.group(P_store, V_store))
P_lds = P_lds.after(pv_barrier)
KV_lds_v = KV_lds.after(pv_barrier)
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
P_lds = P_lds.after(UOp.group(P_store, V_store))
KV_lds_v = KV_lds.after(UOp.group(P_store, V_store))
# -- acc += P @ V via WMMA --
k_pv = UOp.range(BLOCK_N // WMMA_K, 400, AxisType.REDUCE)
@@ -161,7 +161,7 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
pv = UOp.wmma(p_frag, v_frag, acc_frag.after(k_pv), *WMMA_ARG)
# end KV tile loop
n_tile_end = acc_frag.store(pv).end(tm2, tn2).end(k_pv).barrier().end(n_tile)
n_tile_end = acc_frag.store(pv).end(tm2, tn2).end(k_pv).end(n_tile)
acc = acc.after(n_tile_end)
l_i = l_i.after(n_tile_end)
m_i = m_i.after(n_tile_end)
+3 -4
View File
@@ -66,9 +66,8 @@ def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
B_local = UOp.placeholder((BLOCK_K, BLOCK_N), dtypes.float, slot=1, addrspace=AddrSpace.LOCAL)
B_local_store = copy(B_local.reshape(-1, THREADS_PER_BLOCK)[:, tid], b.reshape(-1, THREADS_PER_BLOCK)[:, tid], rng=200)
# TODO: can we automate barrier?
barrier = UOp.barrier(A_local_store, B_local_store)
A_local, B_local = A_local.after(barrier), B_local.after(barrier)
# NOTE: no explicit barrier needed, the AFTER on the LOCAL buffers implies it in late codegen
A_local, B_local = A_local.after(A_local_store, B_local_store), B_local.after(A_local_store, B_local_store)
# open inner k range
k = UOp.range(BLOCK_K, 3, AxisType.REDUCE)
@@ -102,7 +101,7 @@ def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
sink = c_regs[*rngs].store(c_regs.after(k)[*rngs] + A_col[iter_m, t_m] * B_row[iter_n, t_n]).end(iter_m, iter_n, t_m, t_n)
# Close k, sync, and close K tiles
sink = sink.end(k).barrier().end(k_tile_range)
sink = sink.end(k).end(k_tile_range)
# ---------------------------
# REG -> GLOBAL (epilogue)
+5 -5
View File
@@ -72,7 +72,7 @@ def hk_fp8_atb_gemm(a:Tensor, b:Tensor, x_scale:Tensor|None=None, g_amax:Tensor|
elif b.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M, N // ndev, dtype=dtypes.bfloat16, device=a.device), 2
elif a.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M // ndev, N, dtype=dtypes.bfloat16, device=a.device), 1
else: inv, out_axis, reduce_out = Tensor.invalids(1, M, N, dtype=dtypes.bfloat16, device=a.device), 0, True
out = Tensor(inv.uop.multi(out_axis), device=a.device)
out = Tensor(inv.uop.unshard(out_axis), device=a.device)
dname = a.device[0]
else:
out = Tensor.invalids(1, M, N, dtype=dtypes.bfloat16, device=a.device)
@@ -234,7 +234,7 @@ def hk_bf16_atb_gemm(a:Tensor, b:Tensor) -> Tensor:
elif b.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M, N // ndev, dtype=a.dtype, device=a.device), 2
elif a.uop.axis == 2: inv, out_axis = Tensor.invalids(1, M // ndev, N, dtype=a.dtype, device=a.device), 1
else: inv, out_axis, reduce_out = Tensor.invalids(1, M, N, dtype=a.dtype, device=a.device), 0, True
out = Tensor(inv.uop.multi(out_axis), device=a.device)
out = Tensor(inv.uop.unshard(out_axis), device=a.device)
dname = a.device[0]
else:
out = Tensor.invalids(1, M, N, dtype=a.dtype, device=a.device)
@@ -366,11 +366,11 @@ def asm_gemm(a:Tensor, b:Tensor, x_scale:Tensor|None=None, w_scale:Tensor|None=N
if is_multi:
if n_sharded:
out = Tensor(Tensor.invalids(batch, M, N//len(a.device), dtype=out_dtype, device=a.device).uop.multi(2), device=a.device)
out = Tensor(Tensor.invalids(batch, M, N//len(a.device), dtype=out_dtype, device=a.device).uop.unshard(2), device=a.device)
elif m_sharded:
out = Tensor(Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device).uop.multi(1), device=a.device)
out = Tensor(Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device).uop.unshard(1), device=a.device)
else:
out = Tensor(Tensor.invalids(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=out_dtype, device=a.device).uop.multi(0),
out = Tensor(Tensor.invalids(batch//len(a.device) if a.uop.axis==0 else batch, M, N, dtype=out_dtype, device=a.device).uop.unshard(0),
device=a.device)
else:
out = Tensor.invalids(batch, M, N, dtype=out_dtype, device=a.device)
+2 -2
View File
@@ -55,7 +55,7 @@ def grouped_mx_wgrad(g:Tensor, xg:Tensor, expert_off:Tensor, n_experts:int) -> T
dname = (g.device[0] if isinstance(g.device, tuple) else g.device).split(":")[0]
is_multi = isinstance(g.device, tuple)
inv = Tensor.invalids(1, n_experts * N, K, dtype=dtypes.bfloat16, device=g.device)
out = Tensor(inv.uop.multi(0), device=g.device) if is_multi else inv
out = Tensor(inv.uop.unshard(0), device=g.device) if is_multi else inv
out = Tensor.custom_kernel(out, gT, xT, g_si, x_si, expert_off,
fxn=functools.partial(custom_hk_grouped_mxfp8_wgrad, dname=dname, n_experts=n_experts))[0]
out = out.sum(0) if is_multi else out.squeeze(0)
@@ -103,7 +103,7 @@ def grouped_mx_gemm(x:Tensor, w:Tensor|tuple[Tensor, Tensor], expert_off:Tensor)
if isinstance(x.device, tuple) and (row_axis := x.uop.axis) is not None:
ndev = len(x.device)
out = Tensor(Tensor.invalids(*(s // ndev if i == row_axis else s for i, s in enumerate(out_shape)),
dtype=dtypes.bfloat16, device=x.device).uop.multi(row_axis), device=x.device)
dtype=dtypes.bfloat16, device=x.device).uop.unshard(row_axis), device=x.device)
else:
out = Tensor.invalids(*out_shape, dtype=dtypes.bfloat16, device=x.device)
return Tensor.custom_kernel(out, x_q, w_q, x_si, w_si, xe_in, w_e8, expert_off,
+31 -24
View File
@@ -3,8 +3,8 @@ 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 extra.hcq2.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
from extra.hcq2.hcq2 import make_binary_patch
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
from tinygrad.runtime.support.hcq2 import make_binary_patch, make_patch
from tinygrad.uop.ops import sint, UOp
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
from tinygrad.dtype import dtypes
@@ -146,31 +146,37 @@ pm_pm4_opsel = PatternMatcher([
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), pm4_store),
])
def pm4_submit(cmdbuf, devs):
size, zero = UOp.const(dtypes.uint32, cmdbuf.nbytes() // dtypes.uint32.itemsize), UOp.const(dtypes.int, 0)
# the compute queue's ring and its host-side ring/write/put pointers (placeholders, resolved in pm_bufferize)
for d in devs: q = Device[d].compute_queue
def pm4_submit(ctx, lin):
# ensure compute queues are allocated
for d in (devs:=ctx.devs): q = Device[d].compute_queue
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COMPUTE:0_{name}")
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
# place the cmdbuf at the ring's write offset, wrapping the ring
put = put_ptr.index(zero)
next_put = put + size.cast(put.dtype)
i = UOp.range(size, 0, dtype=dtypes.int, src=(cmdbuf,))
ring_idx = ((put + i.cast(put.dtype)) % q.ring.size).cast(dtypes.int)
# two tail dwords coordinate safe IB reuse: GPU completions and host submits
size_dw = sum(len(ins.src) for ins in lin.src) + len(release_mem(ctx, 0, 0).src)
assert size_dw < (1 << 20), f"indirect buffer of {size_dw} dwords doesn't fit one packet"
# copy the cmdbuf into the ring and advance the put/write pointers
copy_to_ring = ring.index(ring_idx).store(cmdbuf.index(i).load()).end(i)
bump_put_ptr = put_ptr.index(zero).store(next_put)
bump_wptr = wptr.index(zero).store(next_put)
ib = UOp.placeholder((size_dw + 2,), dtypes.uint32, next(UOp.unique_num), device=devs, volatile=True).rtag("cmdbuf")
done_idx, submit_idx = UOp.const(dtypes.int, size_dw + 0), UOp.const(dtypes.int, size_dw + 1)
submitted = (counter:=ib.after(*[make_patch(ib, (size_dw + i) * 4, UOp.const(dtypes.uint32, 0)) for i in range(2)]).index(submit_idx)).load()
completed = ib.after(loop:=UOp.loop(0)).index(done_idx).load()
ib_free = completed.end(loop, completed != submitted)
# ring the doorbell once the copy and pointer bumps have landed
flush = UOp.barrier(copy_to_ring, bump_put_ptr, bump_wptr)
return doorbell.after(flush).index(zero).store(next_put)
bump_fence = pm4_store(ctx, UOp(Ops.SLICE, dtypes.uint32, (ib, UOp.const(dtypes.weakint, size_dw)), 2), (submitted + 1).cast(dtypes.uint64))
cmdbuf = make_cmdbuf(lin.replace(src=lin.src + (bump_fence,)), devs, buf=ib, dep=ib_free)
pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
lambda lin: pm4_submit(make_cmdbuf(lin, to_tuple(lin.arg[0])), to_tuple(lin.arg[0])))])
# the ring itself only carries a packet pointing at the ib, wrapping the ring
put = put_ptr.index(zero:=UOp.const(dtypes.int, 0))
pkt = (ctx.pm4.PACKET3(ctx.pm4.PACKET3_INDIRECT_BUFFER, 2), *data64_le(cmdbuf.getaddr(devs)), size_dw | ctx.pm4.INDIRECT_BUFFER_VALID)
write_pkt = UOp.barrier(*[ring.index(((put + off) % q.ring.size).cast(dtypes.int)).store(UOp.const(dtypes.uint32, x)) for off,x in enumerate(pkt)])
# advance the put/write pointers past the packet
bump_put_ptr = put_ptr.index(zero).store(put + len(pkt))
bump_wptr = wptr.index(zero).store(put + len(pkt))
flush = UOp.barrier(write_pkt, bump_put_ptr, bump_wptr, counter.store(submitted + 1))
return doorbell.after(flush).index(zero).store(put + len(pkt))
pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"), pm4_submit)])
# *****************
# SDMA
@@ -242,7 +248,7 @@ def sdma_submit(cmdbuf, devs):
return doorbell.after(flush).index(zero).store(next_put_b)
pm_sdma_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
lambda lin: sdma_submit(make_cmdbuf(lin, to_tuple(lin.arg[0])), to_tuple(lin.arg[0])))])
lambda ctx, lin: sdma_submit(make_cmdbuf(lin, ctx.devs), ctx.devs))])
@dataclass(frozen=True)
class AMDEncodeCtx: # encode-time constants for one queue: devs (every cmdbuf address resolves into these) + gfx version + packet/ip modules
@@ -253,7 +259,7 @@ def encode_queue(q:UOp) -> UOp|None:
d = Device[(devs:=to_tuple(q.arg[0]))[0]]
ctx = AMDEncodeCtx(devs, d.target, d.pm4, d.sdma, d.soc, d.gc, d.nbio, d.xccs, d.max_copy_size, d.tmpring_size)
opsel, submit = (pm_pm4_opsel, pm_pm4_submit) if q.arg[1].startswith("COMPUTE") else (pm_sdma_opsel, pm_sdma_submit)
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"))
return submit.rewrite(graph_rewrite(q, opsel + pm_flatten_linear, walk=True, ctx=ctx, name=f"{q.arg[1]} opsel"), ctx)
@dataclass(frozen=True)
class AMDProgramData:
@@ -511,7 +517,8 @@ class PCIIface(PCIIfaceBase):
cq = d.compute_queue
for b in (cq.put_value, cq.read_ptr, cq.write_ptr): b._buf.view.view(fmt='Q')[0] = 0
d.iface.dev_impl.gfx.setup_ring(*cq.params)
d.timeline_signal()._buf.cpu_view().mv.cast('Q')[0] = d.timeline_value().as_memoryview(force_zero_copy=True).cast('Q')[0] - 1
d.timeline_signal('COMPUTE:0')._buf.cpu_view().mv.cast('Q')[0] = \
d.timeline_value('COMPUTE:0').as_memoryview(force_zero_copy=True).cast('Q')[0] - 1
def sleep(self, timeout):
if hasattr(self.pci_dev, 'irq_poller') and self.pci_dev.irq_poller is not None and (events_cnt:=len(self.pci_dev.irq_poller.poll(timeout))):
+2 -2
View File
@@ -31,12 +31,12 @@ def dname_of(device) -> str:
def alloc_like(shape, dtype, device, axis=None) -> Tensor:
if isinstance(device, tuple) and axis is not None:
return Tensor(Tensor.invalids(*shard_shape(shape, axis, len(device)), dtype=dtype, device=device).uop.multi(axis), device=device)
return Tensor(Tensor.invalids(*shard_shape(shape, axis, len(device)), dtype=dtype, device=device).uop.unshard(axis), device=device)
return Tensor.invalids(*shape, dtype=dtype, device=device)
def alloc_local(shape, dtype, device, axis=None) -> Tensor:
if isinstance(device, tuple) and axis is not None:
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=device).uop.multi(0), device=device)
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=device).uop.unshard(0), device=device)
return Tensor.invalids(*shape, dtype=dtype, device=device)
def compile_hip(src:str, defines:list[str]):
+4 -4
View File
@@ -48,7 +48,7 @@ def _fused_ce_loss_bwd(gradient:UOp, kernel:UOp, label_smoothing:float):
axis = logits_u.axis
ndev = len(device)
local_shape = tuple(s//ndev if i == axis else s for i,s in enumerate((MBS, SEQ, VOCAB)))
d_logits = Tensor(Tensor.invalids(*local_shape, dtype=dtypes.bfloat16, device=device).uop.multi(axis), device=device)
d_logits = Tensor(Tensor.invalids(*local_shape, dtype=dtypes.bfloat16, device=device).uop.unshard(axis), device=device)
rows_per_dev = local_shape[0] * local_shape[1]
seq_per_dev = local_shape[1]
else:
@@ -74,11 +74,11 @@ def fused_ce_loss(logits:Tensor, targets:Tensor, label_smoothing:float=0.1) -> T
axis = logits.uop.axis
assert axis in (0, 1), f"unsupported sharding axis={axis} for CE loss"
ndev = len(logits.device)
loss_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
loss_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
device=logits.device)
max_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
max_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
device=logits.device)
lse_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.multi(0),
lse_out = Tensor(Tensor.invalids(rows // ndev, dtype=dtypes.float32, device=logits.device).uop.unshard(0),
device=logits.device)
local_shape = tuple(s//ndev if i == axis else s for i,s in enumerate(logits.shape))
rows_per_dev = local_shape[0] * local_shape[1]
@@ -36,13 +36,13 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
lmax_val = lmax.after(lmax_store.end(it))[0]
lds = UOp.placeholder((THREADS_PER_WG,), dtypes.float, slot=0, addrspace=AddrSpace.LOCAL)
lds = lds.after(lds[tid].store(lmax_val).barrier())
lds = lds.after(lds[tid].store(lmax_val))
step = THREADS_PER_WG // 2
while step:
active = tid < step
other = lds[(tid + step).valid(active)].load()
lds = lds.after(lds[tid.valid(active)].store(lds[tid].maximum(other)).barrier())
lds = lds.after(lds[tid.valid(active)].store(lds[tid].maximum(other)))
step //= 2
device = device[0].split(":")[0] if isinstance(device, tuple) else device.split(":")[0]
+1 -1
View File
@@ -14,7 +14,7 @@ def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None, dtype:DTypeLike|None
shard_axis = ref.uop.axis if axis is None else axis
shape = tuple(s // len(ref.device) if i == shard_axis else s for i, s in enumerate(shape))
axis = ref.uop.axis if axis is None else axis
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=ref.device).uop.multi(axis), dtype=dtype, device=ref.device)
return Tensor(Tensor.invalids(*shape, dtype=dtype, device=ref.device).uop.unshard(axis), dtype=dtype, device=ref.device)
@functools.cache
def custom_fused_qkv_rope_forward(q:UOp, k:UOp, v:UOp, xqkv:UOp, freqs_cis:UOp,
+1 -1
View File
@@ -16,7 +16,7 @@ def _sharded_empty(shape:Tensor, ref:Tensor, axis:int|None) -> Tensor:
if not isinstance(ref.device, tuple): return Tensor.empty(*shape, dtype=ref.dtype, device=ref.device)
shape = tuple(s // len(ref.device) if i == ref.uop.axis else s for i, s in enumerate(shape))
axis = ref.uop.axis if axis is None else axis
return Tensor(Tensor.empty(*shape, dtype=ref.dtype, device=ref.device).uop.multi(axis), dtype=ref.dtype, device=ref.device)
return Tensor(Tensor.empty(*shape, dtype=ref.dtype, device=ref.device).uop.unshard(axis), dtype=ref.dtype, device=ref.device)
def _sharded_empty_like(ref:Tensor, axis:int|None=None) -> Tensor:
return _sharded_empty(ref.shape, ref, axis)
+3 -3
View File
@@ -224,7 +224,7 @@ class Group:
# store to shared memory
red_local_store = red_local[self.laneid].store(red_reg[0])
red_local = red_local.after(red_local_store.barrier()).reshape(red_local.shape)
red_local = red_local.after(red_local_store).reshape(red_local.shape)
# reduce from shared memory
for inner in self.ker.range(3, axis_type=AxisType.REDUCE, track=False):
@@ -258,7 +258,7 @@ class Group:
# store to shared memory
red_local_store = red_local[self.laneid].store(red_reg[0])
red_local = red_local.after(red_local_store.barrier()).reshape(red_local.shape)
red_local = red_local.after(red_local_store).reshape(red_local.shape)
# reduce from shared memory
for inner in self.ker.range(3, axis_type=AxisType.REDUCE, track=False):
@@ -342,7 +342,7 @@ class Group:
if src.dtype != dst.dtype:
src_load = src_load.cast(dst.dtype)
dst_store = dst[*dst_idxs, height, width, srow, scol].store(src_load)
dst_store = dst_store.end(height, width, outer, inner).barrier()
dst_store = dst_store.end(height, width, outer, inner)
elif dst.addrspace == AddrSpace.REG and src.addrspace == AddrSpace.GLOBAL and isinstance(dst, RT):
srcf = src.flatten()
row_stride = prod(src.shape[axis+1:])
+2
View File
@@ -555,6 +555,8 @@ def wrap_out(f):
assert out.shape == assigned.shape, f"shape mismatch: {assigned.shape} -> {out.shape}"
assert out.device == assigned.device or out.device is None or assigned.device is None, f"device mismatch: {assigned.device} -> {out.device}"
assert out.dtype == assigned.dtype, f"dtype mismatch: {assigned.dtype} -> {out.dtype}"
# an out= that is a view has to be written through its base, and _apply_inplace gives a deviceless base its buffer first
if canonical_base(out) is not out: return _apply_inplace(out, assigned) or out
if out.device is None and assigned.device is not None: out.replace(out.empty_like(device=assigned.device))
return out.assign(assigned)
return _wrap_out
+15
View File
@@ -98,6 +98,21 @@ class TestTorchBackend(unittest.TestCase):
def test_empty_strided_default_dtype(self):
self.assertEqual(torch.empty_strided((2,3), (1,2), device=device).dtype, torch.get_default_dtype())
@unittest.expectedFailure # TODO: empty_strided ignores the requested strides, the backend treats everything as contiguous
def test_empty_strided_honors_strides(self):
self.assertEqual(tuple(torch.empty_strided((2,3), (1,2), device=device).stride()), (1,2))
@unittest.expectedFailure # TODO: torch refuses an out= that overlaps an input, we compute silently
def test_out_overlapping_input_is_rejected(self):
x = torch.arange(6., device=device)
with self.assertRaises(RuntimeError): torch.add(x[:-1], 10, out=x[1:])
def test_out_disjoint_input_is_allowed(self):
# torch permits an out= that shares a base with an input as long as they do not overlap
x, xc = torch.arange(6., device=device), torch.arange(6.)
torch.add(x[:3], 10, out=x[3:]); torch.add(xc[:3], 10, out=xc[3:])
np.testing.assert_equal(x.cpu().numpy(), xc.numpy())
def test_plus_inplace(self):
a = torch.ones(4, device=device)
b = torch.ones(4, device=device)
BIN
View File
Binary file not shown.
+7 -1
View File
@@ -29,6 +29,7 @@
\definecolor{axbrred}{HTML}{E53935} % GROUP_REDUCE
\definecolor{axyellow}{HTML}{F9A825} % UPCAST
\definecolor{axmagenta}{HTML}{7B1FA2} % UNROLL
\definecolor{axgreen}{HTML}{2E7D32} % DEVICE
\title{tinygrad: a single dialect from Tensor programs to Command Buffers}
\author{tinygrad, Corp. \\ \texttt{[email protected]}}
@@ -79,6 +80,7 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
\op{Index} & $(T, i_0, i_1, \ldots)$ & --- & Index from left. $()$-shaped $i$ removes dim; $(k,)$-shaped makes it $k$. \\
\op{Stack} & $(T_0, T_1, \ldots)$ & --- & Join along a newly created leading axis. All shapes must match. \\
\op{Bitcast} & $(T,)$ & dtype & Reinterpret storage as target dtype; preserve total bytes. \\
\op{Unshard} & $(T, R)$ & axis $a$ & Concatenate the shards indexed by \op{Range} $R$ along $a$; $R$ is outer. \\
\bottomrule
\end{tabular}
@@ -258,6 +260,7 @@ Every UOp has a \textbf{dtype}, \textbf{shape}, \textbf{device}, \textbf{addrspa
\op{Const} & from arg & $()$ & \textsc{null} & $[v, v]$ \\
\op{Param} & from arg & from $\mathrm{src}[0]$ & from arg & from src or dtype range \\[3pt]
Movement ops & $\mathrm{src}[0].\mathrm{dtype}$ & (see op) & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
\op{Unshard} & $\mathrm{src}[0].\mathrm{dtype}$ & $\mathrm{src}[0]$, axis $\times n$ & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
\op{Reduce} & $\mathrm{src}[0].\mathrm{dtype}$ & remove first $n$ axes & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\[3pt]
\op{Cast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & clamped to dtype \\
\op{Bitcast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\
@@ -283,7 +286,7 @@ $[a,A]$, $[b,B]$, $[c,C]$ denote min\_max of $\mathrm{src}[0]$, $\mathrm{src}[1]
Default \emph{dtype range}: $[\mathrm{dtype\_min},\, \mathrm{dtype\_max}]$.
\medskip
\textbf{axis} tracks the multi-device sharding dimension. \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim).
\textbf{axis} tracks the multi-device sharding dimension. \op{Unshard} defines it (axis $=$ arg). \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim).
\op{Reshape} remaps axis to preserve the shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts axis right by $|\mathbf{n}|$.
\op{Reduce} on the shard axis $\to$ \textsc{null} (shard axis is among the first $n$ axes). \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
@@ -297,6 +300,7 @@ Each kernel's iteration space is a set of \op{Range} axes. Every range has an \t
\toprule
\textbf{AxisType} & \textbf{Letter} & \textbf{Split from} & \textbf{Direction} & \textbf{Semantics} \\
\midrule
{\color{axgreen}\texttt{DEVICE}} & \texttt{d} & --- & --- & Multi-device sharding dimension. \\
{\color{axblue}\texttt{GLOBAL}} & \texttt{g} & --- & --- & GPU global workgroup dimension. \\
{\color{axcyan}\texttt{LOCAL}} & \texttt{l} & g, L & inner & Workgroup local dimension (shared memory). \\
{\color{axbrcyan}\texttt{WARP}} & \texttt{w} & \multicolumn{2}{l}{(created by \op{TC})} & Warp-level lanes for tensor cores. \\
@@ -378,6 +382,8 @@ def scatter_add(T, idx, val):
Let $D = (d_0, \ldots, d_{n-1})$ be an $n$-tuple device.
\op{Copy} to an $n$-tuple device reshards with axis $= 0$. \op{Copy} never changes shape.
\textbf{Sharding} splits a tensor along an axis across $n$ devices. It opens a \op{Range} of type \texttt{DEVICE} (a symbolic per-device index $d$), shrinks each device's view to its piece, then closes the range with \op{Unshard}$(T, R, a)$. The result is a logical tensor whose shape along axis $a$ is the full size; each device holds $1/n$ of it. \op{Unshard} is the inverse of sharding --- it marks the boundary between per-device computation and the logical multi-device tensor. The range need not be \texttt{DEVICE}; e.g.\ a \texttt{WARP} range closes the same way, concatenating per-lane shards along $a$ with the range as the outer factor.
\begin{lstlisting}
# T has shape (s,) on a single device.
+6
View File
@@ -19,6 +19,12 @@ class TestArange(unittest.TestCase):
self.assertLess(self._get_flops(Tensor.arange(256).clone(), np.arange(256)), 256*4)
self.assertLess(self._get_flops(Tensor.arange(2560).clone(), np.arange(2560)), 2560*4)
def test_cat_complexity(self):
x = Tensor.arange(2**10) + Tensor.empty((), dtype=dtypes.uint32)
out = x.cat(x).cat(Tensor.empty(1, dtype=dtypes.uint32))
linear = compile_linear(out.schedule_linear())
self.assertLessEqual(estimate_uop(linear.src[-1]).ops, out.numel()*20)
@unittest.skipIf(Device.DEFAULT == "CL", "flaky in CI")
def test_arange_cumsum(self):
np.testing.assert_equal(Tensor.arange(513).cumsum(0).numpy(), np.arange(513).cumsum())
+13 -6
View File
@@ -55,7 +55,7 @@ def flip_contract_kernel(dest:UOp, src:UOp):
return store.end(i, j).sink(arg=KernelInfo(name=f"flip_contract_{dest.numel()}", opts_to_apply=()))
def slice_sum_kernel(dest:UOp, src:UOp):
G = UOp.range(src.shape[0], 0)
G = UOp.range(src.shape[0], 0, dtype=dtypes.int)
slice_src = src[G, :]
reg = UOp.placeholder((1,), dest.dtype, 0, addrspace=AddrSpace.REG)
reg = reg.after(G)[0].set(0)
@@ -117,13 +117,20 @@ class TestCustomKernel(unittest.TestCase):
out = c.flatten().tolist()
assert all(x == 2 for x in out), "all 2"
def test_duplicate_call_arg(self):
x = Tensor.arange(4).clone().realize()
x = Tensor.custom_kernel(x, x, fxn=custom_add_one_kernel)[0]
# webgpu silently errors when a kernel has duplicate buffer args, so the list stays the same.
# https://gpuweb.github.io/gpuweb/#abstract-opdef-encoder-bind-groups-alias-a-writable-resource
self.assertEqual(x.tolist(), [1, 2, 3, 4] if Device.DEFAULT != "WEBGPU" else [0, 1, 2, 3])
def test_simple_sharded(self):
devs = ("CPU:0", "CPU:1")
a = Tensor.ones(16, 16).contiguous().shard(devs, axis=0)
b = Tensor.ones(16, 16).contiguous().shard(devs, axis=0)
# ugly construction to get a sharded empty tensor
c = Tensor(Tensor.empty(8, 16, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.empty(8, 16, device=devs).uop.unshard(0), device=devs)
c = Tensor.custom_kernel(c,a,b, fxn=custom_elementwise_add_kernel)[0]
out = c.flatten().tolist()
assert all(x == 2 for x in out), "all 2"
@@ -132,7 +139,7 @@ class TestCustomKernel(unittest.TestCase):
# PYTHON backend explicitly checks for OOB access for wrong multi shape regression
devs = ("PYTHON:0", "PYTHON:1")
a = Tensor.ones(4, 4).contiguous().shard(devs, axis=0)
c = Tensor(Tensor.empty(2, 4, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.empty(2, 4, device=devs).uop.unshard(0), device=devs)
c = Tensor.custom_kernel(c, a, fxn=custom_add_one_kernel)[0]
assert (c == 2).all().item()
@@ -206,7 +213,7 @@ class TestCustomKernel(unittest.TestCase):
N = 16
a = Tensor.randn(N, N).shard_(devs, axis=0)
b = Tensor.randn(N, N).to(devs)
c = Tensor(Tensor.empty(N//2, N, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.empty(N//2, N, device=devs).uop.unshard(0), device=devs)
tst = Tensor.custom_kernel(c, a, b, fxn=custom_gemm)[0]
self.assertTrue(tst.allclose(a@b, atol=1e-3).item())
@@ -323,7 +330,7 @@ class TestCustomKernel(unittest.TestCase):
def test_multi_invalids_custom_kernel_no_copy(self):
devs = ("CPU:0", "CPU:1")
a = Tensor.ones(4, 4).shard(devs, axis=0).realize()
c = Tensor(Tensor.invalids(2, 4, dtype=dtypes.float, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.invalids(2, 4, dtype=dtypes.float, device=devs).uop.unshard(0), device=devs)
c = Tensor.custom_kernel(c, a, fxn=custom_add_one_kernel)[0]
GlobalCounters.reset()
c.realize()
@@ -388,7 +395,7 @@ class TestCustomKernel(unittest.TestCase):
y = Tensor.custom_kernel(y, x, fxn=custom_add_one_kernel)[0]
if use_custom:
z = Tensor.empty_like(x)
z = Tensor.custom_kernel(y, y.T.T, fxn=custom_add_one_kernel)[0]
z = Tensor.custom_kernel(z, y.T.T, fxn=custom_add_one_kernel)[0]
else: z = y.T.T+1
GlobalCounters.reset()
z.realize()
+1
View File
@@ -124,6 +124,7 @@ class TestFp8sConversions(unittest.TestCase):
def test_float_to_fp8e4m3(self, x):
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e4m3), torch.tensor(x, dtype=torch.float8_e4m3fn).view(torch.uint8).item())
@unittest.skip("fp8 overflow semantics are inconsistent")
def test_float_to_fp8e4m3_extreme_values(self):
for x in [FP8E4M3_MAX, FP8E4M3_MAX*1.01, -FP8E4M3_MAX, -FP8E4M3_MAX*1.01, math.inf, -math.inf, math.nan, -math.nan]:
np.testing.assert_equal(float_to_fp8(x, dtypes.fp8e4m3), torch.tensor(x, dtype=torch.float8_e4m3fn).view(torch.uint8).item())
+1 -1
View File
@@ -80,7 +80,7 @@ class TestQuantizeFP8(unittest.TestCase):
@needs_second_gpu
def test_multi(self):
devs = tuple(f"{Device.DEFAULT}:{i}" for i in range(8))
x = Tensor.empty(2048*8, 1024, dtype=dtypes.bfloat16, device=devs).uop.multi(0)
x = Tensor.empty(2048*8, 1024, dtype=dtypes.bfloat16, device=devs).uop.unshard(0)
x = Tensor(x, device=devs)
amax_state = Tensor.full((), 2.0, dtype=dtypes.float32, device=devs).contiguous()
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=devs).realize()
+5 -3
View File
@@ -1,6 +1,6 @@
import unittest, random
from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variable
from tinygrad.uop.ops import Ops, UOp
from tinygrad.uop.ops import Ops, UOp, AxisType
from tinygrad.helpers import getenv, prod, Context
from tinygrad.nn.state import get_parameters
from tinygrad.engine.realize import run_linear, compile_linear
@@ -52,8 +52,10 @@ class TestMultiTensor(unittest.TestCase):
def test_shard(self):
X = Tensor.ones(256).contiguous().realize()
X.shard_(devices_2, 0)
for lb in X.uop.src:
assert lb.shape == (128,)
assert X.uop.src[0].shape == (128,)
# the MULTI carries and ends the DEVICE range as its second src
assert X.uop.src[1].op is Ops.RANGE and X.uop.src[1].arg[-1] is AxisType.DEVICE
assert X.uop.ended_ranges == X.uop.src[1:]
(X + X).realize()
@unittest.expectedFailure # TODO: fix
+8 -8
View File
@@ -340,11 +340,11 @@ class TestOps(unittest.TestCase):
def test_where(self):
helper_test_op([], lambda: torch.where(torch.tensor([True, False]), 1, 3).type(torch.int32),
lambda: Tensor([True, False]).where(1, 3), forward_only=True)
lambda: Tensor([True, False]).where(1, 3).clone(), forward_only=True)
helper_test_op(
[(100,)],
lambda x: torch.where(x > 0.5, 4, 2).type(torch.int32),
lambda x: (x > 0.5).where(4, 2), forward_only=True)
lambda x: (x > 0.5).where(4, 2).clone(), forward_only=True)
for shps in [[(8,),(1,),(1,)], [(10,10),(10,),(10,)], [(100,)]*3, [(10,10)]*3]:
helper_test_op(
@@ -356,7 +356,7 @@ class TestOps(unittest.TestCase):
helper_test_op(
[(5, 5)],
lambda x: torch.where(x > 0.5, 4, 2).type(torch.int32).permute((1, 0)),
lambda x: (x > 0.5).where(4, 2).permute((1, 0)), forward_only=True)
lambda x: (x > 0.5).where(4, 2).clone().permute((1, 0)), forward_only=True)
def _test_cmp(self, fxn, reverse=True):
# test different dtypes
@@ -636,9 +636,9 @@ class TestOps(unittest.TestCase):
helper_test_op(None, lambda x,y: x%y, forward_only=True, vals=[va, vb])
helper_test_op(None, lambda x: x%2, forward_only=True, vals=[va])
helper_test_op(None, lambda x: x%3, forward_only=True, vals=[va])
helper_test_op(None, lambda x: x%3.5, forward_only=True, vals=[va])
helper_test_op(None, lambda x: x%3.5, lambda x: (x%3.5).clone(), forward_only=True, vals=[va])
helper_test_op(None, lambda x: 100%x, forward_only=True, vals=[va])
helper_test_op(None, lambda x: 100.5%x, forward_only=True, vals=[va])
helper_test_op(None, lambda x: 100.5%x, lambda x: (100.5%x).clone(), forward_only=True, vals=[va])
def test_fmod(self):
a = [-4, 7, 5, 4, -7, 8, -9]
@@ -649,7 +649,7 @@ class TestOps(unittest.TestCase):
vb = [float(bi) for bi in b] if float_b else b
helper_test_op(None, lambda x,y: x.fmod(y), forward_only=True, vals=[va, vb])
helper_test_op(None, lambda x: x.fmod(2), forward_only=True, vals=[va])
helper_test_op(None, lambda x: x.fmod(3.5), forward_only=True, vals=[va])
helper_test_op(None, lambda x: x.fmod(3.5), lambda x: x.fmod(3.5).clone(), forward_only=True, vals=[va])
def test_mul_naninf(self):
helper_test_op([(45,65)], lambda x: x*math.inf)
@@ -706,7 +706,7 @@ class TestOps(unittest.TestCase):
helper_test_op(None, lambda x: 0.7**x, vals=[[-2.,-1,0,1,2,3]])
helper_test_op(None, lambda x: (-2)**x, vals=[[-2.,-1,0,1,2,3]])
# float to power of int
helper_test_op(None, lambda x: 0.7**x, vals=[[-2,-1,0,1,2,3]], forward_only=True)
helper_test_op(None, lambda x: 0.7**x, lambda x: (0.7**x).clone(), vals=[[-2,-1,0,1,2,3]], forward_only=True)
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, NIRRenderer), "TODO: broken in LVP")
@@ -775,7 +775,7 @@ class TestOps(unittest.TestCase):
def test_pow_int_base_float_exponent(self):
for exponent in (0.5, 1.5, 2.0, -1.0, 0.0):
helper_test_op([], lambda: torch.tensor([1, 2, 3, 4], dtype=torch.int) ** exponent,
lambda: Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent, forward_only=True)
lambda: (Tensor([1, 2, 3, 4], dtype=dtypes.int32) ** exponent).clone(), forward_only=True)
def test_sqrt(self):
helper_test_op([(45,65)], lambda x: x.sqrt())
+1 -2
View File
@@ -143,12 +143,11 @@ class TestOptim(unittest.TestCase):
@unittest.skipUnless(dtypes.half in Device[Device.DEFAULT].renderer.supported_dtypes(), "need half")
def test_mixed_precision(self):
old_default_float, dtypes.default_float = dtypes.default_float, dtypes.half
self.enterContext(Context(DEFAULT_FLOAT=dtypes.half))
# weight update would overflow without upcasting
self._test_sgd(10, {'lr': 1e10}, 1e-6, 3e-4)
self._test_adam(1, {'lr': 1e10}, 1e-4, 1e-4)
self._test_adamw(1, {'lr': 1e10}, 1e-4, 1e-4)
dtypes.default_float = old_default_float
def test_assert_tensor_train(self):
t = Tensor.ones((1,1))
+2 -4
View File
@@ -1,7 +1,7 @@
import unittest, math
from tinygrad import dtypes, Tensor, Device
from tinygrad.helpers import getenv, DEV
from tinygrad.helpers import getenv, DEV, Context
from tinygrad.codegen import to_program
from tinygrad.uop.ops import Ops
@@ -232,16 +232,14 @@ class TestRandomness(unittest.TestCase):
@given(strat.sampled_from([dtypes.float, dtypes.float16, dtypes.bfloat16]))
def test_randn_finite(self, default_float):
if default_float not in Device[Device.DEFAULT].renderer.supported_dtypes(): return
old_default_float = dtypes.default_float
# low precision can result in inf from randn
dtypes.default_float = default_float
self.enterContext(Context(DEFAULT_FLOAT=default_float))
t = Tensor.randn(64, 64)
mx = t.max().numpy().item()
mn = t.min().numpy().item()
print(f"testing with {default_float=}")
assert math.isfinite(mx), mx
assert math.isfinite(mn), mn
dtypes.default_float = old_default_float
def test_random_counter_overflow(self):
device = Device.DEFAULT
+1 -1
View File
@@ -363,7 +363,7 @@ class TestCopyFolding(unittest.TestCase):
def test_one_hot_with_copy(self):
y = Tensor([1, 2, 3]).to("CPU")
x = y.one_hot(10)
x = y.one_hot(10).int()
check_schedule(x, 3, filter_sink=False)
@unittest.skip("no longer supported")
+3 -2
View File
@@ -1,7 +1,8 @@
from tinygrad import Tensor, dtypes
dtypes.default_float = dtypes.float16
from tinygrad.dtype import to_dtype
from tinygrad.helpers import getenv
from tinygrad.helpers import getenv, Context
Context(DEFAULT_FLOAT=dtypes.float16).__enter__()
if __name__ == "__main__":
# matmuls in bert layers
+3 -2
View File
@@ -1,9 +1,10 @@
from tinygrad import Tensor, dtypes, GlobalCounters
dtypes.default_float = dtypes.float16
from tinygrad.dtype import to_dtype
from tinygrad.helpers import getenv
from tinygrad.helpers import getenv, Context
from test.backend.test_softmax_fusion import single_kernel_softmax
Context(DEFAULT_FLOAT=dtypes.float16).__enter__()
if __name__ == "__main__":
# softmax in bert layers
BS = getenv("BS", 96//6)
+7 -18
View File
@@ -5,8 +5,9 @@ These tests intentionally cause GPU faults to verify error handling.
Run with: DEV=AMD python -m pytest test/external/external_test_gpu_crash.py -v
"""
import unittest, re, importlib
from tinygrad.device import Device, TinyELF
from tinygrad.helpers import Target
from tinygrad import Device, UOp, dtypes
from tinygrad.engine.realize import run_linear
from tinygrad.uop.ops import Ops, KernelInfo
from tinygrad.renderer.amd.dsl import s, v, Inst, NULL
RDNA3_CDNA3_MAP = {"v_mov_b32_e32": "v_mov_b32_e32", "s_mov_b32": "s_mov_b32", "s_waitcnt": "s_waitcnt", "s_endpgm": "s_endpgm",
@@ -14,20 +15,11 @@ RDNA3_CDNA3_MAP = {"v_mov_b32_e32": "v_mov_b32_e32", "s_mov_b32": "s_mov_b32", "
"global_atomic_add_u32": "global_atomic_add", "flat_load_b32": "flat_load_dword",
"flat_store_b32": "flat_store_dword", "flat_atomic_add_u32": "flat_atomic_add", "s_load_b32": "s_load_dword"}
def assemble(code:str, name:str="test", is_cdna:bool=False) -> str:
kd = {"next_free_vgpr": 8, "next_free_sgpr": 8, "user_sgpr_kernarg_segment_ptr": 1, "kernarg_size": 8}
if is_cdna: kd["accum_offset"] = 8
else: kd["wavefront_size32"] = 1
return f".text\n.globl {name}\n.p2align 8\n.type {name},@function\n{name}:\n{code}\n.rodata\n.p2align 6\n.amdhsa_kernel {name}\n" + \
"\n".join(f".amdhsa_{k} {v}" for k,v in kd.items()) + "\n.end_amdhsa_kernel"
@unittest.skipIf(Device.DEFAULT != "AMD", "AMD required")
class TestGPUCrash(unittest.TestCase):
@classmethod
def setUpClass(cls):
from tinygrad.runtime.support.compiler_amd import HIPCompiler
cls.dev = Device["AMD"]
cls.compiler = HIPCompiler(cls.dev.arch)
cls.is_cdna = cls.dev.target[0] < 10
ins = importlib.import_module('tinygrad.runtime.autogen.amd.' + ('cdna' if cls.is_cdna else 'rdna3') + '.ins')
for rdna3_name, cdna3_name in RDNA3_CDNA3_MAP.items():
@@ -42,14 +34,11 @@ class TestGPUCrash(unittest.TestCase):
except Exception:
self.fail("Device not working before test")
def _run(self, code: str):
prg = self.dev.runtime(TinyELF(self.compiler.compile(assemble(code, is_cdna=self.is_cdna)), "test",
Target("AMD", arch=self.dev.arch), ()))
prg(self.dev.allocator.alloc(64), global_size=(1,1,1), local_size=(1,1,1), wait=True)
def _run_insts(self, insts: list[Inst]):
from test.amd.disasm import disasm
self._run("\n".join(disasm(i) for i in insts))
buf = UOp.new_buffer("AMD", 64, dtypes.uint8)
sink = UOp.sink(UOp.param(0, dtypes.uint8, (64,), device="AMD"), UOp.special(1, "lidx0"), arg=KernelInfo("test"))
prg = UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=i) for i in insts))))
run_linear(UOp(Ops.LINEAR, src=(prg.call(buf),)), wait=True)
def _assert_gpu_fault(self, func):
"""Assert that func raises a RuntimeError indicating a GPU fault (not a setup error)."""
+2 -3
View File
@@ -3,6 +3,7 @@ import unittest
import numpy as np
from tinygrad import Tensor, dtypes
from tinygrad.engine.jit import TinyJit
from tinygrad.helpers import Context
from test.helpers import derandomize_model
from examples.llama import Transformer
@@ -14,8 +15,7 @@ def helper_test_jitted_correctness(gen, train, train_jit):
class TestJittedModels(unittest.TestCase):
def test_jitted_tiny_llama(self):
old_float = dtypes.default_float
dtypes.default_float = dtypes.float16
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
args_tiny = {"dim": 1024, "hidden_dim": 1024, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-05, "vocab_size": 1000}
model = Transformer(**args_tiny)
@@ -25,7 +25,6 @@ class TestJittedModels(unittest.TestCase):
@TinyJit
def test_jit(t): return model(t, 0).realize()
helper_test_jitted_correctness(lambda: (Tensor([[1,]]),), test, test_jit)
dtypes.default_float = old_float
def test_jitted_stable_diffusion(self):
from examples.stable_diffusion import UNetModel, unet_params
@@ -51,7 +51,7 @@ class TestEval(unittest.TestCase):
vae = AutoencoderKL()
for p in get_parameters(vae): p.to_(GPUS).realize()
x = Tensor.zeros(48,4,64,64).contiguous().to(GPUS).realize()
x.uop = x.uop.multi(0)
x.uop = x.uop.unshard(0)
for _ in range(2): vae_decode(x, vae)
if __name__=="__main__":
+2 -1
View File
@@ -131,7 +131,8 @@ class PM4Executor(AMDQueue):
_ = self._next_dword() # ev
ptr = to_mv(self.gpu.translate_addr(addr_lo + (addr_hi << 32)), 8)
if mem_data_sel == 1 or mem_data_sel == 2: ptr.cast('Q')[0] = val
if mem_data_sel == 1: ptr.cast('I')[0] = val & 0xffffffff
elif mem_data_sel == 2: ptr.cast('Q')[0] = val
elif mem_data_sel == 3:
if mem_event_type == CACHE_FLUSH_AND_INV_TS_EVENT: ptr.cast('Q')[0] = int(time.perf_counter() * 1e8)
else: raise RuntimeError(f"Unknown {mem_data_sel=} {mem_event_type=}")
+26 -36
View File
@@ -2,7 +2,7 @@ import unittest, math, struct, operator
from tinygrad import Tensor, Device
from tinygrad.dtype import DTYPES_DICT, dtypes, Invalid, truncate, float_to_fp16, float_to_bf16, _to_np_dtype, least_upper_dtype, least_upper_float
from tinygrad.helpers import getenv
from tinygrad.helpers import getenv, Context
from hypothesis import given, settings, strategies as strat
import numpy as np
import torch
@@ -58,8 +58,8 @@ class TestHelpers(unittest.TestCase):
def test_from_py(self):
assert dtypes.from_py(True) == dtypes.bool
assert dtypes.from_py(Invalid) == dtypes.bool
assert dtypes.from_py(2) == dtypes.default_int
assert dtypes.from_py(3.0) == dtypes.default_float
assert dtypes.from_py(2) == dtypes.weakint
assert dtypes.from_py(3.0) == dtypes.weakfloat
assert dtypes.from_py([]) == dtypes.default_float
assert dtypes.from_py(()) == dtypes.default_float
assert dtypes.from_py([True]) == dtypes.bool
@@ -245,19 +245,14 @@ class TestTypePromotion(unittest.TestCase):
assert least_upper_dtype(dtypes.weakfloat, dtypes.float64) == dtypes.float64
class TestTypeSpec(unittest.TestCase):
def setUp(self):
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
def tearDown(self):
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
def test_set_dtype_default(self):
for default_int in [dtypes.int8, dtypes.int16, dtypes.int32, dtypes.int64]:
dtypes.default_int = default_int
assert dtypes.default_int == default_int
with Context(DEFAULT_INT=default_int):
assert dtypes.default_int == default_int
for default_float in [*dtypes.fp8s, dtypes.float16, dtypes.bfloat16, dtypes.float32, dtypes.float64]:
dtypes.default_float = default_float
assert dtypes.default_float == default_float
with Context(DEFAULT_FLOAT=default_float):
assert dtypes.default_float == default_float
@given(strat.sampled_from(core_dtypes), strat.sampled_from([operator.gt, operator.ge, operator.le, operator.lt, operator.eq, operator.ne]))
def test_bool_ops(self, dtype, op):
@@ -265,7 +260,7 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(core_dtypes), strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_functions_return_index(self, dtype, default_int, default_float):
dtypes.default_int, dtypes.default_float = default_int, default_float
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
assert Tensor([0, 1], dtype=dtype).argmax().dtype == dtypes.int32
assert Tensor([0, 1], dtype=dtype).argmin().dtype == dtypes.int32
assert Tensor([0, 1], dtype=dtype).multinomial().dtype == dtypes.int32
@@ -285,7 +280,7 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(dtype_floats), strat.sampled_from(dtype_floats))
def test_attention_returns_same_dtype(self, data_dtype, default_float):
dtypes.default_float = default_float
self.enterContext(Context(DEFAULT_FLOAT=default_float))
query = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
key = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
value = Tensor.rand(32, 8, 128, 64, dtype=data_dtype)
@@ -296,32 +291,27 @@ class TestTypeSpec(unittest.TestCase):
assert query.scaled_dot_product_attention(key, value, attn_mask=mask).dtype == data_dtype
class TestAutoCastType(unittest.TestCase):
def setUp(self):
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
def tearDown(self):
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
@given(strat.sampled_from(dtype_floats), strat.sampled_from(dtype_floats))
def test_least_upper_float_input_is_float(self, input_dtype, default_float):
dtypes.default_float = default_float
self.enterContext(Context(DEFAULT_FLOAT=default_float))
self.assertEqual(least_upper_float(input_dtype), input_dtype)
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_least_upper_float_input_is_int(self, input_dtype, default_float):
dtypes.default_float = default_float
self.enterContext(Context(DEFAULT_FLOAT=default_float))
self.assertEqual(least_upper_float(input_dtype), default_float)
@given(strat.sampled_from(core_dtypes))
def test_broadcast_scalar(self, dt):
assert (Tensor.ones(4, 4, dtype=dt) + 2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
assert (Tensor.ones(4, 4, dtype=dt) + 2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
assert (Tensor.ones(4, 4, dtype=dt) + 2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
assert (Tensor.ones(4, 4, dtype=dt) + True).dtype == dt
@given(strat.sampled_from(core_dtypes))
def test_pad_scalar(self, dt):
t = Tensor.ones(4, dtype=dt)
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
assert t.pad(((1, 1),), value=2.3).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
assert t.pad(((1, 1),), value=2).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
assert t.pad(((1, 1),), value=True).dtype == dt
@given(strat.sampled_from(core_dtypes))
@@ -332,7 +322,7 @@ class TestAutoCastType(unittest.TestCase):
@given(strat.sampled_from(dtype_floats))
def test_int_div_int(self, default_float):
dtypes.default_float = default_float
self.enterContext(Context(DEFAULT_FLOAT=default_float))
self.assertEqual(Tensor([1]).div(Tensor([2])).dtype, default_float)
def test_sum(self):
@@ -420,16 +410,16 @@ class TestAutoCastType(unittest.TestCase):
@given(strat.sampled_from(core_dtypes))
def test_where_one_scalar(self, dt):
t = Tensor(2, dtype=dt)
self.check_where_alternate_input_other(t, 3.2, (dt if dtypes.is_float(dt) else dtypes.default_float))
self.check_where_alternate_input_other(t, 3, (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int))
self.check_where_alternate_input_other(t, 3.2, (dt if dtypes.is_float(dt) else dtypes.weakfloat))
self.check_where_alternate_input_other(t, 3, (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint))
self.check_where_alternate_input_other(t, True, dt)
def test_where_two_scalars(self):
self.check_where_alternate_input_other(3.1, 3.2, dtypes.default_float)
self.check_where_alternate_input_other(3.1, 3, dtypes.default_float)
self.check_where_alternate_input_other(3.1, True, dtypes.default_float)
self.check_where_alternate_input_other(3, 2, dtypes.default_int)
self.check_where_alternate_input_other(3, True, dtypes.default_int)
self.check_where_alternate_input_other(3.1, 3.2, dtypes.weakfloat)
self.check_where_alternate_input_other(3.1, 3, dtypes.weakfloat)
self.check_where_alternate_input_other(3.1, True, dtypes.weakfloat)
self.check_where_alternate_input_other(3, 2, dtypes.weakint)
self.check_where_alternate_input_other(3, True, dtypes.weakint)
def test_where_non_bool_cond_raises(self):
with self.assertRaises(RuntimeError): Tensor([1, 0, 2]).where(1, 0)
@@ -441,8 +431,8 @@ class TestAutoCastType(unittest.TestCase):
@given(strat.sampled_from(core_dtypes))
def test_maximum_const(self, dt):
assert Tensor([1, 2], dtype=dt).maximum(3.1).dtype == (dt if dtypes.is_float(dt) else dtypes.default_float)
assert Tensor([1, 2], dtype=dt).maximum(3).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.default_int)
assert Tensor([1, 2], dtype=dt).maximum(3.1).dtype == (dt if dtypes.is_float(dt) else dtypes.weakfloat)
assert Tensor([1, 2], dtype=dt).maximum(3).dtype == (dt if dtypes.is_float(dt) or dtypes.is_int(dt) else dtypes.weakint)
assert Tensor([1, 2], dtype=dt).maximum(True).dtype == dt
def test_div(self):
@@ -453,7 +443,7 @@ class TestAutoCastType(unittest.TestCase):
def test_div_const(self):
assert (Tensor([1, 2], dtype=dtypes.int32) / 2).dtype == dtypes.default_float
assert (Tensor([1, 2], dtype=dtypes.int32) / 2.0).dtype == dtypes.default_float
assert (Tensor([1, 2], dtype=dtypes.int32) / 2.0).dtype == dtypes.weakfloat
assert (Tensor([1, 2], dtype=dtypes.float16) / 2).dtype == dtypes.float16
assert (Tensor([1, 2], dtype=dtypes.float16) / 2.0).dtype == dtypes.float16
+7 -2
View File
@@ -1,5 +1,5 @@
import gc, unittest
from tinygrad import Tensor, GlobalCounters, dtypes
from tinygrad import Tensor, UOp, GlobalCounters, dtypes
from tinygrad.engine.jit import TinyJit
from tinygrad.helpers import Context
@@ -181,7 +181,7 @@ class TestMultiScalarALU(unittest.TestCase):
@functools.cache
def _fxn(x_p, device):
t = Tensor(x_p, device=device)
inner = Tensor(t.uop.src[0]) if t.uop.op is Ops.MULTI else t
inner = Tensor(t.uop.src[0]) if t.uop.op is Ops.UNSHARD else t
return (inner.sum(),)
param = x.as_param(0)
fxn = _fxn(param.uop, x.device)
@@ -218,5 +218,10 @@ class TestMultiAxis(unittest.TestCase):
self.assertEqual(e.uop.axis, 0)
self.assertTrue(e.uop.has_buffer_identity())
def test_symbolic_reshape_shard_axis(self):
rows = UOp.variable("rows", 1, 4).bind(3)
x = Tensor.empty(4, 2).shard(("NULL:1", "NULL:2"), axis=1)[:rows]
self.assertEqual(x.reshape(rows, 1, 2).uop.axis, 2)
if __name__ == '__main__':
unittest.main()
+3 -7
View File
@@ -47,12 +47,8 @@ class TestRealWorld(unittest.TestCase):
gc.collect()
global global_mem_used
global_mem_used = GlobalCounters.mem_used
self.old_float = dtypes.default_float
np.random.seed(2002)
def tearDown(self):
dtypes.default_float = self.old_float
@slow
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
def test_stable_diffusion(self):
@@ -81,7 +77,7 @@ class TestRealWorld(unittest.TestCase):
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
def test_llama(self):
dtypes.default_float = dtypes.float16
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
args_tiny = {"dim": 1024, "hidden_dim": 2048, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-05, "vocab_size": 1000}
model = LLaMaTransformer(**args_tiny)
@@ -93,7 +89,7 @@ class TestRealWorld(unittest.TestCase):
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
def test_gpt2(self):
dtypes.default_float = dtypes.float16
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
args_tiny = {"dim": 1024, "n_heads": 8, "n_layers": 8, "norm_eps": 1e-5, "vocab_size": 1000}
model = GPT2Transformer(**args_tiny)
@@ -150,7 +146,7 @@ class TestRealWorld(unittest.TestCase):
@unittest.skipUnless(dtypes.float16 in supported_dtypes, "need dtypes.float16")
def test_train_cifar_hyp(self):
dtypes.default_float = dtypes.float16
self.enterContext(Context(DEFAULT_FLOAT=dtypes.float16))
with Context(TRAINING=1):
model = SpeedyResNet(Tensor.ones((12,3,2,2)))
optimizer = optim.SGD(get_parameters(model), lr=0.01, momentum=hyp['opt']['momentum'], nesterov=True, weight_decay=hyp['opt']['bias_decay'])
+1 -3
View File
@@ -603,14 +603,12 @@ class TestSchedule(unittest.TestCase):
check_schedule(p, 4)
def test_conv2d(self, allowed=4, dtype=dtypes.float):
old_default_float, dtypes.default_float = dtypes.default_float, dtype
dtypes.default_float = dtype
self.enterContext(Context(DEFAULT_FLOAT=dtype))
Tensor.manual_seed(0)
BS, CIN = 2, 3
img = Tensor.randn(BS, CIN, 64, 64).realize()
w = Tensor.uniform(16, CIN, 3, 3).realize()
ret = Tensor.conv2d(img, w).relu().mean().backward()
dtypes.default_float = old_default_float
linear, var_vals = Tensor.linear_with_vars(ret, img.grad, w.grad)
cnt = len([call for call in linear.src if call.src[0].op is Ops.SINK])
assert cnt == allowed, f"expected {allowed} kernels, got {cnt}"
+1 -1
View File
@@ -181,7 +181,7 @@ class TestTensorPad(unittest.TestCase):
t = Tensor.arange(9).reshape(1, 1, 3, 3)
self.assertEqual(t.dtype, dtypes.int)
r = t.pad((1, 2, 0, -1), value=-float('inf'))
self.assertEqual(r.dtype, dtypes.float)
self.assertEqual(r.dtype, dtypes.weakfloat)
self.assertEqual(r.shape, (1, 1, 2, 6))
class TestTensorDeviceMismatch(unittest.TestCase):
+6 -1
View File
@@ -1,5 +1,5 @@
# uops tests that pass on NULL backend (no copyout needed)
import unittest
import math, unittest
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.helpers import Timing, Context, cdiv
@@ -115,6 +115,11 @@ class TestExecALU(unittest.TestCase):
def test_sqrt(self):
self.assertEqual(exec_alu(Ops.SQRT, dtypes.float, (0.0,)), 0.0)
def test_trunc_nonfinite(self):
self.assertEqual(exec_alu(Ops.TRUNC, dtypes.float, (math.inf,)), math.inf)
self.assertEqual(exec_alu(Ops.TRUNC, dtypes.float, (-math.inf,)), -math.inf)
self.assertTrue(math.isnan(exec_alu(Ops.TRUNC, dtypes.float, (math.nan,))))
def test_invalid_poison(self):
# Invalid poisons any binary op regardless of result dtype: a comparison must not fold to a boolean
self.assertIs(exec_alu(Ops.CMPLT, dtypes.bool, (Invalid, 1)), Invalid)
+3 -3
View File
@@ -270,7 +270,7 @@ class TestKernelOpts(unittest.TestCase):
def test_padto_where(self):
Tensor.manual_seed(0)
N = 17
a = (Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1).where(1, 0)
a = (Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1).where(1, 0).int()
helper_linearizer_opt(a.max(0), [
[Opt(OptOps.PADTO, 0, 32)],
[Opt(OptOps.PADTO, 0, 32), Opt(OptOps.UPCAST, 0, 8),],
@@ -280,8 +280,8 @@ class TestKernelOpts(unittest.TestCase):
Tensor.manual_seed(0)
N = 17
r = Tensor.randn(N, N).realize().max(axis=0, keepdim=True) > 1
a0 = r.where(1, 0)
a1 = r.where(2, 0)
a0 = r.where(1, 0).int()
a1 = r.where(2, 0).int()
helper_linearizer_opt([a0.max(0), a1.max(0)], [
[Opt(OptOps.PADTO, 0, 32)],
[Opt(OptOps.PADTO, 0, 32), Opt(OptOps.UPCAST, 0, 8),],
+8
View File
@@ -1066,5 +1066,13 @@ class TestAfterCachePatterns(unittest.TestCase):
np.testing.assert_array_equal(head.numpy(), [3])
np.testing.assert_array_equal(full.numpy(), [1, 2])
class TestBatchNormRunningStats(unittest.TestCase):
@unittest.expectedFailure # TODO: nothing reads the stat update so it is never scheduled, and the chain grows every step
def test_running_stats_are_realized(self):
from tinygrad import nn
bn, x = nn.BatchNorm(4), Tensor.randn(2, 4, 3, 3).contiguous().realize()
with Context(TRAINING=1): bn(x).realize()
self.assertTrue(bn.running_mean.uop.base.is_realized)
if __name__ == "__main__":
unittest.main()
+28 -32
View File
@@ -1,8 +1,8 @@
import unittest, math, subprocess
from tinygrad.tensor import Tensor
from tinygrad.dtype import dtypes, DType, DTYPES_DICT
from tinygrad.dtype import dtypes, DType, DTYPES_DICT, strong_dtype
from tinygrad.device import Device
from tinygrad.helpers import getenv, DEBUG, EMULATED_DTYPES
from tinygrad.helpers import getenv, DEBUG, EMULATED_DTYPES, Context
from test.helpers import slow
from hypothesis import given, settings, strategies as strat
import numpy as np
@@ -25,6 +25,8 @@ def _assert_eq(tensor:Tensor, target_dtype:DType, target, tol_target_dtype:float
if DEBUG >= 2: print(tensor.numpy())
try:
assert tensor.dtype == target_dtype
# weak values read back at their default.
target_dtype = strong_dtype(target_dtype)
# denormals are zero
if target_dtype in dtypes.floats and (target_dtype not in supported_dtypes or target_dtype in EMULATED_DTYPES.tolist(dtypes)):
fe, fm = dtypes.finfo(target_dtype)
@@ -37,10 +39,13 @@ def _assert_eq(tensor:Tensor, target_dtype:DType, target, tol_target_dtype:float
raise AssertionError(f"\ntensor {tensor.numpy()} dtype {tensor.dtype} does not match target {target} with dtype {target_dtype}") from e
class TestTypeSpec(unittest.TestCase):
def setUp(self):
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
def tearDown(self):
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
def test_default_dtype_context(self):
default_float, default_int = dtypes.default_float, dtypes.default_int
with Context(DEFAULT_FLOAT=dtypes.half, DEFAULT_INT=dtypes.int16):
assert dtypes.default_float is dtypes.half
assert dtypes.default_int is dtypes.int16
assert dtypes.default_float is default_float
assert dtypes.default_int is default_int
@unittest.skip("this test is slow and spawning whole pythons")
def test_env_set_default_float(self):
@@ -79,11 +84,11 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_creation(self, default_int, default_float):
dtypes.default_int, dtypes.default_float = default_int, default_float
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
_assert_eq(Tensor(True), dtypes.bool, True)
_assert_eq(Tensor(None), dtypes.default_float, [])
_assert_eq(Tensor(2), dtypes.default_int, 2)
_assert_eq(Tensor(2.34), dtypes.default_float, 2.34)
_assert_eq(Tensor(None), dtypes.weakfloat, [])
_assert_eq(Tensor(2), dtypes.weakint, 2)
_assert_eq(Tensor(2.34), dtypes.weakfloat, 2.34)
_assert_eq(Tensor([]), dtypes.default_float, [])
_assert_eq(Tensor([1]), dtypes.default_int, [1])
# list elements are python scalars; a numpy scalar in a list has no inferred dtype (use np.array or state a dtype)
@@ -98,7 +103,7 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_full(self, default_int, default_float):
dtypes.default_int, dtypes.default_float = default_int, default_float
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
_assert_eq(Tensor.zeros((2, 3)), dtypes.default_float, np.zeros((2, 3)))
_assert_eq(Tensor.zeros((2, 3), dtype=dtypes.int64), dtypes.int64, np.zeros((2, 3)))
@@ -121,7 +126,7 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_reduce_0d_default(self, default_int, default_float):
dtypes.default_int, dtypes.default_float = default_int, default_float
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
_assert_eq(Tensor.ones((2,3,0)).sum(2), dtypes.default_float, np.zeros((2, 3)))
# TODO: what should this one be?
# _assert_eq(Tensor.ones((2,3,0), dtype=dtypes.default_int).sum(2), dtypes.default_int, np.zeros((2, 3)))
@@ -129,7 +134,7 @@ class TestTypeSpec(unittest.TestCase):
@given(strat.sampled_from(dtype_ints), strat.sampled_from(dtype_floats))
def test_arange(self, default_int, default_float):
dtypes.default_int, dtypes.default_float = default_int, default_float
self.enterContext(Context(DEFAULT_INT=default_int, DEFAULT_FLOAT=default_float))
_assert_eq(Tensor.arange(5), dtypes.default_int, np.arange(5))
_assert_eq(Tensor.arange(120), dtypes.default_int, np.arange(120))
@@ -146,11 +151,6 @@ class TestTypeSpec(unittest.TestCase):
_assert_eq(Tensor.arange(5.0, 3.0), dtypes.default_float, np.arange(5.0, 3.0))
class TestAutoCastType(unittest.TestCase):
def setUp(self):
self.old_default_int, self.old_default_float = dtypes.default_int, dtypes.default_float
def tearDown(self):
dtypes.default_int, dtypes.default_float = self.old_default_int, self.old_default_float
def test_int_sqrt(self):
_assert_eq(Tensor([1, 4, 9, 16]).sqrt(), dtypes.default_float, [1, 2, 3, 4])
@@ -190,22 +190,18 @@ class TestAutoCastType(unittest.TestCase):
np.testing.assert_allclose(t.prod(dtype=dtypes.float32).numpy(), 20000)
def test_gradient_dtype(self):
old_default_float = dtypes.default_float
for default_dtype in dtypes.floats:
if default_dtype not in supported_dtypes: continue
dtypes.default_float = default_dtype
for dtype in dtypes.floats:
if dtype not in supported_dtypes: continue
if DEBUG >= 2:
print(f"testing {default_dtype=}, {dtype=}")
a = Tensor([1, 2, 3], dtype=dtype)
b = (a * 5).sum()
b.backward() # if there is dtype mismatch, lazy should assert
assert a.grad.dtype == a.dtype
np.testing.assert_allclose(a.grad.numpy(), [5, 5, 5])
dtypes.default_float = old_default_float
with Context(DEFAULT_FLOAT=default_dtype):
for dtype in dtypes.floats:
if dtype not in supported_dtypes: continue
if DEBUG >= 2:
print(f"testing {default_dtype=}, {dtype=}")
a = Tensor([1, 2, 3], dtype=dtype)
b = (a * 5).sum()
b.backward() # if there is dtype mismatch, lazy should assert
assert a.grad.dtype == a.dtype
np.testing.assert_allclose(a.grad.numpy(), [5, 5, 5])
@unittest.skipIf(Device.DEFAULT == "PYTHON", "very slow")
@slow
+52 -14
View File
@@ -1,9 +1,11 @@
import tempfile, unittest, math
from tinygrad import Tensor, dtypes
from tinygrad import Tensor, dtypes, TinyJit
from tinygrad.helpers import Context
from tinygrad.uop.ops import UOp, Ops
from tinygrad.dtype import least_upper_float
from tinygrad.uop.ops import UOp, Ops, dtype_from_uop
from tinygrad.uop.spec import spec_shared, type_verify
from tinygrad.engine.jit import JitError
class TestWeakPromotion(unittest.TestCase):
@@ -12,10 +14,11 @@ class TestWeakPromotion(unittest.TestCase):
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).rand_like()
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).randn_like()
def test_sum_stays_weak(self):
for weak, value in ((dtypes.weakfloat, 1.0),):
self.assertEqual(Tensor.const(weak, value).expand(3).sum().dtype, weak)
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float16)
def test_reduce_strips_weakness(self):
for weak, value, strong in ((dtypes.weakint, 1, dtypes.default_int), (dtypes.weakfloat, 1.0, dtypes.default_float)):
t = Tensor.const(weak, value).expand(3)
for out in (t.sum(), t.max(), t.prod(), t.cumsum(0), t.cummax(0)[0]): self.assertEqual(out.dtype, strong)
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float32)
def test_materialize_at_default_dtype(self):
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
@@ -23,7 +26,22 @@ class TestWeakPromotion(unittest.TestCase):
self.assertEqual(t.dtype, weak)
self.assertEqual(t.data().itemsize, strong.itemsize)
self.assertEqual(t.numpy().dtype.itemsize, strong.itemsize)
with self.assertRaises(RuntimeError): t.clone("CPU")
# materializing commits at the kind default; contiguous has no layout to fix so it stays weak
self.assertEqual((c := t.clone("CPU")).dtype, strong)
self.assertEqual(c.item(), value)
self.assertEqual(t.contiguous().dtype, weak)
def test_assign_into_weak_commits(self):
t = Tensor.const(dtypes.weakfloat, 0.5)
t.assign(Tensor(1.0, dtype=dtypes.default_float))
self.assertEqual((t.dtype, t.item()), (dtypes.default_float, 1.0))
def test_float_unary_on_weakint_stays_weak(self):
self.assertIs(least_upper_float(dtypes.weakint), dtypes.weakfloat)
def test_copysign_meets_operands(self):
r = Tensor([2], dtype=dtypes.uint8, device="CPU").copysign(Tensor([1], dtype=dtypes.uint32, device="CPU"))
self.assertEqual((r.dtype, r.tolist()), (dtypes.uint32, [2]))
def test_uop_scalar_const_unchanged(self):
for dtype, value in ((dtypes.weakint, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
@@ -50,7 +68,10 @@ class TestWeakPromotion(unittest.TestCase):
self.assertEqual((t_f32 + t_f16).dtype, dtypes.float32)
self.assertEqual(Tensor([2], dtype=dtypes.uint8).pad(((1, 1),), value=1).dtype, dtypes.uint8)
@unittest.expectedFailure # TODO: dot of a weak const tensor defers to the other operand once python scalars are weak consts
def test_concrete_pair_promotes_weak(self):
out = Tensor([-1], dtype=dtypes.int64, device="CPU") + Tensor([3], dtype=dtypes.uint64, device="CPU") + Tensor(0.5)
self.assertEqual((out.dtype, out.tolist()), (dtypes.weakfloat, [2.5]))
def test_dot_defers_weak(self):
weak = Tensor([True, False]).where(Tensor(1), 2)
self.assertEqual(weak.dot(Tensor([1, 1], dtype=dtypes.int8)).dtype, dtypes.int8)
@@ -58,14 +79,18 @@ class TestWeakPromotion(unittest.TestCase):
def test_weak_int_binop(self):
v = UOp.variable("i", 0, 10, dtypes.weakint)
self.assertEqual((v << 1).dtype, dtypes.weakint)
self.assertEqual(dtype_from_uop(Ops.SHL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.uint32, 1)), None), dtypes.int64)
self.assertEqual(UOp.const(dtypes.weakint, 1).alu(Ops.SHL, UOp.const(dtypes.uint8, 1)).dtype, dtypes.uint8)
self.assertEqual((v & 3).dtype, dtypes.weakint)
with self.assertRaises(RuntimeError): Tensor.const(dtypes.weakfloat, 1.0) << Tensor.const(dtypes.weakfloat, 1.0)
with self.assertRaises(RuntimeError): UOp.const(dtypes.int32, 1).alu(Ops.SHL, UOp.const(dtypes.float64, 1))
# float bitwise/shift builds, the spec rejects it
for op in (Ops.SHL, Ops.SHR):
with self.assertRaises(RuntimeError):
UOp.const(dtypes.float32, 1).alu(op, UOp.const(dtypes.int32, 1))
# float bitwise builds, the spec rejects it
with Context(SPEC=1):
f32, wf = UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.weakfloat, 1.0)
for bad in (f32.alu(Ops.AND, f32), f32.alu(Ops.SHL, UOp.const(dtypes.int32, 1)),
UOp(Ops.AND, dtypes.float32, (f32, f32)), UOp(Ops.AND, dtypes.int32, (wf, wf))):
for bad in (f32.alu(Ops.AND, f32), UOp(Ops.AND, dtypes.float32, (f32, f32)), UOp(Ops.AND, dtypes.int32, (wf, wf))):
with self.assertRaises(RuntimeError): type_verify([bad], spec_shared)
def test_integer_values(self):
@@ -81,7 +106,6 @@ class TestWeakPromotion(unittest.TestCase):
x32 = Tensor.full((1,), 0.0, dtype=dtypes.float32, device="CPU")
self.assertEqual((x32 + value).item(), 1.0)
@unittest.expectedFailure # TODO: exp/cos/sigmoid of a weak const stay weak instead of casting to a concrete float
def test_weak_transcendentals(self):
t_f16 = Tensor([1], dtype=dtypes.float16)
for out in (Tensor(2).exp(), Tensor(2).cos(), Tensor(2).sigmoid()):
@@ -125,10 +149,24 @@ class TestWeakMaterializationEntries(unittest.TestCase):
self.assertEqual(weak_val().numpy().dtype.itemsize, strong.itemsize)
self.assertEqual(weak_val().tolist(), [value])
self.assertEqual(weak_val().cast(strong).realize().uop.buffer.dtype, strong)
for entry in (lambda t: t.contiguous(), lambda t: t.realize(), lambda t: t.clone(),
lambda t: t.to("CPU:1").realize(), lambda t: t.as_param(0)):
self.assertEqual(weak_val().contiguous().dtype, weak) # no layout to fix, stays weak
self.assertEqual(weak_val().realize().dtype, weak) # no width to store, stays weak
self.assertEqual(weak_val().clone().dtype, strong) # storage commits at the default
for entry in (lambda t: t.to("CPU:1").realize(), lambda t: t.as_param(0)):
with self.assertRaises(RuntimeError): entry(weak_val())
def test_weak_is_virtual(self):
# NOTE: int64 lub uint64 is weakfloat, so this is device-ful weak from promotion, never from a cast to weak
devful = Tensor([1], dtype=dtypes.int64, device="CPU") + Tensor([1], dtype=dtypes.uint64, device="CPU")
for t in (Tensor.const(dtypes.weakfloat, 0.5), devful):
self.assertTrue(t.uop.is_virtual)
# realize is a no-op, so a weak input can never become the real buffer TinyJit needs
with self.assertRaises(JitError): TinyJit(lambda x: (x+1).realize())(t)
# callify must not silently commit a weak CONTIGUOUS to storage
c = devful.alu(Ops.CONTIGUOUS)
c.callify()
self.assertIs(c.dtype, dtypes.weakfloat)
def test_empty_reads_commit(self):
for weak, strong in ((dtypes.weakfloat, dtypes.default_float),):
empty = Tensor.const(weak, 0).reshape(1).shrink(((0, 0),))
+3 -3
View File
@@ -535,7 +535,7 @@ class TestFunctionTuple(unittest.TestCase):
@function(precompile=True, precompile_backward=True)
def f(a:Tensor):
c = Tensor(Tensor.invalids(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.invalids(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
np.testing.assert_allclose(f(a).numpy(), 14.0)
@@ -543,7 +543,7 @@ class TestFunctionTuple(unittest.TestCase):
# g is f with empty output instead of invalids
@function(precompile=True, allow_implicit=True)
def g(a:Tensor):
c = Tensor(Tensor.empty(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.multi(0), device=devs)
c = Tensor(Tensor.empty(a.shape[0]//len(devs), a.shape[1], dtype=a.dtype, device=devs).uop.unshard(0), device=devs)
return Tensor.custom_kernel(c, a, fxn=double_kernel, grad_fxn=double_grad)[0]
np.testing.assert_allclose(g(a).numpy(), 14.0)
@@ -608,7 +608,7 @@ class TestFunctionTuple(unittest.TestCase):
@function(precompile=True)
def f(a:Tensor):
c = Tensor.invalids(*a.uop.shard_shape, dtype=a.dtype, device=a.device)
if multi: c = Tensor(c.uop.multi(a.uop.axis), device=a.device)
if multi: c = Tensor(c.uop.unshard(a.uop.axis), device=a.device)
c = Tensor.custom_kernel(c, a, fxn=my_kernel)[0]
return c + 1
+26
View File
@@ -79,6 +79,10 @@ class TestMultiTensor(unittest.TestCase):
with self.assertRaises(RuntimeError):
X.shard_(devices_3, 0)
def test_shard_reshape_cross_boundary(self):
X = Tensor.ones(5, 4).contiguous().realize().shard(devices_2, 1)
with self.assertRaises(RuntimeError): X.reshape(10, 2).uop.axis
def test_tensor_from_multi(self):
X = Tensor([1, 2], dtype=dtypes.int).shard_(devices_2, 0)
Y = Tensor(X.uop)
@@ -123,6 +127,28 @@ class TestMultiTensor(unittest.TestCase):
with Context(RING=use_ring):
np.testing.assert_equal(t.shard(devices_2, axis=axis).sum().item(), 10)
def test_allreduce_cast_half(self, assign=False, kernel_count=8):
devices = tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
a_src = Tensor.arange(2*3, dtype=dtypes.half).reshape(2, 3).clone().realize()
b_src = Tensor.arange(2*3, dtype=dtypes.half).reshape(2, 3).clone().realize()
a = a_src.shard(devices, axis=0).realize()
b = b_src.shard(devices, axis=0).realize()
# assigning creates a copy of the output before allreduce
if assign:
tst = Tensor.empty_like(b)
tst.assign(a + b)
else:
tst = a + b
tst = tst.float().sum(0)
GlobalCounters.reset()
with Context(ALLREDUCE_CAST=1, RING=0, ALL2ALL=0):
tst.realize()
kernel_count = GlobalCounters.kernel_count
np.testing.assert_allclose(tst.numpy(), (a_src.numpy()+b_src.numpy()).sum(0))
self.assertEqual(kernel_count, kernel_count)
def test_allreduce_cast_half_assign(self): self.test_allreduce_cast_half(assign=True, kernel_count=10)
def test_multiple_to_single_device(self):
kernel_counts = {}
for ring in (0, 2):
+1 -1
View File
@@ -30,7 +30,7 @@ class TestRealizeIsRealized(unittest.TestCase):
def test_multi(self):
d = Device.DEFAULT
t = Tensor.ones(8).contiguous().shard((d, d), axis=0).realize()
assert all(u.is_realized for u in t.uop.src)
assert t.uop.src[0].is_realized
def test_empty(self):
t = Tensor.empty(4, 4).realize()
+11 -11
View File
@@ -38,8 +38,8 @@ add_tags = PatternMatcher([
])
def replace_contig_with_store_after(u:UOp):
# can't allocate a buffer without a device (e.g., inside a CALL function body with only PARAMs)
if u.device is None: return None
# can't allocate a buffer for a virtual value
if u.is_virtual: return None
# if size is 0, remove the contig
if 0 in u.shape: return u.src[0]
# no real contig for DISK/TINYFS tensors, they are left alone
@@ -49,7 +49,7 @@ def replace_contig_with_store_after(u:UOp):
def replace_store_after_with_contig(u:UOp, src:UOp):
assigned_to = u
while assigned_to.op in {Ops.BITCAST, Ops.AFTER, Ops.MULTI}: assigned_to = assigned_to.src[0].base
while assigned_to.op in {Ops.BITCAST, Ops.AFTER, Ops.UNSHARD}: assigned_to = assigned_to.src[0].base
if assigned_to.op not in {Ops.BUFFER, Ops.SLICE}: return src.contiguous(tag=u.tag)
def _make_buffer_view(src:UOp) -> UOp|None:
@@ -61,29 +61,29 @@ def _make_buffer_view(src:UOp) -> UOp|None:
buf = buf.src[0]
if byte_offset % buf.dtype.itemsize != 0: return None
offset = byte_offset // buf.dtype.itemsize
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.weakint, offset)), src.numel())
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(None, offset)), src.numel())
def contiguous_mops_to_view(c:UOp, src:UOp):
"""MOPS(BUFFER) → SLICE when movement ops collapse to a contiguous range."""
buf = src.base
if buf.op not in {Ops.BUFFER, Ops.SLICE, Ops.MULTI}: return None
if buf.op not in {Ops.BUFFER, Ops.SLICE, Ops.UNSHARD}: return None
if src.op is Ops.RESHAPE and src.src[0].op in {Ops.BUFFER, Ops.SLICE} and c.op is not Ops.BITCAST: return None
if c.op is not Ops.BITCAST and src.op is Ops.BUFFER: return None
# no symbolic shape
if not all_int(c.shape): return None
if buf.op is not Ops.MULTI and (view := _make_buffer_view(src)) is not None:
if buf.op is not Ops.UNSHARD and (view := _make_buffer_view(src)) is not None:
view = (view.replace(dtype=c.dtype, arg=c.numel()) if c.op is Ops.BITCAST else view).reshape(c.shape)
return c.replace(src=(view,)) if c.op is Ops.COPY else view
# for MULTI tensors, use multi_pm to resolve per-shard movement ops, then create SLICE on the resolved result
# for UNSHARD tensors, use multi_pm to resolve per-shard movement ops, then create SLICE on the resolved result
if not isinstance(c.device, str):
from tinygrad.schedule.multi import multi_pm
resolved = graph_rewrite(src, multi_pm, name="multi_buffer_view")
if resolved.op is not Ops.MULTI: return None
if resolved.op is not Ops.UNSHARD: return None
if (view := _make_buffer_view(resolved.src[0])) is None: return None
return view.reshape(resolved.src[0].shape).multi(resolved.arg).contiguous(tag=c.tag)
return view.reshape(resolved.src[0].shape).unshard(resolved.arg, resolved.src[1]).contiguous(tag=c.tag)
return None
@@ -92,7 +92,7 @@ def _precompiled_output_redirect(s:UOp, t:UOp) -> UOp|None:
# materialize straight into t
if s.op is Ops.CONTIGUOUS: return t.after(t.store(s.src[0]))
# rebind output storage to t
if s.op in {Ops.BUFFER, Ops.MULTI} and s.has_buffer_identity(): return t
if s.op in {Ops.BUFFER, Ops.UNSHARD} and s.has_buffer_identity(): return t
return None
def transform_precompiled_call(c:UOp) -> UOp|None:
@@ -203,7 +203,7 @@ def transform_to_call(big_sink:UOp) -> tuple[UOp, dict[UOp, UOp]]:
if VIZ: graph_rewrite(big_sink, PatternMatcher([]), name="View Tensor Graph")
# uop list is a list in the original_sink graph and we can map to the tags later
# same predicate as Tensor.realize
ctx = AllocCtx(bases={base for x in big_sink.src if (base:=x.base).device is not None and not base.has_buffer_identity()
ctx = AllocCtx(bases={base for x in big_sink.src if not (base:=x.base).is_virtual and not base.has_buffer_identity()
and base.op is not Ops.AFTER and base.addrspace is not AddrSpace.ALU})
# this rewrite is "read-only", it adds simple things to buffer_map and may sink things on big_sink, bottom_up
+35 -6
View File
@@ -1,7 +1,7 @@
from dataclasses import replace, dataclass
import itertools, functools
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
from tinygrad.helpers import ALLOW_TF32, TracingKey, Context, panic
from tinygrad.helpers import ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT, TracingKey, Context, panic
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, pm_lower_index_dtype, Ops, UPat, track_rewrites, KernelInfo, ProgramInfo, GroupOp
from tinygrad.uop.ops import AxisType
from tinygrad.uop.render import pyrender
@@ -125,7 +125,7 @@ def do_devectorize(b:UOp):
if not all(x.shape == b.shape or x.base.arg is Invalid for x in b.src): return None
src = []
for idx in itertools.product(*[range(x) for x in b.shape]):
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
idx_c = [UOp.const(None, i) for i in idx]
src.append(b.replace(src=tuple(x.base if x.base.arg is Invalid else x.index(*idx_c) for x in b.src)))
return UOp.stack(*src).reshape(b.shape) if b.op is not Ops.STORE else UOp.group(*src)
@@ -135,7 +135,7 @@ def do_stack_wmma(u:UOp):
src = []
for b in u.src:
if b.op != Ops.STACK:
src.append(UOp.stack(*[b.index(UOp.const(dtypes.weakint, i)) for i in range(b.max_numel())]))
src.append(UOp.stack(*[b.index(i) for i in range(b.max_numel())]))
else:
src.append(b)
return u.replace(src=tuple(src))
@@ -161,7 +161,7 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
# RESHAPE a void is removed (hack for AFTER)
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
# reshape of a single element shaped value to scalar is an index
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(0) if x.marg == () and x.src[0].shape == (1,) else None),
# EXPAND on scalar -> STACK
(UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
lambda x,out: UOp.stack(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
@@ -242,7 +242,7 @@ pm_add_loads = PatternMatcher([
def add_local_buffer(ctx, x:UOp):
buf = UOp.placeholder(x.max_shape, x.dtype, slot=next(ctx), addrspace=x.arg.addrspace)
return buf.after(buf.index(*x.src[1:]).store(x.src[0]).end(*x.src[1:]).barrier())
return buf.after(buf.index(*x.src[1:]).store(x.src[0]).end(*x.src[1:]))
pm_add_local_buffers = PatternMatcher([
(UPat(Ops.STAGE, name="x"), add_local_buffer),
@@ -255,6 +255,32 @@ pm_cast_float_alu = PatternMatcher([
lambda u,x: u.replace(src=(x.cast(u.dtype),)) if x.dtype != u.dtype else None),
])
def _is_local_store(x:UOp): return x.op is Ops.STORE and x.addrspace is AddrSpace.LOCAL
def add_raw_barrier(after:UOp):
# loads from a LOCAL buffer that depend (via AFTER) on stores to LOCAL memory need a workgroup barrier
if after.addrspace is not AddrSpace.LOCAL: return None
# one toposort over all the deps
deps = UOp.sink(*after.src[1:]).backward_slice
if not any(_is_local_store(x) for x in deps) or any(x.op is Ops.BARRIER for x in deps): return None
return after.src[0].after(UOp(Ops.BARRIER, src=after.src[1:]))
def add_war_barrier(end:UOp):
# a LOCAL buffer stored and loaded in the same loop needs a barrier at the end of the loop body
rngs = [r for r in end.src[1:] if r.op is Ops.RANGE and r.arg[1] in (AxisType.REDUCE, AxisType.LOOP) and r.vmax > 0]
if not rngs or end.src[0].op is Ops.BARRIER: return None
sl = end.src[0].backward_slice_with_self
# only stores that are inside this loop body (not in the backward slice through AFTER chains from other loops)
store_bufs = {x.buf_uop for x in sl if _is_local_store(x) and any(r in x.ranges for r in rngs)}
# a load whose buffer matches a local store's buffer is necessarily a local load
if not (loads:=[x for x in sl if x.op is Ops.LOAD and x.src[0].buf_uop in store_bufs]): return None
return end.replace(src=(UOp(Ops.BARRIER, src=(end.src[0], *loads)),)+end.src[1:])
pm_implicit_barriers = PatternMatcher([
(UPat(Ops.AFTER, name="after"), add_raw_barrier),
(UPat(Ops.END, name="end"), add_war_barrier),
])
def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
if VIZ: graph_rewrite(ast, PatternMatcher([]), name="View Base AST")
if DEBUG >= 5: print(pyrender(ast))
@@ -344,6 +370,9 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
pm_final_rewrite = pm_decomp+extra_matcher+pm_split_ends
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
# add implicit barriers (stores/loads through LOCAL memory ordered by AFTER or across loop iterations need workgroup barriers)
sink = graph_rewrite(sink, pm_implicit_barriers, name="add implicit barriers")
# this was the linearizer
sink = graph_rewrite(sink, pm_add_control_flow, ctx=CFGContext(sink), name="add control flow", bottom_up=True)
@@ -450,7 +479,7 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
to_program_cache: dict[tuple, UOp] = {}
def to_program(ast:UOp, renderer:Renderer) -> UOp:
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32)
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32, DEFAULT_FLOAT, DEFAULT_INT)
key = (ast.key, type(renderer), renderer.target, *[x.value for x in config])
if (prg:=to_program_cache.get(key)) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
return prg
+38 -22
View File
@@ -20,13 +20,17 @@ def reindex(idx:UOp, off:int, mul=2) -> UOp:
def l2i(op: Ops, dt: DType, *uops:UOp):
zero = UOp.const(dt, 0)
if len(uops) == 2: a0, a1 = uops
elif len(uops) == 3: a0, a1, b0 = uops # a shift's count is a single word
elif len(uops) == 4: a0, a1, b0, b1 = uops
match op:
case Ops.NEG: return l2i(Ops.SUB, dt, zero, zero, *uops)
case Ops.CAST if dt in (dtypes.long, dtypes.ulong) and uops[0].dtype not in dtypes.floats:
return uops[0].cast(l2i_dt[dt]), (uops[0] < 0).where(UOp.const(l2i_dt[dt], -1), UOp.const(l2i_dt[dt], 0))
# the high word is the sign extension; bool has no sign, test the already-cast low word instead (bool < 0 would promote to weakint)
x, lo = uops[0], uops[0].cast(l2i_dt[dt])
sign = lo if x.dtype is dtypes.bool else x
return lo, (sign < sign.const_like(0)).where(lo.const_like(-1), lo.const_like(0))
case Ops.CAST if dt in (dtypes.long, dtypes.ulong):
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0)).cast(l2i_dt[dt])
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0))
case Ops.CAST if dt in dtypes.floats:
small = (a1.eq(0) & (a0 >= 0)) | (a1.eq(-1) & (a0 < 0))
return small.where(a0.cast(dt), ((a1.cast(dtypes.float32) * (2**32)) + a0.bitcast(dtypes.uint).cast(dtypes.float32)).cast(dt))
@@ -41,8 +45,8 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
lo, hi = ((a0u >> n) | ((a1u << 1) << (31 - n))).bitcast(dt), a1 >> (b0 & 31)
fill = a1 >> 31 if dt == dtypes.int else zero # vacated high word: sign bits when signed, else 0
return (b0 >= 32).where(hi, lo), (b0 >= 32).where(fill, hi)
case Ops.ADD: return (low:=a0+b0), (a1 + b1).replace(dtype=dt) + (low.bitcast(dtypes.uint) < a0.bitcast(dtypes.uint)).cast(dt)
case Ops.SUB: return a0 - b0, a1 - b1 - (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint)).cast(dt)
case Ops.ADD: return (low:=a0+b0), a1 + b1 + (low.bitcast(dtypes.uint) < a0.bitcast(dtypes.uint))
case Ops.SUB: return a0 - b0, a1 - b1 - (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint))
case Ops.MUL:
(a00, a01), (b00, b01) = unpack32(a0), unpack32(b0)
mid = l2i(Ops.ADD, dt, shl(a00*b01, 16).bitcast(dt), shr(a00*b01, 16).bitcast(dt), shl(a01*b00, 16).bitcast(dt), shr(a01*b00, 16).bitcast(dt))
@@ -65,19 +69,23 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
(nq0, nq1), (nr0, nr1) = l2i(Ops.BITCAST, dt, *l2i(Ops.NEG, dtypes.uint, *q)), l2i(Ops.BITCAST, dt, *l2i(Ops.NEG, dtypes.uint, *r))
(q0, q1), (r0, r1) = l2i(Ops.BITCAST, dt, *q), l2i(Ops.BITCAST, dt, *r)
return (a_neg.where(nr0, r0), a_neg.where(nr1, r1)) if op == Ops.CMOD else ((a_neg^b_neg).where(nq0, q0), (a_neg^b_neg).where(nq1, q1))
return (r[0].bitcast(dt), r[1].bitcast(dt)) if op == Ops.CMOD else (q[0].bitcast(dt), q[1].bitcast(dt))
return r if op == Ops.CMOD else q
case Ops.CMPLT: return (a1 < b1) | ((a1.eq(b1)) & (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint)))
case Ops.CMPEQ: return a0.eq(b0) & a1.eq(b1)
case Ops.CMPNE: return a0.ne(b0) | a1.ne(b1)
case Ops.XOR | Ops.OR | Ops.AND: return UOp(op, dt, src=(a0, b0)), UOp(op, dt, src=(a1, b1))
case Ops.XOR | Ops.OR | Ops.AND: return UOp(op, src=(a0, b0)), UOp(op, src=(a1, b1))
case Ops.WHERE: return uops[0].where(uops[1], uops[3]), uops[0].where(uops[2], uops[4])
case Ops.MAX: return l2i(Ops.WHERE, dt, l2i(Ops.CMPLT, dt, *uops), b0, b1, a0, a1)
case _: raise NotImplementedError(f"long decomposition of {op} unsupported")
def split_l2i(op: Ops, dt: DType, *uops:UOp):
# l2i does arithmetic on its inputs; rules enter here to split them to 32-bit words first, l2i recurses on itself
return l2i(op, dt, *graph_rewrite(UOp.sink(*uops), pm_long_decomp, bottom_up=True).src)
# ***** floats *****
f2f_dt = { f:getattr(dtypes, f"uint{f.bitsize}") for f in dtypes.floats }
def rne(v: UOp, s) -> UOp: return shr(v, s) + ((shr(v, s - 1) & 1) & ((v & ((1 << (s - 1)) - 1)).ne(0).cast(v.dtype) | (shr(v, s) & 1)))
def rne(v: UOp, s) -> UOp: return shr(v, s) + ((shr(v, s - 1) & 1) & ((v & ((1 << (s - 1)) - 1)).ne(0) | (shr(v, s) & 1)))
def f2f(v, fr:DType, to:DType, sat=True):
fs, fb, (fe, fm), ts, tb, (te, tm) = fr.bitsize, exponent_bias(fr), dtypes.finfo(fr), to.bitsize, exponent_bias(to), dtypes.finfo(to)
@@ -122,27 +130,35 @@ def f2f_store(st, idx, val, fr:DType, to:DType):
if (n:=val.max_numel()) == 1: return st.replace(src=(idx, f2f(val.bitcast(f2f_dt[to]), to, fr)))
return UOp.group(*(st.replace(src=(reindex(idx, i, 1), f2f(val.index(i).bitcast(f2f_dt[to]), to, fr))) for i in range(n)))
# tag is the 32-bit word this node becomes - (0 for the low word, 1 for the high, the dtype the consumer wants)
pm_long_decomp = PatternMatcher([
(UPat(GroupOp.Defines, src=(UPat.var("sz"),), name="x"), lambda x,sz:
x.replace(dtype=l2i_dt[x.dtype], arg=replace(x.arg, dtype=l2i_dt[x.dtype]), src=(sz*2,)) if x.dtype in l2i_dt else None),
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x: reindex(x, x.tag).replace(dtype=l2i_dt[x.dtype]) if x.tag is not None else None),
(UPat(Ops.STORE, src=(UPat.var('idx'), UPat.var('val', tuple(l2i_dt.keys()))), name='st'), lambda st,idx,val:
st.replace(src=(idx.rtag(0), val.rtag(0))).group(st.replace(src=(idx.rtag(1), val.rtag(1)))) if val.tag is None else None),
(UPat(GroupOp.Comparison, src=(UPat.var('a', tuple(l2i_dt.keys())), UPat.var('b', tuple(l2i_dt.keys()))), name="x"), lambda a,b,x:
l2i(x.op, dt:=l2i_dt[a.dtype], a.rtag(0).cast(dt), a.rtag(1).cast(dt), b.rtag(0).cast(dt), b.rtag(1).cast(dt))),
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a'),), name="x"), lambda a,x:
l2i(x.op, x.dtype, a)[x.tag] if x.tag is not None and a.dtype not in l2i_dt else None),
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
reindex(x, x.tag[0]).replace(dtype=x.tag[1], tag=None) if x.tag is not None else None),
(UPat(Ops.STORE, src=(UPat.var('idx', tuple(l2i_dt.keys())), UPat.var('val')), name='st'), lambda st,idx,val:
st.replace(src=(idx.rtag((0, dt:=l2i_dt[idx.dtype])), val.rtag((0, dt)))).group(
st.replace(src=(idx.rtag((1, dt)), val.rtag((1, dt))))) if val.tag is None else None),
(UPat(GroupOp.Comparison, src=[UPat.var('a', tuple(l2i_dt.keys())), UPat()], name="x"), lambda a,x:
split_l2i(x.op, dt:=l2i_dt[a.dtype], *flatten((s.rtag((0, dt)), s.rtag((1, dt))) for s in x.src))),
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda a,x:
(a.rtag(0).cast(dt:=l2i_dt[a.dtype]).bitcast(xdt:=l2i_dt[x.dtype]), a.rtag(1).cast(dt).bitcast(xdt))[x.tag]),
split_l2i(Ops.BITCAST, l2i_dt[x.dtype], a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt)))[x.tag[0]]),
(UPat(Ops.CAST, tuple(l2i_dt.keys()), src=(UPat.var('a'),), name="x"), lambda a,x:
split_l2i(x.op, x.dtype, a)[x.tag[0]] if x.tag is not None else None),
(UPat(Ops.CAST, src=(UPat.var('a', tuple(l2i_dt.keys())),), name="x"), lambda a,x:
l2i(x.op, x.dtype, a.rtag(0).cast(dt:=l2i_dt[a.dtype]), a.rtag(1).cast(dt)) if x.dtype not in l2i_dt and a.tag is None else None),
(UPat((*(GroupOp.ALU - GroupOp.Comparison), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda x:
l2i(x.op, l2i_dt[x.dtype], *flatten((a.rtag(0).cast(dt:=l2i_dt[x.src[-1].dtype]), a.rtag(1).cast(dt))
if a.dtype in l2i_dt else (a,) for a in x.src))[x.tag] if x.tag is not None else None),
split_l2i(x.op, x.dtype, a.rtag((0, dt:=l2i_dt[a.dtype])), a.rtag((1, dt))) if x.dtype not in l2i_dt and a.tag is None else None),
(UPat((Ops.SHL, Ops.SHR), tuple(l2i_dt.keys()), src=(UPat.var('a'), UPat.var('b')), name="x"), lambda a,b,x:
split_l2i(x.op, dt:=l2i_dt[x.dtype], a.rtag((0, dt)), a.rtag((1, dt)), b.rtag((0, dt)))[x.tag[0]] if x.tag is not None else None),
(UPat(Ops.WHERE, tuple(l2i_dt.keys()), src=(UPat.var('c'), UPat.var('a'), UPat.var('b')), name="x"), lambda a,b,c,x:
split_l2i(x.op, dt:=l2i_dt[x.dtype], c, a.rtag((0, dt)), a.rtag((1, dt)), b.rtag((0, dt)), b.rtag((1, dt)))[x.tag[0]]
if x.tag is not None else None),
(UPat((*(GroupOp.ALU - GroupOp.Comparison - {Ops.SHL, Ops.SHR, Ops.WHERE}), Ops.BITCAST), tuple(l2i_dt.keys()), name="x"), lambda x:
split_l2i(x.op, l2i_dt[x.dtype], *flatten((a.rtag((0, l2i_dt[x.dtype])), a.rtag((1, l2i_dt[x.dtype]))) for a in x.src))[x.tag[0]]
if x.tag is not None else None),
(UPat(Ops.LOAD, tuple(l2i_dt.keys()), src=(UPat.var('idx'),), name='x'), lambda x,idx:
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag).replace(dtype=l2i_dt[x.dtype]),)) if x.tag is not None else None),
(UPat(Ops.CONST, tuple(l2i_dt.keys()), name='x'), lambda x:
UOp.const(dt:=l2i_dt[x.dtype], truncate[dt]((x.arg >> 32) if x.tag == 1 else (x.arg & 0xFFFFFFFF))))
x.replace(dtype=l2i_dt[x.dtype], src=(reindex(idx, x.tag[0]).replace(dtype=l2i_dt[x.dtype], tag=None),), tag=None) if x.tag is not None else None),
(UPat(Ops.CONST, tag={(w, dt) for w in (0, 1) for dt in l2i_dt.values()}, name='x'), lambda x:
UOp.const(x.tag[1], truncate[x.tag[1]]((x.arg >> 32) if x.tag[0] == 1 else (x.arg & 0xFFFFFFFF))))
])
# float decomposition patterns - ctx is (fr, to) tuple
+4 -4
View File
@@ -63,7 +63,7 @@ def threefry2x32(x: UOp, key: UOp):
def floordiv_to_idiv(a:UOp, b:UOp) -> UOp:
if (a.vmin >= 0 and b.vmin > 0) or (a.vmax <= 0 and b.vmax < 0): return a.alu(Ops.CDIV, b)
return a.alu(Ops.CDIV, b) - (a.alu(Ops.CMOD, b).ne(0) & (a<0).ne(b<0)).cast(a.dtype)
return a.alu(Ops.CDIV, b) - (a.alu(Ops.CMOD, b).ne(0) & (a<0).ne(b<0))
def floormod_to_mod(a:UOp, b:UOp) -> UOp:
if (a.vmin >= 0 and b.vmin > 0) or (a.vmax <= 0 and b.vmax < 0): return a.alu(Ops.CMOD, b)
@@ -114,8 +114,8 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
if Ops.CMPLT in ops:
# These are late rewrites because simplex expects equalities to be a certain format
pat += [
((UPat.var("x", dtypes.sints) < UPat.cvar("c", dtypes.sints)).logical_not(), lambda x,c: c-1<x),
((UPat.cvar("c", dtypes.sints) < UPat.var("x", dtypes.sints)).logical_not(), lambda x,c: x<c+1),
((UPat.var("x", dtypes.sints) < UPat.cvar("c")).logical_not(), lambda x,c: c-1<x),
((UPat.cvar("c") < UPat.var("x", dtypes.sints)).logical_not(), lambda x,c: x<c+1),
(UPat.var("x", dtypes.sints)*-1 < UPat.var("y", dtypes.sints)*UPat.cvar("c"), lambda x,y,c: y*(-c)<x),
(UPat.var("x", dtypes.sints)*-1 < UPat.cvar("c"), lambda x,c:-c<x),
((UPat.cvar("c1")<UPat.var("x", dtypes.sints)) & (UPat.var("x", dtypes.sints)<UPat.cvar("c2")),
@@ -129,5 +129,5 @@ def get_late_rewrite_patterns(ops:tuple[Ops, ...], disable_fast_idiv:bool) -> Pa
# some backends emit FDIV for RECIP, in that case: a*(1/b) -> a/b
if Ops.FDIV in ops:
pat += [(UPat.var("x").reciprocal(), lambda x: x.const_like(1).alu(Ops.FDIV, x))]
pat += [(UPat.var("a", dtypes.floats) * UPat.const(dtypes.floats, 1).alu(Ops.FDIV, UPat.var("b")), lambda a,b: a.alu(Ops.FDIV, b))]
pat += [(UPat.var("a", dtypes.floats) * UPat(Ops.FDIV, dtypes.floats, src=(UPat.const(None, 1), UPat.var("b"))), lambda a,b: a.alu(Ops.FDIV, b))]
return PatternMatcher(pat)
+1 -1
View File
@@ -200,7 +200,7 @@ def xexp2(d:UOp) -> UOp:
x = _lazy_map_numbers(d, d.const_like(0.0), d.const_like(0.0), d.const_like(0.0), d)
q = rintk(x)
# s = d - round(d)
s = x - q.cast(x.dtype)
s = x - q
# a polynomial approximation with 13 non-zero terms in the range of [(log 2)/2,(log 2)/2].
if d.dtype == dtypes.float64:
u = polyN(s, [0.4434359082926529454e-9, 0.7073164598085707425e-8, 0.1017819260921760451e-6, 0.1321543872511327615e-5, 0.1525273353517584730e-4,
+9 -1
View File
@@ -87,7 +87,15 @@ def add_gpudims(ctx:Renderer, s:UOp):
except ValueError: continue
return s.substitute(subs)
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),
# 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),
])
pm_add_gpudims = PatternMatcher([
# add gpudims must be last
(UPat(Ops.SINK, name="s"), add_gpudims),
])
])+pm_device_to_var
+3 -3
View File
@@ -141,12 +141,12 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
grouped_offsets = [[x for _,x in group] for _,group in itertools.groupby(enumerate(sorted(offsets.keys())), lambda x: x[1]-x[0])]
for full_grp in grouped_offsets:
while len(full_grp):
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(dtypes.weakint, full_grp[0])
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(None, full_grp[0])
length = [l for l in lengths if l <= len(full_grp) and (not must_divide or offset.divides(l) is not None)][0]
grp = full_grp[:length]
# NOTE: we apply the valid again after we determine the length
offset = offset.valid(valid) if valid is not None else offset
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(dtypes.weakint, len(grp)))) if len(grp) > 1 else buf.index(offset)
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(None, len(grp)))) if len(grp) > 1 else buf.index(offset)
if op == Ops.STORE:
datas = []
for i,g in enumerate(grp):
@@ -158,7 +158,7 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
ld = idx.load()
for i,g in enumerate(grp):
for oo in offsets[g]:
replacements[oo] = ld.index(UOp.const(dtypes.weakint, i)) if len(grp) > 1 else ld
replacements[oo] = ld.index(i) if len(grp) > 1 else ld
full_grp = full_grp[length:]
# apply
+5 -5
View File
@@ -21,9 +21,9 @@ class Scheduler:
@property
def rngs(self):
# always in order by axistype. void RANGEs are loops, not opt axes
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.dtype is not dtypes.void and u.vmax > 0],
key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
# always in order by axistype. void RANGEs are loops, not opt axes. the DEVICE axis is launched, not an opt axis
return sorted([u for u in self.ast.backward_slice if u.op is Ops.RANGE and u.dtype is not dtypes.void and u.vmax > 0
and u.arg[-1] is not AxisType.DEVICE], key=lambda x: (axis_to_pos[x.arg[-1]],) + x.arg[0:-1])
@property
def shape_len(self) -> int: return len(self.rngs)
@property
@@ -95,7 +95,7 @@ class Scheduler:
def shift_to(self, rng:UOp, amount:int, new_type:AxisType, top:bool=False, input_new_rng:UOp|None=None):
if (old_sz:=rng.src[0].divides(amount)) is None:
raise KernelOptError(f"{amount} can't divide {rng.src[0]} in {self.colored_shape()}")
new_rng = UOp.range(amount, next(self.opt_range), new_type) if input_new_rng is None else input_new_rng
new_rng = UOp.range(amount, next(self.opt_range), new_type, dtype=rng.dtype) if input_new_rng is None else input_new_rng
replaced_rng = rng.replace(src=(old_sz,))
sub_axis = (new_rng * old_sz + replaced_rng) if top else (replaced_rng * amount + new_rng)
self.ast = self.ast.substitute({rng:sub_axis}, name=f"shift {rng.arg[:-1]} {amount} {str(new_type).split('.')[1].lower()}")
@@ -191,7 +191,7 @@ class Scheduler:
check(rng.arg[-1] is not AxisType.THREAD, "cannot pad thread")
new_sz = round_up(int(rng.vmax+1), cast(int, opt.arg))
check(rng.vmax+1 > new_sz//4, "pad adds more than quadruple the work")
replaced_rng = UOp.range(new_sz, *rng.arg)
replaced_rng = UOp.range(new_sz, *rng.arg, dtype=rng.dtype)
replaces = {rng:replaced_rng}
valid = replaced_rng < rng.vmax+1
store_targets = {s.src[0] for s in self.ast.backward_slice_with_self if s.op is Ops.STORE}
+11 -8
View File
@@ -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
from tinygrad.uop.symbolic import symbolic, invalid_gate
from tinygrad.helpers import partition
from tinygrad.dtype import dtypes
@@ -60,7 +60,9 @@ pm_simplify_ranges = PatternMatcher([
])
def mark_range_mod(ctx:dict[UOp, UOp|None], r:UOp, c:UOp) -> None:
if r not in ctx and r.arg[-1] is not AxisType.WARP and r.src[0].op is Ops.CONST and r.src[0].divides(c.arg) is not None: ctx[r] = c
# ranges that aren't looped over can't be split
if r not in ctx and r.arg[-1] not in {AxisType.WARP, AxisType.DEVICE} \
and r.src[0].op is Ops.CONST and r.src[0].divides(c.arg) is not None: ctx[r] = c
def do_substitute(ctx:dict, x: UOp, sub_fxn:Callable[[UOp, UOp], UOp]) -> UOp|None:
ret = x.substitute({k:sub_fxn(k,v) for k,v in ctx.items() if v is not None})
@@ -84,9 +86,9 @@ def reduce_unparented(red:UOp) -> UOp|None:
if len(reduce_unparented) == 0: return None
ret = red.replace(src=(red.src[0],)+tuple(reduce_parented)) if len(reduce_parented) or red.dtype != red.src[0].dtype else red.src[0]
if red.arg[0] is Ops.ADD:
for r in reduce_unparented: ret = ret * r.src[0].cast(ret.dtype)
for r in reduce_unparented: ret = ret * r.src[0]
if red.arg[0] is Ops.MUL:
for r in reduce_unparented: ret = ret ** r.src[0].cast(ret.dtype)
for r in reduce_unparented: ret = ret ** r.src[0]
return ret
pm_reduce_unparented = PatternMatcher([
@@ -96,7 +98,7 @@ pm_reduce_unparented = PatternMatcher([
pm_reduce_collapse = pm_reduce_unparented + PatternMatcher([
# lift x+y out of reduce on lt
((UPat.var("x")+UPat.var("y")).or_casted() < UPat.var("c"), lambda x,y,c: (x < (c.cast(y.dtype)-y)) if no_range(y) and no_range(c) else None),
((UPat.var("x")+UPat.var("y")).or_casted() < UPat.var("c"), lambda x,y,c: (x < (c-y)) if no_range(y) and no_range(c) else None),
# lift x*y out of reduce
((UPat.var("x")*UPat.var("y")) < UPat.var("c"),
lambda x,y,c: (x < ((c+y-1) // y)) if no_range(y) and no_range(c) and dtypes.is_int(y.dtype) and y.vmin > 0 else None),
@@ -107,13 +109,14 @@ pm_reduce_collapse = pm_reduce_unparented + PatternMatcher([
((UPat.var("r")<UPat.var("lower")).logical_not()&(UPat(Ops.RANGE, name="r")<UPat.var("upper"))).where(UPat.var("val"), 0),
).reduce(UPat.var("r"), arg=Ops.ADD), lambda r,val,lower=None,upper=None:
((upper.minimum(r.src[0]) if upper is not None else r.src[0]) -
(lower.maximum(0) if lower is not None else r.const_like(0))).maximum(0).minimum(r.src[0]).cast(val.dtype) * val if no_range(val) else None),
# REDUCE on ADD
(lower.maximum(0) if lower is not None else r.const_like(0))).maximum(0).minimum(r.src[0]) * val if no_range(val) else None),
(invalid_gate.reduce(arg=Ops.ADD, allow_any_len=True, name="r"),
lambda cond,x,i,r: cond.where(x.reduce(*r.src[1:], arg=Ops.ADD), i) if no_range(cond) else None),
((UPat.var("x")+UPat.var("y")).reduce(arg=Ops.ADD, allow_any_len=True, name="r"),
lambda x,y,r: x.reduce(*r.src[1:], arg=Ops.ADD) + y.reduce(*r.src[1:],arg=Ops.ADD)),
# AND on WHERE
((UPat(Ops.PARAM, name="x") & UPat.var("y")).where(UPat.var("c"), 0).reduce(arg=Ops.ADD, allow_any_len=True, name="r"),
lambda x,y,c,r: y.where(c, 0).reduce(*r.src[1:], arg=Ops.ADD)*x.cast(c.dtype)),
lambda x,y,c,r: y.where(c, 0).reduce(*r.src[1:], arg=Ops.ADD)*x),
# MUL casted bool
((UPat.var("x") * UPat.var("gate", dtype=dtypes.bool).cast()), lambda x,gate: gate.where(x, 0)),
])+symbolic
+31
View File
@@ -268,6 +268,34 @@ class LRUAllocator(Allocator, Generic[DeviceType]):
if LRU and (options is None or (not options.nolru and options.external_ptr is None)): self.cache[(size, options)].append(opaque)
else: super().free(opaque, size, options)
class DepsTracker:
def __init__(self):
# tracks (offset, end, dep) ranges per base buffer id to handle suballocated buffers correctly.
self.w_dependency_map: dict[int, list[tuple[int, int, Any]]] = defaultdict(list)
self.r_dependency_map: dict[int, list[tuple[int, int, Any]]] = defaultdict(list)
@staticmethod
def _key(buf:Any) -> tuple[Any, int, int]: return id(buf.base), buf.offset, buf.offset + buf.nbytes
def access_resources(self, bufs:list[Any], write:list[int], new_dependency:Any):
wait_nodes = []
for i,buf in enumerate(bufs):
key, s, e = self._key(buf)
wait_nodes += [dep for st,en,dep in self.w_dependency_map[key] if st < e and s < en]
if i in write: wait_nodes += [dep for st,en,dep in self.r_dependency_map[key] if st < e and s < en]
for i,buf in enumerate(bufs):
key, s, e = self._key(buf)
if i in write:
for dmap in [self.w_dependency_map, self.r_dependency_map]:
kept = []
for st,en,dep in dmap[key]:
if st < min(s, en): kept.append((st, min(s, en), dep))
if max(e, st) < en: kept.append((max(e, st), en, dep))
dmap[key] = kept
self.w_dependency_map[key].append((s, e, new_dependency))
else: self.r_dependency_map[key].append((s, e, new_dependency))
return list({id(x):x for x in wait_nodes}.values())
# **************** for Compiled Devices ****************
class CompileError(Exception): pass
@@ -299,6 +327,9 @@ class Program(Generic[DeviceType]):
class Compiled:
profile_events:list[ProfileEvent] = [ProfileDeviceEvent("CPU")] # NOTE: CPU is the default device.
pm_lower:Any = None
pm_bufferize:Any = None
def __init__(self, device:str, allocator:Allocator, renderers:list[type[Renderer]], runtime:type[Program[Self]]|None, graph=None, arch=None):
from tinygrad.renderer import Renderer
self.device, self.allocator, self.runtime_t, self.graph, self.renderers = device, allocator, runtime, graph, renderers or [Renderer]
+16 -13
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Final, ClassVar, Callable, Literal
import math, struct, ctypes, functools
from dataclasses import dataclass, fields
from tinygrad.helpers import getenv
from tinygrad.helpers import getenv, DEFAULT_FLOAT, DEFAULT_INT
from enum import IntEnum, auto
class ConstFloat(float):
@@ -83,7 +83,7 @@ class DType(metaclass=DTypeMetaClass):
return ConstFloat(float(val)) if dtypes.is_float(self) else bool(val) if dtypes.is_bool(self) else int(val)
class dtypes:
class DTypes:
@staticmethod
@functools.cache
def is_float(x: DType) -> bool: return x in (dtypes.floats + (dtypes.weakfloat,))
@@ -99,10 +99,10 @@ class dtypes:
def from_py(x) -> DType:
# NOTE: isinstance(True, int) is True, so bool must be checked before int
if isinstance(x, (bool, InvalidType)): return dtypes.bool
if isinstance(x, float): return dtypes.default_float
if isinstance(x, int): return dtypes.default_int
if isinstance(x, float): return dtypes.weakfloat
if isinstance(x, int): return dtypes.weakint
# put this in the last is faster because there are more items than lists/tuples to check
if isinstance(x, (list, tuple)): return max(dtypes.from_py(xi) for xi in x) if x else dtypes.default_float
if isinstance(x, (list, tuple)): return strong_dtype(max(dtypes.from_py(xi) for xi in x)) if x else dtypes.default_float
raise RuntimeError(f"Could not infer dtype of {x} with type {type(x)}")
@staticmethod
def finfo(dtype:DType) -> tuple[int, int]:
@@ -138,8 +138,10 @@ class dtypes:
uchar = uint8; ushort = uint16; uint = uint32; ulong = uint64 # noqa: E702
char = int8; short = int16; int = int32; long = int64 # noqa: E702
default_float: ClassVar[DType] = float32
default_int: ClassVar[DType] = int32
@property
def default_float(self) -> DType: return to_dtype(DEFAULT_FLOAT.value)
@property
def default_int(self) -> DType: return to_dtype(DEFAULT_INT.value)
fp8_ocp = (fp8e4m3, fp8e5m2)
fp8_fnuz = (fp8e4m3fnuz, fp8e5m2fnuz)
@@ -155,12 +157,12 @@ class dtypes:
weaks = (weakint, weakfloat)
all = floats + ints + (bool,) # noqa: A003
if (env_default_float := getenv("DEFAULT_FLOAT", "")):
dtypes.default_float = getattr(dtypes, env_default_float.lower())
assert dtypes.is_float(dtypes.default_float), f"{env_default_float} is not a float dtype"
dtypes = DTypes()
DTypeLike = str|DType
def to_dtype(dtype:DTypeLike) -> DType: return dtype if isinstance(dtype, DType) else getattr(dtypes, dtype.lower())
assert dtypes.is_float(dtypes.default_float), f"{DEFAULT_FLOAT.value} is not a float dtype"
assert dtypes.is_int(dtypes.default_int), f"{DEFAULT_INT.value} is not an int dtype"
def strong_dtype(dtype:DType) -> DType:
return {dtypes.weakint: dtypes.default_int, dtypes.weakfloat: dtypes.default_float}.get(dtype, dtype)
@@ -181,9 +183,10 @@ def _get_recursive_parents(dtype:DType) -> set[DType]:
@functools.cache
def least_upper_dtype(*ds:DType) -> DType:
return min(set.intersection(*[_get_recursive_parents(d) for d in ds]))
def least_upper_float(dt:DType) -> DType: return dt if dtypes.is_float(dt) else least_upper_dtype(dt, dtypes.default_float)
def least_upper_float(dt:DType) -> DType:
return dtypes.weakfloat if dt is dtypes.weakint else dt if dtypes.is_float(dt) else least_upper_dtype(dt, dtypes.default_float)
DTYPES_DICT = {k: v for k, v in dtypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "_"))}
DTYPES_DICT = {k: v for k, v in DTypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "_"))}
INVERSE_DTYPES_DICT = {**{v.name:k for k,v in DTYPES_DICT.items()}, "void": "void", "weakint":"weakint", "weakfloat":"weakfloat"}
@functools.cache
@@ -207,7 +210,6 @@ def can_lossless_cast(dt0:DType, dt1:DType) -> bool:
def sum_acc_dtype(dt:DType):
# default acc dtype for sum
if dt in dtypes.weaks: return dt
if dtypes.is_unsigned(dt): return least_upper_dtype(dt, dtypes.uint)
if dtypes.is_int(dt) or dt == dtypes.bool: return least_upper_dtype(dt, dtypes.int)
return least_upper_dtype(dt, to_dtype(getenv("SUM_DTYPE", "float32")))
@@ -302,6 +304,7 @@ def _from_np_dtype(npdtype:'np.dtype') -> DType: # type: ignore [name-defined] #
@functools.cache
def _to_torch_dtype(dtype:DType) -> 'torch.dtype'|None: # type: ignore [name-defined] # noqa: F821
import numpy as np, torch
dtype = strong_dtype(dtype)
if dtype == dtypes.uint64: return torch.uint64
if dtype == dtypes.bfloat16: return torch.bfloat16
if dtype in dtypes.fp8s: return torch.uint8
+4 -32
View File
@@ -1,8 +1,8 @@
from typing import TypeVar, Generic, Callable, Any
import functools, collections
import functools
from tinygrad.tensor import Tensor, all_tensors
from tinygrad.helpers import flatten, merge_dicts, DEBUG, Context, BEAM, getenv, JIT, JIT_BATCH_SIZE, dedup, pluralize, VIZ, disable_gc
from tinygrad.device import Buffer, Compiled, Device, MultiBuffer
from tinygrad.device import Buffer, Compiled, Device, MultiBuffer, DepsTracker
from tinygrad.dtype import DType
from tinygrad.uop.ops import UOp, PatternMatcher, Variable, sym_infer, Ops, buffers, track_rewrites, graph_rewrite
from tinygrad.renderer import Estimates
@@ -88,34 +88,6 @@ def _check_no_non_tensor_return(ret):
def graph_class(dev): return dev.graph.func if isinstance(dev.graph, functools.partial) else dev.graph
class DepsTracker:
def __init__(self):
# tracks (offset, end, dep) ranges per base buffer id to handle suballocated buffers correctly.
self.w_dependency_map: dict[int, list[tuple[int, int, Any]]] = collections.defaultdict(list)
self.r_dependency_map: dict[int, list[tuple[int, int, Any]]] = collections.defaultdict(list)
@staticmethod
def _key(buf:Any) -> tuple[Any, int, int]: return id(buf.base), buf.offset, buf.offset + buf.nbytes
def access_resources(self, bufs:list[Any], write:list[int], new_dependency:Any):
wait_nodes = []
for i,buf in enumerate(bufs):
key, s, e = self._key(buf)
wait_nodes += [dep for st,en,dep in self.w_dependency_map[key] if st < e and s < en]
if i in write: wait_nodes += [dep for st,en,dep in self.r_dependency_map[key] if st < e and s < en]
for i,buf in enumerate(bufs):
key, s, e = self._key(buf)
if i in write:
for dmap in [self.w_dependency_map, self.r_dependency_map]:
kept = []
for st,en,dep in dmap[key]:
if st < min(s, en): kept.append((st, min(s, en), dep))
if max(e, st) < en: kept.append((max(e, st), en, dep))
dmap[key] = kept
self.w_dependency_map[key].append((s, e, new_dependency))
else: self.r_dependency_map[key].append((s, e, new_dependency))
return list({id(x):x for x in wait_nodes}.values())
class GraphRunner:
def __init__(self, linear:UOp, input_uops:tuple[UOp, ...]=()):
self.linear = linear.src[0]
@@ -234,8 +206,8 @@ def _prepare_jit_inputs(args, kwargs):
for x in args + tuple(kwargs.values()):
it = x if isinstance(x, (tuple,list)) else x.values() if isinstance(x, dict) else []
tensors += [t for t in it if t.__class__ is Tensor and not any(t is y for y in tensors)]
def get_input_uops() -> list[UOp]: return flatten([t.uop.src if t.uop.op is Ops.MULTI else [t.uop] for t in tensors])
if any(u.device is None for u in get_input_uops()): raise JitError("JIT inputs must be real buffers; use .clone()")
def get_input_uops() -> list[UOp]: return flatten([[t.uop.src[0]] if t.uop.op is Ops.UNSHARD else [t.uop] for t in tensors])
if any(u.is_virtual for u in get_input_uops()): raise JitError("JIT inputs must be real buffers; use .clone()")
if len(unrealized_tensors := [x for x in tensors if not x.uop.is_realized]): Tensor.realize(*unrealized_tensors)
input_uops = get_input_uops()
# collect buffer UOps (including MultiBuffer)
+17 -18
View File
@@ -4,7 +4,7 @@ import time, random, itertools, math, contextlib, weakref, array
from dataclasses import dataclass, replace, field
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansilen, all_int, prod, flatten, Context, getenv, to_tuple
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, sym_infer, buffers, graph_rewrite
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, buffers, graph_rewrite
from tinygrad.device import Device, Buffer, MultiBuffer
from tinygrad.renderer import Estimates
from tinygrad.codegen import to_program
@@ -148,8 +148,10 @@ def unwrap_multi(call:UOp, resolved:list[UOp]) -> Iterator[tuple[list[Buffer], d
bufs = [b.buffer for b in resolved]
if not any(isinstance(b, MultiBuffer) for b in bufs): yield cast(list[Buffer], bufs), {}
else:
dnum = next((x.expr for x in call.src[0].variables() if x.expr == '_device_num'), None)
for j, per_dev in enumerate(zip(*[cast(MultiBuffer, b).bufs for b in bufs])): yield list(per_dev), {dnum: j} if dnum else {}
# the DEVICE axis is bound per device at launch: it's a RANGE in the AST and the _device_num variable after codegen
has_dnum = any((x.op is Ops.RANGE and x.arg[-1] is AxisType.DEVICE) or (x.op is Ops.PARAM and x.arg.name == '_device_num')
for x in call.src[0].toposort())
for j, per_dev in enumerate(zip(*[cast(MultiBuffer, b).bufs for b in bufs])): yield list(per_dev), {"_device_num": j} if has_dnum else {}
def exec_view(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
resolved = resolve_params(call, ctx.input_uops)
@@ -207,14 +209,15 @@ def exec_graph(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
return t[0]
def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
if call.arg.aux.inputs is not None:
if (inputs:=call.arg.aux.inputs) is not None:
bufs = [_resolve(ctx.input_uops[i], ctx.input_uops).buffer for i in call.arg.aux.input_idxs]
table = call.src[1+inputs].buffer
for j,dev in enumerate(call.arg.aux.device):
addrs = [(b.bufs[j] if isinstance(b, MultiBuffer) else b).get_buf(dev).va_addr for b in bufs]
buf = b.bufs[j] if isinstance(b:=call.src[1+call.arg.aux.inputs].buffer, MultiBuffer) else b
buf.ensure_allocated()._buf.cpu_view().view(fmt='Q')[:len(addrs)] = array.array('Q', addrs)
addrs = array.array('Q', [(b.bufs[j] if isinstance(b, MultiBuffer) else b).get_buf(dev).va_addr for b in bufs])
buf = table.bufs[j] if isinstance(table, MultiBuffer) else table
buf.ensure_allocated()._buf.cpu_view().view(fmt='Q')[:len(addrs)] = addrs
pm_exec.rewrite(call.replace(src=(ast,) + call.src[1:]), replace(ctx, update_stats=False))
exec_kernel(replace(ctx, update_stats=False), call, ast)
st = time.perf_counter()
for d in call.arg.aux.device:
@@ -260,20 +263,16 @@ pm_exec = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="validate", name="ast"),), name="call", allow_any_len=True), exec_validate),
])
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link # noqa: E402 # down here, hcq2 imports the helpers above
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, jit=False) -> UOp:
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
if getenv("HCQ2"):
from extra.hcq2.hcq2 import hcq_compile
linear = hcq_compile(linear, input_uops, jit=jit)
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, jit=jit)
return graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
def link_linear(linear:UOp, jit=False) -> UOp:
if getenv("HCQ2"):
from extra.hcq2.hcq2 import hcq_link
linear = hcq_link(linear, jit=jit)
return linear
def link_linear(linear:UOp, jit=False, cache=True) -> UOp: return hcq_link(linear, jit=jit, cache=cache) if getenv("HCQ2") else linear
def run_linear(linear:UOp, var_vals:dict[str, int]|None=None, input_uops:Sequence[UOp]=(), update_stats=True, jit=False, wait=False):
inputs = list(input_uops)
@@ -287,5 +286,5 @@ def time_call(call:UOp, var_vals:dict[str, int]|None=None, timeout:int|None=None
else:
from tinygrad.tensor import Tensor
with Context(DEBUG=0, BEAM=0, CAPTURING=0, TRACK_MATCH_STATS=0): Tensor.ones(1024, 1024).contiguous().realize(do_update_stats=False)
call = link_linear(compile_linear(UOp(Ops.LINEAR, src=(call,)), beam=0)).src[0]
return pm_exec.rewrite(call, ExecContext(var_vals or {}, update_stats=False, wait=True, timeout=timeout, cache=False))
ctx = ExecContext(var_vals or {}, update_stats=False, wait=True, timeout=timeout, cache=False)
return pm_exec.rewrite(link_linear(compile_linear(UOp(Ops.LINEAR, src=(call,)), beam=0), cache=ctx.cache).src[0], ctx)
+13 -1
View File
@@ -247,8 +247,20 @@ FUSE_OPTIM = ContextVar("FUSE_OPTIM", 0)
ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE = ContextVar("ALLOW_DEVICE_USAGE", 1), ContextVar("MAX_BUFFER_SIZE", 0)
MAX_KERNEL_BUFFERS = ContextVar("MAX_KERNEL_BUFFERS", 0)
EMULATED_DTYPES = ContextVar("EMULATED_DTYPES", "")
DEFAULT_FLOAT, DEFAULT_INT = ContextVar("DEFAULT_FLOAT", "float32"), ContextVar("DEFAULT_INT", "int32")
CAPTURE_PROCESS_REPLAY = ContextVar("CAPTURE_PROCESS_REPLAY", 0)
CPU_COUNT = ContextVar("CPU_COUNT", max(1, len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 1)))
def _get_cpu_count() -> int:
# os.process_cpu_count (3.13+) respects cgroup limits
if hasattr(os, "process_cpu_count"): return max(1, os.process_cpu_count())
# cgroup v2 (containers with --cpus=N)
try:
with open("/sys/fs/cgroup/cpu.max") as f:
quota, period = f.read().strip().split()
if quota != "max": return max(1, int(quota) // int(period))
except (FileNotFoundError, ValueError, ZeroDivisionError): pass
# fall back to affinity (respects taskset but not cgroup quota)
return max(1, len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 1))
NUM_CPU_THREADS = ContextVar("NUM_CPU_THREADS", _get_cpu_count())
NULL_ALLOW_COPYOUT = ContextVar("NULL_ALLOW_COPYOUT", 0)
# VIZ implies PROFILE, but you can run PROFILE without VIZ
VIZ = ContextVar("VIZ", 0)
+2 -2
View File
@@ -85,8 +85,8 @@ models = {
"qwen3.5:0.8b": "https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q8_0.gguf",
"qwen3.5:4b": "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf",
"qwen3.5:9b": "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf",
"qwen3.5:27b": "https://huggingface.co/unsloth/Qwen3.5-27B-GGUF/resolve/main/Qwen3.5-27B-Q4_K_M.gguf",
"qwen3.5:35b-a3b": "https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf",
"qwen3.6:27b": "https://huggingface.co/unsloth/Qwen3.6-27B-GGUF/resolve/main/Qwen3.6-27B-Q4_K_M.gguf",
"qwen3.6:35b-a3b": "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF/resolve/main/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf",
"olmoe": "https://huggingface.co/allenai/OLMoE-1B-7B-0924-Instruct-GGUF/resolve/main/olmoe-1b-7b-0924-instruct-q4_k_m.gguf",
"moonlight": "https://huggingface.co/gabriellarson/Moonlight-16B-A3B-Instruct-GGUF/resolve/main/Moonlight-16B-A3B-Instruct-Q4_K_M.gguf",
"glm-4.7-flash": "https://huggingface.co/unsloth/GLM-4.7-Flash-GGUF/resolve/main/GLM-4.7-Flash-Q4_K_M.gguf",
+1 -1
View File
@@ -105,7 +105,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
if ggml_type == 39:
e = blocks[:, 0].cast(dtypes.uint32)
small_bits = Tensor([0x00200000, 0x00400000], dtype=dtypes.uint32, device=t.device)[e.clip(0, 1).cast(dtypes.int32)] # e = 0 or e = 1 case
d = (e < 2).where(small_bits, ((e - 1) * 0x00800000).cast(dtypes.uint32)).bitcast(dtypes.float32).unsqueeze(-1)
d = (e < 2).where(small_bits, (e - 1) * 0x00800000).bitcast(dtypes.float32).unsqueeze(-1)
codes = q_to_uint8(blocks[:, 1:17], 4)
fp4_lut = Tensor([0.0, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0,
-0.0,-1.0,-2.0,-3.0,-4.0,-6.0,-8.0,-12.0],
+4 -6
View File
@@ -1,5 +1,5 @@
from typing import TYPE_CHECKING, Callable, Self
from tinygrad.dtype import ConstType, DTypeLike, Invalid, dtypes, to_dtype, strong_dtype
from tinygrad.dtype import ConstType, DTypeLike, Invalid, dtypes, to_dtype
from tinygrad.helpers import argfix, prod
from tinygrad.mixin.dtype import DTypeMixin
from tinygrad.mixin.movement import MovementMixin
@@ -17,7 +17,7 @@ class CreationMixin(DTypeMixin, MovementMixin):
from tinygrad.uop.ops import UOp
assert isinstance(self.device, tuple), f"_multi_like needs a multi device tensor, got {self.device}"
if self._uop.axis is None: return self._wrap_uop(fxn(self.shape, None)._uop.shard(self.device, None))
return self._wrap_uop(UOp.mstack(*[fxn(self._uop.shard_shape, d)._uop for d in self.device]).multi(self._uop.axis))
return self._wrap_uop(UOp.mstack(*[fxn(self._uop.shard_shape, d)._uop for d in self.device]).unshard(self._uop.axis))
@classmethod
def empty(cls, *shape, device:str|tuple[str, ...]|None=None, dtype:DTypeLike|None=None) -> Self:
@@ -78,12 +78,10 @@ class CreationMixin(DTypeMixin, MovementMixin):
from tinygrad.uop.ops import UOp
new_shape = argfix(shape)
dt = to_dtype(dtype) if dtype is not None else fill_value.dtype if isinstance(fill_value, UOp) else dtypes.from_py(fill_value)
# materializing commits an inferred weak width
if dtype is None and buffer: dt = strong_dtype(dt)
val = cls.const(dt, fill_value)
val = val.reshape((1,)*len(new_shape)).expand(new_shape)
if not buffer or val._uop.base.arg is not Invalid or val.dtype == dt: return val.clone(device=device) if buffer else val
ret = val.empty_like(dt, device)
if not buffer: return val
ret = val.empty_like(dt if dtype is not None else None, device)
return cls._wrap_uop(ret._uop.after(ret._uop.store(val._uop)))
def full_like(self, fill_value:ConstType, dtype:DTypeLike|None=None, device:str|tuple[str, ...]|None=None, buffer=True) -> Self:
+9 -10
View File
@@ -1,7 +1,7 @@
import math, functools, operator
from typing import TYPE_CHECKING, Literal, Self
from tinygrad.uop import Ops
from tinygrad.dtype import dtypes, ConstType, PyConst, least_upper_dtype
from tinygrad.dtype import dtypes, ConstType, PyConst, least_upper_dtype, least_upper_float
from tinygrad.helpers import argfix, polyN
from tinygrad.mixin.creation import CreationMixin
@@ -21,7 +21,6 @@ class ElementwiseMixin(CreationMixin):
def _broadcasted(self, y: 'Self|ConstType|UOp', reverse: bool = False) -> tuple[Self, Self]:
y = self.ufix(y)
x, y = (self, y) if not reverse else (y, self)
if x.dtype == y.dtype: return x, y
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
def _binop(self, op: Ops, x: Self | ConstType, reverse: bool) -> Self:
@@ -51,7 +50,7 @@ class ElementwiseMixin(CreationMixin):
"""
Returns a contiguous tensor.
"""
if self.dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {self.dtype}")
if self.dtype in dtypes.weaks: return self
uop = self._uop
if uop.op is Ops.CONTIGUOUS or self.device is None or uop.has_buffer_identity(): return self._wrap_uop(uop)
return self._wrap_uop(uop.alu(Ops.CONTIGUOUS, **kwargs))
@@ -240,6 +239,7 @@ class ElementwiseMixin(CreationMixin):
if dtypes.is_int(a.dtype) and dtypes.is_int(b.dtype):
if rounding_mode == "trunc": return a.alu(Ops.CDIV, b)
if rounding_mode == "floor": return a.alu(Ops.FLOORDIV, b)
a = a.cast(dtypes.default_float)
d = a * b.reciprocal()
if rounding_mode is None: return d
if rounding_mode == "trunc": return d.trunc()
@@ -398,7 +398,7 @@ class ElementwiseMixin(CreationMixin):
"""
# NOTE: torch always return in float, we return based on the broadcasting rule.
a, b = self._broadcasted(other)
return a.abs() * ((b < 0) | (b.reciprocal() < 0)).where(-1, 1)
return ((b < 0) | (b.reciprocal() < 0)).where(-(mag := a.abs()), mag)
def logaddexp(self, other: Self | ConstType) -> Self:
"""
@@ -491,8 +491,8 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([0., math.pi/2, math.pi, 3*math.pi/2, 2*math.pi]).cos().numpy())
```
"""
if self.is_floating_point(): return ((math.pi/2)-self.cast(least_upper_dtype(self.dtype, dtypes.float32))).sin().cast(self.dtype)
return ((math.pi/2)-self).sin()
self = self.cast(least_upper_float(self.dtype))
return ((math.pi/2)-self.cast(least_upper_dtype(self.dtype, dtypes.float32))).sin().cast(self.dtype)
def exp(self) -> Self:
"""
@@ -504,9 +504,8 @@ class ElementwiseMixin(CreationMixin):
print(Tensor([0., 1., 2., 3.]).exp().numpy())
```
"""
if self.is_floating_point():
return self.cast(least_upper_dtype(self.dtype, dtypes.float32)).mul(1/math.log(2)).exp2().cast(self.dtype)
return self.mul(1/math.log(2)).exp2()
self = self.cast(least_upper_float(self.dtype))
return self.cast(least_upper_dtype(self.dtype, dtypes.float32)).mul(1/math.log(2)).exp2().cast(self.dtype)
def log2(self) -> Self:
"""
@@ -549,7 +548,7 @@ class ElementwiseMixin(CreationMixin):
"""
base, exponent = self._broadcasted(x, reverse=reverse)
# TODO: int pow
if not base.is_floating_point() and isinstance(x, ConstType) and not (isinstance(x, int) and x >= 0):
if not dtypes.is_float(least_upper_dtype(base.dtype, exponent.dtype)) and isinstance(x, ConstType) and not (isinstance(x, int) and x >= 0):
raise RuntimeError("base needs to be float")
return base.alu(Ops.POW, exponent)
+1 -1
View File
@@ -69,7 +69,7 @@ pm_gradient = PatternMatcher([
(UPat(Ops.FLIP, name="ret"), lambda ctx, ret: (ctx.flip([i for i,x in enumerate(ret.marg) if x]),)),
(UPat(Ops.STACK, name="ret"), lambda ctx, ret: tuple(ctx[i] for i in range(len(ret.src)))),
(UPat(Ops.COPY, name="ret"), lambda ctx, ret: (ctx.copy_to_device(ret.src[0].device),)),
(UPat(Ops.MULTI, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
(UPat(Ops.UNSHARD, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
(UPat(Ops.TUPLE), lambda ctx: ctx.src),
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
(ctx, UOp.maketuple(*(ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1))))),
+5 -5
View File
@@ -139,13 +139,13 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
if v is None: return x # advanced getitem
# advanced setitem: resolve tensor dims in collapsed space, then fall through to basic setitem path
vb = v.cast(self.dtype)._broadcast_to(_broadcast_shape(x.shape, v.shape))
vb = v._broadcast_to(_broadcast_shape(x.shape, v.shape))
for dim in sum_axis: vb = vb.unsqueeze(dim) # add back reduced dims from sum
start = dims[0] if not permuted else 0
vb = x_pre._masked_merge(vb, mask, tuple(range(start, start + len(big_shape))))
elif v is None: return x # basic getitem
# basic setitem: broadcast v, reshape to self.ndim (unsqueeze int dims, squeeze None dims)
else: vb = v.cast(self.dtype)._broadcast_to(x.shape)
else: vb = v._broadcast_to(x.shape)
vb = vb.reshape(tuple(1 if p['collapse_dim'] else p['size'] for p in indices_parsed if p['index'] is not None))
per_dim = []
for d, m in enumerate(mops):
@@ -621,8 +621,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
print(t.normalize(p=1, dim=0).numpy())
```
"""
if p == 0: return self / self.ne(0).sum(dim, keepdim=True).maximum(eps)
return self / self.abs().pow(p).sum(dim, keepdim=True).pow(1/p).maximum(eps)
den = self.ne(0).sum(dim, keepdim=True) if p == 0 else self.abs().pow(p).sum(dim, keepdim=True).pow(1/p)
return self / den.maximum(eps)
def logsumexp(self, axis=None, keepdim=False) -> Self:
"""
@@ -784,7 +784,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
if self.ndim == 0: return self._split_cumalu(axis, Ops.MAX), type(self).zeros(self.shape, dtype=dtypes.int32, buffer=False)
values, n = self._split_cumalu(axis, Ops.MAX), int(self.shape[axis])
x, values_t = self.transpose(axis, -1), values.transpose(axis, -1)
match = x.unsqueeze(-1).eq(values_t.unsqueeze(-2)) * type(self).ones(n, n, buffer=False).triu()
match = x.unsqueeze(-1).eq(values_t.unsqueeze(-2)) * type(self).ones(n, n, dtype=dtypes.bool, buffer=False).triu()
idx = (-(match * type(self).arange(n, 0, -1).reshape(n, 1)).max(-2) + n).cast(dtypes.int32)
return values, idx.transpose(-1, axis)
+1 -1
View File
@@ -21,7 +21,7 @@ class RandMixin(OpMixin):
for i in range(0, num, dtypes.uint32.max):
chunk_num = min(num - i, dtypes.uint32.max)
c_low = low + (i & 0xffffffff)
c_high = high + (i >> 32) + (c_low < low).cast(dtypes.uint32)
c_high = high + (i >> 32) + (c_low < low)
new_key = cls._threefry_random_bits(key, c_low, c_high)
counts0 = cls.arange(ceildiv(chunk_num, 2), dtype=dtypes.uint32)
counts1 = counts0 + ceildiv(chunk_num, 2)
+2 -1
View File
@@ -1,6 +1,6 @@
from typing import Self, Sequence
from tinygrad.uop import Ops
from tinygrad.dtype import DTypeLike, dtypes, sum_acc_dtype, to_dtype
from tinygrad.dtype import DTypeLike, dtypes, strong_dtype, sum_acc_dtype, to_dtype
from tinygrad.helpers import make_tuple
from tinygrad.mixin.dtype import DTypeMixin
from tinygrad.mixin.movement import MovementMixin
@@ -11,6 +11,7 @@ class ReduceMixin(DTypeMixin, MovementMixin):
raise NotImplementedError
def _reduce(self, op:Ops, axis:int|Sequence[int]|None=None, keepdim=False) -> Self:
self = self.cast(strong_dtype(self.dtype))
axis = tuple(self._resolve_dim(x) for x in (range(self.ndim) if axis is None else make_tuple(axis, 1)))
if self.ndim == 0: axis = ()
ret = self._rop(op, axis)
+2 -2
View File
@@ -315,7 +315,7 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
if is_vocab_sharded:
ndev = len(weight.device)
local_vocab_size = weight.shape[0] // ndev
grad_weight_uop = Tensor.empty(local_vocab_size, weight.shape[1], dtype=dtypes.float, device=weight.device).uop.multi(axis=0)
grad_weight_uop = Tensor.empty(local_vocab_size, weight.shape[1], dtype=dtypes.float, device=weight.device).uop.unshard(axis=0)
else:
# weight is replicated (or single device), grad_weight should match
grad_weight_uop = Tensor.empty(weight.shape, dtype=dtypes.float, device=weight.device).uop
@@ -346,7 +346,7 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
if is_vocab_sharded:
# each device owns [offset, offset+local_vocab_size) of the global vocabulary
dnum = UOp.variable("_device_num", 0, ndev-1)
dnum = UOp.range(ndev, -1, AxisType.DEVICE)
offset = dnum * local_vocab_size
global_token_id = idx_flat[i].cast(dtypes.weakint)
local_token_id = (global_token_id - offset).clip(0, grad_weight.shape[0]-1)
+1 -1
View File
@@ -1093,7 +1093,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
def RMSNormalization(X:Tensor, scale:Tensor, axis:int=-1, epsilon:float=1e-5, stash_type:int=1):
assert stash_type == 1, "only float32 is supported"
norm = X.cast(dtypes.float).square().mean(axis=tuple(range(axis + X.ndim if axis < 0 else axis, X.ndim)), keepdim=True).add(epsilon).rsqrt()
return X.cast(X.dtype) * norm * scale
return X * norm * scale
def RotaryEmbedding(X:Tensor, cos_cache:Tensor, sin_cache:Tensor, position_ids:Tensor|None=None, interleaved:int=0, num_heads:int|None=None,
rotary_embedding_dim:int=0):
+2 -3
View File
@@ -3,7 +3,7 @@ import math, sys, struct
from collections import defaultdict, Counter
from tinygrad.codegen.opt import tc
from tinygrad.uop.ops import GroupOp, Ops, UOp, PatternMatcher, UPat, range_str, axis_letters
from tinygrad.helpers import strip_parens, getenv, prod, dedup, Target, CPU_COUNT, IMAGE, FLOAT16, is_image_shape
from tinygrad.helpers import strip_parens, getenv, prod, dedup, Target, NUM_CPU_THREADS, IMAGE, FLOAT16, is_image_shape
from tinygrad.dtype import dtypes, DType, AddrSpace, truncate, float_to_bf16
from tinygrad.renderer import Renderer
@@ -258,7 +258,7 @@ class ClangRenderer(CStyleLanguage):
gep_arr_threshold = 0
has_local = False
has_threads = bool(getenv("THREADS", 1))
global_max = (CPU_COUNT.value, 0, 0)
global_max = (NUM_CPU_THREADS.value, 0, 0)
infinity = "__builtin_inff()"
nan = '__builtin_nanf("")'
@@ -339,7 +339,6 @@ class OpenCLRenderer(CStyleLanguage):
(d != dtypes.double or "cl_khr_fp64" in self.target.arch) and d not in dtypes.fp8s}
class MetalRenderer(CStyleLanguage):
shared_max = 32768
def __init__(self, target:Target):
super().__init__(target)
from tinygrad.runtime.ops_metal import MetalCompiler
+4 -3
View File
@@ -6,7 +6,7 @@ from tinygrad.dtype import dtypes, DType, truncate, AddrSpace
from tinygrad.uop import FastEnum, auto, Ops, GroupOp
from tinygrad.uop.ops import UOp, UPat, PatternMatcher
from tinygrad.renderer.isa import ISARenderer, IselContext, Register, PreRegAllocContext, greg
from tinygrad.helpers import getenv, CPU_COUNT, unwrap, Target
from tinygrad.helpers import getenv, NUM_CPU_THREADS, unwrap, Target
# ***** X86 Ops *****
@@ -193,8 +193,9 @@ pre_isel_matcher = PatternMatcher([
(UPat((Ops.INDEX, Ops.SHRINK), name="addr").store(UPat.var("val"), UPat.var("gate")), gated_store),
# TODO: remove this once we allow all flag producing ops in cmove
# if gate in scalar int cmove is not a comparison need to add one to set the flag
# NOTE: the 0 is int so the bool gate zero-extends and compares as int (a byte compare renders different kernels)
(UPat.var("m", dtypes.bool).where(UPat.var("a"), UPat.var("b")),
lambda m,a,b: m.ne(0).where(a,b) if m.op not in GroupOp.Comparison else None),
lambda m,a,b: m.ne(UOp.const(dtypes.int, 0)).where(a,b) if m.op not in GroupOp.Comparison else None),
])
# ***** X86 registers *****
@@ -803,7 +804,7 @@ class X86Renderer(ISARenderer):
device = "CPU"
has_local = False
has_threads = bool(getenv("THREADS", 1))
global_max = (CPU_COUNT.value, 0, 0)
global_max = (NUM_CPU_THREADS.value, 0, 0)
extra_matcher = extra_matcher
pre_isel_matcher = pre_isel_matcher
isel_matcher = isel_matcher
+2 -4
View File
@@ -5,7 +5,7 @@ from tinygrad.renderer.cstyle import HIPRenderer, create_non_native_float_pats,
from tinygrad.codegen.decomp.transcendental import xexp2, xlog2
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, GroupOp, range_str
from tinygrad.dtype import dtypes, float_to_fp8, DType, truncate, AddrSpace
from tinygrad.helpers import prod, Target, CPU_COUNT, getenv, OSX
from tinygrad.helpers import prod, Target, NUM_CPU_THREADS, getenv, OSX
def is_volatile(u:UOp) -> bool: return (buf:=u.buf_uop).op is Ops.PARAM and buf.arg.volatile
@@ -131,7 +131,6 @@ base_rewrite = PatternMatcher([
])
class LLVMRenderer(Renderer):
supports_float4 = True
abi: str | None
string_rewrite: PatternMatcher
code_for_op = {k:lambda:None for v in lop.values() for k in v.keys()}
@@ -190,7 +189,7 @@ class LLVMRenderer(Renderer):
class CPULLVMRenderer(LLVMRenderer):
has_local = False
has_threads = bool(getenv("THREADS", 1))
global_max = (CPU_COUNT.value, 0, 0)
global_max = (NUM_CPU_THREADS.value, 0, 0)
abi = 'win64cc' if sys.platform == 'win32' else None
string_rewrite = base_rewrite
def render(self, uops: list[UOp]) -> str: return "\n".join((k:=self._render_kernel(uops))[0] + (k[1], self._render_footer(uops)))
@@ -211,7 +210,6 @@ code_for_workitem = {"g": lambda x: f"tail call i32 @llvm.amdgcn.workgroup.id.{c
# https://rocm.docs.amd.com/projects/llvm-project/en/latest/LLVM/llvm/html/AMDGPUUsage.html#llvm-ir-intrinsics
llvm_intrinsics = {Ops.SQRT: "sqrt", Ops.LOG2: "log2", Ops.EXP2: "exp2"}
class AMDLLVMRenderer(LLVMRenderer):
has_local = True
shared_max = HIPRenderer.shared_max
global_max = HIPRenderer.global_max
global_prod_max = HIPRenderer.global_prod_max
+2 -2
View File
@@ -52,8 +52,8 @@ ptx_matcher = PatternMatcher([
(UPat(Ops.STORE, src=(UPat(name="idx"), UPat(dtype=dtypes.bool)), name="x", allow_any_len=True),
lambda x,idx: UOp(x.op, src=(x.src[0], x.src[1].cast(dtypes.uint8))+x.src[2:]) if idx.addrspace != AddrSpace.REG else None),
# ptx shr and shl instructions require y to be uint
(UPat.var("x") << UPat.var("y"), lambda x,y: UOp(Ops.SHL, src=(x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, src=(x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
(UPat.var("x") << UPat.var("y"), lambda x,y: UOp(Ops.SHL, x.dtype, (x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, x.dtype, (x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
])
def mem_type(x:UOp) -> str: return 'shared' if x.addrspace == AddrSpace.LOCAL else 'global'
+3 -1
View File
@@ -13,6 +13,8 @@ def sign_extend(val:UOp, sext_am:int):
def packed_store(bidx:UOp, var:UOp, gate:UOp|None=None):
elems, mask = 4//var.dtype.itemsize, _mask(var.dtype)
shift_am, div_idx = (bidx.src[1].cast(dtypes.uint32) % elems) * (8*var.dtype.itemsize), bidx.src[1] // elems
# bool does its mask math at int32: renderer rewrites run after weak dtypes are lowered, and bool & 0xFF would create a weakint const
if var.dtype == dtypes.bool: var = var.cast(dtypes.int32)
new_v, wmask = (var & mask).cast(dtypes.uint32) << shift_am, ((mask << shift_am) ^ 0xFFFFFFFF).cast(dtypes.uint32)
idx = UOp(Ops.INDEX, src=(bidx.src[0], div_idx))
buf = UOp.load(idx, *((UOp.const(dtypes.uint32, 0), gate) if gate is not None else ()), dtype=dtypes.uint32)
@@ -47,7 +49,7 @@ wgsl_matcher = PatternMatcher([
lambda b,var,gate,s: packed_store(b,var,gate) if is_packed(s) else None),
(UPat.store(UPat.var("b"), UPat.var("var"), name="s"), lambda b,var,s: packed_store(b,var) if is_packed(s) else None),
(UPat.var("a") << UPat.var("b"),lambda a,b:(a.bitcast(dtypes.uint32)<<b.cast(dtypes.uint32)).bitcast(a.dtype) if b.dtype!=dtypes.uint32 else None),
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, src=(x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, x.dtype, (x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
# fix nan check: 'a != a -> is_nan()'
(UPat.var("a") != UPat.var("a"), is_nan),
])
+29 -4
View File
@@ -1,8 +1,8 @@
from __future__ import annotations
import platform, sys, os, ctypes, functools, mmap, threading, array
import platform, sys, os, ctypes, 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
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
@@ -15,9 +15,9 @@ from tinygrad.runtime.autogen import libc
from tinygrad.codegen import do_to_program
from tinygrad import UOp, dtypes
from tinygrad.dtype import AddrSpace
from tinygrad.uop.ops import sint, KernelInfo
from tinygrad.uop.ops import sint, KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
MAX_ARGS, CMD_SIZE, RING_SLOTS = 31, 32, (16 << 10)
MAX_ARGS, CMD_SIZE, RING_SLOTS = 63, 64, (16 << 10)
def signal_prog():
val = UOp.param(1, dtypes.int, (), vmin_vmax=(0, dtypes.int.max), name="value", addrspace=AddrSpace.ALU)
@@ -55,6 +55,17 @@ def worker_prog():
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)
def host_wait(ctx, dst:UOp, val:UOp) -> UOp:
return (cur:=dst.after(loop:=UOp.loop(next(ctx))).index(UOp.const(dtypes.int, 0)).load()).end(loop, cur < val)
pm_host_opsel = PatternMatcher([(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), host_wait)])
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(dtypes.int, 0)).store(store.src[1])
class CPUComputeQueue(HWQueue):
def __init__(self, dev):
super().__init__()
@@ -149,6 +160,20 @@ class CPUAllocator(HCQAllocator):
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].timeline("sentinel", (1 << 64) - 1)),
(UPat(Ops.PARAM, tag="COMPUTE:0_timeline_signal"), lambda ctx: ctx[0].timeline("signal", 0)),
(UPat(Ops.PARAM, tag="COMPUTE:0_timeline_value"), lambda ctx: ctx[0].timeline("value", 1)),
])
@functools.cache
def timeline(self, tag:str, init_value:int) -> 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
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")
-11
View File
@@ -11,26 +11,15 @@ if getenv("IOCTL"): import extra.dsp.run # noqa: F401 # pylint: disable=unused-i
from tinygrad.uop.ops import PatternMatcher, UPat
dsp_pm_late = PatternMatcher([
(UPat.var("x")+UPat(Ops.STACK,src=UPat.var("y")), lambda x,y: x+UOp(Ops.CUSTOMI,x.dtype,(y,),arg="{0}") if x.op is not Ops.CUSTOMI else None),
(UPat.var("x")*UPat(Ops.STACK,src=UPat.var("y")), lambda x,y: x*UOp(Ops.CUSTOMI,x.dtype,(y,),arg="{0}") if x.op is not Ops.CUSTOMI else None),
(UPat.var("x")//UPat(Ops.STACK,src=UPat.var("y")), lambda x,y: x//UOp(Ops.CUSTOMI,x.dtype,(y,),arg="{0}") if x.op is not Ops.CUSTOMI else None),
(UPat(Ops.BUFFER, src=(UPat(Ops.STACK, src=UPat(Ops.CONST, arg=0)),), dtype=dtypes.uchar, name="d", allow_any_len=True),
lambda d: d.replace(src=(UOp(Ops.CUSTOMI, d.dtype, arg="__builtin_HEXAGON_V6_vd0_128B()"),)+d.src[1:]) if d.addrspace is AddrSpace.REG else None),
])
# NOTE: this just increases readability of the generated code
dsp_string = PatternMatcher([
(UPat(Ops.CONST, (dtypes.int8, dtypes.uint8), name="x"), lambda ctx,x: str(x.arg)),
])
class DSPRenderer(ClangRenderer):
supports_float4 = True
has_threads = False
buffer_suffix = " restrict __attribute__((align_value(128)))"
kernel_typedef = "__attribute__((noinline)) void"
extra_args = []
extra_matcher = dsp_pm_late+ClangRenderer.extra_matcher
string_rewrite = dsp_string+ClangRenderer.string_rewrite
type_map = { **ClangRenderer.type_map, dtypes.uint64: "unsigned long long", dtypes.int64: "long long" }
code_for_op = {k:v for k,v in ClangRenderer.code_for_op.items() if k != Ops.SQRT}
+2 -1
View File
@@ -187,7 +187,8 @@ class AM_SMU(AM_IP):
def mode1_reset(self):
if DEBUG >= 2: print(f"am {self.adev.devfmt}: mode1 reset")
if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0): self._send_msg(__DEBUGSMC_MSG_Mode1Reset:=2, 0, debug=True)
if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0) or self.adev.ip_ver[am.MP0_HWIP] in {(13,0,0), (13,0,7), (13,0,10)}:
self._send_msg(__DEBUGSMC_MSG_Mode1Reset:=2, 0, debug=True)
elif self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,12)}: self._send_msg(self.smu_mod.PPSMC_MSG_GfxDriverReset, 1)
else: self._send_msg(self.smu_mod.PPSMC_MSG_Mode1Reset, 0)
+1 -1
View File
@@ -80,7 +80,7 @@ class PTXCompiler(Compiler):
class NVPTXCompiler(PTXCompiler):
def __init__(self, arch:str):
nvrtc_check(jitlink.nvJitLinkVersion(ctypes.byref(ctypes.c_uint()), ctypes.byref(ctypes.c_uint())))
jitlink_check(jitlink.nvJitLinkVersion(ctypes.byref(ctypes.c_uint()), ctypes.byref(ctypes.c_uint())))
super().__init__(arch, cache_key="nv_ptx")
def compile(self, src:str) -> bytes:
jitlink_check(jitlink.nvJitLinkCreate(handle := jitlink.nvJitLinkHandle(), 1, to_char_p_p([f'-arch={self.arch}'.encode()])), handle)
@@ -4,15 +4,15 @@ import struct, functools, time, collections, itertools
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
from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, ContextVar
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer, DepsTracker
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, track_rewrites, GroupOp
from tinygrad.uop.symbolic import symbolic
from tinygrad.dtype import dtypes, truncate
from tinygrad.runtime.support.hcq import MMIOInterface
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, pm_flatten_linear
from tinygrad.engine.jit import DepsTracker
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop
from tinygrad.engine.realize import pm_flatten_linear
# *****************
# 0. helpers
@@ -38,7 +38,8 @@ class HCQInfo:
def all_devices_in(d:Any, c:frozenset[str]) -> bool: return {x.split(":")[0] for x in to_tuple(d)} <= c
def unwrap_mstack(u):
return tuple(x for s in u.src for x in unwrap_mstack(s)) if u.op is Ops.MSTACK else (unwrap_mstack(u.src[0]) if u.op in {Ops.MSELECT, Ops.SLICE} else (u,))
if u.op is Ops.MSTACK: return tuple(x for s in u.src for x in unwrap_mstack(s))
return unwrap_mstack(u.src[0]) if u.op in {Ops.MSELECT, Ops.SLICE} else (u,)
def make_patch(buf:UOp, off:sint, val:UOp) -> UOp:
return buf.index(UOp.const(dtypes.int, off // buf.dtype.itemsize)).store(val.simplify().cast(buf.dtype))
@@ -48,18 +49,18 @@ def make_binary_patch(buf:UOp, blob:bytes) -> UOp:
r = UOp.range(len(blob) // buf.dtype.itemsize, 0, dtype=dtypes.int, src=(buf, data))
return buf.index(r).store(data.index(r).load()).end(r)
def make_cmdbuf(lin, devs):
def make_cmdbuf(lin, devs, buf:UOp|None=None, dep:UOp|None=None):
blob, patches = b'', []
for s in (s for ins in lin.src for s in ins.src):
if (ssimp:=s.simplify()).op is not Ops.CONST: patches.append((len(blob), ssimp))
blob += struct.pack(f'<{ssimp.dtype.fmt}', ssimp.arg if ssimp.op is Ops.CONST else 0x0)
cmdbuf = UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
return cmdbuf.after(make_binary_patch(cmdbuf, blob), *[make_patch(cmdbuf, off, s) for off, s in patches])
cmdbuf = buf if buf is not None else UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
writable = cmdbuf.after(dep) if dep is not None else cmdbuf
return cmdbuf.after(make_binary_patch(writable, blob), *[make_patch(writable, off, s) for off, s in patches])
def make_signal(devs, queue="COMPUTE:0", sentinel=False):
return UOp.placeholder((1,), dtypes.uint64, 0, device=devs).rtag("sentinel_signal" if sentinel else f"{queue}_timeline_signal")
def make_signal_value(devs, queue="COMPUTE:0"):
return UOp.placeholder((1,), dtypes.uint64, 0, device=devs).rtag(f"{queue}_timeline_value")
return UOp.placeholder((1,), dtypes.uint64, 0, device=devs, volatile=True).rtag("sentinel_signal" if sentinel else f"{queue}_timeline_signal")
def make_signal_value(devs, queue="COMPUTE:0"): return UOp.placeholder((1,), dtypes.uint64, 0, device=devs).rtag(f"{queue}_timeline_value")
def make_submit(*cmds, devs:str|tuple[str, ...], queue:str) -> UOp:
return UOp.custom_function("submit_cmdbuf", UOp(Ops.LINEAR, src=tuple(cmds), arg=(to_tuple(devs), queue)))
@@ -106,7 +107,8 @@ def _get_call_bufs_by_lane(call:UOp, devices:tuple[str, ...]) -> list[list[Any]]
def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tuple[str, ...], str, int]) -> list[tuple[tuple, int, int]]:
dep_lanes:list[tuple[tuple, int, int]] = []
for lane, bufs in enumerate(bufs_by_lane):
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, write if write is not None else range(len(bufs)), (key, lane))]
written = write if write is not None else list(range(len(bufs)))
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, written, (key, lane))]
return dep_lanes
def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
@@ -134,8 +136,8 @@ 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
zero, n, finalizers, waited = UOp.const(dtypes.int, 0), len(batch_info), [], set()
for _, devgroup in itertools.groupby(sorted(dedup([d for devs, _ in batch_info for d in devs])), key=lambda d: d.split(":")[0]):
zero, n, submits, bumps, waited = UOp.const(dtypes.int, 0), len(batch_info), [], [], set()
for _, devgroup in itertools.groupby(sorted(dev_bufs), key=lambda d: d.split(":")[0]):
devs = tuple(devgroup)
# to finalize the batch, sync all accesses from other devices to buffers that belong to this device
@@ -143,14 +145,16 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
waits, cur_waited = _build_wait_cmds(fin_deps, devs, "COMPUTE:0")
waited |= cur_waited
# wait the syncs, store the device epoch; value bumps are a separate call: no lane may bump until every lane has patched its waits
# wait the syncs, store the device epoch
store = UOp(Ops.INS, arg="store", src=(make_signal(devs), (tl:=make_signal_value(devs)).index(zero) + n))
submit = make_submit(*waits, store, devs=devs, queue="COMPUTE:0")
submits.append((devs, make_submit(*waits, store, devs=devs, queue="COMPUTE:0")))
upd = [(tl, n + 1)] + [(make_signal_value(devs, queue=qn), n)
for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)]) if qn != "COMPUTE:0"]
bump = UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])
finalizers += [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for b in (submit, bump)]
return finalizers, waited
bumps.append((devs, UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])))
# NOTE: submit before bumps
fins = [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for devs, b in submits + bumps]
return fins, waited
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
@@ -206,7 +210,7 @@ def _merged_hcq_call(calls:list[UOp]) -> UOp: # TODO: simplify?
def merge_queues(linear:UOp) -> UOp:
new_src:list[UOp] = []
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of hcq calls, kept in submit order
limits = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
limits:dict[tuple[tuple[str, ...], str], int] = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
for call in linear.src:
if not isinstance(info:=call.arg.aux, HCQInfo) or info.name == "hcq_finalizer": # non-hcq call or finalizer: close all open queues
@@ -231,7 +235,8 @@ pm_merge_queues = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), merge_queues
def encode_cmdbuf(submit:UOp, lin:UOp) -> UOp|None:
if (pm:=Device.get_class(lin.arg[0][0]).pm_lower) is None: return None
return graph_rewrite(submit, pm, name=f"encode {lin.arg[0]}", enter_calls=True)
pm_encode_cmdbufs = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf)])
pm_encode_cmdbufs = PatternMatcher([
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="lin"),), name="submit"), encode_cmdbuf)])
# *****************
@@ -243,7 +248,7 @@ def is_value_known_at_link(val:UOp) -> bool:
return not val.variables() and not runtime_reads and all(b.op is not Ops.PARAM or b.tag is not None for b in addressed_bufs)
def is_link_patch(p:UOp, jit:bool) -> bool:
store = p.src[0] if (is_binary_patch:=p.op is Ops.END) else p
store = p.src[0] if (is_binary_patch:=(p.op is Ops.END and p.src[0].op is Ops.STORE)) else p
if not jit: return store.buf_uop.tag == "program"
return is_binary_patch or (store.op is Ops.STORE and is_value_known_at_link(store.src[1]))
@@ -257,7 +262,8 @@ def trim_link_patches(ctx:tuple[bool, list[UOp]], a:UOp) -> UOp|None:
pm_trim_link_patches = PatternMatcher([(UPat(Ops.AFTER, src=(UPat((Ops.PARAM, Ops.MSTACK)),), allow_any_len=True, name="a"), trim_link_patches)])
def split_patches(ctx:bool, call:UOp) -> UOp|None:
body = graph_rewrite(call.src[0], pm_trim_link_patches, ctx=(ctx, lt_patches:=[]), name=f"trim link-time patches ({call.arg.aux.name})")
lt_patches:list[UOp] = []
body = graph_rewrite(call.src[0], pm_trim_link_patches, ctx=(ctx, lt_patches), name=f"trim link-time patches ({call.arg.aux.name})")
lt_srcs = collections.defaultdict(list)
for p in lt_patches: lt_srcs[p.buf_uop].append(p)
@@ -304,8 +310,13 @@ def replace_params(call:UOp) -> UOp|None:
by_root = {p.src[0]: p for p in patched}
c_args = [by_root.get(a, a) for a in args]
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=u.device) 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}
# keep buffers whose addresses become link-time constants alive and mapped
held = args + [r.without_after for r in refhold]
addrs = dedup([g.src[0].without_after for x in call.src for g in x.toposort() if g.op is Ops.GETADDR])
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=u.device, 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}
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args) if u.tag == "inputs"), None))
return call.replace(src=(body.substitute(sub), *c_args, *refhold), arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
pm_replace_params = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
@@ -315,7 +326,7 @@ pm_replace_params = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTIO
def resolve_getaddr_slice(bv:UOp, g:UOp) -> UOp:
base = bv.src[0].after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ())
itemsize = bv.src[0].dtype.itemsize if bv.src[0].without_after.op in (Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
return UOp(Ops.GETADDR, dtypes.uint64, src=(base,), arg=g.arg) + UOp.const(dtypes.uint64, bv.src[1].arg * itemsize)
return UOp(Ops.GETADDR, src=(base,), arg=g.arg) + UOp.const(dtypes.uint64, bv.src[1].arg * itemsize)
pm_early_simplify = PatternMatcher([
(UPat(Ops.GETADDR, src=(UPat.any(sl:=UPat(Ops.SLICE, name="bv"), sl.after(allow_any_len=True)),), name="g"), resolve_getaddr_slice),
@@ -328,7 +339,8 @@ pm_early_simplify = PatternMatcher([
def pack_hcq_placeholders(call:UOp) -> UOp|None:
bufs = [b for b in call.src[0].toposort() if b.op is Ops.PARAM and b.tag in {"scratch", "kernargs"}]
offs, sizes = {}, {}
offs:dict[UOp, int] = {}
sizes:dict[Any, int] = {}
for b in bufs:
if b.tag == "scratch": sizes[b.tag] = max(sizes.get(b.tag, 0), b.max_numel())
else:
@@ -336,9 +348,10 @@ def pack_hcq_placeholders(call:UOp) -> UOp|None:
sizes[b.tag] = offs[b] + b.max_numel()
counts = collections.Counter(b.tag for b in bufs)
bases = {b.tag:UOp.placeholder((sizes[b.tag],), b.dtype, next(UOp.unique_num), device=b.device).rtag(b.tag) for b in bufs if counts[b.tag] > 1}
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.weakint, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(None, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
return call.replace(src=(call.src[0].substitute(subs, walk=True), *call.src[1:])) if subs else None
pm_pack_placeholders = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
pm_pack_placeholders = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
# *****************
# 8. callify hcq programs
@@ -389,8 +402,8 @@ pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, name="buf"), bufferize_buf)])
# *****************
# 7. resolve patches
def push_stack(op, s): return UOp(Ops.STACK, op.dtype.scalar(),
tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
def push_stack(op, s): return UOp(Ops.STACK,
src=tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
def fold_binary(buf:UOp, blob:UOp) -> UOp:
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)):
@@ -410,7 +423,7 @@ def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
bufs = tuple(cast(Buffer, x.buffer) for x in buf.src) if buf.op is Ops.MSTACK else tuple(b.bufs if isinstance(b, MultiBuffer) else (b,)*len(devs))
assert len(bufs) == len(devs), f"can't resolve {len(bufs)} buffers on {len(devs)} devices"
addrs = tuple(UOp.const(dtypes.uint64, x.get_buf(d).va_addr) for x, d in zip(bufs, devs))
return addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, dtypes.uint64, addrs)
return addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, src=addrs)
pm_resolve_patches = PatternMatcher([
# multi
@@ -430,21 +443,25 @@ pm_resolve_patches = PatternMatcher([
pm_assert_no_afters = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: panic(RuntimeError, f"AFTER left at hcq_link: {a.src[0].op}"))])
hcq_link_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
linked_buf_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
linked_linear_cache:dict[tuple[bytes, bool], UOp] = {}
def link_cache_key(a:UOp): return a.key, to_tuple(a.device)
pm_link_cache = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: hcq_link_cache.get(link_cache_key(a)))])
def linked_buf_key(a:UOp): return a.key, to_tuple(a.device)
pm_linked_bufs = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: linked_buf_cache.get(linked_buf_key(a)))])
@track_rewrites(lambda _,jit,cache,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
def hcq_link(linear:UOp, jit=False, cache=True) -> UOp:
if (linked:=linked_linear_cache.get(linear_key:=(linear.key, jit))) is not None: return linked
@track_rewrites(lambda _,jit,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
def hcq_link(linear:UOp, jit=False) -> UOp:
cacheable = {(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}
hits = {a.src[0]:hcq_link_cache[key] for a in cacheable.values() if (key:=link_cache_key(a)) in hcq_link_cache}
linear = graph_rewrite(linear, pm_link_cache, name="apply link cache").substitute(hits, walk=True)
linear = graph_rewrite(linear, pm_bufferize, ctx=jit, bottom_up=True, walk=True, name="bufferize placeholders")
hits = {a.src[0]:linked_buf_cache[key] for a in cacheable.values() if (key:=linked_buf_key(a)) in linked_buf_cache}
linear = graph_rewrite(linear, pm_linked_bufs, name="reuse linked bufs").substitute(hits, walk=True)
linear = graph_rewrite(linear, pm_bufferize, ctx=cache, bottom_up=True, walk=True, name="bufferize placeholders")
linear = graph_rewrite(linear, pm_resolve_patches + symbolic, bottom_up=False, name="simplify patches")
linear = graph_rewrite(linear, pm_assert_no_afters, name="assert no afters")
for (j,i),a in cacheable.items(): hcq_link_cache.setdefault(link_cache_key(a), linear.src[j].src[i])
for (j,i),a in cacheable.items(): linked_buf_cache.setdefault(linked_buf_key(a), linear.src[j].src[i])
if cache: linked_linear_cache[linear_key] = linear
return linear
# *****************
@@ -458,39 +475,41 @@ class HCQ2Compiled(Compiled):
self.pm_bufferize = PatternMatcher([
(UPat(Ops.PARAM, tag="sentinel_signal"), lambda ctx: ctx[0].timeline_signal("sentinel", (1 << 64) - 1)),
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if b.tag is None else ctx[0].new_buffer(b, jit=ctx[1]))
(UPat(Ops.PARAM, name="b"), lambda ctx, b: None if b.tag is None else ctx[0].new_buffer(b, cache=ctx[1]))
])
super().__init__(device, allocator, compilers, lambda *a, **kw: None, None, arch=arch)
super().__init__(device, allocator, compilers, runtime, None, arch=arch)
self.rt_buffer = Buffer(self.device, 64 << 20, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True))
self.rt_allocator = BumpAllocator(64 << 20, wrap=False)
self.rt_allocator = BumpAllocator(64 << 20)
def new_buffer(self, b:UOp, jit:bool) -> Buffer:
if jit or b.tag in HCQ_CACHE_TAGS:
def new_buffer(self, b:UOp, cache:bool) -> Buffer:
if cache or b.tag in HCQ_CACHE_TAGS:
return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(uncached=True, cpu_access=True, nolru=True))
return self.rt_buffer.view(b.max_numel(), b.dtype, self.rt_allocator.alloc(b.max_numel() * b.dtype.itemsize, alignment=128))
@functools.cache
def timeline_signal(self, queue:str="COMPUTE:0", init_value:int=0) -> Buffer:
def timeline_signal(self, queue:str, init_value:int=0) -> Buffer:
buf = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
return buf
@functools.cache
def timeline_value(self, queue:str="COMPUTE:0", init_value:int=1) -> Buffer:
def timeline_value(self, queue:str, init_value:int=1) -> Buffer:
buf = Buffer("CPU", 1, dtypes.uint64, preallocate=True)
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
return buf
def synchronize(self, timeout:int|None=None):
if not hasattr(self, 'iface'): return
sig = self.timeline_signal().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
tl = self.timeline_value().as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
sig = self.timeline_signal("COMPUTE:0").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
tl = self.timeline_value("COMPUTE:0").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
st = time.perf_counter()
while sig[0] < tl[0] - 1:
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
def on_device_hang(self): raise RuntimeError(f"{self.device} hang detected")
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
def count(self) -> int: return self.iface.count if hasattr(self, 'iface') else 1
+1 -1
View File
@@ -140,7 +140,7 @@ def assert_all_same_devices(ast:UOp):
def copy_kernel_to_copy_uop(call:UOp, dst:UOp, src:UOp, r:UOp|None=None):
if dst.device == src.device and not (isinstance(dst.device, str) and dst.device.startswith("DISK")): return None
return call.replace(src=(UOp(Ops.COPY, dtype=src.dtype, src=(src,), arg=dst.device),) + call.src[1:])
return call.replace(src=(UOp(Ops.COPY, src=(src,), arg=dst.device),) + call.src[1:])
def simplify_copy_kernel(call:UOp, ast:UOp, dst:UOp, src:UOp):
# NOTE: this is a codegen for SDMA devices
+5 -5
View File
@@ -53,7 +53,7 @@ class IndexingContext:
def new_range(self, s:sint, axistype:AxisType=AxisType.LOOP) -> UOp:
if isinstance(s, UOp) and s.op is Ops.RANGE: return s
# if a range has a 1 src, it's the same as UOp.const(dtypes.weakint, 0)
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(dtypes.weakint, 0)
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(None, 0)
def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
if x.op not in GroupOp.Broadcastable: return rngs
@@ -100,7 +100,7 @@ def create_bufferize_and_index_based_on_ranges(ctx:IndexingContext, x:UOp):
def convert_pad_to_where_to_keep_behavior_local(ctx:IndexingContext, x:UOp):
if x not in ctx.range_map: return None
bx = create_bufferize_and_index_based_on_ranges(ctx, x)
valid: UOp = UOp.const(dtypes.bool, True).uprod([r.get_valid() for r in ctx.range_map[x][0]])
valid: UOp = UOp.const(None, True).uprod([r.get_valid() for r in ctx.range_map[x][0]])
return valid.where(bx.src[0], UOp.const(x.dtype, 0))
def convert_reduce_to_reduce_with_ranges(ctx:IndexingContext, x:UOp):
@@ -148,7 +148,7 @@ def _apply_reshape(in_shape:tuple[sint,...], out_shape:tuple[sint, ...], urngs:U
for s,src in list(zip(out_shape, urngs.src))[::-1]:
axes_in.append(acc*src)
acc *= s
combined_axes = UOp.const(dtypes.weakint, 0).usum(axes_in)
combined_axes = UOp.const(None, 0).usum(axes_in)
axes_out:list[UOp] = []
for s in in_shape[::-1]:
axes_out.append(combined_axes % s)
@@ -171,7 +171,7 @@ def apply_movement_op(op:Ops, in_shape:tuple[sint,...], arg:tuple, rngs:tuple[UO
symbolic+pm_simplify_valid, name="pad")) for r,sh,(off,sz) in zip(rngs, in_shape, arg))
case Ops.RESHAPE:
sink = UOp.sink(*rngs).simplify() # NOTE: this applies any commutative flips to the rngs early
sub_array = {r:UOp.range(r.src[0], i, AxisType.PLACEHOLDER, dtype=r.dtype) for i,r in enumerate(sink.ranges)}
sub_array = {r:r.replace(src=r.src[:1], arg=(i, AxisType.PLACEHOLDER)) for i,r in enumerate(sink.ranges)}
rngs = _apply_reshape(in_shape, arg, sink.substitute(sub_array)).substitute({v:k for k,v in sub_array.items()}).src
case _: raise RuntimeError(f"{op} is not a MovementOp")
return rngs
@@ -248,7 +248,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
# we compare the ranges without their valids
if all_all_same or (PCONTIG and all_same(local_rngs)):
# the new valid is the OR of all the children valids
minimum_valid = UOp.const(dtypes.bool, False).usum(valids)
minimum_valid = UOp.const(None, False).usum(valids)
_out_rngs.append(graph_rewrite(local_rngs[0].valid(minimum_valid), symbolic, name="minimum_valid"))
else:
_out_rngs.append(rctx.new_range(x.shape[i]))
+1 -1
View File
@@ -56,7 +56,7 @@ def memory_plan_rewrite(linear:UOp, held_bufs:set[UOp]|None=None) -> UOp:
arenas = {key: UOp.new_buffer(key[0], sz, dtypes.int8) for key, sz in arena_sizes.items()}
replace_map:dict[UOp, UOp] = {}
for buf_uop, offset in offsets.items():
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(dtypes.weakint, offset)), buf_uop.max_numel())
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(None, offset)), buf_uop.max_numel())
if DEBUG >= 1 and (omem:=sum(nbytes.values()) / 1e6) != (nmem:=sum(arena_sizes.values()) / 1e6):
print(f"memory reduced from {omem:.2f} MB -> {nmem:.2f} MB, {len(first_appearance)} -> {len(arenas)} bufs")
+52 -50
View File
@@ -1,21 +1,22 @@
from tinygrad.helpers import all_same, prod, getenv, ALLREDUCE_CAST
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, graph_rewrite, broadcast_axes, _broadcast_shape
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, AxisType, graph_rewrite, broadcast_axes, _broadcast_shape
from tinygrad.dtype import dtypes
from tinygrad.schedule.allreduce import handle_allreduce
# ***** multi rewrite MSELECT/MSTACK *****
def _apply_shrink(marg, s:UOp, i:int) -> UOp:
new_arg = [tuple([x.substitute({drng[0]:drng[0].const_like(i)}) if isinstance(x, UOp) and
(drng:=[r for r in x.ranges if r.arg[-1] is AxisType.DEVICE]) else x for x in ss]) for ss in marg]
return s._mop(Ops.SHRINK, tuple(new_arg))
def mstack_early_shrink(ms:UOp, shrink:UOp):
ret:list[UOp] = []
def apply_shrink(s:UOp, i:int) -> UOp:
new_arg = [tuple([x.substitute({dvar[0]:dvar[0].const_like(i)}) if isinstance(x, UOp) and
(dvar:=[v for v in x.variables() if v.expr=='_device_num']) else x for x in ss]) for ss in shrink.marg]
return s._mop(Ops.SHRINK, tuple(new_arg))
for i, x in enumerate(ms.src):
if x.op is Ops.COPY:
ret.append(apply_shrink(x.src[0], i).copy_to_device(x.device))
ret.append(_apply_shrink(shrink.marg, x.src[0], i).copy_to_device(x.device))
else:
ret.append(apply_shrink(x, i).contiguous())
ret.append(_apply_shrink(shrink.marg, x, i).contiguous())
return ms.replace(src=tuple(ret))
def lower_broadcast_copy(c:UOp, x:UOp):
@@ -68,7 +69,7 @@ def alu_multi(root:UOp):
axis = root.axis
assert axis is not None
srcs = shard_srcs(root.src, axis)
return srcs[0].alu(root.op, *srcs[1:]).multi(axis)
return srcs[0].alu(root.op, *srcs[1:]).unshard(axis, next(m.src[1] for m in root.src if m.op is Ops.UNSHARD))
def reduce_multi(root:UOp, multi:UOp):
op, num_axes = root.arg
@@ -81,25 +82,25 @@ def reduce_multi(root:UOp, multi:UOp):
return local.allreduce(op, multi.device)
# reduce on non sharded axes, piecewise is fine. if axis is None this is also correct
new_axis = multi.axis - num_axes if multi.axis is not None else None
return multi.src[0]._rop(op, tuple(range(num_axes))).multi(axis=new_axis)
return multi.src[0]._rop(op, tuple(range(num_axes))).unshard(new_axis, multi.src[1])
def reshape_multi(root:UOp, multi:UOp):
if prod(multi.shape) != prod(new_shape:=root.marg): raise RuntimeError("reshape must maintain prod(shape)")
if (new_axis:=root.axis) is not None: new_shape = tuple(s//len(multi.device) if a==new_axis else s for a,s in enumerate(new_shape))
return multi.src[0].reshape(new_shape).multi(new_axis)
return multi.src[0].reshape(new_shape).unshard(new_axis, multi.src[1])
def expand_multi(root:UOp, multi:UOp):
new_axis = None if multi.axis is None else multi.axis + len(root.marg)
return multi.src[0]._mop(Ops.EXPAND, arg=root.marg).multi(new_axis)
return multi.src[0]._mop(Ops.EXPAND, arg=root.marg).unshard(new_axis, multi.src[1])
def pad_multi(root:UOp, multi:UOp):
assert multi.axis is None or root.marg[multi.axis] == (0, multi.shape[multi.axis]), f"padding not supported for {root.marg=}"
local_pad = tuple((0, multi.src[0].shape[multi.axis]) if a == multi.axis else s for a,s in enumerate(root.marg))
return multi.src[0]._mop(Ops.PAD, local_pad).multi(multi.axis)
return multi.src[0]._mop(Ops.PAD, local_pad).unshard(multi.axis, multi.src[1])
def permute_multi(root:UOp, multi:UOp):
# all permutes supported!
return multi.src[0].permute(root.marg).multi(root.axis)
return multi.src[0].permute(root.marg).unshard(root.axis, multi.src[1])
def shrink_multi(root:UOp, multi:UOp):
shard_bounds = tuple((s,e-s) for s,e in multi.bounds) if multi.axis is not None else ()
@@ -111,17 +112,17 @@ def shrink_multi(root:UOp, multi:UOp):
non_shard_shrink = tuple((0, multi.src[0].shape[i]) if i == multi.axis else s for i, s in enumerate(root.marg))
return multi.src[0].copy_to_device(multi.device, arg=shard_bounds.index(root.marg[multi.axis]))._mop(Ops.SHRINK, non_shard_shrink)
local_shrink = tuple((0, multi.src[0].shape[multi.axis]) if a == multi.axis else s for a,s in enumerate(root.marg))
return multi.src[0]._mop(Ops.SHRINK, local_shrink).multi(multi.axis)
return multi.src[0]._mop(Ops.SHRINK, local_shrink).unshard(multi.axis, multi.src[1])
def flip_multi(root:UOp, multi:UOp):
assert multi.axis is None or not root.marg[multi.axis], "flipping not supported on sharded axis"
return multi.src[0].flip([i for i,x in enumerate(root.marg) if x]).multi(multi.axis)
return multi.src[0].flip([i for i,x in enumerate(root.marg) if x]).unshard(multi.axis, multi.src[1])
def stack_multi(root:UOp):
# STACK adds a leading axis: srcs are sharded one axis below the output
axis = root.axis
assert axis is not None
return UOp(Ops.STACK, src=tuple(shard_srcs(root.src, axis-1))).multi(axis)
return UOp(Ops.STACK, src=tuple(shard_srcs(root.src, axis-1))).unshard(axis, next(m.src[1] for m in root.src if m.op is Ops.UNSHARD))
def copy_multi(multi:UOp, device:str | tuple[str, ...]):
assert multi.axis is not None, "all multi ops have axis"
@@ -130,59 +131,60 @@ def copy_multi(multi:UOp, device:str | tuple[str, ...]):
return pieces[0].cat(*pieces[1:], dim=multi.axis)
return multi.src[0]._unshard(multi.axis).allreduce(Ops.ADD, device)
def store_after_multi(dest:UOp, src:UOp): return dest.after(dest.store(src.src[0])).multi(src.axis)
def store_after_multi(dest:UOp, src:UOp): return dest.after(dest.store(src.src[0])).unshard(src.axis, src.src[1])
def passthrough_multi(root:UOp, multi:UOp):
new_src = (multi.src[0],)+tuple(x.src[0] if x.op is Ops.MULTI else x for x in root.src[1:])
return UOp(root.op, root.dtype, src=new_src, arg=root.arg).multi(multi.axis)
new_src = (multi.src[0],)+tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src[1:])
return UOp(root.op, root.dtype, src=new_src, arg=root.arg).unshard(multi.axis, multi.src[1])
def rewrite_into_function(call:UOp):
if call.arg.precompile: return None
new_body = graph_rewrite(call.src[0], multi_pm, name="subcall")
new_args = tuple(a.src[0] if a.op is Ops.MULTI else a for a in call.src[1:])
# after multi resolution, TUPLE elements may be MULTI — strip MULTI from body, create per-shard FUNCTION, wrap each GETTUPLE in its own MULTI
new_args = tuple(a.src[0] if a.op is Ops.UNSHARD else a for a in call.src[1:])
# after multi resolution, TUPLE elements may be UNSHARD — strip UNSHARD from body, create per-shard FUNCTION, wrap each GETTUPLE in its own UNSHARD
assert new_body.op is Ops.TUPLE
if any(s.op is Ops.MULTI for s in new_body.src):
shard_call = call.replace(src=(UOp.maketuple(*[s.src[0] if s.op is Ops.MULTI else s for s in new_body.src]),)+new_args)
return UOp.maketuple(*[shard_call.gettuple(i).multi(s.axis) if s.op is Ops.MULTI else shard_call.gettuple(i) for i, s in enumerate(new_body.src)])
if any(s.op is Ops.UNSHARD for s in new_body.src):
shard_call = call.replace(src=(UOp.maketuple(*[s.src[0] if s.op is Ops.UNSHARD else s for s in new_body.src]),)+new_args)
return UOp.maketuple(*[shard_call.gettuple(i).unshard(s.axis, s.src[1]) if s.op is Ops.UNSHARD else shard_call.gettuple(i)
for i, s in enumerate(new_body.src)])
return call.replace(src=(new_body,)+new_args)
def param_to_multi(p:UOp):
if p.axis is None: return None
return UOp.param(p.arg.slot, p.dtype, p.shard_shape, p.device, p.arg.vmin_vmax, p.arg.multiple_of, p.arg.name, p.arg.addrspace).multi(p.axis)
return UOp.param(p.arg.slot, p.dtype, p.shard_shape, p.device, p.arg.vmin_vmax, p.arg.multiple_of, p.arg.name, p.arg.addrspace).unshard(p.axis)
# NOTE: this is the same pattern as unrolled ranges
multi_pm = PatternMatcher([
(UPat(Ops.PARAM, name="p"), param_to_multi),
(UPat(GroupOp.ALU, name="root", custom_early_reject=set([Ops.MULTI])), alu_multi),
(UPat(Ops.REDUCE, src=(UPat(Ops.MULTI, name="multi"), ), name="root"), reduce_multi),
(UPat(Ops.RESHAPE, src=(UPat(Ops.MULTI, name="multi"), UPat()), name="root"), reshape_multi),
(UPat(Ops.EXPAND, src=(UPat(Ops.MULTI, name="multi"), UPat()), name="root"), expand_multi),
(UPat(Ops.PAD, src=(UPat(Ops.MULTI, name="multi"), UPat(), UPat()), name="root"), pad_multi),
(UPat(Ops.SHRINK, src=(UPat(Ops.MULTI, name="multi"), UPat(), UPat()), name="root"), shrink_multi),
(UPat(Ops.PERMUTE, src=(UPat(Ops.MULTI, name="multi"), ), name="root"), permute_multi),
(UPat(Ops.FLIP, src=(UPat(Ops.MULTI, name="multi"), ), name="root"), flip_multi),
(UPat(Ops.STACK, name="root", custom_early_reject=set([Ops.MULTI])), stack_multi),
(UPat(Ops.AFTER, src=(UPat(Ops.MULTI), UPat(Ops.STORE, src=(UPat(Ops.MULTI, name="dest"), UPat(Ops.MULTI, name="src"))))), store_after_multi),
(UPat(Ops.COPY, src=(UPat(Ops.MULTI, name="multi"),), name="copy"), lambda multi,copy: copy_multi(multi, copy.arg)),
(UPat(Ops.ALLREDUCE, src=(UPat(Ops.MULTI, name="multi"),), name="red"),
lambda multi,red: multi.src[0].allreduce(*red.arg).multi(axis=multi.axis)),
(UPat(GroupOp.ALU, name="root", custom_early_reject=set([Ops.UNSHARD])), alu_multi),
(UPat(Ops.REDUCE, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), reduce_multi),
(UPat(Ops.RESHAPE, src=(UPat(Ops.UNSHARD, name="multi"), UPat()), name="root"), reshape_multi),
(UPat(Ops.EXPAND, src=(UPat(Ops.UNSHARD, name="multi"), UPat()), name="root"), expand_multi),
(UPat(Ops.PAD, src=(UPat(Ops.UNSHARD, name="multi"), UPat(), UPat()), name="root"), pad_multi),
(UPat(Ops.SHRINK, src=(UPat(Ops.UNSHARD, name="multi"), UPat(), UPat()), name="root"), shrink_multi),
(UPat(Ops.PERMUTE, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), permute_multi),
(UPat(Ops.FLIP, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), flip_multi),
(UPat(Ops.STACK, name="root", custom_early_reject=set([Ops.UNSHARD])), stack_multi),
(UPat(Ops.AFTER, src=(UPat(Ops.UNSHARD), UPat(Ops.STORE, src=(UPat(Ops.UNSHARD, name="dest"), UPat(Ops.UNSHARD, name="src"))))), store_after_multi),
(UPat(Ops.COPY, src=(UPat(Ops.UNSHARD, name="multi"),), name="copy"), lambda multi,copy: copy_multi(multi, copy.arg)),
(UPat(Ops.ALLREDUCE, src=(UPat(Ops.UNSHARD, name="multi"),), name="red"),
lambda multi,red: multi.src[0].allreduce(*red.arg).unshard(multi.axis, multi.src[1])),
# resolve TUPLE+GETTUPLE (needed in multi)
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
# GETTUPLE on MULTI: passthrough MULTI (e.g. when FUNCTION was replaced by MULTI(GETTUPLE(...)))
(UPat(Ops.GETTUPLE, src=(UPat(Ops.MULTI, name="multi"),), name="g"),
lambda g, multi: multi.src[0].gettuple(g.arg).multi(multi.axis) if multi.src[0].op in {Ops.FUNCTION, Ops.TUPLE}
# GETTUPLE on UNSHARD: passthrough UNSHARD (e.g. when FUNCTION was replaced by UNSHARD(GETTUPLE(...)))
(UPat(Ops.GETTUPLE, src=(UPat(Ops.UNSHARD, name="multi"),), name="g"),
lambda g, multi: multi.src[0].gettuple(g.arg).unshard(multi.axis, multi.src[1]) if multi.src[0].op in {Ops.FUNCTION, Ops.TUPLE}
else multi),
# rewrite into FUNCTION calls explicitly for MULTI (value-producing)
# rewrite into FUNCTION calls explicitly for UNSHARD (value-producing)
(UPat(Ops.FUNCTION, name="call"), rewrite_into_function),
(UPat((Ops.CALL, Ops.FUNCTION, Ops.AFTER), src=(UPat(Ops.MULTI, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
# just strip the MULTI from non-value-producing CALLs (custom kernels, etc.) — FUNCTION is handled by rewrite_into_function
(UPat(Ops.CALL, dtype=dtypes.void, name="root", custom_early_reject=set([Ops.MULTI])), lambda root:
UOp(root.op, root.dtype, tuple(x.src[0] if x.op is Ops.MULTI else x for x in root.src), root.arg)),
(UPat((Ops.CALL, Ops.FUNCTION, Ops.AFTER), src=(UPat(Ops.UNSHARD, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
# just strip the UNSHARD from non-value-producing CALLs (custom kernels, etc.) — FUNCTION is handled by rewrite_into_function
(UPat(Ops.CALL, dtype=dtypes.void, name="root", custom_early_reject=set([Ops.UNSHARD])), lambda root:
UOp(root.op, root.dtype, tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src), root.arg)),
(UPat((Ops.CAST, Ops.BITCAST, Ops.CONTIGUOUS, Ops.DETACH, Ops.CONTIGUOUS_BACKWARD),
src=(UPat(Ops.MULTI, name="multi"), ), name="root"), passthrough_multi),
# remove MULTI from STORE
(UPat(Ops.STORE, src=(UPat(Ops.MULTI, name="multi"), ), name="root", allow_any_len=True),
lambda root,multi: UOp(root.op, root.dtype, (multi.src[0],)+tuple(x.src[0] if x.op is Ops.MULTI else x for x in root.src[1:]), root.arg)),
src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), passthrough_multi),
# remove UNSHARD from STORE
(UPat(Ops.STORE, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root", allow_any_len=True),
lambda root,multi: UOp(root.op, root.dtype, (multi.src[0],)+tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src[1:]), root.arg)),
])+replace_allreduce
+21 -13
View File
@@ -1,7 +1,7 @@
from dataclasses import dataclass, field, replace
from typing import cast
import itertools
from tinygrad.dtype import dtypes, AddrSpace, Invalid, to_dtype
from tinygrad.dtype import dtypes, AddrSpace, Invalid, to_dtype, strong_dtype
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, KernelInfo, ParamArg, shape_to_shape_arg
from tinygrad.uop.ops import graph_rewrite, sint, AxisType, BottomUpGate, profile_matches, identity_element
from tinygrad.uop.symbolic import symbolic
@@ -78,7 +78,7 @@ def split_reduceop(reduce:UOp, x:UOp):
# split is moved to the end to provide maximum locality for the second phase reduce.
# get expanded by rangeifying the UOp x
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else UOp.const(dtypes.weakint, 0) for i,s in enumerate(x.shape)])
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else 0 for i,s in enumerate(x.shape)])
range_nums = [y.arg[0] for y in indexed.substitute({x.base:UOp(Ops.NOOP, x.base.dtype)}, extra_pm=pm_mops).ranges]
is_expanded = [i not in range_nums for i in range(len(x.shape))]
@@ -314,7 +314,7 @@ pm_const_buffer_folding = pm_mops+PatternMatcher([
(UPat(Ops.NOOP, src=(UPat.cvar("c"),)), lambda c: c),
# mstack on CONST is CONST
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True),
lambda s: UOp.const(c.dtype, c.arg) if (c:=s.base).op is Ops.CONST else None),
lambda s: c if (c:=s.base).op is Ops.CONST else None),
])
pm_remove_bufferize = PatternMatcher([
@@ -342,8 +342,10 @@ def limit_bufs(ctx:IndexingContext, root:UOp):
srcs = []
for s in root.src:
if s.op in GroupOp.Elementwise and s.device is not None:
# Insert bufferize: all AxisType.REDUCE before bufferize are AxisType.LOOP
orig_ranges, end_ranges = s.ranges, [x.replace(arg=(next(ctx.range_idx), AxisType.LOOP)) if x.op is Ops.RANGE else x for x in s.ranges]
# Insert bufferize: all AxisType.REDUCE before bufferize are AxisType.LOOP, the DEVICE range stays a launched axis
orig_ranges = s.ranges
end_ranges = [x.replace(arg=(next(ctx.range_idx), AxisType.LOOP)) if x.op is Ops.RANGE and x.arg[-1] is not AxisType.DEVICE else x
for x in s.ranges]
s = s.substitute(dict(zip(orig_ranges, end_ranges))).bufferize(*end_ranges, arg=BufferizeOpts(device=s.device)).index(*orig_ranges)
srcs.append(s)
return root.replace(src=tuple(srcs))
@@ -359,6 +361,7 @@ pm_limit_bufs = PatternMatcher([(UPat(set.union(GroupOp.Binary, GroupOp.Ternary)
def bufferize_to_store(ctx:itertools.count, x:UOp, idx:UOp, allow_locals=True):
size = prod(x.shape)
dtype = strong_dtype(x.dtype) # a BUFFER is never weak: store at the concrete dtype, the .cast(x.dtype) on the result keeps readers unchanged
rngs = sorted(idx.ranges, key=lambda x: x.arg)
assert size > 0 and isinstance(size, int), f"no zero sized or symbolic sized buffers {size}"
@@ -379,15 +382,15 @@ def bufferize_to_store(ctx:itertools.count, x:UOp, idx:UOp, allow_locals=True):
# NOTE: the local BUFFER needs to be disambiguated here
if x.arg.addrspace == AddrSpace.GLOBAL:
buf = UOp(Ops.BUFFER, src=(shape_to_shape_arg((size,)),), arg=ParamArg(next(ctx), x.dtype, device=x.arg.device, addrspace=AddrSpace.GLOBAL))
do_store = buf.index(idx).store(x.src[0]).end(*rngs)
return buf.after(do_store)
buf = UOp(Ops.BUFFER, src=(shape_to_shape_arg((size,)),), arg=ParamArg(next(ctx), dtype, device=x.arg.device, addrspace=AddrSpace.GLOBAL))
do_store = buf.index(idx).store(x.src[0].cast(dtype)).end(*rngs)
return buf.after(do_store).cast(x.dtype)
if allow_locals:
# handle locals
buf = UOp.placeholder((size,), x.dtype, next(ctx), AddrSpace.LOCAL)
do_store = buf.index(idx).store(x.src[0]).end(*rngs)
return buf.after(do_store.barrier())
buf = UOp.placeholder((size,), dtype, next(ctx), AddrSpace.LOCAL)
do_store = buf.index(idx).store(x.src[0].cast(dtype)).end(*rngs)
return buf.after(do_store).cast(x.dtype)
# collapse any BUFFERIZE to single input BUFFERIZE
def flatten_bufferize(x:UOp):
@@ -412,6 +415,11 @@ def remove_noop_afters(x:UOp) -> UOp|None:
pm_add_buffers = pm_mops+pm_flatten_bufferize+PatternMatcher([
(UPat(Ops.STAGE, src=(UPat(), UPat(name="idx")), name="x"), lambda ctx,x,idx: bufferize_to_store(ctx, x, idx, allow_locals=False)),
# INDEX of a buffer through the weak cast added above: index the buffer directly and cast the loaded value instead.
# this must run in the same rewrite that adds the cast, or the expander expands the whole casted buffer into one big VECTORIZE
(UPat(Ops.INDEX, src=(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat.var("buf"),)),), allow_any_len=True, name="u"),
lambda u,buf: u.replace(dtype=None, src=(buf,)+u.src[1:]).cast(u.dtype)),
# move RESHAPEs through MSELECT/MSTACK
(UPat((Ops.MSELECT, Ops.MSTACK), src=UPat(Ops.RESHAPE), name="m"),
lambda m: m.replace(src=tuple([x.src[0].base for x in m.src])).reshape(m.shape)),
@@ -510,8 +518,8 @@ pm_add_param_range_tags = PatternMatcher([
])
def split_store(x:UOp) -> UOp|None:
# if we have any open ranges here, we don't split
if x.ranges: return 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
# local kernel rewrite
lctx = LocalAddBufferContext()
+8 -6
View File
@@ -71,14 +71,15 @@ class Tensor(RandMixin):
# create a UOp from the different types of inputs
if data is None:
data = UOp.const(_dtype or dtypes.default_float, 0)
data = UOp.const(_dtype, 0.0)
elif isinstance(data, get_args(ConstType)):
data = UOp.const(_dtype or dtypes.from_py(data), data)
data = UOp.const(_dtype, data)
elif is_numpy_ndarray(data) and data.shape == ():
data = UOp.const(_dtype or _from_np_dtype(data.dtype), data.item())
elif not isinstance(data, UOp):
if _dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {_dtype}")
if isinstance(data, bytes): data = UOp._frompy(data, _dtype or dtypes.uint8, _device)
if isinstance(data, bytes):
data = UOp._frompy(data, _dtype or dtypes.uint8, _device)
elif isinstance(data, (list, tuple)):
data = UOp._frompy(data, _dtype or dtypes.from_py(data), _device)
elif is_numpy_ndarray(data):
@@ -93,7 +94,7 @@ class Tensor(RandMixin):
# data might be on a different device
self.uop:UOp = data if data.device is None or data.device == _device else data.copy_to_device(_device)
# cast on the target device, the source may not hold the dtype (numpy has no fp8/bfloat16) or be able to compute it (DISK)
if _dtype is not None and self.uop.dtype != _dtype: self.uop = self.uop.cast(_dtype)
if _dtype is not None: self.uop = self.uop.cast(_dtype)
# add to all_tensors after construction succeeds
all_tensors[weakref.ref(self)] = None
@@ -189,7 +190,8 @@ class Tensor(RandMixin):
@disable_gc()
def realize(self, *lst:Tensor, do_update_stats=True) -> Tensor:
"""Triggers the computation needed to create these Tensor(s)."""
if len(to_realize:=[x for x in (self,)+lst if x.uop.device is not None and not x.uop.has_buffer_identity()]):
to_realize = [x for x in (self,)+lst if not x.uop.is_virtual and not x.uop.has_buffer_identity()]
if len(to_realize):
run_linear(*Tensor.linear_with_vars(*to_realize), update_stats=do_update_stats)
return self
@@ -203,7 +205,7 @@ class Tensor(RandMixin):
return self
def assign(self, x:Tensor|PyConst|list|tuple) -> Tensor:
if self.dtype in dtypes.weaks: raise RuntimeError("cannot assign into a weak tensor; it has no storage")
if self.dtype in dtypes.weaks: self.uop = self.uop.clone()
is_disk = isinstance(self.device, str) and self.device.startswith(("DISK", "TINYFS"))
if not isinstance(x, Tensor): x = Tensor(x, device="CPU" if is_disk else self.device, dtype=self.dtype)
if self.uop is x.uop: return self # a self assign is a NOOP
+1 -1
View File
@@ -97,7 +97,7 @@ class Ops(FastEnum):
# the core 6 movement ops! these only exist in the tensor graph
RESHAPE = auto(); PERMUTE = auto(); EXPAND = auto(); PAD = auto(); FLIP = auto()
MULTI = auto() # MULTI is really a movement op
UNSHARD = auto() # UNSHARD is really a movement op
# reduce
REDUCE = auto(); ALLREDUCE = auto()
+75 -59
View File
@@ -16,8 +16,8 @@ if TYPE_CHECKING:
class AxisType(Enum):
def __repr__(self): return str(self)
GLOBAL = auto(); WARP = auto(); LOCAL = auto(); LOOP = auto(); GROUP_REDUCE = auto(); REDUCE = auto(); UPCAST = auto(); UNROLL = auto() # noqa: E702
THREAD = auto(); PLACEHOLDER = auto() # noqa: E702
DEVICE = auto(); GLOBAL = auto(); WARP = auto(); LOCAL = auto(); LOOP = auto(); GROUP_REDUCE = auto(); REDUCE = auto(); UPCAST = auto() # noqa: E702
UNROLL = auto(); THREAD = auto(); PLACEHOLDER = auto() # noqa: E702
@dataclass(frozen=True, order=True)
class ParamArg:
@@ -35,14 +35,14 @@ class ParamArg:
("volatile", False))
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
return f"ParamArg({', '.join(args)})"
axis_letters = {AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L", AxisType.UPCAST: "u",
AxisType.GROUP_REDUCE: "G", AxisType.REDUCE: "R", AxisType.UNROLL: "r"}
axis_colors = {AxisType.GLOBAL: "blue", AxisType.THREAD: "BLUE", AxisType.LOCAL: "cyan", AxisType.WARP: "CYAN", AxisType.LOOP: "WHITE",
AxisType.UPCAST: "yellow", AxisType.GROUP_REDUCE: "RED", AxisType.REDUCE: "red", AxisType.UNROLL: "magenta"}
axis_letters = {AxisType.DEVICE: "d", AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L",
AxisType.UPCAST: "u", AxisType.GROUP_REDUCE: "G", AxisType.REDUCE: "R", AxisType.UNROLL: "r"}
axis_colors = {AxisType.DEVICE: "green", AxisType.GLOBAL: "blue", AxisType.THREAD: "BLUE", AxisType.LOCAL: "cyan", AxisType.WARP: "CYAN",
AxisType.LOOP: "WHITE", AxisType.UPCAST: "yellow", AxisType.GROUP_REDUCE: "RED", AxisType.REDUCE: "red", AxisType.UNROLL: "magenta"}
# NOTE: LOCAL and GROUP_REDUCE have the same priority. the order here matters
axis_to_pos = {AxisType.LOOP: -1, AxisType.THREAD: 0, AxisType.GLOBAL: 0, AxisType.WARP: 1, AxisType.LOCAL: 2, AxisType.UPCAST: 3,
AxisType.GROUP_REDUCE: 2, AxisType.REDUCE: 4, AxisType.UNROLL: 5}
axis_to_pos = {AxisType.DEVICE: -2, AxisType.LOOP: -1, AxisType.THREAD: 0, AxisType.GLOBAL: 0, AxisType.WARP: 1, AxisType.LOCAL: 2,
AxisType.UPCAST: 3, AxisType.GROUP_REDUCE: 2, AxisType.REDUCE: 4, AxisType.UNROLL: 5}
range_start = {Ops.STAGE: 1, Ops.REDUCE: 1, Ops.WMMA: 3, Ops.END: 1, Ops.CALL: 1, Ops.FUNCTION: 1,
Ops.SLICE: 2, Ops.LINEAR: 0}
@@ -98,7 +98,7 @@ def shape_to_shape_arg(arg:tuple[sint, ...]) -> UOp:
if isinstance(x, UOp) and not dtypes.is_int(x.dtype): raise RuntimeError(f"shape must be int, got {x.dtype} in {arg}")
if len(arg) == 0: return UOp(Ops.STACK)
elif len(arg) == 1: return UOp.const(dtypes.weakint, arg[0])
else: return UOp(Ops.STACK, src=tuple(UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in arg))
else: return UOp(Ops.STACK, src=tuple(UOp.const(None, x) if isinstance(x, int) else x for x in arg))
def consumer_map_from_toposort(lst:Iterable[UOp]):
ret: dict[UOp, dict[UOp, None]] = {}
@@ -128,7 +128,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
case Ops.NOOP:
# NOOP can be void or carry any dtype (e.g. x.f(Ops.NOOP) or substitute base with NOOP)
return None
case Ops.LOAD | Ops.INDEX | Ops.MULTI | Ops.REDUCE | Ops.AFTER | Ops.RANGE | \
case Ops.LOAD | Ops.INDEX | Ops.UNSHARD | Ops.REDUCE | Ops.AFTER | Ops.RANGE | \
Ops.CONTIGUOUS | Ops.CONTIGUOUS_BACKWARD | Ops.COPY | Ops.STAGE | Ops.DETACH | \
Ops.MSTACK | Ops.MSELECT | Ops.ALLREDUCE | Ops.SPECIAL:
# pass through first
@@ -156,8 +156,8 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
case Ops.GETADDR:
return dtypes.uint64
case Ops.SHL | Ops.SHR:
if not dtypes.is_int(src[1].dtype): raise RuntimeError(f"shift distance must be int, got {src[1].dtype}")
return src[0].dtype
if not all(dtypes.is_int(x.dtype) for x in src): raise RuntimeError(f"shift operands must be int, got {[x.dtype for x in src]}")
return promo_dtype(src)
case Ops.BUFFER | Ops.PARAM:
assert isinstance(arg, ParamArg), "BUFFER/PARAM must have ParamArg"
return arg.dtype
@@ -173,7 +173,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
# derived from the value. order matters: bool is an int subclass, ConstFloat is a float subclass
if isinstance(arg, InvalidType): return dtypes.bool # Invalid is always bool, the promo lattice bottom
if isinstance(arg, bool): return dtypes.bool
if isinstance(arg, int): return None
if isinstance(arg, int): return dtypes.weakint
if isinstance(arg, float): return dtypes.weakfloat
raise TypeError(f"no dtype for CONST with arg {arg}")
if op in GroupOp.Unary: return src[0].dtype
@@ -188,9 +188,10 @@ class UOpMetaClass(type):
metadata:tuple[Metadata,...]|None=None, _buffer:Buffer|None=None):
if op is Ops.CONST and arg is Invalid: dtype = dtypes.bool
if dtype is None: dtype = dtype_from_uop(op, src, arg) or dtypes.void
# CONST derives its dtype by value only when the constructor omits one; an explicit (strong) const dtype is legal until the field is removed
if SPEC == 2 and op is not Ops.CONST and not any(s.base.arg is Invalid for s in src) and \
(expected_dtype:=dtype_from_uop(op, src, arg)) is not None and expected_dtype != dtype:
# CONST derives its dtype by value only when the constructor omits one
# TODO: delete this once the dtype field is removed, for now it just re-implements spec.py
if SPEC == 2 and op is not Ops.CONST and not (op in (Ops.SHL, Ops.SHR) and src[1].dtype == dtypes.uint and dtype == src[0].dtype) and \
not any(s.base.arg is Invalid for s in src) and (expected_dtype:=dtype_from_uop(op, src, arg)) is not None and expected_dtype != dtype:
raise RuntimeError(f"bad dtype {dtype}, expected {expected_dtype} on {op}")
if (wret:=UOpMetaClass.ucache.get(key:=(op, dtype, src, arg, tag), None)) is not None and (ret:=wret()) is not None: return ret
UOpMetaClass.ucache[key] = weakref.ref(created:=super().__call__(*key))
@@ -406,12 +407,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return ps[:-1]+(ssimplify((ps[-1]*input_sz) // output_sz),)
return ps
# MULTI marker has no shape
case Ops.MULTI if len(self.src) == 0: return None
# UNSHARD marker has no shape
case Ops.UNSHARD if len(self.src) == 0: return None
# movement ops change the shape
# NOTE: ssimplify is required because the shape needs to be canonical for broadcasting and same shape checking
if self.op in GroupOp.Movement.union({Ops.MULTI, Ops.REDUCE}):
if self.op in GroupOp.Movement.union({Ops.UNSHARD, Ops.REDUCE}):
ps = self.src[0]._shape
if ps is None: raise RuntimeError(f"movement op {self.op} requires shape, {self.src[0].op} doesn't have one")
match self.op:
@@ -437,7 +438,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
case Ops.FLIP:
if len(ps) != len(self.marg) or not all(isinstance(x, bool) for x in self.marg): raise ValueError(f"bad flip on {ps}, {self.marg}")
return ps
case Ops.MULTI: return tuple(s*len(self.device) if a == self.axis else s for a,s in enumerate(ps))
case Ops.UNSHARD: return tuple(s*(int(self.src[1].vmax)+1) if a == self.axis else s for a,s in enumerate(ps))
case Ops.REDUCE:
num_axes = self.arg[1]
if not isinstance(num_axes, int) or num_axes < 0 or num_axes > len(ps):
@@ -472,7 +473,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
@property
def shard_shape(self) -> tuple[sint, ...]:
if not isinstance(self.device, tuple) or self.axis is None: return self.shape
return tuple(x//len(self.device) if i == self.axis else x for i,x in enumerate(self.shape))
dcount = int(self.src[1].vmax)+1 if self.op is Ops.UNSHARD else len(self.device)
return tuple(x//dcount if i == self.axis else x for i,x in enumerate(self.shape))
@property
def max_shard_shape(self) -> tuple[int, ...]: return to_max_shape(self.shard_shape)
@@ -481,6 +483,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def ended_ranges(self) -> tuple[UOp, ...]:
if self.op in range_start: return self.src[range_start[self.op]:]
if self.op is Ops.AFTER: return tuple(flatten([x.ended_ranges for x in self.src[1:]]))
# UNSHARD ends the DEVICE range: its src is per-device index math, the device axis is carried by the axis metadata
if self.op is Ops.UNSHARD: return self.src[1:]
return ()
# determine what ranges this is in
@@ -553,7 +557,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if len(srcs) == 1 and isinstance(srcs[0], UOp): return srcs[0]
return UOp(Ops.GROUP, src=tuple([x for x in srcs if x is not None]))
def index(self, *srcs:UOp|int|None, **kwargs):
new_srcs: list[UOp] = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in srcs if x is not None]
new_srcs: list[UOp] = [UOp.const(None, x) if isinstance(x, int) else x for x in srcs if x is not None]
if len(new_srcs) == 1 and new_srcs[0].op is Ops.CONST and self.op is Ops.STACK: return self.src[new_srcs[0].arg]
return UOp(Ops.INDEX, src=(self,)+tuple(new_srcs), **kwargs)
def __getitem__(self, idx):
@@ -565,11 +569,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
bounds = tuple((s.start or 0, s.stop if s.stop is not None else self.shape[i]) if isinstance(s, slice) else (0, self.shape[i])
for i, s in enumerate(idx))
src = self.shrink(bounds)
non_slice_args = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx if not isinstance(x, slice)]
non_slice_args = [x for x in idx if not isinstance(x, slice)]
if not non_slice_args: return src # all dims are slices, no indexing needed
perm = src.permute(tuple([i for i in range(src.ndim) if i not in slice_idx] + slice_idx))
return perm.index(*non_slice_args)
return self.index(*[UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx])
return self.index(*idx)
@property
def _uop(self) -> UOp: return self
@classmethod
@@ -581,9 +585,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return UOp.const(dtype or self.dtype, b).broadcast(self.max_numel())
def ufix(self, x):
if isinstance(x, UOp): return x
# float self keeps its dtype for any scalar, int self only for int/Invalid scalars
dtype = self.dtype if dtypes.is_float(self.dtype) or (dtypes.is_int(self.dtype) and isinstance(x, (int, InvalidType))) else dtypes.from_py(x)
return UOp.const(dtype, x)
return UOp.const(least_upper_dtype(self.dtype, dtypes.from_py(x)), x)
def broadcast(self, count:int):
if count == 1: return self
return UOp(Ops.STACK, src=(self,)*count)
@@ -603,7 +605,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
for idx in itertools.product(*[range(int(r.vmax)+1) for r in rngs])])
def alu(self, op, *src:UOp, **kwargs): return UOp(op, src=(self, *src), **kwargs)
@staticmethod
def const(dtype:DType, b:ConstLike, shape:tuple[sint, ...]|None=None):
def const(dtype:DType|None, b:ConstLike, shape:tuple[sint, ...]|None=None):
if dtype is None: dtype = dtypes.from_py(b)
if isinstance(b, UOp): return b.cast(dtype)
# NOTE: it always has to be STACK now, even if they are all the same
if isinstance(b, tuple):
@@ -616,7 +619,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
@staticmethod
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, dtypes.void, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.LOOP)+arg)
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.LOOP)+arg)
@staticmethod
def special(end:sint, name:str, dtype=dtypes.weakint): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
@staticmethod
@@ -634,7 +637,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
ret = UOp(Ops.REDUCE, src=(self.permute(perm),), arg=(op, len(reduce_axis)))
return ret.reshape(tuple(s for i,s in enumerate(self.shape) if i not in axis)) if axis != reduce_axis else ret
@staticmethod
def invalid(): return UOp.const(dtypes.bool, Invalid)
def invalid(): return UOp.const(None, Invalid)
def valid(self, cond):
return cond.where(self, self.const_like(Invalid))
def get_idx(self) -> UOp:
@@ -642,7 +645,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return self.src[1] if self.op is Ops.WHERE and self.src[2].arg is Invalid else self
def get_valid(self) -> UOp:
if self.op is Ops.STACK: return UOp.stack(*(x.get_valid() for x in self.src))
return self.src[0] if self.op is Ops.WHERE and self.src[2].arg is Invalid else UOp.const(dtypes.bool, self.arg is not Invalid)
return self.src[0] if self.op is Ops.WHERE and self.src[2].arg is Invalid else UOp.const(None, self.arg is not Invalid)
def reduce(self, *src:UOp, **kwargs):
arg = kwargs.pop('arg', None)
if isinstance(arg, Ops): arg = (arg, 0)
@@ -661,10 +664,13 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# *** multi-device helpers ***
def multi(self, axis:int|None):
def unshard(self, axis:int|None, device_range:UOp|None=None):
assert isinstance(self.device, tuple), f"multi device must be tuple, {self.device} isn't"
assert axis is not None, "multi None is no longer supported"
return UOp(Ops.MULTI, src=(self,), arg=axis)
# an UNSHARD always has two srcs: the value and the DEVICE range it ends (defaults to a DEVICE range over the devices)
if device_range is None: device_range = UOp.range(len(self.device), -1, AxisType.DEVICE)
assert device_range.op is Ops.RANGE and device_range.arg[-1] is AxisType.DEVICE
return UOp(Ops.UNSHARD, src=(self, device_range), arg=axis)
@property
def bounds(self):
@@ -675,7 +681,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def axis(self) -> int|None:
# COPY removes axis. TODO: add more tests for this, and consider MSELECT/MSTACK
if self.op is Ops.COPY: return None
if self.op is Ops.MULTI: return self.arg
if self.op is Ops.UNSHARD: return self.arg
# GETTUPLE: axis comes from the specific TUPLE element, not src[0]
if self.op is Ops.GETTUPLE:
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
@@ -694,9 +700,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return src_axis - self.arg[1]
if self.op is Ops.RESHAPE:
if src_axis is None: return None
arg_acc:list[sint] = list(itertools.accumulate(self.marg, operator.mul, initial=1))
arg_acc:list[sint] = [ssimplify(x) for x in itertools.accumulate(self.marg, operator.mul, initial=1)]
# new_axis is the last one that preserves prod(prior to new_axis) and must not move items between shards
new_axis = len(arg_acc) - arg_acc[::-1].index(prod(self.src[0].shape[:src_axis])) - 1
target = ssimplify(prod(self.src[0].shape[:src_axis]))
if target not in arg_acc: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
new_axis = len(arg_acc) - arg_acc[::-1].index(target) - 1
if self.shape[new_axis] % len(self.device) != 0: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
return new_axis
if self.op is Ops.PERMUTE: return self.marg.index(src_axis) if src_axis is not None else None
@@ -705,18 +713,18 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def _unshard(self, axis:int) -> UOp:
bsz, dcount = self.shape[axis], len(self.device)
dnum = UOp.variable("_device_num", 0, dcount-1)
dnum = UOp.range(dcount, -1, AxisType.DEVICE)
return self.pad(tuple((0,0) if a != axis else (bsz*dnum, bsz*(dcount-1) - bsz*dnum) for a in range(len(self.shape))))
def _shard(self, axis:int, dcount:int) -> UOp:
if len(self.shape) == 0: return self # scalars broadcast, no sharding needed
dnum = UOp.variable("_device_num", 0, dcount-1)
dnum = UOp.range(dcount, -1, AxisType.DEVICE)
if self.shape[axis] % dcount != 0: raise RuntimeError(f"multi axis uneven: {self.shape[axis]=} {axis=} {dcount=}")
sz = self.shape[axis] // dcount
return self.shrink(tuple((0,s) if i != axis else (dnum*sz,dnum*sz+sz) for i,s in enumerate(self.shape)))
def shard(self, devices:tuple[str, ...], axis:int|None=None) -> UOp:
copied = self.copy_to_device(devices)
return copied if axis is None else copied._shard(axis, len(devices)).multi(axis)
return copied if axis is None else copied._shard(axis, len(devices)).unshard(axis)
def copy_to_device(self, device:str|tuple[str, ...], arg=None):
assert arg is None or isinstance(self.device, tuple)
@@ -776,7 +784,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def getaddr(self, device=None) -> UOp:
if self.without_after.op not in {Ops.BUFFER, Ops.SLICE, Ops.BINARY, Ops.MSTACK, Ops.MSELECT, Ops.PARAM}: return self
return UOp(Ops.GETADDR, dtypes.uint64, src=(self,), arg=device or to_tuple(self.device)[0])
return UOp(Ops.GETADDR, src=(self,), arg=device or to_tuple(self.device)[0])
@staticmethod
def new_buffer(device:str|tuple[str, ...], size:int, dtype:DType, num=None):
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {dtype}")
@@ -790,8 +798,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def empty_like(self, dtype:DTypeLike|None=None, device:str|tuple[str, ...]|None=None) -> UOp:
device = canonicalize_device(self.device if device is None else device)
axis = self.axis if isinstance(device, tuple) else None
ret = UOp.empty(self.shard_shape if axis is not None else self.shape, dtype=self.dtype if dtype is None else dtype, device=device)
return ret.multi(axis) if axis is not None else ret
ret = UOp.empty(self.shard_shape if axis is not None else self.shape, dtype=strong_dtype(self.dtype) if dtype is None else dtype, device=device)
return ret.unshard(axis) if axis is not None else ret
@staticmethod
def _frompy(x:list|tuple|bytes, dtype:DType, device:str|tuple[str, ...]|None=None) -> UOp:
device = canonicalize_device(device)
@@ -825,6 +833,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
for x in self.src:
if x.device is not None: return x.device
return None
@property
def is_virtual(self) -> bool:
# NOTE: no device means no place to store, weak means no width to store. neither can back a buffer as-is
# TODO: unify with has_buffer_identity
return self.device is None or self.dtype in dtypes.weaks
@recursive_property
def addrspace(self) -> AddrSpace|None:
if self.op is Ops.PARAM: return self.arg.addrspace
@@ -866,8 +879,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return out.arg if out.op is Ops.CONST and isinstance(out.arg, int) else None
def has_buffer_identity(self, after_ok=False):
"""Check if this UOp has a concrete buffer identity in the graph (RESHAPE/MULTI -> BUFFER chain)."""
if self.op in {Ops.RESHAPE, Ops.MULTI, Ops.MSELECT}: return self.src[0].has_buffer_identity(after_ok)
"""Check if this UOp has a concrete buffer identity in the graph (RESHAPE/UNSHARD -> BUFFER chain)."""
# TODO: this is confusing because UOp.variable('v', 0, 1, dtypes.weakfloat) is True for jit to work, but it doesn't have a buffer
if self.op in {Ops.RESHAPE, Ops.UNSHARD, Ops.MSELECT}: return self.src[0].has_buffer_identity(after_ok)
if after_ok and self.op == Ops.AFTER: return self.src[0].has_buffer_identity(after_ok)
return self.op in {Ops.BUFFER, Ops.SLICE, Ops.PARAM}
@@ -879,7 +893,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
@property
def buffer(self) -> Buffer|MultiBuffer:
if self.op in {Ops.CONTIGUOUS, Ops.RESHAPE, Ops.MULTI, Ops.DETACH, Ops.AFTER}: return self.src[0].buffer
if self.op in {Ops.CONTIGUOUS, Ops.RESHAPE, Ops.UNSHARD, Ops.DETACH, Ops.AFTER}: return self.src[0].buffer
# this buffer can process disk tensors and simple movement ops
if self is not self.base:
buf = self.base.buffer
@@ -921,7 +935,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return ret
@property
def realized(self) -> Buffer|MultiBuffer|None:
if self.op is Ops.MULTI: return self.src[0].realized
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
@@ -1092,9 +1106,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# *** uop high level syntactic sugar ***
@staticmethod
def placeholder(shape:tuple[int, ...], dtype:DType, slot:int, addrspace=AddrSpace.GLOBAL, device=None):
def placeholder(shape:tuple[int, ...], dtype:DType, slot:int, addrspace=AddrSpace.GLOBAL, device=None, volatile=False):
if addrspace is AddrSpace.GLOBAL:
ret = UOp(Ops.PARAM, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace, device=device))
ret = UOp(Ops.PARAM, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace, device=device,volatile=volatile))
else:
assert addrspace in (AddrSpace.LOCAL, AddrSpace.REG)
assert device is None, "LOCAL and REG placeholders cannot have a device"
@@ -1131,7 +1145,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def call(self, *srcs:UOp, ret_dtype:DType|None=None, grad_fxn:Callable|None=None,
name:str|None=None, precompile:bool=False, precompile_backward:bool=False, aux:Any=None) -> UOp:
if ret_dtype is not None: return UOp(Ops.CALL, ret_dtype, src=(self,)+srcs)
assert len(self.ranges) == 0, f"ranges {self.ranges} are leaking out of the call in {self.pyrender()}"
# calls are launched per device, so an open DEVICE range is allowed to cross the call boundary
assert all(r.arg[-1] is AxisType.DEVICE for r in self.ranges), \
f"ranges {self.ranges} are leaking out of the call in {self.pyrender()}"
if self.op in UOp._OPAQUE_CALL_BODIES:
return UOp(Ops.CALL, src=(self,)+srcs, arg=CallInfo(grad_fxn, name, precompile, precompile_backward, aux))
# value-producing bodies are always wrapped in TUPLE so FUNCTION dtype is always void
@@ -1237,7 +1253,8 @@ def safe_pow(x, y):
python_alu: dict[Ops, Callable] = {
Ops.LOG2: lambda x: math.log2(x) if x > 0 else -math.inf if x == 0 else math.nan, Ops.EXP2: safe_exp2,
Ops.SQRT: lambda x: math.sqrt(x) if x >= 0 else math.nan, Ops.RECIPROCAL: lambda x: 1/x if x != 0 else math.copysign(math.inf, x),
Ops.SIN: lambda x: math.sin(x) if not math.isinf(x) else math.nan, Ops.POW: safe_pow, Ops.TRUNC: math.trunc,
Ops.SIN: lambda x: math.sin(x) if not math.isinf(x) else math.nan, Ops.POW: safe_pow,
Ops.TRUNC: lambda x: math.trunc(x) if math.isfinite(x) else x,
Ops.NEG: operator.neg, Ops.ADD: operator.add, Ops.SUB: operator.sub, Ops.MUL: operator.mul, Ops.CMPNE: operator.ne, Ops.CMPLT: operator.lt,
Ops.XOR: operator.xor, Ops.OR: operator.or_, Ops.AND: operator.and_, Ops.SHR: operator.rshift, Ops.SHL: operator.lshift, Ops.MAX: max,
Ops.CMOD: cmod, Ops.CDIV: cdiv, Ops.FLOORDIV: floordiv, Ops.FLOORMOD: floormod,
@@ -1350,7 +1367,7 @@ class UPat(OpMixin):
def _broadcasted(self, y, reverse=False) -> tuple[UPat, UPat]:
y = self.ufix(y)
return (y, self) if reverse else (self, y)
def ufix(self, x): return self.const_like(x) if not isinstance(x, UPat) else x
def ufix(self, x): return UPat.cvar(arg=x) if not isinstance(x, UPat) else x
def __floordiv__(self, x): return self._binop(Ops.FLOORDIV, x, False)
def __rfloordiv__(self, x): return self._binop(Ops.FLOORDIV, x, True)
def mod(self, x, reverse=False): return self._binop(Ops.FLOORMOD, x, reverse)
@@ -1718,16 +1735,15 @@ def lower_weak_node(u:UOp) -> UOp|None:
return u.replace(dtype=None, src=src[:start]+tuple(s.cast(dt) for s in src[start:])).cast(u.dtype)
pm_lower_weak = PatternMatcher([
(UPat(Ops.CONST, dtype=dtypes.weaks, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype)),
# two stacked weak casts are a weakint value used as weakfloat (or vice versa): resolve the inner one at the outer kind's default.
# a SINGLE weak cast is never rewritten here, each consumer absorbs it on its own edge (see lower_weak_srcs)
(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat(Ops.CAST, dtype=dtypes.weaks, src=(UPat.var("x"),)),), name="u"),
lambda u,x: x.cast(select_dtype(u)).cast(u.dtype) if x.dtype not in dtypes.weaks else None),
# 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}, name="u"), lower_weak_node),
# special can only be int32
(UPat(Ops.SPECIAL, src=(UPat.var("var").cast(dtypes.weakint),), name="u"),
lambda u,var: u.replace(dtype=dtypes.int, src=(var,)).cast(dtypes.weakint)),
(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"),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.weakint), UPat.cvar("val").cast(dtypes.weakint))),
lambda var,val: var.bind(val).cast(dtypes.weakint)),
])
def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
if ctx is None: ctx = {}
@@ -1767,8 +1783,8 @@ pm_unbind = PatternMatcher([(UPat(Ops.BIND, name="x"), do_unbind)])
# ctx is source UOp for which we are finding a contiguous view for. used in contiguous_view_offset
pm_contiguous_view_offset = PatternMatcher([
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(dtypes.weakint, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(dtypes.weakint, 0)),
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(None, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(None, 0)),
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE)+UPat.cvar('c'))), lambda c: c),
(UPat(Ops.INDEX, src=(UPat(), UPat.cvar('c'))), lambda ctx, c: c if resolve(ctx.numel() == 1, False) else None),
])
+8 -6
View File
@@ -11,7 +11,7 @@ def validate_index(uidx:UOp, gate:UOp|None=None):
if len(uidx.src) != 2: return True # skip for non final index. TODO: check more complex index with shape
buf,idx = uidx.src
if idx.op is Ops.CONST and idx.arg is Invalid: return True
if gate is None: gate = UOp.const(dtypes.bool, True)
if gate is None: gate = UOp.const(None, True)
# TODO: check for overflow
if not CHECK_OOB or is_image_shape(buf._shape): return True
@@ -59,10 +59,10 @@ spec_shared = PatternMatcher([
# STACK is everywhere too
(UPat(Ops.STACK, dtype=dtypes.void, src=()), lambda: True),
(UPat(Ops.STACK, src=(UPat(),), allow_any_len=True, name="s"),
lambda s: all_same([x.shape for x in s.src]) and all(matches_dtype(x, s.dtype) for x in s.src)),
lambda s: all_same([x.shape for x in s.src]) and all(matches_dtype(x, s.dtype) or x.dtype in dtypes.weaks for x in s.src)),
# ALUs: operands match the result dtype, except comparisons/WHERE; renderer-lowered shifts may use a uint32 count
# a weak dtype matches any dtype (TODO: make python scalars weak consts)
# a weak dtype matches any dtype until lowering commits its operand
(UPat(Ops.WHERE, name="w", src=(UPat(dtype=dtypes.bool), UPat(), UPat())),
lambda w: all(matches_dtype(s, w.dtype) or s.dtype in dtypes.weaks for s in w.src[1:])),
(UPat(GroupOp.Comparison, dtype=dtypes.bool, src=(UPat.var("x"), UPat.var("y"))),
@@ -95,7 +95,7 @@ spec_shared = PatternMatcher([
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, or another AFTER
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.INDEX,
Ops.AFTER, Ops.MULTI, Ops.BITCAST, Ops.INS})),),
Ops.AFTER, Ops.UNSHARD, Ops.BITCAST, Ops.INS})),),
allow_any_len=True, name="x"), lambda x: matches_dtype(x.src[0], x.dtype)),
# CUSTOM (inline and non inline)
@@ -174,8 +174,10 @@ spec_tensor = PatternMatcher([
(UPat(Ops.ALLREDUCE, name="red", src=(UPat.var("x"),)), lambda red,x: matches_dtype(x, red.dtype) and isinstance(red.arg, tuple) and
len(red.arg) == 2 and red.arg[0] in GroupOp.Reduce and is_device(red.arg[1])),
# MULTI/MSELECT/MSTACK
(UPat(Ops.MULTI, name="multi"), lambda multi: all(matches_dtype(x, multi.dtype) for x in multi.src) and isinstance(multi.arg, int)),
# UNSHARD/MSELECT/MSTACK
# an UNSHARD always has two srcs: the value and the DEVICE range it ends
(UPat(Ops.UNSHARD, name="multi"), lambda multi: len(multi.src) == 2 and matches_dtype(multi.src[0], multi.dtype)
and isinstance(multi.arg, int) and multi.src[1].op is Ops.RANGE and multi.src[1].arg[-1] is AxisType.DEVICE),
(UPat(Ops.MSELECT, name="x"), lambda x: isinstance(x.src[0].device, tuple) and x.arg < len(x.src[0].device)),
(UPat(Ops.MSTACK, name="x"), lambda x: all(isinstance(s.device, str) for s in x.src) or (all_same(x.src) and x.src[0].device is None)),
+8 -8
View File
@@ -70,16 +70,16 @@ invalid_pat = UPat(Ops.CONST, arg=Invalid, name="i")
invalid_gate = UPat.var("cond").where(UPat.var("x"), invalid_pat)
pm_data_invalid = PatternMatcher([
(invalid_pat.broadcast(), lambda i: i),
(UPat(GroupOp.Unary|{Ops.BITCAST}, src=(invalid_pat,), name="op"), lambda i,op: i.cast(op.dtype)),
(UPat(GroupOp.Unary|{Ops.BITCAST}, src=(invalid_pat,)), lambda i: i),
(UPat(GroupOp.Unary|{Ops.CAST, Ops.BITCAST}, src=(invalid_gate,), name="op"),
lambda cond,x,op,i: cond.where(op.replace(src=(x,)), i.cast(op.dtype))),
lambda cond,x,op,i: cond.where(op.replace(src=(x,)), i)),
# binary ops move inside the gate, with Invalid in the false branch
(UPat(GroupOp.Binary, src=(invalid_gate, UPat.var("y")), name="alu"), lambda cond,x,y,alu,i: cond.where(x.alu(alu.op,y), i.cast(alu.dtype))),
(UPat(GroupOp.Binary, src=(UPat.var("y"), invalid_gate), name="alu"), lambda cond,x,y,alu,i: cond.where(y.alu(alu.op,x), i.cast(alu.dtype))),
(UPat(GroupOp.Binary, src=(invalid_gate, UPat.var("y")), name="alu"), lambda cond,x,y,alu,i: cond.where(x.alu(alu.op,y), i)),
(UPat(GroupOp.Binary, src=(UPat.var("y"), invalid_gate), name="alu"), lambda cond,x,y,alu,i: cond.where(y.alu(alu.op,x), i)),
(UPat(GroupOp.Binary-GroupOp.Comparison, src=[invalid_pat, UPat()]), lambda i: i),
# an Invalid condition poisons the whole where; a gated Invalid condition lifts the gate out
(invalid_pat.where(UPat.var("a"), UPat()), lambda i,a: i.cast(a.dtype)),
(invalid_gate.where(UPat.var("a"), UPat.var("b")), lambda cond,x,i,a,b: cond.where(x.where(a,b), i.cast(a.dtype))),
(invalid_pat.where(UPat(), UPat()), lambda i: i),
(invalid_gate.where(UPat.var("a"), UPat.var("b")), lambda cond,x,i,a,b: cond.where(x.where(a,b), i)),
# normalize where(cond, Invalid, val) -> where(~cond, val, Invalid)
(UPat.var("cond").where(invalid_pat, UPat.var("val")), lambda cond, i, val: cond.logical_not().where(val, i) if val.arg != Invalid else i),
# lift Invalid out: a.where(cond.where(x, Invalid), c) -> (~a|cond).where(a.where(x, c), Invalid)
@@ -383,7 +383,7 @@ def reduce_mul_chain(r:UOp) -> UOp|None:
def drop_and_clauses(cond:UOp, x:UOp, i:UOp) -> UOp|None:
keep, drop = partition(cond.split_uop(Ops.AND), lambda c: any(r in x.ranges for r in c.ranges))
return UOp.const(dtypes.bool, True).uprod(*keep).where(x, i) if drop else None
return UOp.const(None, True).uprod(*keep).where(x, i) if drop else None
pm_drop_and_clauses = PatternMatcher([(invalid_gate, drop_and_clauses)])
# move conditions from where to load's valid, drop clauses already in load
@@ -398,7 +398,7 @@ def where_on_load(cond:UOp, buf:UOp, idx:UOp, or_cast:UOp) -> UOp|None:
if len(keep) == len(where_clauses): return None
idx = buf.index(idx.get_idx().valid(load_valid.uprod(*moved)))
ret_idx = idx.cast(or_cast.dtype) if or_cast.op is Ops.CAST else idx
return UOp.const(dtypes.bool, True).uprod(*keep).where(ret_idx, ret_idx.const_like(0))
return UOp.const(None, True).uprod(*keep).where(ret_idx, ret_idx.const_like(0))
# where after gated load becomes alt value, TODO: this is sort of duplicated with rules in devectorizer
pm_move_where_on_load = PatternMatcher([

Some files were not shown because too many files have changed in this diff Show More