Compare commits

..
Author SHA1 Message Date
Comma Device 6d082d46ce fp32 paeak 2026-05-16 03:03:59 +00:00
Comma Device 9169a9b674 thread128 support 2026-05-16 03:00:57 +00:00
Comma Device a4b9f67153 add qcom_fp16_mad_peak.py 2026-05-16 02:41:31 +00:00
sirhcmandGitHub 79c0ae5b89 metal: arch is GPU family (#16223) 2026-05-15 21:22:48 -04:00
sirhcmandGitHub 2c61f65211 cl: device extensions in arch (#16220) 2026-05-15 18:59:20 -04:00
George HotzandGitHub 2549b14ec2 fix caformer onnx run (#16222) 2026-05-15 15:08:36 -07:00
George HotzandGitHub 2570bded8b update spec for LOAD (#16221)
* add load to the spec

* can
2026-05-15 14:46:00 -07:00
chenyuandGitHub d62c1d83c0 remove Tensor.eye override (#16219)
* remove Tensor.eye override

was only needed for requires_grad arg

* README
2026-05-15 15:40:34 -04:00
chenyuandGitHub 07a172dbbb remove noop requires_grad_ calls (#16213) 2026-05-15 13:31:10 -04:00
chenyuandGitHub c6cf9e8f0c remove test_svd_nonfull_5_5 (#16217)
flaky, kinda overlap with test_svd_general
2026-05-15 13:10:02 -04:00
qazalandGitHub d54fa86b71 viz/cli: select all calls in graph by default (#16214) 2026-05-15 21:01:44 +09:00
nimlgenandGitHub 28b98e529d nv: move structs to vram (#16184)
* nv: vram

* x

* 4090

* x

* move and sysmem on macos

* x

* remove hp
2026-05-15 13:41:42 +03:00
chenyuandGitHub 409bb0c9ad requires_grad cannot be None (#16212)
final goal is to remove requires_grad, first change the default to True, and don't allow None
2026-05-15 02:01:04 -04:00
sirhcmandGitHub c7870f11ff mesa: suggest curl install tip (#16211) 2026-05-15 00:29:06 -04:00
chenyuandGitHub a612b88abb better assert when setitem a refed tensor (#16210)
also decouple from requires_grad
2026-05-14 23:40:29 -04:00
chenyuandGitHub a75c14f010 some setitem tests (#16209) 2026-05-14 22:36:25 -04:00
sirhcmandGitHub 891a1ae7c2 onnx: remove dtype_fallback (#15717) 2026-05-14 22:06:57 -04:00
wozeparrotandGitHub b4d267dfd4 llama: only save when small (#16208) 2026-05-14 17:46:29 -07:00
chenyuandGitHub ffa1aac7b1 gradient for STORE/AFTER ala clone (#16205) 2026-05-14 20:17:27 -04:00
chenyuandGitHub 09096ea565 test_gradient_through_clone (#16203)
backward through clone crashes now
2026-05-14 19:26:47 -04:00
George HotzandGitHub d4dcd8487b aggressive shape check to prepare for broadcasting (#16202)
* add implicit broadcasting to shape

* NOOP/ALLREDUCE fixes
2026-05-14 16:15:44 -07:00
George HotzandGitHub 83ec66da34 fix a fastdiv edge case (#16199) 2026-05-14 13:12:18 -07:00
nimlgenandGitHub 62ea73719d hcq2: share more with graph (#16196)
* share more with graph

* comment
2026-05-14 22:28:11 +03:00
George HotzandGitHub 3b8cc31759 disable fast idiv by default, it's broken (#16197)
* disable fast idiv by default, it's broken

* fix fast idiv tests
2026-05-14 11:48:27 -07:00
sirhcmandGitHub 8f811649ff better compiler_cpu invalid arch errors (#16194) 2026-05-14 14:36:14 -04:00
qazalandGitHub f03a7fd6d1 viz/cli: readable uop json (#16195)
* viz/cli: readable uop json repr

* work

* better
2026-05-14 21:33:10 +09:00
C TandGitHub 1b779a9058 add gelu approximate="none" (match pytorch) (#16162)
* add gelu approximate="none" (match pytorch)

* lint

* pass through onnx Gelu approximate

* type annotate

* explicit math.sqrt

* keep tinygrad's gelu approximate="tanh" default
2026-05-13 18:53:24 -07:00
chenyuandGitHub dd9187d9ee minor hash cleanups (#16190)
same kernels
2026-05-13 20:59:24 -04:00
wozeparrotandGitHub 88ac2ac1fd llama: cleanups (#16189) 2026-05-13 17:08:06 -07:00
sirhcmandGitHub 9a365d9978 ci: fix null image tests (#16188) 2026-05-13 18:00:05 -04:00
nimlgenandGitHub ad1fb7c981 hcq2: graph (#16186)
* keep this for now

* early graph
2026-05-13 22:49:43 +03:00
chenyuandGitHub 3f9f6a51b2 minor image_conv2d cleanup (#16187)
remove some no-op slices
2026-05-13 15:47:40 -04:00
b1tgandGitHub 59c34b9fe0 llm: precise device (#16159)
* llm: precise device

* llm: pass device to precompute_freqs_cis
2026-05-12 21:16:42 -07:00
b1tgandGitHub 3c806ff406 clean up gguf (#16160) 2026-05-12 21:16:10 -07:00
wozeparrotandGitHub e97f2c1114 llama: only gemm + fa custom kernel (#16180)
* llama: tie store to grad directly

* llama: set mp flags

* llama: non fused grad fp8 quantize path
2026-05-12 21:03:49 -07:00
chenyuandGitHub 38d407fd58 simplify svd more (#16181)
all the slowness is scheduling
2026-05-12 23:48:22 -04:00
sirhcmandGitHub f1fdd2ccec ci: add IMAGE=1 compile-only tests (#16182)
* ci: add IMAGE=1 compile-only tests

* fix
2026-05-12 23:40:32 -04:00
George HotzandGitHub faf7fb7513 update nir renderer for new image style (#16179)
* update nir renderer for new image style

* don't cast image indexes
2026-05-12 20:25:01 -07:00
sirhcmandGitHub 7d0c5ab689 ci: ocelot needs nvcc on linux (#16178)
* ci: ocelot needs nvcc on linux

* cudart
2026-05-12 23:13:48 -04:00
chenyuandGitHub 32138c2418 svd to mixin (#16175) 2026-05-12 22:29:01 -04:00
George HotzandGitHub 69e1f3b551 remove vec2 from image in gater (#16165)
* remove vec2 from image in gater

* only simple idx

* fix python with new image style

* fix vconst

* just vconst and stack

* cast to int there

* fix for const

* fix process replay
2026-05-12 19:25:52 -07:00
chenyuandGitHub 2172363be5 don't use Tensor indexing in svd (#16174)
prepare mixin, also about 4X faster for 8x8 input
2026-05-12 21:56:19 -04:00
chenyuandGitHub 420a08c6d1 qr to mixin (#16173) 2026-05-12 21:23:25 -04:00
chenyuandGitHub c6a82fe927 functional qr and svd (#16172)
no clone and setitem, will move to mixin next. slightly faster but still quite slow
2026-05-12 19:12:08 -04:00
sirhcmandGitHub 3844a31f87 ci: untangle cuda/ocelot, less apt (#16171)
* ci: untangle cuda/ocelot, less apt

* ldconfig
2026-05-12 18:14:03 -04:00
sirhcmandGitHub 316607f004 dsp: don't use docker in ci (#16167)
* dsp: don't use docker in ci

* add setup script for macos docker
2026-05-12 17:11:03 -04:00
chenyuandGitHub bdcdf1f1a1 jittable masked_select and nonzero (#16170)
* jittable masked_select and nonzero

make jittable with `size=`, matches jax

* COMPILE_ONLY
2026-05-12 16:39:36 -04:00
wozeparrotandGitHub a613bcfc6d allow after on contiguous in spec (#16169)
* feat: allow after on contiguous

* feat: add test
2026-05-12 13:11:44 -07:00
chenyuandGitHub 7c3e3fa154 fix empty input for masked_select and nonzero (#16168) 2026-05-12 15:36:51 -04:00
chenyuandGitHub da3b7e89a4 atol in test_custom_kernel_multi_output_backward_interacting (#16166) 2026-05-12 14:42:12 -04:00
chenyuandGitHub 25583f6dc1 fix cumsum dtype for 0d input (#16164) 2026-05-12 14:18:08 -04:00
George HotzandGitHub 64c81dfd24 add all codegen stages to spec_tensor (#16163) 2026-05-12 10:35:38 -07:00
84 changed files with 1170 additions and 739 deletions
+32 -8
View File
@@ -49,6 +49,10 @@ inputs:
description: "Install tinydreno"
required: false
default: 'false'
qemu:
description: "Install qemu"
required: false
default: 'false'
runs:
using: "composite"
steps:
@@ -129,7 +133,7 @@ runs:
# ******************* apt *******************
- name: Setup apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
shell: bash
run: |
sudo chown -R $USER:$USER /var/cache/apt/archives
@@ -161,7 +165,7 @@ runs:
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
- name: Compute Package List + Hash
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
id: apt-pkgs
shell: bash
run: |
@@ -177,10 +181,10 @@ runs:
if [[ "${{ inputs.amd }}" == "true" ]]; then
pkgs+=" hsa-rocr comgr hsa-rocr-dev liburing-dev libibverbs-dev libc6-dev"
fi
# **** CUDA ****
if [[ "${{ inputs.cuda }}" == "true" ]]; then
# **** ocelot (dependencies) ****
if [[ "${{ inputs.ocelot }}" == "true" ]]; then
pkgs+=" git g++ cmake ninja-build llvm-15-dev zlib1g-dev libglew-dev \
flex bison libfl-dev libboost-thread-dev libboost-filesystem-dev nvidia-cuda-toolkit-gcc libzstd-dev"
flex bison libfl-dev libboost-thread-dev libboost-filesystem-dev libzstd-dev"
fi
# **** WebGPU (dependencies for software-based vulkan) ****
if [[ "${{ inputs.webgpu }}" == "true" ]]; then
@@ -190,25 +194,29 @@ runs:
if [[ "${{ inputs.llvm }}" == "true" ]]; then
pkgs+=" libllvm20 clang-20 lld-20"
fi
# **** QEMU ****
if [[ "${{ inputs.qemu }}" == "true" ]]; then
pkgs+=" qemu-user-static"
fi
echo "pkgs=$pkgs" >> "$GITHUB_OUTPUT"
echo "hash=$(echo -n "$pkgs" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Cache apt (PR)
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true') && github.event_name == 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name == 'pull_request'
uses: actions/cache/restore@v4
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Cache apt
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true') && github.event_name != 'pull_request'
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true') && github.event_name != 'pull_request'
uses: actions/cache@v5
with:
path: /var/cache/apt/archives/
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
- name: Run apt Update + Install
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.cuda == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true')
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.ocelot == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true')
shell: bash
run: |
sudo apt -qq update || true
@@ -239,6 +247,17 @@ runs:
jq -r '.assets[] | select(.name == "libamd_comgr.dylib").browser_download_url' | \
sudo xargs curl -fL -o /usr/local/lib/libamd_comgr.dylib
# **** CUDA ****
- name: Install CUDA
if: inputs.cuda == 'true'
shell: bash
run: |
sudo mkdir -p /usr/local/cuda/targets/x86_64-linux
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/linux-x86_64/cuda_nvrtc-linux-x86_64-11.5.119-archive.tar.xz \
| sudo tar -xJ -C /usr/local/cuda/targets/x86_64-linux --strip-components=1
echo /usr/local/cuda/targets/x86_64-linux/lib | sudo tee /etc/ld.so.conf.d/cuda-nvrtc.conf
sudo ldconfig
# **** gpuocelot ****
- name: Install gpuocelot dependencies (MacOS)
@@ -286,6 +305,11 @@ runs:
if [[ "${{ runner.os }}" == "macOS" ]]; then
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
CMAKE_ARGS="$CMAKE_ARGS -DBoost_INCLUDE_DIR=$(brew --prefix boost)/include -DBoost_LIBRARY_DIR=$(brew --prefix boost)/lib"
else
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvcc/linux-x86_64/cuda_nvcc-linux-x86_64-11.5.119-archive.tar.xz \
| sudo tar -xJ -C /usr/ --strip-components=1
curl -fL https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-11.5.117-archive.tar.xz \
| sudo tar -xJ -C /usr/ --strip-components=1
fi
cmake .. $CMAKE_ARGS
+19 -13
View File
@@ -417,7 +417,7 @@ jobs:
llvm: 'true'
- name: Test openpilot model kernel count and gate usage
run: |
ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1486 ALLOWED_GATED_READ_IMAGE=18 FLOAT16=1 DEV=CL IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1468 ALLOWED_GATED_READ_IMAGE=18 FLOAT16=1 DEV=CL IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
- name: Test openpilot CL compile fp16
run: FLOAT16=1 DEV=CL IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
- name: Test openpilot CL compile fp32 (test correctness)
@@ -594,17 +594,7 @@ jobs:
deps: testing_unit
pydeps: "onnx==1.18.0 onnxruntime ml_dtypes"
llvm: "true"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build QEMU Docker with cache
uses: docker/build-push-action@v7
with:
file: extra/dsp/Dockerfile
push: false
load: true
tags: qemu-hexagon:latest
cache-from: type=gha
cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=min' || '' }}
qemu: "true"
- name: Set MOCKDSP env
run: printf "MOCKDSP=1" >> $GITHUB_ENV
- name: Run test_tiny on DSP
@@ -835,7 +825,6 @@ jobs:
deps: testing
python-version: '3.12'
amd: 'true'
cuda: 'true'
ocelot: 'true'
llvm: 'true'
- name: Run unit tests
@@ -1014,6 +1003,15 @@ jobs:
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
python -m pytest -n=auto test/backend/test_ops.py --durations=20
- name: Run test_ops (IMAGE)
if: matrix.backend == 'ir3'
shell: bash
env:
IMAGE: 1
DEV: "NULL:IR3:a630,IMAGE_PITCH_ALIGNMENT=64"
run: |
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_gemm | grep image_load
python -m pytest -n=auto test/backend/test_ops.py --durations=20
qcomclcompiletests:
name: Compile-only (QCOM CL)
runs-on: ubuntu-24.04-arm
@@ -1037,3 +1035,11 @@ jobs:
python -c "from tinygrad import Device; assert Device.DEFAULT == 'NULL'"
DEBUG=4 python3 test/backend/test_ops.py TestOps.test_add
python -m pytest -n=auto test/backend/test_ops.py --durations=20
- name: Run test_ops (IMAGE)
shell: bash
env:
IMAGE: 1
DEV: "NULL:QCOMCL:a630,IMAGE_PITCH_ALIGNMENT=64"
run: |
DEBUG=4 python test/backend/test_ops.py TestOps.test_gemm | grep read_imagef
python -m pytest -n=auto test/backend/test_ops.py --durations=20
+2 -2
View File
@@ -140,8 +140,8 @@ Documentation along with a quick start guide can be found on the [docs website](
```python
from tinygrad import Tensor
x = Tensor.eye(3, requires_grad=True)
y = Tensor([[2.0,0,-2.0]], requires_grad=True)
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]])
z = y.matmul(x).sum()
z.backward()
+1 -2
View File
@@ -35,9 +35,8 @@ if __name__ == "__main__":
params = nn.state.get_parameters(model)
# init params, set requires grad on the ones we need gradients of
# init params
for x in params:
if x.requires_grad is None: x.requires_grad_()
x.replace(x.contiguous())
Tensor.realize(*params)
+2 -2
View File
@@ -1442,7 +1442,7 @@ def train_llama3():
from tinygrad.nn.state import get_state_dict
model_state = get_state_dict(model)
for wname in ["wqkv", "wo", "w13", "w2"]:
for wname in model._fp8_inv_scale:
w = model_state[wname]
w._inv_scale = model._fp8_inv_scale[wname]
if optim.master_params:
@@ -1458,7 +1458,7 @@ def train_llama3():
if is_dp: tokens = tokens.to(None).shard(device, 0)
if is_mp: tokens = tokens.shard(device)
if not is_sharding: tokens = tokens.to(None)
logits:Tensor = model(tokens[:, :-1])
logits:Tensor = model(tokens[:, :-1], save=bool(SMALL))
if getenv("FAST_CE", 0):
from extra.llama_kernels.fused_ce import fused_ce_loss
loss = fused_ce_loss(logits.cast(dtypes.bfloat16), tokens[:, 1:], label_smoothing=0.0)
+73 -90
View File
@@ -105,13 +105,16 @@ class FlatTransformer:
scaled_std = 0.02 / math.sqrt(2 * n_layers)
# Attention
self._init_inv_scales = [] # populated by lin_per_layer
self.wqkv = self.lin_per_layer(dim, self.n_heads * self.head_dim + self.n_kv_heads * self.head_dim * 2)
self.wo = self.lin_per_layer(self.n_heads * self.head_dim, dim, std=scaled_std)
self.wqkv, s_qkv = self.lin_per_layer(dim, self.n_heads * self.head_dim + self.n_kv_heads * self.head_dim * 2)
self.wo, s_o = self.lin_per_layer(self.n_heads * self.head_dim, dim, std=scaled_std)
# FeedForward
self.w13 = self.lin_per_layer(dim, hidden_dim * 2)
self.w2 = self.lin_per_layer(hidden_dim, dim, std=scaled_std)
if SPLIT_W13:
self.w1, s_1 = self.lin_per_layer(dim, hidden_dim)
self.w3, s_3 = self.lin_per_layer(dim, hidden_dim)
else:
self.w13, s_13 = self.lin_per_layer(dim, hidden_dim * 2)
self.w2, s_2 = self.lin_per_layer(hidden_dim, dim, std=scaled_std)
self.norm_eps = norm_eps
self.attention_norm = Tensor.ones(n_layers, dim).contiguous()
@@ -125,35 +128,34 @@ class FlatTransformer:
self.freqs_cis = precompute_freqs_cis(dim // n_heads, max_context * 2, rope_theta).contiguous().requires_grad_(False)
def _amax(): return Tensor.full((), FP8_MAX, dtype=dtypes.float32).contiguous().requires_grad_(False)
names = ["xqkv", "xo", "x13", "x2"]
names = ["xqkv", "xo", "x2"]
names += ["x1", "x3"] if SPLIT_W13 else ["x13"]
self._fp8_amax = {name: [_amax() for _ in range(n_layers)] for name in names}
grad_names = ["xqkv", "xo", "xw13", "xout"]
if SPLIT_W13: grad_names.append("xw3")
grad_names = ["xqkv", "xo", "xout"]
grad_names += ["xw1", "xw3"] if SPLIT_W13 else ["xw13"]
self._fp8_grad_amax = {name: [_amax() for _ in range(n_layers)] for name in grad_names}
w_names = ["wqkv", "wo", "w13", "w2"]
self._fp8_inv_scale = {wname: inv_scales.float().contiguous().requires_grad_(False)
for wname, inv_scales in zip(w_names, self._init_inv_scales)}
del self._init_inv_scales
w_scales = [("wqkv", s_qkv), ("wo", s_o), ("w2", s_2)]
w_scales += [("w1", s_1), ("w3", s_3)] if SPLIT_W13 else [("w13", s_13)]
self._fp8_inv_scale = {name: s.float().contiguous().requires_grad_(False) for name, s in w_scales}
def lin_per_layer(self, in_features:int, out_features:int, std:float=0.02):
if getenv("ZEROS"): w = Tensor.zeros(self.n_layers, out_features, in_features)
else: w = Tensor.normal(self.n_layers, out_features, in_features, mean=0.0, std=std)
amax = w.abs().flatten(1).max(1).detach()
scale = FP8_MAX / (amax + 1e-8)
self._init_inv_scales.append((amax + 1e-8) / FP8_MAX)
return (w * scale.reshape(-1, 1, 1)).clamp(-FP8_MAX, FP8_MAX).cast(FP8_DTYPE)
inv_scale = (amax + 1e-8) / FP8_MAX
return (w * scale.reshape(-1, 1, 1)).clamp(-FP8_MAX, FP8_MAX).cast(FP8_DTYPE), inv_scale
def attention(self, x:Tensor, freqs_cis:Tensor, attention_norm:Tensor, wqkv:Tensor, wo:Tensor,
def attention(self, x:Tensor, freqs_cis:Tensor, *, attention_norm:Tensor, wqkv:Tensor, wo:Tensor,
amax_xqkv:Tensor, amax_xo:Tensor, s_qkv:Tensor, s_o:Tensor,
grad_amax_xqkv:Tensor, grad_amax_xo:Tensor):
bsz, seqlen, _ = x.shape
new_amaxs, saves = [], []
amaxs, saves = [], []
xqkv, x_normed, rrms, ret = norm_quantize_matmul(x, attention_norm, wqkv, s_qkv, self.norm_eps,
amax_x=amax_xqkv, grad_amax_state=grad_amax_xqkv)
saves.extend([x_normed, rrms])
new_amaxs.extend(ret[:1])
saves.extend(ret[1:] + [xqkv])
xqkv, x_normed, rrms, (new_amax, *s) = norm_quantize_matmul(x, attention_norm, wqkv, s_qkv, self.norm_eps,
amax_x=amax_xqkv, grad_amax_state=grad_amax_xqkv)
amaxs.append(new_amax)
saves.extend([x_normed, rrms, *s, xqkv])
xqkv = xqkv.reshape(bsz, seqlen, self.n_kv_heads, self.n_rep + 2, self.head_dim)
xq = xqkv[:, :, :, :self.n_rep].reshape(bsz, seqlen, self.n_heads, self.head_dim)
xk = xqkv[:, :, :, self.n_rep].reshape(bsz, seqlen, self.n_kv_heads, self.head_dim)
@@ -170,67 +172,48 @@ class FlatTransformer:
attn = xq.scaled_dot_product_attention(xk, xv, is_causal=True, enable_gqa=True).transpose(1, 2)
attn = attn.reshape(bsz, seqlen, -1)
out, *ret = matmul(attn, wo, amax_x=amax_xo, w_inv_scale=s_o, grad_amax_state=grad_amax_xo)
new_amaxs.extend(ret[:1])
saves.extend(ret[1:] + [out])
return (out, *new_amaxs, *saves)
out, new_amax, *s = matmul(attn, wo, amax_x=amax_xo, w_inv_scale=s_o, grad_amax_state=grad_amax_xo)
amaxs.append(new_amax)
saves.extend([*s, out])
return out, amaxs, saves
def feed_forward(self, x:Tensor, residual:Tensor, ffn_norm:Tensor, w13:Tensor, w2:Tensor,
amax_x13:Tensor, amax_x2:Tensor, s_13:Tensor, s_2:Tensor,
grad_amax_xw13:Tensor, grad_amax_xout:Tensor,
w1:Tensor|None=None, w3:Tensor|None=None, grad_amax_xw3:Tensor|None=None):
new_amaxs, saves = [], []
def feed_forward(self, x:Tensor, residual:Tensor, **kwargs):
amaxs, saves = [], []
if SPLIT_W13:
assert w1 is not None and w3 is not None and grad_amax_xw3 is not None
h = x + residual
x_normed, rrms = rmsnorm(h, self.norm_eps)
saves.extend([x_normed, rrms])
inp = x_normed * ffn_norm
# separate w1 and w3 matmuls
x_w1, *ret1 = matmul(inp, w1, amax_x=amax_x13, w_inv_scale=s_13, grad_amax_state=grad_amax_xw13)
new_amaxs.extend(ret1[:1])
saves.extend(ret1[1:] + [x_w1])
x_w3, *ret3 = matmul(inp, w3, amax_x=amax_x13, w_inv_scale=s_13, grad_amax_state=grad_amax_xw3)
saves.extend(ret3[1:] + [x_w3])
# silu * mul + w2 matmul
out, *ret2 = matmul(x_w1.silu() * x_w3, w2, amax_x=amax_x2, w_inv_scale=s_2, grad_amax_state=grad_amax_xout)
new_amaxs.extend(ret2[:1])
saves.extend(ret2[1:] + [out])
return (out, h, *new_amaxs, *saves)
x_w13, h, x_normed, rrms, ret = add_norm_quantize_matmul(x, residual, ffn_norm, w13, s_13, self.norm_eps,
amax_x=amax_x13, grad_amax_state=grad_amax_xw13)
saves.extend([x_normed, rrms])
new_amaxs.extend(ret[:1])
saves.extend(ret[1:] + [x_w13])
out, ret = silu_w13_quantize_matmul(x_w13, w2, s_2, amax_x2=amax_x2, grad_amax_xw13=grad_amax_xw13, grad_amax_xout=grad_amax_xout)
new_amaxs.extend(ret[:1])
saves.extend(ret[1:] + [out])
return (out, h, *new_amaxs, *saves)
inp = x_normed * kwargs["ffn_norm"]
x_w1, new_amax, *s = matmul(inp, kwargs["w1"], amax_x=kwargs["amax_x1"], w_inv_scale=kwargs["s_1"], grad_amax_state=kwargs["grad_amax_xw1"])
amaxs.append(new_amax)
saves.extend([*s, x_w1])
x_w3, new_amax, *s = matmul(inp, kwargs["w3"], amax_x=kwargs["amax_x3"], w_inv_scale=kwargs["s_3"], grad_amax_state=kwargs["grad_amax_xw3"])
amaxs.append(new_amax)
saves.extend([*s, x_w3])
out, new_amax, *s = matmul(x_w1.silu() * x_w3, kwargs["w2"], amax_x=kwargs["amax_x2"], w_inv_scale=kwargs["s_2"],
grad_amax_state=kwargs["grad_amax_xout"])
amaxs.append(new_amax)
saves.extend([*s, out])
else:
x_w13, h, x_normed, rrms, (new_amax, *s) = add_norm_quantize_matmul(x, residual, kwargs["ffn_norm"], kwargs["w13"], kwargs["s_13"],
self.norm_eps, amax_x=kwargs["amax_x13"],
grad_amax_state=kwargs["grad_amax_xw13"])
amaxs.append(new_amax)
saves.extend([x_normed, rrms, *s, x_w13])
out, (new_amax, *s) = silu_w13_quantize_matmul(x_w13, kwargs["w2"], kwargs["s_2"], amax_x2=kwargs["amax_x2"],
grad_amax_xw13=kwargs["grad_amax_xw13"], grad_amax_xout=kwargs["grad_amax_xout"])
amaxs.append(new_amax)
saves.extend([*s, out])
return out, h, amaxs, saves
@function(precompile=True, precompile_backward=True)
def run_layer(self, x:Tensor, freqs_cis:Tensor,
attention_norm:Tensor, wqkv:Tensor, wo:Tensor,
ffn_norm:Tensor, w13:Tensor, w2:Tensor,
amax_xqkv:Tensor, amax_xo:Tensor,
amax_x13:Tensor, amax_x2:Tensor,
s_qkv:Tensor, s_o:Tensor, s_13:Tensor, s_2:Tensor,
grad_amax_xqkv:Tensor, grad_amax_xo:Tensor,
grad_amax_xw13:Tensor, grad_amax_xout:Tensor,
w1:Tensor|None=None, w3:Tensor|None=None, grad_amax_xw3:Tensor|None=None):
attn, *attn_ret = self.attention(x, freqs_cis, attention_norm, wqkv, wo,
amax_xqkv=amax_xqkv, amax_xo=amax_xo, s_qkv=s_qkv, s_o=s_o,
grad_amax_xqkv=grad_amax_xqkv, grad_amax_xo=grad_amax_xo)
attn_amaxs, attn_saves = attn_ret[:2], attn_ret[2:]
ffn, h, *ffn_ret = self.feed_forward(x, attn, ffn_norm, w13, w2,
amax_x13=amax_x13, amax_x2=amax_x2, s_13=s_13, s_2=s_2,
grad_amax_xw13=grad_amax_xw13, grad_amax_xout=grad_amax_xout,
w1=w1, w3=w3, grad_amax_xw3=grad_amax_xw3)
ffn_amaxs, ffn_saves = ffn_ret[:2], ffn_ret[2:]
def run_layer(self, x:Tensor, freqs_cis:Tensor, attn_kwargs:dict, ffn_kwargs:dict, save:bool=True):
attn, attn_amaxs, attn_saves = self.attention(x, freqs_cis, **attn_kwargs)
ffn, h, ffn_amaxs, ffn_saves = self.feed_forward(x, attn, **ffn_kwargs)
h = h + ffn
return (h, *attn_amaxs, *ffn_amaxs, *attn_saves, *ffn_saves)
if save: return (h, *attn_amaxs, *ffn_amaxs, *attn_saves, *ffn_saves)
else: return (h, *attn_amaxs, *ffn_amaxs)
def shard(self, device:tuple[str, ...], mp:bool=False):
from tinygrad.nn.state import get_parameters
@@ -241,11 +224,10 @@ class FlatTransformer:
self.wqkv.shard_(device, axis=1).realize() # (n_layers, out, dim) shard out
self.wo.shard_(device, axis=2).realize() # (n_layers, dim, in) shard in
if SPLIT_W13:
self.w1 = self.w13[:, :self.hidden_dim, :].contiguous()
self.w3 = self.w13[:, self.hidden_dim:, :].contiguous()
self.w1.shard_(device, axis=1).realize()
self.w3.shard_(device, axis=1).realize()
self.w13.shard_(device, axis=1).realize() # (n_layers, hidden*2, dim) shard out
else:
self.w13.shard_(device, axis=1).realize() # (n_layers, hidden*2, dim) shard out
self.w2.shard_(device, axis=2).realize() # (n_layers, dim, hidden) shard in
self.attention_norm.shard_(device, axis=None).realize()
self.ffn_norm.shard_(device, axis=None).realize()
@@ -260,23 +242,24 @@ class FlatTransformer:
for name in self._fp8_inv_scale:
self._fp8_inv_scale[name] = self._fp8_inv_scale[name].to(device).contiguous().requires_grad_(False)
def __call__(self, tokens:Tensor):
def __call__(self, tokens:Tensor, save:bool=True):
h = self.tok_embeddings(tokens)
freqs_cis = self.freqs_cis.cast(h.dtype)[:, :tokens.shape[1], :, :, :]
a, ga, s = self._fp8_amax, self._fp8_grad_amax, self._fp8_inv_scale
for i in range(self.n_layers):
split_kwargs = dict(w1=self.w1[i], w3=self.w3[i], grad_amax_xw3=ga["xw3"][i]) if SPLIT_W13 else {}
h, *ret = self.run_layer(h, freqs_cis,
self.attention_norm[i], self.wqkv[i], self.wo[i],
self.ffn_norm[i], self.w13[i], self.w2[i],
amax_xqkv=a["xqkv"][i], amax_xo=a["xo"][i],
amax_x13=a["x13"][i], amax_x2=a["x2"][i],
s_qkv=s["wqkv"][i], s_o=s["wo"][i],
s_13=s["w13"][i], s_2=s["w2"][i],
grad_amax_xqkv=ga["xqkv"][i], grad_amax_xo=ga["xo"][i],
grad_amax_xw13=ga["xw13"][i], grad_amax_xout=ga["xout"][i],
**split_kwargs)
for name, new_val in zip(["xqkv", "xo", "x13", "x2"], ret[:5]):
attn_kwargs = dict(attention_norm=self.attention_norm[i], wqkv=self.wqkv[i], wo=self.wo[i],
amax_xqkv=a["xqkv"][i], amax_xo=a["xo"][i], s_qkv=s["wqkv"][i], s_o=s["wo"][i],
grad_amax_xqkv=ga["xqkv"][i], grad_amax_xo=ga["xo"][i])
ffn_kwargs = dict(ffn_norm=self.ffn_norm[i], w2=self.w2[i],
amax_x2=a["x2"][i], s_2=s["w2"][i], grad_amax_xout=ga["xout"][i])
if SPLIT_W13:
ffn_kwargs.update(w1=self.w1[i], w3=self.w3[i], amax_x1=a["x1"][i], amax_x3=a["x3"][i],
s_1=s["w1"][i], s_3=s["w3"][i], grad_amax_xw1=ga["xw1"][i], grad_amax_xw3=ga["xw3"][i])
else:
ffn_kwargs.update(w13=self.w13[i], amax_x13=a["x13"][i], s_13=s["w13"][i], grad_amax_xw13=ga["xw13"][i])
h, *ret = self.run_layer(h, freqs_cis, attn_kwargs, ffn_kwargs, save=save)
amax_names = ["xqkv", "xo"] + (["x1", "x3"] if SPLIT_W13 else ["x13"]) + ["x2"]
for name, new_val in zip(amax_names, ret[:len(amax_names)]):
a[name][i].assign(new_val)
logits = matmul(self.norm(h), self.output[0], fp8=False)[0]
@@ -324,7 +307,7 @@ if __name__ == "__main__":
# preallocate all the grad buffers and zero them out
grads = {x:Tensor.zeros(x.shape, dtype=x.dtype, device=x.device).contiguous()
for x in state.values() if x.requires_grad is None}
for x in state.values() if x.requires_grad}
# print model size
sz = 0
@@ -3,7 +3,6 @@ os.environ["WQKV"] = "1"
import unittest
import numpy as np
from tinygrad import Tensor, nn, dtypes
from tinygrad.nn.state import get_parameters
from tinygrad.device import is_dtype_supported, Device
from examples.mlperf.models.llama import Transformer
from examples.mlperf.models.flat_llama import FlatTransformer
@@ -45,8 +44,6 @@ class TestFlatLlama(unittest.TestCase):
flat = FlatTransformer(**params)
copy_weights(flat, ref)
for p in get_parameters(ref): p.requires_grad_(True)
for p in get_parameters(flat): p.requires_grad_(True)
Tensor.realize(*nn.state.get_state_dict(flat).values())
tokens = Tensor([[1, 50, 100, 999, 2, 10]])
@@ -18,6 +18,7 @@ export FP8=${FP8:-1}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
export FAST_CE=${FAST_CE:-1}
export FUSED_INPUT_QUANTIZE=${FUSED_INPUT_QUANTIZE:-1}
export FUSED_GRAD_QUANTIZE=${FUSED_GRAD_QUANTIZE:-1}
export FUSED_ADD_NORM_MUL_QUANTIZE=${FUSED_ADD_NORM_MUL_QUANTIZE:-1}
export FUSED_SILU_W13=${FUSED_SILU_W13:-1}
export FUSED_PAD_GRAD_ACCUM=${FUSED_PAD_GRAD_ACCUM:-1}
@@ -16,7 +16,8 @@ export MASTER_WEIGHTS=${MASTER_WEIGHTS:-1}
export FP8=${FP8:-1}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
export FAST_CE=${FAST_CE:-0}
export FUSED_INPUT_QUANTIZE=${FUSED_INPUT_QUANTIZE:-1}
export FUSED_INPUT_QUANTIZE=${FUSED_INPUT_QUANTIZE:-0}
export FUSED_GRAD_QUANTIZE=${FUSED_GRAD_QUANTIZE:-0}
export FUSED_ADD_NORM_MUL_QUANTIZE=${FUSED_ADD_NORM_MUL_QUANTIZE:-0}
export FUSED_SILU_W13=${FUSED_SILU_W13:-0}
export FUSED_PAD_GRAD_ACCUM=${FUSED_PAD_GRAD_ACCUM:-0}
@@ -18,6 +18,7 @@ export FP8=${FP8:-1}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
export FAST_CE=${FAST_CE:-1}
export FUSED_INPUT_QUANTIZE=${FUSED_INPUT_QUANTIZE:-1}
export FUSED_GRAD_QUANTIZE=${FUSED_GRAD_QUANTIZE:-1}
export FUSED_ADD_NORM_MUL_QUANTIZE=${FUSED_ADD_NORM_MUL_QUANTIZE:-1}
export FUSED_SILU_W13=${FUSED_SILU_W13:-1}
export FUSED_PAD_GRAD_ACCUM=${FUSED_PAD_GRAD_ACCUM:-1}
@@ -10,9 +10,19 @@ export DEVICE_IN_FUNCTION_BUG=1
export DEBUG=${DEBUG:-0}
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
export ALL2ALL=${ALL2ALL:-1}
export USE_ATOMICS=${USE_ATOMICS:-0}
export USE_ATOMICS=${USE_ATOMICS:-1}
export ASM_GEMM=${ASM_GEMM:-1}
export WQKV=${WQKV:-1}
export MASTER_WEIGHTS=${MASTER_WEIGHTS:-1}
export FP8=${FP8:-1}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
export FAST_CE=${FAST_CE:-0}
export FUSED_INPUT_QUANTIZE=${FUSED_INPUT_QUANTIZE:-0}
export FUSED_GRAD_QUANTIZE=${FUSED_GRAD_QUANTIZE:-0}
export FUSED_ADD_NORM_MUL_QUANTIZE=${FUSED_ADD_NORM_MUL_QUANTIZE:-0}
export FUSED_SILU_W13=${FUSED_SILU_W13:-0}
export FUSED_PAD_GRAD_ACCUM=${FUSED_PAD_GRAD_ACCUM:-0}
export SPLIT_W13=${SPLIT_W13:-1}
export OFFLOAD_OPTIM=${OFFLOAD_OPTIM:-1}
export DEFAULT_FLOAT="bfloat16" OPTIM_DTYPE="bfloat16"
@@ -19,6 +19,7 @@ export FP8=1
export ALLREDUCE_CAST=1
export FAST_CE=1
export FUSED_INPUT_QUANTIZE=1
export FUSED_GRAD_QUANTIZE=1
export FUSED_ADD_NORM_MUL_QUANTIZE=1
export FUSED_SILU_W13=1
export FUSED_PAD_GRAD_ACCUM=1
+2
View File
@@ -21,6 +21,8 @@ def compile(onnx_file):
# TODO this seems dumb
input_types = {k:(dtypes.float32 if v is dtypes.float16 else v) for k,v in input_types.items()}
Tensor.manual_seed(100)
# replace symbolic dimensions (e.g. 'b' for dynamic batch) with 1
input_shapes = {k:tuple(s if isinstance(s, int) else 1 for s in shp) for k,shp in input_shapes.items()}
inputs = {k:Tensor(Tensor.randn(*shp, dtype=input_types[k]).mul(8).realize().numpy(), device='NPY') for k,shp in sorted(input_shapes.items())}
if not getenv("NPY_IMG"):
inputs = {k:Tensor(v.numpy(), device=Device.DEFAULT).realize() if 'img' in k else v for k,v in inputs.items()}
+11 -4
View File
@@ -2713,12 +2713,20 @@ def custom_gemm_bw(gradient:UOp, kernel:UOp):
gbase = gradient.base if hasattr(gradient, "base") else gradient
mailbox_entry = _grad_fp8_mailbox.pop(gbase, None) or _grad_fp8_mailbox.pop(gradient, None)
if mailbox_entry is not None:
g_fp8_u, inv_scale_u, _new_amax_u, store_effect = mailbox_entry
g_fp8_u, inv_scale_u = mailbox_entry
g_fp8 = Tensor(g_fp8_u, device=a.device)[:a.shape[0]]
g_scale = Tensor(inv_scale_u, device=a.device)
else:
assert grad_amax_state is not None, "fp8 matmul bwd needs either a mailbox entry or a grad_amax_state"
g_fp8, g_scale, _, store_effect = quantize_fp8_delayed(g_t, Tensor(grad_amax_state, device=a.device))
if getenv("FUSED_GRAD_QUANTIZE", 0):
g_fp8, g_scale, _, store_effect = quantize_fp8_delayed(g_t, Tensor(grad_amax_state, device=a.device))
assert g_fp8.uop.op is Ops.AFTER, f"expected AFTER, got {g_fp8.uop.op}"
g_fp8 = Tensor(g_fp8.uop.replace(src=g_fp8.uop.src + (store_effect,)), device=a.device)
else:
grad_amax_t = Tensor(grad_amax_state, device=a.device)
g_fp8, g_scale, new_grad_amax = quantize_fp8(g_t, amax_state=grad_amax_t)
store_effect = grad_amax_state.store(new_grad_amax.uop)
g_fp8 = Tensor(g_fp8.contiguous().uop.after(store_effect), device=a.device)
# dgrad: uses g_scale * x_scale * w_scale
grad_a = asm_gemm(g_fp8, b_t, x_scale=g_scale * s_x_t, w_scale=s_w_t)
# wgrad: no w_scale
@@ -2729,8 +2737,7 @@ def custom_gemm_bw(gradient:UOp, kernel:UOp):
else:
g_fp8_T = g_fp8.permute(2, 0, 1).reshape(g_t.shape[-1], -1)
grad_b = asm_gemm(g_fp8_T, a_t.reshape(-1, a_t.shape[-1]), x_scale=g_scale * s_x_t)
# Attach the delayed-amax store effect (if any) to grad_a so realizing grads commits the amax update.
ret = (None, grad_a.uop.after(store_effect), grad_b.uop, None, None)
ret = (None, grad_a.uop, grad_b.uop, None, None)
if len(inputs) == 6: ret = ret + (None,)
return ret
else:
+131
View File
@@ -0,0 +1,131 @@
from __future__ import annotations
import time
from typing import cast
from tinygrad.device import Buffer, BufferSpec, Compiled, Device, MultiBuffer
from tinygrad.dtype import dtypes
from tinygrad.engine.jit import GraphRunner
from tinygrad.engine.realize import get_call_outs_ins, get_runtime
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, graph_rewrite
from extra.hcq2.hcq2 import HCQ2Compiled, HCQ2DeviceCtx, HCQ2LowerCtx, prep_runtime, pm_lower_kernargs, pm_lower_ops
from extra.hcq2.hcq2 import pm_split_into_queues, pm_add_barriers, pm_add_signals, build_host_program
# **************** insert deps ****************
def insert_deps(ctx:HCQ2Graph, linear:UOp) -> UOp:
src = []
for j, call in enumerate(linear.src):
call = call.replace(tag=j)
_, _, bufs, _ = ctx.calls[j]
outs, ins = get_call_outs_ins(call)
deps = ctx._access_resources([bufs[i] for i in outs + ins], list(range(len(outs))), call)
src.append(UOp(Ops.AFTER, call.dtype, (call, *deps), tag=call.tag))
return linear.replace(src=tuple(src))
pm_insert_deps = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), insert_deps)])
def replace_params(ctx:HCQ2Graph, call:UOp) -> UOp|None:
if not any(x.op is Ops.PARAM for x in call.src[1:]): return None
return call.replace(src=tuple(ctx.input_addrs_uop[x.arg] if x.op is Ops.PARAM else x for x in call.src))
pm_replace_params = PatternMatcher([(UPat(Ops.CALL, name="call", allow_any_len=True), replace_params)])
# **************** graph-only passes ****************
def alloc_queue_sig(ctx:HCQ2Graph, q:UOp) -> None:
if q.arg in ctx.queue_sigs: return None
buf = Buffer(q.arg[0], 0x100, dtypes.uint8, options=BufferSpec(host=True, uncached=True, cpu_access=True), preallocate=True)
ctx.queue_sig_bufs.append(buf)
ctx.queue_sigs[q.arg] = UOp.from_buffer(buf, q.arg[0])
return None
pm_alloc_queue_sigs = PatternMatcher([(UPat(Ops.LINEAR, src=UPat({Ops.PROGRAM, Ops.COPY}), name="q"), alloc_queue_sig)])
def lower_queue_deps(ctx:HCQ2Graph, after:UOp) -> UOp:
wrapper, deps, call_idx = after.src[0], after.src[1:], after.tag
def store(q_arg, v): return ctx.queue_sigs[q_arg].store(UOp.const(dtypes.uint32, v))
waits = tuple(UOp(Ops.WAIT, dtypes.void, (ctx.queue_sigs[dep.src[0].arg], UOp.const(dtypes.uint32, dep.tag),
store(dep.src[0].arg, dep.tag))) for dep in deps)
return wrapper.replace(src=tuple(q.replace(src=(*waits, *q.src, store(q.arg, call_idx))) for q in wrapper.src))
pm_lower_queue_deps = PatternMatcher([(UPat(Ops.AFTER, src=UPat(Ops.LINEAR), name="after"), lower_queue_deps)])
def optimize_queue_deps(ctx:HCQ2Graph, queue:UOp) -> UOp|None:
src, seen, pending, queue_sig = [], {}, {}, ctx.queue_sigs[queue.arg]
for x in queue.src:
if x.op is Ops.WAIT:
sig, val = x.src[0], x.src[1]
if sig is queue_sig or seen.get(sig, -1) >= val.arg: continue
if (old:=pending.get(sig)) is None or old.src[1].arg < val.arg: pending[sig] = x
continue
for wait in pending.values():
src.append(wait)
seen[wait.src[0]] = wait.src[1].arg
pending.clear()
src.append(x)
src += pending.values()
return queue.replace(src=tuple(src)) if tuple(src) != queue.src else None
pm_optimize_queue_deps = PatternMatcher([
(UPat(Ops.LINEAR, src=UPat({Ops.BARRIER, Ops.WAIT, Ops.STORE, Ops.PROGRAM, Ops.COPY}), name="queue"), optimize_queue_deps),
])
def drop_dead_stores(ctx:HCQ2Graph, outer:UOp) -> UOp:
live = {u.src[2] for u in outer.toposort() if u.op is Ops.WAIT}
return outer.replace(src=tuple(q.replace(src=tuple(x for x in q.src if x.op is not Ops.STORE or x in live)) for q in outer.src))
pm_drop_dead_stores = PatternMatcher([(UPat(Ops.LINEAR, src=UPat(Ops.LINEAR), name="outer"), drop_dead_stores)])
def add_queue_sig_resets(ctx:HCQ2Graph, outer:UOp) -> UOp|None:
if not ctx.queue_sig_bufs: return None
resets = tuple(ctx.hcq_ctx.host_param(sig).index(UOp.const(dtypes.int, 0), ptr=True).cast(dtypes.uint64.ptr())
.store(UOp.const(dtypes.uint64, 0)) for sig in ctx.queue_sig_bufs)
return outer.replace(src=tuple(c.replace(src=c.src + resets) if c.op is Ops.AFTER else c.after(*resets) for c in outer.src))
pm_add_queue_sig_resets = PatternMatcher([(UPat(Ops.LINEAR, name="outer"), add_queue_sig_resets)])
# **************** Graph ****************
class HCQ2Graph(GraphRunner):
def __init__(self, linear:UOp, input_uops:tuple[UOp, ...]=()):
super().__init__(linear, input_uops)
self.dev = cast(HCQ2Compiled, Device[self.device])
self.hcq_ctx = HCQ2LowerCtx(name="hcq_graph")
self.input_addrs = Buffer("CPU", max(len(input_uops), 1), dtypes.uint64, preallocate=True)
self.input_addrs_uop = self.hcq_ctx.host_param(self.input_addrs)
self.linear = graph_rewrite(self.linear, pm_insert_deps, ctx=self, name="hcq: insert deps", walk=True)
self.linear, sizes = prep_runtime(self.hcq_ctx, self.linear)
for dev_name, sz in sizes.items():
buf = Buffer(dev_name, sz, dtypes.uint8, options=BufferSpec(cpu_access=True), preallocate=True)
self.hcq_ctx.devs[dev_name] = HCQ2DeviceCtx(dev_name, UOp.from_buffer(buf, dev_name), UOp.const(dtypes.uint64, buf._buf.va_addr))
self.linear = graph_rewrite(self.linear, pm_replace_params, ctx=self, name="hcq: replace params", walk=True)
self.linear = graph_rewrite(self.linear, pm_lower_kernargs + pm_lower_ops, ctx=self.hcq_ctx, name="hcq: lower ops")
# per-queue signal state — populated as a side-effect by pm_alloc_queue_sigs walking the lowered linear.
self.queue_sig_bufs:list[Buffer] = []
self.queue_sigs:dict[tuple[str, str], UOp] = {}
graph_rewrite(self.linear, pm_alloc_queue_sigs, ctx=self, name="hcq: alloc queue sigs", walk=True)
self.linear = graph_rewrite(self.linear, pm_lower_queue_deps, ctx=self, name="hcq: lower queue deps")
self.linear = graph_rewrite(self.linear, pm_split_into_queues, ctx=self.hcq_ctx, name="hcq: split into queues")
self.linear = graph_rewrite(self.linear, pm_add_barriers, ctx=self.hcq_ctx, name="hcq: add barriers", walk=True)
self.linear = graph_rewrite(self.linear, pm_optimize_queue_deps, ctx=self, name="hcq: optimize queue deps", walk=True)
self.linear = graph_rewrite(self.linear, pm_drop_dead_stores, ctx=self, name="hcq: drop dead stores")
self.linear = graph_rewrite(self.linear, pm_add_signals, ctx=self.hcq_ctx, name="hcq: add signals", walk=True)
self.linear = graph_rewrite(self.linear, self.dev.pm_lower, ctx=self.hcq_ctx, name=f"hcq: encode cmdbuf {self.dev.device}", walk=True)
self.linear = graph_rewrite(self.linear, pm_add_queue_sig_resets, ctx=self, name="hcq: add queue sig resets", walk=True)
self.host_call = build_host_program(self.hcq_ctx, self.linear, None, self.dev)
self.host_rt, self.host_globals = get_runtime("CPU", self.host_call.src[0]), self.host_call.src[0].arg.globals
def __call__(self, input_uops:tuple[UOp, ...], var_vals:dict[str, int], wait=False) -> float|None:
addrs = self.input_addrs.as_memoryview(force_zero_copy=True).cast('Q')
for i, u in enumerate(input_uops):
buf = next(b for b in u.buffer.bufs if b.device == self.dev.device) if isinstance(u.buffer, MultiBuffer) else u.buffer
addrs[i] = buf._buf.va_addr
self.host_rt(*[self.hcq_ctx.inputs[i].get_buf("CPU") for i in self.host_globals], vals=self.host_call.src[0].arg.vals(var_vals), wait=True)
if wait:
st = time.perf_counter()
self.dev.synchronize()
return time.perf_counter() - st
return None
@staticmethod
def supports_uop(batch_devs:list[Compiled], new_call:UOp) -> bool:
all_devs = GraphRunner._all_devs(batch_devs, new_call)
return new_call.src[0].op in (Ops.PROGRAM, Ops.COPY) and len(all_devs) == 1 and isinstance(all_devs[0], HCQ2Compiled)
+120 -96
View File
@@ -1,9 +1,9 @@
from __future__ import annotations
from typing import cast, Callable, TypeVar, Generic, Any, TYPE_CHECKING
import struct, functools, time, itertools
import struct, functools, time, collections
from dataclasses import replace
if TYPE_CHECKING: from tinygrad.engine.realize import ExecContext
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, wait_cond, mv_address, round_up, DEBUG
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, mv_address, round_up, DEBUG, dedup
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, track_rewrites
from tinygrad.dtype import dtypes
@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
from tinygrad.runtime.support.memory import BumpAllocator
from tinygrad.runtime.support.hcq import MMIOInterface
from tinygrad.renderer import Renderer, Estimates
from tinygrad.engine.realize import pm_flatten_linear, to_program, track_stats
from tinygrad.engine.realize import to_program, track_stats, get_call_arg_uops, resolve_params
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
@@ -25,7 +25,8 @@ class HCQ2Compiled(Compiled):
kernargs_size=(16 << 20), can_recover:bool=False, arch=None):
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
super().__init__(device, allocator, compilers, runtime, None, arch=arch)
from extra.hcq2.graph.hcq import HCQ2Graph
super().__init__(device, allocator, compilers, lambda *a, **kw: None, HCQ2Graph, arch=arch)
self.kernargs_size = kernargs_size
self.kernargs_offset_allocator:BumpAllocator = BumpAllocator(kernargs_size, wrap=True)
@@ -52,7 +53,9 @@ class HCQ2Compiled(Compiled):
if not hasattr(self, 'iface'): return
sig = self.timeline_signal._buf.cpu_view().mv.cast('Q')
tl = self.timeline_value.as_memoryview(force_zero_copy=True).cast('Q')
wait_cond(lambda: sig[0] >= tl[0] - 1, timeout_ms=3000, msg=f"{sig[0]} < {tl[0] - 1}")
st = time.perf_counter()
while sig[0] < tl[0] - 1:
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
@@ -139,38 +142,36 @@ class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
# **************** lower context ****************
@dataclass
class HCQ2DeviceCtx:
device:str # device name; resolve to instance via Device[device]
kernargs_host:UOp # UOp whose .buffer is dev.kernargs_buf (BUFFER UOp in runtime, PARAM in graph)
kernargs_gpu:UOp # va_addr const of dev.kernargs_buf
kernargs_allocator:BumpAllocator = field(default_factory=lambda: BumpAllocator(2 << 20, wrap=False))
@dataclass
class HCQ2LowerCtx:
dev:HCQ2Compiled
name:str
kernargs_host:UOp|None = None
kernargs_gpu:UOp|None = None
kernargs_allocator:BumpAllocator = field(default_factory=lambda: BumpAllocator(0x1000, wrap=False))
timestamps_gpu:UOp|None = None
next_timestamp:itertools.count = field(default_factory=itertools.count)
inputs:list[Buffer] = field(default_factory=list)
holds:list[UOp] = field(default_factory=list)
devs:dict[str, HCQ2DeviceCtx] = field(default_factory=dict)
def host_param(self, buf:Buffer) -> UOp:
if buf not in self.inputs: self.inputs.append(buf)
return UOp.placeholder((buf.size,), buf.dtype, self.inputs.index(buf))
class HCQEncoder:
def __init__(self, ctx:HCQ2LowerCtx): self.ctx, self.dev, self.blob, self.patches, self.deps = ctx, ctx.dev, b'', [], set()
def __init__(self, ctx:HCQ2LowerCtx, dev:HCQ2Compiled): self.ctx, self.dev, self.blob, self.patches, self.deps = ctx, dev, b'', [], []
@property
def src(self) -> tuple[UOp, ...]: return tuple(self.patches + list(self.deps))
def src(self) -> tuple[UOp, ...]: return tuple(self.patches + dedup(self.deps))
def get_dev_addr(self, uop:UOp) -> sint|UOp:
# unwrap transient AFTER on the value: deps flow into enc.deps separately, the outer wrapper never reaches the final graph
while uop.op is Ops.AFTER:
self.deps.update(uop.src[1:])
self.deps.extend(uop.src[1:])
uop = uop.src[0]
self.deps.add(uop)
return uop.buffer.get_buf(self.dev.device).va_addr if uop.op in (Ops.BUFFER, Ops.BUFFER_VIEW) else uop.ssimplify()
if isinstance(val:=uop.ssimplify(), UOp): self.deps.append(uop)
return uop.buffer.get_buf(self.dev.device).va_addr if uop.op in (Ops.BUFFER, Ops.BUFFER_VIEW) else val
def append(self, *data, dtype=dtypes.uint32):
for d in data:
@@ -186,59 +187,81 @@ class HCQEncoder:
pm_prep_runtime = PatternMatcher([
# device-specific lowering of the program
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, src=(UPat(), UPat(), UPat(), UPat(), UPat(Ops.BINARY)), name="prg"),),
name="call", allow_any_len=True), lambda ctx,call,prg: call.replace(src=(ctx.dev.pm_lower.rewrite(prg, ctx),) + call.src[1:])),
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, src=(UPat(), UPat(Ops.DEVICE), UPat(), UPat(), UPat(Ops.BINARY)), name="p"),), name="c", allow_any_len=True),
lambda ctx, c, p: c.replace(src=(Device[p.src[1].arg].pm_lower.rewrite(p, ctx),) + c.src[1:])),
])
# **************** lower hcq ****************
def calc_kernargs_sizes(ctx:dict[str,int], u:UOp) -> None:
d = u.src[0].buffer.device
ctx[d] = ctx.get(d, 0) + round_up(u.arg[0].kernargs_alloc_size, 16)
pm_calc_kernargs_sizes = PatternMatcher([(UPat(Ops.PROGRAM, name="u"), calc_kernargs_sizes)])
# **************** lower kernargs ****************
def lower_kernargs(ctx:HCQ2LowerCtx, call:UOp, prg:UOp) -> UOp:
data, info = prg.arg
# after amd_build_program, prg.src is (BUFFER_lib_gpu,); the buffer's device names the device
dctx = ctx.devs[prg.src[0].buffer.device]
enc = HCQEncoder(ctx)
enc = HCQEncoder(ctx, Device[dctx.device])
for gi in info.globals: enc.append(enc.get_dev_addr(call.src[1+gi]), dtype=dtypes.uint64)
for v in info.vars: enc.append(v, dtype=dtypes.uint32)
args_off = ctx.kernargs_allocator.alloc(data.kernargs_alloc_size, 16)
assert ctx.kernargs_host is not None and ctx.kernargs_gpu is not None
ctx.kernargs_host.buffer.view(len(enc.blob), dtypes.uint8, args_off).ensure_allocated().as_memoryview(force_zero_copy=True)[:] = enc.blob
args_off = dctx.kernargs_allocator.alloc(data.kernargs_alloc_size, 16)
dctx.kernargs_host.buffer.view(len(enc.blob), dtypes.uint8, args_off).ensure_allocated().as_memoryview(force_zero_copy=True)[:] = enc.blob
args_uop = (ctx.kernargs_gpu + args_off).after(ctx.kernargs_host.after(*tuple(p.replace(arg=p.arg+args_off) for p in enc.patches)))
args_uop = (dctx.kernargs_gpu + args_off).after(dctx.kernargs_host.after(*tuple(p.replace(arg=p.arg+args_off) for p in enc.patches)))
return call.replace(src=(prg.replace(src=prg.src + (args_uop,), arg=(data, info)),) + call.src[1:])
pm_lower_kernargs = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, src=(UPat(Ops.BUFFER),), name="prg"),), name="call", allow_any_len=True), lower_kernargs),
])
# **************** lower ops ****************
def lower_program(ctx:HCQ2LowerCtx, call:UOp, prg:UOp) -> UOp:
sig, tl = UOp.from_buffer(ctx.dev.timeline_signal), ctx.host_param(ctx.dev.timeline_value)
return UOp(Ops.LINEAR, dtypes.void, (
sig.wait(tl[0] - 1),
UOp(Ops.BARRIER, dtypes.void),
UOp(Ops.CUSTOM_FUNCTION, dtypes.void, src=(ctx.timestamps_gpu + next(ctx.next_timestamp) * 8,), arg="timestamp"),
prg,
UOp(Ops.CUSTOM_FUNCTION, dtypes.void, src=(ctx.timestamps_gpu + next(ctx.next_timestamp) * 8,), arg="timestamp"),
sig.store(tl[0])))
q = UOp(Ops.LINEAR, dtypes.void, (prg,), arg=(prg.src[0].buffer.device, "COMPUTE"))
return UOp(Ops.LINEAR, dtypes.void, (q,), tag=call.tag)
def lower_copy(ctx:HCQ2LowerCtx, call:UOp, copy:UOp) -> UOp:
dst, src, dev = call.src[1], call.src[2], ctx.dev
devs = [dev, src_dev] if (src_dev:=Device[src.device]) is not dev else [dev]
sigs_tls = [(UOp.from_buffer(d.timeline_signal), ctx.host_param(d.timeline_value)) for d in devs]
return UOp(Ops.LINEAR, dtypes.void, (
*[s.wait(t[0] - 1) for s,t in sigs_tls],
UOp(Ops.BARRIER, dtypes.void),
UOp(Ops.CUSTOM_FUNCTION, dtypes.void, src=(ctx.timestamps_gpu + next(ctx.next_timestamp) * 8,), arg="timestamp"),
UOp(Ops.COPY, dtypes.void, src=(dst, src), arg=src.buffer.nbytes),
UOp(Ops.CUSTOM_FUNCTION, dtypes.void, src=(ctx.timestamps_gpu + next(ctx.next_timestamp) * 8,), arg="timestamp"),
*[s.store(t[0]) for s,t in sigs_tls]))
dst, src = call.src[1], call.src[2]
q = UOp(Ops.LINEAR, dtypes.void, (UOp(Ops.COPY, dtypes.void, src=(dst, src), arg=src.buffer.nbytes),), arg=(dst.buffer.device, "COPY"))
return UOp(Ops.LINEAR, dtypes.void, (q,), tag=call.tag)
# lower to hcq-specific commands
pm_hcq_lower = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, src=(UPat(Ops.BUFFER),), name="prg"),), name="call", allow_any_len=True), lower_kernargs),
pm_lower_ops = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, src=(UPat(Ops.BUFFER), UPat()), name="prg"),), name="call", allow_any_len=True), lower_program),
(UPat(Ops.CALL, src=(UPat(Ops.COPY, name="copy"),), name="call", allow_any_len=True), lower_copy),
])
# **************** split into queues ****************
def split_into_queues(ctx:HCQ2LowerCtx, outer:UOp) -> UOp:
groups:dict[tuple, list[UOp]] = collections.defaultdict(list)
for child in outer.src:
wrapper = child.src[0] if child.op is Ops.AFTER else child
for q in wrapper.src: groups[q.arg].extend(q.src)
return outer.replace(src=tuple(UOp(Ops.LINEAR, dtypes.void, tuple(cmds), arg=k) for k, cmds in groups.items()))
pm_split_into_queues = PatternMatcher([(UPat(Ops.LINEAR, src=UPat(Ops.LINEAR, src=UPat(Ops.LINEAR)).or_after(), name="outer"), split_into_queues)])
# **************** add signals (runtime) ****************
def add_signals(ctx:HCQ2LowerCtx, outer:UOp) -> UOp:
def wrap(q:UOp) -> UOp:
(dev_name, qname), devs = q.arg, {q.arg[0]} | {u.buffer.device for u in q.toposort() if u.op in (Ops.BUFFER, Ops.BUFFER_VIEW)}
sigs_tls = [(UOp.from_buffer(Device[d].timeline_signal), ctx.host_param(Device[d].timeline_value)) for d in sorted(devs) if d.startswith("AMD")]
return q.replace(src=(*(s.wait(t[0]-1) for s,t in sigs_tls), *q.src, *(s.store(t[0]) for s,t in sigs_tls)), arg=qname)
return outer.replace(src=tuple(wrap(q) for q in outer.src))
pm_add_barriers = PatternMatcher([(UPat(Ops.LINEAR, src=UPat(Ops.LINEAR), name="outer"),
lambda ctx, outer: outer.replace(src=tuple(q.replace(src=(UOp(Ops.BARRIER, dtypes.void), *q.src)) for q in outer.src)))])
pm_add_signals = PatternMatcher([(UPat(Ops.LINEAR, src=UPat(Ops.LINEAR), name="outer"), add_signals)])
# **************** build host program ****************
def resolve_cmdbuf(ctx:HCQ2LowerCtx, blob:UOp) -> UOp:
inner = blob.src[0] if blob.op is Ops.AFTER else blob
dev_name, qtype = inner.tag
# prepare the cmdbuf and make it a param
bb = Buffer("CPU", len(inner.arg)//4, dtypes.uint32, preallocate=True)
@@ -246,10 +269,10 @@ def resolve_cmdbuf(ctx:HCQ2LowerCtx, blob:UOp) -> UOp:
bb_param = ctx.host_param(bb)
submit_cf = UOp(Ops.CUSTOM_FUNCTION, dtypes.void, src=(bb_param.after(*(blob.src[1:] if blob.op is Ops.AFTER else ())),),
arg=f"submit_{inner.tag.lower()}")
arg=f"submit_{qtype.lower()}", tag=dev_name)
# increment the timeline value
tl = ctx.host_param(ctx.dev.timeline_value)
tl = ctx.host_param(Device[dev_name].timeline_value)
return tl.after(UOp(Ops.BARRIER, dtypes.void, src=(submit_cf,))).index(UOp.const(dtypes.int, 0), ptr=True).store(tl[0] + 1)
def resolve_patches(ctx:HCQ2LowerCtx, buf:UOp) -> UOp|None:
@@ -289,65 +312,66 @@ pm_resolve_ref_buffers = PatternMatcher([(UPat((Ops.BUFFER, Ops.BUFFER_VIEW), na
pm_callify = PatternMatcher([(UPat(Ops.SINK, name="sink"), hcq_callify)])
def hcq_build_host_program(ctx:HCQ2LowerCtx, linear:UOp, ast:UOp) -> UOp:
# **************** schedule ****************
def prep_runtime(ctx:HCQ2LowerCtx, linear:UOp) -> tuple[UOp, dict[str,int]]:
linear = graph_rewrite(linear, pm_prep_runtime, ctx=ctx, name="hcq: prepare runtime")
graph_rewrite(linear, pm_calc_kernargs_sizes, ctx=(sizes:={}), enter_calls=True)
return linear, sizes
def build_host_program(ctx:HCQ2LowerCtx, linear:UOp, ast:UOp, dev:HCQ2Compiled) -> UOp:
sink = graph_rewrite(linear, pm_create_host_sink, ctx=ctx, name="hcq: create host sink", walk=True)
sink = graph_rewrite(sink, pm_lower_cmdbufs, ctx=ctx, bottom_up=True, name="hcq: lower cmdbufs")
sink = graph_rewrite(sink, pm_resolve_patches, ctx=ctx, bottom_up=True, name="hcq: resolve patches")
sink = graph_rewrite(sink, pm_resolve_ref_buffers, ctx=ctx, bottom_up=True, name="hcq: resolve ref buffers")
sink = graph_rewrite(sink, ctx.dev.pm_lower, ctx=ctx, name="hcq: device lower", walk=True)
sink = graph_rewrite(sink, dev.pm_lower, ctx=ctx, name=f"hcq: device lower {dev.device}", walk=True)
return graph_rewrite(sink, pm_callify, ctx=ctx, name="hcq: callify")
# **************** schedule ****************
@track_rewrites(name=lambda ctx,linear,ast,dev,**kw: f"hcq schedule {getattr(ast.arg, 'name', ast.op.name.lower())}")
def hcq_schedule(ctx:HCQ2LowerCtx, linear:UOp, ast:UOp, dev:HCQ2Compiled) -> UOp:
linear, sizes = prep_runtime(ctx, linear)
for dev_name, sz in sizes.items():
off = dev.kernargs_offset_allocator.alloc(sz, 16)
ctx.devs[dev_name] = HCQ2DeviceCtx(dev_name, UOp.from_buffer(dev.kernargs_buf.view(sz, dtypes.uint8, off), dev_name),
UOp.const(dtypes.uint64, dev.kernargs_buf.get_buf(dev_name).va_addr + off))
linear = graph_rewrite(linear, pm_lower_kernargs + pm_lower_ops, ctx=ctx, name="hcq: lower ops")
linear = graph_rewrite(linear, pm_split_into_queues, ctx=ctx, name="hcq: split into queues")
linear = graph_rewrite(linear, pm_add_barriers, ctx=ctx, name="hcq: add barriers", walk=True)
linear = graph_rewrite(linear, pm_add_signals, ctx=ctx, name="hcq: add signals", walk=True)
linear = graph_rewrite(linear, dev.pm_lower, ctx=ctx, name=f"hcq: encode cmdbuf {dev.device}", walk=True)
return build_host_program(ctx, linear, ast, dev)
@track_rewrites(name=lambda dev,ctx,linear,ast,**kw: f"hcq schedule {getattr(ast.arg, 'name', ast.op.name.lower())}")
def hcq_schedule(dev:HCQ2Compiled, ctx:HCQ2LowerCtx, linear:UOp, ast:UOp) -> UOp:
linear = graph_rewrite(linear, pm_prep_runtime, ctx=ctx, name="hcq: prepare runtime")
linear = graph_rewrite(linear, pm_hcq_lower + pm_flatten_linear, ctx=ctx, name="hcq: lower to cmdbuf ops")
linear = UOp(Ops.LINEAR, dtypes.void, (graph_rewrite(linear, dev.pm_lower, ctx=ctx, name="hcq: encode cmdbuf ops"),))
return hcq_build_host_program(ctx, linear, ast)
def ensure_accessible(ctx:HCQ2LowerCtx, call:UOp, copy:UOp) -> UOp|None:
src_buf = call.src[2].buffer # TODO: cleanup
dev = call.src[1].buffer.device
try: src_buf.get_buf(dev)
except Exception:
(cpubuf := Buffer("CPU", src_buf.nbytes, dtypes.uint8, preallocate=True)).copyin(src_buf.ensure_allocated().as_memoryview())
ctx.holds.append(buf_uop:=UOp.from_buffer(cpubuf, dev))
return call.replace(src=call.src[:2] + (buf_uop,) + call.src[3:])
pm_ensure_bufs_accessible = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.COPY, name="copy"),), name="call", allow_any_len=True), ensure_accessible)])
def _resolve_call(ctx:ExecContext, call:UOp, ast:UOp) -> UOp:
from tinygrad.engine.realize import resolve_params
return call.replace(src=(ast,) + tuple(resolve_params(call, ctx.input_uops)) + tuple(s for s in call.src[1:] if s.op is Ops.BIND))
def _run_host_call(ctx:ExecContext, call:UOp, dev:HCQ2Compiled, host_call:UOp, bufs:list[Buffer], ts_buf:Buffer) -> float:
def hcq_exec(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
from tinygrad.engine.realize import run_linear
if ast.src[1].arg.split(":")[0] != "AMD": return None
# TODO: this mess should gone
resolved_call = call.replace(src=(ast,) + tuple(resolve_params(call, ctx.input_uops)) + tuple(s for s in call.src[1:] if s.op is Ops.BIND))
bufs = [cast(Buffer, resolved_call.src[1+gi].buffer) for gi in ast.arg.globals] if ast.op is Ops.PROGRAM \
else [cast(Buffer, resolved_call.src[i].buffer) for i in range(1, len(resolved_call.src))]
dev = cast(HCQ2Compiled, Device[bufs[0].device])
hcq_ctx = HCQ2LowerCtx(name="submit")
linear = graph_rewrite(UOp(Ops.LINEAR, dtypes.void, (resolved_call,)), pm_ensure_bufs_accessible, ctx=hcq_ctx)
host_call = hcq_schedule(hcq_ctx, linear, ast, dev)
with track_stats(ctx, call, dev.device, bufs, ctx.var_vals) as tm:
st = time.perf_counter() if ctx.wait else 0.0
run_linear(UOp(Ops.LINEAR, dtypes.void, (host_call,)), var_vals=ctx.var_vals, jit=True, update_stats=DEBUG>=3)
if ctx.wait:
dev.synchronize()
tss = ts_buf._buf.cpu_view().mv.cast('Q')
tm[0] = (tss[1] - tss[0]) / dev.timestamp_divider / 1e6
tm[0] = time.perf_counter() - st
return tm[0] if tm[0] is not None else 0.0
def hcq_exec_program(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
if ast.src[1].arg.split(":")[0] != "AMD": return None
dev, resolved_call = Device[ast.src[1].arg], _resolve_call(ctx, call, ast)
hcq_ctx = HCQ2LowerCtx(dev=dev, name="submit_program",
kernargs_host=UOp.from_buffer(dev.kernargs_buf, dev.device),
kernargs_gpu=UOp.const(dtypes.uint64, dev.kernargs_buf.get_buf(dev.device).va_addr),
kernargs_allocator=dev.kernargs_offset_allocator, # allocator is passed and it will rotate kernargs
timestamps_gpu=UOp.const(dtypes.uint64, dev.timestamps_buf.get_buf(dev.device).va_addr))
host_call = hcq_schedule(dev, hcq_ctx, UOp(Ops.LINEAR, dtypes.void, (resolved_call,), arg="COMPUTE"), ast)
prg_bufs = [cast(Buffer, resolved_call.src[1+gi].buffer) for gi in ast.arg.globals]
return _run_host_call(ctx, call, dev, host_call, prg_bufs, ts_buf=dev.timestamps_buf)
def hcq_exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
if ast.src[1].arg.split(":")[0] != "AMD": return None
dev, resolved_call = Device[ast.src[1].arg], _resolve_call(ctx, call, ast)
hcq_ctx = HCQ2LowerCtx(name="submit_copy", dev=dev, timestamps_gpu=UOp.const(dtypes.uint64, dev.timestamps_buf.get_buf(dev.device).va_addr))
src_buf = resolved_call.src[2].buffer
try: src_buf.get_buf(dev.device)
except Exception:
(cpubuf := Buffer("CPU", src_buf.nbytes, dtypes.uint8, preallocate=True)).copyin(src_buf.ensure_allocated().as_memoryview())
hcq_ctx.holds.append(buf_uop:=UOp.from_buffer(cpubuf, dev.device))
resolved_call = resolved_call.replace(src=resolved_call.src[:2] + (buf_uop,) + resolved_call.src[3:])
host_call = hcq_schedule(dev, hcq_ctx, UOp(Ops.LINEAR, dtypes.void, (resolved_call,), arg="COPY"), ast)
bufs = [cast(Buffer, resolved_call.src[1].buffer), cast(Buffer, resolved_call.src[2].buffer)]
return _run_host_call(ctx, call, dev, host_call, bufs, ts_buf=dev.timestamps_buf)
pm_hcq_exec = PatternMatcher([
# TODO: use upat device=?
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="ast"),), name="call", allow_any_len=True), hcq_exec_program),
(UPat(Ops.CALL, src=(UPat(Ops.COPY, name="ast"),), name="call", allow_any_len=True), hcq_exec_copy),
(UPat(Ops.CALL, src=(UPat({Ops.PROGRAM, Ops.COPY}, name="ast"),), name="call", allow_any_len=True), hcq_exec),
])
+36 -53
View File
@@ -16,10 +16,9 @@ from tinygrad.runtime.autogen.am import am
from tinygrad.runtime.support.elf import elf_loader
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
from tinygrad.runtime.support.system import System, PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
from tinygrad.runtime.support.usb import USB3
from tinygrad.runtime.support.memory import AddrSpace, BumpAllocator
from tinygrad.runtime.support.hcq import MMIOInterface
from tinygrad.runtime.ops_amd import SQTT, SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE, PMC
from tinygrad.runtime.ops_amd import EVENT_INDEX_PARTIAL_FLUSH, WAIT_REG_MEM_FUNCTION_EQ, WAIT_REG_MEM_FUNCTION_NEQ, WAIT_REG_MEM_FUNCTION_GEQ
if getenv("IOCTL"): import extra.hip_gpu_driver.hip_ioctl # noqa: F401 # pylint: disable=unused-import
@@ -29,8 +28,8 @@ from tinygrad.engine.realize import get_runtime
from tinygrad.uop.ops import Ops, UPat, PatternMatcher, graph_rewrite
class AMDComputeQueue(HCQEncoder):
def __init__(self, ctx:HCQ2LowerCtx):
super().__init__(ctx)
def __init__(self, ctx:HCQ2LowerCtx, dev:AMDDevice):
super().__init__(ctx, dev)
self.pm4, self.gc, self.nbio, self.soc = self.dev.pm4, self.dev.gc, self.dev.nbio, self.dev.soc
def pkt3(self, cmd, *vals): self.q(self.pm4.PACKET3(cmd, len(vals) - 1), *vals)
@@ -142,13 +141,16 @@ amd_inner_pm = PatternMatcher([
])
def amd_lower_pm4(ctx, linear):
enc = AMDComputeQueue(ctx)
prg = next(s for s in linear.src if s.op is Ops.PROGRAM)
dev = Device[prg.src[1].arg]
enc = AMDComputeQueue(ctx, dev)
graph_rewrite(linear, amd_inner_pm, ctx=enc, name="amd: encode")
return UOp(Ops.BINARY, dtypes.void, arg=enc.blob).rtag("COMPUTE").after(*enc.src)
return UOp(Ops.BINARY, dtypes.void, arg=enc.blob).rtag((dev.device, "COMPUTE")).after(*enc.src)
def amd_submit_pm4(ctx, cf):
dev = Device[cf.tag]
bb_param = cf.src[0]
q = ctx.dev.compute_queue
q = dev.compute_queue
ring, wptr, doorbell, put_ptr = (ctx.host_param(b) for b in (q.ring, q.write_ptr, q.doorbell, q.put_value))
size, ring_dwords = UOp.const(dtypes.uint32, bb_param.dtype.size), q.ring.size
@@ -164,8 +166,8 @@ def amd_submit_pm4(ctx, cf):
return doorbell.after(flush)[0].store(next_put)
class AMDCopyQueue(HCQEncoder):
def __init__(self, ctx:HCQ2LowerCtx, queue_idx=0):
super().__init__(ctx)
def __init__(self, ctx:HCQ2LowerCtx, dev:AMDDevice, queue_idx=0):
super().__init__(ctx, dev)
self.sdma, self.queue_idx, self.max_copy_size = self.dev.sdma, queue_idx, self.dev.max_copy_size
def copy(self, x):
@@ -192,9 +194,11 @@ class AMDCopyQueue(HCQEncoder):
*data64_le(self.get_dev_addr(x.src[0])))
def amd_lower_sdma(ctx, linear):
enc = AMDCopyQueue(ctx)
copy = next(s for s in linear.src if s.op is Ops.COPY)
dev = Device[copy.src[0].buffer.device]
enc = AMDCopyQueue(ctx, dev)
graph_rewrite(linear, amd_inner_sdma_pm, ctx=enc, name="amd: encode sdma")
return UOp(Ops.BINARY, dtypes.void, arg=enc.blob).rtag("COPY").after(*enc.src)
return UOp(Ops.BINARY, dtypes.void, arg=enc.blob).rtag((dev.device, "COPY")).after(*enc.src)
amd_inner_sdma_pm = PatternMatcher([
(UPat(Ops.WAIT, name="x"), lambda ctx, x: ctx.wait(x)),
@@ -205,8 +209,9 @@ amd_inner_sdma_pm = PatternMatcher([
])
def amd_submit_sdma(ctx, cf):
dev = Device[cf.tag]
bb_param = cf.src[0]
q = ctx.dev.sdma_queue(0)
q = dev.sdma_queue(0)
ring, wptr, doorbell, put_ptr = (ctx.host_param(b) for b in (q.ring, q.write_ptr, q.doorbell, q.put_value))
size_dw, ring_bytes = bb_param.dtype.size, q.ring.size * 4
@@ -237,23 +242,24 @@ class AMDProgramData:
_amd_program_cache:dict[tuple[bytes,str], tuple[AMDProgramData,Buffer]] = {}
def amd_build_program(ctx:HCQ2LowerCtx, prg:UOp) -> UOp:
if (cached:=_amd_program_cache.get(key:=(lib:=prg.src[4].arg, ctx.dev.device))) is None:
dev = Device[prg.src[1].arg]
if (cached:=_amd_program_cache.get(key:=(lib:=prg.src[4].arg, dev.device))) is None:
image, sections, relocs = elf_loader(lib)
rodata = next(sh.header.sh_addr for sh in sections if sh.name == ".rodata")
for off, sym, typ, addent in relocs:
assert typ == 5, f"unknown AMD reloc {typ}" # R_AMDGPU_REL64
image[off:off+8] = struct.pack('<q', sym - off + addent)
lib_gpu = Buffer(ctx.dev.device, round_up(image.nbytes, 0x1000), dtypes.uint8, options=BufferSpec(nolru=True), preallocate=True)
ctx.dev.allocator._copyin(lib_gpu._buf, image)
ctx.dev.synchronize()
lib_gpu = Buffer(dev.device, round_up(image.nbytes, 0x1000), dtypes.uint8, options=BufferSpec(nolru=True), preallocate=True)
dev.allocator._copyin(lib_gpu._buf, image)
dev.synchronize()
desc = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t.from_buffer_copy(bytes(image[rodata:rodata+ctypes.sizeof(amdgpu_kd.llvm_amdhsa_kernel_descriptor_t)]))
if (lds:=((desc.group_segment_fixed_size+511)//512)&0x1FF) > (ctx.dev.iface.props['lds_size_in_kb']*1024)//512:
if (lds:=((desc.group_segment_fixed_size+511)//512)&0x1FF) > (dev.iface.props['lds_size_in_kb']*1024)//512:
raise RuntimeError("Too many resources requested: group_segment_size")
ctx.dev._ensure_has_local_memory(desc.private_segment_fixed_size)
dev._ensure_has_local_memory(desc.private_segment_fixed_size)
edp = desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_DISPATCH_PTR
cached = _amd_program_cache[key] = (AMDProgramData(
entry_point_offset=rodata + desc.kernel_code_entry_byte_offset,
rsrc1=desc.compute_pgm_rsrc1 | ((1<<20) if ctx.dev.target[0]==11 else 0), # priv=1 on gfx11 for cwsr
rsrc1=desc.compute_pgm_rsrc1 | ((1<<20) if dev.target[0]==11 else 0), # priv=1 on gfx11 for cwsr
rsrc2=desc.compute_pgm_rsrc2 | (lds<<15), rsrc3=desc.compute_pgm_rsrc3,
wave32=bool(desc.kernel_code_properties & 0x400),
kernargs_segment_size=desc.kernarg_size,
@@ -262,7 +268,7 @@ def amd_build_program(ctx:HCQ2LowerCtx, prg:UOp) -> UOp:
enable_private_segment_sgpr=desc.kernel_code_properties & hsa.AMD_KERNEL_CODE_PROPERTIES_ENABLE_SGPR_PRIVATE_SEGMENT_BUFFER,
), lib_gpu)
data, lib_gpu = cached
return prg.replace(src=(UOp.from_buffer(lib_gpu, ctx.dev.device),), arg=(data, prg.arg))
return prg.replace(src=(UOp.from_buffer(lib_gpu, dev.device),), arg=(data, prg.arg))
class AMDAllocator(HCQAllocator['AMDDevice']):
def __init__(self, dev:AMDDevice):
@@ -284,29 +290,6 @@ class AMDQueueDesc:
put_value: Buffer # uint64[1]
params: tuple|None = None # setup_ring params for recovery
@property
def ring_mv(self) -> MMIOInterface: return self.ring._buf.view.view(fmt='I')
@property
def rptr_mv(self) -> MMIOInterface: return self.read_ptr._buf.view.view(fmt='Q')
@property
def wptr_mv(self) -> MMIOInterface: return self.write_ptr._buf.view.view(fmt='Q')
@property
def doorbell_mv(self) -> MMIOInterface: return self.doorbell._buf.view.view(fmt='Q')
@property
def put(self) -> int: return self.put_value._buf.view.view(fmt='Q')[0]
@put.setter
def put(self, v:int): self.put_value._buf.view.view(fmt='Q')[0] = v
def signal_doorbell(self, dev, doorbell_value:int|None=None):
try:
self.wptr_mv[0] = self.put
System.memory_barrier()
if dev.is_am() and not dev.is_usb(): dev.iface.dev_impl.gmc.flush_hdp()
self.doorbell_mv[0] = self.put if doorbell_value is None else doorbell_value
except Exception as e:
dev.error_state = e
raise
class PCIIface(PCIIfaceBase):
def __init__(self, dev, dev_id):
super().__init__(dev, dev_id, vendor=0x1002, devices=((0xffff, (0x74a1,0x744c,0x7480,0x7550,0x7551,0x7590,0x75a0)),), vram_bar=0,
@@ -348,22 +331,22 @@ class PCIIface(PCIIfaceBase):
eop_buffer.va_addr, eop_buffer.size, is_aql:=(queue_type==kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL), is_aql)))
ext = lambda addr,n,dt: Buffer("CPU", n, dt, options=BufferSpec(external_ptr=addr), preallocate=True)
(put_value := Buffer("CPU", 1, dtypes.uint64, preallocate=True))._buf.view.view(fmt='Q')[0] = 0
return AMDQueueDesc(ring=ext(ring.va_addr, ring.size//4, dtypes.uint32),
doorbell=ext(self.dev_impl.doorbell64.addr + doorbell_index*8, 1, dtypes.uint64),
read_ptr=ext(gart.va_addr+rptr, 1, dtypes.uint64), write_ptr=ext(gart.va_addr+wptr, 1, dtypes.uint64),
put_value=Buffer("CPU", 1, dtypes.uint64, preallocate=True), params=rcvr_params)
put_value=put_value, params=rcvr_params)
def _collect_interrupts(self, reset=False, drain_only=False):
devs:list[AMDDevice] = [d for pg in HCQCompiled.peer_groups.values() for d in pg if isinstance(d, AMDDevice) and d.is_am()]
for d in devs:
if drain_only: d.iface.dev_impl.ih.drain()
else: d.iface.dev_impl.ih.interrupt_handler()
d = self.dev
if drain_only: d.iface.dev_impl.ih.drain()
else: d.iface.dev_impl.ih.interrupt_handler()
if reset and d.iface.dev_impl.recover(force=d.error_state is not None):
d.compute_queue.put = d.compute_queue.rptr_mv[0] = d.compute_queue.wptr_mv[0] = 0
d.iface.dev_impl.gfx.setup_ring(*d.compute_queue.params)
d.timeline_signal.value = d.timeline_value - 1
d.error_state = None
if reset and d.iface.dev_impl.recover():
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
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))):
+4 -2
View File
@@ -53,8 +53,10 @@ def _fused_quantize_bwd_w13(gradient:UOp, kernel:UOp):
inv_scale = (grad_amax_state_t.float() + 1e-8) / FP8_MAX
new_grad_amax = scalar_amax(grad_amax_buf)
store_effect = grad_amax_state_t.uop.store(new_grad_amax.uop)
# Stash fp8 companion + amax store for cdna_asm_gemm's bwd to attach to grad_a.
_grad_fp8_mailbox[grad_xw13.uop] = (grad_xw13_fp8.uop, inv_scale.uop, new_grad_amax.uop, store_effect)
assert grad_xw13_fp8.uop.op is Ops.AFTER, f"expected AFTER, got {grad_xw13_fp8.uop.op}"
grad_xw13_fp8_uop = grad_xw13_fp8.uop.replace(src=grad_xw13_fp8.uop.src + (store_effect,))
# Stash fp8 companion for cdna_asm_gemm's bwd to attach to grad_a.
_grad_fp8_mailbox[grad_xw13.uop] = (grad_xw13_fp8_uop, inv_scale.uop)
return (None, None, grad_xw13.uop, None, None)
def fused_quantize_fp8_w13(xw13:Tensor, amax_state:Tensor, fp8_dtype, grad_amax_state:Tensor) -> tuple[Tensor, Tensor, Tensor]:
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""FP16/FP32 MAD peak repro for comparing DEV=CL and DEV=QCOM.
Example:
DEV=CL python3 extra/mmapeak/qcom_fp16_mad_peak.py
DEV=QCOM python3 extra/mmapeak/qcom_fp16_mad_peak.py --dtype fp32
"""
from __future__ import annotations
import argparse
from tinygrad import Device, dtypes
from tinygrad.device import Buffer
MAD_OPS_PER_LOOP = 16
VEC = 16
def kernel_name(dtype:str) -> str:
return f"{dtype}_mad_peak"
def make_kernel(loops:int, dtype:str="fp16") -> str:
assert dtype in {"fp16", "fp32"}
scalar = "half" if dtype == "fp16" else "float"
vec_type = f"{scalar}{VEC}"
prefix = "#pragma OPENCL EXTENSION cl_khr_fp16 : enable\n" if dtype == "fp16" else ""
cast = "(half)" if dtype == "fp16" else ""
suffix = "f"
mad_block = "\n".join([
" x = mad(y, x, y);",
" y = mad(x, y, x);",
] * (MAD_OPS_PER_LOOP // 2))
x_init = ",\n ".join(f"bx + {cast}{(i + 1) * 0.001:.3f}{suffix}" for i in range(VEC))
y_init = ",\n ".join(f"by + {cast}{(i + 17) * 0.001:.3f}{suffix}" for i in range(VEC))
sum_terms = " + ".join([f"x.s{'0123456789abcdef'[i]}" for i in range(VEC)] +
[f"y.s{'0123456789abcdef'[i]}" for i in range(VEC)])
return f"""{prefix}__kernel void {kernel_name(dtype)}(__global {scalar} *out) {{
int lid = get_local_id(0);
int gid = get_group_id(0);
{scalar} bx = {cast}1.0f + {cast}(lid & 15) * {cast}0.001f;
{scalar} by = {cast}1.0f + {cast}(gid & 15) * {cast}0.001f;
{vec_type} x = ({vec_type})(
{x_init});
{vec_type} y = ({vec_type})(
{y_init});
for (int i = 0; i < {loops}; i++) {{
{mad_block}
}}
out[get_global_id(0)] = {sum_terms};
}}"""
def run(args:argparse.Namespace) -> None:
dev = Device[Device.DEFAULT]
renderer = type(dev.renderer).__name__
if renderer == "IR3Renderer":
raise SystemExit("This repro uses OpenCL source. Use DEV=QCOM or DEV=CL, not DEV=QCOM:IR3.")
dtype = args.dtype
dt = dtypes.half if dtype == "fp16" else dtypes.float
src = make_kernel(args.loops, dtype)
if args.print_source: print(src)
lib = dev.compiler.compile_cached(src)
if args.disasm: dev.compiler.disassemble(lib)
# Runtime aux mirrors OpenCLRenderer.aux: one __global output pointer at kernel arg 0.
global_size = (args.groups, 1, 1)
local_size = (args.local, 1, 1)
workitems = args.groups * args.local
flops = workitems * args.loops * MAD_OPS_PER_LOOP * VEC * 2
prg = dev.runtime(kernel_name(dtype), lib, (((0, dt.ptr()),),))
out = Buffer(dev.device, workitems, dt, preallocate=True)
for _ in range(args.warmup):
prg(out._buf, global_size=global_size, local_size=local_size, wait=True)
times = [prg(out._buf, global_size=global_size, local_size=local_size, wait=True) for _ in range(args.iters)]
best = min(t for t in times if t is not None)
out_bits = out.copyout(memoryview(bytearray(out.nbytes))).cast("H" if dtype == "fp16" else "I")[0]
out_fmt = "04x" if dtype == "fp16" else "08x"
print(f"device={dev.device} renderer={renderer} arch={dev.arch}")
print(f"dtype={dtype} groups={args.groups} local={args.local} workitems={workitems} loops={args.loops} flops={flops}")
print(f"best={best*1e6:.2f} us {dtype}_mad_peak={flops / best * 1e-9:.2f} GFLOPS out0=0x{out_bits:{out_fmt}}")
if args.show_times:
print("times_us=" + ",".join(f"{t*1e6:.2f}" for t in times if t is not None))
def main() -> None:
parser = argparse.ArgumentParser(description="FP16/FP32 MAD peak repro for DEV=CL vs DEV=QCOM")
parser.add_argument("--dtype", choices=("fp16", "fp32"), default="fp16", help="MAD datatype")
parser.add_argument("--groups", type=int, default=2048, help="number of workgroups")
parser.add_argument("--local", type=int, default=256, help="workitems per workgroup")
parser.add_argument("--loops", type=int, default=8, help="inner loop count; default matches clpeak vec16")
parser.add_argument("--warmup", type=int, default=2, help="warmup launches")
parser.add_argument("--iters", type=int, default=10, help="timed launches")
parser.add_argument("--show-times", action="store_true", help="print every timed launch")
parser.add_argument("--print-source", action="store_true", help="print generated OpenCL source")
parser.add_argument("--disasm", action="store_true", help="call the tinygrad compiler disassembler after compile")
run(parser.parse_args())
if __name__ == "__main__":
main()
-2
View File
@@ -84,8 +84,6 @@ def serve(conn:socket.socket):
conn.sendall(resp_err(str(e)))
if __name__ == "__main__":
if not OSX: System.reserve_hugepages(128) # for sysmem allocations
port = int(sys.argv[1]) if len(sys.argv) > 1 else 6667
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
install_loc="$HOME/.local/bin"
docker build -t qemu-hexagon-static:latest - <<'EOF'
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends qemu-user-static ca-certificates && rm -rf /var/lib/apt/lists/*
EOF
mkdir -p "$install_loc"
tee "$install_loc/qemu-hexagon-static" >/dev/null <<'EOF'
#!/bin/sh
set -eu
exec docker run --rm -i \
-v /var/folders:/var/folders -v "$HOME":"$HOME" \
qemu-hexagon-static:latest qemu-hexagon-static "$@"
EOF
chmod +x "$install_loc/qemu-hexagon-static"
+1 -1
View File
@@ -105,7 +105,7 @@ class TestKernelFusionRegression(unittest.TestCase):
view = x[1:3]
view += 1.0
return x.sum()
self._check_kernel_count(fn, 8)
self._check_kernel_count(fn, 7)
def test_batchnorm_running_stats_update(self):
def fn():
+4 -4
View File
@@ -7,15 +7,15 @@ def get_node(graph:dict, key): return graph[str(key)]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="print CALL graph from DEBUG=5 tinygrad.viz.cli --json output")
parser.add_argument("kernel", type=str, default=None, help="Kernel name to stop at (default: print all kernels)")
parser.add_argument("kernel", type=str, nargs="?", default="ALL", metavar="NAME", help="Kernel name to stop at (default: print all kernels)")
args = parser.parse_args()
ref:int|None = None
for line in sys.stdin:
if not line.strip(): continue
graph = json.loads(line)
if ref is not None and graph.get("ref") == ref:
if graph.get("ref") is not None and (args.kernel == "ALL" or graph["ref"] == ref):
print(graph)
if (v:=json.loads(next(sys.stdin)).get("value")): print(v)
if (v:=json.loads(next(sys.stdin, "{}")).get("value")): print(v)
if ref is not None or not isinstance(rec:=next(iter(graph.values()), {}), dict) or "label" not in rec: continue
for v in graph.values():
if not v["label"].startswith("CALL"): continue
@@ -39,6 +39,6 @@ if __name__ == "__main__":
src_str = ["SRC"]+get_node(graph, get_node(graph, s)["src"][0][1])["label"].splitlines()[1:]
print(" ".join(idx_str+src_str))
ss += [x[1] for x in get_node(graph, s)["src"]]
if args.kernel is not None and args.kernel in ansistrip(v["label"]):
if args.kernel != "ALL" and args.kernel in ansistrip(v["label"]):
ref = v["ref"]
break
BIN
View File
Binary file not shown.
+20 -10
View File
@@ -16,6 +16,7 @@
\definecolor{elwyellow}{HTML}{F9A825}
\definecolor{callblue}{HTML}{1565C0}
\definecolor{assignbrown}{HTML}{795548}
\definecolor{loadred}{HTML}{c08080}
\definecolor{multipurple}{HTML}{7B1FA2}
\definecolor{markerorange}{HTML}{E65100}
% AxisType colors (from tinygrad)
@@ -48,16 +49,16 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
\toprule
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
\midrule
\op{Param} & $(\mathbf{s})$ & slot, dtype, device?, addrspace? &
Placeholder with shape $\mathbf{s}$. Substituted in \op{Function}. \\[4pt]
\op{Buffer} & () & size, dtype, device, addrspace &
Shape $(n \cdot \textit{size},)$ if device is $n$-tuple, else $(\textit{size},)$. \\
\op{BufferView} & (buf,) & size, dtype, offset &
Typed access into a buffer. Zero-copy $(\textit{size},)$ slice at offset; inherits addrspace. \\
\op{Param} & $(\mathbf{s})$ or $(\mathbf{s}, \text{min}, \text{max})$ & slot, dtype, device? &
Placeholder with shape $\mathbf{s}$. Substituted in \op{Function}. \\[4pt]
\op{Const} & () & value, dtype &
A scalar constant with shape $(\ )$. \\
\op{Vconst} & () & values, dtype &
A vector constant with shape $(n,)$. \\
& & & Form vector consts with \op{Stack} \\
\op{Binary} & () & data & Raw binary data, has dtype uint8 and shape len($data$) \\
\bottomrule
\end{tabular}
@@ -90,7 +91,7 @@ A \op{Buffer}'s \textbf{addrspace} is \texttt{GLOBAL}, \texttt{LOCAL}, or \textt
\toprule
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
\midrule
\op{Reduce} & $(T,)$ & op, axes & Reduce $T$ along axes. Op is \op{Add}, \op{Max}, or \op{Mul}. \\
\op{Reduce} & ($T$, $r_0$, $r_1$, \ldots) & op, axes & Reduce $T$ along axes or ranges. Op is \op{Add}, \op{Max}, or \op{Mul}. \\
\bottomrule
\end{tabular}
@@ -109,13 +110,25 @@ A \op{Buffer}'s \textbf{addrspace} is \texttt{GLOBAL}, \texttt{LOCAL}, or \textt
\end{tabular}
%% ============================================================
\subsection*{{\color{multipurple}Store Ops} \normalfont\small--- side effects}
\subsection*{{\color{loadred}Load Ops} \normalfont\small--- can change device or addrspace}
\begin{tabular}{@{}l l l l@{}}
\toprule
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
\midrule
\op{Store} & (buf, val, gate?) & --- & Write val into buf. buf.shape $=$ val.shape. \\
\op{Load} & (buf, alt?, gate?) & device, addrspace & Read (pull) from buffer into a new anonymous buffer. \\
& & & Note: this replaces \op{Copy} and \op{Contiguous}. \\
\bottomrule
\end{tabular}
%% ============================================================
\subsection*{{\color{multipurple}Store Ops} \normalfont\small--- the only op with observable side effects}
\begin{tabular}{@{}l l l l@{}}
\toprule
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
\midrule
\op{Store} & (buf, val, gate?) & --- & Write (push) val into buf. buf.shape $=$ val.shape. \\
& & & If gate is present, write only when gate is true. Output is void. \\
\bottomrule
\end{tabular}
@@ -205,7 +218,6 @@ Ternary & $(P, A, B)$
\op{Contiguous} & $(T,)$ & --- & Force contiguous memory layout. \\
\op{ContiguousBackward} & $(T,)$ & --- & Force contiguous in backward pass. \\
\op{Detach} & $(T,)$ & --- & Stops gradient propagation. \\
\op{Copy} & $(T,)$ & device & Copy to target device. \\
\bottomrule
\end{tabular}
@@ -216,8 +228,6 @@ Ternary & $(P, A, B)$
\toprule
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
\midrule
\op{Load} & (idx,alt?,gate?) & --- & Dereference: read element at index from buffer. \\
& & & All loads will be replaced by \op{Store}. \\
\op{Barrier} & (deps\ldots) & --- & Synchronize threads within a workgroup. \\
\op{Ins} & \ldots & \ldots & A single machine instruction (e.g.\ AMD ISA). \\
\op{Special} & (bound,) & name & GPU thread/workgroup index (e.g.\ \texttt{gidx0}, \texttt{lidx1}). \\
+2 -2
View File
@@ -12,8 +12,8 @@ def is_cdna4(): return Device[Device.DEFAULT].renderer.target.arch.startswith("g
def run_asm_gemm(a_shape, b_shape, dtype=dtypes.float16, a_shard=None, b_shard=None, gpus:int=1) -> None:
Tensor.manual_seed(0)
a_rand = Tensor.randn(a_shape, dtype=dtypes.float).sub(0.5).cast(dtype)
b_rand = Tensor.randn(b_shape, dtype=dtypes.float).sub(0.5).cast(dtype)
a_rand = Tensor.randn(a_shape, dtype=dtypes.float, requires_grad=False).sub(0.5).cast(dtype)
b_rand = Tensor.randn(b_shape, dtype=dtypes.float, requires_grad=False).sub(0.5).cast(dtype)
with Context(DEBUG=0):
Tensor.realize(a_rand, b_rand)
-4
View File
@@ -330,10 +330,6 @@ class TestBitCast(unittest.TestCase):
# should fail because 3 int8 is 3 bytes but float16 is two and 3 isn't a multiple of 2
Tensor.empty((3,), dtype=dtypes.int8).bitcast(dtypes.float16)
with self.assertRaises(RuntimeError):
# should fail because backprop through bitcast is undefined
Tensor.empty((4,), dtype=dtypes.int8, requires_grad=True).bitcast(dtypes.float16)
def test_bitcast_float_to_int32(self):
a = Tensor([1.,2,3])
b = a.bitcast(dtypes.int32)
-1
View File
@@ -91,7 +91,6 @@ class TestEmptyTensorEdgeCases(unittest.TestCase):
with self.assertRaises(RuntimeError):
Tensor([]).argmax()
@unittest.expectedFailure
def test_masked_select_empty(self):
# Masked select on empty tensors should return an empty tensor.
torch_out = torch.tensor([], dtype=torch.float32).masked_select(torch.tensor([], dtype=torch.bool))
+19
View File
@@ -333,6 +333,25 @@ class TestJitFootguns(unittest.TestCase):
with self.assertRaises(JitError):
f(Tensor([1, 2, 3, 4]), Tensor([True, False, True, False])) # capture - .item() raises
def test_masked_select_static_size_jittable(self):
@TinyJit
def f(x, mask): return x.masked_select(mask, size=4, fill_value=-1).realize()
for _ in range(3):
np.testing.assert_equal(f(Tensor([1, 2, 3, 4]), Tensor([True, False, True, False])).numpy(), [1, 3, -1, -1])
np.testing.assert_equal(f(Tensor([5, 6, 7, 8]), Tensor([False, True, True, True])).numpy(), [6, 7, 8, -1])
np.testing.assert_equal(f(Tensor([9, 8, 7, 6]), Tensor([True, True, True, True])).numpy(), [9, 8, 7, 6])
np.testing.assert_equal(f(Tensor([1, 1, 1, 1]), Tensor([False, False, False, False])).numpy(), [-1, -1, -1, -1])
def test_nonzero_static_size_jittable(self):
@TinyJit
def f(x): return x.nonzero(size=3, fill_value=-1).realize()
for _ in range(3):
np.testing.assert_equal(f(Tensor([1, 0, 2, 0, 3])).numpy(), [[0], [2], [4]])
np.testing.assert_equal(f(Tensor([0, 0, 5, 0, 0])).numpy(), [[2], [-1], [-1]])
np.testing.assert_equal(f(Tensor([0, 0, 0, 0, 0])).numpy(), [[-1], [-1], [-1]])
def test_tolist_bakes_in_values(self):
""".tolist() raises error during JIT capture (would bake in values)."""
@TinyJit
+33 -3
View File
@@ -1060,10 +1060,17 @@ class TestOps(unittest.TestCase):
helper_test_op([()], torch.erf, Tensor.erf)
def test_gelu(self):
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), Tensor.gelu)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), lambda x: Tensor.gelu(x, approximate="tanh"))
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="none"), lambda x: Tensor.gelu(x, approximate="none"))
def test_gelu_extreme(self):
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), Tensor.gelu, low=300, high=400)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), Tensor.gelu, low=-400, high=-300)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), lambda x: Tensor.gelu(x, approximate="tanh"),
low=300, high=400)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="tanh"), lambda x: Tensor.gelu(x, approximate="tanh"),
low=-400, high=-300)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="none"), lambda x: Tensor.gelu(x, approximate="none"),
low=300, high=400)
helper_test_op([(45,65)], lambda x: torch.nn.functional.gelu(x, approximate="none"), lambda x: Tensor.gelu(x, approximate="none"),
low=-400, high=-300)
def test_quick_gelu(self):
helper_test_op([(45,65)], lambda x: x * torch.sigmoid(1.702 * x), Tensor.quick_gelu)
helper_test_op([()], lambda x: x * torch.sigmoid(1.702 * x), Tensor.quick_gelu)
@@ -3330,10 +3337,33 @@ class TestOps(unittest.TestCase):
helper_test_op([(32, 10)], lambda x: x.masked_select(x>0.5), lambda x: x.masked_select(x>0.5), forward_only=True)
helper_test_op([(32, 10)], lambda x: x.masked_select(torch.tensor(True)), lambda x: x.masked_select(Tensor(True)), forward_only=True)
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
def test_masked_select_size(self):
t = Tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])
mask = Tensor([True, False, True, False, True, False, False, False, True])
np.testing.assert_equal(t.masked_select(mask, size=4).numpy(), [0, 2, 4, 8])
np.testing.assert_equal(t.masked_select(mask, size=6, fill_value=-1).numpy(), [0, 2, 4, 8, -1, -1])
np.testing.assert_equal(t.masked_select(mask, size=2).numpy(), [0, 2])
np.testing.assert_equal(Tensor([], dtype=dtypes.int32).masked_select(Tensor([], dtype=dtypes.bool), size=2, fill_value=-1).numpy(), [-1, -1])
# fill_value must not alter output dtype
self.assertEqual(Tensor([1.0, 2.0]).masked_select(Tensor([True, False]), size=3, fill_value=-1).dtype, dtypes.default_float)
def test_nonzero(self):
helper_test_op([(32, 10)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
helper_test_op([(20,)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
helper_test_op([(10, 5, 3)], lambda x: (x>0.5).nonzero().int(), lambda x: (x>0.5).nonzero(), forward_only=True)
for v in (0, 1, 0.0, 2.5, True, False):
helper_test_op(None, lambda x: x.nonzero().int(), lambda x: x.nonzero(), vals=[v], forward_only=True)
@unittest.skipIf(COMPILE_ONLY, "test requires runtime")
def test_nonzero_size(self):
np.testing.assert_equal(Tensor([1, 0, 2, 0, 3]).nonzero(size=3).numpy(), [[0], [2], [4]])
np.testing.assert_equal(Tensor([1, 0, 2, 0, 3]).nonzero(size=5, fill_value=-1).numpy(), [[0], [2], [4], [-1], [-1]])
np.testing.assert_equal(Tensor([[1, 0], [0, 2]]).nonzero(size=2).numpy(), [[0, 0], [1, 1]])
self.assertEqual(Tensor(5).nonzero(size=4).shape, (4, 0))
np.testing.assert_equal(Tensor([], dtype=dtypes.int32).nonzero(size=3, fill_value=-1).numpy(), [[-1], [-1], [-1]])
# fill_value must not promote dtype to float
self.assertEqual(Tensor([1, 0]).nonzero(size=3, fill_value=-1.5).dtype, dtypes.default_int)
def test_cast(self):
helper_test_op([(3, 3)], lambda x: x.float())
+2 -2
View File
@@ -109,9 +109,9 @@ def fa():
def fa_bw():
Tensor.manual_seed(1337)
with Context(DEBUG=0):
q,k,v = [Tensor.rand(BS, HEADS, SEQLEN, EMB).contiguous().realize().requires_grad_() for _ in range(3)]
q,k,v = [Tensor.rand(BS, HEADS, SEQLEN, EMB).contiguous().realize() for _ in range(3)]
attn_output = nn.Linear(HEADS*EMB, HEADS*EMB, bias=False)
attn_output.weight.requires_grad_().realize()
attn_output.weight.realize()
target = Tensor.rand(BS, SEQLEN, HEADS*EMB).contiguous().realize()
GlobalCounters.reset()
+3 -13
View File
@@ -238,19 +238,9 @@ class TestSchedule(unittest.TestCase):
run_linear(*check_schedule(out, 4))
np.testing.assert_allclose(out.numpy(), (x.numpy() - x.numpy().max(keepdims=True)).max())
@unittest.skip("these two Tensors are the same")
def test_example_matmul(self):
x = Tensor.eye(64, requires_grad=True)
y = Tensor.eye(64, requires_grad=True)
z = y.matmul(x).sum()
z.backward()
out = x.grad.contiguous()
run_linear(*check_schedule(out, 1))
np.testing.assert_allclose(out.numpy(), np.ones((64,64)))
def test_example_matmul_contig(self):
x = Tensor.eye(64, requires_grad=True).contiguous().realize()
y = Tensor.eye(64, requires_grad=True).contiguous().realize()
x = Tensor.eye(64).contiguous().realize()
y = Tensor.eye(64).contiguous().realize()
z = y.matmul(x).sum()
z.backward()
out = x.grad.contiguous()
@@ -258,7 +248,7 @@ class TestSchedule(unittest.TestCase):
np.testing.assert_allclose(out.numpy(), np.ones((64,64)))
def test_example_matmul_same(self):
x = Tensor.eye(64, requires_grad=True)
x = Tensor.eye(64)
z = x.matmul(x).sum()
z.backward()
out = x.grad.contiguous()
+22
View File
@@ -344,6 +344,28 @@ class TestWithGrad(unittest.TestCase):
with self.assertRaises(RuntimeError):
z[:2] = Tensor([0.0, 0.0])
def test_setitem_raises_with_unrealized_downstream(self):
x = Tensor([1.0, 2.0, 3.0, 4.0]).realize()
_y = x * 2.0
with self.assertRaises(RuntimeError):
x[0] = 99.0
def test_setitem_raises_on_unrealized_compute_base(self):
# y has a compute (unrealized) base; tmp is a view of y. eager: tmp would follow y's mutation. lazy: tmp keeps the old MUL graph.
x = Tensor([1.0, 2.0, 3.0, 4.0]).realize()
y = x * 2.0
_tmp = y[:1]
with self.assertRaises(RuntimeError):
y[0] = 99.0
def test_setitem_raises_on_aliased_uop(self):
# two Tensor objects sharing the exact same unrealized uop. setitem on one updates its uop, the other keeps the stale graph reference.
x = Tensor([1.0, 2.0, 3.0, 4.0]).realize()
y = x * 2.0
_z = Tensor(y.uop)
with self.assertRaises(RuntimeError):
y[0] = 99.0
class TestSetitemLoop(unittest.TestCase):
def test_arange(self):
N = 10
-1
View File
@@ -190,7 +190,6 @@ class TestSoftmaxFusion(unittest.TestCase):
def test_softmax_bw(self):
print("*** softmax bw ***")
self.test.requires_grad_()
with Context(NOOPT=1, DEBUG=max(DEBUG.value, 2)):
self.test.softmax(-1).sum().backward()
sg = self.test.grad.realize()
+2 -3
View File
@@ -179,8 +179,7 @@ class TestTinygrad(unittest.TestCase):
def test_tinygrad():
w1 = Tensor(init)
w2 = Tensor(init)
assert w1.requires_grad is None and w2.requires_grad is None
# optimizer sets requires_grad=True for params with requires_grad=None
assert w1.requires_grad is True and w2.requires_grad is True
nn.optim.SGD([w1, w2], lr=0.01)
assert w1.requires_grad is True and w2.requires_grad is True
out = w1.add(w2)
@@ -599,7 +598,7 @@ class TestMoveTensor(unittest.TestCase):
assert x is y
def test_to_grad(self):
x = Tensor.eye(3, requires_grad=True, device=self.d0)
x = Tensor.eye(3, device=self.d0)
y = Tensor([[2.0,0,-2.0]], requires_grad=True, device=self.d0)
z = y.matmul(x).to(self.d1).sum()
z.backward()
-5
View File
@@ -59,8 +59,3 @@ kernel void r_5(device int* data0, const device int* data1, uint3 gid [[threadgr
self.assertEqual(curr:=device.sysdevice.currentAllocatedSize(), before+size, msg=f"{curr=} - {before=}")
device.allocator.free(buf, buf.size, BufferSpec(nolru=True))
self.assertEqual(curr:=device.sysdevice.currentAllocatedSize(), before, msg=f"{curr=} - {before=}")
def test_gpu_family(self):
device = Device['METAL']
self.assertGreater(device.gpu_family, 0)
self.assertLessEqual(device.gpu_family, 15)
+3 -3
View File
@@ -40,7 +40,7 @@ class TestExample(unittest.TestCase):
@multidevice_test
def test_example_readme(self, device):
x = Tensor.eye(3, device=device, requires_grad=True)
x = Tensor.eye(3, device=device)
y = Tensor([[2.0,0,-2.0]], device=device, requires_grad=True)
z = y.matmul(x).sum()
z.backward()
@@ -59,8 +59,8 @@ class TestExample(unittest.TestCase):
print(f"WARNING: {device} test isn't running")
return
x = Tensor.eye(8, device=device, requires_grad=True)
y = Tensor.eye(8, device=device, requires_grad=True)
x = Tensor.eye(8, device=device)
y = Tensor.eye(8, device=device)
z = y.matmul(x).sum()
z.backward()
+2 -11
View File
@@ -1,6 +1,6 @@
import unittest, onnx, tempfile, pathlib
import numpy as np
from tinygrad import dtypes, Tensor
from tinygrad import Tensor
from tinygrad.uop.ops import Ops
from tinygrad.device import is_dtype_supported
from typing import Any
@@ -96,16 +96,7 @@ class TestOnnxRunnerDtypes(unittest.TestCase):
Internal tensors (initializers, attributes) fallback to default dtype if unsupported by device.
External tensors (inputs) preserve their original dtype - user must ensure compatibility with device.
"""
def _get_expected_dtype(self, onnx_dtype: int, is_input: bool):
true_dtype = OnnxDataType(onnx_dtype).to_dtype()
# inputs always preserve their true dtype.
if is_input:
return true_dtype
# supported types are always themselves.
if onnx_dtype in device_supported_dtypes:
return true_dtype
# otherwise it's an unsupported dtype that's internal to the ONNX model, which should fallback to default.
return dtypes.default_int if dtypes.is_int(true_dtype) else dtypes.default_float
def _get_expected_dtype(self, onnx_dtype: int, is_input: bool): return OnnxDataType(onnx_dtype).to_dtype()
@given(onnx_dtype=st.sampled_from(all_dtypes))
def test_input_dtype(self, onnx_dtype: int):
+5 -5
View File
@@ -24,27 +24,27 @@ def two_plus_two_linearize():
def two_plus_two_realize(): (Tensor([2])+Tensor([2])).realize()
def two_plus_two_item(): (Tensor([2])+Tensor([2])).item()
def gradient_test():
x = Tensor.eye(3, requires_grad=True)
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]], requires_grad=True)
z = y.matmul(x).sum()
z.backward()
def realized_eye():
Tensor.eye(3, requires_grad=True).realize()
Tensor.eye(3).realize()
def realized_list():
Tensor([[2.0,0,-2.0]], requires_grad=True).realize()
def kernel_matmul():
x = Tensor.eye(3, requires_grad=True)
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]], requires_grad=True)
z = y.matmul(x)
linear = z.schedule_linear()
to_program(linear.src[-1].src[0], Device.default.renderer)
def realized_matmul():
x = Tensor.eye(3, requires_grad=True)
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]], requires_grad=True)
z = y.matmul(x)
Tensor.realize(z)
def realized_gradient():
x = Tensor.eye(3, requires_grad=True)
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]], requires_grad=True)
z = y.matmul(x).sum()
z.backward()
+1 -1
View File
@@ -14,7 +14,7 @@ if __name__ == "__main__":
print(f"Progress: {i}")
dt = random.choice(dtypes.ints + tuple(dt.vec(4) for dt in dtypes.ints))
u = UOp.variable('x', random.randint(dt.min, 0), random.randint(1, dt.max), dtype=dt)
d = random.randint(1, max(1, u.arg[2]))
d = random.randint(1, max(1, u.arg[2])*2)
if d in powers_of_two: continue
expr = fast_idiv(DEV.target(Device.DEFAULT), u, d)
if expr is None: continue
+2 -2
View File
@@ -76,7 +76,7 @@ def timeit(fxn:Callable[..., T], *args, **kwargs) -> tuple[T, float]:
ret = fxn(*args, **kwargs)
return ret, (time.perf_counter_ns()-st)*1e-6
def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None):
def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None, vals:tuple[int, ...]=()):
allocator = Device['PYTHON'].allocator
bufs = []
for buf_dt, data in inputs or []:
@@ -85,7 +85,7 @@ def eval_uop(uop:UOp, inputs:list[tuple[DType, list[Any]]]|None=None):
g = UOp(Ops.PARAM, uop.dtype.ptr(), arg=0, src=())
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[3].arg))
prog(out_buf:=allocator.alloc(uop.dtype.itemsize), *bufs)
prog(out_buf:=allocator.alloc(uop.dtype.itemsize), *bufs, vals=vals)
return out_buf.cast(uop.dtype.fmt or "").tolist()[0]
def to_uops_list(u:list[UOp], ren=None) -> list[UOp]:
+5
View File
@@ -363,6 +363,11 @@ class TestAutoCastType(unittest.TestCase):
assert (Tensor([0, 1], dtype=dtypes.float32)).cumsum(0).dtype == dtypes.float32
assert (Tensor([0, 1], dtype=dtypes.float64)).cumsum(0).dtype == dtypes.float64
def test_cumsum_empty(self):
# empty cumsum dtype must match non-empty
for d in (dtypes.bool, dtypes.int8, dtypes.uint8, dtypes.float16, dtypes.float32):
self.assertEqual(Tensor([], dtype=d).cumsum(0).dtype, Tensor([0, 1], dtype=d).cumsum(0).dtype)
@given(strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes), strat.sampled_from(core_dtypes))
def test_matmul(self, dt1, dt2, acc_dt):
t1 = Tensor([0, 1], dtype=dt1)
+1 -1
View File
@@ -118,7 +118,7 @@ class TestTraceMetaShutdown(unittest.TestCase):
def test_tracemeta_del_no_shutdown_error(self):
import subprocess, os
result = subprocess.run(['python3', '-c', 'from tinygrad import Tensor\n'
'x=Tensor.eye(3,requires_grad=True); (x@x).sum().backward()'],
'x=Tensor.eye(3); (x@x).sum().backward()'],
env={**os.environ, "TRACEMETA": "2"}, capture_output=True)
self.assertEqual(result.returncode, 0)
self.assertNotIn(b"Exception", result.stderr)
+26
View File
@@ -328,6 +328,32 @@ class TestTensorUOpSoftmax(unittest.TestCase):
def test_log_softmax_default(self): _check(self, _t(2, 3).float(), lambda x: x.log_softmax())
def test_log_softmax_axis0(self): _check(self, _t(2, 3).float(), lambda x: x.log_softmax(axis=0))
class TestTensorUOpQR(unittest.TestCase):
def _check(self, t):
qt, rt = t.qr()
qu, ru = t.uop.qr()
self.assertIs(_strip_unique(qt.uop), _strip_unique(qu))
self.assertIs(_strip_unique(rt.uop), _strip_unique(ru))
def test_qr_square(self): self._check(_t(3, 3).float())
def test_qr_tall(self): self._check(_t(4, 3).float())
def test_qr_wide(self): self._check(_t(3, 4).float())
def test_qr_zero_col(self): self._check(Tensor([[0.0, 1.0], [0.0, 2.0]]))
def test_qr_batched(self): self._check(_t(2, 3, 3).float())
class TestTensorUOpSVD(unittest.TestCase):
def _check(self, t, **kw):
ut, st, vt = t.svd(**kw)
uu, su, vu = t.uop.svd(**kw)
self.assertIs(_strip_unique(ut.uop), _strip_unique(uu))
self.assertIs(_strip_unique(st.uop), _strip_unique(su))
self.assertIs(_strip_unique(vt.uop), _strip_unique(vu))
def test_svd_square(self): self._check(_t(2, 2).float())
def test_svd_tall(self): self._check(_t(3, 2).float())
def test_svd_wide(self): self._check(_t(2, 3).float())
def test_svd_odd_num(self): self._check(_t(3, 3).float()) # exercises odd-num runoff path
def test_svd_batched(self): self._check(_t(2, 2, 2).float())
def test_svd_nonfull(self): self._check(_t(3, 2).float(), full_matrices=False)
# UOp.empty / UOp.empty_like are the canonical buffer allocators; Tensor.empty / Tensor.empty_like just forward.
class TestUOpEmpty(unittest.TestCase):
def test_empty_dtype_string(self):
+10 -2
View File
@@ -2,13 +2,13 @@
import unittest
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.helpers import Timing, Context
from tinygrad.helpers import Timing, Context, cdiv
from tinygrad.dtype import dtypes, ConstFloat # noqa: F401
from tinygrad.device import Device
from tinygrad.uop.ops import Ops, UOp, UPat, exec_alu
from tinygrad.uop.spec import spec_shared
from tinygrad.uop.symbolic import sym
from test.helpers import to_uops_list
from test.helpers import eval_uop, to_uops_list
class TestSafeCast(unittest.TestCase):
def test_cast_folds(self):
@@ -201,6 +201,7 @@ class TestFastIdiv(unittest.TestCase):
self.assertNotIn(Ops.CDIV, ops, f"For dtype={dt} FLOORDIV by power of two did not simplify to shift")
self.assertNotIn(Ops.FLOORDIV, ops, f"For dtype={dt} FLOORDIV survived past late rewrite")
@Context(DISABLE_FAST_IDIV=0)
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "WEBGPU doesn't support long")
def test_fast_idiv_and_mod(self):
g = UOp(Ops.PARAM, dtypes.uint32.ptr(), (), 0)
@@ -220,6 +221,13 @@ class TestFastIdiv(unittest.TestCase):
self.assertIn(Ops.SHR, ops)
self.assertNotIn(Ops.CMOD, ops)
@Context(DISABLE_FAST_IDIV=0)
def test_fast_idiv_bounded_numerator_zero(self):
x = UOp.variable("x", 0, 1, dtype=dtypes.int32)
for val in range(2):
self.assertEqual(eval_uop(x.alu(Ops.CDIV, x.const_like(3)), vals=(val,)), cdiv(val, 3))
@Context(DISABLE_FAST_IDIV=0)
def test_fast_idiv_remove_powers_of_two(self):
ridx = UOp.range(2**20, 0)
uops = to_uops_list([ridx//(7*64)], ren=Device[Device.DEFAULT].renderer)
-1
View File
@@ -141,7 +141,6 @@ class TestTiny(unittest.TestCase):
Tensor.realize(*[p.replace(Tensor.ones_like(p).contiguous()) for p in nn.state.get_parameters(layers)])
# realize gradients
for x in nn.state.get_parameters(layers): x.requires_grad_()
Tensor.empty(4, 1, 14, 14).sequential(layers).sum().backward()
Tensor.realize(*[x.grad for x in nn.state.get_parameters(layers) if x.grad is not None])
+12 -12
View File
@@ -142,9 +142,9 @@ class TestFA(unittest.TestCase):
base_do = Tensor.ones(B, N, H, D, dtype=dtypes.float32).contiguous()
with Context(DEBUG=0):
q = base_q.clone().requires_grad_(True).shard(GPUS, axis=0)
k = base_k.clone().requires_grad_(True).shard(GPUS, axis=0)
v = base_v.clone().requires_grad_(True).shard(GPUS, axis=0)
q = base_q.clone().shard(GPUS, axis=0)
k = base_k.clone().shard(GPUS, axis=0)
v = base_v.clone().shard(GPUS, axis=0)
Tensor.realize(q, k, v)
do = base_do.clone().shard(GPUS, axis=0)
@@ -157,9 +157,9 @@ class TestFA(unittest.TestCase):
Tensor.realize(q.grad, k.grad, v.grad)
with Context(DEBUG=0):
q_ref = base_q.clone().requires_grad_(True)
k_ref = base_k.clone().requires_grad_(True)
v_ref = base_v.clone().requires_grad_(True)
q_ref = base_q.clone()
k_ref = base_k.clone()
v_ref = base_v.clone()
Tensor.realize(q_ref, k_ref, v_ref)
do_ref = base_do.clone()
@@ -189,9 +189,9 @@ class TestFA(unittest.TestCase):
base_do = Tensor.ones(B, N, H, D, dtype=dtypes.float32).contiguous()
with Context(DEBUG=0):
q = base_q.clone().requires_grad_(True).shard(GPUS, axis=2)
k = base_k.clone().requires_grad_(True).shard(GPUS, axis=2)
v = base_v.clone().requires_grad_(True).shard(GPUS, axis=2)
q = base_q.clone().shard(GPUS, axis=2)
k = base_k.clone().shard(GPUS, axis=2)
v = base_v.clone().shard(GPUS, axis=2)
Tensor.realize(q, k, v)
do = base_do.clone().shard(GPUS, axis=2)
@@ -204,9 +204,9 @@ class TestFA(unittest.TestCase):
Tensor.realize(q.grad, k.grad, v.grad)
with Context(DEBUG=0):
q_ref = base_q.clone().requires_grad_(True)
k_ref = base_k.clone().requires_grad_(True)
v_ref = base_v.clone().requires_grad_(True)
q_ref = base_q.clone()
k_ref = base_k.clone()
v_ref = base_v.clone()
Tensor.realize(q_ref, k_ref, v_ref)
do_ref = base_do.clone()
+6 -6
View File
@@ -951,9 +951,9 @@ class TestTK(unittest.TestCase):
base_do = Tensor.ones(B, N, H, D, dtype=dtypes.float32).contiguous()
with Context(DEBUG=0):
q = base_q.clone().requires_grad_(True).shard(GPUS, axis=0)
k = base_k.clone().requires_grad_(True).shard(GPUS, axis=0)
v = base_v.clone().requires_grad_(True).shard(GPUS, axis=0)
q = base_q.clone().shard(GPUS, axis=0)
k = base_k.clone().shard(GPUS, axis=0)
v = base_v.clone().shard(GPUS, axis=0)
Tensor.realize(q, k, v)
do = base_do.clone().shard(GPUS, axis=0)
@@ -966,9 +966,9 @@ class TestTK(unittest.TestCase):
Tensor.realize(q.grad, k.grad, v.grad)
with Context(DEBUG=0):
q_ref = base_q.clone().requires_grad_(True)
k_ref = base_k.clone().requires_grad_(True)
v_ref = base_v.clone().requires_grad_(True)
q_ref = base_q.clone()
k_ref = base_k.clone()
v_ref = base_v.clone()
Tensor.realize(q_ref, k_ref, v_ref)
do_ref = base_do.clone()
-4
View File
@@ -92,10 +92,6 @@ class TestRawDiskBuffer(unittest.TestCase):
# should fail because 3 int8 is 3 bytes but float16 is two and 3 isn't a multiple of 2
Tensor.empty((3,), dtype=dtypes.int8, device=f"DISK:{tmp}").bitcast(dtypes.float16)
with self.assertRaises(RuntimeError):
# should fail because backprop through bitcast is undefined
Tensor.empty((4,), dtype=dtypes.int8, requires_grad=True, device=f"DISK:{tmp}").bitcast(dtypes.float16)
pathlib.Path(tmp).unlink()
@unittest.skipUnless(is_dtype_supported(dtypes.uint8), "need uint8")
+14
View File
@@ -495,6 +495,20 @@ class TestFunctionTuple(unittest.TestCase):
Tensor.realize(a.grad)
np.testing.assert_allclose(a.grad.numpy(), [2., 2., 2., 2.])
def test_custom_kernel_precompile_further_compute(self):
def my_kernel(C:UOp, A:UOp) -> UOp:
i = UOp.range(A.shape[0], 0)
return C[i].store(A[i] * 2.0).end(i).sink(arg=KernelInfo(name="my_kernel"))
@function(precompile=True)
def f(a:Tensor):
c = Tensor.invalids(*a.shape, dtype=a.dtype, device=a.device)
c = Tensor.custom_kernel(c, a, fxn=my_kernel)[0]
return c + 1
a = Tensor([1., 2., 3., 4.]).contiguous().realize()
np.testing.assert_allclose(f(a).numpy(), [3., 5., 7., 9.])
class TestFunctionGrad(unittest.TestCase):
def test_function_grad_ops(self, precompile=False, precompile_backward=False):
N = 64
+25 -23
View File
@@ -115,33 +115,35 @@ class TestGGUF(unittest.TestCase):
with self.assertRaises(ValueError):
ggml_data_to_tensor(Tensor.empty(512, dtype=dtypes.uint8), 256, 1337)
def test_multi_part_load(self):
def build(n_total, part_no, tensors):
# [header] [kv_data] [tensor_infos] [padding] [tensor_data_blob]
buf = bytearray()
# Header: magic "GGUF" + version=3 + n_tensors + n_kv=2
buf += struct.pack("<4siqq", b"GGUF", 3, len(tensors), 2)
# KV entries: [key_len: uint64][key bytes][type: int32][value]
for k, v in [("split.count", n_total), ("split.no", part_no)]:
kb = k.encode()
buf += struct.pack("<Q", len(kb)) + kb + struct.pack("<i", 4) + struct.pack("<I", v)
data_off = 0
# Tensor infos: [name_len][name][ndims][dims reversed][qtype][offset_into_data_blob]
for name, dims, qtype, data in tensors:
nb = name.encode()
buf += struct.pack("<Q", len(nb)) + nb + struct.pack("<I", len(dims))
for d in reversed(dims): buf += struct.pack("<Q", d)
buf += struct.pack("<i", qtype) + struct.pack("<Q", data_off)
data_off += len(data)
buf += b"\x00" * ((32 - len(buf) % 32) % 32)
for _, _, _, data in tensors: buf += data
return bytes(buf)
@staticmethod
def _build_gguf(tensors, kvs):
# [header] [kv_data] [tensor_infos] [padding] [tensor_data_blob]
buf = bytearray()
# Header: magic "GGUF" + version=3 + n_tensors + n_kv
buf += struct.pack("<4siqq", b"GGUF", 3, len(tensors), len(kvs))
# KV entries: [key_len: uint64][key bytes][type: int32][value]
for k, v in kvs:
kb = k.encode()
if isinstance(v, str): buf += struct.pack("<Q", len(kb)) + kb + struct.pack("<i", 8) + struct.pack("<Q", len(v)) + v.encode()
else: buf += struct.pack("<Q", len(kb)) + kb + struct.pack("<i", 4) + struct.pack("<I", v)
data_off = 0
# Tensor infos: [name_len][name][ndims][dims reversed][qtype][offset_into_data_blob]
for name, dims, qtype, data in tensors:
nb = name.encode()
buf += struct.pack("<Q", len(nb)) + nb + struct.pack("<I", len(dims))
for d in reversed(dims): buf += struct.pack("<Q", d)
buf += struct.pack("<i", qtype) + struct.pack("<Q", data_off)
data_off += len(data)
buf += b"\x00" * ((32 - len(buf) % 32) % 32)
for _, _, _, data in tensors: buf += data
return bytes(buf)
def test_multi_part_load(self):
with tempfile.TemporaryDirectory() as d:
d = pathlib.Path(d)
a, b = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32), np.array([5.0, 6.0], dtype=np.float32)
(d / "test-00001-of-00002.gguf").write_bytes(build(2, 0, [("a", (4,), 0, a.tobytes())]))
(d / "test-00002-of-00002.gguf").write_bytes(build(2, 1, [("b", (2,), 0, b.tobytes())]))
(d / "test-00001-of-00002.gguf").write_bytes(self._build_gguf([("a", (4,), 0, a.tobytes())], [("split.count", 2), ("split.no", 0)]))
(d / "test-00002-of-00002.gguf").write_bytes(self._build_gguf([("b", (2,), 0, b.tobytes())], [("split.count", 2), ("split.no", 1)]))
kv, ts = gguf_load(d / "test-00001-of-00002.gguf")
self.assertEqual(kv["split.count"], 2)
np.testing.assert_equal(ts["a"].numpy(), a)
+23 -2
View File
@@ -69,6 +69,27 @@ class TestTensorGradient(unittest.TestCase):
np.testing.assert_allclose(x.grad.numpy(), [2.0+3.0+2*3.0])
self.assertIs(x.grad, old_grad)
def test_gradient_through_clone_from_non_grad_src(self):
src = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=False)
x = src.clone().requires_grad_(True)
(x * 2.0).sum().backward()
np.testing.assert_allclose(x.grad.numpy(), [2.0, 2.0, 2.0, 2.0])
self.assertIsNone(src.grad)
def test_gradient_through_clone_from_grad_src(self):
# unlike torch, tinygrad accumulates grad on all requires_grad tensors, including non-leaf x
src = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
x = src.clone()
(x * 2.0).sum().backward()
np.testing.assert_allclose(src.grad.numpy(), [2.0, 2.0, 2.0, 2.0])
np.testing.assert_allclose(x.grad.numpy(), [2.0, 2.0, 2.0, 2.0])
def test_setitem_on_grad_used_tensor_raises(self):
x = Tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True).realize()
_ = (x * 2.0).sum()
with self.assertRaises(RuntimeError):
x[0] = 99.0
def test_gradient_through_chained_unrealized_setitem(self):
g1 = Tensor.zeros(4).contiguous()
g1[2] = Tensor(1.0)
@@ -113,8 +134,8 @@ class TestMultiOutputGradient(unittest.TestCase):
Tensor.realize(a, b)
c, d, _, _ = Tensor.custom_kernel(Tensor.empty(4, 4), Tensor.empty(4, 4), a, b, fxn=self.addmul_kernel, grad_fxn=self.backward_addmul)
(c * d).sum().backward()
np.testing.assert_allclose(a.grad.numpy(), a_ref.grad.numpy(), rtol=1e-5)
np.testing.assert_allclose(b.grad.numpy(), b_ref.grad.numpy(), rtol=1e-5)
np.testing.assert_allclose(a.grad.numpy(), a_ref.grad.numpy(), rtol=1e-5, atol=1e-7)
np.testing.assert_allclose(b.grad.numpy(), b_ref.grad.numpy(), rtol=1e-5, atol=1e-7)
def test_custom_kernel_three_output_backward(self):
def addmulsub_kernel(C:UOp, D:UOp, E:UOp, A:UOp, B:UOp) -> UOp:
-1
View File
@@ -12,7 +12,6 @@ def reconstruction_helper(A:list[Tensor],B:Tensor, tolerance=1e-5):
np.testing.assert_allclose(reconstructed_tensor.numpy(),B.numpy(),atol=tolerance,rtol=tolerance)
class TestLinAlg(unittest.TestCase):
@unittest.skip("flaky on CI")
def test_svd_general(self):
sizes = [(2,2),(5,3),(3,5),(3,4,4),(2,2,2,2,3)]
for size in sizes:
+1 -1
View File
@@ -69,7 +69,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
sink = graph_rewrite(sink, pm_add_loads, name="** add loads (code)")
# create image buffers
if IMAGE and ren.target.device in {"QCOM", "CL", "PYTHON"}:
if IMAGE and ren.target.device in {"QCOM", "CL", "PYTHON", "NULL"}:
sink = graph_rewrite(sink, pm_make_images, name="create image buffers", bottom_up=True, ctx=ren.target.arch)
# devectorize (TODO: does this need opts?)
+5 -5
View File
@@ -1,5 +1,5 @@
# this is a temporary intermediate step while we remove this index style
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp
from tinygrad.uop.ops import PatternMatcher, UPat, Ops
from tinygrad.dtype import Invalid, dtypes
pm_move_gates_from_index = PatternMatcher([
@@ -10,12 +10,12 @@ pm_move_gates_from_index = PatternMatcher([
lambda buf,gate,idx,cast,data: buf.index(idx, ptr=True).cast(cast.dtype).store(data, gate)),
# Where after gated load becomes alt value
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate"), name="l").or_casted(), UPat.var("a")), lambda gate,l,a:
(UPat.var("gate").where(UPat().load(UPat(), UPat.var("gate", dtype=dtypes.bool), name="l").or_casted(), UPat.var("a")), lambda gate,l,a:
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
(UPat.var("gate").where(UPat.var("a"), UPat().load(UPat(), ~UPat.var("gate", dtype=dtypes.bool), name="l").or_casted()), lambda gate,l,a:
l.replace(src=(l.src[0], a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(a.dtype)),
# vectorized indexes (ie. images) must be int
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.STACK, dtypes.long, name="vec")), allow_any_len=True, name="idx"),
lambda idx,vec: idx.replace(src=(idx.src[0], UOp.vectorize(*(u.cast(dtypes.int) for u in vec.src)), *idx.src[2:])))
# images use 2D INDEX now (y,x)
(UPat(Ops.INDEX, src=(UPat(), UPat((Ops.CONST, Ops.VCONST, Ops.STACK), name="vec")), name="idx"),
lambda idx,vec: idx.replace(src=(idx.src[0], vec.gep(1).cast(dtypes.int), vec.gep(0).cast(dtypes.int))) if vec.dtype.count == 2 else None),
])
+2 -2
View File
@@ -342,7 +342,7 @@ def is_dtype_supported(dtype:DType, target:Target|None=None) -> bool:
target = target or DEV.target(Device.DEFAULT)
if dtype == dtypes.bfloat16:
match target.device:
case "METAL": return not CI or BENCHMARKS
case "METAL": target.arch.startswith("Apple") and int(target.arch[5:]) >= 6
case "CUDA": return (not CI or BENCHMARKS) and target.renderer != "PTX"
case "NV": return (not CI or BENCHMARKS) and target.renderer not in ("PTX", "NAK")
case "CPU": return (not CI or BENCHMARKS) and platform.machine() in {"arm", "arm64", "aarch64", "x86_64", "amd64"} and target.renderer != "LVP"
@@ -364,7 +364,7 @@ def is_dtype_supported(dtype:DType, target:Target|None=None) -> bool:
# PYTHON supports half memoryview in 3.12+ https://github.com/python/cpython/issues/90751
if dtype == dtypes.half:
match target.device:
case "CL": return (not CI or BENCHMARKS) and not OSX
case "CL": return "cl_khr_fp16" in target.arch
case "QCOM": return bool(IMAGE) and bool(FLOAT16) # QCOM compiler is flaky with half
case "CUDA" | "NV": return not CI or BENCHMARKS or target.renderer == "PYTHON"
case "CPU" if target.renderer == "LLVM": return OSX
+3
View File
@@ -78,6 +78,9 @@ pm_gradient = PatternMatcher([
(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))))),
# clone/assign gradient passes through to val
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
# there's no gradient for bitcast
(UPat(Ops.BITCAST), lambda: (None,)),
])
+3 -1
View File
@@ -240,7 +240,9 @@ TRANSCENDENTAL, NOLOCALS = ContextVar("TRANSCENDENTAL", 1), ContextVar("NOLOCALS
SPLIT_REDUCEOP, NO_MEMORY_PLANNER, LRU = ContextVar("SPLIT_REDUCEOP", 1), ContextVar("NO_MEMORY_PLANNER", 0), ContextVar("LRU", 1)
RING, ALL2ALL, ALLREDUCE_CAST = ContextVar("RING", 1), ContextVar("ALL2ALL", 0), ContextVar("ALLREDUCE_CAST", 1)
CACHELEVEL, IGNORE_BEAM_CACHE, DEVECTORIZE = ContextVar("CACHELEVEL", 2), ContextVar("IGNORE_BEAM_CACHE", 0), ContextVar("DEVECTORIZE", 1)
VALIDATE_WITH_CPU, DISABLE_FAST_IDIV = ContextVar("VALIDATE_WITH_CPU", 0), ContextVar("DISABLE_FAST_IDIV", 0)
VALIDATE_WITH_CPU = ContextVar("VALIDATE_WITH_CPU", 0)
# TODO: this is broken for some indexing
DISABLE_FAST_IDIV = ContextVar("DISABLE_FAST_IDIV", 1)
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)
+14 -15
View File
@@ -12,6 +12,14 @@ def _ggml_iq_grid(device: str, grid: tuple[int, ...], grid_shape: tuple[int, int
values = [float((w >> (8*i)) & 0xFF) for w in grid for i in range(grid_shape[1])]
return Tensor(values, dtype=dtypes.float32, device=device).reshape(grid_shape)
# native types {ggml_type: dtype}
_GGML_NATIVE = {0: dtypes.float32, 1: dtypes.float16, 24: dtypes.int8, 25: dtypes.int16,
26: dtypes.int32, 27: dtypes.int64, 28: dtypes.float64, 30: dtypes.bfloat16}
# quant types {ggml_type: (number of elements, number of bytes)}
_GGML_QUANT = {2:(32,18), 3:(32,20), 6:(32,22), 7:(32,24), 8:(32,34),
12:(256,144), 13:(256,176), 14:(256,210), 18:(256,98), 21:(256,110), 22:(256,82), 23:(256,136), 39:(32,17), 41:(128,18)}
def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
"""
Converts ggml tensor data to a tinygrad tensor.
@@ -24,11 +32,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
"""
# https://github.com/ggerganov/ggml/blob/323951f1bdcdfbd5b5ff3a9a7c3770e63b1a560e/include/ggml.h#L356
# native types
if (dtype := {
0: dtypes.float32, 1: dtypes.float16, 24: dtypes.int8,
25: dtypes.int16, 26: dtypes.int32, 27: dtypes.int64, 28: dtypes.float64, 30: dtypes.bfloat16,
}.get(ggml_type)) is not None:
if (dtype := _GGML_NATIVE.get(ggml_type)) is not None:
return t[:dtype.itemsize * n].contiguous().bitcast(dtype)
def q_to_uint8(t: Tensor, b: int) -> Tensor:
@@ -36,12 +40,7 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
shift_tensor, bitmask = Tensor.stack(*[ Tensor(2**(i*b), device=t.device, dtype=t.dtype) 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)
# map to (number of elements, number of bytes)
if (nelements_nbytes := {
2:(32,18), 3:(32,20), 6:(32,22), 7:(32,24), 8:(32,34),
12:(256,144), 13:(256,176), 14:(256,210), 18:(256,98), 21:(256,110), 22:(256,82), 23:(256,136), 39:(32,17),
41:(128,18)
}.get(ggml_type)) is not None:
if (nelements_nbytes := _GGML_QUANT.get(ggml_type)) is not None:
from tinygrad.runtime.autogen import ggml_common as _ggml
blocks = t[:(n//nelements_nbytes[0])*nelements_nbytes[1]].reshape((-1, nelements_nbytes[1])).contiguous()
if ggml_type == 2: return (q_to_uint8(blocks[:,2:], 4).bitcast(dtypes.int8) - 8) * blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32)
@@ -132,6 +131,8 @@ readers: dict[int, Callable[[io.BufferedIOBase], Any]] = { 8: read_str, 9: read_
read_uint32, read_int32, read_uint64, read_int64 = readers[4], readers[5], readers[10], readers[11]
def _gguf_parse(tensor: Tensor) -> tuple[dict, dict[str, Tensor]]:
# TODO: remove the need for copy to default device
tensor = tensor.to(None).realize()
r = io.BufferedReader(TensorIO(tensor), 1_000_000)
magic, version, n_tensors, n_kv = r.read(4), read_int32(r), read_int64(r), read_int64(r)
if magic != b"GGUF" or version not in [2, 3]: raise ValueError("Invalid GGUF format!")
@@ -169,10 +170,8 @@ def gguf_load(fn: Tensor|str|pathlib.Path) -> tuple[dict, dict[str, Tensor]]:
NOTE: The provided tensor must be on a device that supports execution.
"""
# TODO: remove the need for copy to default device
def load(p): return _gguf_parse(p if isinstance(p, Tensor) else Tensor(p).to(None).realize())
kv, sd = load(fn)
kv, sd = _gguf_parse(fn if isinstance(fn, Tensor) else Tensor(pathlib.Path(fn)))
if kv.get('split.count', 1) <= 1: return kv, sd
if isinstance(fn, Tensor): raise ValueError("multi-part GGUF requires a path argument (got Tensor)")
for pp in _gguf_split_paths(pathlib.Path(fn), kv)[1:]: sd.update(load(pp)[1])
for pp in _gguf_split_paths(pathlib.Path(fn), kv)[1:]: sd.update(_gguf_parse(Tensor(pp))[1])
return kv, sd
+7 -7
View File
@@ -6,9 +6,9 @@ from tinygrad.llm.gguf import gguf_load
from tinygrad.uop.ops import resolve
@functools.cache
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0) -> Tensor:
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2)[:(dim // 2)] / dim))
freqs = Tensor.arange(end).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, device:str|None=None) -> Tensor:
freqs = 1.0 / (theta ** (Tensor.arange(0, dim, 2, device=device)[:(dim // 2)] / dim))
freqs = Tensor.arange(end, device=device).unsqueeze(dim=1) * freqs.unsqueeze(dim=0)
return freqs.cos().cat(freqs.sin(), dim=-1).contiguous()
class ExpertWeights:
@@ -27,9 +27,9 @@ def apply_rope(x:Tensor, freqs_cis:Tensor) -> Tensor:
def pairwise_topk(x: Tensor, k: int) -> tuple[Tensor, Tensor]:
n = x.shape[-1]
vals = Tensor.arange(n).reshape(1,1,n).cast(x.dtype).expand(x.shape)
vals = Tensor.arange(n, device=x.device).reshape(1,1,n).cast(x.dtype).expand(x.shape)
cmp = (x.unsqueeze(-1) > x.unsqueeze(-2)) | ((x.unsqueeze(-1) == x.unsqueeze(-2)) & \
(Tensor.arange(n).reshape(1,1,n,1) < Tensor.arange(n).reshape(1,1,1,n)))
(Tensor.arange(n, device=x.device).reshape(1,1,n,1) < Tensor.arange(n, device=x.device).reshape(1,1,1,n)))
sel = Tensor.zeros_like(x).scatter(-1, cmp.sum(axis=-1).cast('int32'), vals)[:,:,n-k:].cast('int32')
return x.gather(-1, sel), sel
@@ -186,7 +186,7 @@ class TransformerBlock(FFNBlock):
if not hasattr(self, "cache_kv"):
# TODO: how is the dtype of this determined?
self.cache_kv = Tensor.empty(2, x.shape[0], self.config.n_kv_heads, self.config.max_context, self.config.head_dim, device=x.device)
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta)
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta, device=x.device)
class MLATransformerBlock(FFNBlock):
def __init__(self, config:TransformerConfig):
@@ -232,7 +232,7 @@ class MLATransformerBlock(FFNBlock):
def _init_state(self, x:Tensor):
if not hasattr(self, "cache_k"):
self.cache_k = Tensor.empty(x.shape[0], 1, self.config.max_context, self.config.kv_lora_rank + self.config.rope_dim, device=x.device)
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta)
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta, device=x.device)
class GatedDeltaNetBlock(FFNBlock):
def __init__(self, config:TransformerConfig, ssm:SSMConfig):
+88 -1
View File
@@ -136,6 +136,19 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
@classmethod
def eye(cls, n:int, m:int|None=None, dtype:DTypeLike|None=None, device:str|tuple[str, ...]|None=None) -> Self:
"""
Returns a 2-D tensor with `n` rows and `m` columns, with ones on the diagonal and zeros elsewhere.
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
```python exec="true" source="above" session="tensor" result="python"
print(Tensor.eye(3).numpy())
```
```python exec="true" source="above" session="tensor" result="python"
print(Tensor.eye(2, 4).numpy())
```
"""
m_ = n if m is None else m
if n < 0 or m_ < 0: raise ValueError(f"cannot have negative {n=}, {m_=}")
out_dtype = to_dtype(dtype) if dtype is not None else dtypes.default_float
@@ -644,7 +657,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
def _split_cumalu(self, axis:int, op:Ops) -> Self:
axis = self._resolve_dim(axis)
if self.ndim == 0 or 0 in self.shape: return self
if self.ndim == 0 or 0 in self.shape: return self.cast(self.sum().dtype) if op is Ops.ADD else self
# TODO: someday the optimizer will find this on its own
# for now this is a two stage cumsum
SPLIT = 256
@@ -1441,6 +1454,80 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
# ***** matrix ops *****
def qr(self) -> tuple[Self, Self]:
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
R, Q = self, type(self).eye(m, dtype=self.dtype, device=self.device).expand(b_shape + (m, m))
idx = type(self).arange(m, device=self.device)
for i in range(min(m, n)):
# full-length Householder reflector v with zeros above row i; w = tau*v is the rank-1 update factor
at_i, x = idx.eq(i), (idx >= i).where(R[..., :, i], 0)
norm = x.square().sum(-1, keepdim=True).sqrt()
x0 = at_i.where(x, 0).sum(-1, keepdim=True)
sgn, active = x0.ne(0).where(x0.sign(), 1), norm.ne(0)
u0 = x0 + sgn * norm
v = (at_i.where(u0, x) / active.where(u0, 1)).unsqueeze(-1)
w = active.where(sgn * u0 / active.where(norm, 1), 0).unsqueeze(-1) * v
R = R - w @ (v.transpose(-2, -1) @ R)
Q = Q - (Q @ v) @ w.transpose(-2, -1)
return Q, R
def svd(self, full_matrices = True) -> tuple[Self, Self, Self]:
#partial implementation of https://www.netlib.org/lapack/lawnspdf/lawn169.pdf , pg 26
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
#preprocess the matrix
Q, R = (self if m >= n else self.transpose(-2, -1)).qr()
num, q_num = min(m, n), max(m, n)
# TODO: codegen infinite loop without contiguous
U = R[..., :num, :num].contiguous()
V = type(self).eye(num, dtype=self.dtype, device=self.device).expand(b_shape + (num, num)).contiguous()
#prepare round robin pairing: identity on first half, reversed on second half
permute = type(self).arange(num//2, dtype=dtypes.int, device=self.device).cat(
type(self).arange(num//2, num, dtype=dtypes.int, device=self.device).flip(0))
cols, h = type(self).arange(num, dtype=dtypes.int, device=self.device), num // 2
eye_num = type(self).eye(num, dtype=self.dtype, device=self.device).expand(b_shape + (num, num))
def one_round_jacobi(U, V, permute):
# permutation matrix P with P[a,b] = (a == permute[b]); first 2h columns are paired-column selectors
P = cols.unsqueeze(1).eq(permute.unsqueeze(0)).cast(U.dtype)
P_pair = P[..., :2*h] # drops the runoff column for odd num
# extract paired columns to compute Jacobi rotation params
U_pair = U @ P_pair
U_left, U_right = U_pair.split(h, -1)
gamma = (U_left * U_right).sum(-2).reshape(b_shape + (1, h))
alpha, beta = U_pair.square().sum(-2).unsqueeze(-2).split(h, -1)
rot = gamma.ne(0)
tau = (beta - alpha) / (2 * rot.where(gamma, 1))
t = tau.ne(0).where(tau.sign(), 1) / (tau.abs() + (1 + tau.square()).sqrt())
t = rot.where(t, 0)
c = 1 / (1 + t.square()).sqrt()
s = c * t
# build rotation matrix R: identity + sum over pairs of 2x2 rotation deltas at (i_k, j_k) positions
Mi, Mj = P_pair.transpose(-2, -1).split(h, -2) # paired-column selectors, each shape (h, num)
Mi_a, Mi_b = Mi.unsqueeze(-1), Mi.unsqueeze(-2)
Mj_a, Mj_b = Mj.unsqueeze(-1), Mj.unsqueeze(-2)
cc, ss = (c - 1).reshape(b_shape + (h, 1, 1)), s.reshape(b_shape + (h, 1, 1))
R = eye_num + (cc * (Mi_a * Mi_b + Mj_a * Mj_b) + ss * (Mi_a * Mj_b - Mj_a * Mi_b)).sum(-3)
U, V = U @ R, V @ R
#prepare the next round robin pairings
if num % 2 == 1: permute = (permute - 1) % num
else: permute = permute[0].reshape(1).cat(((permute[1:num] - 2) % (num - 1)) + 1)
return U, V, permute
# classical Jacobi converges in ~4 sweeps; one full sweep is (num-1) rounds for even num
for _ in range(4 * num): U, V, permute = one_round_jacobi(U, V, permute)
#extract singular values and sort. construct U from Q
S, indices = U.square().sum(-2).sqrt().sort(dim=-1, descending=True)
new_indices = indices.unsqueeze(-2).expand(b_shape + (num, num))
U = U.gather(-1, new_indices) / S.ne(0).where(S, 1).unsqueeze(-2)
V = V.gather(-1, new_indices)
# place U into the top-left num×num block of a q_num×q_num identity matrix
pad_arg = (None,) * len(b_shape) + ((0, q_num - num), (0, q_num - num))
eye_q = type(self).eye(q_num, dtype=U.dtype, device=U.device).expand(b_shape + (q_num, q_num))
eye_n = type(self).eye(num, dtype=U.dtype, device=U.device).expand(b_shape + (num, num)).pad(pad_arg)
U = Q @ (U.pad(pad_arg) + eye_q - eye_n)
if not full_matrices: U = U[..., 0:num]
return (U, S, V.transpose(-2, -1)) if m >= n else (V, S, U.transpose(-2, -1))
def newton_schulz(self, steps:int, params:tuple[int, ...], eps:float=1.0e-7) -> Self:
"""
Performs the newton-schulz algorithm for odd polynomials. The degree of the odd polynomial depends on the number of params.
+7 -2
View File
@@ -750,7 +750,7 @@ class ElementwiseMixin(DTypeMixin, CreationMixin):
"""
return self * (self * 1.702).sigmoid()
def gelu(self) -> Self:
def gelu(self, approximate:str="tanh") -> Self:
"""
Applies the Gaussian Error Linear Unit (GELU) function element-wise.
@@ -760,7 +760,12 @@ class ElementwiseMixin(DTypeMixin, CreationMixin):
print(Tensor([-3., -2., -1., 0., 1., 2., 3.]).gelu().numpy())
```
"""
return 0.5 * self * (1 + (math.sqrt(2 / math.pi) * (self + 0.044715 * self ** 3)).tanh())
if approximate == "tanh":
return 0.5 * self * (1 + (math.sqrt(2 / math.pi) * (self + 0.044715 * self ** 3)).tanh())
elif approximate == "none":
return self * 0.5 * (1.0 + (self / math.sqrt(2)).erf())
else:
raise RuntimeError(f"{approximate=} is not supported")
def swish(self) -> Self:
"""
+13 -20
View File
@@ -6,7 +6,7 @@ from tinygrad.tensor import Tensor, _broadcast_shape
from tinygrad.mixin import ReductionStr
from tinygrad.helpers import getenv, all_same, prod, flatten, make_tuple, argsort, is_numpy_ndarray, get_single_element, polyN
from tinygrad.dtype import DType, ConstType, dtypes, _from_np_dtype, truncate, least_upper_dtype, DTYPES_DICT
from tinygrad.device import is_dtype_supported, Device
from tinygrad.device import Device
from tinygrad.uop.ops import sint
# ***** protobuf definitions ******
@@ -36,13 +36,6 @@ class OnnxDataType(enum.IntEnum):
def to_dtype(self) -> DType: return DTYPES_DICT[self.name.lower()]
def dtype_fallback(dtype: DType, fallback_context: str) -> DType:
if is_dtype_supported(dtype): return dtype
default_dtype = dtypes.default_int if dtypes.is_int(dtype) else dtypes.default_float
warnings.warn(f"dtype {dtype} on {Device.DEFAULT} from {fallback_context} is not supported, falling back to {default_dtype}")
assert is_dtype_supported(default_dtype), f"dtype {default_dtype} must be supported on {Device.DEFAULT}"
return default_dtype
# ***** onnx spec definitions *****
class Domain(enum.Enum):
ONNX = "ai.onnx"
@@ -240,21 +233,20 @@ class OnnxPBParser:
obj["data_location"] = 0
# parse tensor
to_dtype = dtype_fallback(true_dtype := OnnxDataType(obj['data_type']).to_dtype(), "buffer parse")
dtype = OnnxDataType(obj['data_type']).to_dtype()
shape = tuple(obj['dims'])
present_fields = [field for field in ['float_data', 'int32_data', 'int64_data', 'double_data', 'uint64_data', 'raw_data'] if field in obj]
assert len(present_fields) == 1, f"only 1 data field is allowed from {obj=}"
data = obj[present_fields[0]]
if not isinstance(data, Tensor):
obj["parsed_tensor"] = Tensor(data, dtype=to_dtype).reshape(shape)
obj["parsed_tensor"] = Tensor(data, dtype=dtype).reshape(shape)
return obj
assert isinstance(data, Tensor) and data.dtype == dtypes.uint8, data
data = data.bitcast(true_dtype).reshape(shape)
data = data.to(Device.DEFAULT) if true_dtype is to_dtype else data.to("cpu").cast(to_dtype).to(Device.DEFAULT)
data = data.bitcast(dtype).reshape(shape).to(Device.DEFAULT)
# const folding
if shape == ():
if data.dtype == dtypes.float16 and sys.version_info < (3, 12): data = data.cast(dtypes.float32)
data = Tensor(data.item(), dtype=to_dtype).reshape(shape)
data = Tensor(data.item(), dtype=dtype).reshape(shape)
obj["parsed_tensor"] = data
return obj
@@ -594,7 +586,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
raise ValueError(f"pixel_format={pixel_format!r} is not supported.")
def EyeLike(x:Tensor, dtype:int|None=None, k:int=0):
ret = Tensor.eye(cast(int, min(x.shape)), dtype=dtype_fallback(OnnxDataType(dtype).to_dtype(), "EyeLike op") if dtype is not None else x.dtype)
ret = Tensor.eye(cast(int, min(x.shape)), dtype=OnnxDataType(dtype).to_dtype() if dtype is not None else x.dtype)
return ret if x.size(0) == x.size(1) else ret.pad(tuple(None if d == ret.size(0) else (k, d-ret.shape[0]-k) for d in x.shape))
def OptionalHasElement(x:Tensor|None=None): return Tensor(x is not None and x.numel() > 0)
@@ -617,7 +609,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
def softmax_13(x:Tensor, axis:int=-1): return x.softmax(axis)
Softmax = {OpSetId(Domain.ONNX, 1):softmax_1, OpSetId(Domain.ONNX, 13):softmax_13}
def HardSigmoid(x:Tensor, alpha:float=0.2, beta:float=0.5): return (alpha*x + beta).clip(0, 1)
def Gelu(x:Tensor, approximate:str|None=None): return x.gelu() if approximate == "tanh" else 0.5 * x * (1 + (x/math.sqrt(2)).erf())
def Gelu(x:Tensor, approximate:str|None=None): return x.gelu(approximate="none" if approximate is None else approximate)
def BiasGelu(x: Tensor, bias: Tensor, approximate: str | None = None) -> Tensor: return Gelu(x + bias, approximate)
def FastGelu(x:Tensor, bias:Tensor|None=None): return (x + bias).gelu() if bias is not None else x.gelu() # this is tanh approximated
def PRelu(X:Tensor, slope:Tensor): return (X > 0).where(X, X * slope)
@@ -648,7 +640,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
# ***** Casting Ops *****
# NOTE: saturate only applies to FP8 types
def Cast(x:Tensor, to:int, saturate:int=1): return x.cast(dtype_fallback(OnnxDataType(to).to_dtype(), "Cast op"))
def Cast(x:Tensor, to:int, saturate:int=1): return x.cast(OnnxDataType(to).to_dtype())
def CastLike(x:Tensor, target_type:Tensor, saturate:int=1): return x.cast(target_type.dtype)
# ***** Reduce Ops *****
@@ -916,12 +908,13 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
return x * scale.reshape(1, -1, *[1] * (x.ndim-2)) + bias.reshape(1, -1, *[1] * (x.ndim-2))
def InstanceNormalization(x:Tensor, scale:Tensor, bias:Tensor, epsilon:float=1e-05):
return GroupNormalization(x, scale, bias, num_groups=cast(int, x.shape[1]), epsilon=epsilon)
def LayerNormalization(x:Tensor, scale:Tensor, bias:Tensor, axis:int=-1, epsilon:float=1e-05, stash_type:int=1):
def LayerNormalization(x:Tensor, scale:Tensor, bias:Tensor|None=None, axis:int=-1, epsilon:float=1e-05, stash_type:int=1):
assert stash_type == 1, "only float32 is supported"
axes = tuple(i for i in range(axis if axis >= 0 else x.ndim + axis, x.ndim))
mean = (x32:=x.cast(dtypes.float)).mean(axis=axes, keepdim=True)
inv_std_dev = (x32.sub(mean)).square().mean(axis=axes, keepdim=True).add(epsilon).rsqrt()
return (x32.sub(mean)*inv_std_dev).cast(x.dtype).mul(scale).add(bias), mean, inv_std_dev
ret = (x32.sub(mean)*inv_std_dev).cast(x.dtype).mul(scale)
return (ret.add(bias) if bias is not None else ret), mean, inv_std_dev
def SkipLayerNormalization(x:Tensor, skip:Tensor, gamma:Tensor, beta:Tensor|None=None, bias:Tensor|None=None, epsilon:float=1e-12):
x = x + skip
if bias is not None: x = x + bias
@@ -986,7 +979,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
size = int(_resolve_const(size))
N, n = (size if periodic else size - 1), Tensor.arange(size, requires_grad=False)
w = a[0] - a[1] * (n * (2 * math.pi / N)).cos() + a[2] * (n * (4 * math.pi / N)).cos()
return w.cast(dtype_fallback(OnnxDataType(output_datatype).to_dtype(), "window op"))
return w.cast(OnnxDataType(output_datatype).to_dtype())
def HannWindow(size, output_datatype:int=1, periodic:int=1): return _window(size, output_datatype, periodic, (0.5, 0.5, 0))
def HammingWindow(size, output_datatype:int=1, periodic:int=1): return _window(size, output_datatype, periodic, (25/46, 21/46, 0))
def BlackmanWindow(size, output_datatype:int=1, periodic:int=1): return _window(size, output_datatype, periodic, (0.42, 0.5, 0.08))
@@ -1212,7 +1205,7 @@ def get_onnx_ops() -> dict[str, types.FunctionType|dict[OpSetId, types.FunctionT
# ***** Quantization Ops *****
def QuantizeLinear(x:Tensor, y_scale:Tensor, y_zero_point:Tensor|int=0, axis:int=1, block_size:int=0, output_dtype:int=0, saturate=1):
if isinstance(y_zero_point, Tensor): out_dtype = y_zero_point.dtype
elif output_dtype != 0: out_dtype = dtype_fallback(OnnxDataType(output_dtype).to_dtype(), "QuantizeLinear op")
elif output_dtype != 0: out_dtype = OnnxDataType(output_dtype).to_dtype()
else: out_dtype = dtypes.uint8
y_scale, y_zero_point = _prepare_quantize(x, y_scale, y_zero_point, axis, block_size)
if out_dtype == dtypes.uchar:
-4
View File
@@ -10,10 +10,6 @@ class Optimizer:
"""
def __init__(self, params: list[Tensor], lr: float, device=None, fused=FUSE_OPTIM):
if lr < 0: raise ValueError(f"Invalid learning rate: {lr}")
# if requires_grad is None, but being put into an optimizer, set it to True
for x in params:
if x.requires_grad is None: x.requires_grad_(True)
self.params: list[Tensor] = dedup([x for x in params if x.requires_grad])
assert len(self.params) != 0, "optimizer must have at least one param"
self.buffers: list[Tensor] = dedup([x for x in params if not x.requires_grad]) # buffers are still realized
+9 -9
View File
@@ -44,8 +44,7 @@ base_rewrite = PatternMatcher([
# default const render
(UPat(Ops.CONST, name="x"), lambda ctx,x: str(x.arg)),
# new load/store
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var('idx'))),
lambda ctx,buf,idx: f"({ctx[buf]}+{strip_parens(ctx[idx]) if idx.arg == Ops.ADD else ctx[idx]})"),
(UPat.var("buf").index(UPat.var('idx')), lambda ctx,buf,idx: f"({ctx[buf]}+{strip_parens(ctx[idx]) if idx.arg == Ops.ADD else ctx[idx]})"),
(UPat(Ops.LOAD, src=(UPat.var('bidx'),)), lambda ctx,bidx: f"(*{ctx[bidx]})"),
(UPat(Ops.LOAD, src=(UPat.var("bidx"), UPat.var("var"), UPat.var("gate"))), lambda ctx,bidx,var,gate: f"({ctx[gate]}?*{ctx[bidx]}:{ctx[var]})"),
(UPat(Ops.STORE, src=(UPat.var('bidx'), UPat.var("var")), allow_any_len=True), lambda ctx,bidx,var: f"*{ctx[bidx]} = {ctx[var]};"),
@@ -301,13 +300,14 @@ class OpenCLRenderer(CStyleLanguage):
(UPat(Ops.CONST, dtypes.bfloat16, name="x"),
lambda ctx,x: f"{(struct.unpack('I', struct.pack('f', float_to_bf16(x.arg)))[0] >> 16)}u"),
# load/store image (OpenCL)
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))), UPat.var("var"), UPat.var("gate"))),
lambda ctx,buf,idx,var,gate: f"({ctx[gate]}?read_imagef({ctx[buf]}, smp, {ctx[idx]}):{ctx[var]})"),
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))),)),
lambda ctx,buf,idx: f"read_imagef({ctx[buf]}, smp, {ctx[idx]})"),
(UPat(Ops.STORE, src=(UPat.var('buf').index(UPat.var('idx', dtypes.int.vec(2))),
(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), lambda ctx,buf,idx_y,idx_x: f"IMAGE<{ctx[buf]}, {ctx[idx_y]}, {ctx[idx_x]}>"),
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("var"), UPat.var("gate"))),
lambda ctx,buf,idx_y,idx_x,var,gate: f"({ctx[gate]}?read_imagef({ctx[buf]}, smp, (int2)({ctx[idx_x]},{ctx[idx_y]})):{ctx[var]})"),
(UPat(Ops.LOAD, dtype=dtypes.float.vec(4), src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')),)),
lambda ctx,buf,idx_y,idx_x: f"read_imagef({ctx[buf]}, smp, (int2)({ctx[idx_x]},{ctx[idx_y]}))"),
(UPat(Ops.STORE, src=(UPat.var('buf').index(UPat.var('idx_y'), UPat.var('idx_x')),
UPat.var("var", dtypes.float.vec(4))), allow_any_len=True),
lambda ctx,buf,idx,var: f"write_imagef({ctx[buf]}, {ctx[idx]}, {ctx[var]});"),
lambda ctx,buf,idx_y,idx_x,var: f"write_imagef({ctx[buf]}, (int2)({ctx[idx_x]},{ctx[idx_y]}), {ctx[var]});"),
]) + base_rewrite
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None) -> str:
@@ -343,7 +343,7 @@ class MetalRenderer(CStyleLanguage):
def __init__(self, target:Target):
super().__init__(target)
from tinygrad.runtime.ops_metal import MetalCompiler
self.compiler, self.tensor_cores = MetalCompiler(), tc.metal if target.arch == "arm64" else []
self.compiler, self.tensor_cores = MetalCompiler(), tc.metal if target.arch.startswith("Apple") and int(target.arch[5:]) >= 7 else []
# language options
kernel_typedef = "kernel void"
+16 -16
View File
@@ -135,9 +135,9 @@ class NIRRenderer(Renderer):
# OpConvertFToU is undefined if Result Type is not wide enough, cast through int32
# ref: https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpConvertFToU
(UPat(Ops.CAST, (dtypes.uchar, dtypes.ushort), src=(UPat.var("x", dtypes.floats),), name="c"), lambda x,c: x.cast(dtypes.int32).cast(c.dtype)),
# load/store use pointer arithmetic, and the cast does nothing
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("off")), allow_any_len=True, name="x"), lambda x,buf,off: x.replace(
src=(buf,off.cast(dtypes.long))+x.src[2:]) if buf.dtype.addrspace != AddrSpace.REG and off.op not in (Ops.CAST, Ops.STACK) else None),
# load/store use pointer arithmetic, and the cast does nothing. NOTE: this doesn't apply to image indexing cause it's 1-D
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("off")), name="x"), lambda x,buf,off: x.replace(
src=(buf,off.cast(dtypes.long))) if buf.dtype.addrspace != AddrSpace.REG and off.op not in (Ops.CAST, Ops.STACK) else None),
(UPat(Ops.CAST, name="x"), lambda x: x.src[0] if isinstance(x.dtype, PtrDType) or x.src[0].dtype == dtypes.void else None),
])
@@ -248,31 +248,31 @@ class LVPRenderer(NIRRenderer):
super().prerender(uops)
self.param_sz = sum([8 if u.op == Ops.PARAM else u.dtype.itemsize for u in uops if u.op in (Ops.PARAM, Ops.DEFINE_VAR)])
# FIXME: this should be a rewrite rule
def tovec(b, coord): return nalu(b, "vec4", nchannel(b, coord, 0), nchannel(b, coord, 1), nundef(b, dtypes.int), nundef(b, dtypes.int))
def tovec(b, idx_y, idx_x): return nalu(b, "vec4", idx_x, idx_y, nundef(b, dtypes.int), nundef(b, dtypes.int))
def nfloat(dtype): return mesa.nir_type_float16 if dtype == dtypes.half else mesa.nir_type_float32
nstore_img = nir_instr(has_def=False, df=lambda img:img, num_components=lambda val:val.num_components,
intrins=lambda dtype:{'IMAGE_DIM':mesa.GLSL_SAMPLER_DIM_2D, 'ACCESS':mesa.ACCESS_CAN_REORDER, 'SRC_TYPE':nfloat(dtype)},
srcs=lambda b,img,coord,val:[nsrc(x) for x in [img, tovec(b, coord), nundef(b, dtypes.int), val, nimm(b, 0, dtypes.int)]])(
lambda b,img,coord,val,dtype:mesa.nir_intrinsic_instr_create(b.shader,g("nir_intrinsic_image_store")))
srcs=lambda b,img,idx_y,idx_x,val:[nsrc(x) for x in [img, tovec(b, idx_y, idx_x), nundef(b, dtypes.int), val, nimm(b, 0, dtypes.int)]])(
lambda b,img,idx_y,idx_x,val,dtype:mesa.nir_intrinsic_instr_create(b.shader,g("nir_intrinsic_image_store")))
_nload_img = nir_instr(intrins=lambda dtype:{'IMAGE_DIM':mesa.GLSL_SAMPLER_DIM_2D, 'ACCESS':mesa.ACCESS_CAN_REORDER, 'DEST_TYPE':nfloat(dtype)},
nc=4, bs=32, num_components=4, srcs=lambda b,img,coord:[nsrc(x) for x in [img, tovec(b, coord), nundef(b, dtypes.int), nimm(b, 0, dtypes.int)]])(
lambda b,img,coord,dtype: mesa.nir_intrinsic_instr_create(b.shader, g("nir_intrinsic_image_load")))
nc=4, bs=32, num_components=4,
srcs=lambda b,img,idx_y,idx_x:[nsrc(x) for x in [img, tovec(b, idx_y, idx_x), nundef(b, dtypes.int), nimm(b, 0, dtypes.int)]])(
lambda b,img,idx_y,idx_x,dtype: mesa.nir_intrinsic_instr_create(b.shader, g("nir_intrinsic_image_load")))
class IR3Renderer(NIRRenderer, OpenCLRenderer):
has_aux = True
def nload_img(ctx,img,coord):
def nload_img(ctx,img,idx_y,idx_x):
ctx.texs.add(img)
return _nload_img(ctx.b, ctx.r[img], ctx.r[coord], img.dtype)
return _nload_img(ctx.b, ctx.r[img], ctx.r[idx_y], ctx.r[idx_x], img.dtype)
def_rewrite = PatternMatcher([
(UPat(Ops.STORE, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))), UPat.var("val")), allow_any_len=True),
lambda ctx,img,coord,val: nstore_img(ctx.b, ctx.r[img], ctx.r[coord], ctx.r[val], val.dtype)),
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))), UPat.var("alt"), UPat.var("gate"))),
lambda ctx,img,coord,alt,gate: if_phi(ctx.b, ctx.r[gate], lambda: ctx.nload_img(img, coord), lambda: ctx.r[alt])),
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('coord', dtypes.int.vec(2))),)), nload_img),
(UPat(Ops.STORE, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("val")), allow_any_len=True),
lambda ctx,img,idx_y,idx_x,val: nstore_img(ctx.b, ctx.r[img], ctx.r[idx_y], ctx.r[idx_x], ctx.r[val], val.dtype)),
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')), UPat.var("alt"), UPat.var("gate"))),
lambda ctx,img,idx_y,idx_x,alt,gate: if_phi(ctx.b, ctx.r[gate], lambda: ctx.nload_img(img, idx_y, idx_x), lambda: ctx.r[alt])),
(UPat(Ops.LOAD, src=(UPat.var('img').index(UPat.var('idx_y'), UPat.var('idx_x')),)), nload_img),
]) + NIRRenderer.def_rewrite
_param = LVPRenderer.param
+4 -2
View File
@@ -155,8 +155,10 @@ def __getattr__(nm):
*[f"python3 src/compiler/{s}_h.py > gen/{s.split('/')[-1]}.h" for s in ["nir/nir_opcodes", "nir/nir_builder_opcodes"]],
*[f"python3 src/compiler/nir/nir_{s}_h.py --outdir gen" for s in ["intrinsics", "intrinsics_indices"]]]), cwd=path, shell=True, check=True),
srcs="https://gitlab.freedesktop.org/mesa/mesa/-/archive/mesa-25.2.7/mesa-25.2.7.tar.gz",
dll="([] if DEV.renderer == 'LVP' else ['tinymesa']) + ['tinymesa_cpu']",
prolog=["from tinygrad.helpers import DEV", "import gzip, base64"],
dll="([] if (_cpu:=DEV.renderer == 'LVP') else ['tinymesa']) + ['tinymesa_cpu'], " \
'emsg="not available on this platform" if WIN or (OSX and (platform.machine() != "arm64" or (_mv:=platform.mac_ver()[0][:2]) not in {"14","15","26"})) or (platform.system() == "Linux" and platform.machine() not in {"x86_64", "aarch64"}) else ' \
'f"run `sudo curl -fL https://github.com/sirhcm/tinymesa/releases/download/v1/libtinymesa{\'_cpu\'*_cpu}-mesa-25.2.7-{\'macos-\'+_mv if OSX else \'linux\'}-{\'amd64\' if ARCH_X86 else \'arm64\'}.{\'dylib\' if OSX else \'so\'} -o /usr/local/lib/libtinymesa{\'_cpu\'*_cpu}.{\'dylib\' if OSX else \'so\'}`"',
prolog=["from tinygrad.helpers import DEV, ARCH_X86, WIN, OSX", "import gzip, base64, platform"],
epilog=lambda path: [system(f"{root}/extra/mesa/lvp_nir_options.sh {path}")])
case "libclang":
return load("libclang",
+3 -3
View File
@@ -4,9 +4,9 @@ import ctypes
from typing import Literal, TypeAlias
from tinygrad.runtime.support.c import _IO, _IOW, _IOR, _IOWR
from tinygrad.runtime.support import c
from tinygrad.helpers import DEV
import gzip, base64
dll = c.DLL('mesa', ([] if DEV.renderer == 'LVP' else ['tinymesa']) + ['tinymesa_cpu'])
from tinygrad.helpers import DEV, ARCH_X86, WIN, OSX
import gzip, base64, platform
dll = c.DLL('mesa', ([] if (_cpu:=DEV.renderer == 'LVP') else ['tinymesa']) + ['tinymesa_cpu'], emsg="not available on this platform" if WIN or (OSX and (platform.machine() != "arm64" or (_mv:=platform.mac_ver()[0][:2]) not in {"14","15","26"})) or (platform.system() == "Linux" and platform.machine() not in {"x86_64", "aarch64"}) else f"run `sudo curl -fL https://github.com/sirhcm/tinymesa/releases/download/v1/libtinymesa{'_cpu'*_cpu}-mesa-25.2.7-{'macos-'+_mv if OSX else 'linux'}-{'amd64' if ARCH_X86 else 'arm64'}.{'dylib' if OSX else 'so'} -o /usr/local/lib/libtinymesa{'_cpu'*_cpu}.{'dylib' if OSX else 'so'}`")
class struct_u_printf_info(c.Struct): pass
u_printf_info: TypeAlias = struct_u_printf_info
uint32_t: TypeAlias = ctypes.c_uint32
+1 -1
View File
@@ -23,7 +23,7 @@ class MetalGraph(GraphRunner):
self.icb = self.dev.sysdevice.newIndirectCommandBufferWithDescriptor_maxCommandCount_options(icb_descriptor, len(self.calls),
metal.MTLResourceCPUCacheModeDefaultCache)
if self.icb.value is None: raise GraphException("create indirect command buffer failed, does your system support this?")
self.needs_icb_fix = int(self.dev.gpu_family < 9) # ICB fix not required on M3+ (Apple9+)
self.needs_icb_fix = int(not self.dev.arch.startswith("Apple") or int(self.dev.arch[5:]) < 9) # ICB fix not required on M3+ (Apple9+)
if len(self.vars): self.int_buf = self.dev.allocator.alloc(len(self.vars)*dtypes.int32.itemsize)
+6 -6
View File
@@ -112,18 +112,18 @@ class CLDevice(Compiled):
self.context = checked(cl.clCreateContext(None, 1, self.device_id, CC_CB(), None, status := ctypes.c_int32()), status)
self.queue = checked(cl.clCreateCommandQueue(self.context, self.device_id, cl.CL_QUEUE_PROFILING_ENABLE, status), status)
self.pending_copyin: list[memoryview] = []
self.device_exts = (cl.clGetDeviceInfo(self.device_id, cl.CL_DEVICE_EXTENSIONS, 4096,
ctypes.byref(buf := ctypes.create_string_buffer(4096)),
ctypes.byref(total := ctypes.c_size_t())),
ctypes.string_at(buf, size=total.value).decode())[1]
check(cl.clGetDeviceInfo(self.device_id, cl.CL_DEVICE_EXTENSIONS, 0, None, ctypes.byref(exts_len:=ctypes.c_size_t())))
self.device_exts = (cl.clGetDeviceInfo(self.device_id, cl.CL_DEVICE_EXTENSIONS, exts_len.value,
ctypes.byref(buf := ctypes.create_string_buffer(exts_len.value)), None),
ctypes.string_at(buf).decode().split())[1]
renderer = IntelRenderer if "cl_intel_subgroup_matrix_multiply_accumulate" in self.device_exts else OpenCLRenderer
self.cl_compiler = CLCompiler(self, f"{hashlib.md5(self.device_name.encode() + self.driver_version.encode()).hexdigest()}")
arch = ",".join(self.device_exts)
if "cl_khr_image2d_from_buffer" in self.device_exts:
check(cl.clGetDeviceInfo(self.device_id, cl.CL_DEVICE_IMAGE_PITCH_ALIGNMENT, 4, ctypes.byref(ipa := ctypes.c_uint32()), None))
arch = f"IMAGE_PITCH_ALIGNMENT={ipa.value}"
else: arch = ""
arch += f",IMAGE_PITCH_ALIGNMENT={ipa.value}"
super().__init__(device, CLAllocator(self), [renderer], functools.partial(CLProgram, self), arch=arch)
def count(self) -> int: return len(unwrap(self.device_ids))
+1 -3
View File
@@ -297,9 +297,7 @@ class MockDSPProgram:
dsp_lib.write(self.lib)
dsp_lib.flush()
os.chmod(dsp_lib.name, 0o0777)
# NOTE: this timing includes a docker launch
proc = subprocess.run(["docker", "run", "--rm", "-i", "-v", f"{os.path.abspath(os.path.dirname(dsp_lib.name))}:/work", "-w", "/work",
"qemu-hexagon", "-c", f"qemu-hexagon {'-strace' if DEBUG >= 5 else ''} /work/"+os.path.basename(dsp_lib.name)],
proc = subprocess.run(["qemu-hexagon-static", *(['-strace'] if DEBUG >= 5 else []), dsp_lib.name],
input=b''.join([bytes(to_mv(x.va_addr, x.size)) for x in bufs] + [struct.pack("I", x) for x in vals]), stdout=subprocess.PIPE, check=True)
offset = 4
for x in bufs:
+3 -7
View File
@@ -37,12 +37,8 @@ class MetalDevice(Compiled):
self.timeline_signal = self.sysdevice.newSharedEvent()
self.timeline_value = 0
# probe GPU family: Apple9=M3/M4, Apple8=M2, Apple7=M1, etc. values are 1000+N.
self.gpu_family = 0
for i in range(15, 0, -1):
if self.sysdevice.supportsFamily(1000 + i):
self.gpu_family = i
break
# https://developer.apple.com/documentation/metal/mtlgpufamily
def check_family(f): return next(filter(self.sysdevice.supportsFamily, reversed([v for v, nm in metal.enum_MTLGPUFamily.items() if f in nm])), 0)
Compiled.profile_events += [ProfileDeviceEvent(device)]
@@ -51,7 +47,7 @@ class MetalDevice(Compiled):
# This can be reproduced locally with any virtualization software (like utm) that can create macOS VMs with apple's own virtualization framework.
super().__init__(device, MetalAllocator(self), [MetalRenderer],
functools.partial(MetalProgram, self), MetalGraph if 'virtual' not in from_ns_str(self.sysdevice.name()).lower() else None,
arch=platform.machine())
arch=metal.enum_MTLGPUFamily[check_family("Apple") or check_family("Mac")][12:])
def synchronize(self):
for cbuf in self.mtl_buffers_in_flight:
-1
View File
@@ -554,7 +554,6 @@ class PCIIface(PCIIfaceBase):
def __init__(self, dev, dev_id):
# PCIIface's MAP_FIXED mmap will overwrite UVM allocations made by NVKIface, so don't try PCIIface if kernel driver was already used.
if NVKIface.root is not None: raise RuntimeError("Cannot use PCIIface after NVKIface has been initialized (would corrupt UVM memory)")
if not OSX: System.reserve_hugepages(64)
super().__init__(dev, dev_id, vendor=0x10de, devices=((0xff00, (0x2200,0x2400,0x2500,0x2600,0x2700,0x2800,0x2b00,0x2c00,0x2d00,0x2f00)),),
base_class=0x03, vram_bar=1, va_start=NVMemoryManager.va_allocator.base, va_size=NVMemoryManager.va_allocator.size, dev_impl_t=NVDev)
+4 -3
View File
@@ -92,14 +92,15 @@ class PythonProgram:
elif arg[0] == 'l': values[i] = [x[2-int(arg[-1])] for x in warp]
elif uop is Ops.CONST: values[i] = [arg] * warp_size
elif uop is Ops.INDEX:
if len(src_values) != 2: raise RuntimeError("gates must be on LOAD/STORE, not INDEX")
ret:list = []
if isinstance(src_dtypes[0], ImageDType):
for m,ox,oy in zip(src_values[0], src_values[1][0], src_values[1][1]):
assert len(src_values) == 3, "image index must be 3 srcs"
for m,oy,ox in zip(*src_values):
if ox < 0 or ox >= src_dtypes[0].shape[1] or oy < 0 or oy >= src_dtypes[0].shape[0]: ret.append((m, None))
else: ret.append((m, ox*4 + oy*src_dtypes[0].shape[1]*4))
else:
for m,o in zip(src_values[0], src_values[1]): ret.append((m,o))
assert len(src_values) == 2, "non-image index must be 2 srcs"
for m,o in zip(*src_values): ret.append((m,o))
values[i] = ret
elif uop is Ops.CAST and isinstance(dtype, PtrDType):
values[i] = src_values[0]
+10 -3
View File
@@ -137,13 +137,15 @@ class QCOMComputeQueue(HWQueue):
self.reg(mesa.REG_A6XX_TPL1_DBG_ECO_CNTL, 0)
self.cmd(mesa.CP_WAIT_FOR_IDLE)
threadsize = prg.threadsize
self.reg(mesa.REG_A6XX_SP_CS_NDRANGE_0,
qreg.a6xx_sp_cs_ndrange_0(kerneldim=3, localsizex=local_size[0] - 1, localsizey=local_size[1] - 1, localsizez=local_size[2] - 1),
global_size_mp[0], 0, global_size_mp[1], 0, global_size_mp[2], 0, 0xccc0cf, 0xfc | qreg.a6xx_sp_cs_wge_cntl(threadsize=mesa.THREAD64),
global_size_mp[0], 0, global_size_mp[1], 0, global_size_mp[2], 0, 0xccc0cf, 0xfc | qreg.a6xx_sp_cs_wge_cntl(threadsize=threadsize),
cast_int(global_size[0], ceil=True), cast_int(global_size[1], ceil=True), cast_int(global_size[2], ceil=True))
self.reg(mesa.REG_A6XX_SP_CS_CNTL_0,
qreg.a6xx_sp_cs_cntl_0(threadsize=mesa.THREAD64, halfregfootprint=prg.hregs, fullregfootprint=prg.fregs, branchstack=prg.brnchstck),
qreg.a6xx_sp_cs_cntl_0(threadsize=threadsize, halfregfootprint=prg.hregs, fullregfootprint=prg.fregs, branchstack=prg.brnchstck),
qreg.a6xx_sp_cs_cntl_1(constantrammode=mesa.CONSTLEN_256, shared_size=prg.shared_size), # should this be CONSTLEN_512?
0, prg.prg_offset, *data64_le(prg.lib_gpu.va_addr),
qreg.a6xx_sp_cs_pvt_mem_param(memsizeperitem=prg.pvtmem_size_per_item), *data64_le(prg.dev._stack.va_addr),
@@ -187,7 +189,7 @@ class QCOMComputeQueue(HWQueue):
if prg.NIR:
self.reg(mesa.REG_A6XX_SP_CS_CONST_CONFIG_0,
qreg.a6xx_sp_cs_const_config_0(wgidconstid=prg.wgid, wgsizeconstid=prg.wgsz, wgoffsetconstid=0xfc, localidregid=prg.lid),
qreg.a6xx_sp_cs_wge_cntl(linearlocalidregid=0xfc, threadsize=mesa.THREAD64))
qreg.a6xx_sp_cs_wge_cntl(linearlocalidregid=0xfc, threadsize=threadsize))
self.cmd(mesa.CP_EXEC_CS, 0,
qreg.cp_exec_cs_1(ngroups_x=global_size[0]), qreg.cp_exec_cs_2(ngroups_y=global_size[1]), qreg.cp_exec_cs_3(_ngroups_z=global_size[2]))
else: self.cmd(mesa.CP_RUN_OPENCL, 0)
@@ -251,6 +253,7 @@ class QCOMProgram(HCQProgram):
self.tex_off, self.ibo_off, self.samp_off = 2048, 2048 + 0x40 * self.tex_cnt, 2048 + 0x40 * (self.tex_cnt + self.ibo_cnt)
self.fregs, self.hregs = v.info.max_reg + 1, v.info.max_half_reg + 1
self.threadsize = mesa.THREAD128 if v.info.double_threadsize else mesa.THREAD64
else: self._parse_lib(lib)
self.lib_gpu: HCQBuffer = self.dev.allocator.alloc(self.image_size, buf_spec:=BufferSpec(cpu_access=True, nolru=True))
@@ -320,6 +323,10 @@ class QCOMProgram(HCQProgram):
reg_desc_off = _read_lib(lib, 0x34)
self.fregs, self.hregs = _read_lib(lib, reg_desc_off + 0x14), _read_lib(lib, reg_desc_off + 0x18)
# The Qualcomm OpenCL stack dispatches these binaries with 128-thread waves.
# THREAD64 leaves half-rate ALU throughput for the same shader image.
self.threadsize = mesa.THREAD128 if getenv("THREAD128") else mesa.THREAD64
class QCOMAllocator(HCQAllocatorBase):
def _alloc(self, size:int, opts:BufferSpec) -> HCQBuffer:
return self.dev._gpu_map(opts.external_ptr, size) if opts.external_ptr else self.dev._gpu_alloc(size)
+2 -2
View File
@@ -6,8 +6,8 @@ from tinygrad.runtime.autogen import llvm
class ClangJITCompiler(Compiler):
def __init__(self, arch:list[str], cachekey="compile_clang_jit"):
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
self.arch, cpu, *feats = arch
assert self.arch and cpu, f"invalid arch string: {arch!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
match self.arch:
case "x86_64": self.args = [f"-march={cpu}"] + [f"-mno{f}" if f.startswith("-") else f"-m{f}" for f in feats]
# on arm march means "runs on this arch and superset" instead of "optimize for this arch". x86 march == arm mcpu
@@ -92,8 +92,8 @@ class LLVMCompiler(Compiler):
class CPULLVMCompiler(LLVMCompiler):
def __init__(self, arch:list[str], cache_key=None):
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
self.arch, cpu, *feats = arch
assert self.arch and cpu, f"invalid arch string: {arch!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
featstr = ','.join(f if f.startswith('-') else '+'+f for f in feats)
if cpu == "native":
cpu = ctypes.string_at(llvm.LLVMGetHostCPUName()).decode()
+45 -36
View File
@@ -163,9 +163,9 @@ class NV_FLCN(NV_IP):
patched_image[(cmd_off:=self.desc_v3.IMEMLoadSize+dmem.cmd_in_buffer_offset) : cmd_off+len(cmd)] = cmd
patched_image[(sig_off:=self.desc_v3.IMEMLoadSize+self.desc_v3.PKCDataOffset) : sig_off+0x180] = signature[-0x180:]
return self.nvdev._alloc_sysmem(len(patched_image), contiguous=True, data=patched_image)
return self.nvdev._alloc_boot_mem(len(patched_image), data=patched_image, sysmem=False)
_, self.frts_image_sysmem = __patch(0x15, bytes(frts_cmd))
_, self.frts_image_paddr, _ = __patch(0x15, bytes(frts_cmd))
def prep_booter(self):
sha = {"ga102":"4497e3eff7e95c774b8a569d17b27c08c9650158d10b229d2be81cdcad9a085b",
@@ -179,14 +179,14 @@ class NV_FLCN(NV_IP):
(patched_image:=bytearray(b[h.data_offset:h.data_offset + h.data_size]))[patch_loc:patch_loc+sig_len] = sig
_, self.booter_image_sysmem = self.nvdev._alloc_sysmem(len(patched_image), contiguous=True, data=patched_image)
_, self.booter_image_paddr, _ = self.nvdev._alloc_boot_mem(len(patched_image), data=patched_image, sysmem=False)
self.booter_data_off, self.booter_data_sz, self.booter_code_off, self.booter_code_sz = lh.os_data_offset, lh.os_data_size, app.offset, app.size
def init_hw(self):
self.falcon, self.sec2 = 0x00110000, 0x00840000
self.reset(self.falcon)
self.execute_hs(self.falcon, self.frts_image_sysmem[0], code_off=0x0, data_off=self.desc_v3.IMEMLoadSize,
self.execute_hs(self.falcon, self.frts_image_paddr, code_off=0x0, data_off=self.desc_v3.IMEMLoadSize,
imemPa=self.desc_v3.IMEMPhysBase, imemVa=self.desc_v3.IMEMVirtBase, imemSz=self.desc_v3.IMEMLoadSize,
dmemPa=self.desc_v3.DMEMPhysBase, dmemVa=0x0, dmemSz=self.desc_v3.DMEMLoadSize,
pkc_off=self.desc_v3.PKCDataOffset, engid=self.desc_v3.EngineIdMask, ucodeid=self.desc_v3.UcodeId)
@@ -195,12 +195,12 @@ class NV_FLCN(NV_IP):
self.reset(self.falcon, riscv=True)
# set up the mailbox
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.nvdev.gsp.libos_args_sysmem[0]))
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.nvdev.gsp.libos_args_sysmem[0]))
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.nvdev.gsp.libos_args_sysmem))
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.nvdev.gsp.libos_args_sysmem))
# booter
self.reset(self.sec2)
mbx = self.execute_hs(self.sec2, self.booter_image_sysmem[0], code_off=self.booter_code_off, data_off=self.booter_data_off,
mbx = self.execute_hs(self.sec2, self.booter_image_paddr, code_off=self.booter_code_off, data_off=self.booter_data_off,
imemPa=0x0, imemVa=self.booter_code_off, imemSz=self.booter_code_sz, dmemPa=0x0, dmemVa=0x0, dmemSz=self.booter_data_sz,
pkc_off=0x10, engid=1, ucodeid=3, mailbox=self.nvdev.gsp.wpr_meta_sysmem)
assert mbx[0] == 0x0, f"Booter failed to execute, mailbox is {mbx[0]:08x}, {mbx[1]:08x}"
@@ -208,11 +208,11 @@ class NV_FLCN(NV_IP):
self.nvdev.NV_PFALCON_FALCON_OS.with_base(self.falcon).write(0x0)
assert self.nvdev.NV_PRISCV_RISCV_CPUCTL.with_base(self.falcon).read_bitfields()['active_stat'] == 1, "GSP Core is not active"
def execute_dma(self, base:int, cmd:int, dest:int, mem_off:int, sysmem:int, size:int):
def execute_dma(self, base:int, cmd:int, dest:int, mem_off:int, src:int, size:int):
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).read_bitfields()['full'], value=0, msg="DMA does not progress")
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE.with_base(base).write(lo32(sysmem >> 8))
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE1.with_base(base).write(hi32(sysmem >> 8) & 0x1ff)
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE.with_base(base).write(lo32(src >> 8))
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE1.with_base(base).write(hi32(src >> 8) & 0x1ff)
xfered = 0
while xfered < size:
@@ -232,19 +232,19 @@ class NV_FLCN(NV_IP):
def wait_cpu_halted(self, base): wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_CPUCTL.with_base(base).read_bitfields()['halted'], msg="not halted")
def execute_hs(self, base, img_sysmem, code_off, data_off, imemPa, imemVa, imemSz, dmemPa, dmemVa, dmemSz, pkc_off, engid, ucodeid, mailbox=None):
def execute_hs(self, base, img_paddr, code_off, data_off, imemPa, imemVa, imemSz, dmemPa, dmemVa, dmemSz, pkc_off, engid, ucodeid, mailbox=None):
self.disable_ctx_req(base)
self.nvdev.NV_PFALCON_FBIF_TRANSCFG.with_base(base)[ctx_dma:=0].update(target=self.nvdev.NV_PFALCON_FBIF_TRANSCFG_TARGET_COHERENT_SYSMEM,
mem_type=self.nvdev.NV_PFALCON_FBIF_TRANSCFG_MEM_TYPE_PHYSICAL)
# target=0 is FB (not in published headers)
self.nvdev.NV_PFALCON_FBIF_TRANSCFG.with_base(base)[ctx_dma:=0].update(target=0, mem_type=self.nvdev.NV_PFALCON_FBIF_TRANSCFG_MEM_TYPE_PHYSICAL)
cmd = self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).encode(write=0, size=self.nvdev.NV_PFALCON_FALCON_DMATRFCMD_SIZE_256B,
ctxdma=ctx_dma, imem=1, sec=1)
self.execute_dma(base, cmd, dest=imemPa, mem_off=imemVa, sysmem=img_sysmem+code_off-imemVa, size=imemSz)
self.execute_dma(base, cmd, dest=imemPa, mem_off=imemVa, src=img_paddr+code_off-imemVa, size=imemSz)
cmd = self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).encode(write=0, size=self.nvdev.NV_PFALCON_FALCON_DMATRFCMD_SIZE_256B,
ctxdma=ctx_dma, imem=0, sec=0)
self.execute_dma(base, cmd, dest=dmemPa, mem_off=dmemVa, sysmem=img_sysmem+data_off-dmemVa, size=dmemSz)
self.execute_dma(base, cmd, dest=dmemPa, mem_off=dmemVa, src=img_paddr+data_off-dmemVa, size=dmemSz)
self.nvdev.NV_PFALCON2_FALCON_BROM_PARAADDR.with_base(base)[0].write(pkc_off)
self.nvdev.NV_PFALCON2_FALCON_BROM_ENGIDMASK.with_base(base).write(engid)
@@ -293,7 +293,9 @@ class NV_FLCN_COT(NV_IP):
self.nvdev.include("dev_fsp_pri", "gh100")
self.nvdev.include("dev_bus", "tu102")
self.fmc_boot_args_view, self.fmc_boot_args_sysmem = self.nvdev._alloc_boot_struct(nv.GSP_FMC_BOOT_PARAMS())
self.fmc_boot_args_view, _, fmc_boot_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(nv.GSP_FMC_BOOT_PARAMS),
data=bytes(nv.GSP_FMC_BOOT_PARAMS()))
self.fmc_boot_args_sysmem = fmc_boot_addrs[0]
self.init_fmc_image()
def init_fmc_image(self):
@@ -302,18 +304,19 @@ class NV_FLCN_COT(NV_IP):
def _section(s): return next((sh.content for sh in sections if sh.name == s))
self.fmc_booter_image, self.fmc_booter_hash = _section("image"), memoryview(_section("hash")).cast('I')
self.fmc_booter_sig, self.fmc_booter_pkey = memoryview(_section("signature")).cast('I'), memoryview(_section("publickey") + b"\x00" * 3).cast('I')
_, self.fmc_booter_sysmem = self.nvdev._alloc_sysmem(len(self.fmc_booter_image), contiguous=True, data=self.fmc_booter_image)
_, _, fmc_booter_addrs = self.nvdev._alloc_boot_mem(len(self.fmc_booter_image), data=self.fmc_booter_image)
self.fmc_booter_bar1 = fmc_booter_addrs[0]
def init_hw(self):
self.falcon = 0x00110000
boot_args = nv.GSP_ACR_BOOT_GSP_RM_PARAMS(gspRmDescOffset=self.nvdev.gsp.wpr_meta_sysmem,
gspRmDescSize=ctypes.sizeof(nv.GspFwWprMeta), target=nv.GSP_DMA_TARGET_COHERENT_SYSTEM, bIsGspRmBoot=True)
rm_args = nv.GSP_RM_PARAMS(bootArgsOffset=self.nvdev.gsp.libos_args_sysmem[0], target=nv.GSP_DMA_TARGET_COHERENT_SYSTEM)
rm_args = nv.GSP_RM_PARAMS(bootArgsOffset=self.nvdev.gsp.libos_args_sysmem, target=nv.GSP_DMA_TARGET_COHERENT_SYSTEM)
self.fmc_boot_args_view[:ctypes.sizeof(nv.GSP_FMC_BOOT_PARAMS)] = bytes(nv.GSP_FMC_BOOT_PARAMS(bootGspRmParams=boot_args, gspRmParams=rm_args))
cot_payload = nv.NVDM_PAYLOAD_COT(version=0x2, size=ctypes.sizeof(nv.NVDM_PAYLOAD_COT), frtsVidmemOffset=0x1c00000, frtsVidmemSize=0x100000,
gspBootArgsSysmemOffset=self.fmc_boot_args_sysmem, gspFmcSysmemOffset=self.fmc_booter_sysmem[0])
gspBootArgsSysmemOffset=self.fmc_boot_args_sysmem, gspFmcSysmemOffset=self.fmc_booter_bar1)
for i,x in enumerate(self.fmc_booter_hash): cot_payload.hash384[i] = x
for i,x in enumerate(self.fmc_booter_sig): cot_payload.signature[i] = x
for i,x in enumerate(self.fmc_booter_pkey): cot_payload.publicKey[i] = x
@@ -360,7 +363,7 @@ class NV_GSP(NV_IP):
# Alloc queues
pte_cnt = ((queue_pte_cnt:=(queue_size * 2) // 0x1000)) + round_up(queue_pte_cnt * 8, 0x1000) // 0x1000
pt_size = round_up(pte_cnt * 8, 0x1000)
queues_view, queues_sysmem = self.nvdev._alloc_sysmem(pt_size + queue_size * 2, contiguous=False)
queues_view, _, queues_sysmem = self.nvdev._alloc_boot_mem(pt_size + queue_size * 2, sysmem=True)
# Fill up ptes
for i, sysmem in enumerate(queues_sysmem): queues_view.view(i * 0x8, 0x8, fmt='Q')[0] = sysmem
@@ -368,7 +371,9 @@ class NV_GSP(NV_IP):
# Fill up arguments
queue_args = nv.MESSAGE_QUEUE_INIT_ARGUMENTS(sharedMemPhysAddr=queues_sysmem[0], pageTableEntryCount=pte_cnt, cmdQueueOffset=pt_size,
statQueueOffset=pt_size + queue_size)
_, self.rm_args_sysmem = self.nvdev._alloc_boot_struct(nv.GSP_ARGUMENTS_CACHED(bDmemStack=True, messageQueueInitArguments=queue_args))
_, _, rm_args_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(nv.GSP_ARGUMENTS_CACHED),
data=bytes(nv.GSP_ARGUMENTS_CACHED(bDmemStack=True, messageQueueInitArguments=queue_args)))
self.rm_args_sysmem = rm_args_addrs[0]
# Build command queue header
# self.cmd_q_va, self.stat_q_va = queues_view.addr + pt_size, queues_view.addr + pt_size + queue_size
@@ -380,11 +385,12 @@ class NV_GSP(NV_IP):
self.cmd_q = NVRpcQueue(self, self.cmd_q_view, None)
def init_libos_args(self):
_, logbuf_sysmem = self.nvdev._alloc_sysmem((2 << 20), contiguous=True)
libos_args_view, self.libos_args_sysmem = self.nvdev._alloc_sysmem(0x1000, contiguous=True)
_, _, logbuf_addrs = self.nvdev._alloc_boot_mem(2 << 20)
libos_args_view, _, libos_addrs = self.nvdev._alloc_boot_mem(0x1000)
self.libos_args_sysmem = libos_addrs[0]
libos_structs = [nv.LibosMemoryRegionInitArgument(kind=nv.LIBOS_MEMORY_REGION_CONTIGUOUS, loc=nv.LIBOS_MEMORY_REGION_LOC_SYSMEM, size=0x10000,
id8=int.from_bytes(bytes(f"LOG{name}", 'utf-8'), 'big'), pa=logbuf_sysmem[0] + 0x10000 * i)
id8=int.from_bytes(bytes(f"LOG{name}", 'utf-8'), 'big'), pa=logbuf_addrs[0] + 0x10000 * i)
for i, name in enumerate(["INIT", "INTR", "RM", "MNOC", "KRNL"])]
libos_structs.append(nv.LibosMemoryRegionInitArgument(kind=nv.LIBOS_MEMORY_REGION_CONTIGUOUS, loc=nv.LIBOS_MEMORY_REGION_LOC_SYSMEM, size=0x1000,
id8=int.from_bytes(bytes("RMARGS", 'utf-8'), 'big'), pa=self.rm_args_sysmem))
@@ -400,7 +406,7 @@ class NV_GSP(NV_IP):
for i in range(3, 0, -1): npages[i-1] = ((npages[i] - 1) >> (nv.LIBOS_MEMORY_REGION_RADIX_PAGE_LOG2 - 3)) + 1
offsets = [sum(npages[:i]) * 0x1000 for i in range(4)]
radix_view, self.gsp_radix3_sysmem = self.nvdev._alloc_sysmem(offsets[-1] + len(self.gsp_image), contiguous=False)
radix_view, _, self.gsp_radix3_addrs = self.nvdev._alloc_boot_mem(offsets[-1] + len(self.gsp_image))
# Copy image
radix_view.view(offsets[-1], len(self.gsp_image))[:] = self.gsp_image
@@ -408,10 +414,11 @@ class NV_GSP(NV_IP):
# Copy level and image pages.
for i in range(0, 3):
cur_offset = sum(npages[:i+1])
radix_view.view(offsets[i], npages[i+1] * 8, fmt='Q')[:] = array.array('Q', self.gsp_radix3_sysmem[cur_offset:cur_offset+npages[i+1]])
radix_view.view(offsets[i], npages[i+1] * 8, fmt='Q')[:] = array.array('Q', self.gsp_radix3_addrs[cur_offset:cur_offset+npages[i+1]])
# Copy signature
_, self.gsp_signature_sysmem = self.nvdev._alloc_sysmem(len(signature), contiguous=True, data=signature)
_, _, gsp_sig_addrs = self.nvdev._alloc_boot_mem(len(signature), data=signature)
self.gsp_signature_bar1 = gsp_sig_addrs[0]
def init_boot_binary_image(self):
sha = {"ga102":"82428f532240727e95bb3083fbaaba9b2cc7b937314323f2d546ce7245f27fad",
@@ -419,15 +426,16 @@ class NV_GSP(NV_IP):
"gb202":"d40b48e431d1707dc77af3605db358ed7a32ebfc2830eb74de2eddb4d3025071"}[self.nvdev.fw_name]
h = nv.struct_nvfw_bin_hdr.from_buffer_copy(b:=fetch_fw(f"nvidia/{self.nvdev.fw_name}/gsp", "bootloader-570.144.bin", sha))
self.booter_image, self.booter_desc = b[h.data_offset:h.data_offset+h.data_size], nv.RM_RISCV_UCODE_DESC.from_buffer_copy(b, h.header_offset)
_, self.booter_sysmem = self.nvdev._alloc_sysmem(len(self.booter_image), contiguous=True, data=self.booter_image)
_, _, booter_addrs = self.nvdev._alloc_boot_mem(len(self.booter_image), data=self.booter_image)
self.booter_bar1 = booter_addrs[0]
def init_wpr_meta(self):
self.init_gsp_image()
self.init_boot_binary_image()
common = {'sizeOfBootloader':(boot_sz:=len(self.booter_image)), 'sysmemAddrOfBootloader':self.booter_sysmem[0],
'sizeOfRadix3Elf':(radix3_sz:=len(self.gsp_image)), 'sysmemAddrOfRadix3Elf': self.gsp_radix3_sysmem[0],
'sizeOfSignature': 0x1000, 'sysmemAddrOfSignature': self.gsp_signature_sysmem[0],
common = {'sizeOfBootloader':(boot_sz:=len(self.booter_image)), 'sysmemAddrOfBootloader':self.booter_bar1,
'sizeOfRadix3Elf':(radix3_sz:=len(self.gsp_image)), 'sysmemAddrOfRadix3Elf': self.gsp_radix3_addrs[0],
'sizeOfSignature': 0x1000, 'sysmemAddrOfSignature': self.gsp_signature_bar1,
'bootloaderCodeOffset': self.booter_desc.monitorCodeOffset, 'bootloaderDataOffset': self.booter_desc.monitorDataOffset,
'bootloaderManifestOffset': self.booter_desc.manifestOffset, 'revision':nv.GSP_FW_WPR_META_REVISION, 'magic':nv.GSP_FW_WPR_META_MAGIC}
@@ -441,7 +449,8 @@ class NV_GSP(NV_IP):
gspFwHeapOffset=(gsp_heap_off:=round_down(gsp_off-gsp_heap_sz, 0x100000)), gspFwWprStart=(wpr_st:=round_down(gsp_heap_off-0x1000, 0x100000)),
nonWprHeapSize=(non_wpr_sz:=0x100000), nonWprHeapOffset=(non_wpr_off:=round_down(wpr_st-non_wpr_sz, 0x100000)), gspFwRsvdStart=non_wpr_off)
assert self.nvdev.flcn.frts_offset == m.frtsOffset, f"FRTS mismatch: {self.nvdev.flcn.frts_offset} != {m.frtsOffset}"
self.wpr_meta, self.wpr_meta_sysmem = self.nvdev._alloc_boot_struct(m)
self.wpr_meta, _, wpr_meta_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(type(m)), data=bytes(m))
self.wpr_meta_sysmem = wpr_meta_addrs[0]
def promote_ctx(self, client:int, subdevice:int, obj:int, ctxbufs:dict[int, GRBufDesc], bufs=None, virt=None, phys=None):
res, prom = {}, nv_gpu.NV2080_CTRL_GPU_PROMOTE_CTX_PARAMS(entryCount=len(ctxbufs), engineType=0x1, hChanClient=client, hObject=obj)
@@ -527,8 +536,8 @@ class NV_GSP(NV_IP):
params.ramfcMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=ramfc_alloc.paddrs[0][0], size=0x200, addressSpace=2, cacheAttrib=0)
params.instanceMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=ramfc_alloc.paddrs[0][0], size=0x1000, addressSpace=2, cacheAttrib=0)
_, method_sysmem = self.nvdev._alloc_sysmem(0x5000, contiguous=True)
params.mthdbufMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=method_sysmem[0], size=0x5000, addressSpace=1, cacheAttrib=0)
_, method_paddr, _ = self.nvdev._alloc_boot_mem(0x5000, sysmem=False)
params.mthdbufMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=method_paddr, size=0x5000, addressSpace=2, cacheAttrib=0)
if client is not None and client != self.priv_root and params.hObjectError != 0:
params.errorNotifierMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=0, size=0xecc, addressSpace=0, cacheAttrib=0)
@@ -629,8 +638,8 @@ class NV_GSP(NV_IP):
elif op == 0x8: # core resume
self.nvdev.flcn.reset(self.nvdev.flcn.falcon, riscv=True)
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.libos_args_sysmem[0]))
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.libos_args_sysmem[0]))
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.libos_args_sysmem))
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.libos_args_sysmem))
self.nvdev.flcn.start_cpu(self.nvdev.flcn.sec2)
wait_cond(lambda: self.nvdev.NV_PGC6_BSI_SECURE_SCRATCH_14.read_bitfields()['boot_stage_3_handoff'], msg="SEC2 didn't hand off")
+9 -10
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
import ctypes, time, functools, tinygrad.runtime.autogen.nv_regs
from tinygrad.helpers import getenv, DEBUG, getbits
import time, functools, tinygrad.runtime.autogen.nv_regs
from tinygrad.helpers import getenv, DEBUG, getbits, round_up
from tinygrad.runtime.autogen import pci
from tinygrad.runtime.support.memory import TLSFAllocator, MemoryManager, AddrSpace
from tinygrad.runtime.support.nv.ip import NV_FLCN, NV_FLCN_COT, NV_GSP
@@ -145,15 +145,14 @@ class NVDev:
self.mm = NVMemoryManager(self, self.vram_size - (64 << 20), boot_size=(2 << 20), pt_t=NVPageTableEntry, va_bits=bits, va_shifts=shifts,
va_base=0, palloc_ranges=[(x, x) for x in [512 << 20, 2 << 20, 4 << 10]], reserve_ptable=not self.large_bar)
def _alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False, data:bytes|None=None) -> tuple[MMIOInterface, list[int]]:
view, paddrs = self.pci_dev.alloc_sysmem(size, vaddr, contiguous=contiguous)
def _alloc_boot_mem(self, size:int, data:bytes|None=None, contiguous:bool=False, sysmem:bool|None=None) -> tuple[MMIOInterface, int, list[int]]:
sz = round_up(size, 0x1000)
if sysmem is True or (sysmem is None and not self.large_bar): view, paddrs = self.pci_dev.alloc_sysmem(size, 0, contiguous=contiguous)
else:
paddr = self.mm.palloc(sz, boot=False)
view, paddrs = self.vram.view(paddr, sz), [self.pci_dev.bar_info(1)[0] + paddr + i * 0x1000 for i in range(sz // 0x1000)]
if data is not None: view[:size] = data
return view, paddrs
def _alloc_boot_struct(self, struct:ctypes.Structure) -> tuple[MMIOInterface, int]:
view, paddrs = self._alloc_sysmem(sz:=ctypes.sizeof(type(struct)), contiguous=True)
view[:sz] = bytes(struct)
return view, paddrs[0]
return view, paddrs[0], paddrs
def include(self, name:str, arch:str):
for k,v in getattr(getattr(tinygrad.runtime.autogen.nv_regs, name), arch or 'regs').items():
-2
View File
@@ -38,8 +38,6 @@ class _System:
return vfio_fd
except OSError: return None
def reserve_hugepages(self, cnt): os.system(f"sudo sh -c 'echo {cnt} > /proc/sys/vm/nr_hugepages'")
@functools.cache
def reserve_va(self, va_start, va_size):
# cached, runs only once per range. used to not collide with other mappings.
+64 -159
View File
@@ -92,7 +92,7 @@ class Tensor(OpMixin):
training: ClassVar[bool] = False
def __init__(self, data:ConstType|bytes|list|tuple|UOp|'numpy.ndarray'|pathlib.Path|None,
device:str|tuple|list|None=None, dtype:DTypeLike|None=None, requires_grad:bool|None=None, _force_unique:bool=False):
device:str|tuple|list|None=None, dtype:DTypeLike|None=None, requires_grad:bool=True, _force_unique:bool=False):
if device is None:
if isinstance(data, pathlib.Path): device = f"DISK:{data.resolve()}" # keep it on the disk if device is None
elif isinstance(data, UOp): device = data._device
@@ -103,9 +103,7 @@ class Tensor(OpMixin):
# tensors can have gradients if you have called .backward
self.grad:Tensor|None = None
# NOTE: this can be in three states. False and None: no gradient, True: gradient
# None (the default) will be updated to True if it's put in an optimizer
self.requires_grad:bool|None = requires_grad
self.requires_grad:bool = requires_grad
# create a UOp from the different types of inputs
if isinstance(data, UOp):
@@ -115,8 +113,8 @@ class Tensor(OpMixin):
elif data is None:
data = UOp.const(_dtype or dtypes.default_float, 0, _device)
elif isinstance(data, get_args(ConstType)):
if _force_unique or requires_grad: data = UOp.unique_const(data, _dtype, _device)
else: data = UOp.const(_dtype or dtypes.from_py(data), data, _device)
dt = _dtype or dtypes.from_py(data)
data = UOp.unique_const(data, dt, _device) if _force_unique or (requires_grad and dtypes.is_float(dt)) else UOp.const(dt, data, _device)
elif isinstance(data, bytes): data = _frompy(data, _dtype or dtypes.uint8, _device)
elif isinstance(data, (list, tuple)):
if _dtype is None:
@@ -151,11 +149,10 @@ class Tensor(OpMixin):
srcs = (self,)+x
new_uop: UOp = fxn(*[t.uop for t in srcs], *extra_args, **kwargs)
if TRACEMETA >= 1 and (metadata:=_METADATA.get()) is not None: all_metadata[new_uop] = (metadata,)
needs_input_grad = [t.requires_grad for t in srcs]
# directly create the Tensor
ret = Tensor.__new__(Tensor)
ret.uop, ret.grad = new_uop, None
ret.requires_grad = True if any(needs_input_grad) else None if None in needs_input_grad else False
ret.requires_grad = any(t.requires_grad for t in srcs)
# add to all_tensors after construction succeeds
all_tensors[weakref.ref(ret)] = None
return ret
@@ -166,7 +163,7 @@ class Tensor(OpMixin):
@staticmethod
def unique_const(fill_value:ConstType|UOp, **kwargs) -> Tensor: return Tensor(fill_value, _force_unique=True, **kwargs)
def requires_grad_(self, requires_grad=True) -> Tensor:
def requires_grad_(self, requires_grad:bool=True) -> Tensor:
# make the UOp unique if it's a CONST to prevent gradient accumulation bugs with cached const UOps
if requires_grad and self.uop.op is Ops.CONST: self.replace(Tensor(self.uop.arg, device=self.device, dtype=self.dtype, requires_grad=True))
self.requires_grad = requires_grad
@@ -566,7 +563,7 @@ class Tensor(OpMixin):
return Tensor._device_seeds[device], low.cat(high)
@staticmethod
def rand(*shape, device:str|None=None, dtype:DTypeLike|None=None, requires_grad:bool|None=None, contiguous:bool=True) -> Tensor:
def rand(*shape, device:str|None=None, dtype:DTypeLike|None=None, requires_grad:bool=True, contiguous:bool=True) -> Tensor:
"""
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[0, 1)`.
@@ -594,32 +591,14 @@ class Tensor(OpMixin):
# ***** creation helper functions *****
@classmethod
def eye(cls, n:int, m:int|None=None, dtype=None, device=None, requires_grad:bool|None=None) -> Tensor:
"""
Returns a 2-D tensor with `n` rows and `m` columns, with ones on the diagonal and zeros elsewhere.
You can pass in `dtype` and `device` keyword arguments to control the data type and device of the tensor.
Additionally, all other keyword arguments are passed to the constructor of the tensor.
```python exec="true" source="above" session="tensor" result="python"
print(Tensor.eye(3).numpy())
```
```python exec="true" source="above" session="tensor" result="python"
print(Tensor.eye(2, 4).numpy())
```
"""
return super().eye(n, m, dtype, device).requires_grad_(requires_grad)
def _multi_like(self, fxn, *args, **kwargs) -> Tensor:
dtype = kwargs.pop("dtype", self.dtype)
if kwargs.get("device") is not None: raise RuntimeError("cannot specify `device` on `*_like` of a multi device tensor")
if self.uop.axis is None: return fxn(self.shape, *args, dtype=dtype, **kwargs).shard(self.device)
stacked = UOp.mstack(*[fxn(self.uop.shard_shape, *args, device=d, dtype=dtype, **kwargs).uop for d in self.device])
return Tensor(stacked.multi(self.uop.axis), requires_grad=kwargs.get("requires_grad"))
return Tensor(stacked.multi(self.uop.axis), requires_grad=kwargs.get("requires_grad", True))
def full_like(self, fill_value:ConstType, dtype=None, device=None, requires_grad=None) -> Tensor:
def full_like(self, fill_value:ConstType, dtype=None, device=None, requires_grad:bool=False) -> Tensor:
"""
Creates a tensor with the same shape as `self`, filled with the given value.
If `dtype` is not specified, the dtype of `self` is used.
@@ -631,12 +610,9 @@ class Tensor(OpMixin):
print(Tensor.full_like(t, 42).numpy())
```
"""
if device is not None:
if isinstance(self.device, tuple): raise RuntimeError("cannot specify `device` on `full_like` of a multi device tensor")
return Tensor.full(self.shape, fill_value, dtype=dtype or self.dtype, device=device).requires_grad_(requires_grad)
if requires_grad:
return Tensor.full(self.shape, fill_value, dtype=dtype or self.dtype, device=self.device).requires_grad_(requires_grad)
return super().full_like(fill_value, dtype)
if device is None: return super().full_like(fill_value, dtype).requires_grad_(requires_grad)
if isinstance(self.device, tuple): raise RuntimeError("cannot specify `device` on `full_like` of a multi device tensor")
return Tensor.full(self.shape, fill_value, dtype=dtype or self.dtype, device=device).requires_grad_(requires_grad)
def rand_like(self, **kwargs) -> Tensor:
"""
@@ -655,7 +631,7 @@ class Tensor(OpMixin):
# ***** random functions *****
def randn_like(self, dtype:DTypeLike|None=None, requires_grad:bool|None=None, **kwargs) -> Tensor:
def randn_like(self, dtype:DTypeLike|None=None, requires_grad:bool=True, **kwargs) -> Tensor:
"""
Creates a tensor with the same shape and sharding as `self`, filled with random values from a normal distribution with mean 0 and variance 1.
@@ -672,7 +648,7 @@ class Tensor(OpMixin):
return (src[0].mul(2*math.pi).cos().mul((1 - src[1]).log().mul(-2).sqrt()).cast(dtype or self.dtype)).requires_grad_(requires_grad)
@staticmethod
def randn(*shape, dtype:DTypeLike|None=None, requires_grad:bool|None=None, **kwargs) -> Tensor:
def randn(*shape, dtype:DTypeLike|None=None, requires_grad:bool=True, **kwargs) -> Tensor:
"""
Creates a tensor with the given shape, filled with random values from a normal distribution with mean `0` and standard deviation `1`.
If `dtype` is not specified, the default type is used.
@@ -707,7 +683,7 @@ class Tensor(OpMixin):
return Tensor.uniform(*shape, low=low, high=high, dtype=dtype, **kwargs)
@staticmethod
def normal(*shape, mean=0.0, std=1.0, requires_grad:bool|None=None, **kwargs) -> Tensor:
def normal(*shape, mean=0.0, std=1.0, requires_grad:bool=True, **kwargs) -> Tensor:
"""
Creates a tensor with the given shape, filled with random values from a normal distribution with the given `mean` and standard deviation `std`.
Requires `std >= 0`.
@@ -724,7 +700,7 @@ class Tensor(OpMixin):
return (std * Tensor.randn(*shape, **kwargs) + mean).requires_grad_(requires_grad)
@staticmethod
def uniform(*shape, low=0.0, high=1.0, dtype:DTypeLike|None=None, requires_grad:bool|None=None, **kwargs) -> Tensor:
def uniform(*shape, low=0.0, high=1.0, dtype:DTypeLike|None=None, requires_grad:bool=True, **kwargs) -> Tensor:
"""
Creates a tensor with the given shape, filled with random values from a uniform distribution over the interval `[low, high)`.
Requires `low < high`.
@@ -815,7 +791,7 @@ class Tensor(OpMixin):
print(Tensor.randperm(6).numpy())
```
"""
return Tensor.rand(n, device=device, **kwargs).argsort().cast(dtype).requires_grad_(kwargs.get("requires_grad"))
return Tensor.rand(n, device=device, **kwargs).argsort().cast(dtype).requires_grad_(kwargs.get("requires_grad", True))
def multinomial(self:Tensor, num_samples:int = 1, replacement:bool = False) -> Tensor:
"""
@@ -883,7 +859,7 @@ class Tensor(OpMixin):
"""
all_uops = self.uop.toposort()
tensors_need_grad: list[Tensor] = [t for tref in all_tensors if (t:=tref()) is not None and \
t.uop in all_uops and t.requires_grad]
t.uop in all_uops and t.requires_grad and t.is_floating_point()]
# clear contexts
for t,g in zip(tensors_need_grad, self.gradient(*tensors_need_grad, gradient=gradient)):
assert g.shape == t.shape, f"grad shape must match tensor shape, {g.shape!r} != {t.shape!r}"
@@ -1027,12 +1003,13 @@ class Tensor(OpMixin):
def __setitem__(self, indices, v:Tensor|PyConst|list|tuple) -> None:
if isinstance(v, Tensor) and v.dtype != self.dtype: raise RuntimeError(f"setitem dtype mismatch: {self.dtype=} != {v.dtype=}")
if self.requires_grad or (isinstance(v, Tensor) and v.requires_grad):
# for +=/-=, v's graph references self.uop through the view — exclude those from the stale-use check
v_uop, v_bw = (v.uop, v.uop.backward_slice) if isinstance(v, Tensor) else (None, {})
if any(self.uop in t.uop.backward_slice for tref in all_tensors
if (t:=tref()) is not None and t is not self and t.uop is not v_uop and t.uop not in v_bw):
raise RuntimeError("can't setitem on a tensor that already has other uses and requires grad")
# raise if mutation would diverge from eager (allow only pure views of a realized buffer; exclude +=/-= RHS via v_uop/v_bw)
v_uop, v_bw = (v.uop, v.uop.backward_slice) if isinstance(v, Tensor) else (None, {})
shared = self.uop.base if self.uop.base.is_realized else None
if any(self.uop in t.uop.backward_slice_with_self and t.uop.base is not shared for tref in all_tensors
if (t:=tref()) is not None and t is not self and t.uop is not v_uop and t.uop not in v_bw):
raise RuntimeError("can't setitem on a tensor with other uses")
if not self.uop.base.is_realized and self.is_floating_point() and (self.requires_grad or (isinstance(v, Tensor) and v.requires_grad)):
if not isinstance(v, Tensor): v = Tensor(v, device=self.device, dtype=self.dtype)
# __iadd__/__isub__ creates AFTER(view, STORE(view, computed)); unwrap to get the computed value
if v.uop.op is Ops.AFTER and any(s.op is Ops.STORE for s in v.uop.src[1:]): v = v._apply_uop(lambda x: x.src[1].src[1])
@@ -1057,10 +1034,13 @@ class Tensor(OpMixin):
def __delitem__(self, indices) -> None:
raise TypeError("Tensor does not support deleting items")
def masked_select(self, mask):
def masked_select(self, mask, size:int|None=None, fill_value:ConstType=0):
"""
Selects elements from `self` based on the boolean `mask`.
With `size=None` (default), output length equals the number of `True` values (not jittable).
With `size=N`, output length is `N`, padded with `fill_value` or truncated (jittable).
```python exec="true" source="above" session="tensor" result="python"
t = Tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
mask = Tensor([[True, False, True], [False, True, False], [False, False, True]])
@@ -1070,19 +1050,25 @@ class Tensor(OpMixin):
```python exec="true" source="above" session="tensor" result="python"
print(t.masked_select(mask).numpy())
```
```python exec="true" source="above" session="tensor" result="python"
print(t.masked_select(mask, size=6, fill_value=-1).numpy())
```
"""
if not dtypes.is_bool(mask.dtype): raise RuntimeError(f"masked_select expects bool mask tensor, got {mask.dtype}")
x, mask = self.flatten(), mask._broadcast_to(self.shape).flatten()
mask_cumsum = mask.cumsum()
counts = Tensor.zeros(mask_cumsum[-1].item(), dtype=dtypes.int32, device=self.device)
idxs = counts.scatter(0, mask_cumsum, 1, reduce='add').cumsum()
return x[idxs]
if size is None:
counts = Tensor.zeros(mask_cumsum[-1].item() if mask.numel() else 0, dtype=dtypes.int32, device=self.device)
return x[counts.scatter(0, mask_cumsum, 1, reduce='add').cumsum()]
counts = Tensor.zeros(size, dtype=dtypes.int32, device=self.device).scatter(0, mask_cumsum, 1, reduce='add')
return (Tensor.arange(size, device=self.device) < mask.sum()).where(x[counts.cumsum()], fill_value).cast(self.dtype)
def nonzero(self) -> Tensor:
def nonzero(self, size:int|None=None, fill_value:ConstType=0) -> Tensor:
"""
Returns the indices of the elements that are non-zero.
Returns a 2D tensor where each row is the index of a non-zero element.
With `size=None` (default), output shape is `(n_nonzero, ndim)` (not jittable).
With `size=N`, output shape is `(N, ndim)`, padded with `fill_value` or truncated (jittable).
```python exec="true" source="above" session="tensor" result="python"
t = Tensor([1, 0, 2, 0, 3])
@@ -1098,11 +1084,17 @@ class Tensor(OpMixin):
```python exec="true" source="above" session="tensor" result="python"
print(t.nonzero().numpy())
```
```python exec="true" source="above" session="tensor" result="python"
print(t.nonzero(size=3, fill_value=-1).numpy())
```
"""
if self.ndim == 0:
return Tensor.zeros(size if size is not None else int((self != 0).item()), 0, dtype=dtypes.int32, device=self.device)
mask = (self != 0).flatten()
indices = Tensor.stack(*[Tensor.arange(s, device=self.device).reshape(*[1]*i, s, *[1]*(self.ndim-i-1)).expand(self.shape).flatten()
for i, s in enumerate(self.shape)], dim=-1)
return indices.masked_select(mask.unsqueeze(-1).expand(*mask.shape, self.ndim)).reshape(-1, self.ndim)
return indices.masked_select(mask.unsqueeze(-1).expand(*mask.shape, self.ndim),
size=size*self.ndim if size is not None else None, fill_value=fill_value).reshape(-1, self.ndim)
# ***** reduce ops *****
@@ -1131,12 +1123,13 @@ class Tensor(OpMixin):
0x8000000000008002, 0x8000000000000080, 0x800a, 0x800000008000000a, 0x8000000080008081, 0x8000000000008080, 0x80000001, 0x8000000080008008)]
rate, dsbyte = {"sha3_224": (144, 6), "sha3_256": (136, 6), "shake_128": (168, 31)}[cfg] if isinstance(cfg, str) else cfg
data, data_pad = self.bitcast(dtypes.uint8).reshape(prod(self.shape[:-1]), self.shape[-1]), rate - (self.shape[-1] * self.dtype.itemsize % rate)
data = self.bitcast(dtypes.uint8).reshape(prod(self.shape[:-1]), self.shape[-1])
data_pad = rate - data.shape[-1] % rate
# pad batches then pad blocks
data = data.pad((None, (0, data_pad))).reshape(bs := data.shape[0], -1, rate).pad((None, None, (0, 200 - rate)))
data = data.pad((None, (0, data_pad))).reshape(bs := data.shape[0], -1, rate).pad_to(None, None, 200)
# create pad mask
lbe = prod(data.shape[1:]) + rate - data_pad - 200
lbe = (data.shape[1] - 1) * 200 + rate - data_pad
if data_pad == 1: mb = [(lbe, 0), (1, dsbyte ^ 0x80), (200 - rate, 0)]
else: mb = [(lbe, 0), (1, dsbyte), (data_pad - 2, 0), (1, 0x80), (200 - rate, 0)]
pad_mask = Tensor.cat(*(Tensor(v, dtype=dtypes.uint8, device=data.device).expand(l) for l, v in mb if l > 0)).unsqueeze(0)
@@ -1145,7 +1138,7 @@ class Tensor(OpMixin):
state = Tensor.zeros(bs, 25, device=self.device, dtype=dtypes.uint64)
for k in range(int(data.shape[1])):
state = state ^ data.shrink((None, (k, k+1), None)).squeeze(1)
state = state ^ data[:, k]
for i in range(24): # f1600
# θ step
p = state.reshape(bs, 5, 5).transpose(2, 1)
@@ -1164,11 +1157,7 @@ class Tensor(OpMixin):
assert self.dtype == dtypes.uint8, "only support uint8 tensors for hashing"
assert self.ndim == 2, "only support batched 1d tensors"
assert self.shape[1] == 1024 * 1024, "only support messages of 1mb"
blocks = self.shape[0] * self.shape[1] // 4096
data = self.reshape(blocks, 4096)
block_hashes = data.keccak("shake_128").reshape(self.shape[0], 4096)
return block_hashes.keccak("shake_128").reshape(self.shape[0], 16)
return self.reshape(-1, 4096).keccak("shake_128").reshape(self.shape[0], -1).keccak("shake_128")
def hash(self) -> Tensor:
"""
@@ -1178,19 +1167,14 @@ class Tensor(OpMixin):
print(t.data().hex())
```
"""
data = self.flatten().bitcast(dtypes.uint8)
if (tsize := data.shape[0]) % 2**20 != 0: data = data.pad((0, 2**20 - tsize % 2**20))
base_chunks = ceildiv(data.shape[0], 2**20)
tree_depth = math.ceil(math.log(base_chunks, 65536)) if base_chunks > 1 else 0
level_chunks = base_chunks
for _ in range(tree_depth + 1):
data = data.reshape(level_chunks, 2**20)._hash_1mb().flatten()
if (tsize := data.shape[0]) % 2**20 != 0: data = data.pad((0, 2**20 - tsize % 2**20))
level_chunks = ceildiv(data.shape[0], 2**20)
return data[:16]
n = data.shape[0]
assert isinstance(n, int), "hash requires concrete shape"
chunks = ceildiv(n, 2**20)
while chunks > 1:
data = data.pad_to(chunks * 2**20).reshape(chunks, 2**20)._hash_1mb().flatten()
chunks = ceildiv(chunks, 65536)
return data.pad_to(2**20).unsqueeze(0)._hash_1mb().flatten()[:16]
# ***** processing ops *****
@@ -1393,77 +1377,6 @@ class Tensor(OpMixin):
qk = qk + attn_mask
return qk.cast(self.dtype).softmax(-1).dropout(dropout_p) @ value
def qr(self) -> tuple[Tensor, Tensor]:
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
R = self.clone()
Q = Tensor.eye(m, dtype=self.dtype, device=self.device).expand(b_shape + (m, m))
for i in range(min(m, n)):
x = R[..., i:m, i]
norm = x.square().sum(-1).sqrt()
mask = norm != 0
s = (x[..., 0] != 0).where(-x[..., 0].sign(), -1)
u1 = x[..., 0] - s * norm
w = x.unsqueeze(-1) / mask.where(u1, 1)[..., None, None]
w[..., 0, 0] = 1
tau = (-s * u1 / mask.where(norm, 1))[..., None, None]
tau = mask[..., None, None].where(tau, 0)
R[..., i:m, :] = R[..., i:m, :] - (w * tau) @ (w.transpose(-2, -1) @ R[..., i:m, :])
Q[..., :, i:m] = Q[..., :, i:m] - (Q[..., :, i:m] @ w) @ (tau * w).transpose(-2, -1)
return Q, R
def svd(self, full_matrices = True) -> tuple[Tensor, Tensor, Tensor]:
#partial implementation of https://www.netlib.org/lapack/lawnspdf/lawn169.pdf , pg 26
assert self.ndim > 1, f"expected two or more dimensions, got {self.ndim}"
b_shape, m, n = self.shape[:-2], int(self.shape[-2]), int(self.shape[-1])
#preprocess the matrix
Q, R = (self if m >= n else self.transpose(-2, -1)).qr()
num, q_num = min(m, n), max(m, n)
# TODO: codegen infinite loop without contiguous
U = R[..., :num, :num].contiguous()
V = Tensor.eye(num, dtype=self.dtype, device=self.device).expand(b_shape + (num, num)).contiguous()
#prepare round robin pairing
permute, inverse_permute = Tensor.arange(0, num, dtype=dtypes.int, device=self.device), Tensor.zeros(num, dtype=dtypes.int, device=self.device)
permute[num//2:num] = permute[num//2:num].flip(0)
inverse_permute[permute] = Tensor.arange(num, dtype=dtypes.int, device=self.device)
def one_round_jacobi(U, V, permute, inverse_permute):
#pair all the columns
V_permuted, runoff_V = (V[..., permute].split(num - 1, -1)) if num % 2 == 1 else (V[..., permute], None)
V_left, V_right = V_permuted.split(num//2, -1)
U_permuted, runoff_U = (U[..., permute].split(num - 1, -1)) if num % 2 == 1 else (U[..., permute], None)
U_left, U_right = U_permuted.split(num//2, -1)
#compute the jacobi rotations for each pairing
gamma = (U_left * U_right).sum(-2).reshape(b_shape + (1, num//2))
alpha, beta = U_permuted.square().sum(-2).unsqueeze(-2).split(num//2, -1)
rot = gamma != 0
tau = (beta - alpha) / (2 * rot.where(gamma, 1))
t = (tau != 0).where(tau.sign(), 1) / (tau.abs() + (1 + tau.square()).sqrt())
t = rot.where(t, 0)
c = 1 / (1 + t.square()).sqrt()
s = c * t
#apply the rotations
U_left, U_right = c * U_left - s * U_right, s * U_left + c * U_right
U = U_left.cat(U_right.cat(runoff_U, dim=-1) if num % 2 == 1 else U_right, dim=-1)[..., inverse_permute]
V_left, V_right = c * V_left - s * V_right, s * V_left + c * V_right
V = V_left.cat(V_right.cat(runoff_V, dim=-1) if num % 2 == 1 else V_right, dim=-1)[..., inverse_permute]
#prepare the next round robin pairings
if num % 2 == 1: permute = (permute - 1) % num
else: permute = permute[0].reshape(1).cat(((permute[1:num] - 2) % (num - 1)) + 1)
inverse_permute = inverse_permute.scatter(0, permute, Tensor.arange(num, dtype=dtypes.int32, device=self.device))
return U, V, permute, inverse_permute
#sorta heuristic, most use num*log2(num)
for _ in range(int(num * math.log2(num) * 2 + 2)): U, V, permute, inverse_permute = one_round_jacobi(U, V, permute, inverse_permute)
#extract singular values and sort. construct U from Q
S, indices = U.square().sum(-2).sqrt().sort(dim=-1, descending=True)
new_indices = indices.unsqueeze(-2).expand(b_shape + (num, num))
U = U.gather(-1, new_indices) / (S != 0).where(S, 1).unsqueeze(-2)
V = V.gather(-1, new_indices)
padded_u = Tensor.eye(q_num, dtype=U.dtype, device=U.device).expand(b_shape + (q_num, q_num))
padded_u[..., 0:num, 0:num] = U
U = Q @ padded_u
if not full_matrices: U = U[..., 0:num]
return (U, S, V.transpose(-2, -1)) if m >= n else (V, S, U.transpose(-2, -1))
# ***** cast ops *****
def cast(self, dtype:DTypeLike) -> Tensor:
@@ -1489,8 +1402,6 @@ class Tensor(OpMixin):
"""
Bitcasts `self` to the given `dtype` of the same itemsize.
`self` must not require a gradient.
```python exec="true" source="above" session="tensor" result="python"
t = Tensor([-1, 2, 3], dtype=dtypes.int32)
print(t.dtype, t.numpy())
@@ -1500,7 +1411,6 @@ class Tensor(OpMixin):
print(t.dtype, t.numpy())
```
"""
if self.requires_grad: raise RuntimeError("can't backprop through bitcast")
dt = to_dtype(dtype)
if (ns:=dt.itemsize) != (os:=self.dtype.itemsize) and (self.shape[-1]*os) % ns != 0: raise RuntimeError("unsupported size in bitcast")
if (not isinstance(self.device, str) or not self.device.startswith("DISK")) and ns != os:
@@ -1533,7 +1443,7 @@ class Tensor(OpMixin):
def image_conv2d(self, weight:Tensor, bias:Tensor|None=None, groups=1, stride=1, dilation=1, padding=0, dtype=None) -> Tensor:
dtsz = 2 if FLOAT16 else 4
(bs,_,iy,ix), (cout,cin,H,W) = self.shape, weight.shape
(bs,_,_,_), (cout,cin,H,W) = self.shape, weight.shape
assert isinstance(cin, int) and isinstance(cout, int)
x, w = self, weight.reshape(groups, (rcout := cout//groups), cin, H, W)
@@ -1569,7 +1479,6 @@ class Tensor(OpMixin):
def ipad(t, i, amt):
shape = (None,)*i + (amt,) + (None,)*(t.ndim-i-1)
return Tensor(True, device=t.device).expand(t.shape).pad_to(shape).where(t.pad_to(shape), Invalid) if amt != t.shape[i] else t
# align a dimension, use at to specify the dimension to pad in, defaults to first
def pad_align(t, dim, at=None, force=False):
# align to 64 pixels when height is real, otherwise 64 bytes is sufficient
@@ -1587,7 +1496,7 @@ class Tensor(OpMixin):
else: x, w = x.contiguous(), w.contiguous()
# undo alignment hacks
if bank_conflict: x, w = x[:, :, :ix, :, :cin // 4, :], w[:, :H, :cin // 4, ...]
if bank_conflict: x, w = x[:, :, :, :, :cin // 4, :], w[:, :, :cin // 4, ...]
else: x, w = x[:, :, :ix, :], w[:, :H, ...]
# expand out
@@ -1610,13 +1519,9 @@ class Tensor(OpMixin):
# the conv!
ret = (x*w).cast(dtypes.float32).sum((-4, -3, -2, -1), dtype=dtype)
if added_ox:
ret = ret.reshape(bs, oy, ox + added_ox, groups, rcout)[:, :, :ox, ...]
ret = ret.reshape(bs, oy, ox + added_ox, groups, rcout)[:, :, :ox, :, :]
# undo hack for non multiples of 4 on C.rcout
if added_output_channels:
ret = ret.reshape(bs, oy, ox, groups, rcout)[:, :, :, :, :-added_output_channels]
if added_output_channels: ret = ret[:, :, :, :, :-added_output_channels]
# NCHW output
ret = ret.reshape(bs, oy, ox, groups * (rcout - added_output_channels)).permute(0,3,1,2)
return ret if bias is None else ret.add(bias.reshape(1, -1, 1, 1))
+1
View File
@@ -286,6 +286,7 @@ def fast_idiv(target: Target, x: UOp, d: int, dont_cast=False) -> UOp|None:
is_unsigned = x.vmin>=0 or x.dtype in dtypes.uints
assert d>0, "Sign should have been taken out of divisor"
vmin,vmax = max(x.vmin, x.dtype.min), min(x.vmax, x.dtype.max)
if vmin > -d and vmax < d: return x.const_like(0)
m,s = magicgu(max(vmax, abs(vmin)), d)
if m*vmin >= x.dtype.min and m*vmax <= x.dtype.max:
return ((x*m) >> s) if is_unsigned else ((x*m) >> s) + (x<0).where(x.ufix(1), 0)
+12 -5
View File
@@ -213,6 +213,10 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.BINARY | Ops.INS | Ops.TUPLE | Ops.CALL | Ops.FUNCTION:
return None
# hacks for NOOP
case Ops.NOOP:
return self.src[0]._shape if len(self.src) >= 1 else None
case Ops.GETTUPLE:
# GETTUPLE extracts from a TUPLE (possibly through a FUNCTION)
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
@@ -258,7 +262,8 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
case Ops.WMMA | Ops.SHAPED_WMMA: return self.src[2]._shape
# passthrough ops
case Ops.MSTACK | Ops.MSELECT | Ops.DETACH | Ops.CONTIGUOUS | Ops.CONTIGUOUS_BACKWARD | Ops.AFTER | Ops.PATCH | Ops.LOAD:
case Ops.MSTACK | Ops.MSELECT | Ops.DETACH | Ops.CONTIGUOUS | Ops.CONTIGUOUS_BACKWARD | Ops.AFTER | Ops.PATCH | Ops.LOAD | \
Ops.COPY | Ops.ALLREDUCE:
return self.src[0]._shape
# REDUCE with empty axis is passthrough (lowered form)
case Ops.REDUCE if len(self.arg[1]) == 0:
@@ -312,10 +317,12 @@ class UOp(OpMixin, metaclass=UOpMetaClass):
return tuple(1 if i in axis_arg else s for i,s in enumerate(ps))
# elementwise ops keep the shape the same. all inputs with shape must match
if self.op in GroupOp.ALU.union({Ops.CAST, Ops.COPY, Ops.NOOP, Ops.GROUP, Ops.SINK, Ops.ALLREDUCE, Ops.STORE}):
input_shapes = [x._shape for x in self.src if x._shape is not None]
if len(input_shapes) == 0: return None
if not all_same(input_shapes): raise RuntimeError(f"shape mismatch at {self.op}: {input_shapes} {[x.op for x in self.src]}")
if self.op in GroupOp.ALU.union({Ops.CAST, Ops.GROUP, Ops.STORE}):
input_shapes = [x._shape for x in self.src]
assert len(self.src) > 0 and all(x is not None for x in input_shapes), f"None input shape not supported for {self.op}"
# TODO: add broadcasting here
if not all_same(input_shapes):
raise RuntimeError(f"shape mismatch at {self.op}: {input_shapes} {[x.op for x in self.src]}")
return input_shapes[0]
# all Ops must be explicitly handled
+11 -8
View File
@@ -7,7 +7,9 @@ from tinygrad.helpers import DEBUG, Context, prod, SPEC, Metadata, panic, CHECK_
# ***** uop helpers *****
def validate_index(buf:UOp, idx:UOp, gate:UOp|None=None):
def validate_index(uidx:UOp, gate:UOp|None=None):
if len(uidx.src) != 2: return True # skip for non final index. TODO: check more complex index with shape
buf,idx = uidx.src
if idx.op is Ops.CONST and idx.arg is Invalid: return True
if gate is None: gate = UOp.const(dtypes.bool, True)
# TODO: check for overflow
@@ -79,8 +81,9 @@ spec_shared = PatternMatcher([
(UPat(Ops.DEFINE_LOCAL, name="x"), lambda x: isinstance(x.dtype, PtrDType) and x.dtype.addrspace == AddrSpace.LOCAL),
(UPat(Ops.DEFINE_REG, src=(), name="x"), lambda x: isinstance(x.arg, int)),
# AFTER on Movement Op, PARAM, BUFFER, or another AFTER
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.DEFINE_REG, Ops.DEFINE_LOCAL, Ops.AFTER, Ops.MULTI, Ops.BITCAST})),),
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, or another AFTER
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.DEFINE_REG, Ops.DEFINE_LOCAL, Ops.AFTER, Ops.MULTI,
Ops.BITCAST})),),
allow_any_len=True), lambda: True),
# CUSTOM (inline and non inline)
@@ -96,11 +99,11 @@ spec_shared = PatternMatcher([
(UPat(Ops.INS), lambda: True),
# LOAD(idx) / STORE(idx, val) with gates on the LOAD/STORE
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().load(), validate_index),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().load(UPat.var("alt"), UPat.var("gate", dtype=dtypes.bool), name="load"),
lambda buf,idx,gate,alt,load: validate_index(buf, idx, gate) if alt.dtype == load.dtype else False),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().store(UPat()), validate_index),
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx"))).or_casted().store(UPat(), UPat.var("gate", dtype=dtypes.bool)), validate_index),
(UPat(Ops.INDEX, name="uidx").or_casted().load(), validate_index),
(UPat(Ops.INDEX, name="uidx").or_casted().load(UPat.var("alt"), UPat.var("gate", dtype=dtypes.bool), name="load"),
lambda uidx,gate,alt,load: validate_index(uidx, gate) if alt.dtype == load.dtype else False),
(UPat(Ops.INDEX, name="uidx").or_casted().store(UPat()), validate_index),
(UPat(Ops.INDEX, name="uidx").or_casted().store(UPat(), UPat.var("gate", dtype=dtypes.bool)), validate_index),
# STORE in tensor graph: store a value into a target
(UPat(Ops.STORE, dtypes.void, (UPat(name="x"), UPat())), lambda x: True),
+8 -3
View File
@@ -68,7 +68,12 @@ def main(args) -> None:
data = viz.get_render(viz_data, step["query"])
if isinstance(data.get("value"), Iterator):
for m in data["value"]:
if "uop" in m: print(emit(m["graph"] if print_graph else m["uop"]))
if print_graph and "graph" in m and not args.json:
for k,v in m["graph"].items():
print(f"[{k}] {' '.join((lines:=v['label'].splitlines())[:5])}{'...' if len(lines) > 5 else ''}"+(f" tag={v['tag']}" if v['tag'] else ''))
if v["src"]:
print(" src: "+", ".join([f"{i}->[{x}]" for i,x in v["src"][:5]])+(f", ... and {len(v['src'])-5} more" if len(v["src"]) > 5 else ""))
elif "uop" in m: print(emit(m["graph"] if print_graph else m["uop"]))
if not reconstruct_matches: return None
if m.get("diff"):
loc = pathlib.Path(m["upat"][0][0])
@@ -194,8 +199,8 @@ def main(args) -> None:
if DEBUG >= 3 and s["name"] == "View Base AST": print_step(s)
if DEBUG >= 4 and s["name"] == "View Source": print_step(s)
if DEBUG >= 5 or ls: print(emit(" "*s["depth"]+s["name"]+(f" - {s['match_count']}" if s.get('match_count', 0) else '')))
if DEBUG >= 6 or (DEBUG >= 5 and s["name"] == "View Kernel Graph"): print_step(s, print_graph=True)
if DEBUG >= 7 or s["name"] in args.src: print_step(s, reconstruct_matches=True)
if DEBUG >= 6 or (DEBUG >= 5 and s["name"] == "View Kernel Graph") or (s["name"] in args.src): print_step(s, print_graph=True)
if DEBUG >= 7: print_step(s, reconstruct_matches=True)
elif DEBUG >= 3 and k.get("ext"): print(emit(k["ext"]))
for k in (produce_top_kernels if args.t else produce_all_kernels)(): render_event(k)