7. GPU 加速分析#

   关键要点

rapids-singlecell 在 GPU 上复刻了 scanpy API,因此大多数预处理和聚类流程都可以通过把 sc 换成 rsc,并用 rsc.get.anndata_to_GPU 将 AnnData 移到 GPU 来完成迁移。

GPU 上的 scanpy 分析流程

只选定一种分配器策略:如果数据能装进 VRAM,就用池分配器(pool allocator);如果装不下,则用托管(统一)内存。两者混用会导致内存碎片化,并削弱加速效果。

用 RMM 管理内存

一旦某个步骤不再需要 GPU,就用 rsc.get.anndata_to_CPU 将数据移回主机内存、释放中间的 CuPy 数组,并调用 cp.get_default_memory_pool().free_all_blocks() 来释放你不再使用的 VRAM。

显式释放中间结果

对于超出单块 GPU 显存的数据集,可结合 dask-cuda 和 AnnData 的 read_elem_lazy 读取器,将矩阵分块流式送入 GPU;QC、HVG 选择、缩放和 PCA 等步骤会偶尔触发同步并强制计算。

使用 dask-cuda 进行核外分析
   环境设置
  1. 安装 conda:

    • 在创建环境之前,请确保 conda 已安装在您的系统中。

  2. 保存 yml 内容:

    • 将 yml 选项卡中的内容复制到名为 environment.yml 的文件中。

  3. 创建环境:

    • 打开终端或命令提示符。

    • 运行以下命令:

      conda env create -f environment.yml
      
  4. 激活环境:

    • 创建好环境后,使用以下命令激活它:

      conda activate <environment_name>
      
    • 替换 <environment_name>,名称就是在 environment.yml 文件中指定的那个。在 yml 文件里,它看起来像这样:

      name: <environment_name>
      
  5. 验证安装:

    • 通过运行以下命令,检查环境是否创建成功:

      conda env list
      
name: rapids_singlecell
channels:
  - conda-forge
  - bioconda
dependencies:
  - python=3.14
  - conda-forge::scanpy=1.12.1
  - pip
  - pip:
      - --extra-index-url=https://pypi.nvidia.com
      - rapids-singlecell-cu13[rapids]
      - lamindb
   获取数据和笔记本

本书使用 lamindb,并通过 theislab/sc-best-practices 实例 来存储、共享和加载数据集与笔记本。感谢 Lamin Labs 提供免费托管服务。

  1. 安装 lamindb

    • 安装 lamindb Python 软件包:

    pip install lamindb
    
  2. 可选择创建 lamin 账户

    • 按照以下 说明 注册并登录。

  3. 验证你的设置

    • 运行 lamin connect 命令:

    import lamindb as ln
    
    ln.Artifact.connect("theislab/sc-best-practices").df()
    

    你现在应该看到最多100个存储的数据集。

  4. 访问数据集(Artifact)

    • Artifacts 页面 搜索该数据集。

    • 加载一个 Artifact 及其对应的对象:

    import lamindb as ln
    af = ln.Artifact.connect("theislab/sc-best-practices").get(key="key_of_dataset", is_latest=True)
    obj = af.load()
    

    该对象现在已可在内存中访问,并可用于分析。请调整 ln.Artifact.connect("theislab/sc-best-practices").get("SOMEIDXXXX") 后缀以获取相应的版本。

  5. 访问笔记本(Transform)

    lamin load <notebook url>
    

    这会把笔记本下载到当前工作目录。与 Artifacts 类似,你可以调整后缀 ID 来获取旧版本。

7.1. 动机#

本书的大多数章节都在 CPU 上运行,数据集规模为数万到数十万个细胞。如今的现代图谱已达到数百万个细胞,同一套 scanpy 风格的流程,在笔记本电脑上几分钟就能跑完,在工作站上却可能耗费数小时,甚至在跑完之前就因内存耗尽而崩溃。

rapids-singlecell (RSC)是一个 scverse 核心包,它在以下库的基础上,将 scanpy 的 API 移植到 GPU 上: RAPIDS、 CuPy 和 cuML [Dicks et al., 2026, Nolet et al., 2024]。它还提供了部分 squidpy [Palla et al., 2022], decoupler [Badia-i-Mompel et al., 2022]和 pertpy [Heumos et al., 2025] 等例程的 GPU 实现。相对于 CPU 版 scanpy,标准的预处理和聚类流程通常可获得 10–20× 的端到端加速,而 UMAP、t-SNE、Leiden 等单个步骤的运行速度可快 50–200×。

7.2. GPU 何时才划算?#

当计算任务规整、且规模大到足以摊薄主机到设备的传输成本时,GPU 才有帮助。对于单细胞数据,大致的经验法则是:

  • ~50k 细胞以下:用 CPU 版 scanpy 即可,此时传输开销可能占主导。只有当你在方法开发过程中需要快速迭代时,才使用 GPU。

  • 单块 GPU 上 50k–1M 个细胞:这是最佳区间。单块 24–80 GB 的 GPU 即可交互式地运行整条标准流程。

  • >1M 细胞,或存在内存压力:将 RSC 与 dask-cuda 结合,用于核外(out-of-core)或多 GPU 执行(见下文)。

单块 GPU 上的主要瓶颈是 VRAM,而不是计算能力。一个 1M 细胞 × 30k 基因的稠密 float32 表达矩阵约为 120 GB,根本无法放入显存。稀疏 CSR 格式让 GPU 上的单细胞分析变得可行;下面的大多数最佳实践,也都是围绕如何让矩阵尽可能久地保持稀疏,并停留在合适的内存位置。

7.3. 安装#

最近的 RSC 发行版提供了预编译的 wheel 包,覆盖 CUDA 12 和 13,以及从 Turing 到 Blackwell 的各代 GPU 架构。请使用 [rapids] 可选项进行安装,这样完整的 RAPIDS 堆栈(cuDF、cuML、cuGraph、RMM、CuPy)就会连同预编译的内核一起被拉取进来:

# CUDA 13
pip install 'rapids-singlecell-cu13[rapids]' --extra-index-url=https://pypi.nvidia.com

# CUDA 12
pip install 'rapids-singlecell-cu12[rapids]' --extra-index-url=https://pypi.nvidia.com

--extra-index-url 是必需的,因为 RAPIDS 的 wheel 包托管在 NVIDIA 的 PyPI 索引上,而不是公共索引上。在较旧的 RSC 版本中,你还必须额外安装 RAPIDS 的 conda 包;现在已不再需要这一步。

7.4. 用 RMM 管理内存#

RSC 的所有 GPU 内存分配都会经由 RAPIDS 内存管理器(RMM)[NVIDIA RAPIDS, 2024]。导入 rapids_singlecell 时,它已经把默认的 CuPy 分配器替换为基于 RMM 的分配器;但 RMM 的默认策略仍是普通 CUDA 分配器:每次分配都会调用 cudaMalloc,每次释放都会调用 cudaFree。单细胞流程会频繁分配内存(每个 scanpy 函数都会产生临时数组),因此这种默认策略会浪费大量性能。

有两种策略,并且应该只选择其中一种:

  • 池分配器 ——RMM 会预先抓取一大块 VRAM,并从中满足后续的内存分配。这是迄今为止最快的方案,但它会把你限制在 VRAM 能容纳的范围内。只要你的数据集放得下,就使用此方案。

  • 托管(统一)内存 ——分配由 CUDA 托管内存支持,因此驱动程序会在主机 RAM 与显存之间透明地换入换出数据。这正是你能在一块 40 GB GPU 上分析 200 GB 数据集的原因。它的单次操作较慢(缺页并非没有代价),但通常仍快于 CPU 版 scanpy。

不同时启用两者

pool_allocator=Truemanaged_memory=True 组合使用看起来很诱人,但内存池会建立在托管内存之上,两种策略会相互冲突。这样既要承担换页带来的延迟,又得不到内存池的速度优势;随着 notebook 运行时间变长,碎片化还会越来越严重。请只选其一。

请在 notebook 的 最顶部 、在创建任何 CuPy 数组之前,配置池分配器。在 CuPy 已经分配之后再重新初始化 RMM,会泄漏旧的分配。

import cupy as cp
import rmm
from rmm.allocators.cupy import rmm_cupy_allocator

rmm.reinitialize(
    pool_allocator=True,
    managed_memory=False,
    initial_pool_size="4GB",  # size to expected peak — growth events defeat the pool's purpose
    maximum_pool_size="32GB",  # leave headroom for plotting libraries, etc.
)
cp.cuda.set_allocator(rmm_cupy_allocator)

如果你的数据集放不下,就改用托管内存。在搭载 Pascal 或更新架构 GPU 的 Linux 上,这是真正的统一内存,开箱即支持超额订阅(oversubscription);而在 WSL2 下的 Windows 上,它是模拟实现的,速度略慢一些。

rmm.reinitialize(
    pool_allocator=False,
    managed_memory=True,
)
cp.cuda.set_allocator(rmm_cupy_allocator)

配置好分配器之后,其余导入就和普通的 scanpy notebook 一样;按照惯例,rapids_singlecell 通常简写为 rsc

import warnings

warnings.filterwarnings("ignore")

import anndata as ad
import pooch
import rapids_singlecell as rsc
import scanpy as sc

7.5. GPU 上的 scanpy 分析流程#

RSC 有意镜像了 scanpy 的模块布局: rsc.pp 用于预处理和 rsc.tl 用于工具。大多数 scanpy 调用都有一个签名相同、一一对应的 RSC 对应函数,因此移植流程基本上只是替换前缀的问题。

思路很简单:

  1. 像平常一样读入 AnnData(保留在 CPU 内存中,且 .X 为稀疏矩阵)。

  2. rsc.get.anndata_to_GPU 一次性移到 GPU。

  3. rsc.* 运行计算量大的预处理和聚类步骤。

  4. rsc.get.anndata_to_CPU 移回主机内存,用于绘图、保存或其他任何仅限 CPU 的操作。

我们使用与上游 RSC 教程相同的示例数据集,即约 90k 细胞的 DLI census 子集,因此这里的运行时间可以直接与 rapids-singlecell 文档中的报告结果比较。

path = pooch.retrieve(
    url="https://exampledata.scverse.org/rapids-singlecell/dli_census.h5ad",
    fname="dli_census.h5ad",
)
adata = ad.read_h5ad(path)
adata.var_names = adata.var.feature_name
adata
AnnData object with n_obs × n_vars = 216611 × 25430
    obs: 'soma_joinid', 'dataset_id', 'assay', 'assay_ontology_term_id', 'cell_type', 'cell_type_ontology_term_id', 'development_stage', 'development_stage_ontology_term_id', 'disease', 'disease_ontology_term_id', 'donor_id', 'is_primary_data', 'observation_joinid', 'self_reported_ethnicity', 'self_reported_ethnicity_ontology_term_id', 'sex', 'sex_ontology_term_id', 'suspension_type', 'tissue', 'tissue_ontology_term_id', 'tissue_type', 'tissue_general', 'tissue_general_ontology_term_id', 'raw_sum', 'nnz', 'raw_mean_nnz', 'raw_variance_nnz', 'n_measured_vars'
    var: 'soma_joinid', 'feature_id', 'feature_name', 'feature_type', 'feature_length', 'nnz', 'n_measured_obs', 'n_cells'

rsc.get.anndata_to_GPU 及其对应的 anndata_to_CPU 只作用于 .X.layersobsvarobsmvarm 始终留在主机内存中。默认只移动 .X;传入 convert_all=True 可同时移动所有 layer,或用 layer="<name>" 移动指定 layer 而不是 .X。传输会就地把矩阵转换为 CuPy CSR(或稠密的 cupy.ndarray),后续 RSC 调用会从设备内存读取。

rsc.get.anndata_to_GPU(adata)

7.5.1. 质量控制#

QC 指标和基因过滤在 rsc.pp 中都有 GPU 实现。至于基因家族掩码本身,我们直接在 adata.var_names 上使用普通 pandas 的 startswith:无论 .X 位于何处,这个索引都是主机侧的 pd.Index,再套一层包装器并不会带来收益。

adata.var["MT"] = adata.var_names.str.startswith("MT-")
adata.var["RIBO"] = adata.var_names.str.startswith(("RPS", "RPL"))
rsc.pp.calculate_qc_metrics(adata, qc_vars=["MT", "RIBO"])

adata = adata[adata.obs["n_genes_by_counts"] < 5000]
adata = adata[adata.obs["pct_counts_MT"] < 20]
rsc.pp.filter_genes(adata, min_cells=3)
adata.shape
(213191, 25430)

7.5.2. 归一化、HVG、缩放#

这些函数是一对一移植自 scanpy,对应前文的 归一化特征选择 章节。scanpy 的 highly_variable_genes 支持的所有 flavor= 选项(seuratseurat_v3seurat_v3_papercell_rangerpearson_residuals)都可在 GPU 上使用。RSC 还额外提供 flavor="poisson_gene_selection",这是 M3Drop 解析式泊松基因选择(analytical Poisson gene selection)的 CUDA 内核移植版本,在 scanpy 中没有对应的 CPU 实现。

rsc.pp.normalize_total(adata, target_sum=1e4)
rsc.pp.log1p(adata)
rsc.pp.highly_variable_genes(adata, n_top_genes=5000, flavor="cell_ranger")
adata.raw = adata
rsc.pp.filter_highly_variable(adata)
rsc.pp.scale(adata, max_value=10)

7.5.3. 主成分分析(PCA)、邻居、UMAP、聚类#

PCA 会根据输入类型分派到不同实现:稠密矩阵交给 cuML(zero_center=True 时使用 PCA,否则使用 TruncatedSVD),而 chunked=True 会使用 cuML 的 IncrementalPCA。对于其他输入(稀疏矩阵和 Dask 输入),RSC 使用自己的 CUDA 内核实现:feature 数 ≤8k 时走协方差特征分解路径,更宽的矩阵则走 Lanczos SVD。近邻默认使用暴力精确 KNN,同时也为超大数据集提供若干由 cuVS 支持的近似方法。UMAP 由 cuML 支持;Leiden 则通过 cuGraph 运行。

rsc.tl.pca(adata, n_comps=50)
rsc.pp.neighbors(adata, n_neighbors=15, n_pcs=40)
rsc.tl.umap(adata)
rsc.tl.leiden(adata, resolution=0.6)

7.5.4. 绘图仍交由 scanpy 完成#

RSC 刻意不重复 scanpy 的绘图 API。重计算步骤完成后,熟悉的 sc.pl.* 函数可以直接使用:obsvar 以及 obsm 中的 Embedding 已经位于主机内存中,因此按某个 obs 列或聚类标签给 UMAP 着色不需要额外传输。只有当图形需要从 .X 本身读取数值时,才需要 rsc.get.anndata_to_CPU,例如按某个基因的表达量着色,或绘制标记基因的 dotplot。序列化也是如此:write_h5adwrite_zarr 期望矩阵位于主机端,因此写入前应调用 rsc.get.anndata_to_CPU(adata);否则要么报错,要么保存出一个由 CuPy 支持的 .X,在只有 CPU 的机器上无法重新加载。

sc.pl.umap(adata, color=["leiden"], legend_loc="on data")
../_images/0139ff9aaf851252c86cb43f4e8a7b209316e9aab00a73b19dec4a6975ab18e1.png

7.6. GPU 最佳实践#

上面的流程在小数据集上几秒钟即可跑完。一旦扩大规模,下面这些习惯,正是区分高效工作流与频繁拖垮 GPU 的工作流的关键。

7.6.1. 尽量减少主机↔设备之间的传输#

每个 anndata_to_GPU / anndata_to_CPU 是一次 PCIe 传输。一个 50 GB 的稀疏矩阵需要几秒钟才能移动,很容易就主导了那些本来很快的预处理步骤。

只移动一次,把所有 GPU 工作集中在一段连续的代码块中完成,然后再移动回来一次。避免在流程中间调用仅支持 CPU 的函数,从而强制触发一次隐式的数据迁移——如果必须混用,就把 CPU 端的工作批量处理。

7.6.2. 显式释放中间结果#

Python 的垃圾回收器并不会像你期望的那样积极地把 CuPy 内存归还给内存池。在一个内存开销较大的步骤之后(rsc.pp.scale 作用于稠密数据、 rsc.pp.regress_out、在大矩阵上做 PCA 等之后),请丢弃你不再需要的层引用,并让 CuPy 把已释放的内存池块归还给驱动。

import gc

# Drop references to large intermediates first.
if "counts" in adata.layers:
    del adata.layers["counts"]
gc.collect()

# Then return any unused pool blocks to the driver.
cp.get_default_memory_pool().free_all_blocks()
cp.get_default_pinned_memory_pool().free_all_blocks()

7.6.3. 监视 VRAM 使用情况#

如果看不到设备上的情况,就很难为内存池设定合适的大小或诊断 OOM。有三种有用的查看方式:

# CuPy: what your process has allocated through the active pool.
pool = cp.get_default_memory_pool()
print(f"CuPy pool used: {pool.used_bytes() / 1e9:.2f} GB")
print(f"CuPy pool total: {pool.total_bytes() / 1e9:.2f} GB")

# CUDA: what the device reports as free vs total (across all processes).
free, total = cp.cuda.runtime.memGetInfo()
print(f"Device free: {free / 1e9:.2f} GB / {total / 1e9:.2f} GB")
CuPy pool used: 0.00 GB
CuPy pool total: 0.00 GB
Device free: 93.73 GB / 101.97 GB

从一个单独的终端 nvidia-smi --query-gpu=memory.used,memory.free,utilization.gpu --format=csv -l 1 会按进程、每秒给你一次反馈——在调试一个跑了三步后突然 OOM 的流程时,这是不可或缺的。

7.6.4. 留意 dtype(数据类型)#

来自 .h5ad 的 AnnData 对象通常是 float64。在 GPU 上, float64 会使你的内存占用翻倍,并且在消费级显卡上明显更慢(相比数据中心显卡,其 FP64 吞吐量受限)。

除非有特定的数值精度理由,否则请在移动到 GPU 之前先转换为 float32

adata.X = adata.X.astype("float32")
rsc.get.anndata_to_GPU(adata)

即便输入是 float32,RSC 在内部也大多以 float64 进行归约计算,以在方差、PCA 等计算中保持数值稳定性。因此,你既能节省内存,又不会遭遇最坏情况下的精度损失。

7.6.5. 为非确定性做好准备#

GPU 的归约默认是非确定性的,因为浮点求和的顺序取决于线程调度。逐元素的差异极小,但可能让接近临界值的 HVG 排名发生翻转,或把某个细胞移到聚类边界的另一侧。如果精确的可复现性很重要,请设置 cp.random.seed(...) 并限制为单个 CUDA 流;但要接受这样一个事实:跨硬件得到逐比特一致的结果是不现实的。

7.6.6. 使 NVIDIA 驱动程序与 CUDA 运行时相匹配#

RSC 的 wheel 包固定了特定的 CUDA 运行时版本。你的 NVIDIA 驱动至少要和该运行时一样新;过旧的驱动会在加载 CuPy 时失败,并给出有误导性的错误。请更新驱动,而不是工具包——wheel 自带其工具包。

7.6.7. 了解哪些功能尚未移植#

严重依赖 R 包的工具(SoupX、scDblFinder、scran)以及那些并非瓶颈的工具(大多数绘图)都运行在 CPU 上。预期的模式是:在 GPU 上完成瓶颈步骤,其余则回到 CPU 处理。

7.6.8. 注意共享服务器上的 MIG#

如果你在共享服务器上,请检查是否启用了 MIG(多实例 GPU,Multi-Instance GPU)。MIG 会把单块 A100/H100 切分成更小的、相互隔离的 GPU,这对公平共享很有利,但也意味着你实际可用的 VRAM 可能比主机上 nvidia-smi 所显示的要少。

7.7. 使用 dask-cuda 进行核外分析#

当数据集即使用托管内存也无法放入单块 GPU 时,RSC 会与 dask-cuda 整合,让矩阵分块流经一块或多块 GPU [Rocklin, 2015]。AnnData 的惰性读取器(anndata ≥0.12 中为 read_elem_lazy,此前为 read_elem_as_dask)会直接从 Zarr 或 HDF5 存储把 .X 加载为 Dask 数组,而不必在主机 RAM 中完整实例化。

流程很短,但每一步都很关键。

from dask.distributed import Client
from dask_cuda import LocalCUDACluster

cluster = LocalCUDACluster(
    CUDA_VISIBLE_DEVICES="0",
    threads_per_worker=8,
    protocol="ucx",  # zero-copy GPU↔GPU comms over NVLink/InfiniBand if available
    rmm_pool_size="20GB",
    rmm_maximum_pool_size="40GB",
    rmm_allocator_external_lib_list="cupy",
)
client = Client(cluster)

隐藏代码单元输出

2026-05-06 11:21:24 | [INFO] To route to workers diagnostics web server please install jupyter-server-proxy: python -m pip install jupyter-server-proxy
2026-05-06 11:21:24 | [INFO] State start
2026-05-06 11:21:24 | [INFO] Found stale lock file and directory '/tmp/dask-scratch-space/scheduler-91l8od54', purging
2026-05-06 11:21:24 | [INFO] Found stale lock file and directory '/tmp/dask-scratch-space/worker-dmwfgw5g', purging
2026-05-06 11:21:24 | [WARNING] A CUDA context for device 0 (GPU-740d12cd-9ded-6e44-a5b5-5cd90037aebe) already exists on process ID 537888. This is often the result of a CUDA-enabled library calling a CUDA runtime function before Dask-CUDA can spawn worker processes. Please make sure any such function calls don't happen at import time or in the global scope of a program.
2026-05-06 11:21:24 | [INFO]   Scheduler at:     ucx://127.0.0.1:53865
2026-05-06 11:21:24 | [INFO]   dashboard at:  http://127.0.0.1:8787/status
2026-05-06 11:21:24 | [INFO] Registering Worker plugin shuffle
[1778059284.202777] [2cab62a-lcedt:537888:0]          parser.c:2359 UCX  WARN  unused environment variable: UCX_MEMTYPE_CACHE (maybe: UCX_MEMTYPE_CACHE?)
[1778059284.202777] [2cab62a-lcedt:537888:0]          parser.c:2359 UCX  WARN  (set UCX_WARN_UNUSED_ENV_VARS=n to suppress this warning)
2026-05-06 11:21:24 | [INFO]         Start Nanny at: 'ucx://127.0.0.1:47175'
2026-05-06 11:21:25 | [INFO] Register worker addr: ucx://127.0.0.1:52107 name: 0
2026-05-06 11:21:25 | [INFO] Starting worker compute stream, ucx://127.0.0.1:52107
2026-05-06 11:21:25 | [INFO] Starting established connection to ucx://127.0.0.1:53865
2026-05-06 11:21:25 | [INFO] Receive client connection: Client-f48fc233-492c-11f1-b520-bcfce74fad1a
2026-05-06 11:21:25 | [INFO] Starting established connection to ucx://127.0.0.1:53865
from pathlib import Path

import zarr
from anndata.experimental import read_elem_lazy

# The lazy reader operates on a zarr store, so convert the cached h5ad once.
# In a real workflow you'd already have a zarr atlas on disk or in object storage.
zarr_path = Path(path).with_suffix(".zarr")
if not zarr_path.exists():
    ad.read_h5ad(path).write_zarr(zarr_path)

store = zarr.open(zarr_path)
X_lazy = read_elem_lazy(store["X"], chunks=(20_000, store["X"].attrs["shape"][1]))

adata = ad.AnnData(
    X=X_lazy,
    obs=ad.io.read_elem(store["obs"]),
    var=ad.io.read_elem(store["var"]),
)
rsc.get.anndata_to_GPU(adata)

第一次使用 Dask 路径时,最容易踩到下面几个实践细节:

大多数操作都是惰性的,但也有例外。 过滤和 log1p 会保持惰性,只是把任务排入队列。normalize_total 只有在显式传入 target_sum= 时才是惰性的;默认 target_sum=None 时,它必须计算每个细胞的总和以找出文库大小的中位数,因此会触发同步。calculate_qc_metricshighly_variable_genesscalepca 都需要对整个矩阵做完整归约,也会触发同步;可以把它们视为 Dask 流程中的自然断点。

用布尔掩码过滤,而不要用 sc.pp.filter_cells. 内置过滤辅助函数会物化 Count 来决定保留哪些细胞,从而破坏惰性执行。应当从 QC 指标中一次性算好掩码,并直接用 .copy() 应用它们,以保持 CSR 格式而非视图:

adata = adata[adata.obs["n_genes_by_counts"].between(200, 10_000)].copy()

分块大小是你必须调节的一个旋钮。 分块太小,会让调度器被海量任务淹没;分块太大,又会在同步时撑爆 VRAM。对于 24–80 GB 的 GPU,每块 20–50k 个细胞是一个合理的起点;如果某一步比预期慢,可用 Dask 仪表板进行性能分析。

7.8. 超越预处理:在 GPU 上使用 decoupler、Squidpy 和 pertpy#

RSC 还封装了来自另外三个 scverse 包的部分例程的 GPU 实现,每个都在各自的命名空间下:

  • rsc.dcg.*decoupler 功能分析(ulmmlmaucellwaggrzscore)。在百万细胞图谱上,转录因子活性推断可从 CPU 上的数小时缩短到单块 GPU 上的几分钟。除命名空间不同外,CPU 与 GPU API 完全相同;参见通路和基因集分析章节

  • rsc.gr.*Squidpy 空间分析(spatial_autocorrco_occurrenceligrec)。Moran / Geary 空间自相关以及配体–受体置换检验,是最能从 GPU 获益的例程;参见空间邻域章节了解这些方法的概念。邻域图本身仍然使用 sq.gr.spatial_neighbors 在 CPU 上构建。

  • rsc.ptg.*pertpy 扰动距离,通过 Distance 类实现。目前只移植了 edistance,更多指标也即将推出。该 GPU 实现会即时重新计算距离,而不是实例化一个 n×n 的细胞距离矩阵,因此在涉及数百种扰动的筛选中,自助法(bootstrap)方差估计变得切实可行;参见扰动建模章节

import decoupler as dc
net = dc.op.collectri(organism="human", license="academic")
rsc.dcg.ulm(adata, net=net, raw=False, bsize=10_000)

import squidpy as sq
sq.gr.spatial_neighbors(adata, coord_type="generic")  # still CPU
rsc.gr.spatial_autocorr(adata, mode="moran")

distance = rsc.ptg.Distance(metric="edistance", obsm_key="X_pca")
df = distance.pairwise(adata, groupby="perturbation")

并非整个上游 API 都被移植——RSC 专注于那些既足够耗时、值得移植到 GPU,又足够“易并行”(embarrassingly parallel)、能保持数值忠实的例程。当前的覆盖范围请查阅 RSC 的 API 参考文档。

7.9. Quiz#

你有一个 80 GB 的 AnnData 对象和一块 40 GB 的 GPU。哪种 RMM 配置是合适的?





在基于 Dask 的 RSC 流程中,以下哪个步骤会触发同步(即不是惰性执行)?





为什么在迁移到 GPU 之前应将 .X 转换为 float32?
它能将显存(VRAM)占用减半,并在 FP64 吞吐受限的消费级 GPU 上运行得更快;对于敏感的归约运算,RSC 内部仍使用 float64。

7.10. 参考文献#

[BiMVelezSB+22]

Pau Badia-i-Mompel, Jesús Vélez Santiago, Jana Braunger, Celina Geiss, Daniel Dimitrov, Sophia Müller-Dott, Petr Taus, Aurelien Dugourd, Christian H. Holland, Ricardo O. Ramirez Flores, and others. Decoupler: ensemble of computational methods to infer biological activities from omics data. Bioinformatics Advances, 2(1):vbac016, 2022.

[DHM+26]

Severin Dicks, Lukas Heumos, Lilly May, Sara Jimenez, Philipp Angerer, Ilan Gold, Isaac Virshup, Felix Fischer, Michelle Gill, Melanie Boerries, Corey J. Nolet, Tiffany J. Chen, and Fabian J. Theis. Gpu-accelerated single-cell analysis at scale with rapids-singlecell. arXiv preprint arXiv:2603.02402, 2026. URL: https://arxiv.org/abs/2603.02402, doi:10.48550/arXiv.2603.02402.

[HJM+25]

Lukas Heumos, Yuge Ji, Lilly May, Tessa D. Green, Stefan Peidli, Xinyue Zhang, Xichen Wu, Johannes Ostner, Antonia Schumacher, Karin Hrovatin, Michaela Müller, Faye Chong, Gregor Sturm, Alejandro Tejada, Emma Dann, Mingze Dong, Gonçalo Pinto, Mojtaba Bahrami, Ilan Gold, Sergei Rybakov, Altana Namsaraeva, Amir Ali Moinfar, Zihe Zheng, Eljas Roellin, Isra Mekki, Chris Sander, Mohammad Lotfollahi, Herbert B. Schiller, and Fabian J. Theis. Pertpy: an end-to-end framework for perturbation analysis. Nature Methods, 2025. URL: https://doi.org/10.1038/s41592-025-02909-7, doi:10.1038/s41592-025-02909-7.

[NGF+24]

Corey J. Nolet, Divye Gala, Alex Fender, Joe Eaton-Patrick Patro, Edward Raff, John Zedlewski, Brad Rees, and Tim Riedel. Cuml: a library for gpu accelerated machine learning. Software available from rapids.ai, 2024. URL: rapidsai/cuml.

[PSK+22]

Giovanni Palla, Hannah Spitzer, Michal Klein, David Fischer, Anna Christina Schaar, Louis Benedikt Kuemmerle, Sergei Rybakov, Ignacio L. Ibarra, Olle Holmberg, Isaac Virshup, Mohammad Lotfollahi, Sabrina Richter, and Fabian J. Theis. Squidpy: a scalable framework for spatial omics analysis. Nature Methods, 19(2):171–178, 2022. URL: https://doi.org/10.1038/s41592-021-01358-2, doi:10.1038/s41592-021-01358-2.

[Roc15]

Matthew Rocklin. Dask: parallel computation with blocked algorithms and task scheduling. Proceedings of the 14th Python in Science Conference, pages 126–132, 2015. URL: https://doi.org/10.25080/Majora-7b98e3ed-013, doi:10.25080/Majora-7b98e3ed-013.

[NVIDIARAPIDS24]

NVIDIA RAPIDS. RMM: RAPIDS memory manager. rapidsai/rmm, 2024. URL: rapidsai/rmm.

7.11. 贡献者#

我们衷心感谢以下人员的贡献:

7.11.1. 作者#

  • Lukas Heumos

7.11.2. 审阅者#

  • Severin Dicks