Compare commits

..
8 Commits
Author SHA1 Message Date
chenyuandGitHub 42714e1399 update a few is CONST check to check device None [pr] (#17563)
* update a few is CONST check to check device None [pr]

* clone
2026-08-17 07:33:16 -04:00
chenyuandGitHub 821e80ff9a remove torch backend detach hack (#17565) 2026-08-17 07:33:05 -04:00
George HotzandGitHub 37a54dc7cf add some dels to jit for OOM fixes (#17566) 2026-08-16 23:39:37 -07:00
chenyuandGitHub e25f86721d more torch backend fixups (#17562) 2026-08-16 21:31:53 -04:00
chenyuandGitHub 138fb4a783 delete dead DType.scalar [PR] (#17561) 2026-08-16 21:12:17 -04:00
chenyuandGitHub bfd4048abf no dtype in vconst_like [PR] (#17560) 2026-08-16 21:06:38 -04:00
chenyuandGitHub 057a18a07c fix emulated long cast to double (#17559) 2026-08-16 20:52:42 -04:00
chenyuandGitHub c30bf116b7 few torch_backend fix (#17558)
* few torch_backend fix

* fix
2026-08-16 20:07:25 -04:00
25 changed files with 226 additions and 102 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ Documentation along with a quick start guide can be found on the [docs website](
```python
from tinygrad import Tensor
x = Tensor.eye(3)
x = Tensor.eye(3).clone() # clone to make it a buffer
y = Tensor([[2.0,0,-2.0]])
z = y.matmul(x).sum()
z.backward()
+1 -1
View File
@@ -209,7 +209,7 @@ class ST:
return cls(uop, rows, cols, layout, base_shape, ker)
def swizzle(self, row, col):
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype.scalar())
swizzled_offset = self.base_shape.swizzle(row, col, self._uop.dtype)
row = swizzled_offset // self.base_shape.cols
col = swizzled_offset % self.base_shape.cols
+57 -42
View File
@@ -4,7 +4,7 @@
# A006 Lambda argument `input` is shadowing a Python builtin
from tinygrad import Tensor, dtypes, Device
from tinygrad.uop.ops import Ops, GroupOp
from tinygrad.helpers import getenv, prod, strides_for_shape, argfix
from tinygrad.helpers import getenv, prod, strides_for_shape
import torch.lib
TORCH_DEBUG = getenv("TORCH_DEBUG")
import torch, pathlib, operator, functools, weakref
@@ -88,9 +88,6 @@ view_ops = {
"aten.diagonal": Tensor.diagonal,
}
# torch 2.10 handles this natively
if tuple(map(int, torch.__version__.split('.')[:2])) < (2, 10): view_ops.update({"aten.detach": Tensor.detach})
for k,v in view_ops.items(): torch.library.impl(k.replace("aten.", "aten::"), "privateuseone")(wrap_view_op(v))
def _get_view_ops(view): return getattr(view, "_view_ops", [])
@@ -99,46 +96,21 @@ def _apply_view_ops(target, ops):
for fn, args, kwargs in ops: target = fn(target, *args, **kwargs)
return target
# similar to https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/InferSize.h
def _reshape_target_shape(shape:tuple[int, ...], args) -> tuple[int, ...]|None:
if not (req := argfix(*args)): return None
new_shape, infer_idx = [], -1
for i, s in enumerate(req):
if s is None: s = shape[i] if i < len(shape) else None
if not isinstance(s, int): return None
if s == -1:
if infer_idx != -1: return None
infer_idx = len(new_shape)
new_shape.append(s)
total = prod(shape)
if infer_idx != -1:
known = prod(x for x in new_shape if x != -1)
if known == 0:
if total != 0: return None
new_shape[infer_idx] = 0
else: new_shape[infer_idx] = total // known
return tuple(new_shape) if prod(new_shape) == total else None
# TODO: can we get rid of this? only for test_flatten_reshape_add
# a chain of reshapes is undone by reshaping the value back to the base
def _try_simple_reshape_view_write(base: Tensor, view: Tensor, val: Tensor) -> bool:
if not (ops := _get_view_ops(view)): return False
shapes = [base.shape]
for fn, args, _ in ops:
if fn is Tensor.reshape:
if not (next_shape := _reshape_target_shape(shapes[-1], args)): return False
shapes.append(next_shape)
if shapes[-1] != view.shape: return False
for s in reversed(shapes[:-1]): val = val.reshape(s)
base.assign(val)
if any(fn is not Tensor.reshape for fn, _, _ in ops): return False
base.assign(val.reshape(base.shape))
return True
def _view_write(base: Tensor, view: Tensor, value: Tensor) -> None:
val = value if value.dtype == base.dtype else value.cast(base.dtype)
if view.shape == base.shape: return base.assign(val)
if _try_simple_reshape_view_write(base, view, val): return
idx_base = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape)
idx_view = _apply_view_ops(idx_base, _get_view_ops(view)).reshape(-1)
flat_base = base.reshape(base.numel()).contiguous()
# clone, not contiguous: contiguous() on a base that already owns its buffer returns the base itself, and scattering
# into that is an in-place write to a buffer other tensors still hold, which setitem refuses
flat_base = base.reshape(base.numel()).clone()
flat_base[idx_view] = val.reshape(-1)
base.assign(flat_base.reshape(base.shape))
@@ -301,6 +273,34 @@ def slice_tensor(self, dim=0, start=None, end=None, step=1):
slices[dim] = slice(start, end, step)
return self[slices]
# the functional scatters. without an impl aten falls back to a path that assumes a real storage: "self.has_storage() INTERNAL ASSERT FAILED"
def _scatter_into(self, src, dim, index):
out = unwrap(self).clone()
slices = [slice(None)] * out.ndim
slices[dim] = index
out[slices] = unwrap(src).cast(out.dtype) # torch casts src to self's dtype, tinygrad setitem demands they already match
return wrap(out)
@torch.library.impl("aten::slice_scatter", "privateuseone")
def slice_scatter(self, src, dim=0, start=None, end=None, step=1): return _scatter_into(self, src, dim, slice(start, end, step))
@torch.library.impl("aten::select_scatter", "privateuseone")
def select_scatter(self, src, dim, index): return _scatter_into(self, src, dim, index)
@torch.library.impl("aten::diagonal_scatter", "privateuseone")
def diagonal_scatter(self, src, offset=0, dim1=0, dim2=1):
# a diagonal is not one axis, so scatter through the flat indices it picks out
base, out = unwrap(self), unwrap(self).clone().reshape(-1)
idx = Tensor.arange(base.numel(), dtype=dtypes.int32).reshape(base.shape).diagonal(offset, dim1, dim2).reshape(-1)
out[idx] = unwrap(src).cast(base.dtype).reshape(-1)
return wrap(out.reshape(base.shape))
# the functional copy_. without an impl the fallback segfaults on a tensor with no storage
@torch.library.impl("aten::copy", "privateuseone")
def copy(self, src, non_blocking=False):
dest = unwrap(self)
return wrap(unwrap(src).cast(dest.dtype).to(dest.device).expand(dest.shape))
@torch.library.impl("aten::slice_backward", "privateuseone")
def slice_backward(grad_out, input_sizes, dim, start, end, step):
grad_input = Tensor.zeros(input_sizes).contiguous()
@@ -341,7 +341,9 @@ for dim in [1, 2, 3]:
torch.library.impl(f"aten::{pad_type}_pad{dim}d", "privateuseone")(functools.partial(pad_forward, mode=mode))
torch.library.impl(f"aten::{pad_type}_pad{dim}d_backward", "privateuseone")(functools.partial(pad_backward, mode=mode))
def upsample(self, size, align_corners=False, mode=None): return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=align_corners))
# the schemas are all positional: (self, output_size, align_corners, *scales) for linear, (self, output_size, *scales) for nearest.
def upsample(self, size, *args, mode=None):
return wrap(Tensor.interpolate(unwrap(self), size, mode=mode, align_corners=args[0] if mode == "linear" else False))
for i,pre in enumerate(["", "bi", "tri"]):
torch.library.impl(f"aten::upsample_{pre}linear{i+1}d", "privateuseone")(functools.partial(upsample, mode="linear"))
torch.library.impl(f"aten::upsample_nearest{i+1}d", "privateuseone")(functools.partial(upsample, mode="nearest"))
@@ -413,6 +415,7 @@ def _linalg_svd(self, full_matrices=False):
from torch._decomp import get_decompositions
decomps = [
aten.native_layer_norm_backward,
aten.native_group_norm_backward,
aten.linalg_cross,
aten.addmm,
aten.addcmul,
@@ -447,12 +450,20 @@ decomps = [
aten._softmax_backward_data, aten.embedding_dense_backward,
aten.linalg_vector_norm,
aten.binary_cross_entropy, aten.binary_cross_entropy_backward,
# the C++ mse/smooth_l1 kernels resize their out tensor, and a tiny tensor has no storage to resize
aten.mse_loss, aten.mse_loss_backward,
aten.smooth_l1_loss, aten.smooth_l1_loss_backward,
aten.upsample_nearest2d.out,
# NOTE: only the "out" overload, the "vec" one is CompositeImplicitAutograd and overriding it loses the autograd kernel
aten.upsample_bicubic2d.out,
aten._adaptive_avg_pool2d,
# activations
aten.hardswish, aten.hardswish_backward,
aten.hardtanh, aten.hardtanh_backward,
aten.gelu, aten.gelu_backward,
aten.logical_and,
# NOTE: no aten.logical_or here, its decomposition reaches aten.bitwise_or through a path that checks aliasing by
# reading storage, which a tiny tensor has none of. it gets a direct impl below instead
aten.logical_and, aten.logical_xor,
aten.randint,
aten.eye,
aten.hardsigmoid_backward,
@@ -579,8 +590,8 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
# inplace ops using replace for fusion
"aten.zero_": lambda x: x.const_like(0),
"aten.fill_.Scalar": lambda x, y: x.const_like(y),
"aten.add_.Tensor": lambda self, other, alpha=1.0: self + other * alpha,
"aten.add_.Scalar": lambda self, other, alpha=1.0: self + other * alpha,
"aten.add_.Tensor": lambda self, other, alpha=1: self + other * alpha,
"aten.add_.Scalar": lambda self, other, alpha=1: self + other * alpha,
"aten.mul_.Tensor": lambda self, other: self * other,
"aten.mul_.Scalar": lambda self, other: self * other,
# relu doesn't have an out form?
@@ -613,7 +624,9 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
# these don't work in out form, they have size 0
"aten.abs": Tensor.abs,
"aten.logical_not": Tensor.logical_not,
"aten.logical_or_": lambda x, y: x | y,
# compare against zero first: logical_* is bool-valued for any input dtype, while | is bitwise
"aten.logical_or": lambda x, y: (x != 0) | (y != 0),
"aten.logical_or_": lambda x, y: (x != 0) | (y != 0),
"aten.multinomial": Tensor.multinomial,
"aten.masked_fill_.Scalar": lambda self, mask, value: self.masked_fill(mask, value),
"aten.masked_fill_.Tensor": lambda self, mask, value: self.masked_fill(mask, value),
@@ -625,8 +638,9 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
"aten.acos": Tensor.acos,
"aten.any": Tensor.any,
"aten.bitwise_not": Tensor.bitwise_not,
"aten.argmax": Tensor.argmax,
"aten.argmin": Tensor.argmin,
# tinygrad indexes with int32, torch's arg reduces return int64
"aten.argmax": lambda self, dim=None, keepdim=False: self.argmax(dim, keepdim).cast(dtypes.int64),
"aten.argmin": lambda self, dim=None, keepdim=False: self.argmin(dim, keepdim).cast(dtypes.int64),
"aten.asinh": Tensor.asinh,
"aten.mul": Tensor.mul,
"aten.atanh": Tensor.atanh,
@@ -652,6 +666,7 @@ tiny_backend = {**{k:wrap_out(v) for k,v in tiny_backend_out.items()}, **{
self.ones_like(**{k: v for k, v in {"dtype": _from_torch_dtype(dtype) if dtype else None,
"device": _from_torch_device(device) if device else None}.items() if v is not None}),
"aten.max.dim": lambda self, dim, keepdim=False: (self.max(dim, keepdim), self.argmax(dim, keepdim).cast(dtype=dtypes.int64)),
"aten.min.dim": lambda self, dim, keepdim=False: (self.min(dim, keepdim), self.argmin(dim, keepdim).cast(dtype=dtypes.int64)),
"aten.cummax": lambda self, dim: ((r := self.cummax(dim))[0], r[1].cast(dtypes.int64)),
"aten.cummin": lambda self, dim: ((r := self.cummin(dim))[0], r[1].cast(dtypes.int64)),
"aten.nonzero": Tensor.nonzero,
+105
View File
@@ -166,6 +166,15 @@ class TestTorchBackend(unittest.TestCase):
expected = np.array([[1.5, 5.2, 9.0], [13.2, 17.1, 18.4]], dtype=np.float32)
np.testing.assert_equal(y3.cpu().numpy(), expected)
def test_argmax_argmin(self):
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
c = a.cpu()
for got, want in [(a.argmax(), c.argmax()), (a.argmin(0), c.argmin(0)), (a.argmax(1, keepdim=True), c.argmax(1, keepdim=True)),
(torch.min(a, 1).indices, torch.min(c, 1).indices), (torch.max(a, 1).indices, torch.max(c, 1).indices),
(torch.min(a, 1).values, torch.min(c, 1).values), (torch.min(a, 1, keepdim=True).indices, torch.min(c, 1, keepdim=True).indices)]:
self.assertEqual(got.dtype, want.dtype) # torch's arg reduces are int64, tinygrad's are int32
np.testing.assert_equal(got.cpu().numpy(), want.numpy())
def test_isfinite(self):
a = torch.ones(4, device=device)
np.testing.assert_equal(torch.isfinite(a).cpu().numpy(), [True, True, True, True])
@@ -373,6 +382,22 @@ class TestTorchBackend(unittest.TestCase):
for bwd_eps in [1e-5, 0.3]:
for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
def test_groupnorm_backward(self):
def run(dev):
x = torch.arange(24., device=dev).reshape(2, 4, 3).requires_grad_()
w = torch.linspace(0.5, 2.0, 4).to(dev).requires_grad_()
torch.nn.functional.group_norm(x, 2, w, torch.zeros(4, device=dev)).square().sum().backward()
return x.grad.cpu().numpy(), w.grad.cpu().numpy()
for got, want in zip(run(device), run("cpu")): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)
def test_mse_smooth_l1_loss_backward(self):
def run(dev, loss):
x = torch.arange(4., device=dev).requires_grad_()
loss(x, torch.ones(4, device=dev)).backward()
return x.grad.cpu().numpy()
for loss in [torch.nn.functional.mse_loss, torch.nn.functional.smooth_l1_loss]:
np.testing.assert_allclose(run(device, loss), run("cpu", loss), atol=1e-6)
def test_batchnorm_unsqueeze(self):
bn = torch.nn.BatchNorm2d(4).to(device)
x = torch.randn(8, 4, 3, 3, device=device)
@@ -796,6 +821,86 @@ class TestTorchBackend(unittest.TestCase):
np.testing.assert_allclose(w_tiny.grad.cpu().numpy(), w_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
np.testing.assert_allclose(b_tiny.grad.cpu().numpy(), b_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
def test_write_through_detach_of_unrealized(self):
a = torch.empty(4, device=device)
a.detach().fill_(3)
np.testing.assert_equal(a.cpu().numpy(), [3, 3, 3, 3])
def test_square_transpose_inplace(self):
# a same-shape transpose is not a reshape: writing the transposed values straight back would scramble the base
a = torch.tensor([[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]], device=device)
a.transpose(0, 1).add_(100)
np.testing.assert_equal(a.cpu().numpy(), [[100., 101., 102.], [103., 104., 105.], [106., 107., 108.]])
def test_interpolate(self):
a = torch.arange(4, dtype=torch.float32, device=device).reshape(1, 1, 2, 2)
nearest = torch.nn.functional.interpolate(a, scale_factor=2.0)
np.testing.assert_equal(nearest.cpu().numpy()[0, 0], [[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 3, 3], [2, 2, 3, 3]])
linear = torch.nn.functional.interpolate(a, size=(4, 4), mode="bilinear", align_corners=False)
ref = torch.nn.functional.interpolate(a.cpu(), size=(4, 4), mode="bilinear", align_corners=False)
np.testing.assert_allclose(linear.cpu().numpy(), ref.numpy(), rtol=1e-5)
def test_interpolate_bicubic_area(self):
a = torch.arange(32, dtype=torch.float32, device=device).reshape(1, 2, 4, 4)
for mode, scale in [("bicubic", 2.0), ("area", 0.5)]:
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=scale, mode=mode)
np.testing.assert_allclose(torch.nn.functional.interpolate(a, scale_factor=scale, mode=mode).cpu().numpy(), ref.numpy(), atol=1e-4)
@unittest.expectedFailure
def test_interpolate_bicubic_backward(self):
# the forward comes from a decomposition, but aten::upsample_bicubic2d_backward has none (nor does
# aten::_adaptive_avg_pool2d_backward, for area), so training through these modes needs a real kernel
x = torch.arange(32., dtype=torch.float32, device=device).reshape(1, 2, 4, 4).requires_grad_()
torch.nn.functional.interpolate(x, scale_factor=2.0, mode="bicubic").sum().backward()
@unittest.expectedFailure
def test_interpolate_inexact_scale(self):
# torch forwards the raw scale_factor, Tensor.interpolate recomputes it from output_size, and they disagree here
a = torch.arange(6, dtype=torch.float32, device=device).reshape(1, 1, 2, 3)
tiny = torch.nn.functional.interpolate(a, scale_factor=2.5, mode="bilinear")
ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=2.5, mode="bilinear")
np.testing.assert_allclose(tiny.cpu().numpy(), ref.numpy(), rtol=1e-5)
def test_logical_or_xor(self):
a = torch.tensor([True, True, False, False], device=device)
b = torch.tensor([True, False, True, False], device=device)
np.testing.assert_equal(torch.logical_or(a, b).cpu().numpy(), [True, True, True, False])
np.testing.assert_equal(torch.logical_xor(a, b).cpu().numpy(), [False, True, True, False])
# bool-valued whatever the input dtype, so this is not | and ^
i, j = torch.tensor([2, 0, 5, 0], device=device), torch.tensor([0, 0, 1, 1], device=device)
np.testing.assert_equal(torch.logical_or(i, j).cpu().numpy(), [True, False, True, True])
np.testing.assert_equal(torch.logical_xor(i, j).cpu().numpy(), [True, False, False, True])
def test_slice_scatter(self):
# the scatters are functional: they return a new tensor and must leave the one they were given alone
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
out = torch.slice_scatter(a, torch.ones(1, 4, device=device), 0, 0, 1)
np.testing.assert_equal(out.cpu().numpy(), [[1, 1, 1, 1], [4, 5, 6, 7], [8, 9, 10, 11]])
np.testing.assert_equal(a.cpu().numpy(), np.arange(12, dtype=np.float32).reshape(3, 4))
def test_slice_scatter_casts_src(self):
a = torch.zeros(3, 4, device=device)
out = torch.slice_scatter(a, torch.ones(1, 4, dtype=torch.int32, device=device), 0, 0, 1)
self.assertEqual(out.dtype, torch.float32)
np.testing.assert_equal(out.cpu().numpy()[0], np.ones(4, dtype=np.float32))
def test_select_scatter(self):
a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
out = torch.select_scatter(a, torch.ones(4, device=device), 0, 1)
np.testing.assert_equal(out.cpu().numpy(), [[0, 1, 2, 3], [1, 1, 1, 1], [8, 9, 10, 11]])
def test_diagonal_scatter(self):
a = torch.zeros(3, 3, device=device)
out = torch.diagonal_scatter(a, torch.arange(3, dtype=torch.float32, device=device))
np.testing.assert_equal(out.cpu().numpy(), np.diag([0., 1., 2.]))
np.testing.assert_equal(a.cpu().numpy(), np.zeros((3, 3), dtype=np.float32))
def test_copy_functional(self):
# without an impl this segfaults rather than fails: a regression here takes the whole run down
a = torch.arange(4, dtype=torch.float32, device=device)
out = torch.ops.aten.copy(a, torch.zeros(4, device=device))
np.testing.assert_equal(out.cpu().numpy(), [0., 0., 0., 0.])
np.testing.assert_equal(a.cpu().numpy(), [0., 1., 2., 3.])
from tinygrad import Tensor
class TestBackendHelpers(unittest.TestCase):
+3
View File
@@ -340,6 +340,9 @@ class TestUint64DType(TestDType):
DTYPE = dtypes.uint64
def test_uint64_load(self):
assert Tensor(2**64 - 1, dtype=dtypes.uint64).numpy() == 2**64 - 1
@unittest.skipIf(dtypes.double not in supported_dtypes, "needs float64")
def test_uint64_cast_double(self):
assert Tensor([2**32 + 1], dtype=dtypes.uint64).cast(dtypes.double).numpy() == 2**32 + 1
@unittest.skipIf(isinstance(Device[Device.DEFAULT].renderer, PTXRenderer), "PTX does indexing math with longs")
class TestEmulatedUInt64DType(TestUint64DType):
+1 -1
View File
@@ -7,7 +7,7 @@ from tinygrad.renderer.isa.x86 import X86Renderer, X86Ops
from tinygrad.renderer.isa import IselContext
# INDEX on a register value with a constant index extracts a single element (the old GEP)
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.const(i, dtypes.int), dtype=y.dtype.scalar())
def lane(y:UOp, i:int) -> UOp: return y.index(UOp.const(i, dtypes.int), dtype=y.dtype)
@unittest.skipUnless(isinstance(Device[Device.DEFAULT].renderer, X86Renderer), "only x86")
class TestIselX86(unittest.TestCase):
+1 -1
View File
@@ -82,7 +82,7 @@ class TestQuantizeOnnxCPU(unittest.TestCase):
linear = run_onnx({"input":inp})["output"].schedule_linear()
prg = to_program(linear.src[-2].src[0], renderer=Device[Device.DEFAULT].renderer)
daccs = [u for u in tuple(prg.src[1].src) if u.op is Ops.BUFFER and u.addrspace is AddrSpace.REG]
assert all(u.dtype.scalar() is dtypes.int for u in daccs)
assert all(u.dtype is dtypes.int for u in daccs)
@unittest.skipIf(Device.DEFAULT != "DSP", "only tests for DSP")
class TestQuantizeOnnx(unittest.TestCase):
+1 -1
View File
@@ -44,7 +44,7 @@ def realized_matmul():
z = y.matmul(x)
Tensor.realize(z)
def realized_gradient():
x = Tensor.eye(3)
x = Tensor.eye(3).clone()
y = Tensor([[2.0,0,-2.0]])
z = y.matmul(x).sum()
z.backward()
-4
View File
@@ -51,10 +51,6 @@ class TestHelpers(unittest.TestCase):
assert dtypes.is_float(dtypes.fp8e4m3)
assert dtypes.is_float(dtypes.fp8e5m2)
@given(strat.sampled_from([d for d in DTYPES_DICT.values() if dtypes.is_float(d) or dtypes.is_int(d)]))
def test_scalar(self, dtype):
assert dtype.scalar() == dtype
def test_from_py(self):
assert dtypes.from_py(True) == dtypes.bool
assert dtypes.from_py(Invalid) == dtypes.bool
+1 -1
View File
@@ -69,7 +69,7 @@ class TestIdxUpcast(unittest.TestCase):
if not isinstance(Device[Device.DEFAULT].renderer, (PTXRenderer, NIRRenderer)):
assert idx.op is Ops.INDEX
idx_val = idx.src[1]
self.assertFalse(idx_val.overflows(idx_val.dtype.scalar()))
self.assertFalse(idx_val.overflows(idx_val.dtype))
# use expand to generate kernel that uses large idx
def do_op_then_assert(self, dtype: DType, dim1, dim2, dim3):
+4 -4
View File
@@ -10,12 +10,12 @@ from test.helpers import replace_opts
class TestFloat4(unittest.TestCase):
@staticmethod
def count_float4(uops: list[UOp], n=4):
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.float and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.float and uop.shape == (4,)]))
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.float and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.float and uop.shape == (4,)]))
@staticmethod
def count_half4(uops: list[UOp]):
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype.scalar() == dtypes.half and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype.scalar() == dtypes.half and uop.shape == (4,)]))
return (len([uop for uop in uops if uop.op is Ops.LOAD and uop.dtype == dtypes.half and uop.shape == (4,)]),
len([uop for uop in uops if uop.op is Ops.STORE and uop.src[1].dtype == dtypes.half and uop.shape == (4,)]))
def test_float4_basic(self):
a = Tensor.empty(2, 8).realize()
+1 -1
View File
@@ -64,7 +64,7 @@ class TestAllreduceCast(unittest.TestCase):
with Context(ALLREDUCE_CAST=allreduce_cast, RING=0, SCACHE=0):
t = Tensor.empty(4, 4, dtype=dtype).shard(ds, axis=0)
linear = t.sum(0).linear_with_vars()[0]
return {si.src[1].buffer.dtype.scalar() for si in linear.src if si.src[0].op is Ops.COPY}
return {si.src[1].buffer.dtype for si in linear.src if si.src[0].op is Ops.COPY}
def test_allreduce_cast_bf16(self):
# with ALLREDUCE_CAST, allreduce copies stay in bfloat16 instead of promoting to float32
+2 -1
View File
@@ -33,7 +33,8 @@ def l2i(op: Ops, dt: DType, *uops:UOp):
return (lo:=uops[0].cast(l2i_dt[dt])), (uops[0] / 2**32).cast(l2i_dt[dt]) - ((uops[0] < 0) & lo.ne(0))
case Ops.CAST if dt in dtypes.floats:
small = (a1.eq(0) & (a0 >= 0)) | (a1.eq(-1) & (a0 < 0))
return small.where(a0.cast(dt), ((a1.cast(dtypes.float32) * (2**32)) + a0.bitcast(dtypes.uint).cast(dtypes.float32)).cast(dt))
cdt = dt if dt == dtypes.float64 else dtypes.float32
return small.where(a0.cast(dt), ((a1.cast(cdt) * (2**32)) + a0.bitcast(dtypes.uint).cast(cdt)).cast(dt))
case Ops.CAST: return a0.bitcast(dtypes.uint).cast(dt)
case Ops.BITCAST: return a0.bitcast(dt), a1.bitcast(dt)
case Ops.SHL:
-1
View File
@@ -66,7 +66,6 @@ class DType(metaclass=DTypeMetaClass):
def __reduce__(self): return type(self), tuple(getattr(self, f.name) for f in fields(self))
def __repr__(self): return f"dtypes.{INVERSE_DTYPES_DICT[self.name]}"
def __lt__(self, o:DType): return (self.priority, self.bitsize, self.name, self.fmt) < (o.priority, o.bitsize, o.name, o.fmt)
def scalar(self) -> DType: return self
@functools.cached_property
def min(self):
if dtypes.is_int(self): return 0 if dtypes.is_unsigned(self) else -2**(self.bitsize-1)
+4
View File
@@ -269,10 +269,14 @@ class _TinyJit(Generic[ReturnType]):
big_linear, onetime_linear = prune_linear(big_linear, set(input_buf_uops))
if DEBUG >= 1: print(f"pruned from {len(big_linear.src) + len(onetime_linear.src)} -> {len(big_linear.src)} kernels")
run_linear(onetime_linear, var_vals)
del onetime_linear
# hold all buffers reachable from live Tensors (e.g. lazy .grad created during capture), the memory planner can't suballocate those
held_bufs = set(buffers) | {u for tref in list(all_tensors) if (t:=tref()) is not None for u in t.uop.toposort() if u.op is Ops.BUFFER}
linear = jit_lower(big_linear, held_bufs, input_buf_uops)
# drop the pre-planning graph: it keeps the whole capture-time working set allocated (big_linear) or referenced (held_bufs).
# the planned linear only uses the arena/held buffers, so the intermediates must be freed before linking and first exec
del big_linear, held_bufs
self.captured = CapturedJit(ret, linear, names, expected_input_info)
ret = self.captured(input_buf_uops, var_vals)
elif self.cnt >= 2:
+1 -1
View File
@@ -33,7 +33,7 @@ def call_gradient(ctx:UOp, k:UOp, needed:set[int]) -> tuple[UOp|None, ...]:
params = {x.arg.slot:x for x in fxn.toposort(enter_calls=False) if x.op == Ops.PARAM}
grad_args = ctx.src
root_grad = UOp(Ops.TUPLE, src=tuple(UOp(Ops.NOOP) if g.op is Ops.NOOP else
g if g.base.op is Ops.CONST else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
g if g.device is None else g.param_like(len(args)+i) for i,g in enumerate(grad_args)))
grads = compute_gradient(fxn, root_grad, set(params.values()))
# for precompiled calls, substitute forward outputs with params so intermediates aren't recomputed
fwd_subs = {src: src.param_like(len(args)+len(grad_args)+i) for i, src in enumerate(fxn.src)} if k.arg.precompile else {}
+4 -4
View File
@@ -35,8 +35,8 @@ class Estimates:
while len(buf.src) and buf.op is not Ops.PARAM: buf = buf.src[0]
if buf.op is Ops.PARAM:
# u.src[0] is INDEX, cap at buffer size for re-reads (e.g. matmul)
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.scalar().itemsize * mults
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.scalar().itemsize)
accessed = mem.get((buf, u.op), 0) + u.src[0].max_numel() * u.src[0].dtype.itemsize * mults
mem[(buf, u.op)] = smin(accessed, buf.max_numel() * buf.dtype.itemsize)
if u.op is Ops.RANGE:
mult_stack.append(mults)
if u.dtype is not dtypes.void: # unbounded loop, unknown trip count
@@ -47,9 +47,9 @@ class Estimates:
elif u.op is Ops.SPECIAL: mults *= cast(sint, u.src[0].ssimplify()) # NOTE: we don't push to the mult_stack here, you can't end these
elif u.op is Ops.PARAM and u.arg.addrspace == AddrSpace.ALU and u.expr == 'core_id': mults *= int(u.vmax) + 1
elif u.op is Ops.LOAD and u.src[0].addrspace != AddrSpace.REG:
lds += u.max_numel() * u.dtype.scalar().itemsize * mults
lds += u.max_numel() * u.dtype.itemsize * mults
elif u.op is Ops.STORE and u.src[0].addrspace != AddrSpace.REG:
lds += u.max_numel() * u.src[1].dtype.scalar().itemsize * mults
lds += u.max_numel() * u.src[1].dtype.itemsize * mults
elif u.op in GroupOp.ALU and u not in excluded:
flops += (mults * (2 if u.op is Ops.MULACC else 1)) * u.max_numel()
elif u.op is Ops.WMMA and u not in excluded:
+6 -6
View File
@@ -107,11 +107,11 @@ def uops_to_dtypes(uops:list[UOp]) -> list[tuple[DType, int]]:
def _wmma_name(u:UOp) -> str:
# sanitize spaces in DType.name (int8 = "signed char")
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.scalar().name}".replace(" ", "_")
return f"WMMA_{'_'.join(map(str, u.arg[0]))}_{u.arg[1].name}_{u.dtype.name}".replace(" ", "_")
# (name, dims, dtype_in, dtype_out, device, threads, upcast_sizes)
def wmma_args(uops:list[UOp]):
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype.scalar(), *(uop.arg[2:4]),
return dedup((_wmma_name(uop), uop.arg[0], uop.arg[1], uop.dtype, *(uop.arg[2:4]),
tuple(uop.src[i].shape[-1] for i in range(3)))
for uop in uops if uop.op is Ops.WMMA)
@@ -182,8 +182,8 @@ class CStyleLanguage(Renderer):
if addrspace in (AddrSpace.LOCAL, AddrSpace.GLOBAL) or override_ptr:
suffix = "*"
if sz > 1:
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name).replace(" ", "_") + str(sz) + suffix
return prefix + self.type_map.get(scalar:=dtype.scalar(), scalar.name) + suffix
return prefix + self.type_map.get(dtype, dtype.name).replace(" ", "_") + str(sz) + suffix
return prefix + self.type_map.get(dtype, dtype.name) + suffix
def render_type(self, u:UOp): return self._render_dtype(u.dtype, u.max_numel(), u.addrspace, shape=u._shape)
def render_access(self, u:UOp):
@@ -472,7 +472,7 @@ class CUDARenderer(CStyleLanguage):
class NVCCRenderer(CUDARenderer):
def __init__(self, target:Target): super().__init__(target, use_nvcc=True)
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype.scalar())
def fp8_index(dtype: DType): return (dtypes.fp8e4m3, dtypes.fp8e5m2).index(dtype)
def _ocml(op): return lambda x,dtype: f"__ocml_{op}_f{ {dtypes.half:16, dtypes.double:64}.get(dtype, 32)}({x})"
class HIPRenderer(CStyleLanguage):
@@ -546,7 +546,7 @@ class HIPRenderer(CStyleLanguage):
ockl = [(f"__ockl_get_{name}", "unsigned int", "size_t", "const") for name in ["local_id", "group_id", "local_size"]]
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) for u in uops) if op in ocml_ops and dt in (dtypes.half, dtypes.float, 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")
+4 -4
View File
@@ -165,7 +165,7 @@ def scratch_buffer(elem_dt:DType, count:int, slot:int) -> UOp:
return UOp.placeholder((count,), elem_dt, slot, AddrSpace.LOCAL)
def gated_load(ctx, addr:UOp, alt:UOp, gate:UOp, x:UOp):
local = scratch_buffer(addr.src[0].dtype.scalar(), x.max_numel(), next(ctx))
local = scratch_buffer(addr.src[0].dtype, x.max_numel(), next(ctx))
local_idx = local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64)
# the selected address is a 64bit value, the AFTER orders the load after the scratch store and carries the element dtype for the encoder
sel = gate.where(addr.replace(dtype=dtypes.uint64), local_idx)
@@ -173,7 +173,7 @@ def gated_load(ctx, addr:UOp, alt:UOp, gate:UOp, x:UOp):
return ptr.load(dtype=x.dtype)
def gated_store(addr:UOp, gate:UOp, val:UOp):
local = scratch_buffer(addr.src[0].dtype.scalar(), val.max_numel(), -1)
local = scratch_buffer(addr.src[0].dtype, val.max_numel(), -1)
sel = gate.where(addr.replace(dtype=dtypes.uint64), local.index(UOp.const(0, dtypes.int32), dtype=dtypes.uint64))
return UOp(Ops.AFTER, addr.dtype, (sel,)).store(val)
@@ -237,7 +237,7 @@ def cmp(x:UOp) -> UOp:
return x.ins(X86Ops.CMP, dtype=dtypes.void) if (i:=to_imm(x.src[1])) is None else x.ins(X86Ops.CMPi, dtype=dtypes.void, src=(x.src[0], i))
def vcmp(x:UOp) -> UOp:
v = imm(dtypes.uint8, {Ops.CMPLT: 1, Ops.CMPNE: 4, Ops.CMPEQ: 0}[x.op])
if x.dtype.scalar() is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
if x.dtype is dtypes.float32: return x.ins(X86Ops.VCMPSS if x.max_numel() == 1 else X86Ops.VCMPPS, src=x.src + (v,))
return x.ins(X86Ops.VCMPSD if x.max_numel() == 1 else X86Ops.VCMPPD, src=x.src + (v,))
# vinsertps xmm2, xmm0, xmm1, imm
@@ -252,7 +252,7 @@ def vinsertps(x:UOp) -> UOp:
# vpinsq xmm2, xmm0, rax, imm
# inserts element in rax into any position in xmm0, result is written to xmm2 according to imm
def vpins(x:UOp) -> UOp:
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.scalar().itemsize]
op = {1: X86Ops.VPINSRB, 2: X86Ops.VPINSRW, 4: X86Ops.VPINSRD, 8: X86Ops.VPINSRQ}[x.dtype.itemsize]
return functools.reduce(lambda ret,i: x.ins(op, src=(ret, x.src[i], imm(dtypes.uint8, i))), range(len(x.src)), def_reg(x.dtype))
# we don't call ctx.vreg on the srcs to avoid duplicates, a rewrite will assign the tuple of valid registers to a vreg
+13 -13
View File
@@ -64,7 +64,7 @@ def render_wmma(ctx: "PTXRenderer", wmma: UOp):
for src, regs in zip(wmma.src, ctx.wmma_r):
for i, reg in enumerate(regs): # pack input and acc registers
if (elems_per_reg := 4 // src.dtype.scalar().itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
if (elems_per_reg := 4 // src.dtype.itemsize) == 1: yield f"mov.b32 {reg}, {ctx.r[src][i]};"
else: yield f"mov.b32 {reg}, {{{', '.join(ctx.r[src][i * elems_per_reg : (i+1) * elems_per_reg])}}};"
dt_map_in, dt_map_out = {dtypes.float: "tf32", dtypes.half: "f16"}, {dtypes.float: "f32", dtypes.half: "f16"}
@@ -101,17 +101,17 @@ string_rewrite = PatternMatcher([
if loc.addrspace == AddrSpace.REG else None),
(UPat(Ops.STORE, src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("var"))),
lambda ctx, loc, var: f"st.{mem_type(loc)}" + \
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype.scalar()]} " + \
f"{f'.v{cnt}' if ((cnt:=var.max_numel())>1) else ''}.{ctx.mem_types[var.dtype]} " + \
f"[{ctx.r[loc]}+0], {('{' + ', '.join(ctx.r[var]) + '}') if var.max_numel() > 1 else ctx.r[var]};"),
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"), UPat.var("alt"), UPat.var("gate"))),
lambda ctx, x, loc, alt, gate: flatten([
[f"mov.{ctx.mem_types[x.dtype.scalar()]} {v}, {render_val(0, x.dtype.scalar())};" for v in ctx.r[x]],
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
[f"mov.{ctx.mem_types[x.dtype]} {v}, {render_val(0, x.dtype)};" for v in ctx.r[x]],
[f"@{ctx.r[gate]} ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];"]
]) if alt.max_numel() > 1 else [
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype.scalar()]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype.scalar()][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
f"@{ctx.r[gate]} ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];",
f"@!{ctx.r[gate]} mov.b{ctx.types[x.dtype][1:]} {ctx.r[x]}, {ctx.r[alt]};"]),
(UPat(Ops.LOAD, name="x", src=(UPat((Ops.INDEX, Ops.SHRINK), name="loc"),)),
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype.scalar()]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
lambda ctx, x, loc: f"ld.{mem_type(loc)}.v{x.max_numel()}.{ctx.mem_types[x.dtype]} {{{', '.join(ctx.r[x])}}}, [{ctx.r[loc]}+0];" \
if x.max_numel() > 1 else f"ld.{mem_type(loc)}.{ctx.mem_types[x.dtype]} {ctx.r[x]}, [{ctx.r[loc]}+0];"),
# simple
(UPat(Ops.BUFFER, name="x"), lambda ctx, x: [] if x.addrspace == AddrSpace.REG else [
@@ -197,7 +197,7 @@ class PTXRenderer(Renderer):
r[u] = [cast(str,r[x]) for x in u.src]
continue
if u.op is Ops.BUFFER and u.addrspace == AddrSpace.REG:
r[u] = [ssa("reg", u, self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
r[u] = [ssa("reg", u, self.types[u.dtype]) for _ in range(u.max_numel())]
continue
if u.op in {Ops.INDEX, Ops.SHRINK, Ops.LOAD} and u.src[0].addrspace in (AddrSpace.REG, AddrSpace.ALU):
# on REG, INDEX/SHRINK pick the register (must be CONST) and LOAD is a noop
@@ -207,14 +207,14 @@ class PTXRenderer(Renderer):
continue
if u.op is Ops.SPECIAL: r[u] = "%" + u.arg
elif u.op is Ops.LOAD:
r[u] = [ssa('val', dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
r[u] = [ssa('val', dtype=self.types[u.dtype]) for _ in range(u.max_numel())] if u.max_numel() > 1 else ssa('val', u)
elif u.op is Ops.PARAM: bufs.append((f"data{u.arg.slot}", u))
elif u.op is Ops.WMMA:
# registers for packing/unpacking input and acc
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.scalar().itemsize)],
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.scalar().itemsize)],
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.scalar().itemsize)]]
r[u] = [ssa("wmma", dtype=self.types[u.dtype.scalar()]) for _ in range(u.max_numel())]
self.wmma_r = [[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[0]]), 4 // u.src[0].dtype.itemsize)],
[ssa("wmma_in", dtype="b32") for _ in range(0, len(r[u.src[1]]), 4 // u.src[0].dtype.itemsize)],
[ssa("wmma_acc", dtype="b32") for _ in range(0, len(r[u.src[2]]), 4 // u.dtype.itemsize)]]
r[u] = [ssa("wmma", dtype=self.types[u.dtype]) for _ in range(u.max_numel())]
prefix, dtype = {Ops.CAST: ("cast", None), Ops.BITCAST: ("cast", None), Ops.END: ("pred", "pred"), Ops.RANGE: ("ridx", None),
Ops.CONST: ("const", None), Ops.BUFFER: ("local", "u64"), Ops.INDEX: ("bidx", "u64"), Ops.SHRINK: ("bidx", "u64"),
Ops.PARAM: ("dat", "u64" if u.addrspace is AddrSpace.GLOBAL else None), **{op: ("alu", None) for op in GroupOp.ALU}}.get(u.op, (None, None))
+1 -1
View File
@@ -466,7 +466,7 @@ pm_bufferize = PatternMatcher([(UPat(Ops.PARAM, name="buf"), bufferize_buf)])
# 7. resolve patches
def push_stack(op, s): return UOp(Ops.STACK,
src=tuple(op.replace(dtype=op.dtype.scalar(), src=tuple(x if y is s else y for y in op.src)) for x in s.src))
src=tuple(op.replace(dtype=op.dtype, src=tuple(x if y is s else y for y in op.src)) for x in s.src))
def fold_binary(buf:UOp, blob:UOp) -> UOp:
for b in (m.bufs if isinstance(m:=buf.buffer, MultiBuffer) else (m,)):
+3 -3
View File
@@ -22,14 +22,14 @@ def mstack_early_shrink(ms:UOp, shrink:UOp):
def lower_broadcast_copy(c:UOp, x:UOp):
if not (isinstance(c.device, tuple) and isinstance(x.device, str)): return None
if (sx:=x.simplify()).device is None and sx.base.op is Ops.CONST: return UOp(Ops.MSTACK, src=(sx,)*len(c.device))
if (sx:=x.simplify()).device is None: return UOp(Ops.MSTACK, src=(sx,)*len(c.device))
return UOp(Ops.MSTACK, src=tuple(x.copy_to_device(d) for d in c.device))
replace_allreduce = PatternMatcher([
# BROADCAST: explicitly expand broadcast copies and combine with MSTACK
(UPat(Ops.COPY, name="c", src=(UPat(GroupOp.All-{Ops.CONST}, name="x"),)), lower_broadcast_copy),
(UPat(Ops.COPY, name="c", src=(UPat(name="x"),)), lower_broadcast_copy),
# COPY_TO_ONE: if copying from multidevice to one, MSELECT the first (TODO: a little from each?)
(UPat(Ops.COPY, name="c", src=(UPat(GroupOp.All-{Ops.CONST}, name="x"),)), lambda c,x:
(UPat(Ops.COPY, name="c", src=(UPat(name="x"),)), lambda c,x:
x.mselect(0).copy_to_device(c.device) if isinstance(c.device, str) and isinstance(x.device, tuple) else None),
# MSELECT on MSTACK is replaced with nothing
(UPat(Ops.MSELECT, src=(UPat(Ops.MSTACK, name="mstack"),), name="ms"), lambda mstack, ms: mstack.src[ms.arg]),
+3 -3
View File
@@ -313,9 +313,9 @@ pm_const_buffer_folding = pm_mops+PatternMatcher([
lambda idx,after: idx.const_like(Invalid) if after_all_invalid(after) else None),
# hack if a noop turned to a const
(UPat(Ops.NOOP, src=(UPat.cvar("c"),)), lambda c: c),
# mstack on CONST is CONST
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True),
lambda s: c if (c:=s.base).op is Ops.CONST else None),
# a deviceless MSTACK src is the same value on every device, so indexing the stack is just indexing that value
(UPat(Ops.MSTACK, src=(UPat.var("s"),), allow_any_len=True).f(Ops.INDEX, allow_any_len=True, name="idx"),
lambda s,idx: idx.replace(src=(s,)+idx.src[1:]) if s.device is None else None),
])
pm_remove_bufferize = PatternMatcher([
+3 -3
View File
@@ -665,13 +665,13 @@ class Tensor(RandMixin):
```
"""
all_uops = self.uop.toposort()
# backward fills .grad for every in-scope non-CONST float tensor
# backward fills .grad for every in-scope float tensor with a device
tensors_need_grad: list[Tensor] = [t for tref in all_tensors if (t:=tref()) is not None and \
t.uop in all_uops and t.is_floating_point() and t.uop.op is not Ops.CONST]
t.uop in all_uops and t.is_floating_point() and t.device is not None]
# clear contexts
for t,g in zip(tensors_need_grad, self.gradient(*tensors_need_grad, gradient=gradient)):
assert g.shape == t.shape, f"grad shape must match tensor shape, {g.shape!r} != {t.shape!r}"
if g.device is None and t.device is not None: g = g.clone(device=t.device)
if g.device is None: g = g.clone(device=t.device)
if t.grad is None: t.grad = g
else: t.grad.assign(t.grad + g.to(t.grad.device))
return self
+6 -5
View File
@@ -242,9 +242,10 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
arg:Any = None
tag:Any = None
def __del__(self):
if Ops is not None and self.op is Ops.BUFFER and (buffer:=buffers.get(self)) is not None: buffer.ref(-1)
# NOTE: getattr because this object may be partially constructed (e.g. if __init__ raised, like the BEAM timeout SIGALRM)
if Ops is not None and getattr(self, 'op', None) 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
except (AttributeError, KeyError): pass
def __reduce__(self):
args = [self.op, self.dtype, self.src, self.arg, self.tag, self.metadata]
if self.op is Ops.BUFFER and self.realized is not None: args.append(self.realized)
@@ -583,9 +584,9 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def const_like(self, b:ConstLike, dtype:DType|None=None):
ret = UOp.const(b, dtype or self.dtype)
return ret._mop(Ops.EXPAND, arg=self._shape) if self._shape and ret._shape != self._shape else ret
def vconst_like(self, b:ConstLike, dtype:DType|None=None):
def vconst_like(self, b:ConstLike):
# for use after movement ops have been removed
return UOp.const(b, dtype or self.dtype).broadcast(self.max_numel())
return UOp.const(b, self.dtype).broadcast(self.max_numel())
def ufix(self, x):
if isinstance(x, UOp): return x
return UOp.const(x)
@@ -1403,7 +1404,7 @@ class UPat(OpMixin):
if self.is_any: return flatten([x.match(uop, store.copy()) for x in self.src[0]])
if (self.op is not None and uop.op not in self.op) or \
(self.name is not None and store.setdefault(self.name, uop) is not uop) or \
(self.match_dtype is not None and uop.dtype not in self.match_dtype and uop.dtype.scalar() not in self.match_dtype) or \
(self.match_dtype is not None and uop.dtype not in self.match_dtype) or \
(self.arg is not None and self.arg != uop.arg) or \
(self.match_tag is not None and uop.tag not in self.match_tag) or \
(len(uop.src) < self.required_len) or \