This vignette shows how to speed up RunCanek() with the ncores parameter, which parallelizes the mutual nearest neighbor (MNN) search. We use the same ifnb dataset as Seurat’s own integration tutorial — stimulated vs. control PBMCs — so if you’ve worked through that one, this data will look familiar. For a general walkthrough of RunCanek() itself, see the main Seurat vignette; this one focuses specifically on ncores.

Install the data

ifnb is loaded from the SeuratData package rather than CRAN, so it needs a one-time install the first time you use it:

if (!requireNamespace("SeuratData", quietly = TRUE)) {
  install.packages("remotes")
  remotes::install_github("satijalab/seurat-data")
}
SeuratData::InstallData("ifnb")
library(Canek)
library(Seurat)
#> Loading required package: SeuratObject
#> Loading required package: sp
#> 
#> Attaching package: 'SeuratObject'
#> The following objects are masked from 'package:base':
#> 
#>     intersect, t
library(SeuratData)
#> ── Installed datasets ──────────────────────────────── SeuratData v0.2.2.9002 ──
#> ✔ ifnb  3.1.0                           ✔ panc8 3.0.2
#> ────────────────────────────────────── Key ─────────────────────────────────────
#> ✔ Dataset loaded successfully
#> ❯ Dataset built with a newer version of Seurat than installed
#> ❓ Unknown version of Seurat installed

Load and preprocess

Standard log-normalization workflow, same preprocessing as the main Seurat vignette.

ifnb <- LoadData("ifnb")
#> Validating object structure
#> Updating object slots
#> Ensuring keys are in the proper structure
#> Warning: Assay RNA changing from Assay to Assay
#> Ensuring keys are in the proper structure
#> Ensuring feature names don't have underscores or pipes
#> Updating slots in RNA
#> Validating object structure for Assay 'RNA'
#> Object representation is consistent with the most current Seurat version
#> Warning: Assay RNA changing from Assay to Assay5
ifnb[["RNA"]] <- split(ifnb[["RNA"]], f = ifnb$stim)

ifnb <- NormalizeData(ifnb, verbose = FALSE)
ifnb <- FindVariableFeatures(ifnb, verbose = FALSE)
ifnb <- ScaleData(ifnb, verbose = FALSE)
ifnb <- RunPCA(ifnb, verbose = FALSE)

The ncores parameter

Finding MNN pairs requires two independent k-nearest-neighbor searches — one from reference to query, one from query to reference. Neither depends on the other’s result, so they can run at the same time instead of one after the other. Setting ncores > 1 does exactly that, via parallel::mclapply().

ncores defaults to 1 (fully sequential) — parallelism is opt-in only, never automatic. Canek never tries to detect how many cores are “available” and use them on its own to avoid any issues when using a shared cluster or HPC job. Silently using them could oversubscribe your allocation and could affect other jobs sharing the same node. Set ncores explicitly to however many cores you know are free to use.

Two things ncores > 1 checks for you, rather than silently doing something unexpected:

  • Windows: parallel::mclapply() relies on fork(), which Windows doesn’t support. Requesting ncores > 1 there raises an error rather than quietly running sequentially anyway and leaving you wondering why it isn’t any faster.
  • Requesting more cores than exist: if ncores exceeds parallel::detectCores(), RunCanek() errors instead of silently reducing it to whatever’s available.

Timing comparison

set.seed(42)
system.time(
  ifnb <- RunCanek(ifnb, "stim")
)
#>    user  system elapsed 
#>  25.965   0.120  21.738
set.seed(42)
system.time(
  ifnb <- RunCanek(ifnb, "stim", ncores = 3, integration.name = "CanekParallel")
)
#> Warning: Key 'Canek_' taken, using 'canekparallel_' instead
#>    user  system elapsed 
#>  24.511   0.925  14.221

How much this actually helps depends on how much of the total runtime is spent in the parallelized MNN search versus everything else (clustering, fuzzy correction, etc.).

Same result, just faster

Parallelizing the two searches doesn’t change what they compute. unname() clears each reduction’s column names before comparing: Seurat renames a reduction’s key prefix when two reductions would otherwise share one (here, "Canek_" becomes "canekparallel_" for the second run), which would otherwise make all.equal() return FALSE even though every number in the embeddings matches.

isTRUE(all.equal(
  unname(Embeddings(ifnb, "canek")),
  unname(Embeddings(ifnb, "canekparallel"))
))
#> [1] TRUE

Session info

sessionInfo()
#> R version 4.4.3 (2025-02-28)
#> Platform: x86_64-conda-linux-gnu
#> Running under: Ubuntu 22.04.2 LTS
#> 
#> Matrix products: default
#> BLAS/LAPACK: /home/mloza/miniconda3/envs/canek-check/lib/libopenblasp-r0.3.33.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: Asia/Tokyo
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] panc8.SeuratData_3.0.2 ifnb.SeuratData_3.1.0  SeuratData_0.2.2.9002 
#> [4] Seurat_5.5.1           SeuratObject_5.4.0     sp_2.2-1              
#> [7] Canek_0.3.1           
#> 
#> loaded via a namespace (and not attached):
#>   [1] deldir_2.0-4           pbapply_1.7-4          gridExtra_2.3.1       
#>   [4] rlang_1.3.0            magrittr_2.0.5         RcppAnnoy_0.0.23      
#>   [7] otel_0.2.0             spatstat.geom_3.8-1    matrixStats_1.5.0     
#>  [10] ggridges_0.5.7         compiler_4.4.3         flexmix_2.3-20        
#>  [13] png_0.1-9              vctrs_0.7.3            reshape2_1.4.5        
#>  [16] stringr_1.6.0          crayon_1.5.3           pkgconfig_2.0.3       
#>  [19] fastmap_1.2.0          promises_1.5.0         numbers_0.9-2         
#>  [22] purrr_1.2.2            xfun_0.60              modeltools_0.2-24     
#>  [25] bluster_1.16.0         jsonlite_2.0.0         goftest_1.2-3         
#>  [28] later_1.4.8            spatstat.utils_3.2-4   fpc_2.2-14            
#>  [31] BiocParallel_1.40.0    irlba_2.3.7            parallel_4.4.3        
#>  [34] prabclus_2.3-5         cluster_2.1.8.2        R6_2.6.1              
#>  [37] ica_1.0-3              spatstat.data_3.1-9    stringi_1.8.7         
#>  [40] RColorBrewer_1.1-3     reticulate_1.46.0      spatstat.univar_3.2-0 
#>  [43] parallelly_1.48.0      lmtest_0.9-40          diptest_0.77-2        
#>  [46] scattermore_1.2        Rcpp_1.1.2             knitr_1.51            
#>  [49] tensor_1.5.1           future.apply_1.20.2    zoo_1.8-15            
#>  [52] sctransform_0.4.3      FNN_1.1.4.1            httpuv_1.6.17         
#>  [55] Matrix_1.7-5           splines_4.4.3          nnet_7.3-20           
#>  [58] igraph_2.3.3           tidyselect_1.2.1       abind_1.4-8           
#>  [61] spatstat.random_3.4-5  spatstat.explore_3.8-0 codetools_0.2-20      
#>  [64] miniUI_0.1.2           listenv_1.0.0          plyr_1.8.9            
#>  [67] lattice_0.22-9         tibble_3.3.1           shiny_1.14.0          
#>  [70] S7_0.2.2               ROCR_1.0-12            evaluate_1.0.5        
#>  [73] Rtsne_0.17             future_1.70.0          fastDummies_1.7.6     
#>  [76] survival_3.8-9         polyclip_1.10-7        fitdistrplus_1.2-6    
#>  [79] mclust_6.1.3           kernlab_0.9-33         pillar_1.11.1         
#>  [82] KernSmooth_2.23-26     stats4_4.4.3           plotly_4.12.0         
#>  [85] generics_0.1.4         RcppHNSW_0.7.0         S4Vectors_0.44.0      
#>  [88] ggplot2_4.0.3          scales_1.4.0           globals_0.19.1        
#>  [91] xtable_1.8-8           class_7.3-23           glue_1.8.1            
#>  [94] lazyeval_0.2.3         tools_4.4.3            BiocNeighbors_2.0.0   
#>  [97] robustbase_0.99-7      data.table_1.18.4      RSpectra_0.16-2       
#> [100] RANN_2.6.2             dotCall64_1.2          cowplot_1.2.0         
#> [103] grid_4.4.3             tidyr_1.3.2            nlme_3.1-169          
#> [106] patchwork_1.3.2        cli_3.6.6              rappdirs_0.3.4        
#> [109] spatstat.sparse_3.2-0  spam_2.11-4            viridisLite_0.4.3     
#> [112] dplyr_1.2.1            uwot_0.2.4             gtable_0.3.6          
#> [115] DEoptimR_1.2-0         digest_0.6.39          progressr_1.0.0       
#> [118] BiocGenerics_0.52.0    ggrepel_0.9.8          htmlwidgets_1.6.4     
#> [121] farver_2.1.2           htmltools_0.5.9        lifecycle_1.0.5       
#> [124] httr_1.4.8             mime_0.13              MASS_7.3-66