Skip to content

fix(ir): validate tile.load / tile.store shapes and valid_shape elements - #2570

Merged
lyfne123 merged 1 commit into
hw-native-sys:mainfrom
Hzfengsy:claude/tile-load-store-validation-d76add
Aug 31, 2026
Merged

fix(ir): validate tile.load / tile.store shapes and valid_shape elements#2570
lyfne123 merged 1 commit into
hw-native-sys:mainfrom
Hzfengsy:claude/tile-load-store-validation-d76add

Conversation

@Hzfengsy

Copy link
Copy Markdown
Member

Summary

  • validate that every element of tile.load's shapes / valid_shape and tile.store's optional shapes is a ScalarType with an integer dtype, closing the last three slots in the family fix(ir): validate that tile.load / tile.store offset elements are integer scalars #2565 started
  • restore the valid-region-fits and reads-past-the-end proofs, which a malformed extent had been silently switching off
  • give the DSL's keyword spelling of the same mistake a diagnostic that names it, instead of a bare Cannot convert <class 'list'>

Why

#2565 gave the offsets tuple of tile.load / tile.store an element-wise check. The shapes and valid_shape tuples on the same two ops never got one, even though both op registrations declare them "TupleType of ScalarType" — so the same silent miscompile stayed reachable through a different argument.

Nothing downstream compensates, by design. InferWindowReadValidShape's obligations are defined only over integer scalars, so a non-scalar element makes them undecidable rather than false. The checks then pass, and codegen lowers whatever the element reduces to — its last leaf, for a nested tuple.

A realistic way to write it: reading valid_shape as per-dim [start, extent] pairs, mirroring the offsets / shapes pair right before it.

@pl.function(type=pl.FunctionType.InCore)
def kernel(self, a: pl.Tensor[[128, 128], pl.FP32],
           out: pl.Tensor[[128, 128], pl.FP32]) -> pl.Tensor[[128, 128], pl.FP32]:
    # meant: valid_shape=[200, 64] — already too big for a 128-row source
    t = pl.load(a, [0, 0], [64, 64], [[200, 200], [64, 64]])
    return pl.store(t, [0, 0], out)

With plain ints, valid_shape=[200, 64] is rejected by tile.load itself:

tile.load valid_shape 0 is 200, which exceeds the window extent 64;
a valid region cannot be larger than the shape that holds it

With the extra bracket level it compiled clean, all the way to valid PTO:

%t = pto.alloc_tile addr = %c0_i64 valid_row = %c200_index valid_col = %c64_index
     : !pto.tile_buf<loc=vec, dtype=f32, rows=64, cols=64, ...>   // 200 valid rows in a 64-row tile
%a_pview = pto.partition_view %a_view, offsets = [%c0, %c0], sizes = [%c200_index, %c64_index]
pto.tload  ins(%a_pview) outs(%t)                                  // reads 200 rows of a 128-row tensor
pto.tstore ins(%t) outs(%out_pview)                                // writes 200 rows into a 128-row output

— an out-of-bounds GM read and an out-of-bounds store, no diagnostic at any layer.

The benign case is arguably worse: valid_shape=[[0, 40], [0, 56]] emits valid_row = 40, valid_col = 56, which looks correct and quietly confirms the wrong mental model.

The other two slots fail less catastrophically but still badly. A nested shapes lands in TileType.shape_ and dies 20 passes later in InitMemRef with InternalError: requires static shape for variable 't__ssa_v0' ... Fix the upstream op — naming neither tile.load nor the nested tuple. tile.store's optional shapes is accepted and silently ignored.

Notes for reviewers

The bar stays IsInt(), matching what tensor.slice enforces for its own shape tuple. Non-INDEX integer extents (INT32) and symbolic dynamic extents remain legal — both are covered by a test, since a stricter bar would break ordinary dynamic-shape kernels.

Only the positional spelling reached the deducer. The DSL parser turns a list literal into a MakeTuple recursively; _wrap_arg unwraps exactly one level, so inner MakeTuples arrive as ir.Expr and sail through _to_make_tuple. Spelled as a kwarg (valid_shape=[[...]]) the list never becomes IR at all — it died in _normalize_expr with Cannot convert <class 'list'> to IR expression, an op-agnostic TypeError naming neither the argument nor the mistake. Two spellings of one mistake, two unrelated behaviours. This adds a nested-sequence branch at that shared conversion point, so both now report something actionable. Nothing in-tree matched on the old message.

Helper rename. ValidateOffsetTupleElements becomes ValidateIntScalarTupleElements(tuple, op_name, role); role names the operand with the spelling the DSL exposes (offset / shapes / valid_shape) so the message points at the argument the author wrote. Existing offset messages are unchanged.

DSL tests use an Any-annotated closure var, as the offsets tests do — pyright rejects a nested list literal statically, so the value's shape has to be knowable only at parse time for the test to exercise what a checker cannot catch.

Testing

  • python -m pytest tests/ut/ir/operators/test_tile_ops.py -k "TileLoadStoreShapeElements or TileLoadStoreOffsetElements" -n "$PYPTO_TEST_JOBS" -v — 17 passed (10 new)
  • python -m pytest tests/ut/ tests/lint/ -n "$PYPTO_TEST_JOBS" — 10787 passed, 3 skipped, 1 xfailed
  • python -m pytest tests/st/codegen -n "$PYPTO_TEST_JOBS" — 60 passed
  • ctest --parallel "$PYPTO_TEST_JOBS" — 1/1 passed
  • pre-commit run --files src/ir/op/tile_ops/memory.cpp python/pypto/ir/utils.py python/pypto/ir/op/tile_ops.py python/pypto/language/op/tile_ops.py tests/ut/ir/operators/test_tile_ops.py

hw-native-sys#2565 gave the offsets tuple of tile.load / tile.store an element-wise
integer-scalar check. The shapes and valid_shape tuples on the same two ops
never got one, even though both op registrations declare them "TupleType of
ScalarType" -- so the same silent miscompile stayed reachable through a
different argument.

A nested valid_shape makes InferWindowReadValidShape's obligations
undecidable rather than false, so the "valid region fits the window" and
"reads past the end" checks stop firing, and codegen lowers the tuple's last
leaf:

    t = pl.load(a, [0, 0], [64, 64], [[200, 200], [64, 64]])

compiled clean on a [128, 128] source, emitting a tload of 200 rows into a
64-row tile:

    %t = pto.alloc_tile addr = %c0_i64 valid_row = %c200_index valid_col = %c64_index
         : !pto.tile_buf<..., rows=64, cols=64, ...>
    %a_pview = pto.partition_view %a_view, offsets = [%c0, %c0], sizes = [%c200_index, %c64_index]

The plain-int spelling of the same request, valid_shape=[200, 64], is
properly rejected. A nested shapes tuple instead lands in TileType.shape_ and
dies 20 passes later in InitMemRef with an InternalError naming neither
tile.load nor the nested tuple; tile.store's optional shapes tuple is
accepted and silently ignored.

Generalize the existing helper to ValidateIntScalarTupleElements(tuple,
op_name, role) and apply it to all three remaining slots. The bar stays
IsInt(), matching what tensor.slice enforces for its own shape tuple, so
non-INDEX integer extents and symbolic dynamic extents remain legal.

The keyword spelling (valid_shape=[[...]]) never becomes IR, so it never
reached the deducer -- it failed in _normalize_expr with a bare "Cannot
convert <class 'list'> to IR expression" that named neither the argument nor
the mistake. Give that shared conversion point a nested-sequence branch, so
both spellings now report something actionable.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T01:57:01.072598Z da76ac2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 165c420b-b054-42e3-962e-a0f0eac7d29d

📥 Commits

Reviewing files that changed from the base of the PR and between 4585aa7 and da76ac2.

📒 Files selected for processing (5)
  • python/pypto/ir/op/tile_ops.py
  • python/pypto/ir/utils.py
  • python/pypto/language/op/tile_ops.py
  • src/ir/op/tile_ops/memory.cpp
  • tests/ut/ir/operators/test_tile_ops.py

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change requires tile shapes and valid_shape elements to be integer scalars. Native validation now covers load and store arguments, Python normalization reports nested sequences clearly, and unit tests cover accepted and rejected forms.

Changes

Tile shape validation

Layer / File(s) Summary
Shape argument contract
python/pypto/ir/op/tile_ops.py, python/pypto/language/op/tile_ops.py, python/pypto/ir/utils.py
Documentation defines flat integer scalar elements for shapes and valid_shape. _normalize_expr reports nested sequences passed where scalar expressions are required.
Native tile validation
src/ir/op/tile_ops/memory.cpp
Load validation now covers offsets, shapes, and valid_shape. Store validation covers offsets and optional shapes. Diagnostics identify the argument role.
Validation coverage
tests/ut/ir/operators/test_tile_ops.py
Tests cover nested and floating-point elements, valid integer and symbolic extents, oversized requests, optional store shapes, and DSL call paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to da76a

The change adds early validation for malformed tile shape arguments and improves diagnostics without expanding runtime or security exposure; no actionable merge-blocking risk remains.

Poem

A rabbit checks each shape with care
Flat integer steps go everywhere
Nested pairs now meet a sign
Clear errors mark the boundary line
Load and store keep bounds in sight
Tests hop through the code just right

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the validation changes, restored bounds checks, diagnostic improvements, affected APIs, rationale, and test coverage.
Title check ✅ Passed The title clearly and concisely describes the main change: validating shapes and valid_shape elements for tile.load and tile.store.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lyfne123
lyfne123 merged commit 4054785 into hw-native-sys:main Aug 31, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants