Skip to content

test(determinism): lock output against map iteration order (#210) - #242

Open
BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
audit/sj-iteration-order
Open

test(determinism): lock output against map iteration order (#210)#242
BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
audit/sj-iteration-order

Conversation

@BenjaminDEMAILLE

Copy link
Copy Markdown
Contributor

Answers #210.

The trace

Every consumer of SpliceJunctionStats::iter() and of SpliceJunctionDb's map, with the verdict for each:

Consumer Reaches an output order? Why
compute_surviving_junctions (sj_output.rs:149) No Sorts, then builds a HashSet; the neighbour-distance pass reads the sorted vector
write_sj_lines (sj_output.rs:291) Yes — these are the bytes of SJ.out.tab Sorts first
sj_feature_order (sj_output.rs:274) Yes — these are the SJ matrix row positions Sorts first
filter_novel_junctions (junction/mod.rs:166) No Its Vec is consumed only by insert_novel, which inserts into a HashMap; contents, not order
insert_novel → two-pass DB No The DB is consulted by lookup at stitch time; nothing iterates it
sjdb_insert::build_gsj No Iterates a slice its callers pass through sort_and_dedup, and the junctions come from the GTF in file order, not from a map

So no order was escaping. But two of those sorts keyed on (chr, start, end) while SjKey also carries strand and motif, which left a tie to fall back on DashMap order. This PR sorts on the whole key in all three places, which is the remedy the issue itself proposes.

Other maps, for the fourth checklist item: DashMap appears nowhere else in src/. The solo UMI paths (cellranger_1mm_map, directional, connected_components) already sort their HashMap contents on a total key (count, then UMI value) before the scan that depends on order. quant counts into vectors indexed by gene, and gtf builds its gene list in file order.

The lock

tests/determinism.rs: the same reads at one thread and at eight, and two runs at eight threads, must produce byte-identical SJ.out.tab and Aligned.out.sam, in single-pass and in two-pass mode, including pass 1's own SJ.out.tab. The @PG header is filtered because it records the command line, which contains the thread count.

The control

A test that cannot fail proves nothing, so it was checked against one before being trusted. With the SJ writer ordering rows by a per-process random hash, all three tests fail. A deterministic permutation does not fail them — correctly, which is why the control had to be per-process random rather than the fixed swap I tried first.

cargo test, cargo clippy --all-targets -- -D warnings on a cold cache, and cargo fmt --check are green.

🤖 Generated with Claude Code

…the SJ sorts total

Junction counts live in a DashMap, whose iteration order varies with hashing
and with concurrent insertion. Every path that emits an order already sorted,
so no order was escaping, but two of those sorts keyed on (chr, start, end)
while the key also carries strand and motif: a tie would have fallen back to
the map's order. They now sort on the whole key.

tests/determinism.rs is the lock: the same reads at one thread and at eight,
and two runs at eight threads, must produce byte-identical SJ.out.tab and
Aligned.out.sam, in single-pass and in two-pass mode, including pass 1's own
SJ.out.tab.

The test was checked against a positive control before being trusted: with the
SJ writer ordering rows by a per-process random hash, all three tests fail. A
deterministic permutation does not fail them, which is correct, and is why the
control had to be per-process random rather than a fixed swap.

Answers #210.
@BenjaminDEMAILLE
BenjaminDEMAILLE force-pushed the audit/sj-iteration-order branch from 8352542 to 4623c0e Compare August 27, 2026 20:01
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