Skip to content

Umbrella: retire strided-einsum2 and strided-opteinsum in favor of tenferro, plus 2026-08-02 audit remediation #199

Description

@shinaoka

Summary and decision

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

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:689 col_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.

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

  • 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.
  • Prior umbrella Umbrella: upstream tenferro CPU kernels into strided-kernel and expose a reusable C ABI #149 (closed): erased and prepared execution upstreaming. This umbrella inherits its scope boundary and narrows the workspace further.
  • Ownership contract: tenferro-rs REPOSITORY_RULES.md, CPU Kernel Implementation section (affine-kernel-owner = strided-rs, einsum-owner = tenferro:benchmark-backed-exception).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions