Seurat包中doParallel与future并行配置及HPC资源优化问题
Seurat嵌套并行场景调度与算力优化问题
问题背景
根据Seurat官方指南文档说明,可借助future包执行future::plan("multiprocess", workers = 4)实现FindMarkers()系列函数的运行加速。
当前需要开展模拟分析:在执行doParallel::registerDoParallel(numCores=10)注册并行后端后,于doParallel::foreach()循环内部调用FindAllMarkers()函数,存在以下3个技术疑问:
- 该嵌套运行场景下,底层实际的并行调度机制是怎样的?
- 该配置场景下如何充分释放HPC(高性能计算集群)的算力潜力?
- 该任务需分配多少个CPU核心,才可实现并行运行效率最大化?
测试用pbmc.rds为Seurat官方示例PBMC数据集,最小可复现代码如下:
library(Seurat) # 为`FindAllMarkers()`开启并行 library(future) plan("multiprocess", workers = 4) # 为`foreach()`循环开启并行 library(doParallel) registerDoParallel(cores = 10) pbmc <- readRDS("pbmc.rds") rst <- foreach(i = 1:10/10, .combine = "cbind") %doPar% { pbmc <- FindClusters(pbmc, resolution = i) # 是否应该把future的plan设置放在循环内部? # plan("multiprocess", workers = 4) DEgenes <- FindAllMarkers(pbmc) write.csv(DEgenes, paste0("DEgenes_resolution_", i, "csv")) pbmc$seurat_clusters }
问题解答
1. 嵌套并行的底层调度逻辑
你当前写的代码会触发严重的资源过载,根本不会按预期的10*4=40核调度,实际逻辑如下:
- 全局环境配置的
future::plan()无法自动继承到doParallel派生的worker子进程。foreach默认派生的10个worker是完全独立的R会话,不会加载全局环境的future并行配置,这种情况下每个worker内运行FindAllMarkers()时默认是单线程执行,根本不会触发你预期的内层并行。 - 如果把
plan("multiprocess", workers = 4)放到foreach循环内部,每个foreach worker都会独立派生4个future子进程,总进程数会直接冲到10*4=40,一旦超过你实际分配的物理核心数,会触发大量CPU上下文切换、资源抢占,运行速度反而比单线程更慢。 - 额外注意:Seurat内置的
FindClusters()本身是单线程函数,不存在嵌套并行问题,只有FindMarkers()/FindAllMarkers()会调用future框架实现并行。
2. HPC算力释放的正确配置方式
禁止做两层嵌套并行,二选一配置单层并行即可,可根据自己的任务场景选方案:
- 方案一(更推荐,IO开销更小):外层foreach不用并行,只给future分配你申请的所有核心,循环串行跑不同分辨率的差异分析。这个方案的优势是不需要在多个worker之间拷贝整个pbmc对象,内存占用低,没有跨进程通信开销。
- 方案二(适合分辨率参数多、单分辨率差异分析耗时短的场景):外层foreach开并行,循环内部强制关闭future的多线程,每个foreach worker独占固定核心,单线程跑
FindAllMarkers。这个方案要注意给每个worker分配足够的内存,Seurat对象拷贝的内存开销是随核心数线性上涨的。 - 不管选哪种方案,都要在HPC提交任务的时候绑定物理核心,不要用系统超线程拆分出来的逻辑核,R的并行计算对逻辑核的利用率极低,反而会拖慢运行速度。另外提前配置
options(future.globals.maxSize = Inf),关掉future的全局变量大小限制,避免运行中报对象大小超限的错误。
3. 最优核心数计算规则
核心数分配完全匹配你选的并行方案,绝对不要超额分配:
- 选方案一(future并行,foreach串行):总核心数 = 你给future设置的worker数,数值和你申请的物理核心数完全一致即可,不要多设。比如申请16个物理核,Linux环境下就设
plan("multicore", workers = 16)(multicore比multiprocess快,没有PSOCK集群的通信开销)。 - 选方案二(foreach并行,future单线程):总核心数 = 你给foreach注册的核心数,每个核心对应跑一个分辨率的任务,循环内部设
plan("sequential")关闭future并行,总核心数不要超过你需要跑的分辨率参数个数(比如只跑10个分辨率,注册10个核就够,多开核心只会空等)。 - 只要出现“外层N核+内层M核”的嵌套配置,总进程数超过申请的物理核心数,运行效率就会指数级下降。
修正后的可运行代码(方案二,适合多分辨率批量跑的场景)
library(Seurat) library(future) # 全局先设为顺序模式,避免worker继承全局并行配置 plan("sequential") options(future.globals.maxSize = Inf) library(doParallel) # 这里的cores数和你申请的HPC物理核心数一致,比如申请10核就设10 cl <- makeCluster(10) registerDoParallel(cl) # 把需要的包加载到所有集群worker clusterEvalQ(cl, { library(Seurat) library(future) plan("sequential") }) # 把分析对象导出到集群worker clusterExport(cl, "pbmc") pbmc <- readRDS("pbmc.rds") rst <- foreach(i = seq(0.1, 1, 0.1), .combine = "cbind", .packages = c("Seurat","future")) %dopar% { # 强制每个worker单线程跑差异分析 plan("sequential") tmp_obj <- FindClusters(pbmc, resolution = i) DEgenes <- FindAllMarkers(tmp_obj) write.csv(DEgenes, paste0("DEgenes_resolution_", i, ".csv"), row.names = FALSE) tmp_obj$seurat_clusters } stopCluster(cl)
注:如果你的分辨率参数少于你申请的核心数,直接选方案一即可,代码更简单,速度也更快:
library(Seurat) library(future) # workers数值和申请的物理核心数一致 plan("multicore", workers = 10) options(future.globals.maxSize = Inf) pbmc <- readRDS("pbmc.rds") rst <- list() for(i in seq(0.1,1,0.1)){ tmp_obj <- FindClusters(pbmc, resolution = i) DEgenes <- FindAllMarkers(tmp_obj) write.csv(DEgenes, paste0("DEgenes_resolution_", i, ".csv"), row.names = FALSE) rst[[as.character(i)]] <- tmp_obj$seurat_clusters } rst <- do.call(cbind, rst)
内容的提问来源于stack exchange,提问作者yuw444
相关产品推荐
相关产品推荐

