fix(linalg): replace lower-enum-index dtype selection with Type.type_promote#984
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors several linear algebra operations (including Axpy, Dot, Gemm, Ger, Matmul, Mod, and Tensordot) to use proper dtype promotion via Type.type_promote instead of hand-rolled selection logic. It also ensures integer inputs to BLAS-only operations floor to Double and adds a comprehensive test suite for mixed-dtype promotion. The review feedback suggests optimizing Dot, Matmul_dg, and Mod by passing false to out.Init to skip redundant zero-initialization, since the output tensors are fully overwritten by their respective kernels.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| } else { | ||
| out.Init(newshape, Tr.dtype(), Tr.device()); | ||
| } | ||
| out.Init(newshape, out_dtype, Tl.device()); |
There was a problem hiding this comment.
Since the Matvec_ii and cuMatvec_ii kernels fully overwrite the output tensor, we can skip the redundant zero-initialization by passing false as the fourth argument to out.Init. This matches the optimization pattern used in Matmul and Tensordot.
| out.Init(newshape, out_dtype, Tl.device()); | |
| out.Init(newshape, out_dtype, Tl.device(), false); |
There was a problem hiding this comment.
Applied in d711fbf. Verified the Matvec path fully overwrites out before adopting it: the integer kernel self-zeros each element (out[n] = 0; out[n] += ...) and the BLAS/cuBLAS gemv variants use beta = 0, so out is written without being read. DtypePromotion.Dot_matvec + the Dot suites pass under ASAN (which fills fresh allocations with 0xbe, so this isn't relying on coincidentally-zero memory).
| } else { | ||
| out.Init({Tl.shape()[0], Tr.shape().back()}, Tr.dtype(), Tr.device()); | ||
| } | ||
| out.Init({Tl.shape()[0], Tr.shape().back()}, out_dtype, Tl.device()); |
There was a problem hiding this comment.
Since the Matmul_dg_ii and cuMatmul_dg_ii kernels fully overwrite the output tensor, we can skip the redundant zero-initialization by passing false as the fourth argument to out.Init. This matches the optimization pattern used in Matmul and Tensordot.
| out.Init({Tl.shape()[0], Tr.shape().back()}, out_dtype, Tl.device()); | |
| out.Init({Tl.shape()[0], Tr.shape().back()}, out_dtype, Tl.device(), false); |
There was a problem hiding this comment.
Not applying this one — it would corrupt results. Unlike Matmul (gemm, beta=0) and Tensordot, the CPU Matmul_dg diag_L path accumulates into out via axpy (_out[i*Nr] += _inl[i]*_inr[i*Nr], in Matmul_dg_internal.cpp), so it reads the existing out values and therefore requires the zero-initialization. (Only the diag_R driver and the GPU kernel overwrite directly.) Since one out.Init feeds both paths, dropping the zero-fill here would leave the diag_L BLAS result adding onto uninitialized memory. Leaving init_zero=true; confirmed DtypePromotion.Matmul_dg_complexfloat_double still passes.
| if (Lt.shape().size() == 1 && Lt.shape()[0] == 1) { | ||
| out.Init(Rt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | ||
| out.Init(Rt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | ||
| icnst = true; | ||
| } else if (Rt.shape().size() == 1 && Rt.shape()[0] == 1) { | ||
| out.Init(Lt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | ||
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | ||
| icnst = true; | ||
| } else { | ||
| cytnx_error_msg(Lt.shape() != Rt.shape(), | ||
| "[Mod] The two tensors do not have the same shape.%s", "\n"); | ||
| out.Init(Lt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | ||
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | ||
| } |
There was a problem hiding this comment.
Since the element-wise modulo kernels fully overwrite the output tensor, we can skip the redundant zero-initialization by passing false as the fourth argument to out.Init. This matches the optimization pattern used in Matmul and Tensordot.
| if (Lt.shape().size() == 1 && Lt.shape()[0] == 1) { | |
| out.Init(Rt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | |
| out.Init(Rt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | |
| icnst = true; | |
| } else if (Rt.shape().size() == 1 && Rt.shape()[0] == 1) { | |
| out.Init(Lt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | |
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | |
| icnst = true; | |
| } else { | |
| cytnx_error_msg(Lt.shape() != Rt.shape(), | |
| "[Mod] The two tensors do not have the same shape.%s", "\n"); | |
| out.Init(Lt.shape(), Lt.dtype() < Rt.dtype() ? Lt.dtype() : Rt.dtype(), Lt.device()); | |
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device()); | |
| } | |
| if (Lt.shape().size() == 1 && Lt.shape()[0] == 1) { | |
| out.Init(Rt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device(), false); | |
| icnst = true; | |
| } else if (Rt.shape().size() == 1 && Rt.shape()[0] == 1) { | |
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device(), false); | |
| icnst = true; | |
| } else { | |
| cytnx_error_msg(Lt.shape() != Rt.shape(), | |
| "[Mod] The two tensors do not have the same shape.%s", "\n"); | |
| out.Init(Lt.shape(), Type.type_promote(Lt.dtype(), Rt.dtype()), Lt.device(), false); | |
| } |
There was a problem hiding this comment.
Applied in d711fbf (all three Init calls). ModInternalImpl (contiguous and non-contiguous) and cuMod assign every output element (_out[i] = ...), so out is fully overwritten. All DtypePromotion Mod cases pass under ASAN.
|
Ok, this is the follow-up that makes the The key pattern here looks right to me: compute the promoted dtype, cast both operands to that dtype, allocate output at that dtype, then dispatch the old single-dtype kernel table. That avoids the dangerous case where only the output dtype changes while the backend still reinterprets input/output buffers according to the old enum-order rule. I would still be careful about the claimed scope. This appears to cover the listed Tensor linalg entry points, but not all arithmetic paths. In particular, general Tensor in-place arithmetic ( I think the only safe way to do this change is to implement #941, followed by making Python code tends to do arithmetc on dtype as well, so the corresponding change to make dtype opaque in python (or switch to the numpy dtypes) should probably be done at the same time; see ianmccul/Cytnx-fixes#3 |
|
There are still float/complex promotion bugs at the tip of PR #984, even if we ignore integer arithmetic completely. The clearest one is Tensor out(new_shape, Type.type_promote(Tl.dtype(), Tr.dtype()), Tl.device());
lii.Outer_ii[Tl.dtype()][Tr.dtype()](out.storage, Tl.storage, Tr.storage, ...);For Outer_general<cytnx_complex64, cytnx_complex64, cytnx_double>(out, Lin, Rin, j1, j2);So the output storage has There are several other non-integer promotion paths still using ordinal dtype comparison rather than
they were found with an AI search, there are probably others. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #984 +/- ##
==========================================
+ Coverage 31.82% 32.63% +0.80%
==========================================
Files 230 230
Lines 33124 33021 -103
Branches 13852 13758 -94
==========================================
+ Hits 10543 10775 +232
+ Misses 15539 14972 -567
- Partials 7042 7274 +232
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 6 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
efa641f to
f56ecea
Compare
…pe sites (#984 review) Follow-up to the review of #984: #982 changed Type.type_promote to promote across the real/complex boundary, but several entry points still hand-rolled the "pick the lower enum index" rule and so disagree with the new promotion (and, for Outer/Exp, write the narrower type into the wider output buffer). Buffer-layout / reinterpret fixes: - Outer (CPU + GPU): allocated the promoted output but dispatched by the raw input dtypes, so ComplexFloat x Double wrote complex64 into a complex128 buffer. Now casts both operands and dispatches by the promoted dtype (only the same-type diagonal kernels, avoiding the mixed-cuComplex path). - Exp/Expf: dispatched by out.dtype() while feeding Tin's original storage, reinterpreting e.g. a float buffer as double*. Now feeds the already-cast `out` storage as the input (kernels support in == out). Lower-enum -> Type.type_promote: - Lstsq, Concatenate, Hstack, Vstack, Directsum. - Scalar binary arithmetic (radd/rmul/rsub/rdiv, both the Scalar and typed overloads). - The UniTensor out-of-place Add/Sub/Mul/Div wrappers (and their scalar variants), which chose the output dtype / cloned operand by enum order. Tests: - New DtypePromotion cases for every fixed entry point (ComplexFloat x Double -> ComplexDouble; Exp(Float) reads the correct buffer width). - Updated the Hstack/Vstack/Directsum test helpers (CPU and GPU copies), which encoded the old min-enum-index contract, to expect Type.type_promote. - Full CPU suite: 1101 passed / 11 skipped / 0 failed. Note: the GPU library does not currently compile on this branch (pre-existing, independent of this change) because #982's promotion change broke the cuKron device kernel's output type; the GPU edits here mirror the CPU fixes verified above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @ianmccul — pushed 731294f addressing the concrete promotion bugs in the second comment. Buffer-layout / reinterpret fixes
Lower-enum-index →
Tests: added
On the broader point (first comment): you're right that this is whack-a-mole, and I hit concrete evidence for it — the GPU library doesn't currently compile on this branch: I've filed the |
…pe sites (#984 review) Follow-up to the review of #984: #982 changed Type.type_promote to promote across the real/complex boundary, but several entry points still hand-rolled the "pick the lower enum index" rule and so disagree with the new promotion (and, for Outer/Exp, write the narrower type into the wider output buffer). Buffer-layout / reinterpret fixes: - Outer (CPU + GPU): allocated the promoted output but dispatched by the raw input dtypes, so ComplexFloat x Double wrote complex64 into a complex128 buffer. Now casts both operands and dispatches by the promoted dtype (only the same-type diagonal kernels, avoiding the mixed-cuComplex path). - Exp/Expf: dispatched by out.dtype() while feeding Tin's original storage, reinterpreting e.g. a float buffer as double*. Now feeds the already-cast `out` storage as the input (kernels support in == out). Lower-enum -> Type.type_promote: - Lstsq, Concatenate, Hstack, Vstack, Directsum. - Scalar binary arithmetic (radd/rmul/rsub/rdiv, both the Scalar and typed overloads). - The UniTensor out-of-place Add/Sub/Mul/Div wrappers (and their scalar variants), which chose the output dtype / cloned operand by enum order. Tests: - New DtypePromotion cases for every fixed entry point (ComplexFloat x Double -> ComplexDouble; Exp(Float) reads the correct buffer width). - Updated the Hstack/Vstack/Directsum test helpers (CPU and GPU copies), which encoded the old min-enum-index contract, to expect Type.type_promote. - Full CPU suite: 1101 passed / 11 skipped / 0 failed. Note: the GPU library does not currently compile on this branch (pre-existing, independent of this change) because #982's promotion change broke the cuKron device kernel's output type; the GPU edits here mirror the CPU fixes verified above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
822f55d to
2fd3149
Compare
That cannot be the case, for sure |
…promote Dot, Matmul, Matmul_dg, Tensordot, Axpy, Gemm, Gemm_Batch, Ger, and Mod still hand-rolled mixed-dtype promotion by picking the lower enum index, so ComplexFloat x Double produced ComplexFloat and silently discarded double precision - diverging from Add/Sub/Mul/Div and Vectordot after the type_promote fix. All sites now follow the Vectordot pattern: promote via Type.type_promote, astype BOTH operands (the promoted dtype can differ from both inputs), allocate the output at the promoted dtype, and dispatch the kernel by it. Latent bugs fixed along the way: * Axpy/Ger floored the dtype with an inverted comparison (fin_dtype < Type.Float), which clamped complex inputs onto the Double kernel (all-ComplexDouble Axpy threw "Cannot cast complex128 to real") and let integer inputs index past the size-5 kernel tables (segfault). Integer/bool now floors to Double after promotion. * Gemm lacked the integer->Double floor that Gemm_ has; integer inputs segfaulted the same way. * Ger built its implicit alpha=1 scalar before the floor could change fin_dtype; it is now created at the final dtype. * Mod's kernel (ModInternalImpl) computes in Type_class::type_promote_t<TL,TR>, but the output buffer used the old min-index rule, so e.g. Uint64 % Int32 wrote Int64 data into a Uint64-labeled buffer. All buffer-dtype sites, including the typed-scalar specializations, now use Type.type_promote. Adds tests/linalg_test/DtypePromotion_test.cpp (20 tests) covering the discriminating pairs (ComplexFloat x Double -> ComplexDouble, Uint64 x Int32 -> Int64) and the integer floors; all were observed failing (16 assertions, 3 segfaults) before the fix. No existing test asserted the old lower-index dtype: the full suite is unchanged vs baseline (1087 passed / 11 skipped / 4 known pre-existing BkUt_*Svd_truncate_return_err failures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pe sites (#984 review) Follow-up to the review of #984: #982 changed Type.type_promote to promote across the real/complex boundary, but several entry points still hand-rolled the "pick the lower enum index" rule and so disagree with the new promotion (and, for Outer/Exp, write the narrower type into the wider output buffer). Buffer-layout / reinterpret fixes: - Outer (CPU + GPU): allocated the promoted output but dispatched by the raw input dtypes, so ComplexFloat x Double wrote complex64 into a complex128 buffer. Now casts both operands and dispatches by the promoted dtype (only the same-type diagonal kernels, avoiding the mixed-cuComplex path). - Exp/Expf: dispatched by out.dtype() while feeding Tin's original storage, reinterpreting e.g. a float buffer as double*. Now feeds the already-cast `out` storage as the input (kernels support in == out). Lower-enum -> Type.type_promote: - Lstsq, Concatenate, Hstack, Vstack, Directsum. - Scalar binary arithmetic (radd/rmul/rsub/rdiv, both the Scalar and typed overloads). - The UniTensor out-of-place Add/Sub/Mul/Div wrappers (and their scalar variants), which chose the output dtype / cloned operand by enum order. Tests: - New DtypePromotion cases for every fixed entry point (ComplexFloat x Double -> ComplexDouble; Exp(Float) reads the correct buffer width). - Updated the Hstack/Vstack/Directsum test helpers (CPU and GPU copies), which encoded the old min-enum-index contract, to expect Type.type_promote. - Full CPU suite: 1101 passed / 11 skipped / 0 failed. Note: the GPU library does not currently compile on this branch (pre-existing, independent of this change) because #982's promotion change broke the cuKron device kernel's output type; the GPU edits here mirror the CPU fixes verified above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guards the remaining CPU-reachable lines of the type_promote conversion: every typed-scalar Mod specialization (both scalar-left and scalar-right) must allocate its output buffer at Type.type_promote of its operands to agree with ModInternalImpl's type_promote_t, the ComplexFloat-scalar variants must promote before the kernel rejects complex Mod, and Axpy without y must allocate its zeros() output at the promoted dtype. Also raises codecov/patch above the auto target: the previous round measured 27.98% because the bulk-edited Mod specializations and GPU-only branches were unexercised; the former are now covered (the latter cannot run on CI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address the @gemini-code-assist review on #984. Pass init_zero=false to out.Init in the Dot mat-vec path and all three Mod paths: those kernels fully overwrite the output, so the zero-fill is dead work. Verified each kernel overwrites before adopting the suggestion: - Dot -> Matvec_ii/cuMatvec_ii: the integer kernel self-zeros each element (out[n] = 0; out[n] += ...) and the BLAS/cuBLAS gemv variants use beta=0, so out is written without being read. - Mod -> ModInternalImpl (contiguous and non-contiguous) and cuMod: each output element is assigned (_out[i] = ...). NOT applied to Gemini's third suggestion (Matmul_dg): the CPU diag_L path uses axpy (out += inl[i]*inr), which accumulates into out and therefore *requires* the zero-initialization. Skipping it there would corrupt results, so that out.Init is left with the default init_zero=true. Tested: DtypePromotion (incl. Dot_matvec and every Mod case), Vectordot, and the broader Dot/Mod/Matmul/Gemm/Ger/Tensordot/Axpy suites all pass under ASAN (which fills fresh allocations with 0xbe, so the passing value checks confirm the kernels overwrite rather than relying on coincidentally-zero memory). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2fd3149 to
d711fbf
Compare
Code ReviewReplaces hand-rolled "pick the lower enum index" dtype selection with Verified correctI traced the headline fixes directly, and a full-breadth pass corroborated every other converted site.
Minor note (not a bug)
No findings requiring changes — a clean, high-value correctness fix that unifies dtype selection and squashes real segfaults/throws. LGTM. Posted by Claude Code on behalf of @pcchen |
pcchen
left a comment
There was a problem hiding this comment.
Approving. Verified across all ~15 converted sites: promotion correctly routed through Type.type_promote (real/complex by precision + signed/unsigned handling), both operands cast, outputs allocated at the promoted dtype, and the size-5 kernel tables only reached with floored/float dtypes. The four latent bugs are genuinely fixed — the Axpy/Ger inverted floor (complex mis-clamp + integer table-overrun segfault), the missing Gemm integer floor, Ger's alpha built after the floor, and Mod's buffer/kernel dtype disagreement. CI green on all platforms; 20 new promotion tests observed red pre-fix (incl. 3 segfaults).
Merge-order note: the Scalar.hpp hunks here are subsumed by #1011's variant rewrite — landing this first means #1011's rebase resolves that file trivially by taking its own version.
Posted by Claude Code on behalf of @pcchen
…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>
Summary
Follow-up to #982: that PR fixed
Type_class::type_promote(mixed complex/real pairs now promote by precision, e.g. ComplexFloat x Double -> ComplexDouble) and convertedVectordot. This PR converts the remaining nine linalg entry points that still hand-rolled "pick the lower enum index" dtype selection and therefore diverged fromAdd/Sub/Mul/DivandVectordot:Dot(Mat-Vec path),Matmul,Matmul_dg,Tensordot(cuTensor path),Axpy/Axpy_,Gemm/Gemm_,Gemm_Batch,Ger, andMod.All sites now follow the
Vectordotreference pattern: promote viaType.type_promote,astypeboth operands (the promoted dtype can differ from both inputs;astypeis a no-op when the dtype already matches), allocate the output at the promoted dtype, and dispatch the kernel by it.Latent bugs fixed along the way
fin_dtype < Type.Floatinstead of>): complex inputs were clamped onto the Double kernel (all-ComplexDoubleAxpythrew "Cannot cast complex128 to real"), and integer inputs indexed past the size-5 kernel tables — a reproducible segfault. Integer/bool now floors to Double after promotion.Gemm(out-of-place) was missing the integer→Double floor thatGemm_has; integer inputs segfaulted the same way.Gerbuilt its implicitalpha = 1scalar before the floor could changefin_dtype; it is now created at the final dtype.Mod's buffer and kernel disagreed:ModInternalImplalready computes inType_class::type_promote_t<TL,TR>, but the output buffer was allocated with the old min-index rule, so e.g.Uint64 % Int32wrote Int64 data into a Uint64-labeled buffer. All buffer-dtype sites (Tensor-Tensor,Scalar, and the typed-scalar specializations) now useType.type_promote.Tests
tests/linalg_test/DtypePromotion_test.cpp(20 tests) covering the discriminating pairs (ComplexFloat x Double -> ComplexDouble,Uint64 x Int32 -> Int64) and the integer floors. All were observed failing before the fix: 16 assertion failures plus 3 segfaults. TheGemm_Batchcase is#ifdef UNI_MKL-guarded like the existing suite; without MKL it still pins the promotion via the pre-dispatch cast ofcplus the expected throw.BkUt_*Svd_truncate_return_err*failures addressed by fix(linalg): report only actually-dropped values in block SVD return_err #977). No existing test asserted the old lower-index dtype, so none needed updating.Dependencies
Stacked on #982 (
fix/type-promotion-cross-kind) — only the last commit (efa641fb) is new here. Merge #982 first; this PR retargets tomasterautomatically when that branch is deleted on merge.🤖 Generated with Claude Code