Compare commits

..
Author SHA1 Message Date
geohot 9540700bbc direct lds 2026-07-31 09:03:32 -07:00
geohot 1cfaa385d6 work 2026-07-30 21:46:06 -07:00
geohot 36bf7cf65e bench 2026-07-30 20:55:22 -07:00
geohot d9ec3d7282 bugfixes 2026-07-30 20:39:25 -07:00
geohot 679faeacc7 hipkittens style gemm (kimi) 2026-07-30 18:06:34 -07:00
sirhcmandGitHub b290372121 ftdi reset chestnut before running comma benchmark (#17306) 2026-07-30 14:28:45 -04:00
sirhcmandGitHub da15c43e51 update openpilot benchmarks (#17304) 2026-07-30 14:22:20 -04:00
chenyuandGitHub b488cc7df2 update dtype_from_uop for SHR/SHL (#17307)
certain backend cast the distance, and we should not re-broadcast it
2026-07-30 13:57:58 -04:00
chenyuandGitHub 341c4ed4f5 rewrite writes new dtype with dtype_from_uop try 2 [pr] (#17305)
with NIR fix which is INDEX related

some Ops currently depends on explicitly set dtype and not dtype_from_uop
2026-07-30 13:47:21 -04:00
chenyuandGitHub d05a3e6c0b Revert "rewrite writes new dtype with dtype_from_uop [pr] (#17302)" (#17303)
This reverts commit 417245ab26.
2026-07-30 13:08:26 -04:00
chenyuandGitHub 417245ab26 rewrite writes new dtype with dtype_from_uop [pr] (#17302)
some Ops currently depends on explicitly set dtype and not dtype_from_uop
2026-07-30 12:46:24 -04:00
b1tgandGitHub e25bf77ce9 kimi delta attention (#17281)
* kimi delta attention

* config

* test
2026-07-30 08:23:56 -07:00
wozeparrotandGitHub b5a2a5666a gptoss moe routing (#17284) 2026-07-30 07:49:14 -07:00
nimlgenandGitHub 3f6f0a19ab hcq2: less graph_rewrites (#17300) 2026-07-30 16:44:45 +03:00
qazalandGitHub 7b7b9c1ae0 spec tests for movement ops before custom_kernel (#17299)
* spec

* fixing

* xfail

* remove

* update tests

* add back

* change to max_kernels

* split and cleanup
2026-07-30 19:20:24 +09:00
qazalandGitHub aba5ba4b40 update rangeify comment for mop after AFTER (#17298) 2026-07-30 17:09:32 +09:00
chenyuandGitHub d52ef3077c fix do_devectorize dtype (#17295)
each one derives its dtype from src, not shared
2026-07-29 23:04:25 -04:00
sirhcmandGitHub bfc9fc6e0a nicer TinyJit decorator (#17293) 2026-07-29 21:48:00 -04:00
sirhcmandGitHub 060f447db6 qcom: match cl for SP_CS_INSTR_SIZE (#17289) 2026-07-29 18:47:02 -04:00
chenyuandGitHub fd912b348c generic c0*x<c1 [pr] (#17291) 2026-07-29 18:45:23 -04:00
George HotzandGitHub 138676ab81 improve fragment example + index unshard (kimi) (#17288)
* fix dtypes in fragment example

* match tilelang

* flip locals

* fix index on unshard

* test fixes

* kimi needs more taste
2026-07-29 15:38:38 -07:00
chenyuandGitHub 027907a544 fix c0*x<c1 symbolic [pr] (#17290)
use `//` and not math.ceil
2026-07-29 18:32:31 -04:00
nimlgenandGitHub d4ba8b6e0f hcq2: use stack (#17286) 2026-07-29 22:09:34 +03:00
George HotzandGitHub b30c7e00d4 support 2d on UNSHARD (kimi) (#17285)
* support 2d on UNSHARD

* fixes

* Fix test and spec

* single barrier

* 2d sharding works for devices too

* cleanups

* no _rewrap
2026-07-29 12:01:59 -07:00
sirhcmandGitHub aab51fb7b6 64-bit UOp.variable support, try 2 (#17256) 2026-07-29 14:39:36 -04:00
chenyuandGitHub dd86a30798 test case for weakfloat cast to weakint INDEX (#17287) 2026-07-29 14:15:52 -04:00
George HotzandGitHub 52c9e5a99e rename LOOP -> WEAK and STRONGLOOP -> LOOP (#17283) 2026-07-29 10:38:36 -07:00
chenyuandGitHub 6c2b9fac08 weak const cleanups [PR] (#17282) 2026-07-29 13:14:20 -04:00
George HotzandGitHub bd296a7359 enable alloc_fragment support with UNSHARD (kimi) (#17272)
* enable alloc_fragment support with UNSHARD (kimi)

* cleaner with implicit barrier

* cleanups

* cleaner

* strongloop

* dcount cleanups
2026-07-29 09:46:45 -07:00
Noah SchiroandGitHub 3df1b07c86 Patch half precision ops for older gpu archs (#17274) 2026-07-29 09:42:24 -07:00
chenyuandGitHub 3803f1583b fix add_raw_barrier [pr] (#17278)
* fix add_raw_barrier [pr]

a kernel might need multiple barriers

* smaller?
2026-07-29 09:30:00 -07:00
qazalandGitHub 6ea7d366fa test permuted input in custom Ops.PROGRAM test (#17279)
* test permuted input in custom Ops.PROGRAM test

* dtype
2026-07-29 18:29:05 +09:00
George HotzandGitHub 527e57300c fix smu reset for kernel >= 7 (#17277)
* fix smu reset for kernel >7

* match kernel
2026-07-28 23:11:21 -07:00
66 changed files with 1586 additions and 426 deletions
+31 -29
View File
@@ -402,6 +402,35 @@ jobs:
run: PYTHONPATH=. DEV=PCI+NV:NAK python3.11 test/test_tiny.py
testcommalatest:
name: comma Benchmark (0.11.2)
runs-on: [self-hosted, Linux, comma]
timeout-minutes: 12
defaults:
run:
shell: bash -e -o pipefail {0}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: setup staging db
if: github.ref == 'refs/heads/update_benchmark_staging'
run: |
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
- name: reset process replay
run: test/external/process_replay/reset.py
- name: openpilot compile3 0.11.2 supercombo
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
- name: openpilot compile3 0.11.2 supercombo (from pickle)
run: BENCHMARK_LOG=openpilot_0_11_2_supercombo_run_pickle RUN_PICKLE=1 PYTHONPATH="." ASSERT_MIN_STEP_TIME=26 DEV=QCOM taskset -c 4-7 python3 examples/openpilot/compile3.py
- name: IR3 openpilot compile3 0.11.2 supercombo
run: BENCHMARK_LOG=ir3_openpilot_0_11_2_supercombo PYTHONPATH="." ASSERT_MIN_STEP_TIME=41 DEV=QCOM:IR3 FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/433f85f956837606ad1f1cbee4aa7e2158ad23c768dea914b20436c97232741b
- name: openpilot compile3 0.11.2 dmonitoring
run: BENCHMARK_LOG=openpilot_0_11_2_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316
- name: Run process replay tests
uses: ./.github/actions/process-replay
testcommaold:
name: comma Benchmark (0.11.0)
runs-on: [self-hosted, Linux, comma]
timeout-minutes: 12
@@ -432,35 +461,6 @@ jobs:
- name: Run process replay tests
uses: ./.github/actions/process-replay
testcommaold:
name: comma Benchmark (0.10.1)
runs-on: [self-hosted, Linux, comma]
timeout-minutes: 12
defaults:
run:
shell: bash -e -o pipefail {0}
if: github.repository_owner == 'tinygrad'
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: setup staging db
if: github.ref == 'refs/heads/update_benchmark_staging'
run: |
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
- name: reset process replay
run: test/external/process_replay/reset.py
- name: DEBUG=2 openpilot compile3 0.10.1 driving_vision
run: PYTHONPATH="." DEBUG=2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot compile3 0.10.1 driving_vision
run: BENCHMARK_LOG=openpilot_0_10_1_vision PYTHONPATH="." ASSERT_MIN_STEP_TIME=17 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot compile3 0.10.1 driving_policy
run: BENCHMARK_LOG=openpilot_0_10_1_policy PYTHONPATH="." ASSERT_MIN_STEP_TIME=3.2 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_policy.onnx
- name: openpilot compile3 0.10.1 dmonitoring
run: BENCHMARK_LOG=openpilot_0_10_1_dmonitoring PYTHONPATH="." ASSERT_MIN_STEP_TIME=11 DEV=QCOM FLOAT16=1 IMAGE=1 NOLOCALS=1 taskset -c 4-7 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/dmonitoring_model.onnx
- name: Run process replay tests
uses: ./.github/actions/process-replay
testqualcommdsp:
name: DSP Benchmark
runs-on: [self-hosted, Linux, comma4]
@@ -515,6 +515,8 @@ jobs:
run: |
echo "CACHEDB=/tmp/staging.db" >> $GITHUB_ENV
rm -f /tmp/staging.db /tmp/staging.db-shm /tmp/staging.db-wal
- name: reset chestnut
run: python3 extra/usbgpu/debug.py -rn
- name: openpilot compile3 0.10.1 driving_vision
run: BENCHMARK_LOG=usbgpu_openpilot_0_10_1_vision PYTHONPATH="." GMMU=0 DEV=USB+AMD:LLVM ASSERT_MIN_STEP_TIME=50 python3 examples/openpilot/compile3.py https://github.com/commaai/openpilot/raw/720392c9a5b986981fdbed1bb8c47a6c5573a50e/selfdrive/modeld/models/driving_vision.onnx
- name: openpilot load_pickle 0.10.1 driving_vision
+1 -2
View File
@@ -50,8 +50,7 @@ def _dequant_fwd_fxn(wq_p, ws_p, device):
return _dequant_fwd(Tensor(wq_p, device=device), Tensor(ws_p, device=device))
def _dequant_bwd(grad:UOp, call:UOp) -> tuple:
w_scale = Tensor(call.src[2])
return ((Tensor(grad).cast(dtypes.bfloat16) * _mx_scale(w_scale).cast(dtypes.bfloat16)).uop, None)
return (Tensor(grad).cast(dtypes.bfloat16).uop, None)
def dequant_weight(w_q:Tensor, w_scale:Tensor) -> Tensor:
fxn = _dequant_fwd_fxn(w_q.as_param(0).uop, w_scale.as_param(1).uop, w_q.device)
+2 -2
View File
@@ -28,8 +28,8 @@ def compile(onnx_file):
inputs = {k:Tensor(v.numpy(), device=Device.DEFAULT).realize() if 'img' in k else v for k,v in inputs.items()}
print("created tensors")
run_onnx_jit = TinyJit(lambda **kwargs:
next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32'), prune=True)
@TinyJit(prune=True)
def run_onnx_jit(**kwargs): return next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32')
for i in range(3):
GlobalCounters.reset()
print(f"run {i}")
+3 -3
View File
@@ -18,9 +18,9 @@ def custom_matmul(output: UOp, inp: UOp, weight: UOp) -> UOp:
SEQ = inp.shape[1]
OUT = weight.shape[0]
IN = weight.shape[-1]
seq_idx = UOp.range(SEQ, 2, AxisType.LOOP)
out_idx = UOp.range(OUT, 3, AxisType.LOOP)
batch_idx = UOp.range(output.size//SEQ//OUT, 1, AxisType.LOOP)
seq_idx = UOp.range(SEQ, 2)
out_idx = UOp.range(OUT, 3)
batch_idx = UOp.range(output.size//SEQ//OUT, 1)
reduce_idx = UOp.range(IN, 0, AxisType.REDUCE)
product = (inp.index((seq_idx*IN+reduce_idx+batch_idx*IN*SEQ)) * weight.index((out_idx*IN+reduce_idx))).cast(dtypes.float)
reduced = product.reduce(reduce_idx, arg=Ops.ADD)
+2 -2
View File
@@ -70,8 +70,8 @@ def block_128x128_gemm(c:UOp, a:UOp, b:UOp) -> UOp:
if use_wmma:
k = UOp.range(BLOCK_K // WMMA_K, 101, AxisType.REDUCE)
tile_m = UOp.range(TM // WMMA_ACC, 200, AxisType.LOOP)
tile_n = UOp.range(TN, 201, AxisType.LOOP)
tile_m = UOp.range(TM // WMMA_ACC, 200)
tile_n = UOp.range(TN, 201)
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0,2,1)[tile_m, tile_n]
a_frag = A_local.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_K // WMMA_K, WMMA_K)[wave_m, tile_m, lane_n, k]
+8 -8
View File
@@ -92,8 +92,8 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
S_reg = UOp.placeholder((TM, TN), dtypes.float, slot=6, addrspace=AddrSpace.REG)
S_reg = S_reg.after(S_reg.after(n_tile).store(S_reg.const_like(0)))
k_qk = UOp.range(D // WMMA_K, 101, AxisType.REDUCE)
tm1 = UOp.range(TM // WMMA_ACC, 200, AxisType.LOOP)
tn1 = UOp.range(TN, 201, AxisType.LOOP)
tm1 = UOp.range(TM // WMMA_ACC, 200)
tn1 = UOp.range(TN, 201)
S_frag = S_reg.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0, 2, 1)[tm1, tn1]
q_frag = Q_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, D // WMMA_K, WMMA_K)[wave_m, tm1, lane_n, k_qk]
k_frag = KV_lds_k.reshape(WAVES_N, TN, WMMA_N, D // WMMA_K, WMMA_K)[wave_n, tn1, lane_n, k_qk]
@@ -110,7 +110,7 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
rm2 = UOp.range(TN, 261, AxisType.REDUCE)
m_ij = m_ij.after(m_ij.store(m_ij.after(rm2).maximum(S_reg[:, rm2])).end(rm2))
# warp reduce max (in-place)
ri_w = UOp.range(TM, 270, AxisType.LOOP)
ri_w = UOp.range(TM, 270)
m_ij = m_ij.after(m_ij[ri_w].store(warp_reduce_max(m_ij[ri_w], lane)).end(ri_w))
# compute P = exp(S - m_ij) in S_reg
@@ -120,7 +120,7 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
p_local = p_local.after(p_local.after(n_tile).store(p_local.const_like(0)))
rp2 = UOp.range(TN, 291, AxisType.REDUCE)
p_local = p_local.after(p_local.store(p_local.after(rp2) + S_reg[:, rp2]).end(rp2))
ri_ws = UOp.range(TM, 295, AxisType.LOOP)
ri_ws = UOp.range(TM, 295)
p_sum = p_local.after(p_local[ri_ws].store(warp_reduce_sum(p_local[ri_ws], lane)).end(ri_ws))
# write P = exp(S - m_ij) to P_lds (reuses slot 0, Q no longer needed)
@@ -130,11 +130,11 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
P_store = P_write[tid].store(S_reg.cast(dtypes.half))
# -- online softmax correction --
ri4 = UOp.range(TM, 330, AxisType.LOOP)
ri4 = UOp.range(TM, 330)
m_new_val = m_i[ri4].maximum(m_ij[ri4])
alpha_val = ((m_i[ri4] - m_new_val) * LOG2E).exp2()
beta_val = ((m_ij[ri4] - m_new_val) * LOG2E).exp2()
rj4 = UOp.range(TD, 331, AxisType.LOOP)
rj4 = UOp.range(TD, 331)
correction = UOp.group(
acc[ri4, rj4].store(alpha_val * acc[ri4, rj4]).end(rj4),
l_i[ri4].store(alpha_val * l_i[ri4] + beta_val * p_sum[ri4]),
@@ -153,8 +153,8 @@ def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
# -- acc += P @ V via WMMA --
k_pv = UOp.range(BLOCK_N // WMMA_K, 400, AxisType.REDUCE)
tm2 = UOp.range(TM // WMMA_ACC, 401, AxisType.LOOP)
tn2 = UOp.range(TD, 402, AxisType.LOOP)
tm2 = UOp.range(TM // WMMA_ACC, 401)
tn2 = UOp.range(TD, 402)
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TD).permute(0, 2, 1)[tm2, tn2]
p_frag = P_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_N // WMMA_K, WMMA_K)[wave_m, tm2, lane_n, k_pv]
v_frag = KV_lds_v.reshape(WAVES_N, TD, WMMA_N, BLOCK_N // WMMA_K, WMMA_K)[wave_n, tn2, lane_n, k_pv]
+2 -2
View File
@@ -28,10 +28,10 @@ REG_TILES_PER_WAVE_M = BLOCK_M // (WAVES_PER_BLOCK_M * LANES_PER_WAVE_M * TM)
assert WAVES_PER_BLOCK_M*REG_TILES_PER_WAVE_M*LANES_PER_WAVE_M*TM == BLOCK_M, "M reshape is wrong"
assert WAVES_PER_BLOCK_N*REG_TILES_PER_WAVE_N*LANES_PER_WAVE_N*TN == BLOCK_N, "N reshape is wrong"
def rngs_for_shape(shape:tuple[sint, ...], rng:int, axis_type=AxisType.LOOP): return [UOp.range(s, rng+i, axis_type) for i,s in enumerate(shape)]
def rngs_for_shape(shape:tuple[sint, ...], rng:int, axis_type=AxisType.WEAK): return [UOp.range(s, rng+i, axis_type) for i,s in enumerate(shape)]
def copy(dest:UOp, src:UOp, rng:int, upcast=False):
assert dest.shape == src.shape
rngs = rngs_for_shape(src.shape, rng, AxisType.UPCAST if upcast else AxisType.LOOP)
rngs = rngs_for_shape(src.shape, rng, AxisType.UPCAST if upcast else AxisType.WEAK)
return dest[*rngs].store(src[*rngs]).end(*rngs)
def hand_spec_kernel3(c:UOp, a:UOp, b:UOp) -> UOp:
+2 -2
View File
@@ -171,8 +171,8 @@ def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
M, K = A.shape[0]*A.shape[1], A.shape[2]
K2, N = B.shape[(1 if B.ndim == 3 else 0):]
assert K == K2
m = UOp.range(M, 1, AxisType.LOOP)
n = UOp.range(N, 2, AxisType.LOOP)
m = UOp.range(M, 1)
n = UOp.range(N, 2)
k = UOp.range(K, 0, AxisType.REDUCE)
mul = (A.flatten().index((m*UOp.const(dtypes.weakint, K)+k))*
B.flatten().index((k*UOp.const(dtypes.weakint, N)+n))).cast(dtypes.float32)
+395
View File
@@ -0,0 +1,395 @@
"""
HipKittens hk_bf16_gemm (extra/thunder/amd/gemm_bf16.cpp) reimplemented with tinygrad UOps.
C[M, N] (bf16) = A[M, K] @ B[N, K]^T, fp32 accumulation, exactly the kittens kernel shape:
- 256x256 output tile per workgroup, K_STEP=64
- 8 warps in a 2x4 grid, each warp owns a 128x64 warp-tile
- v_mfma_f32_16x16x32_bf16 on CDNA4 (gfx950) / v_wmma_f32_16x16x16_bf16 (wave32, gfx12) on RDNA4,
fp32 accumulators
- shared tiles As/Bs with the kittens st_16x32_s swizzle (16x32 subtiles of 1024B)
- K stages (STAGES=1: synchronous single buffer)
Validated on gfx1201 hardware (exact for identity-B, rounding-level noise otherwise), rendered
and compiled to gfx950 with comgr for assembly comparison against gemm_bf16.cpp.
What is NOT expressible vs the kittens C++:
- explicit s_waitcnt vmcnt()/lgkmcnt() pipelining and s_setprio: tinygrad models async copy
overlap with slot dependencies and emits full workgroup barriers; instruction scheduling
is left to clang/LLVM
- direct-to-LDS global loads (buffer_load_lds): tinygrad goes global->reg->LDS
Pipelining status: STAGES=2 gives the kittens-shaped double-buffered pipeline (2 x 64KB LDS
like gemm_bf16.cpp, copies overlap the previous pair's mma's), written with FA/gemm_fragment
conventions: LDS buffers are (2, tile) placeholders indexed by symbolic parity (ko % 2),
which sidesteps static slot choice, fill iterations, predication and duplicate static stores.
Validated on the CDNA4 emulator for all tile counts (amt = K//64 in {1..32}, odd/even),
single- AND multi-workgroup (bit-close to stages=1 / to hippkittens at rounding level).
Bug hunt notes (all fixed on this branch; they were entangled for a long time):
1. The double-buffered pipeline REGISTER-SPILLS (255+ VGPRs vs 166 for stages=1), and the
mock emulator aliased the spill (scratch) segment of ALL waves of a workgroup onto one
64-lane region. On real HW each wavefront owns a per-lane segment of the scratch ring
(indexed by (wave_id, lane)); waves trampled each other's spilled accumulators, giving
the "only the last wave's output survives" signature. emu.py now allocates per-wave
scratch buffers.
2. The remaining "shape-dependent" corruption (NaNs, mispositioned values in contiguous
copies feeding the GEMM) came from tinygrad's devectorizer fusing adjacent bf16 stores
into 32-bit stores with UNALIGNED (2-byte) granularity: legal on AMD FLAT/GLOBAL (the
hardware splits them), but the emulator floored misaligned addresses to the word below.
_mem_store now handles unaligned 32-bit (and wider) accesses byte-exactly.
3. memory_coalescing (late/coalesce.py) assumed a single static store per (buffer, index)
("attempting multiple stores"); aliased stores (a double-buffered LDS slot written in a
prologue AND a loop body) are now simply kept scalar instead of asserting/merging.
4. pm_split_ranges may only split ranges WITHOUT hardware meaning (WEAK/REDUCE/LOOP);
splitting LOCAL/WARP/THREAD/GLOBAL/GROUP_REDUCE/UPCAST ranges scrambles the
logical<->hardware mapping of hand-written kernels such as this one.
RDNA4 (gfx12) uses 8-element accumulator fragments, so the 8x4 tile grid needs
256 fp32 acc registers per thread -> guaranteed spills (0.85 TF vs 96 TF default on
gfx1201). The kernel is right-sized for CDNA4 (fragsz 4 -> 128 acc regs).
Lane layouts (RDNA4 verified with probing on gfx1201 hardware; CDNA from the mfma docs):
CDNA (64 thr/warp, 16x16x32): A/B frag: tile-row = l%16, k = (l//16)*8+i (i in 0..7)
RDNA4 (32 thr/warp, 16x16x16): A/B frag: tile-row = l%16, k = (l//16)*4+(i%4)+8*(i//4) (i in 0..7)
both: acc frag: CDNA m=(l//16)*4+i (i<4) / RDNA4 m=(l//16)*8+i (i<8), n=l%16
The RDNA4 fragment k-set {k0..3, k0+8..11} is not contiguous, so on RDNA4 the LDS column layout
is block-permuted (4-element blocks within each 16-col group are stored as [0,2,1,3]) making
every fragment 8 contiguous halves (one 16B chunk) on both archs; the copy path applies the
same permutation.
NOTE: thread ids come from UOp.special (like mi350x_uop_matmul.py), not an AxisType.LOCAL
RANGE. (pm_split_ranges now only splits WEAK/REDUCE/LOOP ranges, so LOCAL ranges would
survive too, but UOp.special is the sanctioned way to tag hardware lane ids.)
NOTE 2: WMMA operand/accumulator fragments must carry the fragment length in their UOp shape.
NOTE 3: swizzled addresses are written in provably-contiguous "base + vector-offset" form,
otherwise the devectorizer emits scalar ds_read_u16/ds_write_b16.
"""
from tinygrad import Tensor, Device, dtypes
from tinygrad.uop.ops import UOp, Ops, AxisType, KernelInfo
from tinygrad.dtype import AddrSpace
from tinygrad.renderer import Estimates
from tinygrad.helpers import getenv, cdiv
# ---- tile shape (identical to gemm_bf16.cpp; HK_TILE=128 overrides for small-LDS devices) ----
BLOCK_M = BLOCK_N = getenv("HK_TILE", 256)
K_STEP = 64
WARPS_M, WARPS_N = 2, 4
NUM_WARPS = WARPS_M * WARPS_N # 8
WARP_TILE_M, WARP_TILE_N = BLOCK_M // WARPS_M, BLOCK_N // WARPS_N # 128 x 64 (64 x 32 at HK_TILE=128)
def arch_params(arch:str):
is_cdna = arch.startswith("gfx9")
if is_cdna: # CDNA mfma 16x16x32 bf16: in-frag 8, acc 4 (16,16,16 on the acc side)
return dict(warp_threads=64, dims=(16,16,32), frag_in=8, frag_out=4,
acc_m=lambda l, i: (l//16)*4 + i, kperm=None, copy_vec=8)
# RDNA4 wmma 16x16x16 (wave32, gfx12 layout): in-frag 8 (permuted in LDS), acc 8
return dict(warp_threads=32, dims=(16,16,16), frag_in=8, frag_out=8,
acc_m=lambda l, i: (l//16)*8 + i, kperm=(0,2,1,3), copy_vec=4)
# ---- kittens st_16x32_s swizzle (byte offset: off ^ (((off % 1024) >> 9) << 5)) ----
# halves-index form: within a 1024B (16x32) subtile, halves-index bit4 ^= row bit3,
# written in "base + vector-offset" form so the devectorizer can prove contiguity.
def st_half_base(r, c, tile_cols:int):
"""swizzled halves-index pre-vector-offset; c is the logical (permuted) column, 4-aligned."""
subtile_id = (r//16) * (tile_cols//32) + (c//32)
r16 = r % 16
flip = (r16 >> 3) & 1
return subtile_id*512 + r16*32 + (((c % 32) >> 2) ^ (flip << 2)) * 4
def hk_bf16_gemm_kernel(C:UOp, A:UOp, B:UOp, *, arch:str, stages:int=1) -> UOp:
"""C = A @ B^T ; A is (M,K), B is (N,K), C is (M,N). HipKittens tile shape."""
M, K = A.shape
N, K2 = B.shape
assert K == K2 and A.dtype == B.dtype == dtypes.bfloat16 and C.dtype == dtypes.bfloat16
assert not (M % BLOCK_M or N % BLOCK_N or K % K_STEP), f"dims must be multiples of {(BLOCK_M, BLOCK_N, K_STEP)}"
ap = arch_params(arch)
warp_threads, dims = ap["warp_threads"], ap["dims"]
FRAG_IN, FRAG_OUT, kperm, acc_m, CPV = ap["frag_in"], ap["frag_out"], ap["kperm"], ap["acc_m"], ap["copy_vec"]
NUM_THREADS = NUM_WARPS * warp_threads
TC_M, TC_N, TC_K = dims
MT, NT = WARP_TILE_M // TC_M, WARP_TILE_N // TC_N # 8, 4 tiles per warp
# permute 4-half blocks within each 16-col group (RDNA4: [0,2,1,3] = swap middle blocks)
def perm_col(c):
if kperm is None: return c
return (c & ~15) | ((((c>>2) & 1) << 1 | ((c>>3) & 1)) << 2) | (c & 3)
bx, by = UOp.special(N//BLOCK_N, "gidx0"), UOp.special(M//BLOCK_M, "gidx1")
lane = UOp.special(warp_threads, "lidx0")
warp = UOp.special(NUM_WARPS, "lidx1")
warp_row, warp_col = warp // WARPS_N, warp % WARPS_N
tid = warp*warp_threads + lane
def smem(slot) -> UOp: return UOp.placeholder((BLOCK_M*K_STEP,), dtypes.bfloat16, slot, AddrSpace.LOCAL)
As = [smem(2*i) for i in range(stages)]
Bs = [smem(2*i+1) for i in range(stages)]
# per-warp accumulator: (MT x NT) 16x16 tiles of FRAG_OUT fp32 per thread
acc = UOp.placeholder((MT, NT, FRAG_OUT), dtypes.float32, 12, AddrSpace.REG)
acc = acc.after(acc.store(acc.const_like(0.0))) # FA-style init: self-store, keeps the value flow loop-carried
# global -> LDS copy: CPV halves per op (16B on CDNA, 8B on RDNA4), thread-major coalescing
OPS_PER_TILE = BLOCK_M*K_STEP//CPV
OPR = K_STEP//CPV
def copy_tile(dst:UOp, src:UOp, base_row:UOp, base_col:UOp, slot:int) -> UOp:
ir = UOp.range(cdiv(OPS_PER_TILE, NUM_THREADS), slot, AxisType.LOOP)
j = UOp.range(CPV, slot+1, AxisType.UPCAST)
chunk = ir*NUM_THREADS + tid
r, cb = chunk // OPR, chunk % OPR # row, 4/8-col block
return dst[st_half_base(r, perm_col(cb*CPV), K_STEP) + j].store(src[base_row + r, base_col + cb*CPV + j]).end(ir, j)
def load_stage(sidx:int, ko, slot:int, barrier:bool) -> tuple[UOp, UOp]:
A_r = copy_tile(As[sidx], A, by*BLOCK_M, ko*K_STEP, slot)
B_r = copy_tile(Bs[sidx], B, bx*BLOCK_N, ko*K_STEP, slot+10)
bar = UOp.barrier(A_r, B_r) if barrier else UOp.group(A_r, B_r)
return As[sidx].after(bar), Bs[sidx].after(bar)
# ---- pipelined path (stages=2) ----
NIT = cdiv(OPS_PER_TILE, NUM_THREADS) # copy ops per thread per tile
def setprio(n:int, slot:int) -> UOp:
"""__builtin_amdgcn_s_setprio(n), like gemm_bf16.cpp: raise warp priority for the mma phase
so global/LDS traffic of the other waves doesn't starve issue slots."""
# distinct src slot per call site so identical-priority instructions at different k-tiles
# don't get UOp-hash-deduped into one placement (s_setprio is position-sensitive)
return UOp(Ops.CUSTOMI, dtypes.void, src=(UOp.const(dtypes.weakint, slot), UOp.const(dtypes.weakint, n)),
arg="__builtin_amdgcn_s_setprio({1}); // {0}")
def gload_write_tile(dst:UOp, src:UOp, base_row:UOp, kt, slot:int) -> UOp:
"""store one global tile into an LDS slot (loads and stores share the vec range j)."""
j = UOp.range(CPV, slot, AxisType.UPCAST)
def one(ir:int) -> UOp:
chunk = ir*NUM_THREADS + tid
r, cb = chunk // OPR, chunk % OPR
return dst[st_half_base(r, perm_col(cb*CPV), K_STEP) + j].store(src[base_row + r, kt*K_STEP + cb*CPV + j])
return UOp.group(*[one(ir) for ir in range(NIT)]).end(j)
def compute(acc:UOp, A_l:UOp, B_l:UOp, afters:tuple[UOp, ...], pred:UOp|None=None, aoff:UOp=None, boff:UOp=None) -> UOp:
"""One K_STEP=64 iteration: (K_STEP//TC_K) k-chunks unrolled, (MT x NT) mma each, like the kittens main loop.
pred (optional): a loop-range condition; accumulator stores are predicated on it so the
first (fill) iteration of a software pipeline can run the body with garbage LDS contents
without contaminating the accumulator."""
arow = warp_row*WARP_TILE_M + lane % 16 # fragment tile row in the LDS tile (m)
brow = warp_col*WARP_TILE_N + lane % 16 # (n)
ja = UOp.range(FRAG_IN, 701, AxisType.UPCAST)
jb = UOp.range(FRAG_IN, 702, AxisType.UPCAST)
acc_k = acc.after(*afters) if afters else acc
last_store = None
# in the permuted layout every fragment is 8 contiguous halves starting at an 8-aligned col
for kk in range(K_STEP//TC_K):
cc = kk*(TC_K//FRAG_IN) + (lane // 16) # fragment chunk col (8 halves)
oa, ob = (aoff, boff) if aoff is not None else (None, None)
a_frags = [A_l[st_half_base(arow + mt*16, cc*8, K_STEP) + ja].contract(ja) if oa is None else
A_l[oa + st_half_base(arow + mt*16, cc*8, K_STEP) + ja].contract(ja) for mt in range(MT)]
b_frags = [B_l[st_half_base(brow + nt*16, cc*8, K_STEP) + jb].contract(jb) if ob is None else
B_l[ob + st_half_base(brow + nt*16, cc*8, K_STEP) + jb].contract(jb) for nt in range(NT)]
for mt in range(MT):
for nt in range(NT):
cur = acc_k[mt, nt]
out = UOp.wmma(a_frags[mt], b_frags[nt], cur, dims, 'AMD', warp_threads)
if pred is not None: out = pred.where(cur, out)
last_store = acc_k[mt, nt].store(out)
acc_k = acc_k.after(last_store)
return last_store
# ---- K loop ----
amt = cdiv(K, K_STEP)
_stages = stages
if _stages == 1:
ko = UOp.range(amt, 600, AxisType.LOOP)
A_l, B_l = load_stage(0, ko, 100, barrier=True)
last = compute(acc, A_l, B_l, afters=(ko,))
acc = acc.after(last.barrier().end(ko))
else:
# Double-buffered pipeline on FA/gemm_fragment conventions: each LDS buffer is a
# (2, tile) placeholder indexed by symbolic parity (ko % 2) -- no static slot choice,
# no duplicate static stores (memory_coalescing-safe), no fill iteration, no predication.
def smem2(slot) -> UOp: return UOp.placeholder((2*BLOCK_M*K_STEP,), dtypes.bfloat16, slot, AddrSpace.LOCAL)
A_l, B_l = smem2(0), smem2(1)
TILE_ELEMS = BLOCK_M * K_STEP
def copy_stage(dst:UOp, slot_off:UOp, src:UOp, base_row:UOp, kt, slot:int) -> UOp:
"""store one global tile into dst + slot_off (flat element offset -- slot_off = parity*TILE_ELEMS).
Each thread's 8-element chunk is a single buffer_load_lds direct-to-LDS instruction
(the kittens '... offen lds' fill path), emitted via Ops.CUSTOMI so it bypasses the
devectorizer (a SHRINK store of a SHRINK load gets expanded to scalars before render)."""
ir = UOp.range(cdiv(OPS_PER_TILE, NUM_THREADS), slot+1, AxisType.LOOP)
chunk = ir*NUM_THREADS + tid
r, cc = chunk // OPR, chunk % OPR
if getenv("HK_G2L", 0) == 3:
# direct-to-LDS fill (kittens '... offen lds' path): the hardware writes each lane's
# chunk to the lane-linear LDS address (M0 + lane*size), so the swizzle is moved to
# the GLOBAL side: lane q's 16B chunk fetches the matrix element that st_half_base
# maps to the tile-linear position q. Verified bijective; the fragment-read layout
# (and therefore the read swizzle) is unchanged.
chunk = ir*NUM_THREADS + tid
p_ = chunk * CPV # tile-linear halves position of this lane's chunk
sub = p_ >> 9 # 16x32 subtile id (512 halves)
r16 = (p_ & 511) >> 5
flip = (r16 >> 3) & 1
cb = ((p_ & 31) >> 2) ^ (flip << 2)
r_ = (sub >> 1) * 16 + r16
c_ = cb*4 + (sub & 1) * 32 # global column (8-aligned)
off_g = (base_row + r_) * K + kt*K_STEP + c_
lds_el = slot_off + ir*NUM_THREADS*CPV # elements; &buf[el*8] = chunk base byte addr
# feed the raw PARAM (unwrapping the scheduler's RESHAPE view, which would otherwise
# live unfused into the program and fail spec: 'movement ops not allowed in programs').
prm = src
while prm.op is not Ops.PARAM and len(prm.src): prm = prm.src[0]
nbytes = prm.max_numel() * prm.dtype.itemsize
gname = f"data{prm.arg.slot}_{prm.max_numel()}"
return UOp(Ops.CUSTOMI, dtypes.void, src=(prm, dst, lds_el, off_g),
arg=(f"llvm_amdgcn_raw_buffer_load_lds(make_srsrc_((void*){gname}, {nbytes}), "
f"(as3_uint32_ptr)(&({{1}}[({{2}})])), {CPV*2}, ((unsigned)({{3}}))*2U, 0, 0, 0);")).end(ir)
# default: elementwise global->LDS stores
off_l = slot_off + st_half_base(r, perm_col(cc*CPV), K_STEP)
off_g = (base_row + r) * K + kt*K_STEP + cc*CPV
j = UOp.range(CPV, slot, AxisType.UPCAST)
return dst[off_l + j].store(src[base_row + r, kt*K_STEP + cc*CPV + j]).end(ir, j)
ZERO = UOp.const(dtypes.weakint, 0)
# prologue: tile 0 into slot 0 of both buffers, barrier before first read
g0 = UOp.group(copy_stage(A_l, ZERO, A, by*BLOCK_M, ZERO, 100),
copy_stage(B_l, ZERO, B, bx*BLOCK_N, ZERO, 110))
bar0 = UOp.barrier(g0)
# Double-buffered pipeline: slot ko%2 holds k-tile ko; the prefetch copy of tile ko+1
# (into the other slot) rides IN FRONT of the wmma's and overlaps them; one barrier per
# k-tile hand-off covers write(ko)->read(ko+1) [and read(ko)->write(ko+1) is closed by
# the ko-1 barrier already]. The parities/offsets are static python constants when
# HK_UNROLL (default on): straight-line like the kittens main loop; the rolled variant
# uses pm_split_ranges to split the ko LOOP range at the (ko % 2) boundary.
if getenv("HK_UNROLL", 1) and amt % (UN := getenv("HK_UNROLL_U", 8)) == 0:
# outer rolled loop of amt//U iterations, U python-unrolled k-tiles inside: nearly the
# kittens straight-line node shape (one barrier per k-tile) at a fraction of the
# full-unroll uop count (full unroll of amt=64 needs ~12 min of schedule time; U=8
# keeps every tile's prefetch + compute + hand-off barrier but stays seconds).
ko_o = UOp.range(amt // UN, 600, AxisType.LOOP)
pa, pb = A_l.after(bar0, ko_o), B_l.after(bar0, ko_o)
for i in range(UN):
kt = ko_o * UN + i
pr, pn = (i % 2) * TILE_ELEMS, ((i + 1) % 2) * TILE_ELEMS
kt_next = UOp.minimum(kt + 1, amt - 1)
ga0 = UOp.group(copy_stage(pa, UOp.const(dtypes.weakint, pn), A, by*BLOCK_M, kt_next, 300 + 4*i),
copy_stage(pb, UOp.const(dtypes.weakint, pn), B, bx*BLOCK_N, kt_next, 302 + 4*i))
sp_hi = setprio(1, 300 + 4*i) # kittens: raised prio for the mma phase
last = compute(acc, pa, pb, afters=(ko_o, sp_hi), aoff=UOp.const(dtypes.weakint, pr), boff=UOp.const(dtypes.weakint, pr))
sp_lo = setprio(0, 301 + 4*i)
handoff = UOp.group(last, sp_lo, ga0).barrier()
acc = acc.after(handoff)
pa, pb = A_l.after(handoff, ga0), B_l.after(handoff, ga0)
acc = acc.after(UOp.group(handoff).end(ko_o))
else:
ko = UOp.range(amt, 600, AxisType.LOOP)
pr, pn = ko % 2, (ko+1) % 2 # slot of the tile being computed / being prefetched
kt_next = UOp.minimum(ko+1, amt-1) # clamped tail prefetch (its data is unused)
pa, pb = A_l.after(bar0, ko), B_l.after(bar0, ko)
ga = UOp.group(copy_stage(pa, pn*TILE_ELEMS, A, by*BLOCK_M, kt_next, 130),
copy_stage(pb, pn*TILE_ELEMS, B, bx*BLOCK_N, kt_next, 140))
sp_hi = setprio(1, 150)
last = compute(acc, pa, pb, afters=(ko, sp_hi), aoff=pr*TILE_ELEMS, boff=pr*TILE_ELEMS)
acc = acc.after(UOp.group(last, setprio(0, 151), ga).barrier().end(ko))
# ---- epilogue: per-thread fragment stores, cast to bf16 (scalar per fragment element) ----
mt, nt = UOp.range(MT, 801, AxisType.LOOP), UOp.range(NT, 802, AxisType.LOOP)
def store_i(i:int) -> UOp:
crow = by*BLOCK_M + warp_row*WARP_TILE_M + mt*16 + acc_m(lane, i)
ccol = bx*BLOCK_N + warp_col*WARP_TILE_N + nt*16 + lane % 16
return C[crow, ccol].store(acc[mt, nt, i].cast(dtypes.bfloat16))
out_st = UOp.group(*[store_i(i) for i in range(FRAG_OUT)])
return out_st.end(mt, nt).sink(arg=KernelInfo(name="hk_bf16_gemm",
estimates=Estimates(ops=2*M*N*K, mem=(M*K+N*K+M*N)*2)))
def hk_bf16_gemm_tiny(a:Tensor, b:Tensor, stages:int=1) -> Tensor:
"""C = a @ b.T for bf16 a (M,K), b (N,K) with the HipKittens-shaped tinygrad kernel."""
arch = Device[a.device].renderer.target.arch
c = Tensor.empty(a.shape[0], b.shape[0], dtype=dtypes.bfloat16, device=a.device)
return c.custom_kernel(a, b, fxn=lambda C, A, B: hk_bf16_gemm_kernel(C, A, B, arch=arch, stages=stages))[0]
if __name__ == "__main__":
import numpy as np
from tinygrad import Device
M = N = K = 512
# exact test: B = identity -> C must equal A bit-exactly
a = Tensor.randn(M, K, dtype=dtypes.bfloat16).contiguous()
bid = Tensor(np.eye(K, N, dtype=np.float32), dtype=dtypes.bfloat16).contiguous()
cid = hk_bf16_gemm_tiny(a, bid, stages=getenv("STAGES", 1)).realize()
assert np.array_equal(cid.float().numpy(), a.float().numpy()), "identity test failed"
# real test: bf16 gemm vs fp32 reference, rounding-level noise
b = Tensor.randn(N, K, dtype=dtypes.bfloat16).contiguous()
c = hk_bf16_gemm_tiny(a, b, stages=getenv("STAGES", 1)).realize()
ref = (a @ b.T).float().realize()
err = (c.float() - ref).abs().max().item()
print(f"identity exact, random max err: {err:.5f}")
# ---- benchmark mode: kittens hk_bf16_gemm vs tinygrad stages={1,2} vs the default scheduled gemm ----
# run on real hardware with: DEV=AMD:HIP:gfx950 DEBUG=2 HK_BENCH=1 python extra/gemm/hk_gemm_frag.py
# sizes via HK_SIZES="2048x2048x2048,4096x4096x4096" (default 2048 cubed), iteration count via ITERS=20.
# timings come from GlobalCounters.time_sum_s (sum of kernel times; same source as the DEBUG=2 'tm' column).
if getenv("HK_BENCH"):
from tinygrad.helpers import GlobalCounters
from extra.gemm.cdna_asm_gemm import asm_gemm
from tinygrad import Device
dev, iters, warm = Device.DEFAULT, getenv("ITERS", 20), 3
arch = Device[dev].renderer.target.arch
assert arch.startswith("gfx9"), "CDNA only"
def bench(label:str, fn, M:int, N:int, K:int) -> float:
try:
for _ in range(warm): fn()
Device[dev].synchronize()
GlobalCounters.reset()
import time
t0 = time.perf_counter()
for _ in range(iters): fn()
Device[dev].synchronize()
wall = time.perf_counter() - t0
except Exception as e:
print(f" {label:32s} unsupported/failed: {type(e).__name__}: {e}")
return float('nan')
# prefer kernel-side time (GlobalCounters matches the DEBUG=2 'tm' column); fall back to wall clock
ms = (GlobalCounters.time_sum_s if GlobalCounters.time_sum_s > 0 else wall) * 1e3 / iters
tf = 2*M*N*K / (ms * 1e-3) / 1e12
print(f" {label:32s} {ms:9.3f} ms {tf:8.1f} TFLOPS")
return tf
for (M, N, K) in [tuple(map(int, s.split("x"))) for s in getenv("HK_SIZES", "2048x2048x2048").split(",")]:
print(f" size ({M},{N},{K}), grid {M//BLOCK_M}x{N//BLOCK_N} WGs, amt={K//K_STEP} k-tiles/WG")
np.random.seed(0)
An, Bn = np.random.randn(M, K), np.random.randn(K, N)
A = Tensor(An, dtype=dtypes.bfloat16).contiguous().realize() # (M,K)
Bk = Tensor(Bn, dtype=dtypes.bfloat16).contiguous().realize() # (K,N) for kittens
Bt = Bk.T.contiguous().realize() # (N,K) for ours
tf_kc = bench("kittens hk_bf16_gemm (asm_gemm)", lambda: asm_gemm(A, Bk).realize(), M, N, K)
tf_s1 = bench("tiny stages=1", lambda: hk_bf16_gemm_tiny(A, Bt, stages=1).realize(), M, N, K)
tf_s2 = bench("tiny stages=2", lambda: hk_bf16_gemm_tiny(A, Bt, stages=2).realize(), M, N, K)
tf_df = bench("tinygrad default (a @ Bt.T)", lambda: (A @ Bt.T).realize(), M, N, K)
err = (hk_bf16_gemm_tiny(A, Bt, stages=2).float() - asm_gemm(A, Bk).float()).abs().max().item()
print(f" correctness tiny-s2 vs kittens max diff: {err:.5f}")
for nm, tf in [("s1", tf_s1), ("s2", tf_s2), ("default", tf_df)]:
if tf == tf and tf_kc == tf_kc: print(f" tiny {nm:8s}/kittens: {tf/tf_kc:6.2%}")
# match the real HipKittens hk_bf16_gemm on the (mock) CDNA4 emulator at small sizes.
# run from the repo root with: DEV=MOCK+AMD:HIP:gfx950 HK_COMPARE=1 python extra/gemm/hk_gemm_frag.py
if getenv("HK_COMPARE"):
from extra.gemm.cdna_asm_gemm import asm_gemm
assert Device[Device.DEFAULT].renderer.target.arch.startswith("gfx950"), "needs CDNA4 (mock emulator or hardware)"
def compare(M:int, N:int, K:int, seed:int=0, identity:bool=False):
np.random.seed(seed)
An = np.random.randn(M, K)
Bn = np.eye(K, N) if identity else np.random.randn(K, N) # (K,N) as expected by asm_gemm
A = Tensor(An, dtype=dtypes.bfloat16).contiguous()
B = Tensor(Bn, dtype=dtypes.bfloat16).contiguous() # (K,N) for asm_gemm
c_hkc = asm_gemm(A, B).realize().float().numpy() # real HipKittens hk_bf16_gemm
c_hkt = hk_bf16_gemm_tiny(A, B.T.contiguous(), stages=getenv("STAGES", 2)).realize().float().numpy()
ref64 = A.float().numpy().astype(np.float64) @ B.float().numpy()
tag = "ident" if identity else "rand "
print(f"({M},{N},{K}) {tag}: tiny-vs-kittens {np.abs(c_hkt-c_hkc).max():9.6f} "
f"tiny-vs-fp64 {np.abs(c_hkt-ref64).max():9.6f} kittens-vs-fp64 {np.abs(c_hkc-ref64).max():9.6f}")
assert np.abs(c_hkt - ref64).max() < 0.26, "tiny kernel must match fp64 at rounding level"
assert np.abs(c_hkt - c_hkc).max() < 0.51, "tiny kernel must match hipkittens"
# NOTE: hk_bf16_gemm requires K % 128 == 0 (its prologue+epilogue unconditionally touch
# k-tiles num_tiles-1 and num_tiles-2); at other K it reads wrong-but-in-bounds global
# memory on the emulator and on real hardware, so only K%128==0 sizes are checked here.
compare(256, 256, 128, seed=1) # single workgroup
compare(256, 256, 256, seed=2)
compare(512, 512, 128, seed=3) # multi workgroup
compare(256, 256, 128, identity=True) # bit-exact check
+1 -1
View File
@@ -29,7 +29,7 @@ TID_SIZE = WARPGROUP_SIZE*WARP_SIZE
def copy(dest:UOp, src:UOp, rng:int, set=False, upcast=()):
assert dest.shape == src.shape
rngs = [UOp.range(s, rng+i, AxisType.UPCAST if i in upcast else AxisType.LOOP) for i,s in enumerate(src.shape)]
rngs = [UOp.range(s, rng+i, AxisType.UPCAST if i in upcast else AxisType.WEAK) for i,s in enumerate(src.shape)]
copy = dest[*rngs].store(src[*rngs]).end(*rngs)
return dest.after(copy) if set else copy
+130
View File
@@ -0,0 +1,130 @@
from tinygrad import Tensor, dtypes
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
BLOCK_ROW = 256
def _sharded_invalids(shape:tuple[int, ...], dtype, device) -> Tensor:
if isinstance(device, tuple):
return Tensor(Tensor.invalids(shape[0] // len(device), *shape[1:], dtype=dtype, device=device).uop.multi(0), device=device)
return Tensor.invalids(*shape, dtype=dtype, device=device)
def _atomic_add(device:str) -> str:
return "__hip_atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);" if device == "AMD" \
else "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
def _blk_for(D:int) -> int:
blk = 64
while D % blk: blk //= 2
return blk
def _kv_ranges(G, N, D, BLK):
g = UOp.range(G, 0)
m = UOp.range(N, 1)
jo = UOp.range(D // BLK, 2)
ji = UOp.range(BLK, 3, AxisType.LOCAL)
return g, m, jo * BLK + ji, jo, ji
def _ggather_fwd_kernel(out:UOp, table:UOp, idx:UOp) -> UOp:
G, M, D = out.shape
g, m, j, jo, ji = _kv_ranges(G, M, D, _blk_for(D))
row = idx.index(g, m).cast(dtypes.weakint)
val = table.index(g, row, j).load()
return out.index(g, m, j).store(val).end(g, m, jo, ji).sink(
arg=KernelInfo(name=f"ggather_fwd_{M}_{D}", opts_to_apply=()))
def _ggather_zero_kernel(out:UOp) -> UOp:
i = UOp.range(out.numel(), 0)
return out.flatten().index(i).store(UOp.const(out.dtype, 0.0)).end(i).sink(arg=KernelInfo(name="ggather_zero"))
def _sharded_zeros(shape:tuple[int, ...], dtype, device) -> Tensor:
return Tensor.custom_kernel(_sharded_invalids(shape, dtype, device), fxn=_ggather_zero_kernel)[0]
def _ggather_bwd(gradient:UOp, kernel:UOp) -> tuple:
_, table_u, idx_u = kernel.src[1:4]
dev = table_u.device
device = (dev[0] if isinstance(dev, tuple) else dev).split(":")[0]
G, R, D = table_u.shape
gt = _sharded_zeros((G, R, D), dtypes.float32, dev)
go = Tensor(gradient, device=dev)
atomic_str = _atomic_add(device)
def _bwd_kernel(gtab:UOp, gout:UOp, idx:UOp) -> UOp:
Gk, M, Dk = gout.shape
g, m, j, jo, ji = _kv_ranges(Gk, M, Dk, _blk_for(Dk))
row = idx.index(g, m).cast(dtypes.weakint)
val = gout.index(g, m, j).load().cast(dtypes.float32)
atomic = UOp(Ops.CUSTOM, dtypes.void, (gtab.index(g, row, j), val), arg=atomic_str)
return atomic.end(g, m, jo, ji).sink(arg=KernelInfo(name=f"ggather_bwd_{M}_{Dk}", opts_to_apply=()))
grad_table = Tensor.custom_kernel(gt, go, Tensor(idx_u, device=dev), fxn=_bwd_kernel)[0]
return (None, grad_table.cast(table_u.dtype).uop, None)
def grouped_gather_rows(table:Tensor, idx:Tensor, n_groups:int) -> Tensor:
G, R, D = table.shape
M = idx.shape[1]
out = _sharded_invalids((G, M, D), table.dtype, table.device)
return Tensor.custom_kernel(out, table, idx, fxn=_ggather_fwd_kernel, grad_fxn=_ggather_bwd)[0]
def _gscatter_fwd_kernel(out:UOp, src:UOp, idx:UOp) -> UOp:
G, M, D = out.shape
k = idx.shape[1] // src.shape[1]
g, m, j, jo, ji = _kv_ranges(G, idx.shape[1], D, _blk_for(D))
row = idx.index(g, m).cast(dtypes.weakint)
val = src.index(g, (m // k).cast(dtypes.weakint), j).load()
return out.index(g, row, j).store(val).end(g, m, jo, ji).sink(
arg=KernelInfo(name=f"gscatter_fwd_{idx.shape[1]}_{D}", opts_to_apply=()))
def _gscatter_bwd(gradient:UOp, kernel:UOp) -> tuple:
_, src_u, idx_u = kernel.src[1:4]
dev = src_u.device
G, T_l, D = src_u.shape
k = idx_u.shape[1] // T_l
sel = grouped_gather_rows(Tensor(gradient, device=dev), Tensor(idx_u, device=dev), G)
return (None, sel.reshape(G, T_l, k, D).sum(2).cast(src_u.dtype).uop, None)
def grouped_scatter_rows(src:Tensor, idx:Tensor, m_l:int) -> Tensor:
G, T_l, D = src.shape
zero = _sharded_zeros((G, m_l, D), src.dtype, src.device)
return Tensor.custom_kernel(zero, src, idx, fxn=_gscatter_fwd_kernel, grad_fxn=_gscatter_bwd)[0]
def m_max_for(t_local:int, experts_per_tok:int, n_experts:int) -> int:
return (-(-t_local * experts_per_tok // BLOCK_ROW) + n_experts) * BLOCK_ROW
class Routing:
def __init__(self, weights:Tensor, dest_row:Tensor, off:Tensor, m_l:int, n_groups:int, t_local:int):
self.weights, self.dest_row = weights, dest_row
self.off = off
self.m_l, self.n_groups, self.t_local = m_l, n_groups, t_local
@property
def rows_e(self) -> Tensor:
G, E = self.off.shape[0], self.off.shape[1] - 1
tr = Tensor.arange(self.m_l // BLOCK_ROW, dtype=dtypes.int32).reshape(1, -1, 1) * BLOCK_ROW
tr = tr.shard(self.off.device) if isinstance(self.off.device, tuple) else tr.to(self.off.device)
tile_e = ((tr >= self.off[:, :E].reshape(G, 1, E)).sum(-1) - 1).cast(dtypes.int32)
return tile_e.reshape(-1, 1).expand(-1, BLOCK_ROW).reshape(-1)
def n_groups_of(t:Tensor) -> int:
return len(t.device) if isinstance(t.device, tuple) else 1
def route(logits:Tensor, experts_per_tok:int, n_experts:int) -> Routing:
T, E = logits.shape
k, G = experts_per_tok, n_groups_of(logits)
assert T % G == 0, f"tokens {T} must split across {G} devices"
T_l, m_l = T // G, m_max_for(T // G, k, n_experts)
topv, topi = logits.reshape(G, T_l, E).topk(k)
weights = topv.softmax(-1)
m = topi.reshape(G, T_l * k).cast(dtypes.int32).one_hot(E).cast(dtypes.int32)
pad = ((m.sum(1) + (BLOCK_ROW - 1)) // BLOCK_ROW) * BLOCK_ROW
off = pad.cumsum(1).pad(((0, 0), (1, 0)))
dest_row = ((m.cumsum(1) + off[:, :E].reshape(G, 1, E)) * m).sum(-1).sub(1).cast(dtypes.int32)
return Routing(weights, dest_row, off, m_l, G, T_l)
def dispatch(x:Tensor, r:Routing) -> Tensor:
G, D = r.n_groups, x.shape[-1]
return grouped_scatter_rows(x.reshape(G, r.t_local, D), r.dest_row, r.m_l).reshape(G * r.m_l, D)
def combine(y:Tensor, r:Routing, n_tokens:int, experts_per_tok:int) -> Tensor:
G, D, k = r.n_groups, y.shape[-1], experts_per_tok
sel = grouped_gather_rows(y.reshape(G, r.m_l, D), r.dest_row, G).reshape(G, r.t_local, k, D)
return (sel * r.weights.reshape(G, r.t_local, k, 1).cast(sel.dtype)).sum(2).reshape(n_tokens, D).cast(y.dtype)
+195
View File
@@ -0,0 +1,195 @@
"""
tilelang-style matmul_relu written with tinygrad UOp APIs.
Demonstrates that tilelang's T.alloc_fragment is expressible with existing
tinygrad primitives: a per-thread REG buffer, wrapped in one Ops.UNSHARD per
sharded axis over the LOCAL thread-grid ranges to form the full logical tile.
Here the 64 threads are an 8x8 grid and each thread owns an 8x8 sub-tile --
the 2-D fragment layout tilelang infers. The kernel is written against the
full-tile UNSHARD view, and multi_pm (the same pass that lowers multi-device
UNSHARDs) resolves it into per-thread shard code.
Reference tilelang kernel:
@tilelang.jit
def matmul_relu(A, B, block_M=64, block_N=64, block_K=64,
dtype=T.float16, accum_dtype=T.float32):
M, N, K = T.const('M, N, K')
C = T.empty([M, N], dtype)
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
A_shared = T.alloc_shared((block_M, block_K), dtype)
B_shared = T.alloc_shared((block_K, block_N), dtype)
C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
T.clear(C_local)
for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
T.copy(A[by * block_M, ko * block_K], A_shared)
T.copy(B[ko * block_K, bx * block_N], B_shared)
T.gemm(A_shared, B_shared, C_local)
for i, j in T.Parallel(block_M, block_N):
C_local[i, j] = T.max(C_local[i, j], 0)
T.copy(C_local, C[by * block_M, bx * block_N])
return C
API mapping (tilelang -> tinygrad UOps, idioms from test/backend/test_custom_kernel.py):
T.Kernel(gx, gy, threads=T) -> AxisType.GLOBAL ranges (blocks) + AxisType.LOCAL ranges (thread grid)
T.alloc_shared(shape, dtype) -> UOp.placeholder(shape, dtype, slot, AddrSpace.LOCAL)
T.alloc_fragment(shape, dt) -> per-thread REG placeholder, wrapped in one Ops.UNSHARD per sharded axis over
the AxisType.LOCAL ranges: fragment.unshard((axis_y, axis_x), (ty, tx)).
The full logical tile is the shard with each sharded axis multiplied by its
range size, exactly like device sharding, but the sharding axes are thread
axes carried by the RANGE metadata instead of a device tuple. C_local[i, j]
with [i, j] in this thread's shard is INDEX on the UNSHARD, which multi_pm
resolves into INDEX on the per-thread REG shard, axis by axis.
T.copy(gmem_slice, smem) -> smem[thread_idx].set(gmem_slice[thread_idx], end=copy_rng). set returns the
smem tile AFTER the copy; the implicit-barrier pass turns the store->load
dependency of the loop that consumes it into a workgroup barrier
T.gemm (no WMMA) -> C_local[..].set(C_local.after(k)[..] + a_shared[..] * b_shared[..], end=k)
with k a loop-carried LOOP range (codegen builds the register accumulator
from this self-referential store automatically)
T.copy(fragment, gmem) -> gmem.index(gidx).store(C_local[..]).end(all_ranges)
UNSHARD lowering -> multi_pm in codegen (full_rewrite_to_sink): INDEX/AFTER/STORE ops on the
full-tile view become per-thread shard ops, no UNSHARD survives into the program.
"""
from tinygrad.dtype import dtypes, AddrSpace, DType
from tinygrad.uop.ops import UOp, Ops, AxisType, KernelInfo
from tinygrad.helpers import cdiv, getenv
from tinygrad.tensor import Tensor
# ---------------------------------------------------------------------------
# tilelang builtins, expressed with tinygrad UOp APIs
# ---------------------------------------------------------------------------
def alloc_shared(shape:tuple[int, ...], dtype:DType) -> UOp:
"""T.alloc_shared: one LOCAL buffer shared by all threads in the block."""
return UOp.placeholder(tuple(shape), dtype, next(UOp.unique_num), AddrSpace.LOCAL)
def alloc_fragment(shape:tuple[int, ...], dtype:DType, axes:tuple[int, ...], rngs:tuple[UOp, ...]) -> UOp:
"""T.alloc_fragment: per-thread REG fragment + UNSHARD over the LOCAL thread grid.
Each thread privately owns shape[axis]//threads elements along every sharded
axis in a REG buffer. The UNSHARDs over the LOCAL thread ranges present the
full logical tile: full_shape = shard_shape with each sharded axis multiplied
by its range size. This is exactly how UNSHARD carries a DEVICE axis today,
except the sharding axes are thread axes carried by the RANGE metadata.
"""
assert len(axes) == len(rngs)
assert all(tnum.op is Ops.RANGE and tnum.arg[-1] is AxisType.LOCAL for tnum in rngs), "fragments shard over LOCAL ranges"
assert all(shape[a] % (int(rng.vmax)+1) == 0 for a, rng in zip(axes, rngs))
by_axis = dict(zip(axes, rngs))
shard_shape = tuple(s // (int(by_axis[i].vmax)+1) if i in by_axis else s for i, s in enumerate(shape))
fragment = UOp.placeholder(shard_shape, dtype, next(UOp.unique_num), AddrSpace.REG)
return fragment.unshard(axes, rngs)
# ---------------------------------------------------------------------------
# GEMM kernel: C = relu(A @ B), float inputs (fp16 or fp32), fp32 fragment accumulator, no WMMA
# ---------------------------------------------------------------------------
# 64x64 output tile per block, 128 threads as an 8x16 grid; each thread owns an 8x4 fragment sub-tile
# (the 2-D per-thread layout tilelang infers for this GEMM). The 4 contiguous columns (TN=4) are what
# let codegen vectorize loads/stores to float4, matching tilelang's lowering exactly.
BLOCK_M = BLOCK_N = BLOCK_K = 64
TY = 8
TX = 16
THREADS = TY * TX
TM = BLOCK_M // TY # fragment rows per thread (8)
TN = BLOCK_N // TX # fragment columns per thread (4)
def matmul_relu_kernel(c:UOp, a:UOp, b:UOp) -> UOp:
"""C[M, N] = relu(A[M, K] @ B[K, N]) -- one 64x64 tile per block, locals + a 2-D fragment."""
M, K = a.shape
K2, N = b.shape
assert K == K2 and a.dtype == b.dtype == c.dtype and not dtypes.is_int(a.dtype)
assert not (K % BLOCK_K or M % BLOCK_M or N % BLOCK_N), "test sizes must be multiples of the block sizes"
# with T.Kernel(T.ceildiv(N, BLOCK_N), T.ceildiv(M, BLOCK_M), threads=128) as (bx, by):
bx = UOp.range(cdiv(N, BLOCK_N), 0, AxisType.GLOBAL)
by = UOp.range(cdiv(M, BLOCK_M), 1, AxisType.GLOBAL)
# tx (N, 16) is the fast/inner LOCAL axis so a warp covers 16 cols x 2 rows --
# matching tilelang's (tidx>>4, tidx&15) warp composition. This keeps the 8 A_shared
# reads in a warp on only 2 row-groups (broadcast across 16 cols) instead of 8 rows
# (8-way bank conflict), since A_shared[row*512 + ...] all map to the same bank when 8
# distinct rows land in one warp.
tx = UOp.range(TX, 2, AxisType.LOCAL)
ty = UOp.range(TY, 3, AxisType.LOCAL)
# A_shared = T.alloc_shared((BLOCK_M, BLOCK_K), dtype)
# B_shared = T.alloc_shared((BLOCK_K, BLOCK_N), dtype)
A_shared = alloc_shared((BLOCK_M, BLOCK_K), a.dtype)
B_shared = alloc_shared((BLOCK_K, BLOCK_N), b.dtype)
# C_local = T.alloc_fragment((BLOCK_M, BLOCK_N), accum_dtype) -- an 8x4 REG tile per thread of the 8x16 grid
C_local = alloc_fragment((BLOCK_M, BLOCK_N), dtypes.float32, (0, 1), (ty, tx))
# T.clear(C_local) -- each thread zeroes its own fragment sub-tile
ic, jc = UOp.range(TM, 4, AxisType.LOOP), UOp.range(TN, 5, AxisType.UPCAST)
C_loc = C_local[ic*TM + ty, tx*TN + jc].set(0.0, end=(ic, jc))
# for ko in T.Pipelined(T.ceildiv(K, BLOCK_K), num_stages=3):
# (num_stages pipelining is async copy + multi-buffering; this is the synchronous single-buffer version)
ko = UOp.range(cdiv(K, BLOCK_K), 6, AxisType.LOOP)
# T.copy(A[by * BLOCK_M, ko * BLOCK_K], A_shared) -- each thread copies its own 8x4 sub-tile.
# Row index is iar*TM + ty (strided by TM across ty), matching tilelang's layout: thread ty owns
# rows {ty, ty+8, ..., ty+56} not {ty*8, ..., ty*8+7}.
iar, ka = UOp.range(TM, 7, AxisType.LOOP), UOp.range(TN, 8, AxisType.UPCAST)
A_store = A_shared[iar*TM + ty, tx*TN + ka].store(a[by*BLOCK_M + iar*TM + ty, ko*BLOCK_K + tx*TN + ka]).end(iar, ka)
# T.copy(B[ko * BLOCK_K, bx * BLOCK_N], B_shared)
kb, ibr = UOp.range(TM, 9, AxisType.LOOP), UOp.range(TN, 10, AxisType.UPCAST)
B_store = B_shared[kb*TM + ty, tx*TN + ibr].store(b[ko*BLOCK_K + kb*TM + ty, bx*BLOCK_N + tx*TN + ibr]).end(kb, ibr)
# get the shared after the stores (single barrier)
A_shared = A_shared.after(A_store, B_store)
B_shared = B_shared.after(A_store, B_store)
# T.gemm(A_shared, B_shared, C_local), no WMMA -- per-thread accumulate over its fragment sub-tile.
# identical to custom_gemm: a self-referential store over the loop-carried kk range,
# which codegen turns into a register accumulator
# kk is the outer compute loop (axis 11) so that for each kk we read all 8 A rows and reuse
# the B[kk] read across them -- matching tilelang's ko > kk > row > col access order exactly.
kk, ir = UOp.range(BLOCK_K, 11, AxisType.LOOP), UOp.range(TM, 12, AxisType.LOOP)
jj = UOp.range(TN, 13, AxisType.UPCAST)
acc = C_loc.after(kk)[ir*TM + ty, tx*TN + jj] + A_shared[ir*TM + ty, kk].cast(dtypes.float32) * B_shared[kk, tx*TN + jj].cast(dtypes.float32)
# closing the ko loop here too; codegen adds the barrier so no thread overwrites the tiles while others still read them
C_loc = C_loc[ir*TM + ty, tx*TN + jj].set(acc, end=(kk, ir, jj, ko))
# for i, j in T.Parallel(BLOCK_M, BLOCK_N): C_local[i, j] = T.max(C_local[i, j], 0)
# T.copy(C_local, C[by * BLOCK_M, bx * BLOCK_N]) -- per-thread store of the fragment shard (relu fused into it)
# LOOP: these loops are the per-thread output layout; convert_loop_to_global must not globalize them
ie, je = UOp.range(TM, 14, AxisType.LOOP), UOp.range(TN, 15, AxisType.UPCAST)
c_st = c[by*BLOCK_M + ie*TM + ty, bx*BLOCK_N + tx*TN + je].store(C_loc[ie*TM + ty, tx*TN + je].relu().cast(c.dtype))
# all open ranges are closed at the final store (ko was closed above).
# the fragment UNSHARDs go to codegen as is: multi_pm there resolves the full-tile view into per-thread shard code
return c_st.end(je, ie, tx, ty, bx, by).sink(arg=KernelInfo(name="matmul_relu", opts_to_apply=()))
# ---------------------------------------------------------------------------
# python wrapper: same signature as the tilelang function
# ---------------------------------------------------------------------------
def matmul_relu(a:Tensor, b:Tensor) -> Tensor:
"""C = relu(A @ B), fp16 in/out with an fp32 fragment accumulator."""
c = Tensor.empty(a.shape[0], b.shape[1], dtype=a.dtype, device=a.device)
return c.custom_kernel(a, b, fxn=matmul_relu_kernel)[0]
# ---------------------------------------------------------------------------
# test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
from tinygrad import Device
assert Device[Device.DEFAULT].renderer.has_local, "this GPU-style kernel needs a backend with local memory (LOCAL ranges + barriers)"
M = K = N = getenv("N", 256) # 4x4 grid of 64x64 tiles, 4 K chunks
dtype_in = dtypes.half if getenv("HALF") else dtypes.float
a = Tensor.randn(M, K, dtype=dtype_in).contiguous()
b = Tensor.randn(K, N, dtype=dtype_in).contiguous()
ref = (a @ b).relu().realize()
out = matmul_relu(a, b).realize()
import numpy as np
np.testing.assert_allclose(out.numpy(), ref.numpy(), atol=1e-1, rtol=1e-2)
print("matmul_relu passed!")
+2 -2
View File
@@ -4,7 +4,7 @@ import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, co
assert sys.platform != 'win32'
from dataclasses import dataclass
from tinygrad.runtime.support.hcq2 import HCQ2Compiled, HCQAllocator, HCQ2Buffer, encode_kernargs_clike, make_cmdbuf
from tinygrad.runtime.support.hcq2 import make_binary_patch, make_patch
from tinygrad.runtime.support.hcq2 import make_binary_patch, make_patches
from tinygrad.uop.ops import sint, UOp
from tinygrad.device import Compiled, BufferSpec, Buffer, Device
from tinygrad.dtype import dtypes
@@ -158,7 +158,7 @@ def pm4_submit(ctx, lin):
ib = UOp.placeholder((size_dw + 2,), dtypes.uint32, next(UOp.unique_num), device=devs, volatile=True).rtag("cmdbuf")
done_idx, submit_idx = UOp.const(dtypes.int, size_dw + 0), UOp.const(dtypes.int, size_dw + 1)
submitted = (counter:=ib.after(*[make_patch(ib, (size_dw + i) * 4, UOp.const(dtypes.uint32, 0)) for i in range(2)]).index(submit_idx)).load()
submitted = (counter:=ib.after(make_patches(ib, [((size_dw + i) * 4, UOp.const(dtypes.uint32, 0)) for i in range(2)])).index(submit_idx)).load()
completed = ib.after(loop:=UOp.loop(0)).index(done_idx).load()
ib_free = completed.end(loop, completed != submitted)
@@ -16,7 +16,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
wg = UOp.range(NUM_WG, 0, AxisType.GLOBAL)
tid = UOp.range(THREADS_PER_WG, 1, AxisType.LOCAL)
it = UOp.range((n_elems // VEC) // (NUM_WG * THREADS_PER_WG), 2, AxisType.LOOP)
it = UOp.range((n_elems // VEC) // (NUM_WG * THREADS_PER_WG), 2, AxisType.WEAK)
lane = UOp.range(VEC, 3, AxisType.UNROLL)
idx = (((it * NUM_WG + wg) * THREADS_PER_WG + tid) * VEC) + lane
+6 -82
View File
@@ -4,7 +4,7 @@ from hexdump import hexdump
from copy import deepcopy
import pathlib, sys
from tinygrad.helpers import to_mv, getenv
from tinygrad.runtime.autogen import adreno
from tinygrad.runtime.autogen import mesa
sys.path.append(pathlib.Path(__file__).parent.parent.parent.as_posix())
IOCTL = getenv("IOCTL", 0)
@@ -23,7 +23,7 @@ for child in xml.getroot():
CAPTURED_STATE = {}
REGS = {}
for k, v in adreno.__dict__.items():
for k, v in mesa.__dict__.items():
if k.startswith("REG_") and isinstance(v, int) and v > 1024: REGS[v] = k
from extra.qcom_gpu_driver import msm_kgsl
@@ -42,7 +42,7 @@ def get_struct(argp, stype):
def format_struct(s):
sdats = []
for field_name, *_ in s._real_fields_:
for field_name, *_ in s._fields_:
if field_name in {"__pad", "PADDING_0"}: continue
dat = getattr(s, field_name)
if isinstance(dat, int): sdats.append(f"{field_name}:0x{dat:X}")
@@ -96,9 +96,9 @@ def parse_cmd_buf(dat):
CAPTURED_STATE['LOAD_FRAGS'].append((state_block, state_type, num_unit, dst_off))
if state_block == SB6_CS_SHADER:
from extra.disassemblers.adreno import disasm_raw
from tinygrad.runtime.support.compiler_mesa import disas_adreno
if state_type == ST6_SHADER and IOCTL > 3:
disasm_raw(get_mem(((vals[2] << 32) | vals[1]), num_unit * 128))
disas_adreno(get_mem(((vals[2] << 32) | vals[1]), num_unit * 128))
if state_type == ST6_CONSTANTS:
x = get_mem(((vals[2] << 32) | vals[1]), num_unit*4)
CAPTURED_STATE['constants'] = x[:]
@@ -142,7 +142,7 @@ def parse_cmd_buf(dat):
vals = struct.unpack("I"*size, dat[ptr+4:ptr+4+4*size])
if IOCTL > 0: print(f"{ptr:3X} -- typ 4: {size=:3d}, {reg_name}", hprint(vals))
for vi,v in enumerate(vals): CAPTURED_STATE[offset+vi] = v
if offset == adreno.REG_A6XX_SP_CS_CONFIG:
if offset == mesa.REG_A6XX_SP_CS_CONFIG:
val = vals[0]
if IOCTL > 0:
print(f"\tBINDLESS_TEX={(val >> 0) & 0b1}")
@@ -215,79 +215,3 @@ def install_hook(c_function, python_function):
libc = ctypes.CDLL(ctypes.util.find_library("libc"))
install_hook(libc.ioctl, ioctl)
def before_launch():
global CAPTURED_STATE
CAPTURED_STATE.clear()
def collect_last_launch_state():
global CAPTURED_STATE
return deepcopy(CAPTURED_STATE)
def compare_launch_state(state, good_state):
cmp = [
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NTEX__MASK),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NSAMP__MASK),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_NIBO__MASK),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_ENABLED),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_TEX),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_SAMP),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_IBO),
(adreno.REG_A6XX_SP_CS_CONFIG, adreno.A6XX_SP_CS_CONFIG_BINDLESS_UBO),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_HALFREGFOOTPRINT__MASK),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_FULLREGFOOTPRINT__MASK),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_BRANCHSTACK__MASK),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_FULLREGFOOTPRINT__MASK),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_THREADMODE__MASK),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_EARLYPREAMBLE),
(adreno.REG_A6XX_SP_CS_CTRL_REG0, adreno.A6XX_SP_CS_CTRL_REG0_MERGEDREGS),
(adreno.REG_A6XX_SP_CS_PVT_MEM_PARAM, adreno.A6XX_SP_CS_PVT_MEM_PARAM_MEMSIZEPERITEM__MASK),
(adreno.REG_A6XX_SP_CS_PVT_MEM_PARAM, adreno.A6XX_SP_CS_PVT_MEM_PARAM_HWSTACKSIZEPERTHREAD__MASK),
(adreno.REG_A6XX_SP_CS_UNKNOWN_A9B1, adreno.A6XX_SP_CS_UNKNOWN_A9B1_UNK5),
(adreno.REG_A6XX_SP_CS_UNKNOWN_A9B1, adreno.A6XX_SP_CS_UNKNOWN_A9B1_UNK6),
(adreno.REG_A6XX_SP_CS_BRANCH_COND, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_KERNELDIM__MASK),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEX__MASK),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEY__MASK),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_0, adreno.A6XX_HLSQ_CS_NDRANGE_0_LOCALSIZEZ__MASK),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_1, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_2, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_3, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_4, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_5, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_NDRANGE_6, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_CNTL_0, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_CNTL_1, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_X, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_Y, 0xffffffff),
(adreno.REG_A6XX_HLSQ_CS_KERNEL_GROUP_Z, 0xffffffff),
]
for x,m in cmp:
print(f"Field {REGS[x]}, mask: 0x{m:X} cmp: {state.get(x, 0) & m} vs {good_state.get(x, 0) & m}")
if state.get(x, 0) & m != good_state.get(x, 0) & m:
return False, f"Field {REGS[x]}, mask: 0x{m:X} mismatch: {state.get(x, 0) & m} vs {good_state.get(x, 0) & m}"
for n in ['descriptors', 'ibos']:
if n not in good_state: continue
mv1, mv2 = state.get(n), good_state.get(n)
if len(mv1) != len(mv2): return False, f"{n}: len mismatch {len(mv1)} != {len(mv2)}"
mv1 = memoryview(bytearray(mv1)).cast('I')
mv2 = memoryview(bytearray(mv2)).cast('I')
for i in range(len(mv2)):
if i % 8 == 5 or i % 8 == 4: continue # addresses
if mv1[i]!=mv2[i]: return False, f"{n}: content mismatch {i} {mv1[i]} {mv2[i]}"
for n in ['samplers']:
if n not in good_state: continue
mv1, mv2 = state.get(n), good_state.get(n)
if len(mv1) != len(mv2): return False, f"{n}: len mismatch {len(mv1)} != {len(mv2)}"
if any(mv1[i]!=mv2[i] for i in range(len(mv1))): return False, f"{n}: content mismatch"
return True, "PASS"
+2 -2
View File
@@ -48,14 +48,14 @@ class Kernel(AbstractContextManager):
@property
def warpgroup(self): return self.group(4)
def range(self, start:int, end:int=0, step:int=1, axis_type:AxisType=AxisType.LOOP, track:bool=True):
def range(self, start:int, end:int=0, step:int=1, axis_type:AxisType=AxisType.WEAK, track:bool=True):
if end == 0: start, end = 0, start
rng = _tk_range(start, end, step, axis_type, self.range_id)
self.range_id += 1
if track: self.range_stack.append(rng)
return rng
def raw_range(self, end:int=0, axis_type:AxisType=AxisType.LOOP):
def raw_range(self, end:int=0, axis_type:AxisType=AxisType.WEAK):
rng = UOp.range(end, self.range_id, axis_type=axis_type)
self.range_id += 1
return rng
BIN
View File
Binary file not shown.
+6 -6
View File
@@ -80,7 +80,7 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
\op{Index} & $(T, i_0, i_1, \ldots)$ & --- & Index from left. $()$-shaped $i$ removes dim; $(k,)$-shaped makes it $k$. \\
\op{Stack} & $(T_0, T_1, \ldots)$ & --- & Join along a newly created leading axis. All shapes must match. \\
\op{Bitcast} & $(T,)$ & dtype & Reinterpret storage as target dtype; preserve total bytes. \\
\op{Unshard} & $(T, R)$ & axis $a$ & Concatenate the shards indexed by \op{Range} $R$ along $a$; $R$ is outer. \\
\op{Unshard} & $(T, R_0, R_1, \ldots)$ & axes $(a_0, a_1, \ldots)$ & Concatenate shards of \op{Range} $R_k$ along axis $a_k$; $R_k$ is outer. \\
\bottomrule
\end{tabular}
@@ -260,7 +260,7 @@ Every UOp has a \textbf{dtype}, \textbf{shape}, \textbf{device}, \textbf{addrspa
\op{Const} & from arg & $()$ & \textsc{null} & $[v, v]$ \\
\op{Param} & from arg & from $\mathrm{src}[0]$ & from arg & from src or dtype range \\[3pt]
Movement ops & $\mathrm{src}[0].\mathrm{dtype}$ & (see op) & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
\op{Unshard} & $\mathrm{src}[0].\mathrm{dtype}$ & $\mathrm{src}[0]$, axis $\times n$ & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
\op{Unshard} & $\mathrm{src}[0].\mathrm{dtype}$ & $\mathrm{src}[0]$, each $a_k \times n_k$ & $\mathrm{src}[0].\mathrm{device}$ & $\mathrm{src}[0]$ \\
\op{Reduce} & $\mathrm{src}[0].\mathrm{dtype}$ & remove first $n$ axes & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\[3pt]
\op{Cast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & clamped to dtype \\
\op{Bitcast} & from arg & $\mathrm{src}[0].\mathrm{shape}$ & $\mathrm{src}[0].\mathrm{device}$ & dtype range \\
@@ -286,9 +286,9 @@ $[a,A]$, $[b,B]$, $[c,C]$ denote min\_max of $\mathrm{src}[0]$, $\mathrm{src}[1]
Default \emph{dtype range}: $[\mathrm{dtype\_min},\, \mathrm{dtype\_max}]$.
\medskip
\textbf{axis} tracks the multi-device sharding dimension. \op{Unshard} defines it (axis $=$ arg). \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim).
\op{Reshape} remaps axis to preserve the shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts axis right by $|\mathbf{n}|$.
\op{Reduce} on the shard axis $\to$ \textsc{null} (shard axis is among the first $n$ axes). \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
\textbf{sharding} tracks multi-device sharding as a set of (axis, \op{Range}) pairs. \op{Unshard} defines it: arg is the tuple of sharded axes, one \op{Range} in src per axis (positional: the $k$-th \op{Range} shards the $k$-th axis). \op{Buffer} with $n$-tuple device: sharded on axis $0$ (device dim). The single-axis convenience \textbf{axis} is \textsc{null} unless exactly one axis is sharded.
\op{Reshape} remaps each sharded axis to preserve its shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts all sharded axes right by $|\mathbf{n}|$.
\op{Reduce} on a sharded axis drops it. \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
%% ============================================================
\subsection*{Kernel Optimizations (OptOps) \normalfont\small--- schedule-level transforms on kernel ranges}
@@ -382,7 +382,7 @@ def scatter_add(T, idx, val):
Let $D = (d_0, \ldots, d_{n-1})$ be an $n$-tuple device.
\op{Copy} to an $n$-tuple device reshards with axis $= 0$. \op{Copy} never changes shape.
\textbf{Sharding} splits a tensor along an axis across $n$ devices. It opens a \op{Range} of type \texttt{DEVICE} (a symbolic per-device index $d$), shrinks each device's view to its piece, then closes the range with \op{Unshard}$(T, R, a)$. The result is a logical tensor whose shape along axis $a$ is the full size; each device holds $1/n$ of it. \op{Unshard} is the inverse of sharding --- it marks the boundary between per-device computation and the logical multi-device tensor. The range need not be \texttt{DEVICE}; e.g.\ a \texttt{WARP} range closes the same way, concatenating per-lane shards along $a$ with the range as the outer factor.
\textbf{Sharding} splits a tensor along an axis across $n$ devices. It opens a \op{Range} of type \texttt{DEVICE} (a symbolic per-device index $d$), shrinks each device's view to its piece, then closes the range with \op{Unshard}$(T, R, a)$. The result is a logical tensor whose shape along axis $a$ is the full size; each device holds $1/n$ of it. \op{Unshard} is the inverse of sharding --- it marks the boundary between per-device computation and the logical multi-device tensor. The range need not be \texttt{DEVICE}; e.g.\ a \texttt{WARP} range closes the same way, concatenating per-lane shards along $a$ with the range as the outer factor. A tensor may be sharded along several axes at once: \op{Unshard}$(T, R_0, R_1, \ldots;\; a_0, a_1, \ldots)$ carries one \op{Range} per sharded axis, and every movement op maps each sharded axis independently.
\begin{lstlisting}
# T has shape (s,) on a single device.
+99 -7
View File
@@ -2,6 +2,7 @@ import unittest
from tinygrad import Tensor, UOp, GlobalCounters, Context, Device
from tinygrad.dtype import AddrSpace, dtypes, Invalid
from tinygrad.uop.ops import KernelInfo, AxisType, Ops
from tinygrad.renderer.ptx import PTXRenderer
# **** kernels ****
@@ -421,19 +422,110 @@ class TestCustomKernel(unittest.TestCase):
@Context(DEV="CPU")
def test_simple_from_source(self):
a = Tensor([0., 1., 2.]).realize()
src = "void test_src(float* restrict a) { a[0] = 1.0; }"
a = Tensor.arange(4).clone().realize()
src = "void test_src(int* restrict a) { a[0] = 1; }"
# TODO: it currently requires a compiler for Ops.BINARY
from tinygrad.device import Device
binary = Device[a.device].renderer.compiler.compile(src)
def custom_src_kernel(A:UOp) -> UOp:
sink = UOp.sink(A, arg=KernelInfo(name="test_src"))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(sink.toposort())),
UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=binary)))
return UOp(Ops.PROGRAM, src=(sink, UOp(Ops.LINEAR, src=tuple(sink.toposort())), UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=binary)))
a = Tensor.custom_kernel(a.reshape(2, 2).T, fxn=custom_src_kernel)[0]
self.assertEqual(a.tolist(), [[1, 2], [1, 3]])
a = Tensor.custom_kernel(a, fxn=custom_src_kernel)[0]
self.assertEqual(a.tolist(), [1., 1., 2.])
class TestCustomKernelInput(unittest.TestCase):
def _test_mop(self, mop_fxn, max_kernels):
# default: input is BUFFER
x = mop_fxn(Tensor.arange(32).clone("CPU").realize())
y = Tensor.custom_kernel(Tensor.empty_like(x), x, fxn=custom_add_one_kernel)[0]
GlobalCounters.reset()
y.realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), x.add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
# same test with @function, input is PARAM
from tinygrad import function
x0 = Tensor.arange(32).clone("CPU").realize()
@function(precompile=True)
def run(a:Tensor) -> Tensor:
xv = mop_fxn(a)
y = Tensor.invalids(*xv.shape, dtype=xv.dtype, device=a.device)
return Tensor.custom_kernel(y, xv, fxn=custom_add_one_kernel)[0]
GlobalCounters.reset()
y = run(x0).realize()
kernel_count = GlobalCounters.kernel_count
self.assertEqual(y.tolist(), mop_fxn(x0).add(1).tolist())
self.assertLessEqual(kernel_count, max_kernels)
def test_reshape(self): self._test_mop(lambda x: x.reshape(16, 2), max_kernels=2)
def test_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T, max_kernels=3)
def test_double_permute(self): self._test_mop(lambda x: x.reshape(4, 8).T.T, max_kernels=3)
def test_shrink(self): self._test_mop(lambda x: x[:4], max_kernels=2)
def test_pad(self): self._test_mop(lambda x: x[:4].pad(((0, 4),)), max_kernels=2)
def test_flip(self): self._test_mop(lambda x: x.flip(0), max_kernels=2)
def test_offset_shrink(self): self._test_mop(lambda x: x[4:8], max_kernels=2)
def test_2d_shrink(self): self._test_mop(lambda x: x.reshape(4, 8)[:, 2:6], max_kernels=3)
def test_expand(self): self._test_mop(lambda x: x.reshape(16, 2)[:, :1].expand(16, 2), max_kernels=3)
class TestUnshardIndex(unittest.TestCase):
"""Regression tests for INDEX on UNSHARD (fragment) resolution in schedule/multi.py.
A fragment is a per-thread REG buffer wrapped in UNSHARD over LOCAL thread ranges.
index_multi must resolve an INDEX on the UNSHARD view into an INDEX on the per-thread
shard. Two ownership patterns must work:
contiguous: idx = rng*shard_sz + local (thread rng owns [rng*shard_sz, ...))
strided: idx = rng + ir*shard_sz (thread rng owns {rng, rng+shard_sz, ...})
"""
def _run(self, kernel, shape=(8, 8)):
c = Tensor.empty(*shape)
out = Tensor.custom_kernel(c, fxn=kernel)[0]
try: return out.numpy()
except RuntimeError as e:
if isinstance(Device[Device.DEFAULT].renderer, PTXRenderer) and "dynamic register indexing" in str(e):
self.skipTest("PTX does not support dynamic register indexing")
raise
@unittest.skipIf(not Device[Device.DEFAULT].renderer.has_local, "fragment tests need LOCAL ranges")
def test_contiguous_fragment_index(self):
# thread ty owns rows [ty*8, ty*8+8) of a 64-row fragment -- contiguous ownership.
# This is the pre-existing case that index_multi always handled.
def kernel(C:UOp) -> UOp:
ty = UOp.range(8, 0, AxisType.LOCAL)
ir = UOp.range(8, 1, AxisType.LOOP)
j = UOp.range(8, 2, AxisType.LOOP)
# 8x8 fragment, 8 threads -> 64x8 full tile. thread ty owns rows [ty*8, ty*8+8).
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
return C[ty*8 + ir, j].store(frag[ty*8 + ir, j]).end(j, ir, ty).sink(arg=KernelInfo(name="contig_frag"))
out = self._run(kernel, (64, 8))
assert out.shape == (64, 8)
@unittest.skipIf(not Device[Device.DEFAULT].renderer.has_local, "fragment tests need LOCAL ranges")
def test_strided_fragment_index(self):
# thread ty owns rows {ty, ty+8, ty+16, ty+24, ..., ty+56} of a 64-row fragment --
# strided ownership. idx = ty + ir*8 where shard_sz=8 (8 threads, shard rows=8).
# The contiguous check (idx - rng*shard_sz) fails; the strided check
# (idx-rng) % shard_sz == 0 must succeed. This is the pattern the index_multi fix adds.
def kernel(C:UOp) -> UOp:
ty = UOp.range(8, 0, AxisType.LOCAL)
ir = UOp.range(8, 1, AxisType.LOOP)
j = UOp.range(8, 2, AxisType.LOOP)
# 8x8 fragment, 8 threads -> 64x8 full tile. thread ty owns rows {ty, ty+8, ..., ty+56}.
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
return C[ty + ir*8, j].store(frag[ty + ir*8, j]).end(j, ir, ty).sink(arg=KernelInfo(name="strided_frag"))
out = self._run(kernel, (64, 8))
assert out.shape == (64, 8)
def test_fragment_index_cannot_shard(self):
# thread ty indexing rows [ty, ty+8) overlaps with other threads' rows -- this matches neither
# the contiguous nor the strided ownership pattern, so index_multi must raise.
def kernel(C:UOp) -> UOp:
ty = UOp.range(8, 0, AxisType.LOCAL)
ir = UOp.range(8, 1, AxisType.LOOP)
j = UOp.range(8, 2, AxisType.LOOP)
frag = UOp.placeholder((8, 8), dtypes.float32, 0, AddrSpace.REG).unshard((0,), (ty,))
return C[ty + ir, j].store(frag[ty + ir, j]).end(j, ir, ty).sink(arg=KernelInfo(name="bad_frag"))
with self.assertRaisesRegex(RuntimeError, "cannot shard index"):
self._run(kernel, (64, 8))
class TestUOpReduce(unittest.TestCase):
def test_uop_sum(self):
+11
View File
@@ -13,6 +13,7 @@ from tinygrad.renderer.ptx import PTXRenderer
from tinygrad.renderer.cstyle import CUDARenderer
from tinygrad.renderer.isa import ISARenderer
from test.helpers import replace_opts
from test.backend.test_softmax_fusion import single_kernel_softmax
MOCKGPU = DEV.interface.startswith("MOCK")
from tinygrad.uop.render import print_uops # noqa: F401 # pylint: disable=unused-import
@@ -392,6 +393,16 @@ class TestLinearizer(unittest.TestCase):
# the global store doesn't change
assert stores[1].src[1].dtype == dtypes.float
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_local, "test requires locals")
@unittest.skipUnless(Device[Device.DEFAULT].renderer.has_shared, "test requires shared")
def test_two_grouped_stores_local(self):
# GROUP on both reduces puts two LOCAL buffers in one kernel, and the store to each needs its own barrier
a = Tensor.rand(32, 32).realize()
opts = [Opt(OptOps.GROUP, 1, 4), Opt(OptOps.GROUP, 2, 4)]
ast = helper_linearizer_opt(single_kernel_softmax(a), [opts])
uops = to_program(replace_opts(ast, opts), renderer=Device[Device.DEFAULT].renderer).src[1].src
self.assertEqual(len([u for u in uops if u.op is Ops.BARRIER]), 2)
# *** helpers ***
def helper_realized_ast(r:Tensor|list[Tensor]) -> tuple[UOp, list[Buffer]]:
+43
View File
@@ -425,6 +425,49 @@ class TestMultiBufferView(unittest.TestCase):
run_linear(linear, var_vals)
np.testing.assert_equal(out.numpy(), ref[5].numpy())
@unittest.skipIf(not_support_multi_device(), "need multi")
class Test2DShard(unittest.TestCase):
def setUp(self):
self.devices_4 = tuple(f"{Device.DEFAULT}:{i}" for i in range(4))
self.rng = UOp.range(4, -1, AxisType.DEVICE)
self.rng0, self.rng1 = self.rng // 2, self.rng % 2
def _shard_2d(self, t:Tensor) -> Tensor:
u = t.uop.copy_to_device(self.devices_4)._shard(0, self.rng0)._shard(1, self.rng1).unshard((0, 1), (self.rng0, self.rng1))
return Tensor(u)
def test_2d_shard_basic(self):
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
t = self._shard_2d(ref)
out = t.contiguous().realize()
np.testing.assert_equal(out.numpy(), ref.numpy())
def test_2d_shard_elementwise(self):
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
t = self._shard_2d(ref)
out = (t + 1).contiguous().realize()
np.testing.assert_equal(out.numpy(), ref.numpy() + 1)
def test_2d_shard_sum_all(self):
ref = Tensor.arange(16).reshape(4, 4).contiguous().realize()
t = self._shard_2d(ref)
out = t.sum().contiguous().realize()
np.testing.assert_equal(out.numpy(), np.array(ref.numpy().sum()))
def test_2d_shard_sum_non_sharded_axis(self):
ref = Tensor.arange(4*4*2).reshape(4, 4, 2).contiguous().realize()
t = self._shard_2d(ref)
out = t.sum(axis=2).contiguous().realize()
np.testing.assert_equal(out.numpy(), ref.numpy().sum(axis=2))
def test_2d_shard_matmul(self):
a = Tensor.arange(16).reshape(4, 4).contiguous().realize()
b = Tensor.arange(16).reshape(4, 4).contiguous().realize()
a_s = self._shard_2d(a)
b_s = self._shard_2d(b)
out = (a_s @ b_s).contiguous().realize()
np.testing.assert_equal(out.numpy(), a.numpy() @ b.numpy())
@unittest.skipIf(not_support_multi_device(), "need multi")
class TestMultiTransformer(unittest.TestCase):
@needs_second_gpu
+12 -4
View File
@@ -1,6 +1,6 @@
import unittest
import numpy as np
from tinygrad import Tensor, Variable, dtypes
from tinygrad import Device, Tensor, Variable, TinyJit, dtypes
from tinygrad.helpers import CHECK_OOB
class TestTensorVariable(unittest.TestCase):
@@ -18,10 +18,18 @@ class TestTensorVariable(unittest.TestCase):
self.assertListEqual((vv * t).tolist(), [2, 2, 2])
except RuntimeError: pass
# TODO: a Variable PARAM lowers to int32, so a bound value that doesn't fit int32 truncates or fails to bind
@unittest.expectedFailure
@unittest.skipUnless(dtypes.long in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires long support")
def test_large_range_variable(self):
self.assertEqual(Tensor(Variable("b", 0, 2**40).bind(2**35)).item(), 2**35)
self.assertEqual(Tensor(Variable("b", 0, 2**40, dtype=dtypes.long).bind(2**35)).clone(Device.DEFAULT).item(), 2**35)
@unittest.skipUnless(dtypes.long in Device[Device.DEFAULT].renderer.supported_dtypes(), "requires long support")
def test_large_range_variable_jit(self):
@TinyJit
def f(a,b): return (Tensor(a+b).clone(Device.DEFAULT) * 2).realize()
for i in range(3):
a = Variable("a", 0, 2**10, dtype=dtypes.int).bind(i)
b = Variable("b", 0, 2**40, dtype=dtypes.long).bind(2**35)
self.assertEqual(f(a,b).item(), (2**35 + i) * 2)
def test_variable_defers_like_a_literal(self):
vv = Variable("a", 1, 10).bind(2)
+7 -7
View File
@@ -13,9 +13,9 @@ from tinygrad.dtype import Invalid
def vision_conv_143():
c0 = UOp.param(0, dtypes.half, shape=(16, 1024, 4))
c2 = UOp.range(32, 3, AxisType.LOOP)
c5 = UOp.range(128, 4, AxisType.LOOP)
c8 = UOp.range(16, 2, AxisType.LOOP)
c2 = UOp.range(32, 3)
c5 = UOp.range(128, 4)
c8 = UOp.range(16, 2)
c16 = UOp.range(7, 0, AxisType.REDUCE)
c17 = c8*2+c16
c24 = ((c17<3)!=True)&(c17<35)
@@ -39,9 +39,9 @@ def vision_conv_143():
def vision_conv_153():
c0 = UOp.param(0, dtypes.half, shape=(8, 1024, 4))
c2 = UOp.range(16, 3, AxisType.LOOP)
c5 = UOp.range(256, 4, AxisType.LOOP)
c8 = UOp.range(8, 2, AxisType.LOOP)
c2 = UOp.range(16, 3)
c5 = UOp.range(256, 4)
c8 = UOp.range(8, 2)
c16 = UOp.range(7, 0, AxisType.REDUCE)
c17 = c8*2+c16
c24 = ((c17<3)!=True)&(c17<19)
@@ -65,7 +65,7 @@ def vision_conv_153():
def dm_conv_172():
c0 = UOp.param(0, dtypes.half, shape=(1, 240, 4))
c2 = UOp.range(960, 4, AxisType.LOOP)
c2 = UOp.range(960, 4)
c5 = UOp.param(1, dtypes.half, shape=(8, 384, 4))
c7 = UOp.range(32, 0, AxisType.REDUCE)
c10 = UOp.range(4, 1, AxisType.REDUCE)
+77 -8
View File
@@ -6,7 +6,7 @@
# arg=3: lds - local data share
# arg=4: scratch - per-lane scratch memory
from __future__ import annotations
import ctypes, functools, re, platform, subprocess, tempfile
import ctypes, functools, re, platform, subprocess, tempfile, os
from typing import Callable
# Set/restore DAZ+FTZ (denormals-are-zero + flush-to-zero) to match RDNA3 default float mode
@@ -51,7 +51,7 @@ class _MXCSRContext:
if lib is None or not hasattr(self, '_saved'): return
lib.set_fpcr(self._saved)
from tinygrad.uop.ops import UOp, Ops, KernelInfo, AxisType
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.device import Buffer, BufferSpec, Device
from tinygrad.runtime.autogen import hsa
@@ -372,11 +372,33 @@ def _write_val(bits: int, val: UOp, wfn, reg_or_addr, *args, is_mem: bool = Fals
return _write_64bit(val, wfn, reg_or_addr, is_mem, *args) if bits == 64 else [wfn(reg_or_addr, _to_u32(val), *args)]
def _mem_store(mem: UOp, addr: UOp, val: UOp, active: UOp, addr_bits: int = 32, data_bits: int = 32) -> list[UOp]:
"""Conditional memory store with sub-word support. Returns list of store UOps."""
"""Conditional memory store with sub-word and unaligned support. Returns list of store UOps.
FLAT/GLOBAL accesses on AMD hardware are allowed to be unaligned, so a 32-bit store at a
byte offset of 1-3 spans two words (handled in 64-bit domain to keep shifts in range)."""
adt = dtypes.uint64 if addr_bits == 64 else dtypes.uint32
if data_bits > 32: # wider stores decompose into dwords; each dword handles its own alignment
ws = val.cast(dtypes.uint64) if data_bits > 64 else val
return [s for i in range(data_bits // 32)
for s in _mem_store(mem, addr + UOp.const(adt, i * 4), ws >> UOp.const(ws.dtype, 32 * i) if i else ws, active, addr_bits, 32)]
word_addr = addr >> UOp.const(adt, 2)
idx = mem.index(word_addr.valid(active))
if data_bits == 32: return [idx.store(active.where(_to_u32(val), idx))]
if data_bits == 32:
byte_off = (addr & UOp.const(adt, 3)).cast(dtypes.uint32)
is_unaligned = byte_off.ne(UOp.const(dtypes.uint32, 0))
if addr.divides(4) is not None: return [idx.store(active.where(_to_u32(val), idx))]
shift = byte_off * UOp.const(dtypes.uint32, 8)
val64 = _to_u32(val).cast(dtypes.uint64)
# word0 keeps its low byte_off*8 bits, gets val's low bits shifted in; word1 gets the rest
low_keep = (UOp.const(dtypes.uint32, 1) << shift) - UOp.const(dtypes.uint32, 1)
lo_bits = ((val64 << shift.cast(dtypes.uint64)) & UOp.const(dtypes.uint64, 0xFFFFFFFF)).cast(dtypes.uint32)
new_word0 = (idx & low_keep) | lo_bits
store0 = idx.store(active.where(is_unaligned.where(new_word0, _to_u32(val)), idx))
idx1 = mem.index((word_addr + UOp.const(adt, 1)).cast(dtypes.int64).valid(active & is_unaligned))
spill = (val64 >> (UOp.const(dtypes.uint64, 32) - shift.cast(dtypes.uint64))).cast(dtypes.uint32)
keep = UOp.const(dtypes.uint32, 0xFFFFFFFF) << shift
new_word1 = (idx1 & keep) | spill
return [store0, idx1.store((active & is_unaligned).where(new_word1, idx1))]
# Sub-word store: read-modify-write with mask
byte_pos = addr.cast(dtypes.uint32) & _c(3)
byte_shift = byte_pos * _c(8)
@@ -446,7 +468,7 @@ class _Ctx:
"""Create a lane range UOp with unique axis ID."""
if n is None: n = self.wave_size
self._axis_id += 1
return UOp.range(n, self._axis_id, AxisType.LOOP, dtype=dtypes.int)
return UOp.range(n, self._axis_id, dtype=dtypes.int)
def unroll_lanes(self, get_lane_bit, exec_mask: UOp, apply_exec: bool = True) -> UOp:
"""Combine lane bits into a mask using RANGE+REDUCE (32-bit for RDNA, 64-bit for CDNA)."""
@@ -2005,7 +2027,9 @@ def _compile_mubuf(inst: irc.MUBUF, ctx: _Ctx) -> UOp:
stores: list[UOp] = []
if is_lds and not is_store:
# LDS load: buffer -> LDS (bypass VGPRs), LDS addr = M0[17:0] + lane * elem_size
# LDS load: buffer -> LDS (bypass VGPRs), LDS addr = M0[17:0] + lane * elem_size.
# HW never takes a per-lane LDS address: kittens' direct fill sets M0 (s_mov_b32 m0, sN)
# before every lds instruction, giving lane-linear chunks with the swizzle on the GLOBAL side.
lds_base = ctx.rsgpr_dyn(_c(124)) & _c(0x3FFFF)
lds_addr = lds_base + lane.cast(dtypes.uint32) * _c(n_dwords * 4)
for i in range(n_dwords):
@@ -2114,6 +2138,8 @@ def _decode_at(pc: int, arch: str):
F32_INLINE = {240: 0x3f000000, 241: 0xbf000000, 242: 0x3f800000, 243: 0xbf800000, # 0.5, -0.5, 1.0, -1.0
244: 0x40000000, 245: 0xc0000000, 246: 0x40800000, 247: 0xc0800000, 248: 0x3e22f983} # 2.0, -2.0, 4.0, -4.0, 1/(2*pi)
_inst_hist: dict = {}
class WaveState:
__slots__ = ('vgpr_buf', 'sgpr_buf', 'accvgpr_buf', '_vgpr_mv', '_sgpr_mv', 'n_lanes', 'wave_size')
@@ -2209,7 +2235,8 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
# Use Buffer objects with external_ptr=0 for vmem
vmem_buf = Buffer('CPU', 1 << 40, dtypes.uint32, options=BufferSpec(external_ptr=0)).ensure_allocated()
lds_buf = Buffer('CPU', max(lds_size // 4, 1), dtypes.uint32).ensure_allocated()
scratch_buf = Buffer('CPU', scratch_size * wave_size, dtypes.uint8).ensure_allocated() if scratch_size else None
ctypes.memset(lds_buf._buf.va_addr, 0, max(lds_size, 4))
# NOTE: scratch (private/spill) memory is per-wavefront; buffers are allocated in the wave loop below
# Initialize SQTT encoder — emits packets inline as instructions execute (only when profiling)
if PROFILE:
@@ -2227,6 +2254,8 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
print(colored(msg, 'green') if len(_canonical_runner_cache) > prev_len else msg)
return program[pc]
if os.getenv("EMU_TRACE_INST") or os.getenv("EMU_WATCH_PC") is not None:
print(f"[emu-dispatch] gx={gx} gy={gy} gz={gz} lx={lx} ly={ly} lz={lz} scratch={scratch_size}", flush=True)
# Set DAZ+FTZ during emulator execution, restore afterward to avoid breaking hypothesis tests
# Only trace the first workgroup (like real HW traces one CU/SIMD), subsequent workgroups run but don't add to trace
tracing = bool(PROFILE)
@@ -2237,18 +2266,27 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
for gidx in range(gx):
# Initialize all wavefronts for this workgroup
waves: list[tuple[WaveState, list]] = []
wave_scratch_bufs: list[Buffer] = [] # keep alive for the dispatch
for wave_start in range(0, total_threads, wave_size):
st = _init_wave(lib, wave_start, total_threads, lx, ly, lz, args_ptr, rsrc2, scratch_size, arch, gidx, gidy, gidz, user_data,
wave_size)
# each wavefront owns its private (spill) scratch segment: on real HW the ring is indexed by
# (wave_id, lane), and scratch addresses here are lane*stride within the wave's segment.
if scratch_size:
wave_scratch_bufs.append(Buffer('CPU', scratch_size * wave_size, dtypes.uint8).ensure_allocated())
ctypes.memset(wave_scratch_bufs[-1]._buf.va_addr, 0, scratch_size * wave_size)
c_bufs = [ctypes.c_uint64(st.sgpr_buf._buf.va_addr), ctypes.c_uint64(st.vgpr_buf._buf.va_addr),
ctypes.c_uint64(vmem_buf._buf.va_addr), ctypes.c_uint64(lds_buf._buf.va_addr),
ctypes.c_uint64(scratch_buf._buf.va_addr if scratch_buf else 0),
ctypes.c_uint64(wave_scratch_bufs[-1]._buf.va_addr if scratch_size else 0),
ctypes.c_uint64(st.accvgpr_buf._buf.va_addr)]
waves.append((st, c_bufs))
# Execute wavefronts with barrier synchronization
# Each wave runs until it hits s_barrier or s_endpgm. When all waves have stopped, release barrier waves.
done = [False] * len(waves)
_exec_dumped: set = set()
_trace_on = bool(os.getenv("EMU_TRACE_INST")) and total_threads >= int(os.getenv("EMU_TRACE_MIN_THREADS", "1"))
if _trace_on: _inst_hist.clear()
for total_inst in range(10_000_000):
if all(done): break
for wi, (st, c_bufs) in enumerate(waves):
@@ -2259,9 +2297,23 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
if pc == ENDPGM_PC:
done[wi] = True
if tracing: sqtt_finish(wi)
if os.getenv("EMU_EXEC_DUMP") and wi not in _exec_dumped:
_exec_dumped.add(wi)
print(f"[exec-dump] gid=({gidx},{gidy},{gidz}) wave{wi} "
f"exec_lo={st._read_sgpr(EXEC_LO.offset):08x} exec_hi={st._read_sgpr(EXEC_LO.offset+1):08x}")
break
fxn, globals_list, is_barrier, inst = _ensure_compiled(pc)
if DEBUG >= 5: print(f" exec gid=({gidx},{gidy},{gidz}) w={wi} PC={pc - lib}: {inst!r}", flush=True)
if _trace_on:
key = (gidx, gidy, gidz, wi)
_inst_hist.setdefault(key, []).append((pc - lib, type(inst).__name__, getattr(inst, 'op', None) and inst.op.name))
wpc, wwave = os.getenv("EMU_WATCH_PC"), int(os.getenv("EMU_WATCH_WAVE", "0"))
if wpc is not None and (int(wpc) < 0 or (pc - lib) == int(wpc)) and wi == wwave and gidx == gidy == gidz == 0:
if int(wpc) < 0 and total_inst < 800: print(f"[watch-stream] pc={pc-lib} {inst!r}", flush=True)
for rv in os.getenv("EMU_WATCH_VGPR", "").split(","):
if rv: print(f"[watch pc={pc-lib}] wave{wi} {rv}:", [st._read_vgpr(int(rv[1:]), l) for l in range(8)], flush=True)
for rv in os.getenv("EMU_WATCH_SGPR", "").split(","):
if rv: print(f"[watch pc={pc-lib}] wave{wi} {rv}:", [st._read_sgpr(int(rv[1:]))], flush=True)
fxn(*[c_bufs[g] for g in globals_list])
if tracing:
inst_op = inst.op.value if hasattr(inst, 'op') else 0
@@ -2269,9 +2321,26 @@ def run_asm(lib: int, lib_sz: int, gx: int, gy: int, gz: int, lx: int, ly: int,
if is_barrier: break # s_barrier hit: PC already advanced past it, pause this wave
else: raise RuntimeError("exceeded 1M instructions in single wave, likely infinite loop")
# All waves have either hit barrier or endpgm — release barrier waves for next round
if os.getenv("EMU_DUMP_ROUNDS") and lds_size > 0 and total_inst < int(os.getenv("EMU_DUMP_ROUNDS")):
import numpy as _np
lds_words = _np.frombuffer((ctypes.c_uint32 * (lds_size//4)).from_address(lds_buf._buf.va_addr), dtype=_np.uint32)
nz = _np.argwhere(lds_words != 0)
print(f"[emu-dump] round={total_inst} lds nonzero words={len(nz)}",
(f"first16={[f'w{w}:0x{lds_words[w]:08x}' for w in nz[:16].flatten()]}" if len(nz) else ""), flush=True)
for wi2, (st2, _) in enumerate(waves):
v = _np.frombuffer((ctypes.c_uint32 * (256*st2.wave_size)).from_address(st2.vgpr_buf._buf.va_addr), dtype=_np.uint32)
if st2.wave_size == 64:
av = _np.frombuffer((ctypes.c_uint32 * (256*st2.wave_size)).from_address(st2.accvgpr_buf._buf.va_addr), dtype=_np.uint32)
else: av = _np.zeros(1, dtype=_np.uint32)
print(f"[emu-dump] wave{wi2} vgpr nonzero={int((v!=0).sum())} accvgpr nonzero={int((av!=0).sum())}", flush=True)
else: raise RuntimeError("exceeded 10M total scheduling rounds")
tracing = False # only trace the first workgroup
if _trace_on:
import pickle
tag = os.environ["EMU_TRACE_INST"]
with open(f"/tmp/emu_trace_{tag}.pkl", "wb") as f: pickle.dump(dict(_inst_hist), f)
os.environ.pop("EMU_TRACE_INST", None) # only dump for the first matching dispatch
# Reset LDS for next workgroup
if lds_size > 0: ctypes.memset(lds_buf._buf.va_addr, 0, max(lds_size, 4))
+1 -1
View File
@@ -91,7 +91,7 @@ class GPFIFO:
args_cnt, vals_cnt = const0[80], const0[81]
args_addr = qmd.constant_buffer_addr_lower_0 + (qmd.constant_buffer_addr_upper_0 << 32) + 0x160
args = to_mv(args_addr, args_cnt*8).cast('Q')
vals = to_mv(args_addr + args_cnt*8, vals_cnt*4).cast('I')
vals = to_mv(args_addr + args_cnt*8, vals_cnt*8).cast('Q')
cargs = [ctypes.cast(args[i], ctypes.c_void_p) for i in range(args_cnt)] + [ctypes.cast(vals[i], ctypes.c_void_p) for i in range(vals_cnt)]
gx, gy, gz = qmd.cta_raster_width, qmd.cta_raster_height, qmd.cta_raster_depth
lx, ly, lz = qmd.cta_thread_dimension0, qmd.cta_thread_dimension1, qmd.cta_thread_dimension2
+2 -2
View File
@@ -8,8 +8,8 @@ from tinygrad.codegen import to_program
class TestLinearizerFailures(unittest.TestCase):
def test_fail_1(self):
c0 = UOp.param(0, dtypes.float, (64,))
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.LOOP)
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.WEAK)
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.WEAK)
c3 = ((c1*UOp.const(dtypes.weakint, 32))+c2)
c4 = UOp.param(1, dtypes.float, (163840,))
c5 = UOp.range(UOp.const(dtypes.weakint, 2560), 0, AxisType.REDUCE)
+30 -4
View File
@@ -248,6 +248,32 @@ class TestUOpGraph(unittest.TestCase):
uops = to_uops_list([out])
self.assertEqual(len(uops), 2) # +1 for SINK
def test_coalesce_aliased_stores(self):
from tinygrad.codegen.late.coalesce import memory_coalescing
from tinygrad import Device
# two distinct stores to the same INDEX (e.g. a double-buffered LDS slot written in a
# prologue and a loop body) can't be merged: merging would drop one write
lbuf = UOp.placeholder((64,), dtypes.half, 0, AddrSpace.LOCAL)
r = UOp.range(4, 0, AxisType.LOOP)
gbuf = UOp.placeholder((64,), dtypes.half, 1, AddrSpace.GLOBAL)
s_a = lbuf.index(r*2).store(gbuf.index(r*2).load())
s_b = lbuf.index(r*2).store(gbuf.index(r*2+64).load()) # aliases s_a
s_c = lbuf.index(r*2+1).store(gbuf.index(r*2+32).load()) # adjacent, unique -> coalesceable with nothing
out = memory_coalescing(UOp.sink(s_a, s_b, s_c).end(r), Device["NULL"].renderer)
stores = [u for u in out.toposort() if u.op is Ops.STORE]
# both aliased stores survive (both datas preserved), nothing merged across them
self.assertEqual(len([u for u in stores if u.src[0].src[1] is not None and u.src[0].op is Ops.INDEX]), 3)
datas = sorted([u.src[1] for u in stores], key=str)
self.assertEqual(len(datas), 3)
def test_devectorize_derives_lane_dtype(self):
from tinygrad.codegen import do_devectorize
# an Invalid lane derives bool while the value lane derives float: the lane rebuild must derive, not inherit
lhs = UOp.stack(UOp.invalid(), UOp.const(None, 1.0).cast(dtypes.float))
out = do_devectorize(lhs * lhs)
invalid_lane_mul = next(u for u in out.src[0].toposort() if u.op is Ops.MUL)
self.assertIs(invalid_lane_mul.dtype, dtypes.bool)
@unittest.skip("this test isn't valid uops")
def test_noop_vectorize_fold(self):
d0 = UOp.param(0, dtypes.float, (1,))
@@ -424,8 +450,8 @@ class TestUOpGraph(unittest.TestCase):
# mnist indexing with split reduceop
# Make sure we are not doign math on the loaded index, which would promote it to long
c0 = UOp.param(0, dtypes.uchar, (128000,))
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.WEAK)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.WEAK)
c3 = UOp.param(1, dtypes.int, (512,))
c4 = c3.index(c1)
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
@@ -441,8 +467,8 @@ class TestUOpGraph(unittest.TestCase):
def test_load_idx_no_math_on_loaded(self):
# test the (x+y)<c pattern where x has loads - we shouldn't do math on loaded indices
c0 = UOp.param(0, dtypes.uchar, (128000,))
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.WEAK)
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.WEAK)
c3 = UOp.param(1, dtypes.int, (512,))
c4 = c3.index(c1) # c4 is a load
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
+6
View File
@@ -347,6 +347,12 @@ class TestSymbolic(unittest.TestCase):
def test_mul_lt(self):
self.helper_test_variable(Variable("a", 0, 5)*4 < 13, 0, 1, "(a<4)")
self.helper_test_variable(Variable("a", 0, 5)*4 < 16, 0, 1, "(a<4)")
self.helper_test_variable(Variable("a", -5, 5)*4 < -13, 0, 1, "(a<-3)")
self.helper_test_variable(Variable("a", -5, 5)*-4 < 13, 0, 1, "((a*-1)<4)")
c0, c1 = 2, 2**54+1
self.helper_test_variable(Variable("a", 0, c1)*c0 < c1, 0, 1, f"(a<{2**53+1})")
c0, c1 = -2, -(2**54-1)
self.helper_test_variable(Variable("a", 0, -c1)*c0 < c1, 0, 1, f"((a*-1)<{-(2**53-1)})")
self.helper_test_variable(Variable("a", 0, 5)*(-2) < 0, 0, 1, "((a*-1)<0)")
self.helper_test_variable(Variable("a", 0, 5)*4 >= 12, 0, 1, "((a<3)!=True)")
self.helper_test_variable(Variable("a", 0, 5)*4 >= 13, 0, 1, "((a<4)!=True)")
+15 -1
View File
@@ -47,7 +47,7 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
def _make_config(self, **kwargs):
return TransformerConfig(**({"num_blocks":1, "dim":4, "hidden_dim":8, "n_heads":1, "n_kv_heads":1,
"norm_eps":1e-5, "vocab_size":32, "head_dim":4, "rope_theta":10000.0,
"rope_dim":4, "v_head_dim":4, "max_context":4, "full_attention_interval":2,
"rope_dim":4, "v_head_dim":4, "max_context":4, "ssm_layers":(True,),
"ssm":SSMConfig(conv_kernel=2, state_size=2, group_count=1, time_step_rank=1, inner_size=2)} | kwargs))
def _make_block(self, config:TransformerConfig) -> GatedDeltaNetBlock:
@@ -176,6 +176,20 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
np.testing.assert_allclose(recurrent_state, expected_recurrent[step], rtol=1e-3, atol=1e-3,
err_msg=f"GatedDeltaNet reset recurrent cache mismatch at step {step}")
def test_kda_channel_decay(self):
config = self._make_config(n_heads=2, ssm=SSMConfig(conv_kernel=2, state_size=2, group_count=2, time_step_rank=2, inner_size=4, kda=True))
block, x = GatedDeltaNetBlock(config, config.ssm), Tensor([[[1., 2., 0., 0.]]])
# f_b(f_a(x)) = [1, 2, 3, 4]
block.ssm_f_a.weight = Tensor([[1., 0., 0., 0.], [0., 1., 0., 0.]])
block.ssm_f_b.weight = Tensor([[1., 0.], [0., 1.], [1., 1.], [2., 1.]])
block._init_state(x)
initial_state = Tensor.arange(8, dtype=dtypes.float32).reshape(1, 2, 2, 2)
block.recurrent_state.assign(initial_state).realize()
block.ssm_a = Tensor([[-1.], [-1.]])
block._attention(x, 0).realize()
alpha = np.exp(-self._softplus_np(np.arange(1, 5)).reshape(1, 2, 1, 2))
np.testing.assert_allclose(block.recurrent_state.numpy(), initial_state.numpy() * alpha, rtol=1e-5, atol=1e-5)
class TestPairwiseTopk(unittest.TestCase):
def test_basic_topk(self):
x = Tensor([[[1.0, 3.0, 2.0, 5.0, 4.0]]])
+9 -2
View File
@@ -79,8 +79,8 @@ class TestWeakPromotion(unittest.TestCase):
def test_weak_int_binop(self):
v = UOp.variable("i", 0, 10, dtypes.weakint)
self.assertEqual((v << 1).dtype, dtypes.weakint)
self.assertEqual(dtype_from_uop(Ops.SHL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.uint32, 1)), None), dtypes.int64)
self.assertEqual(UOp.const(dtypes.weakint, 1).alu(Ops.SHL, UOp.const(dtypes.uint8, 1)).dtype, dtypes.uint8)
self.assertEqual(dtype_from_uop(Ops.SHL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.uint32, 1)), None), dtypes.int8)
self.assertEqual(UOp.const(dtypes.weakint, 1).alu(Ops.SHL, UOp.const(dtypes.uint, 1)).dtype, dtypes.weakint)
self.assertEqual((v & 3).dtype, dtypes.weakint)
with self.assertRaises(RuntimeError): Tensor.const(dtypes.weakfloat, 1.0) << Tensor.const(dtypes.weakfloat, 1.0)
with self.assertRaises(RuntimeError): UOp.const(dtypes.int32, 1).alu(Ops.SHL, UOp.const(dtypes.float64, 1))
@@ -117,6 +117,13 @@ class TestWeakPromotion(unittest.TestCase):
t.realize()
self.assertNotIn(t.uop.buffer.dtype, dtypes.weaks)
def test_computed_float_index_lowers(self):
# a half-pixel nearest index resolves its float-scaled range before the gather
idx = (Tensor.arange(8) + 0.5) / 4 - 0.5
idx = (idx.clip(0, 1) - 0.5).ceil().int()
out = Tensor([0, 1], device="NULL")[idx].contiguous().realize()
self.assertNotIn(out.uop.buffer.dtype, dtypes.weaks)
class TestWeakStorageBoundary(unittest.TestCase):
# weak has no storage: a weak assignment source casts when it defers to the destination, everything else raises
+1 -1
View File
@@ -597,7 +597,7 @@ class TestShrinkMultiTensorShardedAxis(unittest.TestCase):
t = Tensor.arange(64).reshape(8, 8).clone().realize()
t.shard_([f"{Device.DEFAULT}:{i}" for i in range(4)], axis=0)
with self.assertRaises(AssertionError):
with self.assertRaises(RuntimeError):
# sharded axis shrink on non-device boundry is not allowed
a = t.shrink(((0, 3), (0, 8))).contiguous()
a.schedule_linear()
+1 -1
View File
@@ -83,7 +83,7 @@ def contiguous_mops_to_view(c:UOp, src:UOp):
resolved = graph_rewrite(src, multi_pm, name="multi_buffer_view")
if resolved.op is not Ops.UNSHARD: return None
if (view := _make_buffer_view(resolved.src[0])) is None: return None
return view.reshape(resolved.src[0].shape).unshard(resolved.arg, resolved.src[1]).contiguous(tag=c.tag)
return view.reshape(resolved.src[0].shape).unshard(resolved.arg, resolved.src[1:]).contiguous(tag=c.tag)
return None
+9 -5
View File
@@ -21,6 +21,7 @@ from tinygrad.codegen.late.coalesce import indexing_simplify
from tinygrad.codegen.opt.postrange import apply_opts
from tinygrad.codegen.late.gater import pm_move_gates_from_index
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
from tinygrad.schedule.multi import multi_pm
from tinygrad.schedule.rangeify import pm_mops
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
@@ -126,7 +127,7 @@ def do_devectorize(b:UOp):
src = []
for idx in itertools.product(*[range(x) for x in b.shape]):
idx_c = [UOp.const(None, i) for i in idx]
src.append(b.replace(src=tuple(x.base if x.base.arg is Invalid else x.index(*idx_c) for x in b.src)))
src.append(b.replace(dtype=None, src=tuple(x.base if x.base.arg is Invalid else x.index(*idx_c) for x in b.src)))
return UOp.stack(*src).reshape(b.shape) if b.op is not Ops.STORE else UOp.group(*src)
def do_stack_wmma(u:UOp):
@@ -261,13 +262,13 @@ def add_raw_barrier(after:UOp):
# loads from a LOCAL buffer that depend (via AFTER) on stores to LOCAL memory need a workgroup barrier
if after.addrspace is not AddrSpace.LOCAL: return None
# one toposort over all the deps
deps = UOp.sink(*after.src[1:]).backward_slice
if not any(_is_local_store(x) for x in deps) or any(x.op is Ops.BARRIER for x in deps): return None
deps = UOp.sink(*after.src[1:]).toposort(gate=lambda x: x.op is not Ops.BARRIER)
if not any(_is_local_store(x) for x in deps): return None
return after.src[0].after(UOp(Ops.BARRIER, src=after.src[1:]))
def add_war_barrier(end:UOp):
# a LOCAL buffer stored and loaded in the same loop needs a barrier at the end of the loop body
rngs = [r for r in end.src[1:] if r.op is Ops.RANGE and r.arg[1] in (AxisType.REDUCE, AxisType.LOOP) and r.vmax > 0]
rngs = [r for r in end.src[1:] if r.op is Ops.RANGE and r.arg[1] in (AxisType.REDUCE, AxisType.WEAK, AxisType.LOOP) and r.vmax > 0]
if not rngs or end.src[0].op is Ops.BARRIER: return None
sl = end.src[0].backward_slice_with_self
# only stores that are inside this loop body (not in the backward slice through AFTER chains from other loops)
@@ -286,8 +287,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
if DEBUG >= 5: print(pyrender(ast))
if SPEC: type_verify(ast, spec_tensor)
# resolve UNSHARDs (multi-device UNSHARDs are already resolved by the scheduler; this handles in-kernel shards, e.g. fragments)
sink = graph_rewrite(ast, multi_pm, name="multi_pm")
# preprocess
sink = graph_rewrite(ast, pm_mops, name="early movement ops", bottom_up=True)
sink = graph_rewrite(sink, pm_mops, name="early movement ops", bottom_up=True)
# first we optimize
if optimize:
+9 -6
View File
@@ -106,7 +106,9 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
# TODO: this should handle images too, it's just memory coalescing
if u.op in {Ops.LOAD, Ops.STORE}:
assert len(u.src) == (2 if u.op is Ops.STORE else 1), "memory coalescing does not support gated loads/stores"
assert u.src[0].op is Ops.INDEX, f"memory coalescing should be on INDEX, not {u.src[0].op}"
# movement-op-wrapped accesses (e.g. a REG placeholder store through a RESHAPE) aren't
# index-addressed; there's nothing to coalesce for them
if u.src[0].op is not Ops.INDEX: continue
buf, idx_u = u.src[0].src
if buf.addrspace == AddrSpace.REG: continue
idx, valid = idx_u.get_idx(), idx_u.get_valid()
@@ -137,8 +139,12 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
# TODO: a better way to get this than ctx
lengths = [8,4,2] if buf.dtype == dtypes.half and getenv("ALLOW_HALF8") else [4,2]
lengths.append(1) # worst case, it's not folded
# stores that alias (same buf+idx+valid from multiple store sites, e.g. a double-buffered LDS
# slot written in a prologue and a loop body) can't be merged: merging would drop one write.
# keep those scalar and only coalesce the unique ones.
keys = [k for k in sorted(offsets.keys()) if op is Ops.LOAD or len(offsets[k]) == 1]
# do the grouping
grouped_offsets = [[x for _,x in group] for _,group in itertools.groupby(enumerate(sorted(offsets.keys())), lambda x: x[1]-x[0])]
grouped_offsets = [[x for _,x in group] for _,group in itertools.groupby(enumerate(keys), lambda x: x[1]-x[0])]
for full_grp in grouped_offsets:
while len(full_grp):
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(None, full_grp[0])
@@ -148,10 +154,7 @@ def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
offset = offset.valid(valid) if valid is not None else offset
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(None, len(grp)))) if len(grp) > 1 else buf.index(offset)
if op == Ops.STORE:
datas = []
for i,g in enumerate(grp):
assert len(offsets[g]) == 1, f"attempting multiple stores: {len(offsets[g])}"
datas.append(offsets[g][0].src[1])
datas = [offsets[g][0].src[1] for g in grp]
store = idx.store(UOp.stack(*datas) if len(datas) > 1 else datas[0])
for i,g in enumerate(grp): replacements[offsets[g][0]] = store
else:
+2 -2
View File
@@ -169,7 +169,7 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
else:
# prioritize making expand axes local
local_axis_ranking = [(any(k.rngs[axis] not in b.src[1].get_idx().backward_slice for b in k.bufs), axis) \
for axis in k.axes_of(AxisType.GLOBAL, AxisType.LOOP) if k.rngs[axis].src[0].op is Ops.CONST]
for axis in k.axes_of(AxisType.GLOBAL, AxisType.WEAK) if k.rngs[axis].src[0].op is Ops.CONST]
to_local: list[tuple[int, int]] = []
for _, axis in sorted(local_axis_ranking, key=lambda x: (-x[0], -x[1])):
local_size = prod(sz for _, sz in to_local)
@@ -188,7 +188,7 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
for threads in [32,16,12,8,6,5,4,3,2]:
# Skip if too many threads. Heuristic: use about 128K ops per thread
if threads > k.ren.global_max[0] or resolve(prod(k.full_shape) // (128 << 10) < threads): continue
for axis in k.axes_of(AxisType.LOOP):
for axis in k.axes_of(AxisType.WEAK):
if k.full_shape[axis] % threads == 0:
try: k.apply_opt(Opt(OptOps.THREAD, axis, threads))
except KernelOptError: pass
+6 -6
View File
@@ -65,7 +65,7 @@ class Scheduler:
def _output_rngs(self) -> list[UOp]:
return flatten([[r for r in UOp.sink(*s.src[1:]).ranges if r.arg[-1] != AxisType.REDUCE] for s in self.ast.src if s.op is Ops.END])
def _globalizable_rngs(self) -> list[UOp]:
ret = [r for r in self._output_rngs() if r.arg[-1] == AxisType.LOOP]
ret = [r for r in self._output_rngs() if r.arg[-1] == AxisType.WEAK]
# exclude any output ranges from global that don't appear in all BUFFERIZE
for x in self.ast.toposort():
if x.op is Ops.STAGE:
@@ -86,8 +86,8 @@ class Scheduler:
ret = []
for x,r in zip(self.axis_types, self.rngs):
if self.dont_use_locals and x == AxisType.GLOBAL: ret.append("BLUE")
elif r not in output_rngs and x == AxisType.LOOP: ret.append("BLACK")
elif r not in globalizible_rngs and x == AxisType.LOOP: ret.append("white")
elif r not in output_rngs and x == AxisType.WEAK: ret.append("BLACK")
elif r not in globalizible_rngs and x == AxisType.WEAK: ret.append("white")
else: ret.append(axis_colors[x])
return ret
def colored_shape(self) -> str: return ' '.join([colored(f'{x.src[0].render():>4s}', color) for x,color in zip(self.rngs, self.colors())])
@@ -108,7 +108,7 @@ class Scheduler:
# copied from kernel.py
@property
def upcastable_dims(self) -> list[int]: return [i for i in self.axes_of(AxisType.GLOBAL, AxisType.LOCAL, AxisType.LOOP) \
def upcastable_dims(self) -> list[int]: return [i for i in self.axes_of(AxisType.GLOBAL, AxisType.LOCAL, AxisType.WEAK) \
if isinstance(s:=self.full_shape[i], int) and s > 1]
@property
def unrollable_dims(self) -> list[int]: return [i for i in self.axes_of(AxisType.GROUP_REDUCE, AxisType.REDUCE) \
@@ -161,10 +161,10 @@ class Scheduler:
check(rng.arg[-1] in {AxisType.GROUP_REDUCE, AxisType.REDUCE}, "unroll is for GROUP_REDUCE/REDUCE")
if opt.op is OptOps.UPCAST:
check((self.ren is not None and self.ren.target.device == "DSP") or amt <= 16, "don't upcast more than 16")
check(rng.arg[-1] in {AxisType.GLOBAL, AxisType.LOCAL, AxisType.LOOP}, f"upcast is for GLOBAL/LOCAL/LOOP, not {rng.arg[-1]}")
check(rng.arg[-1] in {AxisType.GLOBAL, AxisType.LOCAL, AxisType.WEAK}, f"upcast is for GLOBAL/LOCAL/LOOP, not {rng.arg[-1]}")
if opt.op is OptOps.LOCAL:
check(not self.dont_use_locals, "can't use locals")
check(rng.arg[-1] in {AxisType.GLOBAL, AxisType.LOOP}, "local is for globals")
check(rng.arg[-1] in {AxisType.GLOBAL, AxisType.WEAK}, "local is for globals")
if opt.op is OptOps.THREAD:
check(self.ren is not None and self.ren.has_threads, "target does not support threads")
check(self.ren is not None and self.ren.global_max is not None and amt <= self.ren.global_max[0], "too many threads")
+6 -2
View File
@@ -59,9 +59,13 @@ pm_simplify_ranges = PatternMatcher([
(UPat(Ops.SINK, name="x"), lambda ctx, x: do_substitute(ctx, x, lambda r,c: r.replace(src=(c,)))),
])
SPLITTABLE_TYPES = {AxisType.WEAK, AxisType.REDUCE, AxisType.LOOP}
def mark_range_mod(ctx:dict[UOp, UOp|None], r:UOp, c:UOp) -> None:
# ranges that aren't looped over can't be split
if r not in ctx and r.arg[-1] not in {AxisType.WARP, AxisType.DEVICE} \
# ranges that aren't looped over can't be split. ranges with hardware meaning are never split
# (LOCAL/WARP/THREAD/GLOBAL/GROUP_REDUCE/DEVICE map to launch dims; UPCAST/UNROLL are vector
# widths): splitting them scrambles the logical<->hardware mapping of hand-written kernels.
if r not in ctx and r.arg[-1] in SPLITTABLE_TYPES \
and r.src[0].op is Ops.CONST and r.src[0].divides(c.arg) is not None: ctx[r] = c
def do_substitute(ctx:dict, x: UOp, sub_fxn:Callable[[UOp, UOp], UOp]) -> UOp|None:
+7 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, Generic, TypeVar, Iterator, Generator, Self, TYPE_CHECKI
import importlib, inspect, functools, pathlib, os, contextlib, re, atexit, pickle, decimal
from tinygrad.helpers import LRU, getenv, diskcache_get, diskcache_put, DEBUG, GlobalCounters, PROFILE, temp, colored
from tinygrad.helpers import Context, CCACHE, ALLOW_DEVICE_USAGE, MAX_BUFFER_SIZE, cpu_events, ProfileEvent, ProfilePointEvent, suppress_finalizing
from tinygrad.helpers import select_by_name, select_first_inited, DEV, TracingKey, size_to_str, pluralize, Target, unwrap
from tinygrad.helpers import select_by_name, select_first_inited, DEV, TracingKey, size_to_str, pluralize, Target, unwrap, round_up
from tinygrad.dtype import DType, _to_np_dtype
if TYPE_CHECKING: from tinygrad.renderer import Renderer
@@ -319,6 +319,12 @@ class TinyELF:
# tuple of (name, slot, dtype, shape)
signature: tuple[tuple[str|None, int, DType, tuple], ...]
@staticmethod
def iter_sig(signature:tuple[tuple[str|None, int, DType, tuple], ...], offset:int=0) -> Generator[tuple[int, DType], None, None]:
for _,_,dt,_ in signature:
yield (offset:=round_up(offset, dt.itemsize)), dt
offset += dt.itemsize
class Program(Generic[DeviceType]):
def __init__(self, dev:DeviceType, obj:TinyELF): pass
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(),
+9 -2
View File
@@ -1,4 +1,4 @@
from typing import TypeVar, Generic, Callable, Any
from typing import TypeVar, Generic, Callable, Any, overload
import functools
from tinygrad.tensor import Tensor, all_tensors
from tinygrad.helpers import flatten, merge_dicts, DEBUG, Context, BEAM, getenv, JIT, JIT_BATCH_SIZE, dedup, pluralize, VIZ, disable_gc
@@ -219,7 +219,7 @@ def _prepare_jit_inputs(args, kwargs):
expected_input_info = [(x[0], tuple(sorted(x[1].keys(), key=lambda v: v.expr)), x[2], x[3]) for x in inputs]
return input_buf_uops, var_vals, names, expected_input_info
class TinyJit(Generic[ReturnType]):
class _TinyJit(Generic[ReturnType]):
def __init__(self, fxn:Callable[..., ReturnType]|None, captured:CapturedJit|None=None, prune=False):
assert fxn or captured, "need either a function or a CapturedJit"
self.fxn = fxn
@@ -287,3 +287,10 @@ class TinyJit(Generic[ReturnType]):
self.cnt += 1
return ret
# overload signatures support both @TinyJit and @TinyJit(prune=True) syntax
@overload
def TinyJit(fxn:Callable[..., ReturnType], *, prune:bool=False) -> _TinyJit[ReturnType]: ...
@overload
def TinyJit(fxn:None=None, *, prune:bool=False) -> Callable[[Callable[..., ReturnType]], _TinyJit[ReturnType]]: ...
def TinyJit(fxn=None, **kwargs): return (lambda f: _TinyJit(f, **kwargs)) if fxn is None else _TinyJit(fxn, **kwargs)
+3 -2
View File
@@ -41,9 +41,10 @@ class SimpleTokenizer:
# https://github.com/ggml-org/llama.cpp/blob/94933c8c2eeaa9a7983e3f6c08af76bd86724094/src/llama-vocab.cpp#L1818-L1820
vocab: typing.Iterable[tuple[str, int]] = ((tok, idx) for idx, tok in enumerate(kv["tokenizer.ggml.tokens"]))
normal_tokens, special_tokens = partition(vocab, lambda e: kv["tokenizer.ggml.token_type"][e[1]] == 1)
return SimpleTokenizer(dict(normal_tokens), dict(special_tokens), kv["tokenizer.ggml.pre"],
special_tokens_dict = dict(special_tokens)
return SimpleTokenizer(dict(normal_tokens), special_tokens_dict, kv["tokenizer.ggml.pre"],
bos_id=kv.get('tokenizer.ggml.bos_token_id') if kv.get('tokenizer.ggml.add_bos_token', True) else None,
eos_id=kv.get('tokenizer.ggml.eos_token_id', 0), eot_id=kv.get('tokenizer.ggml.eot_token_id'))
eos_id=kv.get('tokenizer.ggml.eos_token_id', 0), eot_id=kv.get('tokenizer.ggml.eot_token_id', special_tokens_dict.get('<|im_end|>')))
def _encode_word(self, word:bytes) -> list[int]:
if (early_token:=self._normal_tokens.get(word)) is not None: return [early_token]
+41 -15
View File
@@ -40,6 +40,7 @@ class SSMConfig:
group_count: int
time_step_rank: int
inner_size: int
kda: bool = False
@dataclass(frozen=True)
class TransformerConfig:
@@ -62,7 +63,7 @@ class TransformerConfig:
q_lora_rank: int = 0
kv_lora_rank: int = 0
shared_expert_dim: int = 0
full_attention_interval: int = 0
ssm_layers: tuple[bool, ...] = ()
attn_output_gate: bool = False
ssm: SSMConfig|None = None
shared_expert_gate: bool = True
@@ -211,13 +212,13 @@ class MLATransformerBlock(FFNBlock):
q_proj = self.attn_q_b(self.attn_q_a_norm(self.attn_q_a(x))) if self.config.q_lora_rank > 0 else self.attn_q(x)
q = q_proj.reshape(B, T, self.config.n_heads, self.config.head_dim).transpose(1, 2)
q_nope, q_rope = q[..., :q_nope_head_dim], q[..., q_nope_head_dim:]
q = (q_nope @ self.attn_k_b["weight"].transpose(-1, -2)).cat(apply_rope(q_rope, self.freqs_cis[start_pos:start_pos+T]), dim=-1)
if not self.config.ssm or not self.config.ssm.kda: q_rope = apply_rope(q_rope, self.freqs_cis[start_pos:start_pos+T])
q = (q_nope @ self.attn_k_b["weight"].transpose(-1, -2)).cat(q_rope, dim=-1)
kv_a = self.attn_kv_a_mqa(x)
c_kv = self.attn_kv_a_norm(kv_a[..., :self.config.kv_lora_rank])
k_rope = apply_rope(
kv_a[..., self.config.kv_lora_rank:].reshape(B, T, 1, self.config.rope_dim).transpose(1, 2),
self.freqs_cis[start_pos:start_pos+T])
k_rope = kv_a[..., self.config.kv_lora_rank:].reshape(B, T, 1, self.config.rope_dim).transpose(1, 2)
if not self.config.ssm or not self.config.ssm.kda: k_rope = apply_rope(k_rope, self.freqs_cis[start_pos:start_pos+T])
k_store = c_kv.reshape(B, 1, T, self.config.kv_lora_rank).cat(k_rope.reshape(B, 1, T, self.config.rope_dim), dim=-1)
k = Tensor(self.cache_k.uop.after(self.cache_k[:, :, start_pos:start_pos+T, :].uop.store(k_store.uop)))[:, :, 0:start_pos+T, :]
@@ -243,11 +244,17 @@ class GatedDeltaNetBlock(FFNBlock):
assert self.num_v_heads % self.num_k_heads == 0
self.head_v_dim, self.ssm_conv_kernel = ssm.inner_size // ssm.time_step_rank, ssm.conv_kernel
self.conv_channels, self.q_dim = ssm.inner_size + 2*ssm.group_count*ssm.state_size, ssm.state_size*ssm.group_count
self.attn_qkv, self.attn_gate = nn.Linear(config.dim, self.conv_channels, bias=False), nn.Linear(config.dim, ssm.inner_size, bias=False)
self.ssm_alpha, self.ssm_beta = nn.Linear(config.dim, self.num_v_heads, bias=False), nn.Linear(config.dim, self.num_v_heads, bias=False)
self.attn_qkv = nn.Linear(config.dim, self.conv_channels, bias=False)
if ssm.kda:
self.ssm_g_a, self.ssm_g_b = nn.Linear(config.dim, self.head_v_dim, bias=False), nn.Linear(self.head_v_dim, ssm.inner_size, bias=False)
self.ssm_f_a, self.ssm_f_b = nn.Linear(config.dim, self.head_k_dim, bias=False), nn.Linear(self.head_k_dim, ssm.inner_size, bias=False)
else:
self.attn_gate = nn.Linear(config.dim, ssm.inner_size, bias=False)
self.ssm_alpha = nn.Linear(config.dim, self.num_v_heads, bias=False)
self.ssm_beta = nn.Linear(config.dim, self.num_v_heads, bias=False)
self.ssm_conv1d = {"weight": Tensor.zeros(self.conv_channels, self.ssm_conv_kernel)}
self.ssm_dt = {"bias": Tensor.zeros(self.num_v_heads)}
self.ssm_a = Tensor.zeros(self.num_v_heads)
self.ssm_dt = {"bias": Tensor.zeros(ssm.inner_size if ssm.kda else self.num_v_heads)}
self.ssm_a = Tensor.zeros(self.num_v_heads, 1) if ssm.kda else Tensor.zeros(self.num_v_heads)
self.ssm_norm, self.ssm_out = nn.RMSNorm(self.head_v_dim, config.norm_eps), nn.Linear(ssm.inner_size, config.dim, bias=False)
def _attention(self, x:Tensor, start_pos:int|UOp) -> Tensor:
@@ -256,9 +263,12 @@ class GatedDeltaNetBlock(FFNBlock):
# input processing
x = x.half()
out_gate = self.attn_gate(x).reshape(B, 1, self.num_v_heads, self.head_v_dim)
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_alpha(x).float() + self.ssm_dt["bias"]).softplus() * self.ssm_a).reshape(B, self.num_v_heads, 1, 1).exp()
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)
# qkv conv
conv_window = self.conv_state.cat(self.attn_qkv(x), dim=1)
@@ -280,7 +290,8 @@ 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))
return self.ssm_out((core_attn_out * out_gate.silu()).reshape(B, 1, -1).cast(x.dtype))
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):
@@ -298,7 +309,8 @@ class Transformer:
dense_config = replace(config, num_experts=0, num_experts_per_tok=0, shared_expert_dim=0, hidden_dim=config.dense_hidden_dim or config.hidden_dim)
if config.ssm: config = replace(config, qk_norm=config.head_dim)
block_cls = MLATransformerBlock if config.kv_lora_rank > 0 else TransformerBlock
self.blk:list[FFNBlock] = [GatedDeltaNetBlock(config, config.ssm) if config.ssm and (i+1) % config.full_attention_interval != 0 else
self.blk:list[FFNBlock] = [GatedDeltaNetBlock(dense_config if i < config.leading_dense_blocks else config, config.ssm)
if config.ssm and config.ssm_layers[i] else
block_cls(dense_config if i < config.leading_dense_blocks else config) for i in range(config.num_blocks)]
self.token_embd = nn.Embedding(config.vocab_size, config.dim)
self.output_norm = nn.RMSNorm(config.dim, config.norm_eps)
@@ -337,8 +349,21 @@ class Transformer:
n_heads, n_kv_heads = kv[f'{arch}.attention.head_count'], kv[f'{arch}.attention.head_count_kv']
ssm = None
ssm_layers: tuple[bool, ...] = ()
if arch in ('qwen35', 'qwen35moe'):
ssm = SSMConfig(**{k: kv[f'{arch}.ssm.{k}'] for k in ('conv_kernel','state_size','group_count','time_step_rank','inner_size')})
ssm_layers = tuple((i+1) % kv[f'{arch}.full_attention_interval'] != 0 for i in range(kv[f'{arch}.block_count']))
elif arch == 'kimi-linear':
ssm_layers = tuple(x == 0 for x in n_kv_heads)
n_kv_heads = max(n_kv_heads)
ssm = SSMConfig(kv[f'{arch}.ssm.conv_kernel'], kv[f'{arch}.kda.head_dim'], n_heads, n_heads, n_heads*kv[f'{arch}.kda.head_dim'], kda=True)
for i, is_ssm in enumerate(ssm_layers):
if not is_ssm: continue
state_dict[f"blk.{i}.attn_qkv.weight"] = state_dict.pop(f"blk.{i}.attn_q.weight").cat(
state_dict.pop(f"blk.{i}.attn_k.weight"), state_dict.pop(f"blk.{i}.attn_v.weight"), dim=0).contiguous()
state_dict[f"blk.{i}.ssm_conv1d.weight"] = state_dict.pop(f"blk.{i}.ssm_conv1d_q.weight").cat(
state_dict.pop(f"blk.{i}.ssm_conv1d_k.weight"), state_dict.pop(f"blk.{i}.ssm_conv1d_v.weight"), dim=0).squeeze(1).contiguous()
state_dict[f"blk.{i}.ssm_out.weight"] = state_dict.pop(f"blk.{i}.attn_output.weight")
if arch in ('qwen35', 'qwen35moe', 'glm4moe'):
state_dict = {k.replace('post_attention_norm', 'ffn_norm'):v for k,v in state_dict.items()}
@@ -348,6 +373,7 @@ class Transformer:
# Permute RoPE weights from interleaved to half-split layout.
for name in state_dict:
if arch == 'kimi-linear': continue
if ('attn_q.weight' in name or 'attn_q_b.weight' in name) and (arch == 'llama' or kv_lora_rank):
w = state_dict[name].reshape(n_heads, state_dict[name].shape[0]//n_heads, -1)
prefix = head_dim-rope_dim
@@ -369,7 +395,7 @@ class Transformer:
max_context=max_context,
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')),
norm_topk_prob=kv.get(f'{arch}.expert_weights_norm', arch in ('qwen3moe', 'qwen35moe', 'kimi-linear')),
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(
@@ -378,7 +404,7 @@ class Transformer:
shared_expert_gate=f"blk.{kv.get(f'{arch}.leading_dense_block_count', 0)}.ffn_gate_inp_shexp.weight" in state_dict,
dense_hidden_dim=kv.get(f'{arch}.feed_forward_length', 0) if kv.get(f'{arch}.leading_dense_block_count', 0) else 0,
routed_scaling_factor=kv.get(f'{arch}.expert_weights_scale', 1.0), attn_output_gate=arch in ('qwen35', 'qwen35moe'), ssm=ssm,
full_attention_interval=kv.get(f'{arch}.full_attention_interval', 0),
ssm_layers=ssm_layers,
qkv_bias='blk.0.attn_q.bias' in state_dict,
expert_bias=f"blk.{kv.get(f'{arch}.leading_dense_block_count', 0)}.exp_probs_b.bias" in state_dict)
model = Transformer(config)
+1 -1
View File
@@ -337,7 +337,7 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
BLOCK_J = min(256, embed_size)
n_j_blocks = (embed_size + BLOCK_J - 1) // BLOCK_J
i = UOp.range(grad_emb_flat.shape[0], 0) # batch_size * sequence_length -> GLOBAL
j_inner = UOp.range(BLOCK_J, 2, AxisType.LOOP if device in ("CPU", "NULL") else AxisType.LOCAL) # BLOCK_J threads per workgroup
j_inner = UOp.range(BLOCK_J, 2, AxisType.WEAK if device in ("CPU", "NULL") else AxisType.LOCAL) # BLOCK_J threads per workgroup
j_outer = UOp.range(n_j_blocks, 1)
j = j_outer * BLOCK_J + j_inner
# mask padded embed
+3 -4
View File
@@ -35,10 +35,9 @@ def assemble_linear(prg:UOp, lin:UOp, arch:str) -> bytes:
elif val.offset < 106: max_sgpr = max(max_sgpr, val.offset + val.sz)
# ** scan sink for metadata
sink, n_bufs, n_vars, lds_size, gids = prg.src[0], 0, 0, 0, set()
sink, param_sizes, lds_size, gids = prg.src[0], {}, 0, set()
for u in sink.toposort():
if u.op is Ops.PARAM and u.addrspace is AddrSpace.ALU: n_vars += 1
elif u.op is Ops.PARAM: n_bufs += 1
if u.op is Ops.PARAM: param_sizes[u.arg.slot] = u.dtype.itemsize if u.addrspace is AddrSpace.ALU else 8
elif u.op is Ops.BUFFER and u.addrspace is AddrSpace.LOCAL: lds_size += u.max_numel() * u.dtype.itemsize
elif u.op is Ops.SPECIAL and u.arg.startswith("gidx"): gids.add(int(u.arg[-1]))
code_bytes = b"".join(inst.to_bytes() for inst in insts)
@@ -60,7 +59,7 @@ def assemble_linear(prg:UOp, lin:UOp, arch:str) -> bytes:
sgpr_granule = max(0, ceildiv(next_free_sgpr + 6, 8) - 1) if is_cdna else 0
desc = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t()
desc.group_segment_fixed_size = lds_size
desc.kernarg_size = n_bufs * 8 + n_vars * 4
for sz in (param_sizes[i] for i in sorted(param_sizes)): desc.kernarg_size = round_up(desc.kernarg_size, sz) + sz
desc.kernel_code_entry_byte_offset = -len(text)
# https://llvm.org/docs/AMDGPUUsage.html#amdgpu-amdhsa-compute-pgm-rsrc1-gfx6-gfx12-table
+49 -6
View File
@@ -123,7 +123,8 @@ class CStyleLanguage(Renderer):
smem_align: str = ""
smem_prefix: str = ""
smem_prefix_for_cast: bool = True
arg_int_prefix: str = "const int"
var_prefix: str = "const "
var_suffix: str = ""
barrier: str = ""
code_for_workitem: dict[Literal["g", "l", "i"], Callable] = {}
extra_args: list[str] = []
@@ -149,9 +150,9 @@ class CStyleLanguage(Renderer):
tmp = ""
if any(is_image_shape(u._shape) for _,(u,_) in bufs):
tmp = "const sampler_t smp = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP | CLK_FILTER_NEAREST;\n"
buftypes = [(name, ("volatile " if u.arg.volatile else "")+
self._render_dtype(u.dtype, sz=1, addrspace=u.addrspace, mutable=mutable, shape=u._shape)+self.buffer_suffix \
if u.addrspace == AddrSpace.GLOBAL else self.arg_int_prefix if u.dtype == dtypes.int else None) for name,(u,mutable) in bufs]
buftypes = [(name, ("volatile " if u.arg.volatile else "")+(self.var_prefix if u.addrspace == AddrSpace.ALU else "")+
self._render_dtype(u.dtype, sz=1, addrspace=u.addrspace, mutable=mutable, shape=u._shape)+
(self.var_suffix if u.addrspace == AddrSpace.ALU else self.buffer_suffix)) for name,(u,mutable) in bufs]
local_dims = [u.src[0] for u in uops if u.op is Ops.SPECIAL and u.arg[0] == "l"]
launch_bounds = prod([d.vmax for d in local_dims])
prg = ''.join([f"{self.kernel_typedef.format(launch_bounds=launch_bounds)} {function_name}(",] +
@@ -235,7 +236,7 @@ class CStyleLanguage(Renderer):
assert l is not None, f"failed to render {u.op} {u.dtype} {[(x.op,x.dtype) for x in u.src]} {u.arg}"
if u.op in {Ops.ENDIF, Ops.END}: depth -= 1
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK} or \
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
@@ -348,7 +349,8 @@ class MetalRenderer(CStyleLanguage):
kernel_typedef = "kernel void"
buffer_prefix = "device "
smem_prefix = "threadgroup __attribute__((aligned(16))) "
arg_int_prefix = "constant int&"
var_prefix = "constant "
var_suffix = "&"
barrier = "threadgroup_barrier(mem_flags::mem_threadgroup);"
float4 = "float4"
code_for_workitem = {"g": lambda x: f"gid.{chr(120+int(x))}", "l": lambda x: f"lid.{chr(120+int(x))}"}
@@ -472,6 +474,42 @@ class NVCCRenderer(CUDARenderer):
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})"
def _g2l_parts(u:UOp) -> tuple[UOp, UOp, UOp, UOp]|None:
"""STORE(local[li]) <- LOAD(global[gi]) (scalar INDEX or vec SHRINK): a global->shared copy expressible
as one buffer_load_lds (direct-to-LDS) instruction on gfx9.4+. Returns (buf, lidx, gbuf, gidx)."""
if u.op is not Ops.STORE or len(u.src) != 2: return None
li, ld = u.src
if li.op is Ops.INDEX and li.addrspace == AddrSpace.LOCAL and len(li.src) == 2: buf, idx = li.src
elif li.op is Ops.SHRINK and li.src[1].dtype is not None and li.src[0].addrspace == AddrSpace.LOCAL: buf, idx = li.src[0], li.src[1]
else: return None
if ld.op is not Ops.LOAD or len(ld.src) != 1: return None
gi = ld.src[0]
if gi.op is Ops.INDEX and gi.addrspace == AddrSpace.GLOBAL and len(gi.src) == 2: gbuf, gidx = gi.src
elif gi.op is Ops.SHRINK and gi.src[0].addrspace == AddrSpace.GLOBAL: gbuf, gidx = gi.src[0], gi.src[1]
else: return None
if li.dtype.scalar() != ld.dtype.scalar(): return None
return buf, idx, gbuf, gidx
def _g2l_match(u:UOp) -> bool: return _g2l_parts(u) is not None
def _render_g2l_lds(ctx, u:UOp) -> str|None:
if (parts := _g2l_parts(u)) is None: return None
buf, idx, gbuf, gidx = parts
sz = u.src[0].dtype.itemsize # whole copy size in bytes (16 for an 8xbf16 chunk)
esz = u.src[0].dtype.scalar().itemsize
return (f"llvm_amdgcn_raw_buffer_load_lds(make_srsrc_((void*){ctx[gbuf]}, {gbuf.max_numel()*gbuf.dtype.itemsize}), "
f"(as3_uint32_ptr)(&({ctx[buf]}[({ctx[idx]})])), {sz}, ((unsigned)({ctx[gidx]}))*{esz}U, 0, 0, 0);")
G2L_LDS_DECLS = [
"typedef int int32x4_t __attribute__((ext_vector_type(4)));",
"typedef __attribute__((address_space(3))) unsigned* as3_uint32_ptr;",
("extern __attribute__((device)) void\n"
"llvm_amdgcn_raw_buffer_load_lds(int32x4_t rsrc, as3_uint32_ptr lds_ptr, int size, int voffset, int soffset, int offset, int aux)\n"
' __asm("llvm.amdgcn.raw.buffer.load.lds");'),
"""static inline __attribute__((device)) int32x4_t make_srsrc_(const void* p, unsigned rb) {
int32x4_t r = {(int)(unsigned long)p, (int)(((unsigned long)p)>>32), (int)rb, 0x110000};
return r;\n}"""]
class HIPRenderer(CStyleLanguage):
shared_max = 65536
# NOTE: this is only really needed on gfx12, even though gfx11 reports the same limitation
@@ -489,6 +527,8 @@ class HIPRenderer(CStyleLanguage):
if not self.is_cdna4(target.arch): self.extra_matcher += pm_manual_bf16_cast
if self.is_cdna(target.arch):
self.string_rewrite = PatternMatcher([
# direct global->LDS copies (buffer_load_lds), skipping the register round-trip
(UPat(Ops.STORE, name="st"), lambda ctx,st: _render_g2l_lds(ctx, st) if getenv("HK_G2L") 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]]},"
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)"),
@@ -534,6 +574,9 @@ class HIPRenderer(CStyleLanguage):
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None) -> str:
prefix, ockl = [], []
g2l_used = any(_g2l_match(u) for u in uops) or \
any(u.op is Ops.CUSTOMI and isinstance(u.arg, str) and u.arg.startswith("llvm_amdgcn_raw_buffer_load_lds") for u in uops)
if self.is_cdna(self.target.arch) and g2l_used: prefix += G2L_LDS_DECLS
type_map = { dtypes.bfloat16: "bf16", dtypes.float: "f32", dtypes.half: "f16", dtypes.fp8e4m3: "_fp8_fp8", dtypes.fp8e5m2: "_bf8_bf8" }
used_dtypes = uops_to_dtypes(uops)
if any(u.op is Ops.CONST and not math.isfinite(u.arg) for u in uops):
+14 -10
View File
@@ -1,6 +1,6 @@
from typing import Callable, Any
from tinygrad.dtype import AddrSpace, DType, dtypes, truncate
from tinygrad.helpers import DEBUG, OSX, unwrap, fromimport, Target, is_image_shape
from tinygrad.helpers import DEBUG, OSX, unwrap, fromimport, Target, is_image_shape, round_up
from tinygrad.renderer import Renderer
from tinygrad.renderer.cstyle import CUDARenderer
from tinygrad.uop.ops import GroupOp, Ops, UOp, PatternMatcher, UPat, range_str
@@ -138,9 +138,9 @@ class NIRRenderer(Renderer):
# 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,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
(UPat.var("buf").index(UPat.var("idx_y"), UPat.var("idx_x")),
lambda buf,idx_y,idx_x: buf.index(idx_y.cast(dtypes.int), idx_x.cast(dtypes.int))),
# 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)))),
])
def_rewrite = PatternMatcher([
@@ -246,9 +246,11 @@ class NIRRenderer(Renderer):
def supported_dtypes(self): return {d for d in Renderer.supported_dtypes(self) if d not in dtypes.fp8s+(dtypes.bfloat16,)}
def padded_idx(param_idx:int, size:int): return round_up(param_idx, size) + size
class NAKRenderer(NIRRenderer):
param = nir_instr(nc=1, num_components=1, bs=lambda sz:sz*8, also=lambda self,sz: setattr(self, "param_idx", self.param_idx + sz),
intrins={"ALIGN_MUL":lambda sz:sz}, srcs=lambda self,b: [nsrc(nimm(b, 0, dtypes.int)), nsrc(nimm(b, self.param_idx, dtypes.int))])(
param = nir_instr(nc=1, num_components=1, bs=lambda sz:sz*8, also=lambda self,sz: setattr(self, "param_idx", padded_idx(self.param_idx, sz)),
intrins={"ALIGN_MUL":lambda sz:sz}, srcs=lambda self,b,sz: [nsrc(nimm(b,0,dtypes.int)), nsrc(nimm(b, round_up(self.param_idx,sz), dtypes.int))])(
lambda self, b, x, sz: mesa.nir_intrinsic_instr_create(b.shader, mesa.nir_intrinsic_ldc_nv))
def supported_dtypes(self): return {d for d in super().supported_dtypes() if (d != dtypes.half or int(self.target.arch[3:]) >= 53)}
@@ -263,12 +265,13 @@ class LVPRenderer(NIRRenderer):
code_for_op = {k:v for k,v in NIRRenderer.code_for_op.items() if k != Ops.EXP2}
param = nir_instr(nc=1, bs=lambda sz: sz * 8, num_components=1, intrins={"ALIGN_MUL":lambda sz: sz, "RANGE":lambda self: self.param_sz},
srcs=lambda b, self: [nsrc(nimm(b, 0, dtypes.int)), nsrc(nimm(b, self.param_idx, dtypes.int))], also=lambda self, sz:
setattr(self, "param_idx", self.param_idx+sz))(lambda self,b,x,sz: mesa.nir_intrinsic_instr_create(b.shader, mesa.nir_intrinsic_load_ubo))
srcs=lambda b,self,sz: [nsrc(nimm(b, 0, dtypes.int)), nsrc(nimm(b, round_up(self.param_idx, sz), dtypes.int))], also=lambda self, sz:
setattr(self, "param_idx", padded_idx(self.param_idx, sz)))(lambda self,b,x,sz:
mesa.nir_intrinsic_instr_create(b.shader, mesa.nir_intrinsic_load_ubo))
def prerender(self, uops:list[UOp]):
super().prerender(uops)
self.param_sz = sum([u.dtype.itemsize if u.addrspace is AddrSpace.ALU else 8 for u in uops if u.op is Ops.PARAM])
self.param_sz = functools.reduce(padded_idx, (u.element_size() if u.addrspace is AddrSpace.ALU else 8 for u in uops if u.op is Ops.PARAM), 0)
def tovec(b, idx_y, idx_x): return nalu(b, "vec4", idx_x, idx_y, nundef(b, dtypes.int), nundef(b, dtypes.int))
def nfloat(dtype): return mesa.nir_type_float16 if dtype == dtypes.half else mesa.nir_type_float32
@@ -306,7 +309,8 @@ class IR3Renderer(NIRRenderer):
super().prerender(uops)
self.texs:set[UOp] = set()
self.img_idx = 0
self.param_sz = sum([u.dtype.itemsize if u.addrspace is AddrSpace.ALU else 8 for u in uops if u.op is Ops.PARAM])
self.param_sz = functools.reduce(padded_idx, (u.element_size() if u.addrspace is AddrSpace.ALU else 8
for u in uops if u.op is Ops.PARAM and not is_image_shape(u._shape)), 0)
def postrender(self, uops:list[UOp]):
bufs = [u for u in uops if u.op is Ops.PARAM and u.addrspace is not AddrSpace.ALU]
+4
View File
@@ -145,6 +145,8 @@ class PTXRenderer(Renderer):
from tinygrad.runtime.support.compiler_cuda import NVPTXCompiler, PTXCompiler
self.compiler = (PTXCompiler if target.interface.startswith("MOCK") or target.device == "CUDA" else NVPTXCompiler)(target.arch)
self.tensor_cores = PTXRenderer.tc_sm80 if (ver:=int(target.arch[3:])) >= 80 else tc.cuda_sm75 if ver >= 75 else []
if ver < 80: self.extra_matcher += PatternMatcher([(UPat((Ops.MAX, Ops.EXP2), dtype=dtypes.half, name="x"),
lambda x: UOp(x.op, src=tuple(vv.cast(dtypes.float32) for vv in x.src), arg=x.arg).cast(dtypes.half))])
# language options
kernel_prefix = """.version VERSION
@@ -199,6 +201,8 @@ class PTXRenderer(Renderer):
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
if u.op is not Ops.LOAD and u.src[1].op is not Ops.CONST:
raise RuntimeError(f"PTX does not support dynamic register indexing: {u}")
r[u] = r[u.src[0]] if u.op is Ops.LOAD else r[u.src[0]][u.src[1].arg]
continue
if u.op is Ops.SPECIAL: r[u] = "%" + u.arg
+1 -1
View File
@@ -20,7 +20,7 @@ class CUDAGraph(MultiGraphRunner):
global_size, local_size = ast.arg.launch_dims({v: 0 for v in self.vars})
c_deps, new_node = self.new_node([b.base for b in bufs], ast.arg.outs)
c_args, vargs = encode_args([b._buf for b in bufs], [device_vars.get(x.expr, 0) for x in ast.arg.vars])
c_args, vargs = encode_args([b._buf for b in bufs], [device_vars.get(x.expr, 0) for x in ast.arg.vars], runtime.signature)
kern_params = cuda.CUDA_KERNEL_NODE_PARAMS_v1(runtime.prg, *global_size, *local_size, runtime.smem,
ctypes.cast(0, ctypes.POINTER(ctypes.c_void_p)), vargs)
check(cuda.cuGraphAddKernelNode(ctypes.byref(new_node), self.graph, c_deps, len(c_deps or []), ctypes.byref(kern_params)))
+12 -8
View File
@@ -1,7 +1,6 @@
from typing import Any, cast
import ctypes, decimal
from tinygrad.dtype import dtypes
from tinygrad.helpers import dedup, getenv, PROFILE
import ctypes, decimal, struct
from tinygrad.helpers import dedup, getenv, unwrap, PROFILE
from tinygrad.device import Buffer, Device, ProfileGraphEntry, ProfileGraphEvent
from tinygrad.uop.ops import UOp, Ops
from tinygrad.engine.jit import GraphRunner, GraphException
@@ -25,9 +24,12 @@ class MetalGraph(GraphRunner):
if self.icb.value is None: raise GraphException("create indirect command buffer failed, does your system support this?")
self.needs_icb_fix = int(not self.dev.arch.startswith("Apple") or int(self.dev.arch[5:]) < 9) # ICB fix not required on M3+ (Apple9+)
if len(self.vars): self.int_buf = self.dev.allocator.alloc(len(self.vars)*dtypes.int32.itemsize)
self.var_bind_data = []
if len(self.vars):
self.var_buf = self.dev.allocator.alloc(sum(dt.itemsize for r in self.runtimes for (_,_,dt,s) in unwrap(r).signature if s == ()))
self.var_buf_view, var_buf_offset = cast(MetalAllocator, self.dev.allocator)._as_buffer(self.var_buf), 0
all_pipelines, all_resources = [], [self.int_buf.buf] if len(self.vars) else []
all_pipelines, all_resources = [], [self.var_buf.buf] if len(self.vars) else []
for j, ((_, ast, bufs, _), runtime, replace) in enumerate(zip(self.calls, self.runtimes, self.uop_replace)):
assert runtime is not None
icb_command = self.icb.indirectComputeCommandAtIndex(j).retained()
@@ -37,7 +39,10 @@ class MetalGraph(GraphRunner):
if not any(pos == i for pos, _ in replace):
icb_command.setKernelBuffer_offset_atIndex(b._buf.buf, b._buf.offset, i)
all_resources.append(b._buf.buf)
for i, v in enumerate(ast.arg.vars): icb_command.setKernelBuffer_offset_atIndex(self.int_buf.buf, self.vars.index(v.expr)*4, len(bufs)+i)
for nm,i,dt,_ in runtime.signature[len(bufs):]:
icb_command.setKernelBuffer_offset_atIndex(self.var_buf.buf, var_buf_offset, i)
self.var_bind_data.append((nm, var_buf_offset, dt.fmt))
var_buf_offset += dt.itemsize
global_size, local_size = ast.arg.launch_dims({v: 0 for v in self.vars})
icb_command.concurrentDispatchThreadgroups_threadsPerThreadgroup(metal.MTLSize(*global_size), metal.MTLSize(*local_size))
icb_command.setBarrier()
@@ -45,7 +50,6 @@ class MetalGraph(GraphRunner):
self.all_resources = dedup(all_resources)
self.all_pipelines = dedup(all_pipelines)
self.command_buffer: Any = None
if len(self.vars): self.int_buf_view = cast(MetalAllocator, self.dev.allocator)._as_buffer(self.int_buf).cast('i')
self.range = metal.NSRange(0, len(self.calls))
self.updatable = sorted({j for j,r in enumerate(self.uop_replace) if r} | self.var_vals_replace.keys() | self.launch_dims_replace.keys())
@@ -66,7 +70,7 @@ class MetalGraph(GraphRunner):
for j, global_dims, local_dims in self.updated_launch_dims(var_vals):
self.icb.indirectComputeCommandAtIndex(j).concurrentDispatchThreadgroups_threadsPerThreadgroup(metal.MTLSize(*global_dims),
metal.MTLSize(*local_dims))
for i, var in enumerate(self.vars): self.int_buf_view[i] = var_vals[var]
for nm,ofs,fmt in self.var_bind_data: struct.pack_into(fmt, self.var_buf_view, ofs, var_vals[nm])
command_buffer = self.dev.mtl_queue.commandBuffer().retained()
encoder = command_buffer.computeCommandEncoder().retained()
+1 -2
View File
@@ -602,8 +602,7 @@ class AMDProgram(HCQProgram['AMDDevice']):
if dev.sqtt_enabled: self.libhash: tuple[int, int] = struct.unpack('<Q', hashlib.md5(self.lib).digest()[:8])*2
super().__init__(CLikeArgsState, self.dev, self.name, kernargs_alloc_size=self.kernargs_segment_size+additional_alloc_sz, lib=self.lib,
base=self.lib_gpu.va_addr)
super().__init__(CLikeArgsState, self.dev, obj, kernargs_alloc_size=self.kernargs_segment_size+additional_alloc_sz, base=self.lib_gpu.va_addr)
weakref.finalize(self, self._fini, self.dev, self.lib_gpu, buf_spec)
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int|None, ...]=(),
+1 -1
View File
@@ -55,7 +55,7 @@ class CLProgram(Program['CLDevice']):
def __call__(self, *bufs:cl.cl_mem, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]|None=None, vals:tuple[int, ...]=(),
wait=False, **kw) -> float|None:
for i, (_, slot, dt, shape) in enumerate(self.signature):
b = bufs[slot] if slot < len(bufs) else ctypes.c_int32(vals[slot-len(bufs)])
b = bufs[slot] if slot < len(bufs) else getattr(ctypes, f"c_int{dt.bitsize}")(vals[slot-len(bufs)])
if is_image_shape(shape):
pitch = (round_up(shape[1], 256) if OSX else shape[1]) * 4 * dt.itemsize
fmt = cl.cl_image_format(cl.CL_RGBA, {2:cl.CL_HALF_FLOAT, 4:cl.CL_FLOAT}[dt.itemsize])
+2 -2
View File
@@ -105,7 +105,7 @@ class CPUProgram(HCQProgram['CPUDevice']):
except OSError: pass
def __init__(self, dev:CPUDevice, obj:TinyELF):
self.runtimevars = {name:slot for name,slot,*_ in obj.signature if name == 'core_id'}
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
@@ -140,7 +140,7 @@ class CPUProgram(HCQProgram['CPUDevice']):
self.fxn = ctypes.CFUNCTYPE(None)(self.addr)
super().__init__(LVPArgsState if LVP else HCQArgsState, dev, obj.name, kernargs_alloc_size=12+256 if LVP else 0)
super().__init__(LVPArgsState if LVP else HCQArgsState, dev, obj, kernargs_alloc_size=12+256 if LVP else 0)
@suppress_finalizing
def __del__(self):
+6 -5
View File
@@ -15,9 +15,10 @@ def check(status):
error = ctypes.string_at(init_c_var(ctypes.POINTER(ctypes.c_char), lambda x: cuda.cuGetErrorString(status, ctypes.byref(x)))).decode()
raise RuntimeError(f"CUDA Error {status}, {error}")
def encode_args(args, vals) -> tuple[ctypes.Structure, ctypes.Array]:
c_args = init_c_struct_t(len(args) * 8 + len(vals) * 4, tuple([(f'f{i}', cuda.CUdeviceptr_v2, i*8) for i in range(len(args))] +
[(f'v{i}', ctypes.c_int, len(args)*8 + i*4) for i in range(len(vals))]))(*args, *vals)
def encode_args(args, vals, signature) -> tuple[ctypes.Structure, ctypes.Array]:
fields = ([(f'f{i}', cuda.CUdeviceptr_v2, i*8) for i in range(len(args))] +
[(f'v{i}', getattr(ctypes, f"c_int{dt.bitsize}"), off) for i,(off,dt) in enumerate(TinyELF.iter_sig(signature[len(args):], len(args)*8))])
c_args = init_c_struct_t(fields[-1][2] + ctypes.sizeof(fields[-1][1]) if len(fields) else 0, tuple(fields))(*args, *vals)
vargs = (ctypes.c_void_p * 5)(ctypes.c_void_p(1), ctypes.cast(ctypes.byref(c_args), ctypes.c_void_p), ctypes.c_void_p(2),
ctypes.cast(ctypes.pointer(ctypes.c_size_t(ctypes.sizeof(c_args))), ctypes.c_void_p), ctypes.c_void_p(0))
return c_args, vargs
@@ -35,7 +36,7 @@ def cu_time_execution(cb, enable=False) -> float|None:
class CUDAProgram(Program['CUDADevice']):
def __init__(self, dev:CUDADevice, obj:TinyELF, smem:int=0):
self.dev, self.name, self.lib, self.smem = dev, obj.name, obj.lib, smem
self.dev, self.name, self.lib, self.signature, self.smem = dev, obj.name, obj.lib, obj.signature, smem
if DEBUG >= 5: print("\n".join([f"{i+1:>3} {line}" for i, line in enumerate(pretty_ptx(obj.lib.decode('utf-8')).split("\n"))]))
check(cuda.cuCtxSetCurrent(self.dev.context))
@@ -54,7 +55,7 @@ class CUDAProgram(Program['CUDADevice']):
def __call__(self, *args, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
check(cuda.cuCtxSetCurrent(self.dev.context))
if not hasattr(self, "vargs"):
self.c_args, self.vargs = encode_args(args, vals)
self.c_args, self.vargs = encode_args(args, vals, self.signature)
# HACK: For MOCKGPU send the args struct itself.
if MOCKGPU: self.vargs = self.c_args # type: ignore[assignment]
+13 -9
View File
@@ -4,7 +4,7 @@ assert sys.platform != 'win32'
from tinygrad.device import BufferSpec, Compiled, Allocator, Compiler, Program, TinyELF
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.uop.ops import Ops, UOp
from tinygrad.helpers import getenv, round_up, mv_address, to_mv, cpu_objdump, system, DEBUG, suppress_finalizing, Target
from tinygrad.helpers import getenv, round_up, mv_address, to_mv, cpu_objdump, system, DEBUG, suppress_finalizing, Target, unwrap
from tinygrad.renderer.cstyle import ClangRenderer
from tinygrad.runtime.autogen import libc, qcom_dsp
if getenv("IOCTL"): import extra.dsp.run # noqa: F401 # pylint: disable=unused-import
@@ -38,7 +38,9 @@ class DSPRenderer(ClangRenderer):
'struct dcvs_v2_req req = {.type=7, .dcvs_enable=0, .set_latency=1, .latency=100, .set_dcvs_params=1, .target_corner = 6 /* TURBO */};',
'HAP_power_set((void*)handle, (void*)&req);']
msrc += ['if ((sc>>24) != 2) return 0;']
msrc += [f'int sz_or_val_{i} = ((int*)pra[0].buf.pv)[{i}];' for i,b in enumerate(bufs)]
msrc += [f'{self._render_dtype(b[1][0].dtype) if b[1][0].addrspace == AddrSpace.ALU else "int"} sz_or_val_{i} = '
f'*({self._render_dtype(b[1][0].dtype) if b[1][0].addrspace == AddrSpace.ALU else "int"}*)((char*)pra[0].buf.pv+{i*8});'
for i,b in enumerate(bufs)]
msrc += [f'int off{i} = ((int*)pra[1].buf.pv)[{i}];' for i,b in enumerate(bufs) if b[1][0].addrspace == AddrSpace.GLOBAL]
msrc += [f'void *buf_{i} = HAP_mmap(0,sz_or_val_{i},3,0,pra[{i+3}].dma.fd,0)+off{i};'
for i,b in enumerate(bufs) if b[1][0].addrspace == AddrSpace.GLOBAL]
@@ -64,15 +66,15 @@ def rpc_prep_args(ins=None, outs=None, in_fds=None):
return pra, fds, attrs, (ins, outs)
class DSPProgram(Program['DSPDevice']):
def __init__(self, dev:DSPDevice, obj:TinyELF):
self.dev, self.lib = dev, obj.lib
def __init__(self, dev:DSPDevice, obj:TinyELF): self.dev, self.lib, self.signature = dev, obj.lib, obj.signature
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
if len(bufs) >= 16: raise RuntimeError(f"Too many buffers to execute: {len(bufs)}")
pra, fds, attrs, _ = rpc_prep_args(ins=[var_vals_mv:=memoryview(bytearray((len(bufs)+len(vals))*4)), off_mv:=memoryview(bytearray(len(bufs)*4))],
pra, fds, attrs, _ = rpc_prep_args(ins=[var_vals_mv:=memoryview(bytearray((len(bufs)+len(vals))*8)), off_mv:=memoryview(bytearray(len(bufs)*4))],
outs=[timer:=memoryview(bytearray(8)).cast('Q')], in_fds=[b.share_info.fd for b in bufs])
var_vals_mv.cast('i')[:] = array.array('i', tuple(b.size for b in bufs) + vals)
for i,b in enumerate(bufs): struct.pack_into('i', var_vals_mv, i*8, b.size)
for i,(v,(_,_,dt,_)) in enumerate(zip(vals, self.signature[len(bufs):]), start=len(bufs)): struct.pack_into(unwrap(dt.fmt), var_vals_mv, i*8, v)
off_mv.cast('I')[:] = array.array('I', tuple(b.offset for b in bufs))
self.dev.exec_lib(self.lib, rpc_sc(method=2, ins=2, outs=1, fds=len(bufs)), pra, fds, attrs)
return timer[0] / 1e6
@@ -266,7 +268,7 @@ class MockDSPRenderer(DSPRenderer):
# for loop for big reads
msrc.append(f"void *buf{i} = mmap2(0, {sz}, 3, 0x21, -1, 0); for(int rd = 0; rd < {sz}; rd += read(0, buf{i}+rd, {sz}-rd));")
else:
msrc.append(f"unsigned int val{i}; read(0, &val{i}, 4);")
msrc.append(f"{self._render_dtype(b[1][0].dtype)} val{i}; read(0, &val{i}, {b[1][0].dtype.itemsize});")
msrc.append("unsigned int st = inscount();")
params = [(f'(void*)buf{i}' if b[1][0].addrspace == AddrSpace.GLOBAL else f'val{i}') for i,b in enumerate(bufs)]
msrc.append(f"{function_name}({', '.join(params)});")
@@ -277,14 +279,16 @@ class MockDSPRenderer(DSPRenderer):
return '\n'.join(msrc)
class MockDSPProgram(Program[DSPDevice]):
def __init__(self, dev:DSPDevice, obj:TinyELF): self.lib = obj.lib
def __init__(self, dev:DSPDevice, obj:TinyELF): self.lib, self.signature = obj.lib, obj.signature
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
with tempfile.NamedTemporaryFile(suffix=".out") as dsp_lib:
dsp_lib.write(self.lib)
dsp_lib.flush()
os.chmod(dsp_lib.name, 0o0777)
proc = subprocess.run(["qemu-hexagon-static", *(['-strace'] if DEBUG >= 5 else []), dsp_lib.name],
input=b''.join([bytes(to_mv(x.va_addr, x.size)) for x in bufs] + [struct.pack("I", x) for x in vals]), stdout=subprocess.PIPE, check=True)
input=b''.join([bytes(to_mv(x.va_addr, x.size)) for x in bufs] +
[struct.pack(unwrap(dt.fmt), x) for x,(_,_,dt,_) in zip(vals, self.signature[len(bufs):])]),
stdout=subprocess.PIPE, check=True)
offset = 4
for x in bufs:
to_mv(x.va_addr, x.size)[:] = proc.stdout[offset:offset+x.size]
+4 -3
View File
@@ -25,7 +25,7 @@ class HIPDevice(Compiled):
class HIPProgram(Program[HIPDevice]):
def __init__(self, dev:HIPDevice, obj:TinyELF):
self.dev, self.name, self.lib = dev, obj.name, obj.lib
self.dev, self.name, self.lib, self.signature = dev, obj.name, obj.lib, obj.signature
check(hip.hipSetDevice(self.dev.device_id))
self.module = init_c_var(hip.hipModule_t, lambda x: check(hip.hipModuleLoadData(ctypes.byref(x), obj.lib)))
self.prg = init_c_var(hip.hipFunction_t, lambda x: check(hip.hipModuleGetFunction(ctypes.byref(x), self.module, obj.name.encode("utf-8"))))
@@ -37,8 +37,9 @@ class HIPProgram(Program[HIPDevice]):
def __call__(self, *args, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int, ...]=(), wait=False, **kw):
check(hip.hipSetDevice(self.dev.device_id))
if not hasattr(self, "vargs"):
fields = [(f'f{i}', hip.hipDeviceptr_t, i*8) for i in range(len(args))] + [(f'v{i}', ctypes.c_int, len(args)*8+i*4) for i in range(len(vals))]
self.c_args = init_c_struct_t(len(args)*8+len(vals)*4, tuple(fields))(*args, *vals)
fields = ([(f'f{i}', hip.hipDeviceptr_t, i*8) for i in range(len(args))] +
[(f'v{i}', getattr(ctypes, f"c_int{dt.bitsize}"), o) for i,(o,dt) in enumerate(TinyELF.iter_sig(self.signature[len(args):], len(args)*8))])
self.c_args = init_c_struct_t(fields[-1][2] + ctypes.sizeof(fields[-1][1]) if len(fields) else 0, tuple(fields))(*args, *vals)
self.vargs = (ctypes.c_void_p * 5)(1, ctypes.cast(ctypes.byref(self.c_args), ctypes.c_void_p), 2,
ctypes.cast(ctypes.pointer(ctypes.c_size_t(ctypes.sizeof(self.c_args))), ctypes.c_void_p), 3)
+3 -2
View File
@@ -113,7 +113,7 @@ class MetalCompiler(Compiler):
class MetalProgram(Program[MetalDevice]):
def __init__(self, dev:MetalDevice, obj:TinyELF):
self.dev, self.name, self.lib = dev, obj.name, obj.lib
self.dev, self.name, self.lib, self.signature = dev, obj.name, obj.lib, obj.signature
data = objc.dispatch_data_create(obj.lib, len(obj.lib), None, None)
self.library = self.dev.sysdevice.newLibraryWithData_error(data, ctypes.byref(error_lib:=metal.NSError().retained())).retained()
error_check(error_lib)
@@ -138,7 +138,8 @@ class MetalProgram(Program[MetalDevice]):
encoder = command_buffer.computeCommandEncoder().retained()
encoder.setComputePipelineState(self.pipeline_state)
for i,a in enumerate(bufs): encoder.setBuffer_offset_atIndex(a.buf, a.offset, i)
for i,a in enumerate(vals, start=len(bufs)): encoder.setBytes_length_atIndex(bytes(ctypes.c_int(a)), 4, i)
for a,(_,i,dt,_) in zip(vals, self.signature[len(bufs):]):
encoder.setBytes_length_atIndex(bytes(getattr(ctypes, f"c_int{dt.bitsize}")(a)), dt.itemsize, i)
encoder.dispatchThreadgroups_threadsPerThreadgroup(metal.MTLSize(*global_size), metal.MTLSize(*local_size))
encoder.endEncoding()
command_buffer.setLabel(to_ns_str(self.name)) # TODO: is this always needed?
+5 -3
View File
@@ -240,8 +240,10 @@ class NVVideoQueue(NVCommandQueue):
class NVArgsState(CLikeArgsState):
def __init__(self, buf:HCQBuffer, prg:NVProgram, bufs:tuple[HCQBuffer, ...], vals:tuple[int, ...]=()):
if isinstance(prg.dev.iface, MOCKIface): prg.cbuf_0[80:82] = [len(bufs), len(vals)]
super().__init__(buf, prg, bufs, vals=vals, prefix=prg.cbuf_0 or None)
if (is_mock:=isinstance(prg.dev.iface, MOCKIface)): prg.cbuf_0[80:82] = [len(bufs), len(vals)]
super().__init__(buf, prg, bufs, vals=() if is_mock else vals, prefix=prg.cbuf_0 or None)
# mock expects all vars to be 64 bit
if is_mock and vals: self.bind_sints_to_buf(*vals, buf=self.buf, fmt='q', offset=len(prg.cbuf_0)*4 + len(bufs)*8)
class NVProgram(HCQProgram['NVDevice']):
def __init__(self, dev:NVDevice, obj:TinyELF):
@@ -314,7 +316,7 @@ class NVProgram(HCQProgram['NVDevice']):
self.max_threads = ((65536 // round_up(max(1, self.regs_usage) * 32, 256)) // 4) * 4 * 32
# NV's kernargs is constbuffer, then arguments to the kernel follows. Kernargs also appends QMD at the end of the kernel.
super().__init__(NVArgsState, self.dev, self.name, kernargs_alloc_size=round_up(self.constbufs[0][1], 1 << 8) + (8 << 8))
super().__init__(NVArgsState, self.dev, obj, kernargs_alloc_size=round_up(self.constbufs[0][1], 1 << 8) + (8 << 8))
weakref.finalize(self, self._fini, self.dev, self.lib_gpu, buf_spec)
def _parse_elf_info(self, sh, start_off=0):
+11 -7
View File
@@ -10,7 +10,7 @@ from tinygrad.renderer.cstyle import QCOMCLRenderer
from tinygrad.renderer.nir import IR3Renderer
from tinygrad.helpers import getenv, mv_address, to_mv, round_up, data64_le, ceildiv, prod, cpu_profile, lo32, suppress_finalizing, is_image_shape
from tinygrad.helpers import next_power2, flatten, PROFILE, IMAGE
from tinygrad.dtype import dtypes
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.runtime.support.system import System
if getenv("IOCTL"): import extra.qcom_gpu_driver.opencl_ioctl # noqa: F401 # pylint: disable=unused-import
@@ -20,7 +20,7 @@ BUFTYPE_BUF, BUFTYPE_TEX, BUFTYPE_IBO = 0, 1, 2
def dcache_flush():
from tinygrad.uop.ops import UOp, Ops, KernelInfo
from tinygrad.codegen import to_program
buf, n = UOp.param(0, dtypes.uint8, shape=(1,)), UOp.param(1, dtypes.int, shape=(1,), name="n", addrspace=None)
buf, n = UOp.param(0, dtypes.uint8, shape=(1,)), UOp.param(1, dtypes.int, shape=(), name="n", addrspace=AddrSpace.ALU)
i = UOp.range(n, 0, dtype=dtypes.int)
flush = UOp(Ops.CUSTOM, src=(buf.index(i * 64),), arg='__asm__ volatile("dc cvac, %0" :: "r"({0}) : "memory");')
sink = UOp.sink(flush.end(i), UOp(Ops.CUSTOM, arg='__asm__ volatile("dsb sy" ::: "memory");'), arg=KernelInfo(name="dcache_flush"))
@@ -154,13 +154,15 @@ class QCOMComputeQueue(HWQueue):
state_block=mesa.SB6_CS_SHADER, num_unit=1024 // 4),
*data64_le(args_state.buf.va_addr))
self.cmd(mesa.CP_LOAD_STATE6_FRAG, qreg.cp_load_state6_0(state_type=mesa.ST_SHADER, state_src=mesa.SS6_INDIRECT,
state_block=mesa.SB6_CS_SHADER, num_unit=round_up(prg.image_size, 128) // 128),
state_block=mesa.SB6_CS_SHADER, num_unit=ceildiv(prg.image_size, 128)),
*data64_le(prg.lib_gpu.va_addr))
self.reg(mesa.REG_A6XX_SP_REG_PROG_ID_0, 0xfcfcfcfc, 0xfcfcfcfc, 0xfcfcfcfc, 0xfc, qreg.a6xx_sp_cs_const_config(constlen=1024 // 4, enabled=True))
self.reg(mesa.REG_A6XX_SP_CS_PVT_MEM_STACK_OFFSET, qreg.a6xx_sp_cs_pvt_mem_stack_offset(prg.hw_stack_offset))
self.reg(mesa.REG_A6XX_SP_CS_INSTR_SIZE, qreg.a6xx_sp_cs_instr_size(prg.image_size // 4))
# image_size is in bytes, but INSTR_SIZE is measured in units of instruction groups (16 instructions, 8 bytes each)
# https://elixir.bootlin.com/mesa/mesa-26.1.5/source/src/freedreno/ir3/ir3_shader.h#L719-L723
self.reg(mesa.REG_A6XX_SP_CS_INSTR_SIZE, qreg.a6xx_sp_cs_instr_size(ceildiv(prg.image_size, 128)))
if prg.samp_cnt > 0:
self.cmd(mesa.CP_LOAD_STATE6_FRAG, qreg.cp_load_state6_0(state_type=mesa.ST_SHADER, state_src=mesa.SS6_INDIRECT,
@@ -210,10 +212,12 @@ class QCOMArgsState(HCQArgsState):
if prg.samp_cnt > 0: to_mv(int(self.buf.va_addr) + prg.samp_off, len(prg.samplers) * 4).cast('I')[:] = array.array('I', prg.samplers)
if prg.NIR:
self.bind_sints_to_buf(*[b.va_addr for b in ubos], buf=self.buf, fmt='Q', offset=prg.buf_off)
self.bind_sints_to_buf(*vals, buf=self.buf, fmt='I', offset=prg.buf_off + len(ubos) * 8)
for v,(o,dt) in zip(vals, TinyELF.iter_sig(prg.signature[len(bufs):], len(ubos)*8)):
self.bind_sints_to_buf(v, buf=self.buf, fmt=dt.fmt, offset=prg.buf_off + o)
else:
for i, b in enumerate(ubos): self.bind_sints_to_buf(b.va_addr, buf=self.buf, fmt='Q', offset=prg.buf_offs[i])
for i, v in enumerate(vals): self.bind_sints_to_buf(v, buf=self.buf, fmt='I', offset=prg.buf_offs[i+len(ubos)])
for i,(v,(_,_,dt,_)) in enumerate(zip(vals, prg.signature[len(bufs):])):
self.bind_sints_to_buf(v, buf=self.buf, fmt=dt.fmt, offset=prg.buf_offs[i+len(ubos)])
def _tex(b, ibo=False):
imgdt, shape, buf = b
@@ -265,7 +269,7 @@ class QCOMProgram(HCQProgram['QCOMDevice']):
dev._ensure_stack_size(self.hw_stack_offset * 4)
kernargs_alloc_size = round_up(2048 + (self.tex_cnt + self.ibo_cnt) * 0x40 + len(self.samplers) * 4, 0x100)
super().__init__(QCOMArgsState, self.dev, self.name, kernargs_alloc_size=kernargs_alloc_size)
super().__init__(QCOMArgsState, self.dev, obj, kernargs_alloc_size=kernargs_alloc_size)
weakref.finalize(self, self._fini, self.dev, self.lib_gpu, buf_spec)
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1),
+7 -6
View File
@@ -6,7 +6,7 @@ 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, 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
from tinygrad.device import Device, BufferSpec, Compiled, LRUAllocator, ProfileDeviceEvent, ProfileProgramEvent, Program, TinyELF
from tinygrad.uop.ops import sym_infer, sint, UOp
from tinygrad.runtime.autogen import libc
from tinygrad.runtime.support.memory import BumpAllocator
@@ -326,14 +326,15 @@ class CLikeArgsState(HCQArgsState[ProgramType]):
if prefix is not None: self.buf.cpu_view().view(size=len(prefix) * 4, fmt='I')[:] = array.array('I', prefix)
self.bind_sints_to_buf(*[b.va_addr for b in bufs], buf=self.buf, fmt='Q', offset=len(prefix or []) * 4)
assert None not in vals
self.bind_sints_to_buf(*cast(tuple[sint, ...], vals), buf=self.buf, fmt='I', offset=len(prefix or []) * 4 + len(bufs) * 8)
for v,(val_offset,dt) in zip(vals, TinyELF.iter_sig(prg.signature[-len(vals):], len(bufs) * 8)):
assert v is not None
self.bind_sints_to_buf(v, buf=self.buf, fmt=dt.fmt, offset=len(prefix or []) * 4 + val_offset)
class HCQProgram(Program[HCQDeviceType]):
def __init__(self, args_state_t:Type[HCQArgsState], dev:HCQDeviceType, name:str, kernargs_alloc_size:int, lib:bytes|None=None, base:int|None=None):
self.args_state_t, self.dev, self.name, self.kernargs_alloc_size = args_state_t, dev, name, kernargs_alloc_size
def __init__(self, args_state_t:Type[HCQArgsState], dev:HCQDeviceType, obj:TinyELF, kernargs_alloc_size:int, base:int|None=None):
self.args_state_t, self.dev, self.name, self.signature, self.kernargs_alloc_size = args_state_t, dev, obj.name, obj.signature, kernargs_alloc_size
self.prof_prg_counter = next(self.dev.prof_prg_counter)
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, name, lib, base, self.prof_prg_counter)]
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, obj.name, obj.lib, base, self.prof_prg_counter)]
@staticmethod
def _fini(dev, buf, spec): dev.allocator.free(buf, buf.size, spec)
+45 -45
View File
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import cast, Callable, TypeVar, Generic, Any
from typing import cast, Callable, TypeVar, Generic, Any, Sequence
import struct, functools, time, collections, itertools
from dataclasses import replace, dataclass
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap
@@ -41,8 +41,9 @@ 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,)
def make_patch(buf:UOp, off:sint, val:UOp) -> UOp:
return buf.index(UOp.const(dtypes.int, off // buf.dtype.itemsize)).store(val.simplify().cast(buf.dtype))
def make_patches(buf:UOp, patches:Sequence[tuple[sint, UOp]]) -> UOp:
return buf.index(UOp.stack(*(UOp.const(dtypes.int, off // buf.dtype.itemsize) for off,_ in patches))) \
.store(UOp.stack(*(val.simplify().cast(buf.dtype) for _,val in patches)))
def make_binary_patch(buf:UOp, blob:bytes) -> UOp:
data = UOp(Ops.BINARY, src=(), arg=blob).bitcast(buf.dtype)
@@ -56,7 +57,7 @@ def make_cmdbuf(lin, devs, buf:UOp|None=None, dep:UOp|None=None):
blob += struct.pack(f'<{ssimp.dtype.fmt}', ssimp.arg if ssimp.op is Ops.CONST else 0x0)
cmdbuf = buf if buf is not None else UOp.placeholder((len(blob) // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("cmdbuf")
writable = cmdbuf.after(dep) if dep is not None else cmdbuf
return cmdbuf.after(make_binary_patch(writable, blob), *[make_patch(writable, off, s) for off, s in patches])
return cmdbuf.after(make_binary_patch(writable, blob), *((make_patches(writable, patches),) if patches else ()))
def make_signal(devs, queue="COMPUTE:0", sentinel=False):
return UOp.placeholder((1,), dtypes.uint64, 0, device=devs, volatile=True).rtag("sentinel_signal" if sentinel else f"{queue}_timeline_signal")
@@ -70,7 +71,7 @@ def encode_kernargs_clike(call:UOp, prg:UOp, devs:str|tuple[str, ...]) -> UOp:
data, info = prg.arg
buf = UOp.placeholder((data.kernargs_alloc_size // 4,), dtypes.uint32, next(UOp.unique_num), device=devs).rtag("kernargs")
words = [w for gi in info.globals for w in data64_le(get_call_arg_uops(call)[gi].getaddr(devs))] + list(info.vars)
return buf.after(*[make_patch(buf, i * 4, w) for i, w in enumerate(words)])
return buf.after(*((make_patches(buf, [(i * 4, w) for i, w in enumerate(words)]),) if words else ()))
# *****************
# 0.1. prep: replace buffers with params
@@ -195,7 +196,6 @@ def sched_hcq_batches(l:UOp) -> UOp:
if (devs:=next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)) is not None: batch.append((call, to_tuple(devs)))
else: srcs, batch = srcs + _finalize_batch(batch) + [call], []
return l.replace(src=tuple(srcs + _finalize_batch(batch)))
pm_sched_hcq_batches = PatternMatcher([(UPat(Ops.LINEAR, name="l"), sched_hcq_batches)])
# *****************
# 3. merge into queues
@@ -227,7 +227,10 @@ def merge_queues(linear:UOp) -> UOp:
new_rec = [call]
opened_qs[(info.device, info.queue)] = new_rec
return linear.replace(src=tuple(new_src + [_merged_hcq_call(c) for c in opened_qs.values()]))
pm_merge_queues = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), merge_queues)])
def schedule_and_merge(ctx:dict[UOp, UOp], linear:UOp) -> UOp:
return merge_queues(sched_hcq_batches(linear).substitute(ctx, walk=True, enter_calls=True))
pm_schedule_and_merge = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), schedule_and_merge)])
# *****************
# 4.2. hcq lowering: ops to ir
@@ -280,7 +283,7 @@ def make_addr_table(call:UOp, gaddrs:list[UOp], name:str) -> tuple[dict[UOp, UOp
table = UOp.placeholder((len(order),), dtypes.uint64, next(UOp.unique_num), device=call.arg.aux.device).rtag(name)
reads = {g: table.after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ()).index(UOp.const(dtypes.int, slots[bare[g]])).load() for g in gaddrs}
return reads, (table.after(*[make_patch(table, i * table.dtype.itemsize, addr) for addr, i in slots.items()]),) if slots else ()
return reads, (table.after(make_patches(table, [(i * table.dtype.itemsize, addr) for addr, i in slots.items()])),) if slots else ()
def make_blob_bufs(call:UOp, blobs:list[UOp]) -> tuple[dict[UOp, UOp], tuple[UOp, ...]]:
bufs = {b: UOp.placeholder((b.max_numel(),), b.dtype, next(UOp.unique_num), device=call.arg.aux.device).rtag("template") for b in blobs}
@@ -298,7 +301,8 @@ def rm_rt_uops(call:UOp) -> UOp|None:
reads, fills = reads | {k:v for r,_ in tables for k,v in r.items()}, [f for _,fs in tables for f in fs]
return call.replace(src=(call.src[0].substitute(reads), *call.src[1:], *fills),
arg=replace(call.arg, aux=replace(call.arg.aux, input_idxs=tuple(sorted(dedup(g.buf_uop.arg.slot for g in inputs))))))
pm_rm_rt_uops = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), rm_rt_uops)])
pm_rm_rt_uops = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), rm_rt_uops)])
# *****************
@@ -318,8 +322,10 @@ def replace_params(call:UOp) -> UOp|None:
sub = {(b:=u.without_after): UOp.param(i, u.dtype, shape=b.shape, device=u.device, volatile=b.op is Ops.PARAM and b.arg.volatile)
for i,u in enumerate(c_args)} | {v: v.replace(arg=replace(v.arg, slot=-1)) for v in variables if v.op is Ops.PARAM}
info = replace(call.arg.aux, inputs=next((i for i,u in enumerate(c_args) if u.tag == "inputs"), None))
return call.replace(src=(body.substitute(sub), *c_args, *refhold), arg=replace(call.arg, aux=info)) # TODO: call.after(*refhold)?
pm_replace_params = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), replace_params)])
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)])
# *****************
@@ -356,36 +362,34 @@ pm_pack_placeholders = PatternMatcher([
# *****************
# 8. callify hcq programs
pm_callify_hcq = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="hcq", src=(UPat(Ops.SINK),), name="cf"),
lambda cf: cf.replace(src=(to_program(cf.src[0].replace(arg=KernelInfo("hcq_submit"), tag=1), Device[HCQ_RUNTIME_DEV.value].renderer),)))])
def callify_hcq(call:UOp, cf:UOp) -> UOp:
prg = to_program(cf.src[0].replace(arg=KernelInfo("hcq_submit"), tag=1), Device[HCQ_RUNTIME_DEV.value].renderer)
return call.replace(src=(cf.replace(src=(prg,), arg="hcq"), *call.src[1:]))
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)])
hcq_compile_cache:dict[tuple[bytes, bool], UOp] = {}
@track_rewrites(lambda linear,input_uops,jit,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
def hcq_compile(linear:UOp, input_uops:list[UOp]|None=None, jit=False) -> UOp:
if input_uops is not None: linear = graph_rewrite(linear, pm_replace_buffers, ctx=input_uops, walk=True, enter_calls=True, name="replace buffer")
if input_uops is not None: linear = graph_rewrite(linear, pm_replace_buffers, ctx=input_uops, walk=True, name="replace buffer")
if (final_linear:=(hcq_compile_cache.get(cache_key:=(linear.key, jit)))) is None:
# prep
linear = linear.substitute(back_map:={s.param_like(i): s for i,s in enumerate(input_uops)} if input_uops is not None else {}, walk=True)
linear = graph_rewrite(linear, pm_insert_copy_staging + pm_flatten_linear, name="insert copy staging")
linear = graph_rewrite(linear, pm_insert_copy_staging+pm_flatten_linear, name="insert copy staging")
# schedule
linear = graph_rewrite(linear, pm_sched_hcq_batches, walk=True, name="schedule hcq batches")
linear = linear.substitute({s: p for p, s in back_map.items()}, walk=True, enter_calls=True)
linear = graph_rewrite(linear, pm_merge_queues, walk=True, name="merge queues")
linear = graph_rewrite(linear, pm_schedule_and_merge, ctx={s:p for p,s in back_map.items()}, walk=True, name="schedule and merge hcq")
# lowering to hcq ir
linear = graph_rewrite(linear, pm_encode_cmdbufs, walk=True, name="encode cmdbufs", enter_calls=True)
linear = graph_rewrite(linear, pm_pack_placeholders, walk=True, name="pack placeholders")
linear = graph_rewrite(linear, pm_encode_cmdbufs+pm_pack_placeholders, walk=True, name="encode and pack", enter_calls=True)
# pie
linear = graph_rewrite(linear, pm_split_patches, ctx=jit, walk=True, name="split rt/lt patches")
linear = graph_rewrite(linear, pm_early_simplify + symbolic, bottom_up=False, name="simplify packed placeholders", enter_calls=True)
linear = graph_rewrite(linear, pm_rm_rt_uops, walk=True, name="replace rt uops")
linear = graph_rewrite(linear, pm_replace_params, walk=True, name="replace with args")
# patches
linear = graph_rewrite(linear, pm_split_patches+pm_early_simplify+symbolic, ctx=jit, bottom_up=False, name="simplify patches", enter_calls=True)
# and compile it
linear = graph_rewrite(linear, pm_replace_params, bpm=pm_rm_rt_uops, name="replace rt uops and params")
final_linear = hcq_compile_cache[cache_key] = graph_rewrite(linear, pm_callify_hcq, name="callify hcq", enter_calls=True)
return final_linear
@@ -411,9 +415,10 @@ def fold_binary(buf:UOp, blob:UOp) -> UOp:
return UOp(Ops.NOOP)
def fold_const_store(buf:UOp, off:UOp, val:UOp) -> UOp:
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.arg))
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(byte_off:=off.arg*buf.dtype.itemsize):byte_off+len(data)] = data
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.arg))
b.ensure_allocated().as_memoryview(force_zero_copy=True, no_sync=True).cast('B')[(bo:=off.arg*buf.dtype.itemsize):bo+len(data)] = data
return UOp(Ops.NOOP)
def resolve_getaddr(buf:UOp, g:UOp) -> UOp:
@@ -437,31 +442,26 @@ 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.cvar("off"))
.store(UPat.any(UPat.cvar("val"), UPat(Ops.STACK, name="val"))), fold_const_store),
(UPat({Ops.BUFFER, Ops.SLICE, 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}"))])
linked_buf_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
linked_linear_cache:dict[tuple[bytes, bool], UOp] = {}
def linked_buf_key(a:UOp): return a.key, to_tuple(a.device)
pm_linked_bufs = PatternMatcher([(UPat(Ops.AFTER, name="a"), lambda a: linked_buf_cache.get(linked_buf_key(a)))])
def link_buf_key(a:UOp): return a.key, to_tuple(a.device)
link_buf_cache:dict[tuple[bytes, tuple[str, ...]], UOp] = {}
link_linear_cache:dict[tuple[bytes, bool], UOp] = {}
@track_rewrites(lambda _,jit,cache,ret: f"HCQ Link {pluralize('Kernel', len(ret.src))}")
def hcq_link(linear:UOp, jit=False, cache=True) -> UOp:
if (linked:=linked_linear_cache.get(linear_key:=(linear.key, jit))) is not None: return linked
if (linked:=link_linear_cache.get(linear_key:=(linear.key, jit))) is not None: return linked
cacheable = {(j,i):a for j,c in enumerate(linear.src) for i,a in enumerate(c.src[1:], 1)
if a.op is Ops.AFTER and unwrap_mstack(a.src[0])[0].tag in HCQ_CACHE_TAGS}
hits = {a.src[0]:linked_buf_cache[key] for a in cacheable.values() if (key:=linked_buf_key(a)) in linked_buf_cache}
linear = graph_rewrite(linear, pm_linked_bufs, name="reuse linked bufs").substitute(hits, walk=True)
linear = graph_rewrite(linear, pm_bufferize, ctx=cache, bottom_up=True, walk=True, name="bufferize placeholders")
linear = graph_rewrite(linear, pm_resolve_patches + symbolic, bottom_up=False, name="simplify patches")
linear = graph_rewrite(linear, pm_assert_no_afters, name="assert no afters")
for (j,i),a in cacheable.items(): linked_buf_cache.setdefault(linked_buf_key(a), linear.src[j].src[i])
if cache: linked_linear_cache[linear_key] = linear
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,
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
return linear
# *****************
+1 -1
View File
@@ -50,7 +50,7 @@ class IndexingContext:
# create ranges
range_idx: Iterator[int] = field(default_factory=itertools.count)
def new_range(self, s:sint, axistype:AxisType=AxisType.LOOP) -> UOp:
def new_range(self, s:sint, axistype:AxisType=AxisType.WEAK) -> UOp:
if isinstance(s, UOp) and s.op is Ops.RANGE: return s
# if a range has a 1 src, it's the same as UOp.const(dtypes.weakint, 0)
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(None, 0)
+142 -42
View File
@@ -1,5 +1,6 @@
from tinygrad.helpers import all_same, prod, getenv, ALLREDUCE_CAST
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, AxisType, graph_rewrite, broadcast_axes, _broadcast_shape
from tinygrad.uop.ops import Ops, UOp, PatternMatcher, UPat, GroupOp, AxisType, graph_rewrite, broadcast_axes, _broadcast_shape, sint_to_uop
from tinygrad.uop.ops import sint, ssimplify
from tinygrad.dtype import dtypes
from tinygrad.schedule.allreduce import handle_allreduce
@@ -50,7 +51,12 @@ def shard_srcs(msrcs:tuple[UOp, ...], axis:int) -> list[UOp]:
# normalize srcs to local shards on axis
devices = [x.device for x in msrcs if x.device is not None]
assert all_same(devices), f"all buffers must have the same device {devices}"
dcount = len(devices[0])
# without devices the sharding range comes from the UNSHARD itself (e.g. a LOCAL thread range);
# device shards range over the devices instead
if len(devices): sharding_rng = UOp.range(len(devices[0]), -1, AxisType.DEVICE)
else:
sharding_rng = next((m.src[1] for m in msrcs if m.op is Ops.UNSHARD), None)
assert sharding_rng is not None, "shard_srcs requires a device or a sharding range"
out_shape = _broadcast_shape(*[x.shape for x in msrcs])
srcs:list[UOp] = []
@@ -60,12 +66,19 @@ def shard_srcs(msrcs:tuple[UOp, ...], axis:int) -> list[UOp]:
# same axis, just copy through
srcs.append(mlb.src[0])
else:
# otherwise every device gets the full copy, sharded iff this src has the axis (broadcast srcs stay whole)
# otherwise every shard gets the full copy, sharded iff this src has the axis (broadcast srcs stay whole)
full = mlb if mlb.axis is None else copy_multi(mlb, mlb.device)
srcs.append(full if axis in broadcast_axes(mlb.shape, out_shape) else full._shard(src_axis, dcount))
srcs.append(full if axis in broadcast_axes(mlb.shape, out_shape) else full._shard(src_axis, sharding_rng))
return srcs
def alu_multi(root:UOp):
multis = [m for m in root.src if m.op is Ops.UNSHARD]
if not multis: return None
sharding = multis[0].sharding
if len(multis) == len(root.src) and all(m.sharding == sharding for m in multis):
srcs = [m.src[0] for m in root.src]
return srcs[0].alu(root.op, *srcs[1:]).unshard(multis[0].arg, multis[0].src[1:])
# resharding: single-axis fallback via shard_srcs
axis = root.axis
assert axis is not None
srcs = shard_srcs(root.src, axis)
@@ -73,69 +86,156 @@ def alu_multi(root:UOp):
def reduce_multi(root:UOp, multi:UOp):
op, num_axes = root.arg
if multi.axis is not None and multi.axis < num_axes:
local = multi.src[0]._rop(op, tuple(range(num_axes)))
# allreduce in pre-cast dtype when sum_acc_dtype promoted from bf16/half
sharding = multi.sharding
reduced = [(ax, rng) for ax, rng in sharding if ax < num_axes]
remaining = [(ax, rng) for ax, rng in sharding if ax >= num_axes]
local = multi.src[0]._rop(op, tuple(range(num_axes)))
if reduced:
assert not remaining, f"partial allreduce not supported for multi-axis sharding {sharding}"
# all sharded axes are reduced: full allreduce
if ALLREDUCE_CAST and multi.src[0].op is Ops.CAST and multi.src[0].src[0].dtype in (dtypes.bfloat16, dtypes.half):
orig_dtype = multi.src[0].src[0].dtype
return local.cast(orig_dtype).allreduce(op, multi.device).cast(local.dtype)
return local.allreduce(op, multi.device)
# reduce on non sharded axes, piecewise is fine. if axis is None this is also correct
new_axis = multi.axis - num_axes if multi.axis is not None else None
return multi.src[0]._rop(op, tuple(range(num_axes))).unshard(new_axis, multi.src[1])
# no sharded axes reduced: piecewise, keep all remaining sharding
new_axes = tuple(ax - num_axes for ax, _ in remaining)
new_rngs = tuple(rng for _, rng in remaining)
return local.unshard(new_axes, new_rngs)
def reshape_multi(root:UOp, multi:UOp):
if prod(multi.shape) != prod(new_shape:=root.marg): raise RuntimeError("reshape must maintain prod(shape)")
if (new_axis:=root.axis) is not None: new_shape = tuple(s//len(multi.device) if a==new_axis else s for a,s in enumerate(new_shape))
return multi.src[0].reshape(new_shape).unshard(new_axis, multi.src[1])
# map every sharded axis through the reshape: the axis boundary must survive intact and stay divisible by its shard count
arg_acc:list[sint] = [1]
for s in new_shape: arg_acc.append(ssimplify(arg_acc[-1]*s))
new_shardings = []
for ax, rng in multi.sharding:
count = int(rng.vmax)+1
target = prod(multi.shape[:ax])
if target not in arg_acc: raise RuntimeError(f"reshape {multi.shape} -> {new_shape} moved items between shards")
new_ax = len(arg_acc) - arg_acc[::-1].index(target) - 1
if new_shape[new_ax] % count != 0: raise RuntimeError(f"reshape {multi.shape} -> {new_shape} moved items between shards")
new_shardings.append((new_ax, rng))
new_axs = {a for a, _ in new_shardings}
new_shape = tuple(s//(int(rng.vmax)+1) if a in new_axs else s for a,s in enumerate(new_shape))
return multi.src[0].reshape(new_shape).unshard(tuple(a for a,_ in new_shardings), tuple(r for _,r in new_shardings))
def expand_multi(root:UOp, multi:UOp):
new_axis = None if multi.axis is None else multi.axis + len(root.marg)
return multi.src[0]._mop(Ops.EXPAND, arg=root.marg).unshard(new_axis, multi.src[1])
shift = len(root.marg)
return multi.src[0]._mop(Ops.EXPAND, arg=root.marg) \
.unshard(tuple(ax+shift for ax,_ in multi.sharding), tuple(r for _,r in multi.sharding))
def pad_multi(root:UOp, multi:UOp):
assert multi.axis is None or root.marg[multi.axis] == (0, multi.shape[multi.axis]), f"padding not supported for {root.marg=}"
local_pad = tuple((0, multi.src[0].shape[multi.axis]) if a == multi.axis else s for a,s in enumerate(root.marg))
return multi.src[0]._mop(Ops.PAD, local_pad).unshard(multi.axis, multi.src[1])
for ax, _ in multi.sharding:
assert root.marg[ax] == (0, multi.shape[ax]), f"padding not supported for {root.marg=}"
counts = {a for a,_ in multi.sharding}
local_pad = tuple((0, multi.src[0].shape[a]) if a in counts else s for a,s in enumerate(root.marg))
return multi.src[0]._mop(Ops.PAD, local_pad).unshard(multi.arg, multi.src[1:])
def permute_multi(root:UOp, multi:UOp):
# all permutes supported!
return multi.src[0].permute(root.marg).unshard(root.axis, multi.src[1])
return multi.src[0].permute(root.marg) \
.unshard(tuple(root.marg.index(ax) for ax,_ in multi.sharding), tuple(r for _,r in multi.sharding))
def shrink_multi(root:UOp, multi:UOp):
shard_bounds = tuple((s,e-s) for s,e in multi.bounds) if multi.axis is not None else ()
assert multi.axis is None or root.marg[multi.axis] == (0, multi.shape[multi.axis]) or root.marg[multi.axis] in shard_bounds, \
f"shrinking not supported for {root.marg=}"
if multi.axis is not None and root.marg[multi.axis] in shard_bounds and root.marg[multi.axis] != (0, multi.shape[multi.axis]):
# NOTE: shrink on the shard axis is only allowed when result is a single partition, denoted by the new real
# we just copy it to all the devices, no real. this will be optimized out later
non_shard_shrink = tuple((0, multi.src[0].shape[i]) if i == multi.axis else s for i, s in enumerate(root.marg))
return multi.src[0].copy_to_device(multi.device, arg=shard_bounds.index(root.marg[multi.axis]))._mop(Ops.SHRINK, non_shard_shrink)
local_shrink = tuple((0, multi.src[0].shape[multi.axis]) if a == multi.axis else s for a,s in enumerate(root.marg))
return multi.src[0]._mop(Ops.SHRINK, local_shrink).unshard(multi.axis, multi.src[1])
# resolve each sharded axis independently: a shrink to exactly this range's own shard resolves the UNSHARD along
# that axis (e.g. a fragment indexed by its LOCAL thread range becomes that thread's REG shard, no copy needed)
local_marg = list(root.marg)
remaining = list(multi.sharding)
for ax, rng in multi.sharding:
shard_sz = multi.src[0].shape[ax]
s, l = root.marg[ax] # SHRINK marg is (start, length)
if sint_to_uop(l).ssimplify() == shard_sz and (sint_to_uop(s)-rng*shard_sz).ssimplify() == 0:
local_marg[ax] = (0, shard_sz)
remaining.remove((ax, rng))
continue
part_bounds = tuple((i*shard_sz, shard_sz) for i in range(int(rng.vmax)+1))
if (s, l) == (0, multi.shape[ax]): local_marg[ax] = (0, shard_sz) # full axis stays sharded, shrink the other axes locally
else:
# NOTE: otherwise a shrink on the shard axis is only allowed on the legacy device path, selecting a single
# partition (which is copied to all the devices and optimized out later)
if len(multi.sharding) != 1 or not isinstance(multi.device, tuple) or (s, l) not in part_bounds:
raise RuntimeError(f"shrinking not supported for {root.marg=}")
non_shard_shrink = tuple((0, shard_sz) if i == ax else t for i, t in enumerate(root.marg))
return multi.src[0].copy_to_device(multi.device, arg=part_bounds.index((s, l)))._mop(Ops.SHRINK, non_shard_shrink)
val = multi.src[0]._mop(Ops.SHRINK, tuple(local_marg))
return val if not remaining else val.unshard(tuple(a for a,_ in remaining), tuple(r for _,r in remaining))
def flip_multi(root:UOp, multi:UOp):
assert multi.axis is None or not root.marg[multi.axis], "flipping not supported on sharded axis"
return multi.src[0].flip([i for i,x in enumerate(root.marg) if x]).unshard(multi.axis, multi.src[1])
for ax, _ in multi.sharding:
if root.marg[ax]: raise RuntimeError(f"flipping not supported on sharded axis {ax}")
return multi.src[0].flip([i for i,x in enumerate(root.marg) if x]).unshard(multi.arg, multi.src[1:])
def stack_multi(root:UOp):
# STACK adds a leading axis: srcs are sharded one axis below the output
multis = [m for m in root.src if m.op is Ops.UNSHARD]
if not multis: return None
sharding = multis[0].sharding
if all(m.sharding == sharding for m in multis):
srcs = [m.src[0] if m.op is Ops.UNSHARD else m for m in root.src]
new_sharding = tuple((ax+1, rng) for ax, rng in sharding)
return UOp(Ops.STACK, src=tuple(srcs)).unshard(tuple(a for a,_ in new_sharding), tuple(r for _,r in new_sharding))
# resharding: single-axis fallback
axis = root.axis
assert axis is not None
return UOp(Ops.STACK, src=tuple(shard_srcs(root.src, axis-1))).unshard(axis, next(m.src[1] for m in root.src if m.op is Ops.UNSHARD))
def copy_multi(multi:UOp, device:str | tuple[str, ...]):
assert multi.axis is not None, "all multi ops have axis"
if isinstance(device, str):
pieces = [multi.src[0].mselect(i).copy_to_device(device) for i in range(len(multi.device))]
return pieces[0].cat(*pieces[1:], dim=multi.axis)
return multi.src[0]._unshard(multi.axis).allreduce(Ops.ADD, device)
def index_multi(root:UOp, multi:UOp):
# INDEX on UNSHARD: resolve each sharded axis into this range's own shard.
# Two ownership patterns are supported:
# contiguous: idx = rng*shard_sz + local (thread rng owns [rng*shard_sz, ...))
# strided: idx = rng + ir*shard_sz (thread rng owns {rng, rng+shard_sz, ...})
idxs = list(root.src[1:])
for ax, rng in multi.sharding:
shard_sz = multi.src[0].shape[ax]
local = (idxs[ax] - rng*shard_sz).simplify()
if local.vmin >= 0 and local.vmax < shard_sz:
idxs[ax] = local
continue
# strided ownership: idx ≡ rng (mod shard_sz), intra-shard position is (idx - rng) // shard_sz
diff = (idxs[ax] - rng).simplify()
if (mod:=(diff % shard_sz).simplify()).op is Ops.CONST and mod.arg == 0:
local = (diff // shard_sz).simplify()
if local.vmin >= 0 and local.vmax < shard_sz:
idxs[ax] = local
continue
raise RuntimeError(f"index_multi: cannot shard index {idxs[ax]} for UNSHARD axis {ax} with shard size {shard_sz}")
return multi.src[0].index(*idxs)
def store_after_multi(dest:UOp, src:UOp): return dest.after(dest.store(src.src[0])).unshard(src.axis, src.src[1])
def _shard_idx(rng:UOp, dev_idx:int) -> int:
drngs = [r for r in rng.ranges if r.arg[-1] is AxisType.DEVICE]
return 0 if not drngs else int(rng.substitute({drngs[0]: drngs[0].const_like(dev_idx)}).ssimplify())
def copy_multi(multi:UOp, device:str | tuple[str, ...]):
sharding = multi.sharding
if isinstance(device, str):
# reconstruct by concatenating along each axis from last to first
piece_info: list[tuple[tuple, UOp]] = []
for i in range(len(multi.device)):
idxs = tuple(_shard_idx(r, i) for _, r in sharding)
piece_info.append((idxs, multi.src[0].mselect(i).copy_to_device(device)))
for j in range(len(sharding) - 1, -1, -1):
ax, rng = sharding[j]
groups: dict[tuple, list[tuple[int, UOp]]] = {}
for idxs, p in piece_info:
key = idxs[:j] + idxs[j+1:]
groups.setdefault(key, []).append((idxs[j], p))
piece_info = []
for key in sorted(groups):
grp = sorted(groups[key], key=lambda x: x[0])
piece_info.append((key, grp[0][1].cat(*[x[1] for x in grp[1:]], dim=ax)))
return piece_info[0][1]
# multi-device target: unshard all axes and allreduce
val = multi.src[0]
for ax, rng in sharding:
bsz = val.shape[ax]
val = val.pad(tuple((0,0) if a != ax else (bsz*rng, bsz*int(rng.vmax) - bsz*rng) for a in range(len(val.shape))))
return val.allreduce(Ops.ADD, device)
def store_after_multi(dest:UOp, src:UOp): return dest.after(dest.store(src.src[0])).unshard(src.arg, src.src[1:])
def passthrough_multi(root:UOp, multi:UOp):
new_src = (multi.src[0],)+tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src[1:])
return UOp(root.op, root.dtype, src=new_src, arg=root.arg).unshard(multi.axis, multi.src[1])
return UOp(root.op, root.dtype, src=new_src, arg=root.arg).unshard(multi.arg, multi.src[1:])
def rewrite_into_function(call:UOp):
if call.arg.precompile: return None
@@ -145,7 +245,7 @@ def rewrite_into_function(call:UOp):
assert new_body.op is Ops.TUPLE
if any(s.op is Ops.UNSHARD for s in new_body.src):
shard_call = call.replace(src=(UOp.maketuple(*[s.src[0] if s.op is Ops.UNSHARD else s for s in new_body.src]),)+new_args)
return UOp.maketuple(*[shard_call.gettuple(i).unshard(s.axis, s.src[1]) if s.op is Ops.UNSHARD else shard_call.gettuple(i)
return UOp.maketuple(*[shard_call.gettuple(i).unshard(s.arg, s.src[1:]) if s.op is Ops.UNSHARD else shard_call.gettuple(i)
for i, s in enumerate(new_body.src)])
return call.replace(src=(new_body,)+new_args)
@@ -165,17 +265,17 @@ multi_pm = PatternMatcher([
(UPat(Ops.PERMUTE, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), permute_multi),
(UPat(Ops.FLIP, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), flip_multi),
(UPat(Ops.STACK, name="root", custom_early_reject=set([Ops.UNSHARD])), stack_multi),
(UPat(Ops.INDEX, src=(UPat(Ops.UNSHARD, name="multi"),), name="root", allow_any_len=True), index_multi),
(UPat(Ops.AFTER, src=(UPat(Ops.UNSHARD), UPat(Ops.STORE, src=(UPat(Ops.UNSHARD, name="dest"), UPat(Ops.UNSHARD, name="src"))))), store_after_multi),
(UPat(Ops.COPY, src=(UPat(Ops.UNSHARD, name="multi"),), name="copy"), lambda multi,copy: copy_multi(multi, copy.arg)),
(UPat(Ops.ALLREDUCE, src=(UPat(Ops.UNSHARD, name="multi"),), name="red"),
lambda multi,red: multi.src[0].allreduce(*red.arg).unshard(multi.axis, multi.src[1])),
lambda multi,red: multi.src[0].allreduce(*red.arg).unshard(multi.arg, multi.src[1:])),
# resolve TUPLE+GETTUPLE (needed in multi)
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
# GETTUPLE on UNSHARD: passthrough UNSHARD (e.g. when FUNCTION was replaced by UNSHARD(GETTUPLE(...)))
(UPat(Ops.GETTUPLE, src=(UPat(Ops.UNSHARD, name="multi"),), name="g"),
lambda g, multi: multi.src[0].gettuple(g.arg).unshard(multi.axis, multi.src[1]) if multi.src[0].op in {Ops.FUNCTION, Ops.TUPLE}
else multi),
lambda g, multi: multi.src[0].gettuple(g.arg).unshard(multi.arg, multi.src[1:]) if multi.src[0].op in {Ops.FUNCTION, Ops.TUPLE} else multi),
# rewrite into FUNCTION calls explicitly for UNSHARD (value-producing)
(UPat(Ops.FUNCTION, name="call"), rewrite_into_function),
(UPat((Ops.CALL, Ops.FUNCTION, Ops.AFTER), src=(UPat(Ops.UNSHARD, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
+3 -3
View File
@@ -50,7 +50,7 @@ def _mop_index(r:UOp, idx:UOp):
pm_mops = PatternMatcher([
# handle movement ops on INDEX
(UPat(GroupOp.Movement, name="r").f(Ops.INDEX, allow_any_len=True, name="idx"), _mop_index),
# move movement ops and INDEX after AFTER (but not when AFTER has a raw STORE with shaped children — from replace_contig_with_store_after)
# move movement ops and INDEX after AFTER
(UPat(GroupOp.Movement|{Ops.INDEX}, name="r").after(name="a", allow_any_len=True),
lambda r,a: UOp(r.op, src=(a.replace(src=(r.src[0],)+a.src[1:]),)+r.src[1:], arg=r.arg)),
(UPat(GroupOp.Movement, name="r").end(name="a", allow_any_len=True), lambda r,a: a.replace(src=(r.src[0],)+a.src[1:])),
@@ -342,9 +342,9 @@ def limit_bufs(ctx:IndexingContext, root:UOp):
srcs = []
for s in root.src:
if s.op in GroupOp.Elementwise and s.device is not None:
# Insert bufferize: all AxisType.REDUCE before bufferize are AxisType.LOOP, the DEVICE range stays a launched axis
# Insert bufferize: all AxisType.REDUCE before bufferize are AxisType.WEAK, the DEVICE range stays a launched axis
orig_ranges = s.ranges
end_ranges = [x.replace(arg=(next(ctx.range_idx), AxisType.LOOP)) if x.op is Ops.RANGE and x.arg[-1] is not AxisType.DEVICE else x
end_ranges = [x.replace(arg=(next(ctx.range_idx), AxisType.WEAK)) if x.op is Ops.RANGE and x.arg[-1] is not AxisType.DEVICE else x
for x in s.ranges]
s = s.substitute(dict(zip(orig_ranges, end_ranges))).bufferize(*end_ranges, arg=BufferizeOpts(device=s.device)).index(*orig_ranges)
srcs.append(s)
+1 -2
View File
@@ -240,7 +240,7 @@ class Tensor(RandMixin):
if capturing and not getenv("UNSAFE_ALLOW_JIT_BUFFER"):
from tinygrad.engine.jit import JitError
raise JitError("cannot access tensor data during JIT capture, the value will be baked in")
x = self.cast(strong_dtype(self.dtype)).contiguous()
x = self.contiguous()
if self.uop.device is None or isinstance(self.device, tuple): x = x.clone("CPU")
return cast(Buffer, x.realize().uop.buffer).ensure_allocated()
@@ -279,7 +279,6 @@ class Tensor(RandMixin):
print(t.tolist())
```
"""
if self.dtype in dtypes.weaks: return self.cast(strong_dtype(self.dtype)).tolist()
# TODO: remove half once minimum python supports it
if self.dtype in (dtypes.half, dtypes.bfloat16, *dtypes.fp8s): return self.cast(dtypes.float32).tolist()
if 0 in self.shape:
+54 -30
View File
@@ -16,8 +16,8 @@ if TYPE_CHECKING:
class AxisType(Enum):
def __repr__(self): return str(self)
DEVICE = auto(); GLOBAL = auto(); WARP = auto(); LOCAL = auto(); LOOP = auto(); GROUP_REDUCE = auto(); REDUCE = auto(); UPCAST = auto() # noqa: E702
UNROLL = auto(); THREAD = auto(); PLACEHOLDER = auto() # noqa: E702
DEVICE = auto(); GLOBAL = auto(); WARP = auto(); LOCAL = auto(); WEAK = auto(); GROUP_REDUCE = auto(); REDUCE = auto(); UPCAST = auto() # noqa: E702
UNROLL = auto(); THREAD = auto(); PLACEHOLDER = auto(); LOOP = auto() # noqa: E702
@dataclass(frozen=True, order=True)
class ParamArg:
@@ -35,14 +35,15 @@ class ParamArg:
("volatile", False))
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
return f"ParamArg({', '.join(args)})"
axis_letters = {AxisType.DEVICE: "d", AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L",
AxisType.UPCAST: "u", AxisType.GROUP_REDUCE: "G", AxisType.REDUCE: "R", AxisType.UNROLL: "r"}
axis_letters = {AxisType.DEVICE: "d", AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.WEAK: "L",
AxisType.LOOP: "L", AxisType.UPCAST: "u", AxisType.GROUP_REDUCE: "G", AxisType.REDUCE: "R", AxisType.UNROLL: "r"}
axis_colors = {AxisType.DEVICE: "green", AxisType.GLOBAL: "blue", AxisType.THREAD: "BLUE", AxisType.LOCAL: "cyan", AxisType.WARP: "CYAN",
AxisType.LOOP: "WHITE", AxisType.UPCAST: "yellow", AxisType.GROUP_REDUCE: "RED", AxisType.REDUCE: "red", AxisType.UNROLL: "magenta"}
AxisType.WEAK: "WHITE", AxisType.LOOP: "WHITE", AxisType.UPCAST: "yellow", AxisType.GROUP_REDUCE: "RED", AxisType.REDUCE: "red",
AxisType.UNROLL: "magenta"}
# NOTE: LOCAL and GROUP_REDUCE have the same priority. the order here matters
axis_to_pos = {AxisType.DEVICE: -2, AxisType.LOOP: -1, AxisType.THREAD: 0, AxisType.GLOBAL: 0, AxisType.WARP: 1, AxisType.LOCAL: 2,
AxisType.UPCAST: 3, AxisType.GROUP_REDUCE: 2, AxisType.REDUCE: 4, AxisType.UNROLL: 5}
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}
@@ -157,7 +158,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
return dtypes.uint64
case Ops.SHL | Ops.SHR:
if not all(dtypes.is_int(x.dtype) for x in src): raise RuntimeError(f"shift operands must be int, got {[x.dtype for x in src]}")
return promo_dtype(src)
return src[0].dtype
case Ops.BUFFER | Ops.PARAM:
assert isinstance(arg, ParamArg), "BUFFER/PARAM must have ParamArg"
return arg.dtype
@@ -190,7 +191,7 @@ class UOpMetaClass(type):
if dtype is None: dtype = dtype_from_uop(op, src, arg) or dtypes.void
# CONST derives its dtype by value only when the constructor omits one
# TODO: delete this once the dtype field is removed, for now it just re-implements spec.py
if SPEC == 2 and op is not Ops.CONST and not (op in (Ops.SHL, Ops.SHR) and src[1].dtype == dtypes.uint and dtype == src[0].dtype) and \
if SPEC == 2 and op is not Ops.CONST and \
not any(s.base.arg is Invalid for s in src) and (expected_dtype:=dtype_from_uop(op, src, arg)) is not None and expected_dtype != dtype:
raise RuntimeError(f"bad dtype {dtype}, expected {expected_dtype} on {op}")
if (wret:=UOpMetaClass.ucache.get(key:=(op, dtype, src, arg, tag), None)) is not None and (ret:=wret()) is not None: return ret
@@ -438,7 +439,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
case Ops.FLIP:
if len(ps) != len(self.marg) or not all(isinstance(x, bool) for x in self.marg): raise ValueError(f"bad flip on {ps}, {self.marg}")
return ps
case Ops.UNSHARD: return tuple(s*(int(self.src[1].vmax)+1) if a == self.axis else s for a,s in enumerate(ps))
case Ops.UNSHARD: return tuple(s*(int(self.src[1:][self.arg.index(a)].vmax)+1) if a in self.arg else s for a,s in enumerate(ps))
case Ops.REDUCE:
num_axes = self.arg[1]
if not isinstance(num_axes, int) or num_axes < 0 or num_axes > len(ps):
@@ -616,10 +617,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
ret = UOp(Ops.CONST, dtype, arg=dtype.const(b), src=())
return ret._mop(Ops.EXPAND, arg=shape) if shape is not None and shape != () and ret.shape != shape else ret
@staticmethod
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
def range(end:sint, axis_id, axis_type=AxisType.WEAK, *arg, dtype=dtypes.weakint, src=(), **kwargs):
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
@staticmethod
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.LOOP)+arg)
def loop(axis_id:int, *arg): return UOp(Ops.RANGE, src=(UOp(Ops.NOOP),), arg=(axis_id, AxisType.WEAK)+arg)
@staticmethod
def special(end:sint, name:str, dtype=dtypes.weakint): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
@staticmethod
@@ -664,24 +665,38 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# *** multi-device helpers ***
def unshard(self, axis:int|None, device_range:UOp|None=None):
assert isinstance(self.device, tuple), f"multi device must be tuple, {self.device} isn't"
def unshard(self, axis:int|tuple[int, ...]|None, device_range:UOp|tuple[UOp, ...]|None=None):
assert axis is not None, "multi None is no longer supported"
# an UNSHARD always has two srcs: the value and the DEVICE range it ends (defaults to a DEVICE range over the devices)
if device_range is None: device_range = UOp.range(len(self.device), -1, AxisType.DEVICE)
assert device_range.op is Ops.RANGE and device_range.arg[-1] is AxisType.DEVICE
return UOp(Ops.UNSHARD, src=(self, device_range), arg=axis)
# an UNSHARD carries the value and one sharding range per sharded axis (arg is the tuple of sharded axes,
# sorted). the single-axis axis form defaults the range to a DEVICE range over the devices; a range need not
# be DEVICE, e.g. a LOCAL range shards a kernel tile into per-thread fragments
if isinstance(axis, int): axis = (axis,)
if device_range is None:
assert isinstance(self.device, tuple), f"multi device must be tuple, {self.device} isn't"
device_range = (UOp.range(len(self.device), -1, AxisType.DEVICE),)
if isinstance(device_range, UOp): device_range = (device_range,)
assert isinstance(device_range, tuple) and len(axis) == len(device_range) and len(set(axis)) == len(axis)
axis, device_range = map(tuple, zip(*sorted(zip(axis, device_range))))
return UOp(Ops.UNSHARD, src=(self, *device_range), arg=axis)
@property
def sharding(self) -> tuple[tuple[int, UOp], ...]:
"""(axis, RANGE) pairs this value is sharded over (the source of truth for shard bounds/counts)."""
return tuple(zip(self.arg, self.src[1:])) if self.op is Ops.UNSHARD else ()
@property
def bounds(self):
if self.axis is None: raise RuntimeError("bounds is not defined when axis is None")
return tuple(itertools.pairwise(itertools.accumulate([self.src[0].shape[self.axis] for _ in self.device], initial=0)))
dcount = int(self.src[1].vmax)+1 if self.op is Ops.UNSHARD else len(self.device)
return tuple(itertools.pairwise(itertools.accumulate([self.src[0].shape[self.axis] for _ in range(dcount)], initial=0)))
@functools.cached_property
def axis(self) -> int|None:
# COPY removes axis. TODO: add more tests for this, and consider MSELECT/MSTACK
if self.op is Ops.COPY: return None
if self.op is Ops.UNSHARD: return self.arg
if self.op is Ops.UNSHARD:
if len(self.arg) != 1: raise RuntimeError(f"UOp is sharded on multiple axes {self.arg}, use .sharding")
return self.arg[0]
# GETTUPLE: axis comes from the specific TUPLE element, not src[0]
if self.op is Ops.GETTUPLE:
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
@@ -705,7 +720,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
target = ssimplify(prod(self.src[0].shape[:src_axis]))
if target not in arg_acc: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
new_axis = len(arg_acc) - arg_acc[::-1].index(target) - 1
if self.shape[new_axis] % len(self.device) != 0: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
dcount = len(self.device) if isinstance(self.device, tuple) else \
int(next(u.src[1] for u in self.src[0].toposort() if u.op is Ops.UNSHARD).vmax)+1
if self.shape[new_axis] % dcount != 0: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
return new_axis
if self.op is Ops.PERMUTE: return self.marg.index(src_axis) if src_axis is not None else None
if self.op is Ops.EXPAND: return src_axis + len(self.marg) if src_axis is not None else None
@@ -716,15 +733,15 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
dnum = UOp.range(dcount, -1, AxisType.DEVICE)
return self.pad(tuple((0,0) if a != axis else (bsz*dnum, bsz*(dcount-1) - bsz*dnum) for a in range(len(self.shape))))
def _shard(self, axis:int, dcount:int) -> UOp:
def _shard(self, axis:int, rng:UOp) -> UOp:
if len(self.shape) == 0: return self # scalars broadcast, no sharding needed
dnum = UOp.range(dcount, -1, AxisType.DEVICE)
dcount = int(rng.vmax)+1
if self.shape[axis] % dcount != 0: raise RuntimeError(f"multi axis uneven: {self.shape[axis]=} {axis=} {dcount=}")
sz = self.shape[axis] // dcount
return self.shrink(tuple((0,s) if i != axis else (dnum*sz,dnum*sz+sz) for i,s in enumerate(self.shape)))
return self.shrink(tuple((0,s) if i != axis else (rng*sz,rng*sz+sz) for i,s in enumerate(self.shape)))
def shard(self, devices:tuple[str, ...], axis:int|None=None) -> UOp:
copied = self.copy_to_device(devices)
return copied if axis is None else copied._shard(axis, len(devices)).unshard(axis)
return copied if axis is None else copied._shard(axis, UOp.range(len(devices), -1, AxisType.DEVICE)).unshard(axis)
def copy_to_device(self, device:str|tuple[str, ...], arg=None):
assert arg is None or isinstance(self.device, tuple)
@@ -844,7 +861,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.op is Ops.BUFFER: return self.arg.addrspace
if self.op in {Ops.SPECIAL, Ops.RANGE}: return AddrSpace.ALU
if self.op is Ops.LOAD: return AddrSpace.ALU # LOAD brings things into the ALU
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT, Ops.END}:
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT, Ops.END, Ops.UNSHARD}:
return self.src[0].addrspace
if self.op in GroupOp.Movement: return self.src[0].addrspace
if self.op in {Ops.STACK, Ops.WMMA, Ops.GROUP} or self.op in GroupOp.Elementwise:
@@ -872,7 +889,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# CL 1.1 provides the clCreateSubBuffer API, but at the time of writing, relevant CL runtimes (rusticl, adreno, nvidia, amd) do not provide
# reasonable values for CL_DEVICE_MEM_BASE_ADDR_ALIGN. cl_ext_buffer_device_address could potentially help, but this extension is not provided
# by relevant CL runtimes at time of writing.
if any(d.startswith(("WEBGPU", "CL")) for d in ((self.device,) if isinstance(self.device, str) else self.device)): return None
if (dev:=self.device) is not None and any(d.startswith(("WEBGPU", "CL")) for d in ((dev,) if isinstance(dev, str) else dev)): return None
idx = self.flatten().index(UOp.range(self.numel(), 0))
out = graph_rewrite(idx, pm_mops+symbolic+pm_contiguous_view_offset, ctx=self, name="contiguous_view_offset")
@@ -1642,7 +1659,7 @@ class RewriteContext:
else:
# rebuild node with rewritten srcs
new_src = tuple(self.replace.get(x, x) for x in n.src)
new_n = UOp(n.op, n.dtype, new_src, n.arg, n.tag) if new_src != n.src else n
new_n = UOp(n.op, _rebuild_dtype(n, new_src), new_src, n.arg, n.tag) if new_src != n.src else n
# top-down: try pm on rebuilt node, use result as-is (no re-traversal)
if self.pm is not None and (rewritten:=self.pm_rewrite(new_n)) is not None: new_n = rewritten
self.replace[n] = new_n
@@ -1701,7 +1718,7 @@ class RewriteContext:
continue
else:
# if srcs changed from rewrites, construct a new UOp with the new srcs
new_src_n = UOp(new_n.op, new_n.dtype, new_src, new_n.arg, new_n.tag)
new_src_n = UOp(new_n.op, _rebuild_dtype(new_n, new_src), new_src, new_n.arg, new_n.tag)
# trigger a rewrite of new_src_n, then after that rewrite is done, link it back to n
stack.append((n, 2, new_src_n))
stack.append((new_src_n, 0, new_src_n))
@@ -1721,6 +1738,13 @@ def graph_rewrite(sink:UOp, pm:PatternMatcher, ctx=None, bottom_up=False, name=N
rewrite_ctx = RewriteContext(pm if not bottom_up else None, pm if bottom_up else bpm, ctx, enter_calls)
return rewrite_ctx.walk_rewrite(sink) if walk else rewrite_ctx.unified_rewrite(sink)
def _rebuild_dtype(n:UOp, new_src:tuple[UOp,...]) -> DType:
# TODO: delete this once the dtype field is removed, every rebuild will re-derive
# TODO: these ops keep their stored dtype until dtype_from_uop works
if n.op in {Ops.INS, Ops.INDEX, Ops.CUSTOM, Ops.CUSTOMI, Ops.PYLITERAL} or \
all(a.dtype is b.dtype or b.base.arg is Invalid for a,b in zip(n.src, new_src)): return n.dtype
return dtype_from_uop(n.op, new_src, n.arg) or n.dtype
def sint_to_uop(x:sint, dtype=dtypes.weakint) -> UOp: return UOp.const(dtype, x)
def to_max_shape(shape:tuple[sint, ...]) -> tuple[int, ...]: return tuple(int(x.vmax) if isinstance(x, UOp) else x for x in shape)
@@ -1743,7 +1767,7 @@ pm_lower_weak = PatternMatcher([
# a weakfloat Unary (sin/exp2/...) must resolve here, before the transcendental decomposition
(UPat(GroupOp.Binary|GroupOp.Unary|{Ops.WHERE, Ops.RANGE, Ops.STACK, Ops.SPECIAL}, name="u"), lower_weak_node),
(UPat(Ops.PARAM, dtype=dtypes.weakint, name="u"),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=select_dtype(u))).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
])
def lower_weak_srcs(ctx:dict[UOp, UOp]|None, u:UOp) -> UOp|None:
if ctx is None: ctx = {}
+6 -5
View File
@@ -91,7 +91,7 @@ spec_shared = PatternMatcher([
isinstance(x.arg, ParamArg) and x.addrspace in (AddrSpace.REG, AddrSpace.LOCAL)),
# GROUP of stores (or groups, or NOOPs)
(UPat(Ops.GROUP, dtypes.void, src=UPat((Ops.GROUP, Ops.STORE, Ops.NOOP, Ops.INS, Ops.END))), lambda: True),
(UPat(Ops.GROUP, dtypes.void, src=UPat((Ops.GROUP, Ops.STORE, Ops.NOOP, Ops.INS, Ops.END, Ops.CUSTOMI))), lambda: True),
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, or another AFTER
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.INDEX,
@@ -142,7 +142,8 @@ spec_tensor = PatternMatcher([
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
# Tensor variable bindings
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.weakint,))), arg=None), lambda: True),
(UPat(Ops.BIND, (dtypes.int, dtypes.long, dtypes.weakint,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.long,dtypes.weakint,))), arg=None),
lambda: True),
# custom function
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
@@ -175,9 +176,9 @@ spec_tensor = PatternMatcher([
len(red.arg) == 2 and red.arg[0] in GroupOp.Reduce and is_device(red.arg[1])),
# UNSHARD/MSELECT/MSTACK
# an UNSHARD always has two srcs: the value and the DEVICE range it ends
(UPat(Ops.UNSHARD, name="multi"), lambda multi: len(multi.src) == 2 and matches_dtype(multi.src[0], multi.dtype)
and isinstance(multi.arg, int) and multi.src[1].op is Ops.RANGE and multi.src[1].arg[-1] is AxisType.DEVICE),
# an UNSHARD carries the value and one sharding range per sharded axis (usually a DEVICE RANGE, but can be a derived expression)
(UPat(Ops.UNSHARD, name="multi"), lambda multi: len(multi.src) == 1+len(multi.arg) and matches_dtype(multi.src[0], multi.dtype)
and all(isinstance(a, int) for a in multi.arg) and all(r.dtype in dtypes.weaks for r in multi.src[1:])),
(UPat(Ops.MSELECT, name="x"), lambda x: isinstance(x.src[0].device, tuple) and x.arg < len(x.src[0].device)),
(UPat(Ops.MSTACK, name="x"), lambda x: all(isinstance(s.device, str) for s in x.src) or (all_same(x.src) and x.src[0].device is None)),
+2 -5
View File
@@ -262,12 +262,9 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
# (x//c1)//c2 -> x//(c1*c2) for c2>0
((UPat.var("x") // UPat.cvar("c1")) // UPat.cvar("c2"), lambda x,c1,c2: x//(c1*c2) if c2.vmin>0 else None),
# ** lt **
# c0*x<c1 for positive int c0,c1
# c0*x<c1 -> sign(c0)*x < ceil(c1/abs(c0))
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
lambda x,c0,c1: x<math.ceil(c1.arg/c0.arg) if c0.arg > 0 and c1.arg > 0 else None),
# c0*x<c1 for negative int c0 and non-positive c1
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
lambda x,c0,c1: (-x)<(-(math.floor(-c1.arg/-c0.arg))) if c0.arg < 0 and c0.arg != -1 and c1.arg <= 0 else None),
lambda x,c0,c1: (x if c0.arg > 0 else -x)<-(-c1.arg//abs(c0.arg)) if abs(c0.arg) > 1 else None),
# x//d<c -> x<c*d for d>0, and -> c*d<x for d<0
((UPat.var("x", dtype=dtypes.weakint)//UPat.cvar("d"))<UPat.cvar("c"),
lambda x,d,c: (x<c.arg*d.arg) if d.arg > 0 else (x>c.arg*d.arg) if d.arg < 0 else None),