perf: cache loaded feature matrices between splits and models - #467
Open
jfrog64 wants to merge 2 commits into
Open
perf: cache loaded feature matrices between splits and models#467jfrog64 wants to merge 2 commits into
jfrog64 wants to merge 2 commits into
Conversation
Feature matrices were reloaded from disk for every split, every hyperparameter setting and, for single drug models, for every drug. On a 545-drug CTRPv2 run this dominated the wall clock: 3270 loads at roughly 17.8 s each, about 16 h of an 18.4 h run. The matrices only depend on the model class, the feature kind, the data path, the dataset name and the hyperparameters (which decide the views and the gene list), so they are cached under exactly that key. The cache holds at most four entries, since an entry is a full feature matrix, and can be disabled with DREVAL_FEATURE_CACHE=0 to A/B check that results are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… state SparseGO's load_cell_line_features does not only return features, it also builds the ontology structure (layer_connections, gene2id_mapping_ont, ontology_gene_order, gene_dim_input) that train() and predict() need. The cache skips the loader for every instance after the first, so a fresh instance per CV split would have been left half built and train() would have raised "layer_connections or gene2id_mapping_ont are not set". DRPModel now carries supports_feature_caching (default True); SparseGO sets it to False and its features are loaded per instance again. A test walks MODEL_FACTORY and asserts that every model whose feature loaders assign to self has opted out, so the next such model cannot slip through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## development #467 +/- ##
===============================================
+ Coverage 80.34% 82.22% +1.87%
===============================================
Files 101 121 +20
Lines 8171 9927 +1756
===============================================
+ Hits 6565 8162 +1597
- Misses 1606 1765 +159 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
PR Checklist for all PRs
docsis updated —runyourmodel.rstdocuments the cache and the opt-out for model authorsChanges
Bug fixes
New features
The pipeline reloads the same feature matrices over and over.
load_featuresloads them once per split,train_and_predictloads them again whenever it is called without prepared features, andtrain_final_modelloads them once more. For single drug models the model is re-instantiated per drug and per split, so the
identical cell line matrix is read from disk for every one of them.
What that costs, measured on a CTRPv2 run with 545 drugs and 893 genes (
ElasticNet, single drug mode,5 splits): 17.8 s per load, 3270 loads, ~16 h of an 18.4 h run — 88 % of the wall clock was feature I/O,
and the expression matrix was re-read 6× per drug. After caching, the marginal cost per drug drops to ~3.3 s
and the projected runtime for the same experiment is ~2.5 h.
_load_features_cachedtherefore keeps loaded matrices in a process-level dict, keyed by everything theydepend on: model class name, feature kind, data path, dataset name, and the model's hyperparameters
(
json.dumps(..., sort_keys=True), so the key does not depend on dict order). The hyperparameters are part ofthe key because they decide which views and which gene list a model loads;
build_modelalways runs beforethe loaders, at all three call sites.
Why handing out the same object is safe. The consumers in
experiment.pyonly ever read.identifiersand
.view_namesfrom the returned dataset and pass.copy()to models,randomize_featuresandcross_study_prediction.FeatureDataset.copy()is a deep copy (copy.deepcopyoffeaturesandmeta_info), so nothing a model does can reach the cached object. The three in-place mutators in the codebaseeither copy first (
prepare_expression_and_methylation,prepare_proteomics) or only ever see a copy(DIPK's
cell_line_input.apply). I verified this end to end on a real experiment before proposing it: over1019 cell line views, the cached object was byte-identical to a fresh load (max |Δ| = 0), both directly after
loading and after a complete 2-drug, 5-split run including the final model; the number of real loads went from
12 to 0.
One class of model cannot use it, and that is now explicit.
SparseGOModel.load_cell_line_featuresdoesnot only return features, it also builds the ontology structure (
layer_connections,gene2id_mapping_ont,ontology_gene_order,gene_dim_input) thattrainandpredictneed. Sincemodel = model_class()createsa fresh instance per split, serving the second split's features from the cache would skip that initialization
and
trainwould raise "layer_connections or gene2id_mapping_ont are not set".DRPModeltherefore gets aclass attribute
supports_feature_caching(defaultTrue); SparseGO sets it toFalseand its loaders runfor every instance, exactly as before this PR. The cleaner long-term fix is to move the ontology construction
out of the loader, but that is a SparseGO refactor and does not belong in this PR.
Bounds and escape hatches.
_FEATURE_CACHE_MAXSIZE = 4entries and evicts in insertion order (FIFO, not LRU —the access pattern is "same key many times in a row", so recency reordering buys nothing). The bound is on
the number of entries, not on bytes, so up to four full matrices can be resident.
clear_feature_cache()is public for callers that want the memory back between datasets; the pipeline itselfnever calls it.
DREVAL_FEATURE_CACHE=0disables it completely, which is how one can A/B check that results are unchanged.Tests:
tests/test_feature_cache.py_FEATURE_CACHE_MAXSIZEand the evicted dataset is loaded againDREVAL_FEATURE_CACHE=0restores the previous load-every-time behaviour and leaves the cache emptyload_features, the entry point the pipeline uses, goes through the cachesupports_feature_caching = Falseloads for every instance, its loader's state is set on eachof them, and nothing is cached
MODEL_FACTORYand asserts that every model whose feature loaders assign toselfhasopted out — so the next stateful loader cannot silently break
Maintenance