Skip to content

Fix EnsembleLightningModel VEP: run models independently to avoid allele/variant dimension scrambling - #57

Merged
MuhammedHasan merged 1 commit into
mainfrom
worktree-fix-ensemble-vep
Jun 4, 2026
Merged

Fix EnsembleLightningModel VEP: run models independently to avoid allele/variant dimension scrambling#57
MuhammedHasan merged 1 commit into
mainfrom
worktree-fix-ensemble-vep

Conversation

@avantikalal

Copy link
Copy Markdown
Collaborator

Problem

predict_variant_effect with model='ensemble' produced wrong log-fold-change predictions. The ensemble mean did not match the arithmetic mean of the four constituent replicate models run individually.

Confirmed by running rs2158799 (chr7:28237488 C>G) against JAZF1 in blood cell types:

Approach Classical monocyte median LFC
model='ensemble' (broken) −0.043
Each replicate individually, averaged −0.268 (correct)

Root cause: two bugs in EnsembleLightningModel.predict_on_dataset

Bug 1 — batch_size=1 allele scrambling

EnsembleLightningModel.predict_step concatenates the outputs of all 4 models along dim=0 within each batch. With the default batch_size=1, ref and alt alleles land in separate batches. After model-concatenation the flattened tensor becomes:

[m0_ref, m1_ref, m2_ref, m3_ref,  m0_alt, m1_alt, m2_alt, m3_alt]

The subsequent rearrange("(b n a) t -> b n a t", a=2) in LightningModel.predict_on_dataset then groups m0_ref with m1_ref as the two "alleles" for a single variant, and the LFC expression[:,:,1,:] - expression[:,:,0,:] computes cross-model differences instead of alt−ref.

Bug 2 — n_seqs > 1 model/variant dimension swap

With batch_size=n_alleles=2 (which does pair alleles correctly), the LFC tensor after all batches are collected is ordered variant-outer:

[JAZF1_m0, JAZF1_m1, JAZF1_m2, JAZF1_m3,
 JAZF1-AS1_m0, JAZF1-AS1_m1, ...,
 CREB5_m0, ...]

EnsembleLightningModel.predict_on_dataset then applied rearrange("(e b) t -> e b t", e=4), which assumed model-outer ordering. With n_seqs=3 genes this sliced off the wrong blocks, mixing LFCs across genes and models. The ensemble mean and the save_replicates per-replicate columns were both wrong.

Bug 2 only manifests when a variant overlaps more than one gene (n_seqs > 1). Gene expression prediction (n_alleles=1) was not affected by either bug.

Fix

Replace the super().predict_on_dataset() + broken rearrange with a loop that runs each constituent model's predict_on_dataset independently, then stacks and averages:

all_preds = [model.predict_on_dataset(dataset, ...) for model in self.models]
expression = np.stack([p["expression"] for p in all_preds])  # (e, b, T)
return {"expression": expression.mean(axis=0), "ensemble_preds": expression, ...}

Each LightningModel.predict_on_dataset correctly handles allele ordering for any batch_size and any n_seqs. The explicit np.stack replaces the incorrect (e b) t -> e b t rearrange.

Test

Added test_EnsembleLightningModel_predict_on_dataset_matches_individual_replicates to tests/test_lightning.py. The test mocks predict_on_dataset on each constituent model (so no GPU forward passes needed) and asserts:

  1. The ensemble mean equals the arithmetic mean of the mocked per-model predictions.
  2. ensemble_preds[i] matches model i's predictions exactly.
  3. Each constituent model was called with the correct forwarded arguments.

The test runs in ~5 seconds and does not require a long-running mark.

Verification

45 passed, 0 failed (full test suite)

🤖 Generated with Claude Code

Two bugs caused wrong variant effect predictions when using the ensemble model:

Bug 1 — batch_size=1 allele scrambling:
EnsembleLightningModel.predict_step concatenates 4 model outputs along dim=0
per batch. With batch_size=1, ref and alt alleles land in separate batches, so
after concatenation the tensor becomes [m0_ref, m1_ref, m2_ref, m3_ref, m0_alt,
...] instead of the expected [ref, alt] pairing. The subsequent rearrange in
predict_on_dataset then computes cross-model differences instead of alt−ref LFC.

Bug 2 — n_seqs > 1 model/variant dimension swap:
With batch_size >= n_alleles (correct allele pairing), when a variant overlaps
multiple genes the LFC tensor is ordered variant-outer: [g0_m0, g0_m1, g0_m2,
g0_m3, g1_m0, ...]. The rearrange '(e b) t -> e b t' with e=n_models treated
model as the outer dimension, so it sliced off the wrong blocks and mixed LFCs
from different genes and models when computing the ensemble mean and when
populating the save_replicates per-replicate columns.

Both bugs are fixed by overriding predict_on_dataset in EnsembleLightningModel
to run each constituent model's predict_on_dataset independently, then stack
and average. This matches the documented semantics of the ensemble and produces
values identical to running each replicate individually and averaging manually.
The fix also simplifies the code: the broken (e b) rearrange is replaced by an
explicit np.stack.

Gene expression prediction (n_alleles=1) was unaffected by these bugs.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@avantikalal
avantikalal requested a review from MuhammedHasan May 28, 2026 09:12
@MuhammedHasan
MuhammedHasan merged commit 5e2439a into main Jun 4, 2026
8 checks passed
@avantikalal
avantikalal deleted the worktree-fix-ensemble-vep branch June 4, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants