Skip to content

Tensor<ArenaTensor>: in-place binary ops must not drop populated cells into null destinations - #575

Open
evaleev wants to merge 4 commits into
masterfrom
evaleev/fix/arena-tot-inplace-add-drops-null-cells
Open

Tensor<ArenaTensor>: in-place binary ops must not drop populated cells into null destinations#575
evaleev wants to merge 4 commits into
masterfrom
evaleev/fix/arena-tot-inplace-add-drops-null-cells

Conversation

@evaleev

@evaleev evaleev commented Sep 4, 2026

Copy link
Copy Markdown
Member

Nested tiles Tensor<ArenaTensor<T>> lost data in in-place addition when a destination inner cell was null while the source cell was populated: the arena add_to kernel returned silently on a null destination (an ArenaTensor is a non-owning view and cannot adopt the source), and out-of-place a(l) + b(r) was routed into that in-place path by Add::eval whenever an operand carried a permutation (ArrayEvalImpl::is_consumable()), so x = a + b could annihilate b while x += b did not. Found through a NaN residual in a downstream application (an all-empty nested intermediate added to a populated one).

Changes:

  • Tensor::add_to / subt_to / mult_to / axpy_to (scaled and unscaled) for tensor-of-view value types fall back to the value-returning union kernel when any destination cell is null and the source cell is not; guarded with if constexpr so non-view tensors (including complex tiles with integral factors) are unaffected.
  • is_consumable_tile<Tensor<T>> is false when T is a tensor view, so BinaryWrapper never converts an out-of-place add over arena tiles into an in-place one.
  • The free arena kernels add_to / subt_to / axpy_to throw TiledArray::Exception on a null destination with a populated source instead of dropping data; mult_to treats a null destination as zero (no error) and now zeroes the destination on a null source (previously left unchanged).
  • Missing return in the tensor-of-tensor Tensor::add when the left operand is empty (fell through into the kernel and produced an empty result).
  • Tests in tests/arena_tot_trivial.cpp: tile-level add_to/subt_to/mult_to/axpy_to with all-null and mixed-null cells, the kernel null-destination policy, an expression-level permuted add over an all-null arena array, an empty-left plain tensor-of-tensor add, and a complex-tile instantiation regression. Each data-loss case failed before the fix.

…l when a destination cell is null; arena kernels throw instead of dropping data

An `ArenaTensor` inner cell is a non-owning view: a null cell has no storage
to write into and cannot allocate. The free in-place kernels `add_to` /
`subt_to` / `mult_to` therefore opened with `if (!dst || !src) return;`, so a
null destination cell silently discarded a populated source cell. Reached
per-cell from `Tensor::add_to` -> `inplace_binary` -> `l += r`, this lost an
entire addend whenever two ToT operands disagreed on which inner cells are
populated -- a routine case, since the same outer tile can aggregate several
pairs with some screened to null.

`Tensor::{add,subt,mult,axpy}_to` (scaled variants included) now detect an
ordinal with a null left cell and a populated right cell and route through
the value-returning kernel, which builds a fresh slab with union sparsity.
The detection is compiled out unless both operands' cells are views, so the
plain-inner and flat paths are untouched.

The free per-cell kernels now throw rather than no-op in that case, so any
remaining path that drops data is loud instead of silent. `mult_to` also
gains the mirror fix: a null *source* is an implicit zero, so it must zero
the destination rather than leave it unchanged.
…n in ToT Tensor::add on an empty left

`ArrayEvalImpl::is_consumable()` is true whenever the operand carries a
permutation, so `BinaryWrapper` rewrote an out-of-place `a(perm) + b` as
`a(perm).add_to(b)`. For `Tensor<ArenaTensor>` that turned a correct
union-sparsity add into an in-place one whose destination cells may be null.
The previous commit makes that fall back safely, but paying for a re-homed
copy on every such add is the wrong shape: consuming a tile is only sound
when every destination cell has storage to write into, which a view cell
that is null does not. Specialize `is_consumable_tile<Tensor<T>>` to false
for view cells so these expressions stay on the value-returning kernel.

Also add the `return` missing from `Tensor::add`'s empty-left early exit, so
a plain-inner ToT `add` on a default-constructed left yields the right-hand
operand instead of falling through to the binary kernel with mismatched
outer volumes (an empty result, or a TA_ASSERT trip when assertions are on).
The arena overload already had it.
…ero, not an error

The previous commit made the free `mult_to` throw on a null destination cell
with a populated source, matching `add_to` / `subt_to`. That is wrong for
multiplication: the product is zero, a null cell is exactly how a sparse ToT
spells zero, and nothing is lost. Screened-pair Hadamard products routinely
produce that shape, so the exception would break real workloads. Restore the
silent no-op there (the destination stays null).

The rest of the null-cell policy stands: `add_to`, `subt_to` and `axpy_to`
still throw on a null destination, where returning would genuinely drop the
source, and `mult_to` still zeroes the destination when the *source* is null
(`dst *= 0` is not a no-op). The header now spells out the asymmetry and why.

`Tensor::mult_to` keeps its tensor-of-view fallback, so an in-place and an
out-of-place mult agree on sparsity as well as on values; the test checks the
numeric result and accepts either spelling of zero.
…(fixes complex tile instantiation)

The null-destination fallback added to the in-place binary ops was a plain
runtime `if`, so its body was instantiated for *every* `Tensor`, not only the
tensor-of-view ones it can ever fire for. The body calls the value-returning
kernels, which need not be well-formed for an operand/scalar mix the in-place
path does support: `Tensor<std::complex<double>>::subt_to(right, int)` is fine
elementwise -- `(l -= r) *= 2` resolves to
`complex<double>::operator*=(const double&)` -- but the `subt(right, int)` it
would fall back to is not, since binary
`operator*(const complex<_Tp>&, const _Tp&)` cannot deduce `_Tp` from a
`complex<double>` / `int` pair. That broke downstream builds instantiating TA
for complex tiles.

Gate every fallback -- `add_to`, `subt_to`, `mult_to`, `axpy_to`, scaled and
unscaled -- on a new `binary_needs_view_cell_fallback_v<Right>` used as an
`if constexpr` condition, so nothing is instantiated for a non-view tensor.
`inplace_binary_drops_cells` consults the same constant, keeping one source of
truth for the gate.

Adds a compile-only regression instantiating the complex-tile in-place ops
with an `int` factor, plus static_asserts pinning the two traits the gate is
built from, so a trait change cannot silently delete the fallback instead.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The fix is narrowly scoped, compile-time guarded, and backed by targeted regressions that exercise the previously silent data-loss paths.

Pull request overview

Fixes a correctness bug for tensor-of-view nested tiles (Tensor<ArenaTensor<T>>) where in-place binary ops could silently drop populated right-hand inner cells when the destination inner cell was null, especially when expression evaluation rewrote out-of-place adds into in-place ops due to “consumable” tiles.

Changes:

  • Add an if constexpr-guarded fallback in Tensor::add_to / subt_to / mult_to / axpy_to to route through value-returning (union-sparsity) kernels when an in-place update would drop populated view-cells.
  • Mark Tensor<T> as non-consumable when T is a tensor view to prevent expression-level rewrites into in-place updates for view-cell tensors.
  • Tighten arena view-cell in-place kernels to throw on null-destination/populated-source for additive ops (and make mult_to treat null source as multiplicative zero).
File summaries
File Description
tests/arena_tot_trivial.cpp Adds regression tests covering null-destination view cells, expression-level permuted add rewrite, and compile-only complex-tile instantiation cases.
src/TiledArray/tensor/type_traits.h Makes Tensor<T> non-consumable when T is a tensor view, preventing unsafe in-place consumption in expression eval.
src/TiledArray/tensor/tensor.h Adds compile-time-gated detection + fallback to value-returning kernels for ToT view-cell in-place binary ops; fixes missing return for empty-left add.
src/TiledArray/tensor/arena_tensor.h Updates arena per-cell _to kernels to throw on null destination for additive ops and to zero destination on null source for mult_to.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants