Working with >1M cells#
Scirpy scales to millions of cells on a single workstation. This page is a work-in-progess collection with advice how to work with large datasets.
Use an up-to-date version!
Scalability has been a major focus of recent developments in Scirpy. Make sure you use the latest version when working with large datasets to take advantage of all speedups.
Distance metrics#
Computing pairwise sequence distances the major bottleneck for large datasets in the scirpy workflow. Here is some advice on how to maximize the speed of this step:
Choose an appropriate distance metric for pp.ir_dist#
Some distance metrics are significantly faster than others. Here are the distance metrics, roughly ordered by speed:
identity > gpu_hamming > hamming = normalized_hamming > tcrdist > levenshtein > fastalignment > alignment
TCRdist, fastalignment and alignment produce very similar distance matrices, but tcrdist is by far the fastest. For this
reason, we’d always recommend to go with tcrdist, when looking for a metric taking into account a substitution matrix.
Multi-machine parallelization with dask#
The hamming, normalized_hamming, tcrdist, levenshtein, fastalignment, and alignment metrics are parallelized
using joblib. This makes it very easy to switch the backend to
dask to distribute jobs across a multi machine cluster. Note that this comes with a
considerable overhead for communication between the workers. It’s only worthwhile when processing on a single
machine becomes infeasible.
from dask.distributed import Client, LocalCluster
import joblib
# substitute this with a multi-machine cluster...
cluster = LocalCluster(n_workers=16)
client = Client(cluster)
with joblib.parallel_config(backend="dask", n_jobs=200, verbose=10):
ir.pp.ir_dist(
mdata,
metric="tcrdist",
n_jobs=1, # jobs per worker
n_blocks = 20, # number of blocks sent to dask
)
Using GPU acceleration for hamming distance#
The Hamming distance metric supports GPU acceleration via cupy.
First, install the optional cupy dependency:
!pip install scirpy[cupy]
Then run
ir.pp.ir_dist(mdata, metric="gpu_hamming")
to use the first available GPU. The calculation is split into tiles. Their size can be adjusted with
gpu_tile_rows and gpu_tile_cols: smaller tiles use less GPU memory but increase processing overhead.
gpu_tile_buffer_cols controls the initially reserved space for retained distances and is enlarged automatically
if necessary.
Using multiple GPUs#
Multiple GPUs can be used through the joblib dask backend and dask-cuda:
!pip install "dask[distributed]" dask-cuda
The following example creates one dask worker per GPU and divides the distance calculation into two outer partitions:
import joblib
from dask.distributed import Client
from dask_cuda import LocalCUDACluster
with LocalCUDACluster(
CUDA_VISIBLE_DEVICES="0,1",
n_workers=2,
threads_per_worker=1,
) as cluster, Client(cluster):
with joblib.parallel_config(backend="dask", n_jobs=2):
ir.pp.ir_dist(
mdata,
metric="gpu_hamming",
n_blocks=2,
)
Set CUDA_VISIBLE_DEVICES to a comma-separated list of GPU IDs, such as "0,1" for GPUs 0 and 1.
For this setup, set n_workers, n_jobs, and n_blocks to the number of selected GPUs. n_blocks controls
the number of outer partitions distributed between workers, whereas gpu_tile_rows and gpu_tile_cols control
the size (rows and columns) of the smaller tiles computed within each worker.
For advanced Dask-CUDA configuration and GPU memory management, see the rapids-singlecell documentation.