From 00f8c8ba4b6884df70c7dfed7e304ec0cf5805d8 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 26 Oct 2026 19:24:48 +0100 Subject: [PATCH] Refinement: the final ladder takes the rungs the step from the best state tried already; the longest trials to the slots first The trajectory ends where a step gains nothing, and that step ran from the best state at the weights the final ladder then tries from the same state: on a positional step every rung, on a B step the middle one. A trial of the same weights from the same model is the same computation (RunTrials: the outcome is the same on any slot), so the final takes those outcomes from the step and runs only the rungs it has not got. The maps' Refit() then starts from the fit the main slot would hold had it run its rung (Context::Restore of that rung's fit) - the solvent grid and the likelihood's D it starts from otherwise depend on which trial the main slot ran last, and that showed as last-bit differences in the map coefficients. RunTrials gives the trials that refine B, the longest, to the slots first: on a machine whose cards hold a slot each the ladder's two B rungs no longer share one card while a positional rung has one of its own. Byte-identical (p.mtz, p.hkl, p.cif, p_model, p_refined_model, p_refined_maps.mtz, p_blobs.json, report minus timing) on three rotation sets run live-path, and on two of them in --mode refine. Refinement wall on one RTX 5080: 1.86 -> 1.71 s, 2.42 -> 2.17 s, 50.0 -> 40.8 s (one stage of the trajectory each). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu --- .../structure_refinement/Refinement.cpp | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/image_analysis/structure_refinement/Refinement.cpp b/image_analysis/structure_refinement/Refinement.cpp index 96c274e52..7b11909a4 100644 --- a/image_analysis/structure_refinement/Refinement.cpp +++ b/image_analysis/structure_refinement/Refinement.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1133,8 +1134,8 @@ TrialOutcome RunTrial(TrialSlot slot, const std::vector &refinable, const s return out; } -// The trials (w, adp_w) side by side: trial i in slot i % slots, each slot's trials in turn on a thread of its -// own (on its GPU), returned in the order given. Every trial starts from pos0 and b0 and takes nothing from the +// The trials (w, adp_w) side by side, the longest to the slots first and each slot's trials in turn on a thread of +// its own (on its GPU), returned in the order given. Every trial starts from pos0 and b0 and takes nothing from the // one before it in its slot, so the outcome is the same for any number of slots. std::vector RunTrials(const std::vector &trial_slots, const std::vector> &trials, @@ -1147,13 +1148,19 @@ std::vector RunTrials(const std::vector &trial_slots, const size_t running = std::min(trial_slots.size(), trials.size()); for (size_t s = 0; s < running; s++) trial_slots[s].ctx->nthreads = std::max(1, settings.nthreads / running); + // The trials that refine B are the longest: they go to the slots first, so that two of them share a slot (or a + // card) only where the slots are fewer than they are. + std::vector order(trials.size()); + std::iota(order.begin(), order.end(), 0); + std::stable_sort(order.begin(), order.end(), + [&](size_t a, size_t b) { return (trials[a].second > 0) > (trials[b].second > 0); }); auto run_slot = [&](size_t s) { try { #ifdef JFJOCH_USE_CUDA const ScopedGpuPin pin(trial_slots[s].ctx->device >= 0 ? trial_slots[s].ctx->device : get_gpu()); #endif - for (size_t i = s; i < trials.size(); i += trial_slots.size()) - if (!trial_slots[s].ctx->Cancelled()) + for (size_t j = s; j < trials.size(); j += trial_slots.size()) + if (const size_t i = order[j]; !trial_slots[s].ctx->Cancelled()) out[i] = RunTrial(trial_slots[s], refinable, pos0, b0, fit, trials[i].first, trials[i].second, cycles, settings); } catch (...) { @@ -1416,6 +1423,10 @@ RefinementResult RefineModel(const gemmi::Structure &placed, const std::vector from_best; + bool state_is_best = true; for (int step = 0; step < settings.max_steps && state.ran && !ctx.Cancelled(); step++) { const double w = state.trial.weight, adp = state.trial.adp_weight; std::vector> trials = {{w, adp}}; @@ -1429,6 +1440,8 @@ RefinementResult RefineModel(const gemmi::Structure &placed, const std::vector> trials; for (double f : settings.final_ladder) if (w * f <= w_max) trials.emplace_back(w * f, adp); - outcomes = RunTrials(trial_slots, trials, refinable, state.pos, state.b, &state.fit, settings.macro_cycles, - settings); + outcomes.assign(trials.size(), TrialOutcome()); + std::vector> missing; + for (size_t i = 0; i < trials.size(); i++) { + for (const TrialOutcome &o : from_best) + if (o.ran && o.trial.weight == trials[i].first && o.trial.adp_weight == trials[i].second) + outcomes[i] = o; + if (!outcomes[i].ran) + missing.push_back(trials[i]); + } + std::vector ran = RunTrials(trial_slots, missing, refinable, state.pos, state.b, &state.fit, + settings.macro_cycles, settings); + for (size_t i = 0, m = 0; i < trials.size(); i++) + if (!outcomes[i].ran) + outcomes[i] = std::move(ran[m++]); k = choose(outcomes, "final"); + // The main slot's fit as it stands where its last rung ran on it, for the maps' Refit() to start from + // (the solvent grid and the likelihood's D) whether the rung ran or was taken from the step. + const size_t main_last = (trials.size() - 1) / trial_slots.size() * trial_slots.size(); + if (outcomes[main_last].ran) + ctx.Restore(outcomes[main_last].fit); if (k >= 0 && displaces(outcomes[k].trial, state.trial)) state = std::move(outcomes[k]); }