Skip to content

Land the diaPASEF speed/memory stack (#196, #198-#206) on main - #209

Merged
RobbinBouwmeester merged 68 commits into
mainfrom
land/tims-8-17
Oct 17, 2026
Merged

RobbinBouwmeester merged 68 commits into
mainfrom
land/tims-8-17

Conversation

@RobbinBouwmeester

Copy link
Copy Markdown
Member

PRs #196 and #198-#206 were each merged into the branch below them, so the stack collapsed into tims/7-docs-bench. That branch had already been merged into main through #194, so none of tims/8 to tims/17 reached main. This PR carries tims/7-docs-bench (at 177e663, the merge of #206) onto main, with main merged in (zip 9 from #208, rustls from #197). The merge was clean.

Contents, as reviewed in the individual PRs:

Measured on the six-run HYE diaPASEF benchmark: 41.6 min against 49.9 min with identical identifications (stack arm, tims/13-16).

No new code relative to the merged PRs; CI here is the first run of the whole stack against current main.

🤖 Generated with Claude Code

RobbinBouwmeester and others added 30 commits October 14, 2026 17:59
`rt_im_train.multihead_anchors = auto` now also refits the multi-head
calibration on search anchors on every adapting run whose spectra carry
1/K0, whatever its seed anchor count. On diaPASEF the gain does not fall
with the count the way it does on Orbitrap data:

- ProteoBench HYE diaPASEF, six 60 min runs, 11,144 seed anchors:
  +5.7% peptides (93,061 -> 98,373), +6.5% ions at k = 3, w_rt 40-45 s
  -> 24-27 s (one seed);
- E. coli 15 min, 4,639 seed anchors: +0.6% over three seeds, every seed
  up, entrapment FDP 0.42-0.51% -> 0.39-0.46%.

`active`, `decide` and `record_auto` take `has_im` (from the input path
in `run`, from the converted spectra in `run-experiment`); the INFO line
names the reason and `anchors_auto` records `ion_mobility`. 3D runs are
unchanged. docs/08 section 4f, docs/35 (arm T2 measured, ProteoBench HYE
diaPASEF table, note that the six-run 5 min tables ran without
multi-head calibration), config reference and CHANGELOG updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- extract.windows_in_flight defaults to 4 on spectra that carry 1/K0 (16
  otherwise). One HYE diaPASEF run: 45.1 GiB at 16 in flight, 21.4 at 4,
  identical output; six-run experiment 181 -> 94 GiB peak at
  bit-identical identifications.
- retrace sums each MS1 frame of a row once (Ms1Seen) instead of once per
  grid point that reaches it; values bit-identical.
- retrace reader budget: four concurrent calls get eight readers each,
  instead of the first call taking the whole process-wide budget (the
  slowest of five concurrent retraces read on one thread for 390 s).
- skeleton centroid table: extract writes the rows retrace rebuilds as
  zeros on their axis, with the same rule retrace classifies by
  (retrace::row_kind, Skeleton::zeroes), recorded in the table's report;
  retrace refuses a skeleton it would not rebuild in full and publishes
  nothing if a skeleton fragment row would be copied.
- docs/35 section 16 (memory and speed), config reference regenerated,
  CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
convert.tdf_ms1_denoise (default false): before centroiding, keep only
MS1 points on runs of at least 5 occupied mobility scans (TOF
neighbourhood +/- 3, gaps of up to 2 scans bridged, two passes), drop
features no diaPASEF window reaches (windows padded by 3 Th and 0.015
1/K0, kept whole on any overlap), and drop the weak m/z halo of bright
ions: the method and default MS1 settings of dnoise (Garrett, Diedrich
and Yates, bioRxiv 2026, doi:10.64898/2026.08.27.747603, MIT licence),
implemented on convert's raw points. Retrace applies the same filter to
the MS1 frames it holds, and decodes no MS1 frame under retrace.ms1 =
false. MS/MS is never filtered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retrace decoded every frame of the run once and held them all (36-39 GB
per HYE diaPASEF run before the MS1 filter). The MS1 frames are still
decoded once and held, since every MS1 trace reads them. The MS2 frames
are now held window by window: a span's candidates are consecutive in
precursor m/z, so before each span retrace decodes the frames whose
slots isolate a window reaching that m/z stretch and drops the others
(hold_window_frames), then re-picks that contiguous range of candidates
on them (repick_rows, with the sidecar and fragment rows loaded once in
RepickData). Every candidate is re-picked once, the candidates after the
last span for the psms patch. Per-candidate results do not depend on
which frames of other windows are held, so every output is unchanged;
the report adds ms_window_frames and counts every frame decode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re a factor

Folds were base_peptide_id % folds, in the engine's classifiers and in
the nn_torch worker. Every base_peptide_id of the imported HYE library
is even, so two folds put every PSM into fold 0 (the worker stopped with
"fold 0 is empty in training or holdout") and four folds used only two.
The fold is now key % folds while that gives every fold at least half
its share, and otherwise the key's splitmix64 mix modulo folds
(rescoring::fold_assignment, nn_rescore_worker.fold_keys_for), with a
warning. A key's PSMs, a target and its paired decoy, stay in one fold,
and every library the plain modulo already spread keeps its folds and
its scores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under parallel_runs = auto with rt_library_scope = first_run_only, the first
run's whole chain ran alone so that its peak could size the chains after it.
When that run adapts the library with a bootstrap search
(rt_im_train.multihead_anchors = search, or auto on 1/K0 spectra), the
adaptation already searches the run in this process: extract, features and
compete on the seed-anchored library, all of the finish but retrace. Its peak
now sizes the chains, and the first run's finish (rt-im-train to compete on
the adapted library) is the first item of the parallel queue instead of a
step of its own.

On the six HYE diaPASEF runs the finish took about 4 min alone while one of
the six per-run pools sat idle in the parallel phase after it. The outputs do
not change: each stage reads only its own run's inputs, and the first run's
finish now runs in a pool of the chain width, as the other runs' do.

An adaptation without an in-process search (seed anchors, fine-tune) keeps the
previous order, as do an explicit parallel_runs and a grouped run.
adapt_rt_library returns an Adapted struct that records whether it searched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_denoise

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s by the chains in flight

The features main pass and retrace each take their extra decode threads from
a process-wide pool (DecoderLease), sized for a few concurrent calls: four
extra feature loaders, and 7 x 4 retrace readers. Under parallel_runs = auto
the per-run chains run in pools of their own, so on the six HYE diaPASEF runs
the first two feature passes took their two extra loaders each and the other
three decoded on one loader: 280-295 s against 95-104 s, and those three
passes were the last four minutes of the parallel phase.

The chains of an automatic plan are sized on the measured peak of a chain or a
bootstrap search of the first run that ran alone with all of its loaders, so
while they run both pools now grow to one call's share per chain
(sched::BudgetGrowth, features::grow_loaders_for_chains,
retrace::grow_readers_for_chains), and shrink back when the phase returns. An
explicit parallel_runs, grouped bands and single runs are unchanged. The
loaders and readers only decode, and the tables keep their order, so no row
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the current one

A main-pass loader decoded a batch of its chunk (on the codec pool, one job
per column group) and then copied it into the chunk, one after the other. gdb
samples of a standalone features pass on a HYE diaPASEF run put the loaders
at 73% waiting for their own batch to decode and 27% copying, with the codec
pool 64% idle: the pass was bound by the loaders, and each loader by that
serial alternation.

Each loader's chunk stream now takes its batches from a thread of its own
that decodes one batch ahead (Batches::prefetch, a channel of one batch), so
the decode of batch i + 1 overlaps the copy of batch i. The batches arrive in
order, so every chunk, feature value and table byte is unchanged
(a_chunk_decoded_ahead_is_the_chunk_decoded_in_line compares every value bit
of every chunk of the kernel fixture, with and without 1/K0). Under
--threads 1 the stream decodes in line as before, adding no thread the
budget did not grant. The other readers of the chromatogram table (the
confident-bounds pass, open_at) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a chunk's features were computed in parallel, the pass wrote them into
the value columns one row at a time on one thread: per row a peptide length
parse, a protein count, two seed-map lookups, the isotope features and one
scattered write into each of ~435 columns. On a HYE diaPASEF run that
assembly took 20-31 s of one core per pass (assemble_ms) while the pass's
pool waited.

The rows are independent, so the chunk is now assembled in blocks of 2,048
rows in parallel, each block writing its own rows of every column through
disjoint slices. Every value is computed as before and lands in the same
place, so the features table is unchanged (the feature-digest and chunk
invariance tests pass). ValueMatrix::set remains for the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…copied

The worker memory-maps the raw handoff matrix and copies it into its own
float32 matrix. Every mapped page it read stayed in its resident set until the
mapping was dropped after the copy, so for a moment both matrices were
resident: on the six-run HYE diaPASEF pool (28.5 M PSMs x 424 features, a
48 GB handoff) the experiment's resident peak was that copy, 95 GB, against
51 GB for the training that follows. With the retrace frames held per window
group it is the experiment's highest point.

fill_raw_matrix now drops each 131,072-row group of the mapping from the
resident set once the group is copied (release_rows, MADV_DONTNEED). The pages
stay in the page cache, so a read of a released row only faults it back in
with the same bytes. Measured on Linux with a 320 MB matrix on disk: RssFile
371 MB after reading it, 58 MB after the release, and a fill that leaves
RssFile at 58 MB; every copied value unchanged. A no-op on Windows (no
madvise) and for an array that is not memory-mapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract writes three MS1 isotope traces (mono, +1, +2) per candidate, each
read on the candidate's grid as the maximum over the nearest MS1 scan and its
ms1_scan_halfwidth neighbours. When retrace follows, the skeleton then zeroed
every one of those rows, because retrace rebuilds them from the raw events.
gdb samples of a standalone extract of a HYE diaPASEF run put about half of
extract's busy time in those MS1 reads (ms1_near).

The skeleton decision reads only the row's identity (Skeleton::zeroes, the
rule retrace classifies by), so it is now taken first, and a row it zeroes is
written as zeros without being read. The chromatogram table is the one the
reading-then-zeroing wrote, and the apex MS1 reads that feed the features are
unchanged. Without retrace, or under retrace.ms1 = false, nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…memory

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RobbinBouwmeester and others added 27 commits October 17, 2026 09:30
…or reads no environment

The config reference scans `let mut name = |param: ...|` closures as
possible environment readers; the parallel assembly's setter matched and
added 51 false entries. It is now bound as `let put = &mut |...|`, the same
closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w_kind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s1Denoise

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(extract): a diaPASEF extraction gate (extract.gate_support_min, opt-in)
@RobbinBouwmeester
RobbinBouwmeester merged commit 41d2ac8 into main Oct 17, 2026
12 checks passed
@RobbinBouwmeester
RobbinBouwmeester deleted the land/tims-8-17 branch October 18, 2026 09:22
@RobbinBouwmeester RobbinBouwmeester mentioned this pull request Oct 18, 2026
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