Compare commits

...
Author SHA1 Message Date
qazalandGitHub 81dc8ec232 Revert "amd: fix ALL2ALL speed on amdgpu (gpt) (#17518)" (#17526)
This reverts commit cc6d33bde783969caa436857a2eb8d0d1dc1345d.
2026-08-14 10:39:14 +09:00
geohot 95ca5081fe hotfix: update extra/runbook_digitalocean_mi350x 2026-08-13 17:26:39 -07:00
sirhcmandGitHub 303d1677b3 qcomcl: use qemu for compilation (#17524) 2026-08-13 20:16:02 -04:00
George HotzandGitHub 673c6463f9 disable HCQ2 for AMD CI (#17523) 2026-08-13 13:14:40 -07:00
wozeparrotandGitHub 849074f0db gptoss: use fa swa (#17522) 2026-08-13 10:39:22 -07:00
George HotzandGitHub 0252cb8fa7 remove anchors from CI flow (#17521)
* ci: remove yaml anchors from test.yml for gitea actions compatibility

Gitea Actions does not support YAML anchors/aliases, which causes the
workflow to fail parsing. Replace the &linux/*linux anchor with a plain
runs-on: ubuntu-24.04.

* ci: keep runner selection, inline expression instead of anchors

Instead of replacing the anchored runs-on with a plain ubuntu-24.04
(which drops the namespace-profile-tinygrad routing for collaborator
PRs), inline the full ${{ }} expression at every job. No YAML anchors,
works with runners that can't parse them (gitea runner), and identical
behavior on GitHub Actions.
2026-08-13 10:17:22 -07:00
qazalandGitHub cc6d33bde7 amd: fix ALL2ALL speed on amdgpu (gpt) (#17518) 2026-08-13 16:24:45 +09:00
qazalandGitHub 16c5ff2490 add external_benchmark_all2all.py (#17507)
* add external_benchmark_all2all.py

* mv

* more minimal

* less

* fix space
2026-08-13 15:50:43 +09:00
wozeparrotandGitHub 1b7f040984 fa: paas through window (#17517) 2026-08-13 14:45:57 +08:00
qazalandGitHub 39d144546e fix mxfp4 mem estimate (#17515)
* add mem estimates

* rename

* move
2026-08-13 14:48:15 +09:00
George HotzandGitHub e103fb2a10 more lil llm improvements (#17514)
* more lil llm improvements

* default float
2026-08-12 20:13:20 -07:00
George HotzandGitHub 2297118541 lil llm improvements (#17513) 2026-08-12 19:29:32 -07:00
sirhcmandGitHub cd6d0d6ee3 allow running QCOMCL compiler in docker (#17499) 2026-08-12 22:27:55 -04:00
geohot ff0cb28c21 skip slow whisper tests 2026-08-12 13:12:37 -07:00
qazalandGitHub ed8297a102 kerenl opts test from nan in llama 8b (#17510)
* all2all

* nan

* remove that

* less

* has_local

* only the nan change here

* use nice getitem syntax for INDEX

* work

* remove

* even simpler
2026-08-13 04:07:30 +09:00
RaineandGitHub de04781b36 simplify equivalent const max (#17505)
* add const max folds

* add regression test

* move
2026-08-12 08:39:29 -07:00
nimlgenandGitHub 4f106ebe87 hcq2: enqueue speed (#17504) 2026-08-12 13:08:53 +03:00
qazalandGitHub 04c271ac41 simplify digitalocean_mi350x (#17502)
* simplify digitalocean_mi350x

* no hardcoded rocm path
2026-08-12 16:00:04 +09:00
qazalandGitHub 2e5a9a4121 no hardcoded device names in test_sliced_buffer_function (#17501) 2026-08-12 15:19:25 +09:00
qazalandGitHub e1013a6356 llama: create dataset cache by default in dev_beam (#17500) 2026-08-12 15:03:39 +09:00
wozeparrotandGitHub f891f5ffd0 gptoss: route lm_head thru asm_gemm (#17497) 2026-08-11 18:32:50 -07:00
George HotzandGitHub 3686a1758f mac/rdma imports lazy (#17496)
* mac/rdma imports lazy

* ish

* fixes
2026-08-11 17:23:11 -07:00
George HotzandGitHub 4a253db9b4 minor cleanups to improve import speed (#17495)
* minor cleanups to improve import speed

* dumb
2026-08-11 16:06:25 -07:00
sirhcmandGitHub 479ffb0cda remove Ops.SLICE (#17492) 2026-08-11 18:50:04 -04:00
nimlgenandGitHub 2b5018e86a hcq2: fix debug 2 info (#17491)
* hcq2: fix debug 2 info

* x

* x

* x
2026-08-12 00:25:44 +03:00
George HotzandGitHub e11df72e0f notes from digitalocean_mi350x (#17494)
* notes from digitalocean_mi350x

* cleanup

* revert non-doc changes on digitalocean_mi350x branch
2026-08-11 13:22:11 -07:00
38 changed files with 519 additions and 253 deletions
-10
View File
@@ -41,10 +41,6 @@ inputs:
description: "Install LLVM?"
required: false
default: 'false'
tinydreno:
description: "Install tinydreno"
required: false
default: 'false'
qemu:
description: "Install qemu"
required: false
@@ -277,12 +273,6 @@ 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'
-4
View File
@@ -94,7 +94,6 @@ jobs:
shell: bash -e -o pipefail {0}
env:
DEV: ${{ matrix.dev }}
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
@@ -149,7 +148,6 @@ jobs:
shell: bash -e -o pipefail {0}
env:
DEV: ${{ matrix.dev }}
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
@@ -202,7 +200,6 @@ jobs:
shell: bash -e -o pipefail {0}
env:
DEV: ${{ matrix.dev }}
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
@@ -252,7 +249,6 @@ jobs:
shell: bash -e -o pipefail {0}
env:
DEV: ${{ matrix.dev }}
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
-32
View File
@@ -179,35 +179,3 @@ 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 -35
View File
@@ -21,7 +21,7 @@ concurrency:
jobs:
docs:
name: Docs
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' }}
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: 10
env:
CHECK_OOB: 0
@@ -61,7 +61,7 @@ jobs:
torchbackend:
name: Torch Backend Tests
runs-on: *linux
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
@@ -88,7 +88,7 @@ jobs:
bepython:
name: Python Backend
runs-on: *linux
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
@@ -126,7 +126,7 @@ jobs:
linter:
name: Linters
runs-on: *linux
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: 10
steps:
@@ -157,7 +157,7 @@ jobs:
nulltest:
name: Null Tests
runs-on: *linux
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:
@@ -191,7 +191,7 @@ jobs:
unittest:
name: Unit Tests
runs-on: *linux
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:
@@ -228,7 +228,7 @@ jobs:
matrix:
group: [1, 2]
name: SPEC=2 (${{ matrix.group }})
runs-on: *linux
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
@@ -244,7 +244,7 @@ jobs:
fuzzing:
name: Fuzzing
runs-on: *linux
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: 10
steps:
- name: Checkout Code
@@ -260,7 +260,7 @@ jobs:
testopenclimage:
name: CL IMAGE Tests
runs-on: *linux
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
@@ -280,7 +280,7 @@ jobs:
testopenpilot:
name: openpilot Compile Tests
runs-on: *linux
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
@@ -309,7 +309,7 @@ jobs:
testonnxcpu:
name: ONNX (CPU) Tests
runs-on: *linux
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: 20
steps:
@@ -328,7 +328,7 @@ jobs:
testoptim:
name: Optimization Tests
runs-on: *linux
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: 20
steps:
- name: Checkout Code
@@ -360,7 +360,7 @@ jobs:
testllm:
name: Test LLM
runs-on: *linux
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
env:
CHECK_OOB: 0
@@ -387,7 +387,7 @@ jobs:
testmodels:
name: Models
runs-on: *linux
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
@@ -407,7 +407,7 @@ jobs:
testdsp:
name: Linux (DSP)
runs-on: *linux
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
@@ -435,7 +435,7 @@ jobs:
- 'WEBGPU'
name: Linux (DEV=${{ matrix.dev }})
runs-on: *linux
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: 20
steps:
- name: Checkout Code
@@ -461,7 +461,7 @@ jobs:
testamdasm:
name: AMD ASM IDE
runs-on: *linux
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: 20
env:
DEV: MOCKKFD+AMD
@@ -507,7 +507,7 @@ jobs:
hcq2:
name: hcq2
runs-on: *linux
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: 5
steps:
- name: Checkout Code
@@ -529,7 +529,7 @@ jobs:
testmockam:
name: Linux (am)
runs-on: *linux
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
env:
DEV: MOCKPCI+AMD
@@ -565,7 +565,7 @@ jobs:
arch: [gfx1100, gfx1201, gfx950]
name: Linux (${{ matrix.backend }} ${{ matrix.arch }})
runs-on: *linux
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
env:
DEV: MOCKKFD+AMD:${{ matrix.backend == 'amdllvm' && 'LLVM' || '' }}:${{ matrix.arch }}
@@ -603,7 +603,7 @@ jobs:
backend: [ptx, nv]
name: Linux (${{ matrix.backend }})
runs-on: *linux
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: 20
env:
FORWARD_ONLY: 1
@@ -637,10 +637,17 @@ jobs:
strategy:
fail-fast: false
matrix:
backend: [ir3, nak]
name: Compile-only (${{ matrix.backend }})
runs-on: *linux
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' }}
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
@@ -649,21 +656,14 @@ jobs:
with:
key: compile-${{ matrix.backend }}
deps: "testing_unit mesa"
- name: Set env
qemu: ${{ contains(matrix.dev, 'QCOMCL') }}
- name: Test IMAGE
shell: bash
run: printf "NULL_ALLOW_COPYOUT=1\n${{ matrix.backend == 'ir3' && 'DEV=NULL:IR3:a630' || matrix.backend == 'nak' && 'DEV=NULL:NAK:sm_120' }}" >> $GITHUB_ENV
if: contains(matrix.dev, 'a630')
run: DEBUG=7 python3 test/backend/test_ops.py TestOps.test_gemm | grep isam
- 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
+10 -6
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
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.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 sliding:
attn = self._sliding_attention(xq, xk, xv, sinks)
elif getenv("HK_FLASH_ATTENTION"):
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)
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:
attn = self._sliding_attention(xq, xk, xv, sinks)
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,7 +263,11 @@ 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)
logits = self.norm(h) @ self.output.T
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
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:-1} BENCHMARK=${BENCHMARK:-10}
export FAKEDATA=${FAKEDATA:-$([[ "$DEV" == NULL:* ]] && echo 1 || echo 0)} 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 PATH="/opt/rocm-7.1.1/bin:$PATH"
export ROCM_PATH="/opt/rocm-7.1.1"
export ROCM_PATH=${ROCM_PATH:-/opt/rocm-7.1.1}
export PATH="$ROCM_PATH/bin:$PATH"
export DEV=${DEV:-AMD}
export CHECK_OOB=0
export REWRITE_STACK_LIMIT=5000000 HCQDEV_WAIT_TIMEOUT_MS=240000
+2 -1
View File
@@ -122,7 +122,8 @@ 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"custom_mxfp4_gemm_{M}_{N}_{K}", estimates=Estimates(ops=2*M*N*K)))
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)))
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))))
+217
View File
@@ -0,0 +1,217 @@
# Runbook: Llama 3 8B Training on DigitalOcean MI350X
## Machine Specs
- 8x MI350X GPUs (gfx950, device ID 75b0), 288GB VRAM each
- 2TB RAM, 192 CPUs, 2TB disk
- ROCm 7.14 at `/opt/rocm` (NOT `/opt/rocm-7.1.1` like the submission scripts assume)
- Python 3.12
## Phase 1: System Setup
### 1.1 Install packages
```bash
apt-get update
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
```
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:
```bash
apt-get install -y amdrocm-core-dev
```
This installs `hip/hip_runtime.h` at `/opt/rocm/core-7.14/include/hip/hip_runtime.h`.
The symlink `/opt/rocm/include``/opt/rocm/core-7.14/include` makes it available at `/opt/rocm/include/hip/hip_runtime.h`.
### 1.4 Configure ROCm comgr
ROCm 7.14 ships comgr 3.3 at `/opt/rocm/lib/libamd_comgr.so`. tinygrad's DLL loader needs explicit env vars to find it (it searches for `libcomgr.so*` by default, not `libamd_comgr.so*`). Set these in the run command:
```bash
export COMGR_PATH=/opt/rocm/lib/libamd_comgr.so
export COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so
```
Also add ROCm libs to ldconfig so comgr's shared library dependencies resolve:
```bash
cat > /etc/ld.so.conf.d/rocm.conf << 'EOF'
/opt/rocm/lib
/opt/rocm/lib/llvm/lib
/opt/rocm/lib/rocm_sysdeps/lib
EOF
ldconfig
```
### 1.5 Install geohot tmux config
```bash
curl -sL https://raw.githubusercontent.com/geohot/configuration/master/.tmux.conf -o ~/.tmux.conf
```
### 1.6 Reload amdgpu driver
tinygrad's HCQ backend needs `/dev/kfd` which is created by the amdgpu kernel driver.
If the driver was unloaded, reload it:
```bash
modprobe amdgpu
ls /dev/kfd # should exist
```
## Phase 2: Clone tinygrad
```bash
cd /root
git clone https://github.com/tinygrad/tinygrad.git
cd tinygrad
python3 -m pip install --break-system-packages -e .
```
## Phase 3: Download C4 Dataset
The C4 data is on the MLCommons Cloudflare R2 bucket in Megatron-LM indexed format.
```bash
rclone config create mlc-training s3 provider=Cloudflare \
access_key_id=76ea42eadb867e854061a1806220ee1e \
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
```
Files downloaded (~85GB total, ~6 minutes):
- `c4-train.en_6_text_document.bin` (79 GB)
- `c4-train.en_6_text_document.idx` (870 MB)
- `c4-validation-91205-samples.en_text_document.bin` (159 MB)
- `c4-validation-91205-samples.en_text_document.idx` (1.8 MB)
- `LICENSE.txt`, `NOTICE.txt`
**Wait for rclone to fully complete before starting training.** Starting training while the dataset is still downloading will read a truncated .bin file, causing `ValueError: all input arrays must have the same shape` in the dataloader. The stale `.index_cache` and `.blend_cache` files must also be deleted if this happens:
```bash
rm -f /raid/datasets/c4-8b/*.index_cache /raid/datasets/c4-8b/*.blend_cache
```
## Phase 4: wandb Login
```bash
wandb login
```
Enter API key from https://wandb.ai/authorize
Alternatively, pass the key directly:
```bash
wandb login <API_KEY>
```
## Phase 5: Run Training
Run training in tmux so it survives SSH disconnects:
```bash
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
```
Attach with `tmux attach -t train`.
### 5.1 Smoke test (beam search, 2 layers, real data)
Always run beam first to validate the pipeline:
```bash
tmux new-session -d -s beam 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_beam.sh 2>&1 | tee /root/beam.log'
```
The beam test runs 10 training steps with 2 layers. Expected results:
- ~0.29s per step after warmup
- ~700K GFLOPS, ~7% MFU (low because only 2 layers)
- ~380 GB VRAM used
- Loss stable at ~12.55 with random init
### 5.2 Full training run
```bash
tmux new-session -d -s train 'cd /root/tinygrad && COMGR_PATH=/opt/rocm/lib/libamd_comgr.so COMGR_3_PATH=/opt/rocm/lib/libamd_comgr.so CC=/opt/rocm/core-7.14/lib/llvm/bin/clang DEV=AMD:HIP ROCM_PATH=/opt/rocm WANDB=1 bash examples/mlperf/training_submission_v6.0/tinycorp/benchmarks/llama31_8b/implementations/tinybox_8xMI350X/dev_run.sh 2>&1 | tee /root/train.log'
```
## Environment Variable Reference
| Variable | Value | Why |
|---|---|---|
| `COMGR_PATH` | `/opt/rocm/lib/libamd_comgr.so` | tinygrad's DLL loader needs explicit path to find comgr 3.3 |
| `COMGR_3_PATH` | `/opt/rocm/lib/libamd_comgr.so` | comgr 3.x uses a separate `comgr_3` module with its own path var |
| `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 |
| `WANDB` | `1` | Enable wandb logging (off by default) |
## Architecture
| Component | Source file |
|---|---|
| Model | `examples/mlperf/models/flat_llama.py` — FlatTransformer, FP8 MXFP4 weights, fused QKV, flash attention |
| Trainer | `examples/mlperf/model_train.py``train_llama3()` |
| Optimizer | `examples/mlperf/optim.py` — GradAccClipAdamW, master weights, FP8 re-quant |
| LR schedule | `examples/mlperf/lr_schedulers.py` — CosineAnnealingLRWithWarmup |
| Dataloader | `examples/mlperf/dataloader.py` — Megatron-LM indexed bin format |
| ASM GEMM | `extra/gemm/cdna_asm_gemm.py` — gfx950 MFMA assembly, MXFP4 |
| Flash attention | `extra/thunder/amd/fa.py` |
| Fused kernels | `extra/llama_kernels/` — rmsnorm, silu, quantize, fused_ce |
| GPU driver | `tinygrad/runtime/ops_amd.py` — HCQ, direct KFD ioctl |
| Renderer | `tinygrad/renderer/cstyle.py` — HIPRenderer for gfx950 |
| comgr compiler | `tinygrad/runtime/support/compiler_amd.py` — HIPCompiler using comgr 3.3 |
## Troubleshooting
### `'hip/hip_runtime.h' file not found`
Install `amdrocm-core-dev`:
```bash
apt-get install -y amdrocm-core-dev
```
### `'gfx950' is not a recognized processor` + LLVM crash
System clang doesn't know gfx950. Set `CC=/opt/rocm/core-7.14/lib/llvm/bin/clang`.
### `comgr not available: try setting COMGR_PATH?`
Add ROCm libs to ldconfig and set `COMGR_PATH` and `COMGR_3_PATH`:
```bash
# /etc/ld.so.conf.d/rocm.conf should contain /opt/rocm/lib paths
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.
### `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`.
## Appendix: KVM Virtualization Observations
### Virtualization detection
```
$ systemd-detect-virt
kvm
$ lspci -nn | grep AMD
83:00.0 ... Device [1002:75b0]
```
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.
### amdgpu driver behavior
On first boot, amdgpu loaded and bound to all 8 GPUs. On one boot it failed to initialize:
```
[ 799.780369] amdgpu 0000:83:00.0: Failed to alloc msi vectors
[ 799.781476] amdgpu 0000:83:00.0: sw_init of IP block <vega20_ih> failed -22
[ 799.782724] amdgpu 0000:83:00.0: amdgpu_device_ip_init failed
[ 799.793885] amdgpu 0000:83:00.0: Fatal error during GPU init
```
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.
### 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.
+55 -9
View File
@@ -110,7 +110,49 @@ def _sharded_empty_like(ref:Tensor, axis:int|None=None) -> Tensor:
return _sharded_empty(ref.shape, ref, axis)
@functools.cache
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 _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 grad(dou:UOp, ker:UOp) -> tuple:
do = Tensor(dou, device=dou.device)
attn = Tensor(ker.src[1].after(ker), device=ker.src[1].device)
@@ -118,6 +160,8 @@ 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
@@ -128,8 +172,10 @@ 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))[: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, window=window))[: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)
@@ -149,7 +195,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):
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):
assert attn_mask is None, "attn_mask not supported"
assert is_causal, "only causal attention supported"
@@ -176,18 +222,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)
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)
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), 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, window=window), 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):
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):
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"-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}"]
Q_BLOCK_SIZE = 32
NUM_WARPS = 8
@@ -247,10 +293,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):
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):
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"-DATTN_B={B}", f"-DATTN_N={N}", f"-DATTN_H={H}", f"-DATTN_H_KV={H_KV}", f"-DATTN_D={D}", f"-DWINDOW={window}"]
BLOCK_SIZE_KV = 256
GROUP_SIZE = H // H_KV
+2 -3
View File
@@ -422,9 +422,8 @@ class TestCustomKernel(unittest.TestCase):
return Tensor.custom_kernel(y, x, fxn=custom_add_one_kernel)[0]
GlobalCounters.reset()
y = run(x[0]).realize()
# 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)
# backends that support contiguous views don't launch extra kernels
assert_kernel_count(2 if x[0].uop.contiguous_view() is None else 1)
self.assertEqual(y.tolist(), [1, 2, 3, 4])
@Context(DEV="CPU")
+17
View File
@@ -0,0 +1,17 @@
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
View File
@@ -74,6 +74,7 @@ class TestWhisper(unittest.TestCase):
err
)
@slow
def test_transcribe_file1(self):
self.assertEqual(transcribe_file(self.model, self.enc, TEST_FILE_1), TRANSCRIPTION_1)
@@ -89,6 +90,7 @@ 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)
+8 -3
View File
@@ -27,10 +27,15 @@ def _make_linear(buffer_lists, copies=None):
calls.append(UOp(Ops.CALL, src=(src0, *bufs)))
return UOp(Ops.LINEAR, src=tuple(calls))
def _get_planned_view(buf:UOp) -> tuple[UOp, int, int]|None:
view = buf.src[0] if buf.op is Ops.BITCAST else buf
if view.op is not Ops.SHRINK or view.src[0].op is not Ops.BUFFER: return None
return (arena:=view.src[0]), view.src[1].val * arena.dtype.itemsize, view.src[2].val * arena.dtype.itemsize
def _get_arena(buf, linear, result):
for orig_si, new_si in zip(linear.src, result.src):
for orig, new in zip(orig_si.src[1:], new_si.src[1:]):
if orig is buf and new.op is Ops.SLICE: return new.src[0]
if orig is buf and (planned:=_get_planned_view(new)) is not None: return planned[0]
return None
def check_assign(buffer_lists, copies=None):
@@ -41,8 +46,8 @@ def check_assign(buffer_lists, copies=None):
replace_map: dict[int, tuple[UOp, int, int]] = {}
for orig_si, new_si in zip(linear.src, result.src):
for orig, new in zip(orig_si.src[1:], new_si.src[1:]):
if new.op is Ops.SLICE and id(orig) not in replace_map:
replace_map[id(orig)] = (new.src[0], new.src[1].val * new.src[0].dtype.itemsize, new.arg * new.dtype.itemsize)
if (planned:=_get_planned_view(new)) is not None and id(orig) not in replace_map:
replace_map[id(orig)] = planned
# verify pinned buffers are not planned
for buf in held_bufs:
+5
View File
@@ -948,6 +948,11 @@ 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)
+9
View File
@@ -239,6 +239,15 @@ 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
+22 -1
View File
@@ -13,12 +13,18 @@ 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):
def generate(tokens, **kwargs):
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
@@ -38,6 +44,15 @@ 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")),
@@ -178,6 +193,12 @@ 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)
+15 -25
View File
@@ -1,5 +1,4 @@
import unittest
from unittest.mock import MagicMock
from tinygrad import Device
from tinygrad.uop.ops import Ops, UOp
from tinygrad.dtype import dtypes
@@ -11,36 +10,27 @@ class TestMetalGraph(unittest.TestCase):
self.MetalGraph = MetalGraph
self.dev = Device[Device.DEFAULT]
def metal_buf(self, offset):
buf = MagicMock()
if offset > 0:
buf.op = Ops.SLICE
src = MagicMock()
src.dtype = dtypes.uint8
buf.src = (src, UOp.const(offset))
buf.dtype = dtypes.uint8
else:
buf.op = Ops.BUFFER
buf.device = Device.DEFAULT
return buf
def metal_buf(self, offset, bitcast=False):
size = 4 if bitcast else 1
buf = UOp.new_buffer(Device.DEFAULT, offset+size, dtypes.uint8)
if offset: buf = buf[offset:offset+size]
return buf.bitcast(dtypes.float32) if bitcast else buf
def call(self, *bufs):
c = MagicMock()
c.src = (MagicMock(op=Ops.PROGRAM),) + tuple(bufs)
return c
def supports_uop(self, *bufs):
return self.MetalGraph.supports_uop([self.dev], UOp(Ops.PROGRAM, src=(UOp.sink(),)).call(*bufs))
def test_supports_uop_normal_offset(self):
assert self.MetalGraph.supports_uop([self.dev], self.call(self.metal_buf(0), self.metal_buf(100), self.metal_buf(0xFFFFFFFF))) is True
assert self.supports_uop(self.metal_buf(0), self.metal_buf(100), self.metal_buf(0xFFFFFFFF)) is True
def test_supports_uop_overflow_offset(self):
assert self.MetalGraph.supports_uop([self.dev], self.call(self.metal_buf(0), self.metal_buf(0x100000000))) is False
assert self.supports_uop(self.metal_buf(0), self.metal_buf(0x100000000)) is False
def test_supports_uop_nonmetal_buf(self):
# non-SLICE ops should not be checked for offset
buf = MagicMock()
buf.op = Ops.BUFFER
buf.device = Device.DEFAULT
self.MetalGraph.supports_uop([self.dev], self.call(buf))
def test_supports_uop_non_view_buf(self):
assert self.supports_uop(self.metal_buf(0)) is True
def test_supports_uop_bitcast(self):
assert self.supports_uop(self.metal_buf(0xFFFFFFFF, bitcast=True)) is True
assert self.supports_uop(self.metal_buf(0x100000000, bitcast=True)) is False
if __name__ == "__main__":
unittest.main()
+1 -3
View File
@@ -44,8 +44,6 @@ def graph_split_rewrite(linear:UOp, max_batch_size:int=0) -> UOp:
current_batch, current_batch_devs = [], []
for si in linear.src:
if si.src[0].op is Ops.SLICE: continue
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
@@ -180,7 +178,7 @@ class CapturedJit(Generic[ReturnType]):
if call.op is not Ops.CALL: continue
arg_uops = get_call_arg_uops(call)
outs, ins = get_call_outs_ins(call)
out |= {arg_uops[k] for k in set(outs) - set(ins) if arg_uops[k].op in (Ops.BUFFER, Ops.SLICE)}
out |= {b for k in set(outs) - set(ins) if (b:=u if (cv:=(u:=arg_uops[k]).contiguous_view()) is None else cv[0]).op is Ops.BUFFER}
return out
def __call__(self, input_uops:list[UOp], var_vals:dict[str, int]) -> ReturnType:
+21 -29
View File
@@ -4,8 +4,8 @@ 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, wait_cond
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, buffers, graph_rewrite
from tinygrad.device import Device, Buffer, MultiBuffer
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
from tinygrad.renderer import Estimates
from tinygrad.codegen import to_program
from tinygrad.codegen.opt.postrange import args_from_ast
@@ -17,7 +17,7 @@ def get_call_arg_uops(call:UOp) -> tuple[UOp, ...]: return tuple(s for s in call
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)
if ast.op in (Ops.COPY, Ops.SLICE): return (0,), (1,)
if ast.op is Ops.COPY: return (0,), (1,)
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "encdec": return (0,), tuple(range(1, len(get_call_arg_uops(call))))
return (), ()
@@ -27,9 +27,6 @@ def get_call_name(call:UOp, bufs:Sequence[Buffer|UOp], var_vals:dict[str, int]|N
ast, arg_uops = call.src[0], get_call_arg_uops(call)
if ast.op is Ops.PROGRAM: return ast.arg.name
if ast.op is Ops.SLICE:
offset = ast.src[1].val * arg_uops[1].dtype.itemsize
return colored(f"view {_uop_sz_to_str(arg_uops[0]):>10} @ {offset:<10d}", "yellow")
if ast.op is Ops.COPY: return colored(f"copy {_uop_sz_to_str(arg_uops[0]):>10}, {_dev_str(bufs[0]):>7s} <- {_dev_str(bufs[1]):7s}", "yellow")
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "encdec": return colored(f"enc/dec {_uop_sz_to_str(arg_uops[0])}", "yellow")
if ast.op is Ops.CUSTOM_FUNCTION and ast.arg == "graph": return colored(f"batched {len(ast.src[0].src)}", "cyan")
@@ -140,7 +137,7 @@ class ExecContext:
cache: bool = True
def _resolve(b:UOp, inputs:tuple[UOp, ...]) -> UOp:
if b.op in (Ops.SLICE, Ops.MSELECT, Ops.SHRINK) and b.src[0].op is Ops.PARAM: return b.replace(src=(inputs[b.src[0].arg.slot], *b.src[1:]))
if b.op in (Ops.MSELECT, Ops.SHRINK) and b.src[0].op is Ops.PARAM: return b.replace(src=(inputs[b.src[0].arg.slot], *b.src[1:]))
if b.op is Ops.MSTACK: return b.replace(src=tuple(_resolve(x, inputs) for x in b.src))
return inputs[b.arg.slot] if b.op is Ops.PARAM else b
def resolve_params(call:UOp, inputs:tuple[UOp, ...]) -> list[UOp]: return [_resolve(b, inputs) for b in get_call_arg_uops(call)]
@@ -154,13 +151,6 @@ def unwrap_multi(call:UOp, resolved:list[UOp]) -> Iterator[tuple[list[Buffer], d
for x in call.src[0].toposort())
for j, per_dev in enumerate(zip(*[cast(MultiBuffer, b).bufs for b in bufs])): yield list(per_dev), {"_device_num": j} if has_dnum else {}
def exec_view(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
resolved = resolve_params(call, ctx.input_uops)
bufs = [cast(Buffer, b.buffer) for b in resolved]
bv = bufs[1].view(resolved[0].max_numel(), ast.dtype, ast.src[1].val*bufs[1].dtype.itemsize)
with track_stats(ctx, call, bv.device, [bv, bufs[1]], ctx.var_vals): buffers[resolved[0]] = bv
return None
def exec_copy(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
for bufs, device_vars in unwrap_multi(call, resolve_params(call, ctx.input_uops)):
dest, src = bufs[0].ensure_allocated(), bufs[1].ensure_allocated()
@@ -210,27 +200,30 @@ def exec_graph(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
return t[0]
def exec_hcq(ctx:ExecContext, call:UOp, ast:UOp) -> float|None:
if (inputs:=call.arg.aux.inputs) is not None:
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+inputs].buffer
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
exec_kernel(replace(ctx, update_stats=False), call, ast)
exec_kernel(replace(ctx, update_stats=DEBUG>=3), call, ast)
tms:list[float|None] = []
for e in (aux:=call.arg.aux).prof: cast(Any, Device[e.device]).prof_ents[e.st_id] = e
for d in [cast(Any, Device[x]) for x in aux.device]:
with track_stats(ctx, call, d.device, [], ctx.var_vals) as et:
if ctx.wait:
d.synchronize(timeout=ctx.timeout)
ts = [d.signal(i)._buf.cpu_view().view(fmt='Q')[0] for e in aux.prof if e.device == d.device for i in (e.st_id, e.en_id)]
if ts: et[0] = float(max(ts)-min(ts))/d.timestamp_divider/1e6
tms += et
return tms[0]
tms = []
for devices,name,estimates,prof in info.kernels:
for device in devices:
tm = None
if prof:
(d:=cast(Any, Device[device])).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)
stat_call = call.replace(arg=replace(call.arg, name=name, aux=replace(info, estimates=estimates, kernels=())))
with track_stats(ctx, stat_call, 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
pm_flatten_linear = PatternMatcher([
@@ -261,7 +254,6 @@ pm_optimize_local_size = PatternMatcher([
])
pm_exec = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.SLICE, name="ast"),), name="call", allow_any_len=True), exec_view),
(UPat(Ops.CALL, src=(UPat(Ops.COPY, name="ast"),), name="call", allow_any_len=True), exec_copy),
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="ast"),), name="call", allow_any_len=True), exec_kernel),
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="encdec", name="ast"),), name="call", allow_any_len=True), exec_encdec),
@@ -276,7 +268,7 @@ def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:li
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE) if profile is None else profile)
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
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
+8 -8
View File
@@ -1,9 +1,9 @@
from __future__ import annotations
import time
START_TIME = time.perf_counter()
import os, functools, platform, re, contextlib, operator, hashlib, pickle, sqlite3, tempfile, pathlib, string, ctypes, sys, gzip, getpass, gc
import os, functools, re, contextlib, operator, hashlib, pickle, sqlite3, tempfile, pathlib, string, ctypes, sys, gzip, getpass, gc
from collections import defaultdict
import subprocess, shutil, math, types, copyreg, inspect, importlib, decimal, itertools, difflib
import shutil, math, types, copyreg, inspect, importlib, decimal, itertools, difflib
from dataclasses import dataclass, field, replace
from typing import ClassVar, Iterable, Any, TypeVar, Callable, Sequence, TypeGuard, Iterator, Generic, Generator, cast, overload
@@ -13,8 +13,7 @@ U = TypeVar("U")
def prod(x:Iterable[T]) -> T|int: return functools.reduce(operator.mul, x, 1)
# NOTE: helpers is not allowed to import from anything else in tinygrad
OSX, WIN = platform.system() == "Darwin", sys.platform == "win32"
ARCH_X86 = any(x in platform.processor() for x in ("Intel", "i386", "x86_64"))
OSX, WIN = sys.platform == "darwin", sys.platform == "win32"
BASEDIR = pathlib.Path(__file__).parent
# fix colors on Windows, https://stackoverflow.com/questions/12492810/python-how-can-i-make-the-ansi-escape-codes-to-work-also-in-windows
@@ -231,7 +230,7 @@ class _DEV(ContextVar):
DEV, DEBUG, BEAM, NOOPT = _DEV("DEV", ""), ContextVar("DEBUG", 0), ContextVar("BEAM", 0), ContextVar("NOOPT", 0)
IMAGE, FLOAT16, OPENPILOT_HACKS = ContextVar("IMAGE", 0), ContextVar("FLOAT16", 0), ContextVar("OPENPILOT_HACKS", 0)
JIT, JIT_BATCH_SIZE = ContextVar("JIT", 2 if OSX and ARCH_X86 else 1), ContextVar("JIT_BATCH_SIZE", 32)
JIT, JIT_BATCH_SIZE = ContextVar("JIT", 1), ContextVar("JIT_BATCH_SIZE", 32)
CHUNK_SIZE = 2**20 # TinyFS content-addressed store: blob chunk + hash-tree node granularity
WINO, CAPTURING, TRACEMETA, NO_COLOR = ContextVar("WINO", 0), ContextVar("CAPTURING", 1), ContextVar("TRACEMETA", 1), ContextVar("NO_COLOR", 0)
TRAINING = ContextVar("TRAINING", 0)
@@ -454,9 +453,9 @@ def _ensure_downloads_dir() -> pathlib.Path:
if pathlib.Path("/etc/tinybox-release").is_file():
# try creating dir with sudo
if not (downloads_dir := pathlib.Path("/raid/downloads")).exists():
subprocess.run(["sudo", "mkdir", "-p", downloads_dir], check=True)
subprocess.run(["sudo", "chown", "tiny:root", downloads_dir], check=True)
subprocess.run(["sudo", "chmod", "775", downloads_dir], check=True)
system(f"sudo mkdir -p {downloads_dir}")
system(f"sudo chown tiny:root {downloads_dir}")
system(f"sudo chmod 775 {downloads_dir}")
return downloads_dir
return pathlib.Path(cache_dir) / "downloads"
@@ -497,6 +496,7 @@ def fetch_fw(path:str, name:str, sha256:str) -> bytes:
# *** Exec helpers
def system(cmd:str, **kwargs) -> str:
import subprocess
st = time.perf_counter()
try: ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT, **kwargs).decode().strip()
except subprocess.CalledProcessError as e:
+13 -8
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import functools, itertools, pathlib
from dataclasses import dataclass, replace
from tinygrad import Tensor, nn, UOp, TinyJit, getenv, function
from tinygrad import Tensor, nn, UOp, TinyJit, getenv, function, dtypes
from tinygrad.nn import Linear
from tinygrad.llm.gguf import gguf_load
from tinygrad.uop.ops import resolve
@@ -187,8 +187,8 @@ class TransformerBlock(FFNBlock):
def _init_state(self, x:Tensor):
if not hasattr(self, "cache_kv"):
# TODO: how is the dtype of this determined?
self.cache_kv = Tensor.empty(2, x.shape[0], self.config.n_kv_heads, self.config.max_context, self.config.head_dim, device=x.device)
self.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)
self.freqs_cis = precompute_freqs_cis(self.config.rope_dim, self.config.max_context, self.config.rope_theta, device=x.device)
class MLATransformerBlock(FFNBlock):
@@ -261,13 +261,14 @@ 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 hasattr(self, "ssm_g_a") else self.attn_gate(x)
out_gate = self.ssm_g_b(self.ssm_g_a(x)) if is_kda 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 hasattr(self, "ssm_f_a") else self.ssm_alpha(x)
alpha = self.ssm_f_b(self.ssm_f_a(x)) if is_kda 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)
@@ -291,14 +292,13 @@ 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 hasattr(self, "ssm_g_a") else out_gate.silu()
out_gate = out_gate.sigmoid() if is_kda 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"):
@@ -326,7 +326,8 @@ 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)
logits = self.output(self.output_norm(x))[:, -1, :]
# only run the output projection on the last token
logits = self.output(self.output_norm(x[:, -1:]))[:, -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)
@@ -420,6 +421,10 @@ 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)
+4 -1
View File
@@ -1,4 +1,4 @@
import json, math, pathlib, zipfile, pickle, tarfile, struct, functools, io, zlib
import json, math, pathlib, struct, functools, io, zlib
from collections import OrderedDict
from typing import Any, Callable, BinaryIO, Iterable, cast
from tinygrad.tensor import Tensor
@@ -219,6 +219,7 @@ def load_state_dict(model, state_dict:dict[str, Tensor], strict=True, verbose=Tr
@accept_filename
def zip_extract(t: Tensor) -> dict[str, Tensor]:
import zipfile
files: dict[str, Tensor] = {}
with zipfile.ZipFile(TensorIO(t), "r") as myzip:
# sadly, the extra length needs to be read from the local header of each file.
@@ -249,6 +250,7 @@ def tar_extract(t: Tensor) -> dict[str, Tensor]:
tensors = nn.state.tar_extract(Tensor(pathlib.Path("archive.tar")))
```
"""
import tarfile
with tarfile.open(fileobj=TensorIO(t), mode="r") as tar:
return {member.name:t[member.offset_data:member.offset_data+member.size] for member in tar if member.type == tarfile.REGTYPE}
@@ -303,6 +305,7 @@ def torch_load(t:Tensor) -> dict[str, Tensor]:
"FloatTensor": None, "Parameter": Parameter}
whitelist = {"torch", "collections", "numpy", "_codecs"} # NOTE: this is not for security, only speed
class Dummy: pass
import pickle, zipfile, tarfile
class TorchPickle(pickle.Unpickler):
def find_class(self, module, name):
module_root = module.split(".")[0]
+3 -2
View File
@@ -1,4 +1,4 @@
import glob, importlib, os, pathlib, shutil, subprocess, tarfile, tempfile
import glob, importlib, os, pathlib, subprocess
from tinygrad.helpers import fetch, flatten, system, getenv
root = (here:=pathlib.Path(__file__).parent).parents[2]
@@ -31,6 +31,7 @@ def load(name, files, **kwargs):
if not (f:=(root/(path:=kwargs.pop("path", __name__)).replace('.','/')/f"{name}.py")).exists() or getenv('REGEN'):
files, kwargs['args'] = files() if callable(files) else files, args() if callable(args:=kwargs.get('args', [])) else args
if (srcs:=kwargs.pop('srcs', None)):
import tempfile, tarfile
srcpath = (td:=tempfile.TemporaryDirectory(f"autogen-src-{name.replace('/','-')}")).name + "/"
for src in (srcs if isinstance(srcs, list) else [srcs]):
if 'tar' in src:
@@ -157,7 +158,7 @@ def __getattr__(nm):
*[f"python3 src/compiler/nir/nir_{s}_h.py --outdir gen" for s in ["intrinsics", "intrinsics_indices"]]]), cwd=path, shell=True, check=True),
srcs="https://gitlab.freedesktop.org/mesa/mesa/-/archive/mesa-25.2.7/mesa-25.2.7.tar.gz",
dll=f"'tinymesa_cpu' if DEV.renderer == 'LVP' else 'tinymesa', {tinymesa_path}, emsg='pip install tinymesa==25.2.7.2'",
prolog=["from tinygrad.helpers import DEV", "import gzip, base64, platform, sysconfig, os"],
prolog=["from tinygrad.helpers import DEV", "import gzip, base64, sysconfig, os"],
epilog=lambda path: [system(f"{root}/extra/mesa/lvp_nir_options.sh {path}")])
case "libclang":
return load("libclang",
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Literal, TypeAlias
from tinygrad.runtime.support.c import _IO, _IOW, _IOR, _IOWR
from tinygrad.runtime.support import c
from tinygrad.helpers import DEV
import gzip, base64, platform, sysconfig, os
import gzip, base64, sysconfig, os
dll = c.DLL('mesa', 'tinymesa_cpu' if DEV.renderer == 'LVP' else 'tinymesa', os.path.join(sysconfig.get_paths()['platlib'], 'tinymesa'), emsg='pip install tinymesa==25.2.7.2')
class struct_u_printf_info(c.Struct): pass
u_printf_info: TypeAlias = struct_u_printf_info
+2 -2
View File
@@ -6,7 +6,6 @@ from tinygrad.device import Buffer, BufferSpec, Compiled, Device, MultiBuffer, P
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import UOp, Ops, Variable
from tinygrad.engine.jit import GraphRunner, MultiGraphRunner
from tinygrad.runtime.ops_rdma import RDMACopyQueue
class HCQGraph(MultiGraphRunner):
def __init__(self, *args, **kwargs):
@@ -50,7 +49,7 @@ class HCQGraph(MultiGraphRunner):
self.comp_queues: dict[HCQCompiled, HWQueue] = {dev: unwrap(dev.hw_compute_queue_t)() for dev in self.devices}
self.copy_queues: dict[tuple[HCQCompiled, int], HWQueue] = {} # lazy allocation, keyed by (device, queue_idx)
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], RDMACopyQueue] = {} # lazy allocation, keyed by device pair
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], "RDMACopyQueue"] = {} # lazy allocation, keyed by device pair
self.num_copy_queues: int = getenv("HCQ_NUM_SDMA", min(len(self.devices), 8) if ALL2ALL >= 1 else 1)
self.num_rdma_ops: dict[tuple[HCQCompiled, HCQCompiled], int] = collections.defaultdict(int)
@@ -104,6 +103,7 @@ class HCQGraph(MultiGraphRunner):
elif is_rdma:
enqueue_queue = self.comp_queues[enqueue_dev]
rdma_key = (cast(HCQCompiled, Device[bufs[0].device]).rdma_dev(), enqueue_dev.rdma_dev())
from tinygrad.runtime.ops_rdma import RDMACopyQueue
self.rdma_queues.setdefault(rdma_key, RDMACopyQueue(enqueue_dev.rdma_dev()))
else:
assert (enqueue_dev.hw_copy_queue_t is not None), "device must implement a copy queue"
+2 -1
View File
@@ -113,5 +113,6 @@ class MetalGraph(GraphRunner):
@staticmethod
def supports_uop(batch_devs, new_call:UOp) -> bool:
# Metal ICB replay encodes offsets as uint32; reject if any Metal buffer offset exceeds 32-bit range.
if any(b.op in {Ops.SLICE, Ops.SHRINK} and b.src[1].val * b.src[0].dtype.itemsize > 0xFFFFFFFF for b in new_call.src[1:]): return False
for shrink in [s for src in new_call.src[1:] if (s:=src.src[0] if src.op is Ops.BITCAST else src).op is Ops.SHRINK]:
if shrink.src[1].val * shrink.src[0].dtype.itemsize > 0xFFFFFFFF: return False
return GraphRunner.supports_uop(batch_devs, new_call)
+1 -1
View File
@@ -1,5 +1,5 @@
from __future__ import annotations
import platform, sys, os, ctypes, functools, mmap, threading, array, itertools
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
+25 -4
View File
@@ -1,6 +1,6 @@
import ctypes, struct
import ctypes, struct, platform, pathlib, shutil, subprocess, sys, tarfile, tempfile
from tinygrad.device import Compiler
from tinygrad.helpers import DEBUG, system
from tinygrad.helpers import DEBUG, system, fetch, unwrap
from tinygrad.runtime.support.compiler_mesa import disas_adreno
# see https://github.com/sirhcm/tinydreno
from tinygrad.runtime.autogen import llvm_qcom
@@ -10,10 +10,17 @@ 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"
self.arch, self.chip_id, self.llvm_inst = arch, 0x6030001, llvm_qcom.cl_compiler_create_llvm_instance()
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)
super().__init__(f"compile_qcomcl_{arch}")
def __del__(self): llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst)
def __del__(self): llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst) if platform.machine() == "aarch64" else self.compiler_process.kill()
def __reduce__(self): return QCOMCompiler, (self.arch,)
@@ -25,6 +32,10 @@ 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)))
@@ -36,3 +47,13 @@ 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()
+15 -14
View File
@@ -33,13 +33,13 @@ class HCQInfo:
input_idxs:tuple[int, ...] = () # indexes into input_uops used by this call
inputs:int|None = None
prof:tuple[ProfileGraphEntry, ...] = () # st_id/en_id are timestamp signal slots until collect
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
def unwrap_mstack(u):
if u.op is Ops.MSTACK: return tuple(x for s in u.src for x in unwrap_mstack(s))
return unwrap_mstack(u.src[0]) if u.op in {Ops.MSELECT, Ops.SLICE} else (u,)
return unwrap_mstack(u.src[0]) if u.op is Ops.MSELECT else (u,)
def is_value_known_at_link(val:UOp) -> bool:
runtime_reads = [u for u in val.toposort() if u.op in (Ops.LOAD, Ops.INDEX)]
@@ -199,10 +199,10 @@ def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]], profile:bool) -> li
signal_tags |= cur_signal_tags
# build fences and finalizers
fences, finalizers, finalizer_signal_tags = _build_finalizers(batch, batch_info, deps_tracker, slots)
fences, fins, finalizer_signal_tags = _build_finalizers(batch, batch_info, deps_tracker, slots)
signal_tags |= finalizer_signal_tags
src, prof = [], []
src, kerns = [], []
for tag, ((call, _), (devices, queue), q) in enumerate(zip(batch, batch_info, call_waits)):
# first queue use, sync prior device work with the device timeline
if batch_info.index((devices, queue)) == tag:
@@ -212,18 +212,18 @@ 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 []
prof += [ProfileGraphEntry(d, name, *ts_ids) for d in devices if 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:]
# signal the queue if someone waits for us
if tag in signal_tags: q += [UOp(Ops.INS, arg="store", src=(make_signal(devices, slots[queue]), UOp.const(tag + 1, dtypes.uint64)))]
src.append(make_call(name, make_submit(*q, devs=devices, queue=queue).sink(), info))
src.append(make_call(f"submit {name}", make_submit(*q, devs=devices, queue=queue).sink(), info))
# append batch timestamps to finalizers
finalizers = [f.replace(arg=replace(f.arg, aux=replace(a:=f.arg.aux, prof=tuple(e for e in prof if e.device in a.device)))) for f in finalizers]
return fences + src + finalizers
fins = [f.replace(arg=replace(f.arg, aux=replace(a:=f.arg.aux, kernels=tuple(x for x in kerns if set(x[0]) & set(a.device))))) for f in fins]
return fences + src + fins
def sched_hcq_batches(l:UOp, profile:bool) -> UOp:
srcs:list[UOp] = []
@@ -377,14 +377,15 @@ pm_replace_params = PatternMatcher([
# *****************
def resolve_getaddr_slice(bv:UOp, g:UOp) -> UOp:
def resolve_getaddr_view(bv:UOp, g:UOp) -> UOp:
base = bv.src[0].after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ())
itemsize = bv.src[0].dtype.itemsize if bv.src[0].without_after.op in (Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
if bv.op is Ops.BITCAST: return UOp(Ops.GETADDR, src=(base,), arg=g.arg)
itemsize = bv.src[0].dtype.itemsize if bv.src[0].without_after.op in (Ops.BUFFER, Ops.MSTACK, Ops.MSELECT) else bv.dtype.itemsize
return UOp(Ops.GETADDR, src=(base,), arg=g.arg) + UOp.const(bv.src[1].val * itemsize, dtypes.uint64)
pm_early_simplify = PatternMatcher([
(UPat(Ops.GETADDR, src=(UPat.any(sl:=UPat((Ops.SLICE, Ops.SHRINK), name="bv"), sl.after(allow_any_len=True)),), name="g"), resolve_getaddr_slice),
(UPat(Ops.INDEX, src=(UPat(Ops.SLICE, name="bv"),), allow_any_len=True, name="x"),
(UPat(Ops.GETADDR, src=(UPat((Ops.SHRINK, Ops.BITCAST), name="bv").or_after(),), name="g"), resolve_getaddr_view),
(UPat(Ops.INDEX, src=(UPat(Ops.SHRINK, name="bv"),), allow_any_len=True, name="x"),
lambda bv,x: x.replace(src=(bv.src[0], x.src[1] + bv.src[1].cast(x.src[1].dtype), *x.src[2:]))),
])
@@ -402,7 +403,7 @@ def pack_hcq_placeholders(call:UOp) -> UOp|None:
sizes[b.tag] = offs[b] + b.max_numel()
counts = collections.Counter(b.tag for b in bufs)
bases = {b.tag:UOp.placeholder((sizes[b.tag],), b.dtype, next(UOp.unique_num), device=b.device).rtag(b.tag) for b in bufs if counts[b.tag] > 1}
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
subs = {b:bases[b.tag][(off:=offs.get(b, 0)):off+b.max_numel()] for b in bufs if b.tag in bases}
return call.replace(src=(call.src[0].substitute(subs, walk=True), *call.src[1:])) if subs else None
pm_pack_placeholders = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
@@ -493,7 +494,7 @@ pm_resolve_patches = PatternMatcher([
(UPat(name="buf").index(UPat(Ops.RANGE), allow_any_len=True)
.store(UPat.any(UPat(Ops.BINARY, name="blob"), UPat(Ops.BINARY, name="blob").bitcast()).index(UPat(Ops.RANGE), allow_any_len=True).load())
.end(UPat(Ops.RANGE)), fold_binary),
(UPat({Ops.BUFFER, Ops.SLICE, Ops.MSTACK}, name="buf").index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
(UPat({Ops.BUFFER, Ops.MSTACK}, name="buf").index(UPat(Ops.STACK, name="off")).store(UPat(Ops.STACK, name="val")), fold_const_store),
])
pm_assert_no_afters = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: panic(RuntimeError, f"AFTER left at hcq_link: {a.src[0].op}"))])
+4 -2
View File
@@ -1,7 +1,8 @@
from __future__ import annotations
import os, mmap, array, functools, ctypes, select, contextlib, dataclasses, sys, itertools, struct, socket, subprocess, time, enum, atexit
import os, mmap, array, functools, ctypes, ctypes.util, select, contextlib, dataclasses, sys, itertools, struct, socket
import subprocess, time, enum, atexit
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir, DEBUG, flatten, pluralize
from tinygrad.runtime.autogen import libc, pci, vfio, iokit, corefoundation
from tinygrad.runtime.autogen import libc, pci, vfio
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface, HCQBuffer, hcq_filter_visible_devices
from tinygrad.runtime.support.memory import VirtMapping, AddrSpace, BumpAllocator
from tinygrad.runtime.support.usb import USB3, CustomASM24Controller, USBMMIOInterface
@@ -55,6 +56,7 @@ class _System:
def pci_scan_bus(self, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None) -> list[str]:
all_devs = []
if OSX:
from tinygrad.runtime.autogen import iokit, corefoundation
def read_prop(svc, key) -> int:
cfkey = corefoundation.CFStringCreateWithCString(None, key.encode(), corefoundation.kCFStringEncodingUTF8)
cfdata = ctypes.cast(iokit.IORegistryEntryCreateCFProperty(svc, ctypes.cast(cfkey, iokit.CFStringRef), None, 0), corefoundation.CFDataRef)
+3 -7
View File
@@ -23,7 +23,7 @@ class IndexingContext:
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(0)
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.BUFFER, Ops.SLICE,
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.BUFFER,
Ops.CONST, Ops.BIND, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
Ops.LOAD, Ops.CALL, Ops.FUNCTION}
@@ -34,10 +34,6 @@ def realize_srcs(ctx:IndexingContext, rb:UOp) -> None:
if s.base.op not in ALWAYS_CONTIGUOUS: ctx.realize_map[s] = None
def realize_store_after_src(ctx:IndexingContext, dest:UOp, src:UOp):
# don't realize SLICE when it's the direct source of STORE+AFTER — the target buffer is the output
if src.op is Ops.SLICE and src in ctx.realize_map \
and not dest.op_in_backward_slice_with_self(Ops.SHRINK, Ops.PERMUTE, Ops.FLIP, Ops.PAD):
del ctx.realize_map[src]
# you don't usually have to do this for assign unless there's a WAR hazard like TestAssign.test_assign_double_diamond_reduce
if dest.base in src.backward_slice_with_self: ctx.realize_map[src] = None
@@ -74,7 +70,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, Ops.BIND}: return ()
if op in GroupOp.Movement|{Ops.INDEX, Ops.SLICE, Ops.STAGE, Ops.REDUCE, Ops.AFTER, Ops.END}: return src[:1]
if op in GroupOp.Movement|{Ops.INDEX, Ops.STAGE, Ops.REDUCE, Ops.AFTER, Ops.END}: return src[:1]
return src
def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
@@ -84,7 +80,7 @@ def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
for i, s in enumerate(x.src):
new_src = s
src_rngs = broadcast_rngs(x, s, ctx.range_map[x][0]) if x in ctx.range_map else ()
if s.op in {Ops.PARAM, Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
if s.op in {Ops.PARAM, Ops.BUFFER, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
if x in ctx.range_map and i < data_src_count: new_src = new_src.index(*src_rngs)
elif s in ctx.realize_map:
realized_ranges = ctx.realize_map[s]
+2 -4
View File
@@ -52,11 +52,9 @@ def memory_plan_rewrite(linear:UOp, held_bufs:set[UOp]|None=None) -> UOp:
peaks[_key(buf)] = (max(peaks[_key(buf)][0], offsets[buf] + buf.max_numel() * buf.dtype.itemsize), peaks[_key(buf)][1])
arena_sizes = {key: round_up(peak, block_size) for key, (peak, _) in peaks.items()}
# build replace_map: each buffer becomes a SLICE into a shared per-device-lane arena
# build replace_map: each buffer becomes a SHRINK/BITCAST into a shared per-device-lane arena
arenas = {key: UOp.new_buffer(key[0], sz, dtypes.int8) for key, sz in arena_sizes.items()}
replace_map:dict[UOp, UOp] = {}
for buf_uop, offset in offsets.items():
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(offset)), buf_uop.max_numel())
replace_map = {buf_uop:arenas[_key(buf_uop)][offset:offset+buf_uop.nbytes()].bitcast(buf_uop.dtype) for buf_uop, offset in offsets.items()}
if DEBUG >= 1 and (omem:=sum(nbytes.values()) / 1e6) != (nmem:=sum(arena_sizes.values()) / 1e6):
print(f"memory reduced from {omem:.2f} MB -> {nmem:.2f} MB, {len(first_appearance)} -> {len(arenas)} bufs")
+1 -1
View File
@@ -93,7 +93,7 @@ class Ops(FastEnum):
CONTIGUOUS = auto(); CONTIGUOUS_BACKWARD = auto(); DETACH = auto()
# buffer ops
STAGE = auto(); COPY = auto(); SLICE = auto(); MSELECT = auto(); MSTACK = auto(); CUSTOM_FUNCTION = auto()
STAGE = auto(); COPY = auto(); MSELECT = auto(); MSTACK = auto(); CUSTOM_FUNCTION = auto()
# the core 6 movement ops! these only exist in the tensor graph
RESHAPE = auto(); PERMUTE = auto(); EXPAND = auto(); PAD = auto(); FLIP = auto()
+8 -25
View File
@@ -45,8 +45,7 @@ axis_colors = {AxisType.DEVICE: "green", AxisType.GLOBAL: "blue", AxisType.THREA
axis_to_pos = {AxisType.DEVICE: -2, AxisType.WEAK: -1, AxisType.LOOP: -1, AxisType.THREAD: 0, AxisType.GLOBAL: 0, AxisType.WARP: 1,
AxisType.LOCAL: 2, AxisType.UPCAST: 3, AxisType.GROUP_REDUCE: 2, AxisType.REDUCE: 4, AxisType.UNROLL: 5}
range_start = {Ops.STAGE: 1, Ops.REDUCE: 1, Ops.WMMA: 3, Ops.END: 1, Ops.CALL: 1, Ops.FUNCTION: 1,
Ops.SLICE: 2, Ops.LINEAR: 0}
range_start = {Ops.STAGE: 1, Ops.REDUCE: 1, Ops.WMMA: 3, Ops.END: 1, Ops.CALL: 1, Ops.FUNCTION: 1, Ops.LINEAR: 0}
# https://en.wikipedia.org/wiki/Identity_element
def identity_element(op:Ops, dt:DType) -> PyConst: return dt.const({Ops.ADD:0, Ops.MUL:1, Ops.MAX:dt.min}[op])
@@ -171,9 +170,6 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
return arg.dtype
case Ops.BINARY:
return dtypes.uint8
case Ops.SLICE:
# TODO: slice just shouldn't exist
return None
case Ops.CAST | Ops.BITCAST:
assert isinstance(arg, DType), f"CAST/BITCAST arg must be DType, got {arg}"
return arg
@@ -221,7 +217,7 @@ class UOpMetaClass(type):
return created
# some uops map to other stuff
buffers:weakref.WeakKeyDictionary[UOp, Buffer|MultiBuffer] = weakref.WeakKeyDictionary() # this maps BUFFER/SLICE uops to their device Buffers
buffers:weakref.WeakKeyDictionary[UOp, Buffer|MultiBuffer] = weakref.WeakKeyDictionary() # this maps BUFFER/view uops to their device Buffers
all_metadata:weakref.WeakKeyDictionary[UOp, tuple[Metadata, ...]] = weakref.WeakKeyDictionary() # TODO: should this be here?
# recursive_property replaces functools.cached_property in recursive UOp functions to prevent RecursionError
@@ -386,10 +382,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
case Ops.BUFFER:
if len(self.src): return self.src[0].as_shape
return ()
case Ops.SLICE:
# HACK: SLICE is used inside kernels, so we set the shape to () if it's on an INDEX
if self.src[0].op is Ops.INDEX: return ()
return (self.arg,)
case Ops.CUSTOM | Ops.CUSTOMI:
if self.dtype is dtypes.void: return None
input_shapes = [x._shape for x in self.src if x._shape is not None]
@@ -822,7 +814,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
unique_num = itertools.count(0)
def getaddr(self, device=None) -> UOp:
if self.without_after.op not in {Ops.BUFFER, Ops.SLICE, Ops.SHRINK, Ops.BINARY, Ops.MSTACK, Ops.MSELECT, Ops.PARAM}: return self
if self.without_after.op not in {Ops.BUFFER, Ops.SHRINK, Ops.BITCAST, Ops.BINARY, Ops.MSTACK, Ops.MSELECT, Ops.PARAM}: return self
return UOp(Ops.GETADDR, src=(self,), arg=device or to_tuple(self.device)[0])
@staticmethod
def new_buffer(device:str|tuple[str, ...], size:int, dtype:DType, num=None):
@@ -924,7 +916,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# TODO: this is confusing because UOp.variable('v', 0, 1, dtypes.weakfloat) is True for jit to work, but it doesn't have a buffer
if self.op in {Ops.RESHAPE, Ops.UNSHARD, Ops.MSELECT}: return self.src[0].has_buffer_identity(after_ok)
if after_ok and self.op == Ops.AFTER: return self.src[0].has_buffer_identity(after_ok)
return self.op in {Ops.BUFFER, Ops.SLICE, Ops.PARAM}
return self.op in {Ops.BUFFER, Ops.PARAM}
def _base_buffer_is_realized(self) -> bool:
"""Walk through AFTER chain to find if the underlying buffer is realized (has allocated memory)."""
@@ -937,25 +929,16 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op in {Ops.CONTIGUOUS, Ops.CONTIGUOUS_BACKWARD, Ops.RESHAPE, Ops.UNSHARD, Ops.DETACH, Ops.AFTER}: return self.src[0].buffer
# this buffer can process disk tensors and simple movement ops
if self is not self.base or self.op is Ops.BITCAST:
if (cret:=buffers.get(self)) is not None: return cret
if (cv := self.contiguous_view()) is None: raise RuntimeError(f"non-contiguous view is not supported for {self.device} buffer")
buf, offset = (b:=cv[0]).base.buffer, cv[1]
if isinstance(buf, MultiBuffer):
mbuf = MultiBuffer.__new__(MultiBuffer)
mbuf.bufs = [x.view(prod(self.max_shape), self.dtype, offset*b.dtype.itemsize) for x in buf.bufs]
return mbuf
return buf.view(prod(self.max_shape), self.dtype, offset*b.dtype.itemsize)
if self.op is Ops.SLICE:
if (cret:=buffers.get(self)) is not None: return cret
buf = self.src[0].buffer
offset = self.src[1].val
if isinstance(buf, MultiBuffer):
mbuf = MultiBuffer.__new__(MultiBuffer)
mbuf.bufs = [b.view(self.arg, self.dtype, offset * self.src[0].dtype.itemsize) for b in buf.bufs]
buffers[self] = mbuf
return mbuf
assert isinstance(buf, Buffer), "must be a Buffer for SLICE"
buffers[self] = bv = buf.view(self.arg, self.dtype, offset * self.src[0].dtype.itemsize)
return bv
buffers[self] = buf.view(prod(self.max_shape), self.dtype, offset*b.dtype.itemsize)
return buffers[self]
if self.op is Ops.MSELECT:
ret = self.src[0].buffer
assert isinstance(ret, MultiBuffer)
@@ -1181,7 +1164,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def custom_function(name:str, *src:UOp) -> UOp: return UOp(Ops.CUSTOM_FUNCTION, src=src, arg=name)
# opaque bodies stay as Ops.CALL; value-producing bodies become Ops.FUNCTION (wrapped in TUPLE)
_OPAQUE_CALL_BODIES = {Ops.SINK, Ops.PROGRAM, Ops.LINEAR, Ops.COPY, Ops.SLICE, Ops.CUSTOM_FUNCTION}
_OPAQUE_CALL_BODIES = {Ops.SINK, Ops.PROGRAM, Ops.LINEAR, Ops.COPY, Ops.CUSTOM_FUNCTION}
def call(self, *srcs:UOp, ret_dtype:DType|None=None, grad_fxn:Callable|None=None,
name:str|None=None, precompile:bool=False, precompile_backward:bool=False, aux:Any=None) -> UOp:
if ret_dtype is not None: return UOp(Ops.CALL, ret_dtype, src=(self,)+srcs)
-7
View File
@@ -233,13 +233,6 @@ spec_hcq = PatternMatcher([
spec_full = PatternMatcher([
(UPat(Ops.REWRITE_ERROR, dtypes.void, name="x"), lambda x: isinstance(x.arg, str)),
# SLICE on BUFFER is allowed if BUFFER is
(UPat(Ops.SLICE, src=(UPat(GroupOp.Movement.union({Ops.BUFFER, Ops.PARAM, Ops.STAGE, Ops.AFTER})),
UPat(Ops.CONST, dtype=dtypes.weakint)), allow_any_len=True, name="bv"),
lambda bv: isinstance(bv.arg, int)),
(UPat(Ops.CALL, dtypes.void, src=(UPat((Ops.SLICE,)),), allow_any_len=True), lambda: True),
# codegen may end ranges after gpudims has replaced RANGE with SPECIAL.
(UPat(Ops.END, src=(UPat(), UPat()), allow_any_len=True), lambda: True),
+2
View File
@@ -249,6 +249,8 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
lambda x: x.const_like(x.vmin) if x.vmin == x.vmax else None),
(UPat(Ops.RANGE, src=(UPat(Ops.CONST,)), name="x"), lambda x: x.const_like(x.vmin) if x.vmin == x.vmax else None),
# max folding
((UPat.cvar("a") < UPat.var("b")).where(UPat.var("b"), UPat.cvar("c")), lambda a,b,c: UOp.maximum(a,b) if a.val == c.val else None),
((UPat.var("a") < UPat.cvar("b")).where(UPat.cvar("c"), UPat.var("a")), lambda a,b,c: UOp.maximum(a,b) if b.val == c.val else None),
(UPat.maximum(UPat.var("x"), UPat.var("y")), lambda x,y: x if x.vmin >= y.vmax else y if x.vmax <= y.vmin else None),
# TODO: why does this rule break beautiful_mnist?
#((UPat.var("x")+UPat.var("z")).maximum(UPat.var("y")+UPat.var("z")), lambda x,y,z: x.maximum(y) + z),
+1 -1
View File
@@ -50,7 +50,7 @@ uops_colors = {Ops.LOAD: "#ffc0c0", Ops.STORE: "#87CEEB", Ops.CONST: "#e0e0e0",
Ops.INDEX: "#CEF9B7", Ops.STACK: "#D8F9E4",
Ops.WMMA: "#efefc0", Ops.UNSHARD: "#f6ccff", Ops.INS: "#eec4ff",
**{x:"#D8F9E4" for x in GroupOp.Movement}, **{x:"#ffffc0" for x in GroupOp.ALU}, Ops.THREEFRY:"#ffff80",
Ops.SLICE: "#E5EAFF", Ops.BUFFER: "#B0BDFF", Ops.GETADDR: "#9DB1F0", Ops.COPY: "#a040a0", Ops.CUSTOM_FUNCTION: "#bf71b6",
Ops.BUFFER: "#B0BDFF", Ops.GETADDR: "#9DB1F0", Ops.COPY: "#a040a0", Ops.CUSTOM_FUNCTION: "#bf71b6",
Ops.CALL: "#00B7C8", Ops.FUNCTION: "#C07788", Ops.PARAM: "#14686F", Ops.SOURCE: "#c0c0c0", Ops.BINARY: "#404040",
Ops.LINEAR: "#7DF4FF",
Ops.ALLREDUCE: "#ff40a0", Ops.MSELECT: "#d040a0", Ops.MSTACK: "#d040a0", Ops.CONTIGUOUS: "#FFC14D",