refactor(Storage)!: typed storage dispatch foundation + capacity_/numpy ownership (#941)#1015
Conversation
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
|
Wow! |
| @@ -344,7 +353,6 @@ | |||
|
|
|||
| void *start_; | |||
There was a problem hiding this comment.
why not value_type *start_ ?
| 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); |
There was a problem hiding this comment.
Better implemented via a variant visitor
| 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); |
There was a problem hiding this comment.
Better implemented as a variant visitor
| 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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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[] ?
| 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); | ||
| } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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;
}There was a problem hiding this comment.
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;
}There was a problem hiding this comment.
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()); | ||
| } |
There was a problem hiding this comment.
also needs to go, same pattern as for Storage_init_interface
| cytnx_error_msg(true, "[ERROR] fatal internal, Storage has invalid type.%s", "\n"); | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Change to visitor pattern
2f09a7d to
ea42698
Compare
|
Once this has been through another iteration to resolve the above issues, I'll work on a PR for #1003 |
pcchen
left a comment
There was a problem hiding this comment.
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.cppallocates the output asmake_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) computesTO = 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 inDiv.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_baseobject alive as long as the numpy array. - But
Storage::resize(Storage.hpp:793) calls_impl->resize(), which doesfree(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 singleappendtriggers 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_item → value_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
|
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. |
Plan for the next iteration + coordination with #1011Agreed 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:
Sequencing with #1011: this PR's first commit ( Posted by Claude Code on behalf of @pcchen |
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>
d289349 to
9afe152
Compare
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
…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>
…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>
#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>
What this delivers (re-scoped)
Rebased onto current
master(which now carries the Scalar→std::variantrefactor via #1011) and narrowed to the foundation, in two commits:StorageVariantgenerated fromType_list(single source of truth, compile-time exhaustiveness),storage_cast<T>,as_storage_variant(),storage_as_type_or_replace<T>, and typedStorageImplementation<T>::data() -> T*. Static-asserts pin the variant to the dtype list. Exercised directly by newStorage_testcases (no dependency on the linalg conversion).capacity_removed + numpy zero-copy capsule ownership (Its time: Typed Storage Dispatch #941 rulings 2, 4) —append/resizeare exact realloc, O(n), numpy-like;Storage.numpy()/Tensor.numpy(share_mem=True)genuinely share via anintrusive_ptr-holding capsule (the leakyrelease()is gone).What changed vs the original PR
The original #1015 also carried two commits that are now dropped:
refactor(Scalar)!: PIMPL → std::variant— fully superseded by refactor(Scalar)!: replace PIMPL virtual dispatch with std::variant (#847, fixes #935) #1011 (the same refactor, in its final reviewed form, already onmaster).fix(linalg)!: convert CPU Add/Sub/Mul/Div/Cpr/Mod to typed dispatch+ true-division — deferred to a follow-up PR. It collides head-on with master's fix(Tensor & UniTensor): support rank-zero tensors and zero extents #1026 (rank-zero refactor of all six arithmetic files + theinit_broadcast_binary_outputhelper), fix(linalg): handle zero-extent tensors #1043 (zero-extent), and fix(linalg): replace lower-enum-index dtype selection with Type.type_promote #984 (type_promote). Correctly landing it means re-implementing thestorage_castdispatch and the breaking true-division (int/int → Double, Its time: Typed Storage Dispatch #941 §True division) on top of master's now-refactored arithmetic — a numerical change that warrants its own focused, reviewable PR against current master. The original work is preserved at commitff30d0090for that follow-up.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-buildfree()hazard). Kept the always-on guard.Testing
Full CPU
test_main1784 tests, 1773 passed / 11 skipped / 0 failed on theopenblas-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