Compare commits

...
Author SHA1 Message Date
geohot 006637cf73 remove unmeasured CIFAR micro-optimizations 2026-07-12 22:51:19 +00:00
geohot 1f335c6eef Merge remote-tracking branch 'origin/master' into cifar_30_clean
# Conflicts:
#	tinygrad/uop/ops.py
2026-07-12 22:17:27 +00:00
geohot 406e314d29 simplify CIFAR speedups 2026-07-12 16:50:28 +00:00
George HotzandGitHub bd1dc49112 Merge branch 'master' into cifar_30_clean 2026-07-12 09:20:47 -07:00
geohot d799b4108a 20 sec 2026-07-12 15:11:11 +00:00
geohot bf919c9430 30s speedrun! 2026-07-12 12:03:08 +00:00
geohot 9c0460e4ee Revert "tile large transposed AMD GEMMs across six waves"
This reverts commit c3ab7c92ae46f36ad474659134561473f290cda4.
2026-07-12 12:03:08 +00:00
geohot 1da9d8386d tile large transposed AMD GEMMs across six waves 2026-07-12 12:03:08 +00:00
geohot 6eb762f5b3 speed up AMD channel reductions and irregular GEMM 2026-07-12 12:03:08 +00:00
geohot 4a4fccb317 fuse shared reductions and large AMD graphs 2026-07-12 12:03:08 +00:00
geohot 50a98c0f20 deduplicate optimized kernel lowering 2026-07-12 12:03:08 +00:00
geohot 40b4377a14 tune long AMD GEMMs and reject irrelevant store hazards 2026-07-12 12:03:08 +00:00
geohot 2e5354af22 deduplicate codegen and tile irregular AMD GEMMs 2026-07-12 12:03:08 +00:00
geohot 24267914f4 compact AMD GEMM sources and tune long reductions 2026-07-12 12:03:08 +00:00
geohot d9610230df reduce registers in long AMD GEMM 2026-07-12 12:03:08 +00:00
geohot 2908c33a13 compile cooperative AMD kernels at O2 2026-07-12 12:03:08 +00:00
geohot a030286672 speed up AMD GEMM lowering and codegen 2026-07-12 12:03:08 +00:00
geohot 8fedf152b1 10.6 seconds 2026-07-12 12:03:08 +00:00
geohot f38cba1c08 16 seconds 2026-07-12 12:02:48 +00:00
geohot fab262fb98 fix codegen correctness and lifetime regressions 2026-07-12 12:02:32 +00:00
geohot 09b9e5e57d speed up CIFAR-10 example 2026-07-12 12:02:13 +00:00
geohot a69186bdf4 speed up code generation and sorting 2026-07-12 12:02:13 +00:00
26 changed files with 2343 additions and 269 deletions
+93 -43
View File
@@ -3,7 +3,7 @@
# tinygrad implementation of https://github.com/tysam-code/hlb-CIFAR10/blob/main/main.py
# https://myrtle.ai/learn/how-to-train-your-resnet-8-bag-of-tricks/
# https://siboehm.com/articles/22/CUDA-MMM
import random, time
import random, time, math
import numpy as np
from typing import Optional
from extra.lr_scheduler import OneCycleLR
@@ -49,14 +49,13 @@ class UnsyncedBatchNorm:
# https://github.com/pytorch/pytorch/blob/c618dc13d2aa23625cb0d7ada694137532a4fa33/aten/src/ATen/native/cuda/Normalization.cuh
# There's "online" algorithms that fix this, like https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_Online_algorithm
batch_mean = x.mean(axis=(1,3,4))
y = (x - batch_mean.detach().reshape(shape=[batch_mean.shape[0], 1, -1, 1, 1])) # d(var)/d(mean) = 0
batch_var = (y*y).mean(axis=(1,3,4))
batch_var = (x*x).mean(axis=(1,3,4)) - batch_mean*batch_mean
batch_invstd = batch_var.add(self.eps).pow(-0.5)
# NOTE: wow, this is done all throughout training in most PyTorch models
if self.track_running_stats:
self.running_mean.assign((1-self.momentum) * self.running_mean + self.momentum * batch_mean.detach().cast(self.running_mean.dtype))
batch_var_adjust = prod(y.shape[1:])/(prod(y.shape[1:])-y.shape[2])
batch_var_adjust = prod(x.shape[1:])/(prod(x.shape[1:])-x.shape[2])
self.running_var.assign((1-self.momentum) * self.running_var + self.momentum * batch_var_adjust * batch_var.detach().cast(self.running_var.dtype))
self.num_batches_tracked += 1
else:
@@ -70,25 +69,37 @@ class BatchNorm(nn.BatchNorm2d if getenv("SYNCBN") else UnsyncedBatchNorm):
super().__init__(num_features, track_running_stats=False, eps=1e-12, momentum=0.85, affine=True)
self.weight.is_param_(False)
class MatmulConv2d(nn.Conv2d):
def __call__(self, x:Tensor) -> Tensor:
if not getenv("MATMUL_CONV", 1): return super().__call__(x)
assert self.groups == 1 and self.stride == self.dilation == self.padding == 1 and self.bias is None
bs, cin, _, _ = x.shape
cout, _, ky, kx = self.weight.shape
patches = x.pad((1, 1, 1, 1))._pool((ky, kx), 1, 1)
oy, ox = patches.shape[2:4]
patches = patches.permute(0, 2, 3, 1, 4, 5).reshape(bs*oy*ox, cin*ky*kx).contiguous().contiguous_backward()
out = (patches @ self.weight.reshape(cout, cin*ky*kx).T).contiguous().contiguous_backward()
return out.reshape(bs, oy, ox, cout).permute(0, 3, 1, 2)
class ConvGroup:
def __init__(self, channels_in, channels_out):
self.conv1 = nn.Conv2d(channels_in, channels_out, kernel_size=3, padding=1, bias=False)
self.conv2 = nn.Conv2d(channels_out, channels_out, kernel_size=3, padding=1, bias=False)
self.conv1 = MatmulConv2d(channels_in, channels_out, kernel_size=3, padding=1, bias=False)
self.conv2 = MatmulConv2d(channels_out, channels_out, kernel_size=3, padding=1, bias=False)
self.norm1 = BatchNorm(channels_out)
self.norm2 = BatchNorm(channels_out)
def __call__(self, x):
x = self.conv1(x)
x = self.conv1(x).contiguous()
x = x.max_pool2d(2)
x = x.float()
x = self.norm1(x)
x = self.norm1(x).contiguous()
x = x.cast(dtypes.default_float)
x = x.quick_gelu()
x = x.quick_gelu().contiguous()
residual = x
x = self.conv2(x)
x = self.conv2(x).contiguous()
x = x.float()
x = self.norm2(x)
x = self.norm2(x).contiguous()
x = x.cast(dtypes.default_float)
x = x.quick_gelu()
@@ -111,7 +122,10 @@ class SpeedyResNet:
def __call__(self, x, training=True):
# pad to 32x32 because whitening conv creates 31x31 images that are awfully slow to compute with
# TODO: remove the pad but instead let the kernel optimize itself
forward = lambda x: x.conv2d(self.whitening).pad((1,0,0,1)).sequential(self.net)
def forward(x):
x = x.conv2d(self.whitening).pad((1,0,0,1)).contiguous()
for layer in self.net: x = layer(x).contiguous()
return x
return forward(x) if training else (forward(x) + forward(x[..., ::-1])) / 2.
# hyper-parameters were exactly the same as the original repo
@@ -216,15 +230,31 @@ def train_cifar():
Y_cutmix = mix_portion * Y_patch + (1. - mix_portion) * Y
return X_cutmix, Y_cutmix
@TinyJit
def augmentations(X:Tensor, Y:Tensor):
perms = Tensor.randperm(X.shape[0], device=X.device) # We reuse perms for cutmix, because they are expensive to generate
def random_permutation(rows:int, cols:int) -> Tensor:
size = rows * cols
# An affine map is a permutation when its stride is coprime to the domain size.
while math.gcd(stride:=random.randrange(1, size), size) != 1: pass
return (Tensor.arange(size) * stride + random.randrange(size)) % size
def shuffled_augmentations(X:Tensor, Y:Tensor):
perms = random_permutation(X.shape[0] // BS, BS)
X, Y = X[:perms.shape[0]], Y[:perms.shape[0]]
if getenv("RANDOM_CROP", 1):
X = random_crop(X, crop_size=32)
if getenv("RANDOM_FLIP", 1):
# NOTE: RANGEIFY=1 needs this contiguous or the X[perms] is very slow
X = (Tensor.rand(X.shape[0],1,1,1) < 0.5).where(X.flip(-1), X).contiguous() # flip LR
X, Y = X[perms], Y[perms]
return X, Y, perms
@TinyJit
def augmentations(X:Tensor, Y:Tensor):
X, Y, _ = shuffled_augmentations(X, Y)
return X, Y
@TinyJit
def augmentations_cutmix(X:Tensor, Y:Tensor):
X, Y, perms = shuffled_augmentations(X, Y)
return X, Y, *cutmix(X, Y, perms, mask_size=hyp['net']['cutmix_size'])
# the operations that remain inside batch fetcher is the ones that involves random operations
@@ -234,8 +264,10 @@ def train_cifar():
st = time.monotonic()
X, Y = X_in, Y_in
if is_train:
X, Y, X_cm, Y_cm = augmentations(X, Y)
if getenv("CUTMIX", 1) and step >= hyp['net']['cutmix_steps']: X, Y = X_cm, Y_cm
if getenv("CUTMIX", 1) and step >= hyp['net']['cutmix_steps']:
_, _, X, Y = augmentations_cutmix(X, Y)
else:
X, Y = augmentations(X, Y)
et = time.monotonic()
print(f"shuffling {'training' if is_train else 'test'} dataset in {(et-st)*1e3:.2f} ms ({epoch=})")
@@ -255,7 +287,6 @@ def train_cifar():
class modelEMA():
def __init__(self, w, net):
# self.model_ema = copy.deepcopy(net) # won't work for opencl due to unpickeable pyopencl._cl.Buffer
self.net_ema = SpeedyResNet(w)
for net_ema_param, net_param in zip(get_state_dict(self.net_ema).values(), get_state_dict(net).values()):
net_ema_param.assign(net_param.numpy())
@@ -280,6 +311,15 @@ def train_cifar():
# initialize model weights
model = SpeedyResNet(W)
model_state = get_state_dict(model)
random_params = [x for name, x in model_state.items() if x.is_param and "bias" not in name]
Tensor.manual_seed(getenv('SEED', hyp['seed']))
random_values = Tensor.rand(sum(x.numel() for x in random_params))
offset = 0
for param in random_params:
bound = prod(param.shape[1:]) ** -0.5
param.replace(((random_values[offset:offset+param.numel()] * (2 * bound)) - bound).reshape(param.shape).cast(param.dtype))
offset += param.numel()
# padding is not timed in the original repo since it can be done all at once
X_train = pad_reflect(X_train, size=hyp['net']['pad_amount'])
@@ -296,7 +336,7 @@ def train_cifar():
x.to_(GPUS)
# parse the training params into bias and non-bias
params_dict = get_state_dict(model)
params_dict = model_state
params_bias = []
params_non_bias = []
for params in params_dict:
@@ -320,7 +360,7 @@ def train_cifar():
lr_sched_non_bias = OneCycleLR(opt_non_bias, max_lr=hyp['opt']['non_bias_lr'], pct_start=pct_start, div_factor=initial_div_factor, final_div_factor=1./(initial_div_factor*final_lr_ratio), total_steps=STEPS)
def train_step(model, optimizer, lr_scheduler, X, Y):
out = model(X)
out = model(X).contiguous()
loss_batchsize_scaler = 512/BS
loss = cross_entropy(out, Y, reduction='none', label_smoothing=hyp['opt']['label_smoothing']).mul(hyp['opt']['loss_scale_scaler']*loss_batchsize_scaler).sum().div(hyp['opt']['loss_scale_scaler'])
@@ -331,15 +371,19 @@ def train_cifar():
return loss.realize(*optimizer.schedule_step(), *lr_scheduler[0].schedule_step(), *lr_scheduler[1].schedule_step())
return loss.realize()
train_step_jitted = TinyJit(train_step)
train_step_jitted = TinyJit(train_step, warmup=False)
def eval_step(model, X, Y):
out = model(X, training=False)
loss = cross_entropy(out, Y, reduction='mean')
def eval_forward(model, X):
return model(X).realize()
def eval_step(out, out_flipped, Y):
out = (out + out_flipped) / 2.
correct = out.argmax(axis=1) == Y.argmax(axis=1)
return correct.realize(), loss.realize()
eval_step_jitted = TinyJit(eval_step)
eval_step_ema_jitted = TinyJit(eval_step)
return correct.sum().realize()
eval_forward_jitted = TinyJit(eval_forward, warmup=False)
eval_forward_ema_jitted = TinyJit(eval_forward, warmup=False)
eval_step_jitted = TinyJit(eval_step, warmup=False)
eval_step_ema_jitted = TinyJit(eval_step, warmup=False)
# 97 steps in 2 seconds = 20ms / step
# step is 1163.42 GOPS = 56 TFLOPS!!!, 41% of max 136
@@ -360,31 +404,37 @@ def train_cifar():
while i <= STEPS:
if i % getenv("EVAL_STEPS", STEPS) == 0 and i > 1 and not getenv("DISABLE_BACKWARD"):
# Using Context(TRAINING=0) here actually bricks batchnorm, even with track_running_stats=True
corrects = []
corrects_ema = []
losses = []
losses_ema = []
correct_sum = correct_sum_ema = None
correct_len = correct_len_ema = 0
for Xt, Yt in fetch_batches(X_test, Y_test, BS=EVAL_BS, is_train=False):
if len(GPUS) > 1:
Xt.shard_(GPUS, axis=0)
Yt.shard_(GPUS, axis=0)
correct, loss = eval_step_jitted(model, Xt, Yt)
losses.append(loss.numpy().tolist())
corrects.extend(correct.numpy().tolist())
Xt_contiguous = Xt.contiguous().realize()
out = eval_forward_jitted(model, Xt_contiguous).clone().realize()
out_flipped = eval_forward_jitted(model, Xt_contiguous[..., ::-1].contiguous().realize())
batch_correct = eval_step_jitted(out, out_flipped, Yt)
correct_sum = batch_correct if correct_sum is None else correct_sum + batch_correct
correct_len += Yt.shape[0]
if model_ema:
correct_ema, loss_ema = eval_step_ema_jitted(model_ema.net_ema, Xt, Yt)
losses_ema.append(loss_ema.numpy().tolist())
corrects_ema.extend(correct_ema.numpy().tolist())
out_ema = eval_forward_ema_jitted(model_ema.net_ema, Xt_contiguous).clone().realize()
out_flipped_ema = eval_forward_ema_jitted(model_ema.net_ema, Xt_contiguous[..., ::-1].contiguous().realize())
batch_correct_ema = eval_step_ema_jitted(out_ema, out_flipped_ema, Yt)
correct_sum_ema = batch_correct_ema if correct_sum_ema is None else correct_sum_ema + batch_correct_ema
correct_len_ema += Yt.shape[0]
# collect accuracy across ranks
correct_sum, correct_len = sum(corrects), len(corrects)
if model_ema: correct_sum_ema, correct_len_ema = sum(corrects_ema), len(corrects_ema)
assert correct_sum is not None
correct_count = correct_sum.item()
if model_ema:
assert correct_sum_ema is not None
correct_count_ema = correct_sum_ema.item()
eval_acc_pct = correct_sum/correct_len*100.0
if model_ema: acc_ema = correct_sum_ema/correct_len_ema*100.0
print(f"eval {correct_sum}/{correct_len} {eval_acc_pct:.2f}%, {(sum(losses)/len(losses)):7.2f} val_loss STEP={i} (in {(time.monotonic()-st)*1e3:.2f} ms)")
if model_ema: print(f"eval ema {correct_sum_ema}/{correct_len_ema} {acc_ema:.2f}%, {(sum(losses_ema)/len(losses_ema)):7.2f} val_loss STEP={i}")
eval_acc_pct = correct_count/correct_len*100.0
if model_ema: acc_ema = correct_count_ema/correct_len_ema*100.0
print(f"eval {correct_count}/{correct_len} {eval_acc_pct:.2f}% STEP={i} (in {(time.monotonic()-st)*1e3:.2f} ms)")
if model_ema: print(f"eval ema {correct_count_ema}/{correct_len_ema} {acc_ema:.2f}% STEP={i}")
if STEPS == 0 or i == STEPS: break
+13
View File
@@ -348,6 +348,19 @@ class TestStopEarly(unittest.TestCase):
ret = (c+d).substitute({c:cn}, extra_pm=pm_cvisit)
assert ret == cn+d
class TestFastSubstitute(unittest.TestCase):
def test_replacement_tree_is_substituted(self):
a, b, c, d = [UOp.variable(x, 0, 10) for x in "abcd"]
self.assertIs((a+4).substitute({a:b+c, b:d}), (d+c)+4)
def test_rebuilt_node_is_substituted(self):
a, b, c, d = [UOp.variable(x, 0, 10) for x in "abcd"]
self.assertIs(((a+b)*2).substitute({a:c, c+b:d}), d*2)
def test_mapping_cycle(self):
a, b = [UOp.variable(x, 0, 10) for x in "ab"]
with self.assertRaises(RuntimeError): (a+1).substitute({a:b, b:a})
class TestWalkRewrite(unittest.TestCase):
"""Tests for graph_rewrite with walk=True (MLIR Walk Pattern Rewrite Driver semantics).
walk=True gives a single-pass traversal that does NOT revisit or re-traverse into rewritten subtrees.
+105 -36
View File
@@ -1,18 +1,19 @@
from dataclasses import replace, dataclass
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 DISABLE_FAST_IDIV, TRANSCENDENTAL, SPEC, DEBUG, VIZ, PROFILE, IMAGE, NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC
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 TRACK_MATCH_STATS
from tinygrad.uop.ops import AxisType
from tinygrad.uop.render import pyrender
from tinygrad.uop.spec import type_verify, spec_tensor, spec_program
from tinygrad.renderer import Renderer, Estimates
from tinygrad.renderer.isa import ISARenderer, IselContext, PreRegAllocContext
from tinygrad.dtype import dtypes, AddrSpace
from tinygrad.dtype import dtypes, AddrSpace, Invalid
# import all pattern matchers here
from tinygrad.codegen.gpudims import pm_add_gpudims
from tinygrad.uop.symbolic import sym, symbolic_simple, symbolic, pm_move_where_on_load, pm_clean_up_group_sink, pm_remove_invalid
from tinygrad.uop.symbolic import sym, symbolic_simple, symbolic, pm_simplify_valid, pm_move_where_on_load, pm_clean_up_group_sink, pm_remove_invalid
from tinygrad.uop.movement import mop_cleanup
from tinygrad.codegen.decomp.dtype import pm_dtype_decomps
from tinygrad.codegen.decomp.op import get_late_rewrite_patterns, get_simplifying_rewrite_patterns
@@ -22,6 +23,7 @@ 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.schedule.indexing import apply_movement_op
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
@@ -29,6 +31,8 @@ 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
empty_matcher = PatternMatcher([])
def do_number_param(ctx:list[int], x:UOp):
if x.arg.slot != -1: return None
ctx[0] += 1
@@ -113,6 +117,10 @@ def broadcast_and_devec_wmma(b:UOp):
src.append(b.replace(src=tuple([x.index(*idx_c) for x in src_reshaped])))
return UOp.stack(*src).reshape(b.shape)
@functools.cache
def shape_indexes(shape:tuple[int, ...]) -> tuple[tuple[UOp, ...], ...]:
return tuple(tuple(UOp.const(dtypes.index, i) for i in idx) for idx in itertools.product(*map(range, shape)))
pm_wmma_add = PatternMatcher([
(UPat(Ops.WMMA, name="wmma") + UPat.var("add"),
lambda add, wmma: UOp(wmma.op, src=(wmma.src[0], wmma.src[1], wmma.src[2]+add), arg=wmma.arg)),
@@ -128,15 +136,48 @@ unbroadcast = pm_wmma_add+PatternMatcher([
(UPat(Ops.WMMA, name="b"), broadcast_and_devec_wmma),
])
def do_devectorize(b:UOp):
if b.shape == (): return None
def do_devectorize(ctx, b:UOp):
ren = ctx.ren if isinstance(ctx, DevectorizeContext) else (ctx[-1] if isinstance(ctx, tuple) else ctx)
if b.op in GroupOp.Elementwise and b.dtype in dtypes.floats and ren.supports_float4: return None
if (shape:=b._shape) is None or shape == (): return None
# broadcasting needs to be already unpacked
if not all_same([x.shape for x in b.src]): return None
if any(x._shape != 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]
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)
for idx_c in shape_indexes(shape):
new_src = tuple(index_lane(ctx, x, idx_c) if isinstance(ctx, DevectorizeContext) else
(x.src[idx_c[0].arg] if len(idx_c) == 1 and x.op is Ops.STACK else UOp(Ops.INDEX, x.dtype, (x,)+idx_c)) for x in b.src)
src.append(UOp(b.op, b.dtype, new_src, b.arg, b.tag))
return UOp(Ops.STACK, b.dtype, tuple(src)).reshape(shape) if b.op is not Ops.STORE else UOp.group(*src)
@dataclass
class DevectorizeContext:
ren: Renderer
lanes: dict[tuple[UOp, tuple[UOp, ...]], UOp]
@property
def rewrite_cache_key(self): return (type(self.ren), self.ren.target)
def index_lane(ctx:DevectorizeContext, x:UOp, idxs:tuple[UOp, ...]) -> UOp:
key = (x, idxs)
if (ret:=ctx.lanes.get(key)) is not None: return ret
if x.op is Ops.STACK and idxs and idxs[0].op is Ops.CONST:
ret = index_lane(ctx, x.src[idxs[0].arg], idxs[1:]) if len(idxs) > 1 else x.src[idxs[0].arg]
elif x.op in GroupOp.Movement and len(idxs) == len(x.shape):
ret = index_lane(ctx, x.src[0], apply_movement_op(x.op, x.src[0].shape, x.marg, idxs))
elif x.op is Ops.INDEX:
ret = index_lane(ctx, x.src[0], x.src[1:]+idxs)
elif x.op in GroupOp.Elementwise:
ret = UOp(x.op, x.dtype, tuple(index_lane(ctx, s, idxs) if s._shape else s for s in x.src), x.arg, x.tag)
else: ret = UOp(Ops.INDEX, x.dtype, (x,)+idxs)
ctx.lanes[key] = ret
return ret
def index_elementwise(x:UOp, idx:UOp):
indexes = idx.src[1:]
return UOp(x.op, x.dtype, tuple(UOp(Ops.INDEX, s.dtype, (s,)+indexes) if s._shape else s for s in x.src), x.arg, x.tag)
def index_elementwise_lane(ctx:DevectorizeContext, x:UOp, idx:UOp):
indexes = idx.src[1:]
return UOp(x.op, x.dtype, tuple(index_lane(ctx, s, indexes) if s._shape else s for s in x.src), x.arg, x.tag)
def do_stack_wmma(u:UOp):
if all(x.op in (Ops.STACK, Ops.WMMA) for x in u.src): return None
@@ -152,11 +193,13 @@ def do_stack_wmma(u:UOp):
ew_devectorizer = PatternMatcher([
# unpack broadcasting
(UPat(GroupOp.Elementwise, name="b"), do_devectorize),
(UPat(GroupOp.Elementwise, name="x").f(Ops.INDEX, allow_any_len=True, name="idx"), index_elementwise),
])
devectorizer2 = mop_cleanup+pm_mops+PatternMatcher([
# unpack broadcasting
(UPat(GroupOp.Elementwise|{Ops.LOAD,Ops.STORE}, name="b"), do_devectorize),
(UPat(GroupOp.Elementwise, name="x").f(Ops.INDEX, allow_any_len=True, name="idx"), index_elementwise_lane),
# INDEX without src is nothing (TODO: this should be in mop_cleanup)
(UPat(Ops.INDEX, src=(UPat.var('x'),)), lambda x: x),
# unpack WMMA
@@ -298,53 +341,54 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
sink = graph_rewrite(sink, pm_add_local_buffers, ctx=itertools.count(0), name="add local buffers")
# add gpu dims (late). this works after devectorize, but it's faster here
sink = graph_rewrite(sink, pm_add_gpudims, ctx=ren, name="add gpudims")
if VIZ or PROFILE or TRACK_MATCH_STATS: sink = graph_rewrite(sink, pm_add_gpudims, ctx=ren, name="add gpudims")
elif (gpu_sink:=pm_add_gpudims.rewrite(sink, ren)) is not None: sink = gpu_sink
# **** optimizations are done, now we lower to actual code ****
sink = graph_rewrite(sink, symbolic_simple+unbroadcast+pm_add_loads, name="*** unbroadcast / add loads")
# devectorize
sink = graph_rewrite(sink, symbolic_simple+devectorizer2, ctx=ren, name="devectorize2")
# simplify indexing
sink = graph_rewrite(sink, indexing_simplify, name="simplify load/store indexing")
sink = graph_rewrite(sink, symbolic_simple+devectorizer2, ctx=DevectorizeContext(ren, {}), name="devectorize2")
# some coalesing misses without this
sink = graph_rewrite(sink, sym, name="early symbolic")
# do memory coalesing (late)
sink = memory_coalesing(sink, ren)
sink = graph_rewrite(sink, symbolic_simple+ew_devectorizer+pm_simplify_add_image, name="add images", ctx=({}, ren), bottom_up=True)
if IMAGE: 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?
sink = graph_rewrite(sink, sym, name="extra symbolic")
has_invalid = (sink.op is Ops.CONST and sink.arg is Invalid) or any(u.op is Ops.CONST and u.arg is Invalid for u in sink.backward_slice)
if has_invalid:
sink = graph_rewrite(sink, pm_simplify_valid, name="simplify valid after coalescing")
has_invalid = (sink.op is Ops.CONST and sink.arg is Invalid) or any(u.op is Ops.CONST and u.arg is Invalid for u in sink.backward_slice)
# lower index dtype
# NOTE: we need indexing_simplify to remove the cast to long using the Invalid
sink = graph_rewrite(sink, pm_lower_index_dtype+indexing_simplify, name="lower all index dtypes")
# final symbolic before decomp
sink = graph_rewrite(sink, symbolic, name="final symbolic")
# **** decomps ****
# floordiv+mod / dtype decomp (early)
# final symbolic + floordiv/mod + dtype decomp
supported_ops = tuple(ren.code_for_op.keys())
pm_decomp = symbolic_simple+get_simplifying_rewrite_patterns(supported_ops)
sink = graph_rewrite(sink, pm_decomp, name="early decompositions")
pm_decomp = symbolic+get_simplifying_rewrite_patterns(supported_ops)
# late decomps + move gates from unrenderable INVALID where
sink = graph_rewrite(sink, pm_dtype_decomps, ctx=(set(), ren), name="decomp dtypes")
candidate_dtypes = {*dtypes.fp8s, dtypes.bfloat16, dtypes.half, dtypes.long, dtypes.ulong}
emulated_dtypes = set(EMULATED_DTYPES.tolist(dtypes)) | (candidate_dtypes - ren.supported_dtypes())
needs_dtype_decomp = sink.dtype in emulated_dtypes or any(u.dtype in emulated_dtypes for u in sink.backward_slice)
if needs_dtype_decomp:
sink = graph_rewrite(sink, pm_decomp, name="early decompositions")
sink = graph_rewrite(sink, pm_dtype_decomps, ctx=(set(), ren), name="decomp dtypes")
pm_decomp = pm_decomp+\
get_late_rewrite_patterns(supported_ops, bool(DISABLE_FAST_IDIV))+\
get_transcendental_patterns(supported_ops, TRANSCENDENTAL>=2)
sink = graph_rewrite(sink, pm_decomp, ctx=ren, name="late decompositions")
sink = graph_rewrite(sink, pm_move_gates_from_index, name="move gates from index")
if has_invalid: sink = graph_rewrite(sink, pm_move_gates_from_index, name="move gates from index")
# final rules for the renderer (without sym)
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else PatternMatcher([])
pm_final_rewrite = pm_decomp+extra_matcher+pm_split_ends+pm_no_index
extra_matcher = ren.extra_matcher if ren.extra_matcher is not None else empty_matcher
pm_final_rewrite = symbolic_simple+extra_matcher+pm_split_ends+pm_no_index
sink = graph_rewrite(sink, pm_final_rewrite+pm_remove_invalid, ctx=ren, name="final rewrite")
# this was the linearizer
@@ -425,7 +469,7 @@ pm_to_program = PatternMatcher([
@track_rewrites(name=lambda ast,renderer,ret,**kwargs: TracingKey(ret.src[0].arg.name,(ret.src[0].arg.function_name, ast), ret=renderer), replay=True)
@Context(ALLOW_DEVICE_USAGE=0)
def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
def do_to_program(ast:UOp, renderer:Renderer, compile_binary=True) -> UOp:
"""
Transform an AST into a compiled PROGRAM. May trigger BEAM search.
@@ -436,25 +480,50 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
Returns:
The Ops.PROGRAM with SINK/LINEAR/SOURCE/BINARY.
"""
from tinygrad.codegen.opt.gemm import cooperative_gemm_program, direct_conv_bwd_activation_program
from tinygrad.codegen.opt.reduce import activation_var_grad_program, bn_grad_512_program, channel_reduce_program, col2im_program
from tinygrad.codegen.opt.reduce import im2col_program, moments_512_program
from tinygrad.codegen.opt.reduce import maxpool_backward_program, maxpool_program
if ast.op is Ops.SINK and (prg:=direct_conv_bwd_activation_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=cooperative_gemm_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=activation_var_grad_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=moments_512_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=bn_grad_512_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=channel_reduce_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=col2im_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=im2col_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=maxpool_backward_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.SINK and (prg:=maxpool_program(ast, renderer, compile_binary)) is not None: return prg
if ast.op is Ops.PROGRAM: prg = ast
elif ast.op is Ops.SINK:
assert isinstance(ast.arg, KernelInfo), "requires KernelInfo on arg to to_program"
full_sink = full_rewrite_to_sink(ast, renderer, optimize=ast.tag is None)
prog_info = ProgramInfo.from_sink(full_sink)
# instruction selection
if isinstance(renderer, ISARenderer):
full_sink = graph_rewrite(full_sink, renderer.pre_isel_matcher, ctx=itertools.count(-1, -1), name="pre instruction selection", bottom_up=True)
full_sink = graph_rewrite(full_sink, renderer.isel_matcher, ctx=IselContext(full_sink), name="instruction selection", bottom_up=True)
prg = UOp(Ops.PROGRAM, src=(full_sink,), arg=prog_info)
prg = UOp(Ops.PROGRAM, src=(full_sink,))
else: raise RuntimeError(f"can't call to_program on {ast.op}")
if VIZ:
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0]))
prg = graph_rewrite(prg, pm_to_program, ctx=renderer, name="linearize/render")
graph_rewrite(prg, PatternMatcher([]), name="View Program")
return prg
# PROGRAM lowering is a linear root-only pipeline. Driving it through graph_rewrite
# needlessly walks the full SINK and LINEAR graphs between each stage.
if len(prg.src) == 1: prg = do_linearize(renderer, prg, prg.src[0])
if not isinstance(prg.arg, ProgramInfo): prg = prg.replace(arg=ProgramInfo.from_sink(prg.src[0]))
prg = graph_rewrite(prg, pm_to_program, ctx=renderer, name="linearize/render")
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
if prg.src[0].arg.estimates is None and (estimated:=do_estimates(prg, prg.src[0], prg.src[1])) is not None: prg = estimated
if len(prg.src) == 2:
prg = do_assemble(renderer, prg, prg.src[1]) if isinstance(renderer, ISARenderer) else do_render(renderer, prg, prg.src[1])
if compile_binary and len(prg.src) == 3 and (compiled:=do_compile(renderer, prg, prg.src[2])) is not None: prg = compiled
return prg
to_program_cache: dict[tuple, UOp] = {}
def to_program(ast:UOp, renderer:Renderer) -> UOp:
def to_program(ast:UOp, renderer:Renderer, compile_binary=True) -> 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: to_program_cache[key] = prg = do_to_program(ast, renderer)
# UOps are structurally interned, so identity is already a collision-free structural
# cache key within this process and avoids recursively hashing every kernel graph.
key = (ast, type(renderer), renderer.target, compile_binary, *[x.value for x in config])
if (prg:=to_program_cache.get(key)) is None: to_program_cache[key] = prg = do_to_program(ast, renderer, compile_binary=compile_binary)
return prg
+8 -5
View File
@@ -43,19 +43,22 @@ def fast_idiv(ren: Renderer, x: UOp, d: int, dont_cast=False) -> UOp|None:
# ***** threefry *****
def threefry2x32(x: UOp, key: UOp):
def threefry2x32(x: UOp, key: UOp, ctx:Renderer|None=None):
native_shifts = ctx is not None and Ops.SHL in ctx.code_for_op and Ops.SHR in ctx.code_for_op
shl = (lambda v, s: v << s) if native_shifts else (lambda v, s: v * 2**s)
shr = (lambda v, s: v >> s) if native_shifts else (lambda v, s: v // 2**s)
# split x and key from uint64 to two uint32
x0, x1 = (x & 0xffffffff).cast(dtypes.uint32), ((x // 2**32) & 0xffffffff).cast(dtypes.uint32)
key0, key1 = (key & 0xffffffff).cast(dtypes.uint32), ((key // 2**32) & 0xffffffff).cast(dtypes.uint32)
x0, x1 = x.cast(dtypes.uint32), shr(x, 32).cast(dtypes.uint32)
key0, key1 = key.cast(dtypes.uint32), shr(key, 32).cast(dtypes.uint32)
rotations = [[13, 15, 26, 6], [17, 29, 16, 24]]
ks = [key1, key0 ^ key1 ^ 0x1BD11BDA, key0]
xr:list[UOp] = [x0 + ks[-1], x1 + ks[0]]
for i in range(5):
for r in rotations[i % 2]: xr[0], xr[1] = (x0 := xr[0] + xr[1]), x0 ^ ((xr[1] * 2**r) + (xr[1] // 2**(32 - r)))
for r in rotations[i % 2]: xr[0], xr[1] = (x0 := xr[0] + xr[1]), x0 ^ (shl(xr[1], r) + shr(xr[1], 32-r))
xr = [(xr[0] + ks[i % 3]), (xr[1] + ks[(i + 1) % 3] + i + 1)]
return xr[1].cast(dtypes.uint64) * 2**32 | xr[0].cast(dtypes.uint64)
return shl(xr[1].cast(dtypes.uint64), 32) | xr[0].cast(dtypes.uint64)
# ***** decomposition patterns *****
+2 -2
View File
@@ -42,7 +42,7 @@ def simplify_valid_image_load(buf:UOp, idx_y:UOp, idx_x:UOp, valid:UOp) -> UOp|N
if not is_image_shape(buf._shape): return None
if idx_x.dtype != idx_y.dtype: idx_x, idx_y = idx_x.cast(dtypes.int), idx_y.cast(dtypes.int)
start_idx = idx_x.stack(idx_y)
idx = uop_given_valid(valid, start_idx)
idx = uop_given_valid(valid, start_idx, try_simplex=True)
drop_stmt = _drop_valid_stmts(valid, idx, buf._shape[0], buf._shape[1])
if not drop_stmt and idx is start_idx: return None
@@ -74,7 +74,7 @@ def transform_to_image(ctx, buf:UOp, x:UOp) -> UOp|None:
# search for dims that drop the most valid statements
best_drop, cands = -1, []
for ch, cw in [shapes[buf.arg.slot]] if buf.arg.slot in shapes else image_valid_dims(buf.dtype, buf.max_numel(), ren.target.arch):
cidx = uop_given_valid(valid, ((x//4)%cw).stack(x//(4*cw)))
cidx = uop_given_valid(valid, ((x//4)%cw).stack(x//(4*cw)), try_simplex=True)
dropped = len(_drop_valid_stmts(valid, cidx, ch, cw))
if dropped > best_drop: best_drop, cands = dropped, [(ch, cw, cidx)]
elif dropped == best_drop: cands.append((ch, cw, cidx))
+461
View File
@@ -0,0 +1,461 @@
from dataclasses import dataclass, replace
from math import prod
from tinygrad.dtype import dtypes
from tinygrad.renderer import Estimates, Renderer
from tinygrad.uop.ops import AxisType, Ops, ProgramInfo, UOp, ssimplify
WMMA_M = WMMA_N = WMMA_K = 16
BLOCK_M = BLOCK_N = 128
BLOCK_K = 32
THREADS = 128
@dataclass(frozen=True)
class GemmMatch:
c:UOp
a:UOp
b:UOp
m:int
n:int
k:int
old:UOp|None = None
scale:float = 0.0
a_kxm:bool = False
b_kxn:bool = False
@dataclass(frozen=True)
class BatchedGemmMatch:
c:UOp
a:UOp
b:UOp
m:int
n:int
k:int
batch:int
@dataclass(frozen=True)
class DirectConvBwdActivationMatch:
m:int
cin:int
cout:int
spatial:int
residual:bool = False
@dataclass(frozen=True)
class GemmOutputNCHW:
spatial:int
def _match_gemm(ast:UOp, device:str, arch:str) -> GemmMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) < 3 or end.src[0].op is not Ops.STORE: return None
store, ranges = end.src[0], end.src[1:]
if any(x.op is not Ops.RANGE or x.arg[1] is not AxisType.LOOP for x in ranges) or store.src[0].op is not Ops.INDEX: return None
value = store.src[1]
old, old_index, scale = None, None, 0.0
if value.op is Ops.CAST and value.dtype == dtypes.half and value.src[0].op is Ops.REDUCE: reduce = value.src[0]
elif value.op is Ops.CAST and value.dtype == dtypes.float and value.src[0].op is Ops.ADD:
add_lhs, add_rhs = value.src[0].src
if add_lhs.op is not Ops.CAST or add_lhs.dtype != dtypes.half or add_lhs.src[0].op is not Ops.REDUCE:
add_lhs, add_rhs = add_rhs, add_lhs
if add_lhs.op is not Ops.CAST or add_lhs.dtype != dtypes.half or add_lhs.src[0].op is not Ops.REDUCE: return None
if add_rhs.op is not Ops.MUL: return None
old_idx = next((x for x in add_rhs.src if x.op is Ops.INDEX and x.dtype == dtypes.half), None)
scale_uop = next((x for x in add_rhs.src if x.op is Ops.CONST and x.dtype == dtypes.half), None)
if old_idx is None or scale_uop is None: return None
reduce, old, old_index, scale = add_lhs.src[0], old_idx.src[0], old_idx.src[1], float(scale_uop.arg)
else: return None
if reduce.arg != (Ops.ADD, 0) or len(reduce.src) != 2 or reduce.src[1].op is not Ops.RANGE: return None
k, product = reduce.src[1], reduce.src[0]
if product.op is not Ops.CAST or product.dtype != dtypes.float or product.src[0].op is not Ops.MUL: return None
lhs, rhs = product.src[0].src
if lhs.op is not Ops.INDEX or rhs.op is not Ops.INDEX: return None
if any(x.dtype != dtypes.half for x in (lhs.src[0], rhs.src[0])): return None
if store.src[0].src[0].dtype not in (dtypes.half, dtypes.float): return None
out_idx = ssimplify(store.src[0].src[1].get_idx())
if old_index is not None and ssimplify(old_index.get_idx()) is not out_idx: return None
k_size, total_size = int(k.vmax)+1, prod(int(x.vmax)+1 for x in ranges)
lhs_idx, rhs_idx = ssimplify(lhs.src[1].get_idx()), ssimplify(rhs.src[1].get_idx())
for m in ranges:
m_size, n_size = int(m.vmax)+1, total_size//(int(m.vmax)+1)
n = ssimplify(out_idx%n_size)
if ssimplify(out_idx//n_size) is not m or int(n.vmin) != 0 or int(n.vmax) != n_size-1: continue
n64 = old is None and n_size == 64
a_mxk_idx, a_kxm_idx = ssimplify(m*k_size+k), ssimplify(k*m_size+m)
b_nxk_idx, b_kxn_idx = ssimplify(n*k_size+k), ssimplify(k*n_size+n)
for a_idx, a_buf, b_idx, b_buf in ((lhs_idx, lhs.src[0], rhs_idx, rhs.src[0]), (rhs_idx, rhs.src[0], lhs_idx, lhs.src[0])):
if not (a_idx is a_mxk_idx or a_idx is a_kxm_idx): continue
if not (b_idx is b_nxk_idx or b_idx is b_kxn_idx): continue
if n64 and (a_idx is not a_mxk_idx or b_idx is not b_nxk_idx): continue
g = GemmMatch(store.src[0].src[0], a_buf, b_buf, m_size, n_size, k_size, old, scale,
a_idx is a_kxm_idx, b_idx is b_kxn_idx)
bm, bn = _gemm_block_m(g), _gemm_block_n(g)
if m_size % bm or n_size % bn or k_size % BLOCK_K: continue
if (m_size//bm)*(n_size//bn) < (32 if old is not None else 512): continue
return g
return None
def _match_batched_gemm(ast:UOp, device:str, arch:str) -> BatchedGemmMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 4 or end.src[0].op is not Ops.STORE: return None
store, ranges = end.src[0], end.src[1:]
if any(x.op is not Ops.RANGE or x.arg[1] is not AxisType.LOOP for x in ranges) or store.src[0].op is not Ops.INDEX: return None
reduce = store.src[1]
if reduce.op is not Ops.REDUCE or reduce.dtype != dtypes.float or reduce.arg != (Ops.ADD, 0) or len(reduce.src) != 2: return None
k, product = reduce.src[1], reduce.src[0]
if k.op is not Ops.RANGE or product.op is not Ops.CAST or product.dtype != dtypes.float or product.src[0].op is not Ops.MUL: return None
lhs, rhs = product.src[0].src
if lhs.op is not Ops.INDEX or rhs.op is not Ops.INDEX or lhs.dtype != dtypes.half or rhs.dtype != dtypes.half: return None
out_idx, lhs_idx, rhs_idx = (ssimplify(x.src[1].get_idx()) for x in (store.src[0], lhs, rhs))
k_size = int(k.vmax)+1
for m in ranges:
for n in ranges:
if n is m: continue
batch = next(x for x in ranges if x is not m and x is not n)
m_size, n_size, batch_size = int(m.vmax)+1, int(n.vmax)+1, int(batch.vmax)+1
if ssimplify(out_idx//(n_size*batch_size)) is not m or ssimplify((out_idx//batch_size)%n_size) is not n or \
ssimplify(out_idx%batch_size) is not batch: continue
if m_size % 64 or n_size % 32 or k_size % BLOCK_K: continue
a_idx, b_idx = ssimplify((batch*k_size+k)*m_size+m), ssimplify((batch*k_size+k)*n_size+n)
if lhs_idx is a_idx and rhs_idx is b_idx: a, b = lhs.src[0], rhs.src[0]
elif rhs_idx is a_idx and lhs_idx is b_idx: a, b = rhs.src[0], lhs.src[0]
else: continue
return BatchedGemmMatch(store.src[0].src[0], a, b, m_size, n_size, k_size, batch_size)
return None
def _gemm_block_m(g:GemmMatch) -> int:
if g.old is None and (g.m,g.n,g.k) in ((393216,576,256), (24576,512,4608)): return 64
return 32 if g.old is not None and (g.m < 512 or (g.a_kxm and g.b_kxn and g.k >= 65536)) else \
64 if g.old is not None else BLOCK_M
def _gemm_block_n(g:GemmMatch) -> int:
if g.old is not None and g.a_kxm and g.b_kxn and (g.m,g.n,g.k) == (256,2304,65536): return 128
if g.old is not None and g.a_kxm and g.b_kxn and (g.m,g.n,g.k) == (256,2304,98304): return 64
if g.n == 288 and g.old is None and not g.a_kxm and g.b_kxn: return 96
if g.n == 576 and g.old is None and not g.a_kxm and g.b_kxn: return 192
if g.old is not None and g.a_kxm and g.b_kxn and g.m == 256 and g.k >= 65536: return BLOCK_K
return 64 if g.n == 64 and g.old is None and not g.a_kxm and not g.b_kxn else BLOCK_N
def _gemm_block_k(g:GemmMatch) -> int:
if g.old is not None and g.a_kxm and g.b_kxn and g.m == 256 and g.k >= 65536: return 64
if g.old is not None and (g.m < 512 or (g.a_kxm and g.b_kxn and g.k >= 65536)) and g.k % 64 == 0: return 64
return 144 if _gemm_block_n(g) == 64 and g.k in (288, 576) else BLOCK_K
def _batched_block_k(g:BatchedGemmMatch) -> int:
if (g.batch, g.m, g.n, g.k) == (192, 64, 288, 8192): return 16
if (g.batch, g.m, g.n, g.k) == (96, 64, 576, 4096): return BLOCK_K
return 128 if g.m == 64 and g.batch >= 96 and g.k % 128 == 0 else BLOCK_K
def _batched_block_n(g:BatchedGemmMatch) -> int:
if (g.batch, g.m, g.n, g.k) == (192, 64, 288, 8192): return 288
return 192 if g.n == 576 else BLOCK_N
def _batched_threads(g:BatchedGemmMatch) -> int:
if _batched_block_n(g) == 288: return 192
return _batched_block_n(g)
def _batched_family_name(g:BatchedGemmMatch) -> str:
return f"coop_bgemm_bm64_bn{_batched_block_n(g)}_bk{_batched_block_k(g)}_t{_batched_threads(g)}" \
f"{'_partial_n' if g.n % _batched_block_n(g) else ''}"
def _gemm_threads(g:GemmMatch) -> int:
if _gemm_block_n(g) == 192: return 192
return THREADS
def _gemm_family_name(g:GemmMatch, output_nchw:GemmOutputNCHW|None) -> str:
return f"coop_gemm_bm{_gemm_block_m(g)}_bn{_gemm_block_n(g)}_bk{_gemm_block_k(g)}_t{_gemm_threads(g)}" \
f"{'_kxm' if g.a_kxm else ''}{'_kxn' if g.b_kxn else ''}{'_acc' if g.old is not None else ''}" \
f"{'_nchw' if output_nchw is not None else ''}"
def _render_gemm(g:GemmMatch, name:str, output_nchw:GemmOutputNCHW|None=None) -> str:
bm, bn_size, bk, threads = _gemm_block_m(g), _gemm_block_n(g), _gemm_block_k(g), _gemm_threads(g)
pack_b = g.b_kxn and not g.a_kxm
as_stride, bs_stride = (bm if g.a_kxm else bk+8), (bn_size if g.b_kxn else bk+8)
waves_m, waves_n = (2, 3) if threads == 192 else (1, 2) if threads == 64 else \
(((1, 8) if bm == 32 else (2, 4) if bm == 64 else (4, 2)) if threads == 256 else
((1, 4) if bm <= 64 else (2, 2)))
tiles_m, tiles_n = bm//(waves_m*WMMA_M), bn_size//(waves_n*WMMA_N)
cslot, aslot, bslot = g.c.arg.slot, g.a.arg.slot, g.b.arg.slot
buffers = (g.c, g.a, g.b) + ((g.old,) if g.old is not None else ())
params = ', '.join(f'{"float" if x.dtype == dtypes.float else "half"}* p{x.arg.slot}' for x in sorted(buffers, key=lambda x:x.arg.slot))
params += ', int M, int N, int K' + (', int S' if output_nchw is not None else '')
lines = [
'#define half _Float16',
'typedef half half16 __attribute__((ext_vector_type(16)));',
'typedef float float8 __attribute__((ext_vector_type(8)));',
'typedef unsigned uint8 __attribute__((ext_vector_type(8)));',
'typedef unsigned uint16 __attribute__((ext_vector_type(16)));',
'typedef unsigned short ushort16 __attribute__((ext_vector_type(16)));',
'#define HALF_BITS(x) (unsigned short)(x), (unsigned short)((x)>>16)',
'#define WMMA __builtin_amdgcn_wmma_f32_16x16x16_f16_w32',
f'extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size({threads}, {threads}))) {name}({params}) {{',
f' __attribute__((shared, aligned(32))) half As[{bk*bm if g.a_kxm else bm*as_stride}], '
f'Bs[{bk*bn_size if g.b_kxn else bn_size*bs_stride}];',
' int bn=__builtin_amdgcn_workgroup_id_x(), bm=__builtin_amdgcn_workgroup_id_y(), tid=__builtin_amdgcn_workitem_id_x();',
f' int wave=tid>>5, lane=tid&31, wm=wave/{waves_n}, wn=wave%{waves_n}, row=lane&15, halfrow=lane>>4;',
]
lines.append(f' float8 c[{tiles_m}][{tiles_n}]={{}};')
lines += [f' for (int kt=0; kt<K/{bk}; kt++) {{']
if g.a_kxm:
aseg_count = bm*bk//16
lines += [' #pragma unroll', f' for (int q=0; q<{(aseg_count+threads-1)//threads}; q++) {{',
f' int aseg=tid+q*{threads};']
if aseg_count % threads: lines.append(f' if (aseg<{aseg_count}) {{')
lines += [f' int ak=aseg/{bm//16}, am=(aseg%{bm//16})*16;',
f' *((half16*)(As+ak*{bm}+am))=*((half16*)(p{aslot}+((long)(kt*{bk}+ak)*M)+bm*{bm}+am));']
if aseg_count % threads: lines.append(' }')
lines.append(' }')
else:
prefix, suffix = (f' if (tid<{bm}) {{ ', ' }') if threads != bm else (' ', '')
lines += [f'{prefix}long ao=((long)(bm*{bm}+tid)*K)+kt*{bk};']
lines += [' #pragma unroll', f' for (int q=0; q<{bk//16}; q++) '
f'*((half16*)(As+tid*{as_stride}+q*16))=*((half16*)(p{aslot}+ao+q*16));']
lines[-1] += suffix
if pack_b:
bsegs = bn_size//16
if threads != bsegs*16: lines.append(f' if (tid<{bsegs*16}) {{')
lines += [' #pragma unroll', f' for (int q=0; q<{bk//32}; q++) {{',
f' int bkp=tid/{bsegs}+q*16, bn0=(tid%{bsegs})*16;',
f' half16 bv0=*((half16*)(p{bslot}+((long)(kt*{bk}+bkp*2)*N)+bn*{bn_size}+bn0));',
f' half16 bv1=*((half16*)(p{bslot}+((long)(kt*{bk}+bkp*2+1)*N)+bn*{bn_size}+bn0));',
' ushort16 pb0=__builtin_bit_cast(ushort16,bv0), pb1=__builtin_bit_cast(ushort16,bv1);',
f' *((uint16*)(((unsigned*)Bs)+bkp*{bn_size}+bn0))=__builtin_convertvector(pb0,uint16)|'
'(__builtin_convertvector(pb1,uint16)<<16);', ' }']
if threads != bsegs*16: lines.append(' }')
elif g.b_kxn:
bsegs = bn_size//16
lines += [' #pragma unroll', f' for (int q=0; q<{bn_size*bk//(threads*16)}; q++) {{',
f' int bseg=tid+q*{threads}, bki=bseg/{bsegs}, bni=(bseg%{bsegs})*16;',
f' *((half16*)(Bs+bki*{bn_size}+bni))=*((half16*)(p{bslot}+'
f'((long)(kt*{bk}+bki)*N)+bn*{bn_size}+bni));', ' }']
else:
prefix, suffix = (f' if (tid<{bn_size}) {{ ', ' }') if threads != bn_size else (' ', '')
lines += [f'{prefix}long bo=((long)(bn*{bn_size}+tid)*K)+kt*{bk};']
lines += [' #pragma unroll', f' for (int q=0; q<{bk//16}; q++) '
f'*((half16*)(Bs+tid*{bs_stride}+q*16))=*((half16*)(p{bslot}+bo+q*16));']
lines[-1] += suffix
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [' #pragma unroll', f' for (int ki=0; ki<{bk//WMMA_K}; ki++) {{']
lines += [f' half16 av[{tiles_m}], bv[{tiles_n}];', ' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{']
if g.a_kxm:
lines += [' half16 v;', ' #pragma unroll',
f' for (int e=0; e<16; e++) v[e]=As[(ki*16+e)*{bm}+(wm*{tiles_m}+im)*16+row];', ' av[im]=v;']
else:
lines += [f' av[im]=*((half16*)(As+(((wm*{tiles_m}+im)*16+row)*{as_stride}+ki*16)));']
lines += [' }', ' #pragma unroll', f' for (int jn=0; jn<{tiles_n}; jn++) {{']
if pack_b:
half_bits = ','.join(f'HALF_BITS(bp[{e}])' for e in range(8))
lines += [' uint8 bp;', ' #pragma unroll',
f' for (int e=0; e<8; e++) bp[e]=((unsigned*)Bs)[(ki*8+e)*{bn_size}+(wn*{tiles_n}+jn)*16+row];',
f' ushort16 bb=(ushort16){{{half_bits}}};', ' bv[jn]=__builtin_bit_cast(half16,bb);']
elif g.b_kxn:
lines += [' half16 v;', ' #pragma unroll',
f' for (int e=0; e<16; e++) v[e]=Bs[(ki*16+e)*{bn_size}+(wn*{tiles_n}+jn)*16+row];', ' bv[jn]=v;']
else:
lines += [f' bv[jn]=*((half16*)(Bs+(((wn*{tiles_n}+jn)*16+row)*{bs_stride}+ki*16)));']
lines += [' }', ' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{', ' #pragma unroll',
f' for (int jn=0; jn<{tiles_n}; jn++) c[im][jn]=WMMA(av[im],bv[jn],c[im][jn]);', ' }', ' }']
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");', ' }']
lines += [' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{', ' #pragma unroll',
f' for (int jn=0; jn<{tiles_n}; jn++) {{', ' #pragma unroll', ' for (int e=0; e<8; e++) {',
f' int om=bm*{bm}+(wm*{tiles_m}+im)*16+e*2+halfrow, on=bn*{bn_size}+(wn*{tiles_n}+jn)*16+row;']
if output_nchw is not None:
lines.append(' int spatial_size=S*S;')
lines.append(' long oi=((long)(om/spatial_size)*N+on)*spatial_size+om%spatial_size;')
else: lines.append(' long oi=(long)om*N+on;')
if g.old is None: lines.append(f' p{cslot}[oi]=(half)c[im][jn][e];')
else: lines.append(f' p{cslot}[oi]=(float)((half)c[im][jn][e]+(half){g.scale}*p{g.old.arg.slot}[oi]);')
lines += [' }', ' }', ' }']
lines += ['}']
return '\n'.join(lines)
def _render_batched_gemm(g:BatchedGemmMatch, name:str) -> tuple[str, int]:
bm, bn_size, bk, threads, waves_m = 64, _batched_block_n(g), _batched_block_k(g), _batched_threads(g), 1
waves_n = threads//32
exact_n = g.n % bn_size == 0
tiles_m, tiles_n = bm//(waves_m*WMMA_M), bn_size//(waves_n*WMMA_N)
cslot, aslot, bslot = g.c.arg.slot, g.a.arg.slot, g.b.arg.slot
params = ', '.join(f'{"float" if x.dtype == dtypes.float else "half"}* p{x.arg.slot}'
for x in sorted((g.c, g.a, g.b), key=lambda x:x.arg.slot))
params += ', int B, int M, int N, int K'
lines = [
'#define half _Float16',
'typedef half half16 __attribute__((ext_vector_type(16)));',
'typedef float float8 __attribute__((ext_vector_type(8)));',
'#define WMMA __builtin_amdgcn_wmma_f32_16x16x16_f16_w32',
f'extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size({threads}, {threads}))) {name}({params}) {{',
f' __attribute__((shared, aligned(32))) half As[{bm*bk}], Bs[{bn_size*bk}];',
' int bn=__builtin_amdgcn_workgroup_id_x(), bm=__builtin_amdgcn_workgroup_id_y(), '
'batch=__builtin_amdgcn_workgroup_id_z(), tid=__builtin_amdgcn_workitem_id_x();',
f' int wave=tid>>5, lane=tid&31, wm=wave/{waves_n}, wn=wave%{waves_n}, row=lane&15, halfrow=lane>>4;',
]
lines.append(f' float8 c[{tiles_m}][{tiles_n}]={{}};')
lines.append(f' for (int kt=0; kt<K/{bk}; kt++) {{')
aseg_count = bm*bk//16
lines += [' #pragma unroll', f' for (int q=0; q<{(aseg_count+threads-1)//threads}; q++) {{',
f' int aseg=tid+q*{threads};']
if aseg_count % threads: lines.append(f' if (aseg<{aseg_count}) {{')
lines += [f' int ak=aseg/{bm//16}, am=(aseg%{bm//16})*16;',
f' *((half16*)(As+ak*{bm}+am))=*((half16*)(p{aslot}+'
f'((long)(batch*K+kt*{bk}+ak)*M)+bm*{bm}+am));']
if aseg_count % threads: lines.append(' }')
lines.append(' }')
bsegs = bn_size//16
lines += [' #pragma unroll', f' for (int q=0; q<{bn_size*bk//(threads*16)}; q++) {{',
f' int bseg=tid+q*{threads}, bki=bseg/{bsegs}, bni=(bseg%{bsegs})*16;',
f' *((half16*)(Bs+bki*{bn_size}+bni))=' + ('' if exact_n else f'bn*{bn_size}+bni<N ? ') +
f'*((half16*)(p{bslot}+((long)(batch*K+kt*{bk}+bki)*N)+bn*{bn_size}+bni))' +
(';' if exact_n else ' : (half16){0};'), ' }']
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [' #pragma unroll', f' for (int ki=0; ki<{bk//WMMA_K}; ki++) {{',
f' half16 av[{tiles_m}], bv[{tiles_n}];', ' #pragma unroll',
f' for (int im=0; im<{tiles_m}; im++) {{', ' half16 v;', ' #pragma unroll',
f' for (int e=0; e<16; e++) v[e]=As[(ki*16+e)*{bm}+(wm*{tiles_m}+im)*16+row];',
' av[im]=v;', ' }', ' #pragma unroll', f' for (int jn=0; jn<{tiles_n}; jn++) {{',
' half16 v;', ' #pragma unroll',
f' for (int e=0; e<16; e++) v[e]=Bs[(ki*16+e)*{bn_size}+(wn*{tiles_n}+jn)*16+row];',
' bv[jn]=v;', ' }', ' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{',
' #pragma unroll', f' for (int jn=0; jn<{tiles_n}; jn++) c[im][jn]=WMMA(av[im],bv[jn],c[im][jn]);',
' }', ' }']
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");', ' }']
lines += [' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{', ' #pragma unroll',
f' for (int jn=0; jn<{tiles_n}; jn++) {{',
f' int om=bm*{bm}+(wm*{tiles_m}+im)*16+halfrow, on=bn*{bn_size}+(wn*{tiles_n}+jn)*16+row;',
(' {' if exact_n else ' if (on<N) {'), ' #pragma unroll',
f' for (int e=0; e<8; e++) p{cslot}[((long)(om+e*2)*N+on)*B+batch]=c[im][jn][e];',
' }', ' }', ' }']
lines += ['}']
return '\n'.join(lines), bm
def _render_direct_conv_bwd_activation(g:DirectConvBwdActivationMatch, name:str) -> str:
bm, bn, bk, threads = 128, min(g.cin, 64), 32, 128
persist_weights = g.cin <= 64
tiles_m = 2
tiles_n, aslot, bslot = bn//16, (3 if g.residual else 2), (4 if g.residual else 3)
params = 'float* p0, float* p1, half* p2, half* p3, half* p4' if g.residual else 'float* p0, half* p1, half* p2, half* p3'
lines = [
'#define half _Float16',
'typedef half half16 __attribute__((ext_vector_type(16)));',
'typedef half half8 __attribute__((ext_vector_type(8)));',
'typedef float float8 __attribute__((ext_vector_type(8)));',
'typedef unsigned uint8 __attribute__((ext_vector_type(8)));',
'typedef unsigned short ushort16 __attribute__((ext_vector_type(16)));',
'#define HALF_BITS(x) (unsigned short)(x), (unsigned short)((x)>>16)',
'#define WMMA __builtin_amdgcn_wmma_f32_16x16x16_f16_w32',
f'extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size({threads},{threads}))) {name}({params}) {{',
f' __attribute__((shared, aligned(32))) half As[{bm*max(bk,bn)}], Bs[{bn*(g.cout if persist_weights else bk)}];',
' int bn=__builtin_amdgcn_workgroup_id_x(), bm=__builtin_amdgcn_workgroup_id_y(), tid=__builtin_amdgcn_workitem_id_x();',
' int wave=tid>>5, lane=tid&31, wm=wave, row=lane&15, halfrow=lane>>4;',
f' float8 total[{tiles_m}][{tiles_n}]={{}};',
' for (int patch=0; patch<9; patch++) {',
f' float8 c[{tiles_m}][{tiles_n}]={{}};',
]
if persist_weights:
lines += [' #pragma unroll', f' for (int q=0; q<{bn*(g.cout//2)//threads}; q++) {{',
f' int be=tid+q*{threads}, bp=be/{bn}, ci=be%{bn};',
f' half blo=p{bslot}[(bp*2)*{g.cin*9}+(bn*{bn}+ci)*9+patch];',
f' half bhi=p{bslot}[(bp*2+1)*{g.cin*9}+(bn*{bn}+ci)*9+patch];',
f' ((unsigned*)Bs)[bp*{bn}+ci]=__builtin_bit_cast(unsigned short,blo)|'
'((unsigned)__builtin_bit_cast(unsigned short,bhi)<<16);', ' }']
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [f' for (int ct=0; ct<{g.cout//bk}; ct++) {{', f' int co0=ct*{bk};']
if not persist_weights:
lines += [' #pragma unroll', f' for (int q=0; q<{bn*(bk//2)//threads}; q++) {{',
f' int be=tid+q*{threads}, bp=be/{bn}, ci=be%{bn};',
f' half blo=p{bslot}[(co0+bp*2)*{g.cin*9}+(bn*{bn}+ci)*9+patch];',
f' half bhi=p{bslot}[(co0+bp*2+1)*{g.cin*9}+(bn*{bn}+ci)*9+patch];',
f' ((unsigned*)Bs)[bp*{bn}+ci]=__builtin_bit_cast(unsigned short,blo)|'
'((unsigned)__builtin_bit_cast(unsigned short,bhi)<<16);', ' }']
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [
' #pragma unroll', f' for (int ki=0; ki<{bk//16}; ki++) {{',
f' half16 av[{tiles_m}], bv[{tiles_n}];', ' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{',
f' int ami=bm*{bm}+(wm*{tiles_m}+im)*16+row, ab=ami/{g.spatial*g.spatial}, apos=ami%{g.spatial*g.spatial};',
f' int ay=apos/{g.spatial}+1-patch/3, ax=apos%{g.spatial}+1-patch%3;',
f' av[im]=(ay>=0 && ay<{g.spatial} && ax>=0 && ax<{g.spatial}) ? '
f'*((half16*)(p{aslot}+((long)ab*{g.spatial*g.spatial}+ay*{g.spatial}+ax)*{g.cout}+co0+ki*16)) : (half16){{0}};',
' }',
' #pragma unroll', f' for (int jn=0; jn<{tiles_n}; jn++) {{', ' uint8 bp;', ' #pragma unroll',
f' for (int e=0; e<8; e++) bp[e]=((unsigned*)Bs)[({"ct*16+" if persist_weights else ""}ki*8+e)*{bn}+jn*16+row];',
' ushort16 bb=(ushort16){HALF_BITS(bp[0]),HALF_BITS(bp[1]),HALF_BITS(bp[2]),HALF_BITS(bp[3]),'
'HALF_BITS(bp[4]),HALF_BITS(bp[5]),HALF_BITS(bp[6]),HALF_BITS(bp[7])};',
' bv[jn]=__builtin_bit_cast(half16,bb);', ' }', ' #pragma unroll',
f' for (int im=0; im<{tiles_m}; im++) {{',
' #pragma unroll', f' for (int jn=0; jn<{tiles_n}; jn++) c[im][jn]=WMMA(av[im],bv[jn],c[im][jn]);',
' }', ' }']
if not persist_weights:
lines += [' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [' }',
' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");',
' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{', ' #pragma unroll',
f' for (int jn=0; jn<{tiles_n}; jn++) total[im][jn]+=__builtin_convertvector('
'__builtin_convertvector(c[im][jn],half8),float8);', ' }', ' }',
' #pragma unroll', f' for (int im=0; im<{tiles_m}; im++) {{', ' #pragma unroll',
f' for (int jn=0; jn<{tiles_n}; jn++) {{', ' #pragma unroll', ' for (int e=0; e<8; e++) {',
f' int lm=(wm*{tiles_m}+im)*16+e*2+halfrow, lc=jn*16+row;',
f' As[lc*{bm}+lm]=(half)total[im][jn][e];', ' }', ' }', ' }',
' __builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier(); '
'__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");']
lines += [' #pragma unroll', f' for (int q=0; q<{bm*bn//(threads*8)}; q++) {{',
f' int oe=tid*{bm*bn//threads}+q*8, lc=oe/{bm}, lm=oe%{bm};',
f' int gm=bm*{bm}+lm, ob=gm/{g.spatial*g.spatial}, pos=gm%{g.spatial*g.spatial};',
f' long oo=((long)ob*{g.cin}+bn*{bn}+lc)*{g.spatial*g.spatial}+pos;',
' half8 grad=*((half8*)(As+oe));']
if g.residual: lines += [' grad+=*((half8*)(p2+oo));', ' half8 z=__builtin_convertvector(*((float8*)(p1+oo)),half8);']
else: lines.append(' half8 z=*((half8*)(p1+oo));')
lines += [' half8 sig=(half8)1.0/((half8)1.0+__builtin_elementwise_exp2(z*(half)-2.4554669595930156));',
' *((float8*)(p0+oo))=__builtin_convertvector(sig*grad+(half)1.702*z*grad*sig*((half)1.0-sig),float8);', ' }']
lines.append('}')
return '\n'.join(lines)
def direct_conv_bwd_activation_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if not isinstance(g:=ast.tag, DirectConvBwdActivationMatch) or renderer.target.device != "AMD" or \
not renderer.target.arch.startswith("gfx11"): return None
name = f"coop_direct_conv_bwd_activation_{g.m}_{g.cin}_{g.cout}{'_res' if g.residual else ''}"
source = _render_direct_conv_bwd_activation(g, name)
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(2*g.m*g.cin*g.cout*9, 0, 0)))
slots = (0,1,2,3,4) if g.residual else (0,1,2,3)
bn = min(g.cin, 64)
info = ProgramInfo(name=name, global_size=((g.cin+bn-1)//bn, g.m//128, 1), local_size=(128,1,1),
globals=slots, outs=(0,), ins=slots[1:])
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
def cooperative_gemm_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if (g:=_match_gemm(ast, renderer.target.device, renderer.target.arch)) is not None:
output_nchw = ast.tag if isinstance(ast.tag, GemmOutputNCHW) else None
name = _gemm_family_name(g, output_nchw)
source = _render_gemm(g, name, output_nchw)
global_size = (g.n//_gemm_block_n(g), g.m//_gemm_block_m(g), 1)
local_size = (_gemm_threads(g), 1, 1)
estimates = Estimates(2*g.m*g.n*g.k, 2*(g.m*g.k+g.n*g.k+g.m*g.n), 2*(g.m*g.k+g.n*g.k+g.m*g.n))
slots = tuple(sorted((g.c.arg.slot, g.a.arg.slot, g.b.arg.slot) + ((g.old.arg.slot,) if g.old is not None else ())))
out_slot = g.c.arg.slot
suffix = f"_{g.m}_{g.n}_{g.k}"
variables:tuple[UOp, ...] = (UOp.variable(f"M{suffix}", g.m, g.m, dtypes.int),
UOp.variable(f"N{suffix}", g.n, g.n, dtypes.int),
UOp.variable(f"K{suffix}", g.k, g.k, dtypes.int))
if output_nchw is not None:
variables += (UOp.variable(f"S{suffix}_{output_nchw.spatial}", output_nchw.spatial, output_nchw.spatial, dtypes.int),)
elif (bg:=_match_batched_gemm(ast, renderer.target.device, renderer.target.arch)) is not None:
name = _batched_family_name(bg)
source, bm = _render_batched_gemm(bg, name)
global_size = ((bg.n+_batched_block_n(bg)-1)//_batched_block_n(bg), bg.m//bm, bg.batch)
local_size = (_batched_threads(bg), 1, 1)
estimates = Estimates(2*bg.batch*bg.m*bg.n*bg.k, 2*bg.batch*(bg.m*bg.k+bg.n*bg.k+2*bg.m*bg.n),
2*bg.batch*(bg.m*bg.k+bg.n*bg.k+2*bg.m*bg.n))
slots, out_slot = tuple(sorted((bg.c.arg.slot, bg.a.arg.slot, bg.b.arg.slot))), bg.c.arg.slot
suffix = f"_{bg.batch}_{bg.m}_{bg.n}_{bg.k}"
variables = (UOp.variable(f"B{suffix}", bg.batch, bg.batch, dtypes.int), UOp.variable(f"M{suffix}", bg.m, bg.m, dtypes.int),
UOp.variable(f"N{suffix}", bg.n, bg.n, dtypes.int), UOp.variable(f"K{suffix}", bg.k, bg.k, dtypes.int))
else: return None
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=estimates))
info = ProgramInfo(name=name, global_size=global_size, local_size=local_size, vars=variables, globals=slots,
outs=(out_slot,), ins=tuple(x for x in slots if x != out_slot))
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
+90 -21
View File
@@ -35,13 +35,54 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
pass
if good_tc_opt:
if rngs is not None:
tc_sizes = [r.src[0] for r in rngs]
skinny_output = any(resolve(sz < 2, False) for sz in tc_sizes[:2])
very_long_reduce = resolve(tc_sizes[2] >= 4096, False)
long_reduce = resolve(tc_sizes[2] >= 1024, False)
small_m = resolve(tc_sizes[0] <= 32, False)
tiny_m = resolve(tc_sizes[0] <= 16, False)
if resolve(tc_sizes[0] >= 32, False) and resolve(tc_sizes[0] < 33, False) and resolve(tc_sizes[1] >= 1536, False) and \
resolve(tc_sizes[2] >= 128, False) and resolve(tc_sizes[2] <= 512, False):
upcast_n = 8 if resolve(tc_sizes[1] >= 4096, False) else 6
rngs[1] = tk.apply_opt(Opt(OptOps.UPCAST, tk.rngs.index(rngs[1]), upcast_n))[0]
rngs[0] = tk.apply_opt(Opt(OptOps.UPCAST, tk.rngs.index(rngs[0]), 2))[0]
rngs[0] = tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(rngs[0]), 8))[0]
rngs[1] = tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(rngs[1]), 2))[0]
return tk
for tc_dim in [1,0]: # attempt to upcast M and N
szs = [sz for sz in [5,4,3,2] if rngs[tc_dim].src[0].divides(sz) is not None]
if skinny_output or resolve(tc_sizes[tc_dim] >= 32768, False): continue
short_conv_n = tc_dim == 1 and resolve(tc_sizes[0] >= 65536, False) and resolve(tc_sizes[1] == 18, False) and \
resolve(tc_sizes[2] <= 4, False)
upcast_sizes = [2] if short_conv_n or (very_long_reduce and (tiny_m or (tc_dim == 0 and small_m))) else [5,4,3,2]
szs = [sz for sz in upcast_sizes if rngs[tc_dim].src[0].divides(sz) is not None]
if szs:
# set it to the replaced range
rngs[tc_dim] = tk.apply_opt(Opt(OptOps.UPCAST, tk.rngs.index(rngs[tc_dim]), szs[0]))[0]
if (szs := [sz for sz in [4,2] if rngs[0].src[0].divides(sz) is not None]): # attempt to local N
tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(rngs[0]), szs[0]))
if skinny_output:
outer_rngs = [r for r in tk.rngs if r.arg[-1] is AxisType.GLOBAL and r not in rngs[:2]]
if outer_rngs and outer_rngs[0].src[0].divides(2) is not None:
tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(outer_rngs[0]), 2))
if very_long_reduce:
outer_rngs = [r for r in tk.rngs if r.arg[-1] is AxisType.GLOBAL and r not in rngs[:2]]
if outer_rngs and (outer_local:=next((x for x in (16, 4, 2) if outer_rngs[0].src[0].divides(x) is not None), None)):
tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(outer_rngs[0]), outer_local))
return tk
local_sizes = [2] if long_reduce and small_m else [4,2]
if (szs := [sz for sz in local_sizes if rngs[0].src[0].divides(sz) is not None]):
rngs[0] = tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(rngs[0]), szs[0]))[0]
if long_reduce and small_m and resolve(tc_sizes[1] >= 128, False) and \
rngs[1].arg[-1] is AxisType.GLOBAL and rngs[1].src[0].divides(3) is not None:
rngs[1] = tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(rngs[1]), 3))[0]
if tk.applied_opts[-1] == Opt(OptOps.LOCAL, 1, 4):
outer_rngs = [r for r in tk.rngs if r.arg[-1] is AxisType.GLOBAL]
if outer_rngs and resolve(outer_rngs[0].src[0] >= 384, False) and \
(outer_local:=next((x for x in (4, 3, 2) if outer_rngs[0].src[0].divides(x) is not None), None)):
tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(outer_rngs[0]), outer_local))
elif resolve(tc_sizes[0] <= 4, False) and resolve(tc_sizes[2] >= 256, False):
outer_rngs = [r for r in tk.rngs if r.arg[-1] is AxisType.GLOBAL]
outer_local = 16 if resolve(tc_sizes[2] >= 512, False) else 2
if (outer_rng:=next((r for r in reversed(outer_rngs) if r.src[0].divides(outer_local) is not None), None)) is not None:
tk.apply_opt(Opt(OptOps.LOCAL, tk.rngs.index(outer_rng), outer_local))
return tk
# make a copy so it does not mutate the input
@@ -82,7 +123,8 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
# are we grouping? (requires local shape support)
if resolve(prod(k.output_shape[i] for i in k.upcastable_dims) <= (240 if NOLOCALS else 2048), False):
for axis, sz in itertools.product((0, 1, 2), (16,)):
group_sizes = (8, 16) if len(k.bufs) >= 7 else (16,)
for axis, sz in itertools.product((0, 1, 2), group_sizes):
try:
k.apply_opt(Opt(OptOps.GROUPTOP, axis, sz))
break
@@ -114,10 +156,11 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
# potentially do more upcasts of non reduce axes based on a heuristic
is_dsp = k.ren is not None and k.ren.target.device == "DSP"
upcasted_axis: set[int] = set()
while resolve(prod(k.output_shape[i] for i in k.upcastable_dims) >= 1024) and (k.upcast_size() < 32):
while resolve(prod(k.output_shape[i] for i in k.upcastable_dims) >= 1024) and (k.upcast_size() < 2):
xb_choices = []
# consider all upcastable axes with 3 or 4 upcast (128 on the DSP)
for axis, upcast_amount in itertools.product(k.upcastable_dims, ([128] if not len(upcasted_axis) else []) if is_dsp else [3,4]):
upcast_amounts = ([128] if not len(upcasted_axis) else []) if is_dsp else ([3,4] if k.reduceop is not None else [2,3,4])
for axis, upcast_amount in itertools.product(k.upcastable_dims, upcast_amounts):
# if we haven't upcasted it, it mods, and buffer has stride 0 on axis while having no stride 0 in the upcasted axis already
if axis in upcasted_axis or k.full_shape[axis]%upcast_amount != 0: continue
rng = k.rngs[axis]
@@ -141,15 +184,17 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
# if last reduce dim is small(ish), loop unroll the reduce
# NOTE: this can fail on multireduce with mismatching dimensions, this is okay
four_by_four_reduce = len(k.unrollable_dims) >= 2 and all(resolve(k.full_shape[x] == 4, False) for x in k.unrollable_dims[-2:])
try:
if k.unrollable_dims and (k.upcast_size() <= 4 or not k.axes_of(AxisType.UNROLL)) and (k.upcast_size() < 64):
if (s:=k.full_shape[k.unrollable_dims[-1]]) <= 32:
if (s:=k.full_shape[k.unrollable_dims[-1]]) <= 4:
k.apply_opt(Opt(OptOps.UNROLL, len(k.unrollable_dims)-1, 0))
# if it's small, upcast a second reduce dimension too
if k.unrollable_dims and s <= 3 and k.full_shape[k.unrollable_dims[-1]] <= 3:
k.apply_opt(Opt(OptOps.UNROLL, len(k.unrollable_dims)-1, 0))
else:
for splits in [4]:
bn_spatial_reduce = len(k.unrollable_dims) >= 2 and resolve(k.full_shape[k.unrollable_dims[-2]] == 6, False)
for splits in ([8, 4] if bn_spatial_reduce else [4]):
if k.full_shape[axis:=k.unrollable_dims[-1]]%splits == 0:
k.apply_opt(Opt(OptOps.UNROLL, len(k.unrollable_dims)-1, splits))
break
@@ -166,20 +211,44 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
if NOLOCALS:
k.apply_opt(Opt(OptOps.NOLOCALS))
else:
special_local = False
if four_by_four_reduce and len(global_axes:=k.axes_of(AxisType.GLOBAL, AxisType.LOOP)) >= 3:
lk, local_rngs = k.copy(), [k.rngs[x] for x in global_axes[-3:]]
try:
for rng, sz in zip(local_rngs, (16, 4, 4)):
lk.apply_opt(Opt(OptOps.LOCAL, lk.rngs.index(rng), sz))
k, special_local = lk, True
except KernelOptError: pass
# prioritize making expand axes local
local_axis_ranking = [(any(k.rngs[axis] not in b.src[1].get_idx().backward_slice for b in k.bufs), axis) \
for axis in k.axes_of(AxisType.GLOBAL, AxisType.LOOP) if k.rngs[axis].src[0].op is Ops.CONST]
to_local: list[tuple[int, int]] = []
for _, axis in sorted(local_axis_ranking, key=lambda x: (-x[0], -x[1])):
local_size = prod(sz for _, sz in to_local)
local_sz: int|None = next((x for x in ([32] * (axis == 0) + [16,8,4,3,2]) if k.full_shape[axis] % x == 0 and local_size * x <= 128), None)
if local_sz is not None: to_local.append((axis, local_sz))
deleted_shape = 0
for axis, local_sz in sorted(to_local[:3]):
axis = axis - deleted_shape
will_delete_shape = local_sz == k.full_shape[axis]
k.apply_opt(Opt(OptOps.LOCAL, axis, local_sz))
if will_delete_shape: deleted_shape += 1
if not special_local:
local_axis_ranking = [(any(k.rngs[axis] not in b.src[1].get_idx().backward_slice for b in k.bufs), axis) \
for axis in k.axes_of(AxisType.GLOBAL, AxisType.LOOP) if k.rngs[axis].src[0].op is Ops.CONST]
to_local: list[tuple[int, int]] = []
for _, axis in sorted(local_axis_ranking, key=lambda x: (-x[0], -x[1])):
local_size = prod(sz for _, sz in to_local)
local_sz: int|None = next((x for x in ([32] * (axis == 0) + [16,8,4,3,2]) if k.full_shape[axis] % x == 0 and local_size * x <= 128), None)
if local_sz is not None: to_local.append((axis, local_sz))
deleted_shape = 0
for axis, local_sz in sorted(to_local[:3]):
axis = axis - deleted_shape
will_delete_shape = local_sz == k.full_shape[axis]
k.apply_opt(Opt(OptOps.LOCAL, axis, local_sz))
if will_delete_shape: deleted_shape += 1
# Both 3x3 reduce axes are already fully unrolled above. Tile the exposed
# spatial axes without changing reduction order.
unroll_sizes = [k.full_shape[x] for x in k.axes_of(AxisType.UNROLL)]
global_axes = k.axes_of(AxisType.GLOBAL, AxisType.LOOP)
if unroll_sizes == [3, 3] and len(global_axes) >= 2:
axis_size = k.full_shape[global_axes[1]]
if axis_size <= 16: k.apply_opt(Opt(OptOps.UPCAST, 1, 0))
elif axis_size % 4 == 0: k.apply_opt(Opt(OptOps.UPCAST, 1, 4))
if len(global_axes) >= 4 and k.full_shape[global_axes[2]] == 4: k.apply_opt(Opt(OptOps.LOCAL, 2, 4))
remaining_reduces = [k.full_shape[x] for x in k.unrollable_dims]
if len(remaining_reduces) == 2 and remaining_reduces[0] == 6 and remaining_reduces[1] >= 8 and \
remaining_reduces[1] % 4 == 0 and k.upcast_size() <= 16:
k.apply_opt(Opt(OptOps.UNROLL, 1, 4))
# **** threading ****
+703
View File
@@ -0,0 +1,703 @@
from collections import Counter
from dataclasses import dataclass, replace
from tinygrad.dtype import dtypes
from tinygrad.renderer import Estimates, Renderer
from tinygrad.uop.ops import AxisType, KernelInfo, Ops, ProgramInfo, UOp, ssimplify
@dataclass(frozen=True)
class ChannelReduceMatch:
kind: str
params: tuple[UOp, ...]
groups: int
channels: int
spatial: int
@dataclass(frozen=True)
class DualChannelReduceMatch:
groups:int
channels:int
spatial:int
@dataclass(frozen=True)
class DualActivationReduceElementwiseMatch:
groups:int
channels:int
spatial:int
@dataclass(frozen=True)
class ActivationVarGradElementwiseSumDMeanMatch:
batch:int
@dataclass(frozen=True)
class DualMoments512Match:
batch:int
@dataclass(frozen=True)
class DualBNGradDMean512Match:
batch:int
@dataclass(frozen=True)
class Col2ImMatch:
params: tuple[UOp, ...]
fused_activation: bool
batch: int
channels: int
spatial: int
@dataclass(frozen=True)
class Im2ColMatch:
params: tuple[UOp, ...]
batch: int
channels: int
spatial: int
def _match_moment_mean_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE), None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
batch, sy, sx = tuple(int(r.vmax)+1 for r in reduce.src[1:])
if batch not in (1024,1280,1536) or (sy,sx) != (4,4) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,512),(1,dtypes.half,batch*8192)): return None
value = reduce.src[0]
if value.op is not Ops.CAST or value.dtype != dtypes.float or value.src[0].op is not Ops.INDEX or value.src[0].src[0] is not params[1]: return None
return batch if Counter(u.op for u in ast.toposort()) in (Counter({Ops.CONST:10,Ops.MUL:5,Ops.RANGE:4,Ops.ADD:4,Ops.PARAM:2,Ops.INDEX:2,
Ops.CAST:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}), Counter({Ops.CONST:8,Ops.MUL:4,Ops.RANGE:4,Ops.ADD:3,Ops.PARAM:2,
Ops.INDEX:2,Ops.CAST:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1})) else None
def _match_moment_var_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE), None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
batch, sy, sx = tuple(int(r.vmax)+1 for r in reduce.src[1:])
if batch not in (1024,1280,1536) or (sy,sx) != (4,4) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,512),(1,dtypes.half,batch*8192),(2,dtypes.float,512)): return None
value = reduce.src[0]
if value.op is not Ops.MUL or value.src[0] is not value.src[1] or value.src[0].op is not Ops.CAST or \
value.src[0].src[0].op is not Ops.INDEX or value.src[0].src[0].src[0] is not params[1]: return None
return batch if Counter(u.op for u in ast.toposort()) in (Counter({Ops.CONST:12,Ops.MUL:8,Ops.ADD:6,Ops.RANGE:4,Ops.PARAM:3,Ops.INDEX:3,
Ops.CAST:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}), Counter({Ops.CONST:10,Ops.MUL:7,Ops.ADD:5,Ops.RANGE:4,Ops.PARAM:3,
Ops.INDEX:3,Ops.CAST:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1})) else None
def moments_512_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if not isinstance(ast.tag,DualMoments512Match) or renderer.target.device != "AMD" or not renderer.target.arch.startswith("gfx11"): return None
batch = ast.tag.batch
name = f"channel_moments_{batch}_512_4_{ast.key.hex()[:8]}"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(128,128))) {name}(
float* p0, float* p1, half* p2) {{
__attribute__((shared, aligned(32))) float partial_sum[128], partial_sq[128];
int c=__builtin_amdgcn_workgroup_id_x(), tid=__builtin_amdgcn_workitem_id_x();
float sum=0.0f, sq=0.0f;
for (int q=0; q<{batch//8}; q++) {{
int r=tid+q*128, idx=(r>>4)*8192+c*16+(r&15);
float v=(float)p2[idx]; sum+=v; sq+=v*v;
}}
partial_sum[tid]=sum; partial_sq[tid]=sq;
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
for (int stride=64; stride; stride>>=1) {{
if (tid<stride) {{ partial_sum[tid]+=partial_sum[tid+stride]; partial_sq[tid]+=partial_sq[tid+stride]; }}
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
}}
if (tid==0) {{ float mean=partial_sum[0]*{1/(batch*16)!r}f; p0[c]=mean;
p1[c]=partial_sq[0]*{1/(batch*16)!r}f-mean*mean+1e-12f; }}
}}'''
sink = ast.replace(arg=replace(ast.arg,name=name,estimates=Estimates(512*batch*16*2,batch*8192*2,512*128*8)))
info = ProgramInfo(name=name,global_size=(512,1,1),local_size=(128,1,1),globals=(0,1,2),outs=(0,1),ins=(2,))
src:tuple[UOp, ...] = (sink,UOp(Ops.LINEAR),UOp(Ops.SOURCE,arg=source))
if compile_binary: src += (UOp(Ops.BINARY,arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM,src=src,arg=info)
def _match_bn_var_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE),None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
batch, sy, sx = tuple(int(r.vmax)+1 for r in reduce.src[1:])
signature = ((0,dtypes.float,512),(1,dtypes.float,512),(2,dtypes.half,batch*8192),(3,dtypes.float,512),
(4,dtypes.float,512),(5,dtypes.float,batch*8192))
return batch if batch in (1024,1280,1536) and (sy,sx) == (4,4) and tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) == signature and \
Counter(u.op for u in ast.toposort()) == Counter({Ops.CONST:11,Ops.MUL:10,Ops.PARAM:6,Ops.INDEX:6,Ops.ADD:5,Ops.RANGE:4,
Ops.RECIPROCAL:1,Ops.SQRT:1,Ops.CAST:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}) else None
def _match_bn_sum_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE),None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
batch, sy, sx = tuple(int(r.vmax)+1 for r in reduce.src[1:])
return batch if batch in (1024,1280,1536) and (sy,sx) == (4,4) and tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) == \
((0,dtypes.float,512),(1,dtypes.float,batch*8192),(2,dtypes.float,512)) and \
Counter(u.op for u in ast.toposort()) == Counter({Ops.CONST:10,Ops.MUL:5,Ops.ADD:5,Ops.RANGE:4,Ops.PARAM:3,Ops.INDEX:3,
Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}) else None
def _match_bn_dmean_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE),None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
batch, sy, sx = tuple(int(r.vmax)+1 for r in reduce.src[1:])
if batch not in (1024,1280,1536) or (sy,sx) != (4,4) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,512),(1,dtypes.float,512),(2,dtypes.float,512),(3,dtypes.float,512),
(4,dtypes.float,512),(5,dtypes.float,batch*8192)): return None
expected = {Ops.CONST:12,Ops.MUL:10,Ops.PARAM:6,Ops.INDEX:6,Ops.ADD:5,Ops.RANGE:4,Ops.RECIPROCAL:1,
Ops.SQRT:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
return batch if Counter(u.op for u in ast.toposort()) == Counter(expected) else None
def bn_grad_512_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if not isinstance(ast.tag,DualBNGradDMean512Match) or renderer.target.device != "AMD" or \
not renderer.target.arch.startswith("gfx11"): return None
batch = ast.tag.batch
name = f"channel_bn_grad_{batch}_512_4_dmean_{ast.key.hex()[:8]}"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(128,128))) {name}(
float* p0, float* p1, float* p2, float* p3, half* p4, float* p5, float* p6, float* p7, float* p8) {{
__attribute__((shared, aligned(32))) float partial_cov[128], partial_sum[128];
int c=__builtin_amdgcn_workgroup_id_x(), tid=__builtin_amdgcn_workitem_id_x();
float cov=0.0f, sum=0.0f, mean=p5[c], weight=p6[c];
for (int q=0; q<{batch//8}; q++) {{
int r=tid+q*128, idx=(r>>4)*8192+c*16+(r&15); float grad=p7[idx];
cov+=((float)p4[idx]-mean)*weight*grad; sum+=grad;
}}
partial_cov[tid]=cov; partial_sum[tid]=sum;
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
for (int stride=64; stride; stride>>=1) {{
if (tid<stride) {{ partial_cov[tid]+=partial_cov[tid+stride]; partial_sum[tid]+=partial_sum[tid+stride]; }}
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
}}
if (tid==0) {{ float inv=1.0f/p3[c], vg=inv*__builtin_elementwise_sqrt(inv)*partial_cov[0]*-0.5f; p0[c]=vg;
p1[c]=partial_sum[0]+0.018447889655172415f*p8[c];
p2[c]=(vg*mean*-2.0f-weight*__builtin_elementwise_sqrt(inv)*partial_sum[0])*{1/(batch*16)!r}f; }}
}}'''
sink = ast.replace(arg=replace(ast.arg,name=name,estimates=Estimates(512*batch*16*5,batch*8192*6,512*128*8)))
info = ProgramInfo(name=name,global_size=(512,1,1),local_size=(128,1,1),globals=tuple(range(9)),outs=(0,1,2),ins=tuple(range(3,9)))
src:tuple[UOp, ...] = (sink,UOp(Ops.LINEAR),UOp(Ops.SOURCE,arg=source))
if compile_binary: src += (UOp(Ops.BINARY,arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM,src=src,arg=info)
@dataclass(frozen=True)
class MaxPoolMatch:
params: tuple[UOp, ...]
batch: int
channels: int
spatial: int
def _match_activation_var_grad(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 2 or end.src[0].op is not Ops.STORE: return None
store, channel = end.src
if channel.op is not Ops.RANGE or channel.arg[1] is not AxisType.LOOP or int(channel.vmax)+1 != 512: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
signature = tuple((u.arg.slot, u.dtype, u.max_numel()) for u in params)
if len(params) != 7 or params[2].max_numel() % 8192: return None
batch = params[2].max_numel()//8192
if batch not in (1024,1280,1536) or signature != ((0,dtypes.float,512), (1,dtypes.float,512), (2,dtypes.half,batch*8192),
(3,dtypes.float,512), (4,dtypes.float,512), (5,dtypes.float,batch*8192), (6,dtypes.half,batch*8192)): return None
expected_ops = {Ops.MUL:17, Ops.CONST:14, Ops.ADD:10, Ops.PARAM:7, Ops.INDEX:7, Ops.RANGE:4, Ops.CAST:3,
Ops.RECIPROCAL:2, Ops.SQRT:1, Ops.EXP2:1, Ops.REDUCE:1, Ops.STORE:1, Ops.END:1, Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE), None)
if reduce is None or reduce.arg != (Ops.ADD, 0) or len(reduce.src) != 4 or \
tuple(int(x.vmax)+1 for x in reduce.src[1:]) != (batch,4,4): return None
return batch if store.src[0].src[0] is params[0] and ssimplify(store.src[0].src[1].get_idx()) is channel else None
def _match_activation_elementwise_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
if len(params) != 5 or params[0].max_numel() % 8192: return None
batch = params[0].max_numel()//8192
if batch not in (1024,1280,1536) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,batch*8192),(1,dtypes.float,512),(2,dtypes.float,512),(3,dtypes.float,batch*8192),(4,dtypes.half,batch*8192)): return None
expected_ops = {Ops.MUL:13,Ops.CONST:12,Ops.ADD:9,Ops.PARAM:5,Ops.INDEX:5,Ops.RANGE:4,Ops.RECIPROCAL:2,
Ops.CAST:2,Ops.SQRT:1,Ops.EXP2:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
return batch if Counter(u.op for u in ast.toposort()) == Counter(expected_ops) else None
def _match_activation_sum_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
if len(params) != 4 or params[1].max_numel() % 8192: return None
batch = params[1].max_numel()//8192
if batch not in (1024,1280,1536) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,512),(1,dtypes.float,batch*8192),(2,dtypes.half,batch*8192),(3,dtypes.float,512)): return None
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE),None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4 or \
tuple(int(r.vmax)+1 for r in reduce.src[1:]) != (batch,4,4): return None
expected = {Ops.CONST:13,Ops.MUL:12,Ops.ADD:10,Ops.PARAM:4,Ops.RANGE:4,Ops.INDEX:4,Ops.CAST:2,
Ops.EXP2:1,Ops.RECIPROCAL:1,Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
return batch if Counter(u.op for u in ast.toposort()) == Counter(expected) else None
def _match_activation_dmean_512(ast:UOp, device:str, arch:str) -> int|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
if len(params) != 4 or params[3].max_numel() % 8192: return None
batch = params[3].max_numel()//8192
if batch not in (1024,1280,1536) or tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,512),(1,dtypes.float,512),(2,dtypes.float,512),(3,dtypes.float,batch*8192)): return None
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE),None)
if reduce is None or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4 or \
tuple(int(r.vmax)+1 for r in reduce.src[1:]) != (batch,4,4): return None
expected = {Ops.CONST:12,Ops.MUL:8,Ops.ADD:5,Ops.PARAM:4,Ops.RANGE:4,Ops.INDEX:4,
Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
return batch if Counter(u.op for u in ast.toposort()) == Counter(expected) else None
def activation_var_grad_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if not isinstance(ast.tag,ActivationVarGradElementwiseSumDMeanMatch) or \
(batch:=_match_activation_var_grad(ast,renderer.target.device,renderer.target.arch)) is None: return None
name = f"direct_activation_var_grad_{batch}_512_4_elementwise_sum_dmean"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(128,128))) {name}(
float* p0, float* p1, float* p2, float* p3, float* p4, half* p5, float* p6, float* p7,
float* p8, half* p9, float* p10) {{
__attribute__((shared, aligned(32))) float partial[128], partial_sum[128], partial_elem[128];
int c=__builtin_amdgcn_workgroup_id_x(), tid=__builtin_amdgcn_workitem_id_x();
float mean=p6[c], norm=p7[c]*__builtin_elementwise_sqrt(1.0f/p4[c]), acc=0.0f, sum=0.0f, elem_sum=0.0f;
for (int q=0; q<{batch//8}; q++) {{
int r=tid+q*128, idx=(r>>4)*8192+c*16+(r&15);
half z=(half)p8[idx], sig=(half)1.0/((half)1.0+__builtin_elementwise_exp2(z*(half)-2.4554669595930156));
half grad=sig*p9[idx]+(half)1.702*z*p9[idx]*sig*((half)1.0-sig);
acc+=((float)p5[idx]-mean)*(float)grad;
float elem=norm*(float)grad; p1[idx]=elem; sum+=(float)grad; elem_sum+=elem;
}}
partial[tid]=acc; partial_sum[tid]=sum; partial_elem[tid]=elem_sum;
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
for (int stride=64; stride; stride>>=1) {{
if (tid<stride) {{ partial[tid]+=partial[tid+stride]; partial_sum[tid]+=partial_sum[tid+stride];
partial_elem[tid]+=partial_elem[tid+stride]; }}
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
}}
if (tid==0) {{ float inv=1.0f/p4[c]; float vg=inv*__builtin_elementwise_sqrt(inv)*partial[0]*p7[c]*-0.5f; p0[c]=vg;
p2[c]=partial_sum[0]+0.018447889655172415f*p10[c];
p3[c]=(vg*p6[c]*-2.0f-partial_elem[0])*{1/(batch*16)!r}f; }}
}}'''
sink = ast.replace(arg=replace(ast.arg,name=name,estimates=Estimates(512*batch*16*20,512*batch*16*12,512*128*4)))
info = ProgramInfo(name=name,global_size=(512,1,1),local_size=(128,1,1),globals=tuple(range(11)),
outs=(0,1,2,3),ins=tuple(range(4,11)))
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
@dataclass(frozen=True)
class MaxPoolBackwardMatch:
params: tuple[UOp, ...]
batch: int
channels: int
spatial: int
def _match_maxpool_backward(ast:UOp, device:str, arch:str) -> MaxPoolBackwardMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1 or not isinstance(ast.arg, KernelInfo): return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 3 or end.src[0].op is not Ops.STORE: return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
if any(not u.src or u.src[0].op is not Ops.CONST or not isinstance(u.src[0].arg, int) for u in params): return None
ranges = end.src[1:]
if any(r.op is not Ops.RANGE or r.arg[1] is not AxisType.LOOP for r in ranges): return None
position_size, channels = tuple(int(r.vmax)+1 for r in ranges)
spatial = {64:32, 256:16, 512:8}.get(channels)
if spatial is None or position_size % (spatial*spatial) or end.src[0].src[0].src[0].arg.slot != 0: return None
batch = position_size//(spatial*spatial)
if batch not in (1024, 1280, 1536): return None
output_size, pooled_size = batch*channels*spatial*spatial, batch*channels*(spatial//2)**2
signature = tuple((u.arg.slot, u.dtype, int(u.src[0].arg)) for u in params)
expected_signature = ((0, dtypes.half, output_size), (1, dtypes.half, output_size),
(2, dtypes.half, pooled_size), (3, dtypes.half, pooled_size), (4, dtypes.half, pooled_size))
if signature != expected_signature: return None
expected_ops = {Ops.CONST:{32:19, 16:18, 8:19}[spatial], Ops.PARAM:5, Ops.RANGE:4, Ops.MUL:14, Ops.ADD:12,
Ops.INDEX:5, Ops.FLOORDIV:6, Ops.FLOORMOD:6, Ops.CMPLT:6, Ops.AND:10, Ops.WHERE:4,
Ops.CMPNE:2, Ops.CAST:3, Ops.RECIPROCAL:1, Ops.REDUCE:1, Ops.STORE:1, Ops.END:1, Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
reduce = next((u for u in ast.toposort() if u.op is Ops.REDUCE), None)
if reduce is None or reduce.arg != (Ops.ADD, 0) or tuple(int(r.vmax)+1 for r in reduce.src[1:]) != (3, 3): return None
return MaxPoolBackwardMatch(params, batch, channels, spatial)
def maxpool_backward_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if (m:=_match_maxpool_backward(ast, renderer.target.device, renderer.target.arch)) is None: return None
name = f"direct_maxpool_backward_{m.channels}_{m.spatial}"
pooled_spatial, pooled_size, block = m.spatial//2, m.batch*m.channels*(m.spatial//2)**2, min(m.spatial, 16)
declarations = "half* p0, half* p1, half* p2, half* p3, half* p4"
source = f'''#define half _Float16
typedef half half2 __attribute__((ext_vector_type(2)));
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size({block//2*16},{block//2*16}))) {name}(
{declarations}) {{
__attribute__((shared, aligned(32))) half tile[{2*block*16}];
int gx=__builtin_amdgcn_workgroup_id_x(), b=__builtin_amdgcn_workgroup_id_z();
int px=__builtin_amdgcn_workitem_id_x(), lc=__builtin_amdgcn_workitem_id_y(), cg=__builtin_amdgcn_workgroup_id_y();
int py=gx/{m.spatial//block}, xb=(gx%{m.spatial//block})*{block}, y=py*2, x=xb+px*2, c=cg*16+lc;
int ii=(b*{m.channels}+c)*{m.spatial*m.spatial}+y*{m.spatial}+x;
int pi=(b*{m.channels}+c)*{pooled_spatial*pooled_spatial}+py*{pooled_spatial}+xb/2+px;
half mx=p2[pi];
half scale=(half)1.0/p3[pi]*p4[pi];
half2 row0=*((half2*)(p1+ii)), row1=*((half2*)(p1+ii+{m.spatial}));
tile[(px*2)*16+lc]=(half)(row0.x==mx)*scale;
tile[(px*2+1)*16+lc]=(half)(row0.y==mx)*scale;
tile[({block}+px*2)*16+lc]=(half)(row1.x==mx)*scale;
tile[({block}+px*2+1)*16+lc]=(half)(row1.y==mx)*scale;
__builtin_amdgcn_fence(__ATOMIC_RELEASE,"workgroup"); __builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE,"workgroup");
int tid=lc*{block//2}+px;
#pragma unroll
for (int q=0; q<4; q++) {{
int e=tid+q*{block//2*16}, pos=e/16, oc=cg*16+e%16;
int oy=y+pos/{block}, ox=xb+pos%{block};
p0[((b*{m.spatial}+oy)*{m.spatial}+ox)*{m.channels}+oc]=tile[e];
}}
}}'''
output_size = pooled_size*4
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(output_size*2, pooled_size*22, 0)))
slots = tuple(u.arg.slot for u in m.params)
info = ProgramInfo(name=name, global_size=(m.spatial//block*pooled_spatial, m.channels//16, m.batch), local_size=(block//2, 16, 1),
globals=slots, outs=(0,), ins=slots[1:])
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
def _match_maxpool(ast:UOp, device:str, arch:str) -> MaxPoolMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 2 or not isinstance(ast.arg, KernelInfo): return None
if any(end.op is not Ops.END or len(end.src) != 5 or end.src[0].op is not Ops.STORE for end in ast.src): return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
if any(not u.src or u.src[0].op is not Ops.CONST or not isinstance(u.src[0].arg, int) for u in params): return None
loop_ranges = ast.src[0].src[1:]
if any(r.op is not Ops.RANGE or r.arg[1] is not AxisType.LOOP for r in loop_ranges): return None
batch, channels, sy, sx = tuple(int(r.vmax)+1 for r in loop_ranges)
if batch not in (1024, 1280, 1536) or sy != sx or (channels,sy) not in ((64,16),(256,8),(512,4)) or ast.src[1].src[1:] != loop_ranges: return None
spatial = sy
out_size = batch*channels*spatial*spatial
signature = tuple((u.arg.slot, u.dtype, int(u.src[0].arg)) for u in params)
if signature != ((0,dtypes.half,out_size),(1,dtypes.half,out_size*4),(2,dtypes.half,out_size)): return None
if tuple(end.src[0].src[0].src[0].arg.slot for end in ast.src) != (0, 2): return None
expected_ops = {Ops.CONST:13 if spatial == 8 else 14, Ops.MUL:9, Ops.ADD:9, Ops.RANGE:6, Ops.PARAM:3,
Ops.INDEX:3, Ops.REDUCE:2, Ops.STORE:2, Ops.END:2, Ops.CMPNE:2, Ops.CAST:1, Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
return MaxPoolMatch(params, batch, channels, spatial)
def maxpool_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if (m:=_match_maxpool(ast, renderer.target.device, renderer.target.arch)) is None: return None
name = f"direct_maxpool_{m.channels}_{m.spatial}"
out_size, in_size = m.batch*m.channels*m.spatial*m.spatial, m.batch*m.channels*m.spatial*m.spatial*4
source = f'''#define half _Float16
typedef half half2 __attribute__((ext_vector_type(2)));
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(1,128))) {name}(half* p0, half* p1, half* p2) {{
int group=__builtin_amdgcn_workgroup_id_x(), lx=__builtin_amdgcn_workitem_id_x();
int ly=__builtin_amdgcn_workitem_id_y(), ii=group*512+lx*{m.spatial*4}+ly*2;
half2 row0=*((half2*)(p1+ii)), row1=*((half2*)(p1+ii+{m.spatial*2}));
int oi=group*128+lx*{m.spatial}+ly;
half mx01=row0.x<row0.y?row0.y:row0.x, mx012=mx01<row1.x?row1.x:mx01;
half mx=mx012<row1.y?row1.y:mx012;
p0[oi]=mx;
p2[oi]=(half)(row0.x==mx)+(half)(row0.y==mx)+(half)(row1.x==mx)+(half)(row1.y==mx);
}}'''
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(out_size*3, in_size*2+out_size*4, 0)))
slots = tuple(u.arg.slot for u in m.params)
info = ProgramInfo(name=name, global_size=(out_size//128, 1, 1), local_size=(128//m.spatial, m.spatial, 1),
globals=slots, outs=(0, 2), ins=(1,))
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
def _match_im2col(ast:UOp, device:str, arch:str) -> Im2ColMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1 or not isinstance(ast.arg, KernelInfo): return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 3 or end.src[0].op is not Ops.STORE: return None
store, position, patch = end.src
if any(r.op is not Ops.RANGE or r.arg[1] is not AxisType.LOOP for r in (position, patch)): return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
if any(not u.src or u.src[0].op is not Ops.CONST or not isinstance(u.src[0].arg, int) for u in params): return None
patch_size, position_size = int(patch.vmax)+1, int(position.vmax)+1
if patch_size % 9: return None
channels = patch_size//9
spatial = {32:32, 64:16, 256:8, 512:4}.get(channels)
if spatial is None or position_size % (spatial*spatial): return None
batch = position_size//(spatial*spatial)
if batch not in (1024, 1280, 1536): return None
input_size = batch*channels*spatial*spatial
signature = tuple((u.arg.slot, u.dtype, int(u.src[0].arg)) for u in params)
if signature != ((0,dtypes.half,input_size*9),(1,dtypes.half,input_size)): return None
if (int(position.vmax)+1, int(patch.vmax)+1) != (batch*spatial*spatial, channels*9): return None
if ssimplify(store.src[0].src[1].get_idx()) is not ssimplify(position*channels*9+patch): return None
loads = [u for u in store.src[1].toposort() if u.op is Ops.INDEX and u.src[0].op is Ops.PARAM and u.src[0].arg.slot == 1]
expected_idx = ssimplify(position//(spatial*spatial)*(channels*spatial*spatial) + patch//9*(spatial*spatial) +
(position//spatial%spatial+patch//3%3)*spatial + (position%spatial+patch%3)-spatial-1)
if len(loads) != 1 or ssimplify(loads[0].src[1].get_idx()) is not expected_idx: return None
expected_ops = {Ops.CONST:15 if spatial != 8 else 14, Ops.ADD:7, Ops.MUL:4, Ops.FLOORDIV:4, Ops.FLOORMOD:4,
Ops.CMPLT:4, Ops.AND:3, Ops.PARAM:2, Ops.RANGE:2, Ops.INDEX:2, Ops.CMPNE:2, Ops.WHERE:2,
Ops.STORE:1, Ops.END:1, Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
return Im2ColMatch(params, batch, channels, spatial)
def im2col_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if (m:=_match_im2col(ast, renderer.target.device, renderer.target.arch)) is None: return None
name = f"direct_im2col_{m.channels}_{m.spatial}"
xblock = min(8, m.spatial)
source = f'''#define half _Float16
typedef half half8 __attribute__((ext_vector_type(8)));
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size({xblock*16},{xblock*16}))) {name}(half* p0, half* p1) {{
int gx=__builtin_amdgcn_workgroup_id_x(), y=__builtin_amdgcn_workgroup_id_y();
int b=__builtin_amdgcn_workgroup_id_z(), lx=__builtin_amdgcn_workitem_id_x();
int lc=__builtin_amdgcn_workitem_id_y(), x=(gx%{m.spatial//xblock})*{xblock}+lx;
int c=(gx/{m.spatial//xblock})*16+lc;
int ii=((b*{m.channels}+c)*{m.spatial}+y)*{m.spatial}+x;
int base=((b*{m.spatial*m.spatial}+y*{m.spatial}+x)*{m.channels}+c)*9;
bool top=y>0, bottom=y<{m.spatial-1}, left=x>0, right=x<{m.spatial-1};
half v0=(top&&left)?p1[ii-{m.spatial+1}]:(half)0, v1=top?p1[ii-{m.spatial}]:(half)0;
half v2=(top&&right)?p1[ii-{m.spatial-1}]:(half)0, v3=left?p1[ii-1]:(half)0, v4=p1[ii];
half v5=right?p1[ii+1]:(half)0, v6=(bottom&&left)?p1[ii+{m.spatial-1}]:(half)0;
half v7=bottom?p1[ii+{m.spatial}]:(half)0, v8=(bottom&&right)?p1[ii+{m.spatial+1}]:(half)0;
*((half8*)(p0+base))=(half8){{v0,v1,v2,v3,v4,v5,v6,v7}}; p0[base+8]=v8;
}}'''
total = m.batch*m.channels*m.spatial*m.spatial
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(0, total*20, 0)))
slots = tuple(u.arg.slot for u in m.params)
info = ProgramInfo(name=name, global_size=(m.spatial//xblock*m.channels//16,m.spatial,m.batch), local_size=(xblock,16,1),
globals=slots, outs=(0,), ins=(1,))
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
def _match_col2im(ast:UOp, device:str, arch:str) -> Col2ImMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1 or not isinstance(ast.arg, KernelInfo): return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 5 or end.src[0].op is not Ops.STORE: return None
store, batch, channel, y, x = end.src
if any(r.op is not Ops.RANGE or r.arg[1] is not AxisType.LOOP for r in (batch, channel, y, x)): return None
batch_size, channels, sy, sx = tuple(int(r.vmax)+1 for r in (batch, channel, y, x))
if batch_size not in (1024,1280,1536) or (channels, sy, sx) not in ((64, 16, 16), (256, 8, 8), (512, 4, 4)): return None
reduces = [u for u in store.src[1].toposort() if u.op is Ops.REDUCE]
if len(reduces) != 1 or reduces[0].arg != (Ops.ADD, 0) or \
tuple(int(r.vmax)+1 for r in reduces[0].src[1:]) != (4, 4): return None
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
signature = tuple((u.arg.slot, u.dtype, int(u.src[0].arg)) for u in params)
output_size, patch_size = batch_size*channels*sy*sx, batch_size*channels*sy*sx*9
plain = ((0, dtypes.half, output_size), (1, dtypes.half, patch_size))
fused = ((0, dtypes.float, output_size), (1, dtypes.float, output_size),
(2, dtypes.half, output_size), (3, dtypes.half, patch_size))
if signature not in (plain, fused): return None
expected_ops = ({Ops.CONST:20, Ops.ADD:13, Ops.MUL:11, Ops.AND:8, Ops.RANGE:6, Ops.CMPLT:4, Ops.PARAM:2,
Ops.INDEX:2, Ops.FLOORMOD:2, Ops.FLOORDIV:2, Ops.WHERE:2, Ops.CAST:2, Ops.REDUCE:1,
Ops.STORE:1, Ops.END:1, Ops.SINK:1} if signature == plain else
{Ops.CONST:22 if sy == 4 else 23, Ops.ADD:19, Ops.MUL:18, Ops.AND:8, Ops.RANGE:6, Ops.PARAM:4, Ops.INDEX:4,
Ops.CAST:4, Ops.CMPLT:4, Ops.FLOORMOD:2, Ops.FLOORDIV:2, Ops.WHERE:2, Ops.EXP2:1,
Ops.RECIPROCAL:1, Ops.REDUCE:1, Ops.STORE:1, Ops.END:1, Ops.SINK:1})
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
return Col2ImMatch(params, signature == fused, batch_size, channels, sy)
def col2im_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if (m:=_match_col2im(ast, renderer.target.device, renderer.target.arch)) is None: return None
name = f"direct_col2im_{m.channels}_{m.spatial}{'_activation' if m.fused_activation else ''}"
declarations = ("float* p0, float* p1, half* p2, half* p3" if m.fused_activation else "half* p0, half* p1")
patch_slot = 3 if m.fused_activation else 1
if m.fused_activation:
output = """half grad=p2[oi]+(half)acc, z=(half)p1[oi];
half sig=(half)1.0/((half)1.0+__builtin_elementwise_exp2(z*(half)-2.4554669595930156));
p0[oi]=(float)(sig*grad+(half)1.702*z*grad*sig*((half)1.0-sig));"""
else: output = "p0[oi]=(half)acc;"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(256,256))) {name}({declarations}) {{
int tile=__builtin_amdgcn_workgroup_id_x(), cg=__builtin_amdgcn_workgroup_id_y();
int b=__builtin_amdgcn_workgroup_id_z(), c=cg*16+__builtin_amdgcn_workitem_id_x();
int y=(tile%{m.spatial//4})*4+__builtin_amdgcn_workitem_id_y();
int x=(tile/{m.spatial//4})*4+__builtin_amdgcn_workitem_id_z();
float acc=0.0f;
#pragma unroll
for (int ky=0; ky<3; ky++) {{
int oy=y+1-ky;
#pragma unroll
for (int kx=0; kx<3; kx++) {{
int ox=x+1-kx;
if (oy>=0 && oy<{m.spatial} && ox>=0 && ox<{m.spatial})
acc+=(float)p{patch_slot}[((((b*{m.spatial}+oy)*{m.spatial}+ox)*{m.channels}+c)*9+ky*3+kx)];
}}
}}
int oi=((b*{m.channels}+c)*{m.spatial}+y)*{m.spatial}+x;
{output}
}}'''
output_size = m.batch*m.channels*m.spatial*m.spatial
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(output_size*18, output_size*20, 0)))
slots = tuple(u.arg.slot for u in m.params)
info = ProgramInfo(name=name, global_size=((m.spatial//4)**2, m.channels//16, m.batch), local_size=(16, 4, 4),
globals=slots, outs=(0,), ins=slots[1:])
src:tuple[UOp, ...] = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
def _match_channel_reduce(ast:UOp, device:str, arch:str) -> ChannelReduceMatch|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1 or not isinstance(ast.arg, KernelInfo): return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 3 or end.src[0].op is not Ops.STORE: return None
store, channel, feature = end.src
if any(x.op is not Ops.RANGE or x.arg[1] is not AxisType.LOOP for x in (channel, feature)): return None
channels, features = int(channel.vmax)+1, int(feature.vmax)+1
if channels not in (64, 256) or features != 256 or store.src[0].op is not Ops.INDEX: return None
reduce = store.src[1]
if reduce.op is not Ops.REDUCE or reduce.arg != (Ops.ADD, 0) or len(reduce.src) != 4: return None
batch, ry, rx = reduce.src[1:]
if any(x.op is not Ops.RANGE or x.arg[1] is not AxisType.REDUCE for x in (batch, ry, rx)): return None
spatial = int(ry.vmax)+1
groups = int(batch.vmax)+1
if groups not in (4,5,6) or int(rx.vmax)+1 != spatial or channels*spatial*spatial != 16384: return None
out_idx = ssimplify(channel*features+feature)
data_idx = ssimplify((feature*groups+batch)*16384+channel*spatial*spatial+ry*spatial+rx)
if ssimplify(store.src[0].src[1].get_idx()) is not out_idx: return None
params = tuple(sorted((x for x in ast.toposort() if x.op is Ops.PARAM), key=lambda x:x.arg.slot))
total = features*groups*16384
signature = tuple((x.arg.slot, x.dtype, int(x.src[0].arg)) for x in params)
signatures = {
"activation": ((0, dtypes.float, channels*features), (1, dtypes.half, total), (2, dtypes.float, channels),
(3, dtypes.float, channels), (4, dtypes.float, total), (5, dtypes.half, total)),
"centered": ((0, dtypes.float, channels*features), (1, dtypes.half, total), (2, dtypes.float, channels),
(3, dtypes.float, channels), (4, dtypes.float, total)),
"scale": ((0, dtypes.float, channels*features), (1, dtypes.float, channels), (2, dtypes.float, channels),
(3, dtypes.float, total)),
}
kind = next((k for k,v in signatures.items() if signature == v), None)
if kind is None: return None
indexes = [(x.src[0].arg.slot, ssimplify(x.src[1].get_idx())) for x in reduce.src[0].toposort() if x.op is Ops.INDEX]
expected_indexes = {
"activation": ((1, data_idx), (2, channel), (3, channel), (4, data_idx), (5, data_idx)),
"centered": ((1, data_idx), (2, channel), (3, channel), (4, data_idx)),
"scale": ((1, channel), (2, channel), (3, data_idx)),
}[kind]
if len(indexes) != len(expected_indexes) or any(slot != eslot or idx is not eidx for (slot,idx),(eslot,eidx) in zip(indexes, expected_indexes)):
return None
op_signature = Counter(x.op for x in reduce.src[0].toposort())
expected_ops = {
"activation": {Ops.CONST:13, Ops.PARAM:5, Ops.RANGE:5, Ops.MUL:15, Ops.ADD:11, Ops.INDEX:5, Ops.CAST:3,
Ops.EXP2:1, Ops.RECIPROCAL:1},
"centered": {Ops.CONST:10, Ops.PARAM:4, Ops.RANGE:5, Ops.MUL:8, Ops.ADD:6, Ops.INDEX:4, Ops.CAST:1},
"scale": {Ops.CONST:10, Ops.PARAM:3, Ops.RANGE:5, Ops.INDEX:3, Ops.RECIPROCAL:1, Ops.SQRT:1, Ops.MUL:8, Ops.ADD:5},
}[kind]
if op_signature != Counter(expected_ops): return None
return ChannelReduceMatch(kind, params, groups, channels, spatial)
def _match_activation_sum(ast:UOp, device:str, arch:str) -> tuple[int, int, int]|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 3 or end.src[0].op is not Ops.STORE: return None
store, channel, feature = end.src
if any(x.op is not Ops.RANGE or x.arg[1] is not AxisType.LOOP for x in (channel,feature)): return None
channels = int(channel.vmax)+1
if channels not in (64,256) or int(feature.vmax)+1 != 256: return None
spatial = 16 if channels == 64 else 8
reduce = store.src[1]
if reduce.op is not Ops.REDUCE or reduce.arg != (Ops.ADD,0) or len(reduce.src) != 4: return None
groups, rsy, rsx = tuple(int(x.vmax)+1 for x in reduce.src[1:])
if groups not in (4,5,6) or (rsy,rsx) != (spatial,spatial): return None
total = 256*groups*16384
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM), key=lambda u:u.arg.slot))
if tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,channels*256), (1,dtypes.float,total), (2,dtypes.half,total)): return None
batch, y, x = reduce.src[1:]
out_idx = ssimplify(channel*256+feature)
data_idx = ssimplify((feature*groups+batch)*16384+channel*spatial*spatial+y*spatial+x)
if ssimplify(store.src[0].src[1].get_idx()) is not out_idx: return None
indexes = [(u.src[0].arg.slot,ssimplify(u.src[1].get_idx())) for u in reduce.src[0].toposort() if u.op is Ops.INDEX]
if len(indexes) != 2 or any(slot != expected or idx is not data_idx for (slot,idx),expected in zip(indexes,(1,2))): return None
expected_ops = ({Ops.MUL:15,Ops.CONST:13} if channels == 256 else {Ops.MUL:14,Ops.CONST:12}) | \
{Ops.ADD:13,Ops.RANGE:5,Ops.PARAM:3,Ops.INDEX:3,Ops.CAST:2,Ops.EXP2:1,Ops.RECIPROCAL:1,
Ops.REDUCE:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected_ops): return None
return channels, spatial, groups
def _match_activation_elementwise(ast:UOp, device:str, arch:str) -> tuple[int, int, int]|None:
if device != "AMD" or not arch.startswith("gfx11") or len(ast.src) != 1: return None
end = ast.src[0]
if end.op is not Ops.END or len(end.src) != 5 or end.src[0].op is not Ops.STORE: return None
store, batch, channel, y, x = end.src
if any(r.op is not Ops.RANGE or r.arg[1] is not AxisType.LOOP for r in (batch,channel,y,x)): return None
batch_size, channels, sy, sx = tuple(int(r.vmax)+1 for r in (batch,channel,y,x))
if batch_size not in (1024,1280,1536) or sy != sx or (channels,sy) not in ((64,16),(256,8)): return None
total = batch_size*channels*sy*sx
params = tuple(sorted((u for u in ast.toposort() if u.op is Ops.PARAM),key=lambda u:u.arg.slot))
if tuple((u.arg.slot,u.dtype,u.max_numel()) for u in params) != \
((0,dtypes.float,total),(1,dtypes.float,channels),(2,dtypes.float,channels),
(3,dtypes.float,total),(4,dtypes.half,total)): return None
expected = {Ops.MUL:13,Ops.CONST:12,Ops.ADD:9,Ops.PARAM:5,Ops.INDEX:5,Ops.RANGE:4,Ops.RECIPROCAL:2,
Ops.CAST:2,Ops.SQRT:1,Ops.EXP2:1,Ops.STORE:1,Ops.END:1,Ops.SINK:1}
if Counter(u.op for u in ast.toposort()) != Counter(expected): return None
flat = ssimplify(batch*(channels*sy*sx)+channel*(sy*sx)+y*sy+x)
return (channels,sy,batch_size//256) if store.src[0].op is Ops.INDEX and ssimplify(store.src[0].src[1].get_idx()) is flat else None
def channel_reduce_program(ast:UOp, renderer:Renderer, compile_binary:bool) -> UOp|None:
if isinstance(am:=ast.tag,DualActivationReduceElementwiseMatch) and renderer.target.device == "AMD" and \
renderer.target.arch.startswith("gfx11"):
name = f"channel_reduce_activation_sum_{am.groups}_{am.channels}_{am.spatial}_elementwise"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(1024,1024))) {name}(
float* p0, float* p1, float* p2, half* p3, float* p4, float* p5, float* p6, float* p7, half* p8) {{
int tid=__builtin_amdgcn_workitem_id_x(), lane=tid&31, wave=tid>>5;
int oi=__builtin_amdgcn_workgroup_id_x()*32+wave, channel=oi/256, feature=oi%256;
float centered=0.0f, plain=0.0f, mean=p4[channel];
for (int r=lane; r<{am.groups*am.spatial*am.spatial}; r+=32) {{
int base=(feature*{am.groups}+r/{am.spatial*am.spatial})*16384+channel*{am.spatial*am.spatial}+r%{am.spatial*am.spatial};
half z=(half)p7[base], grad=p8[base];
half sig=(half)1.0/((half)1.0+__builtin_elementwise_exp2(z*(half)-2.4554669595930156));
float ag=(float)(sig*grad+(half)1.702*z*grad*sig*((half)1.0-sig));
centered+=((float)p3[base]-mean)*ag;
plain+=ag;
p2[base]=p5[channel]*(__builtin_elementwise_sqrt(1.0f/p6[channel])*ag);
}}
unsigned centered_bits=__builtin_bit_cast(unsigned,centered), plain_bits=__builtin_bit_cast(unsigned,plain);
if (lane==0) {{
#pragma unroll
for (int i=1; i<32; i++) {{
centered+=__builtin_bit_cast(float,__builtin_amdgcn_readlane(centered_bits,i));
plain+=__builtin_bit_cast(float,__builtin_amdgcn_readlane(plain_bits,i));
}}
p0[oi]=centered*p5[channel];
p1[oi]=plain;
}}
}}'''
elements, reduce_size = am.channels*256, am.groups*am.spatial*am.spatial
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(elements*reduce_size*18, elements*reduce_size*8, 0)))
slots = tuple(range(9))
info = ProgramInfo(name=name, global_size=(elements//32,1,1), local_size=(1024,1,1), globals=slots,outs=(0,1,2),ins=slots[3:])
src:tuple[UOp, ...] = (sink,UOp(Ops.LINEAR),UOp(Ops.SOURCE,arg=source))
if compile_binary: src += (UOp(Ops.BINARY,arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM,src=src,arg=info)
if isinstance(dm:=ast.tag, DualChannelReduceMatch) and renderer.target.device == "AMD" and renderer.target.arch.startswith("gfx11"):
name = f"channel_reduce_centered_scale_{dm.groups}_{dm.channels}_{dm.spatial}"
source = f'''#define half _Float16
extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(1024,1024))) {name}(
float* p0, float* p1, half* p2, float* p3, float* p4, float* p5, float* p6, float* p7) {{
int tid=__builtin_amdgcn_workitem_id_x(), lane=tid&31, wave=tid>>5;
int oi=__builtin_amdgcn_workgroup_id_x()*32+wave, channel=oi/256, feature=oi%256;
float centered=0.0f, scale=0.0f, mean=p3[channel];
for (int r=lane; r<{dm.groups*dm.spatial*dm.spatial}; r+=32) {{
int base=(feature*{dm.groups}+r/{dm.spatial*dm.spatial})*16384+channel*{dm.spatial*dm.spatial}+r%{dm.spatial*dm.spatial};
float grad=p7[base];
centered+=((float)p2[base]-mean)*grad;
scale+=grad;
}}
unsigned centered_bits=__builtin_bit_cast(unsigned, centered), scale_bits=__builtin_bit_cast(unsigned, scale);
if (lane==0) {{
#pragma unroll
for (int i=1; i<32; i++) {{
centered+=__builtin_bit_cast(float, __builtin_amdgcn_readlane(centered_bits, i));
scale+=__builtin_bit_cast(float, __builtin_amdgcn_readlane(scale_bits, i));
}}
p0[oi]=centered*p4[channel];
p1[oi]=-(scale*p5[channel]*__builtin_elementwise_sqrt(1.0f/p6[channel]));
}}
}}'''
elements, reduce_size = dm.channels*256, dm.groups*dm.spatial*dm.spatial
sink = ast.replace(arg=replace(ast.arg, name=name, estimates=Estimates(elements*reduce_size*7, elements*reduce_size*8, 0)))
info = ProgramInfo(name=name, global_size=(elements//32,1,1), local_size=(1024,1,1), globals=tuple(range(8)),
outs=(0,1), ins=(2,3,4,5,6,7))
src = (sink, UOp(Ops.LINEAR), UOp(Ops.SOURCE, arg=source))
if compile_binary: src += (UOp(Ops.BINARY, arg=renderer.compiler.compile_cached(source)),)
return UOp(Ops.PROGRAM, src=src, arg=info)
return None
+29 -18
View File
@@ -1,7 +1,7 @@
import itertools
from typing import Callable
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, graph_rewrite, _substitute, range_start, AxisType
from tinygrad.uop.symbolic import symbolic
from tinygrad.uop.ops import UOp, PatternMatcher, UPat, Ops, graph_rewrite, range_start, AxisType
from tinygrad.uop.symbolic import symbolic, symbolic_simple, commutative
from tinygrad.helpers import partition
from tinygrad.dtype import dtypes
@@ -16,25 +16,35 @@ pm_flatten_range = PatternMatcher([
(UPat((Ops.REDUCE, Ops.END), name="r"), flatten_range),
])
pm_merge_ranges = symbolic_simple+commutative+PatternMatcher([
((UPat.var("x", dtype=dtypes.index)//UPat.cvar("d"))<UPat.cvar("c"),
lambda x,d,c: x<(c.arg*d.arg) if d.arg > 0 else None),
])+pm_flatten_range
# 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 len(u.ended_ranges) < 2: return None
pairs = zip(u.ended_ranges, u.ended_ranges[1:]) if u.op is Ops.END else itertools.combinations(u.ended_ranges, 2)
candidates = [(r0, r1) for r0,r1 in pairs if r0.arg[-1] == r1.arg[-1]]
if not candidates: return None
reduce_ranges = [x.ranges for x in u.backward_slice_with_self if x.op is Ops.REDUCE]
divmod_count = count_divmod(u)
# 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)):
# check same type
if r0.arg[-1] == r1.arg[-1]:
# check if the ranges to merge are in the same reduces
if all((r0 in rngs) == (r1 in rngs) for rngs in reduce_ranges):
s0, s1 = r0.src[0], r1.src[0]
# do the merge
new_range = r0.replace(src=(s0*s1,))
nidx = graph_rewrite(u, _substitute+symbolic+pm_flatten_range, ctx={r0:new_range//s1, r1:new_range%s1},
name=f"check_merge_{r0.arg[0]}_{r1.arg[0]}")
for r0, r1 in candidates:
# An earlier accepted merge can remove either original range from u, making this substitution a no-op.
if r0 not in u.backward_slice or r1 not in u.backward_slice: continue
# check if the ranges to merge are in the same reduces
if all((r0 in rngs) == (r1 in rngs) for rngs in reduce_ranges):
s0, s1 = r0.src[0], r1.src[0]
# do the merge
new_range = r0.replace(src=(s0*s1,))
nidx = u.substitute({r0:new_range//s1, r1:new_range%s1})
nidx = graph_rewrite(nidx, pm_merge_ranges, name=f"check_merge_{r0.arg[0]}_{r1.arg[0]}")
# check if it simplifies
if count_divmod(nidx) <= count_divmod(u):
u = nidx
# check if it simplifies
if (new_divmod_count:=count_divmod(nidx)) <= divmod_count:
u, divmod_count = nidx, new_divmod_count
return u
def mark_gated(ctx, idx):
@@ -59,15 +69,16 @@ pm_simplify_ranges = PatternMatcher([
def mark_range_mod(ctx:dict[UOp, UOp|None], r:UOp, c:UOp) -> None:
if r not in ctx and r.arg[-1] is not AxisType.WARP and r.src[0].op is Ops.CONST and r.src[0].divides(c.arg) is not None: ctx[r] = c
def do_substitute(ctx:dict, x: UOp, sub_fxn:Callable[[UOp, UOp], UOp]) -> UOp|None:
def do_substitute(ctx:dict, x:UOp, sub_fxn:Callable[[UOp, UOp], UOp], simplify:bool=True) -> UOp|None:
ret = x.substitute({k:sub_fxn(k,v) for k,v in ctx.items() if v is not None})
ctx.clear()
return None if ret is x else ret.simplify()
return None if ret is x else ret.simplify() if simplify else ret
pm_split_ranges = PatternMatcher([
(UPat(Ops.RANGE, name="r")%UPat.cvar("c"), mark_range_mod),
(UPat(Ops.SINK, name="x"), lambda ctx, x: do_substitute(ctx, x,
lambda k,v: k.replace(src=(k.src[0]//v,), arg=k.arg[0:-1]+(0,k.arg[-1]))*v + k.replace(src=(v,), arg=k.arg[0:-1]+(1,k.arg[-1])))),
lambda k,v: k.replace(src=(k.src[0]//v,), arg=k.arg[0:-1]+(0,k.arg[-1]))*v + k.replace(src=(v,), arg=k.arg[0:-1]+(1,k.arg[-1])),
simplify=False)),
])
# **** reduce simplification ****
+2
View File
@@ -284,6 +284,8 @@ class Compiler:
lib = self.compile(src)
if self.cachekey is not None: diskcache_put(self.cachekey, src, lib)
return lib
def compile_cached_batch(self, srcs:list[tuple[str, str]]) -> list[tuple[str, str, bytes]]:
return [(name, src, self.compile_cached(src)) for name,src in srcs]
def disassemble(self, lib:bytes): pass
class Compiled:
+8 -5
View File
@@ -72,7 +72,8 @@ def jit_lower(linear:UOp, held_bufs:set[UOp], input_uops:list[UOp]) -> UOp:
linear = linear.substitute({u: UOp.param(i, u.dtype, u.shape, u.device) for i,u in enumerate(input_uops)}, walk=True)
linear = memory_plan_rewrite(linear, held_bufs)
linear = compile_linear(linear, beam=getenv("JITBEAM", BEAM.value), jit=True)
if JIT < 2: linear = graph_split_rewrite(linear, max_batch_size=JIT_BATCH_SIZE.value)
amd_graph = len(linear.src) > 128 and bool(input_uops) and all(isinstance(u.device, str) and u.device.split(":")[0] == "AMD" for u in input_uops)
if JIT < 2: linear = graph_split_rewrite(linear, max_batch_size=getenv("JIT_BATCH_SIZE", 0 if amd_graph else JIT_BATCH_SIZE.value))
if VIZ: graph_rewrite(linear, PatternMatcher([]), name="View graphed linear")
return linear
@@ -136,6 +137,7 @@ class GraphRunner:
def is_sym_dim(dim) -> bool: return not all(isinstance(d, (int, float)) for d in dim)
crs = [(j, self.calls[j][1].arg, self.calls[j][3]) for j in range(len(self.calls)) if self.calls[j][1].op is Ops.PROGRAM]
self.fixedvars = {v.expr:int(v.vmin) for _,p,_ in crs for v in p.vars if v.vmin == v.vmax}
self.vars = sorted({v.expr for _,p,dv in crs for v in p.vars if v.expr not in dv | p.runtimevars})
self.symbolic_dims = dedup(tuple(d) for _,p,_ in crs for d in (p.local_size, p.global_size) if d and is_sym_dim(d))
@@ -160,7 +162,7 @@ class GraphRunner:
def __call__(self, input_uops:tuple[UOp, ...], var_vals:dict[str, int], wait=False) -> float|None: raise NotImplementedError("override this")
def updated_vars(self, var_vals: dict[str, int]):
vals = [var_vals[v] for v in self.vars]
vals = [(var_vals | self.fixedvars)[v] for v in self.vars]
for j, vidxs in self.var_vals_replace.items():
for i, v in vidxs: yield j, i, vals[v]
@@ -248,18 +250,19 @@ def _prepare_jit_inputs(args, kwargs):
return input_buf_uops, var_vals, names, expected_input_info
class TinyJit(Generic[ReturnType]):
def __init__(self, fxn:Callable[..., ReturnType]|None, captured:CapturedJit|None=None, prune=False):
def __init__(self, fxn:Callable[..., ReturnType]|None, captured:CapturedJit|None=None, prune=False, warmup=True):
assert fxn or captured, "need either a function or a CapturedJit"
self.fxn = fxn
self.captured: CapturedJit|None = captured
self.cnt: int = 2 if self.fxn is None else 0
self.warmup = warmup
self.cnt: int = 2 if self.fxn is None else 0 if warmup else 1
self.prune = prune
def add_linear(self, linear:UOp, var_vals:dict[str, int]): self._linears.append(linear)
def reset(self):
assert self.fxn is not None, "can't reset without function"
self.cnt = 0
self.cnt = 0 if self.warmup else 1
self.captured = None
def __reduce__(self):
+44 -6
View File
@@ -1,11 +1,11 @@
from __future__ import annotations
from typing import cast, Iterator, Any, Sequence
import time, random, itertools, math, contextlib, weakref, array
import time, random, itertools, math, contextlib, weakref, array, re
from dataclasses import dataclass, replace, field
from tinygrad.helpers import colored, DEBUG, GlobalCounters, ansilen, all_int, prod, flatten, Context, getenv, to_tuple
from tinygrad.helpers import BEAM, size_to_str, time_to_str, VALIDATE_WITH_CPU, PROFILE, ProfilePointEvent, cpu_events
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, sym_infer, buffers, graph_rewrite, ProgramInfo
from tinygrad.device import Device, Buffer, MultiBuffer
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat, sym_infer, buffers, graph_rewrite, ProgramInfo, scoped_rewrite_cache
from tinygrad.device import Device, Buffer, MultiBuffer, Compiler
from tinygrad.renderer import Estimates
from tinygrad.codegen import to_program
from tinygrad.codegen.opt.postrange import bufs_from_ast
@@ -240,11 +240,47 @@ pm_beam = PatternMatcher([
lambda ctx,call,sink: call.replace(src=(sink.replace(arg=replace(sink.arg, beam=ctx)), *call.src[1:])) if sink.arg.beam == 0 else None),
])
def compile_call(ctx, call:UOp, ast:UOp):
dev = call.device if isinstance(call.device, str) else call.device[0]
return call.replace(src=(to_program(ast, Device[dev].renderer, compile_binary=not ctx), *call.src[1:]))
pm_compile = PatternMatcher([
(UPat(Ops.CALL, src=(UPat((Ops.SINK, Ops.PROGRAM), name="ast"),), name="call", allow_any_len=True), lambda call,ast:
call.replace(src=(to_program(ast, Device[call.device if isinstance(call.device, str) else call.device[0]].renderer), *call.src[1:]))),
(UPat(Ops.CALL, src=(UPat((Ops.SINK, Ops.PROGRAM), name="ast"),), name="call", allow_any_len=True), compile_call),
])
batch_compiled_cache: dict[tuple[Compiler, str], tuple[str, str, bytes]] = {}
def _batch_spec_key(name:str, src:str) -> str:
if f" {name}(" not in src: return f"{name}\n{src}"
src = src.replace(f" {name}(", " __batch_cached_kernel__(", 1)
return re.sub(r"\b(data\d+)_\d+\b|/\* \d+ \*/", lambda x: x[1] or "", src)
def batch_compile(linear:UOp) -> UOp:
groups:dict[Compiler, list[UOp]] = {}
replacements = {}
for call in linear.toposort():
if call.op is not Ops.CALL or call.src[0].op is not Ops.PROGRAM or len(call.src[0].src) != 3: continue
dev = call.device if isinstance(call.device, str) else call.device[0]
compiler = Device[dev].compiler
cache = call.src[0].__dict__.setdefault('_compiled_programs', {})
if (cached:=cache.get(compiler)) is not None: replacements[call.src[0]] = cached
else: groups.setdefault(compiler, []).append(call.src[0])
for compiler,prgs in groups.items():
prgs = list(dict.fromkeys(prgs))
specs = [(p.arg.function_name, p.src[2].arg) for p in prgs]
keys = [_batch_spec_key(*spec) for spec in specs]
unique_keys = list(dict.fromkeys(keys))
missing_keys = [key for key in unique_keys if (compiler, key) not in batch_compiled_cache]
missing_specs = [specs[keys.index(key)] for key in missing_keys]
if missing_specs:
batch_compiled_cache.update({(compiler, key):compiled for key,compiled in
zip(missing_keys, compiler.compile_cached_batch(missing_specs))})
for prg,key in zip(prgs, keys):
name,src,lib = batch_compiled_cache[(compiler, key)]
replacements[prg] = compiled_prg = prg.replace(src=prg.src[:2]+(UOp(Ops.SOURCE, arg=src), UOp(Ops.BINARY, arg=lib)),
arg=replace(prg.arg, name=name))
prg.__dict__['_compiled_programs'][compiler] = compiled_prg
return linear.substitute(replacements, walk=True, enter_calls=True) if replacements else linear
pm_optimize_local_size = PatternMatcher([
(UPat(Ops.CALL, src=(UPat(Ops.PROGRAM, name="prg"),), name="call", allow_any_len=True), optimize_local_size),
])
@@ -262,7 +298,9 @@ pm_exec = PatternMatcher([
def compile_linear(linear:UOp, beam:int|None=None, validate=False, input_uops:list[UOp]|None=None, jit=False) -> UOp:
if validate: linear = graph_rewrite(linear, pm_validate, name="validate", walk=True)
if (beam_val:=BEAM.value if beam is None else beam) >= 1: linear = graph_rewrite(linear, pm_beam, ctx=beam_val, walk=True)
linear = graph_rewrite(linear, pm_compile, name="precompile kernels", walk=True)
with scoped_rewrite_cache():
linear = graph_rewrite(linear, pm_compile, ctx=True, name="render kernels", walk=True)
linear = batch_compile(linear)
if getenv("HCQ2"):
from extra.hcq2.hcq2 import hcq_compile
linear = hcq_compile(linear, input_uops, jit=jit)
+4
View File
@@ -95,6 +95,10 @@ class MovementMixin:
*bound, stride = index.indices(int(size.vmax) if isinstance(size, UOp) else size)
bound = [0, 0] if stride * (bound[1] - bound[0]) < 0 else ([bound[1]+1, bound[0]+1] if stride < 0 else bound)
return {"size":ceildiv(bound[1]-bound[0], abs(stride)), "boundary":tuple(bound), "stride":stride, "collapse_dim":False}
if step == 1 and isinstance(start, UOp) and isinstance(stop, UOp) and stop.op is Ops.ADD:
base, delta = (stop.src[1], stop.src[0]) if stop.src[1] is start else (stop.src[0], stop.src[1])
if base is start and delta.op is Ops.CONST and isinstance(delta.arg, int) and delta.arg >= 0:
return {"size":delta.arg, "boundary":(start, stop), "stride":1, "collapse_dim":False}
if resolve(step == 1, False) and resolve((stop-start) >= 0, False):
return {"size":stop-start, "boundary":(start, stop), "stride":step, "collapse_dim":False}
raise TypeError(f"slice {index=} is not supported")
+36 -10
View File
@@ -466,6 +466,14 @@ class NVCCRenderer(CUDARenderer):
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype.scalar())
def _ocml(op): return lambda x,dtype: f"__ocml_{op}_f{ {dtypes.half:16, dtypes.double:64}.get(dtype, 32)}({x})"
def _hip_math(op):
return lambda x,dtype: _ocml(op)(x, dtype) if dtype.scalar() == dtypes.double else f"__builtin_elementwise_{op}({x})"
def hip_threefry(x:UOp, key:UOp) -> UOp:
x2 = x.cast(dtypes.uint).stack((x >> 32).cast(dtypes.uint))
key2 = key.cast(dtypes.uint).stack((key >> 32).cast(dtypes.uint))
ret = UOp(Ops.THREEFRY, dtypes.uint, src=(x2, key2))
return (ret.index(1).cast(dtypes.ulong) << 32) | ret.index(0).cast(dtypes.ulong)
class HIPRenderer(CStyleLanguage):
shared_max = 65536
@@ -500,10 +508,13 @@ class HIPRenderer(CStyleLanguage):
# https://clang.llvm.org/docs/AttributeReference.html#amdgpu-flat-work-group-size
# NOTE: this makes hlb_cifar10 twice as fast, there may be more gains in tweaking these parameters
kernel_typedef = 'extern "C" __attribute__((global)) void __attribute__((amdgpu_flat_work_group_size(1, {launch_bounds})))'
code_for_workitem = {"g": lambda x: f"__ockl_get_group_id({x})", "l": lambda x: f"__ockl_get_local_id({x})",
"i": lambda x: f"(__ockl_get_group_id({x})*__ockl_get_local_size({x})+__ockl_get_local_id({x}))"}
code_for_op = {**CStyleLanguage.code_for_op, Ops.TRUNC: _ocml("trunc"), Ops.SIN: _ocml("sin"),
Ops.LOG2: _ocml("log2"), Ops.EXP2: _ocml("exp2"), Ops.SQRT: _ocml("sqrt")}
code_for_workitem = {"g": lambda x: f"__builtin_amdgcn_workgroup_id_{'xyz'[int(x)]}()",
"l": lambda x: f"__builtin_amdgcn_workitem_id_{'xyz'[int(x)]}()",
"i": lambda x: f"(__builtin_amdgcn_workgroup_id_{'xyz'[int(x)]}()*"
f"((unsigned short *)__builtin_amdgcn_dispatch_ptr())[2+{x}]+__builtin_amdgcn_workitem_id_{'xyz'[int(x)]}())"}
code_for_op = {**CStyleLanguage.code_for_op, Ops.THREEFRY: lambda x,key,dtype: f"threefry({x},{key})",
Ops.TRUNC: _hip_math("trunc"), Ops.SIN: _hip_math("sin"),
Ops.LOG2: _hip_math("log2"), Ops.EXP2: _hip_math("exp2"), Ops.SQRT: _hip_math("sqrt")}
smem_prefix = "__attribute__((shared, aligned(16)))"
smem_prefix_for_cast: bool = False
barrier = '__builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup");' + '__builtin_amdgcn_s_barrier();' + \
@@ -511,6 +522,7 @@ class HIPRenderer(CStyleLanguage):
float4 = "make_float4"
type_map = {dtypes.bfloat16: "hip_bfloat16", dtypes.fp8e4m3: "hip_fp8", dtypes.fp8e5m2: "hip_bf8"}
extra_matcher = create_non_native_float_pats((dtypes.bfloat16, *dtypes.fp8s)) + PatternMatcher([
(UPat(Ops.THREEFRY, dtype=dtypes.ulong, src=(UPat.var("x"), UPat.var("key"))), hip_threefry),
(UPat(Ops.WMMA, name="x", dtype=dtypes.float),
lambda x: UOp(Ops.WMMA, src=(x.src[0].bitcast(dtypes.uint64), x.src[1].bitcast(dtypes.uint64),
x.src[2]), arg=(*x.arg,)) if x.src[0].max_numel() == 8 and x.src[0].dtype in dtypes.fp8_ocp else None),
@@ -528,17 +540,15 @@ class HIPRenderer(CStyleLanguage):
f"{vec} make_{vec}({', '.join([f'{scal} {x}' for x in _nms[:count]])}) {{ return {{ {', '.join(_nms[:count])} }}; }}"
def render_kernel(self, function_name, kernel, bufs, uops, prefix=None) -> str:
prefix, ockl = [], []
prefix = []
type_map = { dtypes.bfloat16: "bf16", dtypes.float: "f32", dtypes.half: "f16", dtypes.fp8e4m3: "_fp8_fp8", dtypes.fp8e5m2: "_bf8_bf8" }
used_dtypes = uops_to_dtypes(uops)
if any(u.op is Ops.CONST and not math.isfinite(u.arg) for u in uops):
prefix += ["#define INFINITY (__builtin_inff())", "#define NAN (__builtin_nanf(\"\"))"]
if any(u.op is Ops.SPECIAL for u in uops):
prefix.append("typedef long unsigned int size_t;")
ockl = [(f"__ockl_get_{name}", "unsigned int", "size_t", "const") for name in ["local_id", "group_id", "local_size"]]
if any(u.op is Ops.SPECIAL for u in uops): prefix.append("typedef long unsigned int size_t;")
ocml_ops = {Ops.EXP2: ("exp2", "pure"), Ops.LOG2: ("log2", "pure"), Ops.SQRT: ("sqrt", "const"), Ops.SIN: ("sin", ""), Ops.TRUNC: ("trunc", "")}
ocml = [(f"__ocml_{ocml_ops[op][0]}_f{dt.bitsize}", dt.name, dt.name, ocml_ops[op][1])
for op, dt in dedup((u.op, u.dtype.scalar()) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, dtypes.double)]
for op, dt in dedup((u.op, u.dtype.scalar()) for u in uops) if op in ocml_ops and dt == dtypes.double]
if any(dt == dtypes.bfloat16 for dt, _ in used_dtypes):
prefix.append(f"typedef {'__bf16' if self.is_cdna4(self.target.arch) else 'unsigned short'} hip_bfloat16;")
if any(dt == dtypes.half for dt, _ in used_dtypes): prefix.append("#define half _Float16")
@@ -549,8 +559,24 @@ class HIPRenderer(CStyleLanguage):
prefix.append("""static inline __attribute__((device)) unsigned char f32_to_fp8(float v, int is_bf8) {
v = (((*(unsigned*)&v)&0x7F800000)!=0x7F800000)?__builtin_amdgcn_fmed3f(v,is_bf8?57344.0f:448.0f,is_bf8?-57344.0f:-448.0f) : v;
return (unsigned char)(is_bf8?__builtin_amdgcn_cvt_pk_bf8_f32(v,v,0,false):__builtin_amdgcn_cvt_pk_fp8_f32(v,v,0,false));\n}""")
prefix += [f'extern "C" __attribute__((device{f", {atr}" if atr else ""})) {dto} {meth}({dti});' for meth,dti,dto,atr in ockl+ocml]
prefix += [f'extern "C" __attribute__((device{f", {atr}" if atr else ""})) {dto} {meth}({dti});' for meth,dti,dto,atr in ocml]
prefix += [self.render_vector_prefix(dt, count) for dt, count in used_dtypes if count > 1]
if any(u.op is Ops.THREEFRY for u in uops):
prefix.append("""static inline __attribute__((device)) unsigned_int2 threefry(unsigned_int2 x, unsigned_int2 key) {
unsigned int ks[3] = {key.y, key.x ^ key.y ^ 0x1BD11BDAu, key.x};
unsigned int xr0 = x.x + ks[2], xr1 = x.y + ks[0];
const unsigned int rotations[2][4] = {{13, 15, 26, 6}, {17, 29, 16, 24}};
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 4; j++) {
xr0 += xr1;
unsigned int r = rotations[i & 1][j];
xr1 = xr0 ^ ((xr1 << r) | (xr1 >> (32 - r)));
}
xr0 += ks[i % 3];
xr1 += ks[(i + 1) % 3] + i + 1;
}
return {xr0, xr1};
}""")
for name, (N, M, K), dtype_in, dtype_out, _, _, _, _ in wmma_args(uops): # TODO: handle TCs f32_bf16 and bf16_bf16 w/ wrapper
if self.is_cdna(self.target.arch):
+1 -1
View File
@@ -20,7 +20,7 @@ class CUDAGraph(MultiGraphRunner):
global_size, local_size = ast.arg.launch_dims({v: 0 for v in self.vars})
c_deps, new_node = self.new_node([b.base for b in bufs], ast.arg.outs)
c_args, vargs = encode_args([b._buf for b in bufs], [device_vars.get(x.expr, 0) for x in ast.arg.vars])
c_args, vargs = encode_args([b._buf for b in bufs], [device_vars.get(x.expr, self.fixedvars.get(x.expr, 0)) for x in ast.arg.vars])
kern_params = cuda.CUDA_KERNEL_NODE_PARAMS_v1(runtime.prg, *global_size, *local_size, runtime.smem,
ctypes.cast(0, ctypes.POINTER(ctypes.c_void_p)), vargs)
check(cuda.cuGraphAddKernelNode(ctypes.byref(new_node), self.graph, c_deps, len(c_deps or []), ctypes.byref(kern_params)))
+3 -3
View File
@@ -6,7 +6,6 @@ from tinygrad.device import Buffer, BufferSpec, Compiled, Device, MultiBuffer, P
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import UOp, Ops, Variable
from tinygrad.engine.jit import GraphRunner, MultiGraphRunner
from tinygrad.runtime.ops_rdma import RDMACopyQueue
class HCQGraph(MultiGraphRunner):
def __init__(self, *args, **kwargs):
@@ -50,7 +49,7 @@ class HCQGraph(MultiGraphRunner):
self.comp_queues: dict[HCQCompiled, HWQueue] = {dev: unwrap(dev.hw_compute_queue_t)() for dev in self.devices}
self.copy_queues: dict[tuple[HCQCompiled, int], HWQueue] = {} # lazy allocation, keyed by (device, queue_idx)
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], RDMACopyQueue] = {} # lazy allocation, keyed by device pair
self.rdma_queues: dict[tuple[HCQCompiled, HCQCompiled], Any] = {} # lazy allocation, keyed by device pair
self.num_copy_queues: int = getenv("HCQ_NUM_SDMA", min(len(self.devices), 8) if ALL2ALL >= 1 else 1)
self.num_rdma_ops: dict[tuple[HCQCompiled, HCQCompiled], int] = collections.defaultdict(int)
@@ -102,6 +101,7 @@ class HCQGraph(MultiGraphRunner):
if runtime is not None:
enqueue_queue = self.comp_queues[enqueue_dev]
elif is_rdma:
from tinygrad.runtime.ops_rdma import RDMACopyQueue
enqueue_queue = self.comp_queues[enqueue_dev]
rdma_key = (cast(HCQCompiled, Device[bufs[0].device]).rdma_dev(), enqueue_dev.rdma_dev())
self.rdma_queues.setdefault(rdma_key, RDMACopyQueue(enqueue_dev.rdma_dev()))
@@ -272,7 +272,7 @@ class HCQGraph(MultiGraphRunner):
for dev in self.devices: self.last_timeline[dev][0].wait(self.last_timeline[dev][1])
if PROFILE and self.kickoff_value > 1: self.collect_timestamps()
hcq_var_vals = {self.kickoff_var.expr: self.kickoff_value, **var_vals,
hcq_var_vals = {self.kickoff_var.expr: self.kickoff_value, **self.fixedvars, **var_vals,
**{var.expr: dev.timeline_value - 1 for dev, var in self.virt_timeline_vals.items()},
**{sig.base_buf.va_addr.expr: dev.timeline_signal.base_buf.va_addr for dev, sig in self.virt_timeline_signals.items()}}
+1 -1
View File
@@ -66,7 +66,7 @@ class MetalGraph(GraphRunner):
for j, global_dims, local_dims in self.updated_launch_dims(var_vals):
self.icb.indirectComputeCommandAtIndex(j).concurrentDispatchThreadgroups_threadsPerThreadgroup(metal.MTLSize(*global_dims),
metal.MTLSize(*local_dims))
for i, var in enumerate(self.vars): self.int_buf_view[i] = var_vals[var]
for i, var in enumerate(self.vars): self.int_buf_view[i] = (var_vals | self.fixedvars)[var]
command_buffer = self.dev.mtl_queue.commandBuffer().retained()
encoder = command_buffer.computeCommandEncoder().retained()
+25 -12
View File
@@ -13,7 +13,7 @@ from tinygrad.renderer.cstyle import HIPRenderer, HIPCCRenderer
from tinygrad.renderer.llvmir import AMDLLVMRenderer
from tinygrad.runtime.autogen import kfd, hsa, sqtt, amdgpu_kd, amdgpu_drm
from tinygrad.runtime.autogen.am import am
from tinygrad.runtime.support.elf import elf_loader
from tinygrad.runtime.support.elf import elf_loader, elf_symbols
from tinygrad.runtime.support.am.amdev import AMDev, AMMemoryManager
from tinygrad.runtime.support.amd import AMDReg, AMDIP, import_module, import_soc, import_pmc
from tinygrad.runtime.support.system import System, PCIIfaceBase, PCIAllocationMeta, USBPCIDevice, MAP_FIXED, MAP_NORESERVE
@@ -559,24 +559,37 @@ class AMDCopyQueue(HWQueue):
sdma_queue.signal_doorbell(dev)
class AMDProgramModule:
__slots__ = ("lib_gpu", "image", "symbols", "rodata_entry", "__weakref__")
def __init__(self, dev:AMDDevice, lib_gpu:HCQBuffer, image:bytes, spec:BufferSpec, symbols:dict[str, int], rodata_entry:int):
self.lib_gpu, self.image, self.symbols, self.rodata_entry = lib_gpu, image, symbols, rodata_entry
weakref.finalize(self, HCQProgram._fini, dev, lib_gpu, spec)
class AMDProgram(HCQProgram):
def __init__(self, dev:AMDDevice, name:str, lib:bytes, **kwargs):
# TODO; this API needs the type signature of the function and global_size/local_size
self.dev, self.name, self.lib = dev, name, lib
image, sections, relocs = elf_loader(self.lib)
module_key = hashlib.sha256(self.lib).digest()
if (module:=self.dev.program_modules.get(module_key)) is None:
image, sections, relocs = elf_loader(self.lib)
symbols = elf_symbols(self.lib)
rodata_entry = next((sh.header.sh_addr for sh in sections if sh.name == ".rodata"), -1)
for apply_image_offset, rel_sym_offset, typ, addent in relocs:
if typ == 5: image[apply_image_offset:apply_image_offset+8] = struct.pack('<q', rel_sym_offset - apply_image_offset + addent) # R_AMDGPU_REL64
else: raise RuntimeError(f"unknown AMD reloc {typ}")
lib_gpu = self.dev.allocator.alloc(round_up(image.nbytes, 0x1000), buf_spec:=BufferSpec(nolru=True))
self.dev.allocator._copyin(lib_gpu, image)
self.dev.synchronize()
rodata_entry = next((sh.header.sh_addr for sh in sections if sh.name == ".rodata"), -1)
self.dev.program_modules[module_key] = module = AMDProgramModule(self.dev, lib_gpu, bytes(image), buf_spec, symbols, rodata_entry)
self.module = module
self.lib_gpu, image, symbols, rodata_entry = module.lib_gpu, module.image, module.symbols, module.rodata_entry
rodata_entry = symbols.get(f"{name}.kd", rodata_entry)
assert rodata_entry >= 0, ".rodata section not found"
for apply_image_offset, rel_sym_offset, typ, addent in relocs:
if typ == 5: image[apply_image_offset:apply_image_offset+8] = struct.pack('<q', rel_sym_offset - apply_image_offset + addent) # R_AMDGPU_REL64
else: raise RuntimeError(f"unknown AMD reloc {typ}")
self.lib_gpu = self.dev.allocator.alloc(round_up(image.nbytes, 0x1000), buf_spec:=BufferSpec(nolru=True))
self.dev.allocator._copyin(self.lib_gpu, image)
self.dev.synchronize()
desc_sz = ctypes.sizeof(amdgpu_kd.llvm_amdhsa_kernel_descriptor_t)
desc = amdgpu_kd.llvm_amdhsa_kernel_descriptor_t.from_buffer_copy(bytes(image[rodata_entry:rodata_entry+desc_sz]))
self.group_segment_size = desc.group_segment_fixed_size
@@ -606,7 +619,6 @@ class AMDProgram(HCQProgram):
super().__init__(CLikeArgsState, self.dev, self.name, kernargs_alloc_size=self.kernargs_segment_size+additional_alloc_sz, lib=self.lib,
base=self.lib_gpu.va_addr)
weakref.finalize(self, self._fini, self.dev, self.lib_gpu, buf_spec)
def __call__(self, *bufs, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1), vals:tuple[int|None, ...]=(),
wait=False, timeout:int|None=None):
@@ -1004,6 +1016,7 @@ class AMDDevice(HCQCompiled):
functools.partial(AMDCopyQueue, self, max_copy_size=self.max_copy_size) if self.has_sdma_queue else None,
kernargs_size=(8 << 10) if self.is_usb() else (16 << 20), sigalloc_size=0x100 if self.is_usb() else 0x1000,
can_recover=self.is_am(), arch=self.arch)
self.program_modules:weakref.WeakValueDictionary[bytes, AMDProgramModule] = weakref.WeakValueDictionary()
# Scratch setup
self.max_private_segment_size = 0
+42 -7
View File
@@ -45,7 +45,7 @@ def set_options(action_info, options:bytes):
return amd_comgr_action_info_set_option_list(action_info, to_char_p_p(options_list:=options.split(b' ')), len(options_list))
# AMD_COMGR_SAVE_TEMPS=1 AMD_COMGR_REDIRECT_LOGS=stdout AMD_COMGR_EMIT_VERBOSE_LOGS=1
def compile_hip(prg:str, arch="gfx1100", asm=False) -> bytes:
def compile_hip(prg:str, arch="gfx1100", asm=False, use_device_libs=True, backend_opt=3) -> bytes:
check(comgr.amd_comgr_create_action_info(ctypes.byref(action_info := comgr.amd_comgr_action_info_t())))
check(comgr.amd_comgr_action_info_set_language(action_info, comgr.AMD_COMGR_LANGUAGE_HIP))
check(comgr.amd_comgr_action_info_set_isa_name(action_info, b"amdgcn-amd-amdhsa--" + arch.encode()))
@@ -70,16 +70,19 @@ def compile_hip(prg:str, arch="gfx1100", asm=False) -> bytes:
check(comgr.amd_comgr_set_data_name(data_src, b"<null>"))
check(comgr.amd_comgr_data_set_add(data_set_src, data_src))
# -include hiprtc_runtime.h was removed
frontend_opt = getenv("AMD_FRONTEND_OPT", 1)
options = [
"-O3", "-mcumode", "--hip-version=6.0.32830", "-DHIP_VERSION_MAJOR=6", "-DHIP_VERSION_MINOR=0", "-DHIP_VERSION_PATCH=32830",
f"-O{frontend_opt}", "-mcumode", "--hip-version=6.0.32830", "-DHIP_VERSION_MAJOR=6", "-DHIP_VERSION_MINOR=0", "-DHIP_VERSION_PATCH=32830",
"-D__HIPCC_RTC__", "-std=c++14", "-nogpuinc", "-Wno-gnu-line-marker", "-Wno-missing-prototypes", f"--offload-arch={arch}",
"-I/opt/rocm/include", "-Xclang -disable-llvm-passes", "-Xclang -aux-triple", "-Xclang x86_64-unknown-linux-gnu"]
check(set_options(action_info, ' '.join(options).encode()))
status = comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_COMPILE_SOURCE_WITH_DEVICE_LIBS_TO_BC, action_info, data_set_src, data_set_bc)
compile_action = comgr.AMD_COMGR_ACTION_COMPILE_SOURCE_WITH_DEVICE_LIBS_TO_BC if use_device_libs else comgr.AMD_COMGR_ACTION_COMPILE_SOURCE_TO_BC
status = comgr.amd_comgr_do_action(compile_action, action_info, data_set_src, data_set_bc)
if status != 0:
print(_get_comgr_data(data_set_bc, comgr.AMD_COMGR_DATA_KIND_LOG).decode())
raise RuntimeError("compile failed")
check(set_options(action_info, b"-O3 -mllvm -amdgpu-internalize-symbols"))
check(set_options(action_info, f"-O{backend_opt} -mllvm -vectorize-loops=false "
"-mllvm -vectorize-slp=false -mllvm -unroll-threshold=0".encode()))
check(comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_CODEGEN_BC_TO_RELOCATABLE, action_info, data_set_bc, data_set_reloc))
check(set_options(action_info, b""))
@@ -93,11 +96,43 @@ def compile_hip(prg:str, arch="gfx1100", asm=False) -> bytes:
class HIPCompiler(Compiler):
def __init__(self, arch:str):
assert comgr.dll.nm in c.DLL._loaded_, f"comgr not available: {comgr.dll.emsg}"
self.arch = arch
super().__init__(f"compile_hip_{self.arch}")
self.arch, self.frontend_opt, self.generic_opt = arch, getenv("AMD_FRONTEND_OPT", 1), getenv("AMD_GENERIC_COMPILE_OPT", 1)
super().__init__(f"compile_hip_{self.arch}_F{self.frontend_opt}G{self.generic_opt}")
def compile(self, src:str) -> bytes:
try: return compile_hip(src, self.arch, src.split('\n', 1)[0].strip() == '.text')
try: return compile_hip(src, self.arch, src.split('\n', 1)[0].strip() == '.text', use_device_libs="__ocml_" in src)
except RuntimeError as e: raise CompileError(e) from e
def compile_cached_batch(self, srcs:list[tuple[str, str]]) -> list[tuple[str, str, bytes]]:
batch_size = getenv("AMD_COMPILE_BATCH_SIZE", 256)
batch_opt = getenv("AMD_COMPILE_OPT", 2)
generic_opt = getenv("AMD_GENERIC_COMPILE_OPT", 1)
if batch_size <= 1 or any(src.split('\n', 1)[0].strip() == '.text' for _,src in srcs): return super().compile_cached_batch(srcs)
renamed = []
for name,src in srcs:
new_name = f"{name}_{hashlib.sha256(src.encode()).hexdigest()[:8]}"
renamed.append((new_name, src.replace(f" {name}(", f" {new_name}(", 1)))
ret:list[tuple[str, str, bytes]|None] = [None] * len(srcs)
groups:dict[tuple[int, bool], list[int]] = {}
opts = [batch_opt if name.startswith("coop_") else generic_opt for name,_ in renamed]
for i,(_,src) in enumerate(renamed): groups.setdefault((opts[i], "__ocml_" in src), []).append(i)
for (opt,use_device_libs),indices in groups.items():
for start in range(0, len(indices), batch_size):
batch = indices[start:start+batch_size]
preamble = dict.fromkeys(line for i in batch for line in
renamed[i][1].split('extern "C" __attribute__((global))', 1)[0].splitlines())
kernels = []
for i in batch:
_, kernel = renamed[i][1].split('extern "C" __attribute__((global))', 1)
kernels.append('extern "C" __attribute__((global))'+kernel)
combined = '\n'.join((*preamble, *kernels))
try: lib = compile_hip(combined, self.arch, use_device_libs=use_device_libs, backend_opt=opt)
except RuntimeError as e: raise CompileError(e) from e
for i in batch:
name,src = renamed[i]
ret[i] = (name, src, lib)
assert all(x is not None for x in ret)
return [x for x in ret if x is not None]
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
class HIPCCCompiler(Compiler):
+16
View File
@@ -6,6 +6,22 @@ from tinygrad.runtime.autogen import libc
@dataclass(frozen=True)
class ElfSection: name:str; header:libc.Elf64_Shdr|libc.Elf32_Shdr; content:bytes # noqa: E702
def elf_symbols(blob:bytes) -> dict[str, int]:
ecls = {libc.ELFCLASS32: "Elf32", libc.ELFCLASS64: "Elf64"}[blob[libc.EI_CLASS]]
header = getattr(libc, f"{ecls}_Ehdr").from_buffer_copy(blob)
section_headers = (getattr(libc, f"{ecls}_Shdr") * header.e_shnum).from_buffer_copy(blob[header.e_shoff:])
sym_t = getattr(libc, f"{ecls}_Sym")
symbols = {}
for sh in section_headers:
if sh.sh_type not in (libc.SHT_SYMTAB, libc.SHT_DYNSYM): continue
str_sh = section_headers[sh.sh_link]
strtab = blob[str_sh.sh_offset:str_sh.sh_offset+str_sh.sh_size]
for sym in (sym_t * (sh.sh_size // sh.sh_entsize)).from_buffer_copy(blob[sh.sh_offset:]):
symbols[strtab[sym.st_name:strtab.find(b'\0', sym.st_name)].decode()] = sym.st_value
return symbols
def elf_symbol_address(blob:bytes, name:str) -> int|None: return elf_symbols(blob).get(name)
def link_sym(sym:str, libs:list[str]) -> int:
for lib in libs:
try: return unwrap(ctypes.cast(getattr(ctypes.CDLL(ctypes.util.find_library(lib)), sym), ctypes.c_void_p).value)
+364 -4
View File
@@ -1,6 +1,8 @@
import time, inspect
from collections import deque
from tinygrad.uop.ops import UOp, Ops, UOpMetaClass, track_rewrites, graph_rewrite, gate_kernel_sink, KernelInfo
from collections import Counter, deque
from dataclasses import replace
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import UOp, Ops, UOpMetaClass, track_rewrites, graph_rewrite, gate_kernel_sink, KernelInfo, ssimplify
from tinygrad.uop.spec import type_verify, spec_tensor
from tinygrad.helpers import DEBUG, cpu_profile, TracingKey, SPEC, pluralize, SCACHE, BASEDIR, partition
@@ -26,6 +28,363 @@ def _split_after(after: UOp) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]:
raise AssertionError(f"AFTER source should be CALL, END, STORE, or AFTER, not {invalid[0].op}")
return tuple(kernels), tuple(deps)
def _kernel_io(call:UOp) -> tuple[tuple[UOp, ...], tuple[UOp, ...]]|None:
if call.op is not Ops.CALL or call.src[0].op is not Ops.SINK: return None
out_slots = {x.src[0].src[0].arg.slot for x in call.src[0].toposort()
if x.op is Ops.STORE and x.src[0].op is Ops.INDEX and x.src[0].src[0].op is Ops.PARAM}
if len(out_slots) != 1 or min(out_slots) < 0 or max(out_slots) >= len(call.src)-1: return None
return tuple(x for i,x in enumerate(call.src[1:]) if i in out_slots), tuple(x for i,x in enumerate(call.src[1:]) if i not in out_slots)
def _input_indices(call:UOp, inputs:tuple[UOp, ...]) -> set[tuple[UOp, tuple[UOp, ...]]]:
return {(call.src[x.src[0].arg.slot+1], x.src[1:]) for x in call.src[0].toposort()
if x.op is Ops.INDEX and x.src[0].op is Ops.PARAM and call.src[x.src[0].arg.slot+1] in inputs}
def _is_sum_sumsq(a:UOp, b:UOp) -> bool:
av, bv = a.src[0], b.src[0]
return (bv.op is Ops.MUL and bv.src == (av, av)) or (av.op is Ops.MUL and av.src == (bv, bv))
def _substitute_uops(ast:UOp, mapping:dict[UOp, UOp]) -> UOp:
memo: dict[UOp, UOp] = {}
def rec(x:UOp) -> UOp:
if x in mapping: return mapping[x]
if x not in memo:
src = tuple(rec(s) for s in x.src)
memo[x] = x if src == x.src else x.replace(src=src)
return memo[x]
return rec(ast)
def _remap_params(ast:UOp, remap:dict[int, int]) -> UOp:
return _substitute_uops(ast, {x:x.replace(arg=replace(x.arg, slot=remap[x.arg.slot])) for x in ast.toposort()
if x.op is Ops.PARAM and x.arg.slot >= 0})
def _fuse_adjacent_reductions(linearized:list[UOp]) -> list[UOp]:
ret: list[UOp] = []
i = 0
while i < len(linearized):
if i+1 == len(linearized):
ret.append(linearized[i])
break
a, b = linearized[i:i+2]
aio, bio = _kernel_io(a), _kernel_io(b)
ar = [x for x in a.src[0].toposort() if x.op is Ops.REDUCE] if aio is not None else []
br = [x for x in b.src[0].toposort() if x.op is Ops.REDUCE] if bio is not None else []
compatible = aio is not None and bio is not None and len(ar) == len(br) == 1 and ar[0].arg == br[0].arg and \
ar[0].src[1:] == br[0].src[1:] and _is_sum_sumsq(ar[0], br[0]) and aio[1] == bio[1] and \
_input_indices(a, aio[1]) == _input_indices(b, bio[1]) and aio[0] != bio[0] and \
len(a.src[0].src) == len(b.src[0].src) == 1 and a.src[0].src[0].op is Ops.END and b.src[0].src[0].op is Ops.END and \
a.src[0].src[0].src[1:] == b.src[0].src[0].src[1:]
if not compatible:
ret.append(a)
i += 1
continue
assert aio is not None and bio is not None
args = list(a.src[1:])
if len(set(args)) != len(args) or any(x in args for x in bio[0]):
ret.append(a)
i += 1
continue
args.extend(bio[0])
remap = {slot:args.index(x) for slot,x in enumerate(b.src[1:])}
bast = _remap_params(b.src[0], remap)
ret.append(a.src[0].replace(src=a.src[0].src+bast.src).call(*args))
i += 2
return ret
def _fuse_dependent_reductions(linearized:list[UOp]) -> list[UOp]:
ret = list(linearized)
i = 0
while i < len(ret):
a, aio = ret[i], _kernel_io(ret[i])
if aio is None or len(a.src[0].src) != 1 or a.src[0].src[0].op is not Ops.END:
i += 1
continue
ar = [x for x in a.src[0].toposort() if x.op is Ops.REDUCE]
if len(ar) != 1 or ar[0].arg != (Ops.MAX, 0) or len(ar[0].src[1:]) != 2 or any(int(x.vmax)+1 != 2 for x in ar[0].src[1:]):
i += 1
continue
aend, astore = a.src[0].src[0], a.src[0].src[0].src[0]
if astore.op is not Ops.STORE:
i += 1
continue
spatial = tuple(int(x.vmax)+1 for x in aend.src[-2:])
if len(aend.src[1:]) != 4 or spatial[0] != spatial[1] or spatial[0] & (spatial[0]-1):
i += 1
continue
for j in range(i+1, min(i+6, len(ret))):
b, bio = ret[j], _kernel_io(ret[j])
if bio is None or len(b.src[0].src) != 1 or b.src[0].src[0].op is not Ops.END: continue
br = [x for x in b.src[0].toposort() if x.op is Ops.REDUCE]
if len(br) != 1 or br[0].arg != (Ops.ADD, 0) or ar[0].src[1:] != br[0].src[1:]: continue
bend = b.src[0].src[0]
if aend.src[1:] != bend.src[1:] or aio[0][0] not in bio[1] or set(bio[1]) != set(aio[1]+aio[0]): continue
args = list(a.src[1:])
if len(set(args)) != len(args) or bio[0][0] in args: continue
args.extend(bio[0])
pout_slot = tuple(b.src[1:]).index(aio[0][0])
pout_idxs = [x for x in b.src[0].toposort() if x.op is Ops.INDEX and x.src[0].op is Ops.PARAM and x.src[0].arg.slot == pout_slot]
if not pout_idxs or any(x.src[1:] != astore.src[0].src[1:] for x in pout_idxs): continue
mapping = {x:astore.src[1] for x in pout_idxs}
for x in b.src[0].toposort():
if x.op is Ops.PARAM and x.arg.slot >= 0 and x.arg.slot != pout_slot:
mapping[x] = x.replace(arg=replace(x.arg, slot=args.index(b.src[x.arg.slot+1])))
bend = _substitute_uops(bend, mapping)
ret[i] = a.src[0].replace(src=(aend, bend)).call(*args)
ret.pop(j)
break
i += 1
return ret
def _fuse_direct_conv_bwd_activation(linearized:list[UOp]) -> list[UOp]:
from tinygrad.codegen.opt.gemm import DirectConvBwdActivationMatch, _match_gemm
cases = {
(288,64): (32, False,
{Ops.CONST:22, Ops.MUL:18, Ops.ADD:18, Ops.AND:8, Ops.RANGE:6, Ops.CMPLT:4, Ops.PARAM:3, Ops.INDEX:3,
Ops.CAST:3, Ops.FLOORMOD:2, Ops.FLOORDIV:2, Ops.WHERE:2, Ops.EXP2:1, Ops.RECIPROCAL:1, Ops.REDUCE:1,
Ops.STORE:1, Ops.END:1, Ops.SINK:1}),
(576,64): (16, True,
{Ops.CONST:23, Ops.ADD:19, Ops.MUL:18, Ops.AND:8, Ops.RANGE:6, Ops.PARAM:4, Ops.INDEX:4, Ops.CAST:4,
Ops.CMPLT:4, Ops.FLOORMOD:2, Ops.FLOORDIV:2, Ops.WHERE:2, Ops.EXP2:1, Ops.RECIPROCAL:1, Ops.REDUCE:1,
Ops.STORE:1, Ops.END:1, Ops.SINK:1}),
(2304,256): (8, True,
{Ops.CONST:23, Ops.ADD:19, Ops.MUL:18, Ops.AND:8, Ops.RANGE:6, Ops.PARAM:4, Ops.INDEX:4, Ops.CAST:4,
Ops.CMPLT:4, Ops.FLOORMOD:2, Ops.FLOORDIV:2, Ops.WHERE:2, Ops.EXP2:1, Ops.RECIPROCAL:1, Ops.REDUCE:1,
Ops.STORE:1, Ops.END:1, Ops.SINK:1}),
}
ret = list(linearized)
i = 0
while i < len(ret):
call = ret[i]
if call.op is not Ops.CALL or call.src[0].op is not Ops.SINK or \
(g:=_match_gemm(call.src[0], "AMD", "gfx11")) is None or \
(g.n, g.k) not in cases or g.old is not None or g.a_kxm or not g.b_kxn:
i += 1
continue
patch_grad = call.src[g.c.arg.slot+1]
consumers = [j for j,x in enumerate(ret) if j > i and patch_grad in x.src[1:]]
if len(consumers) != 1 or consumers[0] > i+3:
i += 1
continue
j, consumer = consumers[0], ret[consumers[0]]
io = _kernel_io(consumer)
params = sorted((x.arg.slot, x.dtype, x.max_numel()) for x in consumer.src[0].toposort() if x.op is Ops.PARAM)
op_counts = Counter(x.op for x in consumer.src[0].toposort())
spatial, residual, expected_ops = cases[(g.n,g.k)]
if g.m % (spatial*spatial):
i += 1
continue
batch, output_size = g.m//(spatial*spatial), g.m*(g.n//9)
if batch not in (1024,1280,1536):
i += 1
continue
if batch == 1024 and (g.n,g.k) == (288,64): expected_ops = expected_ops | {Ops.CONST:expected_ops[Ops.CONST]-1}
expected_params = ([(0,dtypes.float,output_size),(1,dtypes.float,output_size),(2,dtypes.half,output_size),
(3,dtypes.half,g.m*g.n)] if residual else
[(0,dtypes.float,output_size),(1,dtypes.half,output_size),(2,dtypes.half,g.m*g.n)])
expected_srcs = 5 if residual else 4
if io is None or len(io[0]) != 1 or len(io[1]) != expected_srcs-2 or len(consumer.src) != expected_srcs or \
consumer.src[-1] is not patch_grad or params != expected_params or op_counts != Counter(expected_ops):
i += 1
continue
reduces = [x for x in consumer.src[0].toposort() if x.op is Ops.REDUCE]
if len(reduces) != 1 or reduces[0].arg != (Ops.ADD, 0) or tuple(int(x.vmax)+1 for x in reduces[0].src[1:]) != (4,4):
i += 1
continue
info = DirectConvBwdActivationMatch(g.m, g.n//9, g.k, spatial, residual)
ast = call.src[0].replace(tag=info, arg=replace(call.src[0].arg, name="direct_conv_bwd_activation"))
extras = (consumer.src[2], consumer.src[3]) if residual else (consumer.src[2],)
ret[j] = ast.call(io[0][0], *extras, call.src[g.a.arg.slot+1], call.src[g.b.arg.slot+1])
ret.pop(i)
return ret
def _fuse_gemm_output_transpose(linearized:list[UOp]) -> list[UOp]:
from tinygrad.codegen.opt.gemm import GemmOutputNCHW, _match_gemm
from tinygrad.device import Device
cases = {
(64,288): (32, 7),
(256,576): (16, 6),
(512,2304): (8, 7),
}
ret = list(linearized)
i = 0
while i < len(ret):
call = ret[i]
if call.op is not Ops.CALL or call.src[0].op is not Ops.SINK or not isinstance(call.device, str):
i += 1
continue
target = Device[call.device].renderer.target
if target.device != "AMD" or not target.arch.startswith("gfx11") or \
(g:=_match_gemm(call.src[0], target.device, target.arch)) is None or (g.n,g.k) not in cases or \
g.old is not None or g.a_kxm or g.b_kxn:
i += 1
continue
gemm_out = call.src[g.c.arg.slot+1]
consumers = [j for j,x in enumerate(ret) if j > i and gemm_out in x.src[1:]]
if consumers != [i+1]:
i += 1
continue
j, consumer = consumers[0], ret[consumers[0]]
io, spatial_const = _kernel_io(consumer), cases[(g.n,g.k)]
spatial, const_count = spatial_const
op_counts = Counter(x.op for x in consumer.src[0].toposort())
expected_ops = {Ops.CONST:const_count, Ops.ADD:6, Ops.MUL:5, Ops.RANGE:4, Ops.PARAM:2, Ops.INDEX:2,
Ops.STORE:1, Ops.END:1, Ops.SINK:1}
if io is None or len(io[0]) != 1 or len(io[1]) != 1 or len(consumer.src) != 3 or io[1][0] is not gemm_out or \
len(consumer.src[0].src) != 1 or op_counts != Counter(expected_ops):
i += 1
continue
end = consumer.src[0].src[0]
end_shape = tuple(int(x.vmax)+1 for x in end.src[1:])
if end.op is not Ops.END or len(end_shape) != 4 or end_shape[0] not in (1024,1280,1536) or end_shape != (end_shape[0],g.n,spatial,spatial):
i += 1
continue
store, (batch, channel, y, x) = end.src[0], end.src[1:]
if store.op is not Ops.STORE or store.src[0].op is not Ops.INDEX or store.src[1].op is not Ops.INDEX:
i += 1
continue
out_idx, in_idx = (ssimplify(u.src[1].get_idx()) for u in (store.src[0], store.src[1]))
spatial_size = spatial**2
if out_idx is not ssimplify(batch*(g.n*spatial_size)+channel*spatial_size+y*spatial+x) or \
in_idx is not ssimplify(batch*(spatial_size*g.n)+y*(spatial*g.n)+x*g.n+channel):
i += 1
continue
args = list(call.src[1:])
args[g.c.arg.slot] = io[0][0]
ret[i] = call.src[0].replace(tag=GemmOutputNCHW(spatial)).call(*args)
ret.pop(j)
i += 1
return ret
def _fuse_channel_reductions(linearized:list[UOp]) -> list[UOp]:
from tinygrad.codegen.opt.reduce import ActivationVarGradElementwiseSumDMeanMatch, DualActivationReduceElementwiseMatch
from tinygrad.codegen.opt.reduce import DualBNGradDMean512Match, DualChannelReduceMatch, DualMoments512Match
from tinygrad.codegen.opt.reduce import _match_activation_elementwise, _match_activation_elementwise_512, _match_activation_sum
from tinygrad.codegen.opt.reduce import _match_activation_sum_512
from tinygrad.codegen.opt.reduce import _match_activation_dmean_512, _match_activation_var_grad, _match_channel_reduce
from tinygrad.codegen.opt.reduce import _match_moment_mean_512, _match_moment_var_512
from tinygrad.codegen.opt.reduce import _match_bn_dmean_512, _match_bn_sum_512, _match_bn_var_512
from tinygrad.device import Device
ret = list(linearized)
i = 0
while i+1 < len(ret):
a, b = ret[i:i+2]
if a.op is not Ops.CALL or b.op is not Ops.CALL or a.src[0].op is not Ops.SINK or b.src[0].op is not Ops.SINK or \
not isinstance(a.device,str) or a.device != b.device:
i += 1
continue
target = Device[a.device].renderer.target
mean_batch, var_batch = _match_moment_mean_512(a.src[0],target.device,target.arch), _match_moment_var_512(b.src[0],target.device,target.arch)
if mean_batch is None or mean_batch != var_batch or \
a.src[1] is not b.src[3] or a.src[2] is not b.src[2]:
i += 1
continue
ret[i] = a.src[0].replace(tag=DualMoments512Match(mean_batch)).call(a.src[1],b.src[1],a.src[2])
ret.pop(i+1)
i += 1
i = 0
while i+1 < len(ret):
a, b = ret[i:i+2]
if a.op is not Ops.CALL or b.op is not Ops.CALL or a.src[0].op is not Ops.SINK or b.src[0].op is not Ops.SINK or \
not isinstance(a.device,str) or a.device != b.device:
i += 1
continue
target = Device[a.device].renderer.target
var_batch, sum_batch = _match_bn_var_512(a.src[0],target.device,target.arch), _match_bn_sum_512(b.src[0],target.device,target.arch)
if var_batch is None or var_batch != sum_batch or \
a.src[6] is not b.src[2]:
i += 1
continue
dmean_idx = next((j for j in range(i+2,min(i+6,len(ret))) if ret[j].op is Ops.CALL and ret[j].src[0].op is Ops.SINK and
_match_bn_dmean_512(ret[j].src[0],target.device,target.arch) == var_batch and ret[j].src[2] is a.src[1] and
ret[j].src[3] is a.src[4] and ret[j].src[4] is a.src[5] and ret[j].src[5] is a.src[2] and ret[j].src[6] is a.src[6]),None)
if dmean_idx is None:
i += 1
continue
d = ret[dmean_idx]
ret[i] = a.src[0].replace(tag=DualBNGradDMean512Match(var_batch)).call(
a.src[1],b.src[1],d.src[1],a.src[2],a.src[3],a.src[4],a.src[5],a.src[6],b.src[3])
ret.pop(dmean_idx)
ret.pop(i+1)
i += 1
i = 0
while i+1 < len(ret):
a, b = ret[i:i+2]
if a.op is not Ops.CALL or b.op is not Ops.CALL or a.src[0].op is not Ops.SINK or b.src[0].op is not Ops.SINK or \
not isinstance(a.device,str) or a.device != b.device:
i += 1
continue
target = Device[a.device].renderer.target
var_batch, elem_batch = _match_activation_var_grad(a.src[0],target.device,target.arch), \
_match_activation_elementwise_512(b.src[0],target.device,target.arch)
if var_batch is None or var_batch != elem_batch or \
a.src[5] is not b.src[2] or a.src[2] is not b.src[3] or a.src[6] is not b.src[4] or a.src[7] is not b.src[5]:
i += 1
continue
c = ret[i+2] if i+2 < len(ret) else None
fuse_sum = c is not None and c.op is Ops.CALL and c.src[0].op is Ops.SINK and \
_match_activation_sum_512(c.src[0],target.device,target.arch) == var_batch and a.src[6] is c.src[2] and a.src[7] is c.src[3]
if not fuse_sum:
i += 1
continue
assert c is not None
dmean_idx = next((j for j in range(i+3,min(i+7,len(ret))) if ret[j].op is Ops.CALL and ret[j].src[0].op is Ops.SINK and
_match_activation_dmean_512(ret[j].src[0],target.device,target.arch) == var_batch and ret[j].src[2] is a.src[1] and
ret[j].src[3] is a.src[4] and ret[j].src[4] is b.src[1]),None)
if dmean_idx is None:
i += 1
continue
d = ret[dmean_idx]
ret[i] = a.src[0].replace(tag=ActivationVarGradElementwiseSumDMeanMatch(var_batch)).call(
a.src[1],b.src[1],c.src[1],d.src[1],a.src[2],a.src[3],a.src[4],a.src[5],a.src[6],a.src[7],c.src[4])
ret.pop(dmean_idx)
ret.pop(i+2)
ret.pop(i+1)
i += 1
i = 0
while i < len(ret):
a = ret[i]
if a.op is not Ops.CALL or a.src[0].op is not Ops.SINK or not isinstance(a.device,str):
i += 1
continue
target = Device[a.device].renderer.target
am = _match_channel_reduce(a.src[0],target.device,target.arch)
if am is None or am.kind != "activation":
i += 1
continue
for j in range(i+1,min(i+4,len(ret))):
b = ret[j]
sm = _match_activation_sum(b.src[0],target.device,target.arch) if b.op is Ops.CALL and b.src[0].op is Ops.SINK else None
if sm != (am.channels,am.spatial,am.groups) or a.src[5] is not b.src[2] or a.src[6] is not b.src[3]: continue
bout = b.src[1]
if any(bout in x.src[1:] for x in ret[i+1:j]): continue
elem_idx = next((k for k in range(i+1,min(i+5,len(ret))) if k != j and ret[k].op is Ops.CALL and ret[k].src[0].op is Ops.SINK and
_match_activation_elementwise(ret[k].src[0],target.device,target.arch) == (am.channels,am.spatial,am.groups) and
a.src[4] is ret[k].src[2] and a.src[5] is ret[k].src[4] and a.src[6] is ret[k].src[5]),None)
if elem_idx is None: continue
elem = ret[elem_idx]
ret[i] = a.src[0].replace(tag=DualActivationReduceElementwiseMatch(am.groups,am.channels,am.spatial)).call(
a.src[1],bout,elem.src[1],a.src[2],a.src[3],a.src[4],elem.src[3],a.src[5],a.src[6])
for k in sorted((j,elem_idx),reverse=True): ret.pop(k)
break
i += 1
i = 0
while i+1 < len(ret):
a, b = ret[i:i+2]
if a.op is not Ops.CALL or b.op is not Ops.CALL or a.src[0].op is not Ops.SINK or b.src[0].op is not Ops.SINK or \
not isinstance(a.device, str) or a.device != b.device:
i += 1
continue
target = Device[a.device].renderer.target
am, bm = _match_channel_reduce(a.src[0], target.device, target.arch), _match_channel_reduce(b.src[0], target.device, target.arch)
if am is None or bm is None or am.kind != "centered" or bm.kind != "scale" or \
(am.groups,am.channels,am.spatial) != (bm.groups,bm.channels,bm.spatial) or a.src[5] is not b.src[4]:
i += 1
continue
ret[i] = a.src[0].replace(tag=DualChannelReduceMatch(am.groups, am.channels, am.spatial)).call(
a.src[1], b.src[1], a.src[2], a.src[3], a.src[4], b.src[2], b.src[3], a.src[5])
ret.pop(i+1)
i += 1
return ret
def create_schedule(sched_sink:UOp) -> UOp:
with cpu_profile(TracingKey("toposort sched_sink")):
# build kernel dependency graph: edges from producer kernel to consumer kernels
@@ -77,7 +436,8 @@ def create_schedule(sched_sink:UOp) -> UOp:
in_degree[x] -= 1
if in_degree[x] == 0: queue.append(x)
if any(in_degree.values()): raise RuntimeError("cycle detected in assign graph")
return UOp(Ops.LINEAR, src=tuple(linearized))
return UOp(Ops.LINEAR, src=tuple(_fuse_gemm_output_transpose(_fuse_channel_reductions(
_fuse_direct_conv_bwd_activation(_fuse_dependent_reductions(_fuse_adjacent_reductions(linearized)))))))
from tinygrad.schedule.memory import memory_plan_rewrite
from tinygrad.engine.realize import capturing, pm_flatten_linear
@@ -109,7 +469,7 @@ schedule_cache: dict[bytes, UOp] = {}
def lower_sink_to_linear(function:UOp) -> UOp|None:
st = time.perf_counter()
if isinstance(function.arg, KernelInfo): return None
cache_key = function.key
cache_key = function.key if SCACHE else b""
if not SCACHE or (sc_ret:=schedule_cache.get(cache_key, None)) is None:
if SPEC: type_verify(function, spec_tensor)
# support recursive CALLs
+56 -4
View File
@@ -1,11 +1,11 @@
from typing import Iterator
from typing import Iterator, cast
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 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
from tinygrad.helpers import argsort, all_same, cpu_profile, PCONTIG, colored, Context, SPEC, prod
ALWAYS_CONTIGUOUS: set[Ops] = {Ops.CONTIGUOUS, Ops.AFTER, Ops.COPY, Ops.BUFFER, Ops.SLICE,
Ops.CONST, Ops.BIND, Ops.MSELECT, Ops.MSTACK, Ops.PARAM,
@@ -135,6 +135,17 @@ def _apply_reshape(in_shape:tuple[sint,...], out_shape:tuple[sint, ...], urngs:U
axes_in.append(acc*src)
acc *= s
combined_axes = UOp.const(dtypes.index, 0).usum(axes_in)
# Devectorization indexes reshapes with concrete lane numbers. Avoid running the
# full symbolic-validity pipeline for each lane when the flat index is constant.
if all(isinstance(s, int) and s > 0 for s in in_shape) and combined_axes.vmin == combined_axes.vmax and isinstance(combined_axes.vmin, int):
flat_idx = int(combined_axes.vmin)
const_axes = []
for s in cast(tuple[int, ...], in_shape)[::-1]:
const_axes.append(UOp.const(dtypes.index, flat_idx % s))
flat_idx //= s
return UOp.sink(*const_axes[::-1])
if len(in_shape) == 1:
return graph_rewrite(UOp.sink(combined_axes), symbolic+pm_simplify_valid+pm_drop_and_clauses, name="reshape")
axes_out:list[UOp] = []
for s in in_shape[::-1]:
axes_out.append(combined_axes % s)
@@ -142,6 +153,35 @@ def _apply_reshape(in_shape:tuple[sint,...], out_shape:tuple[sint, ...], urngs:U
# this simplify is doing a lot of heavy lifting. this is the replacement for the reshape view merging code
return graph_rewrite(UOp.sink(*axes_out[::-1]), symbolic+pm_simplify_valid+pm_drop_and_clauses, name="reshape")
def _apply_int_reshape(in_shape:tuple[int, ...], out_shape:tuple[int, ...], rngs:tuple[UOp, ...]) -> tuple[UOp, ...]|None:
"""Index pure contiguous splits/merges without flattening and symbolically rebuilding the entire shape."""
ret:list[UOp] = []
i = j = 0
while i < len(in_shape) or j < len(out_shape):
while i < len(in_shape) and in_shape[i] == 1:
ret.append(UOp.const(dtypes.index, 0))
i += 1
while j < len(out_shape) and out_shape[j] == 1: j += 1
if i == len(in_shape) or j == len(out_shape): break
ni, nj, in_sz, out_sz = i+1, j+1, in_shape[i], out_shape[j]
while in_sz != out_sz:
if in_sz < out_sz and ni < len(in_shape): in_sz, ni = in_sz*in_shape[ni], ni+1
elif out_sz < in_sz and nj < len(out_shape): out_sz, nj = out_sz*out_shape[nj], nj+1
else: return None
in_group, out_group, out_idxs = in_shape[i:ni], out_shape[j:nj], rngs[j:nj]
if in_group == out_group: ret.extend(out_idxs)
elif len(in_group) == 1:
ret.append(UOp.const(dtypes.index, 0).usum([r*prod(out_group[k+1:]) for k,r in enumerate(out_idxs)]))
elif len(out_group) == 1:
flat, rev = out_idxs[0], []
for sz in in_group[:0:-1]:
rev.append(flat % sz)
flat //= sz
ret.extend((flat, *rev[::-1]))
else: return None
i, j = ni, nj
return tuple(ret) if i == len(in_shape) and j == len(out_shape) else None
# this is the definition of the movement ops
@functools.cache
def apply_movement_op(op:Ops, in_shape:tuple[sint,...], arg:tuple, rngs:tuple[UOp, ...]) -> tuple[UOp, ...]:
@@ -156,6 +196,20 @@ def apply_movement_op(op:Ops, in_shape:tuple[sint,...], arg:tuple, rngs:tuple[UO
rngs = tuple(r if (sz == sh and off == 0) else (r-off).valid(graph_rewrite((r >= off) & (r < (sh+off)),
symbolic+pm_simplify_valid, name="pad")) for r,sh,(off,sz) in zip(rngs, in_shape, arg))
case Ops.RESHAPE:
if all(isinstance(s, int) for s in in_shape+arg) and tuple(s for s in in_shape if s != 1) == tuple(s for s in arg if s != 1):
out_rngs = iter(r for s,r in zip(arg, rngs) if s != 1)
return tuple(UOp.const(dtypes.index, 0) if s == 1 else next(out_rngs) for s in in_shape)
if all(isinstance(s, int) and s > 0 for s in in_shape+arg) and all(r.op is Ops.CONST and isinstance(r.arg, int) for r in rngs):
flat_idx = sum(int(r.arg)*prod(arg[i+1:]) for i,r in enumerate(rngs))
ret = []
for s in cast(tuple[int, ...], in_shape)[::-1]:
ret.append(UOp.const(dtypes.index, flat_idx % s))
flat_idx //= s
return tuple(ret[::-1])
if len(in_shape) == 1:
return (UOp.const(dtypes.index, 0).usum([r*prod(arg[i+1:]) for i,r in enumerate(rngs)]),)
if all(isinstance(s, int) and s > 0 for s in in_shape+arg) and \
(reshaped:=_apply_int_reshape(cast(tuple[int, ...], in_shape), cast(tuple[int, ...], arg), rngs)) is not None: return reshaped
sink = UOp.sink(*rngs).simplify() # NOTE: this applies any commutative flips to the rngs early
sub_array = {r:UOp.range(r.src[0], i, AxisType.PLACEHOLDER, dtype=r.dtype) for i,r in enumerate(sink.ranges)}
rngs = _apply_reshape(in_shape, arg, sink.substitute(sub_array)).substitute({v:k for k,v in sub_array.items()}).src
@@ -286,8 +340,6 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
# NOTE: SPEC=3 is broken here with shape
with Context(SPEC=min(SPEC.value, 2)):
tsink = graph_rewrite(tsink, pm_apply_rangeify, ctx=rctx, bottom_up=True, name="apply rangeify")
# if a deviceless value must materialize, place it on the sink device
tsink = graph_rewrite(tsink, pm_fix_deviceless, ctx=tsink.device, name="add device to deviceless")
return tsink, rctx
def render_ranges(*rngs_list, realized) -> str:
+6 -3
View File
@@ -10,7 +10,7 @@ from tinygrad.helpers import prod, all_same, getenv, dedup, all_int, DEBUG, SPLI
from tinygrad.helpers import PCONTIG, FLOAT16, OPENPILOT_HACKS, argsort, partition, get_single_element
from tinygrad.codegen.simplify import pm_flatten_range, pm_reduce_simplify
from tinygrad.codegen.opt import Opt
from tinygrad.schedule.indexing import run_rangeify, BufferizeOpts, IndexingContext, apply_movement_op
from tinygrad.schedule.indexing import run_rangeify, BufferizeOpts, IndexingContext, apply_movement_op, pm_fix_deviceless
from tinygrad.schedule.multi import multi_pm
from tinygrad.schedule.allreduce import create_allreduce_function
@@ -60,9 +60,10 @@ pm_mops = PatternMatcher([
# 0. do some cleanup rewrites, mostly copied from the old stuff
def fix_store_hazard(target:UOp, src:UOp):
base = target.base
if src is not base and base not in src.backward_slice: return None
# PERMUTE and FLIP reorder indices, SHRINK can have overlapping regions when dest is also shrunk
unsafe = {Ops.PERMUTE, Ops.FLIP} | ({Ops.SHRINK} if target.op_in_backward_slice_with_self(Ops.SHRINK) else set())
base = target.base
reaches_base: dict[UOp, bool] = {}
for s in src.toposort(gate=lambda s: s.op is not Ops.CONTIGUOUS):
reaches_base[s] = s is base or any(reaches_base.get(c) for c in s.src)
@@ -540,7 +541,9 @@ def get_kernel_graph(sink:UOp) -> UOp:
# convert movement ops to ranges
tsink, rctx = run_rangeify(tsink, bool(DEBUG_RANGEIFY))
tsink = graph_rewrite(tsink, symbolic+pm_reduce_simplify+pm_const_buffer_folding+pm_remove_bufferize, name="symbolic+reduce_collapse+debuf")
# Device attachment is valid only after rangeify, but can share this fixed-point traversal.
tsink = graph_rewrite(tsink, pm_fix_deviceless+symbolic+pm_reduce_simplify+pm_const_buffer_folding+pm_remove_bufferize,
ctx=tsink.device, name="symbolic+reduce_collapse+debuf")
tsink = graph_rewrite(tsink, pm_limit_bufs, ctx=rctx, name="limit buffers")
if VIZ: graph_rewrite(tsink, PatternMatcher([]), name="View Rangeify")
+200 -75
View File
@@ -193,16 +193,25 @@ buffers:weakref.WeakKeyDictionary[UOp, Buffer|MultiBuffer] = weakref.WeakKeyDict
all_metadata:weakref.WeakKeyDictionary[UOp, tuple[Metadata, ...]] = weakref.WeakKeyDictionary() # TODO: should this be here?
# recursive_property replaces functools.cached_property in recursive UOp functions to prevent RecursionError
class recursive_property(property):
class recursive_property:
def __init__(self, fxn):
self.fxn = fxn
self.nm = "_RECURSIVE_PROPERTY_"+fxn.__name__
self.nm = fxn.__name__
self.__doc__ = fxn.__doc__
def __get__(self, x:UOp|None, owner=None):
if x is None: return self
if self.nm in x.__dict__: return x.__dict__[self.nm]
for node in x.toposort(gate=lambda node: self.nm not in node.__dict__): node.__dict__[self.nm] = self.fxn(node)
return x.__dict__[self.nm]
if (nm:=self.nm) in x.__dict__: return x.__dict__[nm]
stack = [(x, iter(x.src))]
while stack:
n, children = stack[-1]
for child in children:
if nm in child.__dict__: continue
stack.append((child, iter(child.src)))
break
else:
n.__dict__[nm] = self.fxn(n)
stack.pop()
return x.__dict__[nm]
# we import this late so we can use resolve/smax in mixins
from tinygrad.mixin.op import OpMixin
@@ -217,6 +226,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
arg:Any = None
tag:Any = None
def __del__(self):
if getattr(sys, "is_finalizing", lambda: True)(): return
if Ops is not None and self.op is Ops.BUFFER and (buffer:=buffers.get(self)) is not None: buffer.ref(-1)
try: del UOpMetaClass.ucache[(self.op, self.dtype, self.src, self.arg, self.tag)]
except AttributeError: pass
@@ -258,16 +268,36 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def toposort(self, gate:Callable|None=None, enter_calls=True) -> dict[UOp, None]:
cache: dict[UOp, None] = {}
stack: list[tuple[UOp, bool]] = [(self, False)] # each stack entry is (node, visited_flag)
if gate is None and enter_calls:
if (backward_slice:=self.__dict__.get("backward_slice")) is not None: return {**backward_slice, self:None}
seen = {self}
fast_stack = [(self, iter(self.src))]
while fast_stack:
node, children = fast_stack[-1]
for child in children:
if child in seen: continue
seen.add(child)
fast_stack.append((child, iter(child.src)))
break
else:
cache[node] = None
fast_stack.pop()
return cache
if gate is not None and not gate(self): return cache
seen = {self}
stack: list[tuple[UOp, int]] = [(self, 0)]
while stack:
node, visited = stack.pop()
if node in cache: continue
if not visited:
if gate is None or gate(node):
stack.append((node, True)) # push node back on stack to process after its srcs
for s in reversed(node.src if enter_calls or node.op not in {Ops.CALL, Ops.FUNCTION} else node.src[1:]):
stack.append((s, False)) # push srcs on the stack
else: cache[node] = None # second time i'm seeing this node, add it to returned toposort
node, idx = stack[-1]
src = node.src if enter_calls or node.op not in {Ops.CALL, Ops.FUNCTION} else node.src[1:]
if idx == len(src):
cache[node] = None
stack.pop()
continue
stack[-1] = (node, idx+1)
child = src[idx]
if child in seen: continue
seen.add(child)
if gate is None or gate(child): stack.append((child, 0))
return cache
def topovisit(self, visitor:Callable[[UOp], T], cache:dict[UOp, T]) -> T:
@@ -479,10 +509,13 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def simplify(self, tracked=False):
if self.op is Ops.CONST: return self
if self.op is Ops.SINK and all(s.op is Ops.CONST or (s.op is Ops.STACK and len(s.src) == 0) for s in self.src): return self
if not tracked and (cached:=self.__dict__.get('_simplified', SENTINEL)) is not SENTINEL: return cached
# late import!
from tinygrad.uop.symbolic import symbolic
with Context(TRACK_MATCH_STATS=0 if not tracked else TRACK_MATCH_STATS.value):
return graph_rewrite(self, symbolic, name="simplify")
ret = graph_rewrite(self, symbolic, name="simplify")
if not tracked: self.__dict__['_simplified'] = ret
return ret
def ssimplify(self) -> UOp|ConstType: return ret.arg if (ret:=self.simplify()).op is Ops.CONST else ret
def _eval(self, dtype, expected_type:Type[T]) -> T:
assert self.dtype in dtype, f"eval with wrong dtype {self}"
@@ -496,6 +529,18 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def substitute(self, dvars:dict[UOp, UOp], name:str|None=None, extra_pm:PatternMatcher|None=None, walk:bool=False, enter_calls:bool=False):
dvars = {k:v for k,v in dvars.items() if k is not v}
if len(dvars) == 0: return self
use_fast = extra_pm is None and not walk and not TRACK_MATCH_STATS and not PROFILE
if name is None:
key = (tuple(dvars.items()), extra_pm, walk, enter_calls)
cache = self.__dict__.setdefault('_substitute_cache', {})
if (cached:=cache.get(key, SENTINEL)) is not SENTINEL: return cached
with Context(TRACK_MATCH_STATS=0):
ret = _substitute_fast(self, dvars, enter_calls) if use_fast else \
graph_rewrite(self, (extra_pm+_substitute) if extra_pm is not None else _substitute, dvars,
bottom_up=True, walk=walk, enter_calls=enter_calls)
cache[key] = ret
return ret
if use_fast: return _substitute_fast(self, dvars, enter_calls)
with Context(TRACK_MATCH_STATS=(0 if name is None else TRACK_MATCH_STATS.value)):
return graph_rewrite(self, (extra_pm+_substitute) if extra_pm is not None else _substitute, dvars,
bottom_up=True, walk=walk, enter_calls=enter_calls, name=name)
@@ -513,7 +558,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
# *** uop syntactic sugar ***
def sink(*srcs:UOp|None, **kwargs): # pylint: disable=no-self-argument
return UOp(Ops.SINK, src=tuple([x for x in srcs if x is not None]), **kwargs)
return UOp(Ops.SINK, kwargs.pop("dtype", dtypes.void), src=tuple([x for x in srcs if x is not None]), **kwargs)
def maketuple(*srcs:UOp): # pylint: disable=no-self-argument
return UOp(Ops.TUPLE, src=srcs)
def gettuple(self, idx:int) -> UOp:
@@ -526,7 +571,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
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]
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)
return UOp(Ops.INDEX, kwargs.pop("dtype", self.dtype), src=(self,)+tuple(new_srcs), **kwargs)
def __getitem__(self, idx):
# buffers index into INDEX UOps (scalar lookup); everything else uses the shared mixin view path
if self.addrspace in (None, AddrSpace.ALU) or self.device is not None: return super(UOp, self).__getitem__(idx)
@@ -549,7 +594,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return UOp.const(dtype or self.dtype, b, shape=self._shape)
def vconst_like(self, b:ConstLike, dtype:DType|None=None):
# for use after movement ops have been removed
return UOp.const(dtype or self.dtype, b).broadcast(self.max_numel())
ret = UOp.const(dtype or self.dtype, b)
if self.shape == (): return ret
if len(self.shape) == 1: return UOp(Ops.STACK, ret.dtype, src=(ret,)*self.max_numel())
raise RuntimeError(f"vconst_like only works on 0 or 1D shapes, not {self.shape}")
def ufix(self, x):
if isinstance(x, UOp): return x
# float self keeps its dtype for any scalar, int self only for int/Invalid scalars
@@ -557,20 +605,20 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return self.const_like(x, dtypes.from_py(x))
def broadcast(self, count:int):
if count == 1: return self
return UOp(Ops.STACK, src=(self,)*count)
return UOp(Ops.STACK, self.dtype, src=(self,)*count)
def cast(self, dtype:DTypeLike):
dtype = to_dtype(dtype)
if self.dtype == dtype: return self
return UOp(Ops.CAST, arg=dtype, src=(self,))
return UOp(Ops.CAST, dtype, arg=dtype, src=(self,))
def bitcast(self, dtype:DTypeLike):
dtype = to_dtype(dtype)
return self if self.dtype == dtype else UOp(Ops.BITCAST, arg=dtype, src=(self,))
def load(self, *src:UOp, **kwargs): return UOp(Ops.LOAD, src=(self,)+src, **kwargs)
def load(self, *src:UOp, **kwargs): return UOp(Ops.LOAD, kwargs.pop("dtype", self.dtype), src=(self,)+src, **kwargs)
def store(self, src:UOp|ConstType, gate:UOp|None=None, **kwargs):
srcs = (self, self.const_like(src) if not isinstance(src, UOp) else src) + ((gate,) if gate is not None else ())
return UOp(Ops.STORE, src=srcs, **kwargs)
def end(self, *src:UOp): return UOp(Ops.END, src=(self,)+src) if len(src) else self
def after(self, *src:UOp, **kwargs): return UOp(Ops.AFTER, src=(self,)+src, **kwargs) if len(src) else self
return UOp(Ops.STORE, kwargs.pop("dtype", dtypes.void), src=srcs, **kwargs)
def end(self, *src:UOp): return UOp(Ops.END, dtypes.void, src=(self,)+src) if len(src) else self
def after(self, *src:UOp, **kwargs): return UOp(Ops.AFTER, kwargs.pop("dtype", self.dtype), src=(self,)+src, **kwargs) if len(src) else self
def barrier(self, *src:UOp): return UOp(Ops.BARRIER, src=(self,)+src)
def ins(self, arg, **kwargs): return UOp(Ops.INS, kwargs.pop("dtype", self.dtype), kwargs.pop("src", self.src), arg, kwargs.pop("tag", self.tag))
def contract(self, *rngs:UOp):
@@ -583,7 +631,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if (shapes := [s for x in all_srcs if (s:=x._shape)]) and not all_same(shapes):
out_shape = _broadcast_shape(*shapes)
all_srcs = tuple(x._broadcast_to(out_shape) if x._shape else x for x in all_srcs)
return UOp(op, src=all_srcs, **kwargs)
dtype = kwargs.pop("dtype", dtypes.bool if op in GroupOp.Comparison else all_srcs[1].dtype if op is Ops.WHERE else all_srcs[0].dtype)
return UOp(op, dtype, src=all_srcs, **kwargs)
@staticmethod
def const(dtype:DType, b:ConstLike, shape:tuple[sint, ...]|None=None):
if isinstance(b, UOp): return b.cast(dtype)
@@ -596,9 +645,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
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):
return UOp(Ops.RANGE, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
return UOp(Ops.RANGE, dtype, 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 special(end:sint, name:str, dtype=dtypes.index): return UOp(Ops.SPECIAL, dtype, src=(sint_to_uop(end, dtype),), arg=name)
def _rop(self, op:Ops, axis:tuple[int, ...]):
# NOTE: we don't allow reduce on 1s axis
axis = tuple(sorted(axis))
@@ -624,7 +673,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def reduce(self, *src:UOp, **kwargs):
arg = kwargs.pop('arg', None)
if isinstance(arg, Ops): arg = (arg, 0)
return UOp(Ops.REDUCE, src=(self,)+src, arg=arg, **kwargs)
return UOp(Ops.REDUCE, kwargs.pop("dtype", self.dtype), src=(self,)+src, arg=arg, **kwargs)
def contiguous(self, *args, **kwargs):
if self.op is Ops.CONTIGUOUS: return self
@@ -750,8 +799,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
return UOp(Ops.STACK, src=tuple(u.cast(dtype_from_uop(Ops.STACK, srcs, None)) for u in srcs))
case _: raise RuntimeError(f"{op} is not a MovementOp")
usrcs = [shape_to_shape_arg(arg) for arg in src_args]
if len(usrcs) == 0: return UOp(op, src=(self,), arg=arg)
return UOp(op, src=(self,)+UOp.sink(*usrcs).simplify().src)
if len(usrcs) == 0: return UOp(op, self.dtype, src=(self,), arg=arg)
return UOp(op, self.dtype, src=(self,)+UOp.sink(*usrcs).simplify().src)
# *** uop Buffer stuff ***
@@ -1150,7 +1199,12 @@ class ProgramInfo:
return global_size, local_size
def vals(self, var_vals:dict[str, int]) -> tuple[int|None, ...]:
try: return tuple(var_vals[k.expr] if k.expr not in self.runtimevars else None for k in self.vars)
def _val(k:UOp) -> int|None:
if k.expr in self.runtimevars: return None
if k.expr in var_vals: return var_vals[k.expr]
if k.vmin == k.vmax: return int(k.vmin)
raise KeyError(k.expr)
try: return tuple(_val(k) for k in self.vars)
except KeyError as e: raise RuntimeError(f"unbound Variable {e} used by {self.function_name}") from None
@staticmethod
@@ -1382,7 +1436,8 @@ class PatternMatcher:
# uop is required, arg is optional
for p,fxn in self.patterns:
assert p.op is not None
entry: list = [p, None, p.early_reject]
entry: list = [p, None, next(iter(p.early_reject)) if len(p.early_reject) == 1 else None,
p.early_reject if len(p.early_reject) > 1 else None, p.match_dtype]
entry[1] = upat_deferred_compile(p, fxn, entry) if compiled else upat_interpret(p, fxn)
for uop in p.op: self.pdict.setdefault(uop, []).append(entry)
@@ -1392,10 +1447,15 @@ class PatternMatcher:
def __add__(self, more:PatternMatcher) -> PatternMatcher: return PatternMatcher(self.patterns+more.patterns)
def rewrite(self, uop:UOp, ctx=None):
if len(pats:=self.pdict.get(uop.op, [])):
if (ler:=uop.__dict__.get('_src_ops')) is None: uop.__dict__['_src_ops'] = ler = {u.op for u in uop.src}
for _,match,early_reject in pats:
if not early_reject.issubset(ler): continue
if pats:=self.pdict.get(uop.op):
ler = None
for _,match,single_reject,multi_reject,match_dtype in pats:
if match_dtype is not None and uop.dtype not in match_dtype: continue
if single_reject is not None or multi_reject is not None:
if ler is None and (ler:=uop.__dict__.get('_src_ops')) is None: uop.__dict__['_src_ops'] = ler = {u.op for u in uop.src}
if single_reject is not None:
if single_reject not in ler: continue
elif not multi_reject.issubset(ler): continue
if (ret:=match(uop, ctx)) is not None and ret is not uop: return ret
return None
@@ -1491,10 +1551,13 @@ class TrackedPatternMatcher(PatternMatcher):
if len(pats:=self.pdict.get(uop.op, [])):
ret = None
ler = {u.op for u in uop.src}
for p,match,early_reject in pats:
for p,match,single_reject,multi_reject,match_dtype in pats:
if p not in match_stats: match_stats[p] = [0,0,0.0,0.0]
st = time.perf_counter()
if not early_reject.issubset(ler):
if match_dtype is not None and uop.dtype not in match_dtype:
match_stats[p][2] += time.perf_counter()-st
continue
if (single_reject is not None and single_reject not in ler) or (multi_reject is not None and not multi_reject.issubset(ler)):
match_stats[p][2] += time.perf_counter()-st
continue
match_stats[p][1] += 1
@@ -1553,14 +1616,20 @@ if TRACK_MATCH_STATS or PROFILE:
# A pure Python sentinel, but *typed* as UOp so it fits all the dict annotations
SENTINEL: Final[UOp] = cast(UOp, object())
class BottomUpGate(Exception): pass
rewrite_caches: list[dict[Any, dict[UOp, UOp]]] = []
class scoped_rewrite_cache:
def __enter__(self): rewrite_caches.append({})
def __exit__(self, *args): rewrite_caches.pop()
class RewriteContext:
def __init__(self, pm, bpm, ctx=None, enter_calls=False):
def __init__(self, pm, bpm, ctx=None, enter_calls=False, shared_cache=None):
self.pm: PatternMatcher|None = pm
self.bpm: PatternMatcher|None = bpm
self.bpm_cache: dict[UOp, UOp|None] = {}
self.ctx = ctx
self.replace: dict[UOp, UOp] = {}
self.enter_calls = enter_calls
self.shared_cache: dict[UOp, UOp]|None = shared_cache
# no cache needed: pm_rewrite is called at most once per UOp due to the replace dict check in unified_rewrite
def pm_rewrite(self, x:UOp) -> UOp|None: return unwrap(self.pm).rewrite(x, self.ctx)
@@ -1596,74 +1665,96 @@ class RewriteContext:
return self.replace.get(root, root)
def unified_rewrite(self, root:UOp) -> UOp:
stack: collections.deque[tuple[UOp, int, UOp]] = collections.deque([(root, 0, root)])
on_stack = {root} # all UOps either on the stack or in self.replace, i.e. dont have to be placed again
stack: list[tuple[UOp, int, UOp]] = [(root, 0, root)]
self.replace[root] = SENTINEL # SENTINEL marks UOps scheduled but not rewritten yet
waitlist: dict[UOp, list[tuple[UOp, int, UOp]]] = {} # UOps waiting on a dependency to be in self.replace
replace, ctx, stack_limit, enter_calls, shared_cache = self.replace, self.ctx, REWRITE_STACK_LIMIT.value, self.enter_calls, self.shared_cache
bpm, cached_bpm_rewrite = self.bpm, self.cached_bpm_rewrite
pm_rewrite = self.pm.rewrite if self.pm is not None else None
stack_pop, stack_append, stack_extend = stack.pop, stack.append, stack.extend
replace_get, waitlist_pop, waitlist_setdefault = replace.get, waitlist.pop, waitlist.setdefault
while stack:
if len(stack) > REWRITE_STACK_LIMIT: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
n, stage, new_n = stack.pop()
if n in self.replace: continue # skip any nodes we have seen
n, stage, new_n = stack_pop()
if stage == 0:
if shared_cache is not None and (cached:=shared_cache.get(n, SENTINEL)) is not SENTINEL:
replace[n] = cached
if (waiting:=waitlist_pop(n,None)) is not None: stack_extend(waiting)
continue
# if bottom up, we rewrite this node early. in both cases, we add its srcs to the stack
if self.bpm is not None:
if bpm is not None:
# apply rewrite rules until a fixed point is reached. may return `uop` itself if PatternMatcher doesn't match
test_n: UOp|None = n
seen = set()
try:
while test_n is not None:
if test_n in seen: raise RuntimeError("infinite loop in fixed_point_rewrite")
seen.add(test_n)
new_n, test_n = test_n, self.cached_bpm_rewrite(test_n)
new_n, test_n = n, cached_bpm_rewrite(n)
if test_n is not None:
seen = {n}
while test_n is not None:
if test_n in seen: raise RuntimeError("infinite loop in fixed_point_rewrite")
seen.add(test_n)
new_n, test_n = test_n, cached_bpm_rewrite(test_n)
except BottomUpGate:
# if the bpm matching raised a gate, we are done with this node and dont continue down the srcs
self.replace[n] = unwrap(test_n)
if n in waitlist: stack.extend(waitlist.pop(n))
replace[n] = unwrap(test_n)
if (waiting:=waitlist_pop(n,None)) is not None: stack_extend(waiting)
continue
stack.append((n, 1, new_n))
stack_append((n, 1, new_n))
# NOTE: CALL/FUNCTION are handled as a special case.
# The function that is called is not included in the graph_rewrite.
# If you want to graph_rewrite a call, you can
if not self.enter_calls and new_n.op in {Ops.CALL, Ops.FUNCTION}: self.replace[new_n.src[0]] = new_n.src[0]
if not enter_calls and (new_n.op is Ops.CALL or new_n.op is Ops.FUNCTION): replace[new_n.src[0]] = new_n.src[0]
for x in reversed(new_n.src):
if x in on_stack: continue
stack.append((x, 0, x))
on_stack.add(x)
if x in replace: continue
replace[x] = SENTINEL
stack_append((x, 0, x))
if len(stack) > stack_limit: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
elif stage == 1:
tmp = []
for x in new_n.src:
if (rx:=self.replace.get(x, SENTINEL)) is SENTINEL:
new_src = None
for i,x in enumerate(new_n.src):
if (rx:=replace_get(x, SENTINEL)) is SENTINEL:
# source not ready: register in waitlist instead of spinning
waitlist.setdefault(x, []).append((n, 1, new_n))
waitlist_setdefault(x, []).append((n, 1, new_n))
break
tmp.append(rx)
if rx is not x:
if new_src is None: new_src = list(new_n.src)
new_src[i] = rx
else:
# in stage 1, once all srcs are rewritten, rebuild (if changed) or run top-down rewrite
if (new_src:=tuple(tmp)) == new_n.src:
if new_src is None:
# if top down, do the rewrite. if no rewrite or bottom up, we are done rewriting this node so we add it to the dict
if self.pm is None or (new_src_n:=self.pm_rewrite(new_n)) is None:
self.replace[n] = new_n
if n in waitlist: stack.extend(waitlist.pop(n))
if pm_rewrite is None or (new_src_n:=pm_rewrite(new_n, ctx)) is None:
replace[n] = new_n
if shared_cache is not None: shared_cache[n] = new_n
if (waiting:=waitlist_pop(n,None)) is not None: stack_extend(waiting)
continue
else:
# if srcs changed from rewrites, construct a new UOp with the new srcs
new_src_n = UOp(new_n.op, new_n.dtype, new_src, new_n.arg, new_n.tag)
new_src_n = UOp(new_n.op, new_n.dtype, tuple(new_src), new_n.arg, new_n.tag)
# trigger a rewrite of new_src_n, then after that rewrite is done, link it back to n
stack.append((n, 2, new_src_n))
stack.append((new_src_n, 0, new_src_n))
stack_append((n, 2, new_src_n))
if new_src_n not in replace:
replace[new_src_n] = SENTINEL
stack_append((new_src_n, 0, new_src_n))
if len(stack) > stack_limit: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
else:
# in stage 2, we link the result of new_n to the result of n
if (replaced_new_n:=self.replace.get(new_n, SENTINEL)) is SENTINEL:
if (replaced_new_n:=replace_get(new_n, SENTINEL)) is SENTINEL:
# not ready: register in waitlist instead of spinning
waitlist.setdefault(new_n, []).append((n, 2, new_n))
waitlist_setdefault(new_n, []).append((n, 2, new_n))
else:
# otherwise we are done
self.replace[n] = replaced_new_n
if n in waitlist: stack.extend(waitlist.pop(n))
return self.replace[root]
replace[n] = replaced_new_n
if shared_cache is not None: shared_cache[n] = replaced_new_n
if (waiting:=waitlist_pop(n,None)) is not None: stack_extend(waiting)
if (ret:=replace[root]) is SENTINEL: raise RuntimeError("infinite loop in graph_rewrite")
return ret
@profile_matches
def graph_rewrite(sink:UOp, pm:PatternMatcher, ctx=None, bottom_up=False, name=None, bpm=None, walk=False, enter_calls=False) -> UOp:
rewrite_ctx = RewriteContext(pm if not bottom_up else None, pm if bottom_up else bpm, ctx, enter_calls)
cache_ctx = None if ctx is None else getattr(ctx, 'rewrite_cache_key', SENTINEL)
cache_key = pm if ctx is None else (pm, cache_ctx)
shared_cache = rewrite_caches[-1].setdefault(cache_key, {}) if rewrite_caches and cache_ctx is not SENTINEL and \
not bottom_up and bpm is None and not walk else None
rewrite_ctx = RewriteContext(pm if not bottom_up else None, pm if bottom_up else bpm, ctx, enter_calls, shared_cache)
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)
@@ -1706,6 +1797,40 @@ pm_lower_index_dtype = PatternMatcher([
])
def _index_to_concrete_int(u:UOp) -> UOp: return graph_rewrite(u.sink(), pm_lower_index_dtype).src[0]
def _substitute_fast(root:UOp, dvars:dict[UOp, UOp], enter_calls:bool=False) -> UOp:
replace:dict[UOp, UOp] = {}
stack:list[tuple[UOp, int, UOp]] = [(root, 0, root)]
stack_limit = REWRITE_STACK_LIMIT.value
while stack:
n, stage, new_n = stack.pop()
if n in replace: continue
if stage == 0:
seen = None
while (mapped:=dvars.get(new_n, SENTINEL)) is not SENTINEL:
if seen is None: seen = {new_n}
elif new_n in seen: raise RuntimeError("infinite loop in fixed_point_rewrite")
else: seen.add(new_n)
new_n = mapped
stack.append((n, 1, new_n))
if not enter_calls and new_n.op in {Ops.CALL, Ops.FUNCTION}: replace[new_n.src[0]] = new_n.src[0]
for x in reversed(new_n.src):
if x not in replace: stack.append((x, 0, x))
if len(stack) > stack_limit: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
elif stage == 1:
new_src = None
for i,x in enumerate(new_n.src):
if (rx:=replace[x]) is not x:
if new_src is None: new_src = list(new_n.src)
new_src[i] = rx
if new_src is None: replace[n] = new_n
else:
rebuilt = UOp(new_n.op, new_n.dtype, tuple(new_src), new_n.arg, new_n.tag)
stack.append((n, 2, rebuilt))
stack.append((rebuilt, 0, rebuilt))
if len(stack) > stack_limit: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
else: replace[n] = replace[new_n]
return replace[root]
_substitute = PatternMatcher([(UPat(tuple(Ops), name="x"), lambda ctx,x: ctx.get(x,None))])
_pm_resolve_params = PatternMatcher([(UPat(Ops.PARAM, name="p"), lambda ctx,p: ctx[p.arg.slot])])
remove_all_tags = PatternMatcher([(UPat(GroupOp.All, name="x"), lambda x: x.replace(tag=None) if x.tag is not None else None)])
+18 -3
View File
@@ -1,5 +1,5 @@
# all of symbolic lives here now
import math, struct
import math, struct, functools
from collections import defaultdict
from tinygrad.uop.ops import Ops, PatternMatcher, UPat, UOp, GroupOp, exec_alu
from tinygrad.dtype import PyConst, ConstType, dtypes, can_lossless_cast, Invalid
@@ -46,10 +46,18 @@ def _quotient_base(q:UOp, base:UOp, div:int) -> UOp|None:
if p is not x or (t:=xa + a - pa) % D: return None
return base - k*div if (k:=t//D - s) else base
def _has_scaled_mod_term(x:UOp) -> bool:
if (cached:=x.__dict__.get("_has_scaled_mod_term")) is not None: return cached
if x.op is Ops.ADD: ret = any(_has_scaled_mod_term(s) for s in x.src)
else: ret = x.op is Ops.FLOORMOD or (x.op is Ops.MUL and any(s.op is Ops.FLOORMOD for s in x.src))
x.__dict__["_has_scaled_mod_term"] = ret
return ret
def fold_add_divmod_recombine(x:UOp) -> UOp|None:
# a scaled mod (base%div)*mul recombines with a partner q*(div*mul) carrying the quotient of a b == base (mod div):
# q == b//div -> b*mul (full recombine)
# q == (b//div)%d -> (b%(div*d))*mul (partial recombine into a wider mod, needs d>0)
if not _has_scaled_mod_term(x): return None
terms = list(x.split_uop(Ops.ADD))
for i,u in enumerate(terms):
mod, mul = u.pop_const(Ops.MUL)
@@ -303,8 +311,10 @@ def parse_valid(v:UOp) -> tuple[UOp, bool, int]|None:
return v.src[0], True, int((v.src[1]).vmax)-1
return None
def uop_given_valid(valid:UOp, uop:UOp, try_simplex=True) -> UOp:
@functools.cache
def uop_given_valid(valid:UOp, uop:UOp, try_simplex=False) -> UOp:
# return simplified uop (might be the same as input)
if valid.op is Ops.CONST: return uop
# first, parse valid into {expr: (lower_bound, upper_bound)}
bounds:defaultdict[UOp, list[PyConst|None]] = defaultdict(lambda: [None, None])
@@ -312,6 +322,11 @@ def uop_given_valid(valid:UOp, uop:UOp, try_simplex=True) -> UOp:
if (res:=parse_valid(stmt)) is None: continue
expr, is_upper, c = res
bounds[expr][int(is_upper)] = c
if not bounds: return uop
if not try_simplex:
uop_nodes = uop.backward_slice_with_self
bounds = defaultdict(lambda: [None, None], ((expr, v) for expr,v in bounds.items() if expr in uop_nodes))
if not bounds: return uop
# simplify uop given that valid is True
all_candidates = []
@@ -399,7 +414,7 @@ def gated_given_valid(cond:UOp, x:UOp, i:UOp) -> UOp|None:
if x.dtype is not dtypes.index: 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)
return cond.where(new_x, i) if (new_x:=uop_given_valid(cond, x, try_simplex=False)) is not x else None
# TODO: this is O(number of WHERE * number of node)
# def fold_where_closure(cond:UOp, t:UOp, f:UOp) -> UOp|None:
+13 -10
View File
@@ -1,7 +1,7 @@
from typing import Any, Callable
import itertools, inspect, functools, types
from tinygrad.helpers import partition, dedup, Context
from tinygrad.uop.ops import UPat, UOp, Ops, PatternMatcher, graph_rewrite, deconstruct_function
from tinygrad.uop.ops import UPat, UOp, Ops, GroupOp, PatternMatcher, graph_rewrite, deconstruct_function
class UPatCompileError(Exception): pass
@@ -11,22 +11,24 @@ class UPatCompileError(Exception): pass
# STORE (bind a matched UOp to a name), PYLITERAL (Python literal for CUSTOM operands),
# AND/OR (clause combininers).
def _get_clause(self:UPat, base:UOp, depth=0) -> UOp:
def _get_clause(self:UPat, base:UOp, depth=0, root=True) -> UOp:
if self.is_any:
assert len(self.src) == 1
return UOp(Ops.AND, src=(UOp(Ops.OR, src=tuple(_get_clause(s, base, depth) for s in self.src[0])),))
return UOp(Ops.AND, src=(UOp(Ops.OR, src=tuple(_get_clause(s, base, depth, False) for s in self.src[0])),))
# build the and_clause for acceptance
and_clause:list[UOp] = []
if self.op is not None:
if self.op is not None and not root:
if len(self.op) > 1: and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.PYLITERAL, arg=tuple(int(x) for x in self.op))), arg="{0}.op in {1}"))
else: and_clause.append(UOp(Ops.CUSTOM, src=(base,), arg="{0}.op == "+str(self.op[0].value)))
if self.arg is not None:
if isinstance(self.arg, int): and_clause.append(UOp(Ops.CUSTOM, src=(base,), arg="{0}.arg == "+str(int(self.arg))))
else: and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.PYLITERAL, arg=self.arg)), arg="{0}.arg == {1}"))
if self.strict_length or self.required_len > 0:
fixed_root_len = root and self.op is not None and all((op in GroupOp.Unary and self.required_len == 1) or
(op in GroupOp.Binary and self.required_len == 2) or (op in GroupOp.Ternary and self.required_len == 3) for op in self.op)
if (self.strict_length or self.required_len > 0) and not fixed_root_len:
and_clause.append(UOp(Ops.CUSTOM, src=(base,), arg=("len({0}.src)"+(" == " if self.strict_length else " >= ")+str(self.required_len))))
if self.name is not None: and_clause.append(UOp(Ops.STORE, src=(UOp(Ops.CUSTOMI, arg=self.name), base)))
if self.match_dtype is not None:
if self.match_dtype is not None and not root:
if len(self.match_dtype) > 1:
and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.PYLITERAL, arg=tuple(self.match_dtype))),
arg="{0}.dtype in {1}"))
@@ -40,18 +42,19 @@ def _get_clause(self:UPat, base:UOp, depth=0) -> UOp:
if self.src is not None:
# single match
if len(self.src) == 1 and isinstance(self.src[0], tuple):
and_clause += [_get_clause(s, base.index(i), depth) for i,s in enumerate(self.src[0])]
and_clause += [_get_clause(s, base.index(i), depth, False) for i,s in enumerate(self.src[0])]
# repeat match
elif len(self.src) == 1 and isinstance(self.src[0], itertools.repeat):
it = UOp(Ops.CUSTOMI, arg=f"ituop{depth}")
match = _get_clause(next(self.src[0]), it, depth+1)
match = _get_clause(next(self.src[0]), it, depth+1, False)
and_clause.append(UOp(Ops.CUSTOM, src=(match, it, base), arg="all([{0} for {1} in {2}.src])"))
# multi match (fork)
elif len(self.src) > 1 and all(isinstance(x, tuple) for x in self.src):
fork_cond = [UOp(Ops.AND, src=tuple([_get_clause(s, base.index(i), depth) for i,s in enumerate(ss)])) for ss in self.src]
fork_cond = [UOp(Ops.AND, src=tuple([_get_clause(s, base.index(i), depth, False) for i,s in enumerate(ss)])) for ss in self.src]
and_clause.append(UOp(Ops.OR, src=tuple(fork_cond)))
else: raise RuntimeError("broken")
return UOp(Ops.AND, src=tuple(and_clause)) if and_clause else UOp(Ops.CUSTOMI, arg="True")
if and_clause: return UOp(Ops.AND, src=tuple(and_clause))
return UOp(Ops.AND, src=(UOp(Ops.CUSTOMI, arg="True"),)) if root else UOp(Ops.CUSTOMI, arg="True")
# *** pattern matcher ***