DeepLC 4.5.0: keep the multitask prediction matrix as its low-rank factors - #126
Merged
RobbinBouwmeester merged 2 commits intoSep 11, 2026
Merged
Conversation
FactorHead finishes with `proj(trunk) @ embedding.T * scale + shift`, so the
(n_peptides, n_tasks) matrix it returns is determined by (n_peptides, rank).
At rank 64 and 6,543 setups that is 102x smaller: a 2.6 M peptide run needs
0.6 GiB of factors instead of 63 GiB of predictions.
`predict_kwargs={"factored": True}` with `return_matrix=True` now returns a
FactoredPredictionMatrix holding those factors. It reports the same shape and
indexes the same way, evaluating only the block asked for, so a caller that
reads a few thousand rows to choose a head and then one column per run never
builds the rest. `np.asarray()` still gives the dense matrix.
Opt-in rather than the default. It was tried as the default and broke eight
tests that call ndarray methods on the result (`out.min()`, `np.isfinite(out)`),
which is exactly what it would do to callers' code: no lazy object can satisfy
the whole ndarray contract, so the choice belongs to the caller who knows
whether they slice the matrix or reduce over it.
Not available for a head fine-tuned onto one setup, which returns that column
alone and has nothing to factor, nor for single-task models.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FactorHead ends with `proj(trunk) @ embedding.T * scale + shift`, so the
(n_peptides, n_tasks) matrix it returns is determined by (n_peptides, rank). At
rank 64 and 6,543 setups that is 102x smaller: 0.6 GiB of factors instead of
63 GiB of predictions for 2.6 M peptides.
`predict(..., return_matrix=True)` now hands back a FactoredPredictionMatrix.
It reports the same shape and indexes the same way, selecting rows alone gives
another factored matrix, and `__getattr__` falls through to the dense array for
anything the factors cannot answer, so a caller that reduces over the result
rather than slicing it is unaffected. `predict_kwargs={"factored": False}`
forces the dense array.
MultiHeadCalibration.fit no longer expands a lazy source either. Head ranking
already walked the heads in blocks and every later step reads single columns,
so nothing in fit needed the matrix whole; the block size is now chosen from
the row count, which holds the peak near 64 MiB whether the reference has a
thousand rows or a million rather than growing with it.
Together these mean a caller that predicts the matrix, slices calibration rows
out of it, fits a calibration and transforms per run never materialises the
wide matrix. That is the path MS2Rescore takes, so it is fixed by upgrading
DeepLC, with no change on its side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RobbinBouwmeester
force-pushed
the
perf/factored-prediction-matrix
branch
from
September 11, 2026 11:30
dee528d to
0771e0e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #125. Ships as 4.5.0, and fixes the reported out-of-memory failure with no change on the MS2Rescore side.
The failure
MS2Rescore asks DeepLC for the whole matrix for every PSM:
At 6,543 setups that is 25.6 kB per PSM, so a 2.6 M PSM run needs 63 GiB and dies:
Change
FactorHeadends withproj(trunk) @ embedding.T * scale + shift, so the(n_peptides, n_tasks)matrix is determined by(n_peptides, rank).(n, 6543)float32(n, 64)float32predict(..., return_matrix=True)returns aFactoredPredictionMatrixholding those factors. It reports the same shape, indexes the same way, and selecting rows alone gives another factored matrix, which is what keeps a per-run slice cheap. Anything the factors cannot answer falls through to the dense array via__getattr__, soout.min()andnp.isfinite(out)behave exactly as before.predict_kwargs={"factored": False}forces the dense array.Second half, without which the first is not enough:
MultiHeadCalibration.fitno longer expands a lazy source. Head ranking already walked the heads in blocks and every later step reads single columns, so nothing infitneeded the matrix whole. Block size is now chosen from the row count, so the peak stays near 64 MiB regardless of reference size:That matters because MS2Rescore's default
calibration_set_size=Nonecalibrates on every target PSM at q <= 0.01, which can be most of the run.Verified against the MS2Rescore path
Replaying ms2rescore 4.0.2's feature generator against this build, with every PSM calibrating (the worst case), instrumented to record every densification:
It still selects the correct head and calibrates correctly.
Why the default, after all
An earlier revision made this opt-in because returning a non-ndarray broke eight tests that call
out.min(). The__getattr__fallback removes that objection: the result answers everything an array does, and is only cheap where it can be. All 246 tests pass with it on by default.Requires ms2rescore >= 4.0.2
4.0.1 imports
deeplc.core._best_correlating_head, removed in 4.3.0. 4.0.2 already guards that import and takes theMultiHeadRidgeCalibrationpath, which is the one this PR makes lazy. So the deployment fix is: upgrade DeepLC to 4.5.0, and ms2rescore to 4.0.2 if not already there. No code change in either.Tests
tests/test_factored.py, 16 cases. Seven indexing forms parametrised against the dense matrix from the same model, agreeing to 7.6e-5 min (float32 accumulation order). Plus row slices staying factored, array-method fallback,np.asarrayround-trip,is_head_sourcepassthrough, both non-factorable cases (single-task, and a head fine-tuned withadd_task), and rank mismatch rejected at construction.246 pass.
ruff checkandruff format --checkclean on every file touched.🤖 Generated with Claude Code