Skip to content

Two-arg tile() is unusable, and the obvious half-fix makes it silently wrong #73

Description

@bigSheep123

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions