Skip to content

refactor(Storage)!: typed storage dispatch foundation + capacity_/numpy ownership (#941)#1015

Merged
yingjerkao merged 2 commits into
masterfrom
refactor/typed-storage-foundation
Jul 15, 2026
Merged

refactor(Storage)!: typed storage dispatch foundation + capacity_/numpy ownership (#941)#1015
yingjerkao merged 2 commits into
masterfrom
refactor/typed-storage-foundation

Conversation

@yingjerkao

@yingjerkao yingjerkao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What this delivers (re-scoped)

Rebased onto current master (which now carries the Scalar→std::variant refactor via #1011) and narrowed to the foundation, in two commits:

  1. Typed storage foundation (Its time: Typed Storage Dispatch #941)StorageVariant generated from Type_list (single source of truth, compile-time exhaustiveness), storage_cast<T>, as_storage_variant(), storage_as_type_or_replace<T>, and typed StorageImplementation<T>::data() -> T*. Static-asserts pin the variant to the dtype list. Exercised directly by new Storage_test cases (no dependency on the linalg conversion).
  2. capacity_ removed + numpy zero-copy capsule ownership (Its time: Typed Storage Dispatch #941 rulings 2, 4) — append/resize are exact realloc, O(n), numpy-like; Storage.numpy()/Tensor.numpy(share_mem=True) genuinely share via an intrusive_ptr-holding capsule (the leaky release() is gone).

What changed vs the original PR

The original #1015 also carried two commits that are now dropped:

Conflict-resolution note

During the rebase, master's StorageTest.InitByPtrRejectsZeroLength (a safety test added after this PR branched) required _Init_byptr(ptr, 0) to always throw; the capacity_-removal commit had weakened that guard to #ifdef UNI_DEBUG (a release-build free() hazard). Kept the always-on guard.

Testing

Full CPU test_main 1784 tests, 1773 passed / 11 skipped / 0 failed on the openblas-cpu (non-ASan) preset; storage/numpy-ownership/append pytests pass; clang-format v14 clean.

Follow-ups (tracked separately)

CPU linalg typed-dispatch conversion + true-division (deferred here); GPU conversion (#1013).

🤖 Generated with Claude Code

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@ianmccul

ianmccul commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Wow!

@@ -344,7 +353,6 @@

void *start_;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not value_type *start_ ?

Comment on lines 301 to 311
void fill(const cytnx_complex128 &val);
void fill(const cytnx_complex64 &val);
void fill(const cytnx_double &val);
void fill(const cytnx_float &val);
void fill(const cytnx_int64 &val);
void fill(const cytnx_uint64 &val);
void fill(const cytnx_int32 &val);
void fill(const cytnx_uint32 &val);
void fill(const cytnx_int16 &val);
void fill(const cytnx_uint16 &val);
void fill(const cytnx_bool &val);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Better implemented via a variant visitor

Comment on lines 315 to 326
void append(const Scalar &val);
void append(const cytnx_complex128 &val);
void append(const cytnx_complex64 &val);
void append(const cytnx_double &val);
void append(const cytnx_float &val);
void append(const cytnx_int64 &val);
void append(const cytnx_uint64 &val);
void append(const cytnx_int32 &val);
void append(const cytnx_uint32 &val);
void append(const cytnx_int16 &val);
void append(const cytnx_uint16 &val);
void append(const cytnx_bool &val);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Better implemented as a variant visitor

Comment on lines 329 to 340
void set_item(const cytnx_uint64 &idx, const Scalar &val);
void set_item(const cytnx_uint64 &idx, const cytnx_complex128 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_complex64 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_double &val);
void set_item(const cytnx_uint64 &idx, const cytnx_float &val);
void set_item(const cytnx_uint64 &idx, const cytnx_int64 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_uint64 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_int32 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_uint32 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_int16 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_uint16 &val);
void set_item(const cytnx_uint64 &idx, const cytnx_bool &val);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Better implemented as a variant visitor

void append(const cytnx_int16 &val);
void append(const cytnx_uint16 &val);
void append(const cytnx_bool &val);
Scalar get_item(const cytnx_uint64 &in) const;

@ianmccul ianmccul Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Better if this returns value_type. Higher level operation on Storage_base layer can turn it into Scalar as needed.

Scalar Storage::get_item(idx) {
  return visit_storage_value(..., [](auto v) { return Scalar(v); });
}

Why not name it operator[] ?

Comment on lines 103 to 144
template <class T>
void _Init_byptr_safe(T *rawptr, const unsigned long long &len_in) {
// check:
if (this->dtype() == Type.Float) {
cytnx_error_msg(typeid(T) != typeid(cytnx_float), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Double) {
cytnx_error_msg(typeid(T) != typeid(cytnx_double), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Uint64) {
cytnx_error_msg(typeid(T) != typeid(cytnx_uint64), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Uint32) {
cytnx_error_msg(typeid(T) != typeid(cytnx_uint32), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Int64) {
cytnx_error_msg(typeid(T) != typeid(cytnx_int64), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Int32) {
cytnx_error_msg(typeid(T) != typeid(cytnx_int32), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.ComplexDouble) {
cytnx_error_msg(typeid(T) != typeid(cytnx_complex128), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.ComplexFloat) {
cytnx_error_msg(typeid(T) != typeid(cytnx_complex64), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Int16) {
cytnx_error_msg(typeid(T) != typeid(cytnx_int16), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Uint16) {
cytnx_error_msg(typeid(T) != typeid(cytnx_uint16), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else if (this->dtype() == Type.Bool) {
cytnx_error_msg(typeid(T) != typeid(cytnx_bool), "%s",
"[ERROR _Init_byptr_safe type not match]");
} else {
cytnx_error_msg(true, "[FATAL] ERROR%s", "\n");
}

this->_Init_byptr((void *)rawptr, len_in);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think its possible to remove this function? It seems to be dead code? Even before this PR, grep -rn _Init_byptr_safe the only hits are these lines.

}
}
};
extern Storage_init_interface __SII;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This has to go. If for no other reason, the extern __SII is an order-of-initialization bug #673 and resolving that is a key goal of #941

Replace with something like

boost::intrusive_ptr<Storage_base> make_storage(unsigned int dtype) {
  constexpr std::size_t n_non_void = std::variant_size_v<Type_list> - 1;
  return internal::make_storage_impl(dtype, std::make_index_sequence<n_non_void>{});
}

which could be implemented using the same std::index_sequence pattern already used for as_storage_variant():

template <std::size_t... Is>
boost::intrusive_ptr<Storage_base>
make_storage_impl(unsigned int dtype, std::index_sequence<Is...>) {
  boost::intrusive_ptr<Storage_base> out;
  bool matched = (... || [&] {
    using T = std::variant_alternative_t<Is + 1, Type_list>; // skip void
    if (dtype == Type_class::cy_typeid_v<T>) {
      out = new StorageImplementation<T>();
      return true;
    }
    return false;
  }());
  cytnx_error_msg(!matched, "[Storage] invalid dtype %u%s", dtype, "\n");
  return out;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Split into two helpers:

boost::intrusive_ptr<Storage_base>
make_storage(unsigned int dtype);

boost::intrusive_ptr<Storage_base>
make_storage(unsigned int dtype, cytnx_uint64 size, int device = Device.cpu,
             bool init_zero = true);

Then for typed C++ sources:

template <CytnxType T>
boost::intrusive_ptr<StorageImplementation<T>>
make_storage(cytnx_uint64 size, int device = Device.cpu, bool init_zero = true) {
  auto out = boost::intrusive_ptr<StorageImplementation<T>>(new StorageImplementation<T>());
  out->Init(size, device, init_zero);
  return out;
}

For from_vector, the cleanest version does not need dtype dispatch at all because T is already known:

template <CytnxType T>
void _from_vector(const std::vector<T>& vin, int device = Device.cpu) {
  auto impl = make_storage<T>(vin.size(), device, false);

  if constexpr (std::is_same_v<T, cytnx_bool>) {
    impl->_cpy_bool(impl->data(), vin);
  } else {
    std::memcpy(impl->data(), vin.data(), sizeof(T) * vin.size());
  }

  this->_impl = impl;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

even better to remove ->Init() and pass size, device and init_zero into the constructor

this->_impl->Init(vin.size(), device);
this->_impl->_cpy_bool(this->_impl->data(), vin);
// memcpy(this->_impl->data(),vin.data(),sizeof(cytnx_bool)*vin.size());
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

also needs to go, same pattern as for Storage_init_interface

Comment thread src/backend/Storage.cpp
cytnx_error_msg(true, "[ERROR] fatal internal, Storage has invalid type.%s", "\n");
}
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Change to visitor pattern

@ianmccul

ianmccul commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Once this has been through another iteration to resolve the above issues, I'll work on a PR for #1003

@pcchen pcchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Verdict: Request changes. The architecture is right, the deletion count (−16.4k) is glorious, and this unblocks #1003/#941 — but there are two verified HIGH runtime defects, plus a needed rebase. Both defects are line-verified against the branch head d2893495.

HIGH-1: every GPU integer÷integer Div now throws at runtime

  • src/linalg/Div.cpp allocates the output as make_floating_point_dtype(type_promote(...))Double for int/int — correct, matches this PR's CPU true-division semantics.
  • But the new cuDiv_dispatch_typed<TL,TR> (cuDiv_dispatch.cu) computes TO = Type_class::type_promote_gpu_t<TL,TR> — the plain promotion (Type.hpp:471; Int64 for int/int, no floating-point flooring) — and then hard-errors on mismatch:
    cytnx_error_msg(out->dtype() != cy_typeid_gpu_v<TO>, "[cuDiv_dispatch] output dtype mismatch...").
  • Net effect: (integer tensor on GPU) / (integer tensor or scalar) throws "output dtype mismatch" on every call.
  • One-line fix: using TO = Type_class::make_floating_point_t<Type_class::type_promote_gpu_t<TL, TR>>; — mirroring the CPU visitor in Div.cpp.
  • ⚠️ CI cannot catch this: there is no GPU CI (#538). It will only surface on a user's machine.

HIGH-2: numpy zero-copy view → use-after-free on resize/append

  • The capsule design (ruling 4) correctly keeps the Storage_base object alive as long as the numpy array.
  • But Storage::resize (Storage.hpp:793) calls _impl->resize(), which does free(this->start_); this->start_ = htmp; on the same impl (StorageImplementation.cpp). The numpy array captured the old data pointer at view creation → dangling after any size change. Ruling 2 (capacity_ removed; every size change reallocates) makes the window maximal — even a single append triggers it.
  • pytests/numpy_ownership_test.py's docstring claims safety on the premise that "every size-changing Storage operation replaces the impl with a fresh buffer" — that premise is false: the impl is mutated in place; only the buffer inside it is replaced.
  • Repro:
    s = cytnx.Storage(5, cytnx.Type.Double)
    arr = s.numpy()   # zero-copy view
    s.resize(10)      # frees the old buffer in place
    arr[0]            # use-after-free; arr[0] = 1 → heap corruption
  • Fix options, in order of preference: (a) make size-changing ops actually swap in a fresh impl, which makes the docstring's safety argument true and matches the evident ruling-4 intent; (b) mark the storage borrowed after numpy() and error on size changes; (c) copy instead of view. Whichever is chosen, add a regression test that resizes under a live view.

Interaction with @ianmccul's comments

His 11 inline comments (visitor-pattern conversions, make_storage() factory replacing the __SII extern globals — which is the #673 order-of-initialization bug that #941 exists to fix — value_type *start_, dead _Init_byptr_safe, get_itemvalue_type/operator[], constructor-instead-of-Init()) are all orthogonal to the two defects above and all worth doing; nothing here conflicts with them. Since #1003 is queued behind this PR, resolving both lists in one iteration keeps that path short.

Housekeeping

  • The PR is CONFLICTING — master has moved since 07-07 (#997, #1010, #1022, #1024, #1031). Rebase needed before the next iteration.
  • Commit ba822de1 (Scalar → std::variant) overlaps #1011's territory — a sentence on how this sequences with #1011 would prevent double-work.

Posted by Claude Code on behalf of @pcchen

@ianmccul

Copy link
Copy Markdown
Collaborator

My recommendation is rebase, and have claude address the two Code Review comments, and also the comments I made - they are all straightforward and might as well get it done in one, rather than churn through another PR.

I wouldn't worry too much about HIGH-1: it will get rewritten with #1003 anyway; an emergency fix would be to revert to the old behavior, but if doing that please make a comment on #1003 that the existing behavior is incorrect.

@pcchen

pcchen commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Plan for the next iteration + coordination with #1011

Agreed with @ianmccul's recommendation above — one combined iteration rather than churning through another round. Claude Code will execute it on this branch. Concretely, the iteration will cover:

  1. Rebase onto current master (refactor(pybind)!: guard UniTensor +/- on matching metadata; drop * / (#934) #997, refactor(Symmetry): make Symmetry a plain value type over std::variant #1010, refactor(UniTensor): add versioned file header #1022, fix(UniTensor): read file names without requiring null terminator #1024, fix(UniTensor): make clone_meta() exception-safe to stop leaking on validation throws #1031 have landed since 07-07).
  2. HIGH-1 (GPU integer Div): fixed properly rather than reverted — it's one line in cuDiv_dispatch_typed: using TO = Type_class::make_floating_point_t<Type_class::type_promote_gpu_t<TL, TR>>;, mirroring the CPU visitor in Div.cpp. That's less work than the revert-plus-Merge CUDA unary and binary elementwise operations through one typed kernel framework #1003-note and keeps GPU semantics consistent with this PR's CPU true-division. (Merge CUDA unary and binary elementwise operations through one typed kernel framework #1003 rewrites it later either way.)
  3. HIGH-2 (numpy view UAF): make size-changing Storage operations swap in a fresh impl instead of mutating the buffer in place — which makes numpy_ownership_test.py's safety argument actually true — plus a regression test that resizes under a live view.
  4. @ianmccul's 11 inline comments: visitor-pattern conversions, make_storage() factory replacing the __SII extern globals (Segmentation fault when initializing Storage in C++ #673), value_type *start_, dead _Init_byptr_safe removal, get_itemvalue_type/operator[], constructor-instead-of-Init().

Sequencing with #1011: this PR's first commit (ba822de1) is a snapshot of #1011 — same refactor, same title — with the Storage work stacked on top, and the snapshot is already one iteration stale (#1011 was revised 07-08 with the visit-variant astype dispatch and in-place-promotion fixes). To avoid restacking on a moving target, the order is: #1011 merges first, then the combined iteration here — the rebase then drops the embedded Scalar commit entirely and this PR shrinks to its true Storage-only scope. Until then, please route Scalar review comments to #1011 and Storage/dispatch/linalg comments here; the Scalar.hpp/Scalar.cpp hunks in this diff are the stale copy.

Posted by Claude Code on behalf of @pcchen

Base automatically changed from refactor/scalar-variant to master July 14, 2026 06:42
yingjerkao and others added 2 commits July 15, 2026 09:30
Add the #941 foundation types on top of T2's Scalar variant substrate:
Type_class::make_floating_point_t<T>/make_floating_point_dtype (true-division
output type rule), StorageImplementation<T>::value_type/dtype_value/typed
data(), StorageVariant (generated from Type_list, excluding void, mirroring
T2's Scalar-variant pattern), storage_cast<T>, Storage::as_storage_variant(),
and storage_as_type_or_replace<T> for in-place output-buffer reuse.

Storage_base::data() (the untyped erased accessor) is retained rather than
removed outright: ruling 3 keeps GPU arithmetic on the legacy cuAri_ii table,
and Tensor::ptr()/gpu_ptr() are out of scope for this task's CPU-family
conversion. Both retention sites are commented with the GPU tracking issue
filed alongside this commit (#1013) and Tensor::ptr()'s broader-scope reason.

Ref #941 (ianmccul design doc), ruling 3 (CPU-first GPU scope).
…ip (#941 rulings 2, 4)

Ruling 2: remove capacity_ from StorageImplementation<T>. Storage now always
allocates exactly size() elements; append()/resize() reallocate exactly
(documented O(n) per call, numpy-like) instead of the previous
STORAGE_DEFT_SZ-rounded over-allocation, which also made two Tensors sharing
storage silently diverge on append() (see #941's discussion thread).
Storage.capacity() is removed from the public C++ and Python API;
_Init_byptr keeps its iscap/cap_in parameters (ignored, documented) to avoid
churning the remaining GPU call sites tracked in #1013. Movemem/cuMovemem's
capacity()-sized scratch allocations become size()-sized.

Ruling 4: Storage.numpy() and Tensor.numpy() now return zero-copy views
backed by a py::capsule owning a copy of the underlying
intrusive_ptr<Storage_base>, keeping the storage alive for the numpy
array's lifetime -- instead of the release()-based path, which both leaked
the original buffer AND still copied (py::array with a raw pointer and no
base object cannot take ownership; see ianmccul's RSS repro in #941's
comments). Storage.numpy() is now a genuine shared view; Tensor.numpy()
keeps its share_mem flag semantics (share_mem=False views a fresh clone, so
mutating the array still does not affect the tensor; share_mem=True now
genuinely shares, where it previously silently copied). This is safe
because ruling 2 guarantees a buffer's allocated extent always equals its
size and every size-changing operation replaces the impl rather than
mutating the allocation in place.

Storage_base::release()/Storage::release() are removed entirely (#941
migration step 12): the .numpy() bindings were their only callers.

Storage.capacity() removal is a python-visible breaking change.

Ref #941 (ianmccul design doc, "Minimal Storage Changes" and the numpy
ownership-transfer comment thread), rulings 2 and 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yingjerkao
yingjerkao force-pushed the refactor/typed-storage-foundation branch from d289349 to 9afe152 Compare July 15, 2026 01:41
@yingjerkao yingjerkao changed the title refactor(Storage)!: typed storage dispatch foundation + first CPU family (#941) refactor(Storage)!: typed storage dispatch foundation + capacity_/numpy ownership (#941) Jul 15, 2026
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.49%. Comparing base (6600059) to head (9afe152).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/backend/Storage.cpp 92.85% 1 Missing ⚠️
src/backend/StorageImplementation.cpp 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1015      +/-   ##
==========================================
+ Coverage   60.38%   60.49%   +0.11%     
==========================================
  Files         229      229              
  Lines       31866    31873       +7     
  Branches       71       71              
==========================================
+ Hits        19241    19282      +41     
+ Misses      12604    12570      -34     
  Partials       21       21              
Flag Coverage Δ
cpp 60.45% <97.22%> (+0.11%) ⬆️
python 64.13% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 58.04% <96.15%> (+<0.01%) ⬆️
Python bindings 75.01% <100.00%> (+0.75%) ⬆️
Python package 64.13% <ø> (ø)
Files with missing lines Coverage Δ
include/Type.hpp 97.77% <ø> (ø)
include/backend/Storage.hpp 98.90% <100.00%> (+2.25%) ⬆️
pybind/storage_py.cpp 81.85% <100.00%> (+13.98%) ⬆️
pybind/tensor_py.cpp 68.70% <100.00%> (+0.20%) ⬆️
src/backend/utils_internal_cpu/Movemem_cpu.cpp 85.00% <100.00%> (ø)
src/backend/Storage.cpp 69.08% <92.85%> (+1.72%) ⬆️
src/backend/StorageImplementation.cpp 64.90% <93.75%> (-2.15%) ⬇️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6600059...9afe152. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yingjerkao
yingjerkao merged commit 1908a05 into master Jul 15, 2026
19 checks passed
@yingjerkao
yingjerkao deleted the refactor/typed-storage-foundation branch July 15, 2026 03:22
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 17, 2026
#1013)

Problem: the iAdd/iSub/iMul/iDiv in-place complex guard rejected EVERY
real-LHS / complex-RHS combination. That wrongly blocked a genuine complex
*tensor* RHS from promoting the real LHS to complex — behavior the
out-of-place operator and the #1013 typed dispatch already support.

Fix: narrow the guard to fire only for a complex python *weak scalar*
(numpy weak-scalar semantics, #980/#1015 preserve the LHS dtype, so a
complex scalar genuinely cannot be stored in a real tensor). A genuine
complex tensor RHS now promotes Lt's storage to complex via the existing
type_promote_t / storage_as_type_or_replace path. The guard stays
device-independent: the GPU kernel's complex-into-real branch silently
returns zero instead of throwing, so it relies on this host-side rejection.

Testing: new CPU tests (InplaceRealOpComplexTensorPromotes,
InplaceRealOpComplexWeakScalarRejected) with independent hand-computed
complex expected values, incl. a rank-0 genuine-tensor RHS; new GPU test
(gpu_inplace_real_op_complex_weak_scalar_rejected) plus real<-complex
promotion coverage in the InplacePromote sweep and literal cases. Both new
CPU tests confirmed to fail on the pre-fix guard. clang-format v14 clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants