Skip to content

Commit 0bb3beb

Browse files
authored
Implement workaround for kernel ridge regression running out of memory (#79)
* Solve guanlab_dengkw_pm KRR by Cholesky * Explain guanlab_dengkw_pm KRR workaround
1 parent 6291e14 commit 0bb3beb

2 files changed

Lines changed: 29 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@
124124

125125
* `scipenn`: Exit 99 (non-applicable) instead of raising a `ValueError` on anything other than GEX -> ADT (PR #77).
126126

127+
* `guanlab_dengkw_pm`: Solve the kernel ridge regression with `scipy.linalg.cho_factor()`/`cho_solve()` instead of `KernelRidge`, whose `scipy.linalg.solve()` raised a `MemoryError` or segfaulted beyond ~30k cells with the OpenBLAS in this image. Predictions are unchanged; the method failed on every dataset except `bmmc_multiome`.
128+
127129
# task_predict_modality 0.1.1
128130

129131
## NEW FUNCTIONALITY

‎src/methods/guanlab_dengkw_pm/script.py‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import sys
22
import anndata as ad
33
import numpy as np
4+
from scipy.linalg import cho_factor, cho_solve
45
from scipy.sparse import csc_matrix
56
from sklearn.decomposition import TruncatedSVD
67
from sklearn.gaussian_process.kernels import RBF
7-
from sklearn.kernel_ridge import KernelRidge
88
from threadpoolctl import threadpool_limits
99

1010
## VIASH START
@@ -29,6 +29,27 @@
2929
if meta.get('cpus'):
3030
threadpool_limits(meta['cpus'])
3131

32+
# Workaround for sklearn's KernelRidge: this computes
33+
# KernelRidge(alpha=alpha, kernel=kernel).fit(X, y).predict(X_test), but solves the system itself.
34+
#
35+
# KernelRidge solves (K + alpha * I) w = y with scipy.linalg.solve(assume_a="pos"). In this image
36+
# (scipy 1.17 and 1.18 with OpenBLAS) that call raises a MemoryError or segfaults once a half of the
37+
# batches holds more than ~30-40k cells, no matter how much memory the job gets (200 GB was not enough
38+
# for 40k cells, which need ~13 GB). So the method failed on every dataset except bmmc_multiome.
39+
#
40+
# K + alpha * I is symmetric positive definite, so solve() does a Cholesky factorisation anyway.
41+
# cho_factor()/cho_solve() run that same factorisation directly and handle 46k cells in about a minute;
42+
# the predictions are identical to KernelRidge's. They also factorise K in place, whereas KernelRidge
43+
# copies it first, which saves one n x n float64 matrix of peak memory.
44+
#
45+
# Switch back to KernelRidge once scipy.linalg.solve() handles matrices of this size again.
46+
def kernel_ridge_predict(kernel, alpha, X, y, X_test):
47+
K = kernel(X)
48+
K.flat[::K.shape[0] + 1] += alpha
49+
# K is symmetric, so its transpose is a Fortran-ordered view LAPACK can factorise in place
50+
dual_coef = cho_solve(cho_factor(K.T, overwrite_a=True), y)
51+
return kernel(X_test, X) @ dual_coef
52+
3253

3354
## Removed PCA and normalization steps, as they arr already performed with the input data
3455
print('Reading input files', flush=True)
@@ -118,13 +139,14 @@
118139

119140
print(batch, flush=True)
120141
kernel = RBF(length_scale = scale)
121-
krr = KernelRidge(alpha=alpha, kernel=kernel)
122142
print('Fitting KRR ... ', flush=True)
123-
krr.fit(
143+
y_pred_batch = kernel_ridge_predict(
144+
kernel,
145+
alpha,
124146
train_norm[input_train_mod1.obs.batch.isin(batch)],
125-
train_gs[input_train_mod2.obs.batch.isin(batch)]
147+
train_gs[input_train_mod2.obs.batch.isin(batch)],
148+
test_norm
126149
)
127-
y_pred_batch = krr.predict(test_norm)
128150
if embedder_mod2 is not None:
129151
# map the predicted components back to the mod2 feature space
130152
y_pred_batch = y_pred_batch @ embedder_mod2.components_

0 commit comments

Comments
 (0)