Skip to content

Bal pf2 gpu testing - #52

Closed
aarmey wants to merge 7 commits into
mainfrom
bal-pf2-gpu-testing
Closed

aarmey wants to merge 7 commits into
mainfrom
bal-pf2-gpu-testing

Conversation

@aarmey

@aarmey aarmey commented Sep 14, 2026

Copy link
Copy Markdown
Member

No description provided.

aarmey and others added 7 commits September 12, 2026 19:05
parafac2 touches its input matrix through matmul/rmatmul (already
supported via __matmul__/__rmatmul__) plus a squared-Frobenius-norm
reduction it currently only knows how to compute for a plain
np.ndarray or scipy.sparse array. Add that as norm_sq()/slice_norms()
on NormalizedViewBase, computed with new numba kernels that reuse the
same O(nnz) traversal as the existing matmul/toarray kernels, so a
normalized view can satisfy parafac2's duck-typed backend contract
without materializing anything.

Also add to_scipy_sparse() (the uncentered, scaled sparse term with
the same sparsity pattern as the raw array) and a means property (the
per-gene correction to subtract from it), for code that only knows
how to move a plain NumPy/SciPy array onto a device -- e.g. so a
normalized view can be materialized into a real (and, for typical
single-cell data, tiny) sparse array before running through
parafac2's existing CuPy/MLX GPU path, rather than needing new
GPU-native kernels of its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VCSCAnnData stores X/raw_X in private _vcs_X/_vcs_raw_X attributes
(anndata's own X validation rejects a VCSCArray/VCSRArray directly),
but never overrode copy(), so the inherited anndata.AnnData.copy()
copies the standard (unused, always-None) _X attribute instead: X
silently comes back None, and the returned object is downgraded to a
plain AnnData rather than preserving this class (or a subclass, such
as one overriding the X property for a lazy-normalized view).

Any caller relying on `adata[mask].copy()` -- e.g. parafac2's BiCV
train/test splitting -- hits this immediately. Add an explicit
override that copies every field, including a real VCSCArray/VCSRArray
copy of X/raw_X, and returns type(self).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The inherited anndata.AnnData.to_memory() has the same root cause as
copy() (previous commit): it only knows about the standard, unused _X
attribute, not this class's _vcs_X/_vcs_raw_X, and reconstructs a plain
AnnData that silently loses X. This class never actually supports a
lazily backed X/raw_X, so to_memory() now just delegates to copy().

scrise's BiCV rank selection calls to_memory() on its input before the
train/test split loop, so this was hit immediately after fixing copy().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
__getitem__ already returned a new, eagerly-copied object (rather than
a lazy view) since anndata's own view machinery can't handle _vcs_X,
but it hardcoded the returned type to VCSCAnnData instead of
type(self) -- so a subclass overriding X (e.g. one that always hands
back a value normalized fresh from _vcs_X, as with copy()/to_memory()
in the previous two commits) silently reverted to the raw,
un-normalized array after any slice.

scrise's BiCV rank selection slices its input for every train/test
split, so this surfaced immediately after fixing copy()/to_memory():
the sliced object's X was the raw VCSRArray, which doesn't implement
the norm_sq() a normalized view needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rnel

The misaligned direction of a normalized view's matmul (self@B for VCSC,
B@self for VCSR) previously regrouped the array into the other VCS format
a chunk of major slices at a time (_chunk_bounds/_transpose_major), caching
a full opposite-format copy (_dual_arr) when the whole array fit one
chunk's budget. At scale (~2.3B nonzeros, 1.3M x 12.2K), this needed
~1,150 chunks per pass, repeated across every power iteration and every
rank/trial in a BiCV sweep -- tens of thousands of variably-sized
alloc/free cycles that fragmented the allocator and drove RSS to ~140 GB
on a shared machine, even though no single chunk's live memory was large.

Replace it with a direct minor-axis kernel that walks the array's own
storage as-is: each thread gets a private, full-output-sized accumulator
and scatters into it while owning a disjoint range of major slices, summed
across threads at the end -- the same pattern _ops.py already uses for
minor_sums/minor_counts/minor_extrema. accumulator_threads caps the thread
count to a fixed byte budget, so the accumulator block is always
nthreads * output_dim * width * 8 bytes: bounded, independent of nnz, and
allocated once per call instead of thousands of times. For a huge output
axis (e.g. millions of cells) that caps down to a single thread, trading
parallelism for a hard memory bound rather than the previous unbounded
chunk churn.

Removes _chunk_bounds/_aligned_source/_build_dual/_dual_arr and the
chunked-transpose test suite; adds tests/test_minor_axis_matmul.py
covering correctness against a dense reference, thread-count invariance,
that no second copy of the array is ever built, and that peak memory
stays bounded by the accumulator budget rather than nnz.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion

select() previously always recomputed statistics fresh for the selected
sub-array. A caller that instead wants to evaluate a fixed (e.g.
train-derived) normalization against a different slice -- such as
bi-cross-validation scoring a held-out test block against train-derived
statistics -- had no lazy option: bracket indexing (__getitem__) keeps the
parent's statistics but is documented to always eagerly materialize the
selection as a dense ndarray, which is fine for a small window but not for
a selection covering a large fraction of a huge array.

select(rows, cols, recalculate=False) reuses this view's existing a/b/c/s
(row_scale sliced by rows -- exact, since it's a per-cell quantity; the
per-gene stats sliced by cols, unchanged by which rows are selected) via
the existing from_stats() classmethod, and returns a real view rather than
an array -- so it composes with `@`/toarray() and stays lazy regardless of
selection size, unlike __getitem__.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aarmey aarmey closed this Sep 14, 2026
@aarmey
aarmey deleted the bal-pf2-gpu-testing branch September 14, 2026 17:10
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.

1 participant