Commit Graph
1610 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5.5 d91fc7ca3d Merge mr-gpumem (51706641f) into rc175: RESULT-CHANGING (owner approved, float rounding): molecular replacement's transform FFT on the GPU wherever it fits the search's budget
4nwv molecular replacement 31.7 -> 20.1 s; outputs identical on myob, cytochrome C and 4nwv on the RTX 5080.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:53:02 +02:00
leonarski_fandClaude Opus 5.5 323df1561e Merge mr-gpumem (011b98838) into rc175: molecular replacement plans its GPU parts once per merge on model validation's budget (one rigid-body pool for every fit, the transform cropped to the indices it is read at, the pool trimmed at each stage), output unchanged
4nwv MR peak 13.77 -> 8.74 GB on the RTX 5080; myob, cytc and 4nwv outputs identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:43:34 +02:00
leonarski_fandClaude Opus 5.5 51706641fa RESULT-CHANGING: molecular replacement's transform FFT on the GPU wherever it fits the search's budget
The molecular transform's FFT went to the GPU only where its buffers took at most a quarter of the card
(MapToFPhiGPUFits), the last quarter rule left in molecular replacement. On a large cell that is never:
4nwv's 768^3 box needs 5.45 GB, so on a 16 GB card it ran on the CPU in FFTW, single-threaded - 12.5 s
of the search's 31 s.

Now it is judged, as every other part of the search, against the search's budget (80% of the card less
what is resident; the search holds nothing else yet when it runs), and the device writes only the
cropped transform the search keeps (CropHalfL, moved to ModelFFT), so it needs 4.46 GB instead of 5.45.
cuFFT and FFTW agree to float rounding, not bit for bit, so wherever the transform moves to the GPU the
search's functions can differ in their last bits: on 4nwv on 16 GB and 8.55 GB cards it moves (it now
does on any card above about 5.6 GB, before above 21.8 GB); the small boxes were on the GPU before and
give the same values (the same kernel, cropped).

Measured on the RTX 5080, timing benchmark: 4nwv's transform 14.1 -> 2.2 s, MR 31.6 -> 20.1 s, run
93.0 -> 89.2 s; MR's peak device memory unchanged at 8.7 GB (the FFT's own 5.2 GB comes before anything
else is held). On this card the reflection files, maps, placed models, reports and the MR candidate lines
of myob, cytc and 4nwv still came out byte-identical to rc175-metal-merge. [MolecularReplacement] passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:41:48 +02:00
leonarski_fandClaude Opus 5.5 011b98838e Molecular replacement: one GPU plan per merge on model validation's budget, the transform cropped, output unchanged
What molecular replacement put on its GPU was decided part by part on a quarter of the card each: every
group's rigid-body pool (up to three engines), every null's (up to nine), the score engines' cache, the
--mr hand check's structure factors. The groups and the nulls run side by side, so the quarters added up:
on 4nwv on a 16 GB card the search held 4 rigid-body engines of 1.88 GB, two score engines of 1.64 GB,
the 1.82 GB molecular transform, two translation functions' 1.2 GB and the 2.4 GB the allocation pool
kept from the validation before - 13.8 GB at its peak, 83% of the card. On an 8.55 GB card that pages.

Now each merge's search is planned once, before it starts, on the budget model validation uses
(ModelGPUBudget: 80% of the card's total, less what a merge engine still at work holds): the transform,
one rigid-body engine and the largest score engine take their place in that order where they still fit,
more rigid-body engines only within half of the budget, and what is left is a room the score engines
(least recently used given up first, as before) and the rotation and translation functions' working
buffers share, each waiting for the other to finish rather than spilling past it
(PlanMolecularReplacementGPU). A part that does not fit runs on the CPU, decided on its size alone.

- One rigid-body pool per merge serves every fit of the merge - the groups' first and further copies, the
  nulls, the tNCS shifts, the carried copies - sized for the largest (the most copies Matthews allows, in
  any group) and reserved once, instead of a pool made and freed for each. RigidBodyGPUPool::Cover widens
  a planned pool to another model or group; its zones are kept by group, atom count and resolution.
  RigidBodyGPUPool::Create (the quarter rule) is gone.
- The sizes are worked out on the CPU while the transform is computed.
- The molecular transform keeps only the indices it is read at (|k|, l <= box / d_min + 1): 0.84 GB
  instead of 1.82 on 4nwv, host and device. Its rows keep their full length: cut short too, the rotation
  function read them 15% slower.
- The translation function's prepared setup is kept on the host and uploaded by each call, so the device
  holds it only while a function runs; the cross-term offsets are uploaded only where there are cross terms.
- The allocation pool is trimmed before the search, after the transform's GPU FFT, before a score
  engine is made and before the hand check, whose structure factors are now judged against the budget.
- rugnux passes the merge engine's bytes, where the finalist ledger keeps it, to the search, the hand
  check and the validation of the placed model.

Measured on the RTX 5080 (16.6 GB), timing benchmark: peak device memory during MR 13.8 -> 8.7 GB on 4nwv
(start 3.0 -> 0.66 GB after the trim), 1.67 -> 1.14 GB on cytc, 6.3 -> 0.76 GB on myob. On 4nwv the plan
holds the transform 0.84 GB, one rigid-body engine of 2.10 GB and leaves 10.35 GB of a 13.29 GB budget
to the score engines and the functions; a second engine would pass half of the budget (one engine
against two: +1.2 s on the rescoring and the null). MR 31.7 -> 31.6 s on 4nwv. Reflection files, maps,
placed models, reports and the MR candidate lines byte-identical to rc175-metal-merge on myob, cytc and
4nwv; [MolecularReplacement], [ModelValidation], RigidBody*, ModelScale*, [CUDAMemHelpers] pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:33:39 +02:00
leonarski_fandClaude Opus 5.5 d24d328e7b Merge gpumem-run into rc175: FFT indexers released (and the allocation pools trimmed) right after the last indexing of a pass; probe passes give their memory back when they end; on a card below 12 GB the speculative probe takes turns with the merge, output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:53:00 +02:00
leonarski_fandClaude Opus 5.5 2091c0524a GPU memory: a probe pass hands its memory to the card when it ends
The speculative geometry probe and the rotation-scale walk's indexing probes
gave their indexers' and image workers' buffers back to the allocation pool
only, which kept them reserved under the main line's merge and analyses: 5.5 GB
on 8qaw and 5.3 GB on 4nwv on the RTX 5080, until the end of the run. The pool
is trimmed (every card: cuda_trim_allocation_pools) when such a probe ends, as
IndexerThreadPool::ReleaseIndexers now does through the same helper.

Measured (RTX 5080): device memory held at the end of the final merge 8.7 ->
3.4 GB (4nwv), 14.5 -> 7.7 GB (8qaw). Output unchanged: p.hkl byte-identical
on cytc_x10sa, myob_x10sa, 4nwv and 8qaw, on this card and with its total read
as 8.55 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:52:14 +02:00
leonarski_fandClaude Opus 5.5 eb4c1d373d Merge scale-footprint round 3 into rc175: each partial's frame derived from the frame CSR, on_ice and clipped in one flags byte, estimate re-fitted to 51 B per observation + 98 B per distinct hkl, output unchanged
8qaw's scaling engine peak 8.16 -> 7.54 GB (12.5 GB before round 1); p.hkl byte-identical on 15 battery sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:06:15 +02:00
leonarski_fandClaude Opus 5.5 fd1fb3e25e Rotation scaling on the GPU: a partial's frame from the frame CSR, its two flag bytes in one, output unchanged
- The partials lie in frame order, so a partial's frame is the last frame
  starting at or before it in the frame CSR (FrameOf, a binary search) and is
  no longer stored: UpdateCorr, SmoothCorr (which takes the fulls' stored
  frames through the same FrameOf), FilterFrame, the combine, and the
  ingest's flux, delta_phi and partiality kernels. 4 B less per observation.
- on_ice and clipped/overloaded share one flags byte (on ice at bit 0, the
  host's OBS_CLIPPED / OBS_OVERLOADED one bit up); a full's on_ice byte is
  0/1, so the kernels shared by the two read either alike. 1 B less.

Measured on an RTX 5080, largest engine of each set, round 2 -> round 3, MB:
  set   obs/hkl          ingest peak (B/refl)      post-decision peak
  4nwv  32.7 M/15.4 M    2336 (72) -> 2172 (67)    3625 -> 3461
  8qaw 122.7 M/ 8.6 M    7421 (61) -> 6808 (56)    8156 -> 7543
  9v16  54.7 M/ 5.7 M    3419 (63) -> 3146 (58)    3774 -> 3500
  7qij  78.1 M/ 2.2 M    4707 (60) -> 4317 (55)    4707 -> 4317
  8sa8  50.0 M/ 5.0 M    3134 (63) -> 2884 (58)    3603 -> 3353
Kernel time on 8qaw (nsys, all calls): UpdateCorr 364 -> 365 ms, SmoothCorr
143 -> 112 ms, combine 477 -> 476 ms, ingest partiality/delta_phi/flux
113 -> 109 ms - the search costs nothing measurable, the saved reads pay for it.

GPUScalingPeakBytes refitted: 56 + 99 -> 51 B/obs + 98 B/hkl, within 10%.
DeviceBytes: 58 -> 53 B per observation, ingest 60 -> 55 B per reflection.

p.hkl byte-identical to the rc175 baseline on 15 battery sets (4nwv, 8qaw,
9v16, 7qij, 8sa8, 11if, 5reo, 6oel, 9jzo, 9min, metformin, lyso_x06da_ref,
thau_bl1a_3p8keV, insu_I_x06da_5keV_2, myob_x06da); compare shows no change.
[RotationScale] and ErrorModel_DeviceFitIsTheHostFit pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:05:48 +02:00
leonarski_fandClaude Opus 5.5 1ebb28e3ab Merge cytc-mr into rc175: molecular replacement decides an R-free tie between groups with as many copies on every reflection (paired, 3 standard errors), else UNDECIDED as before
Cytochrome C from its sequence-exact AlphaFold model now places in P 32 2 1; MR replay survey of 193
sets: UNDECIDED 7 -> 4, all three newly decided correctly, no wrong decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:44:13 +02:00
leonarski_fandClaude Opus 5.5 6cdc063b4c Molecular replacement: an R-free tie between groups with as many copies decided on every reflection
Two space groups of a merge whose placements are within R_FREE_MARGIN (0.03) of each other in R-free
left the choice UNDECIDED. Where both placed the same number of copies they have the same parameters,
so the working reflections compare them as fairly as the free ones, and there are about twenty times as
many: the other group is now ruled out where the chosen placement's |F_obs - F_calc| is lower by 3
standard errors of the paired per-reflection difference over all reflections to the scoring limit.
Groups with different copy counts, and the tNCS carry rule, are unchanged.

Found on the timing benchmark's cytochrome C set started from the bovine AlphaFold model (the
sequence-exact one): three copies placed in both P 32 2 1 (R-free 0.427) and P 31 2 1 (0.437), so
MR_DECISION was UNDECIDED. Not a regression: the 2026-10-11 tNCS work ran this set with the horse
model, whose first copy did not place in P 31 2 1 at all, so P 32 2 1 was chosen with no tie (still
so at rc175 HEAD). With the 380 free reflections at 3 A the two hands differ by 0.010 +- 0.012 in
R-free; over all 7285 reflections by 0.029 at 11 standard errors. Now ENANTIOMORPH P 32 2 1, 3 copies,
placement CC 0.744.

MR replay survey of 193 sets (AlphaFold models): multi-copy UNDECIDED 7 -> 4, all three now correct
(7q6j P 21 21 21; both cytochrome C x06da sets P 32 2 1); one PAE-domain case (8tha) breaks its whole-
model hand tie towards the deposited group but stays UNDECIDED by the domain rule; no other set changes,
no wrong decision. Every tie the rule decided did so at 8.5-12.4 standard errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:43:33 +02:00
leonarski_fandClaude Opus 5.5 8f722227e8 Merge rc175 (bd7464d70) into gpumem-run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:39:31 +02:00
leonarski_fandClaude Opus 5.5 1c3a2e202c GPU memory over the run: FFT indexers given back after the last indexing; speculative probe takes turns with the merge on a small card
- IndexerThreadPool::ReleaseIndexers(): each worker drops the indexers it has
  built (once its current frame is done) and the allocation pool is trimmed, so
  their memory goes back to the card, not only to the pool. A later frame builds
  them again, as on first use. A rotation pass calls it once its first pass is
  done and again when the leftover-lattice census - the last of its indexing -
  ends. Measured on the RTX 5080: the 1.24 GB (pre-pass) / 0.62 GB (later
  passes) the pass's pool held through the image pass and the scaling is gone;
  4nwv's pre-pass image pass peaks 5.97 instead of 7.18 GB of device memory.
- GPUMerge: the main line holds one through ScaleMergeAndSymmetry. The
  speculative geometry probe (GPUWorkBeside(after_merges=true)) waits for it on
  a card below the take-turns size (12 GB), where the probe's FFT indexers and
  image workers beside the merge's arrays would overfill the card - on a
  Windows (WDDM) card that does not fail an allocation but pages it and crawls.
  The merge waits for nothing, so nothing the merge waits on (the rotation-scale
  walk's probes) can wait in a circle with it. No change on a 16 GB card.

Output unchanged: p.hkl byte-identical on cytc_x10sa, myob_x10sa, 4nwv and
8qaw on this card; also identical with the card's total read as 8.55 GB
(10 / 9 image workers instead of 16) and with 4 image workers, so the worker
count does not change the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:39:19 +02:00
leonarski_fandClaude Opus 5.5 bd7464d705 Merge scale-footprint round 2 into rc175: two-pass scaling ingest with the GPU/CPU decision before the card is touched, px/py and h,k,l kept on the host, estimate re-fitted to 56 B per observation + 99 B per distinct hkl, output unchanged
8qaw's scaling engine peak 10.76 -> 8.16 GB; p.hkl byte-identical on 15 battery sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:14:49 +02:00
leonarski_fandClaude Opus 5.5 f46dc303a3 Rotation scaling on the GPU: two-pass ingest, decided before the card is touched, output unchanged
- The resident ingest reads the reflections twice: first only what the sort
  and the cut need (frame position, resolution, background, raw-hkl key),
  then - once the GPU/CPU decision has been made on the exact counts - the
  observations the cut kept, written straight to their place. The
  observation arrays and the sort buffers are never on the card together,
  and no observation field is cut (held twice) any more.
- A set too large even for that sort scales on the CPU before anything is
  allocated on the card (ScaleOnCPUWhereSortTooLarge, same 80%-of-total rule).
- The predicted detector position never goes to the card: it is taken on the
  host, and the combine emits the index of each full's peak partial instead
  of its px/py (8 B less per observation and 4 per full).
- A full carries its raw-hkl run index instead of h, k, l (8 B less per
  full); the constant-1 partiality array of the fulls is gone.
- The fulls' CSR sorts and the error-model rank sort run on cub::DoubleBuffer.

Measured with per-array device accounting on an RTX 5080, largest engine of
each set, before (c130c7923) -> after, MB:
  set   obs/hkl          ingest peak (B/refl)        post-decision peak
  4nwv  32.7 M/15.4 M     3277 (100) ->  2336 (72)    4265 -> 3625
  8qaw 122.7 M/ 8.6 M    10756 (88)  ->  7421 (61)    9327 -> 8156
  9v16  54.7 M/ 5.7 M     4838 (89)  ->  3419 (63)    4283 -> 3774
  7qij  78.1 M/ 2.2 M     6768 (87)  ->  4707 (60)    5242 -> 4707
  8sa8  50.0 M/ 5.0 M     4107 (82)  ->  3134 (63)    4205 -> 3603
The sort before the decision now holds 32 B per reflection (46 on a P1-like
set with many runs); the engine's overall peak is after the decision on all
of them.

GPUScalingPeakBytes refitted on the same fourteen engines: 63 + 115 ->
56 B/obs + 99 B/hkl, within 9%. DeviceBytes as an upper bound: 66/94/52 ->
58/30/86 per obs/event/hkl, ingest 89/56 -> 60/42 per reflection/run.

p.hkl byte-identical to the rc175 baseline on 15 battery sets (4nwv, 8qaw,
9v16, 7qij, 8sa8, 11if, 5reo, 6oel, 9jzo, 9min, metformin, lyso_x06da_ref,
thau_bl1a_3p8keV, insu_I_x06da_5keV_2, myob_x06da); compare shows no change.
[RotationScale] (RotationIngestGPU now also checks the host positions) and
ErrorModel_DeviceFitIsTheHostFit pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:13:41 +02:00
leonarski_f d7d138fd25 Merge rc175 into scale-footprint 2026-10-15 10:37:58 +02:00
leonarski_f 9d8b06525f Merge rc175-metal-integration (bd27d802b) into rc175: CPU fulls scaling loop on a working copy, combine prefetches, output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	image_analysis/scale_merge/RotationScaleMerge.cpp
2026-10-15 10:16:01 +02:00
leonarski_fandClaude Opus 5.5 36062a55f1 Merge mv-gpu-budget into rc175: model validation plans its GPU parts against one budget, 80% of the card's total, after trimming the allocation pool and releasing the FFT indexers; extra engines only within half of it
On the RTX 5080 nothing moves between GPU and CPU and the output is byte-identical; on cards below
about 9-10 GB parts that used to fall back to the CPU now stay on the GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:15:42 +02:00
leonarski_fandClaude Opus 5.5 640b2e464a Merge scale-footprint into rc175: rotation scaling's GPU peak cut (allocation pool trimmed before scaling, arrays freed at last use, ingest sorts without cub's extra copies), estimate re-fitted to 63 B per observation + 115 B per distinct hkl, output unchanged
p.hkl byte-identical on the RTX 5080 for 4nwv, lysozyme, cytochrome C, myoglobin, 8qaw and 9v16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:57:21 +02:00
leonarski_fandClaude Opus 5.5 3373106ae1 Model validation: budget counts only what is really held; fewer extra engines
On an 8.55 GB card (WDDM) the validation planned 6.81 GB of a 6.84 GB budget, but the card already
held more than the 20% headroom allowed, and the driver paged GPU memory to system RAM: every
rigid-body fit ran ~5x slower. Measured on 4nwv on a 16 GB card, at the start of model validation
the device held 1.1 GB beyond a fresh context, and on 6oel 2.7 GB:
  - the stream-ordered allocation pool's free-but-reserved memory, left by the image passes and the
    scaling's pooled buffers (1.07 GB on 4nwv, 2.1 GB on 6oel); the validation's engines are allocated
    outside the pool and cannot reuse it;
  - the image pass's FFT indexers (IndexerThreadPool), alive for the whole run (0.8 GB on 4nwv,
    0.5 GB on 6oel), although nothing indexes after the merge.
Both are now released: the validation trims the pool (cuda_trim_allocation_pool) before it plans,
and rugnux drops the indexer pool once the leftover-lattice census, its last user, has ended, before
the model is validated or searched for. What is left beside the validation is 0.06 GB over a fresh
context on both sets (streams, idle cuFFT plans).

Extra engines are now added only within half of the budget. They change no number and bought little
where measured (4nwv, 16 GB card: 13.6 s with one rigid-body engine, 13.1 s with two, 12.6 s with
five), so the card keeps headroom instead. On 16 GB 4nwv now plans 2 rigid-body and 2 scale-fit
engines, 6.56 GB (measured peak 7.3 GB with the context); on an 8.55 GB card one of each, 4.42 GB.

Reflection files, maps and models byte-identical to rc175 HEAD on 4nwv, 8tyy, 6oel and 3inp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:56:15 +02:00
leonarski_f 1ae834e39c Merge rc175 into scale-footprint 2026-10-15 09:51:48 +02:00
leonarski_fandClaude Opus 5.5 c130c79236 Rotation scaling on the GPU: smaller device footprint, output unchanged
The scaling engine held far more on the card than any step needed, and the
image passes' pooled memory was still reserved when it started.

- The image passes' allocation pool is trimmed before the scaling's arrays
  are allocated (they are not pooled): 4.3-8.8 GB of the 16 GB card was held
  by others when the scaling started, now 1.5-2.3 GB (context, cuFFT and the
  run's FFT indexers, which live for the whole run).
- A new grouping or combine frees the previous one's fulls, CSRs and merge
  results before allocating its own; the final merge of a large cell used to
  hold the P1 search's per-group arrays beside its own.
- The partials' group CSR goes at the combine; the arrays only the fulls'
  scaling and download read (records, zeta, frame CSR, group means, image
  number, px/py, clipped) go once the fulls are on the host.
- Merge buffers live only in the call that needs them: the error-model sums
  and sample places, the outlier medians, half-sets and CC factors, the R_meas
  sums; MergeAccum's per-group results go once downloaded (MergeAccumRelease).
- The combine's per-run scratch is local to the combine.
- Ingest: both radix sorts run on cub::DoubleBuffer (no cub copies of keys and
  values), the sort outputs are allocated after the raw-hkl key is gathered and
  freed, the uncut runs and old order go before the observations are cut, and
  the delta_phi re-solve takes the hkl from its run, so the per-observation
  64-bit key no longer outlives the sort.

Measured with per-array device accounting on an RTX 5080, engine peak after
the GPU/CPU decision (MB, before -> after; largest engine of each set):
  4nwv  32.7 M obs, 15.4 M hkl:  7536 -> 4265
  8qaw 122.7 M obs,  8.6 M hkl: 11716 -> 9327 (ingest 12533 -> 10756)
  9v16  54.7 M obs,  5.7 M hkl:  5831 -> 4283
  7qij  78.1 M obs,  2.2 M hkl:  6108 -> 5242 (ingest  7978 ->  6768)
  8sa8  50.0 M obs,  5.0 M hkl:  5490 -> 4205
The ingest's own peak went from ~104 to 82-100 bytes per reflection.

GPUScalingPeakBytes refitted on fourteen engines of these five sets to the
peak after the decision it makes (the ingest has passed by then): 81 B/obs +
291 B/hkl -> 63 + 115, within 12%. DeviceBytes refitted as an upper bound:
67/260/200 -> 66/94/52 per obs/event/hkl (up to 18% over), ingest
103/58 -> 89/56 per reflection/run. With it the all-observation arm now runs
beside the first merge on a 16 GB card for 4nwv, 9v16 and 8sa8.

p.hkl byte-identical to the rc175 baseline on 15 battery sets (the five above,
11if, 5reo, 6oel, 9jzo, 9min, metformin, lyso_x06da_ref, thau_bl1a_3p8keV,
insu_I_x06da_5keV_2, myob_x06da), p.mtz identical apart from the history;
battery compare shows no change. [RotationScale] and
ErrorModel_DeviceFitIsTheHostFit pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:49:11 +02:00
Filip LeonarskiandClaude Opus 5.5 bd27d802b7 RotationScaleMerge: CPU fulls scaling loop on a working copy, combine prefetches, output unchanged
Build Packages / Create release (push) Successful in 18s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m53s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m37s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 12m1s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m35s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 22m23s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m33s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m13s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 11m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m58s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 15m55s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m20s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m14s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 20m30s
Build Packages / Build documentation (push) Successful in 51s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 14m3s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 20m46s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 14m33s
Build Packages / Unit tests (push) Successful in 1h26m4s
Build Packages / Validation Battery (push) Failing after 36m1s
The fulls' scaling loop now works as the partials' loop does since 4e177bdb2: a compact copy of the
fulls in the group CSR's order (ScalingRef, corr beside it), so the group means read sequentially
instead of gathering eighty-byte Obs through fulls_group_perm, and the per-frame fit over a
ScalingFit list built once with the Unity model's filters (FitScalingFrames, which takes the minimum
count as a parameter). FitPerFrameG, SolveScale and ReduceFullGroupMeans had no callers left and are
gone; their rationale comments moved to where the filters and the fit now are.

Combine prefetches the partials of the raw hkl 8 runs ahead: a run's partials lie a frame apart in
memory and the work per rocking event kept the CPU from reaching the next runs' loads, so it waited
on a cache miss for nearly every partial.

Same values, same operations in the same order: p.hkl and the merged MTZ data are byte-identical on
lysozyme, cytochrome C, myoglobin, a small-molecule set and 4nwv. Measured on 4nwv (Apple M6, two
interleaved runs each): the combine-and-scale-fulls stages 12.6 / 12.7 s -> 5.6 / 8.9 s; the scaling
steps (5 + 9) 54.6 / 53.0 s -> 52.0 / 51.0 s; total wall time is within run-to-run noise (127-131 s).
Cytochrome C scaling and merging 3.35 / 3.43 s -> 3.19 / 3.19 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:28:20 +02:00
leonarski_fandClaude Opus 5.5 b9fd1c8d75 Merge timing-bench into rc175: battery.py timing, a three-set benchmark (myoglobin and cytochrome C from AlphaFold DB models, 4nwv from a moved deposited model) placed by molecular replacement, checked and timed per step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:25:34 +02:00
leonarski_fandClaude Opus 5.5 c28f7683c8 Merge rc175-metal-integration into rc175: Metal Bragg integration engine on macOS; CPU profile fit in single precision with a shared exp (result-changing at ulp level on the CPU path only); CPU partial scaling loop reads its working copy in order
The CUDA engine is untouched: on the RTX 5080 p.hkl is byte-identical to rc175 on 4nwv, lysozyme
and cytochrome C. Under --no-gpu (lysozyme, cytochrome C, myoglobin) intensities move at the ulp
level from the single-precision fit and nothing changes beyond noise in battery compare; wall
44/117/71 s -> 41/105/65 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:25:26 +02:00
leonarski_fandClaude Opus 5.5 f99ed0b5ac battery.py timing: a three-set benchmark placed by molecular replacement, checked and timed per step
`battery.py timing --rugnux BIN` runs myob_x10sa and cytc_x10sa from their AlphaFold DB
predictions (AF-P68082-F1 and AF-P62894-F1, v6, committed verbatim with their predicted
aligned error under tools/battery/models/, CC BY 4.0) and 4nwv from its deposited model with
strict NCS expanded and moved by a fixed rigid-body transform (30 deg about (0.36, 0.48, 0.80)
through the centroid, shift (12, -18, 25) A), made at run time from the site's pdb_cache. Each set
runs the battery's own command for its arm, in an ordinary run directory, so the site config,
results.json, report, table and compare all apply.

A set passes when the battery's own verdict passes, MODEL_PLACEMENT is MR, MR_SPACE_GROUP is the
expected group with its hand, R_FREE is under its limit, CC_MODEL_CONFIRMED_TO_D_MIN is not NONE,
and the placed model's intensities correlate with the reference model's (|F|^2, shell-normalised,
10-4 A, best over the lattices' changes of basis) at 0.30 or more. That last check is independent
of rugnux's scores: correct placements give 0.55 / 0.90 / 0.96; the myoglobin placement turned by
30 deg with rugnux's report unchanged gives 0.035, the unplaced 4nwv start model 0.011, a
placement relabelled to the other hand 0.18. Rows carry the checks and the per-step times from the
TIMING step lines; the report gets a Timing benchmark section; the run exits 1 on any failure.

On the workstation (RTX 5080, rc175 8eacce): myoglobin passes in 10 s (R-free 0.44), 4nwv in 106 s
(MR 34 s, R-free 0.315 against 0.297 for the deposited placement). Cytochrome c fails: MR places
three copies of the monomer in both P 32 2 1 and P 31 2 1 within 0.03 in R-free and so places
neither (MR_DECISION UNDECIDED), where the three-copy reference model decides the hand at once.

battery.py now imports fcntl only for the full-run lock, falls back to Popen.kill where there are
no process groups, and keeps the binary's extension in the run directory, so a timing run can work
on Windows (untried there).

Licence and credit: licenses/alphafold-db.txt (from models/LICENSE.txt via COLLECT.sh), a new
"Data files in the repository" table in THIRD_PARTY_NOTICES.md, and the AlphaFold DB with Jumper
et al. 2021 and Varadi et al. 2022 and 2024 in docs/ACKNOWLEDGEMENT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:01:46 +02:00
Filip LeonarskiandClaude Opus 5.5 229a432a66 RESULT-CHANGING (owner approved, ulp level): Metal profile fit, CPU Gaussian from a shared exp
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m54s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m49s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m58s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m10s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m35s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m13s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 25m6s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m15s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m34s
Build Packages / Generate python client (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m1s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m5s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m36s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 24m54s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m7s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 25m53s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 28m8s
Build Packages / Unit tests (push) Failing after 3h11m38s
Build Packages / Validation Battery (push) Successful in 33m42s
Pass B of BraggIntegrationEngineCPU is split where a device can take over: per reflection, what the
fit needs is worked out in double (BraggFitSetup), the profile, its readable mass and the reweighted
fit run in single precision (FitProfile -> BraggFitOut), and the error, the summation guard and the
result are finished in double. BraggIntegrationEngineMetal runs the middle as a Metal kernel, one
thread per reflection, summing in the CPU's order (FitOnDevice); the CUDA engine is untouched.

The Gaussian's exp is now BraggExp (BraggExp.h), on the CPU and on the GPU: no GPU exp reproduces the
host's expf, and BraggExp uses only +, -, * and exact power-of-two scaling, so both compile it to the
same bits. Checked on the Apple M6 over every float in [-110, 89] (2.2e9 arguments), with subnormal
products and sums matching too. It is closer to correctly rounded than macOS expf (max 0.53 ulp;
0.2% of arguments below -1 not correctly rounded vs 0.62%). This is the one result change: per
observation |dI|/sigma p99.9 <= 1.7e-6, max 1e-4, 10-30x below what ba3f75475 moved. Merged
statistics (resolution, completeness, CC1/2, R_meas, I/sigma, ISa, space group) are identical on
lysozyme, cytochrome C, myoglobin, a small-molecule set and 4nwv; only tNCS fields that flip on any
last-digit change move (cytochrome C's pseudo-translation flag, whose modulation ratio sits at the
2.5 threshold, turns off).

The Metal engine stays bit-identical to the CPU engine: BraggIntegrationEngineMetalTest passes
unchanged, and catches planted kernel bugs (another exp, 3 iterations, a dropped lost-peak test, <=
for the disk, inverted exclude logic).

Measured wall time, Apple M6 (CPU pass B -> GPU pass B): lysozyme 24.7 -> 18.6 s, myoglobin 39.8 ->
35.7 s, cytochrome C 59.3 -> 49.0 s, 4nwv 150.2 -> 131.3 s; the small-molecule set, sparse frames,
92.5 -> 95.6 s (noise of +-3 s). Metal's fast::exp in the kernel instead measured the same on every
set (18.6, 35.8, 49.1, 131.2 s), so it would buy nothing for the bitwise check it would cost. On the
CPU alone BraggExp costs nothing measurable (cytochrome C --no-gpu 72.8 -> 73.1 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:18:43 +02:00
leonarski_fandClaude Opus 5.5 0d25d765cf Model validation: one GPU budget for all its parts, 80% of the card's total
Build Packages / Create release (push) Successful in 22s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Failing after 1m31s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 9m12s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m39s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m39s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m36s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m15s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m15s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m8s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 17m13s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m33s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m58s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 24m10s
Build Packages / Build documentation (push) Successful in 1m9s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m10s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m23s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m59s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 21m53s
Build Packages / Unit tests (push) Successful in 2h5m52s
Build Packages / Validation Battery (push) Successful in 36m32s
What a validation puts on its GPU was decided part by part on fixed fractions of the card: the
structure factors to d_min on a quarter, the null's structure factors on whatever of that quarter
the first left, the rigid-body engines on another quarter, the scale fits on an eighth. On an 8 GB
card that sent the null's structure factors of a 4.4 GB validation to the CPU (and held the rigid
body to one engine) although everything the validation holds fits the card with room to spare.

Now PlanModelValidationGPU decides all of it at once, before anything is reserved, from the card's
total memory: every engine is reserved up front and kept to the end, so their sum is held against
one budget, 80% of the card less what a merge engine still at work holds there (the finalist
ledger's P1 merge, RotationScaleMerge::DeviceBytes). Each part takes one engine where it still fits,
in the order structure factors to d_min, rigid body, the null's structure factors, scale fits; a part
that does not fit runs on the CPU. What is left goes to more rigid-body engines (up to one per null
replicate plus the real fit) and then more scale-fit engines, which only change how many fits run at
once. A second validation may run beside where twice this one's whole GPU plan fits the cards'
budgets; it gets this one's bytes as gpu_beside_bytes, which limit its engine counts and never its
placement, so its numbers are those of the validation it stands in for. One log line states the
placement, the bytes held and the budget.

- RigidBodyGPUPool::Plan sizes the pool without engines (EngineBytes), Reserve(n) makes them; Create
  (Plan + engines on a quarter of the card) stays for molecular replacement, whose own rules, and the
  --mr hand check's structure factors (ReadModelAnomalousMap, a quarter), are unchanged.
- The null's resolution is worked out before the plan, so its engine is decided and reserved with
  the others instead of after the indexing probe.

Placement changes only on smaller cards: with the logged footprints of the open-arm sets, 4nwv's
null structure factors move to the GPU below ~9 GB (and its rigid body at 6 GB), 8tyy's structure
factors to d_min below ~8.7 GB; on 16 GB and larger nothing moves. On the RTX 5080 (16 GB), 4nwv,
8tyy, 6oel and 3inp give byte-identical reflection files, maps and models to rc175 HEAD, with more
rigid-body engines where they fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:05:50 +02:00
Filip LeonarskiandClaude Opus 5.5 4e177bdb21 RotationScaleMerge: CPU partial scaling loop reads its working copy in order, output unchanged
The loop's corr now lives in the group CSR's order next to the references (the partials in no group
after them), so the group means read both sequentially instead of gathering corr through the
permutation, and the corr update walks the same arrays. FitPerFrameG's filters, which do not change
over the loop, are applied once into a per-frame ScalingFit list with the reference-independent
factors precomputed (FitScalingFrames). ReferenceWeight is inline, and the group means are no longer
pre-filled with NaN serially.

Same values, same operations in the same order: p.hkl and the merged and unmerged MTZ data are
byte-identical on lysozyme, cytochrome C, myoglobin, a small-molecule set and 4nwv. On 4nwv (Apple
M6, 12 threads) the partial scaling iterations take 3-4x less time, ~18 s -> ~6 s over 168
iterations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:50:16 +02:00
Filip LeonarskiandClaude Opus 5.5 f05e67b2b4 Metal Bragg integration engine on macOS, output unchanged (bit-identical to the CPU engine)
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m16s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m41s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m24s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m56s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 22m55s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m7s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 13m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 19m6s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 19m0s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m33s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m43s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m21s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m25s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 19m20s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 24m18s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 23m34s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 24m21s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m7s
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / Validation Battery (push) Successful in 35m52s
BraggIntegrationEngineMetal is BraggIntegrationEngineCPU with its integer pixel work run as Metal
kernels, one thread per reflection: the neighbour mask, the owner map, pass A's walk over every box
and the background clip. Everything in double, the profile learning and pass B stay on the CPU, in
the CPU engine's own code - Apple GPUs have no double, and no GPU exp matches macOS expf bit for bit.
The sums are integers, so the GPU's order does not matter; the stencil arithmetic is the same
BraggStencil.h, compiled at run time (MSL 3.0, no offline Metal toolchain needed) with fast math off
(sqrt and division correctly rounded, measured) and fp contraction off, as the host already builds
(-ffp-contract=off, now extended to Objective-C++). The image is read in place (unified memory).

The CPU engine is split so both engines share the finishing code: pass A counts into BraggBoxSums
(integer; the double ring sums were exact integers already), the clip threshold is its integer floor,
FinishPassA and ProfilePasses are shared. Verified byte-identical p.hkl and MTZ data before/after the
split and with the Metal engine on lysozyme, cytochrome C, myoglobin, 4nwv and a small-molecule set.

Measured on an Apple M6 (wall, CPU engine -> Metal): 4nwv 180.5 -> 151.2 s, cytochrome C 72.7 ->
59.8 s, lysozyme 29.2 -> 22.5 s, myoglobin 43.3 -> 41.2 s; the sparse small-molecule set +1.5 s
(few reflections per frame leave the GPU idle).

CI: a macOS step runs tests/BraggIntegrationEngineMetalTest.cpp, which requires every float of every
reflection and every count to match the CPU engine over 19 setting combinations on random crowded
frames (negative pixels, exact-pixel ties, hot, masked and overloaded pixels), three frames per
engine; it fails without a Metal device and if the GPU path fell back to the CPU. Ten planted kernel
bugs were each caught. rugnux --no-gpu hides the Metal device too (gpus_are_hidden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:18:42 +02:00
leonarski_fandClaude Opus 5.5 228f0fc00f SpotTiles_MatchWholeFrame_GPU: wait for the upload before the squares are cut
Build Packages / Create release (push) Successful in 7s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Failing after 1m18s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 8m39s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 12m27s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 17m23s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 15m5s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m8s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 20m52s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m35s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m45s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 16m28s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 1m6s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m46s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 21m12s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 24m56s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m25s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 24m47s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m16s
Build Packages / Unit tests (push) Successful in 1h52m27s
Build Packages / Validation Battery (push) Failing after 36m29s
cudaMemcpy from pageable memory may return before the copy lands on the card, and
ImagePreprocessorBufferGPU gathers its squares on a non-blocking stream that does not wait for the
NULL stream, so on a loaded GPU the squares could be cut from the previous frame. Test-only race;
the pre-scan's engines synchronise their own streams before the buffer is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:35:48 +02:00
leonarski_f 8eacce02f7 Merge unified-nvtx into rc175: NVTX range per timed section and console step; kernels read host buffers in place on unified-memory GPUs (discrete cards unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	common/CUDAWrapper.cu
2026-10-14 22:13:14 +02:00
leonarski_f c17002379c Merge cufft-cache into rc175: cuFFT plans reused within a rugnux run and destroyed when it ends, output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	image_analysis/geom_refinement/BeamCenterFFTGPU.cu
2026-10-14 22:12:53 +02:00
leonarski_fandClaude Opus 5.5 7f57cfb8d4 Merge gpucpu-decide into rc175: GPU or CPU decided up front from the card's total memory, never from what is free
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:12:44 +02:00
leonarski_fandClaude Opus 5.5 3917bb9c79 cuFFT plans kept only within a processing run's scope, destroyed when it ends, output unchanged
The plan cache of 310222156 never destroyed its plans: a long-lived process would carry every
shape it ever planned, and rugnux's cudaDeviceReset at exit (release_gpu_contexts) left the static
cache holding handles of a reset context. Plans are now kept only while a CudaFFTPlanScope is alive
(reference counted); when the last one ends every kept plan is destroyed on its own device, and
outside any scope a plan is destroyed as soon as its user is done - what every cuFFT user did
before the cache. rugnux opens one scope for the whole run (FFTPlanReuse, a CUDA-free wrapper, in
RunRugnux), so all its modes keep the reuse and the plans go before the device reset.
jfjoch_viewer and jfjoch_broker open none: their plans live exactly as long as the indexer that
holds them, as before, and nothing carries over to the next processing or collection.

Measured with nsys on an RTX 5080: cuModuleLoadData unchanged from 310222156 (cytc_x10sa 303,
4nwv 161, down from 450/458 before the cache); every load is now unloaded at the end of the run
(303/303, 161/161). An idle plan of the indexer shape (3142 x 16384 R2C) holds 1.6 MB of device
and 1.3 MB of host memory, a 3D grid plan 0.26 MB device. Outputs bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:11:56 +02:00
leonarski_fandClaude Opus 5.5 3102221568 cuFFT plans kept for the process and lent to one user at a time, output unchanged
Every cuModuleLoadData/Unload in a run is cuFFT making or destroying a plan, and each plan loads
its own kernel modules under a process-wide lock: over a second a plan on a busy GB10, and a PTX
compile per load on Windows. The FFT indexers, rigid-body engines, structure-factor engines,
molecular replacement and beam-centre FFT each made their plans afresh and destroyed them again,
and the sizing functions (rigid-body and structure-factor FFTWorkBytes, beam-centre
DeviceMemoryNeeded via cufftEstimate*) made throwaway plans only to read a work size.

CudaFFTPlan now borrows a plan from a per-process cache keyed by device and full plan shape
(CudaFFTShape: n, embeds, strides, distances, type, batch). A plan is lent to one user at a time
(cuFFT is thread safe only across distinct plans), goes back when its owner is done - after a wait
for the owner's stream - and is never destroyed. Kept plans hold no work area: the user passes its
own, or CudaFFTPlan allocates one from the pool while it is borrowed. The sizing functions read the
work size from the kept plan (CudaFFTPlan::WorkBytes), which the engines then reuse. Plan creation
keeps the NULL-stream wait of 90ba15871, which now also covers the rigid-body, structure-factor
and molecular-replacement plans that were raw cufftHandles without it. A rigid-body engine hands
its previous zone's plans back on SetZone. FitsInDeviceMemory answers false where cuFFT cannot
plan the transform, as the ignored cufftEstimate failure did.

Measured with nsys on an RTX 5080: cuModuleLoadData/Unload 450/450 -> 303/51 on cytc_x10sa,
458/458 -> 161/51 on 4nwv (the remaining unloads are cuFFT's own inside plan creation; the
remaining loads are plans that are alive at the same time, mostly the concurrent rigid-body
engines). Wall unchanged on this workstation (ABAB), where a load costs ~1 ms; the gain is for
GB10 and Windows. Outputs bit-identical (p.hkl and the refined model on 4nwv, cytc_x10sa,
lyso_x06da_ref); cufftPlan3d vs cufftMakePlanMany with a caller work area checked bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:54:26 +02:00
leonarski_fandClaude Opus 5.5 e3b33591ca GPU: kernels read host buffers in place on unified-memory systems
gpu_direct_host_read() (CUDAWrapper) is true where the device accesses pageable
memory through the host's page tables (cudaDevAttrPageableMemoryAccess and
...UsesHostPageTables: GB10, GH200, GB200), asked once per device;
JFJOCH_FORCE_DIRECT_HOST_READ forces it on for testing with page-locked inputs.
Where it holds:
- BSLZ4DecoderGPU: the LZ4/fused kernels and the tail copy read the reader's
  compressed chunk directly, no pinned staging memcpy and no H2D copy;
- ImagePreprocessorGPU: Analyze and the UINT16_WIDENED path hand the host
  buffer to preprocess_kernel (if aligned to the pixel type), and nothing is
  page-locked;
- RotationScaleMergeGPU::IngestUpload: IngestExtractKernel reads the pinned
  stage in place, without device staging buffers; the stage is reused only
  after the kernel has run.
Discrete cards take the unchanged path. Every caller synchronises its stream
before the host buffer goes away. DirectHostReadGPUTest checks the three
sites against the CPU on page-locked inputs, in either mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:49:37 +02:00
leonarski_fandClaude Opus 5.5 fe4db26716 rugnux: an NVTX range per timed section and per console step
Every TimingMark ("TIMING <section>") and every rugnux step ("TIMING step ...") now
opens an NVTX v3 range of the same name, so Nsight Systems traces from different
machines can be lined up by pipeline step. Start/end ranges, not push/pop, so a
range may end on another thread and parallel arms may overlap. Header-only NVTX
(CUDA::nvtx3), a no-op without a profiler attached and compiled out without CUDA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:49:37 +02:00
leonarski_fandClaude Opus 5.5 5de7189643 GPU or CPU decided up front from the card's total memory, never from what is free
- Scaling: the 80%-of-card check runs right after the device ingest's sort and cut, where the
  observation and distinct-hkl counts are first known, instead of after the geometry smoothing;
  same counts, same decision, but a run that goes to the CPU no longer does the device smoothing
  (6.9 s on an 8 GB card for a 33 M observation set) or holds its memory meanwhile.
- Rigid body: engines budgeted on a quarter of the card's total (the validation's plan), not
  min(total/4, free - 1 GB). A busy card no longer sends the rigid body to the CPU (measured on an
  8 GB card: ~80 s and a different fit); engine allocations wait for GPU work beside the run, and
  an engine that cannot be set up fails the validation (RigidBodyGPUFailure) instead of falling
  back to the CPU.
- Beam-centre FFT: GPU when its footprint is at most half of the card's total, otherwise CPU,
  logged; no catch-and-rescore on the CPU after a GPU failure.
- FitPerFrameG reserves its per-frame vector (CPU scaling waited on the heap lock on Windows).
- get_gpu_free_memory(): cudaMemGetInfo, except on an integrated GPU under Linux, where free also
  counts MemAvailable less a tenth of the total. Used only where free memory is waited on
  (cuda_memory_available) or quoted in out-of-memory messages; it decides nothing.
  The two card-filling [CUDAMemHelpers] tests skip on integrated GPUs.
- Out-of-memory messages point at rugnux --no-gpu instead of "the CPU build".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:28:38 +02:00
leonarski_fandClaude Opus 5.5 f96ee13629 Merge rc175-mac exact commits into rc175: CPU spot-finder ring histogram value-first; CPU Bragg background clip reuses pass A's ring pixels; integrated reflections relabelled in parallel; tNCS modulation-null searches on several workers (all output unchanged)
ba3f75475 (CPU profile fit in single precision) left out: it made CPU-vs-GPU
agreement 3-5x worse (2-4e-7 -> 0.6-1.9e-6 relative) for 1.5-1.9% on CPU-only runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:53:11 +02:00
Filip LeonarskiandClaude Opus 5.5 ba3f754752 RESULT-CHANGING (owner approved, ulp level): CPU profile fit in the GPU engine's arithmetic
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m55s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m12s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 12m20s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 22m9s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m11s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 10m37s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 13m30s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m22s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m5s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 18m9s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m7s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m10s
Build Packages / Generate python client (push) Successful in 21s
Build Packages / Build documentation (push) Successful in 1m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m6s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m42s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m34s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 19m24s
Build Packages / Unit tests (push) Failing after 1h54m39s
Build Packages / Validation Battery (push) Failing after 41m38s
Pass B of BraggIntegrationEngineCPU now does what BraggIntegrationEngineGPU's fit does: profiles in
float, the Gaussian from expf with the widths' reciprocals taken once and normalised by a reciprocal,
and the reweighted fit in float with one reciprocal of the variance for its three sums (the GPU's
__frcp_rn). Per-reflection geometry and the final sigma stay in double. The logic was already the
same on both; the CPU now differs from the GPU only in summation order.

Measured (CPU-only, Apple M6): Pass B loops 1.2x faster on P cores; cytochrome C 1.9%, myoglobin 1.5%
faster end to end, 4nwv integration steps 3%. Intensities move by ~1e-6 relative; merged statistics
in the fourth digit (error model b 1.3259e-3 -> 1.3258e-3, R_work 0.2440 -> 0.2439).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:31 +02:00
Filip LeonarskiandClaude Opus 5.5 d1f4c86298 tNCS: the modulation null's searches on several workers, output unchanged
The 20 shuffled copies are drawn in order from the one generator as before, then each greedy search
runs on a worker of its own and reads only its copy, so the nulls are the serial loop's bit for bit.
Measured on 4nwv (CPU-only): the analysis was ~3.9 s on the main thread, most of it these searches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:31 +02:00
Filip LeonarskiandClaude Opus 5.5 ec02b181db rugnux: relabel the integrated reflections in parallel, output unchanged
reseat_integrated changed the basis of every integrated reflection on the main thread. One image per
task now; each touches only its own outcome. Measured on 4nwv (CPU-only): 2.1 s of serial time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:31 +02:00
Filip LeonarskiandClaude Opus 5.5 1c748ba41e BraggIntegrationEngineCPU: background clip reads the ring pixels pass A gathered, output unchanged
The one-pass sigma clip of the background ring walked the reflection's whole box a second time,
recomputing every pixel's stencil distances and neighbour mask. Pass A now keeps the ring pixels the
clip reads (readable, not a neighbour's flux), with their radial offset, in pixel order, and the clip
sums those - the same values in the same order.

Measured on cytochrome C (CPU-only, Apple M6): the clip loop was 8.4% of CPU; the run is 4-5% faster
with this and the ring-histogram change together. Merged reflections and maps byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:31 +02:00
Filip LeonarskiandClaude Opus 5.5 93958cfdc7 AdaptiveSpotFinderCPU: ring histogram indexed [value][ring], output unchanged
The per-ring value histogram of the plain ring pass was laid out [ring][value], so the counters of
neighbouring pixels - neighbouring rings, low values - lay 4 KB apart and fell in the same few L1
sets, evicting each other on every pixel. Indexed [value][ring] they share a few cache lines. The
sigma-clip passes read it value-major; integer sums, so the same totals.

Measured on a 16M-pixel frame (microbenchmark of AccumulateRingsBlock, Apple M6): 0.97 -> 0.80
ns/pixel on a P core, 5.3 -> 3.7 on an E core. The function was 9-38% of CPU on the battery sets run
CPU-only; one small-molecule set ran 4% faster end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:31 +02:00
Filip LeonarskiandClaude Opus 5.5 212fa457d6 CI macOS: hardened-runtime checks read codesign's output from a file
Build Packages / Create release (push) Successful in 6s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m21s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m16s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m0s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 17m35s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m32s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m40s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 16m39s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 16m33s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 13m50s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m0s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m28s
Build Packages / Generate python client (push) Successful in 20s
Build Packages / Build documentation (push) Successful in 1m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m57s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 23m9s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m19s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 24m24s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m59s
Build Packages / Unit tests (push) Failing after 2h0m28s
Build Packages / Validation Battery (push) Successful in 52m44s
The viewer-bundle and rugnux-tarball checks piped `codesign -d` into `grep -q`. grep exits at the
first match, codesign then dies of SIGPIPE (141), and under `set -o pipefail` that is the pipeline's
status, so a correctly signed bundle was reported as "no hardened runtime". Both checks now write to a
file and grep it, as the other checks in this workflow do, and print the signature when they fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:42:06 +02:00
leonarski_fandClaude Opus 5.5 76adf3f0fa Merge origin/rc175 (macOS signing and notarization) into rc175
Build Packages / Create release (push) Successful in 8s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m53s
Build Packages / build:portable:macos-arm64:nocuda (push) Failing after 10m37s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m38s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 19m15s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m49s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 23m5s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 23m47s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m39s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m52s
Build Packages / Generate python client (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m25s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 21m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m32s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m48s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 18m46s
Build Packages / Unit tests (push) Successful in 1h37m57s
Build Packages / Validation Battery (push) Successful in 37m52s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:47:07 +02:00
leonarski_fandClaude Opus 5.5 082781490a Merge stillsint into rc175: the stills pre-scan masks pixels holding the error value and dead pixels (no false overloads; +4% observations at high resolution)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:32:34 +02:00
leonarski_fandClaude Opus 5.5 3f25bbf003 Merge fftpeak exact commits into rc175: GPU peak search four threads per direction; CudaFFTPlan waits for cuFFT's NULL-stream plan setup (latent race)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:37:54 +02:00
leonarski_fandClaude Opus 5.5 90ba15871f CudaFFTPlan: wait for the plan's NULL-stream setup before it is used, output unchanged
A transform launched on a non-blocking stream straight after cufftPlanMany could read the
plan before its setup, still queued on the NULL stream, had finished: Xid 31, a read at a
fixed near-null address, "an illegal memory access" in the indexer threads. It showed at
transform length 3072, in 3 of 4 runs of one rotation set where a pool built its FFT
indexers at the start of integration (6 of 28 ci-tier sets, 9 of 25 null-model runs).
It went away under CUDA_LAUNCH_BLOCKING=1 and with a cudaStreamSynchronize(NULL) after
the plan (0 of 11 runs). The 3142 length the indexer used so far never hit it, but
nothing ordered it either.

Plans are built once per indexer, so the wait costs nothing measurable. The other cuFFT
users (beam-centre FFT, model structure factors, rigid body) create raw plans and are not
covered by this wrapper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 16:40:34 +02:00
leonarski_fandClaude Opus 5.5 eb53b11b88 Merge stillsmerge into rc175: stills crystals indexed in a lattice containing the run's are relabelled onto its cell; unmerged MTZ only for crystals on the run's cell; French-Wilson on the merge's workers
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:34:40 +02:00