forked from tinygrad/tinygrad
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6977efe8 | ||
|
|
1b195e76ac | ||
|
|
3856c334a8 |
+68
-36
@@ -10,7 +10,7 @@ from extra.lr_scheduler import OneCycleLR
|
||||
from tinygrad import nn, dtypes, Tensor, Device, GlobalCounters, TinyJit, Variable
|
||||
from tinygrad.nn.state import get_state_dict
|
||||
from tinygrad.nn import optim
|
||||
from tinygrad.helpers import Context, BEAM, WINO, getenv, colored, prod, TRAINING
|
||||
from tinygrad.helpers import Context, BEAM, WINO, getenv, colored, prod, TRAINING, disable_gc
|
||||
from extra.bench_log import BenchEvent, WallTimeEvent
|
||||
|
||||
cifar_mean = [0.4913997551666284, 0.48215855929893703, 0.4465309133731618]
|
||||
@@ -79,16 +79,16 @@ class ConvGroup:
|
||||
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 +111,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
|
||||
@@ -144,6 +147,7 @@ hyp = {
|
||||
},
|
||||
}
|
||||
|
||||
@disable_gc()
|
||||
def train_cifar():
|
||||
|
||||
def set_seed(seed):
|
||||
@@ -152,23 +156,20 @@ def train_cifar():
|
||||
|
||||
# ========== Model ==========
|
||||
def whitening(X, kernel_size=hyp['net']['kernel_size']):
|
||||
def _cov(X):
|
||||
return (X.T @ X) / (X.shape[0] - 1)
|
||||
|
||||
def _patches(data, patch_size=(kernel_size,kernel_size)):
|
||||
def _patches(data:Tensor, patch_size=(kernel_size,kernel_size)):
|
||||
h, w = patch_size
|
||||
c = data.shape[1]
|
||||
axis = (2, 3)
|
||||
return np.lib.stride_tricks.sliding_window_view(data, window_shape=(h,w), axis=axis).transpose((0,3,2,1,4,5)).reshape((-1,c,h,w))
|
||||
_, c, H, W = data.shape
|
||||
return Tensor.stack(*[data[:, ch, y:y+H-h+1, x:x+W-w+1].permute(0, 2, 1).flatten()
|
||||
for ch in range(c) for y in range(h) for x in range(w)])
|
||||
|
||||
def _eigens(patches):
|
||||
n,c,h,w = patches.shape
|
||||
Σ = _cov(patches.reshape(n, c*h*w))
|
||||
n = patches.shape[1]
|
||||
Σ = ((patches @ patches.T) / (n - 1)).numpy()
|
||||
Λ, V = np.linalg.eigh(Σ, UPLO='U')
|
||||
return np.flip(Λ, 0), np.flip(V.T.reshape(c*h*w, c, h, w), 0)
|
||||
return np.flip(Λ, 0), np.flip(V.T.reshape(patches.shape[0], X.shape[1], kernel_size, kernel_size), 0)
|
||||
|
||||
# NOTE: np.linalg.eigh only supports float32 so the whitening layer weights need to be converted to float16 manually
|
||||
Λ, V = _eigens(_patches(X.float().numpy()))
|
||||
Λ, V = _eigens(_patches(X.float()))
|
||||
W = V/np.sqrt(Λ+1e-2)[:,None,None,None]
|
||||
|
||||
return Tensor(W.astype(np.float32)).cast(dtypes.default_float).is_param_(False)
|
||||
@@ -221,15 +222,23 @@ 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 expensivne to generate
|
||||
def shuffled_augmentations(X:Tensor, Y:Tensor, perms:Tensor):
|
||||
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, perms:Tensor):
|
||||
X, Y, _ = shuffled_augmentations(X, Y, perms)
|
||||
return X, Y
|
||||
|
||||
@TinyJit
|
||||
def augmentations_cutmix(X:Tensor, Y:Tensor, perms:Tensor):
|
||||
X, Y, perms = shuffled_augmentations(X, Y, perms)
|
||||
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
|
||||
@@ -239,8 +248,13 @@ 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
|
||||
# Cold-codegen for Tensor.randperm's bitonic sort is much slower than sorting these keys on the host.
|
||||
keys = Tensor.rand(X.shape[0], device=X.device).numpy()
|
||||
perms = Tensor(np.argsort(keys, kind="stable").astype(np.int32), device=X.device)
|
||||
if getenv("CUTMIX", 1) and step >= hyp['net']['cutmix_steps']:
|
||||
_, _, X, Y = augmentations_cutmix(X, Y, perms)
|
||||
else:
|
||||
X, Y = augmentations(X, Y, perms)
|
||||
et = time.monotonic()
|
||||
print(f"shuffling {'training' if is_train else 'test'} dataset in {(et-st)*1e3:.2f} ms ({epoch=})")
|
||||
|
||||
@@ -285,6 +299,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'])
|
||||
@@ -301,7 +324,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:
|
||||
@@ -325,7 +348,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'])
|
||||
|
||||
@@ -333,20 +356,24 @@ def train_cifar():
|
||||
# index 0 for bias and 1 for non-bias
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
lr_scheduler[0].step()
|
||||
lr_scheduler[1].step()
|
||||
Tensor.realize(loss, *optimizer.schedule_step(), *lr_scheduler[0].schedule_step(), *lr_scheduler[1].schedule_step())
|
||||
return loss
|
||||
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)
|
||||
def eval_forward(model, X):
|
||||
return model(X).realize()
|
||||
|
||||
def eval_step(out, out_flipped, Y):
|
||||
out = (out + out_flipped) / 2.
|
||||
loss = cross_entropy(out, Y, reduction='mean')
|
||||
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)
|
||||
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
|
||||
@@ -376,11 +403,16 @@ def train_cifar():
|
||||
Xt.shard_(GPUS, axis=0)
|
||||
Yt.shard_(GPUS, axis=0)
|
||||
|
||||
correct, loss = eval_step_jitted(model, Xt, Yt)
|
||||
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())
|
||||
correct, loss = eval_step_jitted(out, out_flipped, Yt)
|
||||
losses.append(loss.numpy().tolist())
|
||||
corrects.extend(correct.numpy().tolist())
|
||||
if model_ema:
|
||||
correct_ema, loss_ema = eval_step_ema_jitted(model_ema.net_ema, Xt, Yt)
|
||||
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())
|
||||
correct_ema, loss_ema = eval_step_ema_jitted(out_ema, out_flipped_ema, Yt)
|
||||
losses_ema.append(loss_ema.numpy().tolist())
|
||||
corrects_ema.extend(correct_ema.numpy().tolist())
|
||||
|
||||
@@ -433,5 +465,5 @@ def train_cifar():
|
||||
raise ValueError(colored(f"{eval_acc_pct=} < {target}", "red"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
with WallTimeEvent(BenchEvent.FULL):
|
||||
with Context(SPEC=0, SCACHE=0, TRACEMETA=0), WallTimeEvent(BenchEvent.FULL):
|
||||
train_cifar()
|
||||
|
||||
@@ -112,6 +112,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.vectorize(*src).reshape(b.shape)
|
||||
|
||||
@functools.cache
|
||||
def shape_indexes(shape:tuple[int, ...]) -> tuple[tuple[UOp, ...], ...]:
|
||||
return tuple(tuple(UOp.const(dtypes.weakint, 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, wmma.dtype, (wmma.src[0], wmma.src[1], wmma.src[2]+add), wmma.arg)),
|
||||
@@ -127,15 +131,22 @@ 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[-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) == (): return None
|
||||
# broadcasting needs to be already unpacked
|
||||
if not all_same([x.shape for x in b.src]): return None
|
||||
src = []
|
||||
for idx in itertools.product(*[range(x) for x in b.shape]):
|
||||
idx_c = [UOp.const(dtypes.weakint, i) for i in idx]
|
||||
src.append(b.replace(src=tuple([x.index(*idx_c) for x in b.src])))
|
||||
return UOp.vectorize(*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(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)
|
||||
|
||||
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 do_stack_wmma(u:UOp):
|
||||
if all(x.op in (Ops.STACK, Ops.WMMA) for x in u.src): return None
|
||||
@@ -151,11 +162,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),
|
||||
# INDEX without src is nothing (TODO: this should be in mop_cleanup)
|
||||
(UPat(Ops.INDEX, src=(UPat.var('x'),)), lambda x: x),
|
||||
# unpack WMMA
|
||||
@@ -306,9 +319,6 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
# 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")
|
||||
|
||||
# some coalesing misses without this
|
||||
sink = graph_rewrite(sink, sym, name="early symbolic")
|
||||
|
||||
@@ -316,9 +326,6 @@ def full_rewrite_to_sink(ast:UOp, ren:Renderer, optimize:bool=True) -> UOp:
|
||||
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)
|
||||
|
||||
# extra symbolic before decomp. crashes without this?
|
||||
sink = graph_rewrite(sink, sym, name="extra symbolic")
|
||||
|
||||
# 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")
|
||||
@@ -442,21 +449,28 @@ def do_to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
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 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")
|
||||
# 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], uops=prg.src[1].src))
|
||||
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 len(prg.src) == 3 and (compiled:=do_compile(renderer, prg, prg.src[2])) is not None: prg = compiled
|
||||
if VIZ: graph_rewrite(prg, PatternMatcher([]), name="View Program")
|
||||
return prg
|
||||
|
||||
to_program_cache: dict[tuple, UOp] = {}
|
||||
def to_program(ast:UOp, renderer:Renderer) -> UOp:
|
||||
config = (NOOPT, EMULATED_DTYPES, NOLOCALS, USE_TC, IMAGE, DISABLE_FAST_IDIV, TRANSCENDENTAL, ALLOW_TF32)
|
||||
key = (ast.key, type(renderer), renderer.target, *[x.value for x in config])
|
||||
# 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, *[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)
|
||||
return prg
|
||||
|
||||
@@ -114,7 +114,7 @@ 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() < 16):
|
||||
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]):
|
||||
@@ -143,7 +143,7 @@ def hand_coded_optimizations(k:Scheduler) -> Scheduler:
|
||||
# NOTE: this can fail on multireduce with mismatching dimensions, this is okay
|
||||
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:
|
||||
|
||||
@@ -21,8 +21,9 @@ pm_flatten_range = PatternMatcher([
|
||||
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:
|
||||
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)):
|
||||
for r0, r1 in (zip(u.ended_ranges, u.ended_ranges[1:]) if u.op is Ops.END else itertools.combinations(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
|
||||
@@ -34,8 +35,8 @@ def simplify_merge_adjacent(u:UOp) -> UOp|None:
|
||||
name=f"check_merge_{r0.arg[0]}_{r1.arg[0]}")
|
||||
|
||||
# check if it simplifies
|
||||
if count_divmod(nidx) <= count_divmod(u):
|
||||
u = nidx
|
||||
if (new_divmod_count:=count_divmod(nidx)) <= divmod_count:
|
||||
u, divmod_count = nidx, new_divmod_count
|
||||
return u
|
||||
|
||||
def mark_gated(ctx, idx):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TypeVar, Generic, Callable, Any
|
||||
import functools, collections
|
||||
from tinygrad.tensor import Tensor, all_tensors
|
||||
from tinygrad.helpers import flatten, merge_dicts, DEBUG, Context, BEAM, getenv, JIT, JIT_BATCH_SIZE, dedup, pluralize, VIZ
|
||||
from tinygrad.helpers import flatten, merge_dicts, DEBUG, Context, BEAM, getenv, JIT, JIT_BATCH_SIZE, dedup, pluralize, VIZ, disable_gc
|
||||
from tinygrad.device import Buffer, Compiled, Device, MultiBuffer
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
from tinygrad.uop.ops import UOp, PatternMatcher, Variable, sym_infer, Ops, buffers, track_rewrites, graph_rewrite
|
||||
@@ -248,18 +248,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):
|
||||
@@ -268,6 +269,7 @@ class TinyJit(Generic[ReturnType]):
|
||||
|
||||
def __get__(self, obj, objtype): return functools.partial(self.__call__, obj) # add support for instance methods
|
||||
|
||||
@disable_gc()
|
||||
def __call__(self, *args, **kwargs) -> ReturnType:
|
||||
input_buf_uops, var_vals, names, expected_input_info = _prepare_jit_inputs(args, kwargs)
|
||||
if not JIT or self.cnt == 0:
|
||||
|
||||
@@ -26,9 +26,8 @@ class Estimates:
|
||||
mult_stack: list[sint] = []
|
||||
excluded: set[UOp] = set()
|
||||
if ignore_indexing:
|
||||
for u in uops:
|
||||
if u.op in {Ops.INDEX, Ops.SHRINK}:
|
||||
excluded = excluded.union(set(UOp.sink(*u.src[1:]).toposort(lambda x: x.op is not Ops.END)))
|
||||
indexing_srcs = [s for u in uops if u.op in {Ops.INDEX, Ops.SHRINK} for s in u.src[1:]]
|
||||
if indexing_srcs: excluded.update(UOp.sink(*indexing_srcs).toposort(lambda x: x.op is not Ops.END))
|
||||
for u in uops:
|
||||
if u.op in {Ops.LOAD, Ops.STORE}:
|
||||
buf = u
|
||||
|
||||
@@ -503,8 +503,10 @@ 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_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.TRUNC: _ocml("trunc"), Ops.SIN: _ocml("sin"),
|
||||
Ops.LOG2: _ocml("log2"), Ops.EXP2: _ocml("exp2"), Ops.SQRT: _ocml("sqrt")}
|
||||
smem_prefix = "__attribute__((shared, aligned(16)))"
|
||||
@@ -531,14 +533,12 @@ class HIPRenderer(CStyleLanguage):
|
||||
f"{vec} make_{vec}({', '.join([f'{scal} {x}' for x in _nms[:dtype.count]])}) {{ return {{ {', '.join(_nms[:dtype.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)]
|
||||
@@ -552,7 +552,7 @@ 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) for dt in used_dtypes if dt.count > 1]
|
||||
|
||||
for name, (N, M, K), dtype_in, dtype_out, _, _, _, _ in wmma_args(uops): # TODO: handle TCs f32_bf16 and bf16_bf16 w/ wrapper
|
||||
|
||||
@@ -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) -> 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()))
|
||||
@@ -75,7 +75,8 @@ def compile_hip(prg:str, arch="gfx1100", asm=False) -> bytes:
|
||||
"-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")
|
||||
@@ -96,7 +97,7 @@ class HIPCompiler(Compiler):
|
||||
self.arch = arch
|
||||
super().__init__(f"compile_hip_{self.arch}")
|
||||
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 disassemble(self, lib:bytes): amdgpu_disassemble(lib)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class LLVMCompiler(Compiler):
|
||||
llvm.LLVMPassBuilderOptionsSetLoopUnrolling(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetLoopVectorization(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetSLPVectorization(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetVerifyEach(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetVerifyEach(self.pbo, bool(getenv("LLVM_VERIFY")))
|
||||
else:
|
||||
self.passes = b'default<O0>'
|
||||
|
||||
|
||||
@@ -108,7 +108,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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Iterator
|
||||
from typing import Iterator, cast
|
||||
import functools, itertools
|
||||
from dataclasses import dataclass, field, replace
|
||||
from tinygrad.dtype import dtypes, AddrSpace
|
||||
@@ -120,6 +120,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.weakint, 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.weakint, 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)
|
||||
|
||||
+54
-27
@@ -64,6 +64,7 @@ def _align_left(*shapes:tuple[sint, ...]) -> tuple[tuple[sint, ...], ...]:
|
||||
max_dim = max(len(s) for s in shapes)
|
||||
return tuple((1,)*(max_dim-len(s))+s for s in shapes)
|
||||
def _broadcast_shape(*shapes:tuple[sint, ...]) -> tuple[sint, ...]:
|
||||
if all_same(shapes): return shapes[0]
|
||||
shaped_aligned_left = _align_left(*shapes)
|
||||
ret = tuple(0 if 0 in nth_dim_sizes else smax(nth_dim_sizes) for nth_dim_sizes in zip(*shaped_aligned_left))
|
||||
if not all(resolve(s == ns) or resolve(s == 1) for shape in shaped_aligned_left for s,ns in zip(shape, ret)):
|
||||
@@ -129,9 +130,13 @@ class recursive_property(property):
|
||||
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]
|
||||
while stack:
|
||||
if nm in (n:=stack[-1]).__dict__: stack.pop()
|
||||
elif todo:=[s for s in n.src if nm not in s.__dict__]: stack.extend(todo)
|
||||
else: n.__dict__[nm] = self.fxn(stack.pop())
|
||||
return x.__dict__[nm]
|
||||
|
||||
# we import this late so we can use resolve/smax in mixins
|
||||
from tinygrad.mixin.op import OpMixin
|
||||
@@ -146,6 +151,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
|
||||
arg:Any = None
|
||||
tag:Any = None
|
||||
def __del__(self):
|
||||
if sys.is_finalizing(): 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
|
||||
@@ -402,10 +408,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}"
|
||||
@@ -419,6 +428,7 @@ 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
|
||||
if name is None: return _cached_substitute(self, tuple(dvars.items()), extra_pm, walk, 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)
|
||||
@@ -1087,14 +1097,14 @@ class ProgramInfo:
|
||||
def vals(self, var_vals:dict[str, int]): return tuple(var_vals[k.expr] if k.expr not in self.runtimevars else None for k in self.vars)
|
||||
|
||||
@staticmethod
|
||||
def from_sink(sink:UOp, aux:tuple=()) -> ProgramInfo:
|
||||
def from_sink(sink:UOp, aux:tuple=(), uops:Iterable[UOp]|None=None) -> ProgramInfo:
|
||||
_vars: list[UOp] = []
|
||||
_globals: list[int] = []
|
||||
outs: list[int] = []
|
||||
ins: list[int] = []
|
||||
global_size: list[int] = [1, 1, 1]
|
||||
local_size: list[int]|None = [1, 1, 1]
|
||||
for u in sink.toposort():
|
||||
for u in sink.toposort() if uops is None else uops:
|
||||
if u.op is Ops.PARAM and u.addrspace == AddrSpace.ALU: _vars.append(u)
|
||||
if u.op is Ops.PARAM and u.addrspace != AddrSpace.ALU: _globals.append(u.arg.slot)
|
||||
if u.op in (Ops.STORE, Ops.LOAD):
|
||||
@@ -1148,7 +1158,8 @@ def exec_alu(op:Ops, dtype:DType, operands, truncate_output=True):
|
||||
return tuple([exec_alu(op, dtype, [x[i] if isinstance(x, tuple) else x for x in operands]) for i in range(count)])
|
||||
if dtype==dtypes.weakint and op in GroupOp.Binary and Invalid in operands: return Invalid
|
||||
alu = python_alu[op](*operands)
|
||||
return truncate.get(dtype, lambda x: x)(alu) if truncate_output else alu
|
||||
if truncate_output and (truncate_fxn:=truncate.get(dtype)) is not None: return truncate_fxn(alu)
|
||||
return alu
|
||||
|
||||
def bitcast(x, in_dtype:DType, out_dtype:DType):
|
||||
assert in_dtype.itemsize == out_dtype.itemsize, "bitcast itemsize mismatch"
|
||||
@@ -1306,6 +1317,7 @@ def upat_deferred_compile(p:UPat, fxn:Callable, entry:list) -> Callable:
|
||||
return lazy_compile
|
||||
|
||||
class PatternMatcher:
|
||||
__slots__ = ("patterns", "pdict")
|
||||
def __init__(self, patterns:Sequence[tuple[UPat, Callable|tuple]], compiled=bool(getenv("UPAT_COMPILE", 1))):
|
||||
# if this comes from a pickle, we reconstruct the lambda functions here
|
||||
self.patterns:list[tuple[UPat, Callable]] = [(p,types.FunctionType(*fxn) if isinstance(fxn, tuple) else fxn) for p,fxn in patterns]
|
||||
@@ -1314,7 +1326,7 @@ 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, p.early_reject or 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)
|
||||
|
||||
@@ -1324,10 +1336,13 @@ 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,early_reject,match_dtype in pats:
|
||||
if match_dtype is not None and uop.dtype not in match_dtype and uop.dtype._scalar not in match_dtype: continue
|
||||
if early_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 not early_reject.issubset(ler): continue
|
||||
if (ret:=match(uop, ctx)) is not None and ret is not uop: return ret
|
||||
return None
|
||||
|
||||
@@ -1423,10 +1438,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,early_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 and uop.dtype._scalar not in match_dtype:
|
||||
match_stats[p][2] += time.perf_counter()-st
|
||||
continue
|
||||
if early_reject is not None and not early_reject.issubset(ler):
|
||||
match_stats[p][2] += time.perf_counter()-st
|
||||
continue
|
||||
match_stats[p][1] += 1
|
||||
@@ -1486,6 +1504,7 @@ if TRACK_MATCH_STATS or PROFILE:
|
||||
SENTINEL: Final[UOp] = cast(UOp, object())
|
||||
class BottomUpGate(Exception): pass
|
||||
class RewriteContext:
|
||||
__slots__ = ("pm", "bpm", "bpm_cache", "ctx", "replace", "enter_calls")
|
||||
def __init__(self, pm, bpm, ctx=None, enter_calls=False):
|
||||
self.pm: PatternMatcher|None = pm
|
||||
self.bpm: PatternMatcher|None = bpm
|
||||
@@ -1528,16 +1547,19 @@ 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)])
|
||||
stack: list[tuple[UOp, int, UOp]] = [(root, 0, root)]
|
||||
on_stack = {root} # all UOps either on the stack or in self.replace, i.e. dont have to be placed again
|
||||
waitlist: dict[UOp, list[tuple[UOp, int, UOp]]] = {} # UOps waiting on a dependency to be in self.replace
|
||||
replace, ctx, stack_limit, enter_calls = self.replace, self.ctx, REWRITE_STACK_LIMIT.value, self.enter_calls
|
||||
bpm, cached_bpm_rewrite = self.bpm, self.cached_bpm_rewrite
|
||||
pm_rewrite = self.pm.rewrite if self.pm is not None else None
|
||||
while stack:
|
||||
if len(stack) > REWRITE_STACK_LIMIT: raise RuntimeError("infinite loop in graph_rewrite (stack too big)")
|
||||
if len(stack) > 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
|
||||
if n in replace: continue # skip any nodes we have seen
|
||||
if stage == 0:
|
||||
# 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()
|
||||
@@ -1545,17 +1567,17 @@ class RewriteContext:
|
||||
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 = 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)
|
||||
replace[n] = unwrap(test_n)
|
||||
if n in waitlist: stack.extend(waitlist.pop(n))
|
||||
continue
|
||||
stack.append((n, 1, new_n))
|
||||
# NOTE: CALL/FUNCTION are handled as a special case.
|
||||
# The function that is called is not included in the graph_rewrite.
|
||||
# If you want to graph_rewrite a call, you can
|
||||
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))
|
||||
@@ -1563,7 +1585,7 @@ class RewriteContext:
|
||||
elif stage == 1:
|
||||
tmp = []
|
||||
for x in new_n.src:
|
||||
if (rx:=self.replace.get(x, SENTINEL)) is SENTINEL:
|
||||
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))
|
||||
break
|
||||
@@ -1572,8 +1594,8 @@ class RewriteContext:
|
||||
# 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 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 pm_rewrite is None or (new_src_n:=pm_rewrite(new_n, ctx)) is None:
|
||||
replace[n] = new_n
|
||||
if n in waitlist: stack.extend(waitlist.pop(n))
|
||||
continue
|
||||
else:
|
||||
@@ -1584,14 +1606,14 @@ class RewriteContext:
|
||||
stack.append((new_src_n, 0, new_src_n))
|
||||
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))
|
||||
else:
|
||||
# otherwise we are done
|
||||
self.replace[n] = replaced_new_n
|
||||
replace[n] = replaced_new_n
|
||||
if n in waitlist: stack.extend(waitlist.pop(n))
|
||||
return self.replace[root]
|
||||
return replace[root]
|
||||
|
||||
@profile_matches
|
||||
def graph_rewrite(sink:UOp, pm:PatternMatcher, ctx=None, bottom_up=False, name=None, bpm=None, walk=False, enter_calls=False) -> UOp:
|
||||
@@ -1639,6 +1661,11 @@ pm_lower_index_dtype = PatternMatcher([
|
||||
def _index_to_concrete_int(u:UOp) -> UOp: return graph_rewrite(u.sink(), pm_lower_index_dtype).src[0]
|
||||
|
||||
_substitute = PatternMatcher([(UPat(tuple(Ops), name="x"), lambda ctx,x: ctx.get(x,None))])
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def _cached_substitute(uop:UOp, dvars:tuple[tuple[UOp, UOp], ...], extra_pm:PatternMatcher|None, walk:bool, enter_calls:bool) -> UOp:
|
||||
with Context(TRACK_MATCH_STATS=0):
|
||||
return graph_rewrite(uop, (extra_pm+_substitute) if extra_pm is not None else _substitute, dict(dvars),
|
||||
bottom_up=True, walk=walk, enter_calls=enter_calls)
|
||||
_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)])
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -304,8 +304,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])
|
||||
@@ -313,6 +315,7 @@ 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
|
||||
|
||||
# simplify uop given that valid is True
|
||||
all_candidates = []
|
||||
|
||||
+11
-9
@@ -1,28 +1,30 @@
|
||||
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
|
||||
|
||||
# **** UPat compiled ****
|
||||
|
||||
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.BIND, 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.BIND, 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.BIND, arg=tuple(self.match_dtype))), arg="({0}.dtype in {1} or {0}.dtype._scalar in {1})"))
|
||||
else: and_clause.append(UOp(Ops.CUSTOM, src=(base, UOp(Ops.BIND, arg=self.match_dtype[0])), arg="({0}.dtype == {1} or {0}.dtype._scalar == {1})"))
|
||||
@@ -33,15 +35,15 @@ 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.NOOP, 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.RANGE, 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))
|
||||
|
||||
Reference in New Issue
Block a user