Model validation: warm up on the GPU when the broker gets a model

WarmUpModelValidation runs one throwaway validation of the uploaded model against its own
calculated intensities on a background thread (holding a GPUWorkBeside), so kernels, cuFFT
kernels and contexts on every card the null's replicates use are loaded before the collection's
validation. ValidateAgainstModel waits for a warm-up still running, so a real validation never
shares the cards with it and computes what it computes without one (test
ModelValidation_WarmUpChangesNothing; CLI run with and without warm-up: identical p.mtz,
p_maps.mtz, report and validation lines). The broker logs the warm-up's time at upload, which
measures the first-use cost on the machine directly (local RTX 5080: 0.88 s first, 0.78 s second).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-19 11:23:51 +02:00
1 parent 8261b7354e
commit 0ff6442765
4 files changed
+107

No files matched your search

+35
View File
@@ -864,6 +864,41 @@ TEST_CASE("ModelValidation_CCModelFollowsTheSignalByShell", "[ModelValidation]")
std::filesystem::remove(path);
}
// A warm-up (WarmUpModelValidation) only loads what the GPU loads on first use: the validation after it,
// which waits for it, gives what it gives without one.
TEST_CASE("ModelValidation_WarmUpChangesNothing", "[ModelValidation]") {
Logger logger("ModelValidation_WarmUpChangesNothing");
const auto path = WriteTemp("warm_up_test.pdb", ClusterPdb().c_str());
auto obs = ModelReferenceIntensities(ReadModel(path), {}, {}, 2.5, logger);
REQUIRE(obs.size() > 1000);
std::mt19937 rng(20261019);
std::normal_distribution<double> normal(0.0, 1.0);
for (size_t i = 0; i < obs.size(); i++) {
obs[i].I = static_cast<float>(obs[i].I * (1.0 + 0.1 * normal(rng)));
obs[i].F = std::sqrt(std::max(0.0f, obs[i].I));
obs[i].rfree_flag = (i % 20) == 0;
}
const UnitCell cell{.a = 30, .b = 34, .c = 38, .alpha = 90, .beta = 90, .gamma = 90};
const auto model = std::make_shared<const RugnuxModel>(ReadModel(path));
const gemmi::SpaceGroup *sg = gemmi::find_spacegroup_by_name("P 21 21 21");
const auto cold = ValidateAgainstModel(obs, cell, *model, logger, sg, false, 4);
WarmUpModelValidation(model, 4, logger);
const auto warm = ValidateAgainstModel(obs, cell, *model, logger, sg, false, 4);
REQUIRE(cold.ok);
REQUIRE(warm.ok);
CHECK(warm.r_work == cold.r_work);
CHECK(warm.r_free == cold.r_free);
CHECK(warm.rigid_body_angle_deg == cold.rigid_body_angle_deg);
CHECK(warm.null_r_work_mean == cold.null_r_work_mean);
CHECK(warm.null_r_work_sd == cold.null_r_work_sd);
CHECK(warm.r_work_sigma == cold.r_work_sigma);
std::filesystem::remove(path);
}
// The model path's structure factors come from FFTW; they must be gemmi's own transform to float
// precision, in the same layout, so prepare_asu_data() reads the same reflections from either.