Compare commits

..
1 Commits
Author SHA1 Message Date
geohot ad7e744382 vf MI350X passthrough mode driver 2026-08-11 19:01:36 -07:00
107 changed files with 1011 additions and 1434 deletions
+10
View File
@@ -41,6 +41,10 @@ inputs:
description: "Install LLVM?"
required: false
default: 'false'
tinydreno:
description: "Install tinydreno"
required: false
default: 'false'
qemu:
description: "Install qemu"
required: false
@@ -273,6 +277,12 @@ runs:
shell: bash
run: brew install llvm@20
# *** tinydreno ***
- name: Install tinydreno (linux)
if: inputs.tinydreno == 'true' && runner.os == 'Linux'
shell: bash
run: sudo curl -fL https://github.com/sirhcm/tinydreno/raw/refs/heads/master/libllvm-qcom.so -o /usr/lib/libllvm-qcom.so
# *** OpenCL ***
- name: Install rusticl
if: inputs.opencl == 'true'
+32
View File
@@ -179,3 +179,35 @@ jobs:
- name: Run test_tiny
shell: bash
run: python -m pytest -n=auto test/test_tiny.py --durations=20
qcomclcompiletests:
name: Compile-only (QCOM CL)
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Setup Environment
uses: ./.github/actions/setup-tinygrad
with:
key: compile-qcomcl
deps: testing_unit
tinydreno: 'true'
- name: Set env
shell: bash
run: printf "DEV=NULL:QCOMCL:a630\nNULL_ALLOW_COPYOUT=1" >> $GITHUB_ENV
- name: Run test_ops
shell: bash
run: |
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
+35 -56
View File
@@ -21,7 +21,7 @@ concurrency:
jobs:
docs:
name: Docs
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: &linux ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
timeout-minutes: 10
env:
CHECK_OOB: 0
@@ -61,7 +61,7 @@ jobs:
torchbackend:
name: Torch Backend Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -86,30 +86,9 @@ jobs:
- name: Custom tests
run: DEV=CPU:LLVM GPUS=4 TINY_BACKEND=1 python3 -m pytest -nauto extra/torch_backend/test.py extra/torch_backend/test_inplace.py extra/torch_backend/test_multigpu.py extra/torch_backend/test_kernel_fusion.py --durations=20
torchbackendtrain:
name: Torch Backend Training
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
timeout-minutes: 15
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Setup Environment
uses: ./.github/actions/setup-tinygrad
with:
key: torch-backend-pillow-torchvision-et-pt
deps: testing_unit
pydeps: "pillow torchvision expecttest"
llvm: 'true'
- name: Install ninja
run: |
sudo apt update || true
sudo apt install -y --no-install-recommends ninja-build
- name: Test beautiful_mnist in torch with TINY_BACKEND
run: STEPS=20 DEV=CPU TARGET_EVAL_ACC_PCT=90.0 MAX_BUFFER_SIZE=0 TINY_BACKEND=1 python3 examples/other_mnist/beautiful_mnist_torch.py
bepython:
name: Python Backend
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -147,7 +126,7 @@ jobs:
linter:
name: Linters
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 10
steps:
@@ -178,7 +157,7 @@ jobs:
nulltest:
name: Null Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
@@ -212,7 +191,7 @@ jobs:
unittest:
name: Unit Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
@@ -249,7 +228,7 @@ jobs:
matrix:
group: [1, 2]
name: SPEC=2 (${{ matrix.group }})
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -265,7 +244,7 @@ jobs:
fuzzing:
name: Fuzzing
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 10
steps:
- name: Checkout Code
@@ -281,7 +260,7 @@ jobs:
testopenclimage:
name: CL IMAGE Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -301,7 +280,7 @@ jobs:
testopenpilot:
name: openpilot Compile Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -330,7 +309,7 @@ jobs:
testonnxcpu:
name: ONNX (CPU) Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 20
steps:
@@ -349,7 +328,7 @@ jobs:
testoptim:
name: Optimization Tests
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 20
steps:
- name: Checkout Code
@@ -381,7 +360,7 @@ jobs:
testllm:
name: Test LLM
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
env:
CHECK_OOB: 0
@@ -408,7 +387,7 @@ jobs:
testmodels:
name: Models
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -428,7 +407,7 @@ jobs:
testdsp:
name: Linux (DSP)
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
steps:
- name: Checkout Code
@@ -456,7 +435,7 @@ jobs:
- 'WEBGPU'
name: Linux (DEV=${{ matrix.dev }})
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 20
steps:
- name: Checkout Code
@@ -482,7 +461,7 @@ jobs:
testamdasm:
name: AMD ASM IDE
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 20
env:
DEV: MOCKKFD+AMD
@@ -528,7 +507,7 @@ jobs:
hcq2:
name: hcq2
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 5
steps:
- name: Checkout Code
@@ -550,7 +529,7 @@ jobs:
testmockam:
name: Linux (am)
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
env:
DEV: MOCKPCI+AMD
@@ -586,7 +565,7 @@ jobs:
arch: [gfx1100, gfx1201, gfx950]
name: Linux (${{ matrix.backend }} ${{ matrix.arch }})
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 15
env:
DEV: MOCKKFD+AMD:${{ matrix.backend == 'amdllvm' && 'LLVM' || '' }}:${{ matrix.arch }}
@@ -624,7 +603,7 @@ jobs:
backend: [ptx, nv]
name: Linux (${{ matrix.backend }})
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
runs-on: *linux
timeout-minutes: 20
env:
FORWARD_ONLY: 1
@@ -658,17 +637,10 @@ jobs:
strategy:
fail-fast: false
matrix:
dev:
- 'NULL:IR3:a630'
- 'NULL:QCOMCL:a630'
- 'NULL:NAK:sm_120'
name: Compile-only (DEV=${{ matrix.dev }})
runs-on: ${{ github.repository == 'tinygrad/tinygrad' && github.event_name == 'pull_request' && github.event.pull_request.author_association == 'COLLABORATOR' && 'namespace-profile-tinygrad' || 'ubuntu-24.04' }}
backend: [ir3, nak]
name: Compile-only (${{ matrix.backend }})
runs-on: *linux
timeout-minutes: 15
env:
NULL_ALLOW_COPYOUT: 1
DEV: ${{ matrix.dev }}${{ contains(matrix.dev, 'a630') && ',IMAGE_PITCH_ALIGNMENT=64' || '' }}
IMAGE: ${{ contains(matrix.dev, 'a630') && '1' || '0' }}
steps:
- name: Checkout Code
uses: actions/checkout@v6
@@ -677,14 +649,21 @@ jobs:
with:
key: compile-${{ matrix.backend }}
deps: "testing_unit mesa"
qemu: ${{ contains(matrix.dev, 'QCOMCL') }}
- name: Test IMAGE
- name: Set env
shell: bash
if: contains(matrix.dev, 'a630')
run: DEBUG=7 python3 test/backend/test_ops.py TestOps.test_gemm | grep isam
run: printf "NULL_ALLOW_COPYOUT=1\n${{ matrix.backend == 'ir3' && 'DEV=NULL:IR3:a630' || matrix.backend == 'nak' && 'DEV=NULL:NAK:sm_120' }}" >> $GITHUB_ENV
- name: Run test_ops
shell: bash
run: |
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
+1 -1
View File
@@ -140,7 +140,7 @@ Documentation along with a quick start guide can be found on the [docs website](
```python
from tinygrad import Tensor
x = Tensor.eye(3).clone() # clone to make it a buffer
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]])
z = y.matmul(x).sum()
z.backward()
+7 -11
View File
@@ -12,7 +12,7 @@ from tinygrad.helpers import Timing, colored, GlobalCounters, profile_marker
from tinygrad.uop.ops import Ops, UOp
from extra.models.llama import apply_rotary_emb
from extra.llama_kernels.rmsnorm import rmsnorm
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8, asm_gemm, can_use_asm_gemm
from extra.gemm.cdna_asm_gemm import _mx_block_scale, _mx_block_scale_3d, quantize_mxfp8
from extra.gemm.moe_gemm import grouped_mx_gemm
from extra.gemm.moe_routing import route, dispatch, combine
@@ -182,12 +182,12 @@ class GPTOSS:
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
xq, xk, xv = xq.cast(dtypes.bfloat16), xk.cast(dtypes.bfloat16), xv.cast(dtypes.bfloat16) # (B,N,H,D)/(B,N,KV,D)
if getenv("HK_FLASH_ATTENTION"):
from extra.thunder.amd.fa import flash_attention
attn, *_ = flash_attention(xq, xk, xv, is_causal=True, write_flat=True, sinks=sinks, window=self.sliding_window if sliding else 0)
attn = attn.reshape(bsz, seqlen, self.n_heads * self.head_dim)
elif sliding:
if sliding:
attn = self._sliding_attention(xq, xk, xv, sinks)
elif getenv("HK_FLASH_ATTENTION"):
from extra.thunder.amd.fa import flash_attention
attn, *_ = flash_attention(xq, xk, xv, is_causal=True, write_flat=True, sinks=sinks)
attn = attn.reshape(bsz, seqlen, self.n_heads * self.head_dim)
else:
xqm = xq.reshape(bsz, seqlen, self.n_kv_heads, self.n_rep, self.head_dim).permute(0, 2, 3, 1, 4)
xkm, xvm = xk.permute(0, 2, 1, 3).unsqueeze(2), xv.permute(0, 2, 1, 3).unsqueeze(2)
@@ -263,11 +263,7 @@ class GPTOSS:
w_down=self.w_down[i], w_down_scale=self.w_down_scale[i], w_down_bias=self.w_down_bias[i])
h, *_ = self.run_layer(h, freqs_cis, mask_full, i % 2 == 0, attn_kwargs, ffn_kwargs, save=save)
h_normed = self.norm(h)
pad = (-self.dim) % 256
h_padded, w_padded = h_normed.pad((None, None, (0, pad))), self.output.pad(((0, 0), (0, pad)))
if ASM_GEMM and can_use_asm_gemm(h_padded, w_padded.T): logits = asm_gemm(h_padded, w_padded.T)
else: logits = h_normed @ self.output.T
logits = self.norm(h) @ self.output.T
return logits
def _get_pads(uop:UOp) -> list[UOp]:
@@ -46,7 +46,7 @@ export DATA_SEED=${DATA_SEED:-5760}
export JITBEAM=${JITBEAM:-3}
export BEAM_UOPS_MAX=6000 BEAM_UPCAST_MAX=256 BEAM_LOCAL_MAX=1024 BEAM_MIN_PROGRESS=5 BEAM_PADTO=1
export FAKEDATA=${FAKEDATA:-$([[ "$DEV" == NULL:* ]] && echo 1 || echo 0)} BENCHMARK=${BENCHMARK:-10}
export FAKEDATA=${FAKEDATA:-1} BENCHMARK=${BENCHMARK:-10}
if [ -z "$FULL_LAYERS" ]; then
export LLAMA_LAYERS=${LLAMA_LAYERS:-2}
fi
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
export PYTHONPATH="."
export ROCM_PATH=${ROCM_PATH:-/opt/rocm-7.1.1}
export PATH="$ROCM_PATH/bin:$PATH"
export PATH="/opt/rocm-7.1.1/bin:$PATH"
export ROCM_PATH="/opt/rocm-7.1.1"
export DEV=${DEV:-AMD}
export CHECK_OOB=0
export REWRITE_STACK_LIMIT=5000000 HCQDEV_WAIT_TIMEOUT_MS=240000
@@ -11,7 +11,6 @@ export DEVICE_IN_FUNCTION_BUG=1
export DEBUG=${DEBUG:-2}
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
export ASM_GEMM=${ASM_GEMM:-1}
export GROUPED_MOE=${GROUPED_MOE:-1}
export ALL2ALL=${ALL2ALL:-1}
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
@@ -11,7 +11,6 @@ export DEVICE_IN_FUNCTION_BUG=1
export DEBUG=${DEBUG:-0}
export HK_FLASH_ATTENTION=${HK_FLASH_ATTENTION:-1}
export ASM_GEMM=${ASM_GEMM:-1}
export GROUPED_MOE=${GROUPED_MOE:-1}
export ALL2ALL=${ALL2ALL:-1}
export LATE_ALLREDUCE=${LATE_ALLREDUCE:-0}
export ALLREDUCE_CAST=${ALLREDUCE_CAST:-1}
+1 -1
View File
@@ -7,7 +7,7 @@ from tinygrad.runtime.support.hcq import FileIOInterface
from tinygrad.runtime.support.am.amdev import AMDev
if __name__ == "__main__":
gpus = System.pci_scan_bus(0x1002, [(0xffff, [0x74a1, 0x75a0])])
gpus = System.pci_scan_bus(0x1002, [(0xffff, [0x74a1, 0x75a0, 0x75b0])])
for gpu in gpus:
drv_path = f"/sys/bus/pci/devices/{gpu}/driver"
if FileIOInterface.exists(drv_path) and os.path.basename(os.readlink(drv_path)) == "amdgpu":
+1 -1
View File
@@ -35,7 +35,7 @@ def compile_net(linear:UOp, output_bufs:List[Buffer]) -> Tuple[Dict[str,str], Li
return name
for call in iter_kernel_calls(linear):
arg_uops = [b for b in call.src[1:] if not b.is_bound_var]
arg_uops = [b for b in call.src[1:] if b.op is not Ops.BIND]
prg = to_program(call.src[0], Device[arg_uops[0].device].renderer)
info = prg.arg
functions[info.function_name] = prg.src[2].arg
+1 -2
View File
@@ -122,8 +122,7 @@ def custom_mxfp4_gemm(C:UOp, A:UOp, B:UOp, scale_a:UOp, scale_b:UOp, *extra:UOp,
groups_x, groups_y = UOp.special(ceildiv(N, tile_n), "gidx0"), UOp.special(ceildiv(M, tile_m), "gidx1")
lds = UOp.placeholder((163840,), dtypes.uint8, 0, AddrSpace.LOCAL)
sink = UOp.sink(C.base, A.base, B.base, scale_a.base, scale_b.base, *(x.base for x in extra), lds, threads, groups_x, groups_y,
arg=KernelInfo(f"mxfp4_gemm_{M}_{N}_{K}",
estimates=Estimates(ops=2*M*N*K, mem=(M*half_k+N*half_k)*A.dtype.itemsize+M*N*C.dtype.itemsize)))
arg=KernelInfo(f"custom_mxfp4_gemm_{M}_{N}_{K}", estimates=Estimates(ops=2*M*N*K)))
insts = build_kernel(M, N, K, tile_m, tile_n)
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=x) for x in insts))))
+1 -1
View File
@@ -79,7 +79,7 @@ if __name__ == "__main__":
linear, var_vals = C.linear_with_vars()
last_call = linear.src[-1]
ast = last_call.src[0]
bufs = [s.buffer for s in last_call.src[1:] if not s.is_bound_var]
bufs = [s.buffer for s in last_call.src[1:] if s.op is not Ops.BIND]
src = compiled.asm["ptx"]
# specify the shared memory here so we don't need to do it dynamically
+7 -5
View File
@@ -3,7 +3,7 @@ from typing import cast, Any, Callable
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
assert sys.platform != 'win32'
from dataclasses import dataclass
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, encode_kernargs_clike, make_cmdbuf
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
from tinygrad.runtime.support.hcq2 import make_binary_patch
from tinygrad.uop.ops import sint, UOp
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
@@ -290,14 +290,14 @@ class AMDAllocator(HCQAllocator['AMDDevice']):
def __init__(self, dev:AMDDevice):
super().__init__(dev, supports_copy_from_disk=dev.has_copy_queue, supports_transfer=dev.has_copy_queue and not dev.is_usb())
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
def _alloc(self, size:int, options:BufferSpec) -> HCQ2Buffer:
return self.dev.iface.alloc(size, host=options.host, uncached=options.uncached, cpu_access=options.cpu_access or not self.dev.has_copy_queue)
def _do_free(self, opaque, options:BufferSpec): self.dev.iface.free(opaque)
def _do_map(self, buf:HCQBuffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
def _do_map(self, buf:HCQ2Buffer): return self.dev.iface.map(buf._base if buf._base is not None else buf)
def _do_unmap(self, buf:HCQBuffer): self.dev.iface.unmap(buf)
def _do_unmap(self, buf:HCQ2Buffer): self.dev.iface.unmap(buf)
@dataclass
class AMDQueueDesc:
@@ -561,7 +561,9 @@ class AMDDevice(HCQ2Compiled):
def is_usb(self) -> bool: return False
def __init__(self, device:str=""):
self.iface = self._select_iface(device)
self.device_id = int(device.split(":")[1]) if ":" in device else 0
self.iface = self._select_iface()
self.target:tuple[int, ...] = ((trgt:=self.iface.props['gfx_target_version']) // 10000, (trgt // 100) % 100, trgt % 100)
self.arch = "gfx%d%x%x" % self.target
+56 -38
View File
@@ -16,9 +16,8 @@ apt-get install -y python3-pip python3-venv git tmux rclone clang
### 1.2 Install Python deps
```bash
python3 -m pip install --break-system-packages --ignore-installed typing-extensions numpy tqdm wandb tiktoken sentencepiece
python3 -m pip install --break-system-packages numpy tqdm wandb tiktoken sentencepiece
```
Note: `--ignore-installed typing-extensions` is needed because the base image ships typing-extensions 4.10.0 without a RECORD file, so pip cannot uninstall it.
### 1.3 Install ROCm dev headers
The base image has ROCm runtime but NOT the HIP dev headers. Need:
@@ -75,8 +74,8 @@ rclone config create mlc-training s3 provider=Cloudflare \
secret_access_key=a53625c4d45e3ca8ac0df8a353ea3a41ffc3292aa25259addd8b7dc5a6ce2936 \
endpoint=c2686074cb2caf5cbaf6d134bdba8b47.r2.cloudflarestorage.com
mkdir -p /raid/datasets/c4-8b
rclone copy mlc-training:mlcommons-training-wg-public/llama3_1/datasets/c4/llama3_1_8b/ /raid/datasets/c4-8b/ -P
mkdir -p /root/datasets/c4-8b
rclone copy mlc-training:mlcommons-training-wg-public/llama3_1/datasets/c4/llama3_1_8b/ /root/datasets/c4-8b/ -P
```
Files downloaded (~85GB total, ~6 minutes):
@@ -86,9 +85,11 @@ Files downloaded (~85GB total, ~6 minutes):
- `c4-validation-91205-samples.en_text_document.idx` (1.8 MB)
- `LICENSE.txt`, `NOTICE.txt`
**Wait for rclone to fully complete before starting training.** Starting training while the dataset is still downloading will read a truncated .bin file, causing `ValueError: all input arrays must have the same shape` in the dataloader. The stale `.index_cache` and `.blend_cache` files must also be deleted if this happens:
### Symlink for the submission script
The `dev_run.sh` script hardcodes `BASEDIR="/raid/datasets/c4-8b/"`. Symlink:
```bash
rm -f /raid/datasets/c4-8b/*.index_cache /raid/datasets/c4-8b/*.blend_cache
mkdir -p /raid/datasets
ln -s /root/datasets/c4-8b /raid/datasets/c4-8b
```
## Phase 4: wandb Login
@@ -97,34 +98,30 @@ wandb login
```
Enter API key from https://wandb.ai/authorize
Alternatively, pass the key directly:
```bash
wandb login <API_KEY>
```
## Phase 5: Run Training
Run training in tmux so it survives SSH disconnects:
```bash
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
```
Attach with `tmux attach -t train`.
### 5.1 Smoke test (beam search, 2 layers, real data)
### 5.1 Smoke test (beam search, 2 layers, fake data)
Always run beam first to validate the pipeline:
```bash
tmux new-session -d -s beam 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh 2>&1 | tee /root/beam.log'
cd /root/tinygrad
COMGR_PATH=/opt/rocm/lib/libamd_comgr.so \
COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so \
CC=/opt/rocm/core-7.14/lib/llvm/bin/clang \
DEV=AMD:HIP \
ROCM_PATH=/opt/rocm BASEDIR=/root/datasets/c4-8b/ \
bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh
```
The beam test runs 10 training steps with 2 layers. Expected results:
- ~0.29s per step after warmup
- ~700K GFLOPS, ~7% MFU (low because only 2 layers)
- ~380 GB VRAM used
- Loss stable at ~12.55 with random init
### 5.2 Full training run
```bash
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
cd /root/tinygrad
COMGR_PATH=/opt/rocm/lib/libamd_comgr.so \
COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so \
CC=/opt/rocm/core-7.14/lib/llvm/bin/clang \
DEV=AMD:HIP \
ROCM_PATH=/opt/rocm BASEDIR=/root/datasets/c4-8b/ \
WANDB=1 \
bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh
```
## Environment Variable Reference
@@ -136,6 +133,7 @@ tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/liba
| `CC` | `/opt/rocm/core-7.14/lib/llvm/bin/clang` | System clang doesn't know gfx950; must use ROCm's bundled clang |
| `DEV` | `AMD:HIP` | Force HIPRenderer (comgr-based) over HIPCCRenderer (hipcc subprocess) |
| `ROCM_PATH` | `/opt/rocm` | Script defaults to `/opt/rocm-7.1.1` which doesn't exist |
| `BASEDIR` | `/root/datasets/c4-8b/` | Where C4 dataset was downloaded (script hardcodes `/raid/datasets/c4-8b/`) |
| `WANDB` | `1` | Enable wandb logging (off by default) |
## Architecture
@@ -175,6 +173,12 @@ ldconfig
### `comgr not available: try setting COMGR_3_PATH?`
comgr 3.x uses a separate module. Set `COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so` too.
### `FileNotFoundError: '/raid/datasets/c4-8b/...'`
Script hardcodes `BASEDIR`. Either symlink or edit the script:
```bash
mkdir -p /raid/datasets && ln -s /root/datasets/c4-8b /raid/datasets/c4-8b
```
### `No such file or directory: 'clang'`
Install clang: `apt-get install -y clang` (for CPU compilation).
For gfx950 HIP compilation, comgr (not clang) is used — ensure the ROCm 7.14 comgr 3.3 is properly loaded via `COMGR_PATH` and `COMGR_3_PATH`.
@@ -190,18 +194,13 @@ $ lspci -nn | grep AMD
```
CPU flags include `hypervisor`. `dmesg` shows `Hypervisor detected: KVM`.
### Working path: amdgpu driver (KFDIface)
The amdgpu driver loads on boot and binds to all 8 GPUs, creating `/dev/kfd` and 64 renderD nodes (`/dev/dri/renderD128` through `/dev/dri/renderD191`). tinygrad's `KFDIface` enumerates GPUs through `/sys/devices/virtual/kfd/kfd/topology/nodes` and uses `/dev/kfd` for ioctl. No PCI device ID patching is needed — the KFD path does not use `PCIIface` or `AMDev._run_discovery()`.
This is the working configuration. No code changes to tinygrad are required.
### PCIIface path (does not work on this VM)
For reference, the `PCIIface` path was also explored but does not work in this KVM guest:
- `PCIIface` in `ops_amd.py` does not list device ID `0x75b0`. Adding it allows PCI detection but `AMDev._run_discovery()` fails because the VRAM BAR reads all `0xFF`.
- This was observed with the GPU unbound from any driver, after PCI reset, and with VFIO bound.
- VFIO binding (`vfio-pci` with `enable_unsafe_noiommu_mode=1`) succeeded but VRAM BAR still reads all `0xFF`.
- No IOMMU in guest — `dmesg` has no `AMD-Vi` entries, PCI devices have no `iommu_group` symlink.
### PCI device ID
`lspci -v` shows device ID `0x75b0` and subsystem ID `0x75a0`:
```
83:00.0 Processing accelerators: ... Device 75b0
Subsystem: ... Device 75a0
```
tinygrad's `PCIIface` in `ops_amd.py` and `hive_reset.py` did not list `0x75b0`, so the GPU was not found. Adding `0x75b0` to the device ID list in both files fixes the detection.
### amdgpu driver behavior
On first boot, amdgpu loaded and bound to all 8 GPUs. On one boot it failed to initialize:
@@ -213,5 +212,24 @@ On first boot, amdgpu loaded and bound to all 8 GPUs. On one boot it failed to i
```
On a subsequent boot, amdgpu initialized successfully (SMU initialized, VRAM ready). After unbinding all 8 GPUs from amdgpu, `rmmod amdgpu` wedged the module (stuck in "Unloading" state in `/proc/modules`), requiring a full VM reboot.
### `/dev/kfd`
`/dev/kfd` exists when amdgpu is loaded. Opening it returns `OSError: [Errno 22] Invalid argument`.
### VRAM BAR reads all 0xFF
After amdgpu initializes the GPU and is then unbound, reading the VRAM BAR (via `/sys/bus/pci/devices/0000:83:00.0/resource0`) returns all `0xFF` at all offsets — including the discovery table at `vram_size - 64KB`. tinygrad's `AMDev._run_discovery()` fails with `AssertionError: discovery signatures mismatch`.
A PCI reset (`echo 1 > /sys/bus/pci/devices/0000:83:00.0/reset`) did not change the VRAM contents — still all `0xFF`.
VRAM was also all `0xFF` when read via `/dev/mem` at the BAR physical address (`0xa0000000000`).
### VFIO attempt
Bound the GPU to `vfio-pci` with `enable_unsafe_noiommu_mode=1`. The GPU bound successfully and `/dev/vfio/noiommu-0` appeared. Running tinygrad with `VFIO=1` still failed with the same `discovery signatures mismatch` — VRAM BAR still reads all `0xFF`.
### No IOMMU in guest
`dmesg` has no `AMD-Vi` entries. PCI devices have no `iommu_group` symlink.
### No fan control
No `fan*` or `pwm*` hwmon entries exist. Only `temp*`, `power*`, `freq*` are exposed. GPU temps read 56-63°C, power ~265W per GPU.
### Current status: NOT WORKING
tinygrad's `PCIIface` finds the GPU (after adding `0x75b0`) but `AMDev._run_discovery()` fails because the VRAM discovery table reads all `0xFF`. This was observed with the GPU unbound from any driver, after PCI reset, and with VFIO bound.
+9 -55
View File
@@ -110,49 +110,7 @@ def _sharded_empty_like(ref:Tensor, axis:int|None=None) -> Tensor:
return _sharded_empty(ref.shape, ref, axis)
@functools.cache
def _windowed_lse(xq:Tensor, xk:Tensor, sinks, W:int) -> Tensor:
B, N, H, hd = xq.shape
H_KV = xk.shape[2]; R = H // H_KV; nb = N // W; sm = hd ** -0.5
q = xq.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
k = xk.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
k_prev = k.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
sc_d = (q @ k.transpose(-1, -2)) * sm
sc_p = (q @ k_prev.transpose(-1, -2)) * sm
li, lj = Tensor.arange(W).reshape(W, 1), Tensor.arange(W).reshape(1, W)
pv = (Tensor.arange(nb).reshape(nb, 1, 1) >= 1)
sc_d = (lj <= li).where(sc_d, -float("inf"))
sc_p = ((li < lj) & pv).where(sc_p, -float("inf"))
m = sc_d.max(-1, keepdim=True).maximum(sc_p.max(-1, keepdim=True))
if sinks is not None: m = m.maximum(sinks.reshape(1, H_KV, R, 1, 1, 1).float())
denom = (sc_d - m).exp().sum(-1, keepdim=True) + (sc_p - m).exp().sum(-1, keepdim=True)
if sinks is not None: denom = denom + (sinks.reshape(1, H_KV, R, 1, 1, 1).float() - m).exp()
return (m + denom.log()).reshape(B, H, N).unsqueeze(2) # (B, H, 1, N), matches saved l_vec
def _windowed_delta(xq:Tensor, xk:Tensor, xv:Tensor, do:Tensor, sinks, W:int) -> Tensor:
B, N, H, hd = xq.shape
H_KV = xk.shape[2]; R = H // H_KV; nb = N // W; sm = hd ** -0.5
q = xq.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
k = xk.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
v = xv.permute(0, 2, 1, 3).reshape(B, H_KV, 1, nb, W, hd).float()
dob = do.reshape(B, N, H_KV, R, hd).permute(0, 2, 3, 1, 4).reshape(B, H_KV, R, nb, W, hd).float()
k_prev = k.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
v_prev = v.pad((None, None, None, (1, 0), None, None))[:, :, :, :nb]
sc_d = (q @ k.transpose(-1, -2)) * sm
sc_p = (q @ k_prev.transpose(-1, -2)) * sm
li, lj = Tensor.arange(W).reshape(W, 1), Tensor.arange(W).reshape(1, W)
pv = (Tensor.arange(nb).reshape(nb, 1, 1) >= 1)
sc_d = (lj <= li).where(sc_d, -float("inf"))
sc_p = ((li < lj) & pv).where(sc_p, -float("inf"))
m = sc_d.max(-1, keepdim=True).maximum(sc_p.max(-1, keepdim=True))
if sinks is not None: m = m.maximum(sinks.reshape(1, H_KV, R, 1, 1, 1).float())
e_d, e_p = (sc_d - m).exp(), (sc_p - m).exp()
denom = e_d.sum(-1, keepdim=True) + e_p.sum(-1, keepdim=True)
if sinks is not None: denom = denom + (sinks.reshape(1, H_KV, R, 1, 1, 1).float() - m).exp()
o = ((e_d / denom) @ v) + ((e_p / denom) @ v_prev)
delta = (dob * o).sum(-1)
return delta.reshape(B, H, N).unsqueeze(2)
def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, shard_axis_t, single_device, arch, has_sink, window=0):
def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, shard_axis_t, single_device, arch, has_sink):
def grad(dou:UOp, ker:UOp) -> tuple:
do = Tensor(dou, device=dou.device)
attn = Tensor(ker.src[1].after(ker), device=ker.src[1].device)
@@ -160,8 +118,6 @@ def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, sha
xq = Tensor(ker.src[3], device=ker.src[3].device)
xk = Tensor(ker.src[4], device=ker.src[4].device)
xv = Tensor(ker.src[5], device=ker.src[5].device)
if window:
l_vec = _windowed_lse(xq, xk, Tensor(ker.src[6], device=ker.src[6].device) if has_sink else None, window)
dq = _sharded_empty((B, H, N, D), xq, axis=shard_axis_t)
GROUP_SIZE = H_local // H_KV_local
@@ -172,10 +128,8 @@ def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, sha
# delta_vec = (do * attn).sum(-1, dtype=dtypes.float32).transpose(1, 2).unsqueeze(-2).detach()
delta_vec = _sharded_empty((B, H, 1, N), xq, dtype=dtypes.float32, axis=shard_axis_t)
delta_vec, dq = Tensor.custom_kernel(delta_vec, dq, attn, do, fxn=functools.partial(custom_fa_backward_pre, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D))[:2]
if window:
delta_vec = _windowed_delta(xq, xk, xv, do, Tensor(ker.src[6], device=ker.src[6].device) if has_sink else None, window)
dq, dk_partial, dv_partial = Tensor.custom_kernel(dq, dk_partial, dv_partial, do, xq, xk, xv, l_vec, delta_vec, fxn=functools.partial(custom_fa_backward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D, window=window))[:3]
dq, dk_partial, dv_partial = Tensor.custom_kernel(dq, dk_partial, dv_partial, do, xq, xk, xv, l_vec, delta_vec, fxn=functools.partial(custom_fa_backward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D))[:3]
if D == 64:
dq = dq.reshape(B, H, N//16, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2).permute(0, 1, 2, 8, 9, 10, 11, 3, 4, 6, 7, 5, 12).reshape(B, H, N, D).transpose(1, 2)
@@ -195,7 +149,7 @@ def _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, sha
return grad
# TODO: remove write_flat once scheduler can remove reshapes between custom_kernel. TestCustomKernel.test_simple_reshape
def flash_attention(xq, xk, xv, attn_mask:Tensor|None=None, is_causal:bool=False, write_flat:bool=False, sinks:Tensor|None=None, window:int=0):
def flash_attention(xq, xk, xv, attn_mask:Tensor|None=None, is_causal:bool=False, write_flat:bool=False, sinks:Tensor|None=None):
assert attn_mask is None, "attn_mask not supported"
assert is_causal, "only causal attention supported"
@@ -222,18 +176,18 @@ def flash_attention(xq, xk, xv, attn_mask:Tensor|None=None, is_causal:bool=False
attn = _sharded_empty((B, N, H * D), xq, axis=shard_axis) if write_flat else _sharded_empty_like(xq, axis=shard_axis)
l_vec = _sharded_empty((B, H, 1, N), xq, dtype=dtypes.float32, axis=shard_axis_t)
grad = _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, shard_axis_t, single_device, arch, has_sink, window=window)
grad = _fa_grad_fxn(B, H, N, D, H_local, H_KV_local, H_KV, B_local, shard_axis, shard_axis_t, single_device, arch, has_sink)
fwd_inputs = (attn, l_vec, xq, xk, xv) + ((sinks,) if has_sink else ())
attn, l_vec = Tensor.custom_kernel(*fwd_inputs, fxn=functools.partial(custom_fa_forward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D, has_sink=has_sink, window=window), grad_fxn=grad)[:2]
attn, l_vec = Tensor.custom_kernel(*fwd_inputs, fxn=functools.partial(custom_fa_forward, device=single_device, arch=arch, B=B_local, N=N, H=H_local, H_KV=H_KV_local, D=D, has_sink=has_sink), grad_fxn=grad)[:2]
return attn, attn, l_vec
@functools.cache
def custom_fa_forward(o:UOp, l_vec:UOp, q:UOp, k:UOp, v:UOp, sinks:UOp|None=None, *, device:str, arch:str, B:int, N:int, H:int, H_KV:int, D:int, has_sink:bool=True, window:int=0):
def custom_fa_forward(o:UOp, l_vec:UOp, q:UOp, k:UOp, v:UOp, sinks:UOp|None=None, *, device:str, arch:str, B:int, N:int, H:int, H_KV:int, D:int, has_sink:bool=True):
code = (pathlib.Path(__file__).parent / "fa_fwd_causal.cpp").read_text()
compile_args = [f"-I{(pathlib.Path(__file__).parent / 'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-DHIP_ENABLE_WARP_SYNC_BUILTINS", "-ffast-math",
f"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}", f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}", f"-DATTN_SINK={int(has_sink)}", f"-DWINDOW={window}"]
f"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}", f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}", f"-DATTN_SINK={int(has_sink)}"]
Q_BLOCK_SIZE = 32
NUM_WARPS = 8
@@ -293,10 +247,10 @@ def custom_fa_backward_pre(delta_vec:UOp, dq:UOp, o:UOp, do:UOp, device:str, arc
src=(sink, UOp(Ops.LINEAR, src=(*sink.src, sink)), UOp(Ops.SOURCE, arg=code), UOp(Ops.BINARY, arg=lib)))
@functools.cache
def custom_fa_backward(dq:UOp, dk:UOp, dv:UOp, do:UOp, q:UOp, k:UOp, v:UOp, l_vec:UOp, delta_vec:UOp, device:str, arch:str, B:int, N:int, H:int, H_KV:int, D:int, window:int=0):
def custom_fa_backward(dq:UOp, dk:UOp, dv:UOp, do:UOp, q:UOp, k:UOp, v:UOp, l_vec:UOp, delta_vec:UOp, device:str, arch:str, B:int, N:int, H:int, H_KV:int, D:int):
code = (pathlib.Path(__file__).parent / "fa_bwd_causal.cpp").read_text()
compile_args = [f"-I{(pathlib.Path(__file__).parent / 'include').as_posix()}", "-std=c++20", "-DKITTENS_CDNA4", "-DHIP_ENABLE_WARP_SYNC_BUILTINS", "-ffast-math",
f"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}", f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}", f"-DWINDOW={window}"]
f"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}", f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}"]
BLOCK_SIZE_KV = 256
GROUP_SIZE = H // H_KV
+1 -1
View File
@@ -209,7 +209,7 @@ class ST:
return cls(uop, rows, cols, layout, base_shape, ker)
def swizzle(self, row, col):
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype)
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype.scalar())
row = swizzled_offset // self.base_shape.cols
col = swizzled_offset % self.base_shape.cols
+125 -87
View File
@@ -4,7 +4,7 @@
# A006 Lambda argument `input` is shadowing a Python builtin
from tinygrad import Tensor, dtypes, Device
from tinygrad.uop.ops import Ops, GroupOp
from tinygrad.helpers import getenv, prod, strides_for_shape
from tinygrad.helpers import getenv, prod, strides_for_shape, argfix
import torch.lib
TORCH_DEBUG = getenv("TORCH_DEBUG")
import torch, pathlib, operator, functools, weakref
@@ -73,12 +73,6 @@ def wrap_view_op(fn):
return wrap(ret)
return _wrap
# NOTE: list assignment raises IndexError on an out of range dim, and the index must be a tuple: a list of all ints is one advanced index
def _index_dim(self, dim, idx):
idxs = [slice(None)] * self.ndim
idxs[dim] = idx
return self[tuple(idxs)]
view_ops = {
"aten.view": Tensor.reshape,
"aten._unsafe_view": Tensor.reshape, # when are views unsafe, and do we care?
@@ -88,13 +82,15 @@ view_ops = {
"aten.transpose.int": Tensor.transpose,
"aten.squeeze.dim": Tensor.squeeze,
"aten.unsqueeze": Tensor.unsqueeze,
"aten.select.int": _index_dim,
"aten.select.int": lambda self, dim, idx: self[(slice(None),) * (dim%self.ndim) + (idx,)],
"aten.permute": Tensor.permute,
"aten.alias": lambda self: self,
"aten.diagonal": Tensor.diagonal,
"aten.slice.Tensor": lambda self, dim=0, start=None, end=None, step=1: _index_dim(self, dim, slice(start, end, step)),
}
# torch 2.10 handles this natively
if tuple(map(int, torch.__version__.split('.')[:2])) < (2, 10): view_ops.update({"aten.detach": Tensor.detach})
for k,v in view_ops.items(): torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(wrap_view_op(v))
def _get_view_ops(view): return getattr(view, "_view_ops", [])
@@ -103,21 +99,46 @@ def _apply_view_ops(target, ops):
for fn, args, kwargs in ops: target = fn(target, *args, **kwargs)
return target
# a chain of reshapes is undone by reshaping the value back to the base
# similar to https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/InferSize.h
def _reshape_target_shape(shape:tuple[int, ...], args) -> tuple[int, ...]|None:
if not (req := argfix(*args)): return None
new_shape, infer_idx = [], -1
for i, s in enumerate(req):
if s is None: s = shape[i] if i < len(shape) else None
if not isinstance(s, int): return None
if s == -1:
if infer_idx != -1: return None
infer_idx = len(new_shape)
new_shape.append(s)
total = prod(shape)
if infer_idx != -1:
known = prod(x for x in new_shape if x != -1)
if known == 0:
if total != 0: return None
new_shape[infer_idx] = 0
else: new_shape[infer_idx] = total // known
return tuple(new_shape) if prod(new_shape) == total else None
# TODO: can we get rid of this? only for test_flatten_reshape_add
def _try_simple_reshape_view_write(base: Tensor, view: Tensor, val: Tensor) -> bool:
if not (ops := _get_view_ops(view)): return False
if any(fn is not Tensor.reshape for fn, _, _ in ops): return False
base.assign(val.reshape(base.shape))
shapes = [base.shape]
for fn, args, _ in ops:
if fn is Tensor.reshape:
if not (next_shape := _reshape_target_shape(shapes[-1], args)): return False
shapes.append(next_shape)
if shapes[-1] != view.shape: return False
for s in reversed(shapes[:-1]): val = val.reshape(s)
base.assign(val)
return True
def _view_write(base: Tensor, view: Tensor, value: Tensor) -> None:
val = value if value.dtype == base.dtype else value.cast(base.dtype)
if view.shape == base.shape: return base.assign(val)
if _try_simple_reshape_view_write(base, view, val): return
idx_base = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape)
idx_view = _apply_view_ops(idx_base, _get_view_ops(view)).reshape(-1)
# clone, not contiguous: contiguous() on a base that already owns its buffer returns the base itself, and scattering
# into that is an in-place write to a buffer other tensors still hold, which setitem refuses
flat_base = base.reshape(base.numel()).clone()
flat_base = base.reshape(base.numel()).contiguous()
flat_base[idx_view] = val.reshape(-1)
base.assign(flat_base.reshape(base.shape))
@@ -145,6 +166,11 @@ def _index_put_impl_(self, indices, values, accumulate=False, unsafe=False):
def index_put(self, indices, values, accumulate=False):
return aten.index_put(self.cpu(), [z.cpu() if isinstance(z, torch.Tensor) else None for z in indices], values.clone().cpu(), accumulate).tiny()
@torch.library.impl("aten::isin.Tensor_Tensor_out", "privateuseone")
def isin_tensor_tensor_out(x, y, *, assume_unique=False, invert=False, out=None):
result = (unwrap(x).unsqueeze(-1) == unwrap(y).flatten()).any(-1)
return out.copy_(wrap(~result if invert else result))
@torch.library.impl("aten::randperm.generator_out", "privateuseone")
def randperm_generator(n, generator=None, out=None):
if generator is not None: raise NotImplementedError("tinygrad torch backend does not support torch.Generator for randperm")
@@ -205,6 +231,49 @@ def as_strided(tensor:torch.Tensor, size, stride, storage_offset=None):
def _reshape_alias(tensor:torch.Tensor, size, stride):
return _as_strided(tensor, size, stride)
@torch.library.impl("aten::empty_strided", "privateuseone")
def empty_strided(size, stride, dtype=None, layout=None, device=None, pin_memory=False):
if TORCH_DEBUG: print(f"empty_strided {size=} {stride=} {dtype=} {layout=} {device=} {pin_memory=}")
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
# TODO: should return with requested strides
return wrap(ret)
@torch.library.impl("aten::empty.memory_format", "privateuseone")
def empty_memory_format(size, dtype=None, layout=None, device=None, pin_memory=False, memory_format=None):
if TORCH_DEBUG: print(f"empty.memory_format {size=} {dtype=} {layout=} {device=} {pin_memory=} {memory_format=}")
ret = Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
return wrap(ret)
@torch.library.impl("aten::max_pool2d_with_indices", "privateuseone")
def max_pool2d_with_indices(self:torch.Tensor, kernel_size:tuple[int, ...], stride=None, padding=0, dilation=1, ceil_mode=False):
# TODO: supprt stride [] in tinygrad?
if stride is not None and len(stride) == 0: stride = None
ret, idx = unwrap(self).max_pool2d(kernel_size, stride, dilation, padding, ceil_mode, return_indices=True)
return (wrap(ret), wrap(idx.cast(dtypes.int64)))
@torch.library.impl("aten::max_pool2d_with_indices_backward", "privateuseone")
def max_pool2d_with_indices_backward(grad_out:torch.Tensor, self:torch.Tensor, kernel_size:tuple[int, ...], stride=None, padding=0, dilation=1, ceil_mode=False, indices=None):
return wrap(Tensor.max_unpool2d(unwrap(grad_out), unwrap(indices), output_size=unwrap(self).shape))
@torch.library.impl("aten::max_unpool2d", "privateuseone")
def max_unpool2d(self:torch.Tensor, indices:torch.Tensor, output_size):
return wrap(unwrap(self).max_unpool2d(unwrap(indices), output_size=output_size))
@torch.library.impl("aten::arange", "privateuseone")
def arange(end, dtype=None, device=None, pin_memory=None):
has_float = isinstance(end, float)
return wrap(Tensor.arange(0, end, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
@torch.library.impl("aten::arange.start", "privateuseone")
def arange_start(start, end, dtype=None, device=None, pin_memory=None):
has_float = any(isinstance(x, float) for x in (start, end))
return wrap(Tensor.arange(start, end, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
@torch.library.impl("aten::arange.start_step", "privateuseone")
def arange_start_step(start, end, step, dtype=None, device=None, pin_memory=None):
has_float = any(isinstance(x, float) for x in (start, end, step))
return wrap(Tensor.arange(start, end, step, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if has_float else torch.int64))))
@torch.library.impl("aten::convolution_overrideable", "privateuseone")
def convolution_overrideable(input, weight, bias, stride, padding, dilation, transposed, output_padding, groups):
if TORCH_DEBUG >= 1:
@@ -225,27 +294,12 @@ def convolution_backward_overrideable(grad_out, input, weight, stride, padding,
grads = out.gradient(*[t for t,m in zip([input, weight, bias], output_mask) if m], gradient=grad_out)
return tuple([wrap(grads.pop(0)) if m else None for m in output_mask])
# the functional scatters. without an impl aten falls back to a path that assumes a real storage: "self.has_storage() INTERNAL ASSERT FAILED"
def _scatter_into(self, src, dim, index):
out = unwrap(self).clone()
slices = [slice(None)] * out.ndim
slices[dim] = index
out[slices] = unwrap(src).cast(out.dtype) # torch casts src to self's dtype, tinygrad setitem demands they already match
return wrap(out)
@torch.library.impl("aten::slice_scatter", "privateuseone")
def slice_scatter(self, src, dim=0, start=None, end=None, step=1): return _scatter_into(self, src, dim, slice(start, end, step))
@torch.library.impl("aten::select_scatter", "privateuseone")
def select_scatter(self, src, dim, index): return _scatter_into(self, src, dim, index)
@torch.library.impl("aten::diagonal_scatter", "privateuseone")
def diagonal_scatter(self, src, offset=0, dim1=0, dim2=1):
# a diagonal is not one axis, so scatter through the flat indices it picks out
base, out = unwrap(self), unwrap(self).clone().reshape(-1)
idx = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape).diagonal(offset, dim1, dim2).reshape(-1)
out[idx] = unwrap(src).cast(base.dtype).reshape(-1)
return wrap(out.reshape(base.shape))
@torch.library.impl("aten::slice.Tensor", "privateuseone")
@wrap_view_op
def slice_tensor(self, dim=0, start=None, end=None, step=1):
slices = [slice(None)] * self.ndim
slices[dim] = slice(start, end, step)
return self[slices]
@torch.library.impl("aten::slice_backward", "privateuseone")
def slice_backward(grad_out, input_sizes, dim, start, end, step):
@@ -287,14 +341,19 @@ for dim in [1, 2, 3]:
torch.library.impl(f"aten::{pad_type}_pad{dim}d", "privateuseone")(functools.partial(pad_forward, mode=mode))
torch.library.impl(f"aten::{pad_type}_pad{dim}d_backward", "privateuseone")(functools.partial(pad_backward, mode=mode))
# the schemas are all positional: (self, output_size, align_corners, *scales) for linear, (self, output_size, *scales) for nearest.
def upsample(self, size, *args, mode=None):
return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=args[0] if mode == "linear" else False))
def upsample(self, size, align_corners=False, mode=None): return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=align_corners))
for i,pre in enumerate(["", "bi", "tri"]):
torch.library.impl(f"aten::upsample_{pre}linear{i+1}d", "privateuseone")(functools.partial(upsample, mode="linear"))
torch.library.impl(f"aten::upsample_nearest{i+1}d", "privateuseone")(functools.partial(upsample, mode="nearest"))
torch.library.impl(f"aten::_upsample_nearest_exact{i+1}d", "privateuseone")(functools.partial(upsample, mode="nearest-exact"))
@torch.library.impl("aten::scatter_add.out", "privateuseone")
def scatter_add(self, dim, index, src, out):
self, index, src, out_unwrapped = unwrap(self), unwrap(index), unwrap(src), unwrap(out)
if self.shape == (): _apply_inplace(out_unwrapped, src)
else: _apply_inplace(out_unwrapped, Tensor.scatter_reduce(self, dim, index, src, reduce='sum'))
return out
def _copy_between_devices(src, dest, cast_dtype, to_device, non_blocking=False):
if src.is_tiny and dest.is_tiny:
src_t, dest_t = unwrap(src), unwrap(dest)
@@ -345,11 +404,15 @@ def sort_values(input, dim=-1, descending=False, stable=True, values=None, indic
_apply_inplace(unwrap(indices), out_indices.cast(dtypes.int64))
return values, indices
@torch.library.impl("aten::_linalg_svd", "privateuseone")
def _linalg_svd(self, full_matrices=False):
U, S, Vh = unwrap(self).svd(full_matrices)
return wrap(U), wrap(S), wrap(Vh)
# register some decompositions
from torch._decomp import get_decompositions
decomps = [
aten.native_layer_norm_backward,
aten.native_group_norm_backward,
aten.linalg_cross,
aten.addmm,
aten.addcmul,
@@ -384,20 +447,12 @@ decomps = [
aten._softmax_backward_data, aten.embedding_dense_backward,
aten.linalg_vector_norm,
aten.binary_cross_entropy, aten.binary_cross_entropy_backward,
# the C++ mse/smooth_l1 kernels resize their out tensor, and a tiny tensor has no storage to resize
aten.mse_loss, aten.mse_loss_backward,
aten.smooth_l1_loss, aten.smooth_l1_loss_backward,
aten.upsample_nearest2d.out,
# NOTE: only the "out" overload, the "vec" one is CompositeImplicitAutograd and overriding it loses the autograd kernel
aten.upsample_bicubic2d.out,
aten._adaptive_avg_pool2d,
# activations
aten.hardswish, aten.hardswish_backward,
aten.hardtanh, aten.hardtanh_backward,
aten.gelu, aten.gelu_backward,
# NOTE: no aten.logical_or here, its decomposition reaches aten.bitwise_or through a path that checks aliasing by
# reading storage, which a tiny tensor has none of. it gets a direct impl below instead
aten.logical_and, aten.logical_xor,
aten.logical_and,
aten.randint,
aten.eye,
aten.hardsigmoid_backward,
@@ -440,7 +495,7 @@ simple_tensor_methods = [
# reduce
"all", "any", "argmax", "argmin", "cumsum", "cumprod",
# complex
"linspace"]
"avg_pool2d", "linspace"]
tiny_backend_out = {**{f"aten.{x}.out":getattr(Tensor,x) for x in simple_tensor_methods}, **{
"aten.add.out": lambda input,other,alpha=1: input+alpha*other,
@@ -485,8 +540,6 @@ tiny_backend_out = {**{f"aten.{x}.out":getattr(Tensor,x) for x in simple_tensor_
"aten.where.self_out": Tensor.where,
"aten.prod.int_out": Tensor.prod,
"aten.scatter.src_out": Tensor.scatter,
"aten.scatter_add.out": lambda self,dim,index,src: src if self.shape == () else Tensor.scatter_reduce(self, dim, index, src, reduce="sum"),
"aten.isin.Tensor_Tensor_out": lambda x,y,assume_unique=False,invert=False: (x.unsqueeze(-1)==y.flatten()).any(-1) != invert,
# NOTE: axis=[] in torch means all, change tinygrad?
"aten.sum.IntList_out": lambda self,axis,keepdim=False,dtype=None:
self.sum(axis if axis is None or len(axis) else None, keepdim,
@@ -502,9 +555,10 @@ def wrap_out(f):
assert out.shape == assigned.shape, f"shape mismatch: {assigned.shape} -> {out.shape}"
assert out.device == assigned.device or out.device is None or assigned.device is None, f"device mismatch: {assigned.device} -> {out.device}"
assert out.dtype == assigned.dtype, f"dtype mismatch: {assigned.dtype} -> {out.dtype}"
# writing out= is an in-place write like any other: through the base if it is a view, refreshing any derived views
_apply_inplace(out, assigned)
return out
# an out= that is a view has to be written through its base, and _apply_inplace gives a deviceless base its buffer first
if canonical_base(out) is not out: return _apply_inplace(out, assigned) or out
if out.device is None and assigned.device is not None: out.replace(out.empty_like(device=assigned.device))
return out.assign(assigned)
return _wrap_out
def _inplace_op(t, new_value):
@@ -512,14 +566,7 @@ def _inplace_op(t, new_value):
else: _apply_inplace(t, new_value)
return t
# the three arange overloads are one function at different arity, and dtype/layout/device/pin_memory are keyword only in all of them
def _arange(*args, dtype=None, **_):
return Tensor.arange(*args, dtype=_from_torch_dtype(dtype or (torch.get_default_dtype() if any(isinstance(x, float) for x in args) else torch.int64)))
def _empty(size, dtype=None, device=None, **_):
return Tensor.empty(*size, dtype=_from_torch_dtype(dtype or torch.get_default_dtype()), device=_from_torch_device(device))
tiny_backend = {**tiny_backend_out, **{
tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
"aten.remainder.Scalar_Tensor": lambda x,y: x%y,
"aten.floor_divide": lambda x,y: x//y,
"aten.floor_divide_.Tensor": lambda x,y: x//y,
@@ -532,8 +579,8 @@ tiny_backend = {**tiny_backend_out, **{
# inplace ops using replace for fusion
"aten.zero_": lambda x: x.const_like(0),
"aten.fill_.Scalar": lambda x, y: x.const_like(y),
"aten.add_.Tensor": lambda self, other, alpha=1: self + other * alpha,
"aten.add_.Scalar": lambda self, other, alpha=1: self + other * alpha,
"aten.add_.Tensor": lambda self, other, alpha=1.0: self + other * alpha,
"aten.add_.Scalar": lambda self, other, alpha=1.0: self + other * alpha,
"aten.mul_.Tensor": lambda self, other: self * other,
"aten.mul_.Scalar": lambda self, other: self * other,
# relu doesn't have an out form?
@@ -566,9 +613,7 @@ tiny_backend = {**tiny_backend_out, **{
# these don't work in out form, they have size 0
"aten.abs": Tensor.abs,
"aten.logical_not": Tensor.logical_not,
# compare against zero first: logical_* is bool-valued for any input dtype, while | is bitwise
"aten.logical_or": lambda x, y: (x != 0) | (y != 0),
"aten.logical_or_": lambda x, y: (x != 0) | (y != 0),
"aten.logical_or_": lambda x, y: x | y,
"aten.multinomial": Tensor.multinomial,
"aten.masked_fill_.Scalar": lambda self, mask, value: self.masked_fill(mask, value),
"aten.masked_fill_.Tensor": lambda self, mask, value: self.masked_fill(mask, value),
@@ -577,7 +622,14 @@ tiny_backend = {**tiny_backend_out, **{
"aten.masked_select": Tensor.masked_select,
"aten.all": Tensor.all,
"aten.sgn": Tensor.sign,
"aten.acos": Tensor.acos,
"aten.any": Tensor.any,
"aten.bitwise_not": Tensor.bitwise_not,
"aten.argmax": Tensor.argmax,
"aten.argmin": Tensor.argmin,
"aten.asinh": Tensor.asinh,
"aten.mul": Tensor.mul,
"aten.atanh": Tensor.atanh,
"aten.fill_.Tensor": lambda self, value: self.const_like(value.reshape(()).item()),
"aten.flip": Tensor.flip,
"aten.scatter_reduce.two": Tensor.scatter_reduce,
@@ -588,22 +640,10 @@ tiny_backend = {**tiny_backend_out, **{
"aten.add.Tensor": lambda input,other,alpha=1: input+alpha*other,
"aten.linspace": lambda start, stop, steps, dtype=None, **kwargs:
Tensor.linspace(start, stop, steps, **({"dtype": _from_torch_dtype(dtype)} if dtype is not None else {})),
# the functional copy_. without an impl the fallback segfaults on a tensor with no storage
"aten.copy": lambda self,src,non_blocking=False: src.cast(self.dtype).to(self.device).expand(self.shape),
"aten.arange": lambda end, **kwargs: _arange(0, end, **kwargs),
"aten.arange.start": _arange,
"aten.arange.start_step": _arange,
# empty_strided takes the strides and drops them: we always allocate contiguous
"aten.empty_strided": lambda size, stride, **kwargs: _empty(size, **kwargs),
"aten.empty.memory_format": _empty,
# TODO: supprt stride [] in tinygrad?
"aten.max_pool2d_with_indices": lambda self,kernel_size,stride=None,padding=0,dilation=1,ceil_mode=False: ((r:=Tensor.max_pool2d(self, kernel_size, stride or None, dilation, padding, ceil_mode, return_indices=True))[0], r[1].cast(dtypes.int64)),
"aten.max_pool2d_with_indices_backward": lambda grad_out,self,kernel_size,stride=None,padding=0,dilation=1,ceil_mode=False,indices=None: Tensor.max_unpool2d(grad_out, indices, output_size=self.shape),
"aten.max_unpool2d": lambda self,indices,output_size: Tensor.max_unpool2d(self, indices, output_size=output_size),
"aten._linalg_svd": lambda self,full_matrices=False: Tensor.svd(self, full_matrices),
"aten.topk": Tensor.topk,
"aten.constant_pad_nd": lambda self, padding, value=0.0: self.pad(padding, mode="constant", value=value).contiguous(),
"aten.cumsum": lambda self, dim: self.cumsum(dim),
# TODO: input contiguous is needed to prevent CFGContext circular dependency assertion for shapes >512 (see test_cumsum_arange_large)
"aten.cumsum": lambda self, dim: self.contiguous().cumsum(dim),
"aten.logsumexp": lambda self, axis, keepdim=False: self.logsumexp(axis[0], keepdim=keepdim),
"aten.roll": Tensor.roll,
"aten.logcumsumexp": Tensor.logcumsumexp,
@@ -612,7 +652,6 @@ tiny_backend = {**tiny_backend_out, **{
self.ones_like(**{k: v for k, v in {"dtype": _from_torch_dtype(dtype) if dtype else None,
"device": _from_torch_device(device) if device else None}.items() if v is not None}),
"aten.max.dim": lambda self, dim, keepdim=False: (self.max(dim, keepdim), self.argmax(dim, keepdim).cast(dtype=dtypes.int64)),
"aten.min.dim": lambda self, dim, keepdim=False: (self.min(dim, keepdim), self.argmin(dim, keepdim).cast(dtype=dtypes.int64)),
"aten.cummax": lambda self, dim: ((r := self.cummax(dim))[0], r[1].cast(dtypes.int64)),
"aten.cummin": lambda self, dim: ((r := self.cummin(dim))[0], r[1].cast(dtypes.int64)),
"aten.nonzero": Tensor.nonzero,
@@ -674,16 +713,15 @@ def wrap_inplace_view_op(f):
return nf
# the aten schema says how an op is called: an inplace view retargets the view, a writable first arg is inplace,
# and a writable out arg gets wrap_out's dtype cast, shape assert, and view write-through
# and a writable out arg must have come from tiny_backend_out so that wrap_out was applied
for k,v in tiny_backend.items():
name, _, overload = k.removeprefix("aten.").partition(".")
op = getattr(getattr(aten, name), overload or "default")
writes = [a.name for a in op._schema.arguments if a.alias_info is not None and a.alias_info.is_write]
if torch.Tag.inplace_view in op.tags: fxn = wrap_inplace_view_op(v)
elif writes == [op._schema.arguments[0].name] and op._schema.returns: fxn = wrap_inplace(v)
elif not writes: fxn = wrap_fxn(k, v)
elif writes == ["out"]: fxn = wrap_fxn(k, wrap_out(v))
else: raise RuntimeError(f"{k} writes {writes}: unhandled writable arg in schema")
elif not writes or (writes == ["out"] and k in tiny_backend_out): fxn = wrap_fxn(k, v)
else: raise RuntimeError(f"{k} writes {writes}: expected an inplace first arg, or an out arg with {k} in tiny_backend_out")
torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(fxn)
@torch.library.impl("aten::equal", "privateuseone")
-120
View File
@@ -83,12 +83,6 @@ class TestTorchBackend(unittest.TestCase):
torch.add(torch.ones(5, device=device), torch.ones(5, device=device), out=a)
self.assertEqual(a.detach().storage_offset(), 3)
def test_out_refreshes_views_of_base(self):
a = torch.zeros(4, device=device)
v = a[2:]
torch.add(torch.ones(4, device=device), torch.ones(4, device=device), out=a)
np.testing.assert_equal(v.cpu().numpy(), [2., 2.])
@unittest.expectedFailure # TODO: storage offset assumes a contiguous source, use UOp.contiguous_view_offset
def test_storage_offset_non_contiguous_source(self):
a = torch.arange(12., device=device).reshape(3,4)
@@ -172,15 +166,6 @@ class TestTorchBackend(unittest.TestCase):
expected = np.array([[1.5, 5.2, 9.0], [13.2, 17.1, 18.4]], dtype=np.float32)
np.testing.assert_equal(y3.cpu().numpy(), expected)
def test_argmax_argmin(self):
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
c = a.cpu()
for got, want in [(a.argmax(), c.argmax()), (a.argmin(0), c.argmin(0)), (a.argmax(1, keepdim=True), c.argmax(1, keepdim=True)),
(torch.min(a, 1).indices, torch.min(c, 1).indices), (torch.max(a, 1).indices, torch.max(c, 1).indices),
(torch.min(a, 1).values, torch.min(c, 1).values), (torch.min(a, 1, keepdim=True).indices, torch.min(c, 1, keepdim=True).indices)]:
self.assertEqual(got.dtype, want.dtype) # torch's arg reduces are int64, tinygrad's are int32
np.testing.assert_equal(got.cpu().numpy(), want.numpy())
def test_isfinite(self):
a = torch.ones(4, device=device)
np.testing.assert_equal(torch.isfinite(a).cpu().numpy(), [True, True, True, True])
@@ -388,22 +373,6 @@ class TestTorchBackend(unittest.TestCase):
for bwd_eps in [1e-5, 0.3]:
for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
def test_groupnorm_backward(self):
def run(dev):
x = torch.arange(24., device=dev).reshape(2, 4, 3).requires_grad_()
w = torch.linspace(0.5, 2.0, 4).to(dev).requires_grad_()
torch.nn.functional.group_norm(x, 2, w, torch.zeros(4, device=dev)).square().sum().backward()
return x.grad.cpu().numpy(), w.grad.cpu().numpy()
for got, want in zip(run(device), run("cpu")): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
def test_mse_smooth_l1_loss_backward(self):
def run(dev, loss):
x = torch.arange(4., device=dev).requires_grad_()
loss(x, torch.ones(4, device=dev)).backward()
return x.grad.cpu().numpy()
for loss in [torch.nn.functional.mse_loss, torch.nn.functional.smooth_l1_loss]:
np.testing.assert_allclose(run(device, loss), run("cpu", loss), atol=1e-6)
def test_batchnorm_unsqueeze(self):
bn = torch.nn.BatchNorm2d(4).to(device)
x = torch.randn(8, 4, 3, 3, device=device)
@@ -547,15 +516,6 @@ class TestTorchBackend(unittest.TestCase):
cpu_res = torch.arange(20, dtype=torch.float32)[::2][1:4].numpy()
np.testing.assert_equal(torch_res, cpu_res)
def test_select_out_of_range_dim(self):
a = torch.arange(12, dtype=torch.int32, device=device).reshape(3, 4)
with self.assertRaises(IndexError): a.select(5, 0)
def test_select_collapses_the_only_dim(self):
a = torch.arange(3, dtype=torch.int32, device=device)
self.assertEqual(a.select(0, 1).shape, ())
np.testing.assert_equal(a.select(0, 1).cpu().numpy(), 1)
def test_slice_negative_dim(self):
a = torch.arange(13, dtype=torch.int32, device=device).repeat(8, 1)
torch_chunks = a.chunk(3, -1)
@@ -836,86 +796,6 @@ class TestTorchBackend(unittest.TestCase):
np.testing.assert_allclose(w_tiny.grad.cpu().numpy(), w_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
np.testing.assert_allclose(b_tiny.grad.cpu().numpy(), b_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
def test_write_through_detach_of_unrealized(self):
a = torch.empty(4, device=device)
a.detach().fill_(3)
np.testing.assert_equal(a.cpu().numpy(), [3, 3, 3, 3])
def test_square_transpose_inplace(self):
# a same-shape transpose is not a reshape: writing the transposed values straight back would scramble the base
a = torch.tensor([[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]], device=device)
a.transpose(0, 1).add_(100)
np.testing.assert_equal(a.cpu().numpy(), [[100., 101., 102.], [103., 104., 105.], [106., 107., 108.]])
def test_interpolate(self):
a = torch.arange(4, dtype=torch.float32, device=device).reshape(1, 1, 2, 2)
nearest = torch.nn.functional.interpolate(a, scale_factor=2.0)
np.testing.assert_equal(nearest.cpu().numpy()[0, 0], [[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 3, 3], [2, 2, 3, 3]])
linear = torch.nn.functional.interpolate(a, size=(4, 4), mode="bilinear", align_corners=False)
ref = torch.nn.functional.interpolate(a.cpu(), size=(4, 4), mode="bilinear", align_corners=False)
np.testing.assert_allclose(linear.cpu().numpy(), ref.numpy(), rtol=1e-5)
def test_interpolate_bicubic_area(self):
a = torch.arange(32, dtype=torch.float32, device=device).reshape(1, 2, 4, 4)
for mode, scale in [("bicubic", 2.0), ("area", 0.5)]:
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=scale, mode=mode)
np.testing.assert_allclose(torch.nn.functional.interpolate(a, scale_factor=scale, mode=mode).cpu().numpy(), ref.numpy(), atol=1e-4)
@unittest.expectedFailure
def test_interpolate_bicubic_backward(self):
# the forward comes from a decomposition, but aten::upsample_bicubic2d_backward has none (nor does
# aten::_adaptive_avg_pool2d_backward, for area), so training through these modes needs a real kernel
x = torch.arange(32., dtype=torch.float32, device=device).reshape(1, 2, 4, 4).requires_grad_()
torch.nn.functional.interpolate(x, scale_factor=2.0, mode="bicubic").sum().backward()
@unittest.expectedFailure
def test_interpolate_inexact_scale(self):
# torch forwards the raw scale_factor, Tensor.interpolate recomputes it from output_size, and they disagree here
a = torch.arange(6, dtype=torch.float32, device=device).reshape(1, 1, 2, 3)
tiny = torch.nn.functional.interpolate(a, scale_factor=2.5, mode="bilinear")
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=2.5, mode="bilinear")
np.testing.assert_allclose(tiny.cpu().numpy(), ref.numpy(), rtol=1e-5)
def test_logical_or_xor(self):
a = torch.tensor([True, True, False, False], device=device)
b = torch.tensor([True, False, True, False], device=device)
np.testing.assert_equal(torch.logical_or(a, b).cpu().numpy(), [True, True, True, False])
np.testing.assert_equal(torch.logical_xor(a, b).cpu().numpy(), [False, True, True, False])
# bool-valued whatever the input dtype, so this is not | and ^
i, j = torch.tensor([2, 0, 5, 0], device=device), torch.tensor([0, 0, 1, 1], device=device)
np.testing.assert_equal(torch.logical_or(i, j).cpu().numpy(), [True, False, True, True])
np.testing.assert_equal(torch.logical_xor(i, j).cpu().numpy(), [True, False, False, True])
def test_slice_scatter(self):
# the scatters are functional: they return a new tensor and must leave the one they were given alone
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
out = torch.slice_scatter(a, torch.ones(1, 4, device=device), 0, 0, 1)
np.testing.assert_equal(out.cpu().numpy(), [[1, 1, 1, 1], [4, 5, 6, 7], [8, 9, 10, 11]])
np.testing.assert_equal(a.cpu().numpy(), np.arange(12, dtype=np.float32).reshape(3, 4))
def test_slice_scatter_casts_src(self):
a = torch.zeros(3, 4, device=device)
out = torch.slice_scatter(a, torch.ones(1, 4, dtype=torch.int32, device=device), 0, 0, 1)
self.assertEqual(out.dtype, torch.float32)
np.testing.assert_equal(out.cpu().numpy()[0], np.ones(4, dtype=np.float32))
def test_select_scatter(self):
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
out = torch.select_scatter(a, torch.ones(4, device=device), 0, 1)
np.testing.assert_equal(out.cpu().numpy(), [[0, 1, 2, 3], [1, 1, 1, 1], [8, 9, 10, 11]])
def test_diagonal_scatter(self):
a = torch.zeros(3, 3, device=device)
out = torch.diagonal_scatter(a, torch.arange(3, dtype=torch.float32, device=device))
np.testing.assert_equal(out.cpu().numpy(), np.diag([0., 1., 2.]))
np.testing.assert_equal(a.cpu().numpy(), np.zeros((3, 3), dtype=np.float32))
def test_copy_functional(self):
# without an impl this segfaults rather than fails: a regression here takes the whole run down
a = torch.arange(4, dtype=torch.float32, device=device)
out = torch.ops.aten.copy(a, torch.zeros(4, device=device))
np.testing.assert_equal(out.cpu().numpy(), [0., 0., 0., 0.])
np.testing.assert_equal(a.cpu().numpy(), [0., 1., 2., 3.])
from tinygrad import Tensor
class TestBackendHelpers(unittest.TestCase):
+1
View File
@@ -188,6 +188,7 @@ class TestTautologicalCompare(unittest.TestCase):
np.testing.assert_equal((Tensor(True) < Tensor(False)).numpy(), False)
np.testing.assert_equal((Tensor(True) < Tensor(True)).numpy(), False)
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "WEBGPU doesn't support NaN comparison correctly")
def test_a_eq_a(self):
# self eq is always true for int or bool
a = Tensor([1, 2, 3])
+3 -2
View File
@@ -422,8 +422,9 @@ class TestCustomKernel(unittest.TestCase):
return Tensor.custom_kernel(y, x, fxn=custom_add_one_kernel)[0]
GlobalCounters.reset()
y = run(x[0]).realize()
# backends that support contiguous views don't launch extra kernels
assert_kernel_count(2 if x[0].uop.contiguous_view() is None else 1)
# it's copying the input and the output
# TODO: subbuffer usage has runtime specific behavior, this will be fixed after the removal of SLICE.
assert_kernel_count(2 if y.device in ("CL", "WEBGPU") else 1)
self.assertEqual(y.tolist(), [1, 2, 3, 4])
@Context(DEV="CPU")
-3
View File
@@ -340,9 +340,6 @@ class TestUint64DType(TestDType):
DTYPE = dtypes.uint64
def test_uint64_load(self):
assert Tensor(2**64 - 1, dtype=dtypes.uint64).numpy() == 2**64 - 1
@unittest.skipIf(dtypes.double not in supported_dtypes, "needs float64")
def test_uint64_cast_double(self):
assert Tensor([2**32 + 1], dtype=dtypes.uint64).cast(dtypes.double).numpy() == 2**32 + 1
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, PTXRenderer), "PTX does indexing math with longs")
class TestEmulatedUInt64DType(TestUint64DType):
+1 -1
View File
@@ -7,7 +7,7 @@ from tinygrad.renderer.isa.x86 import X86Renderer, X86Ops
from tinygrad.renderer.isa import IselContext
# INDEX on a register value with a constant index extracts a single element (the old GEP)
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.const(i, dtypes.int), dtype=y.dtype)
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.const(i, dtypes.int), dtype=y.dtype.scalar())
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "only x86")
class TestIselX86(unittest.TestCase):
+4 -4
View File
@@ -360,7 +360,7 @@ class TestJitGraphSplit(unittest.TestCase):
self.expect(f, inp, inp_cpu,
graph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()],
multigraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()],
hcqgraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp()]) # cpu is hcq2 now, it does not join hcq graphs
hcqgraph=[self.ji_graph(4)])
def test_jit_cpu_several(self):
if Device.DEFAULT == "CPU": raise unittest.SkipTest("CPU is not a valid default device for this test")
@@ -377,9 +377,9 @@ class TestJitGraphSplit(unittest.TestCase):
inp = Tensor.randn(10, 10, device=Device.DEFAULT).realize()
inp_cpu = Tensor.randn(10, 10, device="CPU").realize()
self.expect(f, inp, inp_cpu,
graph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()],
multigraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()],
hcqgraph=[self.ji_graph(2), self.ji_comp(), self.ji_comp(), self.ji_comp()])
graph=[self.ji_graph(2), self.ji_graph(2), self.ji_comp()],
multigraph=[self.ji_graph(2), self.ji_graph(2), self.ji_comp()],
hcqgraph=[self.ji_graph(5)])
def test_jit_multidev(self):
if Device.DEFAULT == "CPU": raise unittest.SkipTest("CPU is not a valid default device for this test")
+2 -2
View File
@@ -30,7 +30,7 @@ class TestLinearizer(unittest.TestCase):
c = ((a.shrink(((0, 2),)) - a.shrink(((2, 4),))) - (b.shrink(((0, 2),)) - b.shrink(((2, 4),))))
linear = c.schedule_linear()
run_linear(linear)
rawbufs = [s.buffer for s in linear.src[-1].src[1:] if not s.is_bound_var]
rawbufs = [s.buffer for s in linear.src[-1].src[1:] if s.op is not Ops.BIND]
assert len(rawbufs) == 3 and set(rawbufs[1:]) == {a.uop.base.realized, b.uop.base.realized}
np_c = (np_a[:2] - np_a[2:]) - (np_b[:2] - np_b[2:])
np.testing.assert_allclose(np_c, c.numpy(), atol=1e-4, rtol=1e-4)
@@ -411,7 +411,7 @@ def helper_realized_ast(r:Tensor|list[Tensor]) -> tuple[UOp, list[Buffer]]:
last_call = linear.src[-1]
ast = last_call.src[0]
assert ast.op is Ops.SINK, f"helper_realized_ast expects a SINK {last_call}"
last_bufs = [s.buffer for s in last_call.src[1:] if not s.is_bound_var]
last_bufs = [s.buffer for s in last_call.src[1:] if s.op is not Ops.BIND]
# now all input buffers in last_call should be realized
# create fresh buffers for the outputs
bufs = [Buffer(x.device, x.size, x.dtype).allocate() if i < len(ast.src) else x for i,x in enumerate(last_bufs)]
+5 -5
View File
@@ -1,9 +1,9 @@
import unittest, random
from tinygrad import Tensor, Device, nn, GlobalCounters, TinyJit, dtypes, Variable
from tinygrad.uop.ops import Ops, UOp, AxisType, graph_rewrite
from tinygrad.uop.ops import Ops, UOp, AxisType
from tinygrad.helpers import getenv, prod, Context
from tinygrad.nn.state import get_parameters
from tinygrad.engine.realize import run_linear, compile_linear, pm_beam, pm_compile
from tinygrad.engine.realize import run_linear, compile_linear
import numpy as np
from hypothesis import given, strategies as strat, settings
from test.helpers import not_support_multi_device, needs_second_gpu, slow, call_is_graph, check_schedule, assert_kernel_count
@@ -79,9 +79,9 @@ class TestMultiTensor(unittest.TestCase):
def test_shard_beam(self):
cpu_2 = ("CPU:1", "CPU:2")
src = Tensor.ones(16).shard(cpu_2, 0).realize()
lin = UOp(Ops.LINEAR, src=(src.to(cpu_2[::-1]).schedule_linear().src[0],))
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): call = graph_rewrite(graph_rewrite(lin, pm_beam, ctx=1, walk=True), pm_compile, walk=True).src[0]
self.assertNotEqual(call.src[0].src[0].arg.applied_opts, ())
pad = src.to(cpu_2[::-1]).schedule_linear().src[0]
with Context(BEAM=1, IGNORE_BEAM_CACHE=1): prg = compile_linear(UOp(Ops.LINEAR, src=(pad,))).src[0].src[0]
self.assertNotEqual(prg.src[0].arg.applied_opts, ())
def test_shard_same_device(self):
X = Tensor.ones(256).contiguous().realize()
+2 -4
View File
@@ -720,11 +720,10 @@ class TestOps(unittest.TestCase):
return torch.autograd.grad(t ** c, t)[0].item()
for x in [-math.inf, 0, 1, math.inf]:
for c in [-1, 0, 0.3, 1, 2]:
torch_out = get_torch_gradient(x, c)
# the pow backward routes through exp2/log2, whose 0/inf behavior is undefined on WEBGPU
if Device.DEFAULT == "WEBGPU" and not math.isfinite(torch_out): continue
tiny_out = get_tiny_gradient(x, c)
torch_out = get_torch_gradient(x, c)
if math.isnan(tiny_out):
if Device.DEFAULT == "WEBGPU": continue # TODO: WEBGPU issue with nan
assert math.isnan(torch_out)
else:
self.assertAlmostEqual(tiny_out, torch_out, msg=f"{x}, {c}")
@@ -750,7 +749,6 @@ class TestOps(unittest.TestCase):
def test_exp2_log2_zero_times_negative(self):
# gallivm's exp2/log2 have "undefined behavior with infs, 0s and nans", so exp2(log2(0)*y) returns 0 instead of inf
helper_test_op(None, lambda x,y: (x.log2()*y).exp2(), lambda x,y: (x.log2()*y).exp2(), vals=[[0.0], [-0.7]], forward_only=True)
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "pow at 0 routes through exp2/log2, whose 0/inf behavior is undefined on WEBGPU")
def test_pow_zero_const(self):
helper_test_op(None, lambda x: x**0.3, vals=[[0.0]])
helper_test_op(None, lambda x: x**0.0, vals=[[0.0]])
+1 -6
View File
@@ -2,7 +2,7 @@ import unittest, pickle, types, tracemalloc
import numpy as np
from tinygrad import Tensor, Device, TinyJit, Variable, dtypes
from tinygrad.helpers import GlobalCounters, ContextVar, Context, DEV
from tinygrad.uop.ops import PatternMatcher, UPat, UOp, deconstruct_function
from tinygrad.uop.ops import PatternMatcher, UPat, UOp
class TestPickle(unittest.TestCase):
def test_pickle_code_object(self):
@@ -11,11 +11,6 @@ class TestPickle(unittest.TestCase):
fxn = types.FunctionType(pickle.loads(code_str), globals())
self.assertEqual(fxn(2), 4)
def test_deconstruct_function_nested_comprehension(self):
# pre PEP 709, each comprehension is its own code object, so dtypes here is referenced two code objects deep
def fxn(): return [[dtypes.int for _ in range(2)] for _ in range(2)]
self.assertEqual(types.FunctionType(*deconstruct_function(fxn))(), fxn())
def test_pickle_pattern_matcher(self):
pm = PatternMatcher([(UPat.cvar('x'), lambda x: x*2)])
sink = UOp.const(2)
+1 -1
View File
@@ -82,7 +82,7 @@ class TestQuantizeOnnxCPU(unittest.TestCase):
linear = run_onnx({"input":inp})["output"].schedule_linear()
prg = to_program(linear.src[-2].src[0], renderer=Device[Device.DEFAULT].renderer)
daccs = [u for u in tuple(prg.src[1].src) if u.op is Ops.BUFFER and u.addrspace is AddrSpace.REG]
assert all(u.dtype is dtypes.int for u in daccs)
assert all(u.dtype.scalar() is dtypes.int for u in daccs)
@unittest.skipIf(Device.DEFAULT != "DSP", "only tests for DSP")
class TestQuantizeOnnx(unittest.TestCase):
-10
View File
@@ -653,19 +653,9 @@ class TestZeroShapeTensor(unittest.TestCase):
np.testing.assert_equal(Tensor([[1, 2]]).pad_to(2, 3).numpy(), [[1, 2, 0], [0, 0, 0]])
np.testing.assert_equal(Tensor([[1, 2]]).pad_to(1, 3).numpy(), [[1, 2, 0]])
np.testing.assert_equal(Tensor([[1, 2]]).pad_to(None, 3).numpy(), [[1, 2, 0]])
np.testing.assert_equal(Tensor([1, 2]).pad_to(4, value=2).numpy(), [1, 2, 2, 2])
np.testing.assert_equal(Tensor([[1, 2]]).pad_to(2, 3, value=-1).numpy(), [[1, 2, -1], [-1, -1, -1]])
np.testing.assert_equal(Tensor([1, 2]).pad_to(None, value=5).numpy(), [1, 2]) # no-op pad ignores the fill
with self.assertRaises(ValueError): Tensor([1, 2]).pad_to(2, 3)
with self.assertRaises(ValueError): Tensor([[1, 2]]).pad_to(3)
def test_max_shape(self):
from tinygrad import UOp
t = Tensor.empty(2, UOp.variable('v', 1, 32), 4)
self.assertEqual(t.max_shape, (2, 32, 4))
self.assertEqual(t.max_numel(), 2*32*4)
self.assertEqual(Tensor.empty(2, 3).max_shape, (2, 3))
def test_shrink_into_zero(self):
t = Tensor.rand(3, 4).realize()
assert t.shrink((None, (2, 2))).realize().shape == (3, 0)
-8
View File
@@ -10,13 +10,5 @@ class TestHCQ2(unittest.TestCase):
with patch.object(Device[Device.DEFAULT], "has_copy_queue", False):
np.testing.assert_equal(Tensor(np.arange(61, dtype=np.float32)).to(Device.DEFAULT).contiguous().realize().numpy(), np.arange(61))
def test_overlapping_device_tuples(self):
# an op on a wide device tuple followed by an op on an overlapping smaller tuple used to MMU-fault the smaller one
d4, d2 = tuple(f"{Device.DEFAULT}:{i}" for i in range(4)), tuple(f"{Device.DEFAULT}:{i}" for i in range(2))
ref = Tensor.arange(16).contiguous().realize()
Tensor(ref.uop.copy_to_device(d4)).realize()
out = Tensor.ones(8).shard(d2, axis=0).contiguous().realize()
np.testing.assert_equal(out.numpy(), np.ones(8))
if __name__ == "__main__":
unittest.main()
-17
View File
@@ -1,17 +0,0 @@
from tinygrad import Device, Tensor, TinyJit, dtypes
from tinygrad.helpers import Timing, Context
GPUS, DEPTH, SZ = 8, 4, 128 * 2**20
WARMUP, ITERS = 3, 5
devs = tuple(f"{Device.DEFAULT}:{i}" for i in range(GPUS))
bufs = tuple(Tensor.empty(SZ, dtype=dtypes.uint8, device=dev).contiguous().realize() for _ in range(DEPTH) for dev in devs)
@TinyJit
def all_to_all(*srcs:Tensor): return Tensor.realize(*(src.to(dst) for i,src in enumerate(srcs) for j,dst in enumerate(devs) if i % GPUS != j))
if __name__ == "__main__":
with Context(ALL2ALL=1, JIT_BATCH_SIZE=0):
for i in range(-WARMUP, ITERS):
with Timing("ALL2ALL ", lambda ns: f" {SZ*GPUS*(GPUS-1)*DEPTH/ns:.2f} GB/s", enabled=i>=0):
all_to_all(*bufs)
for dev in devs: Device[dev].synchronize()
+2 -15
View File
@@ -1,5 +1,5 @@
import unittest, time, itertools
from tinygrad import Tensor, Context
import unittest, time
from tinygrad import Tensor
class TestScheduleScaling(unittest.TestCase):
"""Test that .schedule() scales linearly with graph size (no O(n^2) behavior)."""
@@ -130,18 +130,5 @@ class TestScheduleScaling(unittest.TestCase):
return parts[0].cat(*parts[1:])
self._assert_linear(concat_chain)
@Context(DEV="NULL:HIP:gfx1100")
def test_custom_kernel_assign_scaling(self):
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.runtime.autogen.amd.rdna3.ins import s_nop
count = itertools.count(0)
def custom_kernel_assign(n):
def custom_asm(out):
return UOp(Ops.PROGRAM, src=(UOp.sink(out, arg=KernelInfo(f"fxn_{next(count)}")),
UOp(Ops.LINEAR, src=tuple(UOp(Ops.INS, arg=s_nop(i)) for i in range(n*8)))))
call = Tensor.custom_kernel(Tensor.empty(1), fxn=custom_asm)[0]
return Tensor.cat(*[Tensor.empty(1).assign(call+i) for i in range(n)])
self._assert_linear(custom_kernel_assign, n_small=50, n_large=500)
if __name__ == '__main__':
unittest.main(verbosity=2)
+1 -1
View File
@@ -44,7 +44,7 @@ def realized_matmul():
z = y.matmul(x)
Tensor.realize(z)
def realized_gradient():
x = Tensor.eye(3).clone()
x = Tensor.eye(3)
y = Tensor([[2.0,0,-2.0]])
z = y.matmul(x).sum()
z.backward()
+1 -3
View File
@@ -86,9 +86,7 @@ def assert_jit_cache_len(fxn, expected_len):
if linear is None or not linear.src:
if expected_len != 0: raise KernelCountException(expected_len, 0)
return
if expected_len and all(call_is_hcq(call) for call in linear.src): # HCQ2: one batch submitter, or fence + reset + merged calls + finalizer
from tinygrad.runtime.support.hcq2 import HCQ_RUNTIME_DEV
expected_len = 1 if HCQ_RUNTIME_DEV.value == "CPU" else 4
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 3 # HCQ2: merged same-queue calls + finalizer + bumps
if call_is_graph(linear.src[0]):
if len(linear.src) != 1: raise KernelCountException(1, len(linear.src))
inner = linear.src[0].src[0].src[0] # LINEAR UOp inside CUSTOM_FUNCTION
+20 -16
View File
@@ -260,6 +260,19 @@ def _cond(cond, if_true, if_false):
def _cond_hi16(cond, val: UOp) -> UOp: return _cond(cond, _hi16(val), val)
def _apply_opsel(val: UOp, sel_bit: int, opsel: int) -> UOp: return _hi16(val) if opsel & (1 << sel_bit) else val
def _set_lane_bit(old: UOp, lane: UOp, val: UOp, exec_mask: UOp) -> UOp:
"""Set/clear a single bit in a mask based on lane index, respecting exec mask."""
if old.dtype in (dtypes.uint64, dtypes.int64):
dt = dtypes.uint64
mask = UOp.const(1, dt) << lane.cast(dt)
new_bit = _to_u32(val).cast(dt) << lane.cast(dt)
cleared = old.cast(dt) & (mask ^ UOp.const(0xFFFFFFFFFFFFFFFF, dt))
return _lane_active(exec_mask, lane).where(cleared | new_bit, old.cast(dt))
mask = _c(1) << lane.cast(dtypes.uint32)
new_bit = _to_u32(val) << lane.cast(dtypes.uint32)
cleared = old & (mask ^ _c(MASK32))
return _lane_active(exec_mask, lane).where(cleared | new_bit, old)
def _val_to_u32(val: UOp) -> UOp:
"""Convert any value to uint32 for storage (bitcast floats, cast ints)."""
if val.dtype == dtypes.uint32: return val
@@ -519,19 +532,6 @@ class _Ctx:
return [self.wsgpr_dyn(reg, lo), self.wsgpr_dyn(reg + _c(1), hi)]
return [self.wsgpr_dyn(reg, val)]
def wmask_lane_bit(self, reg: UOp, lane: UOp, val: UOp, exec_mask: UOp) -> list[UOp]:
"""Set/clear bit `lane` of the mask at `reg` from val for exec-active lanes, preserving memory for inactive lanes"""
active, bit = _lane_active(exec_mask, lane), _to_u32(val)
if self.wave_size <= 32:
old = self.rsgpr_dyn(reg)
mask = _c(1) << lane.cast(dtypes.uint32)
return [self.wsgpr_dyn(reg, active.where((old & (mask ^ _c(MASK32))) | (bit << lane.cast(dtypes.uint32)), old))]
off = (lane & _c(31, dtypes.int)).cast(dtypes.uint32)
mask = _c(1) << off
def half(old: UOp, sel: UOp) -> UOp: return sel.where(active.where((old & (mask ^ _c(MASK32))) | (bit << off), old), old)
return [self.wsgpr_dyn(reg, half(self.rsgpr_dyn(reg), lane < _c(32, dtypes.int))),
self.wsgpr_dyn(reg + _c(1), half(self.rsgpr_dyn(reg + _c(1)), _c(32, dtypes.int) <= lane))]
def rmask(self, reg: UOp) -> UOp:
"""Read a lane mask (VCC/EXEC). Combines lo/hi for wave64."""
if self.wave_size > 32: return _u64(self.rsgpr_dyn(reg), self.rsgpr_dyn(reg + _c(1)))
@@ -718,7 +718,9 @@ class _Ctx:
raw_stores.append(('vgpr_direct', self.vgpr.index(val[0].valid(active)).store(new_val)))
continue
if 'D0' in dest and '[laneId]' in dest:
raw_stores.extend([('vcc', s) for s in self.wmask_lane_bit(_c(VCC_LO.offset), lane, val, exec_mask)])
old_vcc = self.rmask(_c(VCC_LO.offset))
new_vcc = _set_lane_bit(old_vcc, lane, val, exec_mask)
raw_stores.extend([('vcc', s) for s in self.wmask(_c(VCC_LO.offset), new_vcc)])
elif dest.startswith('D0'):
dest_suffix = re.match(r'D0\.(\w+)', dest)
if dest_suffix is not None:
@@ -1037,11 +1039,13 @@ def _compile_sdwa(inst: irc.VOP1_SDWA | irc.VOP2_SDWA | irc.VOP2_SDWA_SDST | irc
result = _sdwa_write(old, result, dst_sel, dst_unused)
stores.append(ctx.wvgpr_dyn(vdst_reg, lane, result, exec_mask))
elif dest.startswith('VCC'):
stores.extend(ctx.wmask_lane_bit(_c(VCC_LO.offset), lane, val, exec_mask))
old_vcc = ctx.rmask(_c(VCC_LO.offset))
stores.extend(ctx.wmask(_c(VCC_LO.offset), _set_lane_bit(old_vcc, lane, val, exec_mask)))
if vcc_val is not None:
# Initialize sdst to 0 before lane loop (old value may be unrelated data), then set lane bits in loop
init_stores = [ctx.wsgpr_dyn(sdst_off, _c(0)), ctx.wsgpr_dyn(sdst_off + _c(1), _c(0))]
stores.extend(ctx.wmask_lane_bit(sdst_off, lane, vcc_val, exec_mask))
old_sdst = ctx.rmask(sdst_off)
stores.extend(ctx.wmask(sdst_off, _set_lane_bit(old_sdst, lane, vcc_val, exec_mask)))
if stores:
return UOp.sink(*init_stores, UOp.sink(*stores).end(lane), *ctx.inc_pc())
return UOp.sink(*init_stores, *ctx.inc_pc())
-2
View File
@@ -74,7 +74,6 @@ class TestWhisper(unittest.TestCase):
err
)
@slow
def test_transcribe_file1(self):
self.assertEqual(transcribe_file(self.model, self.enc, TEST_FILE_1), TRANSCRIPTION_1)
@@ -90,7 +89,6 @@ class TestWhisper(unittest.TestCase):
self.assertEqual(TRANSCRIPTION_1, transcriptions[0])
self.assertEqual(TRANSCRIPTION_2, transcriptions[1])
@slow
def test_transcribe_batch21(self):
waveforms = [load_file_waveform(TEST_FILE_2), load_file_waveform(TEST_FILE_1)]
transcriptions = transcribe_waveform(self.model, self.enc, waveforms)
+11 -8
View File
@@ -1,6 +1,6 @@
import unittest, itertools, math
from tinygrad import Tensor, dtypes, Context
from tinygrad.dtype import DType, ConstType
from tinygrad.dtype import DType, ConstType, truncate
from tinygrad.uop.ops import Ops, UOp
from test.helpers import full_rewrite
import numpy as np
@@ -51,13 +51,16 @@ class TestWeakConstFolding(unittest.TestCase):
def test_invalid_poison(self):
self.assertTrue(UOp.invalid().alu(Ops.CDIV, UOp.const(0)).simplify().is_invalid)
def test_single_rounding_log10_backward(self):
# log10 backward folds log10(2)/log(2) = 1/log(10) in one rounding, not the double-rounded 1/float32(log(10))
x = Tensor([1.0, 2.0, 3.0])
ast = next(s.src[0] for s in x.log10().sum().gradient(x)[0].schedule_linear().src if s.src[0].op is Ops.SINK)
const = next(u.arg for u in full_rewrite(ast).toposort() if u.op is Ops.CONST and u.dtype is dtypes.float32)
# correctly rounded: within half a float32 ulp of the exact value (folding at float32 lands 0.66 ulp off)
self.assertLess(abs(const - 1/math.log(10)), 2**-26)
def test_cast_commits_to_dtype_grid(self):
# committing a weak const to a stated width puts the value on that width's grid, same as storage packing and native compilers
v = 1/123008 # not representable in float16
out = UOp.const(v).cast(dtypes.half).simplify()
self.assertEqual((out.op, out.dtype, out.val), (Ops.CONST, dtypes.half, truncate[dtypes.half](v)))
self.assertNotEqual(out.val, v)
# the grid commit preserves the sign of zero
self.assertEqual(math.copysign(1, UOp.const(-0.0).cast(dtypes.half).simplify().val), -1)
# observable at tensor level: the const-folded comparison agrees with the committed value
self.assertTrue((Tensor(-3.2).cast(dtypes.float32) <= truncate[dtypes.float32](-3.2)).item())
class TestBinaryOpsConstFolding(unittest.TestCase):
def test_add_literal_zero(self):
+4
View File
@@ -51,6 +51,10 @@ class TestHelpers(unittest.TestCase):
assert dtypes.is_float(dtypes.fp8e4m3)
assert dtypes.is_float(dtypes.fp8e5m2)
@given(strat.sampled_from([d for d in DTYPES_DICT.values() if dtypes.is_float(d) or dtypes.is_int(d)]))
def test_scalar(self, dtype):
assert dtype.scalar() == dtype
def test_from_py(self):
assert dtypes.from_py(True) == dtypes.bool
assert dtypes.from_py(Invalid) == dtypes.bool
+2 -2
View File
@@ -143,13 +143,13 @@ class TestModuloAndDivisionFolding(unittest.TestCase):
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.weakint) % 10).render(simplify=False), x_var_uop.render(simplify=False))
def test_full_graph_rewrite_division_with_remainder(self):
x_var_uop = UOp.variable('x', 7, 9, param=True)
x_var_uop = UOp.variable('x', 7, 9)
optimized_sink = apply_rewrite(x_var_uop // 2)
for x_value in range(7, 10):
self.assertEqual(x_value // 2, evaluate_uop(optimized_sink, {'x': x_value}))
def test_full_graph_rewrite_complex_mod_div_expression(self):
x_var_uop = UOp.variable('x', 1, 10, param=True)
x_var_uop = UOp.variable('x', 1, 10)
optimized_sink = apply_rewrite(((x_var_uop * 5) % 3) // 2)
for x_value in range(1, 11):
original_result = ((x_value * 5) % 3) // 2
+1 -1
View File
@@ -25,7 +25,7 @@ def get_load_image_uop(image_shape:tuple[int, ...], valid:UOp, idx:tuple[UOp, UO
))
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(nmax),), arg=expr)
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax, param=True)
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax)
def Range(n, nmax): return UOp.range(nmax, n)
class TestValidIdxSimplification(unittest.TestCase):
+1 -1
View File
@@ -69,7 +69,7 @@ class TestIdxUpcast(unittest.TestCase):
if not isinstance(Device[Device.DEFAULT].renderer, (PTXRenderer, NIRRenderer)):
assert idx.op is Ops.INDEX
idx_val = idx.src[1]
self.assertFalse(idx_val.overflows(idx_val.dtype))
self.assertFalse(idx_val.overflows(idx_val.dtype.scalar()))
# use expand to generate kernel that uses large idx
def do_op_then_assert(self, dtype: DType, dim1, dim2, dim3):
+2 -2
View File
@@ -157,7 +157,7 @@ class TestGraphRewrite(unittest.TestCase):
self.assertEqual(nout.val, 3.0)
def test_depth_2_fold(self):
v = UOp.variable("v", 0, 1, dtypes.float, param=True)
v = UOp.variable("v", 0, 1, dtypes.float)
c1 = UOp.const(1.0)
c2 = UOp.const(2.0)
nout = graph_rewrite(v+c1+c2, simple_pm)
@@ -339,7 +339,7 @@ class TestUOpGraph(unittest.TestCase):
self.assertEqual(len([x for x in uops if x.op is Ops.CAST]), 1)
def test_depth_2_const_fold(self):
v = UOp.variable("tmp", 0, 1, dtypes.int, param=True)
v = UOp.variable("tmp", 0, 1, dtypes.int)
c2 = UOp.const(2, dtypes.int)
c4 = UOp.const(4, dtypes.int)
vc = v+c2
+26 -33
View File
@@ -3,10 +3,10 @@ import unittest, pickle, functools, math
import z3
from tinygrad.dtype import dtypes, ConstType, DType, Invalid
from test.helpers import get_uops
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, sym_infer
from tinygrad.uop.spec import spec_shared, type_verify
from tinygrad.uop.symbolic import sym, commutative, pm_simplify_valid, pm_move_where_on_load
from tinygrad.uop.weak import pm_cast_weak
from tinygrad.uop.symbolic import sym, pm_fold_cast_const, commutative, pm_simplify_valid, pm_move_where_on_load
from tinygrad.uop.validate import uops_to_z3
def check_uop_against_string(self, v:UOp, s:str):
@@ -16,8 +16,7 @@ def check_uop_against_string(self, v:UOp, s:str):
s_eval = graph_rewrite(s_eval, commutative, name="cannonicalize eval")
self.assertIs(s_eval, v, f"eval did not match simplified: {s_eval} != {v.render()} for {s}")
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint):
return UOp.variable(name, min_val, max_val, dtype, param=True)
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint): return UOp.variable(name,min_val,max_val,dtype)
def uconst(val): return UOp.const(val)
def usum(ops): return functools.reduce(lambda x,y: x+y, ops)
def uand(ops): return functools.reduce(lambda x,y: x*y, ops)
@@ -36,7 +35,7 @@ class TestSymbolic(unittest.TestCase):
self.assertEqual(solver.check(expr1 != expr2), z3.unsat, "simplified expression not equal to original")
def helper_test_variable(self, v, n, m, s, test_z3:bool=True):
v_simplified = graph_rewrite(v, sym+pm_cast_weak, name="simplify symbolic uop")
v_simplified = graph_rewrite(v, sym+pm_fold_cast_const, name="simplify symbolic uop")
if test_z3: self.check_equal_z3(v, v_simplified)
nmin, nmax = v_simplified.vmin, v_simplified.vmax
check_uop_against_string(self, v_simplified, s)
@@ -443,7 +442,7 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(uand([uconst(1), Variable("a", 0, 1)]), 0, 1, "a")
def test_masked_shr_fold(self):
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32, param=True)
x = UOp.variable('x', 0, 255, dtype=dtypes.uint32)
self.helper_test_variable((x & -4) >> 2, 0, 63, "(x>>2)")
def test_bool_or_not_tautology(self):
@@ -484,12 +483,12 @@ class TestSymbolic(unittest.TestCase):
def test_div_drop_small_terms(self):
# from openpilot, shouldnt simplify
gidx0 = UOp.variable("gidx0", 0, 10, param=True)
gidx1 = UOp.variable("gidx1", 0, 10, param=True)
lidx0 = UOp.variable("lidx0", 0, 1, param=True)
lidx1 = UOp.variable("lidx1", 0, 1, param=True)
ridx1005 = UOp.variable("ridx1005", 0, 2, param=True)
ridx1006 = UOp.variable("ridx1006", 0, 2, param=True)
gidx0 = UOp.variable("gidx0", 0, 10)
gidx1 = UOp.variable("gidx1", 0, 10)
lidx0 = UOp.variable("lidx0", 0, 1)
lidx1 = UOp.variable("lidx1", 0, 1)
ridx1005 = UOp.variable("ridx1005", 0, 2)
ridx1006 = UOp.variable("ridx1006", 0, 2)
self.helper_test_variable((lidx1+((gidx1*18)+(ridx1005*18)+(lidx0*162))+(gidx0*2)+(ridx1006*2)+-40)//18, -3, 20,
"(gidx1+ridx1005+lidx0*9+(gidx0+ridx1006+7)//9+-3)")
@@ -949,11 +948,6 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(cond.where(u0, u1), 0, 1, "((a<2)!=True)")
self.helper_test_variable(cond.where(u0, u1).where(u0, u1), 0, 1, "(a<2)")
def test_equivalent_const_max(self):
x = Variable("x", -10, 10)
self.helper_test_variable((x < 0).where(0, x), 0, 10, "x.maximum(0)")
self.helper_test_variable((0 < x).where(x, 0), 0, 10, "x.maximum(0)")
def test_where_combine(self):
cond = Variable("x", 0, 3) < 2
a = Variable("a", 0, 3)
@@ -998,7 +992,7 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(cond.ne(False), 0, 1, "(x<2)")
def test_bitcast_chain(self):
a = UOp.variable("a", 0, 3, dtype=dtypes.int32, param=True)
a = UOp.variable("a", 0, 3, dtype=dtypes.int32)
self.assertIs(graph_rewrite(a.bitcast(dtypes.float32).bitcast(a.dtype), sym), a)
def test_negation_in_where(self):
@@ -1014,11 +1008,20 @@ class TestSymbolic(unittest.TestCase):
self.helper_test_variable(-a<-b, False, True, "(b<a)")
def test_where_cast(self):
cond = Variable("s", 0, 3, dtypes.int) < 2
s = Variable("s", 0, 3, dtypes.int)
cond = s < 2
a = Variable("a", 0, 3, dtypes.int)
self.assertIs(graph_rewrite(cond.where(a, a+1).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), (a+1).cast(dtypes.half)))
self.assertIs(graph_rewrite(cond.where(a, uconst(2)).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.const(2, dtypes.half)))
self.assertIs(graph_rewrite(cond.where(a, UOp.invalid()).cast(dtypes.half), sym), cond.where(a.cast(dtypes.half), UOp.invalid()))
b = Variable("b", 0, 3, dtypes.int)
expr = cond.where(a, b).cast(dtypes.half)
# TODO: copied from render, render does not support cast
glbl = UOp.param(0, dtypes.int, (1,))
uops = get_uops(UOp(Ops.STORE, src=(glbl.index(UOp.const(0, dtypes.int)), expr)).sink())
rewritten_uop = [uop for uop in uops if uop.op is Ops.STORE][0].src[1]
# the vars are now scalar PARAMs
pvar = {u.expr: u for u in rewritten_uop.toposort() if u.op is Ops.PARAM}
self.assertEqual(rewritten_uop, (pvar['s']<UOp.const(2, dtypes.int)).where(pvar['a'].cast(dtypes.half), pvar['b'].cast(dtypes.half)))
def test_where_merge_branches(self):
cond1 = Variable("s", 0, 10) < 6
@@ -1172,7 +1175,7 @@ class TestSymbolicVariables(unittest.TestCase):
assert (a//4 + a//6).variables() == [a]
def test_variable_min_eq_max_bind_folds(self):
b = UOp.variable("x", 1, 1).bind(1)
b = Variable("x", 1, 1).bind(1)
s = b.simplify()
self.assertEqual(s.op, Ops.CONST)
self.assertEqual(s.val, 1)
@@ -1366,16 +1369,6 @@ class TestInvalidIndex(unittest.TestCase):
c2 = UOp.const((1, Invalid, 1, 1))
self.assertIs((c1+c2).simplify(), UOp.const((2, Invalid, Invalid, Invalid)))
def test_gated_load_keeps_index_valid(self):
# the load executes even on gated-off iterations: gated_given_valid must not erase its mask (PADTO OOB shape)
buf = UOp.param(0, dtypes.bool, (17,))
ridx = Variable("ridx", 0, 31)
cond = ridx < 17
load = buf.index(ridx.valid(cond))
out = graph_rewrite(cond.where(load.where(uconst(2), uconst(0)), UOp.invalid()), sym)
idx = next(u for u in out.toposort() if u.op is Ops.INDEX)
self.assertIs(idx.src[1].get_valid(), cond.simplify())
class TestStoreLoadFolding(unittest.TestCase):
"""Tests for store(index, load(index)) -> NOOP rule. This rule matches patterns that EMERGE during simplification."""
def test_store_load_folding(self):
+1 -3
View File
@@ -1,12 +1,10 @@
import unittest
from tinygrad import dtypes
from tinygrad import dtypes, Variable
from tinygrad.dtype import AddrSpace
from tinygrad.helpers import Context
from tinygrad.uop.ops import Ops, UOp, AxisType
from test.helpers import to_uops_list
def Variable(name, nmin, nmax): return UOp.variable(name, nmin, nmax, param=True)
class TestValidateOOB(unittest.TestCase):
"""Test z3 validation of index bounds for different ALU ops and patterns."""
+4 -4
View File
@@ -305,10 +305,10 @@ class TestVizTree(unittest.TestCase):
def test_tree_view(self):
with save_viz() as viz:
a = UOp.variable("a",0,10,param=True)
b = UOp.variable("b",0,10,param=True)
c = UOp.variable("c",0,10,param=True)
d = UOp.variable("d",0,10,param=True)
a = UOp.variable("a",0,10)
b = UOp.variable("b",0,10)
c = UOp.variable("c",0,10)
d = UOp.variable("d",0,10)
sink = UOp.sink(a+b, c+d)
def tree_rewrite(): return graph_rewrite(sink, root, name="root")
tree_rewrite()
+4 -4
View File
@@ -10,12 +10,12 @@ from test.helpers import replace_opts
class TestFloat4(unittest.TestCase):
@staticmethod
def count_float4(uops: list[UOp], n=4):
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.float and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.float and uop.shape == (4,)]))
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.float and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.float and uop.shape == (4,)]))
@staticmethod
def count_half4(uops: list[UOp]):
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.half and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.half and uop.shape == (4,)]))
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.half and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.half and uop.shape == (4,)]))
def test_float4_basic(self):
a = Tensor.empty(2, 8).realize()
-9
View File
@@ -239,15 +239,6 @@ class TestKernelOpts(unittest.TestCase):
helper_linearizer_opt(a.sum().exp(), [[Opt(OptOps.PADTO, 0, 32)],])
helper_linearizer_opt(a.sum(0).exp(), [[Opt(OptOps.PADTO, 1, 32)],])
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_local, "test requires locals")
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared")
@unittest.expectedFailure
def test_padto_group_full_unroll_sum(self):
a = Tensor.ones(2, 28, 4096, dtype=dtypes.bfloat16).realize()
out = ((a * 0.5).float().square()).sum(axis=(0, 2))
opts_to_apply = [Opt(OptOps.GROUPTOP, 1, 256), Opt(OptOps.PADTO, 3, 32), Opt(OptOps.UNROLL, 2, 0), Opt(OptOps.UPCAST, 0, 7)]
helper_linearizer_opt(out, [opts_to_apply], check_default_opt=False)
def test_padto_sum(self):
N = 18
# NOTE: this setup prevents 17 * 17 contiguous merged into one dimension
+1 -1
View File
@@ -64,7 +64,7 @@ class TestAllreduceCast(unittest.TestCase):
with Context(ALLREDUCE_CAST=allreduce_cast, RING=0, SCACHE=0):
t = Tensor.empty(4, 4, dtype=dtype).shard(ds, axis=0)
linear = t.sum(0).linear_with_vars()[0]
return {si.src[1].buffer.dtype for si in linear.src if si.src[0].op is Ops.COPY}
return {si.src[1].buffer.dtype.scalar() for si in linear.src if si.src[0].op is Ops.COPY}
def test_allreduce_cast_bf16(self):
# with ALLREDUCE_CAST, allreduce copies stay in bfloat16 instead of promoting to float32
-4
View File
@@ -540,10 +540,6 @@ class TestAssign(unittest.TestCase):
c = Tensor([1.0, 2.0, 3.0, 4.0], dtype=dtypes.float32).realize()
c[0:2].bitcast(dtypes.uint32).assign(Tensor([0x40800000, 0x40400000], dtype=dtypes.uint32)).realize()
np.testing.assert_allclose(c.numpy(), [4.0, 3.0, 3.0, 4.0])
# without .realize()
a = Tensor([1.0, 2.0, 3.0, 4.0], dtype=dtypes.float32).realize()
a.bitcast(dtypes.uint32).assign(Tensor([0x40800000, 0x40400000, 0x40000000, 0x3f800000], dtype=dtypes.uint32))
np.testing.assert_allclose(a.numpy(), [4.0, 3.0, 2.0, 1.0])
def test_assign_bitcast_different_size(self):
# assign to a shape-changing bitcast view (only works on DISK currently)
-12
View File
@@ -76,11 +76,6 @@ class TestWeakPromotion(unittest.TestCase):
committed = graph_rewrite((UOp.const(1).cast(dtypes.int32) + UOp.const(1.0)).cast(dtypes.float32), pm_lower_index_dtype, ctx={})
self.assertEqual([u.dtype for u in committed.toposort() if u.op is Ops.ADD], [dtypes.float32])
def test_div_sub_operand_kept_weak(self):
a = Tensor.empty(4, dtype=dtypes.float32)
for t in (a / 1, a - 0):
self.assertEqual(t.uop.src[1].dtype, dtypes.weakfloat)
def test_cast_weak_expression_commits_at_cast_floor(self):
# the floor never narrows: a cast BELOW the default does not pull the compute width down with it
with Context(DEFAULT_FLOAT=dtypes.float32):
@@ -93,13 +88,6 @@ class TestWeakPromotion(unittest.TestCase):
out = Tensor(1.0, dtype=dtypes.float32, device="CPU") / denom
self.assertAlmostEqual(out.item(), 1 / (70000 + 1e-5), places=10)
def test_stacked_weak_casts_convert_each_kind(self):
# each weak cast is a kind conversion: weakint truncates before weakfloat re-lifts (neither is only a marker)
x = Tensor([2.5, -3.7], dtype=dtypes.float32, device="CPU")
stacked = x.cast(dtypes.weakint).cast(dtypes.weakfloat)
self.assertIs(stacked.dtype, dtypes.weakfloat)
self.assertEqual(stacked.tolist(), [2.0, -3.0])
def test_uop_scalar_const_lifts_kind(self):
for dtype, value, out_dtype, const_dtype in ((dtypes.weakint, 1, dtypes.weakint, dtypes.weakint),
(dtypes.int32, 1, dtypes.int32, dtypes.weakint),
+3 -7
View File
@@ -51,10 +51,6 @@ class TestTensorGradient(unittest.TestCase):
with self.assertRaises(RuntimeError): x.sum().gradient(x)
with self.assertRaises(RuntimeError): x.float().sum().gradient(x)
def test_const_target_raise(self):
t = Tensor(2.0)
with self.assertRaises(RuntimeError): (t * 2.0).gradient(t)
def test_copy_to_device_gradient(self):
t = Tensor([1.0, 2, 3]).realize()
t.to("CPU:1").square().sum().backward()
@@ -104,7 +100,7 @@ class TestTensorGradient(unittest.TestCase):
def test_implicit_broadcast_where_gradient(self):
# WHERE with a bare ()-shape branch: the scalar's gradient counts the positions where it is selected
cond, x, w = Tensor([True, False, True]), Tensor([1.0, 2.0, 3.0]), Tensor(4.0, dtype=dtypes.float32)
cond, x, w = Tensor([True, False, True]), Tensor([1.0, 2.0, 3.0]), Tensor(4.0)
dw = Tensor(cond.uop.alu(Ops.WHERE, x.uop, w.uop)).sum().gradient(w)[0]
self.assertEqual(dw.shape, ())
self.assertEqual(dw.item(), 1.0)
@@ -113,7 +109,7 @@ class TestTensorGradient(unittest.TestCase):
def test_implicit_broadcast_alu_gradient(self):
# MUL with a bare ()-shape src, no EXPAND in the graph
x, w = Tensor([1.0, 2.0, 3.0]), Tensor(2.0, dtype=dtypes.float32)
x, w = Tensor([1.0, 2.0, 3.0]), Tensor(2.0)
m = x.uop.alu(Ops.MUL, w.uop)
self.assertIs(m.src[1], w.uop)
dw = Tensor(m).sum().gradient(w)[0]
@@ -122,7 +118,7 @@ class TestTensorGradient(unittest.TestCase):
def test_implicit_broadcast_intermediate_accumulation(self):
# s is used directly and through an implicit broadcast edge, each edge's gradient reduces to s's shape before they sum
x, p = Tensor([1.0, 2.0, 3.0]), Tensor(0.5, dtype=dtypes.float32)
x, p = Tensor([1.0, 2.0, 3.0]), Tensor(0.5)
s = p.sin()
z = Tensor(x.uop.alu(Ops.MUL, s.uop)).sum() + s
dp = z.gradient(p)[0]
+3 -3
View File
@@ -25,10 +25,10 @@ class TestHCQUnit(unittest.TestCase):
cpu_call = UOp(Ops.PROGRAM, src=(UOp.sink(),)).call(UOp.new_buffer("CPU", 1, dtypes.float))
gpu_devs = [d0]
# CPU uses HCQ2 and is no longer batched into legacy HCQ graphs.
# local MMIO: GPU works alone and with CPU in batch (cpu_support=True)
assert HCQGraph.supports_uop(gpu_devs, gpu_call) is True
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is False
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is False
assert HCQGraph.supports_uop(gpu_devs, cpu_call) is True
assert HCQGraph.supports_uop(gpu_devs + [cpu_dev], gpu_call) is True
# USB MMIO: GPU-only still works, but CPU batching must be rejected (cpu_support=False)
orig_view = d0.timeline_signal.base_buf.view
+1 -28
View File
@@ -2,7 +2,7 @@ import unittest
import numpy as np
from dataclasses import replace
from tinygrad import Tensor
from tinygrad.llm.model import ExpertGating, TransformerBlock, TransformerConfig
from tinygrad.llm.model import TransformerBlock, TransformerConfig
def _moe_config(dim=8, hidden=16, n_heads=2, num_experts=4, num_experts_per_tok=2):
return TransformerConfig(
@@ -96,32 +96,5 @@ class TestMoEFeedForward(unittest.TestCase):
expected = moe_expected + shared_expected
np.testing.assert_allclose(out.numpy(), expected, rtol=1e-2)
def test_moe_feed_forward_gating_funcs(self):
dim, hidden, n_heads = 8, 16, 2
num_experts, k = 4, 2
logits = np.array([4.0, 3.0, 0.0, -1.0], dtype=np.float32)
def softmax(x):
probs = np.exp(x - x.max())
return probs / probs.sum()
for gating_func in ExpertGating:
for norm_topk_prob in (False, True):
block = TransformerBlock(replace(_moe_config(dim, hidden, n_heads, num_experts, k),
expert_gating_func=gating_func, norm_topk_prob=norm_topk_prob))
block.ffn_gate_exps.weight = Tensor.stack(*[Tensor.eye(hidden, dim) for _ in range(num_experts)])
block.ffn_up_exps.weight = Tensor.stack(*[Tensor.eye(hidden, dim) * (i + 1) for i in range(num_experts)])
block.ffn_down_exps.weight = Tensor.stack(*[Tensor.eye(dim, hidden) for _ in range(num_experts)])
block.ffn_gate_inp.weight = Tensor((logits / dim)[None, :].repeat(dim, 0).T)
out = block._feed_forward(Tensor.ones(1, 1, dim)).numpy()[0, 0, 0]
if gating_func == ExpertGating.SOFTMAX: selection_scores = softmax(logits)
elif gating_func == ExpertGating.SIGMOID: selection_scores = 1 / (1 + np.exp(-logits))
elif gating_func == ExpertGating.SOFTMAX_WEIGHT: selection_scores = logits
else: selection_scores = np.sqrt(np.logaddexp(0, logits))
sel = np.argsort(selection_scores)[-k:]
weights = softmax(logits[sel]) if gating_func == ExpertGating.SOFTMAX_WEIGHT else selection_scores[sel]
if norm_topk_prob: weights /= weights.sum()
expected = (weights * (sel + 1)).sum() / (1 + np.exp(-1))
np.testing.assert_allclose(out, expected, rtol=1e-3)
if __name__ == '__main__':
unittest.main()
+1 -22
View File
@@ -13,18 +13,12 @@ V_TOKS = UOp.variable("toks", 1, 32) # 32 is the default chunk_size in generate
class TestTransformerGenerate(unittest.TestCase):
def test_warmup(self):
model, calls = Transformer(TEST_CONFIG), []
def generate(tokens, **kwargs):
def generate(tokens):
calls.append(tokens)
yield from (1, 2)
with patch.object(model, "generate", generate): model.warmup()
self.assertEqual(calls, [[0], [0]])
def test_warmup_then_generate_with_default_chunk(self):
# warmup must not capture JIT graphs that generate()'s default chunk_size then rejects
model = Transformer(TEST_CONFIG)
model.warmup()
self.assertIsInstance(next(model.generate([5, 6, 7, 8])), int)
def test_first_recurrent_generate_before_state_init(self):
model = Transformer(TEST_CONFIG)
model.has_recurrent_block = True
@@ -44,15 +38,6 @@ class TestTransformerGenerate(unittest.TestCase):
next(model.generate([1, 2, 3, 4, 5, 42, 10]))
self.assertEqual(calls, [((1, 1), V_START_POS.bind(5)), ((1, 1), V_START_POS.bind(6))])
def test_recurrent_divergent_prompt_restarts(self):
model, calls = Transformer(TEST_CONFIG), []
model.has_recurrent_block, model._cached_tokens = True, [1, 2, 9]
def mock_call(self, tokens, start_pos, temperature):
calls.append(start_pos)
return Tensor([[42]])
with patch.object(Transformer, '__call__', mock_call): next(model.generate([1, 2, 10, 11]))
self.assertEqual(calls[0], V_START_POS.bind(0))
def test_template_starts_reasoning(self):
router = StreamRouter(reasoning=True)
self.assertEqual(list(router.route("reasoning</think>answer")),
@@ -193,12 +178,6 @@ class TestTransformerGenerate(unittest.TestCase):
# with temperature=2.0, we should see at least 2 distinct outputs across 5 runs
self.assertGreater(len(runs), 1, "high temperature should produce varied outputs")
def test_recurrent_temperature_high_produces_variety(self):
model = Transformer(TEST_CONFIG)
model.has_recurrent_block = True
outputs = {model.forward(Tensor([[1]]), 0, Tensor([2.0])).item() for _ in range(5)}
self.assertGreater(len(outputs), 1)
def test_temperature_passed_to_forward(self):
"""Temperature from generate should be passed through to __call__."""
model = Transformer(TEST_CONFIG)
-6
View File
@@ -390,12 +390,6 @@ class TestMultiTensor(unittest.TestCase):
self.assertEqual(out.shape, (rows, 8))
np.testing.assert_equal(out[:3].to(Device.DEFAULT).numpy(), np.ones((3, 8)))
def test_symbolic_broadcast_consumed(self):
rows = Variable("rows", 1, 4).bind(3)
out = (Tensor.ones(rows).to(devices_2) + 1).realize()
self.assertEqual(out.shape, (rows,))
np.testing.assert_equal(out[:3].to(Device.DEFAULT).numpy(), np.full(3, 2))
def test_multitensor_jit_in_list(self):
# test MULTI tensor inside a list container - exercises the container unpacking + MULTI unpacking
@TinyJit
+13 -42
View File
@@ -1,16 +1,11 @@
import unittest
import functools
from tinygrad import Tensor, Variable, UOp, function
from tinygrad import Tensor, Variable, UOp
from tinygrad.uop.ops import KernelInfo
from tinygrad.schedule import schedule_cache
def custom_add_kernel(A:UOp, B:UOp, num:int=0) -> UOp:
return A[0].set(B[0] + num).sink(arg=KernelInfo(f"custom_add_{num}"))
def custom_add_backward(grad_output:UOp, _) -> tuple[None, UOp]:
grad = Tensor.invalids(*grad_output.shape, dtype=grad_output.dtype, device=grad_output.device)
grad = Tensor.custom_kernel(grad, Tensor(grad_output, device=grad_output.device), fxn=functools.partial(custom_add_kernel, num=0))[0]
return None, grad.uop
def custom_set0_kernel(A:UOp, num:int) -> UOp:
return A[0].set(num).sink(arg=KernelInfo(f"custom_set0_{num}"))
class TestScheduleCache(unittest.TestCase):
def test_bound_variable_reuses_cache(self):
@@ -30,27 +25,27 @@ class TestScheduleCache(unittest.TestCase):
def test_custom_kernel(self):
for i in range(4):
a, b = Tensor.empty(1), Tensor.ones(1)
a = Tensor.custom_kernel(a, b, fxn=functools.partial(custom_add_kernel, num=i))[0]
a = Tensor.empty(1)
a = Tensor.custom_kernel(a, fxn=functools.partial(custom_set0_kernel, num=i))[0]
a.realize()
self.assertEqual(a.item(), i+1)
self.assertEqual(a.item(), i)
def test_same_custom_function_reuses_cache(self):
schedule_cache.clear()
fxn = functools.partial(custom_add_kernel, num=10)
fxn = functools.partial(custom_set0_kernel, num=10)
# first run
a, x = Tensor.empty(1), Tensor.ones(1)
a = Tensor.custom_kernel(a, x, fxn=fxn)[0]
a = Tensor.empty(1)
a = Tensor.custom_kernel(a, fxn=fxn)[0]
a.realize()
self.assertEqual(a.item(), 11)
self.assertEqual(a.item(), 10)
cache_size_after_first = len(schedule_cache)
# second run with same function should reuse cache
b, x = Tensor.empty(1), Tensor.ones(1)
b = Tensor.custom_kernel(b, x, fxn=fxn)[0]
b = Tensor.empty(1)
b = Tensor.custom_kernel(b, fxn=fxn)[0]
b.realize()
self.assertEqual(b.item(), 11)
self.assertEqual(b.item(), 10)
self.assertEqual(len(schedule_cache), cache_size_after_first)
def test_simple(self):
@@ -70,29 +65,5 @@ class TestScheduleCache(unittest.TestCase):
print(num)
self.assertEqual(len(schedule_cache), start_len_schedule_cache)
def test_simple_precompile(self):
@function(precompile=True, precompile_backward=True)
def f(x:Tensor) -> Tensor:
out = Tensor.invalids(*x.shape, dtype=x.dtype, device=x.device)
out = Tensor.custom_kernel(out, x, fxn=functools.partial(custom_add_kernel, num=10), grad_fxn=custom_add_backward)[0]
return out + x
# warmup
x = Tensor.ones(1).realize()
out = f(x)
out.backward(x)
self.assertEqual(out.item(), 12)
self.assertEqual(x.grad.item(), 2)
# use the cache next time function is called
start_len_schedule_cache = len(schedule_cache)
for _ in range(3):
x = Tensor.ones(1).realize()
out = f(x)
out.backward(x)
self.assertEqual(out.item(), 12)
self.assertEqual(x.grad.item(), 2)
self.assertEqual(len(schedule_cache), start_len_schedule_cache)
if __name__ == "__main__":
unittest.main()
+4 -4
View File
@@ -12,7 +12,7 @@ from tinygrad.dtype import dtypes, AddrSpace
# import all pattern matchers here
from tinygrad.codegen.gpudims import pm_add_gpudims
from tinygrad.uop.symbolic import sym, symbolic_simple, symbolic, pm_move_where_on_load, pm_clean_up_group_sink, pm_remove_invalid
from tinygrad.uop.symbolic import sym, symbolic_simple, symbolic, pm_fold_cast_const, pm_move_where_on_load, pm_clean_up_group_sink, pm_remove_invalid
from tinygrad.uop.movement import mop_cleanup
from tinygrad.codegen.decomp.dtype import pm_dtype_decomps
from tinygrad.codegen.decomp.op import get_late_rewrite_patterns, get_simplifying_rewrite_patterns
@@ -301,7 +301,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
sink = graph_rewrite(sink, pm_split_ranges+pm_flatten_range, ctx={}, name="split ranges")
# symbolic (NOTE: this is a requirement for pm_simplify_ranges to be correct)
sink = graph_rewrite(sink, sym+pm_flatten_range, name="initial symbolic")
sink = graph_rewrite(sink, sym+pm_fold_cast_const+pm_flatten_range, name="initial symbolic")
# optimize (schedule) the AST
sink = graph_rewrite(sink, pm_flatten_range+pm_simplify_ranges, ctx={}, name="simplify ranges")
@@ -346,7 +346,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# lower index dtype
# NOTE: we need indexing_simplify to remove the cast to long using the Invalid
sink = graph_rewrite(sink, symbolic_simple+pm_lower_index_dtype+indexing_simplify, ctx={}, name="lower all index dtypes")
sink = graph_rewrite(sink, symbolic_simple+pm_fold_cast_const+pm_lower_index_dtype+indexing_simplify, ctx={}, name="lower all index dtypes")
# final symbolic before decomp
sink = graph_rewrite(sink, symbolic, name="final symbolic")
@@ -357,7 +357,7 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
# floordiv+mod / dtype decomp (early)
supported_ops = tuple(ren.code_for_op.keys())
pm_decomp = symbolic_simple+get_simplifying_rewrite_patterns(supported_ops)
pm_decomp = symbolic_simple+pm_fold_cast_const+get_simplifying_rewrite_patterns(supported_ops)
sink = graph_rewrite(sink, pm_decomp, name="early decompositions")
# late decomps + move gates from unrenderable INVALID where
+1 -2
View File
@@ -33,8 +33,7 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0))
case Ops.CAST if dt in dtypes.floats:
small = (a1.eq(0) & (a0 >= 0)) | (a1.eq(-1) & (a0 < 0))
cdt = dt if dt == dtypes.float64 else dtypes.float32
return small.where(a0.cast(dt), ((a1.cast(cdt) * (2**32)) + a0.bitcast(dtypes.uint).cast(cdt)).cast(dt))
return small.where(a0.cast(dt), ((a1.cast(dtypes.float32) * (2**32)) + a0.bitcast(dtypes.uint).cast(dtypes.float32)).cast(dt))
case Ops.CAST: return a0.bitcast(dtypes.uint).cast(dt)
case Ops.BITCAST: return a0.bitcast(dt), a1.bitcast(dt)
case Ops.SHL:
+2 -2
View File
@@ -57,7 +57,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
# get the idxs
ki: KernelInfo = s.arg
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int, param=True).cast(dtypes.weakint)]
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.weakint)]
elif ki.dont_use_locals:
assert not local_dims, "can't use locals if there's no local dims"
idxs = get_grouped_dims("idx", global_shape, ctx.global_max, reverse=True)
@@ -89,7 +89,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
pm_device_to_var = PatternMatcher([
# the DEVICE axis is not a program axis, it's bound per device at launch. lower it to the _device_num variable (like SPECIAL for devices)
(UPat(Ops.RANGE, name="r"), lambda r: UOp.variable("_device_num", 0, r.vmax, dtype=r.dtype, param=True) if r.arg[-1] is AxisType.DEVICE else None),
(UPat(Ops.RANGE, name="r"), lambda r: UOp.variable("_device_num", 0, r.vmax, dtype=r.dtype) if r.arg[-1] is AxisType.DEVICE else None),
# ENDs that closed a DEVICE range no longer close it
(UPat(Ops.END, name="e"), lambda e: e.replace(src=(e.src[0],)+tuple(s for s in e.src[1:] if s.op is not Ops.PARAM))
if any(s.op is Ops.PARAM and s.arg.name == '_device_num' for s in e.src[1:]) else None),
+1 -1
View File
@@ -26,7 +26,7 @@ def _drop_valid_stmts(valid:UOp, idx:UOp, height:int, width:int) -> list[UOp]:
# check if idx is out of bound when X is on the wrong side of the bound: X in [c+1, vmax] or [vmin, c-1]
lo, hi = (c + 1, X.vmax) if is_upper_bound else (X.vmin, c - 1)
if lo <= hi:
fake = UOp.variable(f"fake{i}", lo, hi, X.dtype, param=True)
fake = UOp.variable(f"fake{i}", lo, hi, X.dtype)
subs = [{X: fake}]
# idx may not have X itself, so also substitute a term of X: v -> fake - (X - v)
terms = list(X.split_uop(Ops.ADD))
+1 -1
View File
@@ -3,7 +3,7 @@ from tinygrad.uop.ops import PatternMatcher, UPat, Ops
from tinygrad.dtype import Invalid, dtypes
def move_where_load(gate, l, a, w):
return l.replace(src=(l.src[0], l.vconst_like(0) if a.is_invalid else l.const_like(a.val) if a.op is Ops.CONST else
return l.replace(src=(l.src[0], l.vconst_like(0) if a.is_invalid else
a.src[0] if a.op is Ops.CAST and a.src[0].dtype == l.dtype else a.cast(l.dtype), l.src[2])).cast(w.dtype)
pm_move_gates_from_index = PatternMatcher([
+3 -3
View File
@@ -1,7 +1,7 @@
import itertools
from typing import Callable
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, graph_rewrite, _substitute, range_start, AxisType
from tinygrad.uop.symbolic import symbolic, invalid_gate
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const, invalid_gate
from tinygrad.helpers import partition
from tinygrad.dtype import dtypes
@@ -32,7 +32,7 @@ def simplify_merge_adjacent(u:UOp) -> UOp|None:
s0, s1 = r0.src[0], r1.src[0]
# do the merge
new_range = r0.replace(src=(s0*s1,))
nidx = graph_rewrite(u, _substitute+symbolic+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1},
nidx = graph_rewrite(u, _substitute+symbolic+pm_fold_cast_const+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1},
name=f"check_merge_{r0.arg[0]}_{r1.arg[0]}")
# check if it simplifies
@@ -137,7 +137,7 @@ def reduce_collapse(red:UOp, u:UOp, pm:PatternMatcher=pm_reduce_collapse) -> UOp
for u in included:
for s in u.src:
if s in included or s in replaces or s.op in {Ops.CONST, Ops.PARAM, Ops.BUFFER}: continue
replaces[s] = UOp.variable(f'in{len(replaces)}', s.vmin, s.vmax, s.dtype, param=True)
replaces[s] = UOp.variable(f'in{len(replaces)}', s.vmin, s.vmax, s.dtype)
collapse_fxn = u.substitute(replaces).reduce(r, arg=Ops.ADD)
sink = graph_rewrite(collapse_fxn, pm, name="reduce_collapse")
if not no_range(sink): return None
+10 -21
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from collections import defaultdict
from typing import Any, Callable, Generic, TypeVar, Iterator, Generator, Self, TYPE_CHECKING
from typing import Any, Generic, TypeVar, Iterator, Generator, Self, TYPE_CHECKING
import importlib, inspect, functools, pathlib, os, contextlib, re, atexit, pickle, decimal
from tinygrad.helpers import LRU, getenv, diskcache_get, diskcache_put, DEBUG, GlobalCounters, PROFILE, temp, colored
from tinygrad.helpers import Context, CCACHE, ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, cpu_events, ProfileEvent, ProfilePointEvent, suppress_finalizing
@@ -103,7 +103,7 @@ class Buffer:
def __init__(self, device:str, size:int, dtype:DType, opaque:Any=None, options:BufferSpec|None=None,
initial_value:bytes|pickle.PickleBuffer|None=None, uop_refcount=0, base:Buffer|None=None, offset:int=0, preallocate=False):
assert isinstance(dtype, DType)
self.device, self.size, self.dtype, self.options, self.offset, self.allocated_views = Device.canonicalize(device), size, dtype, options, offset, 0
self.device, self.size, self.dtype, self.options, self.offset, self.allocated_views = device, size, dtype, options, offset, 0
self._bufs: dict[str, Any] = {}
if base is None:
assert offset == 0, "base buffers can't have offset"
@@ -116,7 +116,7 @@ class Buffer:
if isinstance(initial_value, pickle.PickleBuffer): initial_value.release()
else:
assert base._base is None, "base can't have a base"
assert self.device == base.device, "base must have the same device"
assert device == base.device, "base must have the same device"
self._base = base
if preallocate: self.allocate()
@property
@@ -133,7 +133,7 @@ class Buffer:
# check if the underlying buffer is allocated, possibly from the base object
def is_allocated(self) -> bool: return self.base.is_allocated() if self._base is not None else self.device in self._bufs
def get_buf(self, device: str) -> Any:
if device not in self._bufs and (device:=Device.canonicalize(device)) not in self._bufs:
if device not in self._bufs:
allocator = Device[device].allocator
if device == self.device: self.ensure_allocated()
elif self._base is not None: self._bufs[device] = allocator._offset(self._base.get_buf(device), self.nbytes, self.offset)
@@ -331,18 +331,17 @@ class Program(Generic[DeviceType]):
wait=False) -> float|None: pass
class Compiled:
ifaces:list[Callable] = []
profile_events:list[ProfileEvent] = [ProfileDeviceEvent("CPU")] # NOTE: CPU is the default device.
has_copy_queue:bool = True
pm_lower:Any = None
pm_bufferize:Any = None
has_copy_queue:bool = True
def __init__(self, device:str, allocator:Allocator, renderers:list[type[Renderer]], runtime:type[Program[Self]]|None, graph=None, arch=None):
from tinygrad.renderer import Renderer
self.device, self.allocator, self.runtime_t, self.graph, self.renderers = device, allocator, runtime, graph, renderers or [Renderer]
self.device_id, self.arch = (int(idx) if ":" in device and (idx:=device.split(":")[1]).isdigit() else 0), arch
self.arch = arch
self.cached_renderer:dict[Any, Renderer] = {}
@property
@@ -365,21 +364,11 @@ class Compiled:
return select_first_inited(select_by_name(self.renderers, self._renderer_name, t.renderer, f"{self.device} has no renderer {t.renderer!r}"),
f"No renderer for {self.device} is available", self.cached_renderer, t)
def _select_iface(self, device:str):
self.device_id = int(device.split(":")[1]) if ":" in device else 0
assert (v:=getenv(k:=f'{type(self).__name__[:-6].upper()}_IFACE', "")) == "", \
f"{k}={v} is deprecated, use DEV={replace(DEV.target(type(self).__name__[:-6]), interface=v)} instead"
t = DEV.target(dev:=type(self).__name__[:-6])
filtered = select_by_name(self.ifaces, lambda i: i.__name__[:-5], t.interface, f"{dev} has no interface {t.interface!r}")
filtered = [i for i in filtered if t.interface.startswith("MOCK") or not i.__name__[:-5].startswith("MOCK")] # never fallback to mock ifaces
return select_first_inited([functools.partial(iface, self, self.device_id) for iface in filtered],
f"No interface for {dev}:{self.device_id} is available")
def count(self) -> int:
"""
Returns the number of physical accelerators available to the runtime.
"""
return self.iface.count if hasattr(self, 'iface') else 1
return 1
def synchronize(self):
"""
@@ -397,7 +386,7 @@ class Compiled:
"""
Called at the end of process lifetime to allow the device to finalize.
"""
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
# override this in your device implementation
if PROFILE:
@atexit.register
@@ -419,7 +408,7 @@ def enumerate_devices_str() -> Generator[str, None, None]:
ren_results, iface_results = [], []
try:
d = Device[device]
for iface in [i for i in d.ifaces if not i.__name__.startswith("MOCK")]:
for iface in [i for i in getattr(d, 'ifaces', []) if not i.__name__.startswith("MOCK")]:
try:
name = iface.__name__[:-5]
default_text, count = ("(default)", d.count()) if type(d.iface) is iface else (f"(DEV={name}+{device} to make default)", iface(d, 0).count) # type: ignore
+1
View File
@@ -66,6 +66,7 @@ class DType(metaclass=DTypeMetaClass):
def __reduce__(self): return type(self), tuple(getattr(self, f.name) for f in fields(self))
def __repr__(self): return f"dtypes.{INVERSE_DTYPES_DICT[self.name]}"
def __lt__(self, o:DType): return (self.priority, self.bitsize, self.name, self.fmt) < (o.priority, o.bitsize, o.name, o.fmt)
def scalar(self) -> DType: return self
@functools.cached_property
def min(self):
if dtypes.is_int(self): return 0 if dtypes.is_unsigned(self) else -2**(self.bitsize-1)
+1 -5
View File
@@ -44,7 +44,7 @@ def graph_split_rewrite(linear:UOp, max_batch_size:int=0) -> UOp:
current_batch, current_batch_devs = [], []
for si in linear.src:
devs = dedup([Device[x] for b in si.src[1:] if not b.is_bound_var for x in (b.device if isinstance(b.device, tuple) else (b.device,))])
devs = dedup([Device[x] for b in si.src[1:] if b.op is not Ops.BIND for x in (b.device if isinstance(b.device, tuple) else (b.device,))])
graph_t = graph_class(devs[0]) if devs[0].graph is not None else None
can_graph = graph_t is not None and graph_t.supports_uop(devs, si)
@@ -269,14 +269,10 @@ class _TinyJit(Generic[ReturnType]):
big_linear, onetime_linear = prune_linear(big_linear, set(input_buf_uops))
if DEBUG >= 1: print(f"pruned from {len(big_linear.src) + len(onetime_linear.src)} -> {len(big_linear.src)} kernels")
run_linear(onetime_linear, var_vals)
del onetime_linear
# hold all buffers reachable from live Tensors (e.g. lazy .grad created during capture), the memory planner can't suballocate those
held_bufs = set(buffers) | {u for tref in list(all_tensors) if (t:=tref()) is not None for u in t.uop.toposort() if u.op is Ops.BUFFER}
linear = jit_lower(big_linear, held_bufs, input_buf_uops)
# drop the pre-planning graph: it keeps the whole capture-time working set allocated (big_linear) or referenced (held_bufs).
# the planned linear only uses the arena/held buffers, so the intermediates must be freed before linking and first exec
del big_linear, held_bufs
self.captured = CapturedJit(ret, linear, names, expected_input_info)
ret = self.captured(input_buf_uops, var_vals)
elif self.cnt >= 2:
+21 -24
View File
@@ -3,20 +3,17 @@ from typing import cast, Iterator, Any, Sequence
import time, random, itertools, math, contextlib, weakref, array
from dataclasses import dataclass, replace, field
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansilen, all_int, prod, flatten, Context, getenv, to_tuple
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events, wait_cond
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
from tinygrad.dtype import dtypes
from tinygrad.renderer import Estimates
from tinygrad.codegen import to_program
from tinygrad.codegen.opt.postrange import args_from_ast
# **************** Helpers ****************
def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call.src[1:] if not s.is_bound_var)
def get_call_var_uops(call:UOp, prg:UOp) -> list[UOp]:
bound = {s.src[0].expr: s.src[1].src[1] for s in call.src[1:] if s.is_bound_var}
return [bound.get(v.expr, v) for v in prg.arg.vars]
def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call.src[1:] if s.op is not Ops.BIND)
def get_call_outs_ins(call:UOp) -> tuple[tuple[int, ...], tuple[int, ...]]:
ast = call.src[0]
if ast.op is Ops.PROGRAM: return tuple(ast.arg.outs), tuple(ast.arg.ins)
@@ -169,10 +166,9 @@ def exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
def exec_kernel(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
et = None
resolved = resolve_params(call, ctx.input_uops)
for device, (bufs, device_vars) in zip(to_tuple(call.src[1].device), unwrap_multi(call, [resolved[i] for i in ast.arg.globals])):
for device, (bufs, device_vars) in zip(to_tuple(call.src[1].device), unwrap_multi(call, resolve_params(call, ctx.input_uops))):
var_vals = {**ctx.var_vals, **device_vars}
prg_bufs = [b.ensure_allocated() for b in bufs]
prg_bufs = [bufs[i].ensure_allocated() for i in ast.arg.globals]
rt = get_runtime(device, ast, cache=ctx.cache)
global_size, local_size = ast.arg.launch_dims(var_vals)
with track_stats(ctx, call, device, prg_bufs, var_vals) as tm:
@@ -204,27 +200,29 @@ def exec_graph(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
return t[0]
def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
dev = cast(Any, Device[(info:= call.arg.aux).device[0]])
addrs = [(b.bufs[j] if isinstance(b:=_resolve(ctx.input_uops[k], ctx.input_uops).buffer, MultiBuffer) else b).get_buf(dev_name).va_addr
for devs, idxs in info.input_idxs for j, dev_name in enumerate(devs) for k in idxs]
dev.rt_buffer._buf.cpu_view().view(offset=(base:=dev.rt_allocator.alloc(len(addrs) * 8)), fmt='Q')[:len(addrs)] = array.array('Q', addrs)
if (info:=call.arg.aux).inputs is not None:
bufs = [_resolve(ctx.input_uops[i], ctx.input_uops).buffer for i in call.arg.aux.input_idxs]
table = call.src[1+info.inputs].buffer
for j,dev in enumerate(call.arg.aux.device):
addrs = array.array('Q', [(b.bufs[j] if isinstance(b, MultiBuffer) else b).get_buf(dev).va_addr for b in bufs])
mv = (table.bufs[j] if isinstance(table, MultiBuffer) else table).ensure_allocated()._buf.cpu_view().view(fmt='Q')
wait_cond(lambda: mv[0], value=0, timeout_ms=ctx.timeout or getenv("HCQDEV_WAIT_TIMEOUT_MS", 30000), msg=f"{dev} hang detected")
mv[:len(addrs)] = addrs
tables = [UOp.from_buffer(dev.rt_buffer.view(len(idxs), dtypes.uint64, base + j*len(idxs)*8), HCQ_RUNTIME_DEV.value)
for devs, idxs in info.input_idxs for j in range(len(devs))]
if info.inputs is not None: call = call.substitute({call.src[1+info.inputs]: UOp.mstack(*tables)})
exec_kernel(replace(ctx, update_stats=DEBUG>=3, var_vals={**ctx.var_vals, "hcq_inputs_ptr": dev.rt_buffer._buf.va_addr + base}), call, ast)
exec_kernel(replace(ctx, update_stats=DEBUG>=3), call, ast)
tms = []
for devices, stat_call, prof in info.kernels:
for devices,name,estimates,prof in info.kernels:
for device in devices:
tm = None
d, tm = cast(Any, Device[device]), None
if prof:
(d:=cast(Any, Device[device])).prof_ents[prof[0]] = ProfileGraphEntry(device, stat_call.arg.name, *prof)
d.prof_ents[prof[0]] = ProfileGraphEntry(device, name, *prof)
if ctx.wait:
d.synchronize(timeout=ctx.timeout)
st, en = (d.signal(x)._buf.cpu_view().view(fmt='Q')[0] for x in prof)
tms.append(tm:=float(en-st)/d.timestamp_divider/1e6)
with track_stats(ctx, stat_call, device, [], ctx.var_vals) as et: et[0] = tm
with track_stats(ctx, call.replace(arg=replace(call.arg, name=name, aux=replace(info, estimates=estimates))), d.device, [], ctx.var_vals) as et:
et[0] = tm
return max(tms) if tms else None
# flatten LINEAR-in-LINEAR: any nested LINEAR child gets inlined into its parent's src
@@ -264,15 +262,14 @@ pm_exec = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="validate", name="ast"),), name="call", allow_any_len=True), exec_validate),
])
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link, HCQ_RUNTIME_DEV # noqa: E402 # down here, hcq2 imports realize
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link # noqa: E402 # down here, hcq2 imports the helpers above
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, profile:bool|None=None) -> UOp:
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
linear = graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
return linear
return graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
def link_linear(linear:UOp, cache=True) -> UOp: return hcq_link(linear, cache=cache) if getenv("HCQ2") else linear
+1 -7
View File
@@ -1,5 +1,4 @@
import functools, time
from dataclasses import replace
from typing import Generic, TypeVar, Callable, cast, overload
from tinygrad.helpers import Context, dedup, getenv, DEBUG
from tinygrad.uop.ops import UOp, Ops, graph_rewrite, PatternMatcher, UPat
@@ -13,7 +12,7 @@ def add_to_ctx(ctx, x:UOp):
return ret
pm_ctx = PatternMatcher([
(UPat(Ops.BUFFER, name="x"), add_to_ctx),
(UPat((Ops.BUFFER, Ops.BIND), name="x"), add_to_ctx),
(UPat((Ops.AFTER, Ops.CONTIGUOUS), name="x"),
lambda ctx,x: add_to_ctx(ctx,x) if not x.op_in_backward_slice_with_self(Ops.PARAM) and x.op_in_backward_slice_with_self(Ops.BUFFER) else None),
])
@@ -24,10 +23,6 @@ def invalid_outputs(uret:UOp) -> set[UOp]:
return {u.src[0].buf_uop for u in uret.backward_slice_with_self
if u.op is Ops.STORE and u.src[1].base.is_invalid and not u.src[0].buf_uop.is_realized}
def renumber_invalid_outputs(uret:UOp) -> UOp:
return uret.substitute({b:b.replace(arg=replace(b.arg, slot=i))
for i,b in enumerate(x for x in uret.toposort(enter_calls=False) if x in invalid_outputs(uret))})
ReturnType = TypeVar('ReturnType')
class _function(Generic[ReturnType]):
depth = 0
@@ -70,7 +65,6 @@ class _function(Generic[ReturnType]):
# the BUFFERs that are left are the implicit inputs
num_explicit = len(call_uops)
uret = graph_rewrite(uret, pm_ctx, (call_uops, invalid_outputs(uret)), bottom_up=True, name="get_implicit_inputs")
uret = renumber_invalid_outputs(uret)
name = getattr(self.fxn, '__qualname__', None) or type(self.fxn).__qualname__
if not self.allow_implicit:
implicit_buffers = [x for x in call_uops[num_explicit:] if x.op is Ops.BUFFER]
+1 -5
View File
@@ -486,15 +486,11 @@ def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip
if length and (file_size:=os.stat(fp).st_size) < length: raise RuntimeError(f"fetch size incomplete, {file_size} < {length}")
return fp
# not all firmware exists at the pinned ref; newer files can be pinned to the commit that introduced them without
# affecting any other firmware (blob contents are checked by sha256 anyway)
FW_REF = "1e2c15348485939baf1b6d1f5a7a3b799d80703d"
FW_REF_OVERRIDES = {"psp_13_0_15_sos.bin": "23e6cdf0409383e29d681c8c14cd6ffd0f394f02"}
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
if sys.version_info >= (3,14) and (p:=pathlib.Path(f"/lib/firmware/{path}/{name}.zst")).is_file():
from compression.zstd import decompress
if hashlib.sha256(b:=decompress(p.read_bytes())).hexdigest() == sha256: return b
return fetch(f"https://gitlab.com/kernel-firmware/linux-firmware/-/raw/{FW_REF_OVERRIDES.get(name, FW_REF)}/{path}/{name}",
return fetch(f"https://gitlab.com/kernel-firmware/linux-firmware/-/raw/1e2c15348485939baf1b6d1f5a7a3b799d80703d/{path}/{name}",
subdir="fw", sha256=sha256).read_bytes()
# *** Exec helpers
+17 -37
View File
@@ -1,17 +1,11 @@
from __future__ import annotations
import enum, functools, itertools, pathlib
import functools, itertools, pathlib
from dataclasses import dataclass, replace
from tinygrad import Tensor, nn, UOp, TinyJit, getenv, function, dtypes
from tinygrad import Tensor, nn, UOp, TinyJit, getenv, function
from tinygrad.nn import Linear
from tinygrad.llm.gguf import gguf_load
from tinygrad.uop.ops import resolve
class ExpertGating(enum.IntEnum):
SOFTMAX = 1
SIGMOID = 2
SOFTMAX_WEIGHT = 3 # softmax over the top-k selected logits
SQRT_SOFTPLUS = 4
@functools.cache
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)[:(dim // 2)] / dim))
@@ -67,7 +61,6 @@ class TransformerConfig:
num_experts: int = 0
num_experts_per_tok: int = 0
norm_topk_prob: bool = False
expert_gating_func: ExpertGating = ExpertGating.SOFTMAX
q_lora_rank: int = 0
kv_lora_rank: int = 0
shared_expert_dim: int = 0
@@ -110,21 +103,14 @@ class FFNBlock:
if hasattr(self, 'ffn_gate_exps'):
h = x.unsqueeze(2) # (B, T, 1, D) - add expert dim for broadcasting
logits = self.ffn_gate_inp(x)
bias = self.exp_probs_b["bias"] if hasattr(self, 'exp_probs_b') else None
gating, normalize_topk = self.config.expert_gating_func, self.config.norm_topk_prob
# fast path: without selection bias, normalized SOFTMAX is equivalent to SOFTMAX_WEIGHT
if gating == ExpertGating.SOFTMAX and bias is None and normalize_topk:
gating, normalize_topk = ExpertGating.SOFTMAX_WEIGHT, False
if gating == ExpertGating.SOFTMAX_WEIGHT: scores = logits
elif gating == ExpertGating.SOFTMAX: scores = logits.softmax(-1)
elif gating == ExpertGating.SIGMOID: scores = logits.sigmoid()
elif gating == ExpertGating.SQRT_SOFTPLUS: scores = logits.softplus().sqrt()
_, sel = pairwise_topk(scores if bias is None else scores + bias, self.config.num_experts_per_tok)
probs = scores.gather(-1, sel)
# SOFTMAX_WEIGHT applies softmax after top-k selection
if gating == ExpertGating.SOFTMAX_WEIGHT: probs = probs.softmax(-1)
if normalize_topk: probs = probs / probs.sum(axis=-1, keepdim=True)
if hasattr(self, 'exp_probs_b'):
probs = logits.sigmoid()
_, sel = pairwise_topk(probs + self.exp_probs_b["bias"], self.config.num_experts_per_tok)
probs = probs.gather(-1, sel)
if self.config.norm_topk_prob: probs = probs / probs.sum(axis=-1, keepdim=True)
else:
vals, sel = pairwise_topk(logits, self.config.num_experts_per_tok)
probs = vals.softmax(-1) if self.config.norm_topk_prob else logits.softmax(-1).gather(-1, sel)
probs = probs * self.config.routed_scaling_factor
x_down = self.ffn_down_exps(sel, (self.ffn_gate_exps(sel, h).silu() * self.ffn_up_exps(sel, h)).contiguous()) # (B, T, k, D)
out = (x_down * probs.unsqueeze(-1)).sum(axis=2) # (B, T, D)
@@ -201,8 +187,8 @@ class TransformerBlock(FFNBlock):
def _init_state(self, x:Tensor):
if not hasattr(self, "cache_kv"):
self.cache_kv = Tensor.empty(2, x.shape[0], self.config.n_kv_heads, self.config.max_context, self.config.head_dim,
dtype=dtypes.default_float, device=x.device)
# 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, device=x.device)
class MLATransformerBlock(FFNBlock):
@@ -275,14 +261,13 @@ class GatedDeltaNetBlock(FFNBlock):
def _attention(self, x:Tensor, start_pos:int|UOp) -> Tensor:
B, T, _ = x.shape
assert T == 1, "GatedDeltaNetBlock currently only supports T=1"
is_kda = hasattr(self, "ssm_g_a")
# input processing
x = x.half()
out_gate = self.ssm_g_b(self.ssm_g_a(x)) if is_kda else self.attn_gate(x)
out_gate = self.ssm_g_b(self.ssm_g_a(x)) if hasattr(self, "ssm_g_a") else self.attn_gate(x)
out_gate = out_gate.reshape(B, 1, self.num_v_heads, self.head_v_dim)
beta = self.ssm_beta(x).sigmoid().reshape(B, self.num_v_heads, 1, 1)
alpha = self.ssm_f_b(self.ssm_f_a(x)) if is_kda else self.ssm_alpha(x)
alpha = self.ssm_f_b(self.ssm_f_a(x)) if hasattr(self, "ssm_f_a") else self.ssm_alpha(x)
alpha = ((alpha.float() + self.ssm_dt["bias"]).softplus().reshape(B, self.num_v_heads, -1) *
self.ssm_a.reshape(1, self.num_v_heads, -1)).exp().unsqueeze(-2)
@@ -306,13 +291,14 @@ class GatedDeltaNetBlock(FFNBlock):
# output
core_attn_out = self.ssm_norm((recurrent_state@q).squeeze(-1).reshape(B, 1, self.num_v_heads, self.head_v_dim))
out_gate = out_gate.sigmoid() if is_kda else out_gate.silu()
out_gate = out_gate.sigmoid() if hasattr(self, "ssm_g_a") else out_gate.silu()
return self.ssm_out((core_attn_out * out_gate).reshape(B, 1, -1).cast(x.dtype))
# recurrent state can't be partially reused after divergence, force a full rebuild
def _state_reset_ops(self):
return [self.conv_state.assign(self.conv_state.const_like(0)),
self.recurrent_state.assign(self.recurrent_state.const_like(0))] if hasattr(self, "conv_state") else []
def _reusable_prefix_len(self, prefix_len:int, cached_len:int) -> int: return 0 if prefix_len != cached_len else prefix_len
def _init_state(self, x):
if not hasattr(self, "conv_state"):
@@ -340,8 +326,7 @@ class Transformer:
def forward(self, tokens:Tensor, start_pos:int|UOp, temperature:Tensor) -> Tensor:
x = self.token_embd(tokens).float() # (B, T, D)
for block in self.blk: x = block(x, start_pos)
# only run the output projection on the last token
logits = self.output(self.output_norm(x[:, -1:]))[:, -1, :]
logits = self.output(self.output_norm(x))[:, -1, :]
# Gumbel-max trick: argmax(logits/temp - log(-log(uniform))) is equivalent to sampling from softmax(logits/temp)
return (logits / temperature.maximum(1e-12) - (Tensor.rand_like(logits).maximum(1e-12).log().neg()).log()).argmax(-1, keepdim=True)
@@ -412,7 +397,6 @@ class Transformer:
qk_norm=int(state_dict['blk.0.attn_q_norm.weight'].shape[0]) if 'blk.0.attn_q_norm.weight' in state_dict else 0,
num_experts=kv.get(f'{arch}.expert_count', 0), num_experts_per_tok=kv.get(f'{arch}.expert_used_count', 0),
norm_topk_prob=kv.get(f'{arch}.expert_weights_norm', arch in ('qwen3moe', 'qwen35moe', 'kimi-linear')),
expert_gating_func=ExpertGating(kv.get(f'{arch}.expert_gating_func', ExpertGating.SOFTMAX)),
kv_lora_rank=kv_lora_rank, q_lora_rank=kv.get(f'{arch}.attention.q_lora_rank', 0),
leading_dense_blocks=kv.get(f'{arch}.leading_dense_block_count', 0),
shared_expert_dim=kv.get(
@@ -436,10 +420,6 @@ class Transformer:
for _ in range(2): list(zip(range(2), self.generate([0])))
def get_start_pos(self, tokens:list[int]) -> int:
# recurrent state can't be partially reused after divergence: reuse it only when tokens extend the cached prefix
if self.has_recurrent_block:
return len(self._cached_tokens) if self._cached_tokens and len(self._cached_tokens) < len(tokens) \
and tokens[:len(self._cached_tokens)] == self._cached_tokens else 0
prefix_len = sum(1 for _ in itertools.takewhile(lambda ab: ab[0] == ab[1], zip(tokens[:-1], self._cached_tokens)))
return min(block._reusable_prefix_len(prefix_len, len(self._cached_tokens)) for block in self.blk)
+3 -5
View File
@@ -115,8 +115,7 @@ class ElementwiseMixin(CreationMixin):
```
"""
a, b = self._broadcasted(x, reverse)
# alu, not +: _broadcasted already promoted these, and a second promote would cast -b (only a bare weak CONST is kept weak)
return a.alu(Ops.ADD, -b)
return a + (-b)
def mul(self, x: Self | ConstType, reverse: bool = False) -> Self:
"""
@@ -246,9 +245,8 @@ class ElementwiseMixin(CreationMixin):
if dtypes.is_int(a.dtype) and dtypes.is_int(b.dtype):
if rounding_mode == "trunc": return a.alu(Ops.CDIV, b)
if rounding_mode == "floor": return a.alu(Ops.FLOORDIV, b)
if dtypes.is_int(a.dtype) or a.dtype == dtypes.bool: a = a.cast(dtypes.default_float)
# alu, not *: _broadcasted already promoted these, and a second promote would cast 1/b (only a bare weak CONST is kept weak)
d = a.alu(Ops.MUL, b.reciprocal())
a = a.cast(dtypes.default_float)
d = a * b.reciprocal()
if rounding_mode is None: return d
if rounding_mode == "trunc": return d.trunc()
if rounding_mode == "floor": return d.floor()
+1 -3
View File
@@ -3,7 +3,6 @@ import math, dataclasses
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, all_metadata, broadcast_axes
from tinygrad.helpers import argsort
from tinygrad.dtype import sum_acc_dtype
from tinygrad.function import renumber_invalid_outputs
def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
if op == Ops.ADD: return (ctx._broadcast_to(ret.src[0].shape),)
@@ -33,7 +32,7 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
grad_args = ctx.src
root_grad = UOp(Ops.TUPLE, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
g if g.device is None else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
g if g.base.op is Ops.CONST else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
grads = compute_gradient(fxn, root_grad, set(params.values()))
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(fxn.src)} if k.arg.precompile else {}
@@ -41,7 +40,6 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
# collect needed gradient bodies, compact unused params, create a single backward CALL
grad_bodies = [(i, grads[p]) for i in needed if (p:=params.get(i)) is not None and p in grads]
bwd_body = UOp.maketuple(*(gb for _, gb in grad_bodies)).substitute(fwd_subs, walk=True)
bwd_body = renumber_invalid_outputs(bwd_body)
bwd_body, compact_args = _compact_params(bwd_body, (*args, *grad_args, *fwd_outs))
bwd_call = bwd_body.call(*compact_args, name=(k.arg.name or "")+"_backward", precompile=k.arg.precompile_backward)
gb_map = {i: idx for idx, (i, _) in enumerate(grad_bodies)}
-10
View File
@@ -46,16 +46,6 @@ class MovementMixin:
"""
return prod(self.shape)
@property
def max_shape(self) -> tuple[int, ...]:
"""The shape with every symbolic dimension replaced by its maximum."""
from tinygrad.uop.ops import to_max_shape # deferred: ops.py imports the mixins
return to_max_shape(self.shape)
def max_numel(self) -> int:
"""The number of elements in `max_shape`."""
return prod(self.max_shape)
def size(self, dim:int|None=None) -> sint|tuple[sint, ...]:
"""
Returns the size of the tensor. If `dim` is specified, return the length along dimension `dim`. Otherwise return the shape of the tensor.
-7
View File
@@ -289,12 +289,6 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
if value == 0: return base
return MovementMixin.pad(X.const_like(True, dtypes.bool), pads).where(base, value)
def pad_to(self, shape, *args, value:ConstType=0) -> Self:
# same mask trick as _pad_constant so the fill survives backends that realize PAD as 0-fill
ret = MovementMixin.pad_to(self, shape, *args)
if value == 0 or ret is self: return ret
return MovementMixin.pad_to(self.const_like(True, dtypes.bool), shape, *args).where(ret, value)
def _pad_circular(self, pX:tuple[tuple[sint, sint], ...]) -> Self:
# shrink first for negative pads, then wrap the non-negative remainder
X = self.shrink(tuple((-smin(pB,0), smin(pA+sh,sh)) for (pB,pA),sh in zip(pX, self.shape)))
@@ -466,7 +460,6 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
"""
assert gradient is not None or self.shape == tuple(), "when no gradient is provided, backward must be called on a scalar tensor"
if not (self.is_floating_point() and all(t.is_floating_point() for t in targets)): raise RuntimeError("only float Tensors have gradient")
if any(t.dtype in dtypes.weaks for t in targets): raise RuntimeError("cannot take gradient wrt a weak Tensor")
from tinygrad.mixin.gradient import compute_gradient
if gradient is None: gradient = self.const_like(1.0)
target_uops = [t._uop for t in targets]
+4 -4
View File
@@ -35,8 +35,8 @@ class Estimates:
while len(buf.src) and buf.op is not Ops.PARAM: buf = buf.src[0]
if buf.op is Ops.PARAM:
# u.src[0] is INDEX, cap at buffer size for re-reads (e.g. matmul)
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.itemsize * mults
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.itemsize)
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.scalar().itemsize * mults
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.scalar().itemsize)
if u.op is Ops.RANGE:
mult_stack.append(mults)
if u.dtype is not dtypes.void: # unbounded loop, unknown trip count
@@ -47,9 +47,9 @@ class Estimates:
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
elif u.op is Ops.LOAD and u.src[0].addrspace != AddrSpace.REG:
lds += u.max_numel() * u.dtype.itemsize * mults
lds += u.max_numel() * u.dtype.scalar().itemsize * mults
elif u.op is Ops.STORE and u.src[0].addrspace != AddrSpace.REG:
lds += u.max_numel() * u.src[1].dtype.itemsize * mults
lds += u.max_numel() * u.src[1].dtype.scalar().itemsize * mults
elif u.op in GroupOp.ALU and u not in excluded:
flops += (mults * (2 if u.op is Ops.MULACC else 1)) * u.max_numel()
elif u.op is Ops.WMMA and u not in excluded:
+25 -23
View File
@@ -7,6 +7,7 @@ from tinygrad.helpers import strip_parens, getenv, prod, dedup, Target, NUM_CPU_
from tinygrad.dtype import dtypes, DType, AddrSpace, truncate, float_to_bf16
from tinygrad.renderer import Renderer
base_rewrite = PatternMatcher([
# local/reg buffers
(UPat(Ops.BUFFER, name="x"), lambda ctx,x: ctx.render_buffer(x)),
@@ -19,9 +20,21 @@ base_rewrite = PatternMatcher([
(UPat(Ops.IF, name="x"), lambda ctx,x: f"if ({ctx[x.src[0]]}) {{"),
(UPat((Ops.ENDIF, Ops.END)), lambda ctx: "}"),
# casting
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"__builtin_convertvector({ctx[x.src[0]]}, {ctx.render_type(x)})" \
if x.max_numel() > 1 and x.addrspace is AddrSpace.REG else None),
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({ctx.render_cast(x, ctx[x.src[0]])})"),
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: ctx[x.src[0]] if x.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"__builtin_bit_cast({ctx.render_type(x)}, ({ctx.render_type(x.src[0])})({ctx[x.src[0]]}))"),
# GPU stuff
(UPat(Ops.BARRIER), lambda ctx: ctx.barrier),
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: f"{ctx.code_for_workitem[x.arg[0]](x.arg[-1])}; /* {(x.src[0]).render()} */"),
# const
(UPat(Ops.CONST, dtype=dtypes.floats, name="x"), lambda ctx,x: None if math.isfinite(v:=x.val) else \
f"({ctx.render_cast(x, ctx.nan if math.isnan(v) else ctx.infinity if v > 0 else f'-{ctx.infinity}')})"),
(UPat(Ops.CONST, arg=math.inf, name="x"), lambda ctx, x: f"({ctx.render_cast(x, ctx.infinity)})"),
(UPat(Ops.CONST, arg=-math.inf, name="x"), lambda ctx, x: f"({ctx.render_cast(x, f'-{ctx.infinity}')})"),
(UPat(Ops.CONST, dtype=dtypes.floats, name="x"), lambda ctx,x: f"({ctx.render_cast(x, ctx.nan)})" if math.isnan(x.val) else None),
(UPat(Ops.CONST, dtype=dtypes.float, name="x"), lambda ctx,x: f"{x.val}f"),
(UPat(Ops.CONST, dtype=dtypes.int64, name="x"), lambda ctx,x: f"{x.val}l"),
(UPat(Ops.CONST, dtype=dtypes.uint64, name="x"), lambda ctx,x: f"{truncate[x.dtype](x.val)}ul"),
@@ -34,17 +47,6 @@ base_rewrite = PatternMatcher([
# default const render
(UPat(Ops.CONST, name="x"), lambda ctx,x: str(x.val)),
# casting
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"__builtin_convertvector({ctx[x.src[0]]}, {ctx.render_type(x)})" \
if x.max_numel() > 1 and x.addrspace is AddrSpace.REG else None),
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({ctx.render_cast(x, ctx[x.src[0]])})"),
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: ctx[x.src[0]] if x.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL) else None),
(UPat(Ops.BITCAST, name="x"), lambda ctx,x: f"__builtin_bit_cast({ctx.render_type(x)}, ({ctx.render_type(x.src[0])})({ctx[x.src[0]]}))"),
# GPU stuff
(UPat(Ops.BARRIER), lambda ctx: ctx.barrier),
(UPat(Ops.SPECIAL, name="x"), lambda ctx,x: f"{ctx.code_for_workitem[x.arg[0]](x.arg[-1])}; /* {(x.src[0]).render()} */"),
# SHRINK/INDEX
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var('idx')), name="x"), lambda ctx,**kwargs: ctx.render_index(**kwargs)),
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var('idx'), UPat.cvar()), name="x"), lambda ctx,**kwargs: ctx.render_index(**kwargs)),
@@ -105,11 +107,11 @@ def uops_to_dtypes(uops:list[UOp]) -> list[tuple[DType, int]]:
def _wmma_name(u:UOp) -> str:
# sanitize spaces in DType.name (int8 = "signed char")
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.name}".replace(" ", "_")
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}".replace(" ", "_")
# (name, dims, dtype_in, dtype_out, device, threads, upcast_sizes)
def wmma_args(uops:list[UOp]):
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype, *(uop.arg[2:4]),
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype.scalar(), *(uop.arg[2:4]),
tuple(uop.src[i].shape[-1] for i in range(3)))
for uop in uops if uop.op is Ops.WMMA)
@@ -180,8 +182,8 @@ class CStyleLanguage(Renderer):
if addrspace in (AddrSpace.LOCAL, AddrSpace.GLOBAL) or override_ptr:
suffix = "*"
if sz > 1:
return prefix + self.type_map.get(dtype, dtype.name).replace(" ", "_") + str(sz) + suffix
return prefix + self.type_map.get(dtype, dtype.name) + suffix
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name).replace(" ", "_") + str(sz) + suffix
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name) + suffix
def render_type(self, u:UOp): return self._render_dtype(u.dtype, u.max_numel(), u.addrspace, shape=u._shape)
def render_access(self, u:UOp):
@@ -262,7 +264,6 @@ class ClangRenderer(CStyleLanguage):
nan = '__builtin_nanf("")'
# language options
barrier = "__atomic_thread_fence(__ATOMIC_SEQ_CST);"
buffer_suffix = " restrict"
type_map = {dtypes.bool:"_Bool", dtypes.half:"__fp16"}
code_for_op = {**({k:v for k,v in CStyleLanguage.code_for_op.items() if k not in [Ops.EXP2, Ops.SIN, Ops.LOG2, Ops.TRUNC, Ops.RECIPROCAL]}),
@@ -470,7 +471,7 @@ class CUDARenderer(CStyleLanguage):
class NVCCRenderer(CUDARenderer):
def __init__(self, target:Target): super().__init__(target, use_nvcc=True)
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype)
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype.scalar())
def _ocml(op): return lambda x,dtype: f"__ocml_{op}_f{ {dtypes.half:16, dtypes.double:64}.get(dtype, 32)}({x})"
class HIPRenderer(CStyleLanguage):
@@ -493,9 +494,10 @@ class HIPRenderer(CStyleLanguage):
(UPat(Ops.WMMA, name="x"), lambda ctx,x: f"__{_wmma_name(x)}({ctx[x.src[0]]}, {ctx[x.src[1]]}, {ctx[x.src[2]]},"
f" {fp8_index(x.src[0].dtype)}, {fp8_index(x.src[0].dtype)}, 0, 0, 0, 0)" if x.arg[0][2] == 128 else None),
(UPat(Ops.WMMA, name="x"), lambda ctx,x: f"__{_wmma_name(x)}({ctx[x.src[0]]}, {ctx[x.src[1]]}, {ctx[x.src[2]]}, 0, 0, 0)"),
(UPat(Ops.CONST, dtypes.fp8s, name="x"), lambda ctx,x:
f"f32_to_fp8({ctx.nan if math.isnan(v:=x.val) else ctx.infinity if v == math.inf else f'-{ctx.infinity}' if v == -math.inf else f'{v}f'},"
f" {fp8_index(x.dtype)})"),
(UPat(Ops.CONST, dtypes.fp8s, name="x"), lambda ctx,x: f"f32_to_fp8({ctx.nan}, {fp8_index(x.dtype)})" if math.isnan(x.val) else None),
(UPat(Ops.CONST, dtypes.fp8s, arg=math.inf, name="x"), lambda ctx,x: f"f32_to_fp8({ctx.infinity}, {fp8_index(x.dtype)})"),
(UPat(Ops.CONST, dtypes.fp8s, arg=-math.inf, name="x"), lambda ctx,x: f"f32_to_fp8(-{ctx.infinity}, {fp8_index(x.dtype)})"),
(UPat(Ops.CONST, dtypes.fp8s, name="x"), lambda ctx,x: f"f32_to_fp8({x.val}f, {fp8_index(x.dtype)})"),
(UPat(Ops.CAST, dtypes.fp8s, (UPat(dtype=dtypes.float),), name="x",),
lambda ctx,x: f"f32_to_fp8({ctx[x.src[0]]}, {fp8_index(x.dtype)})"),
(UPat(Ops.CAST, dtypes.float, (UPat.var("y", dtypes.fp8s),), name="x",),
@@ -543,7 +545,7 @@ class HIPRenderer(CStyleLanguage):
ockl = [(f"__ockl_get_{name}", "unsigned int", "size_t", "const") for name in ["local_id", "group_id", "local_size"]]
ocml_ops = {Ops.EXP2: ("exp2", "pure"), Ops.LOG2: ("log2", "pure"), Ops.SQRT: ("sqrt", "const"), Ops.SIN: ("sin", ""), Ops.TRUNC: ("trunc", "")}
ocml = [(f"__ocml_{ocml_ops[op][0]}_f{dt.bitsize}", dt.name, dt.name, ocml_ops[op][1])
for op, dt in dedup((u.op, u.dtype) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, dtypes.double)]
for op, dt in dedup((u.op, u.dtype.scalar()) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, dtypes.double)]
if any(dt == dtypes.bfloat16 for dt, _ in used_dtypes):
prefix.append(f"typedef {'__bf16' if self.is_cdna4(self.target.arch) else 'unsigned short'} hip_bfloat16;")
if any(dt == dtypes.half for dt, _ in used_dtypes): prefix.append("#define half _Float16")
+4 -4
View File
@@ -165,7 +165,7 @@ def scratch_buffer(elem_dt:DType, count:int, slot:int) -> UOp:
return UOp.placeholder((count,), elem_dt, slot, AddrSpace.LOCAL)
def gated_load(ctx, addr:UOp, alt:UOp, gate:UOp, x:UOp):
local = scratch_buffer(addr.src[0].dtype, x.max_numel(), next(ctx))
local = scratch_buffer(addr.src[0].dtype.scalar(), x.max_numel(), next(ctx))
local_idx = local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64)
# the selected address is a 64bit value, the AFTER orders the load after the scratch store and carries the element dtype for the encoder
sel = gate.where(addr.replace(dtype=dtypes.uint64), local_idx)
@@ -173,7 +173,7 @@ def gated_load(ctx, addr:UOp, alt:UOp, gate:UOp, x:UOp):
return ptr.load(dtype=x.dtype)
def gated_store(addr:UOp, gate:UOp, val:UOp):
local = scratch_buffer(addr.src[0].dtype, val.max_numel(), -1)
local = scratch_buffer(addr.src[0].dtype.scalar(), val.max_numel(), -1)
sel = gate.where(addr.replace(dtype=dtypes.uint64), local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64))
return UOp(Ops.AFTER, addr.dtype, (sel,)).store(val)
@@ -237,7 +237,7 @@ def cmp(x:UOp) -> UOp:
return x.ins(X86Ops.CMP, dtype=dtypes.void) if (i:=to_imm(x.src[1])) is None else x.ins(X86Ops.CMPi, dtype=dtypes.void, src=(x.src[0], i))
def vcmp(x:UOp) -> UOp:
v = imm(dtypes.uint8, {Ops.CMPLT: 1, Ops.CMPNE: 4, Ops.CMPEQ: 0}[x.op])
if x.dtype is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
if x.dtype.scalar() is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
return x.ins(X86Ops.VCMPSD if x.max_numel() == 1 else X86Ops.VCMPPD, src=x.src + (v,))
# vinsertps xmm2, xmm0, xmm1, imm
@@ -252,7 +252,7 @@ def vinsertps(x:UOp) -> UOp:
# vpinsq xmm2, xmm0, rax, imm
# inserts element in rax into any position in xmm0, result is written to xmm2 according to imm
def vpins(x:UOp) -> UOp:
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.itemsize]
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.scalar().itemsize]
return functools.reduce(lambda ret,i: x.ins(op, src=(ret, x.src[i], imm(dtypes.uint8, i))), range(len(x.src)), def_reg(x.dtype))
# we don't call ctx.vreg on the srcs to avoid duplicates, a rewrite will assign the tuple of valid registers to a vreg
+3 -3
View File
@@ -142,7 +142,7 @@ base_rewrite = PatternMatcher([
(UPat(Ops.IF, name="x"), lambda ctx,x: f" br i1 {ctx[x.src[0]]}, label %ifbody_{ctx[x][1:]}, label %ifskip_{ctx[x][1:]}\nifbody_{ctx[x][1:]}:"),
(UPat(Ops.ENDIF, name="x"), lambda ctx,x: f" br label %ifskip_{ctx[x.src[0]][1:]}\nifskip_{ctx[x.src[0]][1:]}:"),
(UPat(Ops.BARRIER), lambda ctx: " fence seq_cst")
(UPat(Ops.BARRIER), lambda ctx: "")
])
class LLVMRenderer(Renderer):
@@ -238,8 +238,8 @@ class AMDLLVMRenderer(LLVMRenderer):
(UPat(Ops.CAST, dtypes.fp8s, (UPat(dtype=dtypes.float),), name="x",), lambda ctx,x:
f" {ctx[x]} = call i8 @f32_to_fp8({ldt(x.src[0].dtype)} {ctx[x.src[0]]}, i1 {'1' if x.dtype == dtypes.fp8e5m2 else '0'})"),
(UPat(Ops.CAST, dtypes.float, (UPat.var("y", dtypes.fp8s),), name="x",), lambda ctx,x,y:
f" {ctx[x]}_i32 = zext i8 {ctx[x.src[0]]} to i32\n"
f" {ctx[x]} = call float @llvm.amdgcn.cvt.f32.{'bf8' if y.dtype == dtypes.fp8e5m2 else 'fp8'}(i32 {ctx[x]}_i32, i32 0)"),
f" {ctx[x.src[0]]}_i32 = zext i8 {ctx[x.src[0]]} to i32\n"
f" {ctx[x]} = call float @llvm.amdgcn.cvt.f32.{'bf8' if y.dtype == dtypes.fp8e5m2 else 'fp8'}(i32 {ctx[x.src[0]]}_i32, i32 0)"),
]) + base_rewrite
extra_matcher = LLVMRenderer.extra_matcher + create_non_native_float_pats(dtypes.fp8s) + PatternMatcher([
# amd llvm intrinsics llvm.log2/llvm.exp2 don't support double
+1 -2
View File
@@ -137,8 +137,7 @@ class NIRRenderer(Renderer):
(UPat(Ops.CAST, (dtypes.uchar, dtypes.ushort), src=(UPat.var("x", dtypes.floats),), name="c"), lambda x,c: x.cast(dtypes.int32).cast(c.dtype)),
# load/store use pointer arithmetic, and the cast does nothing. NOTE: this doesn't apply to image indexing cause it's 1-D
(UPat((Ops.INDEX, Ops.SHRINK), src=(UPat.var("buf"), UPat.var("off")), allow_any_len=True, name="x"), lambda x,buf,off: x.replace(
src=(buf,UOp.const(off.val, dtypes.long) if off.op is Ops.CONST else off.cast(dtypes.long))+x.src[2:])
if buf.addrspace != AddrSpace.REG and not is_image_shape(buf._shape) else None),
src=(buf,off.cast(dtypes.long))+x.src[2:]) if buf.addrspace != AddrSpace.REG and not is_image_shape(buf._shape) else None),
# images need index to be int for nir (coordinates only: the INDEX keeps its access dtype)
(UPat.var("buf").index(UPat.var("idx_y"), UPat.var("idx_x"), name="x"),
lambda x,buf,idx_y,idx_x: x.replace(src=(buf, idx_y.cast(dtypes.int), idx_x.cast(dtypes.int)))),
+13 -13
View File
@@ -64,7 +64,7 @@ def render_wmma(ctx: "PTXRenderer", wmma: UOp):
for src, regs in zip(wmma.src, ctx.wmma_r):
for i, reg in enumerate(regs): # pack input and acc registers
if (elems_per_reg := 4 // src.dtype.itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
if (elems_per_reg := 4 // src.dtype.scalar().itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
else: yield f"mov.b32 {reg}, {{{', '.join(ctx.r[src][i * elems_per_reg : (i+1) * elems_per_reg])}}};"
dt_map_in, dt_map_out = {dtypes.float: "tf32", dtypes.half: "f16"}, {dtypes.float: "f32", dtypes.half: "f16"}
@@ -101,17 +101,17 @@ string_rewrite = PatternMatcher([
if loc.addrspace == AddrSpace.REG else None),
(UPat(Ops.STORE, src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("var"))),
lambda ctx, loc, var: f"st.{mem_type(loc)}" + \
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype]} " + \
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype.scalar()]} " + \
f"[{ctx.r[loc]}+0], {('{' + ', '.join(ctx.r[var]) + '}') if var.max_numel() > 1 else ctx.r[var]};"),
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("alt"), UPat.var("gate"))),
lambda ctx, x, loc, alt, gate: flatten([
[f"mov.{ctx.mem_types[x.dtype]} {v}, {render_val(0, x.dtype)};" for v in ctx.r[x]],
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
[f"mov.{ctx.mem_types[x.dtype.scalar()]} {v}, {render_val(0, x.dtype.scalar())};" for v in ctx.r[x]],
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
]) if alt.max_numel() > 1 else [
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype.scalar()]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype.scalar()][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"),)),
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
if x.max_numel() > 1 else f"ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];"),
# simple
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
@@ -197,7 +197,7 @@ class PTXRenderer(Renderer):
r[u] = [cast(str,r[x]) for x in u.src]
continue
if u.op is Ops.BUFFER and u.addrspace == AddrSpace.REG:
r[u] = [ssa("reg", u, self.types[u.dtype]) for _ in range(u.max_numel())]
r[u] = [ssa("reg", u, self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
continue
if u.op in {Ops.INDEX, Ops.SHRINK, Ops.LOAD} and u.src[0].addrspace in (AddrSpace.REG, AddrSpace.ALU):
# on REG, INDEX/SHRINK pick the register (must be CONST) and LOAD is a noop
@@ -207,14 +207,14 @@ class PTXRenderer(Renderer):
continue
if u.op is Ops.SPECIAL: r[u] = "%" + u.arg
elif u.op is Ops.LOAD:
r[u] = [ssa('val', dtype=self.types[u.dtype]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
r[u] = [ssa('val', dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
elif u.op is Ops.PARAM: bufs.append((f"data{u.arg.slot}", u))
elif u.op is Ops.WMMA:
# registers for packing/unpacking input and acc
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.itemsize)],
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.itemsize)],
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.itemsize)]]
r[u] = [ssa("wmma", dtype=self.types[u.dtype]) for _ in range(u.max_numel())]
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.scalar().itemsize)],
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.scalar().itemsize)],
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.scalar().itemsize)]]
r[u] = [ssa("wmma", dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
+2 -3
View File
@@ -50,9 +50,8 @@ wgsl_matcher = PatternMatcher([
(UPat.store(UPat.var("b"), UPat.var("var"), name="s"), lambda b,var,s: packed_store(b,var) if is_packed(s) else None),
(UPat.var("a") << UPat.var("b"),lambda a,b:(a.bitcast(dtypes.uint32)<<b.cast(dtypes.uint32)).bitcast(a.dtype) if b.dtype!=dtypes.uint32 else None),
(UPat.var("x") >> UPat.var("y"), lambda x,y: UOp(Ops.SHR, x.dtype, (x,y.cast(dtypes.uint))) if y.dtype != dtypes.uint else None),
# fix nan check: 'a != a -> is_nan()'. the decomp rewrites (a != a).logical_not() to CMPEQ, so match both forms
(UPat.var("a", dtypes.floats) != UPat.var("a"), is_nan),
(UPat.var("a", dtypes.floats).alu(Ops.CMPEQ, UPat.var("a")), lambda a: is_nan(a).ne(True)),
# fix nan check: 'a != a -> is_nan()'
(UPat.var("a") != UPat.var("a"), is_nan),
])
class WGSLRenderer(CStyleLanguage):
-1
View File
@@ -3,7 +3,6 @@ hashes = {
'psp_13_0_10_sos.bin': '0bcaaad9cd8578d3841ae69155a6bd4fc3ceae8f4fb5a6ba4f576e7ace94d1d9',
'psp_13_0_12_sos.bin': '89da90bf4286b38678b1fd175c78462a426afa3d258d15872cd14072d7098b9b',
'psp_13_0_14_sos.bin': 'a4f0d5f76d27b77409ec0b71d7cc6a848ddfd29f8c84f3003edf74ad3999fb7d',
'psp_13_0_15_sos.bin': '3b28d53e75a88131155e3931378ac8434eca4880ada9211d3b4e8915b6289583',
'psp_13_0_6_sos.bin': '27657daa0f91ad8095d3610224a7de748b8b348a4cb211ecb5fccabe47369716',
'psp_13_0_7_sos.bin': 'ef1af0ecea38abbac6f85cce71789f19848c498d0cb8ef13748dab2d65b23c31',
'psp_14_0_2_sos.bin': '7b538448b57d4f9dd06b2eea90d4f86a16e65e3027cdecee8db71c2c5f1fa243',
+9 -7
View File
@@ -842,7 +842,7 @@ class KFDIface:
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,0x75a8)),), vram_bar=0,
super().__init__(dev, dev_id, vendor=0x1002, devices=((0xffff, (0x74a1,0x744c,0x7480,0x7550,0x7551,0x7590,0x75a0,0x75b0)),), vram_bar=0,
va_start=AMMemoryManager.va_allocator.base, va_size=AMMemoryManager.va_allocator.size, dev_impl_t=AMDev)
self._compute_props()
@@ -880,11 +880,6 @@ class PCIIface(PCIIfaceBase):
doorbell_index = self.dev_impl.gfx.setup_ring(*(rcvr_params:=(ring.va_addr, ring.size, gart.va_addr+rptr, gart.va_addr+wptr,
eop_buffer.va_addr, eop_buffer.size, is_aql:=(queue_type==kfd.KFD_IOC_QUEUE_TYPE_COMPUTE_AQL), is_aql)))
if queue_type == kfd.KFD_IOC_QUEUE_TYPE_SDMA and self.dev_impl.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}:
# aqua (NBIO 7.9): kernel submits SDMA queues by writing the RB_WPTR register directly (SDMA 4.4.4 doorbell regs are firmware-managed)
doorbell = self.dev_impl.mmio.view(self.dev_impl.reg('regSDMA_GFX_RB_WPTR').addr[idx] * 4, 8, fmt='Q')
return AMDQueueDesc(ring=ring.cpu_view().view(fmt='I'), doorbell=doorbell, put_value=0, params=rcvr_params,
read_ptr=gart.cpu_view().view(offset=rptr, size=8, fmt='Q'), write_ptr=gart.cpu_view().view(offset=wptr, size=8, fmt='Q'))
return AMDQueueDesc(ring=ring.cpu_view().view(fmt='I'), doorbell=self.dev_impl.doorbell64.view(doorbell_index * 8, 8, fmt='Q'), put_value=0,
read_ptr=gart.cpu_view().view(offset=rptr, size=8, fmt='Q'), write_ptr=gart.cpu_view().view(offset=wptr, size=8, fmt='Q'), params=rcvr_params)
@@ -949,7 +944,9 @@ class AMDDevice(HCQCompiled):
def is_usb(self) -> bool: return isinstance(self.iface, USBIface)
def __init__(self, device:str=""):
self.iface = self._select_iface(device)
self.device_id = int(device.split(":")[1]) if ":" in device else 0
self.iface = self._select_iface()
self.target:tuple[int, ...] = ((trgt:=self.iface.props['gfx_target_version']) // 10000, (trgt // 100) % 100, trgt % 100)
self.arch = "gfx%d%x%x" % self.target
@@ -1099,6 +1096,11 @@ class AMDDevice(HCQCompiled):
def on_device_hang(self): self.iface.on_device_hang()
def finalize(self):
try: super().finalize()
finally:
if self.is_am(): self.iface.dev_impl.release_vf_access()
def device_props(self): return self.iface.props
def hw_copy_queues(self): return [(f"SDMA:{i}", functools.partial(unwrap(self.hw_copy_queue_t), queue_idx=i)) for i in self.sdma_queues]
+13 -13
View File
@@ -24,10 +24,10 @@ class CLCompiler(Compiler):
super().__init__(f"compile_cl_{compile_key}")
def compile(self, src:str) -> bytes:
program = checked(cl.clCreateProgramWithSource(self.dev.context, 1, to_char_p_p([src.encode()]), None, status := ctypes.c_int32()), status)
build_status: int = cl.clBuildProgram(program, 1, self.dev.cl_dev, None, BP_CB(), None)
build_status: int = cl.clBuildProgram(program, 1, self.dev.device_id, None, BP_CB(), None)
if build_status != 0:
cl.clGetProgramBuildInfo(program, self.dev.cl_dev, cl.CL_PROGRAM_BUILD_LOG, 0, None, log_size := ctypes.c_size_t())
cl.clGetProgramBuildInfo(program, self.dev.cl_dev, cl.CL_PROGRAM_BUILD_LOG,
cl.clGetProgramBuildInfo(program, self.dev.device_id, cl.CL_PROGRAM_BUILD_LOG, 0, None, log_size := ctypes.c_size_t())
cl.clGetProgramBuildInfo(program, self.dev.device_id, cl.CL_PROGRAM_BUILD_LOG,
log_size.value, mstr := ctypes.create_string_buffer(log_size.value), None)
raise CompileError(f"OpenCL Compile Error\n\n{mstr.value.decode()}")
check(cl.clGetProgramInfo(program, cl.CL_PROGRAM_BINARY_SIZES, ctypes.sizeof(ctypes.c_size_t), binary_sizes := (ctypes.c_size_t * 1)(), None))
@@ -39,11 +39,11 @@ class CLCompiler(Compiler):
class CLProgram(Program['CLDevice']):
def __init__(self, device:CLDevice, obj:TinyELF):
self.dev, self.lib, self.signature = device, device.cl_compiler.compile_cached(obj.lib.decode()), obj.signature
self.program = checked(cl.clCreateProgramWithBinary(device.context, 1, device.cl_dev, (ctypes.c_size_t * 1)(len(self.lib)),
self.program = checked(cl.clCreateProgramWithBinary(device.context, 1, device.device_id, (ctypes.c_size_t * 1)(len(self.lib)),
to_char_p_p([self.lib], ctypes.c_ubyte), binary_status := ctypes.c_int32(),
errcode_ret := ctypes.c_int32()), errcode_ret)
check(binary_status.value)
check(cl.clBuildProgram(self.program, 1, device.cl_dev, None, BP_CB(), None)) # NOTE: OSX requires this
check(cl.clBuildProgram(self.program, 1, device.device_id, None, BP_CB(), None)) # NOTE: OSX requires this
self.kernel = checked(cl.clCreateKernel(self.program, obj.name.encode(), status := ctypes.c_int32()), status)
def __del__(self):
@@ -101,17 +101,17 @@ class CLDevice(Compiled):
CLDevice.device_ids = c.init_c_var((cl.cl_device_id * num_devices.value),
lambda x: check(cl.clGetDeviceIDs(platform_ids[0], device_type, num_devices, x, None)))
self.cl_dev = CLDevice.device_ids[0 if ":" not in device else int(device.split(":")[1])]
self.device_name = (cl.clGetDeviceInfo(self.cl_dev, cl.CL_DEVICE_NAME, 256,
self.device_id = CLDevice.device_ids[0 if ":" not in device else int(device.split(":")[1])]
self.device_name = (cl.clGetDeviceInfo(self.device_id, cl.CL_DEVICE_NAME, 256,
buf:=ctypes.create_string_buffer(256), None), buf.value.decode())[1]
self.driver_version = (cl.clGetDeviceInfo(self.cl_dev, cl.CL_DRIVER_VERSION, 256,
self.driver_version = (cl.clGetDeviceInfo(self.device_id, cl.CL_DRIVER_VERSION, 256,
buf:=ctypes.create_string_buffer(256), None), buf.value.decode())[1]
if DEBUG >= 1: print(f"CLDevice: opening {self.device_name} with version {self.driver_version}")
self.context = checked(cl.clCreateContext(None, 1, self.cl_dev, CC_CB(), None, status := ctypes.c_int32()), status)
self.queue = checked(cl.clCreateCommandQueue(self.context, self.cl_dev, cl.CL_QUEUE_PROFILING_ENABLE, status), status)
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] = []
check(cl.clGetDeviceInfo(self.cl_dev, cl.CL_DEVICE_EXTENSIONS, 0, None, ctypes.byref(exts_len:=ctypes.c_size_t())))
self.device_exts = (cl.clGetDeviceInfo(self.cl_dev, cl.CL_DEVICE_EXTENSIONS, exts_len.value,
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]
@@ -119,7 +119,7 @@ class CLDevice(Compiled):
arch = ",".join(self.device_exts)
if "cl_khr_image2d_from_buffer" in self.device_exts:
check(cl.clGetDeviceInfo(self.cl_dev, cl.CL_DEVICE_IMAGE_PITCH_ALIGNMENT, 4, ctypes.byref(ipa := ctypes.c_uint32()), None))
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}"
super().__init__(device, CLAllocator(self), [OpenCLRenderer], CLProgram, arch=arch)
+112 -135
View File
@@ -1,11 +1,11 @@
from __future__ import annotations
import platform, sys, os, ctypes, functools, mmap, threading, array, struct, time
from dataclasses import dataclass, replace
from typing import cast, Callable
from tinygrad.helpers import to_mv, from_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le, to_tuple
from tinygrad.device import Buffer, BufferSpec, TinyELF, Program, Device
from tinygrad.runtime.support.hcq import HCQBuffer, MMIOInterface
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, make_cmdbuf, make_signal
import platform, sys, os, ctypes, ctypes.util, functools, mmap, threading, array, itertools
from dataclasses import replace
from typing import cast
from tinygrad.helpers import to_mv, OSX, WIN, Context, mv_address, suppress_finalizing, unwrap, data64_le, partition
from tinygrad.device import Buffer, BufferSpec, TinyELF
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, HCQArgsState, HCQSignal, HCQProgram, MMIOInterface
from tinygrad.runtime.support.hcq import CLikeArgsState
from tinygrad.renderer.cstyle import ClangRenderer
from tinygrad.renderer.llvmir import CPULLVMRenderer
from tinygrad.renderer.nir import LVPRenderer
@@ -13,15 +13,11 @@ from tinygrad.renderer.isa.x86 import X86Renderer
from tinygrad.runtime.support.elf import jit_loader
from tinygrad.runtime.autogen import libc
from tinygrad.codegen import do_to_program
from tinygrad.engine.realize import pm_flatten_linear, get_call_arg_uops, get_call_var_uops, get_runtime
from tinygrad import UOp, dtypes
from tinygrad.dtype import AddrSpace
from tinygrad.uop.ops import KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
from tinygrad.uop.ops import sint, KernelInfo, Ops, UPat, PatternMatcher, graph_rewrite
MAX_ARGS, CMD_SIZE, RING_SLOTS, FUNCS = 63, 64, (16 << 10), (() if WIN else ('clock_gettime', 'sem_wait', 'sem_post'))
# *****************
# 1. workers
MAX_ARGS, CMD_SIZE, RING_SLOTS = 63, 64, (16 << 10)
def signal_prog():
val = UOp.param(1, dtypes.int, (), vmin_vmax=(0, dtypes.int.max), name="value", addrspace=AddrSpace.ALU)
@@ -39,87 +35,79 @@ def timestamp_prog():
val = ts.after(call)[0].load() * 1_000_000_000 + ts.after(call)[1].load()
return UOp.param(0, dtypes.uint64, (1,))[0].store(val)
def quit_prog():
fn = UOp.param(0, dtypes.uint64, (1 if WIN else 3,))
if WIN: return fn[0].load().call(UOp.const(0, dtypes.uint64), ret_dtype=dtypes.void) # ExitThread(0)
sem = UOp.param(1, dtypes.uint64, (1,))
close = fn[2].load().call(sem[0], ret_dtype=dtypes.void) # sem_close(sem)
return fn.after(close)[0].load().call(UOp.const(0, dtypes.uint64), ret_dtype=dtypes.void) # pthread_exit(0)
def worker_prog():
ring = UOp.param(0, dtypes.uint64, (RING_SLOTS * CMD_SIZE,), volatile=True)
wait, done = UOp.param(1, dtypes.uint64, (1,), volatile=True), UOp.param(2, dtypes.uint64, (1,), volatile=True)
sem, cur = UOp.param(3, dtypes.uint64, (1,)), UOp.range(2**64-1, 0, dtype=dtypes.uint64) # sem is unused on windows, it has to come last
wait, sem = UOp.param(1, dtypes.uint64, (1,), volatile=True), UOp.param(2, dtypes.uint64, (1,))
cur = UOp.range(2**64-1, 0, dtype=dtypes.uint64)
# spin on windows, sem_wait to sleep on posix
if WIN: ready = (v:=wait.after(lw:=UOp.loop(1), cur)[0].load()).end(lw, v <= cur)
else: ready = (rv:=wait.after(lw:=UOp.loop(1), cur)[0].load().call(sem.after(cur)[0], ret_dtype=dtypes.int)).end(lw, rv != 0)
entry = [ring.after(ready).index((cur % RING_SLOTS) * CMD_SIZE + i).load() for i in range(CMD_SIZE)]
return done.after(entry[0].call(*entry[1:], ret_dtype=dtypes.void)).index(0).store(cur + 1).end(cur)
return entry[0].call(*entry[1:], ret_dtype=dtypes.void).end(cur)
@dataclass
class CPUWorker: ring:Buffer; put:Buffer; sem:Buffer; sys:Buffer; done:Buffer; thread:threading.Thread # noqa: E702
def host_wait(ctx, dst:UOp, val:UOp) -> UOp:
return (cur:=dst.after(loop:=UOp.loop(next(ctx))).index(UOp.const(0, dtypes.int)).load()).end(loop, cur < val)
# *****************
# 2. queue encoders
pm_host_opsel = PatternMatcher([(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))), host_wait)])
def cpu_cmd(devs:tuple[str, ...], prog, *args:UOp) -> UOp:
progs = [get_runtime(d, prog) if isinstance(prog, UOp) else cast(CPUDevice, Device[d]).prgs[prog] for d in devs]
addrs = tuple(UOp.const(p.addr, dtypes.uint64) for p in progs)
words = ((addrs[0] if len(addrs) == 1 else UOp(Ops.STACK, dtypes.uint64, addrs)),) + args
return UOp(Ops.INS, dtypes.void, words + (UOp.const(0, dtypes.uint64),) * (CMD_SIZE - len(words)), arg="cmd")
def encode_host_queue(q:UOp) -> UOp:
# TODO: subset of hcq2 for now
spins, (store,) = partition(graph_rewrite(q, pm_host_opsel, ctx=itertools.count(), walk=True, name="host opsel").src, lambda u: u.op is Ops.END)
assert store.op is Ops.INS and store.arg == "store", f"host queue cannot encode {store.op} {store.arg}"
return store.src[0].after(*spins).index(UOp.const(0, dtypes.int)).store(store.src[1])
def cpu_exec(ctx:tuple[str, ...], call:UOp, prg:UOp) -> UOp:
args = [get_call_arg_uops(call)[i].getaddr(ctx) for i in prg.arg.globals] + [v.cast(dtypes.uint64) for v in get_call_var_uops(call, prg)]
if (core:=prg.arg.runtimevars.get('core_id')) is None: return cpu_cmd(ctx, prg, *args)
class CPUComputeQueue(HWQueue):
def __init__(self, dev):
super().__init__()
self.dev = dev
def _cmd(self, prog, args=(), vals=()): return self.exec(prg:=self.dev.prgs[prog], prg.fill_kernargs(args, vals), None, None)
def memory_barrier(self): return self
def exec(self, prg:CPUProgram, args_state:HCQArgsState, global_size, local_size):
if (lvp:=isinstance(args_state, LVPArgsState)): self.bind_args_state(args_state)
args:list[sint|None] = [args_state.buf.va_addr] if lvp else [*[x.va_addr for x in args_state.bufs], *args_state.vals]
assert len(args) <= MAX_ARGS, f"CPU programs support at most {MAX_ARGS} arguments, got {len(args)}"
for tid in range(1 if lvp else (global_size or (1,))[0]):
if not lvp and 'core_id' in prg.runtimevars: args[prg.runtimevars['core_id']] = tid
self.q(prg, *[unwrap(x) for x in args], *([0] * (MAX_ARGS - len(args))))
return self
def wait(self, signal, value=0): return self._cmd(wait_prog, (signal.base_buf,), (value,))
def timestamp(self, signal): return self._cmd(timestamp_prog, (signal.base_buf.offset(8, 8), self.dev.func_table._buf.offset(0, 8)))
def signal(self, signal, value:sint=0): return self._cmd(signal_prog, (signal.base_buf,), (value,))
def _submit(self, dev):
dev.ensure_worker()
ring_view = dev.ring.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
for off in range(0, len(self._q), CMD_SIZE):
entry = [self._q[off].addr, *self._q[off+1:off+CMD_SIZE]]
ring_view[(base:=(dev.ring_pos % RING_SLOTS) * CMD_SIZE):base+CMD_SIZE] = array.array('Q', (int(x) & ((1<<64)-1) for x in entry))
dev.ring_pos += 1
if WIN: dev.sys.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = dev.ring_pos
else: assert libc.sem_post(dev.sem) == 0
la = [cpu_cmd(ctx,prg,*args[:(cid:=(len(prg.arg.globals)+core))],UOp.const(t, dtypes.uint64),*args[cid+1:]) for t in range(prg.arg.global_size[0])]
return UOp(Ops.LINEAR, dtypes.void, tuple(la))
pm_cpu_opsel = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), cpu_exec),
(UPat(Ops.INS, arg="barrier"), lambda: UOp(Ops.NOOP, dtypes.void, ())),
(UPat(Ops.INS, arg="wait", src=(UPat(name="dst"), UPat(name="val"))),
lambda ctx, dst, val: cpu_cmd(ctx, wait_prog, dst.getaddr(ctx), val.cast(dtypes.uint64))),
(UPat(Ops.INS, arg="store", src=(UPat((Ops.BUFFER, Ops.PARAM), name="dst"), UPat(name="val"))),
lambda ctx, dst, val: cpu_cmd(ctx, signal_prog, dst.getaddr(ctx), val.cast(dtypes.uint64))),
(UPat(Ops.INS, arg="timestamp", src=(UPat(name="dst"),)),
lambda ctx, dst: cpu_cmd(ctx, timestamp_prog, dst.getaddr(ctx), *(() if WIN else (make_signal(ctx, tag="func:clock_gettime").getaddr(ctx),)))),
])
def encode_queue(q:UOp) -> UOp:
devs, queue = to_tuple(q.arg[0]), q.arg[1]
lin = graph_rewrite(q, pm_cpu_opsel+pm_flatten_linear, ctx=devs, walk=True, name=f"{queue} opsel")
cnt = sum(len(ins.src) for ins in lin.src) // CMD_SIZE
assert cnt < RING_SLOTS, f"submit of {cnt} entries doesn't fit the ring"
cmdbuf = make_cmdbuf(lin, devs, buf=UOp.placeholder((cnt*CMD_SIZE,), dtypes.uint64, next(UOp.unique_num), device=devs).rtag("cmdbuf"))
ring = UOp.placeholder((ring_words:=RING_SLOTS*CMD_SIZE,), dtypes.uint64, 0, device=devs, volatile=True).rtag(f"{queue}_ring")
put, done, sem, sysbuf = (make_signal(devs, tag=f"{queue}_{name}") for name in ("put", "done", "sem", "sys"))
# submits are serialized on the submitter, so they can bump put without atomics
ran = done.after(l:=UOp.loop(next(UOp.unique_num))).index(0).load()
room = ran.end(l, put.index(0).load() - ran > RING_SLOTS - cnt) # wait until cnt entries fit in the ring
base = ((put.after(room).index(0).load() % RING_SLOTS) * CMD_SIZE).cast(dtypes.int)
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(cmdbuf, ring))
copy = UOp.group(*[ring.index((base + e*CMD_SIZE + w) % ring_words).store(cmdbuf.index(e*CMD_SIZE + w).load()) for w in range(CMD_SIZE)])
bumped = put.after(copy.end(e)).index(0).store(put.index(0).load() + cnt)
if WIN: return sysbuf.after(bumped).index(0).store(put.after(bumped).index(0).load())
e = UOp.range(cnt, next(UOp.unique_num), dtype=dtypes.int, src=(bumped,))
return make_signal(devs, tag="func:sem_post").after(e).index(0).load().call(sem.after(e).index(0), ret_dtype=dtypes.void).end(e)
# *****************
class LVPArgsState(CLikeArgsState):
def __init__(self, buf, prg, bufs, vals=()): super().__init__(buf, prg, bufs, vals, [*data64_le(buf.va_addr + 12), (len(bufs) + len(vals)) * 2])
# NOTE: MAP_JIT is added to mmap module in python 3.13
MAP_JIT = 0x0800
class CPUProgram(Program['CPUDevice']):
class CPUProgram(HCQProgram['CPUDevice']):
rt_lib = None
try: rt_lib = ctypes.CDLL(ctypes.util.find_library('System' if OSX else 'kernel32') if OSX or WIN else 'libgcc_s.so.1')
except OSError: pass
def __init__(self, dev:CPUDevice, obj:TinyELF):
self.dev, self.name, self.signature = dev, obj.name, obj.signature
self.runtimevars = {name:slot for name,slot,*_ in obj.signature if name == 'core_id'}
self.lvp = obj.target.renderer == "LVP"
self.signature, self.runtimevars = obj.signature, {name:slot for name,slot,*_ in obj.signature if name == 'core_id'}
LVP = obj.target.renderer == "LVP"
if sys.platform == "win32": # mypy doesn't understand when WIN is used here
PAGE_EXECUTE_READWRITE, MEM_COMMIT, MEM_RESERVE = 0x40, 0x1000, 0x2000
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
@@ -129,7 +117,7 @@ class CPUProgram(Program['CPUDevice']):
ctypes.windll.kernel32.GetCurrentProcess.restype = ctypes.c_void_p
proc = ctypes.windll.kernel32.GetCurrentProcess()
ctypes.windll.kernel32.FlushInstructionCache(ctypes.c_void_p(proc), ctypes.c_void_p(self.addr), ctypes.c_size_t(len(obj.lib)))
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
else:
# On apple silicon with SPRR enabled (it always is in macos) RWX pages are unrepresentable: https://blog.svenpeter.dev/posts/m1_sprr_gxf/
# MAP_JIT allows us to easily flip pages from RW- to R-X and vice versa. It is a noop on intel cpus. (man pthread_jit_write_protect_np)
@@ -137,7 +125,7 @@ class CPUProgram(Program['CPUDevice']):
self.addr = mv_address(self.mem)
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(False)
lib = jit_loader(obj.lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m']) if self.lvp else obj.lib
lib = jit_loader(obj.lib, base=ctypes.addressof(ctypes.c_void_p.from_buffer(self.mem)), link_libs=['m']) if LVP else obj.lib
self.mem.write(lib)
if OSX: unwrap(CPUProgram.rt_lib).pthread_jit_write_protect_np(True)
@@ -150,30 +138,15 @@ class CPUProgram(Program['CPUDevice']):
# msync should be a universal POSIX way to do this
libc.msync(ctypes.c_void_p(self.addr), len(lib), libc.MS_SYNC | libc.MS_INVALIDATE)
self.fxn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.addr) if self.lvp else ctypes.CFUNCTYPE(None)(self.addr)
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
def __call__(self, *bufs:HCQBuffer, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1),
vals:tuple[int|None, ...]=(), wait:bool=False, timeout:int|None=None) -> float|None:
st = time.perf_counter()
if self.lvp:
lvp_args = bytearray(12 + (len(bufs) + len(vals)) * 8)
addr = mv_address(lvp_args)
struct.pack_into(f'<3I{len(bufs)}Q', lvp_args, 0, *data64_le(addr+12), (len(bufs)+len(vals))*2, *[b.va_addr for b in bufs])
for v,(off,dt) in zip(vals, TinyELF.iter_sig(self.signature[-len(vals):], len(bufs)*8)): struct.pack_into(f'<{dt.fmt}', lvp_args, 12+off, v)
self.fxn(addr)
else:
args = [*[cast(int, b.va_addr) for b in bufs], *cast(tuple[int, ...], vals)]
assert len(args) <= MAX_ARGS, f"CPU programs support at most {MAX_ARGS} arguments, got {len(args)}"
for tid in range(global_size[0]):
if 'core_id' in self.runtimevars: args[self.runtimevars['core_id']] = tid
self.fxn(*[ctypes.c_uint64(x) for x in args])
return time.perf_counter() - st if wait else None
super().__init__(LVPArgsState if LVP else HCQArgsState, dev, obj, kernargs_alloc_size=12+256 if LVP else 0)
@suppress_finalizing
def __del__(self):
if sys.platform == 'win32': ctypes.windll.kernel32.VirtualFree(ctypes.c_void_p(self.addr), ctypes.c_size_t(0), 0x8000) #0x8000 - MEM_RELEASE
class CPUAllocator(HCQAllocator['CPUDevice']):
class CPUAllocator(HCQAllocator):
def __init__(self, dev:CPUDevice): super().__init__(dev, supports_copy_from_disk=False, supports_transfer=False)
def _alloc(self, size:int, options:BufferSpec) -> HCQBuffer:
if options.external_ptr is not None: addr, buf = options.external_ptr, None
@@ -181,64 +154,68 @@ class CPUAllocator(HCQAllocator['CPUDevice']):
else: addr = mv_address(buf:=mmap.mmap(-1, size, mmap.MAP_ANON | mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE))
return HCQBuffer(va:=addr, sz:=size, meta=buf, view=MMIOInterface(va, sz, fmt='B'), owner=self.dev)
def _as_buffer(self, src) -> memoryview: return to_mv(src.va_addr, src.size)
def _copyin(self, dest:HCQBuffer, src:memoryview):
self.dev.synchronize()
ctypes.memmove(int(dest.va_addr), from_mv(src), len(src))
def _copyout(self, dest:memoryview, src:HCQBuffer):
self.dev.synchronize()
ctypes.memmove(from_mv(dest), int(src.va_addr), len(dest))
def _do_map(self, buf:HCQBuffer):
if buf.view is None or not isinstance(buf.view, MMIOInterface): raise RuntimeError("Cannot map buffer without view to cpu")
return HCQBuffer(buf.view.addr, buf.size, view=buf.view, owner=buf.owner)
def _unmap(self, mb): pass # CPU _do_map returns a view wrapper, nothing to release
class CPUDevice(HCQ2Compiled):
wait_timeout_ms, has_copy_queue = 30000, False
pm_lower = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_queue)])
class CPUDevice(HCQCompiled):
pm_lower = PatternMatcher([
(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),)), encode_host_queue)])
pm_bufferize = PatternMatcher([
(UPat(Ops.PARAM, tag="sentinel_signal"), lambda ctx: ctx[0].signal("sentinel", (1 << 64) - 1)),
(UPat(Ops.PARAM, tag="timeline_signal"), lambda ctx: ctx[0].signal("timeline")),
(UPat(Ops.PARAM, tag="timeline_value"), lambda ctx: ctx[0].signal("value", 1)),
(UPat(Ops.PARAM, tag="signal", name="b"), lambda ctx, b: ctx[0].signal(b.arg.slot)),
])
@functools.cache
def signal(self, name:str|int, init_value:int=0) -> Buffer:
(buf:=Buffer(self.device, 1, dtypes.uint64, preallocate=True)).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
return buf
def __init__(self, device:str=""):
self.workers:list[CPUWorker] = []
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], CPUProgram,
arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
super().__init__(device, CPUAllocator(self), [ClangRenderer, CPULLVMRenderer, LVPRenderer, X86Renderer], CPUProgram, HCQSignal,
functools.partial(CPUComputeQueue, self), arch={'amd64':'x86_64', 'aarch64':'arm64'}.get(m:=platform.machine().lower(), m)+",native")
self.pm_bufferize = PatternMatcher(
[(UPat(Ops.PARAM, tag=f"{q}_{n}"), lambda ctx, q=q,n=n: getattr(ctx[0].worker(q), n))
for q in ("COMPUTE:0", "SUBMIT:0") for n in ("ring", "put", "sem", "sys", "done")] +
[(UPat(Ops.PARAM, tag=f"func:{f}"), lambda ctx, f=f: ctx[0].func_ptr(f)) for f in FUNCS]) + self.pm_bufferize
self.ring_pos = 0
# posix uses sem to put cpus into sleep
self.sem_addr = 0
if not WIN:
self.sem = libc.sem_open(sem_name:=f"/tinygrad-{os.getpid()}-{id(self):x}".encode(), os.O_CREAT|os.O_EXCL, 0o600, 0) # type: ignore[call-arg]
self.sem_addr = unwrap(ctypes.cast(self.sem, ctypes.c_void_p).value)
if self.sem_addr == ctypes.c_void_p(-1).value or libc.sem_unlink(sem_name): raise OSError(ctypes.get_errno(), "semaphore")
# TODO: move to hcq2
with Context(EMULATED_DTYPES="", TRACK_MATCH_STATS=0):
clang = ClangRenderer(replace(self.renderer.target, renderer="CLANG"))
self.prgs:dict[Callable, CPUProgram] = {f: CPUProgram(self, do_to_program(f().sink(arg=KernelInfo(f.__name__), tag=1), clang).to_elf())
for f in (signal_prog, wait_prog, timestamp_prog, worker_prog)}
def func_ptr(self, name:str) -> Buffer: return self.func_table.view(1, dtypes.uint64, FUNCS.index(name)*8).ensure_allocated()
def synchronize(self, timeout:int|None=None):
for worker in self.workers:
put, done = (getattr(worker, x)._buf.cpu_view().view(fmt='Q') for x in ("put", "done"))
while done[0] < put[0]: self._wait_signal(done, put[0], timeout)
super().synchronize(timeout)
prgs = {f: f().sink(arg=KernelInfo(f.__name__), tag=1) for f in (signal_prog, wait_prog, timestamp_prog, quit_prog, worker_prog)}
self.prgs = {f: self.runtime(do_to_program(v, ClangRenderer(replace(self.renderer.target, renderer="CLANG"))).to_elf()) for f,v in prgs.items()}
@functools.cached_property
def ring(self) -> Buffer: return Buffer(self.device, RING_SLOTS * CMD_SIZE, dtypes.uint64, preallocate=True)
@functools.cached_property
def sys(self) -> Buffer: return Buffer(self.device, 1, dtypes.uint64, preallocate=True)
@functools.cached_property
def sem_buf(self) -> Buffer: return Buffer(self.device, 1, dtypes.uint8, options=BufferSpec(external_ptr=self.sem_addr), preallocate=True)
# TODO: move to hcq2 infra
@functools.cached_property
def func_table(self) -> Buffer:
lib = ctypes.windll.kernel32 if sys.platform == "win32" else libc.dll # type: ignore[attr-defined]
(ft:=Buffer(self.device, len(FUNCS), dtypes.uint64, preallocate=True))._buf.cpu_view().view(fmt='Q')[:] = \
array.array('Q', [unwrap(ctypes.cast(getattr(lib, f), ctypes.c_void_p).value) for f in FUNCS])
fns = ([0, ctypes.windll.kernel32.ExitThread, 0, 0] if WIN else # type: ignore[attr-defined]
[libc.dll.clock_gettime, libc.dll.pthread_exit, libc.dll.sem_wait, libc.dll.sem_close])
addrs = array.array('Q', [unwrap(ctypes.cast(f, ctypes.c_void_p).value) if f else 0 for f in fns])
(ft:=Buffer(self.device, len(fns), dtypes.uint64, preallocate=True)).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[:] = addrs
return ft
@functools.cache
def worker(self, queue:str) -> CPUWorker:
ring, put, sysbuf, done = (Buffer(self.device, sz, dtypes.uint64, preallocate=True) for sz in (RING_SLOTS*CMD_SIZE, 1, 1, 1))
addr, hsem = 0, None
def ensure_worker(self):
threading.Thread(target=cast(CPUProgram, self.prgs[worker_prog]).fxn, daemon=True, args=[ctypes.c_uint64(x) for x in
[self.ring._buf.va_addr, self.sys._buf.va_addr if WIN else self.func_table._buf.va_addr+16, self.sem_addr]]).start()
# sem are posix-only
if not WIN:
hsem = libc.sem_open(nm:=f"/tinygrad-{os.getpid()}-{id(ring):x}".encode(), os.O_CREAT|os.O_EXCL, 0o600, 0) # type: ignore[call-arg]
if (addr:=unwrap(ctypes.cast(hsem, ctypes.c_void_p).value)) == ctypes.c_void_p(-1).value or libc.sem_unlink(nm):
raise OSError(ctypes.get_errno(), "semaphore")
sem = Buffer(self.device, 1, dtypes.uint64, options=BufferSpec(external_ptr=addr), preallocate=True)
worker_args = [ring._buf.va_addr, sysbuf._buf.va_addr if WIN else self.func_ptr('sem_wait')._buf.va_addr, done._buf.va_addr, addr]
(thread:=threading.Thread(target=self.prgs[worker_prog].fxn, daemon=True, args=[ctypes.c_uint64(x) for x in worker_args])).start()
self.workers.append(worker:=CPUWorker(ring, put, sem, sysbuf, done, thread))
return worker
def finalize(self):
if self.ring_pos == 0: return # the worker starts with the first submit
ft = self.func_table._buf
CPUComputeQueue(self)._cmd(quit_prog, (ft.offset(8, 8),) if WIN else (ft.offset(8, 24), self.sem_buf._buf)).submit(self)
self.ring_pos = 0
+2 -3
View File
@@ -49,8 +49,7 @@ class DiskDevice(Compiled):
DiskDevice._tried_io_uring_init = True
if sys.platform == 'linux' and not hasattr(sys, "getandroidapilevel"):
p = io_uring.struct_io_uring_params(flags=io_uring.IORING_SETUP_SQPOLL, sq_thread_idle=0xffffffff)
fd = libc.syscall(io_uring.NR_io_uring_setup, 4096, ctypes.byref(p))
fd = libc.syscall(io_uring.NR_io_uring_setup, 4096, ctypes.byref(p:=io_uring.struct_io_uring_params()))
if fd < 0: return
sq_ptr = libc.mmap(0, p.sq_off.array + p.sq_entries * 4, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | MAP_POPULATE, fd, 0)
@@ -68,7 +67,6 @@ class DiskDevice(Compiled):
kring_mask=u32ptr(sq_ptr+p.cq_off.ring_mask), cqes=ctypes.cast(cq_ptr+p.cq_off.cqes, ctypes.POINTER(io_uring.struct_io_uring_cqe)))
DiskDevice.io_uring = io_uring.struct_io_uring(ring_fd=fd, sq=sqdesc, cq=cqdesc) # type: ignore
libc.syscall(io_uring.NR_io_uring_enter, fd, 0, 0, io_uring.IORING_ENTER_SQ_WAKEUP)
class DiskBuffer:
def __init__(self, device:DiskDevice, size:int, offset=0):
@@ -126,6 +124,7 @@ class DiskAllocator(Allocator):
# Send sqe
DiskDevice.io_uring.sq.array[sqe_index] = sqe_index
DiskDevice.io_uring.sq.ktail[0] = tail + 1
libc.syscall(io_uring.NR_io_uring_enter, DiskDevice.io_uring.ring_fd, 1, 1, io_uring.IORING_ENTER_GETEVENTS)
reqs.append((copy_batch, copied_in, minor_offset, real_copy_size:=min(sqe.len - minor_offset, size - copied_in)))
next_read_offset += sqe.len
+2 -1
View File
@@ -588,7 +588,8 @@ class NVDevice(HCQCompiled[NVSignal]):
def is_nvd(self) -> bool: return isinstance(self.iface, PCIIface)
def __init__(self, device:str=""):
self.iface = self._select_iface(device)
self.device_id = int(device.split(":")[1]) if ":" in device else 0
self.iface = self._select_iface()
device_params = nv_gpu.NV0080_ALLOC_PARAMETERS(deviceId=self.iface.gpu_instance, hClientShare=self.iface.root,
vaMode=nv_gpu.NV_DEVICE_ALLOCATION_VAMODE_OPTIONAL_MULTIPLE_VASPACES)
+2 -9
View File
@@ -3,10 +3,10 @@
# works to test the tensor cores, and all the uops in general
# this is the (living) definition of uops
from typing import Any, TYPE_CHECKING
import pickle, base64, itertools, time, sys, functools, ctypes
import pickle, base64, itertools, time, sys, functools
from dataclasses import replace
from tinygrad.dtype import bitcast, DType, dtypes, AddrSpace, truncate, storage_fmt_for_dtype, to_storage_scalar, from_storage_scalar
from tinygrad.helpers import all_same, getenv, flatten, Target, IMAGE, is_image_shape, cpu_profile, mv_address
from tinygrad.helpers import all_same, getenv, flatten, Target, IMAGE, is_image_shape, cpu_profile
from tinygrad.device import Buffer, Compiled, Compiler, Allocator, Program, TinyELF
from tinygrad.codegen.opt import tc
from tinygrad.uop.ops import exec_alu, python_alu, Ops, UOp, GroupOp
@@ -134,13 +134,6 @@ class PythonProgram(Program['PythonDevice']):
for k in range(len(src_values))], j, u.dtype) for j in range(load_sz)]
else:
values[u] = load(src_values, 0, u.dtype)
elif u.op is Ops.CALL:
assert u.dtype is dtypes.void
cfunc = ctypes.CFUNCTYPE(None, *[ctypes.c_uint64] * (len(src_values)-1))
values[u] = []
for args,gate in zip(zip(*src_values), exec_masks[-1]):
call_args = [(mv_address(x[0]) + x[1]*dt.itemsize) if isinstance(x, tuple) else x for x,dt in zip(args, src_dtypes)]
values[u].append(cfunc(call_args[0])(*call_args[1:]) if gate else None)
elif u.op is Ops.WMMA:
first_src_dtype = u.src[0].dtype
assert isinstance(first_src_dtype, DType) # mypy
+2 -1
View File
@@ -101,5 +101,6 @@ class RDMAAllocator(HCQAllocatorBase):
class RDMADevice(HCQCompiled):
def __init__(self, device:str=""):
self.iface = MLXIface(self, int(device.split(":")[1]) if ":" in device else 0)
self.device_id = int(device.split(":")[1]) if ":" in device else 0
self.iface = MLXIface(self, self.device_id)
super().__init__(device, RDMAAllocator(self), [], None, signal_t=None)
+75 -44
View File
@@ -1,5 +1,5 @@
from __future__ import annotations
import ctypes, collections, dataclasses, functools, hashlib, array
import ctypes, collections, dataclasses, functools, hashlib, array, time, contextlib
from tinygrad.helpers import mv_address, getenv, DEBUG, lo32, hi32, fetch_fw
from tinygrad.runtime.autogen import pci
from tinygrad.runtime.autogen.am import am, fw
@@ -149,12 +149,18 @@ class AMDev:
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
self.vram, self.doorbell64, self.mmio = self.pci_dev.map_bar(0), self.pci_dev.map_bar(2, fmt='Q'), self.pci_dev.map_bar(5, fmt='I')
# MI350X VFs start with most MMIO and VRAM access gated by the host PF. Ask the PF for full access only when discovery isn't readable yet.
self.is_vf = bool(self.mmio[0xde5] & 1) # RCC_IOV_FUNC_IDENTIFIER.FUNC_IDENTIFIER
self.vf_access_acquired, self.vf_initialized = False, False
if self.is_vf:
self._vf_mailbox_request(6, 7, data2=2, retries=5, event_timeout=2) # IDH_REQ_GPU_INIT_DATA -> IDH_REQ_GPU_INIT_DATA_READY
self._vf_mailbox_request(1, 1, retries=5, event_timeout=2) # IDH_REQ_GPU_INIT_ACCESS -> IDH_READY_TO_ACCESS_GPU
self.vf_access_acquired = True
self._run_discovery()
self._build_regs()
# on NBIO 7.9 the HDP flush doorbell remap register must be programmed before any flush (nbio_v7_9_remap_hdp_registers).
# the silicon default is bogus and flushing without this hangs the chip. flush_hdp is used before SOC init (by the PSP).
if self.ip_ver[am.NBIO_HWIP][:2] == (7,9): self.reg("regBIF_BX0_REMAP_HDP_MEM_FLUSH_CNTL").write(0x1A000)
# AM boot Process:
# The GPU being passed can be in one of several states: 1. Not initialized. 2. Initialized by amdgpu. 3. Initialized by AM.
# The 1st and 2nd states require a full GPU setup since their states are unknown. The 2nd state also requires a mode1 reset to
# reinitialize all components.
@@ -167,28 +173,21 @@ class AMDev:
self.is_booting = True # During boot only boot memory can be allocated. This flag is to validate this.
self.init_sw(smi_dev=False)
self.partial_boot = (self.reg("regSCRATCH_REG7").read() == AMDev.Version) and (getenv("AM_RESET", 0) != 1)
self.partial_boot = not self.is_vf and (self.reg("regSCRATCH_REG7").read() == AMDev.Version) and (getenv("AM_RESET", 0) != 1)
if self.partial_boot and (self.reg("regSCRATCH_REG6").read() != 0 or self.reg(self.gmc.pf_status_reg("GC")).read() != 0):
if DEBUG >= 2: print(f"am {self.devfmt}: Malformed state. Issuing a full reset.")
self.partial_boot = False
# Init hw for IP blocks where it is needed
# Init hw for IP blocks where it is needed. PSP and SMU are PF-owned on a VF and must not be reset or reloaded by the guest.
if not self.partial_boot:
fw_is_ours = False
if self.psp.is_sos_alive() and self.smu.is_smu_alive():
if not self.is_vf and self.psp.is_sos_alive() and self.smu.is_smu_alive():
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) & ~pci.PCI_COMMAND_MASTER, 2)
if self.is_hive():
if reset_mode: return # in reset mode, do not raise
raise RuntimeError("Malformed state. Use extra/amdpci/hive_reset.py to reset the hive")
# mode1 reset is only needed for foreign/unknown firmware state. If the running firmware was set up by AM itself
# (SCRATCH_REG7 is ours), a full AM re-init can run on top of it; SMU mode1 leaves the PSP/BL dead on some chips.
fw_is_ours = self.reg("regSCRATCH_REG7").read() == AMDev.Version
if not fw_is_ours: self.smu.mode1_reset()
self.smu.mode1_reset()
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
# when the firmware is AM's own and still running, skip the PSP stage: PSP ring commands over a live sOS are not
# serviced between sessions, and its firmware is already loaded.
self.init_hw(*([self.soc, self.gmc, self.ih, self.smu] if (self.psp.is_sos_alive() and self.smu.is_smu_alive() and fw_is_ours) else
[self.soc, self.gmc, self.ih, self.psp, self.smu]))
self.init_hw(self.soc, self.gmc, self.ih, *(() if self.is_vf else (self.psp, self.smu)))
# Booting done
self.is_booting = False
@@ -196,15 +195,17 @@ class AMDev:
# Re-initialize main blocks
self.init_hw(self.gfx, self.sdma)
# TODO: MP0 13.0.15 PMFW doesn't answer DPM clock msgs without the full amdgpu pptable/DPM setup, skip clock programming
if self.ip_ver[am.MP0_HWIP] == (13,0,15) and (max_power:=0.0) == 0.0: pass
elif (max_power:=getenv("AM_POWER_LIMIT", 0.0)) > 0:
self.smu.set_power_limit(max_power)
self.smu.set_clocks(level=None)
else: self.smu.set_clocks(level=-1) # last level, max perf.
for ip in [self.soc, self.gfx]: ip.set_clockgating_state()
self.reg("regSCRATCH_REG7").write(AMDev.Version)
self.reg("regSCRATCH_REG6").write(1) # set initialized state.
if not self.is_vf:
if (max_power:=getenv("AM_POWER_LIMIT", 0.0)) > 0:
self.smu.set_power_limit(max_power)
self.smu.set_clocks(level=None)
else: self.smu.set_clocks(level=-1) # last level, max perf.
if not self.is_vf:
for ip in [self.soc, self.gfx]: ip.set_clockgating_state()
if not self.is_vf:
self.reg("regSCRATCH_REG7").write(AMDev.Version)
self.reg("regSCRATCH_REG6").write(1) # set initialized state.
self.vf_initialized = self.is_vf
if DEBUG >= 2: print(f"am {self.devfmt}: boot done")
def init_sw(self, smi_dev=False):
@@ -213,7 +214,8 @@ class AMDev:
# Memory manager & firmware
self.mm = AMMemoryManager(self, self.vram_size - self.reserved_vram_size, boot_size=(32 << 20), pt_t=AMPageTableEntry, va_shifts=[12, 21, 30, 39],
va_bits=48, first_lv=am.AMDGPU_VM_PDB2, va_base=AMMemoryManager.va_allocator.base, reserve_ptable=not self.large_bar,
palloc_ranges=[(1 << (i + 12), (2 << 20) if i >= 9 else 0x1000) for i in range(9 * (3 - am.AMDGPU_VM_PDB2), -1, -1)])
palloc_ranges=[(1 << (i + 12), (2 << 20) if i >= 9 else 0x1000) for i in range(9 * (3 - am.AMDGPU_VM_PDB2), -1, -1)],
paddr_base=(1 << 20) if self.is_vf else 0)
self.fw = AMFirmware(self)
# Initialize IP blocks
@@ -235,10 +237,52 @@ class AMDev:
def fini(self):
if DEBUG >= 2: print(f"am {self.devfmt}: Finalizing")
for ip in [self.sdma, self.gfx]: ip.fini_hw()
self.smu.set_clocks(level=0)
self.ih.interrupt_handler()
self.reg("regSCRATCH_REG6").write(self.is_err_state) # set finalized state.
try:
for ip in [self.sdma, self.gfx]: ip.fini_hw()
if not self.is_vf: self.smu.set_clocks(level=0)
self.ih.interrupt_handler()
if not self.is_vf: self.reg("regSCRATCH_REG6").write(self.is_err_state) # set finalized state.
finally: self.release_vf_access()
def release_vf_access(self):
if not getattr(self, "vf_access_acquired", False): return
# tinygrad retains IDH_REQ_GPU_INIT_ACCESS for direct MMIO/VRAM access, so always release that same lease.
with contextlib.suppress(Exception): self._vf_mailbox_request(2, None) # IDH_REL_GPU_INIT_ACCESS
self.vf_access_acquired = False
def __del__(self):
# Constructor failures do not reach HCQ finalization; return a partially acquired VF init lease to the PF.
self.release_vf_access()
def _vf_mailbox_request(self, req:int, event:int|None, data1=0, data2=0, data3=0, retries=1, event_timeout=2.0):
# Navi VF/PF mailbox protocol from the kernel's mxgpu_nv driver. This requests access only; it never requests a GPU or PCI reset.
mmio8, control, trn, rcv = self.mmio.view(fmt='B'), 0xe5e * 4, 0xe56, 0xe5a
if mmio8[control+1] & 1: mmio8[control+1] = 2 # acknowledge a stale PF event before transmitting a new request
for retry in range(retries):
deadline = time.monotonic() + 1
while True:
mmio8[control] = 0 # clear TRN_MSG_VALID and wait for the old PF acknowledgement to drop
if not (mmio8[control] & 2): break
if time.monotonic() > deadline: raise TimeoutError("VF mailbox acknowledgement did not clear")
time.sleep(0.001)
for i, val in enumerate((req, data1, data2, data3)): self.mmio[trn+i] = val
mmio8[control] = 1
deadline = time.monotonic() + 0.5
while not (mmio8[control] & 2):
if time.monotonic() > deadline: raise TimeoutError(f"VF mailbox request {req:#x} was not acknowledged")
time.sleep(0.005)
mmio8[control] = 0
if event is None: return
deadline = time.monotonic() + event_timeout
while time.monotonic() <= deadline:
if self.mmio[rcv] == event:
mmio8[control+1] = 2 # acknowledge RCV_MSG_VALID
return
time.sleep(0.01)
if DEBUG >= 2 and retry+1 < retries: print(f"am {self.devfmt}: retrying VF mailbox request {req:#x} ({retry+1}/{retries})")
raise TimeoutError(f"VF mailbox request {req:#x} did not receive event {event:#x}")
def recover(self, force=False) -> bool:
if not force and not self.is_err_state: return False
@@ -249,8 +293,7 @@ class AMDev:
if DEBUG >= 3: print(f"am {self.devfmt}: Recovery complete")
return True
# a hive has multiple XGMI regions; single-node parts (like MI350P) may still program LFB_SIZE with region 0 only
def is_hive(self) -> bool: return self.gmc.xgmi_seg_sz > 0 and self.gmc.xgmi_max_region > 0
def is_hive(self) -> bool: return self.gmc.xgmi_seg_sz > 0
def paddr2mc(self, paddr:int) -> int: return self.gmc.mc_base + paddr
def paddr2xgmi(self, paddr:int) -> int: return self.gmc.paddr_base + paddr
@@ -327,18 +370,6 @@ class AMDev:
ip_offset += 8 + (8 if ihdr.base_addr_64_bit else 4) * ip.num_base_address
# HARV(EST) table: harvested instances must be excluded (like amdgpu_discovery_harvest_ip)
self.harvested:dict[int, set[int]] = collections.defaultdict(set)
if (harv_off:=self.bhdr.table_list[am.HARVEST_INFO].offset) != 0:
hv = ctypes.c_uint32.from_address(ctypes.addressof(self.bhdr) + harv_off).value
if hv == am.HARVEST_TABLE_SIGNATURE:
for i in range(32):
hw_id = ctypes.c_uint16.from_address(ctypes.addressof(self.bhdr) + harv_off + 8 + i*4).value
if hw_id == 0: continue
inst = ctypes.c_uint8.from_address(ctypes.addressof(self.bhdr) + harv_off + 8 + i*4 + 2).value
for hw_ip in am.hw_id_map:
if am.hw_id_map[hw_ip] == hw_id: self.harvested[hw_ip].add(inst)
gc_info = am.struct_gc_info_v1_0.from_address(gc_addr:=ctypes.addressof(self.bhdr) + self.bhdr.table_list[am.GC].offset)
self.gc_info = getattr(am, f"struct_gc_info_v{gc_info.header.version_major}_{gc_info.header.version_minor}").from_address(gc_addr)
self.reserved_vram_size = (384 << 20) if self.ip_ver[am.GC_HWIP][:2] in {(9,4), (9,5)} else (64 << 20)
+30 -75
View File
@@ -29,8 +29,7 @@ class AM_SOC(AM_IP):
def init_hw(self):
if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}:
# kernel programs regXCC_DOORBELL_FENCE = 0xff & ~xcc_mask; on this PF 4 of 8 XCCs are harvested, so fence xcc4-7
self.adev.regXCC_DOORBELL_FENCE.write(0xF0)
self.adev.regXCC_DOORBELL_FENCE.write(0x0)
for aid in range(1, self.adev.gmc.vmhubs):
self.adev.indirect_wreg_pcie(self.adev.regXCC_DOORBELL_FENCE.addr[0], self.adev.regXCC_DOORBELL_FENCE.encode(shub_slv_mode=1), aid=aid)
self.adev.regBIFC_GFX_INT_MONITOR_MASK.write(0x7ff)
@@ -51,19 +50,15 @@ class AM_SOC(AM_IP):
class AM_GMC(AM_IP):
def init_sw(self):
self.vmhubs = len(self.adev.regs_offset[am.MMHUB_HWIP])
# aqua (NBIO 7.9): only the first 2 mmhubs exist in the host window, instances 2+ are phantom layouts (like amdgpu's aid_mask)
if self.adev.ip_ver[am.NBIO_HWIP][:2] == (7,9): self.vmhubs = min(self.vmhubs, 2)
# XGMI (for supported systems)
xgmi_lfb_cntl = self.adev.regMMMC_VM_XGMI_LFB_CNTL.read_bitfields() if hasattr(self.adev, 'regMMMC_VM_XGMI_LFB_CNTL') else {}
self.xgmi_phys_id, self.xgmi_max_region = xgmi_lfb_cntl.get('pf_lfb_region', 0), xgmi_lfb_cntl.get('pf_max_region', 0)
self.xgmi_phys_id = self.adev.regMMMC_VM_XGMI_LFB_CNTL.read_bitfields()['pf_lfb_region'] if hasattr(self.adev, 'regMMMC_VM_XGMI_LFB_CNTL') else 0
self.xgmi_seg_sz = self.adev.regMMMC_VM_XGMI_LFB_SIZE.read_bitfields()['pf_lfb_size']<<24 if hasattr(self.adev, 'regMMMC_VM_XGMI_LFB_SIZE') else 0
self.paddr_base = self.xgmi_phys_id * self.xgmi_seg_sz
# compute fb_end like the kernel does (vram_start + vram_size), MMMC_VM_FB_LOCATION_TOP is not reliable on all SKUs
self.fb_base = (self.adev.regMMMC_VM_FB_LOCATION_BASE.read() & 0xFFFFFF) << 24
self.fb_end = self.fb_base + self.adev.vram_size
self.fb_end = (self.adev.regMMMC_VM_FB_LOCATION_TOP.read() & 0xFFFFFF) << 24
# Memory controller aperture
self.mc_base = self.fb_base + self.paddr_base
@@ -181,22 +176,10 @@ class AM_SMU(AM_IP):
self.smu_mod = self.adev._ip_module("smu", am.MP1_HWIP)
self.driver_table_paddr = self.adev.mm.palloc(0x4000, zero=False, boot=True)
def wait_alive(self):
# poll until the SMU mailbox starts ACKing GetSmuVersion (single-shot attempts, mirroring amdgpu which issues one check)
t0 = time.time()
while time.time() - t0 < 60:
if self.is_smu_alive(): return
time.sleep(0.5)
raise TimeoutError("SMU not alive")
def init_hw(self):
self.wait_alive()
# MP0 13.0.15 PMFW answers the dram addr msgs with an error response (as seen in amdgpu logs), tolerate any nonzero resp
dram_tolerant = self.adev.ip_ver[am.MP0_HWIP] == (13,0,15)
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrHigh, hi32(self.adev.paddr2mc(self.driver_table_paddr)), any_resp=dram_tolerant)
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrLow, lo32(self.adev.paddr2mc(self.driver_table_paddr)), any_resp=dram_tolerant)
# not valid on smu_v13_0_12-family pmfw
if self.adev.ip_ver[am.MP0_HWIP] != (13,0,15): self._send_msg(self.smu_mod.PPSMC_MSG_EnableAllSmuFeatures, 0, any_resp=dram_tolerant)
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrHigh, hi32(self.adev.paddr2mc(self.driver_table_paddr)))
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrLow, lo32(self.adev.paddr2mc(self.driver_table_paddr)))
self._send_msg(self.smu_mod.PPSMC_MSG_EnableAllSmuFeatures, 0)
def is_smu_alive(self):
with contextlib.suppress(TimeoutError): self._send_msg(self.smu_mod.PPSMC_MSG_GetSmuVersion, 0, timeout=100)
@@ -206,13 +189,13 @@ class AM_SMU(AM_IP):
if DEBUG >= 2: print(f"am {self.adev.devfmt}: mode1 reset")
if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0) or self.adev.ip_ver[am.MP0_HWIP] in {(13,0,0), (13,0,7), (13,0,10)}:
self._send_msg(__DEBUGSMC_MSG_Mode1Reset:=2, 0, debug=True)
elif self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,12), (13,0,15)}: self._send_msg(self.smu_mod.PPSMC_MSG_GfxDriverReset, 1)
elif self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,12)}: self._send_msg(self.smu_mod.PPSMC_MSG_GfxDriverReset, 1)
else: self._send_msg(self.smu_mod.PPSMC_MSG_Mode1Reset, 0)
if not self.adev.is_hive(): time.sleep(0.5) # 500ms
def read_table(self, table_t, arg):
if self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6),(13,0,12),(13,0,15)}: self._send_msg(self.smu_mod.PPSMC_MSG_GetMetricsTable, arg)
if self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6),(13,0,12)}: self._send_msg(self.smu_mod.PPSMC_MSG_GetMetricsTable, arg)
else: self._send_msg(self.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, arg)
return table_t.from_buffer(bytearray(self.adev.vram.view(self.driver_table_paddr, ctypes.sizeof(table_t))[:]))
@@ -223,7 +206,7 @@ class AM_SMU(AM_IP):
def set_clocks(self, level:int|None):
clks = tuple([self.smu_mod.PPCLK_UCLK, self.smu_mod.PPCLK_FCLK, self.smu_mod.PPCLK_SOCCLK])
if self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,12), (13,0,15)}: clks += (self.smu_mod.PPCLK_GFXCLK,)
if self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,12)}: clks += (self.smu_mod.PPCLK_GFXCLK,)
if level is None:
for clck in clks:
@@ -255,27 +238,23 @@ class AM_SMU(AM_IP):
(self.adev.mmMP1_SMN_C2PMSG_82 if not debug else self.adev.mmMP1_SMN_C2PMSG_53).write(param)
(self.adev.mmMP1_SMN_C2PMSG_66 if not debug else self.adev.mmMP1_SMN_C2PMSG_75).write(msg)
def _send_msg(self, msg:int, param:int, read_back_arg=False, timeout=10000, debug=False, any_resp=False): # default timeout is 10 seconds
def _send_msg(self, msg:int, param:int, read_back_arg=False, timeout=10000, debug=False): # default timeout is 10 seconds
self._smu_cmn_send_msg(msg, param, debug=debug)
rc = self.adev.mmMP1_SMN_C2PMSG_90 if not debug else self.adev.mmMP1_SMN_C2PMSG_54
# amdgpu tolerates any nonzero resp
cond, val = (lambda: rc.read() != 0, True) if any_resp else (rc.read, 1)
wait_cond(cond, value=val, timeout_ms=timeout, msg=f"SMU msg {msg:#x} timeout")
wait_cond((self.adev.mmMP1_SMN_C2PMSG_90 if not debug else self.adev.mmMP1_SMN_C2PMSG_54).read, value=1, timeout_ms=timeout,
msg=f"SMU msg {msg:#x} timeout")
return (self.adev.mmMP1_SMN_C2PMSG_82 if not debug else self.adev.mmMP1_SMN_C2PMSG_53).read() if read_back_arg else None
class AM_GFX(AM_IP):
def init_sw(self):
self.xccs = sum(1 for i in self.adev.regs_offset[am.GC_HWIP] if i not in self.adev.harvested[am.GC_HWIP])
self.xccs = len(self.adev.regs_offset[am.GC_HWIP])
self.mqd_paddr = [self.adev.mm.palloc(0x1000 * self.xccs, zero=False, boot=True) for i in range(2)]
self.mqd_mc = [self.adev.paddr2mc(mqd_paddr) for mqd_paddr in self.mqd_paddr]
def init_hw(self):
# Wait for RLC autoload to complete
# regRLC_RLCS_BOOTLOAD_STATUS doesn't exist on gc 9.4.3 (used for gfx942/gfx950), gate on it only if present
def bootload_done():
return getattr(self.adev, 'regRLC_RLCS_BOOTLOAD_STATUS', None) is None or \
self.adev.regRLC_RLCS_BOOTLOAD_STATUS.read_bitfields()['bootload_complete'] == 0
wait_cond(lambda: self.adev.regCP_STAT.read() == 0 or bootload_done(), value=True, msg="RLC autoload timeout")
# Wait for RLC autoload to complete on architectures that expose the bootload status register.
if hasattr(self.adev, "regRLC_RLCS_BOOTLOAD_STATUS"):
wait_cond(lambda: self.adev.regCP_STAT.read() == 0 or self.adev.regRLC_RLCS_BOOTLOAD_STATUS.read_bitfields()['bootload_complete'] == 0,
value=True, msg="RLC autoload timeout")
self.adev.gmc.init_hub("GC", inst_cnt=self.xccs)
if self.adev.partial_boot: return self.reset_mec()
@@ -319,8 +298,8 @@ class AM_GFX(AM_IP):
self._enable_mec()
# Set 1 partition (skip on MP0 13.0.15 (MI350P): the XCP transition is firmware-owned there)
if self.xccs > 1 and self.adev.ip_ver[am.MP0_HWIP] != (13,0,15): self.adev.psp._spatial_partition_cmd(1)
# Set 1 partition on bare metal. A VF must use the spatial partition assigned by its host PF.
if self.xccs > 1 and not self.adev.is_vf: self.adev.psp._spatial_partition_cmd(1)
def fini_hw(self): self._dequeue_hqds()
@@ -336,9 +315,7 @@ class AM_GFX(AM_IP):
self._enable_mec()
def setup_ring(self, ring_addr:int, ring_size:int, rptr_addr:int, wptr_addr:int, eop_addr:int, eop_size:int, idx:int, aql:bool) -> int:
# aqua (NBIO 7.9) uses DOORBELL_LAYOUT1 (see aqua_vanjaram_doorbell_index_init): its mec ring0 starts at 8, not 3
pipe, queue, doorbell = idx // 4, idx % 4, (am.AMDGPU_DOORBELL_LAYOUT1_MEC_RING_START if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}
else am.AMDGPU_NAVI10_DOORBELL_MEC_RING0)
pipe, queue, doorbell = idx // 4, idx % 4, am.AMDGPU_NAVI10_DOORBELL_MEC_RING0
for xcc in range(self.xccs if aql else 1):
self._grbm_select(me=1, pipe=pipe, queue=queue, inst=xcc)
@@ -436,23 +413,15 @@ class AM_IH(AM_IP):
def _alloc_ring(size): return (self.adev.mm.palloc(size, zero=False, boot=True), self.adev.mm.palloc(0x1000, zero=False, boot=True))
self.rings = [(*_alloc_ring(self.ring_size), "", 0), (*_alloc_ring(self.ring_size), "_RING1", 1)]
self.ring_view = self.adev.vram.view(offset=self.rings[0][0], size=self.ring_size, fmt='I')
# on gfx950 (aqua), the IH rings must live in host system memory (like use_bus_addr=true in amdgpu)
self.rings_in_sysmem = self.adev.ip_ver[am.GC_HWIP][:2] == (9,5) # scoped to gfx950 for now (validated symptom there)
if self.rings_in_sysmem:
# OSSSYS 4.4.2 (aqua): only one IH ring, the second one is skipped in amdgpu too
self.sysmem_rings = [self.adev.pci_dev.alloc_sysmem(self.ring_size + 0x1000) for _ in range(1)]
self.rings = [(sr[1][0], sr[1][self.ring_size // 0x1000], s, i) for sr, (_, _, s, i) in zip(self.sysmem_rings, self.rings)]
self.ring_view = self.sysmem_rings[0][0].view(0, self.ring_size, fmt='I')
def init_hw(self):
for ring_vm, rwptr_vm, suf, ring_id in self.rings:
self.adev.wreg_pair("regIH_RB_BASE", suf, f"_HI{suf}", ring_vm >> 8)
self.adev.wreg_pair("regIH_RB_BASE", suf, f"_HI{suf}", self.adev.paddr2mc(ring_vm) >> 8)
mc_space = 1 if self.rings_in_sysmem else 4
self.adev.reg(f"regIH_RB_CNTL{suf}").write(mc_space=mc_space, wptr_overflow_clear=1, rb_size=((self.ring_size//4)-1).bit_length(),
self.adev.reg(f"regIH_RB_CNTL{suf}").write(mc_space=4, wptr_overflow_clear=1, rb_size=((self.ring_size//4)-1).bit_length(),
mc_snoop=1, mc_ro=0, mc_vmid=0, **({'wptr_overflow_enable': 1, 'rptr_rearm': 1} if ring_id == 0 else {'rb_full_drain_enable': 1}))
if ring_id == 0: self.adev.wreg_pair("regIH_RB_WPTR_ADDR", "_LO", "_HI", (rwptr_vm if self.rings_in_sysmem else self.adev.paddr2mc(rwptr_vm)))
if ring_id == 0: self.adev.wreg_pair("regIH_RB_WPTR_ADDR", "_LO", "_HI", self.adev.paddr2mc(rwptr_vm))
self.adev.reg(f"regIH_RB_WPTR{suf}").write(0)
self.adev.reg(f"regIH_RB_RPTR{suf}").write(0)
@@ -464,12 +433,6 @@ class AM_IH(AM_IP):
self.adev.regIH_INT_FLOOD_CNTL.update(flood_cntl_enable=1)
self.adev.regIH_MSI_STORM_CTRL.update(delay=3)
# aqua (OSSSYS 4.4.2): IH_CHICKEN.MC_SPACE_GPA_ENABLE + retry-int-cam must be set before RB_ENABLE (as in vega20_ih)
if self.rings_in_sysmem and hasattr(self.adev, 'regIH_CHICKEN'):
self.adev.regIH_CHICKEN.update(mc_space_gpa_enable=1)
oss_base = self.adev.regs_offset[am.OSSSYS_HWIP][0][0]
self.adev.wreg(oss_base + 0xEA, self.adev.rreg(oss_base + 0xEA) | 0x10000) # IH_RETRY_INT_CAM_CNTL_ALDEBARAN
# toggle interrupts
for _, rwptr_vm, suf, ring_id in self.rings:
self.adev.reg(f"regIH_RB_CNTL{suf}").update(rb_enable=1, **({'enable_intr': 1} if ring_id == 0 else {}))
@@ -524,7 +487,7 @@ class AM_IH(AM_IP):
if athub_err or cntlr_err:
print(f"am {self.adev.devfmt}: fatal hardware error detected: {'RAS_ATHUB_ERR_EVENT ' if athub_err else ''}{'RAS_CNTLR' if cntlr_err else ''}")
acas = self.adev.smu._aca_read_banks(ue=True) + self.adev.smu._aca_read_banks(ue=False)
acas = [] if self.adev.is_vf else self.adev.smu._aca_read_banks(ue=True) + self.adev.smu._aca_read_banks(ue=False)
for regs in acas:
acatyp = 'Uncorrectable' if (regs[1] >> 61) & 1 and (regs[1] >> 57) & 1 else 'Correctable'
hwname = f'{self.adev.hwid_names.get((regs[5] >> 32) & 0xFFF, "")} ({(regs[5] >> 32) & 0xFFF:#03x})'
@@ -536,9 +499,6 @@ class AM_IH(AM_IP):
class AM_SDMA(AM_IP):
def init_sw(self): self.sdma_reginst, self.sdma_name = [], "F32" if self.adev.ip_ver[am.SDMA0_HWIP] < (7,0,0) else "MCU"
def init_hw(self):
# aqua (NBIO 7.9): SDMA doorbell routing/trap config is firmware/RLC-managed; host programming here tears the fabric
# (~40ms later: RAS_ATHUB_ERR_EVENT and host BAR0 access to VRAM dies until the next cold boot).
if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}: return
for pipe_id in range(16 if self.adev.ip_ver[am.SDMA0_HWIP] < (5,0,0) else 1):
pipe, inst = ("", pipe_id) if self.adev.ip_ver[am.SDMA0_HWIP] < (5,0,0) else (str(pipe_id), 0)
@@ -589,14 +549,11 @@ class AM_SDMA(AM_IP):
self.adev.wreg_pair(f"{reg}_RB_BASE", "", "_HI", ring_addr >> 8, inst=inst)
self.adev.wreg_pair(f"{reg}_RB_RPTR_ADDR", "_LO", "_HI", rptr_addr, inst=inst)
self.adev.wreg_pair(f"{reg}_RB_WPTR_POLL_ADDR", "_LO", "_HI", wptr_addr, inst=inst)
# aqua (NBIO 7.9): kernel leaves SDMA doorbell regs 0 and submits via the WPTR register
if self.adev.ip_ver[am.NBIO_HWIP] not in {(7,9,0), (7,9,1)}:
self.adev.reg(f"{reg}_DOORBELL_OFFSET").update(offset=doorbell * 2, inst=inst)
self.adev.reg(f"{reg}_DOORBELL").update(enable=1, inst=inst)
self.adev.reg(f"{reg}_DOORBELL_OFFSET").update(offset=doorbell * 2, inst=inst)
self.adev.reg(f"{reg}_DOORBELL").update(enable=1, inst=inst)
self.adev.reg(f"{reg}_MINOR_PTR_UPDATE").write(0x0, inst=inst)
self.adev.reg(f"{reg}_RB_CNTL").write(**({f'{self.sdma_name.lower()}_wptr_poll_enable':1} if self.adev.ip_ver[am.SDMA0_HWIP][:2]!=(4,4) else {}),
rb_vmid=0, rptr_writeback_enable=1, rptr_writeback_timer=4, rb_enable=1,
rb_priv=1 if self.adev.ip_ver[am.NBIO_HWIP] not in {(7,9,0), (7,9,1)} else 0, rb_size=(ring_size//4).bit_length()-1, inst=inst)
rb_vmid=0, rptr_writeback_enable=1, rptr_writeback_timer=4, rb_enable=1, rb_priv=1, rb_size=(ring_size//4).bit_length()-1, inst=inst)
self.adev.reg(f"{reg}_IB_CNTL").update(ib_enable=1, inst=inst)
return doorbell
@@ -619,15 +576,13 @@ class AM_PSP(AM_IP):
self.ring_paddr = self.adev.mm.palloc(self.ring_size, zero=False, boot=True)
self.max_tmr_size, self.tmr_size = 0x1300000, 0
self.boot_time_tmr = self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,14), (13,0,15), (14,0,2), (14,0,3)}
self.autoload_tmr = self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,14), (13,0,15)}
self.boot_time_tmr = self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,14), (14,0,2), (14,0,3)}
self.autoload_tmr = self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,14)}
self.tmr_paddr = self.adev.mm.palloc(self.max_tmr_size, align=am.PSP_TMR_ALIGNMENT, zero=False, boot=True) if not self.boot_time_tmr else 0
def init_hw(self):
spl_key = am.PSP_FW_TYPE_PSP_SPL if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0) else am.PSP_FW_TYPE_PSP_KDB
# SPL is preloaded on MP0 13.0.15
sos_components = [] if self.adev.ip_ver[am.MP0_HWIP] == (13,0,15) else [(spl_key, am.PSP_BL__LOAD_TOS_SPL_TABLE)]
sos_components += [(am.PSP_FW_TYPE_PSP_KDB, am.PSP_BL__LOAD_KEY_DATABASE),
sos_components = [(am.PSP_FW_TYPE_PSP_KDB, am.PSP_BL__LOAD_KEY_DATABASE), (spl_key, am.PSP_BL__LOAD_TOS_SPL_TABLE),
(am.PSP_FW_TYPE_PSP_SYS_DRV, am.PSP_BL__LOAD_SYSDRV), (am.PSP_FW_TYPE_PSP_SOC_DRV, am.PSP_BL__LOAD_SOCDRV),
(am.PSP_FW_TYPE_PSP_INTF_DRV, am.PSP_BL__LOAD_INTFDRV), (am.PSP_FW_TYPE_PSP_DBG_DRV, am.PSP_BL__LOAD_DBGDRV),
(am.PSP_FW_TYPE_PSP_RAS_DRV, am.PSP_BL__LOAD_RASDRV), (am.PSP_FW_TYPE_PSP_SOS, am.PSP_BL__LOAD_SOSDRV)]
+4 -25
View File
@@ -1,6 +1,6 @@
import ctypes, struct, platform, pathlib, shutil, subprocess, sys, tarfile, tempfile
import ctypes, struct
from tinygrad.device import Compiler
from tinygrad.helpers import DEBUG, system, fetch, unwrap
from tinygrad.helpers import DEBUG, system
from tinygrad.runtime.support.compiler_mesa import disas_adreno
# see https://github.com/sirhcm/tinydreno
from tinygrad.runtime.autogen import llvm_qcom
@@ -10,17 +10,10 @@ def _read_lib(lib, off) -> int: return struct.unpack("I", lib[off:off+4])[0]
class QCOMCompiler(Compiler):
def __init__(self, arch:str):
assert arch.split(',')[0] == "a630", "only a630 supported"
if platform.machine() == "aarch64": self.arch, self.chip_id, self.llvm_inst = arch, 0x6030001, llvm_qcom.cl_compiler_create_llvm_instance()
else:
self.arch, self.chip_id, self.fs = arch, 0x6030001, tempfile.TemporaryDirectory()
with tarfile.open(fetch('https://git.tinygrad.win/sirhcm/images/releases/download/v2/qcomcl.tar.gz')) as t: t.extractall(fs:=self.fs.name)
if (qemu:=shutil.which("qemu-aarch64-static")): argv = f"{qemu} -cpu max,pauth=off -L {fs} {fs}/usr/bin/python3 {__file__} {arch}"
else: argv = (f"docker run --rm -i --platform linux/aarch64 -v {fs}/usr:/usr -v {pathlib.Path(__file__).parents[2]}:/tinygrad "
f"-e PYTHONPATH=/ -e QEMU_CPU=max,pauth=off gcr.io/distroless/static python3 /tinygrad/runtime/support/compiler_qcom.py {arch}")
self.compiler_process = subprocess.Popen(argv.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0)
self.arch, self.chip_id, self.llvm_inst = arch, 0x6030001, llvm_qcom.cl_compiler_create_llvm_instance()
super().__init__(f"compile_qcomcl_{arch}")
def __del__(self): llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst) if platform.machine() == "aarch64" else self.compiler_process.kill()
def __del__(self): llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst)
def __reduce__(self): return QCOMCompiler, (self.arch,)
@@ -32,10 +25,6 @@ class QCOMCompiler(Compiler):
return handle
def compile(self, src) -> bytes:
if platform.machine() != "aarch64":
unwrap(self.compiler_process.stdin).write(struct.pack("I", len(src.encode())) + src.encode())
if (lib:=unwrap(self.compiler_process.stdout).read(struct.unpack("I", unwrap(self.compiler_process.stdout).read(4))[0])): return lib
raise RuntimeError("QCOM Compilation Error")
ch = self.checked(llvm_qcom.cl_compiler_compile_source(self.llvm_inst, self.chip_id, llvm_qcom.CL_MODE_64BIT, b"", 0, 0, 0, src.encode(), 0,
llvm_qcom.CL_SRC_STR, None))
if DEBUG >= 8: print(system("llvm-dis", input=ctypes.string_at((comp:=ch.contents.compiled.contents).llvm_bitcode, comp.llvm_bitcode_size)))
@@ -47,13 +36,3 @@ class QCOMCompiler(Compiler):
return ret
def disassemble(self, lib: bytes): disas_adreno(lib[(ofs:=_read_lib(lib, 0xc0)):ofs+_read_lib(lib, 0x100)], self.chip_id)
if __name__ == "__main__":
compiler = QCOMCompiler(sys.argv[1])
while (amt:=sys.stdin.buffer.read(4)):
try: lib = compiler.compile(sys.stdin.buffer.read(struct.unpack("I", amt)[0]).decode())
except Exception as e:
lib = b""
print(e, file=sys.stderr, flush=True)
sys.stdout.buffer.write(struct.pack("I", len(lib)) + lib)
sys.stdout.buffer.flush()
+22 -4
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
from typing import cast, Callable, Type, TypeVar, Generic, Any
import contextlib, decimal, statistics, time, ctypes, array, os, struct, collections, itertools
import contextlib, decimal, statistics, time, ctypes, array, os, struct, collections, functools, itertools
from dataclasses import replace
try: import fcntl # windows misses that
except ImportError: fcntl = None #type:ignore[assignment]
from tinygrad.helpers import DEV, PROFILE, getenv, to_mv, from_mv, cpu_profile, ProfileRangeEvent, unwrap
from tinygrad.helpers import DEV, PROFILE, getenv, to_mv, from_mv, cpu_profile, ProfileRangeEvent, select_first_inited, select_by_name, unwrap
from tinygrad.helpers import suppress_finalizing, pluralize, TracingKey
from tinygrad.device import Device, BufferSpec, Compiled, LRUAllocator, ProfileDeviceEvent, ProfileProgramEvent, Program, TinyELF
from tinygrad.uop.ops import sym_infer, sint, UOp
@@ -392,6 +393,8 @@ class HCQCompiled(Compiled, Generic[SignalType]):
def __init__(self, device:str, allocator:HCQAllocatorBase, compilers:list[type[Renderer]], runtime:type[Program]|None,
signal_t:Type[SignalType]|None=None, comp_queue_t:Callable[..., HWQueue]|None=None, copy_queue_t:Callable[..., HWQueue]|None=None,
kernargs_size=(16 << 20), sigalloc_size=0x1000, can_recover:bool=False, arch=None):
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
from tinygrad.runtime.graph.hcq import HCQGraph
super().__init__(device, allocator, compilers, runtime, HCQGraph, arch=arch)
@@ -421,6 +424,8 @@ class HCQCompiled(Compiled, Generic[SignalType]):
if self._is_cpu(): HCQCompiled.cpu_devices.append(self)
def count(self) -> int: return self.iface.count if hasattr(self, 'iface') else 1
def synchronize(self, timeout:int|None=None):
if self.error_state is not None: raise self.error_state
if not hasattr(self, 'timeline_signal'): return
@@ -486,6 +491,16 @@ class HCQCompiled(Compiled, Generic[SignalType]):
buf, realloced = self.allocator.alloc(oldbuf.size if oldbuf is not None else new_size, options=options), False
return buf, realloced
def _select_iface(self):
assert (v:=getenv(k:=f'{type(self).__name__[:-6].upper()}_IFACE', "")) == "", \
f"{k}={v} is deprecated, use DEV={replace(DEV.target(type(self).__name__[:-6]), interface=v)} instead"
assert hasattr(self, "ifaces"), "must have ifaces to select an iface"
t = DEV.target(dev:=type(self).__name__[:-6])
filtered = select_by_name(self.ifaces, lambda i: i.__name__[:-5], t.interface, f"{dev} has no interface {t.interface!r}")
filtered = [i for i in filtered if t.interface.startswith("MOCK") or not i.__name__[:-5].startswith("MOCK")] # never fallback to mock ifaces
return select_first_inited([functools.partial(cast(Callable, iface), self, self.device_id) for iface in filtered],
f"No interface for {dev}:{self.device_id} is available")
def _is_cpu(self) -> bool: return hasattr(self, 'device') and self.device.split(":")[0] == "CPU"
def rdma_dev(self):
@@ -497,10 +512,13 @@ class HCQCompiled(Compiled, Generic[SignalType]):
def finalize(self):
try: self.synchronize() # Try to finalize device in any case.
except RuntimeError as e: print(f"{self.device} synchronization failed before finalizing: {e}")
super().finalize()
# If the device has an interface, call its device_fini method to clean up resources.
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
class HCQBuffer:
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQBuffer|None=None, view:MMIOInterface|None=None, owner:Any=None):
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQBuffer|None=None, view:MMIOInterface|None=None,
owner:HCQCompiled|None=None):
self.va_addr, self.size, self.meta, self._base, self.view = va_addr, size, meta, _base, view
self._devs, self.owner = ([owner] if owner is not None else []), owner
self._mappings:dict[HCQCompiled, HCQBuffer] = {} # mapping to the other devices
+74 -100
View File
@@ -1,15 +1,15 @@
from __future__ import annotations
from typing import cast, TypeVar, Generic, Any, Sequence, Iterable
from typing import cast, Callable, TypeVar, Generic, Any, Sequence
import struct, functools, time, collections, itertools, decimal, statistics
from dataclasses import replace, dataclass
from tinygrad.helpers import suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap, PROFILE
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap, PROFILE
from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, ContextVar, perf_counter_us, Context
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer, DepsTracker
from tinygrad.device import ProfileDeviceEvent, ProfileGraphEntry, ProfileGraphEvent
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, rewrite_group, GroupOp
from tinygrad.uop.symbolic import symbolic
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const
from tinygrad.dtype import dtypes, truncate
from tinygrad.runtime.support.hcq import MMIOInterface, HCQBuffer
from tinygrad.runtime.support.hcq import MMIOInterface
from tinygrad.runtime.support.memory import BumpAllocator
from tinygrad.renderer import Renderer, Estimates
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop
@@ -22,17 +22,18 @@ HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQ2Compiled')
HCQ_RUNTIME_DEV = ContextVar("HCQ_RUNTIME_DEV", "CPU")
HCQ_DEVS = frozenset(("AMD", "CPU"))
HCQ_CACHE_TAGS = frozenset(("program", "systems"))
HCQ_DEVS = frozenset(("AMD",))
HCQ_P2P_DEVS = HCQ_DEVS | frozenset(("CPU",))
HCQ_CACHE_TAGS = frozenset(("program", "systems", "template"))
@dataclass(frozen=True)
class HCQInfo:
device:tuple[str, ...]
estimates:Estimates = Estimates()
input_idxs:tuple[tuple[tuple[str, ...], tuple[int, ...]], ...] = () # per inputs table: (devices, indexes into input_uops)
inputs:int|None = None # index of the inputs table in call.src
kernels:tuple[tuple[tuple[str, ...], UOp, tuple[int, ...]], ...] = () # per kernel: (devices, a call carrying its name and estimates, timestamps)
input_idxs:tuple[int, ...] = () # indexes into input_uops used by this call
inputs:int|None = None
kernels:tuple[tuple[tuple[str, ...], str, Estimates, tuple[int, ...]], ...] = ()
def all_devices_in(d:Any, c:frozenset[str]) -> bool: return {x.split(":")[0] for x in to_tuple(d)} <= c
@@ -86,20 +87,16 @@ def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
def replace_call_buffers(ctx:tuple[list[UOp], dict[UOp, int]], call:UOp) -> UOp|None:
bufs, slots = ctx
for s in call.src[1:]:
if s.op is not Ops.PARAM and not s.is_bound_var and slots.setdefault(s, len(bufs)) == len(bufs): bufs.append(s)
return call.replace(src=call.src[:1] + tuple(s if s.op is Ops.PARAM or s.is_bound_var else s.param_like(slots[s]) for s in call.src[1:]))
if s.op not in (Ops.PARAM, Ops.BIND) and slots.setdefault(s, len(bufs)) == len(bufs): bufs.append(s)
return call.replace(src=call.src[:1] + tuple(s if s.op in (Ops.PARAM, Ops.BIND) else s.param_like(slots[s]) for s in call.src[1:]))
pm_replace_buffers = PatternMatcher([(UPat(Ops.CALL, name="call"), replace_call_buffers)])
# *****************
# 1.1. prep: staging copies
def _need_staging(a, b): return all_devices_in(a.device, HCQ_DEVS - {"CPU"}) and not all_devices_in(b.device, HCQ_DEVS)
def _need_staging(a, b): return all_devices_in(a.device, HCQ_DEVS) and not all_devices_in(b.device, HCQ_P2P_DEVS)
def _get_enqueue_devs(call:UOp) -> Any|None:
if not (bufs:=call.src[1:]) or not all(all_devices_in(b.device, HCQ_DEVS) for b in bufs): return None
if call.src[0].op is Ops.COPY: bufs = bufs[::-1] # copies push from the src device: p2p writes are faster than reads
devs = min(bufs, key=lambda b: to_tuple(b.device)[0].startswith("CPU")).device # prio to enqueue on not CPU device
return devs if all_devices_in(devs, HCQ_DEVS) else None
def hcq_call_devs(call:UOp) -> Any|None: return next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)
def stage_copy(dst:UOp, src:UOp) -> UOp|None:
if not (_need_staging(src, dst) or _need_staging(dst, src)): return None
@@ -108,7 +105,7 @@ def stage_copy(dst:UOp, src:UOp) -> UOp|None:
return UOp(Ops.LINEAR, src=(src.copy_to_device("CPU").call(stage, src), stage.copy_to_device(dst.device).call(dst, stage)))
def kernel_copy(call:UOp, dst:UOp, src:UOp) -> UOp|None:
if (devs:=_get_enqueue_devs(call)) is None or Device[(dev:=to_tuple(devs)[0])].has_copy_queue: return None
if (devs:=hcq_call_devs(call)) is None or Device[(dev:=to_tuple(devs)[0])].has_copy_queue: return None
d, s = (UOp.param(i, dst.dtype, (n:=dst.max_numel(),), device=devs) for i in range(2))
ast = d.index(r:=UOp.range(n, 0)).store(s.index(r).load()).end(r).sink(arg=KernelInfo(name="copy"), tag=1)
return call.replace(src=(to_program(ast, Device[dev].renderer), dst, src))
@@ -139,7 +136,7 @@ def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tu
def _build_wait_cmds(slots:dict[str, int], dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
# opt1: same-queue ops are fifo-ordered
if devices[0].split(":")[0] in {"AMD", "QCOM", "CPU"} or queue.startswith("COPY"):
if devices[0].split(":")[0] in {"AMD", "QCOM"} or queue.startswith("COPY"):
dep_lanes = [(dep, dlane, lane) for dep, dlane, lane in dep_lanes if (dep[0][dlane], dep[1]) != (devices[lane], queue)]
# opt2: keep latest dep per (dep device, queue, cur lane)
@@ -162,7 +159,7 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
for b in itertools.chain.from_iterable(_get_call_bufs_by_lane(call, devices)):
for bd in to_tuple(b.device): dev_bufs[bd][id(b)] = b
n, fences, resets, fins, signal_tags = len(batch_info), [], [], [], set()
n, fences, fins, signal_tags = len(batch_info), [], [], set()
for _, devgroup in itertools.groupby(sorted(dev_bufs), key=lambda d: d.split(":")[0]):
devs = tuple(devgroup)
@@ -176,17 +173,16 @@ def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[t
fin_submit = make_submit(*waits, UOp(Ops.INS, arg="store", src=(tl_signal, tl_value.index(0))), devs=devs, queue="COMPUTE:0")
epoch = (epoch_slot:=tl_value.after(fin_submit).index(0)).load()
# fence once per device group on this schedule's previous epoch
# fence once per device group on this schedule's previous epoch, then reset any queue signals used by the group
qs = dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])
sched_epoch = make_signal(devs, next(UOp.unique_num))
wait_device_epoch = (done:=tl_signal.after(loop:=UOp.loop(0)).index(0).load()).end(loop, done < sched_epoch.index(0).load())
fences.append(make_call("hcq_fence", UOp.sink(wait_device_epoch), HCQInfo(devs)))
resets = [make_signal(devs, slots[q]).after(wait_device_epoch).index(0).store(0) for q in qs]
# queues of other groups wait on these signals, so reset them only after every group reached its epoch
if qs: resets.append(make_call("hcq_reset", UOp.sink(*[make_signal(devs, slots[q]).index(0).store(0) for q in qs]), HCQInfo(devs)))
fences.append(make_call("hcq_fence", UOp.sink(*(resets or [wait_device_epoch])), HCQInfo(devs)))
fins.append(make_call("hcq_finalizer", UOp.sink(epoch_slot.store(epoch + 1), sched_epoch.after(fin_submit).index(0).store(epoch)), HCQInfo(devs)))
return fences + resets, fins, signal_tags
return fences, fins, signal_tags
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> list[UOp]:
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
@@ -216,7 +212,7 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> li
# and make hcq call
name, info = get_call_name(call, get_call_arg_uops(call)), HCQInfo(devices, estimate_uop(call))
ts_ids = [next(UOp.unique_num) for _ in range(2)] if profile else []
kerns.append((devices, make_call(name, call.src[0], info), tuple(ts_ids)))
kerns.append((devices, name, info.estimates, tuple(ts_ids)))
ts_ins = [UOp(Ops.INS, arg="timestamp", src=(make_signal(devices, s),)) for s in ts_ids]
q += ts_ins[:1] + [call.replace(arg=replace(call.arg, aux=info))] + ts_ins[1:]
@@ -233,7 +229,7 @@ def sched_hcq_batches(l:UOp, profile:bool) -> UOp:
srcs:list[UOp] = []
batch:list[tuple[UOp, tuple[str, ...]]] = []
for call in l.src:
if (devs:=_get_enqueue_devs(call)) is not None: batch.append((call, to_tuple(devs)))
if (devs:=hcq_call_devs(call)) is not None: batch.append((call, to_tuple(devs)))
else: srcs, batch = srcs + _finalize_batch(batch, profile) + [call], []
return l.replace(src=tuple(srcs + _finalize_batch(batch, profile)))
@@ -346,22 +342,21 @@ def split_patches(call:UOp) -> UOp|None:
scatter = make_scatter_loops(input_patches, tables[0], lt_patches)
body = body.substitute({p:p.substitute(scatter | reads) for p in rt_patches})
if inputs: # fence inputs
fills.append((t:=tables[0][0]).after(make_binary_patch(t, bytes(t.max_numel() * 8)))) # zeroed at link, slot 0 is the host fence
body = body.replace(src=(UOp.sink(*body.src[0].src, t.after(*body.src[0].src).index(0).store(0)),)) # open it once consumed
lt_srcs = collections.defaultdict(list)
for p in lt_patches: lt_srcs[p.buf_uop].append(p)
return call.replace(src=(body, *call.src[1:], *[b.after(*ps) for b,ps in lt_srcs.items()], *fills),
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=((call.arg.aux.device,
tuple(sorted(dedup(b.arg.slot for g in inputs for b in unwrap_mstack(g.buf_uop))))),) if inputs else call.arg.aux.input_idxs)))
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=tuple(sorted(dedup(b.arg.slot for g in inputs for b in unwrap_mstack(g.buf_uop)))))))
pm_split_patches = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), split_patches)])
# *****************
def _rank_ranges(uops:Iterable[UOp]) -> dict[UOp, UOp]:
return {r: r.replace(arg=(i,)+r.arg[1:]) for i,r in enumerate(sorted([u for u in uops if u.op is Ops.RANGE], key=lambda r: r.arg))}
def replace_params(call:UOp) -> UOp|None:
body, variables, param_ops = call.src[0], call.src[0].variables(), {Ops.PARAM, Ops.MSTACK}
tops = body.toposort(gate=lambda u: u.op not in param_ops)
args = dedup([s for u in tops for s in u.src if s.op in param_ops and s not in variables])
args = dedup([s for u in body.toposort(gate=lambda u: u.op not in param_ops) for s in u.src if s.op in param_ops and s not in variables])
patched, refhold = partition(call.src[1:], lambda x: x.src[0] in args)
by_root = {p.src[0]: p for p in patched}
@@ -369,13 +364,14 @@ def replace_params(call:UOp) -> UOp|None:
# keep buffers whose addresses become link-time constants alive and mapped
held = args + [r.without_after for r in refhold]
addrs = dedup([g.src[0].without_after for g in call.toposort() if g.op is Ops.GETADDR])
addrs = dedup([g.src[0].without_after for x in call.src for g in x.toposort() if g.op is Ops.GETADDR])
refhold += [a for a in addrs if a not in held and all(b.op is not Ops.PARAM or b.tag is not None for b in unwrap_mstack(a))]
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=HCQ_RUNTIME_DEV.value, volatile=b.op is Ops.PARAM and b.arg.volatile)
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM} | _rank_ranges(tops)
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args + refhold) if u.without_after.tag == "inputs"), None))
return call.replace(src=(body.substitute(sub).replace(arg="hcq_args"), *c_args, *refhold), arg=replace(call.arg, aux=info))
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM}
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args) if u.without_after.tag == "inputs"), None))
return call.replace(src=(body.substitute(sub).replace(arg="hcq_args"), *c_args, *refhold),
arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
pm_replace_params = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
@@ -421,44 +417,8 @@ def callify_hcq(call:UOp, cf:UOp) -> UOp:
pm_callify_hcq = PatternMatcher([(UPat(Ops.CALL, src=(
UPat(Ops.CUSTOM_FUNCTION, arg="hcq_args", src=(UPat(Ops.SINK),), name="cf"),), name="call", allow_any_len=True), callify_hcq)])
# *****************
# 9. merge submitters
def _lane_arg(a:UOp, lane:int, table:UOp) -> UOp: return table if a.tag == "inputs" else a.mselect(lane) if len(to_tuple(a.device)) > 1 else a
def merge_batch(batch:list[UOp]) -> UOp:
tables = UOp.variable("hcq_inputs_ptr", 0, 2**64-1, dtypes.uint64, param=True)
lanes = [(c, j, sum(len(idxs) * 8 for _, idxs in c.arg.aux.input_idxs)) for c in batch for j in range(len(c.arg.aux.device))] # (call, lane, bytes)
offs = itertools.accumulate((table_bytes for _, _, table_bytes in lanes), initial=0) # every lane owns the next table of the region
cmds = [c.src[0].src[0].call(*[_lane_arg(a.without_after, j, tables + off) for a in c.src[1:]], UOp.variable("_device_num", 0, 1 << 30).bind(j))
for (c, j, _), off in zip(lanes, offs)]
info = HCQInfo((HCQ_RUNTIME_DEV.value,), sum((c.arg.aux.estimates for c in batch), start=Estimates()),
input_idxs=tuple(x for c in batch for x in c.arg.aux.input_idxs), kernels=tuple(k for c in batch for k in c.arg.aux.kernels))
body = UOp.custom_function("hcq", make_submit(*cmds, devs=HCQ_RUNTIME_DEV.value, queue="SUBMIT:0").sink())
return body.call(*[s for c in batch for s in c.src[1:] if s.without_after.tag != "inputs"], name=f"hcq_submitter ({len(batch)})", aux=info)
def merge_submitters(linear:UOp) -> UOp:
batches = [(k, list(g)) for k, g in itertools.groupby(linear.src, key=lambda c: isinstance(c.arg.aux, HCQInfo))]
return linear.replace(src=tuple(c for is_hcq, b in batches for c in ([merge_batch(b)] if is_hcq else b)))
# *****************
# hcq schedule
hcq_compile_cache:dict[tuple[bytes, bool], UOp] = {}
def hcq_lower(linear:UOp, pm_encode:PatternMatcher) -> UOp:
# lowering to hcq ir
linear = graph_rewrite(linear, pm_encode, walk=True, name="encode and pack", enter_calls=True)
# patches and runtime uops
linear = graph_rewrite(linear, pm_early_simplify+symbolic, bottom_up=False, name="simplify patches", enter_calls=True)
linear = graph_rewrite(linear, pm_split_patches, walk=True, name="split patches")
# and compile it
linear = graph_rewrite(linear, pm_replace_params, name="replace params")
return graph_rewrite(linear, pm_callify_hcq, name="callify hcq", enter_calls=True)
@rewrite_group(lambda linear,input_uops,profile,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
if input_uops is not None:
@@ -473,9 +433,16 @@ def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
# schedule
linear = graph_rewrite(linear, pm_schedule_and_merge, ctx=({s:p for p,s in back_map.items()}, profile), walk=True, name="schedule and merge hcq")
# lower to hcq programs, then pack the programs of every batch into one C submitter (needs a C runtime device for the program addresses)
linear = hcq_lower(linear, pm_encode_cmdbufs+pm_pack_placeholders)
final_linear = hcq_compile_cache[cache_key] = hcq_lower(merge_submitters(linear), pm_encode_cmdbufs) if HCQ_RUNTIME_DEV.value == "CPU" else linear
# lowering to hcq ir
linear = graph_rewrite(linear, pm_encode_cmdbufs+pm_pack_placeholders, walk=True, name="encode and pack", enter_calls=True)
# patches and runtime uops
linear = graph_rewrite(linear, pm_early_simplify+symbolic+pm_fold_cast_const, bottom_up=False, name="simplify patches", enter_calls=True)
linear = graph_rewrite(linear, pm_split_patches, walk=True, name="split patches")
# and compile it
linear = graph_rewrite(linear, pm_replace_params, name="replace params")
final_linear = hcq_compile_cache[cache_key] = graph_rewrite(linear, pm_callify_hcq, name="callify hcq", enter_calls=True)
return final_linear
@@ -492,7 +459,7 @@ pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, name="buf"), bufferize_buf)])
# 7. resolve patches
def push_stack(op, s): return UOp(Ops.STACK,
src=tuple(op.replace(dtype=op.dtype, src=tuple(x if y is s else y for y in op.src)) for x in s.src))
src=tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
def fold_binary(buf:UOp, blob:UOp) -> UOp:
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)):
@@ -502,7 +469,7 @@ def fold_binary(buf:UOp, blob:UOp) -> UOp:
def fold_const_store(buf:UOp, off:UOp, val:UOp) -> UOp:
for off,val in zip(off.src, val.src):
for b,v in zip((bs:=mb.bufs if isinstance((mb:=buf.buffer), MultiBuffer) else (mb,)), val.src if val.op is Ops.STACK else (val,)*len(bs)):
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype]((v.src[0] if v.op is Ops.CAST else v).val))
data = struct.pack(f'<{v.dtype.fmt}', truncate[v.dtype](v.val))
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(bo:=off.val*buf.dtype.itemsize):bo+len(data)] = data
return UOp(Ops.NOOP)
@@ -517,7 +484,7 @@ def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
pm_resolve_patches = PatternMatcher([
# multi
(UPat(GroupOp.ALU, src=[UPat(Ops.STACK, name="s"), UPat.any(UPat(Ops.CONST), UPat(Ops.CAST, src=(UPat(Ops.CONST),)))], name="op"), push_stack),
(UPat(GroupOp.ALU, src=[UPat(Ops.STACK, name="s"), UPat(Ops.CONST)], name="op"), push_stack),
(UPat(Ops.CAST, src=(UPat(Ops.STACK, name="s"),), name="op"), push_stack),
# getaddr
@@ -543,7 +510,7 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
bufs = {(j,i):a for j,c in enumerate(linear.src) for i,a in enumerate(c.src[1:], 1)
if a.op is Ops.AFTER and unwrap_mstack(a.src[0])[0].tag in HCQ_CACHE_TAGS}
linear = linear.substitute({x:link_buf_cache[k] for a in bufs.values() if (k:=link_buf_key(a)) in link_buf_cache for x in (a, a.src[0])}, walk=True)
linear = graph_rewrite(linear, pm_resolve_patches+symbolic+pm_assert_no_afters, bpm=pm_bufferize, ctx=cache, bottom_up=False,
linear = graph_rewrite(linear, pm_resolve_patches+symbolic+pm_fold_cast_const+pm_assert_no_afters, bpm=pm_bufferize, ctx=cache, bottom_up=False,
name="resolve patches")
for (j,i),a in bufs.items(): link_buf_cache.setdefault(link_buf_key(a), linear.src[j].src[i])
if cache: link_linear_cache[linear_key] = linear
@@ -554,9 +521,9 @@ def hcq_link(linear:UOp, cache=True) -> UOp:
class HCQ2Compiled(Compiled):
timestamp_divider: float = 1000.0
wait_timeout_ms: float = 30000.0
def __init__(self, device:str, allocator:HCQAllocator, compilers:list[type[Renderer]], runtime, can_recover:bool=False, arch=None):
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
self.can_recover = can_recover
self.pm_bufferize = PatternMatcher([
@@ -569,6 +536,7 @@ class HCQ2Compiled(Compiled):
super().__init__(device, allocator, compilers, runtime, None, arch=arch)
self.rt_buffer = Buffer(self.device, 64 << 20, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True))
self.rt_allocator = BumpAllocator(64 << 20)
self.prof_ents:dict[int, ProfileGraphEntry] = {}
@@ -592,13 +560,9 @@ class HCQ2Compiled(Compiled):
tdiffs.append((st+perf_counter_us())/2 - gpu)
Compiled.profile_events.append(ProfileDeviceEvent(self.device, statistics.median(tdiffs), self.device_props()))
@functools.cached_property
def rt_buffer(self) -> Buffer:
return Buffer(self.device, self.rt_allocator.size, dtypes.uint8, options=BufferSpec(uncached=True, cpu_access=True), preallocate=True)
def new_buffer(self, b:UOp, cache:bool) -> Buffer:
if cache or b.tag in HCQ_CACHE_TAGS:
return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(uncached=b.tag != "program", cpu_access=True, nolru=True))
return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(uncached=True, cpu_access=True, nolru=True))
return self.rt_buffer.view(b.max_numel(), b.dtype, self.rt_allocator.alloc(b.max_numel() * b.dtype.itemsize, alignment=128))
@functools.cache
@@ -607,31 +571,41 @@ class HCQ2Compiled(Compiled):
buf.as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')[0] = init_value
return buf
def _wait_signal(self, sig:memoryview, value:int, timeout:int|None=None):
timeout = timeout if timeout is not None and self.can_recover else None
st, done = time.perf_counter(), sig[0]
while done < value:
if done != (done:=sig[0]): st = time.perf_counter()
elif time.perf_counter() - st > (timeout or self.wait_timeout_ms) / 1000: self.on_device_hang()
def synchronize(self, timeout:int|None=None):
if HCQ_RUNTIME_DEV.value != self.device: Device[HCQ_RUNTIME_DEV.value].synchronize()
sig = self.signal("timeline").as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
tl = self.signal("value", 1).as_memoryview(force_zero_copy=True, no_sync=True).cast('Q')
self._wait_signal(sig, tl[0] - 1, timeout)
timeout = timeout if timeout is not None and self.can_recover else None
st = time.perf_counter()
while sig[0] < tl[0] - 1:
if time.perf_counter() - st > (timeout or 3000) / 1000: self.on_device_hang()
if self.prof_ents: self.collect_prof()
def on_device_hang(self): raise RuntimeError(f"{self.device} hang detected")
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
def count(self) -> int: return self.iface.count if hasattr(self, 'iface') else 1
def _select_iface(self):
assert (v:=getenv(k:=f'{type(self).__name__[:-6].upper()}_IFACE', "")) == "", \
f"{k}={v} is deprecated, use DEV={replace(DEV.target(type(self).__name__[:-6]), interface=v)} instead"
assert hasattr(self, "ifaces"), "must have ifaces to select an iface"
t = DEV.target(dev:=type(self).__name__[:-6])
filtered = select_by_name(self.ifaces, lambda i: i.__name__[:-5], t.interface, f"{dev} has no interface {t.interface!r}")
filtered = [i for i in filtered if t.interface.startswith("MOCK") or not i.__name__[:-5].startswith("MOCK")] # never fall back to mock ifaces
return select_first_inited([functools.partial(cast(Callable, iface), self, self.device_id) for iface in filtered],
f"No interface for {dev}:{self.device_id} is available")
def _is_cpu(self) -> bool: return hasattr(self, 'device') and self.device.split(":")[0] == "CPU"
def finalize(self):
try: self.synchronize() # try to finalize the device in any case
except RuntimeError as e: print(f"{self.device} synchronization failed before finalizing: {e}")
super().finalize()
# if the device has an interface, call device_fini to clean up resources
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
@dataclass
class HCQ2Buffer:
@@ -643,17 +617,17 @@ class HCQ2Buffer:
return HCQ2Buffer(self.va_addr+offset, meta=self.meta, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
def _as_buffer(self, buf:HCQBuffer) -> memoryview:
def _as_buffer(self, buf:HCQ2Buffer) -> memoryview:
return unwrap(buf.view).mv
def _map(self, buf:HCQBuffer) -> HCQBuffer:
def _map(self, buf:HCQ2Buffer) -> HCQ2Buffer:
if not hasattr(self, '_do_map'): raise NotImplementedError("map failed: no method implemented")
return self._do_map(buf)
def _do_unmap(self, mb): self.dev.iface.free(mb)
@suppress_finalizing
def _free(self, buf:HCQBuffer, options:BufferSpec|None=None):
def _free(self, buf:HCQ2Buffer, options:BufferSpec|None=None):
if options is not None and options.external_ptr is not None: return
self.dev.synchronize()
if hasattr(self, '_do_free'): self._do_free(buf, options)
@@ -662,4 +636,4 @@ class HCQAllocator(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
self.dev.synchronize()
self._do_unmap(mb)
def _offset(self, buf, size:int, offset:int) -> HCQBuffer: return buf.offset(offset=offset, size=size)
def _offset(self, buf, size:int, offset:int) -> HCQ2Buffer: return buf.offset(offset=offset, size=size)
+6 -4
View File
@@ -173,14 +173,16 @@ class MemoryManager:
va_allocator: ClassVar[TLSFAllocator|None] = None
def __init__(self, dev, vram_size:int, boot_size:int, pt_t, va_bits:int, va_shifts:list[int], va_base:int,
palloc_ranges:list[tuple[int, int]], first_lv:int=0, reserve_ptable=False):
palloc_ranges:list[tuple[int, int]], first_lv:int=0, reserve_ptable=False, paddr_base:int=0):
self.dev, self.vram_size, self.va_shifts, self.va_base, lvl_msb = dev, vram_size, va_shifts, va_base, va_shifts + [va_bits + 1]
self.pte_covers, self.pte_cnt = [1 << x for x in va_shifts][::-1], [1 << (lvl_msb[i+1] - lvl_msb[i]) for i in range(len(lvl_msb) - 1)][::-1]
self.pt_t, self.palloc_ranges, self.level_cnt, self.va_bits, self.reserve_ptable = pt_t, palloc_ranges, len(va_shifts), va_bits, reserve_ptable
self.boot_allocator = TLSFAllocator(boot_size, base=0)
self.ptable_allocator = TLSFAllocator(round_up(vram_size // 512, 1 << 20) if self.reserve_ptable else 0, base=self.boot_allocator.size)
self.pa_allocator = TLSFAllocator(vram_size - (off_sz:=self.boot_allocator.size + self.ptable_allocator.size), base=off_sz)
self.boot_allocator = TLSFAllocator(boot_size, base=paddr_base)
self.ptable_allocator = TLSFAllocator(round_up(vram_size // 512, 1 << 20) if self.reserve_ptable else 0,
base=paddr_base + self.boot_allocator.size)
off_sz = paddr_base + self.boot_allocator.size + self.ptable_allocator.size
self.pa_allocator = TLSFAllocator(vram_size - off_sz, base=off_sz)
self.root_page_table = pt_t(self.dev, self.palloc(0x1000, zero=not self.dev.smi_dev, boot=True), lv=first_lv)
def _frag_size(self, va, sz, must_cover=True):
+9 -9
View File
@@ -8,13 +8,14 @@ from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SC
# unwrap VIEW/CAST/etc to find the actual data source (kernel output, buffer, or multi-device op)
def _unwrap_src(s: UOp) -> UOp:
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK}: s = s.src[0]
while len(s.src) and s.op not in {Ops.AFTER, Ops.BUFFER, Ops.PARAM, Ops.MSELECT, Ops.MSTACK, Ops.BIND}: s = s.src[0]
return s
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states
# a buffer state is AFTER | BUFFER | PARAM. MSELECT/MSTACK join per-device states, BIND is not a buffer dependency
def _states(s: UOp) -> list[UOp]:
s = _unwrap_src(s)
if s.op in {Ops.MSELECT, Ops.MSTACK}: return [st for ss in s.src for st in _states(ss)]
if s.op is Ops.BIND: return []
assert s.op in {Ops.AFTER, Ops.BUFFER, Ops.PARAM}, f"input to kernel must resolve to a buffer state, not {s.op}"
return [s]
@@ -70,7 +71,7 @@ def create_schedule(sched_sink:UOp) -> UOp:
else:
k = rk.src[0] if rk.op is Ops.END else rk
assert k.op is Ops.CALL, f"unexpected op in queue: {k.op}"
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if not s.is_bound_var)
buf_uops = tuple(_unwrap_src(s).buf_uop for s in k.src[1:] if s.op is not Ops.BIND)
linearized.append(k.src[0].call(*buf_uops))
for x in children.get(rk, []):
in_degree[x] -= 1
@@ -99,8 +100,7 @@ pm_post_sched_cache = PatternMatcher([
def resolve_linear_call(linear_call:UOp):
linear = graph_rewrite(linear_call.src[0], pm_post_sched_cache, ctx=({}, linear_call.src[1:]), walk=True, name="params to buffers")
# map the call body params back to the original Variables stored in the call args
binds = {f"p{i}":x.src[0].replace(op=Ops.PARAM) for i,x in enumerate(linear_call.src[1:]) if x.is_bound_var}
binds = {f"p{i}":x.src[0] for i,x in enumerate(linear_call.src[1:]) if x.op is Ops.BIND}
return linear.substitute({v:binds[v.expr] for v in linear.variables() if v.expr in binds}, enter_calls=True, name="resolve scalar params")
pm_resolve_linear_call = PatternMatcher([
@@ -184,13 +184,13 @@ def create_linear_with_vars(big_sink:UOp) -> tuple[UOp, dict[str, int]]:
# vars used in the schedule
used_vars = set().union(*[{v.expr for v in si.src[0].variables()} for si in linear.src])
# get var_vals from the bound Variables in the call args
# get var_vals
var_vals: dict[str, int] = {}
for b in big_sink.src[1:]:
if b.is_bound_var:
v, val = b.unbind()
nm = v.expr
if b.op is Ops.BIND:
nm = b.src[0].expr
if nm not in used_vars: continue
val = b.src[1].val
if var_vals.get(nm, val) != val: raise RuntimeError(f"bind mismatch on {nm}, {var_vals[nm]} != {val}")
var_vals[nm] = val
+3 -5
View File
@@ -24,7 +24,7 @@ class IndexingContext:
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.BUFFER,
Ops.CONST, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
Ops.CONST, Ops.BIND, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
Ops.LOAD, Ops.CALL, Ops.FUNCTION}
def realize(ctx:IndexingContext, tr:UOp) -> None: ctx.realize_map[tr] = None
@@ -35,7 +35,7 @@ def realize_srcs(ctx:IndexingContext, rb:UOp) -> None:
def realize_store_after_src(ctx:IndexingContext, dest:UOp, src:UOp):
# you don't usually have to do this for assign unless there's a WAR hazard like TestAssign.test_assign_double_diamond_reduce
if dest.base in src.toposort(enter_calls=False): ctx.realize_map[src] = None
if dest.base in src.backward_slice_with_self: ctx.realize_map[src] = None
def realize_custom_kernel_srcs(ctx:IndexingContext, c:UOp) -> None:
for s in c.src[1:]:
@@ -69,9 +69,7 @@ def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
# TODO: srcs contain (real data srcs, something else, ranges) and the boundary is confusing. see range_start
def data_srcs(op:Ops, src:tuple[UOp, ...]) -> tuple[UOp, ...]:
if op in {Ops.PARAM, Ops.BUFFER, Ops.RANGE, Ops.SPECIAL}: return ()
# the store of a bound Variable only carries the input value, it has no data srcs
if op is Ops.STORE and src[0].is_variable: return ()
if op in {Ops.PARAM, Ops.BUFFER, Ops.RANGE, Ops.SPECIAL, Ops.BIND}: return ()
if op in GroupOp.Movement|{Ops.INDEX, Ops.STAGE, Ops.REDUCE, Ops.AFTER, Ops.END}: return src[:1]
return src
+3 -3
View File
@@ -22,14 +22,14 @@ def mstack_early_shrink(ms:UOp, shrink:UOp):
def lower_broadcast_copy(c:UOp, x:UOp):
if not (isinstance(c.device, tuple) and isinstance(x.device, str)): return None
if (sx:=x.simplify()).device is None: return UOp(Ops.MSTACK, src=(sx,)*len(c.device))
if (sx:=x.simplify()).device is None and sx.base.op is Ops.CONST: return UOp(Ops.MSTACK, src=(sx,)*len(c.device))
return UOp(Ops.MSTACK, src=tuple(x.copy_to_device(d) for d in c.device))
replace_allreduce = PatternMatcher([
# BROADCAST: explicitly expand broadcast copies and combine with MSTACK
(UPat(Ops.COPY, name="c", src=(UPat(name="x"),)), lower_broadcast_copy),
(UPat(Ops.COPY, name="c", src=(UPat(GroupOp.All-{Ops.CONST}, name="x"),)), lower_broadcast_copy),
# COPY_TO_ONE: if copying from multidevice to one, MSELECT the first (TODO: a little from each?)
(UPat(Ops.COPY, name="c", src=(UPat(name="x"),)), lambda c,x:
(UPat(Ops.COPY, name="c", src=(UPat(GroupOp.All-{Ops.CONST}, name="x"),)), lambda c,x:
x.mselect(0).copy_to_device(c.device) if isinstance(c.device, str) and isinstance(x.device, tuple) else None),
# MSELECT on MSTACK is replaced with nothing
(UPat(Ops.MSELECT, src=(UPat(Ops.MSTACK, name="mstack"),), name="ms"), lambda mstack, ms: mstack.src[ms.arg]),
+17 -22
View File
@@ -4,7 +4,7 @@ import itertools
from tinygrad.dtype import dtypes, AddrSpace, Invalid, to_dtype, strong_dtype
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, KernelInfo, ParamArg, shape_to_shape_arg
from tinygrad.uop.ops import graph_rewrite, sint, AxisType, BottomUpGate, rewrite_group, identity_element
from tinygrad.uop.symbolic import symbolic
from tinygrad.uop.symbolic import symbolic, pm_fold_cast_const
from tinygrad.uop.movement import mop_cleanup
from tinygrad.helpers import prod, getenv, dedup, all_int, DEBUG, SPLIT_REDUCEOP, DEBUG_RANGEIFY, VIZ, MAX_KERNEL_BUFFERS, SPEC
from tinygrad.helpers import PCONTIG, FLOAT16, OPENPILOT_HACKS, argsort, partition, get_single_element
@@ -60,7 +60,7 @@ pm_mops = PatternMatcher([
# 0. do some cleanup rewrites, mostly copied from the old stuff
def fix_store_hazard(target:UOp, src:UOp):
if (base:=target.base) not in src.toposort(enter_calls=False): return None
if (base:=target.base) not in src.backward_slice_with_self: return None
# PERMUTE and FLIP reorder indices, SHRINK can have overlapping regions when dest is also shrunk
unsafe = {Ops.PERMUTE, Ops.FLIP} | ({Ops.SHRINK} if target.op_in_backward_slice_with_self(Ops.SHRINK) else set())
reaches_base: dict[UOp, bool] = {}
@@ -313,9 +313,9 @@ pm_const_buffer_folding = pm_mops+PatternMatcher([
lambda idx,after: idx.const_like(Invalid) if after_all_invalid(after) else None),
# hack if a noop turned to a const
(UPat(Ops.NOOP, src=(UPat.cvar("c"),)), lambda c: c),
# a deviceless MSTACK src is the same value on every device, so indexing the stack is just indexing that value
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True, name="idx"),
lambda s,idx: idx.replace(src=(s,)+idx.src[1:]) if s.device is None else None),
# mstack on CONST is CONST
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True),
lambda s: c if (c:=s.base).op is Ops.CONST else None),
])
pm_remove_bufferize = PatternMatcher([
@@ -327,9 +327,6 @@ pm_remove_bufferize = PatternMatcher([
(UPat(Ops.END, src=(UPat(Ops.NOOP, name="x"),), allow_any_len=True), lambda x: x),
])
def strip_zero_offset_shrink(x:UOp) -> UOp:
return x.src[0] if x.op is Ops.SHRINK and all(resolve(start == 0, False) for start,_ in x.marg) else x
def no_indexing_calls(u:UOp):
new_srcs = []
for x in u.src:
@@ -339,9 +336,8 @@ def no_indexing_calls(u:UOp):
new_srcs.append(x.src[0])
elif x.op is Ops.SHRINK:
# SHRINK with offset 0 is fine
new_srcs.append(strip_zero_offset_shrink(x))
elif x.op is Ops.MSTACK:
new_srcs.append(x.replace(src=tuple(strip_zero_offset_shrink(s) for s in x.src)))
# TODO: check offset
new_srcs.append(x.src[0])
else:
# everything else we pass through
new_srcs.append(x)
@@ -465,12 +461,11 @@ pm_add_buffers = pm_mops+pm_flatten_bufferize+PatternMatcher([
class LocalAddBufferContext:
dg:int = 0
map:dict = field(default_factory=dict)
vars:dict = field(default_factory=dict)
range:int = 0
opts:tuple|None = None
def debuf(ctx:LocalAddBufferContext, buf:UOp):
# Variables (ALU buffers with a value range) are scalar symbolic values, not real buffers: they become ALU params with no slot
if buf.is_variable: return buf.replace(op=Ops.PARAM)
param = UOp(Ops.PARAM, src=(UOp.const(prod(buf.max_shape)),),
arg=ParamArg(ctx.dg, buf.dtype, addrspace=buf.addrspace, device=buf.device))
ret = param.reshape(buf.max_shape)
@@ -480,6 +475,10 @@ def debuf(ctx:LocalAddBufferContext, buf:UOp):
ctx.dg += 1
return ret
def unbind_kernel(ctx:LocalAddBufferContext, b:UOp):
ctx.vars[b] = None
return b.src[0]
def handle_after(ctx:LocalAddBufferContext, after:UOp):
if after.addrspace == AddrSpace.LOCAL: return None
buf = after.buf_uop
@@ -503,7 +502,8 @@ to_define_global = PatternMatcher([
(UPat(Ops.STORE, name="x"), find_bufs),
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
(UPat(Ops.PARAM, name="v"), lambda v:
v.replace(arg=replace(v.arg, slot=-1)) if v.arg.name is not None and v.arg.vmin_vmax is not None and v.arg.slot != -1 else None),
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype, multiple_of=v.arg.multiple_of)
if v.arg.name is not None and v.arg.vmin_vmax is not None else None),
# this renumbers the params
(UPat(Ops.PARAM, name="buf"), lambda ctx, buf:
@@ -512,8 +512,7 @@ to_define_global = PatternMatcher([
# ALU params are scalar symbolic values, not buffers.
(UPat(Ops.INDEX, src=(UPat(Ops.PARAM, name="v"),)), lambda v: v if v.addrspace == AddrSpace.ALU else None),
# bound Variables are stores into Variable buffers: strip the store, the buffer becomes an ALU param via debuf
(UPat(Ops.AFTER, name="b"), lambda b: b.src[0] if b.is_bound_var else None),
(UPat(Ops.BIND, name="b"), unbind_kernel),
(UPat(Ops.AFTER, name="after"), handle_after),
# remove device from local BUFFERIZE
@@ -542,16 +541,13 @@ pm_add_param_range_tags = PatternMatcher([
def split_store(x:UOp) -> UOp|None:
# if we have any open ranges here, we don't split. open DEVICE ranges are fine, they are bound per device at launch
if any(r.arg[-1] is not AxisType.DEVICE for r in x.ranges): return None
# the store of a bound Variable is an input value, not a kernel
st = x.src[0] if x.op is Ops.END else x
if st.op is Ops.STORE and st.src[0].is_variable: return None
# local kernel rewrite
lctx = LocalAddBufferContext()
ret = graph_rewrite(x, to_define_global+pm_flatten_range+rangeify_codegen, ctx=lctx, name="kernel split", bottom_up=True)
# create the Kernel. NOTE: buffers can be on different devices here now, they are compiled to SDMA copies later by schedule
return ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts)).call(*lctx.map.values())
return ret.sink(arg=KernelInfo(opts_to_apply=lctx.opts)).call(*lctx.map.values(), *lctx.vars.keys())
split_kernels = PatternMatcher([
(UPat((Ops.STORE, Ops.END), name="x"), split_store),
@@ -588,7 +584,7 @@ def get_kernel_graph(sink:UOp) -> UOp:
tsink, rctx = run_rangeify(tsink, bool(DEBUG_RANGEIFY))
tsink = graph_rewrite(tsink,
symbolic+pm_reduce_simplify+pm_const_buffer_folding+pm_remove_bufferize,
symbolic+pm_fold_cast_const+pm_reduce_simplify+pm_const_buffer_folding+pm_remove_bufferize+pm_no_indexing_calls,
name="symbolic+reduce_collapse+debuf")
tsink = graph_rewrite(tsink, pm_limit_bufs, ctx=rctx, name="limit buffers")
@@ -599,7 +595,6 @@ def get_kernel_graph(sink:UOp) -> UOp:
paramarg_start: int = max([-1]+slots) + 1
tsink = graph_rewrite(tsink, pm_add_buffers+pm_add_param_range_tags, ctx=itertools.count(paramarg_start), bottom_up=True, name="stage to store")
tsink = graph_rewrite(tsink, split_kernels, bottom_up=True, name="split kernels")
tsink = graph_rewrite(tsink, pm_no_indexing_calls, name="remove indexing from call args")
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Kernel Graph")
if SPEC:
+13 -14
View File
@@ -109,7 +109,7 @@ def _precompiled_output_redirect(s:UOp, t:UOp) -> UOp|None:
def transform_precompiled_call(c:UOp) -> UOp|None:
if not c.arg.precompile: return None
assert c.src[0].op is Ops.TUPLE, f"expected TUPLE body for precompiled FUNCTION, got {c.src[0].op}"
input_buffers = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in c.src[1:])
input_buffers = tuple(x.contiguous() if x.op not in {Ops.AFTER, Ops.BIND} else x for x in c.src[1:])
# add the outputs to the call
srcs = c.src[0].src
@@ -176,8 +176,6 @@ pm_early_transform_tensor_graph = PatternMatcher([
])
def finalize_after(ctx:AllocCtx, x:UOp):
# bound Variables are call inputs, not assigns: they stay in the graph and pm_replace_buf turns them into call args
if x.is_bound_var: return None
# untagged: record as an assign for the call body
if x.tag is None:
ctx.assigns.append(x)
@@ -198,7 +196,7 @@ def finalize_after(ctx:AllocCtx, x:UOp):
def replace_input_buffer(ctx:AllocCtx, b:UOp):
ctx.replacements.append(b)
if b.is_bound_var or b.is_variable: return b.param_like(len(ctx.replacements)-1)
if b.op is Ops.BIND: return b.param_like(len(ctx.replacements)-1)
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
@@ -216,8 +214,8 @@ pm_replace_buf = PatternMatcher([
# replace SHRINK with PARAM
(UPat(Ops.SHRINK, src=(UPat(Ops.BUFFER),), name="b", allow_any_len=True), replace_input_view),
(UPat(Ops.BITCAST, src=(UPat.any(UPat(Ops.SHRINK, src=(UPat(Ops.BUFFER),), allow_any_len=True), UPat(Ops.BUFFER)),), name="b"), replace_input_view),
# strip the stored value from bound Variables for cache key normalization, so different values hit same cache
(UPat(Ops.AFTER, name="b"), lambda ctx,b: replace_input_buffer(ctx, b) if b.is_bound_var else None),
# strip value from BIND for cache key normalization, so different values hit same cache
(UPat(Ops.BIND, src=(UPat(Ops.PARAM), UPat(Ops.CONST)), name="b"), replace_input_buffer),
])
@rewrite_group(lambda _,ret: f"Callify {pluralize('Buffer', len(ret[1]))}")
@@ -453,11 +451,12 @@ class Tensor(RandMixin):
return self
# STORE+AFTER: STORE is the write effect (void), AFTER wraps the view for correct shape/ranging
assign = self.uop.after(self.uop.store(x.uop))
ib = self.uop
while not ib.has_buffer_identity() and ib.op in GroupOp.Movement|{Ops.BITCAST, Ops.DETACH}: ib = ib.src[0]
if ib is not self.uop and ib.has_buffer_identity(after_ok=True):
if (base := self.uop.base).op in {Ops.BUFFER, Ops.AFTER} and self.uop is not base and not self.uop.has_buffer_identity():
# view assign: replace at the buffer-identity level (e.g. RESHAPE(BUFFER)) so @function's substitution catches it
_apply_map_to_tensors({ib: ib.after(assign)}, name="Embed View Assign")
ib = self.uop
while not ib.has_buffer_identity() and ib is not base: ib = ib.src[0]
assigned_ib = ib.after(assign)
_apply_map_to_tensors({ib: assigned_ib}, name="Embed View Assign")
else:
# simple assign
self.uop = assign
@@ -665,13 +664,13 @@ class Tensor(RandMixin):
```
"""
all_uops = self.uop.toposort()
# backward fills .grad for every in-scope float tensor with a device
# backward fills .grad for every in-scope non-CONST float tensor
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.is_floating_point() and t.device is not None]
t.uop in all_uops and t.is_floating_point() and t.uop.op is not Ops.CONST]
# 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}"
if g.device is None: g = g.clone(device=t.device)
if g.device is None and t.device is not None: g = g.clone(device=t.device)
if t.grad is None: t.grad = g
else: t.grad.assign(t.grad + g.to(t.grad.device))
return self
@@ -742,7 +741,7 @@ class Tensor(RandMixin):
the reference frames (`ref_frames`).
"""
ref_frames = [x.contiguous() for x in ref_frames or []]
assert frame_pos.is_bound_var, "frame_pos must be a bound Variable"
assert frame_pos.op is Ops.BIND, "frame_pos must be a bound Variable"
srcs = (out:=Tensor.empty(*shape, device=self.device, dtype=self.dtype), self.contiguous(), state.contiguous(), *ref_frames)
fn = UOp(Ops.CUSTOM_FUNCTION, src=(frame_pos.src[0], *[UOp.const(s, dtypes.int) for s in shape]), arg="encdec")
return Tensor(out.uop.after(fn.call(*[s.uop for s in srcs], frame_pos)))

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