forked from tinygrad/tinygrad
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5021b74631 | ||
|
|
8eb22983d4 | ||
|
|
53a11f59ab | ||
|
|
287679a88a | ||
|
|
2eacd4fa68 | ||
|
|
7fdc58b1cc | ||
|
|
cb857a5a40 | ||
|
|
6083de5cff | ||
|
|
55c8e18930 | ||
|
|
76dce1eb8d | ||
|
|
6242b09066 | ||
|
|
92d92c70a9 | ||
|
|
26c4b6319e | ||
|
|
53cad325c7 | ||
|
|
4456e62343 | ||
|
|
39d7fd0981 | ||
|
|
4e6bdac412 | ||
|
|
bc485d271f | ||
|
|
6b361e4de7 | ||
|
|
38e0fe103b | ||
|
|
e6ad2907a8 | ||
|
|
fede358811 | ||
|
|
f06832bf6f |
@@ -194,21 +194,29 @@ runs:
|
||||
echo "pkgs=$pkgs" >> "$GITHUB_OUTPUT"
|
||||
echo "hash=$(echo -n "$pkgs" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
installed=true
|
||||
for pkg in $pkgs; do
|
||||
info=$(dpkg-query -W -f='${db:Status-Abbrev} ${Version}' "$pkg" 2> /dev/null || true)
|
||||
echo "${pkg}: ${info:-not in dpkg database}"
|
||||
[[ "$info" == ii* ]] || installed=false
|
||||
done
|
||||
echo "installed=$installed" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache apt (PR)
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name == 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name == 'pull_request' && steps.apt-pkgs.outputs.installed == 'false'
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
- name: Cache apt
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name != 'pull_request'
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && github.event_name != 'pull_request' && steps.apt-pkgs.outputs.installed == 'false'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives/
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-apt-${{ steps.apt-pkgs.outputs.hash }}-${{ env.CACHE_VERSION }}
|
||||
|
||||
- name: Run apt Update + Install
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true')
|
||||
if: runner.os == 'Linux' && (inputs.opencl == 'true' || inputs.amd == 'true' || inputs.webgpu == 'true' || inputs.llvm == 'true' || inputs.qemu == 'true' || inputs.ninja == 'true') && steps.apt-pkgs.outputs.installed == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt -qq update || true
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -275,7 +275,7 @@ jobs:
|
||||
shell: bash -e -o pipefail {0}
|
||||
env:
|
||||
DEV: ${{ matrix.dev }}
|
||||
HCQ2: ${{ matrix.dev == 'AMD' && '1' || '0' }}
|
||||
HCQ2: '0'
|
||||
if: github.repository_owner == 'tinygrad'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
||||
@@ -390,10 +390,10 @@ jobs:
|
||||
run: |
|
||||
parallel --link --tagstring '[{1}]' '{2}' \
|
||||
::: llama 'llama q4' qwen3.5 qwen \
|
||||
::: $'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model llama3.2:1b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model llama3.2:1b-q4 | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --model qwen3.5:0.8b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a female chicken called? Answer with only one word." | python3 -m tinygrad.llm --model qwen3:0.6b | tee /dev/stderr | grep -i hen'
|
||||
::: $'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model llama3.2:1b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model llama3.2:1b-q4 | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a male chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model qwen3.5:0.8b | tee /dev/stderr | grep -i rooster' \
|
||||
$'echo "What\'s a female chicken called? Answer with only one word." | python3 -m tinygrad.llm --no_chat_template --model qwen3:0.6b | tee /dev/stderr | grep -i hen'
|
||||
# NOTE: qwen is dumb and only knows about female chickens
|
||||
|
||||
# ****** Models Tests ******
|
||||
|
||||
@@ -1667,7 +1667,7 @@ def train_llama3():
|
||||
def train_gptoss():
|
||||
from examples.mlperf.models.gpt_oss import GPTOSS, GPT_OSS_20B, apply_grad, FP8_DTYPE
|
||||
from examples.mlperf.lr_schedulers import CosineAnnealingLRWithWarmup
|
||||
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, clip_grads
|
||||
from examples.mlperf.optim import GradAccClipAdamW, GradAccClipAdamWGroup, fclip_grads
|
||||
|
||||
BENCHMARK = getenv("BENCHMARK")
|
||||
|
||||
@@ -1785,12 +1785,10 @@ def train_gptoss():
|
||||
|
||||
Tensor.realize(loss, *grads)
|
||||
|
||||
grad_norm = clip_grads(grads, 1, 1.0)
|
||||
optim.fstep(grads, grad_norm)
|
||||
clipped_grads, grad_norm = fclip_grads(grads, 1.0)
|
||||
optim.fstep(clipped_grads, grad_norm)
|
||||
scheduler.step()
|
||||
|
||||
for g in grads: g.assign(0)
|
||||
|
||||
loss_cpu = loss.flatten().float().to("CPU")
|
||||
lr_cpu = optim.lr.float().to("CPU")
|
||||
grad_norm_cpu = grad_norm.float().to("CPU")
|
||||
|
||||
@@ -282,14 +282,14 @@ def apply_grad(grad_buf:Tensor, new_grad:UOp):
|
||||
pads = _get_pads(new_grad)
|
||||
if len(pads) <= 1:
|
||||
new_grad = new_grad.cast(grad_buf.dtype)
|
||||
grad_buf.uop = grad_buf.uop.after(grad_buf.uop.store(grad_buf.uop + new_grad))
|
||||
grad_buf.uop = grad_buf.uop.after(grad_buf.uop.store(new_grad))
|
||||
return
|
||||
cur = grad_buf.uop
|
||||
for pad in sorted(pads, key=lambda p: p.marg[0][0] if p.op == Ops.PAD else 0, reverse=True):
|
||||
if pad.op == Ops.PAD:
|
||||
grad_shrink = tuple([(p[0], s+p[0]) for s,p in zip(pad.src[0].shape, pad.marg)])
|
||||
grad_shrink = tuple((p[0], s+p[0]) for s,p in zip(pad.src[0].shape, pad.marg))
|
||||
buf_slice = cur.shrink(grad_shrink)
|
||||
cur = cur.after(buf_slice.store(buf_slice + pad.src[0].cast(cur.dtype)))
|
||||
cur = cur.after(buf_slice.store(pad.src[0].cast(cur.dtype)))
|
||||
else:
|
||||
cur = cur.after(cur.store(cur + pad.cast(cur.dtype)))
|
||||
grad_buf.uop = cur
|
||||
|
||||
@@ -27,6 +27,11 @@ def clip_grads(grads:list[Tensor], grad_acc, clip_norm) -> Tensor:
|
||||
for g in grads: g.assign((g * (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)).cast(g.dtype))
|
||||
return total_norm
|
||||
|
||||
def fclip_grads(grads:list[Tensor], clip_norm) -> Tensor:
|
||||
total_norm = Tensor.stack(*[g.float().square().sum() for g in grads]).sum().sqrt().contiguous()
|
||||
scale = (clip_norm / (total_norm + 1e-6)).clamp(max_=1.0)
|
||||
return [(g * scale).cast(g.dtype) for g in grads], total_norm
|
||||
|
||||
class GradAccClipAdamW(Optimizer):
|
||||
def __init__(self, params:list[Tensor], lr=0.001, b1=0.9, b2=0.999, eps=1e-6, weight_decay=0.0, grad_acc=1, clip_norm=1.0, device=None, fused=FUSE_OPTIM):
|
||||
super().__init__(params, lr, device, fused)
|
||||
|
||||
+68
-115
@@ -20,34 +20,39 @@ def v_mfma_fp4(dst, a, b, opsel, opsel_hi, scale_a, scale_b):
|
||||
def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k = Kernel()
|
||||
scale_k = K // 32
|
||||
k.emit(s_and_b32(s[1], s[1], LIT, 65535))
|
||||
if (tile_m, tile_n) == (128, 512):
|
||||
k.emit(s_and_b32(s[1], s[1], LIT, 65535))
|
||||
k.emit(s_mov_b32(s[47], s[2]))
|
||||
k.emit(s_mov_b32(s[48], s[3]))
|
||||
k.emit(s_load_dwordx2(s[4:5], s[0:1], s[0], 0, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[8], 0))
|
||||
k.emit(s_mov_b32(s[9], 0))
|
||||
k.emit(s_load_dwordx2(s[12:13], s[0:1], s[0], 8, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[16:17], s[0:1], s[0], 16, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[36], N))
|
||||
k.emit(s_mov_b32(s[37], K))
|
||||
k.emit(s_mov_b32(s[38], K))
|
||||
k.emit(s_mov_b32(s[43], M))
|
||||
k.emit(s_mov_b32(s[44], N))
|
||||
k.emit(s_mov_b32(s[45], K))
|
||||
k.emit(s_load_dwordx2(s[20:21], s[0:1], s[0], 24, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[24:25], s[0:1], s[0], 32, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[39], scale_k))
|
||||
k.emit(s_mov_b32(s[40], scale_k))
|
||||
k.emit(v_lshrrev_b32_e32(v[1], 10))
|
||||
k.emit(v_lshrrev_b32_e32(v[2], 10, v[1]))
|
||||
k.emit(v_and_b32_e32(v[2], LIT, v[2], 1023))
|
||||
k.emit(v_and_b32_e32(v[1], LIT, v[1], 1023))
|
||||
k.emit(v_and_b32_e32(v[0], LIT, v[0], 1023))
|
||||
k.emit(v_lshrrev_b32_e32(v[3], 6))
|
||||
k.emit(v_and_b32_e32(v[0], 63))
|
||||
k.emit(v_readfirstlane_b32_e32(v[46], v[3]))
|
||||
k.emit(s_waitcnt(49279))
|
||||
k.emit(s_load_dwordx2(s[4:5], s[0:1], s[0], 0, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[8], 0))
|
||||
k.emit(s_mov_b32(s[9], 0))
|
||||
k.emit(s_load_dwordx2(s[12:13], s[0:1], s[0], 8, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[16:17], s[0:1], s[0], 16, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[36], N))
|
||||
k.emit(s_mov_b32(s[37], K))
|
||||
k.emit(s_mov_b32(s[38], K))
|
||||
k.emit(s_mov_b32(s[43], M))
|
||||
k.emit(s_mov_b32(s[44], N))
|
||||
k.emit(s_mov_b32(s[45], K))
|
||||
k.emit(s_load_dwordx2(s[20:21], s[0:1], s[0], 24, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[24:25], s[0:1], s[0], 32, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[39], scale_k))
|
||||
k.emit(s_mov_b32(s[40], scale_k))
|
||||
k.emit(v_lshrrev_b32_e32(v[1], 10))
|
||||
k.emit(v_lshrrev_b32_e32(v[2], 10, v[1]))
|
||||
k.emit(v_and_b32_e32(v[2], LIT, v[2], 1023))
|
||||
k.emit(v_and_b32_e32(v[1], LIT, v[1], 1023))
|
||||
k.emit(v_and_b32_e32(v[0], LIT, v[0], 1023))
|
||||
k.emit(v_lshrrev_b32_e32(v[3], 6))
|
||||
k.emit(v_and_b32_e32(v[0], 63))
|
||||
if (tile_m, tile_n) == (256, 256):
|
||||
k.emit(s_mov_b32(s[49], s[2]))
|
||||
k.emit(s_mov_b32(s[47], s[3]))
|
||||
k.emit(v_readfirstlane_b32_e32(v[46], v[3]))
|
||||
k.emit(s_waitcnt(49279))
|
||||
|
||||
if (tile_m, tile_n) == (128, 512):
|
||||
for i in range(2):
|
||||
k.emit(s_mov_b32(s[6 + i * 8], -16))
|
||||
k.emit(s_mov_b32(s[10 + i * 12], -16))
|
||||
@@ -1213,31 +1218,6 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(s_waitcnt())
|
||||
k.emit(s_endpgm())
|
||||
elif (tile_m, tile_n) == (192, 256):
|
||||
k.emit(s_and_b32(s[1], s[1], LIT, 65535))
|
||||
k.emit(s_load_dwordx2(s[4:5], s[0:1], s[0], 0, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[8], 0))
|
||||
k.emit(s_mov_b32(s[9], 0))
|
||||
k.emit(s_load_dwordx2(s[12:13], s[0:1], s[0], 8, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[16:17], s[0:1], s[0], 16, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[36], N))
|
||||
k.emit(s_mov_b32(s[37], K))
|
||||
k.emit(s_mov_b32(s[38], K))
|
||||
k.emit(s_mov_b32(s[43], M))
|
||||
k.emit(s_mov_b32(s[44], N))
|
||||
k.emit(s_mov_b32(s[45], K))
|
||||
k.emit(s_load_dwordx2(s[20:21], s[0:1], s[0], 24, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[24:25], s[0:1], s[0], 32, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[39], scale_k))
|
||||
k.emit(s_mov_b32(s[40], scale_k))
|
||||
k.emit(v_lshrrev_b32_e32(v[1], 10))
|
||||
k.emit(v_lshrrev_b32_e32(v[2], 10, v[1]))
|
||||
k.emit(v_and_b32_e32(v[2], LIT, v[2], 1023))
|
||||
k.emit(v_and_b32_e32(v[1], LIT, v[1], 1023))
|
||||
k.emit(v_and_b32_e32(v[0], LIT, v[0], 1023))
|
||||
k.emit(v_lshrrev_b32_e32(v[3], 6))
|
||||
k.emit(v_and_b32_e32(v[0], 63))
|
||||
k.emit(v_readfirstlane_b32_e32(v[46], v[3]))
|
||||
k.emit(s_waitcnt(49279))
|
||||
k.emit(s_mul_i32(s[63], LIT, 8, 192))
|
||||
k.emit(v_cvt_f32_u32_e32(v[4], s[63]))
|
||||
k.emit(s_sub_i32(s[62], 0, s[63]))
|
||||
@@ -2234,49 +2214,22 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(s_waitcnt())
|
||||
k.emit(s_endpgm())
|
||||
elif (tile_m, tile_n) == (256, 256):
|
||||
k.emit(s_and_b32(s[1], s[1], LIT, 65535))
|
||||
k.emit(s_load_dwordx2(s[4:5], s[0:1], s[0], 0, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[8], 0))
|
||||
k.emit(s_mov_b32(s[9], 0))
|
||||
k.emit(s_load_dwordx2(s[12:13], s[0:1], s[0], 8, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[16:17], s[0:1], s[0], 16, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[40], N))
|
||||
k.emit(s_mov_b32(s[41], K))
|
||||
k.emit(s_mov_b32(s[42], K))
|
||||
k.emit(s_mov_b32(s[43], M))
|
||||
k.emit(s_mov_b32(s[44], N))
|
||||
k.emit(s_mov_b32(s[45], K))
|
||||
k.emit(s_load_dwordx2(s[20:21], s[0:1], s[0], 24, 0, 0, 0, 1))
|
||||
k.emit(s_load_dwordx2(s[24:25], s[0:1], s[0], 32, 0, 0, 0, 1))
|
||||
k.emit(s_mov_b32(s[36], scale_k))
|
||||
k.emit(s_mov_b32(s[37], scale_k))
|
||||
k.emit(v_lshrrev_b32_e32(v[1], 10))
|
||||
k.emit(v_lshrrev_b32_e32(v[2], 10, v[1]))
|
||||
k.emit(v_and_b32_e32(v[2], LIT, v[2], 1023))
|
||||
k.emit(v_and_b32_e32(v[1], LIT, v[1], 1023))
|
||||
k.emit(v_and_b32_e32(v[0], LIT, v[0], 1023))
|
||||
k.emit(v_lshrrev_b32_e32(v[3], 6))
|
||||
k.emit(v_and_b32_e32(v[0], 63))
|
||||
k.emit(s_mov_b32(s[46], s[2]))
|
||||
k.emit(s_mov_b32(s[47], s[3]))
|
||||
k.emit(v_readfirstlane_b32_e32(v[49], v[3]))
|
||||
k.emit(s_waitcnt(49279))
|
||||
k.emit(s_add_u32(s[55], s[44], LIT, 255))
|
||||
k.emit(s_lshr_b32(s[54], s[55], 8))
|
||||
k.emit(s_mul_i32(s[48], s[54], s[47]))
|
||||
k.emit(s_add_i32(s[48], s[48], s[46]))
|
||||
k.emit(s_add_i32(s[48], s[48], s[49]))
|
||||
k.emit(s_add_u32(s[55], s[43], LIT, 255))
|
||||
k.emit(s_lshr_b32(s[52], s[55], 8))
|
||||
k.emit(s_lshl_b32(s[52], s[52], 5))
|
||||
k.emit(s_mov_b32(s[46], 0))
|
||||
k.emit(s_mov_b32(s[49], 0))
|
||||
k.label('L2_00E8')
|
||||
k.emit(s_cmp_lt_i32(s[48], s[52]))
|
||||
k.emit(s_cbranch_scc1(3), target='L2_00FC')
|
||||
k.emit(s_sub_i32(s[48], s[48], s[52]))
|
||||
k.emit(s_add_i32(s[46], s[46], 32))
|
||||
k.emit(s_add_i32(s[49], s[49], 32))
|
||||
k.emit(s_branch(65531), target='L2_00E8')
|
||||
k.label('L2_00FC')
|
||||
k.emit(s_sub_i32(s[54], s[54], s[46]))
|
||||
k.emit(s_sub_i32(s[54], s[54], s[49]))
|
||||
k.emit(s_cmp_lt_i32(s[54], 32))
|
||||
k.emit(s_cbranch_scc1(3), target='L2_0114')
|
||||
k.emit(s_lshr_b32(s[47], s[48], 5))
|
||||
@@ -2311,7 +2264,7 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(s_mul_i32(s[52], s[54], s[47]))
|
||||
k.emit(s_sub_i32(s[52], s[48], s[52]))
|
||||
k.label('L2_0194')
|
||||
k.emit(s_add_i32(s[46], s[52], s[46]))
|
||||
k.emit(s_add_i32(s[49], s[52], s[49]))
|
||||
k.emit(s_mov_b32(s[6], -16))
|
||||
k.emit(s_mov_b32(s[10], -16))
|
||||
k.emit(s_mov_b32(s[18], -16))
|
||||
@@ -2328,18 +2281,18 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(s_or_b32(s[9], s[9], LIT, 262144))
|
||||
k.emit(s_or_b32(s[17], s[17], LIT, 262144))
|
||||
k.emit(s_or_b32(s[13], s[13], LIT, 262144))
|
||||
k.emit(s_lshr_b32(s[41], s[41], 1))
|
||||
k.emit(s_mul_i32(s[52], s[41], s[43]))
|
||||
k.emit(s_lshr_b32(s[37], s[37], 1))
|
||||
k.emit(s_mul_i32(s[52], s[37], s[43]))
|
||||
k.emit(s_mov_b32(s[14], s[52]))
|
||||
k.emit(s_lshr_b32(s[42], s[42], 1))
|
||||
k.emit(s_mul_i32(s[52], s[42], s[44]))
|
||||
k.emit(s_lshr_b32(s[38], s[38], 1))
|
||||
k.emit(s_mul_i32(s[52], s[38], s[44]))
|
||||
k.emit(s_mov_b32(s[18], s[52]))
|
||||
k.emit(s_add_u32(s[52], s[43], 31))
|
||||
k.emit(s_lshr_b32(s[52], s[52], 5))
|
||||
k.emit(s_lshl_b32(s[52], s[52], 5))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[36]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[39]))
|
||||
k.emit(s_mov_b32(s[22], s[53]))
|
||||
k.emit(s_mul_i32(s[53], s[44], s[37]))
|
||||
k.emit(s_mul_i32(s[53], s[44], s[40]))
|
||||
k.emit(s_mov_b32(s[26], s[53]))
|
||||
k.emit(s_mov_b32(s[23], LIT, 131072))
|
||||
k.emit(s_mov_b32(s[27], LIT, 131072))
|
||||
@@ -2356,23 +2309,23 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(v_add_u32_e32(v[5], v[5], v[6]))
|
||||
k.emit(v_and_b32_e32(v[4], 1, v[4]))
|
||||
k.emit(v_add_u32_e32(v[5], v[5], v[4]))
|
||||
k.emit(v_mul_lo_u32(v[212], s[41], v[5]))
|
||||
k.emit(v_mul_lo_u32(v[212], s[37], v[5]))
|
||||
k.emit(v_and_b32_e32(v[4], 7))
|
||||
k.emit(v_lshlrev_b32_e32(v[4], 4, v[4]))
|
||||
k.emit(v_add_u32_e32(v[212], v[212], v[4]))
|
||||
k.emit(s_lshr_b32(s[52], s[49], 1))
|
||||
k.emit(s_lshr_b32(s[52], s[46], 1))
|
||||
k.emit(s_mul_i32(s[52], s[52], 8))
|
||||
k.emit(s_and_b32(s[53], s[49], 1))
|
||||
k.emit(s_and_b32(s[53], s[46], 1))
|
||||
k.emit(s_mul_i32(s[53], s[53], 2))
|
||||
k.emit(s_add_u32(s[52], s[52], s[53]))
|
||||
k.emit(s_mul_i32(s[53], s[47], LIT, 256))
|
||||
k.emit(s_add_u32(s[52], s[52], s[53]))
|
||||
k.emit(s_mul_i32(s[52], s[41], s[52]))
|
||||
k.emit(s_mul_i32(s[52], s[37], s[52]))
|
||||
k.emit(v_add_u32_e32(v[212], s[52], v[212]))
|
||||
k.emit(s_mul_i32(s[52], s[41], 32))
|
||||
k.emit(s_mul_i32(s[52], s[37], 32))
|
||||
for i in range(7):
|
||||
k.emit(v_add_u32_e32(v[213 + i * 1], s[52], v[212 + i * 1]))
|
||||
k.emit(s_mul_i32(s[59], LIT, s[49], 1056))
|
||||
k.emit(s_mul_i32(s[59], LIT, s[46], 1056))
|
||||
k.emit(s_add_u32(s[59], LIT, s[59], 4096))
|
||||
k.emit(v_and_b32_e32(v[4], 15))
|
||||
k.emit(v_lshrrev_b32_e32(v[5], 3, v[4]))
|
||||
@@ -2396,35 +2349,35 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(v_add_u32_e32(v[221], LIT, v[220], 33792))
|
||||
k.emit(v_lshlrev_b32_e32(v[222], 2))
|
||||
k.emit(s_mul_i32(s[52], s[47], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[49], 32))
|
||||
k.emit(s_mul_i32(s[53], s[46], 32))
|
||||
k.emit(s_add_i32(s[52], s[53], s[52]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[36]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[39]))
|
||||
k.emit(v_add_u32_e32(v[222], s[53], v[222]))
|
||||
k.emit(s_mul_i32(s[53], LIT, s[36], 128))
|
||||
k.emit(s_mul_i32(s[53], LIT, s[39], 128))
|
||||
k.emit(v_add_u32_e32(v[223], s[53], v[222]))
|
||||
k.emit(s_mul_i32(s[60], s[49], LIT, 256))
|
||||
k.emit(s_mul_i32(s[60], s[46], LIT, 256))
|
||||
k.emit(s_add_i32(s[60], s[60], 0))
|
||||
k.emit(v_lshlrev_b32_e32(v[224], 2))
|
||||
k.emit(v_add_u32_e32(v[224], 0, v[224]))
|
||||
k.emit(v_lshlrev_b32_e32(v[225], 4))
|
||||
k.emit(s_mul_i32(s[52], s[46], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[49], 64))
|
||||
k.emit(s_mul_i32(s[52], s[49], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[46], 64))
|
||||
k.emit(s_add_u32(s[52], s[52], s[53]))
|
||||
k.emit(s_mul_i32(s[52], s[52], s[42]))
|
||||
k.emit(s_mul_i32(s[52], s[52], s[38]))
|
||||
k.emit(v_add_u32_e32(v[225], s[52], v[225]))
|
||||
k.emit(s_mul_i32(s[52], 16, s[42]))
|
||||
k.emit(s_mul_i32(s[52], 16, s[38]))
|
||||
k.emit(v_add_u32_e32(v[226], s[52], v[225]))
|
||||
k.emit(v_add_u32_e32(v[227], s[52], v[226]))
|
||||
k.emit(v_add_u32_e32(v[228], s[52], v[227]))
|
||||
for i in range(4):
|
||||
k.emit(v_add_u32_e32(v[229 + i * 1], LIT, v[225 + i * 1], 1024))
|
||||
k.emit(v_lshlrev_b32_e32(v[233], 2))
|
||||
k.emit(s_mul_i32(s[52], s[46], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[49], 64))
|
||||
k.emit(s_mul_i32(s[52], s[49], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[46], 64))
|
||||
k.emit(s_add_i32(s[52], s[53], s[52]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[37]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[40]))
|
||||
k.emit(v_add_u32_e32(v[233], s[53], v[233]))
|
||||
k.emit(s_mul_i32(s[52], 32, s[37]))
|
||||
k.emit(s_mul_i32(s[52], 32, s[40]))
|
||||
k.emit(v_add_u32_e32(v[234], s[52], v[233]))
|
||||
k.emit(s_mov_b32(s[61], LIT, 128))
|
||||
k.emit(s_mov_b32(s[62], LIT, 2048))
|
||||
@@ -2510,18 +2463,18 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(ds_read_b32(v[201], v[224], v[0], v[0], 0, 0, 1))
|
||||
k.emit(ds_read_b32(v[202], v[224], v[0], v[0], 0, 0, 2))
|
||||
k.emit(ds_read_b32(v[203], v[224], v[0], v[0], 0, 0, 3))
|
||||
k.emit(s_lshl_b32(s[40], s[40], 1))
|
||||
k.emit(s_lshl_b32(s[36], s[36], 1))
|
||||
k.emit(s_mul_i32(s[52], s[47], LIT, 256))
|
||||
k.emit(s_mul_hi_u32(s[53], s[52], s[40]))
|
||||
k.emit(s_mul_hi_u32(s[53], s[52], s[36]))
|
||||
k.emit(s_add_u32(s[5], s[5], s[53]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[40]))
|
||||
k.emit(s_mul_i32(s[53], s[52], s[36]))
|
||||
k.emit(s_add_u32(s[4], s[4], s[53]))
|
||||
k.emit(s_addc_u32(s[5], 0, s[5]))
|
||||
k.emit(s_sub_i32(s[52], s[43], s[52]))
|
||||
k.emit(s_mul_i32(s[52], s[52], s[40]))
|
||||
k.emit(s_mul_i32(s[52], s[52], s[36]))
|
||||
k.emit(s_mov_b32(s[6], s[52]))
|
||||
k.emit(v_and_b32_e64(v[235], v[0], 15))
|
||||
k.emit(v_mul_lo_u32(v[235], v[235], s[40]))
|
||||
k.emit(v_mul_lo_u32(v[235], v[235], s[36]))
|
||||
k.emit(v_lshrrev_b32_e32(v[4], 5))
|
||||
k.emit(v_mul_i32_i24_e32(v[4], 16, v[4]))
|
||||
k.emit(v_add_u32_e32(v[235], v[4], v[235]))
|
||||
@@ -2529,12 +2482,12 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(v_and_b32_e32(v[4], 1, v[4]))
|
||||
k.emit(v_mul_i32_i24_e32(v[4], 32, v[4]))
|
||||
k.emit(v_add_u32_e32(v[235], v[4], v[235]))
|
||||
k.emit(s_mul_i32(s[52], s[46], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[49], 64))
|
||||
k.emit(s_mul_i32(s[52], s[49], LIT, 256))
|
||||
k.emit(s_mul_i32(s[53], s[46], 64))
|
||||
k.emit(s_add_i32(s[52], s[52], s[53]))
|
||||
k.emit(s_lshl_b32(s[52], s[52], 1))
|
||||
k.emit(v_add_u32_e32(v[235], s[52], v[235]))
|
||||
k.emit(s_mul_i32(s[53], s[40], 16))
|
||||
k.emit(s_mul_i32(s[53], s[36], 16))
|
||||
for i in range(15):
|
||||
k.emit(v_add_u32_e64(v[236 + i * 1], v[235 + i * 1], s[53]))
|
||||
k.emit(s_mov_b32(s[50], 0))
|
||||
@@ -2543,7 +2496,7 @@ def build_kernel(M: int, N: int, K: int, tile_m: int, tile_n: int):
|
||||
k.emit(s_cmp_lt_u32(LIT, s[51], 512 + i * -256))
|
||||
k.emit(s_cselect_b32(s[61 + i * 1], s[61 + i * 1], 0))
|
||||
k.emit(s_cselect_b32(s[63 + i * 1], s[63 + i * 1], 0))
|
||||
k.emit(s_cmp_lt_i32(s[49], 2))
|
||||
k.emit(s_cmp_lt_i32(s[46], 2))
|
||||
k.emit(s_cbranch_scc0(1367), target='L2_25B8')
|
||||
k.label('L2_105C')
|
||||
k.emit(s_waitcnt(122))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import functools, pathlib
|
||||
from dataclasses import replace
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop.ops import shape_to_shape_arg
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.runtime.support.compiler_amd import HIPCCCompiler
|
||||
|
||||
FP8_MAX = 448.0
|
||||
@@ -12,13 +10,14 @@ NUM_WG, THREADS_PER_WG = 1024, 256
|
||||
@functools.cache
|
||||
def _local_abs_max_fxn(x_p, device):
|
||||
x = Tensor(x_p, device=device)
|
||||
inner = Tensor(x.uop.replace(src=(shape_to_shape_arg(x.uop.shard_shape),), arg=replace(x.uop.arg, axis=None))) if x.uop.axis is not None else x
|
||||
inner = Tensor(x.uop.src[0]) if x.uop.axis is not None else x # the per-shard view of the flat param
|
||||
return (inner.abs().max(),)
|
||||
|
||||
def local_abs_max(x:Tensor) -> Tensor:
|
||||
param = x.as_param(0)
|
||||
fxn = _local_abs_max_fxn(param.uop, x.device)
|
||||
return Tensor(fxn[0].uop.call(x.uop).gettuple(0))
|
||||
call = fxn[0].uop.call(x.uop)
|
||||
return Tensor(call.src[-1].after(call))
|
||||
|
||||
def shard_shape(shape:tuple, axis:int, ndev:int) -> list:
|
||||
s = list(shape)
|
||||
|
||||
@@ -13,12 +13,12 @@ def _rmsnorm_fwd_fxn(x_in_p, eps, device):
|
||||
return rmsnorm_fwd(Tensor(x_in_p, device=device), eps)
|
||||
|
||||
def _rmsnorm_bwd(grad:UOp, call:UOp) -> tuple:
|
||||
x_normed = Tensor(call.gettuple(0)).float()
|
||||
do_float = Tensor(grad).float()
|
||||
d_x = Tensor(call.gettuple(1)) * (do_float - x_normed * (do_float * x_normed).mean(-1, keepdim=True))
|
||||
x_normed, rrms = call.returned_outputs(call.src[-2:])
|
||||
x_normed_f, do_float = Tensor(x_normed).float(), Tensor(grad).float()
|
||||
d_x = Tensor(rrms) * (do_float - x_normed_f * (do_float * x_normed_f).mean(-1, keepdim=True))
|
||||
return (d_x.cast(call.src[1].dtype).uop,)
|
||||
|
||||
def rmsnorm(x_in:Tensor, eps:float) -> tuple[Tensor, Tensor]:
|
||||
fxn = _rmsnorm_fwd_fxn(x_in.as_param(0).uop, eps, x_in.device)
|
||||
call = UOp.maketuple(fxn[0].uop, fxn[1].uop).call(x_in.uop, grad_fxn=_rmsnorm_bwd)
|
||||
return Tensor(call.gettuple(0)), Tensor(call.gettuple(1))
|
||||
call = UOp.call_outputs([fxn[0].uop, fxn[1].uop], x_in.uop, grad_fxn=_rmsnorm_bwd)
|
||||
return map(Tensor, call.returned_outputs(call.src[-2:]))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.helpers import NUM_CPU_THREADS
|
||||
from tinygrad.tensor import _to_np_dtype
|
||||
from tinygrad.nn.onnx import OnnxRunner, OnnxValue
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
ort_options = ort.SessionOptions()
|
||||
ort_options.log_severity_level = 3
|
||||
ort_options.intra_op_num_threads = NUM_CPU_THREADS.value
|
||||
|
||||
def get_example_inputs(graph_inputs:dict[str, OnnxValue], config={}):
|
||||
"""
|
||||
|
||||
Binary file not shown.
+6
-6
@@ -50,10 +50,10 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
|
||||
\toprule
|
||||
\textbf{Op} & \textbf{src} & \textbf{arg} & \textbf{Semantics} \\
|
||||
\midrule
|
||||
\op{Param} & $(\mathbf{s})$ & slot, dtype, device?, addrspace? &
|
||||
Placeholder with shape $\mathbf{s}$. Substituted in \op{Function}. \\[4pt]
|
||||
\op{Buffer} & $(\mathbf{s})$ & slot, dtype, device, addrspace &
|
||||
Concrete buffer slot with shape $\mathbf{s}$. If device is a tuple, it creates the fully sized buffer across multiple devices. \\
|
||||
\op{Param} & () & slot, dtype, size?, device?, addrspace? &
|
||||
Placeholder with flat storage of $\mathrm{size}$ elements. Substituted in \op{Call}. \\[4pt]
|
||||
\op{Buffer} & () & slot, dtype, size, device, addrspace &
|
||||
Concrete buffer slot with flat storage of $\mathrm{size}$ elements. \\
|
||||
\op{Const} & () & value, dtype &
|
||||
A scalar constant with shape $(\ )$. \\
|
||||
& & & Form vector consts with \op{Stack} \\
|
||||
@@ -256,9 +256,9 @@ Every UOp has a \textbf{dtype}, \textbf{shape}, \textbf{device}, \textbf{addrspa
|
||||
\toprule
|
||||
\textbf{Op} & \textbf{dtype} & \textbf{shape} & \textbf{device} & \textbf{min\_max} \\
|
||||
\midrule
|
||||
\op{Buffer} & from arg & from $\mathrm{src}[0]$ & from arg & dtype range \\
|
||||
\op{Buffer} & from arg & from arg ($\mathrm{size}$) & from arg & dtype range \\
|
||||
\op{Const} & from arg & $()$ & \textsc{null} & $[v, v]$ \\
|
||||
\op{Param} & from arg & from $\mathrm{src}[0]$ & from arg & from src or dtype range \\[3pt]
|
||||
\op{Param} & from arg & from arg ($\mathrm{size}$) & 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]$, 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]
|
||||
|
||||
@@ -258,6 +258,11 @@ class TestDoubleDType(TestDType):
|
||||
a = [2, 3, 4]
|
||||
np.testing.assert_allclose(func(Tensor(a, dtype=self.DTYPE)).numpy(), func(torch.tensor(a, dtype=torch.float64)), rtol=1e-12, atol=1e-12)
|
||||
|
||||
def test_float32_compare_selecting_float64(self):
|
||||
a = Tensor([1.0, 2.0, 5.0, 9.0], dtype=dtypes.float32)
|
||||
p, q = Tensor([10., 20., 30., 40.], dtype=self.DTYPE), Tensor([50., 60., 70., 80.], dtype=self.DTYPE)
|
||||
_test_op(lambda: (a < 3.0).where(p, q), self.DTYPE, [10., 20., 70., 80.])
|
||||
|
||||
def test_float64_to_float32_cast_inf(self):
|
||||
_test_op(lambda: Tensor([3.4e40, 3.4e38, 1, 0], dtype=dtypes.float64).cast(dtypes.float32),
|
||||
dtypes.float32, [float('inf'), 3.4e38, 1, 0])
|
||||
@@ -423,6 +428,11 @@ class TestDtypeUsage(unittest.TestCase):
|
||||
t = Tensor([[1, 2], [3, 4]], dtype=d)
|
||||
(t*t).max().item()
|
||||
|
||||
def test_where_float16_compare_to_const(self):
|
||||
# t > 0 is CMPLT(0, t): the float16 operand is on the right
|
||||
t = Tensor([-1.0, 1.0], dtype=dtypes.float16)
|
||||
np.testing.assert_equal((t > 0).where(Tensor.ones(2, dtype=dtypes.float16), Tensor.zeros(2, dtype=dtypes.float16)).numpy(), [0.0, 1.0])
|
||||
|
||||
@unittest.skipUnless(dtypes.bfloat16 in supported_dtypes, f"no bfloat16 on {Device.DEFAULT}")
|
||||
class TestOpsBFloat16(unittest.TestCase):
|
||||
def test_cast(self):
|
||||
|
||||
@@ -100,13 +100,6 @@ class TestEncodingsX86(unittest.TestCase):
|
||||
# vaddss xmm0, xmm0, xmm8
|
||||
self.assertEqual(bytes.fromhex(self.encode(add)), bytes.fromhex("C4 C1 7A 58 C0"))
|
||||
|
||||
# test ymm encoding
|
||||
def test_ymm_encoding(self):
|
||||
xmm0, xmm1 = def_reg(dtypes._uint256, XMM[0]), def_reg(dtypes._uint256, XMM[1])
|
||||
add = ins(X86Ops.VADDPS, dtypes._uint256, (xmm0, xmm1), XMM[0])
|
||||
# vaddps ymm0, ymm0, ymm1
|
||||
self.assertEqual(bytes.fromhex(self.encode(add)), bytes.fromhex("C5 FC 58 C1"))
|
||||
|
||||
# test encoding where register is in the immediate field
|
||||
def test_reg_in_imm_field(self):
|
||||
xmm0, xmm1, xmm2 = def_reg(dtypes.float32, XMM[0]), def_reg(dtypes.float32, XMM[1]), def_reg(dtypes.float32, XMM[2])
|
||||
|
||||
@@ -6,7 +6,7 @@ from test.helpers import assert_jit_cache_len, call_is_graph, not_support_multi_
|
||||
from test.unit.test_jit import _simple_test
|
||||
from tinygrad import Tensor, Variable, TinyJit, Device, dtypes
|
||||
from tinygrad.engine.jit import graph_class
|
||||
from tinygrad.helpers import JIT, DEV, GlobalCounters
|
||||
from tinygrad.helpers import JIT, DEV, GlobalCounters, HCQ2
|
||||
from tinygrad.uop.ops import Ops
|
||||
from tinygrad.renderer.isa.x86 import X86Renderer
|
||||
|
||||
@@ -235,6 +235,7 @@ class TestJitPrune(unittest.TestCase):
|
||||
assert_jit_cache_len(w2_prune, 1)
|
||||
|
||||
class TestJitFree(unittest.TestCase):
|
||||
@unittest.skipIf(HCQ2, "hcq2 keeps refs to intermediate buffers")
|
||||
def test_free_intermediates(self):
|
||||
ext_tensor = Tensor([1,24,23,45,1])
|
||||
@TinyJit
|
||||
|
||||
@@ -187,6 +187,13 @@ class TestMultiTensor(unittest.TestCase):
|
||||
a,b = jit_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
def test_allreduce_all2all_jit(self):
|
||||
with Context(ALL2ALL=2):
|
||||
jit_allreduce = TinyJit(_test_allreduce)
|
||||
for _ in range(5):
|
||||
a,b = jit_allreduce(Tensor.rand(256, 256))
|
||||
np.testing.assert_almost_equal(a.numpy(), b.numpy(), decimal=5)
|
||||
|
||||
def test_multitensor_jit_input(self):
|
||||
@TinyJit
|
||||
def f(x): return (x+1).contiguous().sum()
|
||||
|
||||
@@ -135,7 +135,7 @@ class TestNN(unittest.TestCase):
|
||||
def test_conv2d_same_padding_large_kernel(self):
|
||||
self._test_conv(Conv2d, torch.nn.Conv2d, BS=16, C1=16, DIMS=[28, 33], C2=32, K=9, S=1, P='same')
|
||||
def test_conv2d_same_padding_with_dilation(self):
|
||||
self._test_conv(Conv2d, torch.nn.Conv2d, BS=16, C1=3, DIMS=[28, 28], C2=32, K=3, S=1, P='same', D=3)
|
||||
self._test_conv(Conv2d, torch.nn.Conv2d, BS=16, C1=3, DIMS=[28, 31], C2=32, K=(3,5), S=1, P='same', D=(2,3))
|
||||
|
||||
def test_conv2d_same_padding_invalid_stride(self):
|
||||
self.assertRaises(ValueError, Conv2d, in_channels=16, out_channels=32, kernel_size=2, stride=2, padding='same')
|
||||
|
||||
@@ -359,6 +359,13 @@ class TestOps(unittest.TestCase):
|
||||
lambda x: torch.where(x > 0.5, 4, 2).type(torch.int32).permute((1, 0)),
|
||||
lambda x: (x > 0.5).where(4, 2).clone().permute((1, 0)), forward_only=True)
|
||||
|
||||
@unittest.skipIf(Device.DEFAULT == "WEBGPU", "software vulkan evaluates a NaN != x as false")
|
||||
def test_where_nan_cond(self):
|
||||
# a NaN compares false against everything except !=.
|
||||
for fxn in (lambda x: x<1, lambda x: x>1, lambda x: x!=1, lambda x: x==1):
|
||||
helper_test_op(None, lambda x,a,b: torch.where(fxn(x), a, b), lambda x,a,b: fxn(x).where(a, b), forward_only=True,
|
||||
vals=[[math.nan, 1.0, 2.0, -1.0], [10, 20, 30, 40], [-1, -2, -3, -4]])
|
||||
|
||||
def _test_cmp(self, fxn, reverse=True):
|
||||
# test different dtypes
|
||||
helper_test_op(None, fxn, fxn, forward_only=True, vals=[[0.,1,2], [2.,1,0]])
|
||||
@@ -2810,7 +2817,7 @@ class TestOps(unittest.TestCase):
|
||||
lambda x: Tensor.interpolate(x, size=out_sz, mode="linear"))
|
||||
|
||||
def test_interpolate_linear_corners_aligned(self):
|
||||
for in_sz, out_sz in [((52,),(29,)), ((29,),(52,))]:
|
||||
for in_sz, out_sz in [((52,),(29,)), ((29,),(52,)), ((29,),(1,))]:
|
||||
helper_test_op([(2,3)+in_sz],
|
||||
lambda x: torch.nn.functional.interpolate(x, size=out_sz, mode="linear", align_corners=True),
|
||||
lambda x: Tensor.interpolate(x, size=out_sz, mode="linear", align_corners=True))
|
||||
@@ -2963,6 +2970,10 @@ class TestOps(unittest.TestCase):
|
||||
data = [math.inf, -math.inf, math.nan]
|
||||
helper_test_op((), lambda: torch.tensor(data)[torch.tensor([0, 1, 2])], lambda: Tensor(data)[Tensor([0, 1, 2])])
|
||||
|
||||
def test_fancy_indexing_index_dtypes(self):
|
||||
helper_test_op((), lambda: torch.tensor([10., 20., 30., 40.])[torch.tensor([1, 2, 3, 0])],
|
||||
lambda: Tensor([10., 20., 30., 40.])[Tensor([1, 2, 3, 0], dtype=dtypes.uint8)])
|
||||
|
||||
@slow_test
|
||||
def test_slice_fancy_indexing_no_dim_collapse(self):
|
||||
a,b,c,d,e,i,j,k,o,p = self._get_index_randoms()
|
||||
|
||||
@@ -379,27 +379,30 @@ class TestCopyFolding(unittest.TestCase):
|
||||
check_schedule(a.clone(), 1, filter_sink=False)
|
||||
|
||||
def test_shrink_copy(self):
|
||||
a = Tensor.arange(4)
|
||||
view = a.shrink(((0, 2),))
|
||||
b = view.clone()
|
||||
run_linear(*check_schedule(b, 1, filter_sink=False))
|
||||
self.assertEqual(b.uop.base.buffer.size, 2)
|
||||
self.assertEqual(b.uop.numel(), 2)
|
||||
self.assertListEqual(b.tolist(), [0, 1])
|
||||
a = Tensor.arange(4).clone("CPU:1").realize()
|
||||
b = a.to("CPU:2").shrink(((1, 3),)).to("CPU:3")
|
||||
GlobalCounters.reset()
|
||||
run_linear(*check_schedule(b, 3, filter_sink=False))
|
||||
# extra E kernel, copy exactly 4 bytes
|
||||
self.assertEqual(GlobalCounters.global_mem, 4*4 + 2*4*2 + 2*4)
|
||||
self.assertListEqual(b.tolist(), [1, 2])
|
||||
|
||||
def test_expanded_copy(self):
|
||||
a = Tensor.arange(2)
|
||||
view = a.reshape(2, 1).expand(2, 2)
|
||||
b = view.clone()
|
||||
run_linear(*check_schedule(b, 1, filter_sink=False))
|
||||
self.assertEqual(b.uop.base.buffer.size, 4)
|
||||
self.assertEqual(b.uop.numel(), 4)
|
||||
self.assertListEqual(b.tolist(), [[0, 0], [1, 1]])
|
||||
a = Tensor.arange(4).clone("CPU:1").realize()
|
||||
b = a.to("CPU:2").reshape(4, 1).expand(4, 2).to("CPU:3")
|
||||
GlobalCounters.reset()
|
||||
run_linear(*check_schedule(b, 3, filter_sink=False))
|
||||
# TODO: expands before copy
|
||||
self.assertEqual(GlobalCounters.global_mem, 4*4 + (4*4 + 8*4) + 8*4)
|
||||
self.assertListEqual(b.tolist(), [[0, 0], [1, 1], [2, 2], [3, 3]])
|
||||
|
||||
def test_permuted_copy(self):
|
||||
a = Tensor.arange(4)
|
||||
b = a.reshape(2, 2).permute(1, 0)
|
||||
b.realize()
|
||||
a = Tensor.arange(4).clone("CPU:1").realize()
|
||||
b = a.to("CPU:2").reshape(2, 2).permute(1, 0).to("CPU:3")
|
||||
GlobalCounters.reset()
|
||||
run_linear(*check_schedule(b, 3, filter_sink=False))
|
||||
# permutes before copy
|
||||
self.assertEqual(GlobalCounters.global_mem, 4*4 + (4*4 + 4*4) + 4*4)
|
||||
self.assertListEqual(b.tolist(), [[0, 2], [1, 3]])
|
||||
|
||||
def test_permute_on_disk(self):
|
||||
|
||||
@@ -3,10 +3,10 @@ from unittest.mock import patch
|
||||
from tinygrad import Device, Tensor
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import getenv
|
||||
from tinygrad.helpers import HCQ2
|
||||
from tinygrad.runtime.support.hcq2 import HCQ_DEVS, all_devices_in
|
||||
|
||||
@unittest.skipUnless(getenv("HCQ2") and all_devices_in(Device.DEFAULT, HCQ_DEVS), "hcq2 device required")
|
||||
@unittest.skipUnless(HCQ2 and all_devices_in(Device.DEFAULT, HCQ_DEVS), "hcq2 device required")
|
||||
class TestHCQ2(unittest.TestCase):
|
||||
def test_copy_without_copy_queue(self):
|
||||
with patch.object(Device[Device.DEFAULT], "has_copy_queue", False):
|
||||
|
||||
@@ -185,7 +185,7 @@ class TestMultiScalarALU(unittest.TestCase):
|
||||
return (inner.sum(),)
|
||||
param = x.as_param(0)
|
||||
fxn = _fxn(param.uop, x.device)
|
||||
per_dev_scalar = Tensor(fxn[0].uop.call(x.uop).gettuple(0))
|
||||
per_dev_scalar = Tensor(fxn[0].uop.call(x.uop).returned_outputs[0])
|
||||
result = x * per_dev_scalar
|
||||
self.assertEqual(result.shape, (4, 4))
|
||||
self.assertEqual(result.uop.axis, 0)
|
||||
|
||||
@@ -441,7 +441,8 @@ class TestMovementOps(unittest.TestCase):
|
||||
self.assertEqual(result.op, Ops.INDEX)
|
||||
self.assertIs(result.src[0], src)
|
||||
self.assertEqual(result.shape, (4,))
|
||||
self.assertNotIn(Ops.RESHAPE, [u.op for u in result.toposort()])
|
||||
# the only RESHAPE is src itself: the view of the flat param, the extra reshape was folded into the INDEX
|
||||
self.assertEqual([u for u in result.toposort() if u.op is Ops.RESHAPE], [src])
|
||||
|
||||
def test_pm_mops_partial_reshape_index_suffix_mismatch_does_nothing(self):
|
||||
from tinygrad.schedule.prepare import pm_mops
|
||||
|
||||
@@ -84,11 +84,10 @@ class TestLowerIndexDtype(unittest.TestCase):
|
||||
|
||||
def test_reg_buffer_size_lowers(self):
|
||||
reg = UOp.placeholder((4,), dtypes.float, 0, addrspace=AddrSpace.REG)
|
||||
self.assertEqual(reg.src[0].dtype, dtypes.weakint)
|
||||
self.assertEqual(reg.arg.size, 4)
|
||||
lowered = graph_rewrite(reg.sink(), pm_lower_weak)
|
||||
self.assertTrue(all(u.op is Ops.CONST for u in lowered.backward_slice_with_self if u.dtype in dtypes.weaks),
|
||||
"lowering must resolve every weak width, except a typed literal's value half")
|
||||
self.assertEqual(next(u for u in lowered.backward_slice_with_self if u.op is Ops.BUFFER).src[0].dtype, dtypes.int)
|
||||
|
||||
class TestSafeCast(unittest.TestCase):
|
||||
def test_cast_folds(self):
|
||||
|
||||
@@ -228,8 +228,7 @@ class TestViz(unittest.TestCase):
|
||||
with save_viz() as viz:
|
||||
inner = UOp.const(3)
|
||||
call = UOp(Ops.CALL, src=(UOp(Ops.SINK, src=(inner,)),))
|
||||
func = UOp(Ops.FUNCTION, src=(UOp(Ops.TUPLE, src=(call,)),))
|
||||
graph_rewrite(func, TrackedPatternMatcher(pm.patterns), enter_calls=True)
|
||||
graph_rewrite(call, TrackedPatternMatcher(pm.patterns), enter_calls=True)
|
||||
details = list(viz.get_details(0, 0))
|
||||
self.assertTrue(details[-1]["change"], "viz replay should detect change inside CALL")
|
||||
|
||||
@@ -244,7 +243,7 @@ class TestViz(unittest.TestCase):
|
||||
self.assertEqual(len(lst), 1)
|
||||
graphs = [x["graph"] for x in viz.get_details(0, 0)]
|
||||
# const is always in the graph, client side hides exclude=True nodes by default
|
||||
self.assertEqual(list(graphs[0]), [id(a.src[0]), id(a), id(z), id(alu), id(y), id(sink)])
|
||||
self.assertEqual(list(graphs[0]), [id(a), id(z), id(alu), id(y), id(sink)])
|
||||
self.assertTrue(graphs[0][id(z)]["exclude"])
|
||||
self.assertTrue(graphs[0][id(y)]["exclude"])
|
||||
self.assertFalse(graphs[0][id(alu)]["exclude"])
|
||||
|
||||
+10
-5
@@ -52,6 +52,11 @@ class TestCall(unittest.TestCase):
|
||||
np.testing.assert_allclose(a.grad.numpy(), gt_a_grad, rtol=1e-5)
|
||||
np.testing.assert_allclose(b.grad.numpy(), gt_b_grad, rtol=1e-5)
|
||||
|
||||
def test_call_scalar_param_shape_mismatch(self):
|
||||
scalar_fxn = UOp.param(0, dtypes.float, ()) * 2
|
||||
with self.assertRaisesRegex(TypeError, "shape mismatch: expected scalar"):
|
||||
Tensor.call(Tensor.ones(2), fxn=scalar_fxn).realize()
|
||||
|
||||
def test_call_gemm(self):
|
||||
M, K, N = 4, 8, 4
|
||||
a = Tensor.randn(M, K)
|
||||
@@ -218,8 +223,8 @@ class TestCallSchedule(unittest.TestCase):
|
||||
a = Tensor.ones(3)
|
||||
x = f(a, UOp.variable("scale_a", 1, 100).bind(2))
|
||||
y = f(a, UOp.variable("scale_b", 1, 100).bind(3))
|
||||
fx = next(u for u in x.uop.toposort() if u.op is Ops.FUNCTION)
|
||||
fy = next(u for u in y.uop.toposort() if u.op is Ops.FUNCTION)
|
||||
fx = next(u for u in x.uop.toposort() if u.op is Ops.CALL and u.num_returned)
|
||||
fy = next(u for u in y.uop.toposort() if u.op is Ops.CALL and u.num_returned)
|
||||
self.assertEqual(fx.src[0].key, fy.src[0].key)
|
||||
np.testing.assert_equal(x.numpy(), [2, 2, 2])
|
||||
np.testing.assert_equal(y.numpy(), [3, 3, 3])
|
||||
@@ -246,9 +251,9 @@ class TestCallSchedule(unittest.TestCase):
|
||||
a = Tensor.empty(4, 8)
|
||||
b = Tensor.empty(4, 8)
|
||||
r0, r1 = f(a), f(b)
|
||||
# find the FUNCTION nodes
|
||||
c0 = next(u for u in r0.uop.toposort() if u.op is Ops.FUNCTION)
|
||||
c1 = next(u for u in r1.uop.toposort() if u.op is Ops.FUNCTION)
|
||||
# find the call nodes
|
||||
c0 = next(u for u in r0.uop.toposort() if u.op is Ops.CALL and u.num_returned)
|
||||
c1 = next(u for u in r1.uop.toposort() if u.op is Ops.CALL and u.num_returned)
|
||||
# the function bodies (src[0]) should have identical keys
|
||||
self.assertEqual(c0.src[0].key, c1.src[0].key)
|
||||
|
||||
|
||||
@@ -174,13 +174,13 @@ class TestFunction(unittest.TestCase):
|
||||
def test_name(self):
|
||||
@function
|
||||
def f(a:Tensor) -> Tensor: return a + 1
|
||||
assert f(Tensor([1])).uop.src[0].arg.name.endswith("f")
|
||||
assert f(Tensor([1])).uop.src[1].arg.name.endswith("f")
|
||||
|
||||
def test_method_name(self):
|
||||
class Foo:
|
||||
@function
|
||||
def __call__(self, x:Tensor) -> Tensor: return x + 1
|
||||
assert Foo()(Tensor([1])).uop.src[0].arg.name.endswith("Foo.__call__")
|
||||
assert Foo()(Tensor([1])).uop.src[1].arg.name.endswith("Foo.__call__")
|
||||
|
||||
def test_callable_instance(self):
|
||||
class Foo:
|
||||
@@ -189,7 +189,7 @@ class TestFunction(unittest.TestCase):
|
||||
foo = Foo()
|
||||
f = function(foo, allow_implicit=True)
|
||||
np.testing.assert_equal(f(Tensor([1,2,3])).numpy(), [11,22,33])
|
||||
assert f(Tensor([1,2,3])).uop.src[0].arg.name.endswith("Foo")
|
||||
assert f(Tensor([1,2,3])).uop.src[1].arg.name.endswith("Foo")
|
||||
|
||||
def test_iadd(self):
|
||||
@function
|
||||
|
||||
+95
-4
@@ -5,17 +5,33 @@ from tinygrad.llm.gguf import _ggml_iq_grid, ggml_data_to_tensor, gguf_load
|
||||
from tinygrad.runtime.autogen import ggml_common as _ggml
|
||||
import numpy as np
|
||||
from gguf import GGUFReader, GGUFValueType, GGMLQuantizationType, GGML_QUANT_SIZES, dequantize, quantize
|
||||
from gguf.quants import IQ2_S, IQ3_S, IQ3_XXS
|
||||
from gguf.quants import IQ1_S, IQ2_S, IQ2_XS, IQ2_XXS, IQ3_S, IQ3_XXS
|
||||
|
||||
ggml_test_block_count = 4
|
||||
supported_dtypes = Device[Device.DEFAULT].renderer.supported_dtypes()
|
||||
|
||||
class TestGGUFTables(unittest.TestCase):
|
||||
def test_iq2_xxs_grid_matches_gguf_py(self):
|
||||
IQ2_XXS.init_grid()
|
||||
grid = _ggml_iq_grid(Device.DEFAULT, _ggml.iq2xxs_grid, (256, 8)).numpy()
|
||||
np.testing.assert_equal(grid, IQ2_XXS.grid.reshape(256, 8))
|
||||
|
||||
def test_iq2_xs_grid_matches_gguf_py(self):
|
||||
IQ2_XS.init_grid()
|
||||
grid = _ggml_iq_grid(Device.DEFAULT, _ggml.iq2xs_grid, (512, 8)).numpy()
|
||||
np.testing.assert_equal(grid, IQ2_XS.grid.reshape(512, 8))
|
||||
|
||||
def test_iq2_s_grid_matches_gguf_py(self):
|
||||
IQ2_S.init_grid()
|
||||
grid = _ggml_iq_grid(Device.DEFAULT, _ggml.iq2s_grid, (1024, 8)).numpy()
|
||||
np.testing.assert_equal(grid, IQ2_S.grid.reshape(1024, 8))
|
||||
|
||||
def test_iq1_s_grid_matches_gguf_py(self):
|
||||
IQ1_S.init_grid()
|
||||
grid = _ggml_iq_grid(Device.DEFAULT, _ggml.iq1s_grid, (2048, 8)).numpy()
|
||||
grid = np.where(grid > 127, grid - 256, grid)
|
||||
np.testing.assert_equal(grid, IQ1_S.grid.reshape(2048, 8))
|
||||
|
||||
def test_iq3_xxs_grid_matches_gguf_py(self):
|
||||
IQ3_XXS.init_grid()
|
||||
grid = _ggml_iq_grid(Device.DEFAULT, _ggml.iq3xxs_grid, (256, 4)).numpy()
|
||||
@@ -39,6 +55,56 @@ class TestGGUF(unittest.TestCase):
|
||||
expected = np.arange(1, 33, dtype=np.float32) * 2.0
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 32, GGMLQuantizationType.Q8_0.value).numpy().flatten(), expected)
|
||||
|
||||
def test_dequantization_q2_k_hardcoded(self):
|
||||
# Q2_K: scales[16] + qs[64] + d(fp16) + dmin(fp16). 16 sub-blocks of 16, x = d*(scale&0xF)*q - dmin*(scale>>4)
|
||||
scales, qs = bytes([0x11]*16), bytes([0x55]*64) # scale=1, min=1; qs=0x55 -> 2-bit quants of 1
|
||||
d, dmin = np.float16(1.0).tobytes(), np.float16(0.0).tobytes()
|
||||
block = np.frombuffer(scales + qs + d + dmin, dtype=np.uint8).copy()
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 10).numpy().flatten(), np.ones(256, dtype=np.float32))
|
||||
|
||||
def test_dequantization_q3_k_hardcoded(self):
|
||||
# Q3_K: hmask[32] + qs[64] + scales[12] + d(fp16). 16 sub-blocks of 16, x = d * (scale-32) * (q - (hbit?0:4))
|
||||
# 6-bit scales 32..47 so (scale-32) = 0..15; qs=0x55 -> 2-bit quants of 1; d=1.0
|
||||
scales = bytes([0x80, 0x91, 0xA2, 0xB3, 0xC4, 0xD5, 0xE6, 0xF7, 0xAA, 0xAA, 0xAA, 0xAA])
|
||||
d = np.float16(1.0).tobytes()
|
||||
qs, ones = bytes([0x55]*64), np.ones(16, dtype=np.float32)
|
||||
# hmask all-ones: high bit set, q=1; hmask zeros: subtract 4, q=-3
|
||||
for hmask, q in ((bytes([0xFF]*32), 1.0), (bytes([0x00]*32), -3.0)):
|
||||
block = np.frombuffer(hmask + qs + scales + d, dtype=np.uint8).copy()
|
||||
expected = np.concatenate([q * s * ones for s in range(16)])
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 11).numpy().flatten(), expected)
|
||||
|
||||
def test_dequantization_iq2_xxs_hardcoded(self):
|
||||
# IQ2_XXS: d + 8 groups of (4 grid bytes + uint32 signs/scale). grid[0]=all 0x08, scale=0, signs=0
|
||||
# db = 1.0 * (0.5 + 0) * 0.25 = 0.125; 0.125 * 8 = 1.0
|
||||
block = np.frombuffer(np.float16(1.0).tobytes() + bytes(64), dtype=np.uint8).copy()
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 16).numpy().flatten(), np.ones(256, dtype=np.float32))
|
||||
|
||||
def test_dequantization_iq2_xs_hardcoded(self):
|
||||
# IQ2_XS: d + 32 uint16 qs + 8 scale bytes. qs=0 -> grid[0]=all 0x08, signs=0; scales=0
|
||||
block = np.frombuffer(np.float16(1.0).tobytes() + bytes(64) + bytes(8), dtype=np.uint8).copy()
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 17).numpy().flatten(), np.ones(256, dtype=np.float32))
|
||||
|
||||
def test_dequantization_iq1_s_hardcoded(self):
|
||||
# IQ1_S: d + qs[32] + qh[16]. qs=qh=0 -> grid[0]=all -1, scale=1, delta=+0.125 -> -0.875
|
||||
block = np.frombuffer(np.float16(1.0).tobytes() + bytes(48), dtype=np.uint8).copy()
|
||||
expected = np.full(256, -0.875, dtype=np.float32)
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 19).numpy().flatten(), expected)
|
||||
|
||||
def test_dequantization_iq1_m_hardcoded(self):
|
||||
# IQ1_M: qs[32] + qh[16] + scales[8]. f16 1.0=0x3C00 packed in high nibbles; qs=qh=0 -> -0.875
|
||||
scales = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x30])
|
||||
block = np.frombuffer(bytes(48) + scales, dtype=np.uint8).copy()
|
||||
expected = np.full(256, -0.875, dtype=np.float32)
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 256, 29).numpy().flatten(), expected)
|
||||
|
||||
def test_dequantization_iq4_nl_hardcoded(self):
|
||||
# IQ4_NL: 2-byte fp16 scale + 16 packed bytes. low nibbles first, then high
|
||||
lut = list(_ggml.kvalues_iq4nl)
|
||||
block = np.frombuffer(np.float16(1.0).tobytes() + bytes(range(16)), dtype=np.uint8).copy()
|
||||
expected = np.array(lut + [lut[0]]*16, dtype=np.float32)
|
||||
np.testing.assert_equal(ggml_data_to_tensor(Tensor(block), 32, 20).numpy().flatten(), expected)
|
||||
|
||||
def test_dequantization_mxfp4_hardcoded(self):
|
||||
# MXFP4: 1 byte shared exponent E + 16 packed bytes (32 x 4-bit values)
|
||||
# nibble: bit3=sign, bit2:1=exp, bit0=mant; E=128 gives scale=1.0
|
||||
@@ -52,13 +118,20 @@ class TestGGUF(unittest.TestCase):
|
||||
def test_dequantization_q5_0(self): self._test_dequantization(GGMLQuantizationType.Q5_0)
|
||||
def test_dequantization_q5_1(self): self._test_dequantization(GGMLQuantizationType.Q5_1)
|
||||
def test_dequantization_q8_0(self): self._test_dequantization(GGMLQuantizationType.Q8_0)
|
||||
def test_dequantization_q2_k(self): self._test_dequantization(GGMLQuantizationType.Q2_K)
|
||||
def test_dequantization_q3_k(self): self._test_dequantization(GGMLQuantizationType.Q3_K)
|
||||
def test_dequantization_q4_k(self): self._test_dequantization(GGMLQuantizationType.Q4_K)
|
||||
def test_dequantization_q5_k(self): self._test_dequantization(GGMLQuantizationType.Q5_K)
|
||||
def test_dequantization_q6_k(self): self._test_dequantization(GGMLQuantizationType.Q6_K)
|
||||
def test_dequantization_iq2_xxs(self): self._test_dequantization(GGMLQuantizationType.IQ2_XXS)
|
||||
def test_dequantization_iq2_xs(self): self._test_dequantization(GGMLQuantizationType.IQ2_XS)
|
||||
def test_dequantization_iq3_xxs(self): self._test_dequantization(GGMLQuantizationType.IQ3_XXS)
|
||||
def test_dequantization_iq1_s(self): self._test_dequantization(GGMLQuantizationType.IQ1_S)
|
||||
def test_dequantization_iq4_nl(self): self._test_dequantization(GGMLQuantizationType.IQ4_NL)
|
||||
def test_dequantization_iq3_s(self): self._test_dequantization(GGMLQuantizationType.IQ3_S)
|
||||
def test_dequantization_iq2_s(self): self._test_dequantization(GGMLQuantizationType.IQ2_S)
|
||||
def test_dequantization_iq4_xs(self): self._test_dequantization(GGMLQuantizationType.IQ4_XS)
|
||||
def test_dequantization_iq1_m(self): self._test_dequantization(GGMLQuantizationType.IQ1_M)
|
||||
def test_dequantization_mxfp4(self): self._test_dequantization(GGMLQuantizationType.MXFP4)
|
||||
@unittest.skipUnless(dtypes.bfloat16 in supported_dtypes, "Backend must support bfloat16")
|
||||
def test_dequantization_bf16(self): self._test_dequantization(GGMLQuantizationType.BF16)
|
||||
@@ -203,12 +276,23 @@ class TestGGUFGEMV(unittest.TestCase):
|
||||
q_data = rng.integers(0, 256, size=n_blocks * type_size, dtype=np.uint8).reshape(n_blocks, type_size)
|
||||
scales = np.float16(rng.standard_normal(n_blocks * 4)).view(np.uint8).reshape(n_blocks, -1)
|
||||
if qtype in (GGMLQuantizationType.Q5_0, GGMLQuantizationType.Q8_0,
|
||||
GGMLQuantizationType.IQ3_XXS,
|
||||
GGMLQuantizationType.IQ2_S,
|
||||
GGMLQuantizationType.IQ2_XXS, GGMLQuantizationType.IQ2_XS,
|
||||
GGMLQuantizationType.IQ3_XXS, GGMLQuantizationType.IQ4_NL,
|
||||
GGMLQuantizationType.IQ1_S, GGMLQuantizationType.IQ2_S,
|
||||
GGMLQuantizationType.IQ3_S, GGMLQuantizationType.IQ4_XS): q_data[:, :2] = scales[:, :2] # d at offset 0
|
||||
elif qtype in (GGMLQuantizationType.Q5_1, GGMLQuantizationType.Q4_K, GGMLQuantizationType.Q5_K):
|
||||
q_data[:, :4] = scales[:, :4] # d, m/dmin at offset 0
|
||||
elif qtype == GGMLQuantizationType.Q6_K: q_data[:, -2:] = scales[:, :2] # d at end
|
||||
elif qtype == GGMLQuantizationType.Q2_K: q_data[:, -4:] = scales[:, :4] # d, dmin at end
|
||||
elif qtype in (GGMLQuantizationType.Q6_K, GGMLQuantizationType.Q3_K): q_data[:, -2:] = scales[:, :2] # d at end
|
||||
elif qtype == GGMLQuantizationType.IQ1_M:
|
||||
s = np.float16(rng.standard_normal(n_blocks)).view(np.uint16)
|
||||
sc = q_data[:, -8:].copy().view(np.uint16).reshape(n_blocks, 4)
|
||||
sc &= np.uint16(0x0FFF)
|
||||
sc[:, 0] |= (s & np.uint16(0x000F)) << 12
|
||||
sc[:, 1] |= (s & np.uint16(0x00F0)) << 8
|
||||
sc[:, 2] |= (s & np.uint16(0x0F00)) << 4
|
||||
sc[:, 3] |= (s & np.uint16(0xF000))
|
||||
q_data[:, -8:] = sc.reshape(n_blocks, -1).view(np.uint8)
|
||||
elif qtype == GGMLQuantizationType.MXFP4: q_data[:, 0] = rng.integers(120, 136, size=n_blocks, dtype=np.uint8) # constrain byte0
|
||||
q_data = q_data.flatten()
|
||||
ref = dequantize(q_data, qtype).reshape(rows, cols)
|
||||
@@ -235,13 +319,20 @@ class TestGGUFGEMV(unittest.TestCase):
|
||||
def test_gguf_gemv_q8_0(self): self._test_gguf_gemv(GGMLQuantizationType.Q8_0)
|
||||
def test_gguf_gemv_q5_0(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_0)
|
||||
def test_gguf_gemv_q5_1(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_1)
|
||||
def test_gguf_gemv_q2_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q2_K)
|
||||
def test_gguf_gemv_q3_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q3_K)
|
||||
def test_gguf_gemv_q4_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q4_K)
|
||||
def test_gguf_gemv_q5_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q5_K)
|
||||
def test_gguf_gemv_q6_k(self): self._test_gguf_gemv(GGMLQuantizationType.Q6_K)
|
||||
def test_gguf_gemv_iq2_xxs(self): self._test_gguf_gemv(GGMLQuantizationType.IQ2_XXS)
|
||||
def test_gguf_gemv_iq2_xs(self): self._test_gguf_gemv(GGMLQuantizationType.IQ2_XS)
|
||||
def test_gguf_gemv_iq3_xxs(self): self._test_gguf_gemv(GGMLQuantizationType.IQ3_XXS)
|
||||
def test_gguf_gemv_iq1_s(self): self._test_gguf_gemv(GGMLQuantizationType.IQ1_S)
|
||||
def test_gguf_gemv_iq4_nl(self): self._test_gguf_gemv(GGMLQuantizationType.IQ4_NL)
|
||||
def test_gguf_gemv_iq3_s(self): self._test_gguf_gemv(GGMLQuantizationType.IQ3_S)
|
||||
def test_gguf_gemv_iq2_s(self): self._test_gguf_gemv(GGMLQuantizationType.IQ2_S)
|
||||
def test_gguf_gemv_iq4_xs(self): self._test_gguf_gemv(GGMLQuantizationType.IQ4_XS)
|
||||
def test_gguf_gemv_iq1_m(self): self._test_gguf_gemv(GGMLQuantizationType.IQ1_M)
|
||||
def test_gguf_gemv_mxfp4(self): self._test_gguf_gemv(GGMLQuantizationType.MXFP4)
|
||||
@unittest.skipUnless(dtypes.bfloat16 in supported_dtypes, "Backend must support bfloat16")
|
||||
def test_gguf_gemv_bf16(self): self._test_gguf_gemv(GGMLQuantizationType.BF16)
|
||||
|
||||
@@ -391,7 +391,15 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
sink = graph_rewrite(sink, pm_number_params, ctx=[num_params], name="number params with -1", walk=True)
|
||||
|
||||
if VIZ: graph_rewrite(sink, PatternMatcher([]), name="View Output AST")
|
||||
if SPEC: type_verify(sink, spec_program)
|
||||
if SPEC:
|
||||
import os
|
||||
if os.environ.get("DBGTV"):
|
||||
try: type_verify(sink, spec_program)
|
||||
except RuntimeError:
|
||||
from tinygrad.uop.render import print_uops
|
||||
print_uops(list(sink.toposort()))
|
||||
raise
|
||||
else: type_verify(sink, spec_program)
|
||||
|
||||
# return the rewritten sink
|
||||
return sink
|
||||
|
||||
@@ -140,8 +140,8 @@ def f2f_store(st, idx, val, fr:DType, to:DType):
|
||||
pm_long_decomp: PatternMatcher = PatternMatcher([
|
||||
# the decomp's own bottom-up rewrite can mint bare consts mid-flight: word splitting commits them at the long sibling's dtype
|
||||
(UPat(GroupOp.All, name='x'), lambda x: commit_weak_consts(x, next((s.dtype for s in x.src if s.dtype in l2i_dt), None))),
|
||||
(UPat(GroupOp.Defines, tuple(l2i_dt.keys()), src=(UPat.var("sz"),), name="x"), lambda x,sz:
|
||||
UOp(x.op, src=(sz*2,), arg=replace(x.arg, dtype=l2i_dt[x.dtype]), tag=x.tag)),
|
||||
(UPat(GroupOp.Defines, tuple(l2i_dt.keys()), name="x"), lambda x:
|
||||
UOp(x.op, arg=replace(x.arg, dtype=l2i_dt[x.dtype], size=None if x.arg.size is None else x.arg.size*2), tag=x.tag)),
|
||||
(UPat(Ops.INDEX, tuple(l2i_dt.keys()), name='x'), lambda x:
|
||||
reindex(x, x.tag[0]).replace(tag=None) if x.tag is not None else None),
|
||||
(UPat(Ops.STORE, src=(UPat.var('idx', tuple(l2i_dt.keys())), UPat.var('val')), name='st'), lambda st,idx,val:
|
||||
|
||||
@@ -73,7 +73,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
subs = {}
|
||||
for r in s_topo:
|
||||
# look for local INDEXes that are not used in the GLOBAL store, then add them as an INVALID
|
||||
if r.op is Ops.STORE and (idx := r.src[0]).src[0].addrspace == AddrSpace.GLOBAL:
|
||||
if r.op is Ops.STORE and len((idx := r.src[0]).src) and idx.src[0].addrspace == AddrSpace.GLOBAL:
|
||||
missing_locals = [all_ranges[rng] for rng in local_dims if all_ranges[rng] not in idx.ranges]
|
||||
if len(missing_locals):
|
||||
assert len(idx.src) == 2, "index has 2 sources"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import itertools, functools
|
||||
from collections import defaultdict
|
||||
from dataclasses import replace
|
||||
from tinygrad.dtype import dtypes, AddrSpace, Invalid, DType
|
||||
from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat, GroupOp, shape_to_shape_arg, graph_rewrite
|
||||
from tinygrad.uop.ops import UOp, Ops, PatternMatcher, UPat, GroupOp, graph_rewrite
|
||||
from tinygrad.uop.symbolic import uop_given_valid, parse_valid, invalid_gate, sym
|
||||
from tinygrad.helpers import getenv, IMAGE, OSX, ceildiv, is_image_shape
|
||||
from tinygrad.renderer import Renderer
|
||||
@@ -85,7 +86,8 @@ def transform_to_image(ctx, buf:UOp, x:UOp) -> UOp|None:
|
||||
if len(cands) == 0: return None
|
||||
# and tiebreak with indexing complexity (ie. number of nodes)
|
||||
h, w, cidx = cands[0] if len(cands) == 1 else min(cands, key=lambda cand: len(cand[2].index(1).simplify().backward_slice))
|
||||
buf = buf.replace(src=(shape_to_shape_arg((h, w, 4)),))
|
||||
# the image dims are stored in the param's arg, the size stays the flat buffer len
|
||||
buf = buf.replace(arg=replace(buf.arg, image=(h, w)))
|
||||
shapes[buf.arg.slot] = (h, w)
|
||||
if valid.op is not Ops.CONST or valid.val is not True:
|
||||
return buf.index(cidx.src[1].valid(valid), cidx.src[0].valid(valid))
|
||||
|
||||
@@ -120,8 +120,6 @@ class DTypes:
|
||||
uint32: Final[DType] = DType.new(6, 32, "unsigned int", 'I')
|
||||
int64: Final[DType] = DType.new(7, 64, "long", 'q')
|
||||
uint64: Final[DType] = DType.new(8, 64, "unsigned long", 'Q')
|
||||
_uint128: Final[DType] = DType.new(8, 128, "uint128", None)
|
||||
_uint256: Final[DType] = DType.new(8, 256, "uint256", None)
|
||||
weakfloat: Final[DType] = DType.new(9, 800, "weakfloat", None)
|
||||
fp8e4m3: Final[DType] = DType.new(10, 8, "float8_e4m3", None)
|
||||
fp8e5m2: Final[DType] = DType.new(11, 8, "float8_e5m2", None)
|
||||
|
||||
@@ -67,7 +67,7 @@ def jit_lower(linear:UOp, held_bufs:set[UOp], input_uops:list[UOp]) -> UOp:
|
||||
if VIZ: graph_rewrite(linear, PatternMatcher([]), name="View captured linear")
|
||||
|
||||
# parametrize input buffers: map each input buffer UOp to a PARAM with the correct slot index
|
||||
linear = linear.substitute({u: UOp.param(i, u.dtype, u.shape, u.device) for i,u in enumerate(input_uops)}, walk=True)
|
||||
linear = linear.substitute({u: UOp.param(i, u.dtype, u.max_numel(), u.device) for i,u in enumerate(input_uops)}, walk=True)
|
||||
linear = memory_plan_rewrite(linear, held_bufs)
|
||||
linear = compile_linear(linear, beam=getenv("JITBEAM", BEAM.value))
|
||||
if JIT < 2: linear = graph_split_rewrite(linear, max_batch_size=JIT_BATCH_SIZE.value)
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
from typing import cast, Iterator, Any, Sequence
|
||||
import random, itertools, math, weakref, array, decimal
|
||||
from dataclasses import dataclass, replace, field
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansipad, all_int, prod, flatten, Context, getenv, to_tuple, tqdm, dedup
|
||||
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events, perf_counter_us
|
||||
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansipad, all_int, prod, flatten, Context, to_tuple, tqdm, dedup
|
||||
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, HCQ2, PROFILE, ProfilePointEvent, cpu_events, perf_counter_us
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, AxisType, sym_infer, graph_rewrite, ProgramInfo
|
||||
from tinygrad.device import Device, Buffer, MultiBuffer, ProfileGraphEntry
|
||||
from tinygrad.dtype import dtypes
|
||||
@@ -305,17 +305,17 @@ pm_exec = PatternMatcher([
|
||||
(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="validate", name="ast"),), name="call", allow_any_len=True), exec_validate),
|
||||
])
|
||||
|
||||
if getenv("HCQ2"): from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link, HCQ_RUNTIME_DEV # noqa: E402 # down here, hcq2 imports realize
|
||||
from tinygrad.runtime.support.hcq2 import hcq_compile, hcq_link, HCQ_RUNTIME_DEV # noqa: E402 # down here, hcq2 imports realize
|
||||
|
||||
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, profile:bool|None=None) -> UOp:
|
||||
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
|
||||
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
|
||||
linear = lower_and_compile(linear)
|
||||
linear = graph_rewrite(linear, pm_optimize_local_size, name="optimize local size", walk=True)
|
||||
if getenv("HCQ2"): linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
|
||||
if HCQ2: linear = hcq_compile(linear, input_uops, bool(PROFILE or DEBUG >= 2) if profile is None else profile)
|
||||
return linear
|
||||
|
||||
def link_linear(linear:UOp, cache=True) -> UOp: return hcq_link(linear, cache=cache) if getenv("HCQ2") else linear
|
||||
def link_linear(linear:UOp, cache=True) -> UOp: return hcq_link(linear, cache=cache) if HCQ2 else linear
|
||||
|
||||
def run_linear(linear:UOp, var_vals:dict[str, int]|None=None, input_uops:Sequence[UOp]=(), update_stats=True, jit=False, wait=False):
|
||||
inputs = list(input_uops)
|
||||
|
||||
@@ -59,7 +59,7 @@ class _function(Generic[ReturnType]):
|
||||
if isinstance(ret, Tensor):
|
||||
uret = ret.uop
|
||||
elif isinstance(ret, tuple) and all(isinstance(x, Tensor) for x in ret):
|
||||
uret = UOp.maketuple(*[x.uop for x in ret])
|
||||
uret = UOp.sink(*[x.uop for x in ret])
|
||||
else:
|
||||
raise RuntimeError(f"function return type {type(ret)} not supported")
|
||||
|
||||
@@ -78,16 +78,19 @@ class _function(Generic[ReturnType]):
|
||||
buf_strs = '\n '.join(f"{i}: dtype={b.dtype}, size={b.max_numel()}, device={b.device}" for i,b in enumerate(implicit_buffers))
|
||||
raise RuntimeError(f"function {name} has {len(implicit_buffers)} implicit buffer(s), but allow_implicit=False\n {buf_strs}")
|
||||
|
||||
fret = uret.call(*call_uops, grad_fxn=self.grad_fxn, name=name, precompile=self.precompile,
|
||||
precompile_backward=self.precompile_backward)
|
||||
fret = (UOp.call_outputs(uret.src, *call_uops, grad_fxn=self.grad_fxn, name=name, precompile=self.precompile,
|
||||
precompile_backward=self.precompile_backward) if isinstance(ret, tuple)
|
||||
else uret.call(*call_uops, grad_fxn=self.grad_fxn, name=name, precompile=self.precompile,
|
||||
precompile_backward=self.precompile_backward))
|
||||
|
||||
if DEBUG >= 2:
|
||||
print(" "*_function.depth+f"function {uret.key.hex()[:8]} in {(time.perf_counter()-st)*1000:8.2f} ms: {name}")
|
||||
|
||||
outs = fret.returned_outputs
|
||||
if isinstance(ret, tuple):
|
||||
return cast(ReturnType, tuple(Tensor(fret.gettuple(i)) for i in range(len(ret))))
|
||||
return cast(ReturnType, tuple(Tensor(o) for o in outs))
|
||||
else:
|
||||
return cast(ReturnType, Tensor(fret.gettuple(0)))
|
||||
return cast(ReturnType, Tensor(outs[0]))
|
||||
|
||||
# overload signatures support both @function and @function(precompile=True) syntax
|
||||
@overload
|
||||
|
||||
+1
-1
@@ -240,7 +240,7 @@ TRANSCENDENTAL, NOLOCALS = ContextVar("TRANSCENDENTAL", 1), ContextVar("NOLOCALS
|
||||
SPLIT_REDUCEOP, NO_MEMORY_PLANNER, LRU = ContextVar("SPLIT_REDUCEOP", 1), ContextVar("NO_MEMORY_PLANNER", 0), ContextVar("LRU", 1)
|
||||
RING, ALL2ALL, ALLREDUCE_CAST = ContextVar("RING", 1), ContextVar("ALL2ALL", 0), ContextVar("ALLREDUCE_CAST", 1)
|
||||
CACHELEVEL, IGNORE_BEAM_CACHE = ContextVar("CACHELEVEL", 2), ContextVar("IGNORE_BEAM_CACHE", 0)
|
||||
VALIDATE_WITH_CPU = ContextVar("VALIDATE_WITH_CPU", 0)
|
||||
VALIDATE_WITH_CPU, HCQ2 = ContextVar("VALIDATE_WITH_CPU", 0), ContextVar("HCQ2", 0)
|
||||
# TODO: this is broken for some indexing
|
||||
DISABLE_FAST_IDIV = ContextVar("DISABLE_FAST_IDIV", 1)
|
||||
FUSE_OPTIM = ContextVar("FUSE_OPTIM", 0)
|
||||
|
||||
+2
-1
@@ -145,6 +145,7 @@ def main():
|
||||
parser.add_argument("--serve", nargs='?', type=int, const=8000, metavar="PORT", help="Run OpenAI compatible API (optional port, default 8000)")
|
||||
parser.add_argument("--warmup", action="store_true", help="warmup the JIT")
|
||||
parser.add_argument("--benchmark", nargs='?', type=int, const=20, metavar="COUNT", help="Benchmark tok/s (optional count, default 20)")
|
||||
parser.add_argument("--no_chat_template", action="store_true", help="Don't use the model's chat template, always use the fallback template")
|
||||
args = parser.parse_args()
|
||||
|
||||
# load the model
|
||||
@@ -160,7 +161,7 @@ def main():
|
||||
|
||||
# use the model's chat template if jinja2 is available (enables model-specific formatting)
|
||||
template: jinja2.Template|FallbackTemplate = FallbackTemplate(tok)
|
||||
if (ct := kv.get('tokenizer.chat_template')) is not None:
|
||||
if not args.no_chat_template and (ct := kv.get('tokenizer.chat_template')) is not None:
|
||||
try:
|
||||
import jinja2
|
||||
env = jinja2.Environment()
|
||||
|
||||
+68
-3
@@ -18,7 +18,9 @@ _GGML_NATIVE = {0: dtypes.float32, 1: dtypes.float16, 24: dtypes.int8, 25: dtype
|
||||
|
||||
# quant types {ggml_type: (number of elements, number of bytes)}
|
||||
_GGML_QUANT = {2:(32,18), 3:(32,20), 6:(32,22), 7:(32,24), 8:(32,34),
|
||||
12:(256,144), 13:(256,176), 14:(256,210), 18:(256,98), 21:(256,110), 22:(256,82), 23:(256,136), 39:(32,17), 41:(128,18)}
|
||||
10:(256,84), 11:(256,110), 12:(256,144), 13:(256,176), 14:(256,210),
|
||||
16:(256,66), 17:(256,74), 18:(256,98), 19:(256,50), 20:(32,18), 21:(256,110), 22:(256,82), 23:(256,136),
|
||||
29:(256,56), 39:(32,17), 41:(128,18)}
|
||||
|
||||
def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
"""
|
||||
@@ -27,8 +29,9 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
Supported native types: float32 (id: 0), float16 (id: 1), int8 (id: 24),
|
||||
int16 (id: 25), int32 (id: 26), int64 (id: 27), float64 (id: 28), bfloat16 (id: 30)
|
||||
Supported quantized types: Q4_0 (id: 2), Q4_1 (id: 3), Q5_0 (id: 6),
|
||||
Q5_1 (id: 7), Q8_0 (id: 8), Q4_K (id: 12), Q5_K (id: 13),
|
||||
Q6_K (id: 14), IQ3_XXS (id: 18), IQ3_S (id: 21), IQ2_S (id: 22), IQ4_XS (id: 23), MXFP4 (id: 39), Q1_0 (id: 41)
|
||||
Q5_1 (id: 7), Q8_0 (id: 8), Q2_K (id: 10), Q3_K (id: 11), Q4_K (id: 12), Q5_K (id: 13),
|
||||
Q6_K (id: 14), IQ2_XXS (id: 16), IQ2_XS (id: 17), IQ3_XXS (id: 18), IQ1_S (id: 19),
|
||||
IQ4_NL (id: 20), IQ3_S (id: 21), IQ2_S (id: 22), IQ4_XS (id: 23), IQ1_M (id: 29), MXFP4 (id: 39), Q1_0 (id: 41)
|
||||
"""
|
||||
# https://github.com/ggerganov/ggml/blob/323951f1bdcdfbd5b5ff3a9a7c3770e63b1a560e/include/ggml.h#L356
|
||||
|
||||
@@ -54,6 +57,19 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
q = q_to_uint8(blocks[:,qh_off+4:], 4).bitcast(dtypes.int8) + qh * 16
|
||||
return q * d + (blocks[:,2:4].bitcast(dtypes.float16).cast(dtypes.float32) if ggml_type == 7 else -16 * d)
|
||||
if ggml_type == 8: return blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32) * blocks[:,2:].bitcast(dtypes.int8)
|
||||
# Q2_K: 256 elements per 84-byte block (scales:16, qs:64, d:2, dmin:2)
|
||||
if ggml_type == 10:
|
||||
d, dmin = (blocks[:,i:i+2].bitcast(dtypes.float16).cast(dtypes.float32).unsqueeze(-1) for i in [80, 82])
|
||||
sc = blocks[:, :16]
|
||||
q = q_to_uint8(blocks[:, 16:80].reshape((-1, 2, 32)), 2).reshape((-1, 16, 16))
|
||||
return (d * sc.bitwise_and(0xF).unsqueeze(-1) * q - dmin * sc.rshift(4).unsqueeze(-1)).flatten(-2)
|
||||
# Q3_K: 256 elements per 110-byte block (hmask:32, qs:64, scales:12, d:2)
|
||||
if ggml_type == 11:
|
||||
d = blocks[:,-2:].bitcast(dtypes.float16).cast(dtypes.float32).unsqueeze(-1)
|
||||
sc = q_to_uint8(blocks[:,96:104], 4).bitwise_or(q_to_uint8(blocks[:,104:108], 2).lshift(4)).bitcast(dtypes.int8) - 32
|
||||
q = q_to_uint8(blocks[:,32:96].reshape((-1, 2, 32)), 2).reshape((-1, 16, 16))
|
||||
qh = q_to_uint8(blocks[:,:32], 1).reshape((-1, 16, 16))
|
||||
return (d * sc.unsqueeze(-1) * (q.bitcast(dtypes.int8) - qh.bitwise_xor(1).lshift(2).bitcast(dtypes.int8))).flatten(-2)
|
||||
# Q4_K: 256 elements per 144-byte block (d:2, dmin:2, scales:12, qs:128)
|
||||
# Q5_K: 256 elements per 176-byte block (d:2, dmin:2, scales:12, qh:32, qs:128)
|
||||
if ggml_type in (12, 13):
|
||||
@@ -79,6 +95,41 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
signs = (q_to_uint8(even_signs[sign_idx].reshape((-1, 32, 1)), 1) == 0).where(1.0, -1.0).reshape((-1, 8, 4, 8))
|
||||
grid = _ggml_iq_grid(t.device, _ggml.iq3xxs_grid, (256, 4))[blocks[:, 2:66]].reshape((-1, 8, 4, 8))
|
||||
return (db * grid * signs).flatten(-3)
|
||||
# IQ2_XXS: 256 elements per 66-byte block (d:2, qs:64). 8 groups of 32: 4 grid bytes + packed signs/scale.
|
||||
if ggml_type == 16:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
|
||||
qs_u32 = blocks[:, 2:].bitcast(dtypes.uint32).reshape((-1, 8, 2))
|
||||
db = d * (qs_u32[:, :, 1].rshift(28).cast(dtypes.float32) + 0.5).reshape((-1, 8, 1, 1)) * 0.25
|
||||
sign_idx = qs_u32[:, :, 1].unsqueeze(-1).rshift(Tensor.const((0, 7, 14, 21), dtypes.uint32))
|
||||
sign_idx = sign_idx.bitwise_and(0x7F).reshape((-1, 32)).cast(dtypes.int32)
|
||||
even_signs = Tensor([i | (0x80 if i.bit_count() % 2 else 0) for i in range(128)], dtype=dtypes.uint8, device=t.device)
|
||||
signs = (q_to_uint8(even_signs[sign_idx].reshape((-1, 32, 1)), 1) == 0).where(1.0, -1.0).reshape((-1, 8, 4, 8))
|
||||
grid = _ggml_iq_grid(t.device, _ggml.iq2xxs_grid, (256, 8))[blocks[:, 2:].reshape((-1, 8, 8))[:, :, :4]].reshape((-1, 8, 4, 8))
|
||||
return (db * grid * signs).flatten(-3)
|
||||
# IQ2_XS: 256 elements per 74-byte block (d:2, qs:64 as uint16, scales:8)
|
||||
if ggml_type == 17:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
|
||||
db = d * (q_to_uint8(blocks[:, 66:74].reshape((-1, 8, 1)), 4).reshape((-1, 16)).cast(dtypes.float32) + 0.5).reshape((-1, 16, 1, 1)) * 0.25
|
||||
qs = blocks[:, 2:66].bitcast(dtypes.uint16)
|
||||
sign_idx = qs.rshift(9).cast(dtypes.int32)
|
||||
even_signs = Tensor([i | (0x80 if i.bit_count() % 2 else 0) for i in range(128)], dtype=dtypes.uint8, device=t.device)
|
||||
signs = (q_to_uint8(even_signs[sign_idx].reshape((-1, 32, 1)), 1) == 0).where(1.0, -1.0).reshape((-1, 16, 2, 8))
|
||||
grid = _ggml_iq_grid(t.device, _ggml.iq2xs_grid, (512, 8))[qs.bitwise_and(511)].reshape((-1, 16, 2, 8))
|
||||
return (db * grid * signs).flatten(-3)
|
||||
# IQ1_S: 256 elements per 50-byte block (d:2, qs:32, qh:16). grid bytes are int8 {-1,0,1}.
|
||||
if ggml_type == 19:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
|
||||
qh = blocks[:, 34:50].bitcast(dtypes.uint16)
|
||||
dl = d * (qh.rshift(12).bitwise_and(7).cast(dtypes.float32) * 2 + 1).reshape((-1, 8, 1, 1))
|
||||
delta = (qh.bitwise_and(0x8000) == 0).where(0.125, -0.125).reshape((-1, 8, 1, 1))
|
||||
qh_hi = qh.unsqueeze(-1).rshift(Tensor.const((0, 3, 6, 9), dtypes.uint16)).bitwise_and(7).lshift(8)
|
||||
q = blocks[:, 2:34].cast(dtypes.uint16) + qh_hi.reshape((-1, 32))
|
||||
grid = _ggml_iq_grid(t.device, _ggml.iq1s_grid, (2048, 8))[q].reshape((-1, 8, 4, 8))
|
||||
grid = (grid > 127).where(grid - 256, grid)
|
||||
return (dl * (grid + delta)).flatten(-3)
|
||||
if ggml_type == 20:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32)
|
||||
return d * Tensor(list(_ggml.kvalues_iq4nl), dtype=dtypes.float32, device=t.device)[q_to_uint8(blocks[:, 2:], 4)]
|
||||
if ggml_type == 21:
|
||||
d = blocks[:, :2].bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1))
|
||||
scales = (1 + 2 * q_to_uint8(blocks[:, 106:110].reshape((-1, 4, 1)), 4).reshape((-1, 8))).cast(dtypes.float32).reshape((-1, 8, 1, 1))
|
||||
@@ -102,6 +153,20 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
scales = (scales_l.bitwise_or(scales_h.lshift(4)).bitcast(dtypes.int8) - 32).cast(dtypes.float32).reshape((-1, 8, 1))
|
||||
q = (qs:=blocks[:, 8:].reshape((-1, 8, 16))).bitwise_and(0xF).cat(qs.rshift(4), dim=2)
|
||||
return (d * scales * iq4_xs_lut[q]).flatten(-2)
|
||||
# IQ1_M: 256 elements per 56-byte block (qs:32, qh:16, scales:8). f16 scale packed in high nibbles.
|
||||
if ggml_type == 29:
|
||||
sc16 = blocks[:, 48:56].bitcast(dtypes.uint16)
|
||||
d = sc16.bitwise_and(0xF000).rshift(Tensor.const((12, 8, 4, 0), dtypes.uint16))
|
||||
d = d[:, 0:1].bitwise_or(d[:, 1:2]).bitwise_or(d[:, 2:3]).bitwise_or(d[:, 3:4])
|
||||
d = d.bitcast(dtypes.float16).cast(dtypes.float32).reshape((-1, 1, 1, 1, 1))
|
||||
scales = sc16.unsqueeze(-1).rshift(Tensor.const((0, 3, 6, 9), dtypes.uint16)).bitwise_and(7)
|
||||
dl = d * (scales.cast(dtypes.float32) * 2 + 1).reshape((-1, 8, 2, 1, 1))
|
||||
qh_n = Tensor.stack(blocks[:, 32:48].bitwise_and(0x0F), blocks[:, 32:48].rshift(4), dim=-1).reshape((-1, 32))
|
||||
q = blocks[:, :32].cast(dtypes.uint16) + qh_n.bitwise_and(7).cast(dtypes.uint16).lshift(8)
|
||||
delta = (qh_n.bitwise_and(0x08) == 0).where(0.125, -0.125).reshape((-1, 8, 2, 2, 1))
|
||||
grid = _ggml_iq_grid(t.device, _ggml.iq1s_grid, (2048, 8))[q].reshape((-1, 8, 2, 2, 8))
|
||||
grid = (grid > 127).where(grid - 256, grid)
|
||||
return (dl * (grid + delta)).flatten(-4)
|
||||
if ggml_type == 39:
|
||||
e = blocks[:, 0].cast(dtypes.uint32)
|
||||
small_bits = Tensor([0x00200000, 0x00400000], dtype=dtypes.uint32, device=t.device)[e.clip(0, 1).cast(dtypes.int32)] # e = 0 or e = 1 case
|
||||
|
||||
+37
-32
@@ -16,7 +16,8 @@ def reduce_gradient(ctx:UOp, ret:UOp, op:Ops):
|
||||
|
||||
def _compact_params(body:UOp, all_args:tuple[UOp, ...]) -> tuple[UOp, tuple[UOp, ...]]:
|
||||
"""Remove unused PARAMs from body and return compacted (body, args)."""
|
||||
used = sorted({p.arg.slot: p for p in body.toposort() if p.op is Ops.PARAM}.items())
|
||||
# NOTE: don't enter nested calls, their PARAMs are lexical params of the subprogram
|
||||
used = sorted({p.arg.slot: p for p in body.toposort(enter_calls=False) if p.op is Ops.PARAM}.items())
|
||||
body = body.substitute({p: p.replace(arg=dataclasses.replace(p.arg, slot=j)) for j,(_, p) in enumerate(used)}, walk=True)
|
||||
return body, tuple(all_args[i] for i,_ in used)
|
||||
|
||||
@@ -24,28 +25,41 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
|
||||
fxn, args = k.src[0], k.src[1:]
|
||||
if k.arg.grad_fxn is not None:
|
||||
# put const on a device, also TODO why do we still have NOOP...
|
||||
def on_dev(g, i): return g.clone(device=args[i].device if k.op is Ops.CALL else k.device) if g.device is None else g
|
||||
if ctx.op is Ops.TUPLE:
|
||||
def on_dev(g, i): return g.clone(device=args[i].device) if g.device is None else g
|
||||
def arg_grads(g): return (None,) + g + (None,)*k.num_returned
|
||||
if ctx.op is Ops.GROUP:
|
||||
real = [on_dev(g, i) for i,g in enumerate(ctx.src) if g.op is not Ops.NOOP]
|
||||
return (None,) + (k.arg.grad_fxn(*real, call=k) if len(real) > 1 else k.arg.grad_fxn(real[0], k))
|
||||
return (None,) + k.arg.grad_fxn(on_dev(ctx, 0), k)
|
||||
assert fxn.op is Ops.TUPLE, f"expected TUPLE body for gradient, got {fxn.op}"
|
||||
return arg_grads(k.arg.grad_fxn(*real, call=k) if len(real) > 1 else k.arg.grad_fxn(real[0], k))
|
||||
return arg_grads(k.arg.grad_fxn(on_dev(ctx, 0), k))
|
||||
# the RETURNED inputs are the call outputs: their positions in the args get the output gradients from the AFTER rule
|
||||
assert fxn.op is Ops.SINK and k.num_returned, f"expected a CALL with RETURNED inputs or a grad_fxn, got {fxn.op}"
|
||||
ret_pos = [i for i, a in enumerate(args) if a.unsharded_base.op is Ops.RETURNED]
|
||||
n_args = len(args) - len(ret_pos)
|
||||
# the body stores the outputs into output PARAMs (slots after the args): the values are the stored values in slot order
|
||||
out_stores = sorted((st for st in fxn.src if st.op is Ops.STORE), key=lambda st: st.src[0].unsharded_base.arg.slot)
|
||||
values = UOp(Ops.GROUP, src=tuple(st.src[1] for st in out_stores))
|
||||
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
|
||||
grad_args = ctx.src
|
||||
root_grad = UOp(Ops.TUPLE, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
|
||||
# grads are collected at the flat param storage: reshape to each arg's view (max view shrunk to symbolic)
|
||||
def shaped_grad(grad:UOp, i:int) -> UOp:
|
||||
a = args[i]
|
||||
return grad.view_as(a.shard_shape, a.axis) if a.axis is not None and isinstance(a.device, tuple) else grad.view_as(a._shape)
|
||||
grad_args = tuple(ctx.src[i] for i in ret_pos)
|
||||
root_grad = UOp(Ops.GROUP, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
|
||||
g if g.device is None else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
|
||||
grads = compute_gradient(fxn, root_grad, set(params.values()))
|
||||
grads = compute_gradient(values, root_grad, set(params.values()))
|
||||
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
|
||||
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(fxn.src)} if k.arg.precompile else {}
|
||||
fwd_outs = tuple(k.gettuple(i) for i in range(len(fxn.src))) if k.arg.precompile else ()
|
||||
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(values.src)} if k.arg.precompile else {}
|
||||
fwd_outs = k.returned_outputs if k.arg.precompile else ()
|
||||
# collect needed gradient bodies, compact unused params, create a single backward CALL
|
||||
grad_bodies = [(i, grads[p]) for i in needed if (p:=params.get(i)) is not None and p in grads]
|
||||
bwd_body = UOp.maketuple(*(gb for _, gb in grad_bodies)).substitute(fwd_subs, walk=True)
|
||||
grad_bodies = [(i, shaped_grad(grads[p], i)) for i in needed if (p:=params.get(i)) is not None and p in grads]
|
||||
bwd_body = UOp(Ops.GROUP, src=tuple(gb for _, gb in grad_bodies)).substitute(fwd_subs, walk=True)
|
||||
bwd_body = renumber_invalid_outputs(bwd_body)
|
||||
# NOTE: args includes the RETURNED inputs so the param slots above line up; they are unused and compacted away
|
||||
bwd_body, compact_args = _compact_params(bwd_body, (*args, *grad_args, *fwd_outs))
|
||||
bwd_call = bwd_body.call(*compact_args, name=(k.arg.name or "")+"_backward", precompile=k.arg.precompile_backward)
|
||||
bwd_outs = UOp.call_outputs(bwd_body.src, *compact_args, name=(k.arg.name or "")+"_backward",
|
||||
precompile=k.arg.precompile_backward).returned_outputs
|
||||
gb_map = {i: idx for idx, (i, _) in enumerate(grad_bodies)}
|
||||
return (None,) + tuple(bwd_call.gettuple(gb_map[i]) if i in gb_map else None for i in range(len(args)))
|
||||
return (None,) + tuple(bwd_outs[gb_map[i]] if i in gb_map else None for i in range(n_args)) + (None,)*len(ret_pos)
|
||||
|
||||
# ctx is grad_output
|
||||
pm_gradient = PatternMatcher([
|
||||
@@ -76,9 +90,9 @@ pm_gradient = PatternMatcher([
|
||||
(UPat(Ops.STACK, name="ret"), lambda ctx, ret: tuple(ctx[i] for i in range(len(ret.src)))),
|
||||
(UPat(Ops.COPY, name="ret"), lambda ctx, ret: (ctx.copy_to_device(ret.src[0].device),)),
|
||||
(UPat(Ops.UNSHARD, name="ret"), lambda ctx, ret: ctx.shard(ret.device, ret.axis).src),
|
||||
(UPat(Ops.TUPLE), lambda ctx: ctx.src),
|
||||
(UPat(Ops.GROUP), lambda ctx: ctx.src),
|
||||
(UPat(Ops.AFTER, src=(UPat.var("d"), UPat(Ops.CALL, name="k"))), lambda ctx, d, k:
|
||||
(ctx, UOp.maketuple(*(ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1))))),
|
||||
(ctx, UOp(Ops.GROUP, src=tuple(ctx if i == k.src.index(d)-1 else UOp(Ops.NOOP) for i in range(len(k.src)-1))))),
|
||||
# clone/assign gradient passes through to val
|
||||
(UPat(Ops.AFTER, src=(UPat(), UPat(Ops.STORE))), lambda ctx: (None, ctx)),
|
||||
(UPat(Ops.STORE, src=(UPat(), UPat())), lambda ctx: (None, ctx)),
|
||||
@@ -98,18 +112,9 @@ def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp
|
||||
grads: dict[UOp, UOp] = {root: root_grad}
|
||||
for t0 in reversed(walk):
|
||||
if t0 not in grads or grads[t0].op is Ops.NOOP: continue
|
||||
# GETTUPLE: accumulate gradient into a TUPLE UOp on the FUNCTION, process when we hit the FUNCTION
|
||||
if t0.op is Ops.GETTUPLE:
|
||||
k = t0.src[0] # the FUNCTION
|
||||
assert k.op is Ops.FUNCTION and k.src[0].op is Ops.TUPLE
|
||||
n_outputs = len(k.src[0].src)
|
||||
prev = grads[k].src if k in grads else tuple(UOp(Ops.NOOP) for _ in range(n_outputs))
|
||||
grads[k] = UOp.maketuple(*(prev[i] + grads[t0] if i == t0.arg and prev[i].op is not Ops.NOOP else
|
||||
grads[t0] if i == t0.arg else prev[i] for i in range(n_outputs)))
|
||||
continue
|
||||
# FUNCTION/CALL: pass needed param set so backward only computes required gradients
|
||||
# (FUNCTION uses implicit TUPLE gradient or grad_fxn; CALL requires an explicit grad_fxn)
|
||||
if t0.op in {Ops.FUNCTION, Ops.CALL}:
|
||||
# CALL: pass needed param set so backward only computes required gradients
|
||||
# (calls with RETURNED inputs use the implicit body gradient or grad_fxn; opaque CALLs require an explicit grad_fxn)
|
||||
if t0.op is Ops.CALL:
|
||||
needed = {i for i, arg in enumerate(t0.src[1:]) if arg in targets or in_target_path.get(arg, False)}
|
||||
lgrads:tuple[UOp|None, ...]|None = call_gradient(grads[t0], t0, needed)
|
||||
else:
|
||||
@@ -122,9 +127,9 @@ def compute_gradient(root:UOp, root_grad:UOp, targets:set[UOp]) -> dict[UOp, UOp
|
||||
if k._shape is not None and v._shape is not None and k._shape != v._shape:
|
||||
v = v.cast(sum_acc_dtype(v.dtype))._rop(Ops.ADD, broadcast_axes(k.shape, v.shape)).reshape(k.shape).cast(v.dtype)
|
||||
if k in grads and grads[k].op is not Ops.NOOP:
|
||||
if v.op is Ops.TUPLE and grads[k].op is Ops.TUPLE:
|
||||
grads[k] = UOp.maketuple(*(p + n if (p.op is not Ops.NOOP and n.op is not Ops.NOOP) else
|
||||
n if p.op is Ops.NOOP else p for p, n in zip(grads[k].src, v.src)))
|
||||
if v.op is Ops.GROUP and grads[k].op is Ops.GROUP:
|
||||
grads[k] = UOp(Ops.GROUP, src=tuple(p + n if (p.op is not Ops.NOOP and n.op is not Ops.NOOP) else
|
||||
n if p.op is Ops.NOOP else p for p, n in zip(grads[k].src, v.src)))
|
||||
else: grads[k] = grads[k] + v
|
||||
else: grads[k] = v
|
||||
if len(forward_metadata:=all_metadata.get(t0, ())):
|
||||
|
||||
@@ -1067,7 +1067,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
reshape[i] = expand[i] = size[i]
|
||||
if mode == "linear":
|
||||
arr = type(self).arange(size[i])
|
||||
num, den = (arr*(in_sz-1), size[i]-1) if align_corners else ((arr*2+1)*in_sz - size[i], size[i]*2)
|
||||
num, den = (arr*(in_sz-1), max(size[i]-1, 1)) if align_corners else ((arr*2+1)*in_sz - size[i], size[i]*2)
|
||||
num = num.clip(0, (in_sz-1)*den)
|
||||
low, high, perc = [y.reshape(reshape).expand(expand) for y in (num//den, (num+den-1)//den, (num % den).cast(dtypes.float32)/den)]
|
||||
x = x.gather(i, low).lerp(x.gather(i, high), perc)
|
||||
|
||||
@@ -99,7 +99,7 @@ class Conv2d:
|
||||
if isinstance(padding, str):
|
||||
if padding.lower() != 'same': raise ValueError(f"Invalid padding string {padding!r}, only 'same' is supported")
|
||||
if stride != 1: raise ValueError("padding='same' is not supported for strided convolutions")
|
||||
pad = [(d*(k-1)//2, d*(k-1) - d*(k-1)//2) for d,k in zip(make_tuple(dilation, len(self.kernel_size)), self.kernel_size[::-1])]
|
||||
pad = [(d*(k-1)//2, d*(k-1) - d*(k-1)//2) for d,k in zip(make_tuple(dilation, len(self.kernel_size))[::-1], self.kernel_size[::-1])]
|
||||
padding = tuple(flatten(pad))
|
||||
self.stride, self.dilation, self.groups, self.padding = stride, dilation, groups, padding
|
||||
scale = 1 / math.sqrt(in_channels * prod(self.kernel_size))
|
||||
@@ -305,7 +305,7 @@ class RMSNorm:
|
||||
|
||||
from tinygrad.uop.ops import UOp, KernelInfo, Ops, AxisType
|
||||
def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
|
||||
weight, idx = call.src[1:]
|
||||
weight, idx = call.src[1:-call.num_returned]
|
||||
is_vocab_sharded = isinstance(weight.device, tuple) and weight.axis == 0
|
||||
# for multi-device: replicate grad_emb and idx on all devices
|
||||
if isinstance(weight.device, tuple):
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys, struct, functools
|
||||
from typing import cast
|
||||
from tinygrad.dtype import dtypes, DType, truncate, AddrSpace
|
||||
from tinygrad.uop import FastEnum, auto, Ops, GroupOp
|
||||
from tinygrad.uop.ops import UOp, UPat, PatternMatcher
|
||||
from tinygrad.uop.ops import UOp, UPat, PatternMatcher, promo_dtype
|
||||
from tinygrad.renderer.isa import ISARenderer, IselContext, Register, PreRegAllocContext, greg
|
||||
from tinygrad.helpers import getenv, NUM_CPU_THREADS, unwrap, Target
|
||||
|
||||
@@ -33,7 +33,6 @@ class X86Ops(FastEnum):
|
||||
# bitcasts
|
||||
VMOVD = auto(); VMOVQ = auto(); VMOVDm = auto(); VMOVQm = auto()
|
||||
# comparisons
|
||||
VUCOMISS = auto(); VUCOMISD = auto()
|
||||
VCMPSS = auto(); VCMPSD = auto(); VCMPPS = auto(); VCMPPD = auto()
|
||||
VPCMPGTB = auto(); VPCMPGTW = auto(); VPCMPGTD = auto(); VPCMPGTQ = auto()
|
||||
VPCMPEQB = auto(); VPCMPEQW = auto(); VPCMPEQD = auto(); VPCMPEQQ = auto()
|
||||
@@ -95,7 +94,7 @@ class X86GroupOp:
|
||||
X86Ops.VPMULLW, X86Ops.VPMULLD, X86Ops.VROUNDSS, X86Ops.VROUNDSD, X86Ops.VSQRTSS, X86Ops.VSQRTSD, X86Ops.VINSERTPS,
|
||||
X86Ops.VPINSRB, X86Ops.VPINSRW, X86Ops.VPINSRD, X86Ops.VPINSRQ, X86Ops.VPAND, X86Ops.VPOR, X86Ops.VPXOR, X86Ops.VPSLLVD,
|
||||
X86Ops.VPSLLVQ, X86Ops.VPSRLVD, X86Ops.VPSRLVQ, X86Ops.VPSRAVD, X86Ops.CMOVNE, X86Ops.CMOVE, X86Ops.CMOVL, X86Ops.CMOVB,
|
||||
X86Ops.VCVTSI2SS, X86Ops.VCVTSI2SD, X86Ops.VCVTSS2SD, X86Ops.VCVTSD2SS, X86Ops.VUCOMISS, X86Ops.VUCOMISD, X86Ops.IDIV, X86Ops.DIV}
|
||||
X86Ops.VCVTSI2SS, X86Ops.VCVTSI2SD, X86Ops.VCVTSS2SD, X86Ops.VCVTSD2SS, X86Ops.IDIV, X86Ops.DIV}
|
||||
|
||||
# X86Ops that can write to memory
|
||||
WriteMem = {X86Ops.MOVm, X86Ops.MOVi, X86Ops.VMOVSSm, X86Ops.VMOVSDm, X86Ops.VMOVUPSm, X86Ops.VMOVDm, X86Ops.VMOVQm,
|
||||
@@ -110,7 +109,7 @@ class X86GroupOp:
|
||||
# X86Ops that write flags or can modify flags to undefined values
|
||||
WriteFlags = {X86Ops.CMP, X86Ops.CMPi, X86Ops.ADD, X86Ops.ADDi, X86Ops.SUB, X86Ops.SUBi, X86Ops.IMUL, X86Ops.IMULi, X86Ops.IDIV, X86Ops.DIV,
|
||||
X86Ops.SHL, X86Ops.SHLi, X86Ops.SHR, X86Ops.SHRi, X86Ops.SAR, X86Ops.SARi, X86Ops.AND, X86Ops.ANDi, X86Ops.XOR, X86Ops.XORi,
|
||||
X86Ops.OR, X86Ops.ORi, X86Ops.VUCOMISS, X86Ops.VUCOMISD}
|
||||
X86Ops.OR, X86Ops.ORi}
|
||||
|
||||
# X86Ops whose first src is the rm field
|
||||
Rm1st = ReadMem1st | (ReadMem2nd & TwoAddress) | {X86Ops.VPSRLDQ}
|
||||
@@ -118,8 +117,6 @@ class X86GroupOp:
|
||||
# X86Ops whose second src is the rm field
|
||||
Rm2nd = ReadMem2nd
|
||||
|
||||
All = set(X86Ops)
|
||||
|
||||
# ***** X86 legalization *****
|
||||
|
||||
extra_matcher = PatternMatcher([
|
||||
@@ -145,14 +142,14 @@ extra_matcher = PatternMatcher([
|
||||
# float16 alus are done in float32
|
||||
(UPat(GroupOp.ALU, dtypes.float16, name="x"), lambda x: UOp(x.op,
|
||||
src=tuple(s.cast(dtypes.float) if s.dtype != dtypes.bool else s for s in x.src)).cast(x.dtype)),
|
||||
(UPat(GroupOp.Comparison, src=(UPat.var("a", dtypes.float16), UPat.var("b")), name="x"),
|
||||
lambda x,a,b: UOp(x.op, src=(a.cast(dtypes.float32), b.cast(dtypes.float32))).cast(x.dtype)),
|
||||
(UPat(GroupOp.Comparison, src=[UPat(dtype=dtypes.float16), UPat()], name="x"),
|
||||
lambda x: UOp(x.op, src=tuple(s.cast(dtypes.float32) for s in x.src)).cast(x.dtype)),
|
||||
# no cmpne for packed ints, y != x => !(y==x)
|
||||
(UPat(Ops.CMPNE, src=(UPat.var("y", dtypes.ints), UPat.var("x")), name="cmp"),
|
||||
lambda y,x,cmp: UOp(Ops.CMPEQ, src=(y,x))^True if y.max_numel() > 1 else None),
|
||||
# float WHERE needs a mask unless its comparison already has a float operand
|
||||
# a float WHERE blends at the width of its value, so it needs a comparison at that width to make the mask
|
||||
(UPat.var("m", dtypes.bool).where(UPat.var("a", dtypes.floats+(dtypes.weakfloat,)), UPat.var("b")).named("w"),
|
||||
lambda m,a,b,w: m.cast(w.dtype).ne(0).where(a, b) if w.dtype in dtypes.floats and not dtypes.is_float(m.src[0].dtype) else None),
|
||||
lambda m,a,b,w: m.cast(w.dtype).ne(0).where(a, b) if w.dtype in dtypes.floats and promo_dtype(m.src) is not w.dtype else None),
|
||||
# rewrite -x -> 0 - x
|
||||
(UPat(Ops.NEG, name="x"), lambda x: UOp(Ops.SUB, src=(x.const_like(0),) + x.src)),
|
||||
# TODO: add support for mod, requires support for accessing the 2nd+ reg of a multi output instruction
|
||||
@@ -177,6 +174,12 @@ def gated_store(addr:UOp, gate:UOp, val:UOp):
|
||||
sel = gate.where(addr.replace(dtype=dtypes.uint64), local.index(UOp.cconst(0, dtypes.int32), dtype=dtypes.uint64))
|
||||
return UOp(Ops.AFTER, addr.dtype, (sel,)).store(val)
|
||||
|
||||
# a gate the flags can be picked with, or the bool compared to zero that replaces one they can't: only an integer
|
||||
# comparison sets the flags, see cmp. NOTE: the 0 is int so the bool zero-extends and compares as int (a byte compare renders
|
||||
# different kernels)
|
||||
def flag_gate(m:UOp) -> UOp|None:
|
||||
return None if m.op in GroupOp.Comparison and m.src[0].dtype not in dtypes.floats else m.ne(UOp.cconst(0, dtypes.int))
|
||||
|
||||
# legalize the new style graph for isel. NOTE: this runs after the spec is verified, some of these rewrites violate it
|
||||
pre_isel_matcher = PatternMatcher([
|
||||
# noop casts: zero extending scalar 32bit int, same-width signed/unsigned, narrowing scalar int
|
||||
@@ -191,11 +194,9 @@ pre_isel_matcher = PatternMatcher([
|
||||
# gated load/store become a conditional move on the address, the load/store are unconditional
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), name="addr").load(UPat.var("alt"), UPat.var("gate"), name="x"), gated_load),
|
||||
(UPat((Ops.INDEX, Ops.SHRINK), name="addr").store(UPat.var("val"), UPat.var("gate")), gated_store),
|
||||
# TODO: remove this once we allow all flag producing ops in cmove
|
||||
# if gate in scalar int cmove is not a comparison need to add one to set the flag
|
||||
# NOTE: the 0 is int so the bool gate zero-extends and compares as int (a byte compare renders different kernels)
|
||||
(UPat.var("m", dtypes.bool).where(UPat.var("a"), UPat.var("b")),
|
||||
lambda m,a,b: m.ne(UOp.cconst(0, dtypes.int)).where(a,b) if m.op not in GroupOp.Comparison else None),
|
||||
# a conditional backedge picks with the flags, and so does the cmove, which is legalized in isel
|
||||
(UPat(Ops.END, src=(UPat(), UPat(), UPat.var("m", dtypes.bool)), name="x"),
|
||||
lambda m,x: x.replace(src=x.src[:2]+(g,)) if (g:=flag_gate(m)) is not None else None),
|
||||
])
|
||||
|
||||
# ***** X86 registers *****
|
||||
@@ -217,7 +218,7 @@ CALLEE_SAVED = (RBX, RBP, GPR[12], GPR[13], GPR[14], GPR[15]) + ((RSI, RDI) + XM
|
||||
|
||||
reg_strs = {"rax": {4:"eax", 2:"ax", 1:"al"}, "rcx": {4:"ecx", 2:"cx", 1:"cl"}, "rdx": {4:"edx", 2:"dx", 1:"dl"}, "rbx": {4:"ebx", 2:"bx", 1:"bl"},
|
||||
"rsp": {4:"esp", 2:"sp", 1:"spl"}, "rbp": {4:"ebp", 2:"bp", 1:"bpl"}, "rsi": {4:"esi", 2:"si", 1:"sil"}, "rdi": {4:"edi", 2:"di", 1:"dil"},
|
||||
**{f"r{i}": {4:f"r{i}d", 2:f"r{i}w", 1:f"r{i}b"} for i in range(8, 16)}, **{f"xmm{i}": {64:f"zmm{i}", 32:f"ymm{i}"} for i in range(16)}}
|
||||
**{f"r{i}": {4:f"r{i}d", 2:f"r{i}w", 1:f"r{i}b"} for i in range(8, 16)}}
|
||||
|
||||
# ***** X86 instruction selection *****
|
||||
def base(x:UOp, i:int) -> UOp: return s.src[0] if (s:=x.src[i]).op is Ops.INDEX else s
|
||||
@@ -230,9 +231,10 @@ def to_imm(c:UOp) -> UOp|None:
|
||||
if c.dtype in dtypes.int64s: return imm(dtypes.int32, v.val) if not v.overflows(dtypes.int32) else None
|
||||
if c.dtype in dtypes.ints+(dtypes.bool,): return imm(c.dtype, v.val)
|
||||
return None
|
||||
# the flag path, which only an integer comparison can take: an x86 float compare sets carry, zero and parity together when an
|
||||
# operand is NaN, so a NaN reads as "below" and as "equal", and it clears sign and overflow, so nothing reads as "less"
|
||||
def cmp(x:UOp) -> UOp:
|
||||
if x.src[0].dtype is dtypes.float32: return x.ins(X86Ops.VUCOMISS, dtype=dtypes.void)
|
||||
if x.src[0].dtype is dtypes.float64: return x.ins(X86Ops.VUCOMISD, dtype=dtypes.void)
|
||||
if x.src[0].dtype in dtypes.floats: raise RuntimeError(f"no flag compare for {x.src[0].dtype}, a float gate must be a mask")
|
||||
return x.ins(X86Ops.CMP, dtype=dtypes.void) if (i:=to_imm(x.src[1])) is None else x.ins(X86Ops.CMPi, dtype=dtypes.void, src=(x.src[0], i))
|
||||
def vcmp(x:UOp) -> UOp:
|
||||
v = imm(dtypes.uint8, {Ops.CMPLT: 1, Ops.CMPNE: 4, Ops.CMPEQ: 0}[x.op])
|
||||
@@ -282,7 +284,7 @@ def shift(x:UOp, op:X86Ops) -> UOp:
|
||||
# it is materialized as an immediate so the address stays correct if the base register is ever spilled and refilled
|
||||
def fold_address(x:UOp) -> tuple[UOp, UOp, UOp, UOp]:
|
||||
def _disp(v:int) -> UOp: return imm(dtypes.int32 if abs(v) > dtypes.int8.max else dtypes.int8, v)
|
||||
def _cast(v:UOp) -> UOp: return v.cast(dtypes.int64) if v.vmin < 0 else v
|
||||
def _cast(v:UOp) -> UOp: return v.cast(dtypes.int64) if v.vmin < 0 else v.cast(dtypes.uint32) if v.dtype.itemsize < 4 else v
|
||||
if x.op not in {Ops.INDEX, Ops.SHRINK}: return (x, UOp(Ops.NOOP), _disp(0), imm(dtypes.uint8, x.dtype.itemsize))
|
||||
base, idx = x.src[0], x.src[1]
|
||||
# buffers are indexed by element, everything else (the stack pointer) by byte
|
||||
@@ -350,8 +352,6 @@ def alloc_vregs(ctx:IselContext, x:UOp) -> UOp|None:
|
||||
|
||||
isel_matcher = PatternMatcher([
|
||||
# **** Op -> Op ****
|
||||
# cast of void is a noop
|
||||
(UPat.var("y").cast(name="x"), lambda y,x: y if y.dtype == dtypes.void else None),
|
||||
# range is lowered to acc, cmp, jmp after regalloc
|
||||
(UPat(Ops.RANGE, src=(UPat.cvar("c").cast(),), allow_any_len=True, name="x"), lambda c,x: x.replace(src=(imm(x.dtype, c.val),) + x.src[1:])),
|
||||
(UPat(Ops.RANGE, name="x"), lambda ctx,x: x.replace(tag=(ctx.vreg(WGPR),)) if not isinstance(x.tag, tuple) else None),
|
||||
@@ -382,6 +382,9 @@ isel_matcher = PatternMatcher([
|
||||
(UPat(GroupOp.Comparison, dtypes.bool, (UPat.var("y", (dtypes.float32, dtypes.float64)), UPat()), name="x"), lambda y,x:
|
||||
UOp(Ops.AND, src=(x.replace(dtype=y.dtype).bitcast(dt:=to_int(y.dtype)), UOp.cconst(1, dt))).f(Ops.NOOP, dtype=dtypes.bool)),
|
||||
# conditional moves that use flags
|
||||
# TODO: remove this once we allow all flag producing ops in cmove
|
||||
# the blends took every float gate a mask can serve, so a gate that is still not an integer comparison becomes one here
|
||||
(UPat.var("m", dtypes.bool).where(UPat.var("a"), UPat.var("b")), lambda m,a,b: g.where(a, b) if (g:=flag_gate(m)) is not None else None),
|
||||
(UPat(Ops.CMPLT, src=(UPat(dtype=dtypes.sints), UPat()), name="m").where(UPat.var("a"), UPat.var("b")), lambda m,a,b:
|
||||
a.ins(X86Ops.CMOVL, src=(b, a, cmp(m)))),
|
||||
(UPat(Ops.CMPLT, name="m").where(UPat.var("a"), UPat.var("b")), lambda m,a,b: a.ins(X86Ops.CMOVB, src=(b, a, cmp(m)))),
|
||||
@@ -629,9 +632,8 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
r, _x, b = reg >> 3, idx >> 3, rm >> 3
|
||||
if sel: # VEX bytes
|
||||
vvvv = cast(Register, greg(vvvv_uop)).index if vvvv_uop is not None else 0
|
||||
l = (max(reg_sz, rm_sz) > 16) & 0b1
|
||||
if sel == 1 and _x == b == we == 0: inst += bytes([0xC5, (~r & 0b1) << 7 | (~vvvv & 0b1111) << 3 | l << 2 | pp])
|
||||
else: inst += bytes([0xC4, (~r & 0b1) << 7 | (~_x & 0b1) << 6 | (~b & 0b1) << 5 | sel, we << 7 | (~vvvv & 0b1111) << 3 | l << 2 | pp])
|
||||
if sel == 1 and _x == b == we == 0: inst += bytes([0xC5, (~r & 0b1) << 7 | (~vvvv & 0b1111) << 3 | pp])
|
||||
else: inst += bytes([0xC4, (~r & 0b1) << 7 | (~_x & 0b1) << 6 | (~b & 0b1) << 5 | sel, we << 7 | (~vvvv & 0b1111) << 3 | pp])
|
||||
else: # optional PREFIX and REX bytes
|
||||
# PREFIX byte signaling 16 bit variant of instruction
|
||||
if sz == 2: inst += bytes([0x66])
|
||||
@@ -695,7 +697,7 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
if x.arg in X86GroupOp.Rm2nd:
|
||||
if len(x.src) > 4: address, rest = x.src[1:5], x.src[:1] + x.src[5:]
|
||||
else: address, rest = (x.src[1], None, None, None), x.src[:1] + x.src[2:]
|
||||
# cmp/vucomiss reg, rm don't define a new register
|
||||
# cmp reg, rm doesn't define a new register
|
||||
return _encode(x, *address, *rest) if x.dtype is not dtypes.void else _encode(rest[0], *address)
|
||||
|
||||
return None
|
||||
@@ -770,8 +772,6 @@ encodings = {
|
||||
X86Ops.VPSUBB: lambda x: encode(x, 0xF8, pp=1, sel=1), X86Ops.VPSUBW: lambda x: encode(x, 0xF9, pp=1, sel=1),
|
||||
X86Ops.VPSUBD: lambda x: encode(x, 0xFA, pp=1, sel=1), X86Ops.VPSUBQ: lambda x: encode(x, 0xFB, pp=1, sel=1),
|
||||
X86Ops.VPSRAVD: lambda x: encode(x, 0x46, pp=1, sel=2),
|
||||
# float cmp
|
||||
X86Ops.VUCOMISS: lambda x: encode(x, 0x2E, pp=0, sel=1), X86Ops.VUCOMISD: lambda x: encode(x, 0x2E, pp=1, sel=1),
|
||||
# scalar / packed float binary
|
||||
X86Ops.VADDSS: lambda x: encode(x, 0x58, pp=2, sel=1), X86Ops.VADDPS: lambda x: encode(x, 0x58, pp=0, sel=1),
|
||||
X86Ops.VADDSD: lambda x: encode(x, 0x58, pp=3, sel=1), X86Ops.VADDPD: lambda x: encode(x, 0x58, pp=1, sel=1),
|
||||
|
||||
@@ -8,7 +8,7 @@ from tinygrad.runtime.support.hcq import MMIOInterface, BumpAllocator, hcq_filte
|
||||
from tinygrad.uop.ops import sint
|
||||
from tinygrad.device import Compiled, BufferSpec, TinyELF
|
||||
from tinygrad.helpers import getenv, round_up, data64_le, DEBUG, PROFILE, ProfileEvent, lo32, hi32, colored, prod, ContextVar, TracingKey
|
||||
from tinygrad.helpers import VIZ, ceildiv, unwrap, pluralize
|
||||
from tinygrad.helpers import VIZ, HCQ2, ceildiv, unwrap, pluralize
|
||||
from tinygrad.renderer.cstyle import HIPRenderer, HIPCCRenderer
|
||||
from tinygrad.renderer.llvmir import AMDLLVMRenderer
|
||||
from tinygrad.runtime.autogen import kfd, hsa, sqtt, amdgpu_kd, amdgpu_drm
|
||||
@@ -1153,4 +1153,4 @@ class AMDDevice(HCQCompiled):
|
||||
|
||||
def hw_copy_queues(self): return [(f"SDMA:{i}", functools.partial(unwrap(self.hw_copy_queue_t), queue_idx=i)) for i in self.sdma_queues]
|
||||
|
||||
if getenv("HCQ2"): from extra.hcq2.ops_amd2 import * # noqa: F401, F403 # pylint: disable=unused-import
|
||||
if HCQ2: from extra.hcq2.ops_amd2 import * # noqa: F401, F403 # pylint: disable=unused-import
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Callable, Type, TypeVar, Generic, Any
|
||||
import contextlib, decimal, statistics, time, ctypes, array, os, struct, collections, itertools
|
||||
import contextlib, decimal, statistics, time, ctypes, array, os, collections, itertools
|
||||
try: import fcntl # windows misses that
|
||||
except ImportError: fcntl = None #type:ignore[assignment]
|
||||
from tinygrad.helpers import DEV, PROFILE, getenv, to_mv, from_mv, cpu_profile, ProfileRangeEvent, unwrap
|
||||
from tinygrad.helpers import DEV, PROFILE, getenv, from_mv, cpu_profile, ProfileRangeEvent, unwrap
|
||||
from tinygrad.helpers import suppress_finalizing, pluralize, TracingKey
|
||||
from tinygrad.device import Device, BufferSpec, Compiled, LRUAllocator, ProfileDeviceEvent, ProfileProgramEvent, Program, TinyELF
|
||||
from tinygrad.uop.ops import sym_infer, sint, UOp
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.runtime.support.memory import BumpAllocator, MMIOInterface
|
||||
from tinygrad.renderer import Renderer
|
||||
|
||||
class MMIOInterface:
|
||||
def __init__(self, addr:int, nbytes:int, fmt='B'): self.mv, self.addr, self.nbytes, self.fmt = to_mv(addr, nbytes).cast(fmt), addr, nbytes, fmt
|
||||
def __len__(self): return self.nbytes // struct.calcsize(self.fmt)
|
||||
def __getitem__(self, k): return (self.mv[k] if self.fmt == 'B' else self.mv[k].tolist()) if isinstance(k, slice) else self.mv[k]
|
||||
def __setitem__(self, k, v): self.mv[k] = v
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None) -> MMIOInterface:
|
||||
return MMIOInterface(self.addr+offset, (self.nbytes - offset) if size is None else size, fmt=fmt or self.fmt)
|
||||
|
||||
class FileIOInterface:
|
||||
"""
|
||||
Hardware Abstraction Layer for HCQ devices. The class provides a unified interface for interacting with hardware devices.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, TypeVar, Generic, Any, Sequence, Iterable
|
||||
from typing import cast, TypeVar, Generic, Any, Sequence, Iterable, TYPE_CHECKING
|
||||
import struct, functools, time, collections, itertools, decimal, statistics
|
||||
from dataclasses import replace, dataclass, field
|
||||
from tinygrad.helpers import suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE, unwrap, PROFILE
|
||||
@@ -9,11 +9,11 @@ from tinygrad.device import ProfileDeviceEvent, ProfileGraphEntry, ProfileGraphE
|
||||
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, rewrite_group, GroupOp
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.dtype import dtypes, truncate, DType
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface, HCQBuffer
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.runtime.support.memory import BumpAllocator, MMIOInterface
|
||||
from tinygrad.renderer import Renderer, Estimates
|
||||
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop
|
||||
from tinygrad.engine.realize import pm_flatten_linear, lower_and_compile
|
||||
from tinygrad.engine.realize import to_program, get_call_arg_uops, get_call_name, get_call_outs_ins, estimate_uop, pm_flatten_linear,lower_and_compile
|
||||
|
||||
if TYPE_CHECKING: from tinygrad.runtime.support.hcq import HCQBuffer # TODO: remove that
|
||||
|
||||
# *****************
|
||||
# 0. helpers
|
||||
@@ -168,8 +168,8 @@ class BatchCtx:
|
||||
slots:dict[str, int] = field(default_factory=lambda: collections.defaultdict(lambda: next(UOp.unique_num)))
|
||||
|
||||
def _get_call_bufs_by_lane(call:UOp, devices:tuple[str, ...]) -> list[list[Any]]:
|
||||
return [[b if (b:=_lane(a, lane)).op is Ops.PARAM or (b.op is Ops.MSELECT and b.src[0].op is Ops.PARAM) else b.buffer
|
||||
for a in get_call_arg_uops(call)] for lane in range(len(devices))]
|
||||
def dep_buf(b:UOp) -> Any: return base if (base:=(b.src[0] if b.op is Ops.MSELECT else b).base).op is Ops.PARAM else b.buffer
|
||||
return [[dep_buf(_lane(a, lane)) for a in get_call_arg_uops(call)] for lane in range(len(devices))]
|
||||
|
||||
def _wait_ins(ctx:BatchCtx, bufs_by_lane:list[list[Any]], write, devices:tuple[str, ...], queue:str, tag:int) -> list[UOp]:
|
||||
deps:list[Dep] = []
|
||||
@@ -462,7 +462,7 @@ def hcq_lower(linear:UOp, pm_encode:PatternMatcher) -> UOp:
|
||||
linear = graph_rewrite(linear, pm_split_patches, walk=True, name="split patches")
|
||||
|
||||
# and compile it
|
||||
return lower_and_compile(graph_rewrite(linear, pm_replace_params, walk=True, name="replace params"))
|
||||
with Context(EMULATED_DTYPES=""): return lower_and_compile(graph_rewrite(linear, pm_replace_params, walk=True, name="replace params"))
|
||||
|
||||
@rewrite_group(lambda linear,input_uops,profile,ret: f"HCQ Compile {pluralize('Kernel', len(ret.src))}")
|
||||
def hcq_compile(linear:UOp, input_uops:list[UOp]|None, profile:bool) -> UOp:
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import collections, functools, dataclasses, enum
|
||||
from __future__ import annotations
|
||||
import collections, functools, dataclasses, enum, struct
|
||||
from typing import Any, ClassVar
|
||||
from tinygrad.helpers import round_up, getenv
|
||||
from tinygrad.helpers import round_up, getenv, to_mv
|
||||
|
||||
class MMIOInterface:
|
||||
def __init__(self, addr:int, nbytes:int, fmt='B'): self.mv, self.addr, self.nbytes, self.fmt = to_mv(addr, nbytes).cast(fmt), addr, nbytes, fmt
|
||||
def __len__(self): return self.nbytes // struct.calcsize(self.fmt)
|
||||
def __getitem__(self, k): return (self.mv[k] if self.fmt == 'B' else self.mv[k].tolist()) if isinstance(k, slice) else self.mv[k]
|
||||
def __setitem__(self, k, v): self.mv[k] = v
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None) -> MMIOInterface:
|
||||
return MMIOInterface(self.addr+offset, (self.nbytes - offset) if size is None else size, fmt=fmt or self.fmt)
|
||||
|
||||
class BumpAllocator:
|
||||
def __init__(self, size:int, base:int=0, wrap:bool=True): self.size, self.ptr, self.base, self.wrap = size, 0, base, wrap
|
||||
|
||||
@@ -94,7 +94,7 @@ pm_post_sched_cache = PatternMatcher([
|
||||
# only resolve buffer PARAMs (slot>=0); ALU/shape vars use slot=-1 and must not be swapped for call args
|
||||
(UPat(Ops.PARAM, name="x"), lambda ctx,x: ctx[1][x.arg.slot] if x.arg.slot >= 0 else None),
|
||||
# create new BUFFERs
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="b"), lambda ctx,b:
|
||||
(UPat(Ops.BUFFER, src=(), name="b"), lambda ctx,b:
|
||||
create_new_buffer(ctx, b) if isinstance(b.arg, ParamArg) and b.addrspace is AddrSpace.GLOBAL else None),
|
||||
])
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class IndexingContext:
|
||||
|
||||
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.BUFFER,
|
||||
Ops.CONST, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
|
||||
Ops.LOAD, Ops.CALL, Ops.FUNCTION}
|
||||
Ops.LOAD, Ops.CALL}
|
||||
|
||||
def realize(ctx:IndexingContext, tr:UOp) -> None: ctx.realize_map[tr] = None
|
||||
|
||||
@@ -204,7 +204,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> UOp:
|
||||
ending_ranges: dict[UOp, list[UOp]] = {}
|
||||
for x in reversed(tsink_toposort):
|
||||
# no ranges on kernels, they are internal
|
||||
if x.op in {Ops.CALL, Ops.FUNCTION, Ops.LINEAR}: continue
|
||||
if x.op in {Ops.CALL, Ops.LINEAR}: continue
|
||||
|
||||
# AFTER doesn't have range
|
||||
if x.op is Ops.AFTER: continue
|
||||
|
||||
+10
-21
@@ -269,24 +269,15 @@ def passthrough_multi(root:UOp, multi:UOp):
|
||||
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
|
||||
if call.arg is None or call.arg.precompile: return None
|
||||
# the call body is a plain parametric program: multi rewrites it like anything else (the output PARAM dests sub-view per
|
||||
# shard through the normal store rules), and all srcs (args and RETURNEDs) become their per-shard views
|
||||
new_body = graph_rewrite(call.src[0], multi_pm, name="subcall")
|
||||
new_args = tuple(a.src[0] if a.op is Ops.UNSHARD else a for a in call.src[1:])
|
||||
# after multi resolution, TUPLE elements may be UNSHARD — strip UNSHARD from body, create per-shard FUNCTION, wrap each GETTUPLE in its own UNSHARD
|
||||
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.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)
|
||||
|
||||
def param_to_multi(p:UOp):
|
||||
if p.axis is None: return None
|
||||
return UOp.param(p.arg.slot, p.dtype, p.shard_shape, p.device, p.arg.vmin_vmax, p.arg.multiple_of, p.arg.name, p.arg.addrspace).unshard(p.axis)
|
||||
assert new_body.op is Ops.SINK
|
||||
return call.replace(src=(new_body,) + tuple(a.src[0] if a.op is Ops.UNSHARD else a for a in call.src[1:]))
|
||||
|
||||
# NOTE: this is the same pattern as unrolled ranges
|
||||
multi_pm = PatternMatcher([
|
||||
(UPat(Ops.PARAM, name="p"), param_to_multi),
|
||||
(UPat(GroupOp.ALU, name="root", custom_early_reject=set([Ops.UNSHARD])), alu_multi),
|
||||
(UPat(Ops.REDUCE, src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), reduce_multi),
|
||||
(UPat(Ops.RESHAPE, src=(UPat(Ops.UNSHARD, name="multi"), UPat()), name="root"), reshape_multi),
|
||||
@@ -302,14 +293,12 @@ multi_pm = PatternMatcher([
|
||||
(UPat(Ops.ALLREDUCE, src=(UPat(Ops.UNSHARD, name="multi"),), name="red"),
|
||||
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]),
|
||||
# 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),
|
||||
# just strip the UNSHARD from non-value-producing CALLs (custom kernels, etc.) — FUNCTION is handled by rewrite_into_function
|
||||
# rewrite value-producing calls explicitly for UNSHARD
|
||||
(UPat(Ops.CALL, name="call"), lambda call: rewrite_into_function(call) if call.num_returned else None),
|
||||
(UPat((Ops.CALL, Ops.AFTER), src=(UPat(Ops.UNSHARD, name="multi"), ), name="root", allow_any_len=True), passthrough_multi),
|
||||
# just strip the UNSHARD from non-value-producing CALLs (custom kernels, etc.) — value-producing CALLs are handled by rewrite_into_function
|
||||
(UPat(Ops.CALL, dtype=dtypes.void, name="root", custom_early_reject=set([Ops.UNSHARD])), lambda root:
|
||||
UOp(root.op, src=tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src), arg=root.arg)),
|
||||
UOp(root.op, src=tuple(x.src[0] if x.op is Ops.UNSHARD else x for x in root.src), arg=root.arg) if root.num_returned == 0 else None),
|
||||
(UPat((Ops.CAST, Ops.BITCAST, Ops.CONTIGUOUS, Ops.DETACH, Ops.CONTIGUOUS_BACKWARD),
|
||||
src=(UPat(Ops.UNSHARD, name="multi"), ), name="root"), passthrough_multi),
|
||||
# STORE of a sharded value into an unsharded dest (e.g. a fragment into a full output tile)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import itertools
|
||||
from tinygrad.dtype import dtypes, to_dtype
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp
|
||||
from tinygrad.uop.ops import graph_rewrite, rewrite_group, shape_to_shape_arg, ParamArg, identity_element
|
||||
from tinygrad.uop.ops import graph_rewrite, rewrite_group, ParamArg, identity_element, resolve_returned_after
|
||||
from tinygrad.uop.movement import mop_cleanup
|
||||
from tinygrad.helpers import prod, getenv, all_int, DEBUG, SPLIT_REDUCEOP, OPENPILOT_HACKS, FLOAT16, argsort
|
||||
from tinygrad.schedule.indexing import apply_movement_op
|
||||
@@ -93,6 +93,7 @@ def resolve_function(c:UOp, allow_param_mismatch=True) -> UOp|None:
|
||||
params: list[UOp] = []
|
||||
graph_rewrite(c.src[0], pm_gather_params, bottom_up=True, ctx=params, name="gather params")
|
||||
params = sorted(params, key=lambda x: x.arg.slot)
|
||||
# the RETURNED inputs bind to the output PARAMs (slots after the input PARAM slots), just like the args bind to input PARAMs
|
||||
args = c.src[1:]
|
||||
|
||||
# NOTE: this isn't really needed. it's okay if there's unused args in the function
|
||||
@@ -100,10 +101,19 @@ def resolve_function(c:UOp, allow_param_mismatch=True) -> UOp|None:
|
||||
if [x.arg.slot for x in params] != list(range(len(params))): raise RuntimeError(f"params not in order: {[x.arg.slot for x in params]}")
|
||||
if len(params) != len(args): raise TypeError(f"expected {len(params)} args, got {len(args)}")
|
||||
|
||||
# params have a flat storage size in the arg, the logical shape is a view (RESHAPE/SHRINK/UNSHARD) on top of it.
|
||||
# substitute args by their flat max-shaped storage view so the movement views on the params stay valid
|
||||
def flat_storage(a:UOp) -> tuple[int, UOp]: # returns (size, view of a as flat max-shaped storage)
|
||||
shp = a.max_shard_shape if a.axis is not None and isinstance(a.device, tuple) else a.max_shape
|
||||
return (n:=prod(shp)), a if a.shape == (n,) else a.pad_to(shp).reshape((n,))
|
||||
dict_map = {x:args[x.arg.slot] for x in params}
|
||||
for i, (p, a) in enumerate(dict_map.items()):
|
||||
if p.axis != a.axis: raise TypeError(f"arg {i} axis mismatch: expected {p.axis}, got {a.axis}")
|
||||
if p.max_shape != a.max_shape: raise TypeError(f"arg {i} shape mismatch: expected {p.shape}, got {a.shape}")
|
||||
if p.arg.size is not None:
|
||||
n, flat = flat_storage(a)
|
||||
if p.arg.size != n: raise TypeError(f"arg {i} shape mismatch: expected size {p.arg.size}, got {a.shape}")
|
||||
dict_map[p] = flat
|
||||
elif a.shape != ():
|
||||
raise TypeError(f"arg {i} shape mismatch: expected scalar, got {a.shape}")
|
||||
if p.dtype != a.dtype: raise TypeError(f"arg {i} dtype mismatch: expected {p.dtype}, got {a.dtype}")
|
||||
return c.src[0].substitute(dict_map, walk=True)
|
||||
|
||||
@@ -120,11 +130,14 @@ def expand_bitcast(bc:UOp) -> UOp|None:
|
||||
return parts[0].stack(*parts[1:], dim=-1).flatten(-2).cast(new_uint).bitcast(bc.dtype)
|
||||
|
||||
earliest_rewrites = mop_cleanup+PatternMatcher([
|
||||
# resolve FUNCTION calls (inline the body)
|
||||
(UPat(Ops.FUNCTION, name="c"), resolve_function),
|
||||
# resolve calls with RETURNED inputs (inline the body)
|
||||
(UPat(Ops.CALL, name="c"), lambda c: resolve_function(c) if c.num_returned else None),
|
||||
|
||||
# resolve TUPLE+GETTUPLE
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
|
||||
# resolve AFTER on RETURNED (call outputs)
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
|
||||
# an AFTER resolved to its return value in the tensor graph (by returned_after_finalize) is just the value
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(name="t")), allow_any_len=True),
|
||||
lambda r,t: t if r.unsharded_base.op is Ops.RETURNED and t.op is not Ops.CALL else None),
|
||||
|
||||
# resolve allreduce (must be bottom up)
|
||||
(UPat(Ops.ALLREDUCE, src=(UPat.var("buf"),), name="red"), create_allreduce_function),
|
||||
@@ -192,9 +205,9 @@ def convert_copy_to_store(ctx, copy:UOp, existing_buf:UOp|None=None):
|
||||
# if there's already a buffer, we just use it
|
||||
return existing_buf.flatten().store(input_src)
|
||||
# create the output buffer
|
||||
buf = UOp(Ops.BUFFER, src=(shape_to_shape_arg(input_src.max_shape),), arg=ParamArg(next(ctx), copy.dtype, device=copy.device))
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(ctx), copy.dtype, size=prod(input_src.max_shape), device=copy.device))
|
||||
# reshape back to input
|
||||
return buf.after(buf.store(input_src)).reshape(copy.shape)
|
||||
return buf.reshape(input_src.max_shape).after(buf.store(input_src)).reshape(copy.shape)
|
||||
|
||||
pm_copy_to_store = PatternMatcher([
|
||||
(UPat(name="existing_buf").store(UPat(Ops.COPY, name="copy")), convert_copy_to_store),
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass, field, replace
|
||||
from typing import cast
|
||||
import itertools
|
||||
from tinygrad.dtype import dtypes, AddrSpace, Invalid
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, KernelInfo, ParamArg, shape_to_shape_arg
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, KernelInfo, ParamArg
|
||||
from tinygrad.uop.ops import graph_rewrite, sint, AxisType, BottomUpGate, rewrite_group
|
||||
from tinygrad.uop.symbolic import symbolic
|
||||
from tinygrad.helpers import prod, dedup, DEBUG_RANGEIFY, VIZ, MAX_KERNEL_BUFFERS, SPEC
|
||||
@@ -162,6 +162,12 @@ pm_no_indexing_calls = PatternMatcher([
|
||||
(UPat(Ops.CALL, name="u"), no_indexing_calls),
|
||||
])
|
||||
|
||||
# the kernel graph is what gets executed: no shape views left in it, the storage of a value is just the storage
|
||||
pm_no_views = PatternMatcher([
|
||||
(UPat((Ops.RESHAPE, Ops.SHRINK), name="v", src=(UPat((Ops.AFTER, Ops.PARAM, Ops.UNSHARD, Ops.MSTACK, Ops.BUFFER)),), allow_any_len=True), lambda v:
|
||||
v.src[0]),
|
||||
])
|
||||
|
||||
DEVICE_MAX_BUFS = {"METAL": 31, "WEBGPU": 8, "CPU": 31} # TODO: get from device?
|
||||
@dataclass
|
||||
class LimitBufsContext:
|
||||
@@ -223,7 +229,7 @@ def bufferize_to_store(ctx:itertools.count, x:UOp, idx:UOp, allow_locals=True):
|
||||
|
||||
# NOTE: the local BUFFER needs to be disambiguated here
|
||||
if x.arg.addrspace == AddrSpace.GLOBAL:
|
||||
buf = UOp(Ops.BUFFER, src=(shape_to_shape_arg((size,)),), arg=ParamArg(next(ctx), x.dtype, device=x.arg.device, addrspace=AddrSpace.GLOBAL))
|
||||
buf = UOp(Ops.BUFFER, arg=ParamArg(next(ctx), x.dtype, size=size, device=x.arg.device, addrspace=AddrSpace.GLOBAL))
|
||||
do_store = buf.index(idx).store(x.src[0]).end(*rngs)
|
||||
return buf.after(do_store)
|
||||
|
||||
@@ -282,8 +288,7 @@ class LocalAddBufferContext:
|
||||
def debuf(ctx:LocalAddBufferContext, buf:UOp):
|
||||
# Variables (ALU buffers with a value range) are scalar symbolic values, not real buffers: they become ALU params with no slot
|
||||
if buf.is_variable: return buf.replace(op=Ops.PARAM)
|
||||
param = UOp(Ops.PARAM, src=(UOp.const(prod(buf.max_shape)),),
|
||||
arg=ParamArg(ctx.dg, buf.dtype, addrspace=buf.addrspace, device=buf.device))
|
||||
param = UOp(Ops.PARAM, arg=ParamArg(ctx.dg, buf.dtype, prod(buf.max_shape), addrspace=buf.addrspace, device=buf.device))
|
||||
ret = param.reshape(buf.max_shape)
|
||||
# if the buffer has symbolic shape, shrink the max-sized view to the actual shape
|
||||
if buf.max_shape != buf.shape: ret = ret.shrink(tuple((0, s) for s in buf.shape))
|
||||
@@ -383,6 +388,7 @@ def get_kernel_graph(tsink:UOp) -> UOp:
|
||||
tsink = graph_rewrite(tsink, pm_add_buffers+pm_add_param_range_tags, ctx=itertools.count(paramarg_start), bottom_up=True, name="stage to store")
|
||||
tsink = graph_rewrite(tsink, split_kernels, bottom_up=True, name="split kernels")
|
||||
tsink = graph_rewrite(tsink, pm_no_indexing_calls, name="remove indexing from call args")
|
||||
tsink = graph_rewrite(tsink, pm_no_views, name="remove views from the kernel graph")
|
||||
|
||||
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Kernel Graph")
|
||||
if SPEC:
|
||||
|
||||
+47
-21
@@ -9,6 +9,7 @@ from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtyp
|
||||
from tinygrad.helpers import all_int, getenv, fetch, Metadata, TRACEMETA, TracingKey
|
||||
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc, VIZ, pluralize
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, Variable, ConstLike, UPat, PatternMatcher, GroupOp, ParamArg, graph_rewrite, rewrite_group
|
||||
from tinygrad.uop.ops import resolve_returned_after
|
||||
from tinygrad.mixin.rand import RandMixin
|
||||
from tinygrad.schedule import create_linear_with_vars
|
||||
from tinygrad.device import Buffer, canonicalize_device
|
||||
@@ -107,15 +108,19 @@ def _precompiled_output_redirect(s:UOp, t:UOp) -> UOp|None:
|
||||
return None
|
||||
|
||||
def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
if not c.arg.precompile: return None
|
||||
assert c.src[0].op is Ops.TUPLE, f"expected TUPLE body for precompiled FUNCTION, got {c.src[0].op}"
|
||||
input_buffers = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in c.src[1:])
|
||||
if c.arg is None or not c.arg.precompile or c.num_returned == 0: return None
|
||||
assert c.src[0].op is Ops.SINK, f"expected SINK body for precompiled call, got {c.src[0].op}"
|
||||
# the RETURNED inputs are the call outputs (they are always the last srcs), the outputs are the stores into the
|
||||
# output PARAMs (slots after the args) in slot order
|
||||
n_ret = c.num_returned
|
||||
call_args, returned = c.src[1:len(c.src)-n_ret], c.src[len(c.src)-n_ret:]
|
||||
input_buffers = tuple(x.contiguous() if x.op is not Ops.AFTER else x for x in call_args)
|
||||
out_stores = sorted((st for st in c.src[0].src if st.op is Ops.STORE), key=lambda st: st.src[0].unsharded_base.arg.slot)
|
||||
srcs = tuple(st.src[1] for st in out_stores)
|
||||
|
||||
# add the outputs to the call
|
||||
srcs = c.src[0].src
|
||||
resolved = [c.gettuple(i) for i in range(len(srcs))]
|
||||
outs = tuple(r.empty_like() for r in resolved)
|
||||
targets = [o.param_like(len(c.src)-1+i).shrink_to(s.shape) for i,(o,s) in enumerate(zip(outs, srcs))]
|
||||
outs = tuple(r.empty_like() for r in returned)
|
||||
targets = [o.param_like(len(input_buffers)+i).shrink_to(s.shape) for i,(o,s) in enumerate(zip(outs, srcs))]
|
||||
|
||||
subs:dict[UOp, UOp] = {}
|
||||
items:list[UOp] = []
|
||||
@@ -131,23 +136,44 @@ def transform_precompiled_call(c:UOp) -> UOp|None:
|
||||
items.append(t.after(t.store(s.after(*after_deps))))
|
||||
fxn = UOp.sink(*(x.substitute(subs) for x in items))
|
||||
|
||||
# body switches from TUPLE to SINK, so the node becomes an opaque CALL (not FUNCTION)
|
||||
# all bodies are SINKs now, the node just becomes an opaque CALL
|
||||
new_call = UOp(Ops.CALL, src=(fxn, *input_buffers, *outs), arg=c.arg)
|
||||
rets = tuple(o.after(new_call) for o in outs)
|
||||
|
||||
# if the CALL has symbolic shapes, shrink the max-sized output to the actual symbolic shape
|
||||
# NOTE: must use resolved shapes from the FUNCTION (which substitutes PARAMs with external args), not raw body shapes
|
||||
rets = tuple(r.shrink_to(rs.shape) for r,rs in zip(rets, resolved))
|
||||
# NOTE: must use the resolved shapes of the RETURNED placeholders (which substitute PARAMs with external args), not raw body shapes
|
||||
rets = tuple(r.shrink_to(rs.shape) for r,rs in zip(rets, returned))
|
||||
|
||||
return UOp.maketuple(*rets)
|
||||
# the AFTER outputs resolve against this: stores of each real output into its RETURNED placeholder
|
||||
return UOp.sink(*[r.store(v) for r, v in zip(returned, rets)])
|
||||
|
||||
_resolved_calls: dict[UOp, UOp|None] = {}
|
||||
def returned_after_finalize(r:UOp) -> UOp|None:
|
||||
# resolve AFTERs on RETURNED placeholders (function call outputs) while we are still in the tensor graph, like any
|
||||
# other value (this inlines the call body); afters between the return and the call don't matter
|
||||
x = r.src[1]
|
||||
if len(r.src) != 2 or x.op is not Ops.CALL or x.src[0].op is not Ops.SINK or x.num_returned == 0 \
|
||||
or r.src[0].unsharded_base.op is not Ops.RETURNED: return None
|
||||
# don't inline calls with bound-variable or unresolved sharded (UNSHARD) args in the tensor graph,
|
||||
# those get resolved at schedule time
|
||||
if any(any(u.op is Ops.UNSHARD or u.is_variable or u.is_bound_var for u in a.toposort(enter_calls=False)) for a in x.src[1:]): return None
|
||||
if x not in _resolved_calls:
|
||||
from tinygrad.schedule.prepare import resolve_function
|
||||
_resolved_calls[x] = resolve_function(x)
|
||||
if (inlined := _resolved_calls[x]) is None or (v := resolve_returned_after(r.src[0], inlined)) is None: return None
|
||||
return r.src[0].after(v).replace(tag=r.tag)
|
||||
|
||||
# NOTE: adding rules to here is bad. these all need to run before the schedule cache
|
||||
pm_early_transform_tensor_graph = PatternMatcher([
|
||||
# transform precompiled FUNCTIONs into CALLs (body becomes SINK with stores)
|
||||
(UPat(Ops.FUNCTION, name="c"), transform_precompiled_call),
|
||||
# transform precompiled value-producing calls into opaque CALLs (outputs become real buffers)
|
||||
(UPat(Ops.CALL, name="c"), transform_precompiled_call),
|
||||
|
||||
# resolve TUPLE+GETTUPLE (for precompiled calls)
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), lambda g,t: t.src[g.arg]),
|
||||
# resolve AFTER on RETURNED placeholders (for precompiled calls)
|
||||
(UPat(Ops.AFTER, src=(UPat(name="r"), UPat(Ops.SINK, name="t")), allow_any_len=True), resolve_returned_after),
|
||||
|
||||
# resolve AFTERs on RETURNED placeholders (function call outputs) into their values while we are still in the tensor
|
||||
# graph, like any other value (this inlines the call body); afters between the return and the call don't matter
|
||||
(UPat(Ops.AFTER, name="r"), returned_after_finalize),
|
||||
|
||||
# fold MOPS+BITCAST over BUFFER into SHRINK when movement ops collapse to contiguous range
|
||||
(UPat((Ops.COPY, Ops.CONTIGUOUS), src=(UPat(GroupOp.Movement|{Ops.BITCAST}, name="src"),), name="c"), contiguous_mops_to_view),
|
||||
@@ -178,6 +204,8 @@ pm_early_transform_tensor_graph = PatternMatcher([
|
||||
def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
# bound Variables are call inputs, not assigns: they stay in the graph and pm_replace_buf turns them into call args
|
||||
if x.is_bound_var: return None
|
||||
# AFTER on a RETURNED placeholder is a call output, not an assign: it's inlined when the call is resolved
|
||||
if x.src[0].unsharded_base.op is Ops.RETURNED: return None
|
||||
# untagged: record as an assign for the call body
|
||||
if x.tag is None:
|
||||
ctx.assigns.append(x)
|
||||
@@ -198,9 +226,7 @@ def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
if b.is_bound_var or b.is_variable: return b.param_like(len(ctx.replacements)-1)
|
||||
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
|
||||
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
|
||||
return b.param_like(len(ctx.replacements)-1)
|
||||
|
||||
def replace_input_view(ctx:AllocCtx, b:UOp): return replace_input_buffer(ctx, b) if b in ctx.views else None
|
||||
|
||||
@@ -211,7 +237,7 @@ pm_finalize_call = PatternMatcher([
|
||||
|
||||
pm_replace_buf = PatternMatcher([
|
||||
# replace BUFFER with PARAM for cache key normalization
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="b"), lambda ctx,b:
|
||||
(UPat(Ops.BUFFER, src=(), name="b"), lambda ctx,b:
|
||||
replace_input_buffer(ctx, b) if isinstance(b.arg, ParamArg) and b.addrspace is AddrSpace.GLOBAL else None),
|
||||
# replace SHRINK with PARAM
|
||||
(UPat(Ops.SHRINK, src=(UPat(Ops.BUFFER),), name="b", allow_any_len=True), replace_input_view),
|
||||
@@ -381,11 +407,11 @@ class Tensor(RandMixin):
|
||||
# ***** data handlers ****
|
||||
|
||||
def as_param(self, slot:int):
|
||||
return Tensor(UOp.param(slot, self.dtype, self.uop.shard_shape, self.device, axis=self.uop.axis))
|
||||
return Tensor(self.uop.param_like(slot))
|
||||
|
||||
def call(self, *lst:Tensor, fxn:Tensor|UOp, grad_fxn:Callable|None=None) -> Tensor:
|
||||
fret = fxn._uop.call(*[t.uop for t in (self,)+lst], grad_fxn=grad_fxn)
|
||||
return Tensor(fret.gettuple(0))
|
||||
return Tensor(fret.returned_outputs[0])
|
||||
|
||||
def custom_kernel(self, *lst:Tensor, fxn:Callable, grad_fxn:Callable|None=None) -> list[Tensor]:
|
||||
"""
|
||||
|
||||
@@ -23,8 +23,8 @@ class Ops(FastEnum):
|
||||
|
||||
# uops that aren't rendered
|
||||
NOOP = auto(); REWRITE_ERROR = auto()
|
||||
# FUNCTION has a TUPLE body and is gradient-able; CALL is an opaque kernel invocation
|
||||
PARAM = auto(); FUNCTION = auto(); CALL = auto()
|
||||
# CALL is a kernel invocation; calls with RETURNED inputs are value-producing (and gradient-able), the rest are opaque
|
||||
PARAM = auto(); CALL = auto()
|
||||
|
||||
# renderer
|
||||
# LINEAR is a list of UOps, SOURCE has a str arg that's human readable, BINARY has bytes arg that's compiled
|
||||
@@ -37,8 +37,8 @@ class Ops(FastEnum):
|
||||
# vector creation / item selection
|
||||
STACK = auto()
|
||||
|
||||
# tuple/gettuple for function with multiple returns
|
||||
TUPLE = auto(); GETTUPLE = auto()
|
||||
# RETURNED is a placeholder for a buffer a call writes and returns: it's an input to the call and you AFTER on it
|
||||
RETURNED = auto()
|
||||
|
||||
# hcq specific
|
||||
GETADDR = auto()
|
||||
|
||||
+102
-68
@@ -23,17 +23,21 @@ class AxisType(Enum):
|
||||
class ParamArg:
|
||||
slot: int
|
||||
dtype: DType
|
||||
# number of elements in the buffer. always a concrete int (never symbolic), None for scalars (shape ())
|
||||
size: int|None = None
|
||||
vmin_vmax: tuple[PyConst, PyConst]|None = None
|
||||
multiple_of: int|None = None
|
||||
name: str|None = None
|
||||
addrspace: AddrSpace|None = AddrSpace.GLOBAL
|
||||
axis: int|None = None
|
||||
device: str|tuple[str, ...]|None = None
|
||||
volatile: bool = False
|
||||
# (h, w) if this is an image2d buffer, then size == h*w*4
|
||||
image: tuple[int, int]|None = None
|
||||
def __repr__(self):
|
||||
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None),
|
||||
("volatile", False))
|
||||
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
|
||||
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("device", None),
|
||||
("volatile", False), ("image", None))
|
||||
args = [repr(self.slot), repr(self.dtype)] + ([repr(self.size)] if self.size is not None else []) + \
|
||||
[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.WEAK: "L",
|
||||
AxisType.LOOP: "L", AxisType.UPCAST: "u", AxisType.GROUP_REDUCE: "G", AxisType.REDUCE: "R", AxisType.UNROLL: "r"}
|
||||
@@ -45,7 +49,7 @@ axis_colors = {AxisType.DEVICE: "green", AxisType.GLOBAL: "blue", AxisType.THREA
|
||||
axis_to_pos = {AxisType.DEVICE: -2, AxisType.WEAK: -1, AxisType.LOOP: -1, AxisType.THREAD: 0, AxisType.GLOBAL: 0, AxisType.WARP: 1,
|
||||
AxisType.LOCAL: 2, AxisType.UPCAST: 3, AxisType.GROUP_REDUCE: 2, AxisType.REDUCE: 4, AxisType.UNROLL: 5}
|
||||
|
||||
range_start = {Ops.STAGE: 1, Ops.REDUCE: 1, Ops.WMMA: 3, Ops.END: 1, Ops.CALL: 1, Ops.FUNCTION: 1, Ops.LINEAR: 0}
|
||||
range_start = {Ops.STAGE: 1, Ops.REDUCE: 1, Ops.WMMA: 3, Ops.END: 1, Ops.CALL: 1, Ops.LINEAR: 0}
|
||||
|
||||
# https://en.wikipedia.org/wiki/Identity_element
|
||||
def identity_element(op:Ops, dt:DType) -> PyConst: return dt.const({Ops.ADD:0, Ops.MUL:1, Ops.MAX:dt.min}[op])
|
||||
@@ -116,7 +120,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
match op:
|
||||
case Ops.STORE | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
|
||||
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | \
|
||||
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.REWRITE_ERROR | Ops.PYLITERAL:
|
||||
Ops.CUSTOM_FUNCTION | Ops.REWRITE_ERROR | Ops.PYLITERAL:
|
||||
# always void
|
||||
return dtypes.void
|
||||
case Ops.CALL:
|
||||
@@ -153,10 +157,6 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
case Ops.WMMA:
|
||||
# WMMA output dtype is the accumulator dtype (src[2])
|
||||
return src[2].dtype
|
||||
case Ops.GETTUPLE:
|
||||
# GETTUPLE extracts from a TUPLE (possibly through a FUNCTION)
|
||||
in_tuple = src[0].src[0] if src[0].op is Ops.FUNCTION else src[0]
|
||||
return in_tuple.src[arg].dtype
|
||||
case Ops.GETADDR:
|
||||
return dtypes.uint64
|
||||
case Ops.THREEFRY:
|
||||
@@ -167,8 +167,8 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
if not all(dtypes.is_int(x.dtype) or x.base.is_invalid for x in src):
|
||||
raise RuntimeError(f"shift operands must be int, got {[x.dtype for x in src]}")
|
||||
return src[0].dtype
|
||||
case Ops.BUFFER | Ops.PARAM:
|
||||
assert isinstance(arg, ParamArg), "BUFFER/PARAM must have ParamArg"
|
||||
case Ops.BUFFER | Ops.PARAM | Ops.RETURNED:
|
||||
assert isinstance(arg, ParamArg), f"{op} must have ParamArg"
|
||||
return arg.dtype
|
||||
case Ops.BINARY:
|
||||
return dtypes.uint8
|
||||
@@ -306,7 +306,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if not visited:
|
||||
if gate is None or gate(node):
|
||||
stack.append((node, True)) # push node back on stack to process after its srcs
|
||||
for s in reversed(node.src if enter_calls or node.op not in {Ops.CALL, Ops.FUNCTION} else node.src[1:]):
|
||||
for s in reversed(node.src if enter_calls or node.op is not Ops.CALL else node.src[1:]):
|
||||
stack.append((s, False)) # push srcs on the stack
|
||||
else: cache[node] = None # second time i'm seeing this node, add it to returned toposort
|
||||
return cache
|
||||
@@ -335,7 +335,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
match self.op:
|
||||
# late ops don't have shape
|
||||
case Ops.IF | Ops.BARRIER | Ops.SINK | Ops.REWRITE_ERROR | Ops.ENDIF | Ops.GROUP | \
|
||||
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE | Ops.TUPLE | Ops.FUNCTION:
|
||||
Ops.LINEAR | Ops.PROGRAM | Ops.SOURCE:
|
||||
return None
|
||||
|
||||
# a void CALL has no shape, the return value of a CALL has the shape of its dtype
|
||||
@@ -355,17 +355,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
case Ops.NOOP:
|
||||
return self.src[0]._shape if len(self.src) >= 1 else None
|
||||
|
||||
case Ops.GETTUPLE:
|
||||
# GETTUPLE extracts from a TUPLE (possibly through a FUNCTION)
|
||||
in_tuple = self.src[0].src[0] if self.src[0].op is Ops.FUNCTION else self.src[0]
|
||||
assert in_tuple.op is Ops.TUPLE
|
||||
inner_shape = in_tuple.src[self.arg]._shape
|
||||
if inner_shape is None: return None
|
||||
# if through a FUNCTION, substitute internal PARAMs in the shape with corresponding args
|
||||
if self.src[0].op is Ops.FUNCTION:
|
||||
return tuple(graph_rewrite(s, _pm_resolve_params, self.src[0].src[1:], walk=True) if isinstance(s, UOp) else s for s in inner_shape)
|
||||
return inner_shape
|
||||
|
||||
case Ops.INDEX:
|
||||
shp:list[sint] = []
|
||||
for s in self.src[1:]: shp.extend(list(s.shape))
|
||||
@@ -381,9 +370,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
case Ops.GETADDR: return ()
|
||||
case Ops.RANGE | Ops.SPECIAL: return ()
|
||||
case Ops.BINARY: return (len(self.arg),)
|
||||
case Ops.BUFFER:
|
||||
if len(self.src): return self.src[0].as_shape
|
||||
return ()
|
||||
case Ops.BUFFER | Ops.PARAM | Ops.RETURNED:
|
||||
# these don't have a shape input, they have a size in the arg: int gives shape (size,), None gives ()
|
||||
if (img:=self.arg.image) is not None: return (img[0], img[1], 4)
|
||||
return () if self.arg.size is None else (self.arg.size,)
|
||||
case Ops.CUSTOM | Ops.CUSTOMI:
|
||||
if self.dtype is dtypes.void: return None
|
||||
input_shapes = [x._shape for x in self.src if x._shape is not None]
|
||||
@@ -394,10 +384,6 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# STAGE adds the existing shape to the front, opposite of INDEX
|
||||
return tuple([int(r.vmax+1) for r in self.src[1:]])+self.src[0].shape
|
||||
|
||||
# param has shape as the only arg
|
||||
case Ops.PARAM:
|
||||
return self.src[0].as_shape
|
||||
|
||||
# wmma output shape = accumulator shape (src[2])
|
||||
case Ops.WMMA:
|
||||
wmma_b = _broadcast_shape(self.src[0].shape[:-1], self.src[1].shape[:-1], self.src[2].shape[:-1])
|
||||
@@ -427,7 +413,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
match self.op:
|
||||
case Ops.RESHAPE:
|
||||
if not all(x >= 0 for x in self.marg): raise ValueError(f"shape can't contain negative numbers {self.marg}")
|
||||
if prod(ps) != prod(self.marg): raise ValueError(f"bad reshape: {ps} -> {self.marg}")
|
||||
# with symbolic views prod equality can be true at runtime but unprovable, only reject provably unequal products
|
||||
if resolve(prod(ps) != prod(self.marg), False): raise ValueError(f"bad reshape: {ps} -> {self.marg}")
|
||||
return self.marg
|
||||
case Ops.EXPAND:
|
||||
return tuple(self.marg) + ps
|
||||
@@ -554,12 +541,24 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
def sink(*srcs:UOp|None, **kwargs): # pylint: disable=no-self-argument
|
||||
return UOp(Ops.SINK, src=tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
def maketuple(*srcs:UOp): # pylint: disable=no-self-argument
|
||||
return UOp(Ops.TUPLE, src=srcs)
|
||||
def gettuple(self, idx:int) -> UOp:
|
||||
in_tuple = self.src[0] if self.op is Ops.FUNCTION else self
|
||||
assert in_tuple.op is Ops.TUPLE, f"gettuple requires FUNCTION or TUPLE source, got {self.op}"
|
||||
return UOp(Ops.GETTUPLE, src=(self,), arg=idx)
|
||||
@staticmethod
|
||||
def returned(placement:int, dtype:DType, shape:tuple[sint, ...]|sint|None=None, device=None, axis:int|None=None) -> UOp:
|
||||
"""create a RETURNED placeholder: like PARAM it has a flat storage size in the arg with a view on top, but it marks a
|
||||
buffer the enclosing call writes and returns. it's an input to the call and you AFTER on it like a normal buffer.
|
||||
the placement is its position among the call's srcs, which is its identity (identical placements merge)"""
|
||||
if isinstance(shape, (int, UOp)): shape = (shape,)
|
||||
if shape is not None and axis is not None and isinstance(device, tuple):
|
||||
# multi-device values have a per-shard sized storage wrapped in UNSHARD: the sharding lives in the graph, not the arg
|
||||
shape = tuple(s // len(device) if i == axis else s for i, s in enumerate(shape))
|
||||
if shape is None or len(shape) == 0: return UOp(Ops.RETURNED, arg=ParamArg(placement, dtype, None, device=device))
|
||||
ret = UOp(Ops.RETURNED, arg=ParamArg(placement, dtype, prod(to_max_shape(shape)), device=device))
|
||||
return ret.view_as(shape, axis)
|
||||
@property
|
||||
def num_returned(self) -> int: return sum(x.unsharded_base.op is Ops.RETURNED for x in self.src[1:])
|
||||
@property
|
||||
def returned_outputs(self) -> tuple[UOp, ...]:
|
||||
"""the outputs of a value-producing call: an AFTER on each RETURNED input, usable like a normal buffer"""
|
||||
return tuple(x.after(self) for x in self.src[1:] if x.unsharded_base.op is Ops.RETURNED)
|
||||
def group(*srcs:UOp|None, **kwargs): # pylint: disable=no-self-argument
|
||||
if len(srcs) == 1 and isinstance(srcs[0], UOp): return srcs[0]
|
||||
return UOp(Ops.GROUP, src=tuple([x for x in srcs if x is not None]), **kwargs)
|
||||
@@ -704,11 +703,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
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]
|
||||
return in_tuple.src[self.arg].axis if in_tuple.op is Ops.TUPLE else None
|
||||
if self.op is Ops.PARAM: return self.arg.axis
|
||||
if self.op is Ops.PARAM: return None
|
||||
# NOTE: they all have to share an axis, we always choose [-1]. src axes are right-aligned into the output shape
|
||||
if self.op in GroupOp.ALU.union({Ops.STACK}):
|
||||
return axes[-1] if (axes := dedup([x.axis+len(self.shape)-len(x.shape) for x in self.src if x.axis is not None])) else None
|
||||
@@ -821,8 +816,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
@staticmethod
|
||||
def new_buffer(device:str|tuple[str, ...], size:int, dtype:DType, num=None):
|
||||
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {dtype}")
|
||||
assert isinstance(size, int), f"new_buffer size must be a concrete int, got {size}"
|
||||
slot = next(UOp.unique_num) if num is None else num
|
||||
return UOp(Ops.BUFFER, src=(shape_to_shape_arg((size,)),), arg=ParamArg(slot, dtype, device=device))
|
||||
return UOp(Ops.BUFFER, arg=ParamArg(slot, dtype, size=size, device=device))
|
||||
@staticmethod
|
||||
def from_buffer(opaque:Buffer, device:str|tuple[str, ...]|None=None):
|
||||
if (uop:=UOp.new_buffer(device or opaque.device, opaque.size, opaque.dtype, num=-id(opaque))) not in buffers: buffers[uop] = opaque.ref(1)
|
||||
@@ -853,7 +849,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
return ret.after(ret.store(src.cast(ret.dtype)))
|
||||
@recursive_property
|
||||
def device(self) -> str|tuple[str, ...]|None:
|
||||
if self.op is Ops.PARAM: return self.arg.device
|
||||
if self.op in (Ops.PARAM, Ops.RETURNED): return self.arg.device
|
||||
if self.op is Ops.STAGE: return self.arg.device
|
||||
if self.op is Ops.AFTER: return self.src[0].device
|
||||
if self.op is Ops.MSELECT:
|
||||
@@ -873,7 +869,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
return self.device is None or self.dtype in dtypes.weaks
|
||||
@recursive_property
|
||||
def addrspace(self) -> AddrSpace|None:
|
||||
if self.op is Ops.PARAM: return self.arg.addrspace
|
||||
if self.op in (Ops.PARAM, Ops.RETURNED): return self.arg.addrspace
|
||||
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
|
||||
@@ -887,7 +883,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
return None
|
||||
@property
|
||||
def buf_uop(self) -> UOp:
|
||||
if self.op in {Ops.BUFFER, Ops.PARAM}: return self
|
||||
if self.op in {Ops.BUFFER, Ops.PARAM, Ops.RETURNED}: return self
|
||||
if self.op is Ops.MSELECT: return self.src[0].buf_uop.mselect(self.arg)
|
||||
if self.op is Ops.MSTACK: return UOp(Ops.MSTACK, src=tuple(x.buf_uop for x in self.src))
|
||||
if self.base.op is Ops.AFTER: return self.base.src[0].buf_uop.base
|
||||
@@ -978,7 +974,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# a Variable is a 0-d BUFFER in the ALU addrspace; binding it is storing a CONST into it
|
||||
# param=True creates the kernel-side form directly: an ALU PARAM (what the BUFFER becomes inside kernels)
|
||||
arg = ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU)
|
||||
return UOp(Ops.PARAM if param else Ops.BUFFER, src=(shape_to_shape_arg(()),), arg=arg)
|
||||
return UOp(Ops.PARAM if param else Ops.BUFFER, arg=arg)
|
||||
@property
|
||||
def is_variable(self) -> bool:
|
||||
# a Variable is a 0-d BUFFER in the ALU addrspace that carries a value range (it becomes a PARAM inside kernels)
|
||||
@@ -1148,11 +1144,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
dtype = strong_dtype(dtype) # storage is never weak: a placeholder commits the width of what's put in it
|
||||
if slot is None: slot = next(UOp.unique_num)
|
||||
if addrspace is AddrSpace.GLOBAL:
|
||||
ret = UOp(Ops.PARAM, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace, device=device,volatile=volatile))
|
||||
ret = UOp(Ops.PARAM, arg=ParamArg(slot, dtype, size=prod(shape), addrspace=addrspace, device=device, volatile=volatile))
|
||||
else:
|
||||
assert addrspace in (AddrSpace.LOCAL, AddrSpace.REG)
|
||||
assert device is None, "LOCAL and REG placeholders cannot have a device"
|
||||
ret = UOp(Ops.BUFFER, src=(shape_to_shape_arg((prod(shape),)),), arg=ParamArg(slot, dtype, addrspace=addrspace))
|
||||
ret = UOp(Ops.BUFFER, arg=ParamArg(slot, dtype, size=prod(shape), addrspace=addrspace))
|
||||
if tag is not None: ret = ret.rtag(tag)
|
||||
if len(shape) > 1: ret = ret.reshape(shape)
|
||||
return ret
|
||||
@@ -1167,25 +1163,39 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# TODO: this should replace placeholder
|
||||
@staticmethod
|
||||
def param(slot:int, dtype:DType, shape:tuple[sint, ...]|sint|None=None, device=None, vmin_vmax:tuple[PyConst, PyConst]|None=None,
|
||||
multiple_of:int|None=None, name=None, addrspace=AddrSpace.GLOBAL, axis:int|None=None, volatile:bool=False):
|
||||
multiple_of:int|None=None, name=None, addrspace=AddrSpace.GLOBAL, volatile:bool=False):
|
||||
"""create a PARAM: a single sint or 1-d shape gives a flat param of that size, a None shape gives a scalar param.
|
||||
the arg only stores the concrete max size (never symbolic): a multi-dim shape is a RESHAPE on top of the flat param,
|
||||
a symbolic shape is a max-size param shrunk to the real shape"""
|
||||
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create param for weak dtype {dtype}")
|
||||
if isinstance(shape, (int, UOp)): shape = (shape,)
|
||||
if shape is not None and axis is not None and isinstance(device, tuple):
|
||||
shape = tuple(s*len(device) if i == axis else s for i,s in enumerate(shape))
|
||||
src: tuple[UOp, ...] = (UOp(Ops.NOOP) if shape is None else shape_to_shape_arg(shape),)
|
||||
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, multiple_of, name, addrspace, axis, device, volatile))
|
||||
if shape is None or len(shape) == 0:
|
||||
return UOp(Ops.PARAM, arg=ParamArg(slot, dtype, None, vmin_vmax, multiple_of, name, addrspace, device, volatile))
|
||||
max_shape = to_max_shape(shape)
|
||||
ret = UOp(Ops.PARAM, arg=ParamArg(slot, dtype, prod(max_shape), vmin_vmax, multiple_of, name, addrspace, device, volatile))
|
||||
return ret.view_as(shape)
|
||||
def param_like(self, slot:int):
|
||||
# Variables become ALU params in the call body; the stored value (if bound) stays in the call args
|
||||
if self.is_bound_var or self.is_variable:
|
||||
b = self.src[0] if self.op is Ops.AFTER else self
|
||||
return UOp(Ops.PARAM, src=b.src, arg=replace(b.arg, slot=slot, name=f"p{slot}"))
|
||||
return UOp(Ops.PARAM, arg=replace(b.arg, slot=slot, name=f"p{slot}"))
|
||||
addrspace = self.addrspace if self.addrspace is not None else AddrSpace.GLOBAL
|
||||
return UOp.param(slot, self.dtype, self.shard_shape if self.axis is not None else self._shape, self.device, addrspace=addrspace, axis=self.axis)
|
||||
# multi-device values become a per-shard sized param wrapped in UNSHARD: the sharding lives in the graph, not the arg
|
||||
if self.axis is not None and isinstance(self.device, tuple):
|
||||
return UOp(Ops.PARAM, arg=ParamArg(slot, self.dtype, prod(to_max_shape(self.shard_shape)),
|
||||
addrspace=addrspace, device=self.device)).view_as(self.shard_shape, self.axis)
|
||||
return UOp.param(slot, self.dtype, self._shape, self.device, addrspace=addrspace)
|
||||
def view_as(self:UOp, shape:tuple[sint, ...], axis:int|None=None) -> UOp:
|
||||
"""view flat storage as the given (possibly symbolic) shape, optionally sharded on axis, the UNSHARD gives back the multiplied shape"""
|
||||
max_shape = to_max_shape(shape)
|
||||
ret = self.reshape(max_shape) if len(shape) > 1 else self
|
||||
if tuple(max_shape) != tuple(shape): ret = ret.shrink_to(shape)
|
||||
return ret if axis is None else ret.unshard(axis)
|
||||
|
||||
@staticmethod
|
||||
def custom_function(name:str, *src:UOp) -> UOp: return UOp(Ops.CUSTOM_FUNCTION, src=src, arg=name)
|
||||
|
||||
# opaque bodies stay as Ops.CALL; value-producing bodies become Ops.FUNCTION (wrapped in TUPLE)
|
||||
# opaque bodies are just CALLs; value-producing bodies become CALLs with RETURNED placeholders as extra inputs
|
||||
_OPAQUE_CALL_BODIES = {Ops.SINK, Ops.PROGRAM, Ops.LINEAR, Ops.COPY, Ops.CUSTOM_FUNCTION}
|
||||
def call(self, *srcs:UOp, ret_dtype:DType|None=None, grad_fxn:Callable|None=None,
|
||||
name:str|None=None, precompile:bool=False, precompile_backward:bool=False, aux:Any=None) -> UOp:
|
||||
@@ -1195,9 +1205,27 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
f"ranges {self.ranges} are leaking out of the call in {self.pyrender()}"
|
||||
if self.op in UOp._OPAQUE_CALL_BODIES:
|
||||
return UOp(Ops.CALL, src=(self,)+srcs, arg=CallInfo(grad_fxn, name, precompile, precompile_backward, aux))
|
||||
# value-producing bodies are always wrapped in TUPLE so FUNCTION dtype is always void
|
||||
body = self if self.op is Ops.TUPLE else UOp.maketuple(self)
|
||||
return UOp(Ops.FUNCTION, src=(body,)+srcs, arg=CallInfo(grad_fxn, name, precompile, precompile_backward, aux))
|
||||
# value-producing bodies delegate to call_outputs with a single output
|
||||
return UOp.call_outputs((self,), *srcs, grad_fxn=grad_fxn, name=name, precompile=precompile,
|
||||
precompile_backward=precompile_backward, aux=aux)
|
||||
|
||||
@staticmethod
|
||||
def call_outputs(values:tuple[UOp, ...], *srcs:UOp, grad_fxn:Callable|None=None,
|
||||
name:str|None=None, precompile:bool=False, precompile_backward:bool=False, aux:Any=None) -> UOp:
|
||||
"""call a body producing the given values: the body stores into output PARAMs, and the outputs are RETURNED
|
||||
placeholders that are inputs to the call (you AFTER on them like normal buffers). the RETURNEDs are bound to the
|
||||
output PARAMs positionally wherever the call is resolved, just like the args are bound to the input PARAMs"""
|
||||
# the device defaults to the first device in the values or args, like srcs-based device resolution
|
||||
default_dev = next((x.device for x in itertools.chain(values, srcs) if x.device is not None), None)
|
||||
# the RETURNED storage has the resolved shape: substitute internal PARAMs in the shapes with corresponding args
|
||||
def returned(o:UOp, i:int) -> UOp:
|
||||
return UOp.returned(len(srcs)+i, o.dtype, None if (shp:=o._shape) is None else
|
||||
tuple(graph_rewrite(s, _pm_resolve_params, srcs, walk=True) if isinstance(s, UOp) else s for s in shp),
|
||||
dev if (dev:=o.device) is not None else default_dev, o.axis if isinstance(o.device, tuple) else None)
|
||||
rets = tuple(returned(o, i) for i, o in enumerate(values))
|
||||
# the body only knows PARAMs: the output PARAMs get the slots right after the input PARAM slots
|
||||
body = UOp.sink(*[v.param_like(len(srcs)+i).store(v) for i, v in enumerate(values)])
|
||||
return UOp(Ops.CALL, src=(body,)+srcs+rets, arg=CallInfo(grad_fxn, name, precompile, precompile_backward, aux))
|
||||
def custom_kernel(*srcs:UOp, fxn:Callable, grad_fxn:Callable|None=None) -> list[UOp]:
|
||||
placeholders = [UOp.placeholder_like(s, slot=i) for i,s in enumerate(srcs)]
|
||||
kernel = fxn(*placeholders).call(*srcs, grad_fxn=grad_fxn)
|
||||
@@ -1669,7 +1697,8 @@ class RewriteContext:
|
||||
continue
|
||||
# no rewrite, process children then come back to rebuild
|
||||
stack.append((n, True))
|
||||
if not self.enter_calls and (n.op is Ops.FUNCTION or (n.op is Ops.CALL and n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
|
||||
# calls with RETURNED inputs are always inlined into the enclosing graph, their bodies are never rewritten separately
|
||||
if n.op is Ops.CALL and (n.num_returned or (not self.enter_calls and n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
|
||||
self.replace[n.src[0]] = n.src[0]
|
||||
for x in reversed(n.src):
|
||||
if x not in self.replace: stack.append((x, False))
|
||||
@@ -1707,11 +1736,10 @@ class RewriteContext:
|
||||
if n in waitlist: stack.extend(waitlist.pop(n))
|
||||
continue
|
||||
stack.append((n, 1, new_n))
|
||||
# NOTE: CALL/FUNCTION are handled as a special case.
|
||||
# The function that is called is not included in the graph_rewrite.
|
||||
# If you want to graph_rewrite a call, you can
|
||||
# A CALL of an address is not a body, its srcs are regular dataflow
|
||||
if not self.enter_calls and (new_n.op is Ops.FUNCTION or (new_n.op is Ops.CALL and new_n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
|
||||
# NOTE: CALLs are handled as a special case: the call body is not included in the graph_rewrite (a CALL of an
|
||||
# address is not a body, its srcs are regular dataflow). calls with RETURNED inputs are always inlined into the
|
||||
# enclosing graph, their bodies are never rewritten separately
|
||||
if new_n.op is Ops.CALL and (new_n.num_returned or (not self.enter_calls and new_n.src[0].op in UOp._OPAQUE_CALL_BODIES)):
|
||||
self.replace[new_n.src[0]] = new_n.src[0]
|
||||
for x in reversed(new_n.src):
|
||||
if x in on_stack: continue
|
||||
@@ -1765,6 +1793,12 @@ def to_max_shape(shape:tuple[sint, ...]) -> tuple[int, ...]: return tuple(int(x.
|
||||
|
||||
_substitute = PatternMatcher([(UPat(tuple(Ops), name="x"), lambda ctx,x: ctx.get(x,None))])
|
||||
_pm_resolve_params = PatternMatcher([(UPat(Ops.PARAM, name="p"), lambda ctx,p: ctx[p.arg.slot])])
|
||||
|
||||
def resolve_returned_after(r:UOp, t:UOp) -> UOp|None:
|
||||
"""AFTER on a RETURNED placeholder extracts the call output value: the value of the matching store in a SINK body"""
|
||||
if (rb:=r.unsharded_base).op is not Ops.RETURNED or t.op is not Ops.SINK: return None
|
||||
vals = [st.src[1] for st in t.src if st.op is Ops.STORE and st.src[0].unsharded_base is rb]
|
||||
return vals[0] if len(vals) == 1 else None
|
||||
remove_all_tags = PatternMatcher([(UPat(GroupOp.All, name="x"), lambda x: x.replace(tag=None) if x.tag is not None else None)])
|
||||
|
||||
def gate_kernel_sink(x:UOp) -> bool:
|
||||
|
||||
@@ -84,7 +84,7 @@ pm_pyrender_extra = PatternMatcher([
|
||||
(UPat(Ops.CONST, src=(), name="x"), lambda x: f"UOp.const({x.val})"),
|
||||
(UPat((Ops.CAST, Ops.BITCAST), name="x"), lambda ctx,x: f"{ctx[x.src[0]]}.{x.op.name.lower()}({x.dtype})" if x.dtype != x.src[0].dtype else None),
|
||||
(UPat(Ops.SPECIAL, src=(UPat(Ops.CONST),), name="x"), lambda x: f"UOp.special({x.src[0].val}, {repr(x.arg)})"),
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="x"), lambda x:
|
||||
(UPat(Ops.BUFFER, src=(), name="x"), lambda x:
|
||||
f"UOp.new_buffer({repr(x.arg.device)}, {x.max_numel()}, {x.dtype}, {x.arg.slot})"
|
||||
if isinstance(x.arg, ParamArg) and x.addrspace is AddrSpace.GLOBAL else None),
|
||||
(UPat(Ops.COPY, src=(UPat(name="x"),), name="copy"), lambda ctx,x,copy: f"{ctx[x]}.copy_to_device({repr(copy.arg)})"),
|
||||
@@ -144,7 +144,7 @@ def pyrender(ast:UOp) -> str:
|
||||
cmap = consumer_map_from_toposort(lst)
|
||||
not_rendered = {Ops.CONST}
|
||||
always_rendered = {Ops.PARAM, Ops.LOAD, Ops.SPECIAL, Ops.RANGE, Ops.CONTIGUOUS, Ops.STACK,
|
||||
Ops.BUFFER, Ops.COPY, Ops.CALL, Ops.FUNCTION, Ops.WHERE, Ops.END}
|
||||
Ops.BUFFER, Ops.COPY, Ops.CALL, Ops.WHERE, Ops.END}
|
||||
|
||||
to_render: set[UOp] = {ast}
|
||||
for u in lst:
|
||||
@@ -152,7 +152,7 @@ def pyrender(ast:UOp) -> str:
|
||||
for s in u.src: to_render.add(s)
|
||||
if u.op is Ops.STORE: to_render.add(u.src[1])
|
||||
if u.op is Ops.REDUCE: to_render.add(u.src[0])
|
||||
if u.op is Ops.FUNCTION or (u.op is Ops.CALL and u.src[0].dtype is dtypes.void): raise NotImplementedError("call can't be pyrendered")
|
||||
if u.op is Ops.CALL and u.src[0].dtype is dtypes.void: raise NotImplementedError("call can't be pyrendered")
|
||||
if u.op in not_rendered: continue
|
||||
# checking the consumers is not enough, you have to make sure it's not used twice by the one consumer
|
||||
if len(cmap[u]) == 1 and len([x for x in list(cmap[u].keys())[0].src if x is u]) == 1 and u.op not in always_rendered: continue
|
||||
|
||||
+17
-21
@@ -87,17 +87,16 @@ spec_shared = PatternMatcher([
|
||||
# a loop-ended END requires a trailing bool condition for the backedge (loop again while true)
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.RANGE, dtypes.void), UPat(dtype=dtypes.bool))), lambda: True),
|
||||
|
||||
# PARAM
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="x"), lambda x:
|
||||
isinstance(x.arg, ParamArg) and x.addrspace in (AddrSpace.REG, AddrSpace.LOCAL)),
|
||||
# PARAM/BUFFER have a size in the arg, no shape input
|
||||
(UPat(Ops.PARAM, src=(), name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
(UPat(Ops.BUFFER, src=(), name="x"), lambda x: 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),
|
||||
|
||||
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, or another AFTER
|
||||
# AFTER on Movement Op, PARAM, BUFFER, CONTIGUOUS, RETURNED, or another AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.BUFFER, Ops.CONTIGUOUS, Ops.INDEX,
|
||||
Ops.AFTER, Ops.UNSHARD, Ops.BITCAST, Ops.INS})),),
|
||||
Ops.AFTER, Ops.UNSHARD, Ops.BITCAST, Ops.INS, Ops.RETURNED})),),
|
||||
allow_any_len=True, name="x"), lambda x: matches_dtype(x.src[0], x.dtype)),
|
||||
|
||||
# CUSTOM (inline and non inline): the arg is the source string and the dtype it produces, void for a bare statement
|
||||
@@ -133,20 +132,18 @@ spec_shared = PatternMatcher([
|
||||
|
||||
def is_device(d): return isinstance(d, str) or (isinstance(d, tuple) and all(isinstance(s, str) for s in d))
|
||||
|
||||
def valid_gettuple(g:UOp, t:UOp): return isinstance(g.arg, int) and 0 <= g.arg < len(t.src) and matches_dtype(t.src[g.arg], g.dtype)
|
||||
|
||||
# these ops can exist in tensor but not programs. example: movement
|
||||
spec_tensor = PatternMatcher([
|
||||
(UPat((Ops.SIN, Ops.LOG2, Ops.EXP2, Ops.SQRT, Ops.RECIPROCAL), src=(UPat(),), name="u"),
|
||||
lambda u: dtypes.is_float(u.dtype) or u.src[0].base.is_invalid),
|
||||
|
||||
# BUFFER
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="buf"), lambda buf:
|
||||
(isinstance(buf.dtype, DType) and matches_dtype(buf.src[0], dtypes.weakint) and is_device(buf.arg.device))
|
||||
(UPat(Ops.BUFFER, src=(), name="buf"), lambda buf:
|
||||
(isinstance(buf.dtype, DType) and isinstance(buf.arg.size, int) and is_device(buf.arg.device))
|
||||
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
|
||||
|
||||
# a Variable is a 0-d ALU BUFFER with a value range and no device
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="buf"), lambda buf: buf.arg.device is None if buf.is_variable else None),
|
||||
(UPat(Ops.BUFFER, src=(), name="buf"), lambda buf: buf.arg.device is None if buf.is_variable else None),
|
||||
|
||||
# custom function
|
||||
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
|
||||
@@ -154,11 +151,8 @@ spec_tensor = PatternMatcher([
|
||||
# CALL
|
||||
(UPat(Ops.CALL, dtypes.void, src=(UPat((Ops.SINK, Ops.LINEAR, Ops.PROGRAM, Ops.COPY, Ops.CUSTOM_FUNCTION)),), allow_any_len=True), lambda: True),
|
||||
|
||||
# FUNCTION + TUPLE must have void dtype, GETTUPLE can only appear on FUNCTION or TUPLE
|
||||
(UPat(Ops.FUNCTION, dtypes.void, src=(UPat(Ops.TUPLE),), allow_any_len=True), lambda: True),
|
||||
(UPat(Ops.TUPLE, dtypes.void), lambda: True),
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.FUNCTION, src=(UPat(Ops.TUPLE, name="t"),), allow_any_len=True),), name="g"), valid_gettuple),
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), valid_gettuple),
|
||||
# RETURNED is a placeholder for a buffer a call writes and returns: it has a size in the arg, no shape input
|
||||
(UPat(Ops.RETURNED, src=(), name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
|
||||
# SPECIAL is index before index lowering. custom_kernel currently has this
|
||||
(UPat(Ops.SPECIAL, src=(UPat.var("x", dtypes.weakint),), name="s"), lambda s,x: matches_dtype(x, s.dtype) and isinstance(s.arg, str)),
|
||||
@@ -229,7 +223,8 @@ spec_program = PatternMatcher([
|
||||
])+spec_shared
|
||||
|
||||
spec_hcq = PatternMatcher([
|
||||
(UPat(Ops.GETADDR, dtypes.uint64, src=(UPat((Ops.BUFFER, Ops.PARAM)).or_after(),), name="x"), lambda x: is_device(x.arg)),
|
||||
(UPat(Ops.GETADDR, dtypes.uint64, src=(UPat((Ops.BUFFER, Ops.PARAM, Ops.SHRINK, Ops.BITCAST, Ops.MSTACK)).or_after(),), name="x"),
|
||||
lambda x: is_device(x.arg)),
|
||||
(UPat(Ops.PROGRAM, dtypes.void, src=(UPat((Ops.BUFFER, Ops.PARAM)).or_after(),)), lambda: True),
|
||||
])+spec_shared
|
||||
|
||||
@@ -260,11 +255,10 @@ spec_kernel_graph = PatternMatcher([
|
||||
(UPat(Ops.STACK, name="s"), lambda s: all(x.op in (Ops.CONST, Ops.PARAM) or x.is_variable or x.is_bound_var for x in s.src) or None),
|
||||
# linear for more kernels (TODO: we should enter non sink calls)
|
||||
#(UPat(Ops.LINEAR), lambda: True),
|
||||
# param is outside buffer, buffer is local buffer
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
# param is outside buffer, buffer is local buffer. params have a size in the arg, no shape input
|
||||
(UPat(Ops.PARAM, src=(), name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
(UPat(Ops.BUFFER, name="x"), lambda x: isinstance(x.arg, ParamArg) and x.addrspace in (AddrSpace.GLOBAL, AddrSpace.ALU)),
|
||||
# RESHAPE/BITCAST are NOOPs in the kernel graph (do we need them?)
|
||||
(UPat((Ops.RESHAPE, Ops.BITCAST)), lambda: True),
|
||||
(UPat(Ops.BITCAST), lambda: True),
|
||||
# mstack/mselect
|
||||
(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)),
|
||||
(UPat(Ops.MSELECT, name="x"), lambda x: isinstance(x.src[0].device, tuple) and x.arg < len(x.src[0].device)),
|
||||
@@ -273,6 +267,8 @@ spec_kernel_graph = PatternMatcher([
|
||||
# after on PARAM or AFTER
|
||||
(UPat(Ops.AFTER, src=(UPat(GroupOp.Movement.union({Ops.PARAM, Ops.AFTER, Ops.BUFFER, Ops.MSTACK, Ops.MSELECT, Ops.BITCAST, Ops.RESHAPE})),),
|
||||
allow_any_len=True, name="x"), lambda x: matches_dtype(x.src[0], x.dtype)),
|
||||
# movement ops (views of the flat PARAM storage) are valid in value positions too
|
||||
(UPat(GroupOp.Movement), lambda: True),
|
||||
])
|
||||
|
||||
# **** pyrender (move this) ****
|
||||
|
||||
@@ -309,7 +309,7 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
|
||||
((UPat.var("x", dtypes.weakint) + UPat.cvar("c")).cast(dtypes.sints, name="cast"), lambda x,c,cast:x.cast(cast.dtype)+cast.const_like(c.val)),
|
||||
# only RANGE/IF/STORE/KERNEL have side effects
|
||||
(UPat(Ops.AFTER, name="x"), lambda x: x.replace(src=(x.src[0],)+
|
||||
tuple(dedup(flatten([(y,) if y.op in {Ops.RANGE, Ops.STORE, Ops.CALL, Ops.FUNCTION, Ops.BARRIER, Ops.END, Ops.LINEAR, Ops.STAGE}
|
||||
tuple(dedup(flatten([(y,) if y.op in {Ops.RANGE, Ops.STORE, Ops.CALL, Ops.BARRIER, Ops.END, Ops.LINEAR, Ops.STAGE}
|
||||
else y.src for y in x.src[1:]]))))),
|
||||
# after/end with 1 src is just src[0]
|
||||
(UPat((Ops.AFTER, Ops.END), src=(UPat.var("s"),)), lambda s: s),
|
||||
|
||||
@@ -51,7 +51,7 @@ uops_colors = {Ops.LOAD: "#ffc0c0", Ops.STORE: "#87CEEB", Ops.CONST: "#e0e0e0",
|
||||
Ops.WMMA: "#efefc0", Ops.UNSHARD: "#f6ccff", Ops.INS: "#eec4ff",
|
||||
**{x:"#D8F9E4" for x in GroupOp.Movement}, **{x:"#ffffc0" for x in GroupOp.ALU}, Ops.THREEFRY:"#ffff80",
|
||||
Ops.BUFFER: "#B0BDFF", Ops.GETADDR: "#9DB1F0", Ops.COPY: "#a040a0", Ops.CUSTOM_FUNCTION: "#bf71b6",
|
||||
Ops.CALL: "#00B7C8", Ops.FUNCTION: "#C07788", Ops.PARAM: "#14686F", Ops.SOURCE: "#c0c0c0", Ops.BINARY: "#404040",
|
||||
Ops.CALL: "#00B7C8", Ops.PARAM: "#14686F", Ops.RETURNED: "#C07788", Ops.SOURCE: "#c0c0c0", Ops.BINARY: "#404040",
|
||||
Ops.LINEAR: "#7DF4FF",
|
||||
Ops.ALLREDUCE: "#ff40a0", Ops.MSELECT: "#d040a0", Ops.MSTACK: "#d040a0", Ops.CONTIGUOUS: "#FFC14D",
|
||||
Ops.STAGE: "#AC640D", Ops.REWRITE_ERROR: "#1a1b26", Ops.AFTER: "#8A7866", Ops.END: "#524C46"}
|
||||
@@ -145,7 +145,7 @@ def uop_to_json(data:VizData, x:UOp) -> dict[int, dict]:
|
||||
label += f"\n({multirange_str(rngs, color=True)})"
|
||||
if u._shape is not None:
|
||||
label += f"\n{shape_to_str(u.shape)}"
|
||||
if u.op in {Ops.CALL, Ops.FUNCTION}:
|
||||
if u.op is Ops.CALL and u.src[0].dtype is dtypes.void:
|
||||
label += f"\n{u.src[0].key.hex()[:8]}\n{u.src[0].op}"
|
||||
if u.op in {Ops.INDEX, Ops.STAGE}:
|
||||
label += f"\n{u.render()}" if sum(len(s.toposort()) for s in u.src[1:]) < 30 else "\nINDEX TOO LARGE"
|
||||
@@ -156,10 +156,10 @@ def uop_to_json(data:VizData, x:UOp) -> dict[int, dict]:
|
||||
label += "\n"+' '.join([f"{range_str(s, color=True)}({s.vmax+1})" for s in trngs])
|
||||
except Exception:
|
||||
label += "\n<ISSUE GETTING LABEL>"
|
||||
ref = data.ref_map.get(canonicalize_ast(u.src[0])) if u.op in {Ops.CALL, Ops.FUNCTION} else None
|
||||
ref = data.ref_map.get(canonicalize_ast(u.src[0])) if u.op is Ops.CALL and u.src[0].dtype is dtypes.void else None
|
||||
if ref is not None: label += f"\ncodegen@{fmt_colored(data.ctxs[ref]['name'])}"
|
||||
# NOTE: kernel already has metadata in arg
|
||||
if TRACEMETA >= 2 and u.metadata is not None and u.op not in {Ops.CALL, Ops.FUNCTION}: label += "\n"+str(u.metadata)
|
||||
if TRACEMETA >= 2 and u.metadata is not None and u.op is not Ops.CALL: label += "\n"+str(u.metadata)
|
||||
# limit SOURCE labels line count
|
||||
if u.op is Ops.SOURCE and len(lines:=label.split("\n")) > 40:
|
||||
label = "\n".join(lines[:30]) + "\n..."
|
||||
|
||||
Reference in New Issue
Block a user