Skip to content

feat: add dask IO helpers and zarr direct storage options - #824

Draft
Intron7 wants to merge 10 commits into
mainfrom
feat/io-dask-helpers
Draft

Intron7 wants to merge 10 commits into
mainfrom
feat/io-dask-helpers

Conversation

@Intron7

@Intron7 Intron7 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Out-of-core analysis on the GPU: read AnnData zarr stores lazily, with chunks read straight into GPU memory and decompressed there, and start a ready-to-use Dask cluster in one call.

LLM usage

  • LLM or coding agent used: Claude Opus 5.5 via Claude Code
  • I, a human, have reviewed, understood and tested every changed line

rapids_singlecell.io

  • read_lazy(path, chunks="auto"): an AnnData zarr store with obs/var in memory and X (CSR or dense) as a Dask array of CuPy chunks. It is anndata.experimental.read_elem_lazy with GPU chunks and a chunk size that worked best in benchmarks (~55 chunks per GPU with GPU reads, 20,000 cells otherwise).
  • rapids_singlecell_zarr.KvikioCodecPipeline: a zarr codec pipeline that reads chunks with KvikIO into GPU memory and decodes zstd (or uncompressed) chunks with nvCOMP, shard by shard. zarr finds it through the zarr.codec_pipeline entry point, so it works in every Dask worker. Chunks it cannot decode on the GPU (e.g. blosc, as in most existing stores, zarr v2, host buffers) fall back to the pipeline configured before (e.g. zarrs) or zarr's fused pipeline.
  • enable() / enable(client): switches GPU reads on in this process or on all workers; as a context manager, undone at the end of the block. release_memory() frees the cached nvCOMP scratch space.
  • check(path): versions, GPUDirect Storage status, and what keeps a store from being read fast on the GPU.
  • convert_zarr(src, dst): rewrites a store for GPU reads (zstd, ~256 KiB chunks in shards, the smallest index dtype). Every matrix (X, layers, raw, obsm, obsp; CSR, CSC or dense) is streamed shard by shard and checked against the source; everything else is copied unchanged.

rapids_singlecell.dask.start_cluster

A LocalCUDACluster with one worker per GPU and defaults that work out of the box, returned as a plain distributed.Client:

  • 4 threads per worker, UCX if available (else TCP).
  • RMM's asynchronous allocator, at most 75% of each GPU (room for the steps in the calling process); RMM's pool allocator and TCP with managed memory. CuPy allocates through RMM.
  • GPU reads switched on for the workers if KvikIO and nvCOMP are installed.
  • Warns if a GPU is already busy; raises a clear error if the workers cannot start (e.g. a script without if __name__ == "__main__":).

Packaging and docs

  • io-cu12 / io-cu13 extras (KvikIO, nvCOMP), io in the CUDA-specific wheels; rapids_singlecell_zarr ships in the wheel; zarr ≥ 3.3. The hatch test envs install the io extras.
  • docs/out_of_core.md: quick start with start_cluster and read_lazy, GPU reads, check/convert_zarr; API pages for io and dask; installation.

Performance

11.4M-cell atlas (19.5B non-zeros), 2 GPUs, calculate_qc_metrics → pca: 26 s with GPU reads (100,000 cells per chunk) vs ~95 s decompressing on the CPU (20,000 cells per chunk, the fastest setting there). The subsetting in these steps relies on scverse/anndata#2675 and scverse/anndata#2676.

Notes for review

  • GPU CI (and with it Codecov) only runs with the run-gpu-ci label.

🤖 Generated with Claude Code

Signed-off-by: Intron7 <sdicks@nvidia.com>
@Intron7
Intron7 marked this pull request as draft October 2, 2026 14:57
Intron7 and others added 9 commits October 6, 2026 17:56
Signed-off-by: Intron7 <sdicks@nvidia.com>
Signed-off-by: Intron7 <sdicks@nvidia.com>
…vert_zarr

- Contiguous reads of sharded 1-D arrays (a sparse matrix's data and indices, e.g. read_lazy's
  row blocks) are planned with numpy, read in one pass into pinned host memory (in parallel for
  large ranges), copied to the GPU at once and decoded in one nvCOMP call.
  `RSC_IO_FETCH=kvikio` reads straight into GPU memory with KvikIO instead (for GPUDirect Storage).
- nvCOMP gets its pinned host memory from a pool: it allocated and freed it in every decode
  call, and freeing it synchronises the whole device.
- convert_zarr writes with zarr's fused pipeline, which stores the chunks of a shard in order
  (zarrs stores them in the order they were compressed in, scattering contiguous ranges).
- check reports 1-D arrays whose shards are out of order.

perturbai demo (4.24M cells, 2 GPUs, start_cluster + read_lazy, normalise through PCA):
33.5-37.2 s -> 25.9-26.0 s on the existing store, 24.5-24.9 s with ordered shards;
identical results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Intron7 <sdicks@nvidia.com>

This branch has not been deployed

No deployments
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