EXPAND adds dims to the front (glm) (#16908)

* EXPAND adds dims to the front (glm)

* more read images

* simpler + spec

* spec should use n instead of s'

* bump comma to 12
This commit is contained in:
George Hotz
2026-07-07 11:03:06 -07:00
committed by GitHub
parent 91cefdb52a
commit d8fbbff260
11 changed files with 30 additions and 25 deletions
+2 -2
View File
@@ -404,7 +404,7 @@ jobs:
testcommalatest: testcommalatest:
name: comma Benchmark (0.11.0) name: comma Benchmark (0.11.0)
runs-on: [self-hosted, Linux, comma] runs-on: [self-hosted, Linux, comma]
timeout-minutes: 10 timeout-minutes: 12
defaults: defaults:
run: run:
shell: bash -e -o pipefail {0} shell: bash -e -o pipefail {0}
@@ -435,7 +435,7 @@ jobs:
testcommaold: testcommaold:
name: comma Benchmark (0.10.1) name: comma Benchmark (0.10.1)
runs-on: [self-hosted, Linux, comma] runs-on: [self-hosted, Linux, comma]
timeout-minutes: 10 timeout-minutes: 12
defaults: defaults:
run: run:
shell: bash -e -o pipefail {0} shell: bash -e -o pipefail {0}
+1 -1
View File
@@ -291,7 +291,7 @@ jobs:
llvm: 'true' llvm: 'true'
- name: Test openpilot model kernel count and gate usage - name: Test openpilot model kernel count and gate usage
run: | run: |
ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1361 ALLOWED_GATED_READ_IMAGE=55 FLOAT16=1 DEV="CL::IMAGE_PITCH_ALIGNMENT=64" IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916 ALLOWED_KERNEL_COUNT=123 ALLOWED_READ_IMAGE=1391 ALLOWED_GATED_READ_IMAGE=58 FLOAT16=1 DEV="CL::IMAGE_PITCH_ALIGNMENT=64" IMAGE=1 python examples/openpilot/compile3.py https://gitlab.com/commaai/openpilot-lfs.git/gitlab-lfs/objects/cf6376aa9a090f0da26c280ef69eabf9bbdd51d1faac9ed392919c3db69be916
# IMAGE_PITCH_ALIGNMENT=64 matches adreno 630 # IMAGE_PITCH_ALIGNMENT=64 matches adreno 630
- name: Test openpilot CL compile fp32 (test correctness) - name: Test openpilot CL compile fp32 (test correctness)
run: | run: |
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -73,7 +73,7 @@ All nodes in the tinygrad graph are \textbf{UOps}. A UOp is a tuple $(\mathrm{op
\op{Permute} & $(T,)$ & axis order $\pi$ & Reorder axes. $\pi = (1,0)$ is transpose. \\ \op{Permute} & $(T,)$ & axis order $\pi$ & Reorder axes. $\pi = (1,0)$ is transpose. \\
\op{Flip} & $(T,)$ & bools $\mathbf{f}$ & Reverse along flagged axes. \\ \op{Flip} & $(T,)$ & bools $\mathbf{f}$ & Reverse along flagged axes. \\
\op{Reshape} & $(T, \mathbf{s'})$ & --- & Reinterpret in row-major order. $\prod s_k = \prod s'_k$. \\ \op{Reshape} & $(T, \mathbf{s'})$ & --- & Reinterpret in row-major order. $\prod s_k = \prod s'_k$. \\
\op{Expand} & $(T, \mathbf{s'})$ & --- & Broadcast size-1 axes. $s_k \in \{1, s'_k\}$. \\ \op{Expand} & $(T, \mathbf{n})$ & --- & Prepend axes $\mathbf{n}$ on the left. Output shape is $\mathbf{n} + T.\mathrm{shape}$. \\
\op{Pad} & $(T, \mathbf{o}, \mathbf{s'})$ & --- & Place $T$ at offset $o_k$ in an invalid-filled output of shape $s'_k$. \\ \op{Pad} & $(T, \mathbf{o}, \mathbf{s'})$ & --- & Place $T$ at offset $o_k$ in an invalid-filled output of shape $s'_k$. \\
\op{Shrink} & $(T, \mathbf{o}, \mathbf{s'})$ & --- & Keep $s'_k$ elements starting at offset $o_k$ per axis. Inverse of \op{Pad}. \\ \op{Shrink} & $(T, \mathbf{o}, \mathbf{s'})$ & --- & Keep $s'_k$ elements starting at offset $o_k$ per axis. Inverse of \op{Pad}. \\
\op{Index} & $(T, i_0, i_1, \ldots)$ & --- & Index from left. $()$-shaped $i$ removes dim; $(k,)$-shaped makes it $k$. \\ \op{Index} & $(T, i_0, i_1, \ldots)$ & --- & Index from left. $()$-shaped $i$ removes dim; $(k,)$-shaped makes it $k$. \\
@@ -284,7 +284,7 @@ Default \emph{dtype range}: $[\mathrm{dtype\_min},\, \mathrm{dtype\_max}]$.
\medskip \medskip
\textbf{axis} tracks the multi-device sharding dimension. \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim). \textbf{axis} tracks the multi-device sharding dimension. \op{Buffer} with $n$-tuple device: axis $= 0$ (device dim).
\op{Reshape} remaps axis to preserve the shard boundary. \op{Permute} follows the permutation. \op{Reshape} remaps axis to preserve the shard boundary. \op{Permute} follows the permutation. \op{Expand} shifts axis right by $|\mathbf{n}|$.
\op{Reduce} on the shard axis $\to$ \textsc{null} (shard axis is among the first $n$ axes). \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}. \op{Reduce} on the shard axis $\to$ \textsc{null} (shard axis is among the first $n$ axes). \op{Replicated} on the shard axis $\to$ \textsc{null}. \op{Copy} $\to$ \textsc{null}. ALU ops inherit from sources. Default: \textsc{null}.
%% ============================================================ %% ============================================================
+2 -3
View File
@@ -252,8 +252,8 @@ class TestViz(unittest.TestCase):
self.assertEqual(list(graphs[1]), [id(z), id(y), id(ret)]) self.assertEqual(list(graphs[1]), [id(z), id(y), id(ret)])
def test_const_reshape_expand_folded(self): def test_const_reshape_expand_folded(self):
# CONST->RESHAPE->EXPAND should be folded into the ALU node, not shown as separate RESHAPE/EXPAND nodes # CONST->EXPAND should be folded into the ALU node, not shown as separate EXPAND nodes
c = UOp.const(dtypes.float, 1.0, shape=(3,4)) # creates CONST->RESHAPE->EXPAND chain c = UOp.const(dtypes.float, 1.0, shape=(3,4)) # creates CONST->EXPAND chain
a = UOp.variable("a", 0.0, 10.0, dtypes.float) a = UOp.variable("a", 0.0, 10.0, dtypes.float)
alu = a + c alu = a + c
with save_viz() as viz: with save_viz() as viz:
@@ -262,7 +262,6 @@ class TestViz(unittest.TestCase):
excluded_nodes = {v["label"].split("\n")[0] for v in graph.values() if v["exclude"]} excluded_nodes = {v["label"].split("\n")[0] for v in graph.values() if v["exclude"]}
self.assertIn("CONST", excluded_nodes) self.assertIn("CONST", excluded_nodes)
self.assertIn("STACK", excluded_nodes) self.assertIn("STACK", excluded_nodes)
self.assertIn("RESHAPE", excluded_nodes)
self.assertIn("EXPAND", excluded_nodes) self.assertIn("EXPAND", excluded_nodes)
self.assertIn("CONST1 1", graph[id(alu)]["label"]) self.assertIn("CONST1 1", graph[id(alu)]["label"])
+3 -3
View File
@@ -172,9 +172,9 @@ devectorizer2 = pm_mops+PatternMatcher([
(UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]), (UPat(Ops.RESHAPE, dtype=dtypes.void, name="x"), lambda x: x.src[0]),
# reshape of a single element shaped value to scalar is an index # reshape of a single element shaped value to scalar is an index
(UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None), (UPat(Ops.RESHAPE, name="x"), lambda x: x.src[0].index(UOp.const(dtypes.weakint, 0)) if x.marg == () and x.src[0].shape == (1,) else None),
# RESHAPE+EXPAND -> STACK # EXPAND on scalar -> STACK
(UPat(Ops.EXPAND, src=(UPat(Ops.RESHAPE, src=(UPat.var("x"), UPat())), UPat()), name="out"), (UPat(Ops.EXPAND, src=(UPat.var("x"), UPat()), name="out"),
lambda x,out: UOp.vectorize(*([x]*out.max_numel())) if out.shape == (out.max_numel(),) else None), lambda x,out: UOp.vectorize(*([x]*out.max_numel())) if x.shape == () and out.shape == (out.max_numel(),) else None),
# INDEX on INDEX is INDEX # INDEX on INDEX is INDEX
(UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"), (UPat(Ops.INDEX, src=(UPat(Ops.INDEX, name="idx1", allow_any_len=True),), allow_any_len=True, name="idx2"),
lambda idx1, idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:])), lambda idx1, idx2: idx1.src[0].index(*idx1.src[1:], *idx2.src[1:])),
+1 -1
View File
@@ -75,7 +75,7 @@ pm_gradient = PatternMatcher([
(UPat(Ops.CONTIGUOUS_BACKWARD), lambda ctx: (ctx.contiguous(),)), (UPat(Ops.CONTIGUOUS_BACKWARD), lambda ctx: (ctx.contiguous(),)),
(UPat(Ops.RESHAPE, name="ret"), lambda ctx, ret: (ctx.reshape(ret.src[0].shape), None)), (UPat(Ops.RESHAPE, name="ret"), lambda ctx, ret: (ctx.reshape(ret.src[0].shape), None)),
(UPat(Ops.EXPAND, name="ret"), lambda ctx, ret: (UPat(Ops.EXPAND, name="ret"), lambda ctx, ret:
(ctx.cast(sum_acc_dtype(ctx.dtype))._rop(Ops.ADD, tuple(i for i,(s,n) in enumerate(zip(ret.src[0].shape, ret.shape)) if s!=n)) (ctx.cast(sum_acc_dtype(ctx.dtype))._rop(Ops.ADD, tuple(range(len(ret.marg))))
.reshape(ret.src[0].shape).cast(ctx.dtype), None)), .reshape(ret.src[0].shape).cast(ctx.dtype), None)),
(UPat(Ops.PAD, name="ret"), lambda ctx, ret: (ctx.shrink(tuple([(p[0], s+p[0]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)), (UPat(Ops.PAD, name="ret"), lambda ctx, ret: (ctx.shrink(tuple([(p[0], s+p[0]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
(UPat(Ops.SHRINK, name="ret"), lambda ctx, ret: (ctx.pad(tuple([(p[0], s-p[0]-p[1]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)), (UPat(Ops.SHRINK, name="ret"), lambda ctx, ret: (ctx.pad(tuple([(p[0], s-p[0]-p[1]) for s,p in zip(ret.src[0].shape, ret.marg)])), None, None)),
+10 -3
View File
@@ -123,9 +123,16 @@ class MovementMixin:
# for each dimension, check either dim is 1, or it does not change # for each dimension, check either dim is 1, or it does not change
if not all(s == ns or s == 1 for s, ns in zip(shape, new_shape)): if not all(s == ns or s == 1 for s, ns in zip(shape, new_shape)):
raise ValueError(f"cannot broadcast {self.shape} to {new_shape=}") raise ValueError(f"cannot broadcast {self.shape} to {new_shape=}")
reshaped = self.reshape(shape) # EXPAND only adds dims on the left. squeeze 1s that need expanding, EXPAND on left, permute back.
ret = reshaped._mop(Ops.EXPAND, arg=new_shape) n_left = len(new_shape) - len(self.shape)
return reshaped if ret.shape == reshaped.shape else ret expand_at = tuple(i for i, s in enumerate(self.shape) if resolve(s == 1, default=False) and resolve(new_shape[n_left+i] != 1))
kept = tuple(i for i in range(len(self.shape)) if i not in expand_at)
squeezed = self.reshape(tuple(self.shape[i] for i in kept))
expanded = squeezed._mop(Ops.EXPAND, arg=new_shape[:n_left] + tuple(new_shape[n_left+i] for i in expand_at))
# expanded shape = [left] + [expand_at dims] + [kept dims], permute to new_shape
perm = tuple(range(n_left)) + tuple(
n_left + (expand_at.index(i) if i in expand_at else len(expand_at) + kept.index(i)) for i in range(len(self.shape)))
return expanded.permute(perm)
def expand(self, shape, *args) -> Self: def expand(self, shape, *args) -> Self:
""" """
+2 -2
View File
@@ -134,7 +134,7 @@ def apply_movement_op(op:Ops, in_shape:tuple[sint,...], arg:tuple, rngs:tuple[UO
case Ops.SHRINK: rngs = tuple(a if off == 0 else a+off for a,(off,_) in zip(rngs, arg)) case Ops.SHRINK: rngs = tuple(a if off == 0 else a+off for a,(off,_) in zip(rngs, arg))
case Ops.PERMUTE: rngs = tuple(rngs[p] for p in argsort(arg)) case Ops.PERMUTE: rngs = tuple(rngs[p] for p in argsort(arg))
case Ops.FLIP: rngs = tuple(((s-1)-a) if f else a for a,s,f in zip(rngs, in_shape, arg)) case Ops.FLIP: rngs = tuple(((s-1)-a) if f else a for a,s,f in zip(rngs, in_shape, arg))
case Ops.EXPAND: rngs = tuple(a if in_sh == out_sh else a.const_like(0) for a,in_sh,out_sh in zip(rngs, in_shape, arg)) case Ops.EXPAND: rngs = rngs[len(arg):]
case Ops.PAD: case Ops.PAD:
# NOTE: the .where(r-s, i) is not inside the graph_rewrite so that `convert_pad_to_where_to_keep_behavior_local` # NOTE: the .where(r-s, i) is not inside the graph_rewrite so that `convert_pad_to_where_to_keep_behavior_local`
# wraps the pad with only the newly added valid # wraps the pad with only the newly added valid
@@ -248,7 +248,7 @@ def run_rangeify(tsink:UOp, debug:bool=False) -> tuple[UOp, IndexingContext]:
# if the EXPAND is used to inject a range, we don't mark it as ending_ranges. otherwise we do. # if the EXPAND is used to inject a range, we don't mark it as ending_ranges. otherwise we do.
# NOTE: this doesn't actually always end a range, but this is why convs are realized, so for now we need it # NOTE: this doesn't actually always end a range, but this is why convs are realized, so for now we need it
if x.op is Ops.EXPAND and all(isinstance(y, int) or y.op is not Ops.RANGE for y in x.shape): if x.op is Ops.EXPAND and all(isinstance(y, int) or y.op is not Ops.RANGE for y in x.shape):
ending_ranges[x] += list(UOp.sink(*[ro for ri, ro in zip(rngs, out_rngs) if ri is not ro]).ranges.keys()) ending_ranges[x] += list(UOp.sink(*out_rngs[:len(x.marg)]).ranges.keys())
# REDUCE creates ranges for the axes it is reducing # REDUCE creates ranges for the axes it is reducing
if x.op is Ops.REDUCE and x.arg[1]: if x.op is Ops.REDUCE and x.arg[1]:
+2 -3
View File
@@ -84,9 +84,8 @@ def reshape_multi(root:UOp, multi:UOp):
return multi.src[0].reshape(new_shape).multi(new_axis) return multi.src[0].reshape(new_shape).multi(new_axis)
def expand_multi(root:UOp, multi:UOp): def expand_multi(root:UOp, multi:UOp):
if multi.axis is None: new_shape = root.marg new_axis = None if multi.axis is None else multi.axis + len(root.marg)
else: new_shape = tuple(multi.src[0].shape[multi.axis] if a == multi.axis else s for a,s in enumerate(root.marg)) return multi.src[0]._mop(Ops.EXPAND, arg=root.marg).multi(new_axis)
return multi.src[0].expand(new_shape).multi(multi.axis)
def pad_multi(root:UOp, multi:UOp): def pad_multi(root:UOp, multi:UOp):
assert multi.axis is None or root.marg[multi.axis] == (0, multi.shape[multi.axis]), f"padding not supported for {root.marg=}" assert multi.axis is None or root.marg[multi.axis] == (0, multi.shape[multi.axis]), f"padding not supported for {root.marg=}"
+5 -5
View File
@@ -322,9 +322,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if prod(ps) != prod(self.marg): raise ValueError(f"bad reshape: {ps} -> {self.marg}") if prod(ps) != prod(self.marg): raise ValueError(f"bad reshape: {ps} -> {self.marg}")
return self.marg return self.marg
case Ops.EXPAND: case Ops.EXPAND:
if len(ps) != len(self.marg) or not all(s==ns or (s==1 and ns>=0) for s,ns in zip(ps, self.marg)): return tuple(self.marg) + ps
raise ValueError(f"bad expand: {ps} -> {self.marg}")
return self.marg
case Ops.PERMUTE: case Ops.PERMUTE:
if sorted(self.marg) != list(range(len(ps))): raise ValueError(f"invalid permutation {self.marg} of len {len(ps)}") if sorted(self.marg) != list(range(len(ps))): raise ValueError(f"invalid permutation {self.marg} of len {len(ps)}")
return tuple(ps[i] for i in self.marg) return tuple(ps[i] for i in self.marg)
@@ -548,7 +546,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
ret = UOp.vectorize(*stk) ret = UOp.vectorize(*stk)
else: else:
ret = UOp(Ops.CONST, dtype, arg=dtype.const(b), src=()) ret = UOp(Ops.CONST, dtype, arg=dtype.const(b), src=())
return ret.reshape((1,)*len(shape)).expand(shape) if shape is not None and shape != () and ret.shape != shape else ret return ret._mop(Ops.EXPAND, arg=shape) if shape is not None and shape != () and ret.shape != shape else ret
@staticmethod @staticmethod
def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs): def range(end:sint, axis_id, axis_type=AxisType.LOOP, *arg, dtype=dtypes.weakint, src=(), **kwargs):
return UOp(Ops.RANGE, dtype=dtype, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs) return UOp(Ops.RANGE, dtype=dtype, src=(sint_to_uop(end, dtype),)+src, arg=(axis_id, axis_type)+arg, **kwargs)
@@ -637,6 +635,7 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
if self.shape[new_axis] % len(self.device) != 0: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards") if self.shape[new_axis] % len(self.device) != 0: raise RuntimeError(f"reshape {self.src[0].shape} -> {self.shape} moved items between shards")
return new_axis return new_axis
if self.op is Ops.PERMUTE: return self.marg.index(src_axis) if src_axis is not None else None if self.op is Ops.PERMUTE: return self.marg.index(src_axis) if src_axis is not None else None
if self.op is Ops.EXPAND: return src_axis + len(self.marg) if src_axis is not None else None
return src_axis return src_axis
def _unshard(self, axis:int) -> UOp: def _unshard(self, axis:int) -> UOp:
@@ -688,7 +687,8 @@ class UOp(RandMixin, metaclass=UOpMetaClass):
def _mop(self, op:Ops, arg) -> UOp: def _mop(self, op:Ops, arg) -> UOp:
# early NOOP # early NOOP
if op in {Ops.SHRINK, Ops.PAD, Ops.EXPAND} and len(arg) == 0: if op is Ops.EXPAND and len(arg) == 0: return self
if op in {Ops.SHRINK, Ops.PAD} and len(arg) == 0:
assert len(self.shape) == 0, "0 len arg only valid on zero length shape" assert len(self.shape) == 0, "0 len arg only valid on zero length shape"
return self return self
match op: match op: