perf: cache loaded feature matrices between splits and models - #467
perf: cache loaded feature matrices between splits and models#467jfrog64 wants to merge 3 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% 83.11% +2.76%
===============================================
Files 101 128 +27
Lines 8171 10433 +2262
===============================================
+ Hits 6565 8671 +2106
- Misses 1606 1762 +156 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Nice, definetly an issue we need to think about more ! Thanks for the PR! One thing I thought about: the key is the whole hpam dict, so with tuning on we get no cache hits at all and every call reloads from disk. I see two ways out: Or just set |
|
I have to think about compatibility with the pipeline here, this one is complex. |
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