You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Decision first: strided-einsum2 and strided-opteinsum are retired, together with the two adapter crates built on them (mdarray-opteinsum, ndarray-opteinsum). They functionally overlap the tenferro side, and contraction is unified there: tenferro-einsum owns subscripts and planning, tenferro-cpu owns CPU GEMM execution. strided-rs narrows to the affine strided primitive layer: strided-traits, strided-view, strided-kernel, strided-perm, and a slimmed strided-rs facade.
This matches the ownership contract already recorded in tenferro's REPOSITORY_RULES.md (strided-rs owns affine kernels: copy, permutation, broadcast, map, zip-map, axis reduction; einsum and dot-general are the tenferro-owned, benchmark-backed exception).
This umbrella also carries the remediation items from the 2026-08-02 audit of origin/main at 9da9b9f (six parallel review tracks; every critical finding re-verified at source). The retirement decision drives the triage: no further investment in the retiring crates; their audit findings are fixed in the absorbed tenferro implementation, not in place. All file:line references below are pinned to 9da9b9f.
Current consumers (verified)
tenferro-cpu is the only in-ecosystem consumer of strided-einsum2: crates/tenferro-cpu/src/gemm/{mod.rs, strided_dot.rs} use dot_general_with_backend_into, the Backend trait (FaerBackend), DotGeneralConfig, ScalarBase, and the view re-exports. Feature wiring: cpu-faer and the blas-openblas/blas-accelerate/blas-mkl lanes enable strided-einsum2 features.
Nothing in tensor4all-rs or tenferro-rs consumes strided-opteinsum or the adapter crates. They serve standalone users only.
Phase 0: declare the retirement and freeze the retiring crates
Mark strided-einsum2, strided-opteinsum, mdarray-opteinsum, ndarray-opteinsum deprecated: README banner, crate-level doc note, migration pointer to tenferro-einsum.
Feature freeze: no new features, no refactors, no performance work in the four crates. Only fixes that protect the current tenferro pin land here, and only if Phase 1 cannot deliver them first.
Record the scope change in root README.md and AGENTS.md (workspace tables shrink to the five retained crates).
Phase 1: absorb the strided GEMM machinery into tenferro
Open the tenferro-rs counterpart issue: move dot-general / batched strided GEMM (planning, contiguous preparation, faer and CBLAS provider glue, the uninit overwrite path) from strided-einsum2 into tenferro-cpu or a tenferro-internal crate. Carry the uninit contract tests along with the code.
Known-bug carry list: the absorbed implementation must fix these, not copy them. They live in code tenferro executes today through the pinned rev, so if any strided-einsum2 release ships before the migration completes, they must be fixed in place instead.
Batch fusion mispairing in the bgemm fast path (silent wrong results).try_fuse_group (strided-einsum2/src/util.rs:82) accepts any axis order; its own tests (util.rs:205) fix both row-major and col-major acceptance as intended behavior. The call sites (bgemm_faer.rs:296, bgemm_blas.rs:695) fuse A, B, C independently and advance one shared linear counter, so operands whose batch groups are contiguous in different axis orders get paired at different logical indices. Worked example: batch dims [2,3], A batch strides [3,1], B batch strides [1,2]; both fuse to (6,1), and at t=1 A sits at logical (0,1) while B sits at (1,0). Fix: require an identical (col-major) axis order across all three operands, otherwise take the MultiIndex fallback; add a mixed-order batch regression test. contiguous.rs:306 already fixes the same class for the inner groups.
Negative destination stride mishandled by the CBLAS ldc computation.bgemm_blas.rs:774 uses c.col_stride().max(m as isize), silently converting a negative fused stride into a positive leading dimension; validate_output (uninit.rs:164) checks only offset distinctness and passes negative strides. On the uninit path this writes out of bounds and leaves reachable slots uninitialized before assume_init; the initialized twins (bgemm_blas.rs:656, :674) share the coordinate bug. Fix: extend checked_operand_layout (introduced for A and B in Add safe uninitialized GEMM overwrite paths #196) to C, reject or copy out negative destination strides, add a negative-stride regression test.
Rank validation missing in the initialized einsum2 entry points.lib.rs:766 panics via dims[i] when label count and operand rank disagree; the uninit path already validates (uninit.rs:278). The absorbed implementation should share one validator across all entry points. The same gap exists in strided-opteinsum at expr.rs:1129 (panic with too many labels, silently wrong labeling with too few); record it in the deprecation notice as a known limitation.
Also worth fixing during the move, not blocking: validate_output builds an O(elements) HashSet twice per uninit call (uninit.rs:189, cheap structural alternative exists at fused.rs:696); the conj path clones the full backing buffer of the operand that does not need conjugation (uninit.rs:596); the beta != 0 provider contract for uninitialized C is undocumented (bgemm_blas.rs:749); a live panic! remains on the initialized beta path (bgemm_blas.rs:595).
Preserve provenance: history and NOTICE / third-party attributions travel with the code per docs/PROVENANCE_AND_CITATION_POLICY.md and the shared provenance rules.
Move the uninit GEMM design context: docs/plans/2026-07-29-uninitialized-static-output.md notes, and Track Faer typed MaybeUninit overwrite API for strided GEMM #198 (faer typed MaybeUninit overwrite API tracking) transfers to tenferro or closes here with a pointer.
Switch tenferro-cpu to the absorbed implementation and drop the strided-einsum2 dependency (update the pinned rev in tenferro's workspace Cargo.toml).
Decide the N-ary story: tenferro-einsum already owns subscripts and planning. Confirm nothing in strided-opteinsum (omeco greedy integration, single-tensor paths, buffer pooling) is worth porting; anything that is goes to tenferro-einsum under its AD and extension conventions.
Phase 2: remove the crates
Publish final versions carrying the deprecation notice (no yank), then remove the four crates from the workspace on main.
Slim the facade: drop the einsum2 / opteinsum / mdarray / ndarray modules, the flat einsum re-exports (einsum, einsum_into, BufferPool, TypedTensor, EinsumError, and friends), and the faer / blas / blas-inject feature plumbing. Bump the version.
Simplify the CI matrix accordingly (the faer / blas lanes leave with the crates).
Phase 3: remediation of the retained crates
Soundness and correctness
StridedView's manual Send impl uses the wrong bound: unsafe impl<T: Send, ...> Send at strided-view/src/view.rs:132 must require T: Sync (the struct holds &[T]). One line.
diagonal_view accepts an axis appearing in more than one pair and builds the result with new_unchecked, allowing out-of-bounds reads from safe code (view.rs:288). Reject duplicate axes or re-run validate_bounds on the result.
Unchecked shape products in StridedArray constructors (view.rs:689col_major and six siblings) and unchecked stride multiplies in col_major_strides / row_major_strides (view.rs:90, :103). Release builds wrap and can dereference a dangling Vec pointer. Replace with the checked fold already used at erased.rs:2580 and copy_plan.rs:232.
add / mul / axpy / fma / copy_transpose_scale_into never validate destination injectivity (ops_view.rs:254, :355, :456, :562, :1528); the copy_scale_raw family accepts non-injective destinations at rank 8 and below yet rejects them at rank 9 and above (raw_ops.rs:162). Consolidate the three validate_destination_layout copies (map_view.rs:39, :51, fused.rs:688) plus the nine inline copies into one pub(crate) helper and apply it across ops_view and raw_ops.
Dead split guard: fuse.rs:199 maps cost 0 to 1, so the costs[i] == 0 guard at threading.rs:344 can never fire (Strided.jl's "do not split this axis" signal is lost). Restore the signal or delete the guard; add a stride-0 destination test under parallel.
reduce_axis forms an out-of-allocation pointer when the reduced axis is empty (reduce_view.rs:338). Guard the zero-length axis before offsetting.
get / set / Index accessors panic without a documented contract, and Index skips the rank check entirely (view.rs:419, :628, :977, :990). Document # Panics or add try_ variants; fix the Index rank gap.
Threading and performance
Deduplicate the 1 << 15 threshold: threading.rs:59, map_view.rs:33, strided-perm/src/hptt/execute.rs:16 (which also differs by one in its comparison direction). One exported constant, one comparison convention.
Extend the rayon centralization guard test (strided-kernel/tests/execution_policy.rs:1185) to strided-perm, and make the HPTT parallel paths ExecutionPolicy-aware (execute.rs:92 reads the ambient pool directly).
Scalar tiled transpose kernels are gated on the parallel feature instead of being unconditional (simd.rs:203, call site map_view.rs:674), so enabling parallel changes single-thread kernel selection. Re-gate.
Refresh the stale serial rationale on reduce_axis (reduce_view.rs:293): the reduction-output partitioner it claims does not exist landed at erased.rs:3394. Parallelize the typed reduce_axis through it, or update the comment.
copy_into_col_major (threading.rs:130) is unconditionally serial with no rationale, and its parallel sibling copy_into_col_major_par is dead public API. After einsum2 leaves, decide: policy-aware, documented-serial, or removed.
Replace per-element checked_strided_offset recomputation in the erased axis reduction and the indexed families with incremental offsets (erased.rs:3378, :3436; the static_indexing_plan.rs and gather_plan.rs sites from the audit), and the per-element div/mod decode in raw_any (erased.rs:2376).
Docs, tests, organization, rules
Move benchmark tables out of strided-kernel/README.md:186 into strided-rs-benchmark-suite (per the existing repository rule); fix the stale 0.1 version strings and the four stale claims in docs/faer_design.md identified by the audit.
CI: add a feature matrix (at minimum parallel and a no-default-features lane), actually run clippy (the component is installed at ci.yml:32 and never invoked), and add RUSTDOCFLAGS: -D warnings to the doc job.
Port the tenferro rule set where the audit measured concrete gaps, adapting for this workspace: // INVARIANT: marker convention plus #[allow] rationale (0 markers today; // SAFETY: on 29 of 418 unsafe blocks), raw uninit acquisition must be unsafe or MaybeUninit-typed (strided-view/src/view.rs:936 already does it right; the einsum2 counterexamples leave with Phase 2), pool and cache ownership (bounded, clearable, documented), # Errors sections on public Result functions (9 of 172 today), doctest policy with no ignore fences (five runnable doctests in all of src/ today), inline #[cfg(test)] extraction to src/<module>/tests/ (53 inline modules; the largest is 1051 lines), Debug on public types (10 missing), the ~1000-line soft file trigger (13 files over; erased.rs is 4067 lines), and the complexity budget marker. Repository-neutral rules belong in tensor4all-agent-rules rather than a local copy.
Suffix vocabulary decision for the retained surface: add / mul / axpy / fma are unsuffixed read-modify-write operations, and mul (ops_view.rs:355, dest *= src) vs mul_into (map_view.rs:1713, dest = a * b) are different operations under a name pair that suggests otherwise. With einsum2 gone, the beta-carrying _into entry points leave this repository, so the remaining decisions are the elementwise accumulate naming (_add_to or explicit accumulate types) and one consistent meaning for _view (metadata-only vs takes-a-view). Record the outcome in REPOSITORY_RULES.md.
strided-perm ships roughly 1000 lines of #[cfg(test)]-gated implementation modules in src/ (lib.rs:14: block, kernel, order, with an unexplained #[allow(dead_code)]). Fold them into test support or remove them.
Sub-issue index
Task 0, first task of this umbrella: tensor4all/tensor4all-agent-rules#6. The repository-neutral rules (INVARIANT markers, unsafe and uninit hygiene, cache ownership, # Errors gate, doctest policy, test organization, API evolution, typed errors, Debug, file size, complexity budget, threading principles) are generalized into tensor4all-agent-rules so every tensor4all project reuses them; this repo and tenferro then adopt by reference instead of vendoring copies.
Task 0 (tensor4all-agent-rules#6) starts first: the shared rule text unblocks the per-repo rule adoption (#216 here, and the corresponding trim in tenferro). Phase 0 (retirement declaration, freeze, issue disposition) lands next and unblocks everything else. Phase 1 and Phase 3 proceed in parallel. Phase 2 follows Phase 1. The CI item (#215) can start now.
References
Audit basis: 2026-08-02, origin/main at 9da9b9f, six review tracks (API uniformity, boundary safety in view/kernel and in the einsum crates, uninit contracts, threading and performance, docs and test organization). Critical findings were re-verified at source; file:line references above are self-contained.
Summary and decision
Decision first:
strided-einsum2andstrided-opteinsumare retired, together with the two adapter crates built on them (mdarray-opteinsum,ndarray-opteinsum). They functionally overlap the tenferro side, and contraction is unified there:tenferro-einsumowns subscripts and planning,tenferro-cpuowns CPU GEMM execution. strided-rs narrows to the affine strided primitive layer:strided-traits,strided-view,strided-kernel,strided-perm, and a slimmedstrided-rsfacade.This matches the ownership contract already recorded in tenferro's
REPOSITORY_RULES.md(strided-rs owns affine kernels: copy, permutation, broadcast, map, zip-map, axis reduction; einsum and dot-general are the tenferro-owned, benchmark-backed exception).This umbrella also carries the remediation items from the 2026-08-02 audit of
origin/mainat 9da9b9f (six parallel review tracks; every critical finding re-verified at source). The retirement decision drives the triage: no further investment in the retiring crates; their audit findings are fixed in the absorbed tenferro implementation, not in place. All file:line references below are pinned to 9da9b9f.Current consumers (verified)
tenferro-cpuis the only in-ecosystem consumer ofstrided-einsum2:crates/tenferro-cpu/src/gemm/{mod.rs, strided_dot.rs}usedot_general_with_backend_into, theBackendtrait (FaerBackend),DotGeneralConfig,ScalarBase, and the view re-exports. Feature wiring:cpu-faerand theblas-openblas/blas-accelerate/blas-mkllanes enablestrided-einsum2features.strided-opteinsumor the adapter crates. They serve standalone users only.Phase 0: declare the retirement and freeze the retiring crates
strided-einsum2,strided-opteinsum,mdarray-opteinsum,ndarray-opteinsumdeprecated: README banner, crate-level doc note, migration pointer totenferro-einsum.README.mdandAGENTS.md(workspace tables shrink to the five retained crates).Phase 1: absorb the strided GEMM machinery into tenferro
strided-einsum2intotenferro-cpuor a tenferro-internal crate. Carry the uninit contract tests along with the code.try_fuse_group(strided-einsum2/src/util.rs:82) accepts any axis order; its own tests (util.rs:205) fix both row-major and col-major acceptance as intended behavior. The call sites (bgemm_faer.rs:296,bgemm_blas.rs:695) fuse A, B, C independently and advance one shared linear counter, so operands whose batch groups are contiguous in different axis orders get paired at different logical indices. Worked example: batch dims[2,3], A batch strides[3,1], B batch strides[1,2]; both fuse to(6,1), and att=1A sits at logical(0,1)while B sits at(1,0). Fix: require an identical (col-major) axis order across all three operands, otherwise take theMultiIndexfallback; add a mixed-order batch regression test.contiguous.rs:306already fixes the same class for the inner groups.ldccomputation.bgemm_blas.rs:774usesc.col_stride().max(m as isize), silently converting a negative fused stride into a positive leading dimension;validate_output(uninit.rs:164) checks only offset distinctness and passes negative strides. On the uninit path this writes out of bounds and leaves reachable slots uninitialized beforeassume_init; the initialized twins (bgemm_blas.rs:656,:674) share the coordinate bug. Fix: extendchecked_operand_layout(introduced for A and B in Add safe uninitialized GEMM overwrite paths #196) to C, reject or copy out negative destination strides, add a negative-stride regression test.lib.rs:766panics viadims[i]when label count and operand rank disagree; the uninit path already validates (uninit.rs:278). The absorbed implementation should share one validator across all entry points. The same gap exists instrided-opteinsumatexpr.rs:1129(panic with too many labels, silently wrong labeling with too few); record it in the deprecation notice as a known limitation.validate_outputbuilds an O(elements)HashSettwice per uninit call (uninit.rs:189, cheap structural alternative exists atfused.rs:696); the conj path clones the full backing buffer of the operand that does not need conjugation (uninit.rs:596); thebeta != 0provider contract for uninitialized C is undocumented (bgemm_blas.rs:749); a livepanic!remains on the initialized beta path (bgemm_blas.rs:595).NOTICE/ third-party attributions travel with the code perdocs/PROVENANCE_AND_CITATION_POLICY.mdand the shared provenance rules.docs/plans/2026-07-29-uninitialized-static-output.mdnotes, and Track Faer typed MaybeUninit overwrite API for strided GEMM #198 (faer typedMaybeUninitoverwrite API tracking) transfers to tenferro or closes here with a pointer.tenferro-cputo the absorbed implementation and drop thestrided-einsum2dependency (update the pinned rev in tenferro's workspaceCargo.toml).tenferro-einsumalready owns subscripts and planning. Confirm nothing instrided-opteinsum(omeco greedy integration, single-tensor paths, buffer pooling) is worth porting; anything that is goes totenferro-einsumunder its AD and extension conventions.Phase 2: remove the crates
main.einsum2/opteinsum/mdarray/ndarraymodules, the flat einsum re-exports (einsum,einsum_into,BufferPool,TypedTensor,EinsumError, and friends), and thefaer/blas/blas-injectfeature plumbing. Bump the version.faer/blaslanes leave with the crates).Phase 3: remediation of the retained crates
Soundness and correctness
StridedView's manualSendimpl uses the wrong bound:unsafe impl<T: Send, ...> Sendatstrided-view/src/view.rs:132must requireT: Sync(the struct holds&[T]). One line.diagonal_viewaccepts an axis appearing in more than one pair and builds the result withnew_unchecked, allowing out-of-bounds reads from safe code (view.rs:288). Reject duplicate axes or re-runvalidate_boundson the result.StridedArrayconstructors (view.rs:689col_majorand six siblings) and unchecked stride multiplies incol_major_strides/row_major_strides(view.rs:90,:103). Release builds wrap and can dereference a danglingVecpointer. Replace with the checked fold already used aterased.rs:2580andcopy_plan.rs:232.add/mul/axpy/fma/copy_transpose_scale_intonever validate destination injectivity (ops_view.rs:254,:355,:456,:562,:1528); thecopy_scale_rawfamily accepts non-injective destinations at rank 8 and below yet rejects them at rank 9 and above (raw_ops.rs:162). Consolidate the threevalidate_destination_layoutcopies (map_view.rs:39,:51,fused.rs:688) plus the nine inline copies into onepub(crate)helper and apply it acrossops_viewandraw_ops.fuse.rs:199maps cost 0 to 1, so thecosts[i] == 0guard atthreading.rs:344can never fire (Strided.jl's "do not split this axis" signal is lost). Restore the signal or delete the guard; add a stride-0 destination test underparallel.reduce_axisforms an out-of-allocation pointer when the reduced axis is empty (reduce_view.rs:338). Guard the zero-length axis before offsetting.get/set/Indexaccessors panic without a documented contract, andIndexskips the rank check entirely (view.rs:419,:628,:977,:990). Document# Panicsor addtry_variants; fix theIndexrank gap.Threading and performance
1 << 15threshold:threading.rs:59,map_view.rs:33,strided-perm/src/hptt/execute.rs:16(which also differs by one in its comparison direction). One exported constant, one comparison convention.strided-kernel/tests/execution_policy.rs:1185) tostrided-perm, and make the HPTT parallel pathsExecutionPolicy-aware (execute.rs:92reads the ambient pool directly).parallelfeature instead of being unconditional (simd.rs:203, call sitemap_view.rs:674), so enablingparallelchanges single-thread kernel selection. Re-gate.reduce_axis(reduce_view.rs:293): the reduction-output partitioner it claims does not exist landed aterased.rs:3394. Parallelize the typedreduce_axisthrough it, or update the comment.copy_into_col_major(threading.rs:130) is unconditionally serial with no rationale, and its parallel siblingcopy_into_col_major_paris dead public API. After einsum2 leaves, decide: policy-aware, documented-serial, or removed.checked_strided_offsetrecomputation in the erased axis reduction and the indexed families with incremental offsets (erased.rs:3378,:3436; thestatic_indexing_plan.rsandgather_plan.rssites from the audit), and the per-element div/mod decode inraw_any(erased.rs:2376).Docs, tests, organization, rules
strided-kernel/README.md:186intostrided-rs-benchmark-suite(per the existing repository rule); fix the stale0.1version strings and the four stale claims indocs/faer_design.mdidentified by the audit.paralleland a no-default-features lane), actually run clippy (the component is installed atci.yml:32and never invoked), and addRUSTDOCFLAGS: -D warningsto the doc job.// INVARIANT:marker convention plus#[allow]rationale (0 markers today;// SAFETY:on 29 of 418 unsafe blocks), raw uninit acquisition must beunsafeorMaybeUninit-typed (strided-view/src/view.rs:936already does it right; the einsum2 counterexamples leave with Phase 2), pool and cache ownership (bounded, clearable, documented),# Errorssections on publicResultfunctions (9 of 172 today), doctest policy with noignorefences (five runnable doctests in all ofsrc/today), inline#[cfg(test)]extraction tosrc/<module>/tests/(53 inline modules; the largest is 1051 lines),Debugon public types (10 missing), the ~1000-line soft file trigger (13 files over;erased.rsis 4067 lines), and the complexity budget marker. Repository-neutral rules belong intensor4all-agent-rulesrather than a local copy.add/mul/axpy/fmaare unsuffixed read-modify-write operations, andmul(ops_view.rs:355,dest *= src) vsmul_into(map_view.rs:1713,dest = a * b) are different operations under a name pair that suggests otherwise. With einsum2 gone, the beta-carrying_intoentry points leave this repository, so the remaining decisions are the elementwise accumulate naming (_add_toor explicit accumulate types) and one consistent meaning for_view(metadata-only vs takes-a-view). Record the outcome inREPOSITORY_RULES.md.strided-permships roughly 1000 lines of#[cfg(test)]-gated implementation modules insrc/(lib.rs:14:block,kernel,order, with an unexplained#[allow(dead_code)]). Fold them into test support or remove them.Sub-issue index
Task 0, first task of this umbrella: tensor4all/tensor4all-agent-rules#6. The repository-neutral rules (INVARIANT markers, unsafe and uninit hygiene, cache ownership,
# Errorsgate, doctest policy, test organization, API evolution, typed errors, Debug, file size, complexity budget, threading principles) are generalized intotensor4all-agent-rulesso every tensor4all project reuses them; this repo and tenferro then adopt by reference instead of vendoring copies.Sendbound), strided-view: diagonal_view with duplicated axes gives out-of-bounds reads from safe code #204 (diagonal_view), strided-view: checked arithmetic for shape products and stride construction in StridedArray constructors #205 (checked shape products), strided-kernel: destination injectivity for elementwise RMW ops; consolidate validate_destination_layout; dead cost==0 split guard #206 (destination injectivity, dead split guard), strided-kernel: reduce_axis forms an out-of-allocation pointer when the reduced axis is empty #207 (reduce_axisdangling pointer), strided-view: panicking element accessors; Index skips the rank check #208 (panicking accessors,Indexrank gap)parallel-gated scalar kernels), strided-kernel: parallelize typed reduce_axis through the existing reduction partitioner #211 (parallel typedreduce_axis), strided-kernel: make copy_into_col_major policy-aware or remove the duplicate API #212 (copy_into_col_majorfate), strided-kernel: incremental offsets in erased reduce and indexed replay hot loops #213 (incremental offsets in hot loops)# Errorsand doctests), Test organization: extract inline cfg(test) modules; resolve strided-perm's test-gated implementation modules #218 (test organization), Marker remediation: SAFETY comments, #[allow] rationales, INVARIANT markers #219 (SAFETY /#[allow]/ INVARIANT markers), Suffix vocabulary for the retained kernel surface (accumulating ops and _view) #220 (suffix vocabulary), Add Debug impls to the remaining public types #221 (Debugimpls)Sequencing
Task 0 (tensor4all-agent-rules#6) starts first: the shared rule text unblocks the per-repo rule adoption (#216 here, and the corresponding trim in tenferro). Phase 0 (retirement declaration, freeze, issue disposition) lands next and unblocks everything else. Phase 1 and Phase 3 proceed in parallel. Phase 2 follows Phase 1. The CI item (#215) can start now.
References
origin/mainat 9da9b9f, six review tracks (API uniformity, boundary safety in view/kernel and in the einsum crates, uninit contracts, threading and performance, docs and test organization). Critical findings were re-verified at source; file:line references above are self-contained.REPOSITORY_RULES.md, CPU Kernel Implementation section (affine-kernel-owner = strided-rs,einsum-owner = tenferro:benchmark-backed-exception).