Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m27s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m19s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 19m23s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 23m8s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 23m47s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 24m24s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 27m33s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 29m33s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 29m21s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 30m3s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 23m43s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m49s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m37s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m42s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 28m43s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m54s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 35m41s
Build Packages / Unit tests (push) Successful in 2h7m57s
Build Packages / Validation Battery (push) Successful in 38m34s
Each block of the candidate-vector and the cell search sorts its own candidates and merges them into one shared top list under a lock, in the order the blocks happen to get there. The merge compared values only, so where two candidates scored the same - common on the 30-spot seed the stills path tries first, since the score is a sum of trimmed terms - the place went to whichever block merged first. The same frame then indexed to a slightly different lattice, or not at all, from one run to the next. Ties are now broken by the candidate's identity - the sample index for a vector; the cell vector (with its flip), candidate and rotation sample for a cell - which makes the order total, so the merged lists are the top-N of everything whatever the merge order. The blocks' own lists are already in that order: the block radix sort is stable and its samples ascend with the thread index. Measured on a 100k-frame serial set: 4-21 of ~33k indexed frames came out with a different lattice in every pair of runs, which the stills scaling and partiality chain turned into 0.97-0.99 CC1/2 and thousands of observations in or out of the merge. With the fix, three runs at the default thread count and one at -N 8 give the md5-identical merged .hkl; a per-frame dump (spot list, indexer answer, refined lattice) of 20k frames is identical across three runs where it differed in 4-6 frames before. FFBIDXIndexerCPU_MatchesGPU and Portable_FFBIDXIndexerCPU_SyntheticStill pass. No regression test: a repeat-and-compare test on the library's spot lists, threaded, passed before the fix too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit 9cff12d652088851b81f6a353ac52aa8318799f2)