51706641fae540d90f09c26dcff667dc05ffe060
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
30cf5856d4 |
docs: bring the rugnux and analysis pages up to the code (MR fallback, GPU decode, live runs)
- CPU_DATA_ANALYSIS_IMAGE §0: CBF byte-offset and marCCD widening are decoded on the card; the host decoders are the CPU build's path. - CPU_DATA_ANALYSIS_DECISIONS §14: the null runs for every model the rigid body is tried on; the device choice is made once on the card's total (structure factors and rigid body a quarter, scale fits an eighth) and a CUDA failure ends the validation, deciding nothing; a subgroup the model fits is written as <prefix>_mr; new §14.10 on molecular replacement (model kind, rotation and translation functions, likelihood, Matthews, tNCS and P1 rules, null, what it may change). - CPU_DATA_ANALYSIS: scope and references gain molecular replacement. - CPU_DATA_ANALYSIS_INDEXING: the near tie decided early and re-checked once every frame is in. - RUGNUX: merging is the default and _process.h5 optional; live following of a collection with a broker URL; --model's MR fallback. - RUGNUX_INSTALL, RUGNUX_OVERVIEW, RUGNUX_TUTORIAL, RUGNUX_REPORT: GPU work list, <prefix>_mr outputs, MR in --mode mx for rotation and stills, rigid body only in --mode scale. - RUGNUX_ADVANCED: -N default as the usage states it. - battery README: the ci tier has 28 sets. - ModelValidation.cpp: a comment still said a GPU failure reran the validation on the CPU (comment only). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
9cb19753c7 |
Merge branch 'gpu-modelval' into rc175: --model validation's scale fits on the GPU, change-of-basis scoring gridded once in parallel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
d0d225cb99 |
rugnux --model: molecular replacement where the rigid body does not place the model
One --model now does the right thing by itself: - A prediction (AlphaFold/ModelCIF, read from the file) goes straight to molecular replacement. - An experimental structure is placed by the rigid body as before. The null now runs on every --model run (no NOT_TESTED): "placed" = the fit beats its null (4.51 sd) in the answer's merge or in the twin hypothesis' merge. Placed: behaviour unchanged (hand and reindex may go into p.mtz); a subgroup the model fits and prefers is written as <prefix>_mr. - Not placed (or no crystal cell): molecular replacement with the experimental-model path. It may relabel the hand into p.mtz past the anomalous veto, never reindexes; another group or the twin subgroup goes only to <prefix>_mr. Maps, R-free and CC come from validating the MR placement with every decision switched off; MODEL_FIT stays the rigid body's verdict. - The twin hypothesis' merge is made in memory with a model where a metric re-ask promoted the group or the model's group has fewer rotations than the adopted one; written and reported only where the model chooses it. - --mr is kept as "--model, searched for by MR straight away". - Report: MODEL_PLACEMENT= RIGID_BODY|MR|NOT_PLACED, MODEL_DECISIONS_TAKEN gains MR_ENANTIOMORPH, MODEL_CHOICE_FILE_PREFIX/SPACE_GROUP; the MR keys move into 5. MODEL VALIDATION; MODEL_FIT absent where no rigid body ran. MR NOT_PLACED is never a warning. - Battery wrong-model step: check (c) also fails a wrong model placed by MR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
1fe73bab41 |
Model validation: scale fits on the GPU, change-of-basis probe gridded once per frame on all threads
The scale fit of every model-validation fit (fit_model: the real model, the indexing probe's relabellings and every null replicate) was FitModelScale on the host - a long-double Levenberg-Marquardt per solvent grid point, ~80% of the validation's CPU on a 400 A cell. It now runs on the GPU through ModelScaleGPU::FitSolvent, the engine the rigid body and molecular replacement already use, from a small pool (ModelScaleGPUPool: up to four engines with their own streams, sized on the card's total memory, at most an eighth of it). The grid, the fits and the winner rule are FitModelScale's; the sums are taken in double on the device, so R-factors agree with the host's to rounding. The CPU build, and a GPU build without a card, keep FitModelScale. The change-of-basis probe gridded each candidate frame with gemmi's serial put_model_density_on_grid, once for the data as merged and once more for each alternative indexing. It now grids each frame once, with ModelGrid's parallel PutModelDensityOnGrid (the same grid, bit for bit), from threads outside the pool so the gridding spreads over it, and scores every labelling on that F_calc. Exact. 4nwv (122k atoms, I222, 400 A cell): model validation 33.2 s -> 14.0 s; every logged R, sigma and decision the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
ee350f1f74 |
rugnux --model: a wrong model confirms no shell, fits no hand, and fits the GPU reserve
Three defects the battery's wrong-model null found (5 sets x 5 deposited models of unrelated proteins): CC(model, data) significance. The per-shell sigma was Fisher's transform on independent reflections, but the data and a scaled model share the falloff across a shell, the anisotropic B fitted to these very data, the bulk solvent and the symmetry enhancement epsilon - whatever the model is. 21 of 24 wrong models read CC_MODEL_CONFIRMED_TO_D_MIN to the data's d_min (10-15 sigma in most shells). The correlation is now formed on I/epsilon and |F_model|^2/epsilon, and its null is empirical: the data at h against the model at h + d for the 26 nearest lattice offsets, which keeps everything smooth in reciprocal space and loses what the model knows of h. SIGMA = (z - mean) / max(sd, 1/sqrt(N-3)) in Fisher z over those 26, a new CC_NULL column shows the null, and a shell confirms only beside a coarser shell that also clears 3 sigma (one shell alone did so once in 216 wrong-model shells). Prototyped on the p_maps.mtz of a full open-arm run and the null runs: wrong models confirm 0/24 (was 21/24), the deposited models confirm 197/200 (was 198/200 in the prototype's reading; the one lost has CC_MODEL 0.05-0.13 in every shell). The report no longer says a model cannot agree by accident. MODEL_FIT gate. Accepted at 3.0 sample sds of a nine-draw null, as if the sd were known; an unrelated P1 helical model on a P3(1) helical crystal was ACCEPTED at +3.05. Against 48 draws it reads +2.09 and the best random placement fits as well as it does, and drawing the null about the cell axes instead changes nothing (+2.15): the cause is the nine-draw sd, not the orientation. The gate is now 3 sigma allowing for that: t(8) * sqrt(1 + 1/9) = 4.51. The deposited models of the open arm read +7.0 and up. The indexing lead stays at 3 sigma (INDEXING_MARGIN_SIGMA): it is read only behind the fit gate, and a deposited model settles one at +4.15. Rigid-body GPU reserve. The finest zone's observations were counted by the data's d, but the zones count them by the d of the index in the model's ASU, and a model's group need not keep the data's metric (an I2(1)3 model on I222 data: 2320 in the zone against 1712 reserved), so the validation died with "zone over the engine's reserve". Counted the zones' way, under every indexing probed. Tests: ModelValidation_CCModelUnrelatedModelConfirmsNothing (Fisher alone reads +4.7 on an unrelated model with an anisotropic falloff; the new sigma stays under 1), ModelValidation_WrongModelInAGroupOffTheDataMetric (fails with the old count), ResultReport_ModelValidationSection (CC_NULL column, lone-shell rule). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
50e3b59fb7 |
rugnux --af-model: hand check reads only the anomalous map, on MR's placement and solvent
The ENANTIOMORPH hand check ran a whole ValidateAgainstModel on the placed model: model read and frame probe, a rigid body, a 9-replicate null (~0.5 s of the ~0.6-0.9 s), sigma_A, three maps written. The decision needs only --model's rule: the model fits its null (the MR choice has already required its own null of the same construction) and the anomalous map at the model's atoms is not inverted. ReadModelAnomalousMap (ModelValidation) computes F_calc and F_mask to the data's resolution (GPU engine where there is one), carries them through the bulk solvent MR scored the placement with (the overall scale and anisotropic B do not move phases), and reads the anomalous map with the same code as the validation: its coefficient construction and its reading at the atoms are factored out of Validate (anomalous_coefficients, read_anomalous_map) and used by both, so plain --model is unchanged. Hand check 0.6/0.6/0.9 s -> 0.027/0.028/0.040 s on 11if/7ris/9zmu; same ADOPTED/VETOED on all seven cases tried (the three demos, two anomalous lysozyme sets and their mirrored-detector runs), with the scatterer means and deepest troughs within 0.1 sigma of the full validation's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
1420908885 |
--model: the bulk-solvent b_sol ceiling follows the model's atomic B
The model scale searched b_sol only over 10-80 A^2. That is the range
Fokine & Urzhumtsev report for models at ordinary B, but on low-resolution
entries whose deposited B is 200-400 A^2 the fit sat on the 80 A^2 ceiling
(and k_sol on its 0.1 floor): a solvent edge far sharper than the atoms it
lies against, reaching too deep in resolution. R-free came out 0.05-0.10
above REFMAC (0 cycles) and phenix.model_vs_data on the same MTZ, model
and flags.
The ceiling is now max(80, 2 x the model's mean atomic B) (SolventBMax),
for the reported fit and for the rigid-body placement's per-zone solvent
(CPU, GPU and the host fallback alike). The grid keeps eight b_sol values
across a wider box, so it costs no more points; the default box, and so
every model with mean B <= 40 A^2, is bit-identical to before.
Measured (10 open-arm sets with --model, GPU build, vs the same base):
high-B set, 7.0 A: R-free 0.462 -> 0.364, b_sol 80 -> 379 (REFMAC 0.366,
REFMAC's own b_sol ~371; model_vs_data 0.291);
the rigid-body step now commits
high-B set, 3.9 A: 0.410 -> 0.331, b_sol 80 -> 388, k_sol 0.10 -> 0.225
(REFMAC 0.328, model_vs_data 0.314); null 10.1 -> 14.2 sigma
B~200 set, 3.2 A: 0.408 -> 0.402, b_sol 80 -> 120; enantiomorph label
decision unchanged (27.2 -> 25.8 sigma)
7 other sets (mean B 16-57): bit-identical R, k_sol, b_sol, placement.
p.hkl md5 and the MTZ reflection data are identical to the base on all 10.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
49384c4aa8 |
Merge branch 'mv-ncs' into rc175: models expanded by their strict NCS operators; the written cell follows the model's indexing
Exact: p.hkl md5 and MTZ reflection data identical on 18 sets. 4nwv R-free 0.545 -> 0.297 and cell in the depositor's frame; written cells corrected on 9rcs, 9b22, 9qw8, myob_x06da_powder_1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
1f27fba1f3 |
Model validation expands strict NCS; the written cell follows the model's indexing
- ModelValidation reads the model and, where it carries non-given NCS operators (_struct_ncs_oper 'generate' / MTRIX without the given flag), expands them (gemmi expand_ncs, vendored assembly.cpp from GEMMI 0.7.5, MPL-2.0 as the rest of gemmi_gph). Same in the battery's model_check.py and depdata_check.py. An icosahedral-virus entry deposited as 3 of the 45 chains of its ASU was scored against a fifteenth of its atoms: R-free 0.545 -> 0.297. - The indexing the model chooses (AdoptModelFrame) now moves the cell with the indices (reseat_integrated in the main path; CellInBasis in the forecast, twin-file and stills/scale paths). A near-symmetry law (b within 2% of c, beta a degree off 90, a~b in P1) relabelled the reflections only, so the files were written with two axes swapped or beta flipped relative to the indices. Where the law moved the cell, the validation is remade on the moved cell. Measured on 18 open/in-house sets with models: p.hkl and MTZ reflection data bit-identical on all; cell and model R unchanged where the law is exact; the cell now matches the deposited one on the 6 pseudo-merohedral cases (orthorhombic a/c swap, three monoclinic beta flips, a P1 alpha/beta swap), R-free within +-0.001 there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3 |
||
|
|
9512736731 |
Merge branch 'mv-cpu' into rc175: model validation CPU path - parallel density and mask, faster model-scale fits (exact)
Exact: p.hkl, MTZ and map data, p_model.pdb identical at -N 1/default/-N 8 on 9 sets (CPU build and GPU build on the CPU path); GPU path unchanged. Indicative: 8xtg CPU validation 32-41 -> 23-25 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
672e182d6a |
GPU engines wait for their stream before their buffers go; a lost context fails where it is seen
A pooled CudaDevicePtr frees on the thread's allocation stream, not on the engine's stream, and the
pool may hand the memory to another engine - or, past its release threshold, unmap it - as soon as
that free is reached, which on an idle allocation stream is at once. An engine destroyed with work
still queued (FFTIndexerGPU after SearchCap's last DirectionsChanged upload, a spot finder between
DetectAt and Extract, a shadow accumulator after a pending fold, any engine on an exception path)
thus had kernels or copies writing memory that was someone else's or no longer mapped. Now:
- CudaStream synchronises before cudaStreamDestroy (destructor and move-assignment), which covers
engines whose own stream is declared after their buffers (FFTIndexerGPU, the gather buffer);
- every engine holding pooled buffers and a stream (shared or own, declared before the buffers)
synchronises it in its destructor; BraggIntegrationEngineGPU also before EnsureCapacity
reallocates, where a Run that threw leaves work queued.
A GPU failure that is handled no longer hides a lost context: ShadowFinder, BeamCenterFFT, the
rigid-body pool and model validation call cuda_throw_if_context_lost() before cuda_clear_error(),
as the device-decode fallbacks already did; RotationScaleMergeGPU's Alloc does so before waiting up
to ten minutes for GPU work beside it and then reporting a lost device as out of memory; and a
failed cudaMalloc says why. BeamCenterFFT logs the failure it used to drop silently, the
speculative geometry probe logs the exception it swallowed (its GPU fault was otherwise reported by
the merge beside it, under the merge's name), and RotationScaleMergeGPU's DeviceGuard no longer
throws from its destructor.
Only synchronisation and error paths change: p.hkl md5 and the MTZ data (gemmi) are identical to
the
|
||
|
|
76f9192fb4 |
rugnux model validation: the CPU structure factors grid the model and the mask on all threads
compute_model_factors' CPU path (no GPU, or the grid does not fit the card) gridded the density with gemmi's serial put_model_density_on_grid and the mask with SolventMasker::put_mask_on_grid. It now uses ModelGrid's PutModelDensityOnGrid / PutMaskOnGrid with -N threads, sharing one set of orbit leaders (the two grids are the same size) - gemmi's grids bit for bit at any thread count, as the rigid body already uses them. The GPU path is untouched. Exactness: p.hkl md5, p.mtz/p_maps.mtz reflection data, the three maps and p_model.pdb identical to the base and the validation log lines identical (up to the order of concurrent replicate lines) - CPU build, --mode scale --model on 5reo, myob_x10sa, 8sa8, 8xtg, 5ebi at -N 1, default and -N 8; full CPU runs of 9ea5, 7dkp, 6iu5, 8tyy; the GPU build with no visible device on the five --mode scale sets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
8442d55f6f |
rugnux --model: score the twin hypothesis too, and take the maps from the merge the model prefers
Where a twin hypothesis (<prefix>_twin_<SG>) was written, the model is also validated against that merge: as merged, and as twinned by its law - |F_twin(h)|^2 = (1-a)|F_model(h)|^2 + a|F_model(Th)|^2 from the placed, scaled model, one overall scale refitted per a, a scanned 0..0.5 on the working set and R-free read at it (ValidateAgainstModel's new twin_law argument; GPU structure factors as before). The maps and the placed model are then written from whichever merge has the lower R-free; the report gives MAPS_FROM and the subgroup's plain and twinned R-work/R-free and fraction. The subgroup file follows its own validation's indexing, as the answer follows its own. The model chooses the maps only: the space group, <prefix>.mtz and R_FREE stay the answer's. Documented in RUGNUX_REPORT.md, RUGNUX_ADVANCED.md and CPU_DATA_ANALYSIS_DECISIONS.md (14.9); Yeates (1997) credited. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
32e78823fb |
ModelValidation: the non-CUDA build compiles again
The multi-GPU placement (replicate threads pinned with pin_gpu) and the second-validation memory rule read RigidBodyGPUPool and get_gpu_count(), which a build without CUDA does not have; both are now inside JFJOCH_USE_CUDA, where the pool can exist at all. Nothing changes in a CUDA build. Non-CUDA rugnux and jfjoch_test build; [ModelValidation] 22 cases pass there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
9a1f729993 |
rugnux: do not make the first validation's maps where the forecast one is kept
Where the validation in the model's setting, started beside the first on
the forecast (
|
||
|
|
278c488c37 |
Merge branch 'modelpar' into gpusf
Both sides kept: modelpar's parallel basis scoring, the second validation started on a forecast beside the first (write gate, schedule parameter), the multi-GPU placement and delete-before-rewrite; gpusf's GPU structure factors, maps and null engine, failure-instead-of-restart, and the merge engine released before the validation (now just before modelpar's ValidateAgainstModel call, after the forecast lambda is set up). Placement: each validation's structure-factor engines (d_min and the null's) are made on the card of the thread that runs it - the main thread's for the first validation, card 1 % count for the speculative second, which pins itself there - so two validations on two cards use both, as the rigid-body pools do. One memory rule for both, per card, from total memory, up front: a validation plans at most half of its card - its structure-factor engines a quarter together (was half for the d_min engine alone), its rigid-body engines a quarter (RigidBodyGPUPool) - and the second validation runs beside the first only where twice the first's plan (rigid-body planned bytes + the d_min engine, twice it where a null is coming, the null's engine being no larger) fits half of all cards' memory together (was: twice the rigid-body plan within a quarter). The card's total is read once when the engine is made; a CUDA error there fails the validation like any other. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
0f707caab9 |
ModelValidation: the null's structure factors on the GPU too
The null - nine random placements of the model, and the real model's side
of the comparison - is scored to a coarser limit (3.5 A, or where the free
set allows), and those structure factors still ran on the CPU, one model
at a time on each replicate thread. Where the d_min structure factors are
on the GPU, a second ModelStructureFactorsGPU engine at the null's limit
is now made, on the same card, before any replicate starts; every
replicate and the real side go through it, so all of them are computed
the same way. It is decided like the first engine, from the card's total
memory: the two together must stay within half of it, else the null stays
on the CPU (logged). The replicates' threads may sit on other cards
(modelpar's
|
||
|
|
ed2d557bb7 |
ModelValidation: a CUDA failure fails the validation instead of restarting it on the CPU
A CUDA error on the validation's GPU path (rigid body, structure factors,
maps) used to start the whole validation again on the CPU - a migration
part way through, which makes how long a run takes, and in principle what
its rigid bodies converge to, depend on a device fault. It now ends the
validation with ok = false and a logged reason ("the GPU failed during the
validation (...)"), which the run reports as MODEL_NOT_VALIDATED. A
validation that did not finish decides nothing, so the reflection files
are those of a run without a model. Where there is no GPU at all, or the
up-front rule sends the work to the CPU, nothing changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
|
||
|
|
d5fcf2f05b |
rugnux: model validation's structure factors and maps on the GPU
ModelStructureFactorsGPU computes what compute_model_factors() and
map_from_coefficients() compute on the CPU - F_calc from the model's
density (IT92, Refmac-compatible blur, unblurred as prepare_asu_data()
does) and F_mask from the Refmac bulk-solvent mask, both on the
reflections prepare_asu_data(d_min) lists, in its order; and a map from
ASU coefficients on the grid get_size_for_hkl(coef, 0, 3.0) sizes - on a
device. Made once per cell, group, resolution and model, then evaluated
as often as the coordinates change, so refinement or MR can call it in a
loop. The device is an explicit parameter; every call leaves the calling
thread's current device as it found it.
Pieces:
- ModelDensityGPU: the rigid body's deterministic brick gather, moved
out of RigidBodyGPU.cu into a component of its own (ModelMaskGPU's
pattern); the rigid body uses it unchanged. MAX_BRICKS_PER_AXIS 8 ->
16, so fine grids with high-B atoms (lysozyme at 1.2 A, a 0.9 A P1
cell) are no longer refused; existing zones are gridded identically.
- One copy of the content is gridded and the symmetry composed in
reciprocal space (SymmetryComposition), operators applied on the fly;
the mask is ModelMaskGPU (every image of every atom, islands, shrink).
- Maps: gemmi's get_f_phi_on_grid() in ZYX order on the host (the
coefficients written are the same), in-place cuFFT c2r, transposed back
to XYZ on the device. One map at a time, in the engine's buffers.
Decided once, up front, per card, from its TOTAL memory: the engine's
bytes (16 N + cuFFT work + reflections, N the larger of the structure-
factor and map grids) must be at most half the card - the rigid body's
engines take at most a quarter beside it. Otherwise, or where the gather
cannot grid the cell, the CPU path runs, logged with needed vs total.
Anything to a resolution other than d_min (the null's 3.5 A fits) stays
on the CPU, so all replicates and the real model's side of the null are
computed the same way. A CUDA failure takes the existing path: the
validation restarts on the CPU.
Measured, model validation total per run (CPU path -> GPU), 16 GB card:
F432 215 A cubic, 1.30 A, 500^3 grid: 47.7 -> 15.6 s (two validations;
14.3 -> 2.4 and 33.4 -> 13.2, the rest of the second is writing the
three 0.5 GB maps); F_calc + F_mask 5.7 s -> 0.05 s
C2 1.11 A: 23.9 -> 13.6 s; P3_2 1.55 A: 18.2 -> 10.2 s;
P2_1 1.25 A: 13.4 -> 6.5 s; F4_132 328 A: 13.0 -> 5.5 s;
P6_5: 8.8 -> 4.2 s; P4_3 0.97 A: 4.6 -> 2.5 s; P1 0.92 A: 4.2 -> 2.2 s;
small P1: 3.2 -> 1.3 s; P6_1: 8.8 -> 6.2 s; lysozyme: 1.8 -> 1.4 s.
p.mtz md5-identical on all 13 sets. Against the CPU path: FC within
1e-4 of mean |F|, phases of the strong half within 0.003 deg, maps within
1e-4 (2mFo-DFc) and 7e-4 (mFo-DFc) of their rms; every logged R, CC,
FOM, k_sol and anomalous site list identical at the printed precision,
except where a rigid-body commit sat on an exact R-free tie (0.2155 ->
0.2155) and fell the other way (R-work 0.2127 vs 0.2129). The GPU result
is bit-identical run to run and with -N 8 (maps, map MTZ, placed model).
Peak device memory of the engine: 2.5 GB at 500^3 (process total peaked
at 14.4 GB with what the merge still holds).
Tests: ModelStructureFactorsGPU_MatchesCPU (five groups, 3.5 and 1.5 A:
same reflections, F_calc <= 1e-5 of mean |F|, F_mask 2e-7 rms, repeat
bit-identical), ModelStructureFactorsGPU_MapMatchesCPU (<= 5e-6 of rms).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
|
||
|
|
89f52fef95 |
ModelValidation: remove a map file before writing it again, rather than truncating it
The validation in the model's setting writes its three maps and the map MTZ over the ones the first validation wrote seconds before, and a two-pass run writes over the first pass's. XFS (like ext4) flushes a file that was truncated and rewritten when it is closed, so each overwrite forced ~155 MB out to the disk on the spot. Measured on /data (XFS on a hard disk): three 155 MB files rewritten 3.16 s, written fresh 0.62 s, removed and written again 0.66 s. The contents are the same bytes either way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
8d6e9d3a44 |
ModelValidation: use every visible GPU - the null's engines and the second validation
The rigid-body pool put all of its engines on the calling thread's current card, so a validation used one GPU whatever the machine had. Now, by fixed rules decided up front and never by momentary free memory: - RigidBodyGPUPool::Create puts engine i on card (d + i) % count, d being the calling thread's card; the pool restores that card afterwards (an engine's constructor sets its own) and an engine is released on its own card. - The threads that run the null's replicates are pinned with pin_gpu (which also binds them to the card's NUMA node where that is enabled), replicate thread t to card (d + 1 + t) % count, and Acquire() hands a thread an idle engine on its own card where there is one, any other otherwise. With one card this is exactly the previous back-of-the-list choice. - The validation started on the forecast runs on card 1 % count, so with two cards or more it is on the other card from the first; the up-front memory rule becomes: twice the planned engine bytes within a quarter of the cards' total memory taken together. The engines' kernels are deterministic (no floating-point atomics) and an engine's result does not depend on which engine it is, so on cards of one model the numbers are those of one card; what changes is only where the work runs. On this one-card workstation the multi-card path cannot be exercised: md5 of p.mtz and the validation outputs are identical to the base, and the card-count arithmetic was checked by reading only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
b5980c9ba0 |
rugnux: start the validation in the model's setting on a forecast, beside the first
Where a model that fits is written on other axes than the data, the files take its setting and the validation is made a second time on the relabelled data. That second validation depends on the first only through the setting and the indexing the first settles, and both are known long before the first has finished: the change of basis right after the frame scoring, the indexing the probe prefers before the null. So ValidateAgainstModel now reports them (ModelFrameForecast, through ModelValidationSchedule:: on_forecast), and the second validation is started there, on the relabelling AdoptModelFrame and relabel_output would make, applied to a copy of the merge - beside the first one's null, real fit and maps. It is kept only where the first decides exactly what was forecast (the model fits, same change of basis, same indexing; a model asserting the other enantiomorph is not forecast, as the label is decided last, on the anomalous map). Until then its log is held (Logger::Buffered, replayed where the serial run logged it) and its files wait on a gate (ModelValidationSchedule::write_gate) placed before the first map is written; otherwise it is released with false and returns unwritten, and the serial validation runs as before. GPU memory: two validations at once take twice the rigid-body engines. The parallel start is decided up front from sizes, never from what is free: allowed where twice what the first pool asked for (bytes per engine times the engines wanted) fits a quarter of the card's TOTAL memory, the share one validation may take. Threads: both validations submit to the one ParallelFor pool from threads outside it (the second runs on a std::async thread, as the null's replicates do), so no pass runs inline on a pool worker and the pool's size bounds the workers. Measured on the loaded 16-core workstation, TIMING model validation: 8sa8 30.6 -> 19.7 s, 8xtg 19.9 -> 13.9 s, 9ea5 22.1 -> 16.4 s (this and the previous commit together). p.mtz, p.hkl, p.cif, the three maps, p_maps.mtz and p_model.cif/pdb md5-identical to the base on 8sa8, 8xtg, 9ea5, 7qis and myob_x10sa; the validation's log lines identical as a set (the null's replicate lines were already in completion order). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
aabac1541a |
ModelValidation: score the candidate changes of basis at the same time
Each candidate setting of the model was scored one after another on the coarse shell (a density, an FFT and an isotropic scale fit, 0.2-0.5 s each), and a run with a model in another setting scores them in both of its validations. They share nothing but what they read, so they now run on ParallelFor, each on its own copy of the model, and are read back in their own order: the log lines, the ranking and the tie-break (first of equal R wins) are those of the serial loop. Measured (16-core workstation, loaded): 8sa8 3 candidates 0.64 s -> 0.22 s; 9ea5 3 candidates 1.47 s -> 0.50 s; 8xtg 1.45 s -> 0.40 s. p.mtz, the maps, the map MTZ and the placed model md5-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
f956eb25e8 |
rugnux: model validation does the same work in less time
--model validation (battery-only for users) was 18% of the battery's time. Every number it produces is unchanged to the bit (p.mtz, maps, placed model and every model-validation line of the report md5/diff-identical on 11 open sets); only when and where the work runs changes: - The bulk-solvent grid fit (FitModelScale, most of the CPU time) fits each solvent pair on a copy of gemmi::Scaling's target that takes |Fcalc + k_sol exp(-b_sol s^2) Fmask| once per pair instead of at every solver evaluation; same expressions, same types (new test checks a grid point against gemmi's own Scaling fit with ==). - Fcalc density and the solvent mask are made on two threads; the model's structure factors beside the GPU engine reservation. - The indexing probe fits the relabellings concurrently. - The null's replicates run beside the real model's placement (they start from a snapshot of the model as read); one GPU engine per replicate plus one for the real fit instead of a cap of 4 (engines are interchangeable and deterministic). - The 2mFo-DFc, mFo-DFc and anomalous maps are made and written concurrently; the placed model is written beside the reflection files. - A rigid-body zone whose solvent-mask grid needs gemmi's shrink is sent to the CPU when the engines are reserved (ModelMaskGPU::ShrinkIsNoOp), instead of failing on the GPU and validating everything again on the CPU - the same CPU result, without the wasted first attempt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |
||
|
|
9ad92b6bfe |
Move the atomic-model code to image_analysis/structure_refinement/ and WriteModel to writer/
A pure move. ModelValidation, RigidBodyRefine, RigidBodyGPU, ModelFFT, ModelGrid, ModelScaling, ModelMaskGPU, ModelScaleGPU and SigmaA - everything that works on an atomic model - become the JFJochStructureRefinement library, linked by JFJochImageAnalysis. WriteModel (the placed-model mmCIF/PDB writer) goes to writer/ as its own small JFJochModelWriter target, so JFJochWriter, which a writer-only build compiles, does not gain a gemmi dependency. Only include paths and CMake lists change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi |