Skip to content

Amortise the multi-head library re-prediction across an experiment - #86

Merged
RobbinBouwmeester merged 3 commits into
mainfrom
feat/multihead-first-run-only
Sep 14, 2026
Merged

Amortise the multi-head library re-prediction across an experiment#86
RobbinBouwmeester merged 3 commits into
mainfrom
feat/multihead-first-run-only

Conversation

@RobbinBouwmeester

Copy link
Copy Markdown
Member

Draft until the A/B on the six Astral files lands. The code is complete and green; the claim about what sharing costs is not made yet.

Reported: multi-head re-predicts the retention times for every file, and the wanted behaviour is to predict on the first run and use the per-run LOESS for the rest.

It was supposed to already do that

experiment.finetune_scope = first_run_only has done exactly this for the DeepLC fine-tune all along. It did not cover multi-head, and not by configuration — process_run tested mh_heads > 0 before the shared-library branch, so the shared path was unreachable whenever multi-head was on.

Confirmed on a real six-file Astral experiment (results3), whose config already set finetune_scope: first_run_only:

  • fragment_library_precursors_multihead.parquet in every one of r0r5, 362 MB each;
  • "repredicted": 9403264 in all six summaries — six full re-predictions of the same 9.4M-row library;
  • no base re-prediction at the root, because multi-head replaces it.

That is where the measured 1.4×–1.7× wall clock goes.

Change

experiment.finetune_scopeexperiment.rt_library_scope, with the old name kept as a serde alias. It now governs whichever adaptation is active, because both are the same shape of work: one full re-prediction of the library against that run's confident seed PSMs. The name says what it governs rather than which mechanism happened to use it first.

  • process_run tries the shared library first, whichever mechanism produced it;
  • multi-head offers its library for sharing, as the fine-tune already did;
  • the first-run-alone gate covers multi-head, resolved the same way the stage resolves it so the decision matches what the run will actually do.

The reasoning it overturns

The old comment said multi-head is "per run, always, and never shared: the whole point is that it is fitted against THIS run's chromatography." That is true — and equally true of the fine-tune, which has been shareable all along. What a shared library fixes is elution order, which replicate injections on one LC method share. A per-run LOESS can stretch and bend the axis but cannot reorder two peptides, so a batch that genuinely reorders — different gradients or columns, a method change part-way — wants per_run, and so does a long batch where drift accumulates.

What is not claimed here

The cost of sharing for multi-head is not yet measured. The guide until it is, recorded on the enum, is the fine-tune's measured cost on the 6-run HYE AIF set: the run that owned the fit reached 15.2 s median RT residual, the five reusing it 20.3–25.4 s (+47%), windows widening 145 s → 179–227 s, degradation monotonic in acquisition order.

Reason to expect better here: the six Astral runs' own calibrations are near-identical — w_rt 20.8–23.2 s, residuals 2.30–2.73 s — so one fit plausibly serves all six. That is the hypothesis the A/B is testing, against results3 (per-run: 128,006 precursors, 115,924 peptides, 12,201 protein groups, decoy fraction 0.0100).

Validation

cargo fmt --check, clippy -D warnings, cargo test --workspace 285 passed (one new test covering the default, the alias, and that both spellings agree). Config reference regenerated; docs/02, docs/08, CLAUDE.md and CHANGELOG updated.

🤖 Generated with Claude Code

RobbinBouwmeester and others added 2 commits September 14, 2026 11:00
Reported: multi-head re-predicts the retention times for every file, and the
wanted behaviour is to predict on the first run and use the per-run LOESS for the
rest.

That is what `experiment.finetune_scope = first_run_only` already did for the
DeepLC fine-tune, and it did not cover multi-head: `process_run` tested
`mh_heads > 0` before the shared-library branch, so the shared path was
unreachable whenever multi-head was on. Confirmed on a real six-file Astral
experiment: `fragment_library_precursors_multihead.parquet` in every one of r0-r5,
362 MB each, `"repredicted": 9403264` in all six summaries.

The field is now `experiment.rt_library_scope`, with `finetune_scope` kept as a
serde alias, and it governs whichever adaptation is active. Both are the same
shape of work -- one full re-prediction of the library against that run's
confident seed PSMs -- so one knob amortises both, and the name now says what it
governs rather than which mechanism happened to use it first.

`process_run` tries the shared library first, whichever mechanism produced it,
and multi-head now offers its library for sharing as the fine-tune already did.
The first-run-alone gate covers multi-head, resolved the same way the stage
resolves it so the decision matches what the run will actually do.

The reasoning for per-run was that the calibration is fitted against THIS run's
chromatography. True, and equally true of the fine-tune. What a shared library
fixes is elution ORDER, which replicate injections on one method share; a per-run
LOESS can stretch and bend the axis but cannot reorder two peptides, so a batch
that genuinely reorders wants `per_run`, and so does a long batch where drift
accumulates. The fine-tune's measured cost of sharing is recorded on the enum:
+47% median RT residual across five reusing runs, monotonic in acquisition order.

The multi-head equivalent is being measured on the six Astral files now; this
commit does not claim it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The A/B the previous commit said it did not claim. Six-file Astral experiment,
one binary, configurations differing only in experiment.rt_library_scope:

  precursors       128,006 -> 126,762   -0.97%
  peptides         115,924 -> 114,704   -1.05%
  protein groups    12,201 ->  12,287   +0.70%
  decoy fraction    0.0100     0.0100   unchanged
  w_rt, reusing     20.8-23.2 s -> 34.9-37.9 s

So it is a trade, not a free win. Sharing gives back 55% of the window narrowing
multi-head's gain came from, and the decoy fraction is identical in both arms, so
the 1% is lost identifications rather than a moved threshold. What it buys is five
full re-predictions of a 9.4M-row library removed: those files finish their chain
in 1.9-3.4 minutes instead of the better part of an hour.

`first_run_only` stays the default. It is the same default the fine-tune has
always had on a considerably worse trade (+47% median RT residual across the
reusing runs), and 1% for that much wall clock is the right shipped balance. The
number is written down so anyone for whom it is not can set `per_run`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RobbinBouwmeester

RobbinBouwmeester commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

A/B result: sharing costs ~1%, and that is the whole cost

Six-file Astral experiment, one binary, configurations differing only in experiment.rt_library_scope. The per-run arm is the results3 run already on disk.

per run first run only
precursors @1% 128,006 126,762 (−0.97%)
stripped peptides @1% 115,924 114,704 (−1.05%)
protein groups @1% 12,201 12,287 (+0.70%)
target PSMs @ pooled 1% 536,462 531,107 (−1.00%)
empirical decoy fraction 0.0100 0.0100
w_rt, run that owns the fit 22.1 s 22.1 s
w_rt, the five reusing runs 20.8–23.2 s 34.9–37.9 s
per-file chain, reusing runs full re-prediction each 1.9–3.4 min

It is a trade, not a free win. Sharing gives back 55% of the window narrowing that multi-head's gain came from — the five reusing runs go from a mean w_rt of 22.0 s to 34.1 s. The decoy fraction is identical in both arms, so the 1% is lost identifications rather than a moved threshold.

What it buys: five full re-predictions of a 9.4M-row library removed. Those files finish their chain in under four minutes instead of the better part of an hour.

Default

first_run_only stays the default. It is the same default the fine-tune has always had, on a considerably worse trade (+47% median RT residual across its reusing runs), and 1% for that much wall clock is the right shipped balance. The number is now in docs/08 so anyone for whom it is not can set per_run.

Worth noting for later: r0's window is 22.1 s in both arms while the reusing runs sit at 35–38 s, which points at the loss being the width rather than the head choice. A periodic re-adaptation — every k-th run rather than once or every time — would likely recover most of the 1% at a fraction of the cost. Not in this PR.

Out of draft.

🤖 Generated with Claude Code

@RobbinBouwmeester
RobbinBouwmeester marked this pull request as ready for review September 14, 2026 12:20
@RobbinBouwmeester
RobbinBouwmeester merged commit af417eb into main Sep 14, 2026
12 checks passed
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.

1 participant