fix(dsl): Reject cross-path kwargs in unified ops instead of dropping them - #2267
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe unified operation layer now validates Tensor- and Tile-specific keyword arguments instead of silently ignoring them. Matrix operations, reductions, arg-reductions, and ChangesUnified operation contracts
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8af9826af4
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Keep the documented default values accepted by the overloads. The new Tile / Tensor overloads omitted the cross-path kwarg entirely, so a caller spelling out the documented default — `pl.rsqrt(tile, high_precision=False)`, `pl.row_max(tensor, None)` — was rejected statically even though the runtime guard and the docs both promise it stays a no-op. Declare those parameters with `Literal[False]` / `None` defaults on the path that cannot honour them, so the static contract matches the runtime exactly: the default type-checks, a non-default value is a static error as well as a runtime one. Verified with a pyright probe — all seven documented-default calls pass, all five non-default calls error. This also sharpens the diagnostic. `pl.matmul(tile, tile, b_trans=True)` previously mis-resolved to the Tensor overload and reported "Tile cannot be assigned to parameter lhs of type Tensor"; it now points at the offending kwarg directly, mirroring the runtime message. Adds runtime coverage for both flagged forms: `rsqrt(tile, high_precision=False)` and the nine reductions with an explicit `tmp_tile=None`.
… them Fixes hw-native-sys#2264 The unified `pl.*` wrappers dispatch on operand type and accept the union of both levels' kwargs. A kwarg that only the other path could honour was silently discarded — no error, no warning, no IR trace. For `pl.matmul` this was a correctness bug. `pl.matmul(tile_a, tile_b, b_trans=True)` compiled `A @ B`: with a square B the shapes stayed consistent and the kernel computed the wrong math cleanly; with a non-square B it failed in C++ type deduction ("lhs K=512 and rhs K=128") without ever naming `b_trans`. That is how it was found — a kernel whose matmul operands were migrated from Tensor to Tile while the `b_trans=True` line was left behind. This is correct IR design, not an IR bug. A tensor value carries no layout, so a flag is the only place transposition can live; at tile level it is a type property expressed by the zero-copy `tile.transpose_view`, and `tile.matmul` maps 1:1 onto `pto.tmatmul`, which has no transpose attribute. Tile ops must not gain transpose args. The defect was that the frontend accepted a request it could not honour and stayed silent — the `@overload` declarations already encoded the right contract for matmul/matmul_acc; only the runtime failed to enforce it. Guards added, each raising only on a non-default value so spelling out `b_trans=False` stays a no-op: - `matmul` / `matmul_acc`: `a_trans`, `b_trans`, `c_matrix_nz` on the Tile path raise and point at `pl.tile.transpose_view`. - `matmul`: `out_dtype` on the Tile path is verified rather than dropped. The Cube accumulator fixes the result dtype (FP32 for float operands, INT32 for int), so a matching request is honoured and a mismatched one raises naming `pl.cast`. The deduced dtype is read off the built call instead of re-deriving the C++ rule in Python. - `rsqrt`: `high_precision=True` on the Tile path raised nothing before and silently yielded the low-precision path, since `tile.rsqrt` selects precision by taking a scratch tile rather than by an attribute. - The nine `tmp_tile` reductions (`row_max`/`row_sum`/`row_min`/`row_prod`, `col_sum`, `row_argmax`/`row_argmin`/`col_argmax`/`col_argmin`): a `tmp_tile` on the Tensor path now raises. For `col_sum` the drop was strategy-meaningful — the requested binary-tree reduction was discarded and the scratch tile allocated but never used; for the rest the user's tile was silently dead while still consuming UB budget. The in-file comment already claimed this family rejected it. Ten functions that had no `@overload` declarations gain the split, so the static contract matches the runtime one. The existing `_reject_tmp_for_tensor` helper moves to the shared-guards section and takes the parameter name, so the reductions reuse it for `tmp_tile`. No existing caller changes behaviour: the only Tile-path sites passing a Tensor-only kwarg are two `out_dtype=pl.FP32` matmuls whose operands are BF16, which is exactly the dtype the accumulator deduces.
Keep the documented default values accepted by the overloads. The new Tile / Tensor overloads omitted the cross-path kwarg entirely, so a caller spelling out the documented default — `pl.rsqrt(tile, high_precision=False)`, `pl.row_max(tensor, None)` — was rejected statically even though the runtime guard and the docs both promise it stays a no-op. Declare those parameters with `Literal[False]` / `None` defaults on the path that cannot honour them, so the static contract matches the runtime exactly: the default type-checks, a non-default value is a static error as well as a runtime one. Verified with a pyright probe — all seven documented-default calls pass, all five non-default calls error. This also sharpens the diagnostic. `pl.matmul(tile, tile, b_trans=True)` previously mis-resolved to the Tensor overload and reported "Tile cannot be assigned to parameter lhs of type Tensor"; it now points at the offending kwarg directly, mirroring the runtime message. Adds runtime coverage for both flagged forms: `rsqrt(tile, high_precision=False)` and the nine reductions with an explicit `tmp_tile=None`.
350a5ed to
66821f1
Compare
Fixes #2264
Problem
The unified
pl.*wrappers dispatch on operand type and accept the union of both levels' kwargs. A kwarg that only the other path could honour was silently discarded — no error, no warning, no IR trace.For
pl.matmulthis is a correctness bug.pl.matmul(tile_a, tile_b, b_trans=True)compiledA @ B:lhs K=512 and rhs K=128) without ever namingb_trans.That is how it was found in the wild — a kernel whose matmul operands were migrated from
TensortoTile(pl.create_l1→pl.create_tile, tensor slice →pl.load) while thepl.matmul(..., b_trans=True)line was left untouched.This is correct IR design, not an IR bug. A tensor value carries no layout, so a flag is the only place transposition can live; at tile level it is a type property expressed by the zero-copy
tile.transpose_view, andtile.matmulmaps 1:1 ontopto.tmatmul, which has no transpose attribute. PRs #1776 → #1866 → #1883 deliberately removed transpose parameters from the tile layer. The defect was that the frontend accepted a request it could not honour and stayed silent — the@overloaddeclarations already encoded the right contract formatmul/matmul_acc; only the runtime failed to enforce it.Change
Twelve silent-drop sites now raise, each only on a non-default value — spelling out
b_trans=Falsestays a no-op:matmul/matmul_acca_trans,b_trans,c_matrix_nzpl.tile.transpose_viewmatmulout_dtypersqrthigh_precisiontile.rsqrtselects precision by taking a scratch tile, not by an attributetmp_tileReductions covered:
row_max,row_sum,row_min,row_prod,col_sum,row_argmax,row_argmin,col_argmax,col_argmin. Forcol_sumthe drop was strategy-meaningful — the requested binary-tree reduction (O(log M) depth, better FP accumulation) was discarded and the scratch tile allocated but never used. For the other eight the conversion pass synthesizes its own scratch, so results matched, but the user's tile was silently dead while still consuming UB budget. The in-file comment already claimed this family rejected it.Ten functions that had no
@overloaddeclarations gain the split, so the static contract matches the runtime one. The existing_reject_tmp_for_tensorhelper moves to a shared-guards section and takes the parameter name so the reductions reuse it fortmp_tile.out_dtypeis verified rather than rejected outrighttile.matmulhardcodes its result dtype — FP32 for float operands, INT32 for int (src/ir/op/tile_ops/matmul.cpp:88-89) — in a 4-byte-fractal Acc (L0C) tile. The hardware has no FP16 matmul output.Tensor-level
out_dtype=FP16compiles because the narrowing is absorbed downstream, not by the matmul. The conversion rule discards the kwarg outright ((void)kwargs;,src/ir/transforms/op_conversion_registry.cpp:1067);out_dtypeonly sets the declared tensor type, and the conversion rides on the consuming store:```python
t__tile: pl.Tile[[32, 128], pl.FP32, pl.Mem.Acc] = pl.tile.matmul(a_mat, b_mat) # always FP32
ret0__store: pl.Tensor[[32, 128], pl.FP16] = pl.tile.store(t__tile, [0, 0], ret0__out)
```
So on the Tile path a matching
out_dtypeis a harmless redundant assertion and a mismatched one is unfulfillable. The guard compares against the dtype read off the built call rather than re-deriving the C++ rule in Python (which would silently rot), accepts the match, and raises otherwise naming `pl.cast`.Auto-inserting a `tile.cast` was considered and rejected: cast is a Vec-unit op while matmul is Cube, so it would hide a real instruction plus a UB buffer behind a kwarg, against the tile layer's explicit-ops design.
Before / after
```python
at = pl.load(a, [0, 0], [32, 128], target_memory=pl.MemorySpace.Mat)
bt = pl.load(b, [0, 0], [128, 128], target_memory=pl.MemorySpace.Mat)
c = pl.matmul(at, bt, b_trans=True, out_dtype=pl.FP32)
```
Before — no diagnostic at any stage, wrong math:
```python
c: pl.Tile[[32, 128], pl.FP32, pl.Mem.Acc] = pl.tile.matmul(at, bt)
```
After:
```text
InvalidOperationError: pl.matmul: 'b_trans' is not supported for Tile operands.
At tile level a transposed operand is an explicit zero-copy view, not an op flag:
wrap the operand with pl.tile.transpose_view(...) and pass it directly.
```
The suggested remedy compiles and yields the correct `[32, 128]` result:
```python
c = pl.matmul(at, pl.tile.transpose_view(bt), out_dtype=pl.FP32)
-> c: pl.Tile[[32, 128], pl.FP32, pl.Mem.Acc] = pl.tile.matmul(at, pl.tile.transpose_view(bt))
```
Compatibility
An AST sweep of `tests/`, `examples/`, and `python/` found only two Tile-path sites passing a Tensor-only kwarg — `tests/st/runtime/ops/test_matmul.py:700,703`, both `out_dtype=pl.FP32` on BF16 operands, i.e. exactly the dtype the accumulator deduces. Both keep working unchanged; I built that kernel directly to confirm it still lowers identically. The other 121 matching call sites are Tensor-path.
Tests
Full unit suite: 8638 passed. Docs updated in both `docs/en` and `docs/zh` with the per-path signatures and the general rule (and a previously missing `row_min` row).
Scope
Silent-drop class only, per the issue's own "Suggested shape of the fix". Deliberately not included, each deserving its own review: the dead-end-to-end `c_matrix_nz` attr, the tensor-vs-tile `out_dtype` default divergence, and the five positional-misbind signature divergences (`random`, `ir.transpose`, `ir.scatter_mask`, `scatter`/`gather`, `mrgsort`).