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
chenyuandGitHub 492dc6d5fb delete _index_to_concrete_int [pr] (#17205)
staying weak is okay
2026-07-25 16:05:08 -04:00
chenyuandGitHub 74c2121d99 promo (uint64, int) -> weakfloat like JAX [pr] (#17204) 2026-07-25 15:20:42 -04:00
chenyuandGitHub f902513355 derive torch backend dispatch from the aten schema (#17203) 2026-07-25 14:21:38 -04:00
chenyuandGitHub ee2ccb1f24 put weakint in dtypes.weaks [pr] (#17202)
* put weakint in dtypes.weaks [pr]

* custom_add_var
2026-07-25 14:00:34 -04:00
wozeparrotandGitHub f0117e98df refactor mlperf optim (#17200) 2026-07-25 10:37:11 -07:00
chenyuandGitHub 983ad3bd95 fix torch backend batchnorm backward (#17201) 2026-07-25 13:15:09 -04:00
chenyuandGitHub 9f78504304 checked cast in torch backend unwrap (#17199) 2026-07-25 12:34:08 -04:00
qazalandGitHub 732e6bd52c add one line viz mention (#17198)
* add one line viz mention

* changes

* edit
2026-07-26 00:18:41 +09:00
chenyuandGitHub 8a10892f5a fix torch backend as_strided (#17195)
0 means 0 offset
2026-07-25 02:24:17 -04:00
chenyuandGitHub 4c58b260fb less wrong calculate_storage_offset (#17194)
initially for speed, then realized it's just wrong
2026-07-25 01:53:54 -04:00
chenyuandGitHub d923263a5b simpler lower_weakint_node [PR] (#17193)
works since weakint is in promo lattice properly now
2026-07-25 00:31:04 -04:00
sirhcmandGitHub 9fdaa4bff2 standardize Program class (#17189) 2026-07-25 00:17:38 -04:00
chenyuandGitHub 0a6125e571 Invalid is bool, put weakint in promo lattice [pr] (#17188) 2026-07-25 00:06:53 -04:00
chenyuandGitHub de3508e375 update create_non_native_float_pats [PR] (#17191)
match based on WHERE dtype, not branch dtyle to prep for new Invalid
2026-07-24 21:21:16 -04:00
George HotzandGitHub a346e2ea7b improve call for viz (#17187) 2026-07-24 15:26:05 -07:00
George HotzandGitHub f65001e297 cleanups because copy is not allowed in rangeify (kimi) (#17186)
* cleanups because copy is not allowed in rangeify (kimi)

* gpt cleanups
2026-07-24 14:17:10 -07:00
George HotzandGitHub dcad119415 convert COPY -> STORE early (#17172)
* convert COPY -> STORE early

* finish it (glm)

* no slop

* fixes

* not None

* copy one

* bugs fixed, just kernel count i think

* fix disk to disk

* move out of realize

* revert that

* simpler

* fixes

* add copy kernel simplification

* fixes

* dead copy

* remove reshape on store

* fix mstack lowering
2026-07-24 13:49:17 -07:00
chenyuandGitHub 38c04fa648 data_src concept in run_rangeify [pr] (#17183)
fixed a skip based on `dtypes.weakint`, it was pulling shape args
2026-07-24 14:20:41 -04:00
chenyuandGitHub 86b42d7cb8 clean up unbroadcast [PR] (#17182)
also renamed
unbroadcast -> pm_expand_broadcast,
broadcast_binary -> expand_broadcast
2026-07-24 11:48:07 -04:00
chenyuandGitHub e0b30b7a1d simpler _split_cumalu [PR] (#17180) 2026-07-24 10:52:50 -04:00
RaineandGitHub fcb1d3fc9f add x86 loops (#17179)
* add x86 loops

* lint
2026-07-24 07:26:09 -07:00
wozeparrotandGitHub c9b60caf8c gptoss: moe gemm kernels (#17178) 2026-07-24 07:25:15 -07:00
chenyuandGitHub 40bfd84956 remove unneeded expand [PR] (#17176) 2026-07-24 10:03:46 -04:00
nimlgenandGitHub 017d1ab56e cpu prep for hcq2 (#17153)
* cpu prep for hcq2

* x
2026-07-24 15:57:21 +03:00
nimlgenandGitHub 0ecef210bb hcq2 cleanup 2 (#17177)
* hcq uops

* x
2026-07-24 13:04:21 +03:00
nimlgenandGitHub 709d7b2343 hcq2 cleanups (#17162) 2026-07-24 10:14:09 +03:00
chenyuandGitHub 2dd2e3d786 don't support Tensor(list(np.array)) [pr] (#17175) 2026-07-24 03:13:28 -04:00
George HotzandGitHub 08eceb0ad9 llm: fix generated tokens in usage accounting (#17174) 2026-07-23 21:38:07 -07:00
chenyuandGitHub a292dab8ab corner cases from weak const branch (#17173) 2026-07-24 00:09:56 -04:00
142 changed files with 2141 additions and 1265 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
View File
@@ -3,3 +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.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),
+12 -5
View File
@@ -1285,7 +1285,7 @@ def train_llama3():
from examples.mlperf.models.flat_llama import FlatTransformer, apply_grad, FP8_DTYPE, MXFP8
from examples.llama3 import MODEL_PARAMS
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
from examples.mlperf.optim import GradAccClipAdamW
from examples.mlperf.optim import GradAccClipAdamW, clip_grads
INITMLPERF = getenv("INITMLPERF")
RUNMLPERF = getenv("RUNMLPERF")
@@ -1482,7 +1482,8 @@ def train_llama3():
@TinyJit
def optim_step():
grad_norm = optim.fstep(grads)
grad_norm = clip_grads(grads, grad_acc, 1.0)
optim.fstep(grads, grad_norm)
scheduler.step()
for g in grads: g.assign(0)
@@ -1667,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
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, clip_grads
BENCHMARK = getenv("BENCHMARK")
@@ -1733,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
@@ -1775,7 +1781,8 @@ def train_gptoss():
@TinyJit
def optim_step():
grad_norm = optim.fstep(grads)
grad_norm = clip_grads(grads, grad_acc, 1.0)
optim.fstep(grads, grad_norm)
scheduler.step()
for g in grads: g.assign(0)
+33 -26
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
@@ -21,6 +21,12 @@ def stochastic_round_bf16(x:Tensor) -> Tensor:
noise = (noise * 0xFFFF).cast(dtypes.uint32)
return ((bits + noise) & 0xFFFF0000).bitcast(dtypes.float32).cast(dtypes.bfloat16)
def clip_grads(grads:list[Tensor], grad_acc, clip_norm) -> Tensor:
for g in grads: g.assign(g / grad_acc)
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
for g in grads: g.assign((g * (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(g.dtype))
return total_norm
class GradAccClipAdamW(Optimizer):
def __init__(self, params:list[Tensor], lr=0.001, b1=0.9, b2=0.999, eps=1e-6, weight_decay=0.0, grad_acc=1, clip_norm=1.0, device=None, fused=FUSE_OPTIM):
super().__init__(params, lr, device, fused)
@@ -37,45 +43,28 @@ 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
n, sz = len(t.device), t.shape[0] // len(t.device)
return Tensor.cat(*[t[p*sz:(p+1)*sz] for p in range(n)], dim=0)
def fstep(self, grads:list[Tensor]):
if self.fused:
out, extra = self._step([], grads)
updates = [out[0][self.pos_params[i]:self.pos_params[i+1]].reshape(tt.shape) for i, tt in enumerate(self.params)]
else:
updates, extra = self._step([], grads)
def fschedule_step(self, grads:list[Tensor]) -> list[Tensor]:
updates, extra = self._step([], grads)
for i, tt in enumerate(self.params): tt.assign(self._apply_update(tt, updates[i], self.master_params[i] if self.master_params else None))
# collect inv_scale tensors attached to fp8 params (set by _apply_update)
fp8_inv_scales = [tt._inv_scale for tt in self.params if hasattr(tt, '_inv_scale')]
fp8_next_inv_scales = [tt._next_inv_scale for tt in self.params if hasattr(tt, '_next_inv_scale')]
to_realize = extra+self.params+self.buffers+(self.master_params or [])+fp8_inv_scales+fp8_next_inv_scales
return extra + self.params + self.buffers + (self.master_params or []) + fp8_inv_scales + fp8_next_inv_scales
Tensor.realize(*to_realize)
return extra[-1]
def fstep(self, grads:list[Tensor], grad_norm:Tensor|None=None):
Tensor.realize(*([grad_norm] if grad_norm is not None else []), *self.fschedule_step(grads))
def _step(self, params:list[Tensor], grads:list[Tensor]) -> tuple[list[Tensor], list[Tensor]]:
grads = list(grads)
for i in range(len(grads)):
if grads[i].device != self.m[i].device: grads[i] = grads[i].to(self.m[i].device)
if self.fused:
grads[0].assign(grads[0] / self.grad_acc)
total_norm = grads[0].float().square().sum().sqrt()
grads[0].assign((grads[0] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[0].dtype))
else:
for i in range(len(grads)):
grads[i].assign(grads[i] / self.grad_acc)
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
for i in range(len(grads)):
grads[i].assign((grads[i] * (self.clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(grads[i].dtype))
ret = []
self.b1_t *= self.b1
self.b2_t *= self.b2
@@ -88,7 +77,7 @@ class GradAccClipAdamW(Optimizer):
v_hat = v_new / (1.0 - self.b2_t)
up = m_hat / (v_hat.sqrt() + self.eps)
ret.append(self.lr * up)
return ret, [self.b1_t, self.b2_t] + self.m + self.v + [total_norm]
return ret, [self.b1_t, self.b2_t] + self.m + self.v
def _apply_update(self, t:Tensor, up:Tensor, master:Tensor|None=None) -> Tensor:
w = master if master is not None else t
@@ -109,7 +98,7 @@ class GradAccClipAdamW(Optimizer):
if self.zero: w_q, w_e8 = self._zero_gather(w_q), self._zero_gather(w_e8)
new_e8 = w_e8.reshape(t._inv_scale.shape)
t._inv_scale.assign(new_e8.shard_like(t._inv_scale) if offloaded else new_e8)
ret = w_q.reshape(new_w.shape)
ret = w_q.reshape(t.shape)
return ret.shard_like(t) if offloaded else ret
from examples.mlperf.models.flat_llama import FP8_MAX
if IMMEDIATE_SCALE:
@@ -132,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)
+111
View File
@@ -0,0 +1,111 @@
import functools, pathlib
from tinygrad import Tensor, dtypes
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.renderer import Estimates
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
from extra.gemm.cdna_asm_gemm import quantize_mxfp8, _mx_block_scale, _mx_block_scale_3d
@functools.cache
def custom_hk_grouped_mxfp8_gemm(C:UOp, A:UOp, B:UOp, scale_A:UOp, scale_B:UOp, *extra:UOp, dname:str, n_experts:int) -> UOp:
M, K = A.shape
E, N, K2 = B.shape
assert K == K2, f"{A.shape} {B.shape}"
assert E == n_experts, f"{E} != {n_experts}"
threads = UOp.special(64 * 8, "lidx0")
workgroups = UOp.special((M // 256) * (N // 256), "gidx0")
sink_inputs = (C.base, A.base, B.base, scale_A.base, scale_B.base, extra[0].base, extra[1].base, extra[2].base, threads, workgroups)
sink = UOp.sink(*sink_inputs,
arg=KernelInfo(f"hk_grouped_mxfp8_gemm_{E}_{M}_{N}_{K}",
estimates=Estimates(ops=2*M*N*K, mem=(M*K+E*N*K)*A.dtype.itemsize+M*N*C.dtype.itemsize)))
kittens_path = pathlib.Path(__file__).parent.parent/"thunder"/"amd"
src = (kittens_path/"grouped_mxfp8_gemm.cpp").read_text()
lib = HIPCCCompiler("gfx950", [f"-I{(kittens_path/'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-ffast-math",
"-DHIP_ENABLE_WARP_SYNC_BUILTINS", f"-DGEMM_M={M}", f"-DGEMM_N={N}", f"-DGEMM_K={K}",
f"-DGEMM_E={E}"]).compile_cached(src)
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
UOp(Ops.BINARY, arg=lib)))
@functools.cache
def custom_hk_grouped_mxfp8_wgrad(C:UOp, A:UOp, B:UOp, scale_A:UOp, scale_B:UOp, expert_off:UOp, *, dname:str, n_experts:int) -> UOp:
N, M = A.shape
K, M2 = B.shape
assert M == M2, f"{A.shape} {B.shape}"
E = n_experts
threads = UOp.special(64 * 8, "lidx0")
workgroups = UOp.special(E * (N // 256) * (K // 256), "gidx0")
sink = UOp.sink(C.base, A.base, B.base, scale_A.base, scale_B.base, expert_off.base, threads, workgroups,
arg=KernelInfo(f"hk_grouped_mxfp8_wgrad_{E}_{M}_{N}_{K}",
estimates=Estimates(ops=2*M*N*K, mem=(N*M+K*M)*A.dtype.itemsize+E*N*K*C.dtype.itemsize)))
kittens_path = pathlib.Path(__file__).parent.parent/"thunder"/"amd"
src = (kittens_path/"grouped_mxfp8_wgrad.cpp").read_text()
lib = HIPCCCompiler("gfx950", [f"-I{(kittens_path/'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-ffast-math",
"-DHIP_ENABLE_WARP_SYNC_BUILTINS", f"-DWGRAD_M={M}", f"-DWGRAD_N={N}", f"-DWGRAD_K={K}",
f"-DWGRAD_E={E}"]).compile_cached(src)
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=src),
UOp(Ops.BINARY, arg=lib)))
def grouped_mx_wgrad(g:Tensor, xg:Tensor, expert_off:Tensor, n_experts:int) -> Tensor:
from extra.llama_kernels.transpose_quantize_mxfp8 import transpose_quantize_mxfp8
M, N = g.shape
M2, K = xg.shape
assert M == M2, f"{g.shape} {xg.shape}"
assert M % 128 == 0 and N % 256 == 0 and K % 256 == 0, f"wgrad needs M%128,N%256,K%256, got {g.shape} {xg.shape}"
gT, _, g_si = transpose_quantize_mxfp8(g.contiguous())
xT, _, x_si = transpose_quantize_mxfp8(xg.contiguous())
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.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)
return out.reshape(n_experts, N, K)
def mx_pack_3d(e8:Tensor) -> Tensor:
E, rows, scale_K = e8.shape
return e8.reshape(E, rows, scale_K // 4, 4).bitcast(dtypes.uint32).reshape(E, rows, scale_K // 4).permute(0, 2, 1).contiguous()
@functools.cache
def custom_grouped_mx_gemm_bw(gradient:UOp, kernel:UOp, w_stored:bool=False) -> tuple:
inputs = kernel.src[1:]
aq = Tensor(inputs[1], device=inputs[1].device)
bq = Tensor(inputs[2], device=inputs[2].device)
ae8 = Tensor(inputs[5], device=inputs[5].device)
be8 = Tensor(inputs[6], device=inputs[6].device)
E, N = bq.shape[0], bq.shape[1]
M, K = aq.shape
g = Tensor(gradient, device=aq.device).reshape(M, N).cast(dtypes.bfloat16)
x_phys = (aq.cast(dtypes.bfloat16) * _mx_block_scale(ae8).cast(dtypes.bfloat16))
w_phys = (bq.cast(dtypes.bfloat16) * _mx_block_scale_3d(be8).cast(dtypes.bfloat16))
expert_off = Tensor(inputs[7], device=inputs[7].device)
grad_x = grouped_mx_gemm(g, w_phys.transpose(1, 2), expert_off)
grad_w = grouped_mx_wgrad(g, x_phys, expert_off, E)
grad_xq = grad_x * _mx_block_scale(ae8).cast(dtypes.bfloat16)
grad_wq = grad_w.contiguous() if w_stored else (grad_w * _mx_block_scale_3d(be8).cast(dtypes.bfloat16)).contiguous()
return (None, grad_xq.uop, grad_wq.uop) + tuple(None for _ in inputs[3:])
_grouped_bw_stored = functools.partial(custom_grouped_mx_gemm_bw, w_stored=True)
def grouped_mx_gemm(x:Tensor, w:Tensor|tuple[Tensor, Tensor], expert_off:Tensor) -> Tensor:
if (pre_quantized := isinstance(w, tuple)):
w_q, w_e8 = w
E, N, K2 = w_q.shape
else:
E, N, K2 = w.shape
M, K = x.shape
assert K == K2, f"shape mismatch {x.shape} {w.shape}"
assert M % 256 == 0 and N % 256 == 0 and K % 128 == 0, f"grouped mxfp8 needs M%256,N%256,K%128, got {x.shape} {w.shape}"
dname = (x.device[0] if isinstance(x.device, tuple) else x.device).split(":")[0]
x_q, x_e8, x_si = quantize_mxfp8(x)
if not pre_quantized: w_q, w_e8, _ = quantize_mxfp8(w)
w_si = mx_pack_3d(w_e8)
xe_in, out_shape = x_e8.reshape(M, K // 32), (M, N)
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.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,
fxn=functools.partial(custom_hk_grouped_mxfp8_gemm, dname=dname, n_experts=E),
grad_fxn=(_grouped_bw_stored if pre_quantized else custom_grouped_mx_gemm_bw))[0]
+64 -55
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_getaddr, make_ins, make_cmdbuf, make_placeholder
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
@@ -36,7 +36,9 @@ class PM4Ops(FastEnum):
SET_SH_REG = auto(); SET_UCONFIG_REG = auto(); WAIT_REG_MEM = auto(); ACQUIRE_MEM = auto() # noqa: E702
RELEASE_MEM = auto(); DISPATCH_DIRECT = auto(); EVENT_WRITE = auto() # noqa: E702
def pkt3(ctx, op:PM4Ops, *vals): return make_ins(op, ctx.pm4.PACKET3(getattr(ctx.pm4, f"PACKET3_{op.name}"), len(vals) - 1), *vals)
def pkt3(ctx, op:PM4Ops, *vals):
return UOp(Ops.INS, arg=op, src=tuple(UOp.const(dtypes.uint32, x)
for x in (ctx.pm4.PACKET3(getattr(ctx.pm4, f"PACKET3_{op.name}"), len(vals) - 1), *vals)))
def wreg(ctx, reg:AMDReg, *args:sint, **kwargs:int):
if bool(args) == bool(kwargs): raise RuntimeError('One (and only one) of *args or **kwargs must be specified')
@@ -90,26 +92,26 @@ def memory_barrier(ctx):
reg_done=getattr(ctx.nbio, f'regBIF_BX_PF{pf}_GPU_HDP_FLUSH_DONE').addr[0], value=0xffffffff),
acquire_mem(ctx)))
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=make_getaddr(dst, ctx.devs))
def pm4_wait(ctx, dst, val): return wait_reg_mem(ctx, val, mem=dst.getaddr(ctx.devs))
def pm4_barrier(ctx): return memory_barrier(ctx)
def pm4_store(ctx, dst, val):
if val.op is Ops.BINARY: return None
return release_mem(ctx, make_getaddr(dst, ctx.devs), val, ctx.pm4.data_sel__mec_release_mem__send_32_bit_low,
return release_mem(ctx, dst.getaddr(ctx.devs), val, ctx.pm4.data_sel__mec_release_mem__send_32_bit_low,
ctx.pm4.int_sel__mec_release_mem__send_interrupt_after_write_confirm, cache_flush=True)
def pm4_timestamp(ctx, dst):
return release_mem(ctx, make_getaddr(dst, ctx.devs), 0, ctx.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
return release_mem(ctx, dst.getaddr(ctx.devs), 0, ctx.pm4.data_sel__mec_release_mem__send_gpu_clock_counter,
ctx.pm4.int_sel__mec_release_mem__none)
def pm4_program(ctx, call, prg):
data, info = prg.arg
lib_gpu = prg.src[0]
args = encode_kernargs_clike(call, prg, ctx.devs)
prog_addr = make_getaddr(lib_gpu, ctx.devs) + data.entry_point_offset
scratch_addr = make_getaddr(make_placeholder(ctx.devs, data.private_segment_size, dtypes.uint8, "scratch", unique=False), ctx.devs)
args_addr = make_getaddr(args, ctx.devs)
prog_addr = lib_gpu.getaddr(ctx.devs) + data.entry_point_offset
scratch_addr = UOp.placeholder((data.private_segment_size,), dtypes.uint8, 0, device=ctx.devs).rtag("scratch").getaddr(ctx.devs)
args_addr = args.getaddr(ctx.devs)
user_regs = []
if data.enable_private_segment_sgpr:
@@ -144,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
ring, wptr, doorbell, put_ptr = (make_placeholder(devs, b.size, b.dtype, ("COMPUTE:0", name), unique=False)
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
@@ -176,36 +184,36 @@ pm_pm4_submit = PatternMatcher([(UPat(Ops.LINEAR, name="lin"),
class SDMAOps(FastEnum): COPY = auto(); POLL_REGMEM = auto(); FENCE = auto(); TRAP = auto(); TIMESTAMP = auto() # noqa: E702
def sdma_copy(ctx, call):
dst, src = call.src[1], call.src[2]
sz = src.max_numel() * src.dtype.itemsize
src_addr, dst_addr = make_getaddr(src, ctx.devs), make_getaddr(dst, ctx.devs)
return UOp(Ops.LINEAR, dtypes.void, tuple([make_ins(SDMAOps.COPY,
ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR),
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz - off, ctx.max_copy_size) - 1), 0,
*data64_le(src_addr + off), *data64_le(dst_addr + off)) for off in range(0, sz, ctx.max_copy_size)]))
sz = call.src[2].max_numel() * call.src[2].dtype.itemsize
src_addr, dst_addr = call.src[2].getaddr(ctx.devs), call.src[1].getaddr(ctx.devs)
return call.ins(SDMAOps.COPY, src=tuple(UOp.const(dtypes.uint32, x) for off in range(0, sz, ctx.max_copy_size) for x in (
ctx.sdma.SDMA_OP_COPY | ctx.sdma.SDMA_PKT_COPY_LINEAR_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_COPY_LINEAR),
ctx.sdma.SDMA_PKT_COPY_LINEAR_COUNT_COUNT(min(sz-off, ctx.max_copy_size)-1), 0, *data64_le(src_addr+off), *data64_le(dst_addr+off))))
def sdma_wait(ctx, dst, val):
def sdma_wait(ctx, ins, dst, val):
op = ctx.sdma.SDMA_OP_POLL_REGMEM | ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_FUNC(WAIT_REG_MEM_FUNCTION_GEQ) \
| ctx.sdma.SDMA_PKT_POLL_REGMEM_HEADER_MEM_POLL(1)
return make_ins(SDMAOps.POLL_REGMEM, op, *data64_le(make_getaddr(dst, ctx.devs)), val, 0xffffffff,
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))
return ins.ins(SDMAOps.POLL_REGMEM, src=tuple(UOp.const(dtypes.uint32, x) for x in (
op, *data64_le(dst.getaddr(ctx.devs)), val, 0xffffffff,
ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_INTERVAL(0x04) | ctx.sdma.SDMA_PKT_POLL_REGMEM_DW5_RETRY_COUNT(0xfff))))
def sdma_store(ctx, dst, val):
def sdma_store(ctx, ins, dst, val):
op = ctx.sdma.SDMA_OP_FENCE | (ctx.sdma.SDMA_PKT_FENCE_HEADER_MTYPE(3) if ctx.target[0] != 9 else 0)
return UOp(Ops.LINEAR, dtypes.void, (
make_ins(SDMAOps.FENCE, op, *data64_le(make_getaddr(dst, ctx.devs)), val), make_ins(SDMAOps.TRAP, ctx.sdma.SDMA_OP_TRAP, 0)))
return UOp(Ops.LINEAR, src=(
ins.ins(SDMAOps.FENCE, src=tuple(UOp.const(dtypes.uint32, x) for x in (op, *data64_le(dst.getaddr(ctx.devs)), val))),
ins.ins(SDMAOps.TRAP, src=tuple(UOp.const(dtypes.uint32, x) for x in (ctx.sdma.SDMA_OP_TRAP, 0)))))
def sdma_timestamp(ctx, dst):
def sdma_timestamp(ctx, ins, dst):
op = ctx.sdma.SDMA_OP_TIMESTAMP | ctx.sdma.SDMA_PKT_TIMESTAMP_GET_HEADER_SUB_OP(ctx.sdma.SDMA_SUBOP_TIMESTAMP_GET_GLOBAL)
return make_ins(SDMAOps.TIMESTAMP, op, *data64_le(make_getaddr(dst, ctx.devs)))
return ins.ins(SDMAOps.TIMESTAMP, src=tuple(UOp.const(dtypes.uint32, x) for x in (op, *data64_le(dst.getaddr(ctx.devs)))))
pm_sdma_opsel = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.COPY),), name="call", allow_any_len=True), sdma_copy),
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), sdma_wait),
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)), sdma_timestamp),
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))), sdma_store),
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val")), name="ins"), sdma_wait),
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),), name="ins"), sdma_timestamp),
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val")), name="ins"), sdma_store),
])
def sdma_submit(cmdbuf, devs):
@@ -214,7 +222,7 @@ def sdma_submit(cmdbuf, devs):
# the sdma queue's ring and its host-side ring/write/put pointers
for d in devs: q = Device[d].sdma_queue(0)
ring, wptr, doorbell, put_ptr = (make_placeholder(devs, b.size, b.dtype, ("COPY:0", name), unique=False)
ring, wptr, doorbell, put_ptr = (UOp.placeholder((b.size,), b.dtype, 0, device=devs).rtag(f"COPY:0_{name}")
for name, b in (("ring", q.ring), ("write_ptr", q.write_ptr), ("doorbell", q.doorbell), ("put_value", q.put_value)))
# sdma needs the cmdbuf contiguous: if it won't fit before the ring end, restart at 0 and zero the tail
@@ -240,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
@@ -251,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:
@@ -279,7 +287,7 @@ def amd_build_program(prg:UOp) -> UOp:
wave32=bool(desc.kernel_code_properties & 0x400), private_segment_size=desc.private_segment_fixed_size, kernargs_segment_size=desc.kernarg_size,
kernargs_alloc_size=desc.kernarg_size + (ctypes.sizeof(hsa.hsa_kernel_dispatch_packet_t) if edp else 0), enable_dispatch_ptr=edp,
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER)
buf = make_placeholder(prg.device, len(image), dtypes.uint8, "program")
buf = UOp.placeholder((len(image),), dtypes.uint8, next(UOp.unique_num), device=prg.device).rtag("program")
cached = _amd_program_cache[key] = prg.replace(src=(buf.after(make_binary_patch(buf, bytes(image))),), arg=(data, prg.arg))
return cached
@@ -509,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))):
@@ -628,10 +637,10 @@ class AMDDevice(HCQ2Compiled):
qname = f"{'COPY' if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA else 'COMPUTE'}:{idx}"
self.pm_bufferize = PatternMatcher([
(UPat(Ops.PARAM, tag={(qname, name)}), lambda ctx, b=getattr(queue, name): b) for name in ["ring", "write_ptr", "doorbell", "put_value"]
(UPat(Ops.PARAM, tag=f"{qname}_{name}"), lambda ctx, b=getattr(queue, name): b) for name in ["ring", "write_ptr", "doorbell", "put_value"]
] + [
(UPat(Ops.PARAM, tag={(qname, "timeline_signal")}), lambda ctx, q=qname: ctx[0].timeline_signal(q)),
(UPat(Ops.PARAM, tag={(qname, "timeline_value")}), lambda ctx, q=qname: ctx[0].timeline_value(q)),
(UPat(Ops.PARAM, tag=f"{qname}_timeline_signal"), lambda ctx, q=qname: ctx[0].timeline_signal(q)),
(UPat(Ops.PARAM, tag=f"{qname}_timeline_value"), lambda ctx, q=qname: ctx[0].timeline_value(q)),
]) + self.pm_bufferize
return queue
+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,
+158
View File
@@ -0,0 +1,158 @@
#include "kittens.cuh"
using namespace kittens;
#ifndef GEMM_M
constexpr int GEMM_M = 8192;
#endif
#ifndef GEMM_N
constexpr int GEMM_N = 8192;
#endif
#ifndef GEMM_K
constexpr int GEMM_K = 8192;
#endif
#ifndef GEMM_E
constexpr int GEMM_E = 8;
#endif
// Kernel
constexpr int NUM_WARPS = 8;
constexpr int WARPS_ROW = 2;
constexpr int WARPS_COL = 4;
constexpr int BLOCK_ROW = 256;
constexpr int BLOCK_COL = 256;
constexpr int BLOCK_K = 128;
constexpr int HALF_ROW = BLOCK_ROW / 2;
constexpr int HALF_COL = BLOCK_COL / 2;
constexpr int REG_M = BLOCK_ROW / WARPS_ROW / 2;
constexpr int REG_N = BLOCK_COL / WARPS_COL / 2;
using G = kittens::group<NUM_WARPS>;
__global__ __launch_bounds__(512, 2) void grouped_mxfp8_gemm_kernel(bf16 *C_ptr, fp8e4m3 *A_ptr, fp8e4m3 *B_ptr, fp8e8m0 *scale_A_ptr, fp8e8m0 *scale_B_ptr,
const uint8_t *__restrict__ a_e8_unused,
const uint8_t *__restrict__ b_e8_unused,
const int *__restrict__ expert_off) {
constexpr int M = GEMM_M, N = GEMM_N, K = GEMM_K, E = GEMM_E;
kittens::gl<fp8e4m3, 1, 1, M, K> A{A_ptr, nullptr, nullptr, nullptr, nullptr};
kittens::gl<fp8e4m3, 1, 1, E * N, K> B{B_ptr, nullptr, nullptr, nullptr, nullptr}; // all experts stacked on rows
kittens::gl<bf16, 1, 1, M, N> C{C_ptr, nullptr, nullptr, nullptr, nullptr};
constexpr int k_iters = K / BLOCK_K;
constexpr int NUM_THREADS = NUM_WARPS * WARP_THREADS;
kittens::gl<fp8e8m0, k_iters * (M / BLOCK_ROW), 1, 16, 64> scale_A_gl{scale_A_ptr, nullptr, nullptr, nullptr, nullptr};
kittens::gl<fp8e8m0, E * k_iters * (N / BLOCK_COL), 1, 16, 64> scale_B_gl{scale_B_ptr, nullptr, nullptr, nullptr, nullptr};
using ST_A = st_fp8e4m3<HALF_ROW, BLOCK_K, st_16x128_s>;
using ST_B = st_fp8e4m3<HALF_COL, BLOCK_K, st_16x128_s>;
using ST_Scale = st<fp8e8m0, 16, 64, st_16x64_s>;
using RT_A = rt_fp8e4m3<REG_M, BLOCK_K>;
using RT_B = rt_fp8e4m3<REG_N, BLOCK_K>;
using RT_C = rt_fl<REG_M, REG_N, col_l, rt_16x16_s>;
__shared__ ST_A As[2][2];
__shared__ ST_B Bs[2][2];
__shared__ ST_Scale scale_A_smem[2], scale_B_smem[2];
RT_A a;
RT_B b0, b1;
RT_C cA, cB, cC, cD;
zero(cA); zero(cB); zero(cC); zero(cD);
constexpr int tiles_M = M / BLOCK_ROW;
constexpr int tiles_N = N / BLOCK_COL;
const int NUM_XCDS = 8;
const int WGM = 8;
int wgid = chiplet_transform_chunked(blockIdx.x, gridDim.x, NUM_XCDS, WGM * WGM);
int num_wgid_in_group = WGM * tiles_N;
int group_id = wgid / num_wgid_in_group;
int first_pid_m = group_id * WGM;
int group_size_m = min(tiles_M - first_pid_m, WGM);
int block_row = first_pid_m + ((wgid % num_wgid_in_group) % group_size_m);
int block_col = (wgid % num_wgid_in_group) / group_size_m;
int block_m = block_row * BLOCK_ROW;
int block_n = block_col * BLOCK_COL;
int e = 0;
#pragma unroll
for (int i = 1; i < E; i++) e += (expert_off[i] <= block_row * BLOCK_ROW);
e = __builtin_amdgcn_readfirstlane(e);
const int bcol_base = e * (N / HALF_COL); // expert base in B row-tile (128-row) units
const int sb_base = e * (k_iters * tiles_N); // expert base into scale_B batches
int warp_m = warpid() / WARPS_COL;
int warp_n = warpid() % WARPS_COL;
using T = fp8e4m3;
constexpr int bpt = ST_A::underlying_subtile_bytes_per_thread;
constexpr int bpm = bpt * NUM_THREADS;
constexpr int copies_A = HALF_ROW * BLOCK_K * sizeof(T) / bpm;
constexpr int copies_B = HALF_COL * BLOCK_K * sizeof(T) / bpm;
uint32_t sw_A[copies_A], sw_B[copies_B];
G::prefill_swizzled_offsets(As[0][0], A, sw_A);
G::prefill_swizzled_offsets(Bs[0][0], B, sw_B);
const T *a_base = (const T *)&A[{0, 0, 0, 0}];
const T *b_base = (const T *)&B[{0, 0, 0, 0}];
const int a_row_stride = A.template stride<2>() * sizeof(T);
const int b_row_stride = B.template stride<2>() * sizeof(T);
i32x4 a_srd = make_srsrc(a_base, (uint32_t)((uint64_t)M * a_row_stride), a_row_stride);
i32x4 b_srd = make_srsrc(b_base, (uint32_t)((uint64_t)E * N * b_row_stride), b_row_stride);
const int wid = warpid() % NUM_WARPS;
constexpr int elem_per_warp = (16 / sizeof(T)) * kittens::WARP_THREADS;
uint32_t a_lds_00 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0][0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t a_lds_01 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0][1].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t a_lds_10 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1][0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t a_lds_11 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1][1].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_00 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0][0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_01 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0][1].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_10 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1][0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_11 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1][1].data[0]) + wid * elem_per_warp * sizeof(T)));
int a_row_h0 = warp_m * REG_M;
int a_row_h1 = HALF_ROW + warp_m * REG_M;
int b_row_h0 = warp_n * REG_N;
int b_row_h1 = HALF_COL + warp_n * REG_N;
uint32_t a_lds[2][2] = {{a_lds_00, a_lds_01}, {a_lds_10, a_lds_11}};
uint32_t b_lds[2][2] = {{b_lds_00, b_lds_01}, {b_lds_10, b_lds_11}};
#pragma unroll 1
for (int kk = 0; kk < k_iters; kk++) {
G::load(As[0][0], A, {0, 0, block_row * 2, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds[0][0]));
G::load(As[0][1], A, {0, 0, block_row * 2 + 1, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds[0][1]));
G::load(Bs[0][0], B, {0, 0, bcol_base + block_col * 2, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds[0][0]));
G::load(Bs[0][1], B, {0, 0, bcol_base + block_col * 2 + 1, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds[0][1]));
G::load(scale_A_smem[0], scale_A_gl, {kk * tiles_M + block_row, 0, 0, 0});
G::load(scale_B_smem[0], scale_B_gl, {sb_base + kk * tiles_N + block_col, 0, 0, 0});
asm volatile("s_waitcnt vmcnt(0)");
asm volatile("s_waitcnt lgkmcnt(0)");
__builtin_amdgcn_s_barrier();
fp8e8m0_4 sa_h0 = pack_scales(scale_A_smem[0].data, a_row_h0);
fp8e8m0_4 sa_h1 = pack_scales(scale_A_smem[0].data, a_row_h1);
fp8e8m0_4 sb_h0 = pack_scales(scale_B_smem[0].data, b_row_h0);
fp8e8m0_4 sb_h1 = pack_scales(scale_B_smem[0].data, b_row_h1);
auto bs0 = subtile_inplace<REG_N, BLOCK_K>(Bs[0][0], {warp_n, 0}); load(b0, bs0);
auto bs1 = subtile_inplace<REG_N, BLOCK_K>(Bs[0][1], {warp_n, 0}); load(b1, bs1);
auto as0 = subtile_inplace<REG_M, BLOCK_K>(As[0][0], {warp_m, 0}); load(a, as0);
asm volatile("s_waitcnt lgkmcnt(0)");
mma_ABt_scaled(cA, a, b0, cA, &sa_h0, &sb_h0);
mma_ABt_scaled(cB, a, b1, cB, &sa_h0, &sb_h1);
auto as1 = subtile_inplace<REG_M, BLOCK_K>(As[0][1], {warp_m, 0}); load(a, as1);
asm volatile("s_waitcnt lgkmcnt(0)");
mma_ABt_scaled(cC, a, b0, cC, &sa_h1, &sb_h0);
mma_ABt_scaled(cD, a, b1, cD, &sa_h1, &sb_h1);
__builtin_amdgcn_s_barrier();
}
store(C, cA, {0, 0, block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + warp_n});
store(C, cB, {0, 0, block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
store(C, cC, {0, 0, block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + warp_n});
store(C, cD, {0, 0, block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
}
+141
View File
@@ -0,0 +1,141 @@
#include "kittens.cuh"
using namespace kittens;
#ifndef WGRAD_M
constexpr int WGRAD_M = 8192;
#endif
#ifndef WGRAD_N
constexpr int WGRAD_N = 8192;
#endif
#ifndef WGRAD_K
constexpr int WGRAD_K = 8192;
#endif
#ifndef WGRAD_E
constexpr int WGRAD_E = 8;
#endif
constexpr int NUM_WARPS = 8;
constexpr int WARPS_ROW = 2;
constexpr int WARPS_COL = 4;
constexpr int BLOCK_ROW = 256;
constexpr int BLOCK_COL = 256;
constexpr int BLOCK_K = 128;
constexpr int HALF_ROW = BLOCK_ROW / 2;
constexpr int HALF_COL = BLOCK_COL / 2;
constexpr int REG_M = BLOCK_ROW / WARPS_ROW / 2;
constexpr int REG_N = BLOCK_COL / WARPS_COL / 2;
using G = kittens::group<NUM_WARPS>;
__global__ __launch_bounds__(512, 2) void grouped_mxfp8_wgrad_kernel(bf16 *C_ptr, fp8e4m3 *A_ptr, fp8e4m3 *B_ptr,
fp8e8m0 *scale_A_ptr, fp8e8m0 *scale_B_ptr,
const int *__restrict__ expert_off) {
constexpr int M = WGRAD_M, N = WGRAD_N, K = WGRAD_K, E = WGRAD_E;
kittens::gl<fp8e4m3, 1, 1, N, M> A{A_ptr, nullptr, nullptr, nullptr, nullptr}; // g^T
kittens::gl<fp8e4m3, 1, 1, K, M> B{B_ptr, nullptr, nullptr, nullptr, nullptr}; // x^T
kittens::gl<bf16, 1, 1, E * N, K> C{C_ptr, nullptr, nullptr, nullptr, nullptr}; // grad_w, experts stacked
constexpr int m_blocks = M / BLOCK_K; // 128-wide blocks along the contraction (token) axis
constexpr int tiles_N = N / BLOCK_ROW;
constexpr int tiles_K = K / BLOCK_COL;
constexpr int NUM_THREADS = NUM_WARPS * WARP_THREADS;
kittens::gl<fp8e8m0, m_blocks * tiles_N, 1, 16, 64> scale_A_gl{scale_A_ptr, nullptr, nullptr, nullptr, nullptr};
kittens::gl<fp8e8m0, m_blocks * tiles_K, 1, 16, 64> scale_B_gl{scale_B_ptr, nullptr, nullptr, nullptr, nullptr};
using ST_A = st_fp8e4m3<HALF_ROW, BLOCK_K, st_16x128_s>;
using ST_B = st_fp8e4m3<HALF_COL, BLOCK_K, st_16x128_s>;
using ST_Scale = st<fp8e8m0, 16, 64, st_16x64_s>;
using RT_A = rt_fp8e4m3<REG_M, BLOCK_K>;
using RT_B = rt_fp8e4m3<REG_N, BLOCK_K>;
using RT_C = rt_fl<REG_M, REG_N, col_l, rt_16x16_s>;
__shared__ ST_A As[2];
__shared__ ST_B Bs[2];
__shared__ ST_Scale scale_A_smem, scale_B_smem;
RT_A a;
RT_B b0, b1;
RT_C cA, cB, cC, cD;
zero(cA); zero(cB); zero(cC); zero(cD);
const int wg = blockIdx.x;
const int e = wg / (tiles_N * tiles_K);
const int rem = wg % (tiles_N * tiles_K);
const int block_row = rem / tiles_K; // over grad_w rows (N)
const int block_col = rem % tiles_K; // over grad_w cols (K)
const int o0 = __builtin_amdgcn_readfirstlane(expert_off[e]);
const int kk0 = o0 / BLOCK_K;
const int nk = (__builtin_amdgcn_readfirstlane(expert_off[e + 1]) - o0) / BLOCK_K;
const int warp_m = warpid() / WARPS_COL;
const int warp_n = warpid() % WARPS_COL;
using T = fp8e4m3;
constexpr int bpt = ST_A::underlying_subtile_bytes_per_thread;
constexpr int bpm = bpt * NUM_THREADS;
constexpr int copies_A = HALF_ROW * BLOCK_K * sizeof(T) / bpm;
constexpr int copies_B = HALF_COL * BLOCK_K * sizeof(T) / bpm;
uint32_t sw_A[copies_A], sw_B[copies_B];
G::prefill_swizzled_offsets(As[0], A, sw_A);
G::prefill_swizzled_offsets(Bs[0], B, sw_B);
const T *a_base = (const T *)&A[{0, 0, 0, 0}];
const T *b_base = (const T *)&B[{0, 0, 0, 0}];
const int a_row_stride = A.template stride<2>() * sizeof(T);
const int b_row_stride = B.template stride<2>() * sizeof(T);
i32x4 a_srd = make_srsrc(a_base, (uint32_t)((uint64_t)N * a_row_stride), a_row_stride);
i32x4 b_srd = make_srsrc(b_base, (uint32_t)((uint64_t)K * b_row_stride), b_row_stride);
const int wid = warpid() % NUM_WARPS;
constexpr int elem_per_warp = (16 / sizeof(T)) * kittens::WARP_THREADS;
uint32_t a_lds_0 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t a_lds_1 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&As[1].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_0 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[0].data[0]) + wid * elem_per_warp * sizeof(T)));
uint32_t b_lds_1 = __builtin_amdgcn_readfirstlane(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&Bs[1].data[0]) + wid * elem_per_warp * sizeof(T)));
const int a_row_h0 = warp_m * REG_M;
const int a_row_h1 = HALF_ROW + warp_m * REG_M;
const int b_row_h0 = warp_n * REG_N;
const int b_row_h1 = HALF_COL + warp_n * REG_N;
#pragma unroll 1
for (int t = 0; t < nk; t++) {
const int kk = kk0 + t;
G::load(As[0], A, {0, 0, block_row * 2, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds_0));
G::load(As[1], A, {0, 0, block_row * 2 + 1, kk}, sw_A, a_srd, a_base, __builtin_amdgcn_readfirstlane(a_lds_1));
G::load(Bs[0], B, {0, 0, block_col * 2, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds_0));
G::load(Bs[1], B, {0, 0, block_col * 2 + 1, kk}, sw_B, b_srd, b_base, __builtin_amdgcn_readfirstlane(b_lds_1));
G::load(scale_A_smem, scale_A_gl, {kk * tiles_N + block_row, 0, 0, 0});
G::load(scale_B_smem, scale_B_gl, {kk * tiles_K + block_col, 0, 0, 0});
asm volatile("s_waitcnt vmcnt(0)");
asm volatile("s_waitcnt lgkmcnt(0)");
__builtin_amdgcn_s_barrier();
fp8e8m0_4 sa_h0 = pack_scales(scale_A_smem.data, a_row_h0);
fp8e8m0_4 sa_h1 = pack_scales(scale_A_smem.data, a_row_h1);
fp8e8m0_4 sb_h0 = pack_scales(scale_B_smem.data, b_row_h0);
fp8e8m0_4 sb_h1 = pack_scales(scale_B_smem.data, b_row_h1);
auto bs0 = subtile_inplace<REG_N, BLOCK_K>(Bs[0], {warp_n, 0}); load(b0, bs0);
auto bs1 = subtile_inplace<REG_N, BLOCK_K>(Bs[1], {warp_n, 0}); load(b1, bs1);
auto as0 = subtile_inplace<REG_M, BLOCK_K>(As[0], {warp_m, 0}); load(a, as0);
asm volatile("s_waitcnt lgkmcnt(0)");
mma_ABt_scaled(cA, a, b0, cA, &sa_h0, &sb_h0);
mma_ABt_scaled(cB, a, b1, cB, &sa_h0, &sb_h1);
auto as1 = subtile_inplace<REG_M, BLOCK_K>(As[1], {warp_m, 0}); load(a, as1);
asm volatile("s_waitcnt lgkmcnt(0)");
mma_ABt_scaled(cC, a, b0, cC, &sa_h1, &sb_h0);
mma_ABt_scaled(cD, a, b1, cD, &sa_h1, &sb_h1);
__builtin_amdgcn_s_barrier();
}
const int crow_base = e * (N / REG_M); // grad_w rows are experts stacked; store coord is in REG_M units
store(C, cA, {0, 0, crow_base + block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + warp_n});
store(C, cB, {0, 0, crow_base + block_row * WARPS_ROW * 2 + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
store(C, cC, {0, 0, crow_base + block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + warp_n});
store(C, cD, {0, 0, crow_base + block_row * WARPS_ROW * 2 + WARPS_ROW + warp_m, block_col * WARPS_COL * 2 + WARPS_COL + warp_n});
}
+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:])
+37 -58
View File
@@ -3,7 +3,7 @@
# A002 Function argument `input` is shadowing a Python builtin
# A006 Lambda argument `input` is shadowing a Python builtin
from tinygrad import Tensor, dtypes, Device
from tinygrad.uop.ops import Ops
from tinygrad.uop.ops import Ops, GroupOp
from tinygrad.helpers import getenv, prod, strides_for_shape, argfix
import torch.lib
TORCH_DEBUG = getenv("TORCH_DEBUG")
@@ -18,12 +18,16 @@ def _to_torch_device(device: str): return torch.device("tiny", int(device.partit
import torch.utils.cpp_extension
mod = torch.utils.cpp_extension.load(name="custom_device_extension", sources=[str(pathlib.Path(__file__).parent / "wrapped_tensor.cpp")])
# TODO: this assumes a contiguous source, so PERMUTE/EXPAND/PAD/FLIP are wrong. UOp.contiguous_view_offset does it
# properly, but it needs a device (these are deviceless)
alias_ops = GroupOp.Movement | {Ops.BITCAST, Ops.DETACH, Ops.AFTER}
def calculate_storage_offset(x: Tensor) -> int:
offset = 0
for u in x.uop.toposort():
if u.op == Ops.SHRINK:
offset, u = 0, x.uop
while u.op in alias_ops:
if u.op is Ops.SHRINK:
u_strides = strides_for_shape(u.src[0].shape)
for i, (start, _) in enumerate(u.marg): offset += start * u_strides[i]
u = u.src[0]
return offset
def wrap(x: Tensor, dev: torch.device|None=None) -> torch.Tensor:
x._strides = strides_for_shape(x.shape) # always recalculate
@@ -220,7 +224,7 @@ def _as_strided(tensor:Tensor, size, stride, storage_offset=0):
@torch.library.impl("aten::as_strided", "privateuseone")
def as_strided(tensor:torch.Tensor, size, stride, storage_offset=None):
storage_offset = storage_offset or tensor.storage_offset()
if storage_offset is None: storage_offset = tensor.storage_offset()
return _as_strided(tensor, size, stride, storage_offset)
@torch.library.impl("aten::_reshape_alias", "privateuseone")
@@ -228,16 +232,16 @@ def _reshape_alias(tensor:torch.Tensor, size, stride):
return _as_strided(tensor, size, stride)
@torch.library.impl("aten::empty_strided", "privateuseone")
def empty_strided(size, stride, dtype, layout=None, device=None, pin_memory=False):
def empty_strided(size, stride, dtype=None, layout=None, device=None, pin_memory=False):
if TORCH_DEBUG: print(f"empty_strided {size=} {stride=} {dtype=} {layout=} {device=} {pin_memory=}")
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype), device=_from_torch_device(device)).contiguous()
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
# TODO: should return with requested strides
return wrap(ret)
@torch.library.impl("aten::empty.memory_format", "privateuseone")
def empty_memory_format(size, dtype=None, layout=None, device=None, pin_memory=False, memory_format=None):
if TORCH_DEBUG: print(f"empty.memory_format {size=} {dtype=} {layout=} {device=} {pin_memory=} {memory_format=}")
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device)).contiguous()
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
return wrap(ret)
@torch.library.impl("aten::max_pool2d_with_indices", "privateuseone")
@@ -551,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
@@ -652,35 +658,6 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
"aten.unfold": Tensor.unfold,
}}
# operations that need inplace treatment (use _inplace_op instead of wrap_fxn) AKA return original tensor
inplace_ops = {
"aten.zero_",
"aten.fill_.Scalar",
"aten.fill_.Tensor",
"aten.add_.Tensor",
"aten.add_.Scalar",
"aten.mul_.Tensor",
"aten.mul_.Scalar",
"aten.floor_divide_.Tensor",
"aten.__ilshift__.Scalar",
"aten.__irshift__.Scalar",
"aten.relu_",
"aten.random_",
"aten.random_.from",
"aten.uniform_",
"aten.normal_",
"aten.logical_or_",
"aten.masked_fill_.Scalar",
"aten.masked_fill_.Tensor",
}
inplace_view_ops = {
"aten.squeeze_.dim",
"aten.unsqueeze_",
"aten.transpose_",
"aten.t_",
}
def wrap_fxn(k,f):
def nf(*args, **kwargs):
if TORCH_DEBUG:
@@ -694,7 +671,7 @@ def wrap_fxn(k,f):
else: raise RuntimeError(f"unknown output type {type(out)}")
return nf
def wrap_inplace(k,f):
def wrap_inplace(f):
def nf(*args, **kwargs):
orig = args[0]
args, kwargs = unwrap_args(args, kwargs)
@@ -702,7 +679,7 @@ def wrap_inplace(k,f):
return orig
return nf
def wrap_inplace_view_op(k,f):
def wrap_inplace_view_op(f):
def nf(*args, **kwargs):
orig = args[0]
args, kwargs = unwrap_args(args, kwargs)
@@ -735,11 +712,17 @@ def wrap_inplace_view_op(k,f):
return orig
return nf
# the aten schema says how an op is called: an inplace view retargets the view, a writable first arg is inplace,
# and a writable out arg must have come from tiny_backend_out so that wrap_out was applied
for k,v in tiny_backend.items():
if k in inplace_view_ops: wrapper = wrap_inplace_view_op
elif k in inplace_ops: wrapper = wrap_inplace
else: wrapper = wrap_fxn
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(wrapper(k,v))
name, _, overload = k.removeprefix("aten.").partition(".")
op = getattr(getattr(aten, name), overload or "default")
writes = [a.name for a in op._schema.arguments if a.alias_info is not None and a.alias_info.is_write]
if torch.Tag.inplace_view in op.tags: fxn = wrap_inplace_view_op(v)
elif writes == [op._schema.arguments[0].name] and op._schema.returns: fxn = wrap_inplace(v)
elif not writes or (writes == ["out"] and k in tiny_backend_out): fxn = wrap_fxn(k, v)
else: raise RuntimeError(f"{k} writes {writes}: expected an inplace first arg, or an out arg with {k} in tiny_backend_out")
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(fxn)
@torch.library.impl("aten::equal", "privateuseone")
def equal(x: torch.Tensor, y: torch.Tensor): return (x==y).all().item()
@@ -775,21 +758,17 @@ def native_batch_norm(input, weight, bias, running_mean, running_var, training,
@torch.library.impl("aten::native_batch_norm_backward", "privateuseone")
def native_batch_norm_backward(grad_out, input, weight, running_mean, running_var, save_mean, save_invstd, train, eps, output_mask):
grad_out_t, input_t = unwrap(grad_out), unwrap(input)
weight_t = unwrap(weight) if weight is not None else None
save_mean_t = unwrap(save_mean)
save_invstd_t = unwrap(save_invstd)
out = input_t.batchnorm(weight_t, None, save_mean_t, save_invstd_t)
targets = [t for t, m in zip([input_t, weight_t], output_mask[:2]) if t is not None and m]
if targets:
grads = out.gradient(*targets, gradient=grad_out_t)
grad_input = grads.pop(0) if output_mask[0] else None
grad_weight = grads.pop(0) if output_mask[1] and weight_t is not None else None
else:
grad_input, grad_weight = None, None
grad_bias = grad_out_t.sum(axis=tuple(x for x in range(grad_out_t.ndim) if x != 1)) if output_mask[2] else None
return (wrap(grad_input) if grad_input is not None else None,
wrap(grad_weight) if grad_weight is not None else None,
wrap(grad_bias) if grad_bias is not None else None)
dims, shape = tuple(x for x in range(input_t.ndim) if x != 1), (1, -1) + (1,)*(input_t.ndim-2)
# training differentiates the batch stats it was given, eval treats the running stats as constants
if train: mean, invstd = unwrap(save_mean), unwrap(save_invstd)
else: mean, invstd = unwrap(running_mean), unwrap(running_var).add(eps).rsqrt()
xhat = (input_t - mean.reshape(shape)) * invstd.reshape(shape)
grad_bias, grad_weight = grad_out_t.sum(axis=dims), (grad_out_t * xhat).sum(axis=dims)
grad_input = grad_out_t if not train else \
grad_out_t - (grad_bias.reshape(shape) + xhat * grad_weight.reshape(shape)) / (input_t.numel() // input_t.shape[1])
grad_input = grad_input * invstd.reshape(shape) * (unwrap(weight).reshape(shape) if weight is not None else 1)
return (wrap(grad_input) if output_mask[0] else None, wrap(grad_weight) if output_mask[1] else None,
wrap(grad_bias) if output_mask[2] else None)
# _pad_circular is not CompositeImplicitAutograd (unlike reflect/replicate pad)
# we need torch.autograd.Function with explicit AutogradPrivateUse1 registration
+71
View File
@@ -71,6 +71,48 @@ class TestTorchBackend(unittest.TestCase):
a = a.as_strided((1,1,5,5), (50,50,7,1), storage_offset=21)
np.testing.assert_equal(a.cpu().numpy().sum(-1), [[[115,150,185,220,255]]])
def test_storage_offset_of_computed_tensor(self):
# a computed result owns its storage, so a slice anywhere in its history must not shift the offset
a = torch.arange(8., device=device)
self.assertEqual((a[3:]+1).storage_offset(), 0)
def test_storage_offset_through_aliases(self):
a = torch.arange(8., device=device)[3:]
self.assertEqual(a.detach().storage_offset(), 3)
self.assertEqual(a.view(torch.int32).storage_offset(), 3)
torch.add(torch.ones(5, device=device), torch.ones(5, device=device), out=a)
self.assertEqual(a.detach().storage_offset(), 3)
@unittest.expectedFailure # TODO: storage offset assumes a contiguous source, use UOp.contiguous_view_offset
def test_storage_offset_non_contiguous_source(self):
a = torch.arange(12., device=device).reshape(3,4)
self.assertEqual(a.permute(1,0)[1:].storage_offset(), 1)
self.assertEqual(a.flatten()[3:].flip(0).storage_offset(), 0)
def test_as_strided_explicit_zero_offset(self):
# storage_offset=0 is a real offset, not "unspecified": it must not fall back to the input's own offset
a = torch.arange(6., device=device)
np.testing.assert_equal(a[3:].as_strided((2,), (1,), 0).cpu().numpy(), [0,1])
np.testing.assert_equal(a[3:].as_strided((2,), (1,)).cpu().numpy(), [3,4])
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)
@@ -316,6 +358,21 @@ class TestTorchBackend(unittest.TestCase):
assert b.shape == (4, 2, 3)
np.testing.assert_equal(b.cpu().numpy(), a.cpu().numpy().transpose(2, 0, 1))
def test_batchnorm_backward_realized_stats(self):
# the saved stats are a function of input in training, so grad_input must flow through them even when handed in realized.
# the backward eps is unused in training: torch differentiates the save_invstd it was given
x0, g0 = torch.randn(8, 4, 3, 3), torch.randn(8, 4, 3, 3)
def run(dev, bwd_eps):
x, go = x0.to(dev), g0.to(dev)
w, b = torch.linspace(0.5, 2.0, 4).to(dev), torch.zeros(4, device=dev)
rm, rv = torch.zeros(4, device=dev), torch.ones(4, device=dev)
out, sm, si = torch.ops.aten.native_batch_norm(x, w, b, rm, rv, True, 0.1, 1e-5)
grads = torch.ops.aten.native_batch_norm_backward(go, x, w, rm, rv, sm.clone().detach(), si.clone().detach(),
True, bwd_eps, [True,True,True])
return [t.cpu().numpy() for t in grads]
for bwd_eps in [1e-5, 0.3]:
for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
def test_batchnorm_unsqueeze(self):
bn = torch.nn.BatchNorm2d(4).to(device)
x = torch.randn(8, 4, 3, 3, device=device)
@@ -742,6 +799,20 @@ class TestTorchBackend(unittest.TestCase):
from tinygrad import Tensor
class TestBackendHelpers(unittest.TestCase):
def test_unwrap_rejects_foreign_tensor(self):
# unwrap casts to the tiny impl, so a tensor from another backend must be refused rather than reinterpreted
with self.assertRaises(RuntimeError): extra.torch_backend.backend.unwrap(torch.ones(4))
def test_update_metadata_rejects_foreign_tensor(self):
# resizing a tensor we don't own would expose memory past its allocation
t = torch.ones(4)
with self.assertRaises(RuntimeError): extra.torch_backend.backend.mod.update_metadata(t, [8], [1], 0)
self.assertEqual(t.shape, (4,))
def test_unwrap_parameter_and_detached(self):
# nn.Parameter and detach rebuild the base OpaqueTensorImpl, which unwrap still has to accept
extra.torch_backend.backend.unwrap(torch.nn.Parameter(torch.ones(4, device="tiny")))
extra.torch_backend.backend.unwrap(torch.ones(4, device="tiny").detach())
def test_calculate_storage_offset_no_shrink(self):
t = Tensor.ones(3, 4)
+9 -4
View File
@@ -124,16 +124,21 @@ at::Tensor wrap_tensor(py::object &py_obj, c10::ScalarType dtype, c10::DeviceInd
sizes, strides, storage_offset);
}
// shallow_copy_and_detach (nn.Parameter, aten.detach) rebuilds the base OpaqueTensorImpl, so that is the type every tiny tensor has
at::OpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>> *tiny_impl(const at::Tensor &tensor) {
auto* impl = dynamic_cast<at::OpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>>*>(tensor.unsafeGetTensorImpl());
TORCH_CHECK(impl != nullptr, "expected a tiny tensor, got a ", tensor.device().str(), " one. move it with .to(\"tiny\") first");
return impl;
}
py::object unwrap_tensor(const at::Tensor &tensor) {
auto* impl = tensor.unsafeGetTensorImpl();
auto* opaque_impl = static_cast<at::TinyOpaqueTensorImpl<std::shared_ptr<c10::SafePyObject>>*>(impl);
std::shared_ptr<c10::SafePyObject> tiny = opaque_impl->opaque_handle();
std::shared_ptr<c10::SafePyObject> tiny = tiny_impl(tensor)->opaque_handle();
return py::reinterpret_borrow<py::object>(tiny->ptr(getPyInterpreter()));
}
void update_metadata(const at::Tensor &tensor, const std::vector<int64_t> &sizes,
const std::vector<int64_t> &strides, int64_t storage_offset) {
auto* impl = tensor.unsafeGetTensorImpl();
auto* impl = tiny_impl(tensor);
impl->set_allow_tensor_metadata_change(true);
impl->set_sizes_and_strides(sizes, strides, storage_offset);
}
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.
+4 -5
View File
@@ -169,11 +169,10 @@ def run_program_emu(instructions: list, n_lanes: int = 1) -> WaveState:
return parse_output(bytes(out_buf), n_lanes)
def run_program_hw(instructions: list, n_lanes: int = 1) -> WaveState:
"""Run instructions on real AMD hardware via HIPCompiler and AMDProgram."""
from tinygrad.device import Device
from tinygrad.runtime.ops_amd import AMDProgram
"""Run instructions on real AMD hardware via HIPCompiler and the AMD runtime."""
from tinygrad.device import Device, TinyELF
from tinygrad.runtime.support.compiler_amd import HIPCompiler
from tinygrad.helpers import flat_mv
from tinygrad.helpers import Target, flat_mv
dev = Device["AMD"]
compiler = HIPCompiler(dev.arch) # type: ignore[attr-defined]
@@ -223,7 +222,7 @@ amdhsa.kernels:
"""
lib = compiler.compile(asm_src)
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
buf_sz = _out_bytes(n_lanes)
out_gpu = dev.allocator.alloc(buf_sz)
+3 -4
View File
@@ -5,7 +5,7 @@ gfx950 hardware when USE_HW=1.
"""
import ctypes, struct, unittest
import tinygrad.runtime.autogen.amd.cdna.ins as cdna
from tinygrad.helpers import flat_mv
from tinygrad.helpers import Target, flat_mv
from tinygrad.renderer.amd.dsl import NULL
from test.amd.hw.helpers import USE_HW, assemble
from test.mockgpu.amd.emu import run_asm
@@ -42,8 +42,7 @@ def _run_emu(instructions: list, out_reg: int = 2) -> int:
return out_buf[0]
def _run_hw(instructions: list, out_reg: int = 2) -> int:
from tinygrad.device import Device
from tinygrad.runtime.ops_amd import AMDProgram
from tinygrad.device import Device, TinyELF
from tinygrad.runtime.support.compiler_amd import HIPCompiler
dev = Device["AMD"]
@@ -86,7 +85,7 @@ amdhsa.kernels:
...
.end_amdgpu_metadata
"""
prg = AMDProgram(dev, "test", HIPCompiler(dev.arch).compile(asm_src))
prg = dev.runtime(TinyELF(HIPCompiler(dev.arch).compile(asm_src), "test", Target("AMD", arch=dev.arch), ()))
prg(global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
out = bytearray(LANES * 4)
dev.allocator._copyout(flat_mv(memoryview(out)), out_gpu)
+3 -4
View File
@@ -6,7 +6,7 @@ when USE_HW=1.
"""
import ctypes, unittest
from tinygrad.runtime.autogen.amd.rdna3.ins import *
from tinygrad.helpers import flat_mv
from tinygrad.helpers import Target, flat_mv
from test.amd.hw.helpers import USE_HW, assemble
from test.mockgpu.amd.emu import run_asm
@@ -37,8 +37,7 @@ def _run_wave64_emu(instructions: list, out_reg: int = 1) -> list[int]:
return list(out_buf)
def _run_wave64_hw(instructions: list, out_reg: int = 1) -> list[int]:
from tinygrad.device import Device
from tinygrad.runtime.ops_amd import AMDProgram
from tinygrad.device import Device, TinyELF
from tinygrad.runtime.support.compiler_amd import HIPCompiler
dev = Device["AMD"]
@@ -84,7 +83,7 @@ amdhsa.kernels:
.end_amdgpu_metadata
"""
lib = compiler.compile(asm_src)
prg = AMDProgram(dev, "test", lib) # type: ignore[arg-type]
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
out_gpu = dev.allocator.alloc(WAVE64 * 4)
prg(out_gpu, global_size=(1, 1, 1), local_size=(WAVE64, 1, 1), wait=True)
out = bytearray(WAVE64 * 4)
+3 -4
View File
@@ -5,7 +5,7 @@ real RDNA4 hardware when USE_HW=1.
"""
import ctypes, unittest
import tinygrad.runtime.autogen.amd.rdna4.ins as r4
from tinygrad.helpers import flat_mv
from tinygrad.helpers import Target, flat_mv
from tinygrad.renderer.amd.dsl import NULL
from test.amd.hw.helpers import USE_HW, assemble
from test.mockgpu.amd.emu import run_asm
@@ -36,8 +36,7 @@ def _run_emu(instructions: list, out_reg: int = 2) -> list[int]:
return list(out_buf)
def _run_hw(instructions: list, out_reg: int = 2) -> list[int]:
from tinygrad.device import Device
from tinygrad.runtime.ops_amd import AMDProgram
from tinygrad.device import Device, TinyELF
from tinygrad.runtime.support.compiler_amd import HIPCompiler
dev = Device['AMD']
@@ -85,7 +84,7 @@ amdhsa.kernels:
.end_amdgpu_metadata
"""
lib = compiler.compile(asm_src)
prg = AMDProgram(dev, 'test', lib)
prg = dev.runtime(TinyELF(lib, "test", Target("AMD", arch=dev.arch), ()))
out_gpu = dev.allocator.alloc(LANES * 4)
prg(out_gpu, global_size=(1, 1, 1), local_size=(LANES, 1, 1), wait=True)
out = bytearray(LANES * 4)
+1 -1
View File
@@ -36,7 +36,7 @@ def custom_add_var(A:UOp, B:UOp) -> UOp:
A,B = A.flatten(), B.flatten()
assert A.dtype == dtypes.uint32, f"buffer dtype must be uint32, got {A.dtype}"
threads = UOp.special(A.numel(), "lidx0")
var = UOp.param(2, dtypes.weakint, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
var = UOp.param(2, dtypes.int, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
insts = [
s_load_b128(s[4:7], s[0:1]),
s_load_b32(s[8], s[0:1], offset=0x10), # all threads load the same variable
+4 -3
View File
@@ -7,15 +7,16 @@ class TestMockGPUInvalidInstruction(unittest.TestCase):
"""Test that unsupported instructions raise immediately through the full MOCKGPU stack."""
test_code = '''
import struct
from dataclasses import replace
from tinygrad import Device, Tensor
from tinygrad.engine.realize import compile_linear
from tinygrad.runtime.ops_amd import AMDProgram
dev = Device["AMD"]
a = Tensor([1.0]).realize()
b = a + 1
linear = compile_linear(b.schedule_linear())
lib = bytearray(linear.src[-1].src[0].src[3].arg)
compiled_prg = linear.src[-1].src[0]
lib = bytearray(compiled_prg.src[3].arg)
# Find s_endpgm (0xBFB00000) and replace with V_MOVRELD_B32 (op=66) which has no pcode
# VOP1 encoding: bits[31:25]=0x7E, op=bits[16:9], so op=66 -> 66<<9 = 0x8400
@@ -27,7 +28,7 @@ for i in range(0, len(lib) - 4, 4):
break
assert found, "s_endpgm not found"
patched_prg = AMDProgram(dev, "patched", bytes(lib))
patched_prg = dev.runtime(replace(compiled_prg.to_elf(), name="patched", lib=bytes(lib)))
b.uop.buffer.allocate()
patched_prg(b.uop.buffer._buf, a.uop.buffer._buf, global_size=(1,1,1), local_size=(1,1,1))
dev.synchronize()
+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(UOp.const(dtypes.float, Invalid, shape=(2, 4)).clone(device=devs).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
+13 -7
View File
@@ -339,10 +339,12 @@ class TestOps(unittest.TestCase):
helper_test_op([], lambda: torch.ones(256,256).max(1)[0], lambda: Tensor.ones(256,256).max(1), forward_only=True)
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).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(
@@ -354,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
@@ -525,6 +527,7 @@ class TestOps(unittest.TestCase):
def test_add(self):
helper_test_op([(45,68), (45,68)], lambda x,y: x+y, Tensor.add)
helper_test_op([], lambda: torch.tensor(1)+0.5, lambda: Tensor(1)+0.5, forward_only=True)
helper_test_op([(45,68), (45,68)], lambda x,y: x+y)
helper_test_op([(), ()], lambda x,y: x+y)
def test_add3(self):
@@ -633,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]
@@ -646,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)
@@ -703,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")
@@ -772,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())
@@ -903,6 +906,7 @@ class TestOps(unittest.TestCase):
@unittest.skipIf(DEV.renderer == "NAK", "MUFU.SIN is not accurate enough")
def test_cos(self):
helper_test_op([(45,65)], lambda x: x.cos())
helper_test_op([], lambda: torch.tensor(2.0).cos(), lambda: Tensor(2).cos(), forward_only=True)
helper_test_op([()], lambda x: x.cos())
if not ((DEV.interface.startswith("MOCK") and Device.DEFAULT == "NV") or Device.DEFAULT == "WEBGPU"):
helper_test_op(None, lambda x: x.cos(), vals=[[math.nan, math.inf, -math.inf, 0.0]])
@@ -981,6 +985,8 @@ class TestOps(unittest.TestCase):
def test_exp(self):
helper_test_op([(45,65)], torch.exp, Tensor.exp)
# int scalar input: the python coefficients in the decomposition must stay float
helper_test_op([], lambda: torch.tensor(2.0).exp(), lambda: Tensor(2).exp(), forward_only=True)
helper_test_op(None, torch.exp, Tensor.exp, vals=[[math.inf, -math.inf, math.nan]])
helper_test_op([()], torch.exp, Tensor.exp)
def test_exp2(self):
+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
+14 -2
View File
@@ -1,7 +1,7 @@
import unittest
from tinygrad import Tensor, nn, Device
from tinygrad import Tensor, nn, Device, dtypes, Variable
from tinygrad.helpers import Context, GlobalCounters, getenv, PCONTIG, DEBUG
from tinygrad.uop.ops import graph_rewrite, PatternMatcher, UPat, Ops
from tinygrad.uop.ops import graph_rewrite, PatternMatcher, UPat, Ops, UOp
from tinygrad.codegen.opt import OptOps, Opt
from tinygrad.renderer.ptx import PTXRenderer
from tinygrad.renderer.nir import NIRRenderer
@@ -66,6 +66,18 @@ class TestRangeifyAssign(unittest.TestCase):
self.assertListEqual(lst2, B.permute(1, 0).tolist())
class TestRangeifyEdgeCase(unittest.TestCase):
def test_variable_stack_data(self):
# a bound-Variable STACK used as data gets ranges from its graph position
v = Variable("v", 0, 10).bind(3)
t = Tensor(UOp.stack(v, v+1).cast(dtypes.int)) + Tensor.arange(2)
self.assertListEqual(t.tolist(), [3, 5])
def test_variable_data_and_shape(self):
# the same Variable has a data edge through CAST and a structural edge through SHRINK
v = Variable("shared_v", 1, 10).bind(3)
t = Tensor.ones(10)[:v] * Tensor(v.cast(dtypes.float))
self.assertEqual(t.sum().item(), 9)
def test_matmul_relu_cat(self):
a = Tensor.ones(100, 512).contiguous().realize()
c = Tensor.ones(1, 512).contiguous().realize()
+3 -1
View File
@@ -363,9 +363,10 @@ 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")
def test_late_const_copy_folding(self):
a = Tensor.arange(3).clone().realize()
zeros = Tensor.zeros(3, buffer=False).realize()
@@ -406,6 +407,7 @@ class TestCopyFolding(unittest.TestCase):
assert t.uop.is_realized, f"didn't realize Tensor {t}"
self.assertListEqual(t.tolist(), [1.,1.,1.,1.])
@unittest.skip("same-device copies are no-ops")
def test_self_assign_same_device_copy(self):
a = Tensor.ones(4, 4).contiguous().realize()
# use copy_to_device to bypass Tensor.to() shortcircuit and force a real same-device COPY in the graph
+1
View File
@@ -464,6 +464,7 @@ class TestTinygrad(unittest.TestCase):
data = data + [-x for x in data]
np.testing.assert_allclose(Tensor(data, dtype=dtypes.int32).numpy(), np.array(data).astype(np.int32))
@unittest.skip("list elements are python scalars: numpy values in a list have no inferred dtype, use Tensor(np.array(data))")
def test_tensor_list_ndarray(self):
data = [np.array([1, 2, 3]), np.array([1, 2, 3]), np.array([1, 2, 3])]
np.testing.assert_equal(Tensor(data).numpy(), np.array(data))
+9 -6
View File
@@ -18,13 +18,16 @@ class TestTensorVariable(unittest.TestCase):
self.assertListEqual((vv * t).tolist(), [2, 2, 2])
except RuntimeError: pass
# TODO: a Variable PARAM lowers to int32, so a bound value that doesn't fit int32 truncates or fails to bind
@unittest.expectedFailure
def test_large_range_variable(self):
vv = Variable("b", 0, 2**40).bind(2**35)
# TODO: pm_lower_index_dtype lowers ALU PARAM to int32 unconditionally
try:
self.assertEqual(Tensor(vv).item(), 2**35)
except AssertionError:
pass
self.assertEqual(Tensor(Variable("b", 0, 2**40).bind(2**35)).item(), 2**35)
def test_variable_defers_like_a_literal(self):
vv = Variable("a", 1, 10).bind(2)
self.assertEqual(Tensor(vv).dtype, dtypes.weakint)
self.assertEqual((Tensor(vv) + Tensor([1], dtype=dtypes.int8)).dtype, dtypes.int8) # takes the concrete side, no widening
self.assertEqual(Tensor(vv).item(), 2) # a read commits at default_int
def test_variable_tensor_dtype_arg(self):
vv = Variable("a", 1, 10).bind(2)
-3
View File
@@ -3,7 +3,6 @@ from tinygrad import Tensor, UOp
from tinygrad.device import Device, Buffer, BufferSpec
from tinygrad.dtype import AddrSpace, dtypes
from tinygrad.engine.realize import run_linear
from tinygrad.renderer.isa.x86 import X86Renderer
from tinygrad.uop.ops import Ops, KernelInfo
def wait_loop_kernel(C:UOp) -> UOp:
@@ -82,7 +81,6 @@ def loop_in_loop_kernel(C:UOp) -> UOp:
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="loop_in_loop", opts_to_apply=()))
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
class TestWaitLoop(unittest.TestCase):
def test_wait_loop(self):
c = Tensor.empty(1, dtype=dtypes.int)
@@ -109,7 +107,6 @@ class TestWaitLoop(unittest.TestCase):
self.assertEqual(c.item(), 12)
@unittest.skipUnless(Device.DEFAULT in ("CPU", "AMD", "NV"), "need proper uncached=True handling")
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "loops are not supported in X86")
class TestVolatileLoops(unittest.TestCase):
def test_async_wait_ext(self):
sig_buf = Buffer(Device.DEFAULT, 1, dtypes.int, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
+4 -3
View File
@@ -1,7 +1,8 @@
import unittest
from tinygrad.device import CompileError, Device, BufferSpec
from tinygrad.device import CompileError, Device, BufferSpec, TinyELF
from tinygrad.helpers import Target
if Device.DEFAULT=="METAL":
from tinygrad.runtime.ops_metal import MetalDevice, MetalCompiler, MetalProgram
from tinygrad.runtime.ops_metal import MetalDevice, MetalCompiler
@unittest.skipIf(Device.DEFAULT!="METAL", "Metal support required")
class TestMetal(unittest.TestCase):
def test_alloc_oom(self):
@@ -48,7 +49,7 @@ kernel void r_5(device int* data0, const device int* data1, uint3 gid [[threadgr
""")
with self.assertRaises(RuntimeError):
compiled = compiled[:40] # corrupt the compiled program
MetalProgram(device, "r_5", compiled)
device.runtime(TinyELF(compiled, "r_5", Target("METAL"), ()))
def test_free(self):
size = 2**16
+7 -5
View File
@@ -1,18 +1,20 @@
import unittest
from unittest.mock import patch
from tinygrad import Device
from tinygrad.device import Buffer
from tinygrad.device import Buffer, TinyELF
from tinygrad.dtype import dtypes
from tinygrad.runtime.ops_cl import CLDevice, CLAllocator, CLCompiler, CLProgram
from tinygrad.helpers import Target
from tinygrad.runtime.ops_cl import CLDevice, CLAllocator, CLCompiler
@unittest.skipUnless(Device.DEFAULT == "CL", "Runs only on OpenCL")
class TestCLCompileCache(unittest.TestCase):
def test_compile_cached(self):
device = Device[Device.DEFAULT]
src = "__kernel void cached_test(__global int* a) { a[0] = 1; }"
CLProgram(device, name="cached_test", lib=src.encode())
obj = TinyELF(src.encode(), "cached_test", Target("CL"), ())
device.runtime(obj)
with patch.object(CLCompiler, 'compile', side_effect=RuntimeError("compile should not be called on cache hit")):
CLProgram(device, name="cached_test", lib=src.encode())
device.runtime(obj)
@unittest.skipUnless(Device.DEFAULT == "CL", "Runs only on OpenCL")
class TestCLError(unittest.TestCase):
@@ -27,7 +29,7 @@ class TestCLError(unittest.TestCase):
def test_invalid_kernel_name(self):
device = Device[Device.DEFAULT]
with self.assertRaises(RuntimeError) as err:
CLProgram(device, name="", lib="__kernel void test(__global int* a) { a[0] = 1; }".encode())
device.runtime(TinyELF(b"__kernel void test(__global int* a) { a[0] = 1; }", "", Target("CL"), ()))
assert str(err.exception) == "OpenCL Error -46: CL_INVALID_KERNEL_NAME"
def test_unaligned_copy(self):
+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)
+2 -2
View File
@@ -16,9 +16,9 @@ def _run(code:str, timeout:float=15.0) -> subprocess.CompletedProcess:
return subprocess.run(["python", "-c", code], env={**os.environ, "AMD": "1"}, capture_output=True, text=True, timeout=timeout)
def _run_asm(asm_src:str) -> subprocess.CompletedProcess:
return _run('from tinygrad.device import Device; from tinygrad.runtime.ops_amd import AMDProgram; '
return _run('from tinygrad.device import Device, TinyELF; from tinygrad.helpers import Target; '
'from tinygrad.runtime.support.compiler_amd import HIPCompiler; dev = Device["AMD"]; '
f'AMDProgram(dev, "test", HIPCompiler(dev.arch).compile("""{asm_src}"""))('
f'dev.runtime(TinyELF(HIPCompiler(dev.arch).compile("""{asm_src}"""), "test", Target("AMD", arch=dev.arch), ()))('
'dev.allocator.alloc(64), global_size=(1,1,1), local_size=(1,1,1), wait=True)')
def _verify_recovery() -> subprocess.CompletedProcess:
+7 -17
View File
@@ -5,7 +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
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",
@@ -13,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():
@@ -41,14 +34,11 @@ class TestGPUCrash(unittest.TestCase):
except Exception:
self.fail("Device not working before test")
def _run(self, code: str):
from tinygrad.runtime.ops_amd import AMDProgram
prg = AMDProgram(self.dev, "test", self.compiler.compile(assemble(code, is_cdna=self.is_cdna)))
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__":
+4 -3
View File
@@ -15,7 +15,7 @@ from tinygrad.codegen.late.linearizer import linearize
# decorator to skip slow tests by default, run with RUN_SLOW=1 to include them
slow = unittest.skipUnless(os.getenv("RUN_SLOW"), "slow test, set RUN_SLOW=1 to run")
from tinygrad.runtime.ops_python import PythonProgram, PythonRenderer, PythonCompiler
from tinygrad.runtime.ops_python import PythonRenderer
def full_rewrite(sink:UOp, ren:Renderer|None=None) -> UOp:
if ren is None: ren = Renderer(Target())
@@ -83,14 +83,15 @@ def timeit(fxn:Callable[..., T], *args, **kwargs) -> tuple[T, float]:
return ret, (time.perf_counter_ns()-st)*1e-6
def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None, vals:tuple[int, ...]=()):
allocator = Device['PYTHON'].allocator
dev = Device['PYTHON']
allocator = dev.allocator
bufs = []
for buf_dt, data in inputs or []:
bufs.append(buf:=allocator.alloc(len(data) * buf_dt.itemsize))
allocator._copyin(buf, memoryview(struct.pack(str(len(data)) + (buf_dt.fmt or ""), *data)))
g = UOp.param(0, uop.dtype, (1,))
prg = to_program(UOp.store(g.index(UOp.const(dtypes.int, 0)), uop).sink(arg=KernelInfo()), PythonRenderer(Target("PYTHON")))
prog = PythonProgram("run", PythonCompiler().compile(prg.src[2].arg))
prog = dev.runtime(prg.to_elf())
prog(out_buf:=allocator.alloc(uop.dtype.itemsize), *bufs, vals=vals)
return out_buf.cast(uop.dtype.fmt or "").tolist()[0]
+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=}")
+3 -3
View File
@@ -837,16 +837,16 @@ class Parser:
idx = addr
if dt in (dtypes.uint64, dtypes.int64, dtypes.float64):
val = _u32(0).cast(dtypes.uint64)
for i in range(8): val = val | (mindex(idx + _const(dtypes.int, i)).load().cast(dtypes.uint64) << _u64(i * 8))
for i in range(8): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint64) << _u64(i * 8))
elif dt in (dtypes.uint8, dtypes.int8):
val = mindex(idx).load().cast(dt)
elif dt in (dtypes.uint16, dtypes.int16, dtypes.short):
lo = mindex(idx).load().cast(dtypes.uint32)
hi = mindex(idx + _const(dtypes.int, 1)).load().cast(dtypes.uint32)
hi = mindex(idx + _const(adt, 1)).load().cast(dtypes.uint32)
val = (lo | (hi << _u32(8))).cast(dt)
else:
val = _u32(0)
for i in range(4): val = val | (mindex(idx + _const(dtypes.int, i)).load().cast(dtypes.uint32) << _u32(i * 8))
for i in range(4): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint32) << _u32(i * 8))
else:
idx = addr >> _const(addr.dtype, 2)
val = mindex(idx)
+31 -39
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
@@ -205,7 +205,8 @@ class TestTypePromotion(unittest.TestCase):
assert least_upper_dtype(dtypes.uint16, dtypes.int32) == dtypes.int32
assert least_upper_dtype(dtypes.int32, dtypes.uint32) == dtypes.int64
assert least_upper_dtype(dtypes.uint32, dtypes.int64) == dtypes.int64
assert least_upper_dtype(dtypes.int64, dtypes.uint64) == dtypes.uint64
# uint64 has no common integer supertype with any signed int (JAX JEP), they all defer up to weakfloat
for st in dtypes.sints: assert least_upper_dtype(st, dtypes.uint64) == dtypes.weakfloat
assert least_upper_dtype(dtypes.float16, dtypes.float32) == dtypes.float32
assert least_upper_dtype(dtypes.float32, dtypes.float64) == dtypes.float64
@@ -224,8 +225,9 @@ class TestTypePromotion(unittest.TestCase):
assert least_upper_dtype(dtypes.fp8e5m2, dtypes.uint64) == dtypes.fp8e5m2
def test_weakint_promo(self):
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.weakint)
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.int8)
assert least_upper_dtype(dtypes.weakint, dtypes.weakint) == dtypes.weakint
assert least_upper_dtype(dtypes.bool, dtypes.weakint) == dtypes.weakint
assert least_upper_dtype(dtypes.weakint, dtypes.int8) == dtypes.int8
def test_weakfloat_promo(self):
# weakfloat is a float, but is not one of dtypes.floats
@@ -243,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):
@@ -263,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
@@ -283,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)
@@ -294,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))
@@ -330,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):
@@ -418,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)
@@ -439,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):
@@ -451,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
+11 -13
View File
@@ -1,5 +1,5 @@
import unittest, threading, time, json
from unittest.mock import Mock
from unittest.mock import Mock, patch
class TestLLMServer(unittest.TestCase):
"""Integration tests using the real OpenAI client."""
@@ -66,20 +66,18 @@ class TestLLMServer(unittest.TestCase):
self.assertEqual(chunk.model, "test-model")
def test_stream_with_usage(self):
stream = self.client.chat.completions.create(
model="test",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
stream_options={"include_usage": True}
)
chunks = list(stream)
def generate(ids, **kwargs):
for token in (300, 301, 999):
ids.append(token)
yield token
with patch.object(self.mock_model, "generate", side_effect=generate):
chunks = list(self.client.chat.completions.create(
model="test", messages=[{"role": "user", "content": "Hello"}], stream=True, stream_options={"include_usage": True}))
last_chunk = chunks[-1]
self.assertIsNotNone(last_chunk.usage)
self.assertIsNotNone(last_chunk.usage.prompt_tokens)
self.assertIsNotNone(last_chunk.usage.completion_tokens)
self.assertIsNotNone(last_chunk.usage.total_tokens)
self.assertEqual(last_chunk.usage.prompt_tokens, 3)
self.assertEqual(last_chunk.usage.completion_tokens, 2)
self.assertEqual(last_chunk.usage.total_tokens, 5)
def test_multi_turn_conversation(self):
stream = self.client.chat.completions.create(
+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):
+5 -7
View File
@@ -5,6 +5,7 @@ import z3
from tinygrad.dtype import dtypes, ConstType, DType, Invalid
from test.helpers import get_uops
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, sym_infer
from tinygrad.uop.spec import spec_shared, type_verify
from tinygrad.uop.symbolic import sym, commutative, pm_simplify_valid, pm_move_where_on_load
from tinygrad.uop.validate import uops_to_z3
@@ -986,7 +987,7 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(cond.ne(False), 0, 1, "(x<2)")
def test_bitcast_chain(self):
a = Variable("a", 0, 3)
a = UOp.variable("a", 0, 3, dtype=dtypes.int32)
self.assertIs(graph_rewrite(a.bitcast(dtypes.float32).bitcast(a.dtype), sym), a)
def test_negation_in_where(self):
@@ -1330,7 +1331,8 @@ class TestInvalidIndex(unittest.TestCase):
def test_invalid_times_0(self):
ridx = Variable("ridx", 0, 10)
idx = (ridx<5).where(ridx, UOp.invalid())*0
self.assertIs(idx.simplify(), (ridx<5).where(0, UOp.invalid()), "multiplying an index by 0 should preserve the invalid")
self.assertIs(idx.simplify(), (ridx<5).where(UOp.const(dtypes.weakint, 0), UOp.invalid()),
"multiplying an index by 0 should preserve the invalid")
def test_alu_moves_inside_invalid(self):
ridx = Variable("ridx", 0, 10)
@@ -1384,11 +1386,7 @@ class TestMoveWhereOnLoad(unittest.TestCase):
idx = buf.index(a.valid(valid))
expr = cond.where(idx, idx.const_like(0))
out = graph_rewrite(expr, pm_move_where_on_load)
# any WHERE in the rewritten graph must have matched-dtype branches
for u in out.toposort():
if u.op is Ops.WHERE:
self.assertEqual(u.dtype, u.src[1].dtype, f"WHERE branch 1 dtype mismatch: {u}")
self.assertEqual(u.dtype, u.src[2].dtype, f"WHERE branch 2 dtype mismatch: {u}")
type_verify(out, spec_shared) # Invalid matches any dtype
class TestSymbolicRealWorld(unittest.TestCase):
def test_resnet_half(self):
+1 -2
View File
@@ -168,8 +168,7 @@ class TestVminVmaxProperties(unittest.TestCase):
def test_vmin_vmax_invalid_vconst(self):
x = UOp.const(dtypes.weakint, (0, 4, Invalid, Invalid))
self.assertLess(x.vmin, 0)
self.assertGreater(x.vmax, 4)
self.assertEqual((x.vmin, x.vmax), (0, 4))
class TestVminVmaxDivMod(unittest.TestCase):
def test_vmin_vmax_division_positive(self):
+29 -3
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
@@ -7,14 +7,13 @@ from tinygrad.dtype import dtypes, AddrSpace, ConstFloat, Invalid # noqa: F401
from tinygrad.device import Device
from tinygrad.uop.ops import Ops, ParamArg, UOp, UPat, dtype_from_uop, exec_alu, graph_rewrite, pm_lower_index_dtype # noqa: F401 # ParamArg used by eval(str(uop)) roundtrip tests
from tinygrad.uop.spec import spec_program, spec_shared, type_verify
from tinygrad.uop.symbolic import sym
from tinygrad.uop.symbolic import sym, pm_remove_invalid
from test.helpers import eval_uop, to_uops_list
class TestDTypeFromUOp(unittest.TestCase):
def test_broadcastable_promotion(self):
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
self.assertEqual(dtype_from_uop(Ops.MUL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.int32, 1)), None), dtypes.int32)
with self.assertRaises(KeyError): dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None)
def test_same_dtype_fast_path(self):
src = (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.weakint, 2))
@@ -45,6 +44,28 @@ class TestDTypeFromUOp(unittest.TestCase):
with self.assertRaises(RuntimeError): type_verify(UOp.const(weak, value).sink(), spec_program)
type_verify(UOp.const(concrete, value).sink(), spec_program)
def test_invalid_dtype_and_consumers(self):
invalid = UOp.invalid()
self.assertIs(invalid.dtype, dtypes.bool)
self.assertIs(UOp.const(dtypes.float32, Invalid), invalid)
self.assertIs((moved:=invalid.reshape((1,))).cast(dtypes.float32), moved)
scratch = Tensor.invalids(4, dtype=dtypes.float32)
self.assertEqual((scratch.dtype, next(u.dtype for u in scratch.uop.toposort() if u.op is Ops.BUFFER), next(u.dtype for u in scratch.uop.toposort()
if u.arg is Invalid)), (dtypes.float32, dtypes.float32, dtypes.bool))
invalid, value = UOp.invalid(), UOp.const(dtypes.float32, 1)
for u in (UOp(Ops.STACK, dtypes.float32, src=(value, invalid)), UOp(Ops.ADD, dtypes.float32, src=(value, invalid)),
UOp.const(dtypes.bool, True).where(value, invalid), UOp(Ops.CMPLT, src=(invalid, value)), UOp(Ops.CMPLT, src=(value, invalid)),
UOp.param(0, dtypes.float32, (4,)).index(invalid)): type_verify(u, spec_shared)
gate, value = UOp.param(0, dtypes.bool, ()), UOp.param(1, dtypes.float, ())
self.assertIs((out:=graph_rewrite(gate.where(value, UOp.invalid()), pm_remove_invalid)).src[2], UOp.const(dtypes.float, 0))
type_verify(out.sink(), spec_program)
def test_remove_invalid_stack_lanes(self):
stack = UOp(Ops.STACK, dtypes.half, (UOp.const(dtypes.half, 1), UOp.invalid()))
out = graph_rewrite(stack, pm_remove_invalid)
self.assertEqual(out.src, (UOp.const(dtypes.half, 1), UOp.const(dtypes.half, 0)))
type_verify(out.sink(), spec_program)
class TestLowerIndexDtype(unittest.TestCase):
def test_gated_shrink_lowers_to_selected_width(self):
# coalesce builds gated SHRINKs for masked vectorized loads; lowering must resolve them at the
@@ -94,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),],
+7
View File
@@ -26,8 +26,14 @@ def run_program(prg:UOp, bufs:list[Buffer]):
for u,b in zip(buf_uops, bufs): buffers[u] = b
run_linear(UOp(Ops.LINEAR, src=(prg.call(*buf_uops),)))
def _skip_unsupported_tc_dtypes(dtype_in:DType, dtype_out:DType):
supported_dtypes = Device[Device.DEFAULT].renderer.supported_dtypes()
if unsupported := [f"{name}={dtype}" for name,dtype in (("dtype_in", dtype_in), ("dtype_out", dtype_out)) if dtype not in supported_dtypes]:
raise unittest.SkipTest(f"tensor core requires unsupported renderer dtype: {', '.join(unsupported)}")
def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0,
ensure_triggered:bool=True):
_skip_unsupported_tc_dtypes(dtype_in, dtype_out)
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
r = a.matmul(b, dtype=dtype_out)
sched = r.schedule_linear()
@@ -47,6 +53,7 @@ def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, d
except KernelOptError: pass
def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0, use_tensor_cores:int=1):
_skip_unsupported_tc_dtypes(dtype_in, dtype_out)
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
np_a, np_b = a.numpy(), b.numpy()
r = a.matmul(b, dtype=dtype_out)
+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()
+30 -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,13 +84,15 @@ 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)
with self.assertRaises(RuntimeError): Tensor([np.int32(1)])
_assert_eq(Tensor([1.1]), dtypes.default_float, [1.1])
_assert_eq(Tensor.eye(0), dtypes.default_float, np.eye(0))
@@ -96,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)))
@@ -119,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)))
@@ -127,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))
@@ -144,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])
@@ -188,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
+67 -15
View File
@@ -1,9 +1,11 @@
import tempfile, unittest
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),))
@@ -137,5 +175,19 @@ class TestWeakMaterializationEntries(unittest.TestCase):
self.assertEqual(empty.tolist(), [])
class TestSignedUint64Weakfloat(unittest.TestCase):
# int64 and uint64 have no common integer supertype (JAX JEP), so the join defers to weakfloat instead of wrapping
def test_no_wrap(self):
r = Tensor([-1], dtype=dtypes.int64, device="CPU") + Tensor([1], dtype=dtypes.uint64, device="CPU")
self.assertEqual((r.dtype, r.item()), (dtypes.weakfloat, 0.0))
def test_weakfloat_lowers(self):
i64, u64 = Tensor([-1], dtype=dtypes.int64, device="CPU"), Tensor([3], dtype=dtypes.uint64, device="CPU")
r = i64 + u64 + Tensor([2], dtype=dtypes.float16, device="CPU") # a concrete consumer takes the join
self.assertEqual((r.dtype, r.cast(dtypes.float32).item()), (dtypes.half, 4.0))
self.assertEqual((i64 < u64).item(), True) # comparison meets at float
self.assertAlmostEqual((i64 + u64).sin().item(), math.sin(2), places=5) # Unary lowers before transcendental
if __name__ == "__main__":
unittest.main()
+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()
+16 -17
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:
@@ -142,11 +142,11 @@ pm_early_transform_tensor_graph = PatternMatcher([
(UPat((Ops.BITCAST, Ops.COPY, Ops.CONTIGUOUS), src=(UPat(GroupOp.Movement|{Ops.BUFFER}, name="src"),), name="c"), contiguous_mops_to_view),
# remove contiguous on movement ops before a copy on disk
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, allow_any_len=True, name="copy"), lambda x,copy:
copy.replace(src=(x,)+copy.src[1:], tag=None) if isinstance(x.device, str) and x.device.startswith("DISK") else None),
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.CONTIGUOUS).f(Ops.COPY, name="copy"), lambda x,copy:
copy.replace(src=(x,), tag=None) if isinstance(x.device, str) and x.device.startswith("DISK") else None),
# push copy past movement ops to disk
(UPat(GroupOp.Movement-{Ops.SHRINK, Ops.RESHAPE}, name="x").f(Ops.COPY, name="copy"), lambda x,copy:
x.replace(src=(copy.replace(src=(x.src[0],)+copy.src[1:], tag=None),)+x.src[1:]) \
x.replace(src=(copy.replace(src=(x.src[0],), tag=None),)+x.src[1:]) \
if isinstance(x.device, str) and x.device.startswith("DISK") else None),
# add CONTIGUOUS to tagged UOps
@@ -179,10 +179,9 @@ def finalize_after(ctx:AllocCtx, x:UOp):
def replace_input_buffer(ctx:AllocCtx, b:UOp):
ctx.replacements.append(b)
if b.op is Ops.BIND: return b.param_like(len(ctx.replacements)-1)
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
b._min_max if b.op is Ops.BIND else None, name=b.src[0].expr if b.op is Ops.BIND else None,
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL,
multiple_of=b.src[0].arg.multiple_of if b.op is Ops.BIND else None)
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
pm_finalize_call = PatternMatcher([
(UPat(Ops.AFTER, name="x"), finalize_after),
@@ -204,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
+52 -29
View File
@@ -1,14 +1,14 @@
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
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
from tinygrad.renderer import Renderer, Estimates
from tinygrad.renderer.isa import ISARenderer, IselContext, PreRegAllocContext
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.dtype import dtypes, AddrSpace, Invalid
# import all pattern matchers here
from tinygrad.codegen.gpudims import pm_add_gpudims
@@ -26,7 +26,7 @@ from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_c
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
from tinygrad.helpers import all_same, flatten, argsort, partition
from tinygrad.uop.ops import _align_left, _broadcast_shape, identity_element
from tinygrad.uop.ops import _broadcast_shape, identity_element
from tinygrad.schedule.rangeify import BufferizeOpts
def do_number_param(ctx:list[int], x:UOp):
@@ -88,25 +88,20 @@ expander2 = PatternMatcher([
(UPat(Ops.WMMA, name="u"), expand_wmma),
])+pm_flatten_range+mop_cleanup
def broadcast_binary(x:UOp):
def expand_broadcast(x:UOp):
shapes = [u._shape for u in x.src]
if any(s is None for s in shapes) or all_same(shapes): return None
shaped_aligned = _align_left(*shapes)
broadcasted = _broadcast_shape(*shapes)
src_reshaped = [u.reshape(shp).expand(broadcasted) for u,shp in zip(x.src, shaped_aligned)]
return x.replace(src=tuple(src_reshaped))
shape = _broadcast_shape(*shapes)
return x.replace(src=tuple([u.expand(shape) for u in x.src]))
def broadcast_and_devec_wmma(b:UOp):
shapes = [u.shape[:-1] for u in b.src]
if all_same(shapes): return None
shaped_aligned = _align_left(*shapes)
broadcasted = _broadcast_shape(*shapes)
src_reshaped = [u.reshape(shp+(u.shape[-1],)).expand(broadcasted+(u.shape[-1],))
for u,shp in zip(b.src, shaped_aligned)]
shape = _broadcast_shape(*shapes)
src_expanded = tuple([u.expand(shape+(u.shape[-1],)) for u in b.src])
src = []
for idx in itertools.product(*[range(i) for i in b.shape[:-1]]):
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
src.append(b.replace(src=tuple([x.index(*idx_c) for x in src_reshaped])))
src.append(b.replace(src=tuple([x.index(*idx) for x in src_expanded])))
return UOp.stack(*src).reshape(b.shape)
pm_wmma_add = PatternMatcher([
@@ -119,19 +114,19 @@ pm_wmma_add = PatternMatcher([
lambda wmma,reshape,permute,add: (wmma + add.permute(argsort(permute.arg)).reshape(wmma.shape)).reshape(reshape.shape).permute(permute.arg)),
])
unbroadcast = pm_wmma_add+PatternMatcher([
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), broadcast_binary),
pm_expand_broadcast = pm_wmma_add+PatternMatcher([
(UPat(GroupOp.Binary|GroupOp.Ternary|{Ops.STORE}, name="x"), expand_broadcast),
(UPat(Ops.WMMA, name="b"), broadcast_and_devec_wmma),
])
def do_devectorize(b:UOp):
if b.shape == (): return None
# broadcasting needs to be already unpacked
if not all_same([x.shape for x in b.src]): return None
# broadcasting needs to be already unpacked, Invalid matches any dtype and shape
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]
src.append(b.replace(src=tuple([x.index(*idx_c) for x in b.src])))
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)
def do_stack_wmma(u:UOp):
@@ -140,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))
@@ -166,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),
@@ -247,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),
@@ -260,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))
@@ -302,7 +323,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# **** optimizations are done, now we lower to actual code ****
sink = graph_rewrite(sink, symbolic_simple+unbroadcast+pm_add_loads, name="*** unbroadcast / add loads")
sink = graph_rewrite(sink, symbolic_simple+pm_expand_broadcast+pm_add_loads, name="*** expand broadcast / add loads")
# devectorize
sink = graph_rewrite(sink, symbolic_simple+devectorizer2, ctx=ren, name="devectorize2")
@@ -349,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)
@@ -408,8 +432,7 @@ def do_assemble(ctx:Renderer, prg:UOp, lin:UOp) -> UOp:
def do_render(ctx:Renderer, prg:UOp, lin:UOp) -> UOp:
src = ctx.render(list(lin.src))
new_arg = replace(prg.arg, aux=tuple(ctx.aux(list(lin.src)))) if ctx.has_aux else prg.arg
return prg.replace(src=prg.src + (UOp(Ops.SOURCE, arg=src),), arg=new_arg)
return prg.replace(src=prg.src + (UOp(Ops.SOURCE, arg=src),))
def do_compile(ctx:Renderer, prg:UOp, source:UOp) -> UOp|None:
if DEBUG >= 4: print(source.arg)
@@ -442,21 +465,21 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
elif ast.op is Ops.SINK:
assert isinstance(ast.arg, KernelInfo), "requires KernelInfo on arg to to_program"
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None)
prog_info = ProgramInfo.from_sink(full_sink)
prog_info = ProgramInfo.from_sink(full_sink, renderer.target)
# instruction selection
if isinstance(renderer, ISARenderer):
full_sink = graph_rewrite(full_sink, renderer.pre_isel_matcher, ctx=itertools.count(-1, -1), name="pre instruction selection", bottom_up=True)
full_sink = graph_rewrite(full_sink, renderer.isel_matcher, ctx=IselContext(full_sink), name="instruction selection", bottom_up=True)
prg = UOp(Ops.PROGRAM, src=(full_sink,), arg=prog_info)
else: raise RuntimeError(f"can't call to_program on {ast.op}")
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0]))
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0], renderer.target))
prg = graph_rewrite(prg, pm_to_program, ctx=renderer, name="linearize/render")
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
return prg
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]),))),
(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)
+2 -2
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,
@@ -226,7 +226,7 @@ def xlog2(d:UOp) -> UOp:
denormal_exp = 10 if d.dtype == dtypes.float16 else 64
FLT_MIN = d.const_like({dtypes.float16: 6.1e-5, dtypes.float32: 1e-4, dtypes.float64: 1e-4}[d.dtype])
is_denormal = d<FLT_MIN
a = is_denormal.where(d * (2 ** denormal_exp), d)
a = is_denormal.where(d * (2.0 ** denormal_exp), d)
e = ilogb2k(a * (1.0 / 0.75)).cast(a.dtype)
m = ldexp3k(a, -e)
+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
+8 -4
View File
@@ -2,6 +2,10 @@
from tinygrad.uop.ops import PatternMatcher, UPat, Ops
from tinygrad.dtype import Invalid, dtypes
def move_where_load(gate, l, a, w):
return l.replace(src=(l.src[0], l.vconst_like(0) if a.arg is Invalid else
a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(w.dtype)
pm_move_gates_from_index = PatternMatcher([
# for image idx (must be first)
(UPat.var("buf").index(UPat.var("gate").where(UPat.var("idx_y"), UPat(arg=Invalid)),
@@ -18,8 +22,8 @@ pm_move_gates_from_index = PatternMatcher([
.store(UPat.var("data")), lambda mop,gate,idx,data: mop.replace(src=(mop.src[0],idx)+mop.src[2:]).store(data, gate)),
# Where after gated load becomes alt value
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate", dtype=dtypes.bool), name="l").or_casted(), UPat.var("a")), lambda gate,l,a:
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
(UPat.var("gate").where(UPat.var("a"), UPat().load(UPat(), ~UPat.var("gate", dtype=dtypes.bool), name="l").or_casted()), lambda gate,l,a:
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate", dtype=dtypes.bool), name="l").or_casted(), UPat.var("a")).named("w"),
move_where_load),
(UPat.var("gate").where(UPat.var("a"), UPat().load(UPat(), ~UPat.var("gate", dtype=dtypes.bool), name="l").or_casted()).named("w"),
move_where_load),
])
+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
+50 -4
View File
@@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from collections import defaultdict
from typing import Any, Generic, TypeVar, Iterator, Generator, TYPE_CHECKING
from typing import Any, Generic, TypeVar, Iterator, Generator, Self, TYPE_CHECKING
import importlib, inspect, functools, pathlib, os, contextlib, re, atexit, pickle, decimal
from tinygrad.helpers import LRU, getenv, diskcache_get, diskcache_put, DEBUG, GlobalCounters, PROFILE, temp, colored
from tinygrad.helpers import Context, CCACHE, ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, cpu_events, ProfileEvent, ProfilePointEvent, suppress_finalizing
from tinygrad.helpers import select_by_name, select_first_inited, DEV, TracingKey, size_to_str, pluralize
from tinygrad.helpers import select_by_name, select_first_inited, DEV, TracingKey, size_to_str, pluralize, Target, unwrap
from tinygrad.dtype import DType, _to_np_dtype
if TYPE_CHECKING: from tinygrad.renderer import Renderer
@@ -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
@@ -283,12 +311,28 @@ class Compiler:
return lib
def disassemble(self, lib:bytes): pass
@dataclass
class TinyELF:
lib: bytes
name: str
target: Target
# tuple of (name, slot, dtype, shape)
signature: tuple[tuple[str|None, int, DType, tuple], ...]
class Program(Generic[DeviceType]):
def __init__(self, dev:DeviceType, obj:TinyELF): pass
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(),
wait=False) -> float|None: pass
class Compiled:
profile_events:list[ProfileEvent] = [ProfileDeviceEvent("CPU")] # NOTE: CPU is the default device.
def __init__(self, device:str, allocator:Allocator, renderers:list[type[Renderer]], runtime, graph=None, arch=None):
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, self.graph, self.renderers = device, allocator, runtime, graph, renderers or [Renderer]
self.device, self.allocator, self.runtime_t, self.graph, self.renderers = device, allocator, runtime, graph, renderers or [Renderer]
self.arch = arch
self.cached_renderer:dict[Any, Renderer] = {}
@@ -300,6 +344,8 @@ class Compiled:
if (ret:=self.renderer.compiler) is None: raise RuntimeError(f"no compiler for {self.device}")
return ret
def runtime(self, obj:TinyELF) -> Program[Self]: return unwrap(self.runtime_t)(self, obj)
def _renderer_name(self, r:type[Renderer]) -> str:
return r.__name__.upper().removesuffix("RENDERER").removeprefix(devname:=self.device.split(':')[0].upper()) or devname
+22 -21
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]:
@@ -111,7 +111,7 @@ class dtypes:
return {dtypes.float16: (5, 10), dtypes.bfloat16: (8, 7), dtypes.float32: (8, 23), dtypes.float64: (11, 52),
dtypes.fp8e4m3: (4, 3), dtypes.fp8e5m2: (5, 2), dtypes.fp8e4m3fnuz: (4, 3), dtypes.fp8e5m2fnuz: (5, 2)}[dtype]
void: Final[DType] = DType.new(-1, 0, "void", None)
weakint: Final[DType] = DType.new(0, 800, "weakint", None) # NOTE: not in the promo lattice: index math never mixes dtypes
weakint: Final[DType] = DType.new(0, 800, "weakint", None) # the weak int position in the promo lattice
bool: Final[DType] = DType.new(0, 1, "bool", '?')
int8: Final[DType] = DType.new(1, 8, "signed char", 'b')
uint8: Final[DType] = DType.new(2, 8, "unsigned char", 'B')
@@ -129,7 +129,6 @@ class dtypes:
fp8e4m3fnuz: Final[DType] = DType.new(10, 8, "float8_e4m3fnuz", None)
fp8e5m2fnuz: Final[DType] = DType.new(11, 8, "float8_e5m2fnuz", None)
float16: Final[DType] = DType.new(12, 16, "half", 'e')
# bfloat16 has higher priority than float16, so least_upper_dtype(dtypes.int64, dtypes.uint64) = dtypes.float16
bfloat16: Final[DType] = DType.new(13, 16, "__bf16", None)
float32: Final[DType] = DType.new(14, 32, "float", 'f')
float64: Final[DType] = DType.new(15, 64, "double", 'd')
@@ -139,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)
@@ -153,24 +154,23 @@ class dtypes:
uints = (uint8, uint16, uint32, uint64)
sints = (int8, int16, int32, int64)
ints = uints + sints
weaks = (weakfloat,)
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:
# TODO: weakint
return dtypes.default_float if dtype == dtypes.weakfloat else dtype
return {dtypes.weakint: dtypes.default_int, dtypes.weakfloat: dtypes.default_float}.get(dtype, dtype)
# https://jax.readthedocs.io/en/latest/jep/9407-type-promotion.html
# we don't support complex type
# TODO: weakint
promo_lattice = { dtypes.bool: [dtypes.int8, dtypes.uint8], dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
dtypes.int64: [dtypes.uint64], dtypes.uint8: [dtypes.int16, dtypes.uint16], dtypes.uint16: [dtypes.int32, dtypes.uint32],
promo_lattice = { dtypes.bool: [dtypes.weakint], dtypes.weakint: [dtypes.int8, dtypes.uint8],
dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
dtypes.int64: [dtypes.weakfloat], dtypes.uint8: [dtypes.int16, dtypes.uint16], dtypes.uint16: [dtypes.int32, dtypes.uint32],
dtypes.uint32: [dtypes.int64, dtypes.uint64], dtypes.uint64: [dtypes.weakfloat],
dtypes.weakfloat: [dtypes.fp8e4m3, dtypes.fp8e5m2, dtypes.fp8e4m3fnuz, dtypes.fp8e5m2fnuz],
dtypes.fp8e4m3: [dtypes.float16, dtypes.bfloat16], dtypes.fp8e5m2: [dtypes.float16, dtypes.bfloat16],
@@ -183,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
@@ -209,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")))
@@ -304,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)
+18 -20
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, ProgramInfo
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
@@ -112,9 +112,8 @@ def optimize_local_size(call:UOp, prg:UOp) -> UOp|None:
runtime_cache: dict[tuple[bytes, str], Any] = {}
def get_runtime(device:str, ast:UOp, cache=True):
assert ast.op is Ops.PROGRAM and isinstance(ast.arg, ProgramInfo), "get_runtime should only be called with a PROGRAM ast"
if (runtime:=runtime_cache.get(key:=(ast.key, device))) is None:
runtime = Device[device].runtime(ast.arg.function_name, ast.src[3].arg, *ast.arg.aux, runtimevars=ast.arg.runtimevars, prg=ast)
runtime = Device[device].runtime(ast.to_elf())
if cache: runtime_cache[key] = runtime
return runtime
@@ -149,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)
@@ -208,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:
@@ -261,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)
@@ -288,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",
+3 -3
View File
@@ -38,7 +38,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
def q_to_uint8(t: Tensor, b: int) -> Tensor:
# TODO: rewrite with arange?
shift_tensor, bitmask = Tensor.const(t.dtype, tuple(2**(i*b) for i in range(8//b))), 0xff >> (8 - b)
return t.unsqueeze(-1).expand((*t.shape,8//b)).div(shift_tensor, rounding_mode="trunc").bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
return t.unsqueeze(-1).div(shift_tensor, rounding_mode="trunc").bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
if (nelements_nbytes := _GGML_QUANT.get(ggml_type)) is not None:
from tinygrad.runtime.autogen import ggml_common as _ggml
@@ -68,7 +68,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
if ggml_type == 14:
xl, xh = q_to_uint8(blocks[:,:128].reshape((-1, 2, 64)), 4), q_to_uint8(blocks[:,128:192].reshape((-1, 2, 32)), 2).lshift(4)
scales = blocks[:,192:208].bitcast(dtypes.int8).unsqueeze(-1).expand((-1, 16, 16)).reshape((-1, 256))
d = blocks[:,-2:].bitcast(dtypes.float16).cast(dtypes.float32).expand((-1, 256))
d = blocks[:,-2:].bitcast(dtypes.float16).cast(dtypes.float32)
return d * (xl.bitwise_or(xh).bitcast(dtypes.int8) - 32).flatten(-2) * scales
if ggml_type == 18:
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
@@ -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 -2
View File
@@ -68,6 +68,7 @@ class Handler(HTTPRequestHandler):
else: self.send_data((pathlib.Path(__file__).parent / "chat.html").read_bytes(), content_type="text/html")
def run_model(self, ids:list[int], model_name:str, include_usage=False, max_tokens:int|None=None, temperature:float=0.0):
model, tok = self.server.model, self.server.tok
prompt_tokens = len(ids)
cache_start_pos = model.get_start_pos(ids)
stderr_log(f"in:{colored(f'{cache_start_pos:5d}', 'green')} +{len(ids)-cache_start_pos:5d} {colored('--', 'BLACK')} ")
tmpl = {"id":f"chatcmpl-{uuid.uuid4().hex[:24]}", "object":"chat.completion.chunk", "created":int(time.time()), "model":model_name}
@@ -79,7 +80,7 @@ class Handler(HTTPRequestHandler):
dec = tok.stream_decoder()
router = StreamRouter()
for next_id in model.generate(ids, temperature=temperature):
if len(out) == 0: stderr_log(f"prefill:{(len(ids)-cache_start_pos)/((pt:=time.perf_counter())-st):4.0f} tok/s {colored('--', 'BLACK')} ")
if len(out) == 0: stderr_log(f"prefill:{(prompt_tokens-cache_start_pos)/((pt:=time.perf_counter())-st):4.0f} tok/s {colored('--', 'BLACK')} ")
if tok.is_end(next_id): break
out.append(next_id)
for field, delta in router.route(dec(next_id)): yield chunk({field:delta})
@@ -101,7 +102,8 @@ class Handler(HTTPRequestHandler):
if finish_reason == "stop": finish_reason = "tool_calls"
yield {"choices": [{"index":0, "delta":{},"finish_reason":finish_reason}], **tmpl}
if include_usage:
yield {"choices": [], "usage": {"prompt_tokens": len(ids), "completion_tokens": len(out), "total_tokens": len(ids) + len(out)}, **tmpl}
yield {"choices": [], "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": len(out),
"total_tokens": prompt_tokens + len(out)}, **tmpl}
et = time.perf_counter()
stderr_log(f"gen:{len(out)/(et-pt) if len(out) > 1 else 0:4.0f} tok/s {colored('--', 'BLACK')} "
f"out:{len(out):5d} {colored('--', 'BLACK')} total:{et-st:6.2f}s\n")
+4 -2
View File
@@ -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:
@@ -80,7 +80,9 @@ class CreationMixin(DTypeMixin, MovementMixin):
dt = to_dtype(dtype) if dtype is not None else fill_value.dtype if isinstance(fill_value, UOp) else dtypes.from_py(fill_value)
val = cls.const(dt, fill_value)
val = val.reshape((1,)*len(new_shape)).expand(new_shape)
return val.clone(device=device) if buffer else val
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:
"""
+2 -2
View File
@@ -1,5 +1,5 @@
from typing import TYPE_CHECKING, Self
from tinygrad.dtype import DType, DTypeLike, dtypes, to_dtype
from tinygrad.dtype import DType, DTypeLike, dtypes, to_dtype, Invalid
from tinygrad.uop import Ops
if TYPE_CHECKING:
@@ -30,7 +30,7 @@ class DTypeMixin:
print(t.dtype, t.numpy())
```
"""
return self if self.dtype == (dt:=to_dtype(dtype)) else self._wrap_uop(self._uop.alu(Ops.CAST, arg=dt))
return self if self.dtype == (dt:=to_dtype(dtype)) or self._uop.base.arg is Invalid else self._wrap_uop(self._uop.alu(Ops.CAST, arg=dt))
def bitcast(self, dtype:DTypeLike) -> Self:
"""
+12 -13
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))
@@ -200,7 +199,7 @@ class ElementwiseMixin(CreationMixin):
```
"""
a, b = self._broadcasted(x, reverse)
if dtypes.is_int(a.dtype): return a.alu(Ops.FLOORMOD, b)
if dtypes.is_int(a.dtype) and dtypes.is_int(b.dtype): return a.alu(Ops.FLOORMOD, b)
return a - a.div(b, rounding_mode="floor") * b
def fmod(self, x: Self | ConstType) -> Self:
@@ -213,7 +212,7 @@ class ElementwiseMixin(CreationMixin):
```
"""
a, b = self._broadcasted(x)
if dtypes.is_int(a.dtype): return a.alu(Ops.CMOD, b)
if dtypes.is_int(a.dtype) and dtypes.is_int(b.dtype): return a.alu(Ops.CMOD, b)
return a - a.div(b, rounding_mode="trunc") * b
def div(self, x: Self | ConstType, reverse: bool = False, rounding_mode: Literal["trunc", "floor"] | None = None) -> Self:
@@ -237,9 +236,10 @@ class ElementwiseMixin(CreationMixin):
```
"""
a, b = self._broadcasted(x, reverse)
if dtypes.is_int(a.dtype):
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))))),

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