osprey: Built the counts-only projection from parquet footers instead of scanning every row - #4651
Conversation
…ll scan * The deferred build streamed 1,342,686,095 rows through a builder that discards every field, to produce 446 file names and 446 counts - 618s and ~5.7 GB, immediately before first-pass FDR re-read the same columns * Kept each file's declared NumRows from the schema probe already opening that footer, so the projection costs nothing on any path * Refused an out-of-range per-file count rather than casting it, and pinned footer count == scan count See TODO-20260909_osprey_projection_scan_progress.md in pwiz-ai/todos Co-Authored-By: Claude <noreply@anthropic.com>
* All three producers streamed 1.34 billion rows through a builder that discards every field, to learn per-file counts the footer declares - 618s and ~5.7 GB measured on the resume path, unmeasured on the other two * Removed counts-only mode from FdrProjectionSet.Builder, so the scan cannot be reintroduced, and dropped the second footer open the join arm paid for a capacity hint that mode ignored * Widened the cohort row totals to long: they wrap past ~505 files, and FirstPassFDR gates protein FDR on TotalRows > 0, so a wrap silently skipped it and exited 0 with every experiment q at its 1.0 placeholder * Deleted the survivor-buffer bench, which could only ever skip See TODO-20260909_osprey_projection_scan_progress.md in pwiz-ai/todos Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ustworthy * ProbeResumeSchemaAndRows now also requires entry_id and is_decoy, the two columns whose absence makes a declared row count disagree with a scan - it tested only the PIN feature schema, which cannot see that shape * Restored both files' original byte-order marks, flipped by editing tools * Fixed a sentence garbled by the builder-to-bool rewrite See TODO-20260909_osprey_projection_scan_progress.md in pwiz-ai/todos Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A couple of updated validation paths now produce misleading user-facing error text and stale commentary relative to the tightened schema probe, which should be corrected before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR removes the expensive “counts-only projection” Parquet row scan by deriving per-file row counts directly from Parquet footer metadata (NumRows), and hardens the pipeline against silent integer overflow in large cohorts by widening projection totals to long. It also tightens the schema probe used to trust footer row counts, adds a unit test that pins footer counts to scan counts across multiple row groups, and deletes an always-skipped Stage 5 bench test.
Changes:
- Build counts-only
FdrProjectionSetinstances from Parquet footer row counts (no per-row scan) and remove counts-only mode fromFdrProjectionSet.Builder. - Widen cohort-level row totals (
TotalRows,totalScored, etc.) tolongto prevent overflow-induced logic skips on large datasets. - Tighten
ProbeResumeSchemaAndRowsto validate additional required columns and addTestFooterRowCountMatchesScan; remove the Stage 5 survivor-buffer bench test.
File summaries
| File | Description |
|---|---|
| pwiz_tools/Osprey/Osprey.Test/Stage5SurvivorBufferBenchTest.cs | Deleted an opt-in bench test that could only ever skip. |
| pwiz_tools/Osprey/Osprey.Test/IOTest.cs | Added a test asserting footer NumRows matches a full scalar scan across multiple row groups. |
| pwiz_tools/Osprey/Osprey.Tasks/PerFileScoringTask.cs | Replaced counts-only scans with footer counts; refactored lean projection building and widened totals. |
| pwiz_tools/Osprey/Osprey.Tasks/FirstPassFdrTask.cs | Widened pre-compaction cohort totals to long and updated related signatures/comments. |
| pwiz_tools/Osprey/Osprey.IO/ParquetScoreCache.cs | Tightened resume probe schema validation to make footer row counts trustworthy. |
| pwiz_tools/Osprey/Osprey.FDR/PercolatorEngine.cs | Updated usage of TotalRows to long. |
| pwiz_tools/Osprey/Osprey.FDR/FdrProjection.cs | Widened TotalRows to long and removed counts-only mode from FdrProjectionSet.Builder. |
Review details
Suppressed comments (1)
pwiz_tools/Osprey/Osprey.Tasks/PerFileScoringTask.cs:1551
- This exception message still mentions "--input-scores" (which is documented elsewhere in the repo as retired) and also only references missing PIN feature columns, even though the probe now fails when entry_id or is_decoy are missing too. Please make the message reflect the actual validation and avoid referring to a removed CLI flag.
if (!probe.HasPinFeatures)
throw new InvalidDataException(string.Format(
@"--input-scores: parquet {0} is missing the PIN feature columns -- it is not a valid Osprey scores parquet. Delete it and re-run so it is regenerated.",
parquetPath));
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // rows while its footer declares N - and the probe above does not | ||
| // catch it, because it tests PIN_FEATURE_NAMES[0], not entry_id. | ||
| // Before the scan went, both numbers came off the same reader and | ||
| // could not diverge; now they can, and the divergence surfaces late, |
…09_osprey_projection_scan_progress
Summary
FdrProjectionSetfrom Parquet footerNumRowsinstead of scanning every row, on all three producersFdrProjectionSet.Builder, so the scan cannot be reintroduced at a call sitelong- they wrap past ~505 files, and a wrap silently skipped first-pass protein FDRFollows #4646. Raised by the developer from a 446-run CHS job.
What the scan was doing
PerFileScoringTaskstreamed every row of every.scores.parquetthroughFdrProjectionSet.Builder(countsOnly: true), whoseAddRowdiscards all five fields andincrements a counter:
entry_id,charge,is_decoy,coelution_sumand a STRINGmodified_sequence, decoded row group by row groupThe phase immediately after it re-reads the same columns from the same files to do the actual
work. The counts were always free: Parquet's footer declares
NumRows, and the load loop wasalready opening that footer for its schema check.
Why it surfaced now, and why it is not a regression
c4921f3d6c(#4633) madeFdrProjectionsa deferred factory. That was right - it made the costCONDITIONAL, and a
--task PerFileRescoringworker skipsFirstPassFdrTask.Runand so neverpays it, which is the
Loading scored entries9m46s -> 10s win recorded inTODO-20260901_osprey_stage5_reload_materialization.md. It just deferred work that should notexist. 9m46s and 618 s are the same scan seen from the two sides of that condition.
Three runs on the same 446-run CHS cohort and shape:
Verified at 446 files
stage5-start-liveandprojection counts-onlyare the two lines that bracket the scan. Theywere 618 s apart; on this build, on the full cohort:
Same second, and the same row total - so the footer counts reproduce the scan's answer on real
data, not just in a unit test. That matches the pre-#4633 behaviour, so this is a restoration.
The overflow, which is the part that could have been wrong output
FdrProjectionSet.TotalRowssummed the per-file counts into an uncheckedint.FirstPassFdrTaskgates first-pass protein FDR onTotalRows > 0, so a wrapped negative totalskips it: every row keeps its placeholder
experiment_protein_qvalueof 1.0, no patch failure iscounted, and the run exits 0 reporting success.
PerFileScoringTaskalready recorded the wrap at~505 files for the 4.2 M-per-file shape, and TEIREX (936 runs) is in
ai/docs/osprey-large-datasets.md.Run'stotalScoredcarried the same exposure while bothresume paths were already
long.Deliberately not in scope
FdrProjectionSet.Builderis now dead production code - its only remaining reference isFdrTest.cs, and its parity test certifies a class nothing ships calls. Worth deleting, butthat is a refactor rather than a defect and does not belong in this change.
required it: it could only ever
Assert.Inconclusivewithout an opt-in multi-GB run directory,so it was a permanent skip reported as green. The coverage it nominally represented is covered
by the planned synthetic test set.
Test plan
Build-Osprey.ps1 -RunTests -RunInspection- 593 tests, 593 passed, 0 skipped, zero-warningregression.ps1 -Dataset Stellar- PASSED,mode1 (vs golden)green,Tokens REQUIRED: 0TestFooterRowCountMatchesScan- footerNumRows== scan count, across multiple row groupsstage5-start-live->projection counts-onlyin the same second, row total identicalpull/<N>See TODO-20260909_osprey_projection_scan_progress.md in pwiz-ai/todos
Co-Authored-By: Claude noreply@anthropic.com