Repository navigation
Conversation
nmalimban
force-pushed
the
tmtensor-bufferize-read-only
branch
from
October 9, 2026 18:56
bd0e89a to
00403bc
Compare
`tm-tensor-bufferize` left each tensor->buffer crossing where the dialect
conversion driver put it: at the operand's *definition*, as a target
materialization. Because the pass is only a partial bufferizer, the
TMTensor op it feeds is a pure memref op whose read is invisible to
One-Shot Bufferize, whose analysis is defined over tensor OpOperands. The
only read One-Shot can see is the `bufferization.to_buffer` itself, and
being at the definition it `happensBefore` any later write, so no
read-after-write conflict is found and both values may be folded onto one
buffer.
`convert-torch-to-tmtensor` emits exactly that shape for
`torch.aten.scatter_reduce.two` with `include_self=false`: the reduction
identity (a `linalg.fill`) and the update values (a `linalg.generic`) are
computed into the same `tensor.empty` init, then read by two separate
`tm_tensor.scatter`s. Collapsing them destroys the identity, so the
"clear" scatter plants the update values and the "add" scatter adds them a
second time -- 2x the expected result at every scatter target for
`reduce='sum'`. Every other reduce mode is wrong too, just differently:
with a non-zero identity it is the updates that are lost, and `amax`
yields the identity `-inf` in place.
State each crossing in the pattern instead, at the TMTensor op that reads
the operand:
- `ins` operands get `bufferization.to_buffer %operand read_only`.
`ToBufferOp::bufferizesToMemoryRead` is unconditionally true, so at this
position the crossing really is a read-after-write against any
intervening write and One-Shot finds the conflict. `read_only` keeps it
from counting as a write as well, which is truthful: a TMTensor op never
writes an `ins` operand.
- A payload-read `outs` operand is copied into the buffer the op updates
in place directly from its *tensor* operand, rather than cloning a
buffer obtained at the definition, for the same reason: a copy with a
tensor source is itself the read, so it lands at the op. `cloneMemref`
is no longer needed.
- With no converted operand consumed, a write-only `outs` operand's
dynamic sizes come from the original tensor operand. Sound by the
destination-passing-style contract: init operands and their tied
OpResults have the same type, and dynamic dimension sizes match at
runtime.
`materializeToTensor` now marks its `bufferization.to_tensor` `restrict`.
Without it, One-Shot Analysis rejects this pass's own output outright
("to_tensor ops without `restrict` are not supported by One-Shot
Analysis"), so no pipeline that analyses rather than copies defensively
could consume it. `restrict` is truthful: every buffer reaching this
materialization comes from `allocateBuffersForResults` and is a fresh
`memref.alloc`.
`bufferization::BufferizationDialect` joins the `ConversionTarget`'s legal
dialects, because ops created by a pattern are legalized -- unlike
materializations, which are not.
Tests:
- `bufferize-scatter-reduce-numeric.mlir` starts from the real
`torch.aten.scatter_reduce.two`, finishes bufferization with plain
`--one-shot-bufferize` (no `copy-before-write`, which skips the analysis
and hides this), lowers to LLVM and executes under `mlir-runner`,
checking printed values for both `sum` and `amax`. `cse` in its pipeline
is load-bearing: the lowering emits two structurally identical
`tensor.empty` + `linalg.fill` pairs and CSE is what merges them into
the shared init.
- `bufferize-one-shot-hazard.mlir` pins the IR-level property: the clear
loop and the add loop must read distinct buffers.
- `bufferize.mlir` CHECKs updated, plus a new `@scan_1d_dynamic` for the
dynamically-shaped write-only `outs` path; every other function in the
file is statically shaped, so that branch was never reached.
All three fail on a build with `Bufferize.cpp` reverted.
`test/lit.cfg.py` gains a `%mlir_lib_dir` substitution, `mlir-opt` and
`mlir-runner` tool substitutions, and an `mlir-runner` feature gate keyed
on the runner support library existing, so a build configured for
torch-mlir alone reports UNSUPPORTED rather than failing.
`test/CMakeLists.txt` adds the matching test dependencies (`mlir-opt` was
already a latent gap -- `check-torch-mlir` never built it).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nmalimban
force-pushed
the
tmtensor-bufferize-read-only
branch
from
October 9, 2026 19:07
00403bc to
09a15ed
Compare
nmalimban
marked this pull request as ready for review
October 9, 2026 19:19
Contributor
Author
|
Hi @sahas3 @hariprasadravi, I am requesting reviews via this comment since I do not have Triage permissions for this repo. Thanks! |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tm-tensor-bufferizeis a partial bufferizer: the TMTensor ops it rewritesbecome pure memref ops, whose reads One-Shot Bufferize cannot see (its
analysis is defined over tensor operands). Each tensor->buffer crossing was
left where the conversion driver put it -- a
bufferization.to_bufferat theoperand's definition. That is the only read One-Shot sees, and since it
happens before any later write, One-Shot finds no read-after-write conflict
and may fold two values onto one buffer.
convert-torch-to-tmtensorproduces exactly this shape fortorch.aten.scatter_reduce.twowithinclude_self=false: the reductionidentity (a
linalg.fill) and the update values (alinalg.generic) shareone
tensor.emptyinit, and are then read by twotm_tensor.scatters. Withthe two writers collapsed,
reduce='sum'returns 2x the expected value atevery scatter target, and the other reduce modes are wrong in other ways
(e.g.
amaxreturns the identity,-inf).This does not show up in the e2e suite, for two independent reasons:
refbackendruns One-Shot withcopy-before-write, which skips theanalysis, and its
linalg-fuse-elementwise-opsdissolves the shared initbefore bufferization runs (any later
csebrings it back).Example
The shape
convert-torch-to-tmtensoremits (reduced fromtest/Dialect/TMTensor/bufferize-one-shot-hazard.mlir). One init, twowriters, two readers:
Without the fix,
-tm-tensor-bufferizestates the read of%zerosatits definition, before the
linalg.genericwrites the same init:%4is the only read of%zerosthat One-Shot can see, and it comes beforethe generic, so One-Shot sees no conflict and bufferizes both writers onto
the same buffer. The generic overwrites the zeros, and both loops then read
the values:
With the fix, the read is stated at the scatter that performs it, after
both writers:
One-Shot now sees a read of
%zerosafter a conflicting write, so it givesthe generic a buffer of its own:
(Final IR is from
-tm-tensor-bufferize -tm-tensor-to-loops | mlir-opt --one-shot-bufferize="allow-unknown-ops" -canonicalize.)Change
Each crossing is now stated inside the pattern, at the TMTensor op that
reads the operand, and built from the original tensor operand rather than
from the adaptor's converted value:
insoperands:bufferization.to_buffer %operand read_only. At the op,this is a read-after-write against any intervening write, so One-Shot
finds the conflict.
read_onlyis truthful: TMTensor ops never writeinsoperands.outsoperands read by the payload:memref.alloc+materialize_in_destinationfrom the tensor operand, replacingcloneMemrefon the converted buffer.outsoperands the payload does not read, with dynamic shapes: sizesare taken from the tensor operand (DPS guarantees they match).
materializeToTensornow marks itsto_tensorrestrict. Without it,One-Shot Analysis rejects the pass's output outright, so nothing that
analyses (rather than copying defensively) could consume it.
restrictholds: every buffer reaching it is a fresh
memref.alloc.Cost
On
main, One-Shot Analysis rejects this pass's output (theto_tensorlacks
restrict), so there is no analysed baseline to compare against.The "without the fix" column below is the cost if One-Shot could run its
analysis on the old output -- i.e. the old crossings plus only the
restrictchange. Counts arememref.copyaftertm-tensor-bufferize,tm-tensor-to-loopsthenone-shot-bufferize{bufferize-function-boundaries function-boundary-type-conversion=identity-layout-map}:scaninclusive / exclusive / dynamicscatterupdate / addsorttopkattentionThe fix adds no copies. On the hazard IR, One-Shot resolves the conflict by
giving the clobbering
linalg.generica freshoutsbuffer. That costs nocopy, because the generic's payload does not read its
outs.Tests
bufferize-scatter-reduce-numeric.mlir: lowers the real torch op, runsplain
--one-shot-bufferize, executes the result undermlir-runner,and checks the printed values for
sumandamax, which fail in the twodifferent ways described above. Guarded by
REQUIRES: mlir-runner.bufferize-one-shot-hazard.mlir: checks at the IR level that the twoscatter loops read distinct buffers.
bufferize.mlir: CHECKs updated, plus@scan_1d_dynamicfor thedynamic write-only
outspath.All three fail with
Bufferize.cppreverted.test/lit.cfg.pyandtest/CMakeLists.txtgain themlir-opt/mlir-runnertools anddependencies these tests need.
🤖 Generated with Claude Code