Skip to content

[5/5] Perf: index the fragment rows instead of the dense slots - #438

Open
GeorgWa wants to merge 1 commit into
perf/build-flat-columns-from-kept-indicesfrom
perf/per-row-fragment-positions
Open

[5/5] Perf: index the fragment rows instead of the dense slots#438
GeorgWa wants to merge 1 commit into
perf/build-flat-columns-from-kept-indicesfrom
perf/per-row-fragment-positions

Conversation

@GeorgWa

@GeorgWa GeorgWa commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Performance. One documented behaviour change, otherwise bit-identical to main.

parse_fragment held the number, the position and the precursor length of every dense slot.

  • Hold the number, the position and the precursor length per fragment row instead of per dense slot, because every charged fragment type of a row shares them. uint16 holds every value, which max_frag_per_peptide bounds.
  • Compute the fragment numbers for the kept fragments only, instead of in a dense pass over every slot.
  • Pass the intensities without a copy. Zeroing the intensity of padding slots was a no-op, because padding is always excluded.

Behaviour change: fragment rows that no precursor covers now report number 0 and position 0. main leaves those slots unwritten (np.empty), so their values were unspecified.

Peak RSS and runtime of the whole stack, keep_top_k_fragments=12:

dense slots peak main peak stack time main time stack
120M 6.45 GB 1.76 GB 7 s 3 s
480M 23.22 GB 6.08 GB 15 s 7 s
1.48G 58.73 GB 16.08 GB 45 s 14 s

Last of 5. Stacked on #437, and lands the same end state as #429.

🤖 Generated with Claude Code

`_parse_fragment` held the fragment number, the position and the precursor length
of every dense slot as uint32, which cost 3 dense arrays of 4 bytes per slot. All
three values are shared by every charged fragment type of a fragment row, so
`_fill_in_indices` now writes one value per fragment row instead, and
`_parse_fragment` no longer takes the tiled directions array.

uint16 holds every value, because `max_frag_per_peptide` of `_fill_in_indices`
bounds the row positions and the row counts at 300. `flatten_fragments` casts the
kept `number` and `position` values back to uint32, so the flat columns keep
their public dtype.

`_calculate_fragment_numbers` takes a direction, a row position and a row count,
and runs for the kept fragments only. The dense pass over every slot is gone, and
so is the discarded output array it needed. Dropping its in-out `frag_number`
parameter also gives direction 0 a defined result.

`_fill_in_indices` writes the row count as a scalar instead of a product with an
array of ones, which removes one allocation per precursor.

`flatten_fragments` passes the intensities without a copy, because nothing writes
to them any more. Zeroing the intensity of padding slots was a no-op, as padding
is always excluded.

Output stays bit-identical to main across 5400 generated configurations, with one
exception: fragment rows that no precursor covers now report number 0 and
position 0. main leaves those slots unwritten, because it allocates the arrays
with `np.empty`, so their values were unspecified.

At 48M dense slots, peak RSS drops from 1.78 GB to 1.32 GB, and from 3.47 GB on
main. The call is ~30% faster than the previous step and ~45% faster than main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GeorgWa GeorgWa changed the title Perf: index the fragment rows instead of the dense slots [5/5] Perf: index the fragment rows instead of the dense slots Aug 31, 2026
Comment on lines +957 to +958
row_positions = np.zeros(n_fragment_rows, dtype=np.uint16)
row_counts = np.zeros(n_fragment_rows, dtype=np.uint16)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we intantiate here, then pass to _fill_in_indices .. why not instantiate there and return?

----------
frag_direction : np.int8
directions of fragments for each peptide
direction of the fragment type. 'abc' ions count from the first amino

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could there be string constants for 'abc' and 'xyz' ?

return row_position + 1
if frag_direction == -1:
return row_count - row_position
return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add this behaviour to the docstring

)
needs_row = bool({"number", "position"}.intersection(custom_columns))
kept_frag_types = kept_indices % n_fragment_types if needs_frag_type else None
kept_rows = kept_indices // n_fragment_types if needs_row else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please double check if a +1 is required due to flooring

)
if "position" in custom_columns:
columns["position"] = positions.reshape(-1)[kept_indices]
# the flat column keeps its uint32 dtype

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what extra information (compared to the next line) does this comment carry?

# fills most dense slots.
mz = fragment_mz_df.values.reshape(-1)
use_intensity = len(fragment_intensity_df) > 0
# `copy=False` keeps a dense copy out of memory whenever the dtype already

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"copy=False keeps a dense copy out of memory " .. wording is confusing (I guess aircraft operators were not trained in python, but maybe COBOL?)

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