Two-arg tile() is unusable, and the obvious half-fix makes it silently wrong
Affects: main @ e55e4b0
Summary
for i in tile(extent, step) with x[:, i] — the documented way to sweep a
tiled window (parser §1.7) —
cannot be used. There are two separate defects, and they interact badly: fixing
only the first turns a compile-time error into an out-of-bounds read.
I am filing this rather than a PR because closing it properly means resolving a
contradiction inside the spec, which is a call for the maintainers.
Defect 1 — the window extent never folds, so the slice fails type inference
K, BK = 512, 256
@func
def sweep(x: Tensor[(4, K), "f32"]) -> Tensor[(4, BK), "f32"]:
acc = x[:, 0:BK]
for kt in tile(K, BK):
acc = tf.add(acc, x[:, kt])
return acc
VerifyError: Binary ADD: cannot broadcast shapes (4, 256) and
(4, ((kt*256 + 256) - (kt*256) + 0) // 1)
RangeSlice gives begin = iv * step and stop = begin + step, so
_slice_dim computes (begin + step) - begin. simplify_dim folds all-constant
chains only and by contract performs no algebraic cancellation, so the extent
stays symbolic and never matches a statically shaped operand.
Defect 2 — the window offset is multiplied by the step twice
The loop domain and the window disagree about what the induction variable
counts:
parser/hir_parser.py builds the domain as (start=0, extent, step), and
evaluator/interpreter.py:152 iterates it as range(start, extent, step).
So for tile(512, 256) the iv takes 0, 256 — element units.
parser/range_slice.py:65 derives start = DimMul(iv, step) — treating the
iv as a tile counter.
Composed, iteration 1 addresses element 256 * 256 = 65536 of a tensor that is
512 wide:
loop domain : start=0 extent=512 step=256
induction var takes: [0, 256] <- element units
window begin expr : kt * 256
iv= 0 -> window starts at element 0
iv=256 -> window starts at element 65536
Why the partial fix is worse than the bug
Defect 1 is easy to patch locally — recover the width when end is structurally
DimAdd(begin, width) and return width directly instead of asking
simplify_dim to cancel. I tried exactly that, and it is a trap:
|
result |
main as-is |
VerifyError at type inference — loud, unusable |
| defect 1 patched alone |
compiles and runs, reading element 65536 — silent |
The broadcast failure is currently the only thing preventing defect 2 from
producing wrong data. Patching it without also settling defect 2 removes the
guard rail. Worth a note in whatever fixes this.
The spec pins both halves
parser §1.7 states both, and they cannot both hold:
tile(...) and range(...) share one loop domain (start, extent, step)
… the domain is half-open [start, extent)
tile(extent, step) binds i to a parser-side RangeSlice
(start = iv * step, stop = start + step)
A shared range-style domain makes the iv advance by step in element units; a
start = iv * step window needs it to advance by 1 in tile units. So this is not
an implementation slip against a clear contract — the contract itself has to
change on one side.
Suggested resolution
Keep the element-unit loop domain, since it is what range() means and what the
shared-domain sentence promises, and change RangeSlice.start from iv * step
to iv, with stop = iv + step. That also makes defect 1 disappear on its own:
stop - start becomes (iv + step) - iv, which the same structural-recovery
rule folds to step — and, unlike the local patch above, it folds to the
correct extent for a window that is now at the right offset.
The alternative — make the loop domain count tiles — would make tile(extent, step) iterate differently from range(0, extent, step) and contradict the
shared-domain sentence instead. Either is defensible; the current pair is not.
Happy to send a PR once you have decided which side gives way.
Two-arg
tile()is unusable, and the obvious half-fix makes it silently wrongAffects:
main@e55e4b0Summary
for i in tile(extent, step)withx[:, i]— the documented way to sweep atiled window (parser §1.7) —
cannot be used. There are two separate defects, and they interact badly: fixing
only the first turns a compile-time error into an out-of-bounds read.
I am filing this rather than a PR because closing it properly means resolving a
contradiction inside the spec, which is a call for the maintainers.
Defect 1 — the window extent never folds, so the slice fails type inference
RangeSlicegivesbegin = iv * stepandstop = begin + step, so_slice_dimcomputes(begin + step) - begin.simplify_dimfolds all-constantchains only and by contract performs no algebraic cancellation, so the extent
stays symbolic and never matches a statically shaped operand.
Defect 2 — the window offset is multiplied by the step twice
The loop domain and the window disagree about what the induction variable
counts:
parser/hir_parser.pybuilds the domain as(start=0, extent, step), andevaluator/interpreter.py:152iterates it asrange(start, extent, step).So for
tile(512, 256)the iv takes 0, 256 — element units.parser/range_slice.py:65derivesstart = DimMul(iv, step)— treating theiv as a tile counter.
Composed, iteration 1 addresses element
256 * 256 = 65536of a tensor that is512 wide:
Why the partial fix is worse than the bug
Defect 1 is easy to patch locally — recover the width when
endis structurallyDimAdd(begin, width)and returnwidthdirectly instead of askingsimplify_dimto cancel. I tried exactly that, and it is a trap:mainas-isVerifyErrorat type inference — loud, unusableThe broadcast failure is currently the only thing preventing defect 2 from
producing wrong data. Patching it without also settling defect 2 removes the
guard rail. Worth a note in whatever fixes this.
The spec pins both halves
parser §1.7 states both, and they cannot both hold:
A shared
range-style domain makes the iv advance bystepin element units; astart = iv * stepwindow needs it to advance by 1 in tile units. So this is notan implementation slip against a clear contract — the contract itself has to
change on one side.
Suggested resolution
Keep the element-unit loop domain, since it is what
range()means and what theshared-domain sentence promises, and change
RangeSlice.startfromiv * stepto
iv, withstop = iv + step. That also makes defect 1 disappear on its own:stop - startbecomes(iv + step) - iv, which the same structural-recoveryrule folds to
step— and, unlike the local patch above, it folds to thecorrect extent for a window that is now at the right offset.
The alternative — make the loop domain count tiles — would make
tile(extent, step)iterate differently fromrange(0, extent, step)and contradict theshared-domain sentence instead. Either is defensible; the current pair is not.
Happy to send a PR once you have decided which side gives way.