Performance: exact mode, fast mode and the alternative solver¶
ecotyper-py has three ways to run discovery and recovery. By default it reproduces R’s results bit for bit, and two opt-in modes trade that exactness for speed.
Mode |
How to select |
Equal to R? |
Use it for |
|---|---|---|---|
exact (default) |
|
same arithmetic as R, file for file (see Validation) |
results you want to compare with, or substitute for, R EcoTyper |
fast |
|
same algorithm; numbers differ from R by about 1e-10 relative; discrete results identical on all tutorials |
large discovery runs when bit-identity with R is not needed |
alternative solver |
|
no: a different NMF objective |
experiments; not EcoTyper-equivalent |
Exact mode¶
The default mode emulates R wherever R’s floating-point results depend on the order of operations. That covers the NMF package’s C++ update kernels (with or without fused multiply-adds, matching how R was compiled), x87 long-double sums on x86-64, R’s 15-digit number formatting, and the text round trips between R scripts. Several optimizations keep these semantics while running faster:
Vectorised NMF kernel (
_nmfk.c, compiled on first use with the system C compiler). The columns of H (and the rows of W) are independent within one Brunet half-step. The kernel processes 16–64 of them together, so the compiler can use SIMD instructions without reordering any sum. Every element still gets exactly R’s sequence of operations, so the output is bit-identical to the scalar kernel. This is tested on random inputs, in both FMA modes, on Apple silicon and x86-64. The kernel is 2.5–3.6× faster on a Xeon (AVX-512) and 3–5× faster on Apple silicon. With OpenMP (gcc), the column blocks of one NMF can also be split across threads, and the result does not depend on the thread count. Recovery uses this to give idle cores to the largest cell types.Number formatting in C (
rcompat/_fmt.c): R’sformatRealfor whole matrices, which produces the same strings as the Python implementation.Scheduling: each NMF restart is its own task, and tasks are submitted longest first, so the slowest runs do not start last. Rank metrics and the per-cell-type cross-correlations of scRNA-seq discovery run in parallel. The Wilcoxon ranks of the cell-type-specific gene test are computed once for all cell types, which gives identical p-values.
Memory: workers receive only the slice of the data they need (the cell type’s cells and the model genes), not the whole expression matrix.
ECOTYPER_NMF_KERNEL=numba and ECOTYPER_FMT=python switch back to the pure numba/Python
implementations, for example to cross-check results.
Fast mode (exact=False)¶
model = et.discovery(adata, exact=False, n_jobs=32)
et.recovery(adata_bulk, model, exact=False)
Fast mode runs the same algorithm: the same random initialisation (R’s seeds), Brunet multiplicative updates, the eps clamp every 10 iterations, and the same connectivity-based stopping rule. The updates are computed with BLAS matrix products, though, and the intermediate values skip R’s 15-digit text round trips. Sums are accumulated in a different order, so W and H differ from R’s in the last bits. An NMF run can therefore stop a few iterations earlier or later, but it converges to the same partition.
Validation against R on x86-64 (all output files of each tutorial):
Tutorial |
Files |
Byte-identical |
Discrete-equal |
Missing |
Max rel. diff |
|---|---|---|---|---|---|
T1 · Bulk recovery, lung |
175 |
103 |
175 |
0 |
9.4e-13 |
T1 · Bulk recovery, lymphoma |
189 |
119 |
189 |
0 |
3.5e-13 |
T2 · scRNA recovery, lymphoma |
13 |
10 |
13 |
0 |
1.1e-13 |
T2 · scRNA recovery, CRC |
150 |
113 |
150 |
0 |
2.3e-13 |
T3 · Visium recovery |
103 |
64 |
103 |
0 |
4.8e-13 |
T4 · Bulk discovery |
608 |
300 |
608 |
0 |
1.2e-10 |
T5 · scRNA discovery |
762 |
375 |
690 |
0 |
inf |
T6 · Presorted discovery |
608 |
300 |
608 |
0 |
1.2e-10 |
In tutorials 1–4 and 6, all discrete outputs agree with R: chosen ranks, state and ecotype assignments, filtered genes and states.
Tutorial 5 (scRNA-seq discovery, 11 cell types, up to 9 states) is the exception. The chosen ranks are the same as R’s for every cell type, but for 5 of the 11 cell types the consensus over NMF restarts lands on a different partition of the cells into initial states (adjusted Rand index against R 0.70 to 0.97; the other 6 cell types are identical). Downstream files (state-specific genes, filtered states, ecotypes) then differ for those cell types. NMF has many local optima, and with 50 restarts of a large matrix the last-bit differences of the BLAS updates are enough to move a restart to a different optimum. This is a property of the fast mode, not a bug: the exact mode reproduces all 762 files of tutorial 5 as discrete-equal. Therefore nothing guarantees identical assignments in fast mode on other data, and samples or cells close to a decision boundary can flip. Use the exact mode when results must match R.
Alternative NMF solver (nmf_method="cd")¶
nmf_method="cd" replaces EcoTyper’s NMF (Kullback–Leibler divergence, Brunet multiplicative
updates) in discovery with Frobenius-loss NMF by coordinate descent (scikit-learn’s
NMF(solver="cd"); install with pip install "ecotyper-py[sklearn]"). It starts from the same
random initialisation and stops by scikit-learn’s tolerance rule. The rest of the pipeline is
unchanged: consensus over restarts, cophenetic rank selection, state filters and ecotypes.
This mode is an extension, not EcoTyper. The objective is different, so states, ranks and ecotypes can differ from R’s. Agreement with R on the tutorials, as adjusted Rand index (ARI) of the assignments and chosen ranks:
Tutorial |
Cell type |
Rank (R) |
Rank (cd) |
ARI states |
ARI initial states |
|---|---|---|---|---|---|
T4 · Bulk discovery |
B.cells |
8 |
2 |
0.30 |
0.13 |
T4 · Bulk discovery |
CD4.T.cells |
7 |
8 |
0.66 |
0.30 |
T4 · Bulk discovery |
CD8.T.cells |
6 |
4 |
0.29 |
0.38 |
T4 · Bulk discovery |
Dendritic.cells |
9 |
19 |
0.34 |
0.30 |
T4 · Bulk discovery |
Endothelial.cells |
8 |
2 |
0.29 |
0.08 |
T4 · Bulk discovery |
Epithelial.cells |
16 |
3 |
-0.01 |
0.05 |
T4 · Bulk discovery |
Fibroblasts |
13 |
2 |
0.13 |
0.03 |
T4 · Bulk discovery |
Mast.cells |
4 |
16 |
0.03 |
0.21 |
T4 · Bulk discovery |
Monocytes.and.Macrophages |
10 |
3 |
0.12 |
0.18 |
T4 · Bulk discovery |
PCs |
8 |
13 |
0.29 |
0.23 |
T4 · Bulk discovery |
PMNs |
5 |
8 |
0.32 |
0.28 |
T4 · Bulk discovery |
(ecotypes) |
7 |
3 |
0.20 |
– |
T5 · scRNA discovery |
B.cells |
7 |
2 |
0.15 |
0.10 |
T5 · scRNA discovery |
CD4.T.cells |
4 |
2 |
0.12 |
0.12 |
T5 · scRNA discovery |
CD8.T.cells |
7 |
3 |
0.06 |
0.06 |
T5 · scRNA discovery |
Dendritic.cells |
7 |
2 |
0.13 |
0.06 |
T5 · scRNA discovery |
Endothelial.cells |
9 |
3 |
0.47 |
0.27 |
T5 · scRNA discovery |
Epithelial.cells |
6 |
4 |
0.36 |
0.34 |
T5 · scRNA discovery |
Fibroblasts |
8 |
15 |
0.33 |
0.24 |
T5 · scRNA discovery |
Mast.cells |
5 |
2 |
0.14 |
0.14 |
T5 · scRNA discovery |
Monocytes.and.Macrophages |
7 |
3 |
0.47 |
0.21 |
T5 · scRNA discovery |
NK.cells |
7 |
12 |
0.03 |
0.03 |
T5 · scRNA discovery |
PCs |
6 |
17 |
0.34 |
0.18 |
T5 · scRNA discovery |
(ecotypes) |
8 |
4 |
0.34 |
– |
T6 · Presorted discovery |
B.cells |
8 |
2 |
0.30 |
0.13 |
T6 · Presorted discovery |
CD4.T.cells |
7 |
8 |
0.66 |
0.30 |
T6 · Presorted discovery |
CD8.T.cells |
6 |
4 |
0.29 |
0.38 |
T6 · Presorted discovery |
Dendritic.cells |
9 |
19 |
0.34 |
0.30 |
T6 · Presorted discovery |
Endothelial.cells |
8 |
2 |
0.29 |
0.08 |
T6 · Presorted discovery |
Epithelial.cells |
16 |
3 |
-0.01 |
0.05 |
T6 · Presorted discovery |
Fibroblasts |
13 |
2 |
0.13 |
0.03 |
T6 · Presorted discovery |
Mast.cells |
4 |
16 |
0.03 |
0.21 |
T6 · Presorted discovery |
Monocytes.and.Macrophages |
10 |
3 |
0.12 |
0.18 |
T6 · Presorted discovery |
PCs |
8 |
13 |
0.29 |
0.23 |
T6 · Presorted discovery |
PMNs |
5 |
8 |
0.32 |
0.28 |
T6 · Presorted discovery |
(ecotypes) |
7 |
3 |
0.20 |
– |
On these data the cd solver does not reproduce R’s partitions: chosen ranks differ for almost every cell type and the ARI of the final states against R is mostly 0.0-0.5 (the best case, CD4 T cells in T4/T6, is 0.66). It is faster on discovery (T4/T6 Python time about 30 s, tutorial 5 about 34 min wall with 32 jobs on the shared server), but it is a different model, so treat its output as exploratory and use the exact mode for anything compared with R.
Memory¶
Peak memory is reported as the summed PSS (proportional set size) of all processes. Pages
that forked workers share with the parent are counted once. Summed RSS counts them once per
worker, which overstates the real use when the parent holds a large matrix.
ECOTYPER_MP_CONTEXT=forkserver (or spawn) starts workers without the parent’s heap; then
RSS and PSS agree.
Environment variables¶
Variable |
Effect |
|---|---|
|
emulate R builds without fused multiply-adds (x86-64 Linux R) |
|
R’s long-double reductions off/on (default: on where long double is 80-bit) |
|
use the numba NMF kernel instead of the C kernel (same results) |
|
use the Python number formatter (same results) |
|
worker start method: |
|
where compiled C kernels are cached (default: the temp directory) |