mirror of
https://github.com/tinygrad/tinygrad.git
synced 2026-08-16 10:18:27 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac20b5e984 | ||
|
|
f64f96ec59 | ||
|
|
7b05caf5c5 | ||
|
|
34bcc5ad63 | ||
|
|
40f0d4af14 | ||
|
|
2864036e8e | ||
|
|
f3a5337825 | ||
|
|
95f5c85bf3 | ||
|
|
2a81616492 | ||
|
|
13ca9bd8a6 | ||
|
|
636a43722d | ||
|
|
980748ccfc | ||
|
|
f7ce7f330d | ||
|
|
4b8db13e01 | ||
|
|
b1cbd1a43f | ||
|
|
dbb0f6067e | ||
|
|
8481eba866 | ||
|
|
2b96d64496 | ||
|
|
ef77963cfd | ||
|
|
abba2aebda | ||
|
|
1cf8f2f68c | ||
|
|
ac3f56a1a2 | ||
|
|
89117d8b9e | ||
|
|
9970a0aad0 | ||
|
|
0146a30125 | ||
|
|
b53cd35cff | ||
|
|
82debb4557 | ||
|
|
ee290b3e39 | ||
|
|
232529ce88 | ||
|
|
24d8681be7 | ||
|
|
47629f4bcf | ||
|
|
f315df29a0 |
@@ -14,12 +14,15 @@ jobs:
|
||||
outputs:
|
||||
branchstat: ${{ steps.brstat.outputs.stat}}
|
||||
steps:
|
||||
- name: Check code from PR branch
|
||||
- name: Check code from PR branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
# PR code is only inspected with git rev-list, never executed
|
||||
allow-unsafe-pr-checkout: true
|
||||
persist-credentials: false
|
||||
- name: Check whether branch is up-to-date
|
||||
id: brstat
|
||||
run: |
|
||||
@@ -51,6 +54,9 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: pr
|
||||
# PR code is only line-counted by master's sz.py, never executed
|
||||
allow-unsafe-pr-checkout: true
|
||||
persist-credentials: false
|
||||
# the base default to tinygrad master and cannot be other fork branch for security purpose
|
||||
- name: Checkout code from tinygrad master
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -1462,6 +1462,8 @@ def train_llama3():
|
||||
|
||||
@TinyJit
|
||||
def minibatch(tokens:Tensor):
|
||||
for nxt in fp8_next_amax: nxt.assign(0)
|
||||
for nxt in fp8_next_grad_amax: nxt.assign(0)
|
||||
if is_dp: tokens = tokens.to(None).shard(device, 0)
|
||||
if is_mp: tokens = tokens.shard(device)
|
||||
if not is_sharding: tokens = tokens.to(None)
|
||||
|
||||
@@ -37,8 +37,8 @@ def quantize_fp8(x:Tensor, amax_state:Tensor|None=None):
|
||||
return x_clamped.cast(FP8_DTYPE), scale.float().reciprocal(), new_amax
|
||||
|
||||
def matmul(x:Tensor, w:Tensor, fp8:bool=True, amax_x:Tensor|None=None, w_inv_scale:Tensor|None=None,
|
||||
x_fp8:Tensor|None=None, x_new_amax:Tensor|None=None,
|
||||
grad_amax_state:Tensor|None=None, next_grad_amax_state:Tensor|None=None, x_prequant_mx:tuple|None=None) -> tuple[Tensor,...]:
|
||||
x_fp8:Tensor|None=None, grad_amax_state:Tensor|None=None, next_grad_amax_state:Tensor|None=None, x_prequant_mx:tuple|None=None,
|
||||
next_amax_x:Tensor|None=None) -> tuple[Tensor,...]:
|
||||
if not fp8:
|
||||
if ASM_GEMM:
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
@@ -56,13 +56,14 @@ def matmul(x:Tensor, w:Tensor, fp8:bool=True, amax_x:Tensor|None=None, w_inv_sca
|
||||
else:
|
||||
x_phys = (x_q.cast(dtypes.bfloat16) * _mx_block_scale(x_e8)).reshape(*l_shape, x_q.shape[-1])
|
||||
out = x_phys @ (w.cast(dtypes.bfloat16) * _mx_block_scale(w_inv_scale)).T
|
||||
return out, (amax_x.detach() if amax_x is not None else None), x_q
|
||||
return out, x_q
|
||||
if x_fp8 is None:
|
||||
if FUSED_INPUT_QUANTIZE and amax_x is not None:
|
||||
if FUSED_INPUT_QUANTIZE:
|
||||
from extra.llama_kernels.quantize_fp8_delayed import quantize_fp8_delayed
|
||||
x_fp8, _, x_new_amax, _ = quantize_fp8_delayed(x, amax_x, FP8_DTYPE)
|
||||
x_fp8, _ = quantize_fp8_delayed(x, amax_x, next_amax_x, FP8_DTYPE)
|
||||
else:
|
||||
x_fp8, _, x_new_amax = quantize_fp8(x, amax_state=amax_x)
|
||||
x_fp8, _, new_amax_x = quantize_fp8(x, amax_state=amax_x)
|
||||
next_amax_x.assign(new_amax_x)
|
||||
if ASM_GEMM:
|
||||
from extra.gemm.cdna_asm_gemm import can_use_asm_gemm, asm_gemm
|
||||
if can_use_asm_gemm(x_fp8, w.T):
|
||||
@@ -73,51 +74,51 @@ def matmul(x:Tensor, w:Tensor, fp8:bool=True, amax_x:Tensor|None=None, w_inv_sca
|
||||
else:
|
||||
out = asm_gemm(x_fp8, w.T, x_scale=amax_x, w_scale=w_inv_scale, grad_amax_state=grad_amax_state,
|
||||
next_grad_amax_state=next_grad_amax_state)
|
||||
return out, x_new_amax, x_fp8
|
||||
return (x_fp8.dot(w.T, dtype=dtypes.float) * ((amax_x.float() + 1e-8) / FP8_MAX) * w_inv_scale).cast(dtypes.bfloat16), x_new_amax, x_fp8
|
||||
return out, x_fp8
|
||||
return (x_fp8.dot(w.T, dtype=dtypes.float) * ((amax_x.float() + 1e-8) / FP8_MAX) * w_inv_scale).cast(dtypes.bfloat16), x_fp8
|
||||
|
||||
def norm_quantize_matmul(x:Tensor, norm:Tensor, w:Tensor, w_inv_scale:Tensor, eps:float, amax_x:Tensor,
|
||||
grad_amax_state:Tensor, next_grad_amax_state:Tensor):
|
||||
next_amax_x:Tensor, grad_amax_state:Tensor, next_grad_amax_state:Tensor):
|
||||
if FUSED_ADD_NORM_MUL_QUANTIZE:
|
||||
from extra.llama_kernels.fused_rmsnorm_mul_quantize_fp8 import fused_rmsnorm_mul_quantize_fp8
|
||||
x_fp8, new_amax, x_normed, rrms = fused_rmsnorm_mul_quantize_fp8(x, norm, amax_x, eps, FP8_DTYPE)
|
||||
out, *ret = matmul(None, w, w_inv_scale=w_inv_scale, x_fp8=x_fp8, amax_x=amax_x, x_new_amax=new_amax,
|
||||
x_fp8, x_normed, rrms = fused_rmsnorm_mul_quantize_fp8(x, norm, amax_x, eps, FP8_DTYPE, next_amax_x)
|
||||
out, *ret = matmul(None, w, w_inv_scale=w_inv_scale, x_fp8=x_fp8, amax_x=amax_x,
|
||||
grad_amax_state=grad_amax_state, next_grad_amax_state=next_grad_amax_state)
|
||||
return out, x_normed, rrms, ret
|
||||
x_normed, rrms = rmsnorm(x, eps)
|
||||
out, *ret = matmul(x_normed * norm, w, amax_x=amax_x, w_inv_scale=w_inv_scale, grad_amax_state=grad_amax_state,
|
||||
next_grad_amax_state=next_grad_amax_state)
|
||||
next_grad_amax_state=next_grad_amax_state, next_amax_x=next_amax_x)
|
||||
return out, x_normed, rrms, ret
|
||||
|
||||
def add_norm_quantize_matmul(x:Tensor, residual:Tensor, norm:Tensor, w:Tensor, w_inv_scale:Tensor, eps:float, amax_x:Tensor,
|
||||
grad_amax_state:Tensor|None=None, next_grad_amax_state:Tensor|None=None):
|
||||
next_amax_x:Tensor, grad_amax_state:Tensor|None=None, next_grad_amax_state:Tensor|None=None):
|
||||
if FUSED_ADD_NORM_MUL_QUANTIZE:
|
||||
from extra.llama_kernels.fused_rmsnorm_mul_quantize_fp8 import fused_add_rmsnorm_mul_quantize_fp8
|
||||
x_fp8, new_amax, h, x_normed, rrms = fused_add_rmsnorm_mul_quantize_fp8(x, residual, norm, amax_x, eps, FP8_DTYPE)
|
||||
out, *ret = matmul(None, w, w_inv_scale=w_inv_scale, x_fp8=x_fp8, amax_x=amax_x, x_new_amax=new_amax,
|
||||
x_fp8, h, x_normed, rrms = fused_add_rmsnorm_mul_quantize_fp8(x, residual, norm, amax_x, eps, FP8_DTYPE, next_amax_x)
|
||||
out, *ret = matmul(None, w, w_inv_scale=w_inv_scale, x_fp8=x_fp8, amax_x=amax_x,
|
||||
grad_amax_state=grad_amax_state, next_grad_amax_state=next_grad_amax_state)
|
||||
return out, h, x_normed, rrms, ret
|
||||
h = x + residual
|
||||
x_normed, rrms = rmsnorm(h, eps)
|
||||
out, *ret = matmul(x_normed * norm, w, amax_x=amax_x, w_inv_scale=w_inv_scale, grad_amax_state=grad_amax_state,
|
||||
next_grad_amax_state=next_grad_amax_state)
|
||||
next_grad_amax_state=next_grad_amax_state, next_amax_x=next_amax_x)
|
||||
return out, h, x_normed, rrms, ret
|
||||
|
||||
def silu_w13_quantize_matmul(x_w13:Tensor, w2:Tensor, s_2:Tensor,
|
||||
amax_x2:Tensor,
|
||||
amax_x2:Tensor, next_amax_x2:Tensor,
|
||||
grad_amax_xw13:Tensor, next_grad_amax_xw13:Tensor,
|
||||
grad_amax_xout:Tensor, next_grad_amax_xout:Tensor):
|
||||
if FUSED_SILU_W13:
|
||||
from extra.llama_kernels.cast_amax import fused_quantize_fp8_w13
|
||||
x2_fp8, new_amax_x2 = fused_quantize_fp8_w13(x_w13, amax_x2, FP8_DTYPE, grad_amax_state=grad_amax_xw13,
|
||||
next_grad_amax_state=next_grad_amax_xw13)
|
||||
out, *ret = matmul(None, w2, w_inv_scale=s_2, x_fp8=x2_fp8, amax_x=amax_x2, x_new_amax=new_amax_x2,
|
||||
x2_fp8 = fused_quantize_fp8_w13(x_w13, amax_x2, FP8_DTYPE, grad_amax_state=grad_amax_xw13,
|
||||
next_grad_amax_state=next_grad_amax_xw13, amax_out=next_amax_x2)
|
||||
out, *ret = matmul(None, w2, w_inv_scale=s_2, x_fp8=x2_fp8, amax_x=amax_x2,
|
||||
grad_amax_state=grad_amax_xout, next_grad_amax_state=next_grad_amax_xout)
|
||||
return out, ret
|
||||
hidden = x_w13.shape[-1] // 2
|
||||
x_w1, x_w3 = x_w13[..., :hidden], x_w13[..., hidden:]
|
||||
out, *ret = matmul(x_w1.silu() * x_w3, w2, amax_x=amax_x2, w_inv_scale=s_2, grad_amax_state=grad_amax_xout,
|
||||
next_grad_amax_state=next_grad_amax_xout)
|
||||
next_grad_amax_state=next_grad_amax_xout, next_amax_x=next_amax_x2)
|
||||
return out, ret
|
||||
|
||||
class FlatTransformer:
|
||||
@@ -186,14 +187,14 @@ class FlatTransformer:
|
||||
|
||||
def attention(self, x:Tensor, freqs_cis:Tensor, *, attention_norm:Tensor, wqkv:Tensor, wo:Tensor,
|
||||
amax_xqkv:Tensor, amax_xo:Tensor, s_qkv:Tensor, s_o:Tensor,
|
||||
next_amax_xqkv:Tensor, next_amax_xo:Tensor,
|
||||
grad_amax_xqkv:Tensor, grad_amax_xo:Tensor, next_grad_amax_xqkv:Tensor, next_grad_amax_xo:Tensor):
|
||||
bsz, seqlen, _ = x.shape
|
||||
amaxs, saves = [], []
|
||||
saves = []
|
||||
|
||||
xqkv, x_normed, rrms, (new_amax, *s) = norm_quantize_matmul(x, attention_norm, wqkv, s_qkv, self.norm_eps,
|
||||
xqkv, x_normed, rrms, s = norm_quantize_matmul(x, attention_norm, wqkv, s_qkv, self.norm_eps,
|
||||
amax_x=amax_xqkv, grad_amax_state=grad_amax_xqkv,
|
||||
next_grad_amax_state=next_grad_amax_xqkv)
|
||||
amaxs.append(new_amax)
|
||||
next_grad_amax_state=next_grad_amax_xqkv, next_amax_x=next_amax_xqkv)
|
||||
saves.extend([x_normed, rrms, *s, xqkv])
|
||||
if getenv("HK_FLASH_ATTENTION"):
|
||||
from extra.thunder.amd.fa import flash_attention, fused_qkv_rope
|
||||
@@ -211,64 +212,62 @@ class FlatTransformer:
|
||||
attn = xq.scaled_dot_product_attention(xk, xv, is_causal=True, enable_gqa=True).transpose(1, 2)
|
||||
attn = attn.reshape(bsz, seqlen, -1)
|
||||
|
||||
out, new_amax, *s = matmul(attn, wo, amax_x=amax_xo, w_inv_scale=s_o, grad_amax_state=grad_amax_xo,
|
||||
next_grad_amax_state=next_grad_amax_xo)
|
||||
amaxs.append(new_amax)
|
||||
out, *s = matmul(attn, wo, amax_x=amax_xo, w_inv_scale=s_o, grad_amax_state=grad_amax_xo,
|
||||
next_grad_amax_state=next_grad_amax_xo, next_amax_x=next_amax_xo)
|
||||
saves.extend([*s, out])
|
||||
return out, amaxs, saves
|
||||
return out, saves
|
||||
|
||||
def feed_forward(self, x:Tensor, residual:Tensor, **kwargs):
|
||||
amaxs, saves = [], []
|
||||
saves = []
|
||||
|
||||
if SPLIT_W13:
|
||||
h = x + residual
|
||||
x_normed, rrms = rmsnorm(h, self.norm_eps)
|
||||
saves.extend([x_normed, rrms])
|
||||
inp = x_normed * kwargs["ffn_norm"]
|
||||
x_w1, new_amax, *s = matmul(inp, kwargs["w1"], amax_x=kwargs["amax_x1"], w_inv_scale=kwargs["s_1"],
|
||||
grad_amax_state=kwargs["grad_amax_xw1"], next_grad_amax_state=kwargs["next_grad_amax_xw1"])
|
||||
amaxs.append(new_amax)
|
||||
x_w1, *s = matmul(inp, kwargs["w1"], amax_x=kwargs["amax_x1"], w_inv_scale=kwargs["s_1"],
|
||||
grad_amax_state=kwargs["grad_amax_xw1"], next_grad_amax_state=kwargs["next_grad_amax_xw1"],
|
||||
next_amax_x=kwargs["next_amax_x1"])
|
||||
saves.extend([*s, x_w1])
|
||||
x_w3, new_amax, *s = matmul(inp, kwargs["w3"], amax_x=kwargs["amax_x3"], w_inv_scale=kwargs["s_3"],
|
||||
grad_amax_state=kwargs["grad_amax_xw3"], next_grad_amax_state=kwargs["next_grad_amax_xw3"])
|
||||
amaxs.append(new_amax)
|
||||
x_w3, *s = matmul(inp, kwargs["w3"], amax_x=kwargs["amax_x3"], w_inv_scale=kwargs["s_3"],
|
||||
grad_amax_state=kwargs["grad_amax_xw3"], next_grad_amax_state=kwargs["next_grad_amax_xw3"],
|
||||
next_amax_x=kwargs["next_amax_x3"])
|
||||
saves.extend([*s, x_w3])
|
||||
if FUSED_SILU_W13 and MXFP8:
|
||||
from extra.llama_kernels.fused_silu_mul_quantize_mxfp8 import fused_silu_mul_quantize_mxfp8
|
||||
aq, ae8, asi = fused_silu_mul_quantize_mxfp8(x_w1.reshape(-1, x_w1.shape[-1]), x_w3.reshape(-1, x_w3.shape[-1]))
|
||||
out, new_amax, *s = matmul(None, kwargs["w2"], x_prequant_mx=(aq, ae8, asi), amax_x=kwargs["amax_x2"],
|
||||
w_inv_scale=kwargs["s_2"], grad_amax_state=kwargs["grad_amax_xout"],
|
||||
next_grad_amax_state=kwargs["next_grad_amax_xout"])
|
||||
out, *s = matmul(None, kwargs["w2"], x_prequant_mx=(aq, ae8, asi), amax_x=kwargs["amax_x2"],
|
||||
w_inv_scale=kwargs["s_2"], grad_amax_state=kwargs["grad_amax_xout"],
|
||||
next_grad_amax_state=kwargs["next_grad_amax_xout"], next_amax_x=kwargs["next_amax_x2"])
|
||||
out = out.reshape(*x_w1.shape[:-1], kwargs["w2"].shape[0])
|
||||
else:
|
||||
out, new_amax, *s = matmul(x_w1.silu() * x_w3, kwargs["w2"], amax_x=kwargs["amax_x2"], w_inv_scale=kwargs["s_2"],
|
||||
grad_amax_state=kwargs["grad_amax_xout"], next_grad_amax_state=kwargs["next_grad_amax_xout"])
|
||||
amaxs.append(new_amax)
|
||||
out, *s = matmul(x_w1.silu() * x_w3, kwargs["w2"], amax_x=kwargs["amax_x2"], w_inv_scale=kwargs["s_2"],
|
||||
grad_amax_state=kwargs["grad_amax_xout"], next_grad_amax_state=kwargs["next_grad_amax_xout"],
|
||||
next_amax_x=kwargs["next_amax_x2"])
|
||||
saves.extend([*s, out])
|
||||
else:
|
||||
x_w13, h, x_normed, rrms, (new_amax, *s) = add_norm_quantize_matmul(x, residual, kwargs["ffn_norm"], kwargs["w13"], kwargs["s_13"],
|
||||
x_w13, h, x_normed, rrms, s = add_norm_quantize_matmul(x, residual, kwargs["ffn_norm"], kwargs["w13"], kwargs["s_13"],
|
||||
self.norm_eps, amax_x=kwargs["amax_x13"],
|
||||
next_amax_x=kwargs["next_amax_x13"],
|
||||
grad_amax_state=kwargs["grad_amax_xw13"],
|
||||
next_grad_amax_state=kwargs["next_grad_amax_xw13"])
|
||||
amaxs.append(new_amax)
|
||||
saves.extend([x_normed, rrms, *s, x_w13])
|
||||
out, (new_amax, *s) = silu_w13_quantize_matmul(x_w13, kwargs["w2"], kwargs["s_2"], amax_x2=kwargs["amax_x2"],
|
||||
out, s = silu_w13_quantize_matmul(x_w13, kwargs["w2"], kwargs["s_2"], amax_x2=kwargs["amax_x2"],
|
||||
next_amax_x2=kwargs["next_amax_x2"],
|
||||
grad_amax_xw13=kwargs["grad_amax_xw13"],
|
||||
next_grad_amax_xw13=kwargs["next_grad_amax_xw13"],
|
||||
grad_amax_xout=kwargs["grad_amax_xout"],
|
||||
next_grad_amax_xout=kwargs["next_grad_amax_xout"])
|
||||
amaxs.append(new_amax)
|
||||
saves.extend([*s, out])
|
||||
return out, h, amaxs, saves
|
||||
return out, h, saves
|
||||
|
||||
@function(precompile=True, precompile_backward=True)
|
||||
def run_layer(self, x:Tensor, freqs_cis:Tensor, attn_kwargs:dict, ffn_kwargs:dict, save:bool=True):
|
||||
attn, attn_amaxs, attn_saves = self.attention(x, freqs_cis, **attn_kwargs)
|
||||
ffn, h, ffn_amaxs, ffn_saves = self.feed_forward(x, attn, **ffn_kwargs)
|
||||
attn, attn_saves = self.attention(x, freqs_cis, **attn_kwargs)
|
||||
ffn, h, ffn_saves = self.feed_forward(x, attn, **ffn_kwargs)
|
||||
h = h + ffn
|
||||
amaxs = tuple(a.detach() for a in (*attn_amaxs, *ffn_amaxs))
|
||||
if save: return (h, *amaxs, *attn_saves, *ffn_saves)
|
||||
else: return (h, *amaxs)
|
||||
if save: return (h, *attn_saves, *ffn_saves)
|
||||
else: return (h,)
|
||||
|
||||
def shard(self, device:tuple[str, ...], mp:bool=False):
|
||||
from tinygrad.nn.state import get_parameters
|
||||
@@ -319,21 +318,21 @@ class FlatTransformer:
|
||||
for i in range(self.n_layers):
|
||||
attn_kwargs = dict(attention_norm=self.attention_norm[i], wqkv=self.wqkv[i], wo=self.wo[i],
|
||||
amax_xqkv=a["xqkv"][i], amax_xo=a["xo"][i], s_qkv=s["wqkv"][i], s_o=s["wo"][i],
|
||||
next_amax_xqkv=na["xqkv"][i], next_amax_xo=na["xo"][i],
|
||||
grad_amax_xqkv=ga["xqkv"][i], grad_amax_xo=ga["xo"][i],
|
||||
next_grad_amax_xqkv=nga["xqkv"][i], next_grad_amax_xo=nga["xo"][i])
|
||||
ffn_kwargs = dict(ffn_norm=self.ffn_norm[i], w2=self.w2[i],
|
||||
amax_x2=a["x2"][i], s_2=s["w2"][i], grad_amax_xout=ga["xout"][i], next_grad_amax_xout=nga["xout"][i])
|
||||
amax_x2=a["x2"][i], s_2=s["w2"][i], grad_amax_xout=ga["xout"][i], next_grad_amax_xout=nga["xout"][i],
|
||||
next_amax_x2=na["x2"][i])
|
||||
if SPLIT_W13:
|
||||
ffn_kwargs.update(w1=self.w1[i], w3=self.w3[i], amax_x1=a["x1"][i], amax_x3=a["x3"][i],
|
||||
next_amax_x1=na["x1"][i], next_amax_x3=na["x3"][i],
|
||||
s_1=s["w1"][i], s_3=s["w3"][i], grad_amax_xw1=ga["xw1"][i], grad_amax_xw3=ga["xw3"][i],
|
||||
next_grad_amax_xw1=nga["xw1"][i], next_grad_amax_xw3=nga["xw3"][i])
|
||||
else:
|
||||
ffn_kwargs.update(w13=self.w13[i], amax_x13=a["x13"][i], s_13=s["w13"][i], grad_amax_xw13=ga["xw13"][i],
|
||||
next_grad_amax_xw13=nga["xw13"][i])
|
||||
h, *ret = self.run_layer(h, freqs_cis, attn_kwargs, ffn_kwargs, save=save)
|
||||
amax_names = ["xqkv", "xo"] + (["x1", "x3"] if SPLIT_W13 else ["x13"]) + ["x2"]
|
||||
for name, new_val in zip(amax_names, ret[:len(amax_names)]):
|
||||
na[name][i].assign(new_val)
|
||||
next_grad_amax_xw13=nga["xw13"][i], next_amax_x13=na["x13"][i])
|
||||
h, *_ = self.run_layer(h, freqs_cis, attn_kwargs, ffn_kwargs, save=save)
|
||||
|
||||
logits = matmul(self.norm(h), self.output[0], fp8=False)[0]
|
||||
return logits
|
||||
@@ -416,6 +415,9 @@ if __name__ == "__main__":
|
||||
@TinyJit
|
||||
def fwd_bwd(tokens:Tensor):
|
||||
with Timing("python forward: "):
|
||||
for amax_dict in (model._fp8_next_amax, model._fp8_next_grad_amax):
|
||||
for ts in amax_dict.values():
|
||||
for nxt in ts: nxt.assign(0)
|
||||
logits = model(tokens[:, :-1], save=llama_size=="8B")
|
||||
loss = vocab_mask.where(-1e9, logits).sparse_categorical_crossentropy(tokens[:, 1:])
|
||||
with Timing("python backward: "):
|
||||
|
||||
@@ -96,7 +96,7 @@ class GradAccClipAdamW(Optimizer):
|
||||
up = up.float().shard_like(w) + self.lr.to(w.device) * wd * w.detach()
|
||||
new_w = w.detach() - up
|
||||
if master is not None: master.assign(new_w)
|
||||
if self.zero: new_w = self._zero_gather(new_w)
|
||||
if self.zero and not (MXFP8 and t.dtype in dtypes.fp8s): new_w = self._zero_gather(new_w)
|
||||
# when master is offloaded to a different device than the param, results are resharded back onto the param's (sharded) device
|
||||
offloaded = master is not None and master.device != t.device
|
||||
if STOCHASTIC_ROUND and t.dtype == dtypes.bfloat16:
|
||||
@@ -106,6 +106,7 @@ class GradAccClipAdamW(Optimizer):
|
||||
if MXFP8:
|
||||
from extra.gemm.cdna_asm_gemm import quantize_mxfp8
|
||||
w_q, w_e8, _ = quantize_mxfp8(new_w.reshape(-1, new_w.shape[-1]))
|
||||
if self.zero: w_q, w_e8 = self._zero_gather(w_q), self._zero_gather(w_e8)
|
||||
new_e8 = w_e8.reshape(t._inv_scale.shape)
|
||||
t._inv_scale.assign(new_e8.shard_like(t._inv_scale) if offloaded else new_e8)
|
||||
ret = w_q.reshape(new_w.shape)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Benchmark tinygrad LLM prefill and decode independently.
|
||||
|
||||
Examples:
|
||||
python -m extra.benchmark_llm --model qwen3:0.6b --max-context 32768
|
||||
python -m extra.benchmark_llm --model /path/to/model.gguf --prompt-tokens 8192 --realize
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse, json, statistics, time
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
from tinygrad.helpers import fetch
|
||||
from tinygrad.llm.cli import models
|
||||
from tinygrad.llm.model import Transformer
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
prompt_tokens: int
|
||||
decode_tokens: int
|
||||
time_to_first_token_s: float
|
||||
prefill_tokens_per_s: float
|
||||
decode_tokens_per_s: float
|
||||
decode_p50_ms: float
|
||||
decode_p95_ms: float
|
||||
|
||||
|
||||
def percentile(values:list[float], percentile:float) -> float:
|
||||
ordered = sorted(values)
|
||||
return ordered[round((len(ordered) - 1) * percentile)]
|
||||
|
||||
|
||||
def synthetic_prompt(length:int, vocab_size:int, salt:int) -> list[int]:
|
||||
# Avoid tokenizer and chat-template work while exercising the same embedding/model path.
|
||||
# Changing token zero guarantees that Transformer.get_start_pos cannot reuse an earlier KV cache.
|
||||
assert length > 0 and vocab_size > 256
|
||||
return [256 + salt % (vocab_size - 256)] + [256 + (i * 7919) % (vocab_size - 256) for i in range(1, length)]
|
||||
|
||||
|
||||
def benchmark(model:Transformer, prompt:list[int], decode_tokens:int, chunk_size:int) -> Result:
|
||||
gen = model.generate(prompt.copy(), chunk_size=chunk_size)
|
||||
begin = time.perf_counter()
|
||||
next(gen)
|
||||
ttft = time.perf_counter() - begin
|
||||
|
||||
decode_times: list[float] = []
|
||||
for _ in range(decode_tokens):
|
||||
begin = time.perf_counter()
|
||||
next(gen)
|
||||
decode_times.append(time.perf_counter() - begin)
|
||||
|
||||
return Result(len(prompt), decode_tokens, ttft, len(prompt) / ttft, decode_tokens / sum(decode_times),
|
||||
statistics.median(decode_times) * 1e3, percentile(decode_times, 0.95) * 1e3)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Measure LLM prefill and steady-state decode speed")
|
||||
parser.add_argument("--model", default="qwen3:0.6b", help="Model preset or local GGUF path")
|
||||
parser.add_argument("--max-context", type=int, default=32768)
|
||||
parser.add_argument("--prompt-tokens", type=int, nargs="+", default=[128, 2048, 8192])
|
||||
parser.add_argument("--decode-tokens", type=int, default=32)
|
||||
parser.add_argument("--chunk-size", type=int, default=256)
|
||||
parser.add_argument("--realize", action="store_true", help="Unpack model weights once at load time")
|
||||
parser.add_argument("--json", action="store_true", help="Print machine-readable results")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.decode_tokens < 1: parser.error("--decode-tokens must be positive")
|
||||
if args.chunk_size < 1: parser.error("--chunk-size must be positive")
|
||||
if max(args.prompt_tokens) + args.decode_tokens >= args.max_context:
|
||||
parser.error("prompt plus decode tokens must fit within --max-context")
|
||||
|
||||
path = fetch(models.get(args.model, args.model))
|
||||
model, kv = Transformer.from_gguf(path, args.max_context, realize=args.realize)
|
||||
vocab_size = len(kv["tokenizer.ggml.tokens"])
|
||||
|
||||
model.warmup(args.chunk_size)
|
||||
|
||||
results = [benchmark(model, synthetic_prompt(n, vocab_size, salt=i+1), args.decode_tokens, args.chunk_size)
|
||||
for i, n in enumerate(args.prompt_tokens)]
|
||||
if args.json:
|
||||
print(json.dumps({"model": args.model, "max_context": args.max_context, "chunk_size": args.chunk_size,
|
||||
"realize": args.realize, "results": [asdict(x) for x in results]}, indent=2))
|
||||
return
|
||||
|
||||
print(f"model={args.model} max_context={args.max_context} chunk_size={args.chunk_size} realize={args.realize}")
|
||||
print(f"{'prompt':>8} {'TTFT':>10} {'prefill':>14} {'decode':>14} {'decode p50':>12} {'decode p95':>12}")
|
||||
for result in results:
|
||||
print(f"{result.prompt_tokens:8d} {result.time_to_first_token_s:9.3f}s {result.prefill_tokens_per_s:11.1f} t/s "
|
||||
f"{result.decode_tokens_per_s:11.1f} t/s {result.decode_p50_ms:9.2f} ms {result.decode_p95_ms:9.2f} ms")
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -1,16 +1,13 @@
|
||||
from tinygrad import Tensor, UOp, getenv
|
||||
from tinygrad.uop.ops import AxisType, KernelInfo, Ops
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.helpers import GlobalCounters, Context
|
||||
import functools, math
|
||||
from tinygrad.helpers import DEBUG, GlobalCounters, Context
|
||||
import math
|
||||
|
||||
BLOCK_M, BLOCK_N = 32, 32
|
||||
DECODE_BLOCK_N = 128
|
||||
DECODE_HEAD_TILE = 4
|
||||
DECODE_WAVES = 4
|
||||
BLOCK_M, BLOCK_N = 64, 64
|
||||
WARP_SIZE = 32
|
||||
WMMA_M, WMMA_N, WMMA_K = 16, 16, 16
|
||||
WAVES_M, WAVES_N = 2, 2
|
||||
WAVES_M, WAVES_N = 4, 1
|
||||
LANES_PER_WAVE_M, LANES_PER_WAVE_N = 2, 16
|
||||
WMMA_ACC = WMMA_M // LANES_PER_WAVE_M
|
||||
THREADS_PER_BLOCK = WARP_SIZE * WAVES_M * WAVES_N
|
||||
@@ -38,137 +35,24 @@ def warp_reduce_sum(val, lane):
|
||||
val = val + warp_shfl_xor(val, offset, lane)
|
||||
return val
|
||||
|
||||
def wave_reduce_sum(val, lane):
|
||||
for offset in [16, 8, 4, 2, 1]: val = val + warp_shfl_xor(val, offset, lane)
|
||||
return val
|
||||
|
||||
@functools.cache
|
||||
def _amd_flash_attention_decode_partial(out:UOp, stats:UOp, q:UOp, cache_kv:UOp, valid_kv_len:int|UOp, max_kv_len:int) -> UOp:
|
||||
_, B, H_KV, N, D = cache_kv.shape
|
||||
_, H, M, _ = q.shape
|
||||
assert M == 1 and H % H_KV == 0 and D % WARP_SIZE == 0 and max_kv_len <= N and max_kv_len % DECODE_BLOCK_N == 0
|
||||
G, CHUNK, DV = H // H_KV, DECODE_BLOCK_N, D // WARP_SIZE
|
||||
assert G % DECODE_HEAD_TILE == 0
|
||||
block_bhkv = UOp.range(B*H_KV*(G//DECODE_HEAD_TILE), 0, AxisType.GLOBAL)
|
||||
block_n = UOp.range((valid_kv_len+CHUNK-1)//CHUNK, 1, AxisType.GLOBAL)
|
||||
lane, wave = UOp.range(WARP_SIZE, 2, AxisType.LOCAL), UOp.range(DECODE_WAVES, 3, AxisType.LOCAL)
|
||||
head_group = block_bhkv % (G//DECODE_HEAD_TILE)
|
||||
bhkv = block_bhkv // (G//DECODE_HEAD_TILE)
|
||||
b, kv_head = bhkv // H_KV, bhkv % H_KV
|
||||
dims = tuple(lane + i*WARP_SIZE for i in range(DV))
|
||||
|
||||
acc = UOp.placeholder((DECODE_HEAD_TILE, DV), dtypes.float, slot=0, addrspace=AddrSpace.REG)
|
||||
row_max = UOp.placeholder((DECODE_HEAD_TILE,), dtypes.float, slot=1, addrspace=AddrSpace.REG)
|
||||
row_sum = UOp.placeholder((DECODE_HEAD_TILE,), dtypes.float, slot=2, addrspace=AddrSpace.REG)
|
||||
init = UOp.group(acc.store(acc.const_like(0)), row_max.store(row_max.const_like(-math.inf)), row_sum.store(row_sum.const_like(0)))
|
||||
acc, row_max, row_sum = acc.after(init), row_max.after(init), row_sum.after(init)
|
||||
|
||||
offset = UOp.range(CHUNK//DECODE_WAVES, 100, AxisType.REDUCE)
|
||||
key = block_n*CHUNK + wave*(CHUNK//DECODE_WAVES) + offset
|
||||
valid = key < valid_kv_len
|
||||
kvals = tuple(cache_kv[0, b, kv_head, key, d].float() for d in dims)
|
||||
vvals = tuple(cache_kv[1, b, kv_head, key, d].float() for d in dims)
|
||||
updates = []
|
||||
for head in range(DECODE_HEAD_TILE):
|
||||
q_head = kv_head*G + head_group*DECODE_HEAD_TILE + head
|
||||
score = wave_reduce_sum(sum((q[b, q_head, 0, d].float()*k for d,k in zip(dims, kvals)), UOp.const(dtypes.float, 0)), lane) / math.sqrt(D)
|
||||
new_max = valid.where(row_max[head].maximum(score), row_max[head])
|
||||
alpha = valid.where(((row_max[head]-new_max)*LOG2E).exp2(), UOp.const(dtypes.float, 1))
|
||||
beta = valid.where(((score-new_max)*LOG2E).exp2(), UOp.const(dtypes.float, 0))
|
||||
updates += [acc[head].store(acc[head]*alpha + UOp.stack(*vvals)*beta),
|
||||
row_sum[head].store(row_sum[head]*alpha + beta), row_max[head].store(new_max)]
|
||||
update = UOp.group(*updates).end(offset)
|
||||
acc, row_max, row_sum = acc.after(update), row_max.after(update), row_sum.after(update)
|
||||
|
||||
partial_acc = UOp.placeholder((DECODE_HEAD_TILE, DECODE_WAVES, D), dtypes.float, slot=3, addrspace=AddrSpace.LOCAL)
|
||||
partial_stats = UOp.placeholder((DECODE_HEAD_TILE, DECODE_WAVES, 2), dtypes.float, slot=4, addrspace=AddrSpace.LOCAL)
|
||||
partial_stores = []
|
||||
for head in range(DECODE_HEAD_TILE):
|
||||
partial_stores += [partial_acc[head, wave, d].store(acc[head, i]) for i,d in enumerate(dims)]
|
||||
partial_stores += [partial_stats[head, wave.valid(lane.eq(0)), 0].store(row_max[head]),
|
||||
partial_stats[head, wave.valid(lane.eq(0)), 1].store(row_sum[head])]
|
||||
merged = UOp.group(*partial_stores).barrier()
|
||||
stores = []
|
||||
for head in range(DECODE_HEAD_TILE):
|
||||
q_head = kv_head*G + head_group*DECODE_HEAD_TILE + head
|
||||
maximum = partial_stats.after(merged)[head, 0, 0].maximum(partial_stats.after(merged)[head, 1, 0])
|
||||
scales = tuple(((partial_stats.after(merged)[head, w, 0]-maximum)*LOG2E).exp2() for w in range(DECODE_WAVES))
|
||||
denominator = sum((partial_stats.after(merged)[head, w, 1]*scales[w] for w in range(DECODE_WAVES)), UOp.const(dtypes.float, 0))
|
||||
stores += [out[b, q_head, block_n, d.valid(wave.eq(0))].store(
|
||||
sum((partial_acc.after(merged)[head, w, d]*scales[w] for w in range(DECODE_WAVES)), UOp.const(dtypes.float, 0))) for d in dims]
|
||||
stores += [stats[b, q_head.valid(lane.eq(0) & wave.eq(0)), block_n, 0].store(maximum),
|
||||
stats[b, q_head.valid(lane.eq(0) & wave.eq(0)), block_n, 1].store(denominator)]
|
||||
return UOp.group(*stores).end(lane, wave, block_n, block_bhkv).sink(arg=KernelInfo(name="flash_decode_partial", opts_to_apply=()))
|
||||
|
||||
@functools.cache
|
||||
def _amd_flash_attention_decode_reduce(out:UOp, partial:UOp, stats:UOp, valid_chunks:int|UOp) -> UOp:
|
||||
B, H, _, D = out.shape
|
||||
assert D % WARP_SIZE == 0
|
||||
DV = D // WARP_SIZE
|
||||
block_bh, lane = UOp.range(B*H, 0, AxisType.GLOBAL), UOp.range(WARP_SIZE, 1, AxisType.LOCAL)
|
||||
b, head = block_bh // H, block_bh % H
|
||||
dims = tuple(lane + i*WARP_SIZE for i in range(DV))
|
||||
|
||||
row_max = UOp.placeholder((1,), dtypes.float, slot=0, addrspace=AddrSpace.REG)
|
||||
row_max = row_max.after(row_max.store(row_max.const_like(-math.inf)))
|
||||
chunk_max = UOp.range(valid_chunks, 100, AxisType.REDUCE)
|
||||
max_done = row_max.store(row_max.after(chunk_max).maximum(stats[b, head, chunk_max, 0])).end(chunk_max)
|
||||
row_max = row_max.after(max_done)
|
||||
|
||||
numerator = UOp.placeholder((DV,), dtypes.float, slot=1, addrspace=AddrSpace.REG)
|
||||
denominator = UOp.placeholder((1,), dtypes.float, slot=2, addrspace=AddrSpace.REG)
|
||||
init = UOp.group(numerator.store(numerator.const_like(0)), denominator.store(denominator.const_like(0)))
|
||||
numerator, denominator = numerator.after(init), denominator.after(init)
|
||||
chunk = UOp.range(valid_chunks, 101, AxisType.REDUCE)
|
||||
scale = ((stats[b, head, chunk, 0]-row_max[0])*LOG2E).exp2()
|
||||
update = UOp.group(numerator.store(numerator.after(chunk) + UOp.stack(*(partial[b, head, chunk, d] for d in dims))*scale),
|
||||
denominator.store(denominator.after(chunk) + stats[b, head, chunk, 1]*scale)).end(chunk)
|
||||
numerator, denominator = numerator.after(update), denominator.after(update)
|
||||
stores = [out[b, head, 0, d].store(numerator[i]/denominator[0]) for i,d in enumerate(dims)]
|
||||
return UOp.group(*stores).end(lane, block_bh).sink(arg=KernelInfo(name="flash_decode_reduce", opts_to_apply=()))
|
||||
|
||||
def amd_flash_attention_decode(q:Tensor, cache_kv:Tensor, valid_kv_len:int|UOp, max_kv_len:int|None=None) -> Tensor:
|
||||
_, B, H_KV, N, D = cache_kv.shape
|
||||
_, H, M, _ = q.shape
|
||||
max_kv_len = N if max_kv_len is None else max_kv_len
|
||||
assert M == 1 and max_kv_len <= N and max_kv_len % DECODE_BLOCK_N == 0
|
||||
chunks = max_kv_len // DECODE_BLOCK_N
|
||||
partial = Tensor.empty(B, H, chunks, D, dtype="float32", device=q.device)
|
||||
stats = Tensor.empty(B, H, chunks, 2, dtype="float32", device=q.device)
|
||||
partial, stats = Tensor.custom_kernel(partial, stats, q, cache_kv,
|
||||
fxn=functools.partial(_amd_flash_attention_decode_partial, valid_kv_len=valid_kv_len, max_kv_len=max_kv_len))[:2]
|
||||
live_chunks = (valid_kv_len+DECODE_BLOCK_N-1)//DECODE_BLOCK_N
|
||||
out = Tensor.empty(B, H, 1, D, dtype="float32", device=q.device)
|
||||
return Tensor.custom_kernel(out, partial, stats,
|
||||
fxn=functools.partial(_amd_flash_attention_decode_reduce, valid_chunks=live_chunks))[0]
|
||||
|
||||
@functools.cache
|
||||
def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:int|UOp|None=None,
|
||||
key_limit:int|UOp|None=None) -> UOp:
|
||||
# inputs are q=(B*H, M, D), k/v=(B*H, N, D). For causal attention q is the final M tokens of k/v.
|
||||
BH, M, D = q.shape
|
||||
physical_n = k.shape[1]
|
||||
N = physical_n if valid_kv_len is None else valid_kv_len
|
||||
assert k.shape == v.shape and BH % k.shape[0] == 0 and k.shape[2] == D
|
||||
gqa_group = BH // k.shape[0]
|
||||
if isinstance(M, int) and isinstance(N, int):
|
||||
assert M % BLOCK_M == 0 and N % BLOCK_N == 0, \
|
||||
f"M={M} and N={N} must be divisible by BLOCK_M={BLOCK_M} and BLOCK_N={BLOCK_N}"
|
||||
assert isinstance(D, int) and D % WMMA_K == 0 and D % LANES_PER_WAVE_N == 0, \
|
||||
f"D={D} must be divisible by WMMA_K={WMMA_K} and LANES_PER_WAVE_N={LANES_PER_WAVE_N}"
|
||||
def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
# inputs are (B*H, N, D)
|
||||
BH, N, D = q.shape
|
||||
assert N % BLOCK_M == 0 and N % BLOCK_N == 0, f"N={N} must be divisible by BLOCK_M={BLOCK_M} and BLOCK_N={BLOCK_N}"
|
||||
assert D % WMMA_K == 0 and D % LANES_PER_WAVE_N == 0, f"D={D} must be divisible by WMMA_K={WMMA_K} and LANES_PER_WAVE_N={LANES_PER_WAVE_N}"
|
||||
assert BLOCK_M % (WAVES_M * WMMA_M) == 0 and BLOCK_N % LANES_PER_WAVE_N == 0
|
||||
TM = BLOCK_M // (WAVES_M * LANES_PER_WAVE_M)
|
||||
# Each N wave computes the same score tile, then owns a disjoint slice of D for P@V.
|
||||
TN = BLOCK_N // LANES_PER_WAVE_N
|
||||
TN = BLOCK_N // (WAVES_N * LANES_PER_WAVE_N)
|
||||
TD = D // (WAVES_N * LANES_PER_WAVE_N)
|
||||
SCALE = 1.0 / math.sqrt(D)
|
||||
|
||||
block_bh = UOp.range(BH, 0, AxisType.GLOBAL)
|
||||
block_m = UOp.range(M // BLOCK_M, 1, AxisType.GLOBAL)
|
||||
block_m = UOp.range(N // BLOCK_M, 1, AxisType.GLOBAL)
|
||||
|
||||
q = q.reshape(BH, M//BLOCK_M, BLOCK_M, D)[block_bh, block_m]
|
||||
k, v = k[block_bh // gqa_group], v[block_bh // gqa_group]
|
||||
o = o.reshape(BH, M//BLOCK_M, BLOCK_M, D)[block_bh, block_m]
|
||||
q = q.reshape(BH, N//BLOCK_M, BLOCK_M, D)[block_bh, block_m]
|
||||
k = k.reshape(BH, N//BLOCK_N, BLOCK_N, D)[block_bh]
|
||||
v = v.reshape(BH, N//BLOCK_N, BLOCK_N, D)[block_bh]
|
||||
o = o.reshape(BH, N//BLOCK_M, BLOCK_M, D)[block_bh, block_m]
|
||||
|
||||
wave_m = UOp.range(WAVES_M, 2, AxisType.LOCAL)
|
||||
wave_n = UOp.range(WAVES_N, 3, AxisType.LOCAL)
|
||||
@@ -179,8 +63,7 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
|
||||
# LDS allocation: slot 0 = Q then P (shared), slot 1 = K then V
|
||||
# TODO: the memory planner should be able to find this reuse
|
||||
Q_ELEMS_PER_THREAD = BLOCK_M * D // THREADS_PER_BLOCK
|
||||
KV_ELEMS_PER_THREAD = BLOCK_N * D // THREADS_PER_BLOCK
|
||||
ELEMS_PER_THREAD = BLOCK_M * D // THREADS_PER_BLOCK
|
||||
QP_lds = UOp.placeholder((BLOCK_M, D + LDS_PAD), dtypes.half, slot=0, addrspace=AddrSpace.LOCAL)
|
||||
KV_lds = UOp.placeholder((BLOCK_N, D + LDS_PAD), dtypes.half, slot=1, addrspace=AddrSpace.LOCAL)[:, :D]
|
||||
|
||||
@@ -193,18 +76,14 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
l_i = l_i.after(l_i.store(l_i.const_like(0)))
|
||||
|
||||
# ====== KV tile loop ======
|
||||
# Causal blocks never need KV tiles strictly to their right. Besides saving work, this avoids an all
|
||||
# -inf tile, whose online-softmax update would otherwise contain -inf - -inf.
|
||||
n_tiles = (N - M + (block_m + 1) * BLOCK_M + BLOCK_N - 1) // BLOCK_N if causal else N // BLOCK_N
|
||||
n_tile = UOp.range(n_tiles, 100, AxisType.REDUCE)
|
||||
n_tile = UOp.range(N // BLOCK_N, 100, AxisType.REDUCE)
|
||||
|
||||
# load Q + K into LDS (Q reloaded each iteration since P overwrites slot 0)
|
||||
Q_lds = QP_lds[:, :D]
|
||||
Q_store = Q_lds.after(n_tile).reshape(THREADS_PER_BLOCK, Q_ELEMS_PER_THREAD)[tid].store(
|
||||
q.reshape(THREADS_PER_BLOCK, Q_ELEMS_PER_THREAD)[tid])
|
||||
load_k = UOp.range(KV_ELEMS_PER_THREAD, 90, AxisType.LOOP)
|
||||
K_store = KV_lds.reshape(THREADS_PER_BLOCK, KV_ELEMS_PER_THREAD)[tid, load_k].store(
|
||||
k.reshape(physical_n*D)[n_tile*BLOCK_N*D + tid*KV_ELEMS_PER_THREAD + load_k]).end(load_k)
|
||||
Q_store = Q_lds.after(n_tile).reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
|
||||
q.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
K_store = KV_lds.reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
|
||||
k[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
qk_load_barrier = UOp.barrier(UOp.group(Q_store, K_store))
|
||||
Q_lds = Q_lds.after(qk_load_barrier)
|
||||
KV_lds_k = KV_lds.after(qk_load_barrier)
|
||||
@@ -217,7 +96,7 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
tn1 = UOp.range(TN, 201, AxisType.LOOP)
|
||||
S_frag = S_reg.reshape(TM // WMMA_ACC, WMMA_ACC, TN).permute(0, 2, 1)[tm1, tn1]
|
||||
q_frag = Q_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, D // WMMA_K, WMMA_K)[wave_m, tm1, lane_n, k_qk]
|
||||
k_frag = KV_lds_k.reshape(TN, WMMA_N, D // WMMA_K, WMMA_K)[tn1, lane_n, k_qk]
|
||||
k_frag = KV_lds_k.reshape(WAVES_N, TN, WMMA_N, D // WMMA_K, WMMA_K)[wave_n, tn1, lane_n, k_qk]
|
||||
qk = UOp.wmma(q_frag, k_frag, S_frag.after(k_qk), *WMMA_ARG)
|
||||
qk_done = S_frag.store(qk).end(tm1, tn1).end(k_qk)
|
||||
S_reg = S_reg.after(qk_done)
|
||||
@@ -225,18 +104,6 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
# -- softmax in registers with warp shuffles --
|
||||
S_reg = S_reg.after(S_reg.store(S_reg * SCALE))
|
||||
|
||||
if causal:
|
||||
# WMMA accumulator ownership: each lane owns an 8x4 fragment of the 64x64 score tile.
|
||||
# q is aligned to the right of k, matching PyTorch's causal_lower_right mask.
|
||||
rm = UOp.range(TM, 250, AxisType.LOOP)
|
||||
rn = UOp.range(TN, 251, AxisType.LOOP)
|
||||
q_idx = N - M + block_m * BLOCK_M + wave_m * WMMA_M + rm * LANES_PER_WAVE_M + lane_m
|
||||
k_idx = n_tile * BLOCK_N + rn * LANES_PER_WAVE_N + lane_n
|
||||
valid = k_idx <= q_idx
|
||||
if key_limit is not None: valid = valid & (k_idx < key_limit)
|
||||
masked = valid.where(S_reg[rm, rn], S_reg[rm, rn].const_like(-math.inf))
|
||||
S_reg = S_reg.after(S_reg[rm, rn].store(masked).end(rm, rn))
|
||||
|
||||
# per-thread local row max over TN=4 elements, then warp reduce across 16 lanes
|
||||
m_ij = UOp.placeholder((TM,), dtypes.float, slot=7, addrspace=AddrSpace.REG)
|
||||
m_ij = m_ij.after(m_ij.after(n_tile).store(m_ij.const_like(-math.inf)))
|
||||
@@ -251,19 +118,18 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
|
||||
p_local = UOp.placeholder((TM,), dtypes.float, slot=8, addrspace=AddrSpace.REG)
|
||||
p_local = p_local.after(p_local.after(n_tile).store(p_local.const_like(0)))
|
||||
rp2 = UOp.range(TN, 291, AxisType.REDUCE)
|
||||
p_local = p_local.after(p_local.store(p_local.after(rp2) + S_reg[:, rp2]).end(rp2))
|
||||
ri_ws = UOp.range(TM, 295, AxisType.LOOP)
|
||||
# Reduce contiguous 16-key groups independently, matching the ordinary softmax reduction tree.
|
||||
p_sum = p_local.after(p_local[ri_ws].store(
|
||||
sum((warp_reduce_sum(S_reg[ri_ws, rn], lane) for rn in range(TN)), S_reg.const_like(0))).end(ri_ws))
|
||||
p_sum = p_local.after(p_local[ri_ws].store(warp_reduce_sum(p_local[ri_ws], lane)).end(ri_ws))
|
||||
|
||||
# Store softmax weights in half for the WMMA P@V product; accumulation remains float.
|
||||
P_lds = QP_lds.flatten()[:WAVES_N * BLOCK_M * BLOCK_N].reshape(WAVES_N, BLOCK_M, BLOCK_N)
|
||||
P_write = P_lds.reshape(WAVES_N, WAVES_M, TM, LANES_PER_WAVE_M, 1, TN, LANES_PER_WAVE_N, 1)
|
||||
P_write = P_write.permute((1, 0, 3, 6, 2, 4, 5, 7)).reshape(THREADS_PER_BLOCK, TM, TN)
|
||||
# write P = exp(S - m_ij) to P_lds (reuses slot 0, Q no longer needed)
|
||||
P_lds = QP_lds[:, :BLOCK_N]
|
||||
P_write = P_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_ACC, LANES_PER_WAVE_M, WAVES_N, TN, LANES_PER_WAVE_N)
|
||||
P_write = P_write.permute((0, 4, 3, 6, 1, 2, 5)).reshape(THREADS_PER_BLOCK, TM, TN)
|
||||
P_store = P_write[tid].store(S_reg.cast(dtypes.half))
|
||||
|
||||
# -- online softmax correction --
|
||||
beta_i = UOp.placeholder((TM,), dtypes.float, slot=9, addrspace=AddrSpace.REG)
|
||||
ri4 = UOp.range(TM, 330, AxisType.LOOP)
|
||||
m_new_val = m_i[ri4].maximum(m_ij[ri4])
|
||||
alpha_val = ((m_i[ri4] - m_new_val) * LOG2E).exp2()
|
||||
@@ -273,43 +139,29 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
acc[ri4, rj4].store(alpha_val * acc[ri4, rj4]).end(rj4),
|
||||
l_i[ri4].store(alpha_val * l_i[ri4] + beta_val * p_sum[ri4]),
|
||||
m_i[ri4].store(m_new_val),
|
||||
beta_i[ri4].store(beta_val),
|
||||
).end(ri4)
|
||||
acc = acc.after(correction)
|
||||
l_i = l_i.after(correction)
|
||||
m_i = m_i.after(correction)
|
||||
beta_i = beta_i.after(correction)
|
||||
|
||||
# Load V transposed into LDS: PV's B operand is logically (D, BLOCK_N), while global V is (BLOCK_N, D).
|
||||
# It reuses K's slot and must wait for QK WMMA to finish reading that slot.
|
||||
V_lds = UOp.placeholder((D, BLOCK_N + LDS_PAD), dtypes.half, slot=1, addrspace=AddrSpace.LOCAL)[:, :BLOCK_N]
|
||||
V_copy = V_lds.after(qk_done).permute(1, 0)
|
||||
load_v = UOp.range(KV_ELEMS_PER_THREAD, 390, AxisType.LOOP)
|
||||
V_store = V_copy.reshape(THREADS_PER_BLOCK, KV_ELEMS_PER_THREAD)[tid, load_v].store(
|
||||
v.reshape(physical_n*D)[n_tile*BLOCK_N*D + tid*KV_ELEMS_PER_THREAD + load_v]).end(load_v)
|
||||
# load V into KV_lds (must wait for QK WMMA to finish reading K from KV_lds)
|
||||
V_store = KV_lds.after(qk_done).reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid].store(
|
||||
v[n_tile].reshape(THREADS_PER_BLOCK, ELEMS_PER_THREAD)[tid])
|
||||
pv_barrier = UOp.barrier(UOp.group(P_store, V_store))
|
||||
P_lds = P_lds.after(pv_barrier)
|
||||
V_lds = V_lds.after(pv_barrier)
|
||||
KV_lds_v = KV_lds.after(pv_barrier)
|
||||
|
||||
# -- acc += beta * (P @ V) via WMMA --
|
||||
pv_acc = UOp.placeholder((TM, TD), dtypes.float, slot=10, addrspace=AddrSpace.REG)
|
||||
pv_acc = pv_acc.after(pv_acc.after(n_tile).store(pv_acc.const_like(0))).after(pv_barrier)
|
||||
# -- acc += P @ V via WMMA --
|
||||
k_pv = UOp.range(BLOCK_N // WMMA_K, 400, AxisType.REDUCE)
|
||||
tm2 = UOp.range(TM // WMMA_ACC, 401, AxisType.LOOP)
|
||||
tn2 = UOp.range(TD, 402, AxisType.LOOP)
|
||||
pv_frag = pv_acc.reshape(TM // WMMA_ACC, WMMA_ACC, TD).permute(0, 2, 1)[tm2, tn2]
|
||||
p_frag = P_lds[wave_n].reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_N // WMMA_K, WMMA_K)[wave_m, tm2, lane_n, k_pv]
|
||||
v_frag = V_lds.reshape(WAVES_N, TD, WMMA_N, BLOCK_N // WMMA_K, WMMA_K)[wave_n, tn2, lane_n, k_pv]
|
||||
pv = UOp.wmma(p_frag, v_frag, pv_frag.after(k_pv), *WMMA_ARG)
|
||||
pv_done = pv_frag.store(pv).end(tm2, tn2).end(k_pv)
|
||||
pv_acc = pv_acc.after(pv_done)
|
||||
|
||||
ri5 = UOp.range(TM, 410, AxisType.LOOP)
|
||||
rj5 = UOp.range(TD, 411, AxisType.LOOP)
|
||||
accumulate = acc[ri5, rj5].store(acc[ri5, rj5] + beta_i[ri5] * pv_acc[ri5, rj5]).end(ri5, rj5)
|
||||
acc_frag = acc.reshape(TM // WMMA_ACC, WMMA_ACC, TD).permute(0, 2, 1)[tm2, tn2]
|
||||
p_frag = P_lds.reshape(WAVES_M, TM // WMMA_ACC, WMMA_M, BLOCK_N // WMMA_K, WMMA_K)[wave_m, tm2, lane_n, k_pv]
|
||||
v_frag = KV_lds_v.reshape(WAVES_N, TD, WMMA_N, BLOCK_N // WMMA_K, WMMA_K)[wave_n, tn2, lane_n, k_pv]
|
||||
pv = UOp.wmma(p_frag, v_frag, acc_frag.after(k_pv), *WMMA_ARG)
|
||||
|
||||
# end KV tile loop
|
||||
n_tile_end = accumulate.barrier().end(n_tile)
|
||||
n_tile_end = acc_frag.store(pv).end(tm2, tn2).end(k_pv).barrier().end(n_tile)
|
||||
acc = acc.after(n_tile_end)
|
||||
l_i = l_i.after(n_tile_end)
|
||||
m_i = m_i.after(n_tile_end)
|
||||
@@ -318,49 +170,33 @@ def _amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp, causal:bool, valid_kv_len:i
|
||||
acc = acc.after(acc.store(acc * (1 / l_i).reshape(TM, 1).expand(TM, TD)))
|
||||
|
||||
# store output
|
||||
o = o.reshape(WAVES_M, TM, LANES_PER_WAVE_M, 1, WAVES_N, TD, LANES_PER_WAVE_N, 1)
|
||||
o = o.permute((0, 4, 2, 6, 1, 3, 5, 7)).reshape(THREADS_PER_BLOCK, TM, TD)
|
||||
o = o.reshape(WAVES_M, TM // WMMA_ACC, WMMA_ACC, LANES_PER_WAVE_M, WAVES_N, TD, LANES_PER_WAVE_N)
|
||||
o = o.permute((0, 4, 3, 6, 1, 2, 5)).reshape(THREADS_PER_BLOCK, TM, TD)
|
||||
return o[tid].store(acc).end(wave_m, wave_n, lane).end(block_m, block_bh).sink(arg=KernelInfo(opts_to_apply=()))
|
||||
|
||||
def amd_flash_attention(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
return _amd_flash_attention(o, q, k, v, causal=False)
|
||||
|
||||
def amd_flash_attention_causal(o:UOp, q:UOp, k:UOp, v:UOp) -> UOp:
|
||||
return _amd_flash_attention(o, q, k, v, causal=True)
|
||||
|
||||
def amd_flash_attention_causal_cached(o:UOp, q:UOp, cache_kv:UOp, *, valid_kv_len:int|UOp, key_limit:int|UOp|None=None) -> UOp:
|
||||
_, B, H_KV, N, D = cache_kv.shape
|
||||
k = cache_kv[0].reshape(B*H_KV, N, D)
|
||||
v = cache_kv[1].reshape(B*H_KV, N, D)
|
||||
return _amd_flash_attention(o, q, k, v, causal=True, valid_kv_len=valid_kv_len, key_limit=key_limit)
|
||||
|
||||
if __name__ == "__main__":
|
||||
B, H, N, D = getenv("B", 1), getenv("H", 32), getenv("N", 1024), getenv("D", 64)
|
||||
M, causal = getenv("M", N), getenv("CAUSAL", 0)
|
||||
q = Tensor.rand(B, H, M, D).cast(dtypes.half)
|
||||
q = Tensor.rand(B, H, N, D).cast(dtypes.half)
|
||||
k = Tensor.rand(B, H, N, D).cast(dtypes.half)
|
||||
v = Tensor.rand(B, H, N, D).cast(dtypes.half)
|
||||
o = Tensor.empty(B, H, M, D, dtype=dtypes.float)
|
||||
o = Tensor.empty(B, H, N, D, dtype=dtypes.float)
|
||||
with Context(DEBUG=0): Tensor.realize(q, k, v)
|
||||
|
||||
q_flat, k_flat, v_flat, o_flat = q.reshape(B*H, M, D), k.reshape(B*H, N, D), v.reshape(B*H, N, D), o.reshape(B*H, M, D)
|
||||
q_flat, k_flat, v_flat, o_flat = q.reshape(B*H, N, D), k.reshape(B*H, N, D), v.reshape(B*H, N, D), o.reshape(B*H, N, D)
|
||||
NUM_RUNS = getenv("CNT", 5)
|
||||
ets = []
|
||||
with Context(DEBUG=2):
|
||||
for _ in range(NUM_RUNS):
|
||||
GlobalCounters.reset()
|
||||
tst = Tensor.custom_kernel(o_flat, q_flat, k_flat, v_flat,
|
||||
fxn=amd_flash_attention_causal if causal else amd_flash_attention)[0].realize()
|
||||
tst = Tensor.custom_kernel(o_flat, q_flat, k_flat, v_flat, fxn=amd_flash_attention)[0].realize()
|
||||
ets.append(GlobalCounters.time_sum_s)
|
||||
print(f"best time: {min(ets)*1e3:.2f}ms")
|
||||
|
||||
if getenv("VERIFY", 1):
|
||||
with Context(DEBUG=0):
|
||||
mask = Tensor.full((1, 1, M, N), float("-inf"), buffer=False).triu(N-M+1) if causal else None
|
||||
ref = q.float().scaled_dot_product_attention(k.float(), v.float(), attn_mask=mask).reshape(B*H, M, D).realize()
|
||||
diff = (ref - tst).abs()
|
||||
err, max_err = diff.square().mean().item(), diff.max().item()
|
||||
print(f"mean squared error {err}, max error {max_err}")
|
||||
ref = q.float().scaled_dot_product_attention(k.float(), v.float()).reshape(B*H, N, D).realize()
|
||||
err = (ref - tst).square().mean().item()
|
||||
print(f"mean squared error {err}")
|
||||
if err > 1e-2:
|
||||
raise RuntimeError("flash attention is wrong!")
|
||||
else:
|
||||
|
||||
@@ -174,10 +174,10 @@ def custom_uop_gemm(C:UOp, A:UOp, B:UOp) -> UOp:
|
||||
m = UOp.range(M, 1, AxisType.LOOP)
|
||||
n = UOp.range(N, 2, AxisType.LOOP)
|
||||
k = UOp.range(K, 0, AxisType.REDUCE)
|
||||
mul = (A.flatten().index((m*UOp.const(dtypes.index, K)+k))*
|
||||
B.flatten().index((k*UOp.const(dtypes.index, N)+n))).cast(dtypes.float32)
|
||||
mul = (A.flatten().index((m*UOp.const(dtypes.weakint, K)+k))*
|
||||
B.flatten().index((k*UOp.const(dtypes.weakint, N)+n))).cast(dtypes.float32)
|
||||
red = mul.reduce(k, arg=Ops.ADD, dtype=dtypes.float32).cast(C.dtype)
|
||||
store = C.flatten().index((m*UOp.const(dtypes.index, N)+n)).store(red).end(m, n)
|
||||
store = C.flatten().index((m*UOp.const(dtypes.weakint, N)+n)).store(red).end(m, n)
|
||||
return store.sink(arg=KernelInfo(name=f'uop_gemm_{M}_{N}_{K}'))
|
||||
|
||||
# ** bf16 A @ B.T kernel in C
|
||||
@@ -280,10 +280,7 @@ def custom_gemm_bw(gradient:UOp, kernel:UOp, n_scales:int=2, has_grad_amax:bool=
|
||||
elif getenv("FUSED_GRAD_QUANTIZE", 0):
|
||||
grad_amax_t = Tensor(grad_amax_state, device=a.device)
|
||||
g_amax = grad_amax_t
|
||||
g_fp8, _, new_grad_amax, _ = quantize_fp8_delayed(g_t, g_amax)
|
||||
store_effect = next_grad_amax_state.store(new_grad_amax.uop)
|
||||
assert g_fp8.uop.op is Ops.AFTER, f"expected AFTER, got {g_fp8.uop.op}"
|
||||
g_fp8 = Tensor(g_fp8.uop.replace(src=g_fp8.uop.src + (store_effect,)), device=a.device)
|
||||
g_fp8, _ = quantize_fp8_delayed(g_t, g_amax, Tensor(next_grad_amax_state, device=a.device))
|
||||
else:
|
||||
grad_amax_t = Tensor(grad_amax_state, device=a.device)
|
||||
g_amax = grad_amax_t
|
||||
|
||||
+114
-137
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from typing import cast, Callable, TypeVar, Generic, Any
|
||||
import struct, functools, time, collections, itertools
|
||||
from dataclasses import replace, dataclass
|
||||
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize
|
||||
from tinygrad.helpers import DEV, getenv, select_first_inited, select_by_name, suppress_finalizing, dedup, pluralize, JIT_BATCH_SIZE
|
||||
from tinygrad.helpers import to_tuple, round_up, partition, data64_le, panic, ContextVar
|
||||
from tinygrad.device import Device, Buffer, BufferSpec, Compiled, LRUAllocator, MultiBuffer
|
||||
from tinygrad.uop.ops import Ops, sint, UOp, UPat, PatternMatcher, KernelInfo, graph_rewrite, track_rewrites, GroupOp
|
||||
@@ -102,159 +102,138 @@ def stage_copy(dst:UOp, src:UOp) -> UOp|None:
|
||||
pm_insert_copy_staging = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.COPY), UPat(name="dst"), UPat(name="src"))), stage_copy)])
|
||||
|
||||
# *****************
|
||||
# 2.1. tag hcq calls
|
||||
|
||||
def tag_hcq_call(ctx:itertools.count, call:UOp) -> UOp:
|
||||
if (hcq_devs:=next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)) is None: return call
|
||||
|
||||
queue = "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0"
|
||||
info = HCQInfo(get_call_name(call, get_call_arg_uops(call)), estimate_uop(call), to_tuple(hcq_devs), queue)
|
||||
return call.replace(arg=replace(call.arg, aux=info)).rtag(next(ctx))
|
||||
pm_tag_hcq_calls = PatternMatcher([(UPat(Ops.LINEAR, name="l"), lambda ctx, l: l.replace(src=tuple(tag_hcq_call(ctx, s) for s in l.src)))])
|
||||
|
||||
# *****************
|
||||
# 2.2. deps tracking
|
||||
# device.timeline_signal/value are the per-device schedule epoch. Before a schedule queue accesses memory owned by device N for the first time,
|
||||
# it waits for device[N].timeline_signal >= device[N].timeline_value - 1. This orders the schedule after all prior schedules that touched device N.
|
||||
#
|
||||
# queue.timeline_signal/value are per-queue progress counters used only inside a schedule.
|
||||
# Only the owner queue signals its queue.timeline_signal. Values are monotonic.
|
||||
#
|
||||
# At schedule end, one finalizer queue per touched device[N] waits for every active queue on device[N] to reach its schedule-local
|
||||
# final queue.timeline value, then signals device[N].timeline_signal with the schedule's reserved device epoch. After that, buffers/transients
|
||||
# for device N from this schedule are safe for the next schedule
|
||||
#
|
||||
# C programs reserve and bump timeline values, then patch command buffers with the concrete wait/signal values.
|
||||
# 2. deps
|
||||
|
||||
class HCQDepsTracker(DepsTracker):
|
||||
@staticmethod
|
||||
def _key(buf:Any) -> tuple[Any, int, int]:
|
||||
return (buf.arg.slot, 0, buf.max_numel() * buf.dtype.itemsize) if isinstance(buf, UOp) else DepsTracker._key(buf)
|
||||
|
||||
def make_deps(u:UOp, dep_lanes:list[tuple[UOp, int, int]], nlanes:int) -> UOp:
|
||||
deps:dict[UOp, list[int|None]] = collections.defaultdict(lambda: [None]*nlanes)
|
||||
for dep, dlane, lane in dep_lanes: deps[dep][lane] = dlane
|
||||
return u.after(*deps, arg=tuple(tuple(v) for v in deps.values()))
|
||||
|
||||
def sched_sync(ctx:DepsTracker, call:UOp) -> UOp|None:
|
||||
if not isinstance(call.arg.aux, HCQInfo): return None
|
||||
|
||||
def _get_call_bufs_by_lane(call:UOp, devices:tuple[str, ...]) -> list[list[Any]]:
|
||||
refs = get_call_arg_uops(call)
|
||||
outs, _ = get_call_outs_ins(call)
|
||||
devices, queue = call.arg.aux.device, call.arg.aux.queue
|
||||
return [[b if b.op is Ops.PARAM else mb.bufs[lane] if isinstance(mb:=b.buffer, MultiBuffer) else mb for b in refs] for lane in range(len(devices))]
|
||||
|
||||
dep_lanes:list[tuple[UOp, int, int]] = []
|
||||
for lane, d in enumerate(devices):
|
||||
lane_refs = [b if b.op is Ops.PARAM else mb.bufs[lane] if isinstance(mb:=b.buffer, MultiBuffer) else mb for b in refs]
|
||||
for dep, dlane in ctx.access_resources(lane_refs, outs, (call, lane)): dep_lanes.append((dep, dlane, lane))
|
||||
def _get_deps(ctx:DepsTracker, bufs_by_lane:list[list[Any]], write, key:tuple[tuple[str, ...], str, int]) -> list[tuple[tuple, int, int]]:
|
||||
dep_lanes:list[tuple[tuple, int, int]] = []
|
||||
for lane, bufs in enumerate(bufs_by_lane):
|
||||
dep_lanes += [(dep, dlane, lane) for dep, dlane in ctx.access_resources(bufs, write if write is not None else range(len(bufs)), (key, lane))]
|
||||
return dep_lanes
|
||||
|
||||
def _build_wait_cmds(dep_lanes:list[tuple[tuple, int, int]], devices:tuple[str, ...], queue:str) -> tuple[list[UOp], set[int]]:
|
||||
# opt1: same-queue ops are fifo-ordered
|
||||
if devices[0].split(":")[0] in {"AMD", "QCOM"} or queue.startswith("COPY"):
|
||||
dep_lanes = [(dep, dlane, lane) for dep, dlane, lane in dep_lanes if (dep.arg.aux.device[dlane], dep.arg.aux.queue) != (devices[lane], queue)]
|
||||
dep_lanes = [(dep, dlane, lane) for dep, dlane, lane in dep_lanes if (dep[0][dlane], dep[1]) != (devices[lane], queue)]
|
||||
|
||||
# keep latest dep per (dep device, queue, cur lane)
|
||||
latest = {((dep.arg.aux.device[dlane], dep.arg.aux.queue), lane): (dep, dlane) for dep, dlane, lane in sorted(dep_lanes, key=lambda x: x[0].tag)}
|
||||
return make_deps(call, [(dep, dlane, lane) for (_, lane), (dep, dlane) in latest.items()], len(devices))
|
||||
pm_sched_sync = PatternMatcher([(UPat(Ops.CALL, name="call"), sched_sync)])
|
||||
# opt2: keep latest dep per (dep device, queue, cur lane)
|
||||
latest = {((dep[0][dlane], dep[1]), lane): (dep, dlane) for dep, dlane, lane in sorted(dep_lanes, key=lambda x: x[0][2])}
|
||||
deps:dict[tuple, list[int|None]] = collections.defaultdict(lambda: [None]*len(devices))
|
||||
for (_, lane), (dep, dlane) in latest.items(): deps[dep][lane] = dlane
|
||||
|
||||
waits = []
|
||||
for (ddevs, dqueue, dtag), lanes in deps.items():
|
||||
sig = make_mstack([make_signal(d if dl is None else ddevs[dl], queue=dqueue, sentinel=dl is None) for dl, d in zip(lanes, devices)])
|
||||
val = make_mstack([make_signal_value(d if dl is None else ddevs[dl], queue=dqueue) for dl, d in zip(lanes, devices)])
|
||||
waits.append((sig.index(zero:=UOp.const(dtypes.int, 0)).load() >= val.index(zero) + dtag).wait())
|
||||
return waits, {dtag for _, _, dtag in deps}
|
||||
|
||||
def _build_finalizers(batch:list[tuple[UOp, tuple[str, ...]]], batch_info:list[tuple[tuple[str, ...], str]],
|
||||
tracker:HCQDepsTracker) -> tuple[list[UOp], set[int]]:
|
||||
# collect all buffers which belong to devices
|
||||
dev_bufs:dict[str, dict[int, Any]] = collections.defaultdict(dict)
|
||||
for call, devices in batch:
|
||||
for b in itertools.chain.from_iterable(_get_call_bufs_by_lane(call, devices)):
|
||||
for bd in to_tuple(b.device): dev_bufs[bd][id(b)] = b
|
||||
|
||||
zero, n, finalizers, waited = UOp.const(dtypes.int, 0), len(batch_info), [], set()
|
||||
for _, devgroup in itertools.groupby(sorted(dedup([d for devs, _ in batch_info for d in devs])), key=lambda d: d.split(":")[0]):
|
||||
devs = tuple(devgroup)
|
||||
|
||||
# to finalize the batch, sync all accesses from other devices to buffers that belong to this device
|
||||
fin_deps = [dl for dl in _get_deps(tracker, [list(dev_bufs[d].values()) for d in devs], None, key=(devs, "COMPUTE:0", n)) if dl[0][2] < n]
|
||||
waits, cur_waited = _build_wait_cmds(fin_deps, devs, "COMPUTE:0")
|
||||
waited |= cur_waited
|
||||
|
||||
# wait the syncs, store the device epoch; value bumps are a separate call: no lane may bump until every lane has patched its waits
|
||||
submit = make_submit(*waits, make_signal(devs).store((tl:=make_signal_value(devs)).index(zero)), devs=devs, queue="COMPUTE:0")
|
||||
upd = [(tl, 1)] + [(make_signal_value(devs, queue=qn), n) for qn in dedup([qn for bdevs, qn in batch_info if set(bdevs) & set(devs)])]
|
||||
bump = UOp.barrier(*[s.index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd])
|
||||
finalizers += [UOp.custom_function("hcq", b.sink()).call(aux=HCQInfo("hcq_finalizer", Estimates(), devs, "COMPUTE:0")) for b in (submit, bump)]
|
||||
return finalizers, waited
|
||||
|
||||
def _finalize_batch(batch:list[tuple[UOp, tuple[str, ...]]]) -> list[UOp]:
|
||||
batch_info = [(devices, "COMPUTE:0" if call.src[0].op is Ops.PROGRAM else "COPY:0") for call, devices in batch]
|
||||
|
||||
# schedule deps
|
||||
waited:set[int] = set()
|
||||
deps_tracker = HCQDepsTracker()
|
||||
call_waits:list[list[UOp]] = []
|
||||
for tag, ((call, _), (devices, queue)) in enumerate(zip(batch, batch_info)):
|
||||
deps = _get_deps(deps_tracker, _get_call_bufs_by_lane(call, devices), get_call_outs_ins(call)[0], key=(devices, queue, tag))
|
||||
cmds, cur_waited = _build_wait_cmds(deps, devices, queue)
|
||||
call_waits.append(cmds)
|
||||
waited |= cur_waited
|
||||
|
||||
# build finalizers
|
||||
finalizers, finalizer_waited = _build_finalizers(batch, batch_info, deps_tracker)
|
||||
waited |= finalizer_waited
|
||||
|
||||
src = []
|
||||
for tag, ((call, _), (devices, queue), cmds) in enumerate(zip(batch, batch_info, call_waits)):
|
||||
# first queue use, sync prior device work with main signal
|
||||
if batch_info.index((devices, queue)) == tag:
|
||||
epoch = (make_signal(devices).index(0).load() >= make_signal_value(devices).index(0) - 1).wait()
|
||||
cmds = [UOp(Ops.BARRIER), epoch] + cmds
|
||||
|
||||
# signal queue timeline if someone waits for us
|
||||
store = make_signal(devices, queue=queue).store(make_signal_value(devices, queue=queue).index(0) + tag) if tag in waited else None
|
||||
|
||||
# and make hcq call
|
||||
info = HCQInfo(get_call_name(call, get_call_arg_uops(call)), estimate_uop(call), devices, queue)
|
||||
cmds = [*cmds, call.replace(arg=replace(call.arg, aux=info))] + ([store] if store is not None else [])
|
||||
src.append(UOp.custom_function("hcq", make_submit(*cmds, devs=devices, queue=queue).sink()).call(name="hcq", aux=info))
|
||||
return src + finalizers
|
||||
|
||||
def sched_hcq_batches(l:UOp) -> UOp:
|
||||
srcs:list[UOp] = []
|
||||
batch:list[tuple[UOp, tuple[str, ...]]] = []
|
||||
for call in l.src:
|
||||
if (devs:=next((b.device for b in call.src[1:] if all_devices_in(b.device, HCQ_DEVS)), None)) is not None: batch.append((call, to_tuple(devs)))
|
||||
else: srcs, batch = srcs + _finalize_batch(batch) + [call], []
|
||||
return l.replace(src=tuple(srcs + _finalize_batch(batch)))
|
||||
pm_sched_hcq_batches = PatternMatcher([(UPat(Ops.LINEAR, name="l"), sched_hcq_batches)])
|
||||
|
||||
# *****************
|
||||
# 2.3. merge into queues
|
||||
# 3. merge into queues
|
||||
|
||||
def _merged_hcq_call(calls:list[UOp]):
|
||||
info = replace(unwrap_after(calls[0]).arg.aux, estimates=sum((unwrap_after(c).arg.aux.estimates for c in calls), start=Estimates()))
|
||||
cmdbuf = make_submit(*calls, devs=info.device, queue=info.queue)
|
||||
return UOp.custom_function("hcq", cmdbuf.sink()).call(name="hcq", aux=info)
|
||||
def _merged_hcq_call(calls:list[UOp]) -> UOp: # TODO: simplify?
|
||||
if len(calls) == 1: return calls[0]
|
||||
info = replace(calls[0].arg.aux, name=f"submit {calls[0].arg.aux.queue} ({len(calls)})",
|
||||
estimates=sum((c.arg.aux.estimates for c in calls), start=Estimates()))
|
||||
cmds = [cmd for c in calls for cmd in get_submit(c).src[0].src]
|
||||
return UOp.custom_function("hcq", make_submit(*cmds, devs=info.device, queue=info.queue).sink()).call(name="hcq", aux=info)
|
||||
|
||||
def merge_queues(linear:UOp) -> UOp:
|
||||
new_src:list[UOp] = []
|
||||
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of calls, kept in submit order
|
||||
opened_qs:dict[tuple[tuple[str, ...], str], list[UOp]] = {} # (devs, queue) -> list of hcq calls, kept in submit order
|
||||
limits = collections.defaultdict(lambda: JIT_BATCH_SIZE.value)
|
||||
|
||||
for call in linear.src:
|
||||
if not isinstance(unwrap_after(call).arg.aux, HCQInfo):
|
||||
if not isinstance(info:=call.arg.aux, HCQInfo) or info.name == "hcq_finalizer": # non-hcq call or finalizer: close all open queues
|
||||
new_src += [_merged_hcq_call(opened_qs.pop(k)) for k in list(opened_qs)] + [call]
|
||||
continue
|
||||
|
||||
devices, queue = unwrap_after(call).arg.aux.device, unwrap_after(call).arg.aux.queue
|
||||
|
||||
if (old:=opened_qs.pop((devices, queue), None)) is not None: new_rec = old + [call]
|
||||
if (old:=opened_qs.pop(key:=(info.device, info.queue), None)) is not None:
|
||||
if limits[key] and len(old) >= limits[key]: new_src, old, limits[key] = new_src + [_merged_hcq_call(old)], [], limits[key] * 2
|
||||
new_rec = old + [call]
|
||||
else:
|
||||
# no such queue opened: close every open submit on this queue that shares a device, so submit order is kept
|
||||
closing = [k for k in opened_qs if k[1] == queue and set(k[0]) & set(devices)]
|
||||
closing = [k for k in opened_qs if k[1] == info.queue and set(k[0]) & set(info.device)]
|
||||
new_src += [_merged_hcq_call(opened_qs.pop(k)) for k in closing]
|
||||
new_rec = [call]
|
||||
opened_qs[(devices, queue)] = new_rec
|
||||
opened_qs[(info.device, info.queue)] = new_rec
|
||||
return linear.replace(src=tuple(new_src + [_merged_hcq_call(c) for c in opened_qs.values()]))
|
||||
pm_merge_queues = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), merge_queues)])
|
||||
|
||||
# *****************
|
||||
# 2.4. finalizer
|
||||
|
||||
def add_finalizer(ctx:itertools.count, linear:UOp) -> UOp:
|
||||
# collect by device type
|
||||
parts:dict[str, list[UOp]] = collections.defaultdict(list)
|
||||
for call in linear.src:
|
||||
if (c:=unwrap_after(call)).src[0].op is not Ops.CUSTOM_FUNCTION or c.src[0].arg != "hcq": continue
|
||||
parts[c.arg.aux.device[0].split(':')[0]].append(unwrap_after(get_submit(call).src[0].src[0]))
|
||||
|
||||
nbump = next(ctx)
|
||||
finalizers = []
|
||||
for calls in parts.values():
|
||||
devs = tuple(dedup(d for call in calls for d in unwrap_after(call).arg.aux.device))
|
||||
zero = UOp.const(dtypes.int, 0)
|
||||
tl = make_signal_value(devs)
|
||||
|
||||
# split each (multi-device) call into per-device deps, then store the device timeline value into the device signal after them
|
||||
dep_lanes = [(call, dlane, devs.index(d)) for call in calls for dlane, d in enumerate(unwrap_after(call).arg.aux.device)]
|
||||
store = make_deps(make_signal(devs).store(tl.index(zero)), dep_lanes, len(devs))
|
||||
submit = make_submit(store, devs=devs, queue="COMPUTE:0")
|
||||
|
||||
upd = [(tl, 1)] + [(make_signal_value(devs, queue=qn), nbump) for qn in dedup([unwrap_after(call).arg.aux.queue for call in calls])]
|
||||
patches = [s.after(submit).index(zero, dtype=s.dtype).store(s.index(zero) + inc) for s, inc in upd]
|
||||
finalizers.append(UOp.custom_function("hcq", UOp.barrier(*patches).sink()).call(aux=HCQInfo("hcq finalizer", Estimates(), devs, "COMPUTE:0")))
|
||||
return linear.replace(src=linear.src + tuple(finalizers))
|
||||
pm_add_finalizer = PatternMatcher([(UPat(Ops.LINEAR, name="linear"), add_finalizer)])
|
||||
|
||||
# *****************
|
||||
# 2.5. global sync
|
||||
|
||||
def add_global_sync(ctx:set[tuple[str, ...]], submit:UOp, q:UOp) -> UOp|None:
|
||||
if (devs:=q.arg[0]) in ctx: return None
|
||||
ctx.add(devs)
|
||||
|
||||
# some devices from a command buffer might be used for the first time this schedule, so we wait for their global timeline epoch.
|
||||
wait = (make_signal(devs).index(zero:=UOp.const(dtypes.int, 0)).load() >= make_signal_value(devs).index(zero) - 1).wait()
|
||||
return submit.replace(src=(q.replace(src=(UOp(Ops.BARRIER, dtypes.void), wait, *q.src)),))
|
||||
pm_add_global_sync = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),), name="submit"), add_global_sync)])
|
||||
|
||||
# *****************
|
||||
# 3.1. lower loads/stores
|
||||
|
||||
def add_loads(ctx:set[int], submit:UOp, q:UOp) -> UOp|None:
|
||||
cur_devs = q.arg[0]
|
||||
new_src:list[UOp] = []
|
||||
for s in q.src:
|
||||
if s.op is Ops.AFTER:
|
||||
for lanes, dep in zip(s.arg, s.src[1:]):
|
||||
devs, queue = dep.arg.aux.device, dep.arg.aux.queue
|
||||
ctx.add(dep.tag) # mark op to update signal.
|
||||
|
||||
sig = make_mstack([make_signal(d if dl is None else devs[dl], queue=queue, sentinel=dl is None) for dl, d in zip(lanes, cur_devs)])
|
||||
val = make_mstack([make_signal_value(d if dl is None else devs[dl], queue=queue) for dl, d in zip(lanes, cur_devs)]).index(UOp.const(dtypes.int, 0))
|
||||
new_src.append((sig.index(UOp.const(dtypes.int, 0)).load() >= val + dep.tag).wait())
|
||||
s = s.src[0]
|
||||
new_src.append(s)
|
||||
return submit.replace(src=(q.replace(src=tuple(new_src)),))
|
||||
pm_add_inner_loads = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),), name="submit"), add_loads)])
|
||||
|
||||
def add_stores(ctx:set[int], submit:UOp, q:UOp) -> UOp|None:
|
||||
devs, queue = q.arg
|
||||
new_src:list[UOp] = []
|
||||
for op in q.src:
|
||||
new_src.append(op)
|
||||
if (sigval:=unwrap_after(op).tag) in ctx:
|
||||
new_src.append(make_signal(devs, queue=queue).store(make_signal_value(devs, queue=queue).index(UOp.const(dtypes.int, 0)) + sigval))
|
||||
return submit.replace(src=(q.replace(src=tuple(new_src)),))
|
||||
pm_add_inner_stores = PatternMatcher([(UPat(Ops.CUSTOM_FUNCTION, arg="submit_cmdbuf", src=(UPat(Ops.LINEAR, name="q"),), name="submit"), add_stores)])
|
||||
|
||||
# *****************
|
||||
# 4.1. hcq lowering: programs
|
||||
|
||||
@@ -279,7 +258,7 @@ def is_value_known_at_link(val:UOp) -> bool:
|
||||
addressed_bufs = [b for g in val.toposort() if g.op is Ops.GETADDR for b in unwrap_mstack(g.buf_uop)]
|
||||
|
||||
# addr of input params is not known at link time
|
||||
return not runtime_reads and all(b.op is not Ops.PARAM or b.tag is not None for b in addressed_bufs)
|
||||
return not val.variables() and not runtime_reads and all(b.op is not Ops.PARAM or b.tag is not None for b in addressed_bufs)
|
||||
|
||||
def is_link_patch(p:UOp, jit:bool) -> bool:
|
||||
store = p.src[0] if (is_binary_patch:=p.op is Ops.END) else p
|
||||
@@ -308,7 +287,7 @@ pm_split_patches = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION
|
||||
def make_addr_table(call:UOp, gaddrs:list[UOp], name:str) -> tuple[dict[UOp, UOp], tuple[UOp, ...]]:
|
||||
bare = {g: g.replace(src=(unwrap_after(g.src[0]),)) for g in gaddrs}
|
||||
|
||||
order = sorted(dedup(bare.values()), key=lambda g: ((b:=unwrap_mstack(g.buf_uop)[0]).arg.slot, to_tuple(b.tag)))
|
||||
order = sorted(dedup(bare.values()), key=lambda g: ((b:=unwrap_mstack(g.buf_uop)[0]).arg.slot, repr(b.tag)))
|
||||
slots, table = {g:i for i,g in enumerate(order)}, make_placeholder(call.arg.aux.device, len(order), dtypes.uint64, name)
|
||||
|
||||
reads = {g: table.after(*g.src[0].src[1:] if g.src[0].op is Ops.AFTER else ()).index(UOp.const(dtypes.int, slots[bare[g]])).load() for g in gaddrs}
|
||||
@@ -374,7 +353,7 @@ def pack_hcq_placeholders(call:UOp) -> UOp|None:
|
||||
sizes[b.tag] = offs[b] + b.max_numel()
|
||||
counts = collections.Counter(b.tag for b in bufs)
|
||||
bases = {b.tag:make_placeholder(b.device, sizes[b.tag], b.dtype, b.tag) for b in bufs if counts[b.tag] > 1}
|
||||
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.index, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
|
||||
subs = {b:UOp(Ops.SLICE, b.dtype, (bases[b.tag], UOp.const(dtypes.weakint, offs.get(b, 0))), b.max_numel()) for b in bufs if b.tag in bases}
|
||||
return call.replace(src=(call.src[0].substitute(subs, walk=True), *call.src[1:])) if subs else None
|
||||
pm_pack_placeholders = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION, arg="hcq"),), name="call", allow_any_len=True), pack_hcq_placeholders)])
|
||||
|
||||
@@ -391,19 +370,16 @@ def hcq_compile(linear:UOp, input_uops:list[UOp]|None=None, jit=False) -> UOp:
|
||||
if input_uops is not None: linear = graph_rewrite(linear, pm_replace_buffers, ctx=input_uops, walk=True, enter_calls=True, name="replace buffer")
|
||||
|
||||
if (final_linear:=(hcq_compile_cache.get(cache_key:=(linear.key, jit)))) is None:
|
||||
# schedule
|
||||
# prep
|
||||
linear = linear.substitute(back_map:={s.param_like(i): s for i,s in enumerate(input_uops)} if input_uops is not None else {}, walk=True)
|
||||
linear = graph_rewrite(linear, pm_insert_copy_staging + pm_flatten_linear, name="insert copy staging")
|
||||
linear = graph_rewrite(linear, pm_tag_hcq_calls, ctx=(enumerator:=itertools.count(0)), walk=True, name="tag hcq calls")
|
||||
linear = graph_rewrite(linear, pm_sched_sync, ctx=HCQDepsTracker(), walk=True, name="schedule sync")
|
||||
linear = linear.substitute({s: p for p, s in back_map.items()}, walk=True)
|
||||
|
||||
# schedule
|
||||
linear = graph_rewrite(linear, pm_sched_hcq_batches, walk=True, name="schedule hcq batches")
|
||||
linear = linear.substitute({s: p for p, s in back_map.items()}, walk=True, enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_merge_queues, walk=True, name="merge queues")
|
||||
linear = graph_rewrite(linear, pm_add_finalizer, ctx=enumerator, walk=True, name="add finalizer")
|
||||
linear = graph_rewrite(linear, pm_add_global_sync, ctx=set(), walk=True, name="add global sync", enter_calls=True)
|
||||
|
||||
# lowering to hcq ir
|
||||
linear = graph_rewrite(linear, pm_add_inner_loads, ctx=(waited:=set()), walk=True, name="add loads", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_add_inner_stores, ctx=waited, walk=True, name="add stores", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_encode_cmdbufs, walk=True, name="encode cmdbufs", enter_calls=True)
|
||||
linear = graph_rewrite(linear, pm_pack_placeholders, walk=True, name="pack placeholders")
|
||||
|
||||
@@ -508,7 +484,8 @@ class HCQ2Compiled(Compiled):
|
||||
self.rt_allocator = BumpAllocator(64 << 20, wrap=False)
|
||||
|
||||
def new_buffer(self, b:UOp, jit:bool) -> Buffer:
|
||||
if jit or b.tag in HCQ_CACHE_TAGS: return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(cpu_access=True, nolru=True))
|
||||
if jit or b.tag in HCQ_CACHE_TAGS:
|
||||
return Buffer(self.device, b.max_numel(), b.dtype, options=BufferSpec(uncached=True, cpu_access=True, nolru=True))
|
||||
return self.rt_buffer.view(b.max_numel(), b.dtype, self.rt_allocator.alloc(b.max_numel() * b.dtype.itemsize, alignment=128))
|
||||
|
||||
@functools.cache
|
||||
|
||||
@@ -43,7 +43,7 @@ def _fused_quantize_bwd_w13(gradient:UOp, kernel:UOp):
|
||||
device = xw13.device
|
||||
axis = xw13.axis if isinstance(device, tuple) else None
|
||||
grad_xw13_fp8 = alloc_like(xw13.shape, dtypes.fp8e4m3, device, axis)
|
||||
grad_amax_next = Tensor.zeros((), dtype=dtypes.float32, device=device).contiguous()
|
||||
grad_amax_next = Tensor(next_grad_amax_state, device=device)
|
||||
grad_amax_state_t = Tensor(grad_amax_state, device=device)
|
||||
fxn = functools.partial(_custom_fused_bwd_w13, dname=dname_of(device))
|
||||
grad_amax = grad_amax_state_t.empty_like()
|
||||
@@ -52,16 +52,14 @@ def _fused_quantize_bwd_w13(gradient:UOp, kernel:UOp):
|
||||
Tensor(xw13, device=device), Tensor(gradient, device=device).cast(dtypes.bfloat16),
|
||||
Tensor(amax_state, device=device), grad_amax_state_t, fxn=fxn)
|
||||
grad_xw13_uop = grad_xw13_fp8.uop.cast(dtypes.bfloat16)
|
||||
store_effect = next_grad_amax_state.store(grad_amax_next.uop)
|
||||
assert grad_xw13_fp8.uop.op is Ops.AFTER, f"expected AFTER, got {grad_xw13_fp8.uop.op}"
|
||||
grad_xw13_fp8_uop = grad_xw13_fp8.uop.replace(src=grad_xw13_fp8.uop.src + (store_effect,))
|
||||
# Stash fp8 companion for cdna_asm_gemm's bwd to attach to grad_a.
|
||||
_grad_fp8_mailbox[grad_xw13_uop] = (grad_xw13_fp8_uop, grad_amax_state_t.uop)
|
||||
_grad_fp8_mailbox[grad_xw13_uop] = (grad_xw13_fp8.uop, grad_amax_state_t.uop)
|
||||
return (None, None, grad_xw13_uop, None, None, None)
|
||||
|
||||
def fused_quantize_fp8_w13(xw13:Tensor, amax_state:Tensor, fp8_dtype, grad_amax_state:Tensor,
|
||||
next_grad_amax_state:Tensor) -> tuple[Tensor, Tensor]:
|
||||
# NOTE: silu(xw1)*xw3 -> fp8 + amax over fused xw13 layout. Returns (fp8, new_amax)
|
||||
next_grad_amax_state:Tensor, amax_out:Tensor) -> Tensor:
|
||||
# NOTE: silu(xw1)*xw3 -> fp8 + amax over fused xw13 layout. Returns fp8.
|
||||
# grad_amax_state: delayed amax for grad_xw13 fp8 quantization in the backward.
|
||||
assert xw13.dtype == dtypes.bfloat16, f"expected bf16, got {xw13.dtype}"
|
||||
MBS, SEQ, H2 = xw13.shape
|
||||
@@ -69,8 +67,7 @@ def fused_quantize_fp8_w13(xw13:Tensor, amax_state:Tensor, fp8_dtype, grad_amax_
|
||||
HIDDEN = H2 // 2
|
||||
axis = xw13.uop.axis if isinstance(xw13.device, tuple) else None
|
||||
fp8_out = alloc_like((MBS, SEQ, HIDDEN), fp8_dtype, xw13.device, axis)
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=xw13.device).contiguous()
|
||||
fxn = functools.partial(_custom_fused_cast_amax_w13, dname=dname_of(xw13.device))
|
||||
fp8_out, amax_out, *_ = Tensor.custom_kernel(fp8_out, amax_out, xw13, amax_state, grad_amax_state, next_grad_amax_state,
|
||||
fxn=fxn, grad_fxn=_fused_quantize_bwd_w13)
|
||||
return fp8_out, amax_out
|
||||
return fp8_out
|
||||
|
||||
@@ -16,7 +16,7 @@ def _custom_fused_ce_loss_fwd(loss_out:UOp, max_out:UOp, lse_out:UOp, logits:UOp
|
||||
row_lse = (logits[b, s, v_lse].cast(dtypes.float) - row_max).exp().reduce(v_lse, arg=Ops.ADD).log() + row_max
|
||||
|
||||
v_smooth = UOp.range(vocab, 3, axis_type=AxisType.REDUCE)
|
||||
target = logits[b, s, targets[row].cast(dtypes.index)].cast(dtypes.float)
|
||||
target = logits[b, s, targets[row].cast(dtypes.weakint)].cast(dtypes.float)
|
||||
mean_logits = logits[b, s, v_smooth].cast(dtypes.float).reduce(v_smooth, arg=Ops.ADD) / vocab
|
||||
loss = row_lse - (1.0 - label_smoothing) * target - label_smoothing * mean_logits
|
||||
stores = UOp.group(loss_out[row].store(loss), max_out[row].store(row_max), lse_out[row].store(row_lse))
|
||||
@@ -32,7 +32,7 @@ def _custom_fused_ce_loss_bwd(d_logits:UOp, logits:UOp, lse:UOp, targets:UOp, sc
|
||||
s = row % seq
|
||||
|
||||
prob = (logits[b, s, v].cast(dtypes.float) - lse[row]).exp()
|
||||
target = v.eq(targets[row].cast(dtypes.index)).where(1.0 - label_smoothing, 0.0)
|
||||
target = v.eq(targets[row].cast(dtypes.weakint)).where(1.0 - label_smoothing, 0.0)
|
||||
smooth = label_smoothing / vocab
|
||||
grad = (prob - target - smooth) * scale[0]
|
||||
|
||||
|
||||
@@ -112,8 +112,9 @@ def _fused_add_bwd(*args, **kwargs):
|
||||
grad_h, grad_w = _bwd_common(fp8_grad_u, h_grad_u, x_u, x_normed_u, rrms_u, weight_u, amax_state_u, kernel)
|
||||
return (None, None, None, None, None, grad_h, grad_h, grad_w, None)
|
||||
|
||||
def fused_rmsnorm_mul_quantize_fp8(x:Tensor, weight:Tensor, amax_state:Tensor, eps:float, fp8_dtype) -> tuple[Tensor, Tensor, Tensor, Tensor]:
|
||||
# NOTE: rmsnorm(x) * weight -> fp8 + amax. Returns (fp8, new_amax, x_normed, rrms).
|
||||
def fused_rmsnorm_mul_quantize_fp8(x:Tensor, weight:Tensor, amax_state:Tensor, eps:float, fp8_dtype,
|
||||
amax_out:Tensor) -> tuple[Tensor, Tensor, Tensor]:
|
||||
# NOTE: rmsnorm(x) * weight -> fp8 + amax. Returns (fp8, x_normed, rrms).
|
||||
# x_normed + rrms are saved for the rmsnorm backward (also recomputed here from x regs).
|
||||
assert x.dtype == dtypes.bfloat16 and weight.dtype == dtypes.bfloat16
|
||||
assert x.shape[-1] == weight.shape[-1], f"HIDDEN mismatch: x={x.shape}, weight={weight.shape}"
|
||||
@@ -123,16 +124,15 @@ def fused_rmsnorm_mul_quantize_fp8(x:Tensor, weight:Tensor, amax_state:Tensor, e
|
||||
fp8_out = alloc_like((MBS, SEQ, HIDDEN), fp8_dtype, x.device, axis)
|
||||
x_normed_out = alloc_like((MBS, SEQ, HIDDEN), dtypes.bfloat16, x.device, axis)
|
||||
rrms_out = alloc_like((MBS, SEQ), dtypes.float32, x.device, axis)
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=x.device).contiguous()
|
||||
fxn = functools.partial(_custom_fwd, dname=dname_of(x.device), eps_val=eps)
|
||||
fp8_out, x_normed_out, rrms_out, amax_out, *_ = Tensor.custom_kernel(
|
||||
fp8_out, x_normed_out, rrms_out, amax_out, x, weight, amax_state, fxn=fxn, grad_fxn=_fused_bwd)
|
||||
return fp8_out, amax_out, x_normed_out, rrms_out
|
||||
return fp8_out, x_normed_out, rrms_out
|
||||
|
||||
def fused_add_rmsnorm_mul_quantize_fp8(x:Tensor, residual:Tensor, weight:Tensor, amax_state:Tensor,
|
||||
eps:float, fp8_dtype) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
|
||||
eps:float, fp8_dtype, amax_out:Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor]:
|
||||
# NOTE: h = x + residual; y_normed = rmsnorm(h); fp8 = quantize(y_normed * weight).
|
||||
# Returns (fp8, new_amax, h, x_normed, rrms). h is also written so downstream can
|
||||
# Returns (fp8, h, x_normed, rrms). h is also written so downstream can
|
||||
# reuse it without recomputing x+residual — eliminates the separate residual-add kernel.
|
||||
assert x.dtype == dtypes.bfloat16 and residual.dtype == dtypes.bfloat16 and weight.dtype == dtypes.bfloat16
|
||||
assert x.shape == residual.shape
|
||||
@@ -143,9 +143,8 @@ def fused_add_rmsnorm_mul_quantize_fp8(x:Tensor, residual:Tensor, weight:Tensor,
|
||||
h_out = alloc_like((MBS, SEQ, HIDDEN), dtypes.bfloat16, x.device, axis)
|
||||
x_normed_out = alloc_like((MBS, SEQ, HIDDEN), dtypes.bfloat16, x.device, axis)
|
||||
rrms_out = alloc_like((MBS, SEQ), dtypes.float32, x.device, axis)
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=x.device).contiguous()
|
||||
fxn = functools.partial(_custom_fwd_add, dname=dname_of(x.device), eps_val=eps)
|
||||
fp8_out, h_out, x_normed_out, rrms_out, amax_out, *_ = Tensor.custom_kernel(
|
||||
fp8_out, h_out, x_normed_out, rrms_out, amax_out, x, residual, weight, amax_state,
|
||||
fxn=fxn, grad_fxn=_fused_add_bwd)
|
||||
return fp8_out, amax_out, h_out, x_normed_out, rrms_out
|
||||
return fp8_out, h_out, x_normed_out, rrms_out
|
||||
|
||||
@@ -48,7 +48,7 @@ def _custom_quantize_fp8_with_amax(fp8_out:UOp, amax_out:UOp, x:UOp, amax_state:
|
||||
device = device[0].split(":")[0] if isinstance(device, tuple) else device.split(":")[0]
|
||||
if device in {"AMD", "NULL"}: atomic_arg = "if ({2} > {3}) __hip_atomic_fetch_max((int*){0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);"
|
||||
else: raise NotImplementedError(f"no atomic max for device {device}")
|
||||
amax_idx = amax_out.reshape((1,)).index(UOp.const(dtypes.index, 0))
|
||||
amax_idx = amax_out.reshape((1,)).index(UOp.const(dtypes.weakint, 0))
|
||||
max_val = lds[0].load()
|
||||
atomic = UOp(Ops.CUSTOM, dtypes.void, (amax_idx, max_val.bitcast(dtypes.int32), max_val, amax_idx.load()), arg=atomic_arg)
|
||||
return atomic.end(tid, wg).sink(arg=KernelInfo(f"quantize_fp8_with_amax_{n_elems}", opts_to_apply=()))
|
||||
@@ -73,24 +73,19 @@ def _quantize_fp8_delayed_bwd(gradient:UOp, kernel:UOp):
|
||||
grad_x = (Tensor(gradient, device=device).float() * scale).cast(dtypes.bfloat16)
|
||||
return (None, None, grad_x.uop, None)
|
||||
|
||||
def quantize_fp8_delayed(x:Tensor, amax_state:Tensor, fp8_dtype=dtypes.fp8e4m3) -> tuple[Tensor, Tensor, Tensor, UOp]:
|
||||
# NOTE: one-pass bf16 -> fp8 quantize with delayed scaling. Returns (fp8, inv_scale, new_amax, store_effect).
|
||||
def quantize_fp8_delayed(x:Tensor, amax_state:Tensor, amax_out:Tensor, fp8_dtype=dtypes.fp8e4m3) -> tuple[Tensor, Tensor]:
|
||||
# NOTE: one-pass bf16 -> fp8 quantize with delayed scaling.
|
||||
# Fused kernel reads x once and writes fp8 + scalar amax via global atomic max.
|
||||
# store_effect writes new_amax into amax_state's buffer — the caller must thread it into a realized
|
||||
# output via `.after(store_effect)`. Calling `amax_state.assign(new_amax)` inside a grad_fxn does
|
||||
# NOT work because .assign mutates only the temp Tensor's .uop, not the original layer-owned buffer.
|
||||
assert x.dtype == dtypes.bfloat16, f"expected bf16, got {x.dtype}"
|
||||
axis = x.uop.axis if isinstance(x.device, tuple) else None
|
||||
fp8_out = alloc_like(x.shape, fp8_dtype, x.device, axis)
|
||||
n_elems = prod(x.uop.shard_shape)
|
||||
assert n_elems % NUM_WG == 0, f"{n_elems=} must divide over {NUM_WG=}"
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=x.device).contiguous()
|
||||
fxn = functools.partial(_custom_quantize_fp8_with_amax, device=x.device)
|
||||
fp8_out, amax_out, *_ = Tensor.custom_kernel(fp8_out, amax_out, x, amax_state,
|
||||
fxn=fxn, grad_fxn=_quantize_fp8_delayed_bwd)
|
||||
inv_scale = (amax_state.float() + 1e-8) / FP8_MAX
|
||||
store_effect = amax_state.uop.store(amax_out.uop)
|
||||
return fp8_out, inv_scale, amax_out, store_effect
|
||||
return fp8_out, inv_scale
|
||||
|
||||
def quantize_fp8_scalar(x:Tensor, amax_state:Tensor, fp8_dtype=dtypes.fp8e4m3) -> Tensor:
|
||||
# NOTE: pure one-pass bf16 -> fp8 quantize with delayed scalar scale. No amax computation.
|
||||
|
||||
@@ -94,7 +94,7 @@ def fused_qkv_rope(xqkv:Tensor, freqs_cis:Tensor, n_heads:int, n_kv_heads:int, h
|
||||
B_local = B // num_devices if is_dp else B
|
||||
H_local = n_heads // num_devices if is_mp else n_heads
|
||||
H_KV_local = n_kv_heads // num_devices if is_mp else n_kv_heads
|
||||
assert (B_local, N, H_local, H_KV_local, head_dim) == (2, 8192, 32, 8, 128)
|
||||
assert H_local % H_KV_local == 0 and head_dim % 2 == 0 and head_dim <= 512
|
||||
single_device = xqkv.device[0] if isinstance(xqkv.device, tuple) else xqkv.device
|
||||
arch = Device[single_device].renderer.target.arch
|
||||
axis = 0 if is_dp else 2 if is_mp else None
|
||||
|
||||
@@ -565,8 +565,10 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
|
||||
"aten.floor_divide": lambda x,y: x//y,
|
||||
"aten.floor_divide_.Tensor": lambda x,y: x//y,
|
||||
"aten.__lshift__.Scalar": lambda x,y: x<<y,
|
||||
"aten.__lshift__.Tensor": lambda x,y: x<<y,
|
||||
"aten.__ilshift__.Scalar": lambda x,y: x<<y,
|
||||
"aten.__rshift__.Scalar": lambda x,y: x>>y,
|
||||
"aten.__rshift__.Tensor": lambda x,y: x>>y,
|
||||
"aten.__irshift__.Scalar": lambda x,y: x>>y,
|
||||
# inplace ops using replace for fusion
|
||||
"aten.zero_": lambda x: x.const_like(0),
|
||||
|
||||
@@ -161,7 +161,6 @@ norecursedirs = [
|
||||
".git",
|
||||
]
|
||||
timeout = 300
|
||||
timeout_method = "thread"
|
||||
timeout_func_only = true
|
||||
testpaths = ["test"]
|
||||
filterwarnings = [
|
||||
|
||||
@@ -36,7 +36,7 @@ def custom_add_var(A:UOp, B:UOp) -> UOp:
|
||||
A,B = A.flatten(), B.flatten()
|
||||
assert A.dtype == dtypes.uint32, f"buffer dtype must be uint32, got {A.dtype}"
|
||||
threads = UOp.special(A.numel(), "lidx0")
|
||||
var = UOp.param(2, dtypes.index, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
|
||||
var = UOp.param(2, dtypes.weakint, vmin_vmax=(0, 10), name="var", addrspace=AddrSpace.ALU)
|
||||
insts = [
|
||||
s_load_b128(s[4:7], s[0:1]),
|
||||
s_load_b32(s[8], s[0:1], offset=0x10), # all threads load the same variable
|
||||
|
||||
@@ -291,6 +291,33 @@ class TestDTypeALU(unittest.TestCase):
|
||||
@Context(EMULATED_DTYPES="long")
|
||||
def test_emulated_int64(self, a, b, op): universal_test(a, b, dtypes.int64, op)
|
||||
|
||||
def _test_shl(self):
|
||||
for dtype, values, distances in ((dtypes.int64, [-0x1234, 0x80000001, -1, 0x1234, 1], [0, 5, 31, 32, 62]),
|
||||
(dtypes.uint64, [0x80000001, 0x80000001, 1, 0xFEDC, 1], [0, 5, 31, 32, 62]),
|
||||
(dtypes.int8, [-3, 1, 7, -2, 1], [0, 1, 3, 5, 6]),
|
||||
(dtypes.uint16, [3, 1, 0xFF, 7, 1], [0, 1, 7, 12, 15])):
|
||||
with self.subTest(dtype=dtype):
|
||||
result = Tensor(values, dtype=dtype) << Tensor(distances, dtype=dtype)
|
||||
np.testing.assert_equal(result.numpy(), [x << d for x, d in zip(values, distances)])
|
||||
|
||||
def _test_shr(self):
|
||||
for dtype, values, distances in ((dtypes.int64, [-(2**40), -1, -(2**50), -(2**40), 0x123456789ABCDEF], [0, 5, 31, 32, 63]),
|
||||
(dtypes.uint64, [0xFEDCBA9876543210] * 5, [0, 5, 31, 32, 63]),
|
||||
(dtypes.int8, [-128, -1, 64, -37, 1], [0, 1, 3, 5, 7]),
|
||||
(dtypes.uint16, [0xFFFF] * 5, [0, 1, 8, 13, 15])):
|
||||
with self.subTest(dtype=dtype):
|
||||
result = Tensor(values, dtype=dtype) >> Tensor(distances, dtype=dtype)
|
||||
np.testing.assert_equal(result.numpy(), [x >> d for x, d in zip(values, distances)])
|
||||
|
||||
def test_shl(self): self._test_shl()
|
||||
def test_shr(self): self._test_shr()
|
||||
|
||||
@Context(EMULATED_DTYPES="long")
|
||||
def test_emulated_shl(self): self._test_shl()
|
||||
|
||||
@Context(EMULATED_DTYPES="long")
|
||||
def test_emulated_shr(self): self._test_shr()
|
||||
|
||||
@given(ht.uint8, strat.sampled_from(integer_unary_operations))
|
||||
def test_uint8_unary(self, a, op): universal_test_unary(a, dtypes.uint8, op)
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@ class TestLinearizerFailure(unittest.TestCase):
|
||||
@unittest.skipUnless(Device.DEFAULT == "METAL", "only tested on METAL")
|
||||
def test_failure_beam_mnist(self):
|
||||
c0 = UOp.param(0, dtypes.uchar, (4014080,))
|
||||
c1 = UOp.range(UOp.const(dtypes.index, 512), 0, AxisType.GLOBAL)
|
||||
c2 = UOp.range(UOp.const(dtypes.index, 784), 1, AxisType.GLOBAL)
|
||||
c3 = UOp.range(UOp.const(dtypes.index, 10), 3, AxisType.GLOBAL)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 0, AxisType.GLOBAL)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 784), 1, AxisType.GLOBAL)
|
||||
c3 = UOp.range(UOp.const(dtypes.weakint, 10), 3, AxisType.GLOBAL)
|
||||
c4 = UOp.param(1, dtypes.int, (512,))
|
||||
c5 = c4.index(c1.valid(UOp.const(dtypes.bool, True)))
|
||||
c6 = UOp.range(UOp.const(dtypes.index, 6000), 1004, AxisType.REDUCE)
|
||||
c7 = UOp.range(UOp.const(dtypes.index, 3750), 2006, AxisType.REDUCE)
|
||||
c8 = UOp.range(UOp.const(dtypes.index, 16), 2007, AxisType.GROUP_REDUCE)
|
||||
c6 = UOp.range(UOp.const(dtypes.weakint, 6000), 1004, AxisType.REDUCE)
|
||||
c7 = UOp.range(UOp.const(dtypes.weakint, 3750), 2006, AxisType.REDUCE)
|
||||
c8 = UOp.range(UOp.const(dtypes.weakint, 16), 2007, AxisType.GROUP_REDUCE)
|
||||
c9 = UOp.param(2, dtypes.uchar, (47040000,))
|
||||
c10 = c9.index((((c3*UOp.const(dtypes.index, 4704000))+c2)+(c6*UOp.const(dtypes.index, 784))).valid(UOp.const(dtypes.bool, True)))
|
||||
c11 = c5.alu(Ops.CMPNE, ((((c3*UOp.const(dtypes.index, 6000))+c6)+((c7*UOp.const(dtypes.index, 16))+c8)).alu(Ops.CMPLT, UOp.const(dtypes.index, 59999)).where(UOp.const(dtypes.int, 0), UOp.const(dtypes.int, 1)).reduce(c7, c8, arg=Ops.ADD)+UOp.const(dtypes.int, -1))).where(UOp.const(dtypes.uchar, 0), c10).reduce(c6, arg=Ops.ADD)
|
||||
c12 = c0.index((((c1*UOp.const(dtypes.index, 7840))+(c2*UOp.const(dtypes.index, 10)))+c3).valid(UOp.const(dtypes.bool, True))).store(c11).end(c1, c2, c3)
|
||||
c10 = c9.index((((c3*UOp.const(dtypes.weakint, 4704000))+c2)+(c6*UOp.const(dtypes.weakint, 784))).valid(UOp.const(dtypes.bool, True)))
|
||||
c11 = c5.alu(Ops.CMPNE, ((((c3*UOp.const(dtypes.weakint, 6000))+c6)+((c7*UOp.const(dtypes.weakint, 16))+c8)).alu(Ops.CMPLT, UOp.const(dtypes.weakint, 59999)).where(UOp.const(dtypes.int, 0), UOp.const(dtypes.int, 1)).reduce(c7, c8, arg=Ops.ADD)+UOp.const(dtypes.int, -1))).where(UOp.const(dtypes.uchar, 0), c10).reduce(c6, arg=Ops.ADD)
|
||||
c12 = c0.index((((c1*UOp.const(dtypes.weakint, 7840))+(c2*UOp.const(dtypes.weakint, 10)))+c3).valid(UOp.const(dtypes.bool, True))).store(c11).end(c1, c2, c3)
|
||||
ast = c12.sink(arg=KernelInfo(name='test', axis_types=(), dont_use_locals=False, applied_opts=(Opt(op=OptOps.GROUP, axis=1, arg=16),), opts_to_apply=None))
|
||||
_ = to_program(ast, Device["METAL"].renderer)
|
||||
|
||||
|
||||
@@ -49,9 +49,10 @@ def run_quantize_fp8(shape:tuple[int, ...], delayed:bool=True) -> None:
|
||||
with Context(DEBUG=0): Tensor.realize(x, amax_state)
|
||||
|
||||
if delayed:
|
||||
fp8, inv_scale, new_amax, _ = quantize_fp8_delayed(x, amax_state, FP8_DTYPE)
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=x.device).realize()
|
||||
fp8, inv_scale = quantize_fp8_delayed(x, amax_state, amax_out, FP8_DTYPE)
|
||||
ref_fp8, ref_inv_scale, ref_new_amax = quantize_fp8(x, amax_state=amax_state)
|
||||
Tensor.realize(fp8, inv_scale, new_amax)
|
||||
Tensor.realize(fp8, inv_scale)
|
||||
Tensor.realize(ref_fp8, ref_inv_scale, ref_new_amax)
|
||||
else:
|
||||
fp8 = quantize_fp8_scalar(x, amax_state, FP8_DTYPE)
|
||||
@@ -63,8 +64,8 @@ def run_quantize_fp8(shape:tuple[int, ...], delayed:bool=True) -> None:
|
||||
assert fp8.cast(dtypes.float).allclose(ref_fp8.cast(dtypes.float), atol=0, rtol=0).item(), "fp8 mismatch"
|
||||
if delayed:
|
||||
assert inv_scale.allclose(ref_inv_scale, atol=0, rtol=0).item(), "inv_scale mismatch"
|
||||
assert new_amax.allclose(ref_new_amax, atol=0, rtol=0).item(), \
|
||||
f"amax mismatch: got={new_amax.item()} ref={ref_new_amax.item()} diff={abs(new_amax.item()-ref_new_amax.item())}"
|
||||
assert amax_out.allclose(ref_new_amax, atol=0, rtol=0).item(), \
|
||||
f"amax mismatch: got={amax_out.item()} ref={ref_new_amax.item()} diff={abs(amax_out.item()-ref_new_amax.item())}"
|
||||
|
||||
@unittest.skipUnless(Device.DEFAULT == "AMD", "requires atomic max")
|
||||
class TestQuantizeFP8(unittest.TestCase):
|
||||
@@ -82,10 +83,11 @@ class TestQuantizeFP8(unittest.TestCase):
|
||||
x = Tensor.empty(2048*8, 1024, dtype=dtypes.bfloat16, device=devs).uop.multi(0)
|
||||
x = Tensor(x, device=devs)
|
||||
amax_state = Tensor.full((), 2.0, dtype=dtypes.float32, device=devs).contiguous()
|
||||
fp8, _, new_amax, _ = quantize_fp8_delayed(x, amax_state, FP8_DTYPE)
|
||||
Tensor.realize(fp8, new_amax)
|
||||
amax_out = Tensor.zeros((), dtype=dtypes.float32, device=devs).realize()
|
||||
fp8, _ = quantize_fp8_delayed(x, amax_state, amax_out, FP8_DTYPE)
|
||||
Tensor.realize(fp8)
|
||||
assert fp8.uop.shape == x.uop.shape
|
||||
assert new_amax.shape == ()
|
||||
assert amax_out.shape == ()
|
||||
|
||||
class TestLocalAmax(unittest.TestCase):
|
||||
def test_multi_tensor_local_shard_amax(self):
|
||||
|
||||
@@ -848,6 +848,8 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor << 0, lambda: (ten << 0).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor << 2, lambda: (ten << 2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor << 31, lambda: (ten << 31).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor << torch.tensor([0,2,4]).int(),
|
||||
lambda: (ten << Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.__lshift__(2), lambda: ten.__lshift__(2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.bitwise_left_shift(2), lambda: ten.lshift(2).cast(dtypes.int32), forward_only=True)
|
||||
|
||||
@@ -859,6 +861,8 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor >> 0, lambda: (ten >> 0).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor >> 2, lambda: (ten >> 2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor >> 31, lambda: (ten >> 31).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor >> torch.tensor([0,2,4]).int(),
|
||||
lambda: (ten >> Tensor([0,2,4], dtype=dtypes.uint32)).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.__rshift__(2), lambda: ten.__rshift__(2).cast(dtypes.int32), forward_only=True)
|
||||
helper_test_op([], lambda: tor.bitwise_right_shift(2), lambda: ten.rshift(2).cast(dtypes.int32), forward_only=True)
|
||||
|
||||
@@ -870,6 +874,7 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor << 2, lambda: ten << 2, forward_only=True)
|
||||
helper_test_op([], lambda: tor << 8, lambda: ten << 8, forward_only=True)
|
||||
helper_test_op([], lambda: tor << 31, lambda: ten << 31, forward_only=True)
|
||||
helper_test_op([], lambda: tor << torch.tensor([0,2,8,31]).int(), lambda: ten << Tensor([0,2,8,31], dtype=dtypes.int), forward_only=True)
|
||||
|
||||
def test_rshift_signed(self):
|
||||
data = [[-1, -3, 1, 7], [0, -2147483648, 2147483647, -1]]
|
||||
@@ -879,6 +884,7 @@ class TestOps(unittest.TestCase):
|
||||
helper_test_op([], lambda: tor >> 2, lambda: ten >> 2, forward_only=True)
|
||||
helper_test_op([], lambda: tor >> 8, lambda: ten >> 8, forward_only=True)
|
||||
helper_test_op([], lambda: tor >> 31, lambda: ten >> 31, forward_only=True)
|
||||
helper_test_op([], lambda: tor >> torch.tensor([0,2,8,31]).int(), lambda: ten >> Tensor([0,2,8,31], dtype=dtypes.int), forward_only=True)
|
||||
|
||||
def test_idiv_shift_rewrite_negative(self):
|
||||
a = Tensor(-5).div(2, rounding_mode="trunc").item()
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ def vision_conv_143():
|
||||
c32 = ((c27<3)!=True)&(c27<67)
|
||||
c34 = UOp.param(1, dtypes.half, shape=(32, 1024, 4))
|
||||
c38 = c5//2
|
||||
c45 = (c32&c24).where((c27*64+c38+c17*4096+-12480), UOp.const(dtypes.index, Invalid))
|
||||
c45 = (c32&c24).where((c27*64+c38+c17*4096+-12480), UOp.const(dtypes.weakint, Invalid))
|
||||
c48 = (c24&c32).where(c34.index(c45), UOp.const(dtypes.float, 0.0))
|
||||
c49 = UOp.param(2, dtypes.half, shape=(64, 49, 4))
|
||||
c61 = c48*c49.index((c26*4+c5%2+c16*28+c38*196))
|
||||
@@ -50,7 +50,7 @@ def vision_conv_153():
|
||||
c32 = ((c27<3)!=True)&(c27<35)
|
||||
c34 = UOp.param(1, dtypes.half, shape=(16, 1024, 4))
|
||||
c38 = c5//2
|
||||
c45 = (c32&c24).where((c27*128+c38+c17*4096+-12672), UOp.const(dtypes.index, Invalid))
|
||||
c45 = (c32&c24).where((c27*128+c38+c17*4096+-12672), UOp.const(dtypes.weakint, Invalid))
|
||||
c48 = (c24&c32).where(c34.index(c45), UOp.const(dtypes.float, 0.0))
|
||||
c49 = UOp.param(2, dtypes.half, shape=(128, 49, 4))
|
||||
c61 = c48*c49.index((c26*4+c5%2+c16*28+c38*196))
|
||||
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
"""End-to-end regression tests for tinygrad's OpenCode integration.
|
||||
|
||||
Run against an existing server:
|
||||
RUN_LLM_OPENCODE_REGRESSION=1 LLM_BASE_URL=http://127.0.0.1:9000/v1 \
|
||||
python -m pytest test/external/external_test_llm_opencode.py -v
|
||||
|
||||
Or let the test start the server:
|
||||
RUN_LLM_OPENCODE_REGRESSION=1 \
|
||||
LLM_GGUF=/raid/models/Qwen3.6-35B-A3B-UD-IQ4_XS.gguf \
|
||||
python -m pytest test/external/external_test_llm_opencode.py -v
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json, os, pathlib, re, shutil, socket, subprocess, sys, tempfile, time, unittest, urllib.request
|
||||
|
||||
|
||||
RUN_REGRESSION = os.getenv("RUN_LLM_OPENCODE_REGRESSION") == "1"
|
||||
DEFAULT_GGUF = "/raid/models/Qwen3.6-35B-A3B-UD-IQ4_XS.gguf"
|
||||
|
||||
SORT_C = r"""#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define N 1000000
|
||||
|
||||
/* Optimized LSD Radix sort using 8-bit chunks (256 buckets) with loop unrolling and prefetching */
|
||||
static void radix_sort(int *a, int *buf, int n) {
|
||||
const int BITS = 8;
|
||||
const int MASK = (1 << BITS) - 1;
|
||||
const int BUCKETS = (1 << BITS);
|
||||
uint32_t count[BUCKETS];
|
||||
uint32_t *u = (uint32_t *)a;
|
||||
uint32_t *ubuf = (uint32_t *)buf;
|
||||
uint32_t *src = u;
|
||||
uint32_t *dst = ubuf;
|
||||
|
||||
/* Convert signed to unsigned by flipping sign bit */
|
||||
for (int i = 0; i < n; i++)
|
||||
u[i] ^= (uint32_t)1U << 31;
|
||||
|
||||
for (int shift = 0; shift < 32; shift += BITS) {
|
||||
memset(count, 0, sizeof(count));
|
||||
|
||||
/* Counting pass - unrolled by 8 with prefetching */
|
||||
int i = 0;
|
||||
for (; i + 7 < n; i += 8) {
|
||||
count[(src[i] >> shift) & MASK]++;
|
||||
count[(src[i+1] >> shift) & MASK]++;
|
||||
count[(src[i+2] >> shift) & MASK]++;
|
||||
count[(src[i+3] >> shift) & MASK]++;
|
||||
count[(src[i+4] >> shift) & MASK]++;
|
||||
count[(src[i+5] >> shift) & MASK]++;
|
||||
count[(src[i+6] >> shift) & MASK]++;
|
||||
count[(src[i+7] >> shift) & MASK]++;
|
||||
}
|
||||
for (; i < n; i++)
|
||||
count[(src[i] >> shift) & MASK]++;
|
||||
|
||||
/* Prefix sums in-place */
|
||||
uint32_t total = 0;
|
||||
for (int i = 0; i < BUCKETS; i++) {
|
||||
uint32_t c = count[i];
|
||||
count[i] = total;
|
||||
total += c;
|
||||
}
|
||||
|
||||
/* Distribution pass - unrolled by 8 with prefetching */
|
||||
i = 0;
|
||||
for (; i + 7 < n; i += 8) {
|
||||
dst[count[(src[i] >> shift) & MASK]++] = src[i];
|
||||
dst[count[(src[i+1] >> shift) & MASK]++] = src[i+1];
|
||||
dst[count[(src[i+2] >> shift) & MASK]++] = src[i+2];
|
||||
dst[count[(src[i+3] >> shift) & MASK]++] = src[i+3];
|
||||
dst[count[(src[i+4] >> shift) & MASK]++] = src[i+4];
|
||||
dst[count[(src[i+5] >> shift) & MASK]++] = src[i+5];
|
||||
dst[count[(src[i+6] >> shift) & MASK]++] = src[i+6];
|
||||
dst[count[(src[i+7] >> shift) & MASK]++] = src[i+7];
|
||||
}
|
||||
for (; i < n; i++)
|
||||
dst[count[(src[i] >> shift) & MASK]++] = src[i];
|
||||
|
||||
/* Swap src/dst pointers */
|
||||
uint32_t *tmp = src;
|
||||
src = dst;
|
||||
dst = tmp;
|
||||
}
|
||||
|
||||
/* Copy back if needed */
|
||||
if (src != u)
|
||||
memcpy(u, src, n * sizeof(uint32_t));
|
||||
|
||||
/* Convert back to signed */
|
||||
for (int i = 0; i < n; i++)
|
||||
u[i] ^= (uint32_t)1U << 31;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int *arr = malloc(N * sizeof(int));
|
||||
int *buf = malloc(N * sizeof(int));
|
||||
if (!arr || !buf) { perror("malloc"); return 1; }
|
||||
|
||||
srand(42);
|
||||
for (int i = 0; i < N; i++)
|
||||
arr[i] = rand() | (rand() << 15);
|
||||
|
||||
clock_t start = clock();
|
||||
radix_sort(arr, buf, N);
|
||||
clock_t end = clock();
|
||||
|
||||
double elapsed = (double)(end - start) / CLOCKS_PER_SEC * 1000;
|
||||
printf("Sorted %d integers in %.3f ms\n", N, elapsed);
|
||||
|
||||
for (int i = 1; i < N; i++) {
|
||||
if (arr[i] < arr[i - 1]) {
|
||||
printf("ERROR: not sorted at index %d\n", i);
|
||||
free(arr);
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
printf("Verification passed.\n");
|
||||
|
||||
free(arr);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def free_port() -> int:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return sock.getsockname()[1]
|
||||
|
||||
|
||||
def server_ready(base_url:str) -> bool:
|
||||
try:
|
||||
with urllib.request.urlopen(base_url.rstrip("/") + "/models", timeout=1) as response:
|
||||
return response.status == 200
|
||||
except (OSError, urllib.error.URLError):
|
||||
return False
|
||||
|
||||
|
||||
@unittest.skipUnless(RUN_REGRESSION, "set RUN_LLM_OPENCODE_REGRESSION=1 to run the real-model OpenCode regression")
|
||||
class TestLLMOpenCode(unittest.TestCase):
|
||||
server: subprocess.Popen|None = None
|
||||
server_log: tempfile._TemporaryFileWrapper|None = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if shutil.which("opencode") is None: raise unittest.SkipTest("opencode is not installed")
|
||||
if (base_url := os.getenv("LLM_BASE_URL")) is not None:
|
||||
cls.base_url = base_url.rstrip("/")
|
||||
if not cls.base_url.endswith("/v1"): cls.base_url += "/v1"
|
||||
if not server_ready(cls.base_url): raise RuntimeError(f"LLM server is not responding at {cls.base_url}")
|
||||
return
|
||||
|
||||
model = pathlib.Path(os.getenv("LLM_GGUF", DEFAULT_GGUF))
|
||||
if not model.is_file(): raise unittest.SkipTest(f"model not found: {model}")
|
||||
port = free_port()
|
||||
cls.base_url = f"http://127.0.0.1:{port}/v1"
|
||||
cls.server_log = tempfile.NamedTemporaryFile(mode="w+", prefix="tinygrad-llm-")
|
||||
cls.server = subprocess.Popen(
|
||||
[sys.executable, "-m", "tinygrad.llm", "--model", str(model), "--serve", str(port), "--max_context", "65536"],
|
||||
stdout=cls.server_log, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline and cls.server.poll() is None:
|
||||
if server_ready(cls.base_url): return
|
||||
time.sleep(0.25)
|
||||
cls.server_log.seek(0)
|
||||
raise RuntimeError(f"LLM server failed to start:\n{cls.server_log.read()[-8000:]}")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.server is not None:
|
||||
cls.server.terminate()
|
||||
try: cls.server.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
cls.server.kill()
|
||||
cls.server.wait(timeout=10)
|
||||
if cls.server_log is not None: cls.server_log.close()
|
||||
|
||||
def setUp(self):
|
||||
# The model and its KV/recurrent caches are stateful. Keep xdist workers from interleaving independent OpenCode
|
||||
# conversations, which changes cache reuse and can hide precisely the incremental path this suite exercises.
|
||||
import fcntl
|
||||
self._fcntl = fcntl
|
||||
self._server_lock = open("/tmp/tinygrad-llm-opencode-regression.lock", "w")
|
||||
self._fcntl.flock(self._server_lock, self._fcntl.LOCK_EX)
|
||||
|
||||
def tearDown(self):
|
||||
self._fcntl.flock(self._server_lock, self._fcntl.LOCK_UN)
|
||||
self._server_lock.close()
|
||||
|
||||
def run_opencode(self, prompt:str, cwd:pathlib.Path, timeout:int=120) -> str:
|
||||
config = cwd / "opencode.json"
|
||||
config.write_text(json.dumps({
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {"*": "allow"},
|
||||
"formatter": False,
|
||||
"lsp": False,
|
||||
"provider": {"regression": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {"baseURL": self.base_url},
|
||||
"models": {"tinygrad": {"name": "tinygrad"}},
|
||||
}},
|
||||
}))
|
||||
env = os.environ.copy()
|
||||
env["OPENCODE_CONFIG"] = str(config)
|
||||
result = subprocess.run(["opencode", "run", "--pure", "--auto", "--dir", str(cwd), "-m", "regression/tinygrad", prompt], cwd=cwd,
|
||||
env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout)
|
||||
self.assertEqual(result.returncode, 0, result.stdout)
|
||||
return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", result.stdout)
|
||||
|
||||
def chat(self, messages:list[dict], max_tokens:int=4) -> dict:
|
||||
request = urllib.request.Request(self.base_url + "/chat/completions", data=json.dumps({
|
||||
"model":"tinygrad", "messages":messages, "max_tokens":max_tokens, "temperature":0,
|
||||
}).encode(), headers={"Content-Type":"application/json"})
|
||||
with urllib.request.urlopen(request, timeout=120) as response: return json.load(response)
|
||||
|
||||
def test_same_session_reuses_prompt_cache(self):
|
||||
# Use a long stable prefix so reuse remains observable after the model rounds cache positions for its serving JIT.
|
||||
messages = [{"role":"system", "content":"You are a concise assistant. " * 400}, {"role":"user", "content":"Reply OK."}]
|
||||
first = self.chat(messages)
|
||||
messages += [{"role":"assistant", "content":first["choices"][0]["message"].get("content") or ""},
|
||||
{"role":"user", "content":"Reply OK again."}]
|
||||
second = self.chat(messages)
|
||||
self.assertGreater(second["usage"]["prompt_tokens_details"]["cached_tokens"], 0)
|
||||
|
||||
def test_reads_and_correctly_explains_valid_c(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
source = cwd / "sort.c"
|
||||
source.write_text(SORT_C)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Werror", "-fsyntax-only", str(source)], check=True)
|
||||
|
||||
output = self.run_opencode("sort.c?", cwd)
|
||||
self.assertRegex(output, r"(?im)^\s*(?:→|>)\s*Read\s+sort\.c\s*$", "OpenCode did not execute the read tool")
|
||||
self.assertRegex(output, r"(?i)radix sort")
|
||||
self.assertNotRegex(output, r"(?i)(corrupt|mangled|not valid C|invalid C|syntax error|does not compile|won't compile)")
|
||||
self.assertEqual(source.read_text(), SORT_C)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Werror", "-fsyntax-only", str(source)], check=True)
|
||||
|
||||
def test_read_tool_preserves_exact_contents(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
marker = "tinygrad-read-regression-7f3a91c2"
|
||||
source = cwd / "exact.txt"
|
||||
source.write_text(marker + "\n")
|
||||
|
||||
output = self.run_opencode("Read exact.txt with a tool and reply with its exact contents, with no other text.", cwd)
|
||||
self.assertRegex(output, r"(?im)^\s*(?:→|>)\s*Read\s+exact\.txt\s*$", "OpenCode did not execute the read tool")
|
||||
self.assertIn(marker, output)
|
||||
self.assertEqual(source.read_text(), marker + "\n")
|
||||
|
||||
def test_executes_shell_tool(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
marker = cwd / "shell-regression.txt"
|
||||
output = self.run_opencode(
|
||||
"Use the shell tool to run `printf tinygrad-shell-regression > shell-regression.txt`, then report completion.", cwd)
|
||||
self.assertRegex(output, r"(?im)^\s*(?:\$|→|>)\s*.*printf\s+tinygrad-shell-regression",
|
||||
"OpenCode did not execute the shell tool")
|
||||
self.assertTrue(marker.is_file(), output)
|
||||
self.assertEqual(marker.read_text(), "tinygrad-shell-regression")
|
||||
|
||||
def test_does_not_repeat_identical_failed_shell_call(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
command = "clang -x c /dev/null -fsyntax-only -mllvm -tinygrad-definitely-invalid-option=1"
|
||||
output = self.run_opencode(
|
||||
f"Run `{command}` exactly once with the shell tool. After it fails, do not retry it; explain that the option is unsupported.", cwd)
|
||||
self.assertIn("Unknown command line argument", output)
|
||||
self.assertLessEqual(output.count(command), 1, output)
|
||||
|
||||
def test_stops_when_benchmark_goal_is_met(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
benchmark = cwd / "benchmark.sh"
|
||||
benchmark.write_text("#!/bin/sh\necho 'Sorted 1000000 integers in 8.300 ms'\n")
|
||||
benchmark.chmod(0o755)
|
||||
output = self.run_opencode(
|
||||
"Use the shell tool to run ./benchmark.sh. Keep optimizing until it reports under 10 ms, then stop immediately.", cwd)
|
||||
self.assertIn("8.300 ms", output)
|
||||
self.assertLessEqual(output.count("$ ./benchmark.sh"), 1, output)
|
||||
|
||||
def test_multiline_tool_argument_preserves_trailing_newline(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cwd = pathlib.Path(directory)
|
||||
target = cwd / "numbers.txt"
|
||||
target.write_text("replace me\n")
|
||||
output = self.run_opencode(
|
||||
"Read numbers.txt, then use the write tool to replace it with the numbers 1 through 300, one number per line. Do not use bash.", cwd)
|
||||
self.assertRegex(output, r"(?im)^\s*(?:←|→|>)\s*Write\s+numbers\.txt\s*$", "OpenCode did not execute the write tool")
|
||||
self.assertEqual(target.read_text(), "".join(f"{i}\n" for i in range(1, 301)))
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
Vendored
+3
-3
@@ -40,7 +40,7 @@ def random_int_expr(depth=10):
|
||||
def random_bool_expr(depth=10, expr1=None):
|
||||
if depth == 0: return True
|
||||
if expr1 is None: expr1 = random_int_expr(depth-1)
|
||||
expr2 = random.choice([random_or_sub_expression_int(depth-1, expr1), UOp.const(dtypes.index, random.randint(-10, 10))])
|
||||
expr2 = random.choice([random_or_sub_expression_int(depth-1, expr1), UOp.const(dtypes.weakint, random.randint(-10, 10))])
|
||||
return random.choice(comp_ops)(expr1, expr2)
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ if __name__ == "__main__":
|
||||
f"v2=Variable(\"{u2.arg[0]}\", {u2.arg[1]}, {u2.arg[2]})\n" +\
|
||||
f"v3=Variable(\"{u3.arg[0]}\", {u3.arg[1]}, {u3.arg[2]})\n" +\
|
||||
f"expr = {expr}\n" +\
|
||||
f"v1_val, v2_val, v3_val = UOp.const(dtypes.index, {n1.as_long()}), UOp.const(dtypes.index, {n2.as_long()})," +\
|
||||
f"UOp.const(dtypes.index, {n3.as_long()})\n" +\
|
||||
f"v1_val, v2_val, v3_val = UOp.const(dtypes.weakint, {n1.as_long()}), UOp.const(dtypes.weakint, {n2.as_long()})," +\
|
||||
f"UOp.const(dtypes.weakint, {n3.as_long()})\n" +\
|
||||
"num = expr.simplify().substitute({v1:v1_val, v2:v2_val, v3:v3_val}).ssimplify()\n" +\
|
||||
"rn = expr.substitute({v1:v1_val, v2:v2_val, v3:v3_val}).ssimplify()\n" +\
|
||||
"assert num==rn, f\"{num} != {rn}\"\n"
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ def assert_jit_cache_len(fxn, expected_len):
|
||||
if linear is None or not linear.src:
|
||||
assert expected_len == 0, expected_len
|
||||
return
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 2 # HCQ2 merges calls on the same queue
|
||||
if expected_len and all(call_is_hcq(call) for call in linear.src): expected_len = 3 # HCQ2: merged same-queue calls + finalizer + bumps
|
||||
if call_is_graph(linear.src[0]):
|
||||
assert len(linear.src) == 1, len(linear.src)
|
||||
inner = linear.src[0].src[0].src[0] # LINEAR UOp inside CUSTOM_FUNCTION
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ctypes, time, os, builtins, fcntl
|
||||
import ctypes, time, os, builtins, fcntl, typing
|
||||
from tinygrad.helpers import DEV
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface
|
||||
from tinygrad.runtime.autogen import libc
|
||||
@@ -9,7 +9,7 @@ start = time.perf_counter()
|
||||
|
||||
drivers = [cls() for t in DEV.value if (cls:={"MOCKPCI+AMD": AMDriver, "MOCKKFD+AMD": AMDDriver, "MOCK+AMD": AMDDriver, "MOCKUSB+AMD": AMUSBDriver,
|
||||
"MOCK+NV": NVDriver}.get(f"{t.interface}+{t.device}"))]
|
||||
tracked_fds = {}
|
||||
tracked_fds: dict[int, typing.Any] = {}
|
||||
|
||||
original_memoryview = builtins.memoryview
|
||||
class TrackedMemoryView:
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestWeakConstFolding(unittest.TestCase):
|
||||
self.assertEqual((out.op, out.dtype, out.arg), (Ops.CONST, dtypes.weakint, 2**41))
|
||||
|
||||
def test_float_unaries(self):
|
||||
for dtype in (dtypes.weakint, dtypes.weakfloat):
|
||||
for dtype in (dtypes.weakfloat,):
|
||||
for op in (Ops.SIN, Ops.LOG2, Ops.EXP2, Ops.SQRT, Ops.RECIPROCAL):
|
||||
out = UOp.const(dtype, 4).alu(op).simplify()
|
||||
self.assertEqual((out.op, out.dtype), (Ops.CONST, dtypes.weakfloat))
|
||||
|
||||
@@ -224,30 +224,16 @@ class TestTypePromotion(unittest.TestCase):
|
||||
assert least_upper_dtype(dtypes.fp8e5m2, dtypes.uint64) == dtypes.fp8e5m2
|
||||
|
||||
def test_weakint_promo(self):
|
||||
# weakint with itself is weakint
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.weakint) == dtypes.weakint
|
||||
# weakint is above bool
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.bool) == dtypes.weakint
|
||||
# weakint defers to any concrete int type
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.int8) == dtypes.int8
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.uint8) == dtypes.uint8
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.int16) == dtypes.int16
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.int32) == dtypes.int32
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.int64) == dtypes.int64
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.uint64) == dtypes.uint64
|
||||
# weakint defers to any float type
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.float16) == dtypes.float16
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.float32) == dtypes.float32
|
||||
assert least_upper_dtype(dtypes.weakint, dtypes.float64) == dtypes.float64
|
||||
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.weakint)
|
||||
with self.assertRaises(KeyError): least_upper_dtype(dtypes.weakint, dtypes.int8)
|
||||
|
||||
def test_weakfloat_promo(self):
|
||||
# weakfloat is a float, but like weakint it is not one of dtypes.floats
|
||||
# weakfloat is a float, but is not one of dtypes.floats
|
||||
assert dtypes.is_float(dtypes.weakfloat) and dtypes.weakfloat not in dtypes.floats
|
||||
# weakfloat with itself is weakfloat
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.weakfloat) == dtypes.weakfloat
|
||||
# weakfloat is above bool, weakint and any concrete int (they defer up to it)
|
||||
# weakfloat is above bool and any concrete int (they defer up to it)
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.bool) == dtypes.weakfloat
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.weakint) == dtypes.weakfloat
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.int32) == dtypes.weakfloat
|
||||
assert least_upper_dtype(dtypes.weakfloat, dtypes.uint64) == dtypes.weakfloat
|
||||
# weakfloat defers to any concrete float type
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestGroupedDims(unittest.TestCase):
|
||||
total = math.prod(dims)
|
||||
specials = sorted(dedup(flatten([[y for y in x.toposort() if y.op is Ops.SPECIAL] for x in idxs])), key=lambda u: u.arg)
|
||||
# build flat index and primed flat (same expression with renamed SPECIALs)
|
||||
flat = UOp.const(dtypes.index, 0)
|
||||
flat = UOp.const(dtypes.weakint, 0)
|
||||
for i, idx in enumerate(idxs):
|
||||
flat = flat + idx * int(math.prod(dims[i+1:]))
|
||||
flat_p = flat.substitute({s: UOp(Ops.SPECIAL, src=s.src, arg=s.arg+"_p") for s in specials})
|
||||
|
||||
@@ -107,21 +107,21 @@ class TestFoldingAndReduction(unittest.TestCase):
|
||||
class TestModuloAndDivisionFolding(unittest.TestCase):
|
||||
def test_full_graph_rewrite_modulo_folding_with_define_var(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
x_var_uop = UOp.variable('x', 0, 100).cast(dtypes.index)
|
||||
x_var_uop = UOp.variable('x', 0, 100).cast(dtypes.weakint)
|
||||
optimized_mod_uop = apply_rewrite(((x_var_uop * 4) + 2) % 4)
|
||||
self.assertEqual(optimized_mod_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_mod_uop.arg, 2)
|
||||
|
||||
def test_full_graph_rewrite_division_folding_with_define_var(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
n_var_uop = UOp.variable('n', 1, 1000).cast(dtypes.index)
|
||||
n_var_uop = UOp.variable('n', 1, 1000).cast(dtypes.weakint)
|
||||
optimized_div_uop = apply_rewrite((n_var_uop * 6) // 3)
|
||||
self.assertEqual(optimized_div_uop.op, Ops.MUL)
|
||||
self.assertEqual(optimized_div_uop.src[1].arg, 2)
|
||||
|
||||
def test_full_graph_rewrite_complex_mod_div_folding(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
k_var_uop = UOp.variable('k', 0, 50).cast(dtypes.index)
|
||||
k_var_uop = UOp.variable('k', 0, 50).cast(dtypes.weakint)
|
||||
optimized_div_uop = apply_rewrite(((k_var_uop * 12 + 8) % 6) // 2)
|
||||
self.assertEqual(optimized_div_uop.op, Ops.CONST)
|
||||
self.assertEqual(optimized_div_uop.arg, 1)
|
||||
@@ -140,7 +140,7 @@ class TestModuloAndDivisionFolding(unittest.TestCase):
|
||||
def test_full_graph_rewrite_modulo_large_divisor(self):
|
||||
# index dtype because div-mod rules only work on index
|
||||
x_var_uop = UOp.variable('x', 1, 5)
|
||||
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.index) % 10).render(simplify=False), x_var_uop.render(simplify=False))
|
||||
self.assertIs(apply_rewrite(x_var_uop.cast(dtypes.weakint) % 10).render(simplify=False), x_var_uop.render(simplify=False))
|
||||
|
||||
def test_full_graph_rewrite_division_with_remainder(self):
|
||||
x_var_uop = UOp.variable('x', 7, 9)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ctypes, gzip, unittest, timeit, pickle
|
||||
from tinygrad import Variable
|
||||
from tinygrad.helpers import Context, ContextVar, argfix, colored, word_wrap, is_numpy_ndarray, mv_address, count, all_same
|
||||
from tinygrad.helpers import Context, ContextVar, argfix, colored, word_wrap, mv_address, count, all_same
|
||||
from tinygrad.tensor import is_numpy_ndarray
|
||||
from tinygrad.helpers import merge_dicts, strip_parens, prod, round_up, fetch, fully_flatten, from_mv, to_mv, polyN, time_to_str, cdiv, cmod, getbits
|
||||
from tinygrad.helpers import ceildiv, ansistrip, get_shape
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
@@ -8,12 +8,12 @@ from tinygrad.codegen import to_program
|
||||
class TestLinearizerFailures(unittest.TestCase):
|
||||
def test_fail_1(self):
|
||||
c0 = UOp.param(0, dtypes.float, (64,))
|
||||
c1 = UOp.range(UOp.const(dtypes.index, 2), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.index, 32), 2, AxisType.LOOP)
|
||||
c3 = ((c1*UOp.const(dtypes.index, 32))+c2)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 2), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 32), 2, AxisType.LOOP)
|
||||
c3 = ((c1*UOp.const(dtypes.weakint, 32))+c2)
|
||||
c4 = UOp.param(1, dtypes.float, (163840,))
|
||||
c5 = UOp.range(UOp.const(dtypes.index, 2560), 0, AxisType.REDUCE)
|
||||
c6 = c4.index(((((((c5//UOp.const(dtypes.index, 8))%UOp.const(dtypes.index, 8))*UOp.const(dtypes.index, 8))+(c5%UOp.const(dtypes.index, 8)))+(((c2*UOp.const(dtypes.index, 40))+(c5//UOp.const(dtypes.index, 64)))*UOp.const(dtypes.index, 64)))+(c1*UOp.const(dtypes.index, 81920))))
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 2560), 0, AxisType.REDUCE)
|
||||
c6 = c4.index(((((((c5//UOp.const(dtypes.weakint, 8))%UOp.const(dtypes.weakint, 8))*UOp.const(dtypes.weakint, 8))+(c5%UOp.const(dtypes.weakint, 8)))+(((c2*UOp.const(dtypes.weakint, 40))+(c5//UOp.const(dtypes.weakint, 64)))*UOp.const(dtypes.weakint, 64)))+(c1*UOp.const(dtypes.weakint, 81920))))
|
||||
c7 = UOp.param(2, dtypes.float, (64,))
|
||||
c8 = c7.index(c3)
|
||||
c9 = ((((c6+(c8*UOp.const(dtypes.float, -1.0)))*(c6+(c8*UOp.const(dtypes.float, -1.0)))).reduce(c5, arg=Ops.ADD)*UOp.const(dtypes.float, 0.000390625))+UOp.const(dtypes.float, 1e-05)).sqrt().reciprocal()
|
||||
|
||||
@@ -184,9 +184,11 @@ class TestLLMToolCalls(unittest.TestCase):
|
||||
cls.mock_model.max_context = 4
|
||||
cls.mock_model.get_start_pos = Mock(return_value=0)
|
||||
|
||||
from tinygrad.llm.cli import FallbackTemplate
|
||||
from tinygrad.llm.serve import LLMServer
|
||||
template = FallbackTemplate(cls.mock_tok)
|
||||
import jinja2
|
||||
# .items() matches tool-aware templates and ensures OpenAI JSON argument strings are normalized before rendering the next turn.
|
||||
template = jinja2.Template("""{% for m in messages %}{{ m.content or '' }}{% for tc in m.tool_calls or [] %}
|
||||
{% for key, value in tc.function.arguments.items() %}{{ key }}={{ value }}{% endfor %}{% endfor %}{% endfor %}""")
|
||||
cls.server = LLMServer(('127.0.0.1', 0), cls.mock_model, "tool-model", cls.mock_tok, template)
|
||||
cls.port = cls.server.server_address[1]
|
||||
cls.server_thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
|
||||
@@ -237,13 +239,6 @@ class TestLLMToolCalls(unittest.TestCase):
|
||||
args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
|
||||
self.assertEqual(args, {"content":"first\nsecond\n", "filePath":"out.txt"})
|
||||
|
||||
def test_prefilled_reasoning_round_trip(self):
|
||||
self.set_output("reasoning\n</think>\n\nanswer")
|
||||
response = self.client.chat.completions.create(model="tool-model", messages=[{"role":"user", "content":"Think"}],
|
||||
extra_body={"enable_thinking":True})
|
||||
self.assertEqual(response.choices[0].message.reasoning_content, "reasoning\n")
|
||||
self.assertEqual(response.choices[0].message.content, "\n\nanswer")
|
||||
|
||||
def test_invalid_tool_call_becomes_content(self):
|
||||
self.set_output("<tool_call>not a call</tool_call>")
|
||||
response = self.client.chat.completions.create(model="tool-model", messages=[{"role":"user", "content":"Hello"}], tools=self.tools())
|
||||
@@ -271,69 +266,5 @@ class TestLLMToolCalls(unittest.TestCase):
|
||||
self.assertEqual(second.choices[0].message.content, "done")
|
||||
self.assertEqual(second.choices[0].finish_reason, "stop")
|
||||
|
||||
def test_tool_turn_remains_a_reusable_prefix_after_next_user_message(self):
|
||||
class Tokenizer:
|
||||
eos_id, eot_id = 0x110000, None
|
||||
def encode(self, text): return [ord(c) for c in text]
|
||||
def stream_decoder(self): return lambda tid=None: "" if tid is None else chr(tid)
|
||||
def is_end(self, token_id): return token_id == self.eos_id
|
||||
class Template:
|
||||
def render(self, messages, tools=None, add_generation_prompt=True, enable_thinking=False, preserve_thinking=False):
|
||||
out = ""
|
||||
for m in messages:
|
||||
content = (m.get("content") or "").strip()
|
||||
if m["role"] == "assistant":
|
||||
reasoning = (m.get("reasoning_content") or "").strip()
|
||||
out += f"<assistant><think>\n{reasoning}\n</think>\n\n{content}"
|
||||
for tc in m.get("tool_calls") or []:
|
||||
fn = tc["function"]
|
||||
out += ("\n\n" if content else "") + f"<tool_call>\n<function={fn['name']}>\n"
|
||||
for name,value in fn["arguments"].items(): out += f"<parameter={name}>\n{value}\n</parameter>\n"
|
||||
out += "</function>\n</tool_call>"
|
||||
else: out += f"<{m['role']}>{content}"
|
||||
out += "</turn>"
|
||||
if add_generation_prompt: out += "<assistant><think>\n" if enable_thinking else "<assistant><think>\n\n</think>\n\n"
|
||||
return out
|
||||
class Model:
|
||||
max_context = 10000
|
||||
def __init__(self):
|
||||
self.cached = []
|
||||
self.outputs = iter(("reasoning\n</think>\n\nChecking now.\n\n<tool_call>\n<function=read>\n"
|
||||
"<parameter=path>\nsort.c\n</parameter>\n</function>\n</tool_call>",
|
||||
"finished reasoning\n</think>\n\nfinished."))
|
||||
def get_start_pos(self, ids):
|
||||
return next((i for i,(a,b) in enumerate(zip(ids, self.cached)) if a != b), min(len(ids), len(self.cached)))
|
||||
def generate(self, ids, **kwargs):
|
||||
output = [ord(c) for c in next(self.outputs)]
|
||||
self.cached = ids + output
|
||||
yield from output
|
||||
yield Tokenizer.eos_id
|
||||
|
||||
from tinygrad.llm.serve import LLMServer
|
||||
model = Model()
|
||||
server = LLMServer(('127.0.0.1', 0), model, "prefix-model", Tokenizer(), Template())
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
from openai import OpenAI
|
||||
client = OpenAI(base_url=f"http://127.0.0.1:{server.server_address[1]}/v1", api_key="test")
|
||||
try:
|
||||
messages = [{"role":"user", "content":"Read sort.c"}]
|
||||
first = client.chat.completions.create(model="prefix-model", messages=messages, tools=self.tools(),
|
||||
extra_body={"enable_thinking":True})
|
||||
self.assertEqual(first.choices[0].message.reasoning_content, "reasoning\n")
|
||||
self.assertEqual(first.choices[0].message.content, "\n\nChecking now.\n\n")
|
||||
cached_len = len(model.cached)
|
||||
call = first.choices[0].message.tool_calls[0]
|
||||
messages += [{"role":"assistant", "content":first.choices[0].message.content,
|
||||
"reasoning_content":first.choices[0].message.reasoning_content, "tool_calls":[call.model_dump()]},
|
||||
{"role":"tool", "tool_call_id":call.id, "content":"file contents"}]
|
||||
second = client.chat.completions.create(model="prefix-model", messages=messages, tools=self.tools(),
|
||||
extra_body={"enable_thinking":True})
|
||||
self.assertEqual(second.choices[0].message.content, "\n\nfinished.")
|
||||
self.assertEqual(second.usage.prompt_tokens_details.cached_tokens, cached_len)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import unittest, base64, functools, sys
|
||||
import unittest, base64, functools, re, sys, time, unicodedata
|
||||
from tinygrad.llm.cli import SimpleTokenizer, FallbackTemplate
|
||||
from tinygrad.helpers import fetch
|
||||
|
||||
@@ -46,6 +46,29 @@ class TestLLMTokenizer(unittest.TestCase):
|
||||
def test_llama_repeat(self): self._test_coding(self.llama_tok, "00000000000000000", [ 931, 931, 931, 931, 931, 410 ])
|
||||
def test_llama_pat(self): self._test_coding(self.llama_tok, "today\n \n", [ 31213, 14211 ])
|
||||
|
||||
def test_split_regex_matches_naive_listing(self):
|
||||
# the compacted codepoint ranges must match the same text as listing every codepoint
|
||||
def naive(pre): return "".join(re.escape(chr(cp)) for cp in range(0x323b0) if unicodedata.category(chr(cp)).startswith(pre))
|
||||
r_ws, r_p_N, r_p_L = r"\t\n\x0b\x0c\r\x85" + naive("Z"), naive("N"), naive("L")
|
||||
naive_re = re.compile("(?i:'s|'t|'re|'ve|'m|'ll|'d)|" +
|
||||
f"[^\\r\\n{r_p_N}{r_p_L}]?[{r_p_L}]+|[{r_p_N}]{{1,3}}| ?[^{r_ws}{r_p_N}{r_p_L}]+[\\r\\n]*|[{r_ws}]*[\\r\\n]+|[{r_ws}]+(?![^{r_ws}])|[{r_ws}]+")
|
||||
sample = "hello world 한국어 中文 текст ١٢٣ 123 😊\n \ttoday\n'équivalent ²³№ "
|
||||
self.assertEqual(SimpleTokenizer({}, {})._split_to_word.findall(sample), naive_re.findall(sample))
|
||||
|
||||
def test_split_regex_speed(self):
|
||||
# the naive listing compiles a 429KB pattern that takes 10+s to match a 225KB prompt; ranges keep it small and fast
|
||||
tok = SimpleTokenizer({}, {})
|
||||
self.assertLess(len(tok._split_to_word.pattern), 100_000)
|
||||
text = "The quick brown fox jumps over the lazy dog. " * 5000
|
||||
tok._split_to_word.findall(text) # warmup
|
||||
tms = []
|
||||
for _ in range(5):
|
||||
st = time.perf_counter()
|
||||
words = tok._split_to_word.findall(text)
|
||||
tms.append(time.perf_counter() - st)
|
||||
self.assertLess(min(tms), 4) # best-of-5 is robust to CI scheduling pauses; new code takes ~60ms
|
||||
self.assertEqual(len(words), 50001)
|
||||
|
||||
def test_llama_continued_conversation(self):
|
||||
self._test_coding(self.llama_tok, "hello <|eot_id|>world", [15339, 220, 128009, 14957])
|
||||
self._test_coding(self.llama_tok, "hello <|eot_id|>world again", [15339, 220, 128009, 14957, 1578])
|
||||
|
||||
@@ -1472,6 +1472,18 @@ class TestSchedule(unittest.TestCase):
|
||||
x.softmax().sum().backward()
|
||||
run_linear(*check_schedule(x.grad, 4))
|
||||
|
||||
def test_logsumexp_backward(self):
|
||||
Tensor.manual_seed(0)
|
||||
x = Tensor.randn(4, 12, 64, 64).realize()
|
||||
x.logsumexp(-1).sum().backward()
|
||||
run_linear(*check_schedule(x.grad, 3))
|
||||
|
||||
def test_logcumsumexp_backward(self):
|
||||
Tensor.manual_seed(0)
|
||||
x = Tensor.randn(4, 512).realize()
|
||||
x.logcumsumexp(-1).sum().backward()
|
||||
run_linear(*check_schedule(x.grad, 3))
|
||||
|
||||
def test_scaled_dot_product_attention_fusion(self):
|
||||
x, y, z, m = (Tensor.empty(32, 8, 16, 16) for _ in range(4))
|
||||
out = Tensor.scaled_dot_product_attention(x, y, z, attn_mask=m)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest, itertools
|
||||
|
||||
from tinygrad.codegen.late.coalese import indexing_simplify
|
||||
from tinygrad.codegen.late.coalesce import indexing_simplify
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops, graph_rewrite
|
||||
from tinygrad.uop.symbolic import simplify_valid, sym, pm_move_where_on_load
|
||||
@@ -23,7 +23,7 @@ def get_load_image_uop(image_shape:tuple[int, ...], valid:UOp, idx:tuple[UOp, UO
|
||||
UOp.param(0, dtypes.float, image_shape).index(idx[1].valid(valid), idx[0].valid(valid)),
|
||||
))
|
||||
|
||||
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(dtypes.index, nmax),), arg=expr)
|
||||
def Special(expr, nmax): return UOp(Ops.SPECIAL, src=(UOp.const(dtypes.weakint, nmax),), arg=expr)
|
||||
def Variable(expr, nmin, nmax): return UOp.variable(expr, nmin, nmax)
|
||||
def Range(n, nmax): return UOp.range(nmax, n)
|
||||
|
||||
@@ -455,7 +455,7 @@ class TestImageSimplification(unittest.TestCase):
|
||||
A1 = lidx0*32 + r0*32 + lidx1*4 - 99
|
||||
valid = ((lidx1 < 1).ne(True)) & ((lidx0 + r0) < 3).ne(True) & ((lidx0 + r0) < 19)
|
||||
alu0 = gidx0 + (A1 % 32)*32 + (A1 // 32 % 16)*1024
|
||||
load = get_load_image_uop((1, 16384, 4), valid, (alu0, UOp.const(dtypes.index, 0)))
|
||||
load = get_load_image_uop((1, 16384, 4), valid, (alu0, UOp.const(dtypes.weakint, 0)))
|
||||
try:
|
||||
self.check(load, None, "(gidx0+lidx0*1024+r0*1024+lidx1*128+-3168)", "0")
|
||||
except AssertionError:
|
||||
@@ -474,7 +474,7 @@ class TestImageSimplification(unittest.TestCase):
|
||||
A1 = lidx0*16 + r0*16 + lidx1*4 - 51
|
||||
valid = ((lidx1 < 1).ne(True)) & ((lidx0 + r0) < 3).ne(True) & ((lidx0 + r0) < 11)
|
||||
alu0 = lidx2 + gidx0*4 + (A1 % 16)*64 + (A1 // 16 % 8)*1024
|
||||
load = get_load_image_uop((1, 8192, 4), valid, (alu0, UOp.const(dtypes.index, 0)))
|
||||
load = get_load_image_uop((1, 8192, 4), valid, (alu0, UOp.const(dtypes.weakint, 0)))
|
||||
try:
|
||||
self.check(load, None, "(lidx2+gidx0*4+lidx0*1024+r0*1024+lidx1*256+-3264)", "0")
|
||||
except AssertionError:
|
||||
@@ -488,18 +488,18 @@ class TestImageSimplification(unittest.TestCase):
|
||||
gidx0 = Special("gidx0", 1064)
|
||||
r12 = Range(12, 3)
|
||||
valid = ((gidx0 < 645).ne(True)) & (gidx0 < 653)
|
||||
idx = (r12*4 + (gidx0+3)%4 + (gidx0+3)//4*24 - 3888, UOp.const(dtypes.index, 0))
|
||||
idx = (r12*4 + (gidx0+3)%4 + (gidx0+3)//4*24 - 3888, UOp.const(dtypes.weakint, 0))
|
||||
load = get_load_image_uop((1, 48, 4), valid, idx)
|
||||
self.check(load, None, "(r12*4+(gidx0+3)%4+(gidx0+3)//4*24+-3888)", "0")
|
||||
|
||||
class TestDropTrueGate(unittest.TestCase):
|
||||
def test_drop_true_gate_on_index(self):
|
||||
# test that INDEX with a constant True valid gets simplified to drop the valid
|
||||
from tinygrad.codegen.late.coalese import indexing_simplify
|
||||
from tinygrad.codegen.late.coalesce import indexing_simplify
|
||||
from tinygrad.uop.ops import graph_rewrite
|
||||
from tinygrad.uop.symbolic import sym
|
||||
buf = UOp.param(0, dtypes.int, (1,))
|
||||
idx = UOp.const(dtypes.index, 0)
|
||||
idx = UOp.const(dtypes.weakint, 0)
|
||||
true_gate = UOp.const(dtypes.bool, True)
|
||||
index_with_gate = UOp(Ops.INDEX, src=(buf, idx.valid(true_gate)))
|
||||
# apply the optimization
|
||||
@@ -516,7 +516,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_shrink_single_guard(self):
|
||||
# range 0..203 guarded by r < 4 everywhere -> shrink to 0..3
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 4)
|
||||
@@ -524,8 +524,8 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_shrink_picks_max_guard(self):
|
||||
# two loads guard the same range with r < 4 and r < 8 -> shrink to max(4, 8) = 8
|
||||
r = Range(0, 204)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
load2 = get_gated_load_uop(r < UOp.const(dtypes.index, 8), r)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
|
||||
load2 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 8), r)
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 8)
|
||||
@@ -533,7 +533,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_no_shrink_guard_ge_max(self):
|
||||
# guard r < 300 with range max 204 -> no shrink (guard doesn't constrain)
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 300), r)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 300), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
self.assertEqual(ranges[0].src[0].arg, 204)
|
||||
@@ -541,7 +541,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_no_shrink_when_unguarded_elsewhere(self):
|
||||
# one load guards r < 4, but another load uses r without a gate -> no shrink
|
||||
r = Range(0, 204)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
load1 = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
|
||||
load2 = UOp(Ops.LOAD, src=(UOp.param(1, dtypes.float, (204,)).index(r),))
|
||||
ranges = self.get_ranges(UOp.sink(load1, load2))
|
||||
self.assertEqual(len(ranges), 1)
|
||||
@@ -550,7 +550,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_no_shrink_when_used_in_reduce(self):
|
||||
# range used in both a gated load AND directly in the reduce expression -> no shrink
|
||||
r = Range(0, 204)
|
||||
gated_load = get_gated_load_uop(r < UOp.const(dtypes.index, 4), r)
|
||||
gated_load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 4), r)
|
||||
red = (r.cast(dtypes.float) + gated_load).reduce(r, arg=Ops.ADD)
|
||||
ranges = self.get_ranges(red.sink())
|
||||
self.assertEqual(len(ranges), 1)
|
||||
@@ -559,7 +559,7 @@ class TestRangeShrink(unittest.TestCase):
|
||||
def test_range_shrink_to_single_iteration(self):
|
||||
# guard r < 1 shrinks range to 1 -> single iteration, range eliminated entirely
|
||||
r = Range(0, 204)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.index, 1), r)
|
||||
load = get_gated_load_uop(r < UOp.const(dtypes.weakint, 1), r)
|
||||
ranges = self.get_ranges(load.sink())
|
||||
self.assertEqual(len(ranges), 0)
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ class TestTensorUOpStack(unittest.TestCase):
|
||||
self.assertIs(_t(2, 3).uop.stack(w.uop).dtype, dtypes.float32)
|
||||
def test_stack_index_dtype(self):
|
||||
# index is outside the promotion lattice, equal dtypes bypass promotion
|
||||
self.assertEqual(UOp.const(dtypes.index, 1).stack(UOp.const(dtypes.index, 2)).shape, (2,))
|
||||
self.assertEqual(UOp.const(dtypes.weakint, 1).stack(UOp.const(dtypes.weakint, 2)).shape, (2,))
|
||||
|
||||
class TestTensorUOpConv2d(unittest.TestCase):
|
||||
def test_conv2d_basic(self):
|
||||
|
||||
+25
-14
@@ -2,7 +2,7 @@ import unittest, pytest
|
||||
from tinygrad import dtypes, Variable
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.helpers import DEBUG, Context
|
||||
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType
|
||||
from tinygrad.uop.ops import Ops, UOp, UPat, PatternMatcher, graph_rewrite, GroupOp, AxisType, broadcast_axes
|
||||
from tinygrad.uop.symbolic import sym
|
||||
from test.helpers import to_uops_list
|
||||
|
||||
@@ -202,7 +202,7 @@ class TestUOpGraph(unittest.TestCase):
|
||||
|
||||
def test_where_same_fold(self):
|
||||
v = UOp.variable('tmp', 0, 1)
|
||||
c0 = UOp.const(dtypes.index, 0)
|
||||
c0 = UOp.const(dtypes.weakint, 0)
|
||||
vc = v != c0
|
||||
c1 = UOp.const(dtypes.float, 1.0)
|
||||
out = vc.where(c1, c1)
|
||||
@@ -424,16 +424,16 @@ class TestUOpGraph(unittest.TestCase):
|
||||
# mnist indexing with split reduceop
|
||||
# Make sure we are not doign math on the loaded index, which would promote it to long
|
||||
c0 = UOp.param(0, dtypes.uchar, (128000,))
|
||||
c1 = UOp.range(UOp.const(dtypes.index, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.index, 250), 2, AxisType.LOOP)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
|
||||
c3 = UOp.param(1, dtypes.int, (512,))
|
||||
c4 = c3.index(c1)
|
||||
c5 = UOp.range(UOp.const(dtypes.index, 240), 0, AxisType.REDUCE)
|
||||
c6 = ((c2*UOp.const(dtypes.index, 240))+c5)
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
|
||||
c6 = ((c2*UOp.const(dtypes.weakint, 240))+c5)
|
||||
c7 = UOp.param(2, dtypes.uchar, (60000,))
|
||||
c8 = c7.index(c6)
|
||||
c9 = ((c4<0).where((c4+60000), c4)!=c6.cast(dtypes.int)).where(0, c8.cast(dtypes.uint).cast(dtypes.uchar)).reduce(c5, arg=Ops.ADD)
|
||||
c10 = c0.index(((c1*UOp.const(dtypes.index, 250))+c2)).store(c9).end(c1, c2)
|
||||
c10 = c0.index(((c1*UOp.const(dtypes.weakint, 250))+c2)).store(c9).end(c1, c2)
|
||||
uops = to_uops_list([c10])
|
||||
for u in uops:
|
||||
self.assertNotEqual(u.dtype, dtypes.long)
|
||||
@@ -441,19 +441,19 @@ class TestUOpGraph(unittest.TestCase):
|
||||
def test_load_idx_no_math_on_loaded(self):
|
||||
# test the (x+y)<c pattern where x has loads - we shouldn't do math on loaded indices
|
||||
c0 = UOp.param(0, dtypes.uchar, (128000,))
|
||||
c1 = UOp.range(UOp.const(dtypes.index, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.index, 250), 2, AxisType.LOOP)
|
||||
c1 = UOp.range(UOp.const(dtypes.weakint, 512), 1, AxisType.LOOP)
|
||||
c2 = UOp.range(UOp.const(dtypes.weakint, 250), 2, AxisType.LOOP)
|
||||
c3 = UOp.param(1, dtypes.int, (512,))
|
||||
c4 = c3.index(c1) # c4 is a load
|
||||
c5 = UOp.range(UOp.const(dtypes.index, 240), 0, AxisType.REDUCE)
|
||||
c6 = ((c2*UOp.const(dtypes.index, 240))+c5)
|
||||
c5 = UOp.range(UOp.const(dtypes.weakint, 240), 0, AxisType.REDUCE)
|
||||
c6 = ((c2*UOp.const(dtypes.weakint, 240))+c5)
|
||||
c7 = UOp.param(2, dtypes.uchar, (60000,))
|
||||
c8 = c7.index(c6)
|
||||
# (loaded + range) < const pattern - loaded value shouldn't be promoted to long
|
||||
loaded_idx = c4.cast(dtypes.index)
|
||||
comparison = (loaded_idx + c5) < UOp.const(dtypes.index, 60000)
|
||||
loaded_idx = c4.cast(dtypes.weakint)
|
||||
comparison = (loaded_idx + c5) < UOp.const(dtypes.weakint, 60000)
|
||||
c9 = comparison.where(c8.cast(dtypes.uint).cast(dtypes.uchar), 0).reduce(c5, arg=Ops.ADD)
|
||||
c10 = c0.index(((c1*UOp.const(dtypes.index, 250))+c2)).store(c9).end(c1, c2)
|
||||
c10 = c0.index(((c1*UOp.const(dtypes.weakint, 250))+c2)).store(c9).end(c1, c2)
|
||||
uops = to_uops_list([c10])
|
||||
for u in uops:
|
||||
self.assertNotEqual(u.dtype, dtypes.long)
|
||||
@@ -707,5 +707,16 @@ class TestUOpBroadcast(unittest.TestCase):
|
||||
c = a + b
|
||||
self.assertEqual(c.op, Ops.ADD)
|
||||
|
||||
def test_broadcast_axes(self):
|
||||
t = Variable("t", 1, 10)
|
||||
self.assertEqual(broadcast_axes((4, 8), (4, 8)), ())
|
||||
self.assertEqual(broadcast_axes((8,), (4, 8)), (0,))
|
||||
self.assertEqual(broadcast_axes((), (4, 8)), (0, 1))
|
||||
self.assertEqual(broadcast_axes((3, 1), (4, 3, 8)), (0, 2))
|
||||
self.assertEqual(broadcast_axes((1, 8), (1, 8)), ())
|
||||
self.assertEqual(broadcast_axes((t, 8), (t, 8)), ())
|
||||
self.assertEqual(broadcast_axes((1, 8), (t, 8)), (0,))
|
||||
with self.assertRaises(RuntimeError): broadcast_axes((4, 8), (8,))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
@@ -11,13 +11,13 @@ from tinygrad.uop.validate import uops_to_z3
|
||||
def check_uop_against_string(self, v:UOp, s:str):
|
||||
sym_vars = {v.render():v for v in v.toposort() if v.op in (Ops.RANGE, Ops.SPECIAL, Ops.PARAM)}
|
||||
s_eval = eval(s, sym_vars)
|
||||
if isinstance(s_eval, int) and v.dtype==dtypes.index: s_eval = UOp.const(dtypes.index, s_eval)
|
||||
if isinstance(s_eval, int) and v.dtype==dtypes.weakint: s_eval = UOp.const(dtypes.weakint, s_eval)
|
||||
elif isinstance(s_eval, (bool, int, float)): s_eval = UOp.const(dtypes.from_py(s_eval), s_eval)
|
||||
s_eval = graph_rewrite(s_eval, commutative, name="cannonicalize eval")
|
||||
self.assertIs(s_eval, v, f"eval did not match simplified: {s_eval} != {v.render()} for {s}")
|
||||
|
||||
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.index): return UOp.variable(name,min_val,max_val,dtype)
|
||||
def uconst(val): return UOp.const(dtypes.index, val)
|
||||
def Variable(name: str, min_val: ConstType, max_val: ConstType, dtype: DType=dtypes.weakint): return UOp.variable(name,min_val,max_val,dtype)
|
||||
def uconst(val): return UOp.const(dtypes.weakint, val)
|
||||
def usum(ops): return functools.reduce(lambda x,y: x+y, ops)
|
||||
def uand(ops): return functools.reduce(lambda x,y: x*y, ops)
|
||||
|
||||
@@ -247,12 +247,12 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.assertEqual((Variable("x", -10, 0)%Variable("y", 1, 10))._min_max, (0, 9))
|
||||
|
||||
def test_range_div_its_symbolic_bound(self):
|
||||
a = Variable("a", 1, 10, dtypes.index)
|
||||
a = Variable("a", 1, 10, dtypes.weakint)
|
||||
ridx0 = UOp.range(a+2, 0)
|
||||
self.helper_test_variable(ridx0//(a+2), 0, 0, "0")
|
||||
|
||||
def test_range_mod_its_symbolic_bound(self):
|
||||
a = Variable("a", 1, 10, dtypes.index)
|
||||
a = Variable("a", 1, 10, dtypes.weakint)
|
||||
ridx = UOp.range(a+2, 0)
|
||||
self.helper_test_variable(ridx%(a+2), 0, 11, "r0")
|
||||
|
||||
@@ -919,8 +919,8 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.helper_test_variable(cond.cast(dtypes.int).ne(2), 1, 1, "True")
|
||||
self.helper_test_variable(cond.cast(dtypes.int).ne(-1), 1, 1, "True")
|
||||
# CAST(bool -> index) folds too
|
||||
self.helper_test_variable(cond.cast(dtypes.index).ne(0), 0, 1, "(a<2)")
|
||||
self.helper_test_variable(cond.cast(dtypes.index).ne(1), 0, 1, "((a<2)!=True)")
|
||||
self.helper_test_variable(cond.cast(dtypes.weakint).ne(0), 0, 1, "(a<2)")
|
||||
self.helper_test_variable(cond.cast(dtypes.weakint).ne(1), 0, 1, "((a<2)!=True)")
|
||||
|
||||
def test_where_removal(self):
|
||||
cond = Variable("a", 0, 3) < 2
|
||||
@@ -1021,7 +1021,7 @@ class TestSymbolic(unittest.TestCase):
|
||||
self.helper_test_variable((numerator//denominator)<=0, 1, 1, "True")
|
||||
|
||||
def test_symbolic_range_doesnt_collapse(self):
|
||||
r0 = UOp.range((Variable("a", 1, 10)<5).cast(dtypes.index), 0)
|
||||
r0 = UOp.range((Variable("a", 1, 10)<5).cast(dtypes.weakint), 0)
|
||||
self.helper_test_variable(r0, 0, 0, "r0")
|
||||
|
||||
def test_const_reciprocal(self):
|
||||
@@ -1289,16 +1289,16 @@ class TestInvalidIndex(unittest.TestCase):
|
||||
self.assertIs((UOp.invalid()<Variable("a",0,10)).simplify().dtype, dtypes.bool)
|
||||
|
||||
def test_alu_invalid_vconst(self):
|
||||
c1 = UOp.const(dtypes.index, (1, 1, Invalid, Invalid))
|
||||
c2 = UOp.const(dtypes.index, (1, Invalid, 1, 1))
|
||||
self.assertIs((c1+c2).simplify(), UOp.const(dtypes.index, (2, Invalid, Invalid, Invalid)))
|
||||
c1 = UOp.const(dtypes.weakint, (1, 1, Invalid, Invalid))
|
||||
c2 = UOp.const(dtypes.weakint, (1, Invalid, 1, 1))
|
||||
self.assertIs((c1+c2).simplify(), UOp.const(dtypes.weakint, (2, Invalid, Invalid, Invalid)))
|
||||
|
||||
class TestStoreLoadFolding(unittest.TestCase):
|
||||
"""Tests for store(index, load(index)) -> NOOP rule. This rule matches patterns that EMERGE during simplification."""
|
||||
def test_store_load_folding(self):
|
||||
# store(idx, load(idx)) -> NOOP, including emergent patterns like store(idx, load(idx) + 0)
|
||||
buf = UOp.param(0, dtypes.int, (1,))
|
||||
index = buf.index(UOp.const(dtypes.index, 0))
|
||||
index = buf.index(UOp.const(dtypes.weakint, 0))
|
||||
# Direct: store(idx, load(idx)) -> NOOP
|
||||
self.assertEqual(graph_rewrite(index.store(index.load()), sym).op, Ops.NOOP)
|
||||
# Emergent: store(idx, load(idx) + 0) -> store(idx, load(idx)) -> NOOP
|
||||
|
||||
@@ -127,6 +127,13 @@ class TestVminVmaxProperties(unittest.TestCase):
|
||||
self.assertEqual(x.vmin, 0)
|
||||
self.assertEqual(x.vmax, 10 >> 2)
|
||||
|
||||
def test_vmin_vmax_cast_unsigned(self):
|
||||
# a fitting source keeps exact bounds: no wrap can occur
|
||||
self.assertEqual(UOp.variable('x', 5, 10).cast(dtypes.uint8)._min_max, (5, 10))
|
||||
# a possibly-negative or too-large source can wrap: conservative
|
||||
self.assertEqual(UOp.variable('x', -1, 10).cast(dtypes.uint8)._min_max, (0, 255))
|
||||
self.assertEqual(UOp.variable('x', 250, 260).cast(dtypes.uint8)._min_max, (0, 255))
|
||||
|
||||
def test_vmin_vmax_xor_neg1(self):
|
||||
x = UOp.variable('x', 3, 7)
|
||||
uop = x ^ -1
|
||||
@@ -160,7 +167,7 @@ class TestVminVmaxProperties(unittest.TestCase):
|
||||
self.assertNotEqual(i.vmin, i.vmax)
|
||||
|
||||
def test_vmin_vmax_invalid_vconst(self):
|
||||
x = UOp.const(dtypes.index, (0, 4, Invalid, Invalid))
|
||||
x = UOp.const(dtypes.weakint, (0, 4, Invalid, Invalid))
|
||||
self.assertLess(x.vmin, 0)
|
||||
self.assertGreater(x.vmax, 4)
|
||||
|
||||
@@ -364,6 +371,10 @@ class TestConstFactor(unittest.TestCase):
|
||||
uop = (x * 3) * 5
|
||||
self.assertEqual(uop.const_factor(), 15) # Constant multipliers are combined (3 * 5 = 15)
|
||||
|
||||
def test_const_factor_variable_multiple_of(self):
|
||||
x = UOp.variable('x', 16, 32, multiple_of=4)
|
||||
self.assertEqual(x.const_factor(), 4)
|
||||
|
||||
class TestDivides(unittest.TestCase):
|
||||
def test_divides_constant_exact(self):
|
||||
# Divides a constant by an exact divisor
|
||||
@@ -402,5 +413,15 @@ class TestDivides(unittest.TestCase):
|
||||
result = uop.divides(3)
|
||||
self.assertIsNone(result) # Cannot divide by 3, since 4 is not divisible by 3
|
||||
|
||||
def test_divides_variable_multiple_of_exact(self):
|
||||
x = UOp.variable('x', 16, 32, multiple_of=4)
|
||||
result = x.divides(4)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_divides_variable_multiple_of_factor(self):
|
||||
x = UOp.variable('x', 16, 32, multiple_of=4)
|
||||
result = x.divides(2)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+11
-13
@@ -14,28 +14,26 @@ class TestDTypeFromUOp(unittest.TestCase):
|
||||
def test_broadcastable_promotion(self):
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
|
||||
self.assertEqual(dtype_from_uop(Ops.MUL, (UOp.const(dtypes.int8, 1), UOp.const(dtypes.int32, 1)), None), dtypes.int32)
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None), dtypes.int8)
|
||||
with self.assertRaises(KeyError): dtype_from_uop(Ops.ADD, (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.int8, 1)), None)
|
||||
|
||||
def test_same_dtype_fast_path(self):
|
||||
src = (UOp.const(dtypes.index, 1), UOp.const(dtypes.index, 2))
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, src, None), dtypes.index)
|
||||
src = (UOp.const(dtypes.weakint, 1), UOp.const(dtypes.weakint, 2))
|
||||
self.assertEqual(dtype_from_uop(Ops.ADD, src, None), dtypes.weakint)
|
||||
|
||||
def test_where_promotion(self):
|
||||
cond = UOp.const(dtypes.bool, True)
|
||||
self.assertEqual(dtype_from_uop(Ops.WHERE, (cond, UOp.const(dtypes.float32, 1.0), UOp.const(dtypes.float16, 1.0)), None), dtypes.float32)
|
||||
idx = UOp.range(4, 0)
|
||||
self.assertEqual(idx.valid(idx < 4).dtype, dtypes.index)
|
||||
self.assertEqual(idx.valid(idx < 4).dtype, dtypes.weakint)
|
||||
|
||||
def test_const_dtype_from_value(self):
|
||||
self.assertEqual(dtype_from_uop(Ops.CONST, (), True), dtypes.bool)
|
||||
self.assertEqual(dtype_from_uop(Ops.CONST, (), 3), dtypes.weakint)
|
||||
self.assertEqual(dtype_from_uop(Ops.CONST, (), ConstFloat(3.0)), dtypes.weakfloat)
|
||||
self.assertEqual(dtype_from_uop(Ops.CONST, (), Invalid), dtypes.bool)
|
||||
self.assertRaises(TypeError, dtype_from_uop, Ops.CONST, (), (1, 2))
|
||||
|
||||
@Context(SPEC=2)
|
||||
def test_const_default_dtype_is_derived(self):
|
||||
self.assertEqual(UOp(Ops.CONST, arg=3).dtype, dtypes.weakint)
|
||||
self.assertEqual(UOp(Ops.CONST, arg=ConstFloat(3.0)).dtype, dtypes.weakfloat)
|
||||
self.assertEqual(UOp(Ops.CONST, arg=True).dtype, dtypes.bool)
|
||||
self.assertEqual(UOp(Ops.CONST, arg=Invalid).dtype, dtypes.bool)
|
||||
@@ -81,7 +79,7 @@ class TestExecALU(unittest.TestCase):
|
||||
# Invalid poisons any binary op regardless of result dtype: a comparison must not fold to a boolean
|
||||
self.assertIs(exec_alu(Ops.CMPLT, dtypes.bool, (Invalid, 1)), Invalid)
|
||||
self.assertIs(exec_alu(Ops.CMPNE, dtypes.bool, (Invalid, 1)), Invalid)
|
||||
self.assertIs(exec_alu(Ops.ADD, dtypes.index, (Invalid, 1)), Invalid)
|
||||
self.assertIs(exec_alu(Ops.ADD, dtypes.weakint, (Invalid, 1)), Invalid)
|
||||
|
||||
def test_div(self):
|
||||
self.assertEqual(exec_alu(Ops.CDIV, dtypes.int8, (8, 2)), 4)
|
||||
@@ -155,8 +153,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
|
||||
def test_tiny_gate_store(self):
|
||||
gmem = UOp.param(0, dtypes.float, (8,))
|
||||
gidx0 = UOp.special(4, 'gidx0')
|
||||
gate = gidx0<UOp.const(dtypes.index, 1)
|
||||
idx = UOp(Ops.INDEX, src=(gmem, (gidx0 * UOp.const(dtypes.index, 2)).valid(gate)))
|
||||
gate = gidx0<UOp.const(dtypes.weakint, 1)
|
||||
idx = UOp(Ops.INDEX, src=(gmem, (gidx0 * UOp.const(dtypes.weakint, 2)).valid(gate)))
|
||||
val = UOp.const(dtypes.float, 42.0)
|
||||
store = UOp(Ops.STORE, src=(idx, val))
|
||||
uops = to_uops_list([store])
|
||||
@@ -172,8 +170,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
|
||||
gmem0 = UOp.param(0, dtypes.float, (8,))
|
||||
gmem1 = UOp.param(1, dtypes.float, (8,))
|
||||
gidx0 = UOp.special(4, 'gidx0')
|
||||
idx = gidx0 * UOp.const(dtypes.index, 2)
|
||||
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gidx0<UOp.const(dtypes.index, 1))))
|
||||
idx = gidx0 * UOp.const(dtypes.weakint, 2)
|
||||
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gidx0<UOp.const(dtypes.weakint, 1))))
|
||||
idx1 = UOp(Ops.INDEX, src=(gmem1, idx))
|
||||
val = UOp.const(dtypes.float, 42.0)
|
||||
stores = [UOp.store(idx0, val), UOp.store(idx1, val)]
|
||||
@@ -192,8 +190,8 @@ class TestGatedStoreRewrite(unittest.TestCase):
|
||||
gmem0 = UOp.param(0, dtypes.float, (8,))
|
||||
gmem1 = UOp.param(1, dtypes.float, (8,))
|
||||
gidx0 = UOp.special(4, 'gidx0')
|
||||
idx = gidx0*UOp.const(dtypes.index, 2)
|
||||
gate = gidx0<UOp.const(dtypes.index, 1)
|
||||
idx = gidx0*UOp.const(dtypes.weakint, 2)
|
||||
gate = gidx0<UOp.const(dtypes.weakint, 1)
|
||||
idx0 = UOp(Ops.INDEX, src=(gmem0, idx.valid(gate)))
|
||||
idx1 = UOp(Ops.INDEX, src=(gmem1, idx.valid(gate)))
|
||||
val = UOp.const(dtypes.float, 42.0)
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestValidateOOB(unittest.TestCase):
|
||||
buf0 = UOp.param(0, dtypes.int, (16,))
|
||||
buf1 = UOp.param(1, dtypes.int, (64,))
|
||||
r = UOp.range(42, 0, AxisType.GLOBAL)
|
||||
ld0 = buf0.index(r.valid(r < 8)).load(dtype=dtypes.int).cast(dtypes.index)
|
||||
ld0 = buf0.index(r.valid(r < 8)).load(dtype=dtypes.int).cast(dtypes.weakint)
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 32))).load(dtype=dtypes.int)]) # valid
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf1.index((ld0 * 2).valid((ld0 >= 0) & (ld0 < 64))).load(dtype=dtypes.int)]) # oob
|
||||
@@ -135,7 +135,7 @@ class TestValidateOOB(unittest.TestCase):
|
||||
with Context(CHECK_OOB=1, SPEC=2):
|
||||
buf_bool = UOp.param(0, dtypes.bool, (16,))
|
||||
buf_int = UOp.param(1, dtypes.int, (8,))
|
||||
gidx = UOp(Ops.SPECIAL, src=(UOp.const(dtypes.index, 16),), arg="gidx0")
|
||||
gidx = UOp(Ops.SPECIAL, src=(UOp.const(dtypes.weakint, 16),), arg="gidx0")
|
||||
ld_bool = buf_bool.index(gidx).load()
|
||||
with self.assertRaises(RuntimeError):
|
||||
to_uops_list([buf_int.index(gidx.valid(ld_bool)).load()]) # gidx 0..15, buf_int size 8
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from tinygrad import Device, Tensor, dtypes
|
||||
from tinygrad import Device, Tensor, Variable, dtypes
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.codegen import to_program
|
||||
from tinygrad.codegen.opt import Opt, OptOps
|
||||
@@ -114,5 +114,31 @@ class TestFloat4(unittest.TestCase):
|
||||
|
||||
assert TestFloat4.count_float4(uops) == (1, 1)
|
||||
|
||||
def test_float4_aligned_variable(self):
|
||||
x = Variable('x', 0, 4, multiple_of=4).bind(4)
|
||||
a = Tensor.empty(4).realize()
|
||||
b = Tensor.empty(12).realize().shrink(((x, x+4),))
|
||||
c = a + b
|
||||
|
||||
# should float4 both
|
||||
|
||||
s = c.linear_with_vars()[0].src[0]
|
||||
uops = tuple(to_program(replace_opts(s.src[0], [Opt(op=OptOps.UPCAST, axis=0, arg=4)]), renderer=Device[Device.DEFAULT].renderer).src[1].src)
|
||||
|
||||
assert TestFloat4.count_float4(uops) == (2, 1)
|
||||
|
||||
def test_float4_unaligned_variable(self):
|
||||
x = Variable('x', 0, 4, multiple_of=2).bind(4)
|
||||
a = Tensor.empty(4).realize()
|
||||
b = Tensor.empty(12).realize().shrink(((x, x+4),))
|
||||
c = a + b
|
||||
|
||||
# should float4 a but not b
|
||||
|
||||
s = c.linear_with_vars()[0].src[0]
|
||||
uops = tuple(to_program(replace_opts(s.src[0], [Opt(op=OptOps.UPCAST, axis=0, arg=4)]), renderer=Device[Device.DEFAULT].renderer).src[1].src)
|
||||
|
||||
assert TestFloat4.count_float4(uops) == (1, 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -18,6 +18,9 @@ from test.backend.test_linearizer import helper_realized_ast, helper_linearizer_
|
||||
|
||||
# NOTE: to_program always passes in Device[Device.DEFAULT].renderer explicitly for process_replay!!!
|
||||
|
||||
def _tc_rand(*shape, dtype:DType) -> Tensor:
|
||||
return Tensor.randint(*shape, low=dtype.min, high=dtype.max+1, dtype=dtype) if dtypes.is_int(dtype) else Tensor.rand(*shape, dtype=dtype)
|
||||
|
||||
def run_program(prg:UOp, bufs:list[Buffer]):
|
||||
buf_uops = [UOp.new_buffer(b.device, b.size, b.dtype) for b in bufs]
|
||||
for u,b in zip(buf_uops, bufs): buffers[u] = b
|
||||
@@ -25,7 +28,7 @@ def run_program(prg:UOp, bufs:list[Buffer]):
|
||||
|
||||
def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0,
|
||||
ensure_triggered:bool=True):
|
||||
a, b = Tensor.rand(M, K, dtype=dtype_in), Tensor.rand(K, N, dtype=dtype_in)
|
||||
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
|
||||
r = a.matmul(b, dtype=dtype_out)
|
||||
sched = r.schedule_linear()
|
||||
realized_ast = sched.src[-1].src[0]
|
||||
@@ -44,7 +47,7 @@ def helper_tc_ensure_uops_and_opts_count(N: int, M:int, K:int, dtype_in:DType, d
|
||||
except KernelOptError: pass
|
||||
|
||||
def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axis:int=0, tc_select:int=-1, tc_opt:int=0, use_tensor_cores:int=1):
|
||||
a, b = Tensor.rand(M, K, dtype=dtype_in), Tensor.rand(K, N, dtype=dtype_in)
|
||||
a, b = _tc_rand(M, K, dtype=dtype_in), _tc_rand(K, N, dtype=dtype_in)
|
||||
np_a, np_b = a.numpy(), b.numpy()
|
||||
r = a.matmul(b, dtype=dtype_out)
|
||||
if dtype_in == dtypes.bfloat16: r = r.float()
|
||||
@@ -57,9 +60,11 @@ def helper_tc_allclose(N:int, M:int, K:int, dtype_in:DType, dtype_out:DType, axi
|
||||
run_program(ast, bufs)
|
||||
if dtype_in == dtypes.half: tc_atol, tc_rtol = 1e-2, 1e-3
|
||||
elif dtype_in == dtypes.bfloat16: tc_atol, tc_rtol = (1e-1, 2e-2) if dtype_out == dtypes.bfloat16 else (1e-2, 1e-2)
|
||||
elif not dtypes.is_float(dtype_in): tc_atol, tc_rtol = 0, 0
|
||||
else: tc_atol, tc_rtol = 5e-3, 1e-4
|
||||
c = bufs[0].numpy().reshape((M,N))
|
||||
np.testing.assert_allclose(c, np_a @ np_b, atol=tc_atol, rtol=tc_rtol)
|
||||
ref = (np_a.astype(np.int32) @ np_b.astype(np.int32)) if not dtypes.is_float(dtype_in) else (np_a @ np_b)
|
||||
np.testing.assert_allclose(c, ref, atol=tc_atol, rtol=tc_rtol)
|
||||
|
||||
class TestTensorCores(unittest.TestCase):
|
||||
# TODO: don't skip bf16 for real device (METAL, AMD)
|
||||
@@ -75,7 +80,7 @@ class TestTensorCores(unittest.TestCase):
|
||||
def test_tensor_cores_codegen(self):
|
||||
for tc in Device[Device.DEFAULT].renderer.tensor_cores:
|
||||
n, m, k = tc.dims
|
||||
a, b = Tensor.rand(m, k, dtype=tc.dtype_in), Tensor.rand(k, n, dtype=tc.dtype_in)
|
||||
a, b = _tc_rand(m, k, dtype=tc.dtype_in), _tc_rand(k, n, dtype=tc.dtype_in)
|
||||
r = a.matmul(b, dtype=tc.dtype_out)
|
||||
prg = to_program(replace_opts(r.schedule_linear().src[-1].src[0],
|
||||
[Opt(op=OptOps.TC, axis=0, arg=(-1, 2, 1))]), Device[Device.DEFAULT].renderer)
|
||||
|
||||
@@ -67,10 +67,7 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
def _run_attention(self, block:GatedDeltaNetBlock, x:Tensor, start_pos:int):
|
||||
x_norm = block.attn_norm(x)
|
||||
block._init_state(x_norm)
|
||||
out = block._attention(x_norm, start_pos).realize()
|
||||
assert block.pending_state is not None
|
||||
Tensor.realize(block.conv_state.assign(block.pending_state[0]), block.recurrent_state.assign(block.pending_state[1]))
|
||||
return out.numpy()
|
||||
return block._attention(x_norm, start_pos).realize().numpy()
|
||||
|
||||
def _cache_views(self, block:GatedDeltaNetBlock) -> tuple[np.ndarray, np.ndarray]:
|
||||
if hasattr(block, 'conv_state'):
|
||||
@@ -89,8 +86,8 @@ class TestGatedDeltaNetBlock(unittest.TestCase):
|
||||
x_float = x.astype(np.float32)
|
||||
return (x_float / np.sqrt((x_float * x_float).mean(axis=-1, keepdims=True) + eps)) * weight.astype(np.float32)
|
||||
|
||||
def _normalize_np(self, x:np.ndarray, eps:float=1e-6) -> np.ndarray:
|
||||
return x / np.sqrt((x * x).sum(axis=-1, keepdims=True) + eps)
|
||||
def _normalize_np(self, x:np.ndarray, eps:float=1e-12) -> np.ndarray:
|
||||
return x / np.maximum(np.sqrt((x * x).sum(axis=-1, keepdims=True)), eps)
|
||||
|
||||
def _softplus_np(self, x:np.ndarray) -> np.ndarray:
|
||||
return np.log1p(np.exp(-np.abs(x))) + np.maximum(x, 0)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import tempfile, unittest
|
||||
|
||||
from tinygrad import Tensor, dtypes
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.uop.ops import UOp
|
||||
from tinygrad.uop.spec import spec_tensor
|
||||
|
||||
|
||||
class TestWeakPromotion(unittest.TestCase):
|
||||
@@ -14,28 +11,20 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
with self.assertRaises(ValueError): Tensor.const(dtypes.weakfloat, 1.0).randn_like()
|
||||
|
||||
def test_sum_stays_weak(self):
|
||||
for weak, value in ((dtypes.weakint, 1), (dtypes.weakfloat, 1.0)):
|
||||
for weak, value in ((dtypes.weakfloat, 1.0),):
|
||||
self.assertEqual(Tensor.const(weak, value).expand(3).sum().dtype, weak)
|
||||
self.assertEqual((Tensor.const(dtypes.weakfloat, 1.0).expand(3).sum() + Tensor([1], dtype=dtypes.float16)).dtype, dtypes.float16)
|
||||
|
||||
def test_storage_width(self):
|
||||
t = Tensor.const(dtypes.weakint, 2)
|
||||
for fn in (lambda: t.bitcast(dtypes.int32), lambda: Tensor.const(dtypes.int32, 2).bitcast(dtypes.weakint), t.element_size, t.nbytes):
|
||||
with self.assertRaises(RuntimeError): fn()
|
||||
|
||||
def test_materialize_at_default_dtype(self):
|
||||
for weak, value, strong in ((dtypes.weakint, 3, dtypes.default_int), (dtypes.weakfloat, 0.5, dtypes.default_float)):
|
||||
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
|
||||
t = Tensor.const(weak, value)
|
||||
self.assertEqual(t.dtype, weak)
|
||||
self.assertEqual(t.data().itemsize, strong.itemsize)
|
||||
self.assertEqual(t.numpy().dtype.itemsize, strong.itemsize)
|
||||
realized = t.clone("CPU").realize()
|
||||
self.assertEqual((realized.dtype, realized.uop.buffer.dtype), (strong, strong))
|
||||
with patch.object(dtypes, "default_int", dtypes.int64):
|
||||
self.assertEqual(Tensor.const(dtypes.weakint, 3).numpy().dtype.itemsize, dtypes.int64.itemsize)
|
||||
with self.assertRaises(RuntimeError): t.clone("CPU")
|
||||
|
||||
def test_uop_scalar_const_unchanged(self):
|
||||
for dtype, value in ((dtypes.index, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
|
||||
for dtype, value in ((dtypes.weakint, 1), (dtypes.int32, 1), (dtypes.float32, 0.5)):
|
||||
out = UOp.variable("x", 0.0 if dtype == dtypes.float32 else 0, 10.0 if dtype == dtypes.float32 else 10, dtype) + value
|
||||
self.assertEqual((out.dtype, out.src[1].dtype), (dtype, dtype))
|
||||
|
||||
@@ -64,17 +53,6 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
weak = Tensor([True, False]).where(Tensor(1), 2)
|
||||
self.assertEqual(weak.dot(Tensor([1, 1], dtype=dtypes.int8)).dtype, dtypes.int8)
|
||||
|
||||
@unittest.expectedFailure # TODO: Tensor(3).uop becomes CONST(weakint); Tensor.dtype is always uop.dtype; buffers lower to the default
|
||||
def test_dtype_is_uop_dtype(self):
|
||||
for value, weak, lowered in ((3, dtypes.weakint, dtypes.default_int), (0.5, dtypes.weakfloat, dtypes.default_float)):
|
||||
t = Tensor(value)
|
||||
self.assertEqual((t.uop.dtype, t.dtype), (weak, weak))
|
||||
self.assertEqual(t.numpy().dtype.itemsize, lowered.itemsize)
|
||||
realized = t.clone("CPU").realize()
|
||||
self.assertEqual((realized.dtype, realized.uop.buffer.dtype), (lowered, lowered))
|
||||
with patch.object(dtypes, "default_int", dtypes.int64):
|
||||
self.assertEqual(Tensor(3).clone("CPU").realize().uop.buffer.dtype, dtypes.int64)
|
||||
|
||||
def test_integer_values(self):
|
||||
x = Tensor.full((1,), 1, dtype=dtypes.int64, device="CPU")
|
||||
self.assertEqual((x + 2**40).item(), 2**40 + 1)
|
||||
@@ -94,15 +72,6 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
for out in (Tensor(2).exp(), Tensor(2).cos(), Tensor(2).sigmoid()):
|
||||
self.assertEqual((out.dtype, (out + t_f16).dtype), (dtypes.weakfloat, dtypes.float16))
|
||||
|
||||
@unittest.expectedFailure # TODO: where of weak consts stays weak and resolves per consumer
|
||||
def test_where_and_shared_literal(self):
|
||||
gate, weak = Tensor([True, False], device="CPU"), Tensor(2)
|
||||
weak_where = gate.where(weak, 3)
|
||||
self.assertEqual(weak_where.dtype, dtypes.weakint)
|
||||
self.assertEqual((weak_where + Tensor([1, 1], dtype=dtypes.int64, device="CPU")).tolist(), [3, 4])
|
||||
self.assertEqual((weak + Tensor([1], dtype=dtypes.int32, device="CPU")).item(), 3)
|
||||
self.assertEqual((weak + Tensor([1], dtype=dtypes.int64, device="CPU")).item(), 3)
|
||||
|
||||
def test_null_lowering(self):
|
||||
for t in (Tensor.full((1,), 1, dtype=dtypes.int64, device="NULL") + 2**40,
|
||||
Tensor.full((1,), 1.0, dtype=dtypes.float64, device="NULL") + (1.0 + 2**-40)):
|
||||
@@ -110,14 +79,47 @@ class TestWeakPromotion(unittest.TestCase):
|
||||
self.assertNotIn(t.uop.buffer.dtype, dtypes.weaks)
|
||||
|
||||
|
||||
class TestWeakSpec(unittest.TestCase):
|
||||
def test_weak_operand_allowed(self):
|
||||
x = UOp.variable("x", 0, 10, dtypes.int64)
|
||||
weak = UOp.const(dtypes.weakint, 3)
|
||||
for u in (x.alu(Ops.ADD, weak), x.alu(Ops.CMPLT, weak), x.alu(Ops.SHL, weak)):
|
||||
self.assertIs(spec_tensor.rewrite(u), True)
|
||||
gate = UOp.variable("gate", False, True, dtypes.bool)
|
||||
self.assertIs(spec_tensor.rewrite(UOp(Ops.WHERE, dtypes.int8, (gate, UOp.const(dtypes.int8, 1), weak))), True)
|
||||
class TestWeakStorageBoundary(unittest.TestCase):
|
||||
# weak has no storage: a weak assignment source casts when it defers to the destination, everything else raises
|
||||
def test_weak_source(self):
|
||||
w05 = Tensor.const(dtypes.weakfloat, 0.5).reshape(1)
|
||||
dst = Tensor.zeros(2, dtype=dtypes.int8, device="CPU").contiguous().realize()
|
||||
with self.assertRaises(RuntimeError): dst.assign(w05.expand(2)) # weakfloat into int does not defer
|
||||
with self.assertRaises(RuntimeError): dst[0:1] = w05
|
||||
fdst = Tensor.zeros(2, dtype=dtypes.float32, device="CPU").contiguous().realize()
|
||||
fdst[0:1] = w05 # weakfloat defers to float
|
||||
self.assertEqual(fdst.tolist(), [0.5, 0.0])
|
||||
with tempfile.TemporaryDirectory() as td: # the DISK path checks the same
|
||||
ddst = Tensor.empty(2, dtype=dtypes.int32, device=f"DISK:{td}/t")
|
||||
with self.assertRaises(RuntimeError): ddst.assign(w05.expand(2))
|
||||
|
||||
def test_weak_has_no_storage(self):
|
||||
import numpy as np
|
||||
with self.assertRaises(RuntimeError): Tensor(np.ones(2, dtype=np.float32), dtype=dtypes.weakfloat)
|
||||
with self.assertRaises(RuntimeError): Tensor(bytes(8), dtype=dtypes.weakfloat)
|
||||
|
||||
class TestWeakMaterializationEntries(unittest.TestCase):
|
||||
# everything that creates storage from a weak value raises
|
||||
def test_reads_commit_storage_raises(self):
|
||||
for weak, value, strong in ((dtypes.weakfloat, 0.5, dtypes.default_float),):
|
||||
def weak_val():
|
||||
return Tensor([True], device="CPU").where(Tensor.const(weak, value), Tensor.const(weak, value))
|
||||
self.assertEqual(weak_val().dtype, weak)
|
||||
self.assertEqual(weak_val().to("CPU").dtype, weak)
|
||||
self.assertEqual(weak_val().data().format, strong.fmt)
|
||||
self.assertEqual(weak_val().numpy().dtype.itemsize, strong.itemsize)
|
||||
self.assertEqual(weak_val().tolist(), [value])
|
||||
self.assertEqual(weak_val().cast(strong).realize().uop.buffer.dtype, strong)
|
||||
for entry in (lambda t: t.contiguous(), lambda t: t.realize(), lambda t: t.clone(),
|
||||
lambda t: t.to("CPU:1").realize(), lambda t: t.as_param(0)):
|
||||
with self.assertRaises(RuntimeError): entry(weak_val())
|
||||
|
||||
def test_empty_reads_commit(self):
|
||||
for weak, strong in ((dtypes.weakfloat, dtypes.default_float),):
|
||||
empty = Tensor.const(weak, 0).reshape(1).shrink(((0, 0),))
|
||||
self.assertEqual(empty.data().format, strong.fmt)
|
||||
self.assertEqual(empty.numpy().dtype.itemsize, strong.itemsize)
|
||||
self.assertEqual(empty.tolist(), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -71,7 +71,7 @@ class TestKeccak(unittest.TestCase):
|
||||
def test_variable_bs(self):
|
||||
data = Tensor([b"abc", b"abc", b"def"], dtype=dtypes.uint8).repeat(2048, 1)
|
||||
bs = UOp.variable("bs", 1, 4096).bind(3)
|
||||
out = data.shrink_to(bs, data.shape[-1]).keccak().shrink_to(3, 32)
|
||||
out = data.shrink_to(bs, data.shape[-1]).keccak().shrink_to(3, 32).realize()
|
||||
self.assertEqual(bytes(out[0].tolist()), bytearray.fromhex("3a985da74fe225b2 045c172d6bd390bd 855f086e3e9d525b 46bfe24511431532"))
|
||||
self.assertEqual(bytes(out[1].tolist()), bytearray.fromhex("3a985da74fe225b2 045c172d6bd390bd 855f086e3e9d525b 46bfe24511431532"))
|
||||
self.assertEqual(bytes(out[2].tolist()), bytearray.fromhex("8e0d8f672252acb0 ffc5093db8653b18 1513bf9a2097e737 b4f73533dcaf46df"))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest, gc
|
||||
import numpy as np
|
||||
from tinygrad.helpers import polyN, is_numpy_ndarray, disable_gc
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import polyN, disable_gc
|
||||
from tinygrad.tensor import Tensor, is_numpy_ndarray
|
||||
|
||||
class TestPolyN(unittest.TestCase):
|
||||
def test_tensor(self):
|
||||
|
||||
@@ -3,6 +3,7 @@ from tinygrad import Tensor
|
||||
from tinygrad.device import Buffer
|
||||
from tinygrad.dtype import Invalid, dtypes
|
||||
from tinygrad.engine.realize import run_linear
|
||||
from tinygrad.uop.ops import Ops, UOp
|
||||
|
||||
class TestInvalidTensor(unittest.TestCase):
|
||||
def _invalid_test_helper(self, out, expected):
|
||||
@@ -132,5 +133,15 @@ class TestInvalidTensor(unittest.TestCase):
|
||||
out = Tensor([1.0, 2.0, 3.0, 4.0])[idx]
|
||||
self._invalid_test_helper(out, [1.0, 2.0, None, None])
|
||||
|
||||
def test_uop_where_keeps_invalid_bare(self):
|
||||
cond = UOp.const(dtypes.weakint, 0) < UOp.const(dtypes.weakint, 1)
|
||||
idx = UOp(Ops.STACK, src=tuple(UOp.const(dtypes.weakint, x) for x in range(3)))
|
||||
out = cond.where(idx, UOp.invalid())
|
||||
self.assertIs(cond.op, Ops.CMPLT)
|
||||
self.assertIs(idx.op, Ops.STACK)
|
||||
self.assertIs(out.op, Ops.WHERE)
|
||||
self.assertIs(out.src[2].op, Ops.CONST)
|
||||
self.assertIs(out.src[2].arg, Invalid)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -17,6 +17,7 @@ class TestLinAlg(unittest.TestCase):
|
||||
for size in sizes:
|
||||
a = Tensor.randn(size).realize()
|
||||
U,S,V = a.svd()
|
||||
Tensor.realize(U,S,V)
|
||||
b_shape,m,n = size[0:-2],size[-2],size[-1]
|
||||
k = min(m,n)
|
||||
s_diag = (S.unsqueeze(-2) * Tensor.eye(k).reshape((1,) * len(b_shape) + (k,k)))
|
||||
@@ -29,6 +30,7 @@ class TestLinAlg(unittest.TestCase):
|
||||
with Context(CHECK_OOB=0): # sometimes this is slow in CI
|
||||
a = Tensor.randn(size).realize()
|
||||
U,S,V = a.svd(full_matrices=False)
|
||||
Tensor.realize(U,S,V)
|
||||
b_shape,m,n = size[0:-2],size[-2],size[-1]
|
||||
k = min(m,n)
|
||||
s_diag = (S.unsqueeze(-2) * Tensor.eye(k).reshape((1,) * len(b_shape) + (k,k)).expand(b_shape + (k,k)))
|
||||
@@ -61,6 +63,7 @@ class TestLinAlg(unittest.TestCase):
|
||||
for size in sizes:
|
||||
a = Tensor.randn(size).realize()
|
||||
Q,R = a.qr()
|
||||
Tensor.realize(Q,R)
|
||||
orthogonality_helper(Q)
|
||||
reconstruction_helper([Q,R],a)
|
||||
|
||||
@@ -73,9 +76,10 @@ class TestLinAlg(unittest.TestCase):
|
||||
reconstruction_helper([Q,R], a)
|
||||
|
||||
def test_svd_identity(self):
|
||||
for a in (Tensor.eye(2), Tensor.zeros(2, 2)):
|
||||
for a in (Tensor.eye(2).clone(), Tensor.zeros(2, 2)):
|
||||
a = a.realize()
|
||||
U,S,V = a.svd()
|
||||
Tensor.realize(U,S,V)
|
||||
assert not np.isnan(U.numpy()).any()
|
||||
assert not np.isnan(S.numpy()).any()
|
||||
assert not np.isnan(V.numpy()).any()
|
||||
@@ -85,6 +89,7 @@ class TestLinAlg(unittest.TestCase):
|
||||
def test_svd_identity_4x4(self):
|
||||
a = Tensor.eye(4).clone()
|
||||
U,S,V = a.svd()
|
||||
Tensor.realize(U,S,V)
|
||||
assert not np.isnan(U.numpy()).any()
|
||||
assert not np.isnan(S.numpy()).any()
|
||||
assert not np.isnan(V.numpy()).any()
|
||||
|
||||
@@ -151,7 +151,7 @@ class TestTransformerGenerate(unittest.TestCase):
|
||||
"""Temperature from generate should be passed through to __call__."""
|
||||
model = Transformer(TEST_CONFIG)
|
||||
captured_temps = []
|
||||
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
||||
def mock_call(self, tokens, start_pos, temperature):
|
||||
captured_temps.append(float(temperature.item()))
|
||||
return Tensor([[42]])
|
||||
with patch.object(Transformer, '__call__', mock_call):
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestMetalGraph(unittest.TestCase):
|
||||
buf.op = Ops.SLICE
|
||||
src = MagicMock()
|
||||
src.dtype = dtypes.uint8
|
||||
buf.src = (src, UOp.const(dtypes.index, offset))
|
||||
buf.src = (src, UOp.const(dtypes.weakint, offset))
|
||||
buf.dtype = dtypes.uint8
|
||||
else:
|
||||
buf.op = Ops.BUFFER
|
||||
|
||||
@@ -119,7 +119,7 @@ class TestRandomness(unittest.TestCase):
|
||||
self.assertRaises(AssertionError, lambda: Tensor(2).multinomial(1, replacement=False))
|
||||
self.assertRaises(AssertionError, lambda: Tensor([1, 9]).multinomial(0, replacement=False))
|
||||
def _check_with_torch(w, num_samples, replacement):
|
||||
tiny_res = Tensor(w).multinomial(num_samples, replacement=replacement)
|
||||
tiny_res = Tensor(w).multinomial(num_samples, replacement=replacement).realize()
|
||||
torch_res = torch.tensor(w).multinomial(num_samples, replacement=replacement)
|
||||
self.assertEqual(tiny_res.shape, torch_res.shape)
|
||||
if torch_res.ndim == 1:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
from tinygrad import Tensor, UOp
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.uop.ops import KernelInfo
|
||||
|
||||
def wait_loop_kernel(C:UOp) -> UOp:
|
||||
N = 10
|
||||
|
||||
# LOOP is a bound-less loop header: a jump target with no induction variable.
|
||||
# the compare and conditional backedge are expanded by the renderers from LOOP/END
|
||||
l = UOp.loop(0)
|
||||
|
||||
i = UOp.placeholder((1,), dtypes.int, 0, addrspace=AddrSpace.REG)
|
||||
|
||||
# i = 0
|
||||
i = i.after(i[0].store(0))
|
||||
|
||||
# i + 1, read loop-carried through after(l)
|
||||
inc = i.after(l)[0].load() + 1
|
||||
|
||||
# i = inc; END(store, l, cond): conditional backedge, loop again while inc < N (do-while)
|
||||
# NOTE: the cond uses the computed value, not a reload of the register
|
||||
st = i[0].store(inc)
|
||||
i = i.after(st.end(l, inc < N))
|
||||
|
||||
return C[0].store(i[0].load()).sink(arg=KernelInfo(name="wait_loop"))
|
||||
|
||||
class TestWaitLoop(unittest.TestCase):
|
||||
def test_wait_loop(self):
|
||||
c = Tensor.empty(1, dtype=dtypes.int)
|
||||
c = Tensor.custom_kernel(c, fxn=wait_loop_kernel)[0]
|
||||
c.realize()
|
||||
self.assertEqual(c.item(), 10)
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
+5
-4
@@ -61,7 +61,7 @@ def _make_buffer_view(src:UOp) -> UOp|None:
|
||||
buf = buf.src[0]
|
||||
if byte_offset % buf.dtype.itemsize != 0: return None
|
||||
offset = byte_offset // buf.dtype.itemsize
|
||||
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.index, offset)), src.numel())
|
||||
return UOp(Ops.SLICE, src.dtype, (buf, UOp.const(dtypes.weakint, offset)), src.numel())
|
||||
|
||||
def contiguous_mops_to_view(c:UOp, src:UOp):
|
||||
"""MOPS(BUFFER) → SLICE when movement ops collapse to a contiguous range."""
|
||||
@@ -180,8 +180,9 @@ def finalize_after(ctx:AllocCtx, x:UOp):
|
||||
def replace_input_buffer(ctx:AllocCtx, b:UOp):
|
||||
ctx.replacements.append(b)
|
||||
return UOp.param(len(ctx.replacements)-1, b.dtype, b.shape, b.device,
|
||||
b._min_max if b.op is Ops.BIND else None, b.src[0].expr if b.op is Ops.BIND else None,
|
||||
b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL)
|
||||
b._min_max if b.op is Ops.BIND else None, name=b.src[0].expr if b.op is Ops.BIND else None,
|
||||
addrspace=b.addrspace if b.addrspace is not None else AddrSpace.GLOBAL,
|
||||
multiple_of=b.src[0].arg.multiple_of if b.op is Ops.BIND else None)
|
||||
|
||||
pm_finalize_call = PatternMatcher([
|
||||
(UPat(Ops.AFTER, name="x"), finalize_after),
|
||||
@@ -193,7 +194,7 @@ pm_replace_buf = PatternMatcher([
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="b"), lambda ctx,b:
|
||||
replace_input_buffer(ctx, b) if isinstance(b.arg, ParamArg) and b.addrspace is AddrSpace.GLOBAL else None),
|
||||
# replace SLICE with PARAM. this rewrite is bottom up so BUFFERs we don't need won't be in the input
|
||||
(UPat(Ops.SLICE, src=(UPat(Ops.BUFFER), UPat(Ops.CONST, dtype=dtypes.index)), name="b"), replace_input_buffer),
|
||||
(UPat(Ops.SLICE, src=(UPat(Ops.BUFFER), UPat(Ops.CONST, dtype=dtypes.weakint)), name="b"), replace_input_buffer),
|
||||
# strip value from BIND for cache key normalization, so different values hit same cache
|
||||
(UPat(Ops.BIND, src=(UPat(Ops.PARAM), UPat(Ops.CONST)), name="b"), replace_input_buffer),
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import replace, dataclass
|
||||
import itertools, functools, hashlib, pickle
|
||||
import itertools, functools
|
||||
from tinygrad.helpers import DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
|
||||
from tinygrad.helpers import ALLOW_TF32, TracingKey, Context, panic, CCACHE, diskcache_get, diskcache_put
|
||||
from tinygrad.helpers import ALLOW_TF32, TracingKey, Context, panic
|
||||
from tinygrad.uop.ops import PatternMatcher, graph_rewrite, UOp, pm_lower_index_dtype, Ops, UPat, track_rewrites, KernelInfo, ProgramInfo, GroupOp
|
||||
from tinygrad.uop.ops import AxisType
|
||||
from tinygrad.uop.render import pyrender
|
||||
@@ -17,14 +17,14 @@ from tinygrad.uop.movement import mop_cleanup
|
||||
from tinygrad.codegen.decomp.dtype import pm_dtype_decomps
|
||||
from tinygrad.codegen.decomp.op import get_late_rewrite_patterns, get_simplifying_rewrite_patterns
|
||||
from tinygrad.codegen.decomp.transcendental import get_transcendental_patterns
|
||||
from tinygrad.codegen.late.coalese import indexing_simplify
|
||||
from tinygrad.codegen.late.coalesce import indexing_simplify
|
||||
from tinygrad.codegen.opt.postrange import apply_opts
|
||||
from tinygrad.codegen.late.gater import pm_move_gates_from_index
|
||||
from tinygrad.codegen.simplify import pm_simplify_ranges, pm_flatten_range, pm_split_ranges, pm_load_collapse
|
||||
from tinygrad.schedule.rangeify import pm_mops
|
||||
from tinygrad.codegen.late.linearizer import CFGContext, pm_split_ends, pm_add_control_flow, linearize
|
||||
from tinygrad.codegen.late.regalloc import LinearScanRegallocContext, pm_regalloc_rewrite
|
||||
from tinygrad.codegen.late.coalese import memory_coalesing, pm_simplify_add_image
|
||||
from tinygrad.codegen.late.coalesce import memory_coalescing, pm_simplify_add_image
|
||||
from tinygrad.helpers import all_same, flatten, argsort, partition
|
||||
from tinygrad.uop.ops import _align_left, _broadcast_shape, identity_element
|
||||
from tinygrad.schedule.rangeify import BufferizeOpts
|
||||
@@ -39,8 +39,8 @@ pm_number_params = PatternMatcher([
|
||||
])
|
||||
|
||||
pm_no_index = PatternMatcher([
|
||||
(UPat(GroupOp.ALU.union({Ops.CONST}), dtype=dtypes.index, name="x"), lambda x: x.replace(dtype=dtypes.int)),
|
||||
(UPat(Ops.CAST, dtype=dtypes.index, src=(UPat.var("x"),)), lambda x: x.cast(dtypes.int)),
|
||||
(UPat(GroupOp.ALU.union({Ops.CONST}), dtype=dtypes.weakint, name="x"), lambda x: x.replace(dtype=dtypes.int)),
|
||||
(UPat(Ops.CAST, dtype=dtypes.weakint, src=(UPat.var("x"),)), lambda x: x.cast(dtypes.int)),
|
||||
])
|
||||
|
||||
def build_range_map(sink:UOp) -> dict[int, int]:
|
||||
@@ -110,7 +110,7 @@ def broadcast_and_devec_wmma(b:UOp):
|
||||
for u,shp in zip(b.src, shaped_aligned)]
|
||||
src = []
|
||||
for idx in itertools.product(*[range(i) for i in b.shape[:-1]]):
|
||||
idx_c = [UOp.const(dtypes.index, i) for i in idx]
|
||||
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
|
||||
src.append(b.replace(src=tuple([x.index(*idx_c) for x in src_reshaped])))
|
||||
return UOp.stack(*src).reshape(b.shape)
|
||||
|
||||
@@ -135,7 +135,7 @@ def do_devectorize(b:UOp):
|
||||
if not all_same([x.shape for x in b.src]): return None
|
||||
src = []
|
||||
for idx in itertools.product(*[range(x) for x in b.shape]):
|
||||
idx_c = [UOp.const(dtypes.index, i) for i in idx]
|
||||
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
|
||||
src.append(b.replace(src=tuple([x.index(*idx_c) for x in b.src])))
|
||||
return UOp.stack(*src).reshape(b.shape) if b.op is not Ops.STORE else UOp.group(*src)
|
||||
|
||||
@@ -145,7 +145,7 @@ def do_stack_wmma(u:UOp):
|
||||
src = []
|
||||
for b in u.src:
|
||||
if b.op != Ops.STACK:
|
||||
src.append(UOp.stack(*[b.index(UOp.const(dtypes.index, i)) for i in range(b.max_numel())]))
|
||||
src.append(UOp.stack(*[b.index(UOp.const(dtypes.weakint, i)) for i in range(b.max_numel())]))
|
||||
else:
|
||||
src.append(b)
|
||||
return u.replace(src=tuple(src))
|
||||
@@ -171,13 +171,10 @@ devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
|
||||
# RESHAPE a void is removed (hack for AFTER)
|
||||
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
|
||||
# reshape of a single element shaped value to scalar is an index
|
||||
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.index, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
|
||||
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
|
||||
# EXPAND on scalar -> STACK
|
||||
(UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
|
||||
lambda x,out: UOp.stack(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
|
||||
# INDEX on INDEX is INDEX
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"),
|
||||
lambda idx1, idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:])),
|
||||
])
|
||||
|
||||
def fix_group_for_reduce(x:UOp):
|
||||
@@ -318,11 +315,11 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
# simplify indexing
|
||||
sink = graph_rewrite(sink, indexing_simplify, name="simplify load/store indexing")
|
||||
|
||||
# some coalesing misses without this
|
||||
# some coalescing misses without this
|
||||
sink = graph_rewrite(sink, sym, name="early symbolic")
|
||||
|
||||
# do memory coalesing (late)
|
||||
sink = memory_coalesing(sink, ren)
|
||||
# do memory coalescing (late)
|
||||
sink = memory_coalescing(sink, ren)
|
||||
sink = graph_rewrite(sink, symbolic_simple+ew_devectorizer+pm_simplify_add_image, name="add images", ctx=({}, ren), bottom_up=True)
|
||||
|
||||
# extra symbolic before decomp. crashes without this?
|
||||
@@ -466,10 +463,5 @@ to_program_cache: dict[tuple, UOp] = {}
|
||||
def to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32)
|
||||
key = (ast.key, type(renderer), renderer.target, *[x.value for x in config])
|
||||
if (prg:=to_program_cache.get(key)) is None:
|
||||
disk_key = hashlib.sha256(pickle.dumps(key)).digest()
|
||||
if not CCACHE or (prg:=diskcache_get("program", {"key":disk_key})) is None:
|
||||
prg = do_to_program(ast, renderer)
|
||||
if CCACHE: diskcache_put("program", {"key":disk_key}, prg)
|
||||
to_program_cache[key] = prg
|
||||
if (prg:=to_program_cache.get(key)) is None: to_program_cache[key] = prg = do_to_program(ast, renderer)
|
||||
return prg
|
||||
|
||||
@@ -33,11 +33,14 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
|
||||
case Ops.CAST: return a0.bitcast(dtypes.uint).cast(dt)
|
||||
case Ops.BITCAST: return a0.bitcast(dt), a1.bitcast(dt)
|
||||
case Ops.SHL:
|
||||
lo, hi = shl(a0, b0_mod:=b0 & 31), shl(a1, b0_mod) | shr(shr(a0, 1), 31 - b0_mod)
|
||||
a0u, a1u, n = a0.bitcast(dtypes.uint), a1.bitcast(dtypes.uint), (b0 & 31).cast(dtypes.uint)
|
||||
lo, hi = (a0u << n).bitcast(dt), ((a1u << n) | ((a0u >> 1) >> (31 - n))).bitcast(dt)
|
||||
return (b0 >= 32).where(zero, lo), (b0 >= 32).where(lo, hi)
|
||||
case Ops.SHR:
|
||||
lo, hi = shr(a0, b0_mod:=b0 & 31) | shl(shl(a1, 1), 31 - b0_mod), shr(a1, b0_mod)
|
||||
return (b0 >= 32).where(hi, lo), (b0 >= 32).where(zero, hi)
|
||||
a0u, a1u, n = a0.bitcast(dtypes.uint), a1.bitcast(dtypes.uint), (b0 & 31).cast(dtypes.uint)
|
||||
lo, hi = ((a0u >> n) | ((a1u << 1) << (31 - n))).bitcast(dt), a1 >> (b0 & 31)
|
||||
fill = a1 >> 31 if dt == dtypes.int else zero # vacated high word: sign bits when signed, else 0
|
||||
return (b0 >= 32).where(hi, lo), (b0 >= 32).where(fill, hi)
|
||||
case Ops.ADD: return (low:=a0+b0), (a1 + b1).replace(dtype=dt) + (low.bitcast(dtypes.uint) < a0.bitcast(dtypes.uint)).cast(dt)
|
||||
case Ops.SUB: return a0 - b0, a1 - b1 - (a0.bitcast(dtypes.uint) < b0.bitcast(dtypes.uint)).cast(dt)
|
||||
case Ops.MUL:
|
||||
|
||||
@@ -57,7 +57,7 @@ def add_gpudims(ctx:Renderer, s:UOp):
|
||||
|
||||
# get the idxs
|
||||
ki: KernelInfo = s.arg
|
||||
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.index)]
|
||||
if ctx.has_threads: idxs = [UOp.variable("core_id", 0, int(global_shape[0])-1, dtypes.int).cast(dtypes.weakint)]
|
||||
elif ki.dont_use_locals:
|
||||
assert not local_dims, "can't use locals if there's no local dims"
|
||||
idxs = get_grouped_dims("idx", global_shape, ctx.global_max, reverse=True)
|
||||
|
||||
@@ -97,16 +97,16 @@ pm_simplify_add_image = PatternMatcher([
|
||||
(UPat.var("x", dtype=dtypes.float).cast(dtypes.half).cast(dtypes.float), lambda x: x),
|
||||
])
|
||||
|
||||
def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
|
||||
def memory_coalescing(sink:UOp, ctx:Renderer) -> UOp:
|
||||
if getenv("DMC"): return sink
|
||||
|
||||
# collect
|
||||
memory: defaultdict[tuple[Ops, UOp, UOp|str, UOp], dict[int, list[UOp]]] = defaultdict(dict)
|
||||
for u in sink.toposort():
|
||||
# TODO: this should handle images too, it's just memory coalesing
|
||||
# TODO: this should handle images too, it's just memory coalescing
|
||||
if u.op in {Ops.LOAD, Ops.STORE}:
|
||||
assert len(u.src) == (2 if u.op is Ops.STORE else 1), "memory coalesing does not support gated loads/stores"
|
||||
assert u.src[0].op is Ops.INDEX, f"memory coalesing should be on INDEX, not {u.src[0].op}"
|
||||
assert len(u.src) == (2 if u.op is Ops.STORE else 1), "memory coalescing does not support gated loads/stores"
|
||||
assert u.src[0].op is Ops.INDEX, f"memory coalescing should be on INDEX, not {u.src[0].op}"
|
||||
buf, idx_u = u.src[0].src
|
||||
if buf.addrspace == AddrSpace.REG: continue
|
||||
idx, valid = idx_u.get_idx(), idx_u.get_valid()
|
||||
@@ -141,12 +141,12 @@ def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
|
||||
grouped_offsets = [[x for _,x in group] for _,group in itertools.groupby(enumerate(sorted(offsets.keys())), lambda x: x[1]-x[0])]
|
||||
for full_grp in grouped_offsets:
|
||||
while len(full_grp):
|
||||
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(dtypes.index, full_grp[0])
|
||||
offset = (base+full_grp[0]) if isinstance(base, UOp) else UOp.const(dtypes.weakint, full_grp[0])
|
||||
length = [l for l in lengths if l <= len(full_grp) and (not must_divide or offset.divides(l) is not None)][0]
|
||||
grp = full_grp[:length]
|
||||
# NOTE: we apply the valid again after we determine the length
|
||||
offset = offset.valid(valid) if valid is not None else offset
|
||||
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(dtypes.index, len(grp)))) if len(grp) > 1 else buf.index(offset)
|
||||
idx = UOp(Ops.SHRINK, src=(buf, offset, UOp.const(dtypes.weakint, len(grp)))) if len(grp) > 1 else buf.index(offset)
|
||||
if op == Ops.STORE:
|
||||
datas = []
|
||||
for i,g in enumerate(grp):
|
||||
@@ -158,8 +158,8 @@ def memory_coalesing(sink:UOp, ctx:Renderer) -> UOp:
|
||||
ld = idx.load()
|
||||
for i,g in enumerate(grp):
|
||||
for oo in offsets[g]:
|
||||
replacements[oo] = ld.index(UOp.const(dtypes.index, i)) if len(grp) > 1 else ld
|
||||
replacements[oo] = ld.index(UOp.const(dtypes.weakint, i)) if len(grp) > 1 else ld
|
||||
full_grp = full_grp[length:]
|
||||
|
||||
# apply
|
||||
return sink.substitute(replacements, name="memory coalesing")
|
||||
return sink.substitute(replacements, name="memory coalescing")
|
||||
@@ -2,7 +2,7 @@ import heapq
|
||||
from typing import Any
|
||||
from collections import defaultdict
|
||||
from tinygrad.uop.ops import PatternMatcher, UOp, Ops, UPat, multirange_str
|
||||
from tinygrad.dtype import AddrSpace
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.helpers import prod, getenv, TUPLE_ORDER
|
||||
|
||||
def linearize(sink:UOp) -> list[UOp]:
|
||||
@@ -27,7 +27,7 @@ def linearize(sink:UOp) -> list[UOp]:
|
||||
case Ops.BUFFER: priority = -17 if u.addrspace == AddrSpace.LOCAL else -18
|
||||
case Ops.LOAD: priority = -1 # place loads early
|
||||
case Ops.STORE: priority = 1 # place stores late
|
||||
case Ops.RANGE: priority = 5 # placing RANGE is good
|
||||
case Ops.RANGE | Ops.LOOP: priority = 5 # placing RANGE/LOOP is good
|
||||
case Ops.END: priority = -5 # placing END is bad
|
||||
case _: priority = 0 # everything else has priority 0
|
||||
priorities[u] = (run_count, priority, extra)
|
||||
@@ -66,7 +66,7 @@ class CFGContext:
|
||||
|
||||
if u.op in (Ops.END, Ops.SINK):
|
||||
nesting |= {x:u for x in deps[u] if x.op is Ops.END and (u.op is Ops.SINK or u.src[1] in deps[x]) and x not in nesting}
|
||||
if u.op in (Ops.RANGE, Ops.END): deps[u][u] = None
|
||||
if u.op in (Ops.RANGE, Ops.LOOP, Ops.END): deps[u][u] = None
|
||||
|
||||
self.edges: dict[UOp, UOp] = {}
|
||||
siblings: dict[UOp, list[UOp]] = {}
|
||||
@@ -81,13 +81,14 @@ class CFGContext:
|
||||
self.edges[y.src[1]] = x
|
||||
|
||||
pm_add_control_flow = PatternMatcher([
|
||||
(UPat(Ops.RANGE, name="x"), lambda ctx,x: x.replace(src=x.src+(y,)) if (y:=ctx.edges.get(x)) is not None else None),
|
||||
(UPat((Ops.RANGE, Ops.LOOP), name="x"), lambda ctx,x: x.replace(src=x.src+(y,)) if (y:=ctx.edges.get(x)) is not None else None),
|
||||
])
|
||||
|
||||
def do_split_ends(e:UOp):
|
||||
ret = e.src[0]
|
||||
# only LOOP and its backedge condition are kept from the non-RANGE srcs (SPECIAL/STACK/CONST srcs are dropped like before)
|
||||
ret, others = e.src[0], tuple(x for x in e.src[1:] if x.op is Ops.LOOP or x.dtype == dtypes.bool)
|
||||
for r in sorted(UOp.sink(*e.src[1:]).ranges, key=lambda x: x.arg, reverse=True): ret = ret.end(r)
|
||||
return ret
|
||||
return ret.end(*others) if len(others) else ret
|
||||
|
||||
pm_split_ends = PatternMatcher([
|
||||
# split the ends
|
||||
|
||||
@@ -2,7 +2,7 @@ import itertools
|
||||
from tinygrad.codegen.opt import Opt, OptOps, KernelOptError
|
||||
from tinygrad.helpers import getenv, DEBUG, prod, NOLOCALS, TC_OPT, TC_SELECT, USE_TC, IMAGE
|
||||
from tinygrad.uop.ops import Ops, resolve, AxisType
|
||||
from tinygrad.codegen.late.coalese import image_valid_dims
|
||||
from tinygrad.codegen.late.coalesce import image_valid_dims
|
||||
from tinygrad.codegen.opt.postrange import Scheduler
|
||||
|
||||
def hand_coded_optimizations(k:Scheduler) -> Scheduler:
|
||||
|
||||
@@ -103,7 +103,7 @@ amd_rdna3 = [TensorCore(dims=(16,16,16), threads=32, elements_per_thread=(16,16,
|
||||
opts=("l0","l0","l0","l0","l1","u1","u1","u1"),
|
||||
swizzle=((('l4', 'u0', 'u1', 'u2', 'l0'), ('r1', 'r2', 'r3'), ('l1', 'l2', 'l3', 'r0')),
|
||||
(('l0', 'l1', 'l2', 'l3', 'l4'), ('r1', 'r2', 'r3'), ('u0', 'u1', 'u2', 'r0'))))
|
||||
for di,do in [(dtypes.half,dtypes.float),(dtypes.half,dtypes.half),(dtypes.bfloat16,dtypes.float)]]
|
||||
for di,do in [(dtypes.half,dtypes.float),(dtypes.half,dtypes.half),(dtypes.bfloat16,dtypes.float),(dtypes.int8,dtypes.int32)]]
|
||||
amd_rdna4 = [TensorCore(dims=(16,16,16), threads=32, elements_per_thread=(8,8,8), dtype_in=di, dtype_out=do,
|
||||
opts=("l0","l0","l0","l0","u1","u1","u1","l1"),
|
||||
swizzle=((('u0', 'u1', 'u2', 'l4', 'r2'), ('r0', 'r1', 'r3'), ('l0', 'l1', 'l2', 'l3')),
|
||||
|
||||
@@ -9,7 +9,8 @@ def flatten_range(r:UOp) -> UOp|None:
|
||||
off = range_start[r.op]
|
||||
rngs = r.src[off:]
|
||||
if not len(rngs): return None
|
||||
return r.replace(src=r.src[:off]+tuple(UOp.sink(*rngs).ranges))
|
||||
# keep only LOOP and its backedge condition from the non-RANGE srcs
|
||||
return r.replace(src=r.src[:off]+tuple(UOp.sink(*rngs).ranges)+tuple(x for x in rngs if x.op is Ops.LOOP or x.dtype == dtypes.bool))
|
||||
|
||||
pm_flatten_range = PatternMatcher([
|
||||
# real ranges only
|
||||
@@ -19,6 +20,7 @@ pm_flatten_range = PatternMatcher([
|
||||
# index/range arithmetic uses FLOORDIV/FLOORMOD prior to late rewrite
|
||||
def count_divmod(x:UOp) -> int: return sum(u.op in {Ops.FLOORDIV, Ops.FLOORMOD} for u in x.backward_slice)
|
||||
def simplify_merge_adjacent(u:UOp) -> UOp|None:
|
||||
if not all(r.op is Ops.RANGE for r in u.ended_ranges): return None
|
||||
reduce_ranges = [x.ranges for x in u.backward_slice_with_self if x.op is Ops.REDUCE]
|
||||
# on END we only want to merge adjacent ranges, on REDUCE we want to try all combinations
|
||||
for r0, r1 in (zip(u.ended_ranges, u.ended_ranges[1:]) if u.op is Ops.END else itertools.permutations(u.ended_ranges, 2)):
|
||||
@@ -149,5 +151,5 @@ def no_load(u:UOp) -> bool: return not any(x.op is Ops.INDEX for x in u.backward
|
||||
pm_load_collapse = PatternMatcher([
|
||||
(UPat(Ops.REDUCE, arg=(Ops.ADD, 0), src=(UPat.var("u"), UPat()), name="red"), reduce_load_collapse),
|
||||
# we want to make sure we dont do math on a loaded index since that can cause overflow, this undoes the rule in pm_reduce_load_collapse
|
||||
((UPat.var("x", dtypes.index)+UPat.var("y"))<UPat.var("c"), lambda x,y,c: x < c-y if no_load(y) and no_load(c) and not no_load(x) else None),
|
||||
((UPat.var("x", dtypes.weakint)+UPat.var("y"))<UPat.var("c"), lambda x,y,c: x < c-y if no_load(y) and no_load(c) and not no_load(x) else None),
|
||||
])
|
||||
|
||||
+10
-10
@@ -89,7 +89,7 @@ class dtypes:
|
||||
def is_float(x: DType) -> bool: return x in (dtypes.floats + (dtypes.weakfloat,))
|
||||
@staticmethod # static methods on top, or bool in the type info will refer to dtypes.bool
|
||||
@functools.cache
|
||||
def is_int(x: DType) -> bool: return x in (dtypes.ints + (dtypes.weakint, dtypes.index))
|
||||
def is_int(x: DType) -> bool: return x in (dtypes.ints + (dtypes.weakint,))
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def is_unsigned(x: DType) -> bool: return x in dtypes.uints
|
||||
@@ -111,8 +111,7 @@ class dtypes:
|
||||
return {dtypes.float16: (5, 10), dtypes.bfloat16: (8, 7), dtypes.float32: (8, 23), dtypes.float64: (11, 52),
|
||||
dtypes.fp8e4m3: (4, 3), dtypes.fp8e5m2: (5, 2), dtypes.fp8e4m3fnuz: (4, 3), dtypes.fp8e5m2fnuz: (5, 2)}[dtype]
|
||||
void: Final[DType] = DType.new(-1, 0, "void", None)
|
||||
weakint: Final[DType] = DType.new(0, 800, "weakint", None)
|
||||
index: Final[DType] = DType.new(0, 800, "index", None) # NOTE: not in the promo lattice: index math never mixes dtypes
|
||||
weakint: Final[DType] = DType.new(0, 800, "weakint", None) # NOTE: not in the promo lattice: index math never mixes dtypes
|
||||
bool: Final[DType] = DType.new(0, 1, "bool", '?')
|
||||
int8: Final[DType] = DType.new(1, 8, "signed char", 'b')
|
||||
uint8: Final[DType] = DType.new(2, 8, "unsigned char", 'B')
|
||||
@@ -154,7 +153,7 @@ class dtypes:
|
||||
uints = (uint8, uint16, uint32, uint64)
|
||||
sints = (int8, int16, int32, int64)
|
||||
ints = uints + sints
|
||||
weaks = (weakint, weakfloat)
|
||||
weaks = (weakfloat,)
|
||||
all = floats + ints + (bool,) # noqa: A003
|
||||
|
||||
if (env_default_float := getenv("DEFAULT_FLOAT", "")):
|
||||
@@ -164,12 +163,13 @@ if (env_default_float := getenv("DEFAULT_FLOAT", "")):
|
||||
DTypeLike = str|DType
|
||||
def to_dtype(dtype:DTypeLike) -> DType: return dtype if isinstance(dtype, DType) else getattr(dtypes, dtype.lower())
|
||||
def strong_dtype(dtype:DType) -> DType:
|
||||
return dtypes.default_int if dtype == dtypes.weakint else dtypes.default_float if dtype == dtypes.weakfloat else dtype
|
||||
# TODO: weakint
|
||||
return dtypes.default_float if dtype == dtypes.weakfloat else dtype
|
||||
|
||||
# https://jax.readthedocs.io/en/latest/jep/9407-type-promotion.html
|
||||
# we don't support complex type
|
||||
promo_lattice = { dtypes.bool: [dtypes.weakint], dtypes.weakint: [dtypes.int8, dtypes.uint8],
|
||||
dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
|
||||
# TODO: weakint
|
||||
promo_lattice = { dtypes.bool: [dtypes.int8, dtypes.uint8], dtypes.int8: [dtypes.int16], dtypes.int16: [dtypes.int32], dtypes.int32: [dtypes.int64],
|
||||
dtypes.int64: [dtypes.uint64], dtypes.uint8: [dtypes.int16, dtypes.uint16], dtypes.uint16: [dtypes.int32, dtypes.uint32],
|
||||
dtypes.uint32: [dtypes.int64, dtypes.uint64], dtypes.uint64: [dtypes.weakfloat],
|
||||
dtypes.weakfloat: [dtypes.fp8e4m3, dtypes.fp8e5m2, dtypes.fp8e4m3fnuz, dtypes.fp8e5m2fnuz],
|
||||
@@ -185,8 +185,8 @@ def least_upper_dtype(*ds:DType) -> DType:
|
||||
return min(set.intersection(*[_get_recursive_parents(d) for d in ds]))
|
||||
def least_upper_float(dt:DType) -> DType: return dt if dtypes.is_float(dt) else least_upper_dtype(dt, dtypes.default_float)
|
||||
|
||||
DTYPES_DICT = {k: v for k, v in dtypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "index", "_"))}
|
||||
INVERSE_DTYPES_DICT = {**{v.name:k for k,v in DTYPES_DICT.items()}, "void": "void", "weakint":"weakint", "index":"index", "weakfloat":"weakfloat"}
|
||||
DTYPES_DICT = {k: v for k, v in dtypes.__dict__.items() if isinstance(v, DType) and not k.startswith(("default", "void", "weak", "_"))}
|
||||
INVERSE_DTYPES_DICT = {**{v.name:k for k,v in DTYPES_DICT.items()}, "void": "void", "weakint":"weakint", "weakfloat":"weakfloat"}
|
||||
|
||||
@functools.cache
|
||||
def can_lossless_cast(dt0:DType, dt1:DType) -> bool:
|
||||
@@ -194,7 +194,7 @@ def can_lossless_cast(dt0:DType, dt1:DType) -> bool:
|
||||
# similar to https://numpy.org/doc/stable/reference/generated/numpy.can_cast.html
|
||||
if dt0 == dt1 or dt0 == dtypes.bool: return True
|
||||
match dt1:
|
||||
case dtypes.weakint | dtypes.index: return dt0 in dtypes.ints
|
||||
case dtypes.weakint: return dt0 in dtypes.ints
|
||||
case dtypes.double: return dt0 in (dtypes.float, dtypes.half, dtypes.bfloat16, *dtypes.fp8s,
|
||||
dtypes.uint32, dtypes.uint16, dtypes.uint8, dtypes.int32, dtypes.int16, dtypes.int8)
|
||||
case dtypes.float: return dt0 in (dtypes.half, dtypes.bfloat16, *dtypes.fp8s, dtypes.uint16, dtypes.uint8, dtypes.int16, dtypes.int8)
|
||||
|
||||
@@ -39,6 +39,7 @@ def graph_split_rewrite(linear:UOp, max_batch_size:int=0) -> UOp:
|
||||
if len(current_batch) <= 1 and not getenv("GRAPH_ONE_KERNEL"): new_src.extend(current_batch)
|
||||
else:
|
||||
new_src.append(create_graph_call(current_batch))
|
||||
max_batch_size *= 2
|
||||
if DEBUG >= 2: print(f"JIT GRAPHing batch with {len(current_batch)} kernels")
|
||||
current_batch, current_batch_devs = [], []
|
||||
|
||||
|
||||
+1
-2
@@ -81,7 +81,6 @@ def to_be32(val:Any) -> Any: return ((val & 0xFF) << 24) | (((val >> 8) & 0xFF)
|
||||
def to_be64(val:Any) -> Any: return to_be32(val >> 32) | (to_be32(val & 0xFFFFFFFF) << 32)
|
||||
def getbits(value: int, start: int, end: int): return (value >> start) & ((1 << (end - start + 1)) - 1)
|
||||
def i2u(bits: int, value: int): return value if value >= 0 else (1<<bits)+value
|
||||
def is_numpy_ndarray(x) -> bool: return str(type(x)) == "<class 'numpy.ndarray'>"
|
||||
def merge_dicts(ds:Iterable[dict[T,U]]) -> dict[T,U]:
|
||||
kvs = set([(k,v) for d in ds for k,v in d.items()])
|
||||
if len(kvs) != len(set(kv[0] for kv in kvs)): raise RuntimeError(f"{kvs} contains different values for the same key")
|
||||
@@ -413,7 +412,7 @@ def diskcache_get(table:str, key:dict|str|int) -> Any:
|
||||
if (val:=res.fetchone()) is not None: return pickle.loads(val[0])
|
||||
return None
|
||||
|
||||
_db_tables = set()
|
||||
_db_tables: set[str] = set()
|
||||
def diskcache_put(table:str, key:dict|str|int, val:Any, prepickled=False):
|
||||
if CACHELEVEL < 1: return val
|
||||
if isinstance(key, (str,int)): key = {"key": key}
|
||||
|
||||
+22
-46
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
import sys, argparse, codecs, typing, re, unicodedata, json, time
|
||||
import sys, argparse, codecs, itertools, typing, re, unicodedata, json, time
|
||||
from typing import TYPE_CHECKING
|
||||
from tinygrad.helpers import BEAM, DEBUG, JIT_BATCH_SIZE, Timing, GlobalCounters, Context, fetch, profile_marker, getenv
|
||||
from tinygrad import nn
|
||||
from tinygrad.uop.ops import UOp, Ops
|
||||
from tinygrad.helpers import partition, DEBUG, Timing, GlobalCounters, Context, fetch, profile_marker, getenv
|
||||
from tinygrad.llm.model import Transformer
|
||||
if TYPE_CHECKING:
|
||||
import jinja2
|
||||
@@ -18,36 +20,28 @@ class SimpleTokenizer:
|
||||
|
||||
# https://github.com/ggml-org/llama.cpp/blob/94933c8c2eeaa9a7983e3f6c08af76bd86724094/src/llama-vocab.cpp#L286
|
||||
# 0x323b0 is one past the max codepoint in unicode categories L/N/Z (0x323af is max L)
|
||||
# Compact adjacent codepoints into ranges. Build L/N/Z together: scanning Unicode three times is measurable at server startup.
|
||||
runs: dict[str, list[tuple[int, int]]] = {pre:[] for pre in "LNZ"}
|
||||
for cp in range(0x323b0):
|
||||
if (pre:=unicodedata.category(chr(cp))[0]) not in runs: continue
|
||||
if runs[pre] and cp == runs[pre][-1][1]+1: runs[pre][-1] = (runs[pre][-1][0], cp)
|
||||
else: runs[pre].append((cp, cp))
|
||||
# compact adjacent codepoints into ranges: listing them all makes re spend seconds on large prompts
|
||||
def ucat_range(pre:str) -> str:
|
||||
def esc(cp:int) -> str: return f"\\U{cp:08x}"
|
||||
return "".join(esc(st) if st == en else f"{esc(st)}-{esc(en)}" for st,en in runs[pre])
|
||||
cps = enumerate(cp for cp in range(0x323b0) if unicodedata.category(chr(cp)).startswith(pre))
|
||||
runs = [list(g) for _, g in itertools.groupby(cps, lambda e: e[1]-e[0])]
|
||||
return "".join(re.escape(chr(g[0][1])) + (f"-{re.escape(chr(g[-1][1]))}" if len(g) > 1 else "") for g in runs)
|
||||
r_ws, r_p_N, r_p_L = r"\t\n\x0b\x0c\r\x85" + ucat_range("Z"), ucat_range("N"), ucat_range("L")
|
||||
self._split_to_word = re.compile("(?i:'s|'t|'re|'ve|'m|'ll|'d)|" + \
|
||||
f"[^\\r\\n{r_p_N}{r_p_L}]?[{r_p_L}]+|[{r_p_N}]{{1,3}}| ?[^{r_ws}{r_p_N}{r_p_L}]+[\\r\\n]*|[{r_ws}]*[\\r\\n]+|[{r_ws}]+(?![^{r_ws}])|[{r_ws}]+")
|
||||
self._split_to_sentence = re.compile("|".join(re.escape(tok) for tok in special_tokens.keys()) if special_tokens else r"(?!)")
|
||||
|
||||
byte_translation = str.maketrans(self._byte_decoder)
|
||||
self._normal_tokens = {tok.translate(byte_translation).encode("latin1"): tid for tok, tid in normal_tokens.items()}
|
||||
self._normal_tokens = {bytes(self._byte_decoder[c] for c in tok): tid for tok, tid in normal_tokens.items()}
|
||||
self._special_tokens = special_tokens
|
||||
self._tok2bytes = {tid: tok for tok, tid in self._normal_tokens.items()} | {tid: tok.encode() for tok, tid in self._special_tokens.items()}
|
||||
self._encode_cache: tuple[str, tuple[int, ...], list[tuple[int, int]]]|None = None
|
||||
self.preset = preset
|
||||
self.bos_id, self.eos_id, self.eot_id = bos_id, eos_id, eot_id
|
||||
|
||||
@staticmethod
|
||||
def from_gguf_kv(kv:dict):
|
||||
# https://github.com/ggml-org/llama.cpp/blob/94933c8c2eeaa9a7983e3f6c08af76bd86724094/src/llama-vocab.cpp#L1818-L1820
|
||||
normal_tokens: dict[str, int] = {}
|
||||
special_tokens: dict[str, int] = {}
|
||||
for idx,(tok,token_type) in enumerate(zip(kv["tokenizer.ggml.tokens"], kv["tokenizer.ggml.token_type"])):
|
||||
(normal_tokens if token_type == 1 else special_tokens)[tok] = idx
|
||||
return SimpleTokenizer(normal_tokens, special_tokens, kv["tokenizer.ggml.pre"],
|
||||
vocab: typing.Iterable[tuple[str, int]] = ((tok, idx) for idx, tok in enumerate(kv["tokenizer.ggml.tokens"]))
|
||||
normal_tokens, special_tokens = partition(vocab, lambda e: kv["tokenizer.ggml.token_type"][e[1]] == 1)
|
||||
return SimpleTokenizer(dict(normal_tokens), dict(special_tokens), kv["tokenizer.ggml.pre"],
|
||||
bos_id=kv.get('tokenizer.ggml.bos_token_id') if kv.get('tokenizer.ggml.add_bos_token', True) else None,
|
||||
eos_id=kv.get('tokenizer.ggml.eos_token_id', 0), eot_id=kv.get('tokenizer.ggml.eot_token_id'))
|
||||
|
||||
@@ -66,23 +60,10 @@ class SimpleTokenizer:
|
||||
def encode(self, text:str) -> list[int]:
|
||||
tokens: list[int] = []
|
||||
pos = 0
|
||||
checkpoints: list[tuple[int, int]] = []
|
||||
if self._encode_cache is not None:
|
||||
old_text, old_tokens, old_checkpoints = self._encode_cache
|
||||
if text == old_text: return list(old_tokens)
|
||||
common, limit = 0, min(len(text), len(old_text))
|
||||
while common+4096 <= limit and text[common:common+4096] == old_text[common:common+4096]: common += 4096
|
||||
common += next((i for i,(a,b) in enumerate(zip(text[common:limit], old_text[common:limit])) if a != b), limit-common)
|
||||
if (checkpoint := next((x for x in reversed(old_checkpoints) if x[0] <= common), None)) is not None:
|
||||
pos, token_pos = checkpoint
|
||||
tokens, checkpoints = list(old_tokens[:token_pos]), [x for x in old_checkpoints if x[0] <= pos]
|
||||
for match in self._split_to_sentence.finditer(text, pos):
|
||||
for match in self._split_to_sentence.finditer(text):
|
||||
tokens.extend(self._encode_sentence(text[pos:match.start(0)]) + [self._special_tokens[text[match.start(0):match.end(0)]]])
|
||||
pos = match.end(0)
|
||||
checkpoints.append((pos, len(tokens)))
|
||||
tokens += self._encode_sentence(text[pos:])
|
||||
self._encode_cache = text, tuple(tokens), checkpoints
|
||||
return tokens
|
||||
return tokens + self._encode_sentence(text[pos:])
|
||||
|
||||
def decode(self, ids:list[int]) -> str: return b''.join(self._tok2bytes[tid] for tid in ids).decode(errors='replace')
|
||||
def stream_decoder(self) -> typing.Callable[..., str]:
|
||||
@@ -131,8 +112,7 @@ class FallbackTemplate:
|
||||
if self.tok.preset == 'glm4': return ""
|
||||
if self.tok.preset == 'tekken': return "[/INST]"
|
||||
return self.tok.decode([self.tok.eos_id])
|
||||
def render(self, messages:list[dict], tools=None, add_generation_prompt:bool=True, enable_thinking:bool=False,
|
||||
preserve_thinking:bool=False) -> str:
|
||||
def render(self, messages:list[dict], tools=None, add_generation_prompt:bool=True) -> str:
|
||||
out = self.tok.decode([] if self.tok.bos_id is None else [self.tok.bos_id]) + ("<sop>" if self.tok.preset == 'glm4' else "")
|
||||
for msg in messages:
|
||||
out += self.role(msg["role"])
|
||||
@@ -154,16 +134,15 @@ def main():
|
||||
parser.add_argument("--max_context", type=int, default=4096, help="Max Context Length")
|
||||
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("--beam", type=int, help="Kernel optimization beam width")
|
||||
parser.add_argument("--benchmark", nargs='?', type=int, const=20, metavar="COUNT", help="Benchmark tok/s (optional count, default 20)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# load the model
|
||||
model_path = fetch(models.get(args.model, args.model))
|
||||
model, kv = Transformer.from_gguf(model_path, args.max_context)
|
||||
model, kv = Transformer.from_gguf(fetch(models.get(args.model, args.model)), args.max_context)
|
||||
model_name = kv.get('general.name') or kv.get('general.basename') or args.model
|
||||
print(f"using model \"{model_name}\" with {model_path.stat().st_size:,} bytes and {model.parameter_count:,} params, "
|
||||
f"max context {model.max_context} on {model.token_embd.weight.device}")
|
||||
file_sizes = [y.nbytes() for y in UOp.sink(*[x.uop for x in nn.state.get_parameters(model)]).toposort() if y.op is Ops.BUFFER]
|
||||
print(f"using model \"{model_name}\" with {sum(file_sizes):,} bytes and {sum(x.numel() for x in nn.state.get_parameters(model)):,} params, "
|
||||
f"max context {args.max_context} on {nn.state.get_parameters(model)[0].device}")
|
||||
|
||||
# get tokenizer
|
||||
tok = SimpleTokenizer.from_gguf_kv(kv)
|
||||
@@ -184,12 +163,9 @@ def main():
|
||||
|
||||
# warmup the JIT
|
||||
if args.warmup or args.serve:
|
||||
amd_server = bool(args.serve) and str(model.token_embd.weight.device).startswith("AMD")
|
||||
beam = args.beam if args.beam is not None else 2 if amd_server else BEAM.value
|
||||
print(f"warming serving JITs with BEAM={beam}")
|
||||
batch_size = 448 if amd_server else JIT_BATCH_SIZE.value
|
||||
with Context(DEBUG=DEBUG.value, BEAM=beam, JIT_BATCH_SIZE=batch_size):
|
||||
model.warmup()
|
||||
# run 2 tokens through the model twice to capture the JIT before serving
|
||||
with Context(DEBUG=max(DEBUG.value, 1)):
|
||||
for _ in range(2): list(zip(range(2), model.generate([0])))
|
||||
|
||||
# start server
|
||||
if args.serve: LLMServer(('', args.serve), model, model_name, tok, template).serve_forever()
|
||||
|
||||
+8
-23
@@ -1,8 +1,7 @@
|
||||
import functools, io, pathlib, re, struct, weakref
|
||||
import functools, io, pathlib, re, struct
|
||||
from typing import Any, Callable
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.uop.ops import UOp
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.helpers import prod, round_up
|
||||
from tinygrad.nn.state import TensorIO
|
||||
@@ -21,14 +20,7 @@ _GGML_NATIVE = {0: dtypes.float32, 1: dtypes.float16, 24: dtypes.int8, 25: dtype
|
||||
_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)}
|
||||
|
||||
_quantized_tensors:weakref.WeakKeyDictionary[UOp, tuple[UOp, int]] = weakref.WeakKeyDictionary()
|
||||
|
||||
def get_ggml_quantization(tensor:Tensor) -> tuple[Tensor, int]|None:
|
||||
if (meta:=_quantized_tensors.get(tensor.uop)) is None: return None
|
||||
packed, ggml_type = meta
|
||||
return Tensor(packed), ggml_type
|
||||
|
||||
def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int, contiguous:bool=True) -> Tensor:
|
||||
def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int) -> Tensor:
|
||||
"""
|
||||
Converts ggml tensor data to a tinygrad tensor.
|
||||
|
||||
@@ -43,14 +35,14 @@ def ggml_data_to_tensor(t: Tensor, n: int, ggml_type: int, contiguous:bool=True)
|
||||
if (dtype := _GGML_NATIVE.get(ggml_type)) is not None:
|
||||
return t[:dtype.itemsize * n].contiguous().bitcast(dtype)
|
||||
|
||||
def q_to_uint8(t:Tensor, b:int) -> Tensor:
|
||||
shift_tensor, bitmask = Tensor.stack(*[Tensor(2**(i*b), device=t.device, dtype=t.dtype) for i in range(8//b)]), 0xff >> (8-b)
|
||||
return t.unsqueeze(-1).expand((*t.shape, 8//b)).div(shift_tensor, rounding_mode="trunc").bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
|
||||
def q_to_uint8(t: Tensor, b: int) -> Tensor:
|
||||
# TODO: rewrite with arange?
|
||||
shift_tensor, bitmask = Tensor.stack(*[ Tensor(2**(i*b), device=t.device, dtype=t.dtype) for i in range(8//b) ]), 0xff >> (8 - b)
|
||||
return t.unsqueeze(-1).expand((*t.shape,8//b)).div(shift_tensor, rounding_mode="trunc").bitwise_and(bitmask).transpose(-1, -2).flatten(-2)
|
||||
|
||||
if (nelements_nbytes := _GGML_QUANT.get(ggml_type)) is not None:
|
||||
from tinygrad.runtime.autogen import ggml_common as _ggml
|
||||
blocks = t[:(n//nelements_nbytes[0])*nelements_nbytes[1]].reshape((-1, nelements_nbytes[1]))
|
||||
if contiguous: blocks = blocks.contiguous()
|
||||
blocks = t[:(n//nelements_nbytes[0])*nelements_nbytes[1]].reshape((-1, nelements_nbytes[1])).contiguous()
|
||||
if ggml_type == 2: return (q_to_uint8(blocks[:,2:], 4).bitcast(dtypes.int8) - 8) * blocks[:,:2].bitcast(dtypes.float16).cast(dtypes.float32)
|
||||
if ggml_type == 3:
|
||||
d, m = (blocks[:,s:s+2].bitcast(dtypes.float16).cast(dtypes.float32) for s in [ 0, 2 ])
|
||||
@@ -154,14 +146,7 @@ def _gguf_parse(tensor: Tensor) -> tuple[dict, dict[str, Tensor]]:
|
||||
alignment, pos = kv_data.get("general.alignment", 32), r.tell()
|
||||
data_start = round_up(pos, alignment)
|
||||
|
||||
state_dict = {}
|
||||
for name, dims, typ, off in t_infos:
|
||||
n, shape = prod(dims), tuple(reversed(dims))
|
||||
decoded = ggml_data_to_tensor(data:=tensor[data_start + off:], n, typ).reshape(*shape)
|
||||
if typ in _GGML_QUANT:
|
||||
block_size, type_size = _GGML_QUANT[typ]
|
||||
_quantized_tensors[decoded.uop] = (data[:n//block_size*type_size].uop, typ)
|
||||
state_dict[name] = decoded
|
||||
state_dict = {name: ggml_data_to_tensor(tensor[data_start + off:], prod(dims), typ).reshape(*reversed(dims)) for name, dims, typ, off in t_infos}
|
||||
return kv_data, state_dict
|
||||
|
||||
def _gguf_split_paths(path: pathlib.Path, kv: dict) -> list[pathlib.Path]:
|
||||
|
||||
+91
-841
File diff suppressed because it is too large
Load Diff
+10
-17
@@ -18,11 +18,7 @@ def parse_tool_call(s:str) -> tuple[str, typing.Any]|None:
|
||||
if (fm := re.match(r"<function=([^>]+)>\s*(.*?)\s*(?:</function>)?$", s, re.DOTALL)):
|
||||
args = {}
|
||||
for pm in re.finditer(r"<parameter=([^>]+)>(.*?)</parameter>", fm.group(2), re.DOTALL):
|
||||
value = pm.group(2)
|
||||
if value.startswith("\r\n"): value = value[2:]
|
||||
elif value.startswith("\n"): value = value[1:]
|
||||
if value.endswith("\r\n"): value = value[:-2]
|
||||
elif value.endswith("\n"): value = value[:-1]
|
||||
value = re.sub(r"^\r?\n|\r?\n\Z", "", pm.group(2))
|
||||
try: args[pm.group(1)] = json.loads(value)
|
||||
except json.JSONDecodeError: args[pm.group(1)] = value
|
||||
return fm.group(1), args
|
||||
@@ -38,9 +34,9 @@ def normalize_messages(messages:list[dict]) -> None:
|
||||
|
||||
class StreamRouter:
|
||||
# routes streamed output text to (field, text) deltas, keeping tool_call regions in .buf for the final parse
|
||||
def __init__(self, thinking:bool=False):
|
||||
def __init__(self):
|
||||
self.buf = ""
|
||||
self.mode = "reasoning" if thinking else "undecided" # output inside a think block is sent as reasoning_content
|
||||
self.mode = "undecided" # output inside a think block is sent as reasoning_content
|
||||
def split(self, tag:str, final:bool) -> tuple[str, bool]:
|
||||
# split buf on the first full tag, holding back a partial tag at the end unless final
|
||||
if tag in self.buf:
|
||||
@@ -70,9 +66,8 @@ class Handler(HTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/v1/models": self.send_data(json.dumps({"object":"list","data":[{"id":self.server.model_name,"object":"model"}]}).encode())
|
||||
else: self.send_data((pathlib.Path(__file__).parent / "chat.html").read_bytes(), content_type="text/html")
|
||||
def run_model(self, ids:list[int], model_name:str, include_usage=False, max_tokens:int|None=None, temperature:float=0.0, thinking:bool=False):
|
||||
def run_model(self, ids:list[int], model_name:str, include_usage=False, max_tokens:int|None=None, temperature:float=0.0):
|
||||
model, tok = self.server.model, self.server.tok
|
||||
prompt_tokens = len(ids)
|
||||
cache_start_pos = model.get_start_pos(ids)
|
||||
stderr_log(f"in:{colored(f'{cache_start_pos:5d}', 'green')} +{len(ids)-cache_start_pos:5d} {colored('--', 'BLACK')} ")
|
||||
tmpl = {"id":f"chatcmpl-{uuid.uuid4().hex[:24]}", "object":"chat.completion.chunk", "created":int(time.time()), "model":model_name}
|
||||
@@ -82,9 +77,9 @@ class Handler(HTTPRequestHandler):
|
||||
finish_reason = "stop"
|
||||
st = time.perf_counter()
|
||||
dec = tok.stream_decoder()
|
||||
router = StreamRouter(thinking)
|
||||
router = StreamRouter()
|
||||
for next_id in model.generate(ids, temperature=temperature):
|
||||
if len(out) == 0: stderr_log(f"prefill:{(prompt_tokens-cache_start_pos)/((pt:=time.perf_counter())-st):4.0f} tok/s {colored('--', 'BLACK')} ")
|
||||
if len(out) == 0: stderr_log(f"prefill:{(len(ids)-cache_start_pos)/((pt:=time.perf_counter())-st):4.0f} tok/s {colored('--', 'BLACK')} ")
|
||||
if tok.is_end(next_id): break
|
||||
out.append(next_id)
|
||||
for field, delta in router.route(dec(next_id)): yield chunk({field:delta})
|
||||
@@ -106,9 +101,7 @@ class Handler(HTTPRequestHandler):
|
||||
if finish_reason == "stop": finish_reason = "tool_calls"
|
||||
yield {"choices": [{"index":0, "delta":{},"finish_reason":finish_reason}], **tmpl}
|
||||
if include_usage:
|
||||
yield {"choices": [], "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": len(out),
|
||||
"total_tokens": prompt_tokens + len(out),
|
||||
"prompt_tokens_details":{"cached_tokens":cache_start_pos}}, **tmpl}
|
||||
yield {"choices": [], "usage": {"prompt_tokens": len(ids), "completion_tokens": len(out), "total_tokens": len(ids) + len(out)}, **tmpl}
|
||||
et = time.perf_counter()
|
||||
stderr_log(f"gen:{len(out)/(et-pt) if len(out) > 1 else 0:4.0f} tok/s {colored('--', 'BLACK')} "
|
||||
f"out:{len(out):5d} {colored('--', 'BLACK')} total:{et-st:6.2f}s\n")
|
||||
@@ -122,11 +115,11 @@ class Handler(HTTPRequestHandler):
|
||||
if self.path == "/v1/chat/completions":
|
||||
# render and tokenize
|
||||
normalize_messages(body["messages"])
|
||||
rendered = self.server.template.render(messages=body["messages"], tools=body.get("tools"), add_generation_prompt=True,
|
||||
enable_thinking=body.get("enable_thinking", False), preserve_thinking=True)
|
||||
rendered = self.server.template.render(messages=body["messages"], tools=body.get("tools"), add_generation_prompt=True)
|
||||
ids: list[int] = self.server.tok.encode(rendered)
|
||||
stderr_log(f"prep:{(time.perf_counter()-request_st)*1e3:5.0f} ms {colored('--', 'BLACK')} ")
|
||||
if len(ids) >= self.server.model.max_context:
|
||||
stderr_log(f"{colored('context length exceeded', 'red')} in:{len(ids):5d} max:{self.server.model.max_context:5d}\n")
|
||||
return self.send_data(json.dumps({"error":{"message":f"prompt has {len(ids)} tokens, but the model context is "
|
||||
f"{self.server.model.max_context}", "type":"invalid_request_error", "param":"messages", "code":"context_length_exceeded"}}).encode(),
|
||||
status_code=400)
|
||||
@@ -134,7 +127,7 @@ class Handler(HTTPRequestHandler):
|
||||
# reply
|
||||
max_tokens = body.get("max_completion_tokens") or body.get("max_tokens")
|
||||
chunks = self.run_model(ids, body["model"], not body.get("stream") or body.get("stream_options",{}).get("include_usage", False),
|
||||
max_tokens=max_tokens, temperature=float(body.get("temperature", 0.0)), thinking=body.get("enable_thinking", False))
|
||||
max_tokens=max_tokens, temperature=float(body.get("temperature", 0.0)))
|
||||
if body.get("stream"): self.stream_json(chunks)
|
||||
else:
|
||||
out, reasoning, tool_calls, finish_reason = [], [], [], "stop"
|
||||
|
||||
@@ -49,6 +49,7 @@ class ElementwiseMixin(CreationMixin):
|
||||
"""
|
||||
Returns a contiguous tensor.
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {self.dtype}")
|
||||
uop = self._uop
|
||||
if uop.op is Ops.CONTIGUOUS or self.device is None or uop.has_buffer_identity(): return self._wrap_uop(uop)
|
||||
return self._wrap_uop(uop.alu(Ops.CONTIGUOUS, **kwargs))
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Self, Sequence
|
||||
from tinygrad.uop import Ops
|
||||
from tinygrad.helpers import prod, argfix, argsort, flatten, dedup, make_tuple, ceildiv, round_up, all_int
|
||||
from tinygrad.uop.ops import resolve, smax, _align_left, _broadcast_shape
|
||||
from tinygrad.uop.ops import resolve, smax, _align_left, _broadcast_shape, broadcast_axes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.uop.ops import sint
|
||||
@@ -125,7 +125,7 @@ class MovementMixin:
|
||||
raise ValueError(f"cannot broadcast {self.shape} to {new_shape=}")
|
||||
# EXPAND only adds dims on the left. squeeze 1s that need expanding, EXPAND on left, permute back.
|
||||
n_left = len(new_shape) - len(self.shape)
|
||||
expand_at = tuple(i for i, s in enumerate(self.shape) if resolve(s == 1, default=False) and resolve(new_shape[n_left+i] != 1))
|
||||
expand_at = tuple(i-n_left for i in broadcast_axes(self.shape, new_shape) if i >= n_left)
|
||||
kept = tuple(i for i in range(len(self.shape)) if i not in expand_at)
|
||||
squeezed = self.reshape(tuple(self.shape[i] for i in kept))
|
||||
expanded = squeezed._mop(Ops.EXPAND, arg=new_shape[:n_left] + tuple(new_shape[n_left+i] for i in expand_at))
|
||||
|
||||
@@ -360,11 +360,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
def _broadcasted(self, y:Self|ConstType|UOp, reverse:bool=False) -> tuple[Self, Self]:
|
||||
if not isinstance(y, type(self)): y = self.ufix(y)
|
||||
x, y = (self, y) if not reverse else (y, self)
|
||||
# ValueError: unsized ptr has shape (-1,) which can't broadcast; RuntimeError: shape mismatch
|
||||
try:
|
||||
out_shape = _broadcast_shape(x.shape, y.shape)
|
||||
x, y = x._broadcast_to(out_shape), y._broadcast_to(out_shape)
|
||||
except (RuntimeError, ValueError): pass
|
||||
out_shape = _broadcast_shape(x.shape, y.shape)
|
||||
x, y = x._broadcast_to(out_shape), y._broadcast_to(out_shape)
|
||||
if x.dtype == y.dtype: return x, y
|
||||
return x.cast(out_dtype := least_upper_dtype(x.dtype, y.dtype)), y.cast(out_dtype)
|
||||
|
||||
@@ -658,7 +655,7 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
print(t.logsumexp(axis=1).numpy())
|
||||
```
|
||||
"""
|
||||
m = self.max(axis=axis, keepdim=True)
|
||||
m = self.max(axis=axis, keepdim=True).detach()
|
||||
return (self - m).exp().sum(axis=axis, keepdim=keepdim).log() + (m if keepdim else m.squeeze(axis))
|
||||
|
||||
def _softmax(self, axis, dtype:DTypeLike|None=None) -> tuple[Self, Self, Self]:
|
||||
@@ -841,8 +838,8 @@ class OpMixin(ElementwiseMixin, ReduceMixin):
|
||||
x = self.transpose(axis, -1)
|
||||
last_dim_size = x.shape[-1]
|
||||
x_unsqueezed = x.unsqueeze(-2).expand((None,)*(self.ndim-1)+(last_dim_size, None))
|
||||
x_cummax, _ = x.cummax(-1)
|
||||
mask = type(self).ones(last_dim_size, last_dim_size, buffer=False).tril()
|
||||
x_cummax = x.cummax(-1)[0].detach()
|
||||
mask = type(self).ones(last_dim_size, last_dim_size, buffer=False, dtype=dtypes.bool).tril()
|
||||
ret = mask.where(x_unsqueezed - x_cummax.unsqueeze(-1), self.dtype.min).exp().sum(-1).log() + x_cummax
|
||||
return ret.transpose(-1, axis)
|
||||
|
||||
|
||||
@@ -348,12 +348,12 @@ def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
|
||||
# each device owns [offset, offset+local_vocab_size) of the global vocabulary
|
||||
dnum = UOp.variable("_device_num", 0, ndev-1)
|
||||
offset = dnum * local_vocab_size
|
||||
global_token_id = idx_flat[i].cast(dtypes.index)
|
||||
global_token_id = idx_flat[i].cast(dtypes.weakint)
|
||||
local_token_id = (global_token_id - offset).clip(0, grad_weight.shape[0]-1)
|
||||
in_range = (global_token_id >= offset) & (global_token_id < (offset + local_vocab_size)) & j_ok
|
||||
grad_val = in_range.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
|
||||
else:
|
||||
local_token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.index)
|
||||
local_token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.weakint)
|
||||
grad_val = j_ok.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
|
||||
# atomic scatter-add: grad_weight[token_id, j] += grad_emb_flat[i, j]
|
||||
if device in ("CPU", "NULL"): atomic_arg = "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
from typing import Any, Sequence, cast, Literal, NamedTuple, Generator
|
||||
import dataclasses, functools, io, math, types, warnings, pathlib, sys, os, struct, enum
|
||||
from tinygrad.nn.state import TensorIO
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.tensor import Tensor, is_numpy_ndarray
|
||||
from tinygrad.mixin.op import ReductionStr
|
||||
from tinygrad.helpers import getenv, all_same, prod, flatten, make_tuple, argsort, is_numpy_ndarray, get_single_element, polyN, Context
|
||||
from tinygrad.helpers import getenv, all_same, prod, flatten, make_tuple, argsort, get_single_element, polyN, Context
|
||||
from tinygrad.dtype import DType, ConstType, dtypes, _from_np_dtype, truncate, least_upper_dtype, DTYPES_DICT
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.uop.ops import sint, _broadcast_shape
|
||||
|
||||
@@ -70,6 +70,7 @@ def safe_save(tensors:dict[str, Tensor], fn:str, metadata:dict[str, Any]|None=No
|
||||
nn.state.safe_save({'t':t}, "test.safetensor")
|
||||
```
|
||||
"""
|
||||
if any(v.dtype in dtypes.weaks for v in tensors.values()): raise ValueError("safe_save requires concrete dtypes")
|
||||
headers, offset = {}, 0
|
||||
if metadata: headers['__metadata__'] = metadata
|
||||
for k,v in tensors.items():
|
||||
|
||||
@@ -43,6 +43,7 @@ class Estimates:
|
||||
# SPECIAL are already counted in mults
|
||||
mults = mults.substitute({x:x.const_like(0) for x in mults.toposort() if x.op is Ops.SPECIAL}) if isinstance(mults, UOp) else mults
|
||||
elif u.op is Ops.END: mults = mult_stack.pop(-1)
|
||||
elif u.op is Ops.LOOP: mult_stack.append(mults) # unbounded loop, unknown trip count
|
||||
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
|
||||
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
|
||||
elif u.op is Ops.LOAD and u.src[0].addrspace != AddrSpace.REG:
|
||||
|
||||
@@ -12,9 +12,11 @@ base_rewrite = PatternMatcher([
|
||||
# local/reg buffers
|
||||
(UPat(Ops.BUFFER, name="x"), lambda ctx,x: ctx.render_buffer(x)),
|
||||
|
||||
# range/if/endif
|
||||
# range/loop/if/endif
|
||||
(UPat(Ops.RANGE, name="x"),
|
||||
lambda ctx,x: f"for ({ctx.render_dtype(x.dtype)} {ctx[x]} = 0; {ctx[x]} < {ctx[x.src[0]]}; {ctx[x]}++) {{"),
|
||||
(UPat(Ops.LOOP, name="x"), lambda ctx,x: "for (;;) {"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.LOOP), UPat(name="c", dtype=dtypes.bool))), lambda ctx,c: f" if (!({ctx[c]})) {{ break; }}\n}}"),
|
||||
(UPat(Ops.IF, name="x"), lambda ctx,x: f"if ({ctx[x.src[0]]}) {{"),
|
||||
(UPat((Ops.ENDIF, Ops.END)), lambda ctx: "}"),
|
||||
|
||||
@@ -99,6 +101,7 @@ def uops_to_dtypes(uops:list[UOp]) -> list[tuple[DType, int]]:
|
||||
return dedup((u.dtype, u.max_numel()) for u in uops if u.addrspace in (AddrSpace.ALU, None) and u.dtype != dtypes.void and u._shape is not None)
|
||||
|
||||
def _wmma_name(u:UOp) -> str:
|
||||
# sanitize spaces in DType.name (int8 = "signed char")
|
||||
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}".replace(" ", "_")
|
||||
|
||||
# (name, dims, dtype_in, dtype_out, device, threads, upcast_sizes)
|
||||
@@ -226,16 +229,16 @@ class CStyleLanguage(Renderer):
|
||||
|
||||
if u.op in {Ops.ENDIF, Ops.END}: depth -= 1
|
||||
if (u.op is not Ops.CAST or u.max_numel() == 1) and (u.op in {Ops.CONST, Ops.INDEX, Ops.SHRINK, Ops.CUSTOMI} or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG) or \
|
||||
(u.op is Ops.LOAD and u.src[0].addrspace == AddrSpace.REG and child_count[u] == 1) or \
|
||||
(u.op is Ops.CAST and u.addrspace in (AddrSpace.GLOBAL, AddrSpace.LOCAL)) or \
|
||||
(u.op in {Ops.STACK, *(GroupOp.ALU-{Ops.WHERE}), Ops.CAST, Ops.BITCAST} and child_count[u] == 1 and not getenv("EXPAND_SSA"))):
|
||||
r[u] = l
|
||||
else:
|
||||
if u.op not in {Ops.RANGE, Ops.STORE, Ops.BUFFER} and u.dtype != dtypes.void:
|
||||
l = f"{self.render_type(u)} {r[u]} = {l}" + (";" if u.op is not Ops.SPECIAL else "")
|
||||
kernel.append(" "*depth + l)
|
||||
kernel.append("\n".join(" "*depth + line for line in l.split("\n")))
|
||||
if prefix: c[prefix] += 1 # if it was used, increment
|
||||
if u.op in {Ops.IF, Ops.RANGE}: depth += 1
|
||||
if u.op in {Ops.IF, Ops.RANGE, Ops.LOOP}: depth += 1
|
||||
del self.r
|
||||
|
||||
# NOTE: this relies on bufs dict preserving order
|
||||
@@ -569,7 +572,7 @@ class HIPRenderer(CStyleLanguage):
|
||||
prefix.append("typedef int wmma_int4 __attribute__((ext_vector_type(4)));\n"+
|
||||
f"static inline __attribute__((device)) int8 __{name}"+"""(signed_char16 a, signed_char16 b, int8 c) {
|
||||
return __builtin_amdgcn_wmma_i32_16x16x16_iu8_w32(true, __builtin_bit_cast(wmma_int4, a),
|
||||
true, __builtin_bit_cast(wmma_int4, b), c, true);\n}""")
|
||||
true, __builtin_bit_cast(wmma_int4, b), c, false);\n}""")
|
||||
elif dtype_out == dtypes.float:
|
||||
prefix.append(f"#define __{name} __builtin_amdgcn_wmma_f32_16x16x16_{'f16' if dtype_in == dtypes.half else 'bf16'}_w32")
|
||||
else: prefix.append(f"static inline __attribute__((device)) half8 __{name}"+"""(half16 a, half16 b, half8 c) {
|
||||
|
||||
@@ -86,7 +86,7 @@ class X86GroupOp:
|
||||
X86Ops.CMPi, X86Ops.IMULi, X86Ops.LEA}
|
||||
|
||||
# X86Ops whose second src can read from memory NOTE: some of these are TwoAddress so the second src is actually the first
|
||||
ReadMem2nd = {X86Ops.ADD, X86Ops.SUB, X86Ops.AND, X86Ops.OR, X86Ops.XOR, X86Ops.SHL, X86Ops.SHR, X86Ops.SAR, X86Ops.IMUL, X86Ops.CMP,
|
||||
ReadMem2nd = {X86Ops.ADD, X86Ops.SUB, X86Ops.AND, X86Ops.OR, X86Ops.XOR, X86Ops.IMUL, X86Ops.CMP,
|
||||
X86Ops.VADDSS, X86Ops.VADDSD, X86Ops.VADDPS, X86Ops.VADDPD, X86Ops.VSUBSS, X86Ops.VSUBSD, X86Ops.VSUBPS, X86Ops.VSUBPD,
|
||||
X86Ops.VMULSS, X86Ops.VMULSD, X86Ops.VMULPS, X86Ops.VMULPD, X86Ops.VDIVSS, X86Ops.VDIVSD, X86Ops.VDIVPS, X86Ops.VDIVPD,
|
||||
X86Ops.VPADDB, X86Ops.VPADDW, X86Ops.VPADDD, X86Ops.VPADDQ, X86Ops.VPSUBB, X86Ops.VPSUBW, X86Ops.VPSUBD, X86Ops.VPSUBQ,
|
||||
@@ -99,8 +99,9 @@ class X86GroupOp:
|
||||
|
||||
# X86Ops that can write to memory
|
||||
WriteMem = {X86Ops.MOVm, X86Ops.MOVi, X86Ops.VMOVSSm, X86Ops.VMOVSDm, X86Ops.VMOVUPSm, X86Ops.VMOVDm, X86Ops.VMOVQm,
|
||||
X86Ops.ADDi, X86Ops.SUBi, X86Ops.ANDi, X86Ops.ORi, X86Ops.XORi, X86Ops.SHLi, X86Ops.SHRi, X86Ops.SARi, X86Ops.SETNE,
|
||||
X86Ops.SETE, X86Ops.SETL, X86Ops.SETB, X86Ops.VCVTPS2PH, X86Ops.VPEXTRB, X86Ops.VPEXTRW, X86Ops.VPEXTRD, X86Ops.VPEXTRQ}
|
||||
X86Ops.ADDi, X86Ops.SUBi, X86Ops.ANDi, X86Ops.ORi, X86Ops.XORi, X86Ops.SHL, X86Ops.SHLi, X86Ops.SHR, X86Ops.SHRi, X86Ops.SAR,
|
||||
X86Ops.SARi, X86Ops.SETNE, X86Ops.SETE, X86Ops.SETL, X86Ops.SETB,
|
||||
X86Ops.VCVTPS2PH, X86Ops.VPEXTRB, X86Ops.VPEXTRW, X86Ops.VPEXTRD, X86Ops.VPEXTRQ}
|
||||
|
||||
# X86Ops that read flags
|
||||
ReadFlags = {X86Ops.CMOVB, X86Ops.CMOVL, X86Ops.CMOVE, X86Ops.CMOVNE, X86Ops.SETB, X86Ops.SETL, X86Ops.SETE, X86Ops.SETNE, X86Ops.JB, X86Ops.JL,
|
||||
@@ -272,6 +273,11 @@ def idiv(ctx:IselContext, x:UOp) -> UOp:
|
||||
# this move "cleanses" the register constraints (rax/rdx) of idiv as that only applies on definition and not on the uses of idiv
|
||||
return x.ins(X86Ops.MOV, src=(idiv,))
|
||||
|
||||
# a variable shift count implicitly reads cl so it goes in rcx, the shifted value can't be in rcx
|
||||
def shift(x:UOp, op:X86Ops) -> UOp:
|
||||
val = x.ins(X86Ops.MOV, src=(x.src[0],), tag=tuple(r for r in WGPR if r is not RCX))
|
||||
return x.ins(op, src=(val, x.ins(X86Ops.MOV, src=(x.src[1],), tag=(RCX,))))
|
||||
|
||||
# a memory address operand is (base, index, displacement, size). size is the element size, it scales the index and is the memory operand width.
|
||||
# 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]:
|
||||
@@ -452,9 +458,9 @@ isel_matcher = PatternMatcher([
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) ^ UPat.cvar("c"), lambda a,c: a.ins(X86Ops.XORi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
(UPat(Ops.SUB, dtypes.ints, (UPat.var("a"), UPat.cvar("c"))), lambda a,c: a.ins(X86Ops.SUBi, src=(a, i)) if (i:=to_imm(c)) is not None else None),
|
||||
# scalar int binary with register
|
||||
(UPat.var("a", dtypes.ints) << UPat.var("b"), lambda a,b: a.ins(X86Ops.SHL, src=(a, b))),
|
||||
(UPat.var("a", dtypes.uints) >> UPat.var("b"), lambda a,b: a.ins(X86Ops.SHR, src=(a, b))),
|
||||
(UPat.var("a", dtypes.sints) >> UPat.var("b"), lambda a,b: a.ins(X86Ops.SAR, src=(a, b))),
|
||||
((UPat(dtype=dtypes.ints) << UPat()).named("x"), lambda x: shift(x, X86Ops.SHL)),
|
||||
((UPat(dtype=dtypes.uints) >> UPat()).named("x"), lambda x: shift(x, X86Ops.SHR)),
|
||||
((UPat(dtype=dtypes.sints) >> UPat()).named("x"), lambda x: shift(x, X86Ops.SAR)),
|
||||
(UPat.var("a", dtypes.ints) + UPat.var("b"), lambda a,b: a.ins(X86Ops.ADD, src=(a, b))),
|
||||
(UPat.var("a", dtypes.ints) * UPat.var("b"), lambda a,b: a.ins(X86Ops.IMUL, src=(a, b))),
|
||||
(UPat.var("a", dtypes.ints+(dtypes.bool,)) & UPat.var("b"), lambda a,b: a.ins(X86Ops.AND, src=(a, b))),
|
||||
@@ -651,7 +657,8 @@ def encode(x:UOp, opc:int, reg:int|None=None, pp:int=0, sel:int=0, we:int=0) ->
|
||||
if x.arg in X86GroupOp.WriteMem:
|
||||
if len(x.src) > 4: address, rest = x.src[:4], x.src[4:]
|
||||
else: address, rest = (x, None, None, None), x.src
|
||||
return _encode(rest[0], *address, *(None, *rest[1:])) if reg is None else _encode(None, *address, *(None, *rest[:1]))
|
||||
imm_uop = rest[:1] if rest and rest[0].op is Ops.CONST else (None,)
|
||||
return _encode(rest[0], *address, *(None, *rest[1:])) if reg is None else _encode(None, *address, *(None, *imm_uop))
|
||||
|
||||
if x.arg in X86GroupOp.Rm1st:
|
||||
if len(x.src) > 3: address, rest = x.src[:4], x.src[4:]
|
||||
@@ -704,8 +711,9 @@ encodings = {
|
||||
# int division
|
||||
X86Ops.IDIV: lambda x: encode(x, 0xF7, reg=7), X86Ops.DIV: lambda x: encode(x, 0xF7, reg=6),
|
||||
# scalar int binary
|
||||
X86Ops.SHLi: lambda x: encode(x, 0xC1, reg=4),
|
||||
X86Ops.SHRi: lambda x: encode(x, 0xC1, reg=5), X86Ops.SARi: lambda x: encode(x, 0xC1, reg=7),
|
||||
X86Ops.SHL: lambda x: encode(x, 0xD3, reg=4), X86Ops.SHLi: lambda x: encode(x, 0xC1, reg=4),
|
||||
X86Ops.SHR: lambda x: encode(x, 0xD3, reg=5), X86Ops.SHRi: lambda x: encode(x, 0xC1, reg=5),
|
||||
X86Ops.SAR: lambda x: encode(x, 0xD3, reg=7), X86Ops.SARi: lambda x: encode(x, 0xC1, reg=7),
|
||||
X86Ops.ADD: lambda x: encode(x, 0x03), X86Ops.ADDi: lambda x: encode(x, 0x81, reg=0),
|
||||
X86Ops.SUB: lambda x: encode(x, 0x2B), X86Ops.SUBi: lambda x: encode(x, 0x81, reg=5),
|
||||
X86Ops.AND: lambda x: encode(x, 0x23), X86Ops.ANDi: lambda x: encode(x, 0x81, reg=4),
|
||||
@@ -784,6 +792,7 @@ class X86Renderer(ISARenderer):
|
||||
post_regalloc_matcher = post_regalloc_matcher
|
||||
code_for_op = {x: lambda: None for x in (Ops.SQRT, Ops.AND, Ops.OR, Ops.SHL, Ops.SHR, Ops.NEG, Ops.SUB, Ops.FDIV, Ops.CMPLT, Ops.CMPEQ)}
|
||||
def __init__(self, target:Target):
|
||||
if target.arch.split(",")[0] != "x86_64": raise RuntimeError(f"X86Renderer only supports x86_64, got {target.arch}")
|
||||
super().__init__(target)
|
||||
from tinygrad.runtime.support.compiler_cpu import X86Compiler
|
||||
self.compiler = X86Compiler()
|
||||
|
||||
@@ -35,7 +35,7 @@ def lcast(input_type:DType, output_type:DType):
|
||||
|
||||
def render_wmma_amd(ctx, wmma: UOp, cdna=False) -> str:
|
||||
dt_map = {dtypes.half: "f16", dtypes.float: "f32", dtypes.ushort: "bf16.1k" if cdna else "bf16", dtypes.bfloat16: "bf16.1k" if cdna else "bf16",
|
||||
dtypes.fp8e4m3: ".fp8.fp8", dtypes.fp8e5m2: ".bf8.bf8"}
|
||||
dtypes.fp8e4m3: ".fp8.fp8", dtypes.fp8e5m2: ".bf8.bf8", dtypes.int8: "iu8", dtypes.int32: "i32"}
|
||||
# https://github.com/llvm/llvm-project/blob/main/clang/test/CodeGenOpenCL/builtins-amdgcn-mfma.cl
|
||||
N,M,K = wmma.arg[0]
|
||||
if cdna:
|
||||
@@ -44,9 +44,10 @@ def render_wmma_amd(ctx, wmma: UOp, cdna=False) -> str:
|
||||
f".{N}x{M}x{K}{dt_map[wmma.arg[1]]}(" + ", ".join([f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]) + ", i32 0, i32 0, i32 0)"
|
||||
# https://github.com/llvm/llvm-project/blob/main/llvm/test/CodeGen/AMDGPU/GlobalISel/llvm.amdgcn.wmma_32.ll
|
||||
# example: %wmma0 = call <8 x float> @llvm.amdgcn.wmma.f32.16x16x16.f16(<16 x half> %v99,<16 x half> %v100,<8 x float> %v101)
|
||||
args = [f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]
|
||||
if wmma.arg[1] == dtypes.int8: args = ["i1 true", args[0], "i1 true", args[1], args[2]] # iu8 flags A/B signed
|
||||
return f" {ctx[wmma]} = call {ldt(wmma.dtype, wmma.max_numel())} @llvm.amdgcn.wmma.{dt_map[wmma.src[-1].dtype]}.16x16x16." + \
|
||||
f"{dt_map[wmma.src[0].dtype]}(" + ", ".join([f"{ldt(w.dtype, w.max_numel())} {ctx[w]}" for w in wmma.src]) + (", i1 false)" \
|
||||
if wmma.dtype != dtypes.float else ")")
|
||||
f"{dt_map[wmma.arg[1]]}(" + ", ".join(args) + (", i1 false)" if wmma.dtype != dtypes.float else ")")
|
||||
|
||||
# llvm ops, lop[<dtype>][<op>]
|
||||
unsigned_lop = { Ops.ADD: "add", Ops.MUL: "mul", Ops.CDIV: "udiv", Ops.CMOD: "urem",
|
||||
@@ -112,6 +113,11 @@ base_rewrite = PatternMatcher([
|
||||
f" br label %loop_latch_{range_str(r)}\n"
|
||||
f"loop_exit_{range_str(r)}:"),
|
||||
|
||||
# loop
|
||||
(UPat(Ops.LOOP, name="l"), lambda ctx,l: f" br label %loop_{ctx[l][1:]}\nloop_{ctx[l][1:]}:"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.LOOP, name="l"), UPat(name="c"))), lambda ctx,l,c:
|
||||
f" br i1 {ctx[c]}, label %loop_{ctx[l][1:]}, label %loop_exit_{ctx[l][1:]}\nloop_exit_{ctx[l][1:]}:"),
|
||||
|
||||
# if
|
||||
(UPat(Ops.IF, name="x"), lambda ctx,x: f" br i1 {ctx[x.src[0]]}, label %ifbody_{ctx[x][1:]}, label %ifskip_{ctx[x][1:]}\nifbody_{ctx[x][1:]}:"),
|
||||
(UPat(Ops.ENDIF, name="x"), lambda ctx,x: f" br label %ifskip_{ctx[x.src[0]][1:]}\nifskip_{ctx[x.src[0]][1:]}:"),
|
||||
@@ -261,6 +267,9 @@ exit: %packed = phi i32 [%packed_bf8, %do_bf8], [%packed_fp8, %do_fp8]\n %trunc
|
||||
])
|
||||
if target.arch in {"gfx1100", "gfx1151"}:
|
||||
self.extra_matcher += PatternMatcher([
|
||||
(UPat(Ops.WMMA, name="x", dtype=dtypes.int32), lambda x: x.replace(
|
||||
src=(x.src[0].bitcast(dtypes.uint32), x.src[1].bitcast(dtypes.uint32), x.src[2]))
|
||||
if x.src[0].dtype == dtypes.int8 and x.src[0].max_numel() == 16 else None),
|
||||
(UPat(Ops.WMMA, name="x", dtype=dtypes.half), lambda x: UOp(Ops.STACK, src=tuple(x.replace(
|
||||
src=(x.src[0], x.src[1], UOp(Ops.STACK, src=tuple(x.src[2].index(j//2) if j%2 == 0 else UOp.const(x.src[2].dtype, 0.0)
|
||||
for j in range(x.max_numel()*2)))),
|
||||
|
||||
@@ -125,6 +125,9 @@ string_rewrite = PatternMatcher([
|
||||
ctx.code_for_op[Ops.ADD](ctx.r[r], ctx.r[r], "1", dtypes.int, ctx.types[dtypes.int]),
|
||||
ctx.code_for_op[Ops.CMPLT](ctx.r[x], ctx.r[r], ctx.r[r.src[0]], dtypes.int, ctx.types[dtypes.int]),
|
||||
f"@{ctx.r[x]} bra LOOP_{ctx.r[r][1:]};"]),
|
||||
(UPat(Ops.LOOP, name="l"), lambda ctx, l: "WAITLOOP_" + f"{ctx.uops.index(l)}:"),
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.LOOP, name="l"), UPat(name="c"))), lambda ctx, l, c:
|
||||
f"@{ctx.r[c]} bra WAITLOOP_{ctx.uops.index(l)};"),
|
||||
(UPat(Ops.IF, name="x"), lambda ctx, x: f"@!{ctx.r[x.src[0]]} bra IF_{ctx.r[x.src[0]][1:]}_{ctx.uops.index(x)};"),
|
||||
(UPat(Ops.ENDIF, name="x"), lambda ctx, x: f"IF_{ctx.r[x.src[0].src[0]][1:]}_{ctx.uops.index(x.src[0])}:"),
|
||||
(UPat(Ops.WMMA, name="x"), lambda ctx, x: list(render_wmma(ctx, x))),
|
||||
|
||||
@@ -48,7 +48,7 @@ class PythonProgram:
|
||||
st = time.perf_counter()
|
||||
warp = list(itertools.product(*[range(x) for x in local_size[::-1]]))
|
||||
warp_size = len(warp)
|
||||
void_ops = {Ops.END, Ops.BARRIER, Ops.IF, Ops.ENDIF, Ops.SINK, Ops.NOOP, Ops.GROUP, Ops.STORE}
|
||||
void_ops = {Ops.END, Ops.BARRIER, Ops.IF, Ops.ENDIF, Ops.SINK, Ops.NOOP, Ops.GROUP, Ops.STORE, Ops.LOOP}
|
||||
for idxs in itertools.product(*[range(x) for x in global_size[::-1]]):
|
||||
values: dict[UOp, Any] = {}
|
||||
pbufs: list[memoryview] = list(bufs)
|
||||
@@ -61,7 +61,11 @@ class PythonProgram:
|
||||
src_dtypes = [v.dtype for v in u.src if v.op not in void_ops]
|
||||
if getenv("TRACE"): print(i, u.op, u.dtype, u.arg, src_values, src_dtypes)
|
||||
if u.op is Ops.END:
|
||||
i = self.uop_to_index[u.src[1]]
|
||||
if len(u.src) == 3:
|
||||
# conditional backedge on LOOP: jump back while the condition is true
|
||||
if values[u.src[2]][0]: i = self.uop_to_index[u.src[1]]
|
||||
else: i += 1
|
||||
else: i = self.uop_to_index[u.src[1]]
|
||||
continue
|
||||
if u.op is Ops.IF:
|
||||
exec_masks.append([x and y for x,y in zip(exec_masks[-1], src_values[0])])
|
||||
@@ -71,7 +75,7 @@ class PythonProgram:
|
||||
exec_masks.pop()
|
||||
i += 1
|
||||
continue
|
||||
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP):
|
||||
if u.op in (Ops.BARRIER, Ops.SINK, Ops.NOOP, Ops.GROUP, Ops.LOOP):
|
||||
# in the python emulator, the warp is always in sync
|
||||
i += 1
|
||||
continue
|
||||
|
||||
@@ -278,7 +278,7 @@ class QCOMProgram(HCQProgram):
|
||||
def _parse_lib(self, lib):
|
||||
# Extract image binary
|
||||
self.image_size = _read_lib(lib, 0x100)
|
||||
self.image = bytearray(lib[(image_offset:=_read_lib(lib, 0xc0)):image_offset+self.image_size])
|
||||
self.image = lib[(image_offset:=_read_lib(lib, 0xc0)):image_offset+self.image_size]
|
||||
|
||||
# Parse image descriptors
|
||||
image_desc_off = _read_lib(lib, 0x110)
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Iterator
|
||||
import functools, itertools
|
||||
from dataclasses import dataclass, field, replace
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, profile_matches
|
||||
from tinygrad.uop.ops import PatternMatcher, UPat, Ops, UOp, resolve, GroupOp, graph_rewrite, sint, AxisType, profile_matches, broadcast_axes
|
||||
from tinygrad.uop.ops import consumer_map_from_toposort, gate_kernel_sink
|
||||
from tinygrad.uop.symbolic import symbolic, pm_simplify_valid, pm_drop_and_clauses
|
||||
from tinygrad.helpers import argsort, all_same, cpu_profile, PCONTIG, colored, Context, SPEC
|
||||
@@ -52,16 +52,22 @@ class IndexingContext:
|
||||
range_idx: Iterator[int] = field(default_factory=itertools.count)
|
||||
def new_range(self, s:sint, axistype:AxisType=AxisType.LOOP) -> UOp:
|
||||
if isinstance(s, UOp) and s.op is Ops.RANGE: return s
|
||||
# if a range has a 1 src, it's the same as UOp.const(dtypes.index, 0)
|
||||
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(dtypes.index, 0)
|
||||
# if a range has a 1 src, it's the same as UOp.const(dtypes.weakint, 0)
|
||||
return UOp.range(s, next(self.range_idx), axistype) if resolve(s!=1) else UOp.const(dtypes.weakint, 0)
|
||||
|
||||
def broadcast_rngs(x:UOp, src:UOp, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
|
||||
if x.op not in GroupOp.Broadcastable: return rngs
|
||||
baxes, nleft = broadcast_axes(src.shape, x.shape), len(x.shape)-len(src.shape)
|
||||
return tuple(r.const_like(0) if j in baxes else r for j,r in enumerate(rngs) if j >= nleft)
|
||||
|
||||
def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
|
||||
new_srcs = []
|
||||
for i, s in enumerate(x.src):
|
||||
new_src = s
|
||||
src_rngs = broadcast_rngs(x, s, ctx.range_map[x][0]) if x in ctx.range_map else ()
|
||||
# shape args of movement ops are at src[1:] and should not be indexed
|
||||
if s.op in {Ops.PARAM, Ops.BUFFER, Ops.SLICE, Ops.MSTACK, Ops.MSELECT, Ops.AFTER}:
|
||||
if x in ctx.range_map and not (x.op in GroupOp.Movement and i > 0): new_src = new_src.index(*ctx.range_map[x][0])
|
||||
if x in ctx.range_map and not (x.op in GroupOp.Movement and i > 0): new_src = new_src.index(*src_rngs)
|
||||
elif s in ctx.realize_map:
|
||||
realized_ranges = ctx.realize_map[s]
|
||||
assert isinstance(realized_ranges, list), "realize map must contain range list"
|
||||
@@ -77,7 +83,7 @@ def create_bufferize_and_index_srcs(ctx:IndexingContext, x:UOp) -> list[UOp]:
|
||||
opts = BufferizeOpts(device=s.device, removable=removable) if len(ctx.range_map[s][1]) == len(realized_ranges) else \
|
||||
BufferizeOpts(device=s.device, addrspace=AddrSpace.LOCAL, removable=removable)
|
||||
new_src = UOp(Ops.STAGE, src=(new_src,)+closed_ranges, arg=opts)
|
||||
if x in ctx.range_map: new_src = new_src.index(*[r for i,r in enumerate(ctx.range_map[x][0]) if i in realized_ranges])
|
||||
if x in ctx.range_map: new_src = new_src.index(*[r for i,r in enumerate(src_rngs) if i in realized_ranges])
|
||||
new_srcs.append(new_src)
|
||||
return new_srcs
|
||||
|
||||
@@ -136,7 +142,7 @@ def _apply_reshape(in_shape:tuple[sint,...], out_shape:tuple[sint, ...], urngs:U
|
||||
for s,src in list(zip(out_shape, urngs.src))[::-1]:
|
||||
axes_in.append(acc*src)
|
||||
acc *= s
|
||||
combined_axes = UOp.const(dtypes.index, 0).usum(axes_in)
|
||||
combined_axes = UOp.const(dtypes.weakint, 0).usum(axes_in)
|
||||
axes_out:list[UOp] = []
|
||||
for s in in_shape[::-1]:
|
||||
axes_out.append(combined_axes % s)
|
||||
@@ -188,7 +194,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
# treat MSTACK/MSELECT like SINK
|
||||
if x.op in {Ops.MSTACK, Ops.MSELECT}: continue
|
||||
|
||||
if x.dtype == dtypes.index: continue # TODO: why do I need this?
|
||||
if x.dtype == dtypes.weakint: continue # TODO: why do I need this?
|
||||
ending_ranges[x] = sum([ending_ranges.get(u, []) for u in consumer_map[x]], [])
|
||||
|
||||
# *** the ranges on the output are
|
||||
@@ -196,7 +202,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
|
||||
# 2. from the single consumer if this op only has one consumer
|
||||
# 3. potentially new if this op has 2+ consumers
|
||||
|
||||
consumer_rngs = [rctx.range_map[c][0] for c in consumer_map[x] if c in rctx.range_map]
|
||||
consumer_rngs = [broadcast_rngs(c, x, rctx.range_map[c][0]) for c in consumer_map[x] if c in rctx.range_map]
|
||||
if x in rctx.realize_map:
|
||||
# if this is in the realize_map, we create new ranges (at the output)
|
||||
out_rngs = tuple(rctx.new_range(s) for s in x.shape)
|
||||
|
||||
@@ -56,7 +56,7 @@ def memory_plan_rewrite(linear:UOp, held_bufs:set[UOp]|None=None) -> UOp:
|
||||
arenas = {key: UOp.new_buffer(key[0], sz, dtypes.int8) for key, sz in arena_sizes.items()}
|
||||
replace_map:dict[UOp, UOp] = {}
|
||||
for buf_uop, offset in offsets.items():
|
||||
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(dtypes.index, offset)), buf_uop.max_numel())
|
||||
replace_map[buf_uop] = UOp(Ops.SLICE, buf_uop.dtype, (arenas[_key(buf_uop)], UOp.const(dtypes.weakint, offset)), buf_uop.max_numel())
|
||||
|
||||
if DEBUG >= 1 and (omem:=sum(nbytes.values()) / 1e6) != (nmem:=sum(arena_sizes.values()) / 1e6):
|
||||
print(f"memory reduced from {omem:.2f} MB -> {nmem:.2f} MB, {len(first_appearance)} -> {len(arenas)} bufs")
|
||||
|
||||
@@ -148,7 +148,7 @@ def rewrite_into_function(call:UOp):
|
||||
|
||||
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.name, p.arg.addrspace).multi(p.axis)
|
||||
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).multi(p.axis)
|
||||
|
||||
# NOTE: this is the same pattern as unrolled ranges
|
||||
multi_pm = PatternMatcher([
|
||||
|
||||
@@ -78,7 +78,7 @@ def split_reduceop(reduce:UOp, x:UOp):
|
||||
# split is moved to the end to provide maximum locality for the second phase reduce.
|
||||
|
||||
# get expanded by rangeifying the UOp x
|
||||
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else UOp.const(dtypes.index, 0) for i,s in enumerate(x.shape)])
|
||||
indexed = x.index(*[UOp.range(s, i) if resolve(s>1) else UOp.const(dtypes.weakint, 0) for i,s in enumerate(x.shape)])
|
||||
range_nums = [y.arg[0] for y in indexed.substitute({x.base:UOp(Ops.NOOP, x.base.dtype)}, extra_pm=pm_mops).ranges]
|
||||
is_expanded = [i not in range_nums for i in range(len(x.shape))]
|
||||
|
||||
@@ -470,7 +470,7 @@ to_define_global = PatternMatcher([
|
||||
(UPat(Ops.STORE, name="x"), find_bufs),
|
||||
(UPat((Ops.BUFFER, Ops.MSTACK, Ops.MSELECT), name="buf"), debuf),
|
||||
(UPat(Ops.PARAM, name="v"), lambda v:
|
||||
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype)
|
||||
UOp.variable(v.arg.name, v.arg.vmin_vmax[0], v.arg.vmin_vmax[1], v.dtype, multiple_of=v.arg.multiple_of)
|
||||
if v.arg.name is not None and v.arg.vmin_vmax is not None else None),
|
||||
|
||||
# this renumbers the params
|
||||
|
||||
+32
-22
@@ -1,10 +1,10 @@
|
||||
# inspired by https://github.com/karpathy/micrograd/blob/master/micrograd/engine.py
|
||||
from __future__ import annotations
|
||||
import time, functools, sys, inspect, pathlib, hashlib, weakref
|
||||
from typing import Any, Callable, cast, get_args, ParamSpec, TypeVar, Generic, TYPE_CHECKING
|
||||
from typing import Any, Callable, cast, get_args, ParamSpec, TypeGuard, TypeVar, Generic, TYPE_CHECKING
|
||||
if TYPE_CHECKING: import numpy
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, to_dtype, strong_dtype, _from_np_dtype, _to_np_dtype, PyConst
|
||||
from tinygrad.helpers import all_int, getenv, fully_flatten, fetch, Metadata, TRACEMETA, is_numpy_ndarray, TracingKey
|
||||
from tinygrad.dtype import DType, DTypeLike, dtypes, ConstType, least_upper_dtype, to_dtype, strong_dtype, _from_np_dtype, _to_np_dtype, PyConst
|
||||
from tinygrad.helpers import all_int, getenv, fully_flatten, fetch, Metadata, TRACEMETA, TracingKey
|
||||
from tinygrad.helpers import cpu_profile, suppress_finalizing, disable_gc
|
||||
from tinygrad.uop.ops import UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable, _broadcast_shape
|
||||
from tinygrad.mixin.rand import RandMixin
|
||||
@@ -34,6 +34,8 @@ def _apply_map_to_tensors(applied_map:dict[UOp, UOp], name:str) -> None:
|
||||
|
||||
# **** Tensor helper functions ****
|
||||
|
||||
def is_numpy_ndarray(x) -> "TypeGuard[numpy.ndarray]": return str(type(x)) == "<class 'numpy.ndarray'>"
|
||||
|
||||
def _fromnp(x: 'numpy.ndarray') -> UOp:
|
||||
ret = UOp.new_buffer("NPY", x.size, _from_np_dtype(x.dtype))
|
||||
# fake realize
|
||||
@@ -69,28 +71,27 @@ class Tensor(RandMixin):
|
||||
|
||||
# create a UOp from the different types of inputs
|
||||
if isinstance(data, UOp):
|
||||
# if data is dtype.index that means that this is a symbolic int and we need to lower it to something we can make a Tensor out of
|
||||
if data.dtype == dtypes.index: data = _index_to_concrete_int(data)
|
||||
# if data is dtype.weakint that means that this is a symbolic int and we need to lower it to something we can make a Tensor out of
|
||||
if data.dtype == dtypes.weakint: data = _index_to_concrete_int(data)
|
||||
elif data is None:
|
||||
data = UOp.const(_dtype or dtypes.default_float, 0)
|
||||
elif isinstance(data, get_args(ConstType)):
|
||||
data = UOp.const(_dtype or dtypes.from_py(data), data)
|
||||
elif isinstance(data, bytes): data = UOp._frompy(data, _dtype or dtypes.uint8, _device)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
if _dtype is None:
|
||||
if (d := fully_flatten(data)) and all(isinstance(s, bool) for s in d): _dtype = dtypes.bool
|
||||
else: _dtype = dtypes.default_int if d and all_int(d) else dtypes.default_float # NOTE: this works because all_int([True, False]) is True
|
||||
data = UOp._frompy(data, _dtype, _device)
|
||||
elif is_numpy_ndarray(data):
|
||||
import numpy as np
|
||||
assert isinstance(data, np.ndarray), f"expected np.ndarray, got {data}"
|
||||
if data.shape == ():
|
||||
data = UOp.const(_dtype or _from_np_dtype(data.dtype), data.item())
|
||||
else:
|
||||
elif is_numpy_ndarray(data) and data.shape == ():
|
||||
data = UOp.const(_dtype or _from_np_dtype(data.dtype), data.item())
|
||||
else:
|
||||
if _dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {_dtype}")
|
||||
if isinstance(data, bytes): data = UOp._frompy(data, _dtype or dtypes.uint8, _device)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
if _dtype is None:
|
||||
if (d := fully_flatten(data)) and all(isinstance(s, bool) for s in d): _dtype = dtypes.bool
|
||||
else: _dtype = dtypes.default_int if d and all_int(d) else dtypes.default_float # NOTE: this works because all_int([True, False]) is True
|
||||
data = UOp._frompy(data, _dtype, _device)
|
||||
elif is_numpy_ndarray(data):
|
||||
data = _fromnp(data.astype(npdtype) if _dtype is not None and (npdtype:=_to_np_dtype(_dtype)) is not None else data)
|
||||
elif isinstance(data, pathlib.Path):
|
||||
_dtype = _dtype or dtypes.uint8
|
||||
data = UOp.new_buffer(f"DISK:{data.resolve()}", data.stat().st_size // _dtype.itemsize, _dtype)
|
||||
elif isinstance(data, pathlib.Path):
|
||||
_dtype = _dtype or dtypes.uint8
|
||||
data = UOp.new_buffer(f"DISK:{data.resolve()}", data.stat().st_size // _dtype.itemsize, _dtype)
|
||||
|
||||
# by this point, it has to be a UOp
|
||||
if not isinstance(data, UOp): raise RuntimeError(f"can't create Tensor from {data!r} with type {type(data)}")
|
||||
@@ -178,6 +179,7 @@ class Tensor(RandMixin):
|
||||
|
||||
def linear_with_vars(self, *lst:Tensor) -> tuple[UOp, dict[str, int]]:
|
||||
"""Creates the LINEAR UOp needed to realize these Tensor(s), with Variables."""
|
||||
if any(t.dtype in dtypes.weaks for t in (self,)+lst): raise RuntimeError("cannot realize a weak dtype; cast to a concrete dtype first")
|
||||
big_sink, becomes_map = transform_to_call(UOp.sink(*[x.uop for x in (self,)+lst]))
|
||||
_apply_map_to_tensors(becomes_map, name="buffers")
|
||||
return create_linear_with_vars(big_sink)
|
||||
@@ -205,14 +207,16 @@ class Tensor(RandMixin):
|
||||
return self
|
||||
|
||||
def assign(self, x:Tensor|PyConst|list|tuple) -> Tensor:
|
||||
if self.dtype in dtypes.weaks: raise RuntimeError("cannot assign into a weak tensor; it has no storage")
|
||||
is_disk = isinstance(self.device, str) and self.device.startswith(("DISK", "TINYFS"))
|
||||
if not isinstance(x, Tensor): x = Tensor(x, device="CPU" if is_disk else self.device, dtype=self.dtype)
|
||||
if self.uop is x.uop: return self # a self assign is a NOOP
|
||||
# broadcast x (shape only, dtype must match)
|
||||
x = x._broadcast_to(self.shape)
|
||||
if x.dtype in dtypes.weaks: x = x.cast(least_upper_dtype(self.dtype, x.dtype))
|
||||
if x.dtype != self.dtype: raise RuntimeError(f"assign dtype mismatch {self.dtype} != {x.dtype}")
|
||||
if not is_disk and x.uop.device is not None and self.device is not None and self.device != x.device:
|
||||
raise RuntimeError(f"assign device mismatch {self.device} != {x.device}")
|
||||
if not is_disk and self.dtype != x.dtype: raise RuntimeError(f"assign dtype mismatch {self.dtype} != {x.dtype}")
|
||||
if isinstance(self.device, tuple) and x.uop.device is not None and self.uop.axis != x.uop.axis:
|
||||
raise RuntimeError(f"multi axis mismatch {self.uop.axis} != {x.uop.axis}")
|
||||
|
||||
@@ -253,6 +257,7 @@ class Tensor(RandMixin):
|
||||
print(np.frombuffer(t.data(), dtype=np.int32))
|
||||
```
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: return self.cast(strong_dtype(self.dtype)).data()
|
||||
if 0 in self.shape: return memoryview(bytearray(0)).cast(self.dtype.fmt) # type: ignore[arg-type,return-value]
|
||||
assert all_int(self.shape), f"no data if shape is symbolic, {self.shape=}"
|
||||
buf = self._buffer()
|
||||
@@ -276,6 +281,7 @@ class Tensor(RandMixin):
|
||||
print(t.tolist())
|
||||
```
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: return self.cast(strong_dtype(self.dtype)).tolist()
|
||||
# TODO: remove half once minimum python supports it
|
||||
if self.dtype in (dtypes.half, dtypes.bfloat16, *dtypes.fp8s): return self.cast(dtypes.float32).tolist()
|
||||
if 0 in self.shape:
|
||||
@@ -293,6 +299,7 @@ class Tensor(RandMixin):
|
||||
print(repr(t.numpy()))
|
||||
```
|
||||
"""
|
||||
if self.dtype in dtypes.weaks: return self.cast(strong_dtype(self.dtype)).numpy()
|
||||
assert all_int(self.shape), f"no data if shape is symbolic, {self.shape=}"
|
||||
import numpy as np
|
||||
if self.dtype in { dtypes.bfloat16, *dtypes.fp8s }: return self.float().numpy()
|
||||
@@ -450,7 +457,10 @@ class Tensor(RandMixin):
|
||||
def _rop(self, op:Ops, axis:tuple[int, ...]) -> Tensor: return self._apply_uop(UOp._rop, op=op, axis=axis)
|
||||
|
||||
def __setitem__(self, indices, v:Tensor|PyConst|list|tuple) -> None:
|
||||
if isinstance(v, Tensor) and v.dtype != self.dtype: raise RuntimeError(f"setitem dtype mismatch: {self.dtype=} != {v.dtype=}")
|
||||
if self.dtype in dtypes.weaks: raise RuntimeError("cannot setitem into a weak tensor; it has no storage")
|
||||
if isinstance(v, Tensor):
|
||||
if v.dtype in dtypes.weaks: v = v.cast(least_upper_dtype(self.dtype, v.dtype))
|
||||
if v.dtype != self.dtype: raise RuntimeError(f"setitem dtype mismatch: {self.dtype=} != {v.dtype=}")
|
||||
# raise if mutation would diverge from eager (allow only pure views of a realized buffer; exclude +=/-= RHS via v_uop/v_bw)
|
||||
v_uop, v_bw = (v.uop, v.uop.backward_slice) if isinstance(v, Tensor) else (None, {})
|
||||
if self.uop.op_in_backward_slice_with_self(Ops.BUFFER):
|
||||
|
||||
@@ -76,7 +76,7 @@ class Ops(FastEnum):
|
||||
# ** 5 -- control flow / consts / custom **
|
||||
|
||||
# control flow ops
|
||||
BARRIER = auto(); RANGE = auto(); IF = auto(); END = auto(); ENDIF = auto(); WAIT = auto()
|
||||
BARRIER = auto(); RANGE = auto(); LOOP = auto(); IF = auto(); END = auto(); ENDIF = auto(); WAIT = auto()
|
||||
|
||||
# const.
|
||||
CONST = auto()
|
||||
|
||||
@@ -11,6 +11,8 @@ def fold_divmod_general(d: UOp) -> UOp|None:
|
||||
if y.vmin==y.vmax==0: raise ZeroDivisionError(f"{'Division' if d.op is Ops.FLOORDIV else 'Mod'} by zero trying to rewrite {x.alu(d.op, y)}")
|
||||
# x//y is constant
|
||||
if (xdiv:=x//y).vmin == xdiv.vmax: return x - xdiv.vmin*y if d.op is Ops.FLOORMOD else xdiv.const_like(xdiv.vmin)
|
||||
# PARAM // c is irreducible
|
||||
if x.op is Ops.PARAM and y.op is Ops.CONST and x.arg.multiple_of % y.arg == 0: return d.const_like(0) if d.op is Ops.FLOORMOD else None
|
||||
|
||||
# split uops for the rest of the processing
|
||||
x_peeled, const = x.pop_const()
|
||||
@@ -98,9 +100,9 @@ div_and_mod_symbolic = PatternMatcher([
|
||||
# (x//c+a)//d -> (x+a*c)//(c*d) for c>0, d>0
|
||||
((UPat.var("x")//UPat.cvar("c") + UPat.cvar("a"))//UPat.cvar("d"), lambda x,c,a,d: (x+a*c)//(c*d) if d.vmin>0 else None),
|
||||
# (x+c)//d -> (x+c%d)//d + c//d for d>0 (split out the multiple of d in the constant)
|
||||
((UPat.var("x", dtypes.index)+UPat.cvar("c"))//UPat.cvar("d"),
|
||||
((UPat.var("x", dtypes.weakint)+UPat.cvar("c"))//UPat.cvar("d"),
|
||||
lambda x,c,d: (x+c.arg%d.arg)//d + c.arg//d.arg if c.arg%d.arg!=c.arg and d.arg>0 else None),
|
||||
|
||||
# ** 2. Slow Rules **
|
||||
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), dtypes.index, name="d"), lambda d: fold_divmod_general(d)),
|
||||
(UPat((Ops.FLOORDIV, Ops.FLOORMOD), dtypes.weakint, name="d"), lambda d: fold_divmod_general(d)),
|
||||
])
|
||||
|
||||
@@ -16,5 +16,11 @@ mop_cleanup = PatternMatcher([
|
||||
lambda src,stk: src if stk.shape == src.shape and list(range(len(stk.src))) == [x.src[1].arg for x in stk.src] else None),
|
||||
# const INDEX into STACK is src
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.STACK, name="a"), UPat.cvar("i")), name="idx", allow_any_len=True),
|
||||
lambda a,i,idx: a.src[i.arg] if len(idx.src) <= 2 else a.src[i.arg].index(*idx.src[2:])),
|
||||
lambda a,i,idx: a.src[i.arg] if len(idx.src) <= 2 else a.src[i.arg].index(*idx.src[2:])),
|
||||
# INDEX on INDEX is INDEX
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"),
|
||||
lambda idx1,idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:]) if all(x.shape == () for x in idx1.src[1:]+idx2.src[1:]) else None),
|
||||
# INDEX on shaped INDEX (TODO: this can be more generic)
|
||||
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx1_arg"))),), allow_any_len=True, name="idx2"),
|
||||
lambda buf,idx1_arg,idx2: buf.index(idx1_arg.index(*idx2.src[1:])) if len(idx1_arg.shape) == len(idx2.src[1:]) else None),
|
||||
])
|
||||
|
||||
+58
-44
@@ -4,7 +4,7 @@ import sys, time, functools, itertools, math, operator, hashlib, os, types, pick
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum, auto
|
||||
from tinygrad.uop import Ops, GroupOp
|
||||
from tinygrad.dtype import ConstType, dtypes, DType, DTypeLike, truncate, least_upper_dtype, least_upper_float, strong_dtype, Invalid, AddrSpace
|
||||
from tinygrad.dtype import ConstType, dtypes, DType, DTypeLike, truncate, least_upper_dtype, least_upper_float, Invalid, AddrSpace
|
||||
from tinygrad.dtype import ConstFloat, PyConst, InvalidType, storage_fmt_for_dtype, to_storage_scalar, from_storage_scalar
|
||||
from tinygrad.device import Buffer, MultiBuffer, canonicalize_device
|
||||
from tinygrad.helpers import ContextVar, all_int, prod, getenv, all_same, Context, partition, temp, unwrap, T, argfix, Metadata, flatten, TRACEMETA
|
||||
@@ -24,12 +24,13 @@ class ParamArg:
|
||||
slot: int
|
||||
dtype: DType
|
||||
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
|
||||
def __repr__(self):
|
||||
fields = (("vmin_vmax", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None))
|
||||
fields = (("vmin_vmax", None), ("multiple_of", None), ("name", None), ("addrspace", AddrSpace.GLOBAL), ("axis", None), ("device", None))
|
||||
args = [repr(self.slot), repr(self.dtype)] + [f"{k}={v!r}" for k,default in fields if (v:=getattr(self, k)) != default]
|
||||
return f"ParamArg({', '.join(args)})"
|
||||
axis_letters = {AxisType.GLOBAL: "g", AxisType.THREAD: "t", AxisType.LOCAL: "l", AxisType.WARP: "w", AxisType.LOOP: "L", AxisType.UPCAST: "u",
|
||||
@@ -73,6 +74,10 @@ def _broadcast_shape(*shapes:tuple[sint, ...]) -> tuple[sint, ...]:
|
||||
raise IndexError(f"shape mismatch: objects cannot be broadcast to a single shape {shapes}")
|
||||
ret.append(rest[0] if rest else 1)
|
||||
return tuple(ret)
|
||||
def broadcast_axes(src_shape:tuple[sint, ...], out_shape:tuple[sint, ...]) -> tuple[int, ...]:
|
||||
# out axes that are added or expanded
|
||||
if (nleft:=len(out_shape)-len(src_shape)) < 0: raise RuntimeError(f"cannot broadcast {src_shape} into {out_shape}")
|
||||
return tuple(range(nleft)) + tuple(nleft+i for i,s in enumerate(src_shape) if resolve(s == 1, default=False) and resolve(out_shape[nleft+i] != 1))
|
||||
|
||||
def ssimplify(uop:sint): return uop.ssimplify() if isinstance(uop, UOp) else uop
|
||||
def sym_infer(uop: UOp|int, var_vals: dict[str, int]) -> int: return uop.sym_infer(var_vals) if isinstance(uop, UOp) else uop
|
||||
@@ -88,8 +93,8 @@ def multirange_str(rngs:Iterable[UOp], color=False, pad=None) -> str:
|
||||
|
||||
def shape_to_shape_arg(arg:tuple[sint, ...]) -> UOp:
|
||||
if len(arg) == 0: return UOp(Ops.STACK)
|
||||
elif len(arg) == 1: return UOp.const(dtypes.index, arg[0])
|
||||
else: return UOp(Ops.STACK, src=tuple(UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in arg))
|
||||
elif len(arg) == 1: return UOp.const(dtypes.weakint, arg[0])
|
||||
else: return UOp(Ops.STACK, src=tuple(UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in arg))
|
||||
|
||||
def consumer_map_from_toposort(lst:Iterable[UOp]):
|
||||
ret: dict[UOp, dict[UOp, None]] = {}
|
||||
@@ -100,7 +105,6 @@ def consumer_map_from_toposort(lst:Iterable[UOp]):
|
||||
return ret
|
||||
|
||||
def promo_dtype(src:tuple[UOp,...]) -> DType:
|
||||
# TODO: delete this once we merge index and weakint
|
||||
dts = [x.dtype for x in src]
|
||||
return dts[0] if all_same(dts) else least_upper_dtype(*dts)
|
||||
|
||||
@@ -108,7 +112,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
# here are the dtype production rules, eventually this will go in UOp as a recursive property
|
||||
match op:
|
||||
case Ops.STORE | Ops.CALL | Ops.LINEAR | Ops.SINK | Ops.PROGRAM | Ops.SOURCE | \
|
||||
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | \
|
||||
Ops.END | Ops.BARRIER | Ops.GROUP | Ops.IF | Ops.ENDIF | Ops.LOOP | \
|
||||
Ops.TUPLE | Ops.FUNCTION | Ops.CUSTOM_FUNCTION | Ops.WAIT | Ops.REWRITE_ERROR:
|
||||
# always void
|
||||
return dtypes.void
|
||||
@@ -125,7 +129,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
case Ops.CMPLT | Ops.CMPNE | Ops.CMPEQ:
|
||||
return dtypes.bool
|
||||
case Ops.SIN | Ops.LOG2 | Ops.EXP2 | Ops.SQRT | Ops.RECIPROCAL:
|
||||
return dtypes.weakfloat if src[0].dtype == dtypes.weakint else least_upper_float(src[0].dtype)
|
||||
return least_upper_float(src[0].dtype)
|
||||
case Ops.WHERE:
|
||||
assert src[0].dtype == dtypes.bool, f"where first arg isn't bool, it's {src[0].dtype}"
|
||||
return promo_dtype(src[1:])
|
||||
@@ -162,7 +166,7 @@ def dtype_from_uop(op:Ops, src:tuple[UOp,...], arg:Any) -> DType|None:
|
||||
# derived from the value. order matters: bool is an int subclass, ConstFloat is a float subclass
|
||||
if isinstance(arg, InvalidType): return dtypes.bool # Invalid is the lattice bottom, typed by its consumer
|
||||
if isinstance(arg, bool): return dtypes.bool
|
||||
if isinstance(arg, int): return dtypes.weakint
|
||||
if isinstance(arg, int): return None
|
||||
if isinstance(arg, float): return dtypes.weakfloat
|
||||
raise TypeError(f"no dtype for CONST with arg {arg}")
|
||||
if op in GroupOp.Unary: return src[0].dtype
|
||||
@@ -342,7 +346,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
# some ops init the shape
|
||||
case Ops.GETADDR: return ()
|
||||
case Ops.BIND | Ops.RANGE | Ops.SPECIAL: return ()
|
||||
case Ops.BIND | Ops.RANGE | Ops.SPECIAL | Ops.LOOP: return ()
|
||||
case Ops.BINARY: return (len(self.arg),)
|
||||
case Ops.BUFFER:
|
||||
if len(self.src): return self.src[0].as_shape
|
||||
@@ -530,7 +534,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
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]))
|
||||
def index(self, *srcs:UOp|int|None, **kwargs):
|
||||
new_srcs: list[UOp] = [UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in srcs if x is not None]
|
||||
new_srcs: list[UOp] = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in srcs if x is not None]
|
||||
if len(new_srcs) == 1 and new_srcs[0].op is Ops.CONST and self.op is Ops.STACK: return self.src[new_srcs[0].arg]
|
||||
return UOp(Ops.INDEX, src=(self,)+tuple(new_srcs), **kwargs)
|
||||
def __getitem__(self, idx):
|
||||
@@ -542,11 +546,11 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
bounds = tuple((s.start or 0, s.stop if s.stop is not None else self.shape[i]) if isinstance(s, slice) else (0, self.shape[i])
|
||||
for i, s in enumerate(idx))
|
||||
src = self.shrink(bounds)
|
||||
non_slice_args = [UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in idx if not isinstance(x, slice)]
|
||||
non_slice_args = [UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx if not isinstance(x, slice)]
|
||||
if not non_slice_args: return src # all dims are slices, no indexing needed
|
||||
perm = src.permute(tuple([i for i in range(src.ndim) if i not in slice_idx] + slice_idx))
|
||||
return perm.index(*non_slice_args)
|
||||
return self.index(*[UOp.const(dtypes.index, x) if isinstance(x, int) else x for x in idx])
|
||||
return self.index(*[UOp.const(dtypes.weakint, x) if isinstance(x, int) else x for x in idx])
|
||||
@property
|
||||
def _uop(self) -> UOp: return self
|
||||
@classmethod
|
||||
@@ -595,10 +599,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
ret = UOp(Ops.CONST, dtype, arg=dtype.const(b), src=())
|
||||
return ret._mop(Ops.EXPAND, arg=shape) if shape is not None and shape != () and ret.shape != shape else ret
|
||||
@staticmethod
|
||||
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.index, src=(), **kwargs):
|
||||
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
|
||||
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
|
||||
@staticmethod
|
||||
def special(end:sint, name:str, dtype=dtypes.index): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
|
||||
def loop(axis_id:int, *arg): return UOp(Ops.LOOP, src=(), arg=(axis_id,)+arg)
|
||||
@staticmethod
|
||||
def special(end:sint, name:str, dtype=dtypes.weakint): return UOp(Ops.SPECIAL, src=(sint_to_uop(end, dtype),), arg=name)
|
||||
@staticmethod
|
||||
def wmma(a:UOp, b:UOp, acc:UOp, dims:tuple[int, int, int], device:str, threads:int, tc_upcast_axes=None):
|
||||
# dtype_in is stored in the arg (not derived from src[0].dtype) because bitcast rewrites change src dtypes
|
||||
@@ -614,7 +620,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
ret = UOp(Ops.REDUCE, src=(self.permute(perm),), arg=(op, len(reduce_axis)))
|
||||
return ret.reshape(tuple(s for i,s in enumerate(self.shape) if i not in axis)) if axis != reduce_axis else ret
|
||||
@staticmethod
|
||||
def invalid(): return UOp.const(dtypes.index, Invalid)
|
||||
def invalid(): return UOp.const(dtypes.weakint, Invalid)
|
||||
def valid(self, cond):
|
||||
return cond.where(self, self.const_like(Invalid))
|
||||
def get_idx(self) -> UOp:
|
||||
@@ -704,6 +710,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
def copy_to_device(self, device:str|tuple[str, ...], arg=None):
|
||||
assert arg is None or isinstance(self.device, tuple)
|
||||
inp = self if arg is None else UOp(Ops.MSELECT, src=(self,), arg=arg)
|
||||
if inp.dtype in dtypes.weaks: raise RuntimeError(f"cannot create storage for weak dtype {inp.dtype}")
|
||||
return UOp(Ops.COPY, src=(inp,), arg=device)
|
||||
def mselect(self, arg:int) -> UOp: return UOp(Ops.MSELECT, src=(self,), arg=arg)
|
||||
def mstack(self, *srcs: UOp) -> UOp: return UOp(Ops.MSTACK, src=(self,)+srcs)
|
||||
@@ -758,6 +765,7 @@ 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}")
|
||||
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))
|
||||
@staticmethod
|
||||
@@ -785,7 +793,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
return ret if ret.device == device else ret.copy_to_device(device)
|
||||
def clone(self, device=None) -> UOp:
|
||||
device = device or self.device
|
||||
ret = self.empty_like(dtype=strong_dtype(self.dtype), device=device)
|
||||
ret = self.empty_like(device=device)
|
||||
src = self if self.device is None or self.device == device else self.copy_to_device(device)
|
||||
return ret.after(ret.store(src.cast(ret.dtype)))
|
||||
@recursive_property
|
||||
@@ -809,10 +817,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.BUFFER: return self.arg.addrspace
|
||||
if self.op in {Ops.SPECIAL, Ops.RANGE}: return AddrSpace.ALU
|
||||
if self.op is Ops.LOAD: return AddrSpace.ALU # LOAD brings things into the ALU
|
||||
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT}:
|
||||
if self.op in {Ops.INDEX, Ops.CAST, Ops.AFTER, Ops.REDUCE, Ops.STORE, Ops.MSTACK, Ops.MSELECT, Ops.END}:
|
||||
return self.src[0].addrspace
|
||||
if self.op in GroupOp.Movement: return self.src[0].addrspace
|
||||
if self.op in {Ops.STACK, Ops.WMMA} or self.op in GroupOp.Elementwise:
|
||||
if self.op in {Ops.STACK, Ops.WMMA, Ops.GROUP} or self.op in GroupOp.Elementwise:
|
||||
ad = [x.addrspace for x in self.src if x.addrspace is not None]
|
||||
if not len(ad) or not all_same(ad): return None
|
||||
return ad[0]
|
||||
@@ -913,9 +921,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
# *** uop Variable stuff ***
|
||||
|
||||
@staticmethod
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.index) -> UOp:
|
||||
def variable(name:str, min_val:PyConst, max_val:PyConst, dtype:DType=dtypes.weakint, multiple_of:int=1) -> UOp:
|
||||
return UOp(Ops.PARAM, src=(shape_to_shape_arg(()),),
|
||||
arg=ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), addrspace=AddrSpace.ALU))
|
||||
arg=ParamArg(-1, dtype, name=name, vmin_vmax=(min_val, max_val), multiple_of=multiple_of, addrspace=AddrSpace.ALU))
|
||||
@property
|
||||
def expr(self) -> str:
|
||||
assert self.op is Ops.PARAM
|
||||
@@ -924,6 +932,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
assert self.op is Ops.PARAM and self.addrspace is AddrSpace.ALU, f"op is {self.op}, need PARAM"
|
||||
uval = self.const_like(val) if isinstance(val, int) else val
|
||||
assert self.vmin <= uval.vmin and uval.vmax <= self.vmax, f"bind {val} not in range [{self.vmin}, {self.vmax}]"
|
||||
assert uval.divides(self.arg.multiple_of) is not None, f"bind {val} not divisible by {self.arg.multiple_of}"
|
||||
return UOp(Ops.BIND, src=(self, uval))
|
||||
def unbind(self) -> tuple[Variable, int]:
|
||||
assert self.op is Ops.BIND and self.src[0].op is Ops.PARAM and self.src[1].op is Ops.CONST, f"can't unbind {self}"
|
||||
@@ -946,6 +955,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.STACK: return math.gcd(*[x.const_factor() for x in self.src])
|
||||
if self.op is Ops.ADD: return math.gcd(self.src[0].const_factor(), self.src[1].const_factor())
|
||||
if self.op is Ops.MUL: return self.src[0].arg if self.src[0].op is Ops.CONST else self.src[1].arg if self.src[1].op is Ops.CONST else 1
|
||||
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self.arg.multiple_of
|
||||
return 1
|
||||
def divides(self, v:int) -> UOp|None:
|
||||
if v==1: return self
|
||||
@@ -957,6 +967,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.MUL:
|
||||
if (d0:=self.src[0].divides(v)) is not None: return d0 * self.src[1]
|
||||
if (d1:=self.src[1].divides(v)) is not None: return self.src[0] * d1
|
||||
if self.op is Ops.PARAM and self.arg.multiple_of is not None: return self // v if self.arg.multiple_of%v == 0 else None
|
||||
return None # generic None if we aren't sure
|
||||
def pop_const(self, op=Ops.ADD) -> tuple[UOp, PyConst]: # NOTE: assume Invalid ALU is resolved
|
||||
return (self.src[0], self.src[1].arg) if self.op is op and self.src[1].op is Ops.CONST else (self, identity_element(op, self.dtype))
|
||||
@@ -1027,9 +1038,12 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
if self.op is Ops.STACK: return min(x.vmin for x in self.src), max(x.vmax for x in self.src)
|
||||
if self.op is Ops.CONST and self.arg is not Invalid: return self.arg, self.arg
|
||||
if self.op is Ops.INDEX: return self.src[0]._min_max
|
||||
# TODO: CAST to bool/unsigned is not monotone, still some case can be simplified
|
||||
if self.op is Ops.CAST and self.dtype in dtypes.floats+dtypes.sints+(dtypes.index,):
|
||||
return max(self.dtype.min, self.src[0].vmin), min(self.src[0].vmax, self.dtype.max)
|
||||
if self.op is Ops.CAST:
|
||||
# a cast to unsigned keeps exact bounds when the source fits
|
||||
# TODO: can do more based on new dtype window
|
||||
if dtypes.is_unsigned(self.dtype) and 0 <= self.src[0].vmin and self.src[0].vmax <= self.dtype.max: return self.src[0]._min_max
|
||||
if self.dtype in dtypes.floats+dtypes.sints+(dtypes.weakint,):
|
||||
return max(self.dtype.min, self.src[0].vmin), min(self.src[0].vmax, self.dtype.max)
|
||||
return self.dtype.min, self.dtype.max
|
||||
|
||||
@functools.cached_property
|
||||
@@ -1082,16 +1096,16 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
|
||||
# TODO: this should replace placeholder
|
||||
@staticmethod
|
||||
def param(slot:int, dtype:DType, shape:tuple[sint, ...]|None=None, device=None, vmin_vmax:tuple[PyConst, PyConst]|None=None, name=None,
|
||||
addrspace=AddrSpace.GLOBAL, axis:int|None=None):
|
||||
def param(slot:int, dtype:DType, shape:tuple[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):
|
||||
if dtype in dtypes.weaks: raise RuntimeError(f"cannot create param for weak dtype {dtype}")
|
||||
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, name, addrspace, axis, device))
|
||||
return UOp(Ops.PARAM, src=src, arg=ParamArg(slot, dtype, vmin_vmax, multiple_of, name, addrspace, axis, device))
|
||||
def param_like(self, slot:int):
|
||||
addrspace = self.addrspace if self.addrspace is not None else AddrSpace.GLOBAL
|
||||
if self.op is Ops.BIND:
|
||||
return UOp.param(slot, self.dtype, self._shape, self.device, cast(tuple[int, int], self._min_max), self.src[0].expr, addrspace)
|
||||
if self.op is Ops.BIND: return self.src[0].replace(arg=replace(self.src[0].arg, slot=slot, addrspace=addrspace))
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
@@ -1666,7 +1680,7 @@ def graph_rewrite(sink:UOp, pm:PatternMatcher, ctx=None, bottom_up=False, name=N
|
||||
rewrite_ctx = RewriteContext(pm if not bottom_up else None, pm if bottom_up else bpm, ctx, enter_calls)
|
||||
return rewrite_ctx.walk_rewrite(sink) if walk else rewrite_ctx.unified_rewrite(sink)
|
||||
|
||||
def sint_to_uop(x:sint, dtype=dtypes.index) -> UOp: return UOp.const(dtype, x) if isinstance(x, int) else x.cast(dtype)
|
||||
def sint_to_uop(x:sint, dtype=dtypes.weakint) -> UOp: return UOp.const(dtype, x) if isinstance(x, int) else x.cast(dtype)
|
||||
def to_max_shape(shape:tuple[sint, ...]) -> tuple[int, ...]: return tuple(int(x.vmax) if isinstance(x, UOp) else x for x in shape)
|
||||
|
||||
def select_dtype(u:UOp):
|
||||
@@ -1676,22 +1690,22 @@ def lower_alu_dtype(u:UOp, x:UOp, y:UOp, dt:DType) -> UOp:
|
||||
return src[0].alu(u.op, *src[1:]).cast(u.dtype)
|
||||
pm_lower_index_dtype = PatternMatcher([
|
||||
# There are no Unary ops at this point in symbolic, those are introduced later
|
||||
(UPat(Ops.CONST, dtype=dtypes.index, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype) if u.arg!=Invalid else None),
|
||||
(UPat(Ops.CONST, dtype=dtypes.weakint, name="u"), lambda u: u.replace(dtype=select_dtype(u)).cast(u.dtype) if u.arg!=Invalid else None),
|
||||
# Binary can widen the dtype, WHERE cannot
|
||||
(UPat(GroupOp.Binary, name="u", src=(UPat.var("x").cast(dtypes.index), UPat.var("y").cast(dtypes.index))),
|
||||
(UPat(GroupOp.Binary, name="u", src=(UPat.var("x").cast(dtypes.weakint), UPat.var("y").cast(dtypes.weakint))),
|
||||
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(select_dtype(u), x.dtype, y.dtype))),
|
||||
(UPat(Ops.WHERE, dtypes.index, src=(UPat(), UPat.var("x").cast(dtypes.index), UPat.var("y").cast(dtypes.index)), name="u"),
|
||||
(UPat(Ops.WHERE, dtypes.weakint, src=(UPat(), UPat.var("x").cast(dtypes.weakint), UPat.var("y").cast(dtypes.weakint)), name="u"),
|
||||
lambda u,x,y: lower_alu_dtype(u, x, y, least_upper_dtype(x.dtype, y.dtype))),
|
||||
(UPat(Ops.RANGE, src=(UPat.var("end").cast(dtypes.index)), name="r"), lambda r,end: r.replace(dtype=end.dtype, src=(end,)).cast(dtypes.index)),
|
||||
(UPat(Ops.STACK, src=UPat().cast(dtypes.index), name="v"),
|
||||
lambda v: v.replace(dtype=(dt:=select_dtype(v)), src=tuple(s.src[0].cast(dt) for s in v.src)).cast(dtypes.index)),
|
||||
(UPat(Ops.RANGE, src=(UPat.var("end").cast(dtypes.weakint)), name="r"), lambda r,end: r.replace(dtype=end.dtype, src=(end,)).cast(dtypes.weakint)),
|
||||
(UPat(Ops.STACK, src=UPat().cast(dtypes.weakint), name="v"),
|
||||
lambda v: v.replace(dtype=(dt:=select_dtype(v)), src=tuple(s.src[0].cast(dt) for s in v.src)).cast(dtypes.weakint)),
|
||||
# special can only be int32
|
||||
(UPat(Ops.SPECIAL, src=(UPat.var("var").cast(dtypes.index),), name="u"),
|
||||
lambda u,var: u.replace(dtype=dtypes.int, src=(var,)).cast(dtypes.index)),
|
||||
(UPat(Ops.PARAM, dtype=dtypes.index, name="u"),
|
||||
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.index) if u.addrspace == AddrSpace.ALU else None),
|
||||
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.index), UPat.cvar("val").cast(dtypes.index))),
|
||||
lambda var,val: var.bind(val).cast(dtypes.index)),
|
||||
(UPat(Ops.SPECIAL, src=(UPat.var("var").cast(dtypes.weakint),), name="u"),
|
||||
lambda u,var: u.replace(dtype=dtypes.int, src=(var,)).cast(dtypes.weakint)),
|
||||
(UPat(Ops.PARAM, dtype=dtypes.weakint, name="u"),
|
||||
lambda u: u.replace(dtype=None, arg=replace(u.arg, dtype=dtypes.int)).cast(dtypes.weakint) if u.addrspace == AddrSpace.ALU else None),
|
||||
(UPat(Ops.BIND, src=(UPat.var("var").cast(dtypes.weakint), UPat.cvar("val").cast(dtypes.weakint))),
|
||||
lambda var,val: var.bind(val).cast(dtypes.weakint)),
|
||||
# remove hanging casts
|
||||
(UPat(Ops.INDEX, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast()),), lambda buf,idx: buf.index(idx)),
|
||||
(UPat(Ops.SHRINK, src=(UPat.var("buf"), UPat.var("idx", dtypes.ints).cast(), UPat.var("slen", dtypes.ints).cast(),), name="shrink"),
|
||||
@@ -1707,7 +1721,7 @@ pm_lower_index_dtype = PatternMatcher([
|
||||
UPat.var("gate").where(UPat.var("idx_x", dtypes.ints).cast(), UPat(Ops.CONST, arg=Invalid)))),
|
||||
lambda buf,idx_x,idx_y,gate: buf.index(idx_y.valid(gate), idx_x.valid(gate), dtype=dtypes.float)),
|
||||
(UPat((Ops.SINK, Ops.NOOP, Ops.END), name="n"),
|
||||
lambda n: n.replace(src=tuple(s.src[0] if s.op is Ops.CAST and s.dtype == dtypes.index else s for s in n.src))),
|
||||
lambda n: n.replace(src=tuple(s.src[0] if s.op is Ops.CAST and s.dtype == dtypes.weakint else s for s in n.src))),
|
||||
])
|
||||
def _index_to_concrete_int(u:UOp) -> UOp: return graph_rewrite(u.sink(), pm_lower_index_dtype).src[0]
|
||||
|
||||
@@ -1728,8 +1742,8 @@ pm_unbind = PatternMatcher([(UPat(Ops.BIND, name="x"), do_unbind)])
|
||||
|
||||
# ctx is source UOp for which we are finding a contiguous view for. used in contiguous_view_offset
|
||||
pm_contiguous_view_offset = PatternMatcher([
|
||||
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(dtypes.index, 0)),
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(dtypes.index, 0)),
|
||||
(UPat(Ops.INDEX, src=(UPat(),)), lambda: UOp.const(dtypes.weakint, 0)),
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE))), lambda: UOp.const(dtypes.weakint, 0)),
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat(Ops.RANGE)+UPat.cvar('c'))), lambda c: c),
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat.cvar('c'))), lambda ctx, c: c if resolve(ctx.numel() == 1, False) else None),
|
||||
])
|
||||
|
||||
@@ -35,6 +35,7 @@ renderer = PatternMatcher([
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: x.arg.name if x.arg.name is not None else f"p{x.arg.slot}"),
|
||||
(UPat((Ops.SPECIAL), name="x"), lambda x: x.arg),
|
||||
(UPat(Ops.RANGE, name="x"), lambda x: f"r{range_str(x)}"),
|
||||
(UPat(Ops.LOOP, name="x"), lambda x: f"loop{x.arg[0]}"),
|
||||
(UPat(Ops.CONST, name="x"), lambda x: str(x.arg)),
|
||||
(UPat(Ops.CAST, name="x"), lambda ctx,x: f"({str(x.dtype)[7:]})({ctx[x.src[0]]})"),
|
||||
(UPat(Ops.BIND, name="x"), lambda ctx,x: ctx[x.src[0]]),
|
||||
@@ -88,7 +89,7 @@ pm_pyrender_extra = PatternMatcher([
|
||||
# NOTE: range has srcs sometimes after control flow
|
||||
(UPat(Ops.RANGE, src=(UPat(Ops.CONST, name="c"),), allow_any_len=True, name="x"), lambda ctx,x,c:
|
||||
"UOp.range("+', '.join([str(c.arg)] + [repr(y) for y in x.arg])+
|
||||
(f', src={srcs(ctx, x.src[1:])}' if len(x.src) > 1 else '')+(', dtype='+str(x.dtype) if x.dtype is not dtypes.index else '')+")"),
|
||||
(f', src={srcs(ctx, x.src[1:])}' if len(x.src) > 1 else '')+(', dtype='+str(x.dtype) if x.dtype is not dtypes.weakint else '')+")"),
|
||||
# TODO: index shouldn't mismatch dtype
|
||||
(UPat(Ops.INDEX, src=(UPat(), UPat()), allow_any_len=True, name="x"), lambda ctx,x:
|
||||
f"{ctx[x.src[0]]}.index({ctx[x.src[1]]}, "+''.join([f"{ctx[xx]}, " for xx in x.src[2:]])+
|
||||
|
||||
+19
-16
@@ -61,15 +61,13 @@ spec_shared = PatternMatcher([
|
||||
(UPat(Ops.STACK, src=(UPat(),), allow_any_len=True, name="s"),
|
||||
lambda s: all_same([x.shape for x in s.src]) and all(x.dtype == s.dtype for x in s.src)),
|
||||
|
||||
# ALUs: most ALUs have all matching dtypes, except CMPLT, CMPNE, and WHERE
|
||||
# ALUs: operands match the result dtype, except comparisons/WHERE; renderer-lowered shifts may use a uint32 count
|
||||
# a weak dtype matches any dtype (TODO: make python scalars weak consts)
|
||||
(UPat(Ops.WHERE, name="w", src=(UPat(dtype=dtypes.bool), UPat.var("x"), UPat.var("y"))),
|
||||
lambda w,x,y: all(s.dtype == w.dtype or s.dtype in dtypes.weaks for s in (x,y))),
|
||||
(UPat((Ops.CMPLT, Ops.CMPNE, Ops.CMPEQ), dtype=dtypes.bool, src=(UPat.var("x"), UPat.var("y"))),
|
||||
(UPat(Ops.WHERE, name="w", src=(UPat(dtype=dtypes.bool), UPat(), UPat())),
|
||||
lambda w: all(s.dtype == w.dtype or s.dtype in dtypes.weaks for s in w.src[1:])),
|
||||
(UPat(GroupOp.Comparison, dtype=dtypes.bool, src=(UPat.var("x"), UPat.var("y"))),
|
||||
lambda x,y: x.dtype == y.dtype or x.dtype in dtypes.weaks or y.dtype in dtypes.weaks),
|
||||
# and SHL/SHR, the shift distance can be an int
|
||||
(UPat((Ops.SHL, Ops.SHR), src=(UPat.var("x"), UPat.var("y")), name="a"),
|
||||
lambda a,x,y: a.dtype == x.dtype and (y.dtype in (x.dtype, dtypes.uint) or y.dtype in dtypes.weaks)),
|
||||
(UPat((Ops.SHL, Ops.SHR), src=(UPat.var("x"), UPat(dtype=dtypes.uint)), name="a"), lambda a,x: a.dtype == x.dtype or None),
|
||||
(UPat((Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD), name="x"), lambda x: None if dtypes.is_int(x.dtype) else False),
|
||||
(UPat(GroupOp.ALU, name="x"), lambda x: all(y.dtype == x.dtype or y.dtype in dtypes.weaks for y in x.src)),
|
||||
|
||||
@@ -81,7 +79,12 @@ spec_shared = PatternMatcher([
|
||||
rng.dtype == x.dtype and isinstance(rng.arg, tuple) and len(rng.arg) >= 2 and \
|
||||
all(isinstance(ra, int) for ra in rng.arg[0:-1]) and isinstance(rng.arg[-1], AxisType)),
|
||||
(UPat(Ops.INDEX, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(dtypes.is_int(y.dtype) for y in x.src[1:]) or None),
|
||||
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:])),
|
||||
# LOOP is a bound-less loop header, the arg is an axis id like RANGE but without an AxisType
|
||||
(UPat(Ops.LOOP, dtypes.void, name="l"), lambda l: isinstance(l.arg, tuple) and all(isinstance(ra, int) for ra in l.arg)),
|
||||
# END closes RANGEs
|
||||
(UPat(Ops.END, src=(UPat(),), allow_any_len=True, name="x"), lambda x: all(u.op is Ops.RANGE for u in x.src[1:]) or None),
|
||||
# a LOOP-ended END requires a trailing bool condition for the backedge (loop again while true)
|
||||
(UPat(Ops.END, src=(UPat(), UPat(Ops.LOOP), UPat(dtype=dtypes.bool))), lambda: True),
|
||||
|
||||
# PARAM
|
||||
(UPat(Ops.PARAM, name="x"), lambda x: isinstance(x.arg, ParamArg)),
|
||||
@@ -136,11 +139,11 @@ spec_tensor = PatternMatcher([
|
||||
|
||||
# BUFFER
|
||||
(UPat(Ops.BUFFER, src=(UPat(),), name="buf"), lambda buf:
|
||||
(isinstance(buf.dtype, DType) and buf.src[0].dtype == dtypes.index and is_device(buf.arg.device))
|
||||
(isinstance(buf.dtype, DType) and buf.src[0].dtype == dtypes.weakint and is_device(buf.arg.device))
|
||||
if isinstance(buf.arg, ParamArg) and buf.addrspace is AddrSpace.GLOBAL else None),
|
||||
|
||||
# Tensor variable bindings
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.index,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.index,))), arg=None), lambda: True),
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint,), (UPat(Ops.PARAM), UPat.cvar(dtype=(dtypes.int,dtypes.weakint,))), arg=None), lambda: True),
|
||||
|
||||
# custom function
|
||||
(UPat(Ops.CUSTOM_FUNCTION, name="x"), lambda x: isinstance(x.arg, str)),
|
||||
@@ -155,10 +158,10 @@ spec_tensor = PatternMatcher([
|
||||
(UPat(Ops.GETTUPLE, src=(UPat(Ops.TUPLE, name="t"),), name="g"), valid_gettuple),
|
||||
|
||||
# SPECIAL is index before index lowering. custom_kernel currently has this
|
||||
(UPat(Ops.SPECIAL, src=(UPat.var("x", dtypes.index),), name="s"), lambda s,x: s.dtype == x.dtype and isinstance(s.arg, str)),
|
||||
(UPat(Ops.SPECIAL, src=(UPat.var("x", dtypes.weakint),), name="s"), lambda s,x: s.dtype == x.dtype and isinstance(s.arg, str)),
|
||||
|
||||
# inputs to movement ops
|
||||
(UPat({Ops.ADD, Ops.MUL, Ops.CDIV, Ops.FLOORDIV}, dtype=dtypes.index), lambda: True),
|
||||
(UPat({Ops.ADD, Ops.MUL, Ops.CDIV, Ops.FLOORDIV}, dtype=dtypes.weakint), lambda: True),
|
||||
|
||||
# movement ops
|
||||
(UPat((Ops.RESHAPE, Ops.EXPAND), src=(UPat(), UPat())), lambda: True),
|
||||
@@ -168,7 +171,7 @@ spec_tensor = PatternMatcher([
|
||||
# REDUCE has arg=(op, num_axes), src[1:] are ranges after lowering
|
||||
(UPat(Ops.REDUCE, src=(UPat(),), allow_any_len=True, name="x"),
|
||||
lambda x: isinstance(x.arg, tuple) and len(x.arg) == 2 and x.arg[0] in GroupOp.Reduce
|
||||
and isinstance(x.arg[1], int) and all(y.dtype in (dtypes.index, dtypes.int) for y in x.src[1:])),
|
||||
and isinstance(x.arg[1], int) and all(y.dtype in (dtypes.weakint, dtypes.int) for y in x.src[1:])),
|
||||
|
||||
# COPY. TODO: this should not have allow_any_len, but something is adding ranges
|
||||
(UPat(Ops.COPY, name="copy", src=(UPat.var("x"),), allow_any_len=True), lambda copy,x: copy.dtype == x.dtype and is_device(copy.arg)),
|
||||
@@ -200,7 +203,7 @@ spec_tensor = PatternMatcher([
|
||||
# these ops can exist in programs but not the tensor spec. example: LOAD
|
||||
spec_program = PatternMatcher([
|
||||
# index and weak dtypes are not allowed in programs
|
||||
(UPat(GroupOp.All, (dtypes.index, dtypes.weakint, dtypes.weakfloat)), lambda: False),
|
||||
(UPat(GroupOp.All, (dtypes.weakint, dtypes.weakfloat)), lambda: False),
|
||||
|
||||
# allow special SHRINK
|
||||
(UPat(Ops.SHRINK, src=(UPat((Ops.PARAM, Ops.BUFFER, Ops.AFTER)), UPat(), UPat(Ops.CONST))), lambda: True),
|
||||
@@ -233,7 +236,7 @@ spec_full = PatternMatcher([
|
||||
|
||||
# SLICE on BUFFER is allowed if BUFFER is
|
||||
(UPat(Ops.SLICE, src=(UPat(GroupOp.Movement.union({Ops.BUFFER, Ops.PARAM, Ops.STAGE, Ops.AFTER})),
|
||||
UPat(Ops.CONST, dtype=dtypes.index)), allow_any_len=True, name="bv"),
|
||||
UPat(Ops.CONST, dtype=dtypes.weakint)), allow_any_len=True, name="bv"),
|
||||
lambda bv: isinstance(bv.arg, int)),
|
||||
|
||||
(UPat(Ops.CALL, dtypes.void, src=(UPat((Ops.SLICE,)),), allow_any_len=True), lambda: True),
|
||||
@@ -248,7 +251,7 @@ spec_full = PatternMatcher([
|
||||
(UPat((Ops.LOAD, Ops.STORE)), lambda: True),
|
||||
|
||||
# while BIND is being casted
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.index), (UPat(), UPat()), arg=None), lambda: True),
|
||||
(UPat(Ops.BIND, (dtypes.int, dtypes.weakint), (UPat(), UPat()), arg=None), lambda: True),
|
||||
])+spec_tensor+spec_program+spec_hcq
|
||||
|
||||
# **** pyrender (move this) ****
|
||||
|
||||
+19
-19
@@ -66,7 +66,7 @@ def fold_add_divmod_recombine(x:UOp) -> UOp|None:
|
||||
|
||||
# an invalid index is cond.where(idx, Invalid) in index. the consumer reads cond back off the WHERE with UOp.get_valid,
|
||||
# so casts and comparisons of a gated index can drop the gate: when the index is invalid the result is never used
|
||||
invalid_idx_gate = UPat().where(UPat.var("x"), UPat(Ops.CONST, dtypes.index, arg=Invalid))
|
||||
invalid_idx_gate = UPat().where(UPat.var("x"), UPat(Ops.CONST, dtypes.weakint, arg=Invalid))
|
||||
pm_index_invalid = PatternMatcher([
|
||||
(invalid_idx_gate.cast(name="cast"), lambda x,cast: x.cast(cast.dtype)),
|
||||
(UPat(GroupOp.Comparison, src=(invalid_idx_gate, UPat.var("y")), name="alu"), lambda x,y,alu: x.alu(alu.op,y)),
|
||||
@@ -110,14 +110,14 @@ symbolic_simple = propagate_invalid + PatternMatcher([
|
||||
# ** self folding **
|
||||
(UPat.var("x") + 0, lambda x: x), # x+0 -> x
|
||||
(UPat.var("x") * 1, lambda x: x), # x*1 -> x
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)) ^ 0, lambda x: x), # x^0 -> x
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) ^ 0, lambda x: x), # x^0 -> x
|
||||
(UPat.var("x") // UPat.var("x"), lambda x: x.const_like(1)), # x//x -> 1
|
||||
(UPat.var("x") // 1, lambda x: x), # x//1 -> x
|
||||
(UPat.var("x") // -1, lambda x: -x), # x//-1 -> -x
|
||||
((UPat.var("x") ^ UPat.var("y")) ^ UPat.var("y"), lambda x,y: x), # (x^y)^y -> x
|
||||
((UPat.var() % UPat.var("y")).named("base") % UPat.var("y"), lambda base,y: base), # (x%y)%y = -> x%y (rewritten with base for speed)
|
||||
# variations of (x%c)+(x//c)*c = x
|
||||
(UPat(Ops.ADD, dtype=dtypes.index, name="x"), fold_add_divmod_recombine),
|
||||
(UPat(Ops.ADD, dtype=dtypes.weakint, name="x"), fold_add_divmod_recombine),
|
||||
(UPat.var("x", dtype=dtypes.bool) & UPat.cvar("c"), lambda x,c: x if c.arg else c),
|
||||
(UPat.var("x", dtype=dtypes.bool) | UPat.cvar("c"), lambda x,c: c if c.arg else x),
|
||||
(UPat(GroupOp.Idempotent, src=(UPat.var("x"), UPat.var("x"))), lambda x: x),
|
||||
@@ -125,9 +125,9 @@ symbolic_simple = propagate_invalid + PatternMatcher([
|
||||
(UPat.var("x", dtype=dtypes.bool).where(UPat.const(dtypes.bool, True), UPat.const(dtypes.bool, False)), lambda x: x),
|
||||
(UPat.var("x", dtype=dtypes.bool).where(UPat.const(dtypes.bool, False), UPat.const(dtypes.bool, True)), lambda x: x.logical_not()),
|
||||
# CAST(bool -> int) != const — CAST(True)=1, CAST(False)=0, so fold based on const value
|
||||
(UPat.var("x", dtype=dtypes.bool).cast(dtypes.ints+(dtypes.weakint, dtypes.index)) != UPat.cvar("c"),
|
||||
(UPat.var("x", dtype=dtypes.bool).cast(dtypes.ints+(dtypes.weakint,)) != UPat.cvar("c"),
|
||||
lambda x,c: x if c.arg == 0 else x.logical_not() if c.arg == 1 else x.const_like(True)),
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)).trunc(), lambda x: x),
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)).trunc(), lambda x: x),
|
||||
# ** zero folding **
|
||||
(UPat.var("x") < UPat.var("x"), lambda x: x.const_like(False, dtypes.bool)), # x < x -> False
|
||||
(UPat.var("x") % UPat.var("x"), lambda x: x.const_like(0)), # x%x -> 0
|
||||
@@ -139,7 +139,7 @@ symbolic_simple = propagate_invalid + PatternMatcher([
|
||||
lambda x,mask,k: x >> k.arg if mask.arg | ((1 << k.arg) - 1) == -1 else None),
|
||||
((UPat.var("x") & UPat.cvar("mask")) // UPat.cvar("c"),
|
||||
lambda x,mask,c: x // c.arg if c.arg > 0 and c.arg & (c.arg-1) == 0 and mask.arg | (c.arg-1) == -1 else None),
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint, dtypes.index)) != UPat.var("x"),
|
||||
(UPat.var("x", dtype=dtypes.ints+(dtypes.bool, dtypes.weakint)) != UPat.var("x"),
|
||||
lambda x: x.const_like(False, dtypes.bool)), # x != x -> False (only ints)
|
||||
# ** constant folding **
|
||||
(UPat(GroupOp.Unary, src=(UPat((Ops.CONST, Ops.STACK)),), name="a"), fold_const_alu),
|
||||
@@ -211,7 +211,7 @@ def canonicalize_simplex(X:UOp) -> UOp|None:
|
||||
commutative = PatternMatcher([
|
||||
# ** COMMUTATIVE flipping (only for index) **
|
||||
# NOTE: this can break merging vector math by only flipping some of them
|
||||
(UPat(GroupOp.Commutative, dtype=dtypes.index, name='x'), lambda x:
|
||||
(UPat(GroupOp.Commutative, dtype=dtypes.weakint, name='x'), lambda x:
|
||||
x.replace(src=x.src[::-1]) if x.src[1].tuplize < x.src[0].tuplize and not x.src[0].tuplize < x.src[1].tuplize else None),
|
||||
])
|
||||
|
||||
@@ -229,7 +229,7 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
|
||||
((UPat.var("y") + UPat.var("x")) + UPat.var("x"), lambda y,x: y+x*2),
|
||||
((UPat.var("x") / UPat.var("x2")) / UPat.var("x3"), lambda x,x2,x3: x/(x2*x3) if x2 is not x3 else None), # (x/x2)/x3 -> x/(x2*x3)
|
||||
(-1 * (UPat.var("x") + UPat.cvar("c")), lambda x,c: (-x)+(-c)), # -(x+c) -> -x + -c
|
||||
(UPat.cvar("y") * (UPat.var("x", dtype=dtypes.index) + UPat.cvar("c")), lambda x,y,c: (y*x)+(y*c)), # y*(x+c) -> y*x + y*c
|
||||
(UPat.cvar("y") * (UPat.var("x", dtype=dtypes.weakint) + UPat.cvar("c")), lambda x,y,c: (y*x)+(y*c)), # y*(x+c) -> y*x + y*c
|
||||
# ** where folding **
|
||||
(UPat.var("cond", dtype=dtypes.bool).logical_not().where(UPat.var("t"), UPat.var("f")),
|
||||
lambda cond, t, f: cond.where(f,t) if f.arg is not Invalid else None),
|
||||
@@ -255,38 +255,38 @@ symbolic = symbolic_simple+commutative+PatternMatcher([
|
||||
((UPat.var("x") // UPat.cvar("c1")) // UPat.cvar("c2"), lambda x,c1,c2: x//(c1*c2) if c2.vmin>0 else None),
|
||||
# ** lt **
|
||||
# c0*x<c1 for positive int c0,c1
|
||||
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.index))<UPat.cvar("c1"),
|
||||
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
|
||||
lambda x,c0,c1: x<math.ceil(c1.arg/c0.arg) if c0.arg > 0 and c1.arg > 0 else None),
|
||||
# c0*x<c1 for negative int c0 and non-positive c1
|
||||
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.index))<UPat.cvar("c1"),
|
||||
((UPat.cvar("c0")*UPat.var("x", dtype=dtypes.weakint))<UPat.cvar("c1"),
|
||||
lambda x,c0,c1: (-x)<(-(math.floor(-c1.arg/-c0.arg))) if c0.arg < 0 and c0.arg != -1 and c1.arg <= 0 else None),
|
||||
# x//d<c -> x<c*d for d>0
|
||||
((UPat.var("x", dtype=dtypes.index)//UPat.cvar("d"))<UPat.cvar("c"),
|
||||
((UPat.var("x", dtype=dtypes.weakint)//UPat.cvar("d"))<UPat.cvar("c"),
|
||||
lambda x,d,c: x<(c.arg*d.arg) if d.arg > 0 else None),
|
||||
# ** move add/mul consts to end (NOTE: this is still happening before constant folding) **
|
||||
((UPat.var("x") + UPat.cvar("c1")) + UPat.var("y"), lambda x,c1,y: (x+y)+c1),
|
||||
((UPat.var("x") * UPat.cvar("c1")) * UPat.var("y"), lambda x,c1,y: (x*y)*c1),
|
||||
# *** rules from symbolic ***
|
||||
# generic lt folding
|
||||
(UPat.var("x", dtypes.index)<UPat.cvar("c"), lambda x,c: lt_folding(x, c.arg) if 0 < c.arg else None),
|
||||
(UPat.var("x", dtypes.index)*-1 < UPat.var("y")*-1, lambda x,y: y<x),
|
||||
(UPat.var("x", dtypes.weakint)<UPat.cvar("c"), lambda x,c: lt_folding(x, c.arg) if 0 < c.arg else None),
|
||||
(UPat.var("x", dtypes.weakint)*-1 < UPat.var("y")*-1, lambda x,y: y<x),
|
||||
# canonicalize a simplex with positive coefficients > 0. NOTE: not x < 1 means x > 0
|
||||
((UPat.var("x", dtypes.index)<1).ne(True), lambda x: (newx<1).ne(True) if (newx:=canonicalize_simplex(x)) is not None else None),
|
||||
((UPat.var("x", dtypes.weakint)<1).ne(True), lambda x: (newx<1).ne(True) if (newx:=canonicalize_simplex(x)) is not None else None),
|
||||
# a range mod its own upper bound is just the range
|
||||
(UPat(Ops.RANGE, src=UPat.var("end"), name="r")%UPat.var("end"), lambda r,end: r),
|
||||
(UPat(Ops.RANGE, src=UPat.var("end"), name="r")//UPat.var("end"), lambda r,end: r.const_like(0)),
|
||||
# cast/long folding
|
||||
# if the intermediate cast doesnt narrow we can do it in one cast
|
||||
(UPat.var('x').cast(name="a").cast(name="b"), lambda x,a,b: x.cast(b.dtype) if can_lossless_cast(x.dtype, a.dtype) else None),
|
||||
(UPat.var('x', dtypes.ints+(dtypes.weakint, dtypes.index)).cast(dtypes.ints+(dtypes.weakint, dtypes.index), name="a").cast(name="b"),
|
||||
(UPat.var('x', dtypes.ints+(dtypes.weakint,)).cast(dtypes.ints+(dtypes.weakint,), name="a").cast(name="b"),
|
||||
lambda x,a,b: x.cast(b.dtype) if a.dtype.min<=x.vmin and x.vmax<=a.dtype.max else None),
|
||||
# try to do math in int instead of long
|
||||
(UPat(GroupOp.Binary, src=(UPat.var("x", dtypes.long), UPat.var("y", dtypes.long)), name="u"), lambda u,x,y:
|
||||
x.cast(dtypes.int).alu(u.op, y.cast(dtypes.int)).cast(u.dtype) if not any(v.overflows(dtypes.int) for v in (u,x,y)) else None),
|
||||
((UPat.var("x", dtypes.index) + UPat.cvar("c")).cast(dtypes.sints, name="cast"), lambda x,c,cast:x.cast(cast.dtype)+c.cast(cast.dtype)),
|
||||
((UPat.var("x", dtypes.weakint) + UPat.cvar("c")).cast(dtypes.sints, name="cast"), lambda x,c,cast:x.cast(cast.dtype)+c.cast(cast.dtype)),
|
||||
# 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.LOOP, Ops.STORE, Ops.CALL, Ops.FUNCTION, Ops.BARRIER, Ops.END, Ops.LINEAR, Ops.STAGE}
|
||||
else y.src for y in x.src[1:]]))))),
|
||||
# after with 1 src is just src[0]
|
||||
(UPat(Ops.AFTER, src=(UPat.var("s"),)), lambda s: s),
|
||||
@@ -399,7 +399,7 @@ pm_move_where_on_load = PatternMatcher([
|
||||
])
|
||||
|
||||
def gated_given_valid(cond:UOp, x:UOp, i:UOp) -> UOp|None:
|
||||
if x.dtype is not dtypes.index: return None
|
||||
if x.dtype is not dtypes.weakint: return None
|
||||
# Skip if x contains DIV/MOD AND IMAGE mode is enabled -> image index e.g. openpilot
|
||||
if IMAGE.value > 0 and x.op_in_backward_slice_with_self(Ops.CDIV, Ops.CMOD, Ops.FLOORDIV, Ops.FLOORMOD): return None
|
||||
return cond.where(uop_given_valid(cond, x, try_simplex=False), i)
|
||||
@@ -463,5 +463,5 @@ sym = symbolic+pm_simplify_valid+PatternMatcher([
|
||||
# ** combine terms (opinionated) **
|
||||
(-1 * (UPat.var("x") + UPat.var("y")), lambda x,y: (-x)+(-y)), # -(x+y) -> -x + -y
|
||||
# (x+y)*c -> x*c+y*c. only for int, float has inf*0=nan issue
|
||||
((UPat.var("x", dtypes.index) + UPat.var("y")) * UPat.cvar("c"), lambda x,y,c: x*c+y*c),
|
||||
((UPat.var("x", dtypes.weakint) + UPat.var("y")) * UPat.cvar("c"), lambda x,y,c: x*c+y*c),
|
||||
])+pm_clean_up_group_sink
|
||||
|
||||
@@ -31,21 +31,21 @@ z3_renderer = PatternMatcher([
|
||||
(UPat(Ops.PARAM, name="x"), lambda x,ctx: create_bounded(x.arg.name, x.vmin, x.vmax, ctx[0])),
|
||||
(UPat(Ops.RANGE, name="x"), lambda x,ctx: create_bounded(x.render(simplify=False), 0, ctx[1][x.src[0]]-1, ctx[0])),
|
||||
# loads are variables bounded by the min/max of the dtype. non-pointer INDEX is also a LOAD
|
||||
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.index,), name="x"), lambda x,ctx:
|
||||
(UPat((Ops.LOAD, Ops.INDEX), dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx:
|
||||
create_bounded(f"load{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
|
||||
(UPat((Ops.LOAD, Ops.INDEX), dtypes.bool), lambda ctx: (z3.Bool(f"load{len(ctx[1])}", ctx=ctx[0]), None)),
|
||||
# constants
|
||||
(UPat(Ops.CONST, arg=Invalid), lambda ctx: (z3.Int("Invalid", ctx=ctx[0]), None)),
|
||||
(UPat(Ops.CONST, dtypes.ints+(dtypes.index,), name="x"), lambda x,ctx: (z3.IntVal(x.arg, ctx=ctx[0]), None)),
|
||||
(UPat(Ops.CONST, dtypes.ints+(dtypes.weakint,), name="x"), lambda x,ctx: (z3.IntVal(x.arg, ctx=ctx[0]), None)),
|
||||
(UPat(Ops.CONST, dtypes.bool, name="x"), lambda x,ctx: (z3.BoolVal(x.arg, ctx=ctx[0]), None)),
|
||||
# casts from floats create new variables
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,), src=(UPat(dtype=dtypes.floats),), name="x"), lambda x,ctx:
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat(dtype=dtypes.floats),), name="x"), lambda x,ctx:
|
||||
create_bounded(f"cast{len(ctx[1])}", x.dtype.min, x.dtype.max, ctx[0])),
|
||||
# A comparison between floats introduces a new bool variable
|
||||
(UPat(GroupOp.Comparison, src=UPat(dtype=dtypes.floats)), lambda ctx: (z3.Bool(f"float_cmp{len(ctx[1])}", ctx=ctx[0]), None)),
|
||||
# casts from bool/int to int/bool
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,),src=(UPat.var("x", dtypes.bool),)), lambda x,ctx: (z3.If(ctx[1][x], 1, 0), None)),
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.index,), src=(UPat.var("x", dtypes.ints+(dtypes.index,)),)), lambda x,ctx: (ctx[1][x], None)),
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,),src=(UPat.var("x", dtypes.bool),)), lambda x,ctx: (z3.If(ctx[1][x], 1, 0), None)),
|
||||
(UPat(Ops.CAST, dtypes.ints+(dtypes.weakint,), src=(UPat.var("x", dtypes.ints+(dtypes.weakint,)),)), lambda x,ctx: (ctx[1][x], None)),
|
||||
(UPat(Ops.CAST, dtypes.bool, name="x"), lambda x,ctx: (ctx[1][x.src[0]]!=0, None)),
|
||||
(UPat(GroupOp.ALU, name="x"), lambda x,ctx: (z3_alu[x.op](*(ctx[1][s] for s in x.src)), None)),
|
||||
])
|
||||
@@ -53,7 +53,7 @@ z3_renderer = PatternMatcher([
|
||||
def uops_to_z3(solver:z3.Solver, *uops: UOp) -> list[z3.ExprRef]:
|
||||
# gate on upstream AFTER/BUFFER, but keep INDEX as an unknown LOAD
|
||||
lst = list(UOp.sink(*uops).toposort(gate=lambda x: x.op not in {Ops.AFTER, Ops.BUFFER} and \
|
||||
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.index) or x.op is Ops.SINK)))[:-1]
|
||||
(x.dtype in dtypes.ints+(dtypes.bool, dtypes.weakint) or x.op is Ops.SINK)))[:-1]
|
||||
z3map: dict[UOp, z3.ExprRef] = {}
|
||||
for u in lst:
|
||||
# NOTE: we skip STACK here, it can't actually be accessed
|
||||
|
||||
@@ -47,6 +47,7 @@ def decode_profile(data:bytes) -> dict:
|
||||
return {"dur":total_dur, "peak":global_peak, "layout":layout, "markers":markers}
|
||||
|
||||
def to_str(k:str, v) -> str:
|
||||
if isinstance(v, str): return f"{k} {v}"
|
||||
if k == "FLOPS" or k.startswith("B/s"): return f"{v*1e-9:.0f} G{k}" if v < 1e13 else f"{v*1e-12:.0f} T{k}"
|
||||
if k == "B": return next((f"{v/s:.0f} {u}" for s,u in ((1e9,"GB"),(1e6,"MB"),(1e3,"KB")) if v>=s), f"{v:.0f} B")
|
||||
return f"{k}={v}"
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
<div class="main-container">
|
||||
<div class="floating-container">
|
||||
<button class="btn collapse-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20"><path d="M15 19l-7-7 7-7"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="20"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16M16 4v16"/></svg>
|
||||
</button>
|
||||
<button class="btn" id="zoom-to-fit-btn" aria-label="Fit graph">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20">
|
||||
|
||||
@@ -735,6 +735,7 @@ async function renderProfiler(path, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
let lastCanvasRect = null;
|
||||
function resize() {
|
||||
const [width, height] = canvasDims();
|
||||
if (canvas.width === width*dpr && canvas.height === height*dpr) return;
|
||||
@@ -743,6 +744,11 @@ async function renderProfiler(path, opts) {
|
||||
canvas.style.height = `${height}px`;
|
||||
canvas.style.width = `${width}px`;
|
||||
ctx.scale(dpr, dpr);
|
||||
const newRect = rect(canvas);
|
||||
if (lastCanvasRect != null && lastCanvasRect.width > 0) {
|
||||
zoomLevel = d3.zoomIdentity.translate(zoomLevel.x+lastCanvasRect.left-newRect.left, 0).scale(zoomLevel.k*lastCanvasRect.width/width);
|
||||
}
|
||||
lastCanvasRect = { left:newRect.left, width };
|
||||
d3.select(canvas).call(canvasZoom.transform, zoomLevel);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const layoutUOp = (g, { graph, change }, opts) => {
|
||||
if (!opts.showIndexing) {
|
||||
for (const n of g.nodes()) {
|
||||
const node = g.node(n);
|
||||
if (node.label.includes("dtypes.index")) g.removeNode(n);
|
||||
if (node.label.includes("dtypes.weakint")) g.removeNode(n);
|
||||
}
|
||||
}
|
||||
// optionally remove node srcs, track affected nodes
|
||||
|
||||
@@ -46,7 +46,7 @@ from tinygrad.device import ProfileDeviceEvent, ProfileGraphEvent, ProfileGraphE
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
|
||||
uops_colors = {Ops.LOAD: "#ffc0c0", Ops.STORE: "#87CEEB", Ops.CONST: "#e0e0e0", Ops.REDUCE: "#FF5B5B",
|
||||
Ops.RANGE: "#c8a0e0", Ops.BARRIER: "#ff8080", Ops.IF: "#c8b0c0", Ops.SPECIAL: "#c0c0ff",
|
||||
Ops.RANGE: "#c8a0e0", Ops.LOOP: "#dd88cc", Ops.BARRIER: "#ff8080", Ops.IF: "#c8b0c0", Ops.SPECIAL: "#c0c0ff",
|
||||
Ops.INDEX: "#D8F9E4", Ops.STACK: "#D8F9E4",
|
||||
Ops.WMMA: "#efefc0", Ops.MULTI: "#f6ccff", Ops.INS: "#eec4ff",
|
||||
**{x:"#D8F9E4" for x in GroupOp.Movement}, **{x:"#ffffc0" for x in GroupOp.ALU}, Ops.THREEFRY:"#ffff80",
|
||||
@@ -237,8 +237,8 @@ def timeline_layout(data:VizData, dev_events:list[tuple[int, int, float, DevEven
|
||||
if (ref:=data.ref_map.get(name)) is not None and ref < len(data.ctxs):
|
||||
name = data.ctxs[ref]["name"]
|
||||
if (ki:=data.ctxs[ref].get("ki")) is not None and ki.estimates is not None and ei is not None:
|
||||
fmt["FLOPS"] = int(sym_infer(ki.estimates.ops, var_vals:=ei.arg['var_vals'])/(t:=dur*1e-6))
|
||||
fmt["B/s mem"], fmt["B/s lds"] = int(sym_infer(ki.estimates.mem, var_vals)/t), int(sym_infer(ki.estimates.lds, var_vals)/t)
|
||||
for est_key,est_val in (("FLOPS", ki.estimates.ops), ("B/s mem", ki.estimates.mem), ("B/s lds", ki.estimates.lds)):
|
||||
with soft_err(lambda _: fmt.update({est_key:"ERR"})): fmt[est_key] = int(sym_infer(est_val, ei.arg['var_vals'])/(dur*1e-6))
|
||||
key = ei.key
|
||||
elif isinstance(e.name, TracingKey):
|
||||
name = e.name.display_name
|
||||
|
||||
Reference in New Issue
Block a user