1use laddu_compile::{
2 CompiledModel, NormalizationDiagnostics, NormalizationStrategy, ReductionPlan,
3};
4use laddu_data::{
5 data::{CacheStorage, Dataset},
6 io::ReadPlan,
7};
8use laddu_expr::parameters::{ParamLayout, ParamValues};
9use laddu_memory::{MemoryFitRequest, MemoryFootprint};
10use num::complex::{Complex32, Complex64};
11use std::sync::{
12 Arc,
13 atomic::{AtomicBool, Ordering},
14};
15
16use crate::{
17 CpuBackend, CpuPlan, Execution, MemoryLease, NormalizationMode, PreparedDataset,
18 PreparedDatasetStats, PreparedModel, RuntimeError, RuntimeResult,
19};
20
21const AUTO_BREAK_EVEN_EVALUATIONS: usize = 16;
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct PreparedNormalizationDiagnostics {
26 strategy: NormalizationStrategy,
27 compiler: NormalizationDiagnostics,
28 retained_bytes: usize,
29 preparation_passes: usize,
30 cache_hit: bool,
31 tag_projection_reused_parent: bool,
32}
33
34impl PreparedNormalizationDiagnostics {
35 pub fn strategy(&self) -> NormalizationStrategy {
37 self.strategy
38 }
39
40 pub fn compiler(&self) -> &NormalizationDiagnostics {
42 &self.compiler
43 }
44
45 pub fn retained_bytes(&self) -> usize {
47 self.retained_bytes
48 }
49
50 pub fn preparation_passes(&self) -> usize {
52 self.preparation_passes
53 }
54
55 pub fn cache_hit(&self) -> bool {
57 self.cache_hit
58 }
59
60 pub fn tag_projection_reused_parent(&self) -> bool {
62 self.tag_projection_reused_parent
63 }
64
65 #[doc(hidden)]
67 pub fn general(compiler: NormalizationDiagnostics) -> Self {
68 Self {
69 strategy: NormalizationStrategy::General,
70 compiler,
71 retained_bytes: 0,
72 preparation_passes: 1,
73 cache_hit: false,
74 tag_projection_reused_parent: false,
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
80struct GeneralResidual {
81 plan: PreparedModel,
82 dataset: PreparedDataset,
83 parameters: ParameterMapping,
84}
85
86#[derive(Debug)]
87enum StoredStatistics {
88 F32(Vec<Complex32>),
89 F64(Vec<Complex64>),
90}
91
92impl StoredStatistics {
93 fn from_f64(values: Vec<Complex64>, precision: crate::Precision) -> Self {
94 if precision == crate::Precision::F32 {
95 Self::F32(
96 values
97 .into_iter()
98 .map(|value| Complex32::new(value.re as f32, value.im as f32))
99 .collect(),
100 )
101 } else {
102 Self::F64(values)
103 }
104 }
105
106 fn resident_bytes(&self) -> usize {
107 match self {
108 Self::F32(values) => values.capacity() * std::mem::size_of::<Complex32>(),
109 Self::F64(values) => values.capacity() * std::mem::size_of::<Complex64>(),
110 }
111 }
112
113 fn evaluator_values(&self) -> Vec<Complex64> {
114 match self {
115 Self::F32(values) => values
116 .iter()
117 .map(|value| Complex64::new(value.re as f64, value.im as f64))
118 .collect(),
119 Self::F64(values) => values.clone(),
120 }
121 }
122}
123
124#[derive(Clone, Debug)]
125struct ParameterMapping {
126 layout: ParamLayout,
127 child_to_parent_free: Vec<usize>,
128 parent_free: usize,
129}
130
131impl ParameterMapping {
132 fn new(child: &ParamLayout, parent: &ParamLayout) -> RuntimeResult<Self> {
133 let child_to_parent_free = child
134 .free_params()
135 .iter()
136 .map(|child_id| {
137 let name = child
138 .name(*child_id)
139 .map_err(|error| RuntimeError::Parameter(error.to_string()))?;
140 let parent_id = parent.id(name).ok_or_else(|| {
141 RuntimeError::Data(format!(
142 "normalization parameter `{name}` is absent from the source model"
143 ))
144 })?;
145 parent
146 .free_id(parent_id)
147 .map_err(|error| RuntimeError::Parameter(error.to_string()))?
148 .map(|id| id.index())
149 .ok_or_else(|| {
150 RuntimeError::Data(format!(
151 "normalization parameter `{name}` is unexpectedly fixed in the source model"
152 ))
153 })
154 })
155 .collect::<RuntimeResult<Vec<_>>>()?;
156 Ok(Self {
157 layout: child.clone(),
158 child_to_parent_free,
159 parent_free: parent.n_free(),
160 })
161 }
162
163 fn project(&self, parent: &ParamValues) -> RuntimeResult<ParamValues> {
164 let free = self
165 .layout
166 .free_params()
167 .iter()
168 .map(|child_id| {
169 let name = self
170 .layout
171 .name(*child_id)
172 .map_err(|error| RuntimeError::Parameter(error.to_string()))?;
173 let parent_id = parent.layout().id(name).ok_or_else(|| {
174 RuntimeError::Data(format!(
175 "normalization parameter `{name}` is absent from supplied values"
176 ))
177 })?;
178 parent
179 .get(parent_id)
180 .map_err(|error| RuntimeError::Parameter(error.to_string()))
181 })
182 .collect::<RuntimeResult<Vec<_>>>()?;
183 self.layout
184 .values(&free)
185 .map_err(|error| RuntimeError::Parameter(error.to_string()))
186 }
187
188 fn scatter_add(&self, child: &[f64], parent: &mut [f64]) -> RuntimeResult<()> {
189 if child.len() != self.child_to_parent_free.len() || parent.len() != self.parent_free {
190 return Err(RuntimeError::Data(
191 "normalization gradient has an incompatible parameter layout".into(),
192 ));
193 }
194 for (value, parent_index) in child.iter().zip(&self.child_to_parent_free) {
195 parent[*parent_index] += value;
196 }
197 Ok(())
198 }
199}
200
201#[derive(Debug)]
203pub struct PreparedNormalization {
204 evaluator: CpuPlan,
205 evaluator_parameters: ParameterMapping,
206 statistics: StoredStatistics,
207 residual: Option<GeneralResidual>,
208 verification: Option<GeneralResidual>,
209 stats: PreparedDatasetStats,
210 diagnostics: PreparedNormalizationDiagnostics,
211 cache_reused: AtomicBool,
212 _memory_lease: MemoryLease,
213}
214
215impl PreparedNormalization {
216 pub fn prepare(
223 model: &CompiledModel,
224 general_plan: &PreparedModel,
225 dataset: &Dataset,
226 execution: &Execution,
227 ) -> RuntimeResult<Option<Arc<Self>>> {
228 if execution.normalization_mode() == NormalizationMode::General
229 || model.normalization_diagnostics().strategy() == NormalizationStrategy::General
230 || (execution.normalization_mode() == NormalizationMode::Auto
231 && !model.normalization_plan().proven_nonnegative())
232 {
233 return Ok(None);
234 }
235
236 let key = (
237 model.optimized_digest(),
238 dataset.identity(),
239 execution.normalization_mode(),
240 );
241 let mut cache = execution
242 .normalization_cache()
243 .lock()
244 .unwrap_or_else(|error| error.into_inner());
245 cache.retain(|_, prepared| prepared.strong_count() > 0);
246 if let Some(prepared) = cache.get(&key).and_then(std::sync::Weak::upgrade) {
247 prepared.cache_reused.store(true, Ordering::Relaxed);
248 return Ok(Some(prepared));
249 }
250 let Some(prepared) = Self::prepare_uncached(model, general_plan, dataset, execution)?
251 else {
252 return Ok(None);
253 };
254 let prepared = Arc::new(prepared);
255 cache.insert(key, Arc::downgrade(&prepared));
256 Ok(Some(prepared))
257 }
258
259 fn prepare_uncached(
260 model: &CompiledModel,
261 general_plan: &PreparedModel,
262 dataset: &Dataset,
263 execution: &Execution,
264 ) -> RuntimeResult<Option<Self>> {
265 let basis_models = model
266 .normalization_plan()
267 .basis_models()
268 .map_err(|error| RuntimeError::Data(error.to_string()))?;
269 let basis_work = basis_models
270 .iter()
271 .map(|basis| basis.graph().nodes().len())
272 .sum::<usize>();
273 let general_work = model.graph().nodes().len().max(1);
274 if execution.normalization_mode() == NormalizationMode::Auto
275 && basis_work > general_work.saturating_mul(AUTO_BREAK_EVEN_EVALUATIONS)
276 {
277 return Ok(None);
278 }
279
280 let statistic_bytes = if execution.precision() == crate::Precision::F32 {
281 std::mem::size_of::<Complex32>()
282 } else {
283 std::mem::size_of::<Complex64>()
284 };
285 let retained_bytes = basis_models.len().saturating_mul(statistic_bytes);
286 let memory_lease = match execution
287 .host_memory()
288 .reserve(u64::try_from(retained_bytes).unwrap_or(u64::MAX))
289 {
290 Ok(lease) => lease,
291 Err(_) if execution.normalization_mode() == NormalizationMode::Auto => return Ok(None),
292 Err(error) => return Err(error.into()),
293 };
294 let basis_plans = basis_models
295 .iter()
296 .map(|basis| {
297 CpuBackend
298 .prepare_with_autodiff_mode(basis, execution.autodiff_mode())
299 .map_err(|error| RuntimeError::Data(error.to_string()))
300 })
301 .collect::<RuntimeResult<Vec<_>>>()?;
302 let basis_params = basis_models
303 .iter()
304 .map(|basis| basis.params().default_values())
305 .collect::<Vec<_>>();
306 let (statistics, stats) =
307 accumulate_statistics(&basis_plans, &basis_params, dataset, execution)?;
308 let statistics = StoredStatistics::from_f64(statistics, execution.precision());
309 let evaluator_statistics = statistics.evaluator_values();
310 let evaluator_model = model
311 .normalization_plan()
312 .evaluator_model(&evaluator_statistics)
313 .map_err(|error| RuntimeError::Data(error.to_string()))?;
314 let evaluator = CpuBackend
318 .prepare_with_autodiff_mode(&evaluator_model, execution.autodiff_mode())
319 .map_err(|error| RuntimeError::Data(error.to_string()))?;
320 let evaluator_parameters = ParameterMapping::new(evaluator_model.params(), model.params())?;
321
322 let residual_model = model
323 .normalization_plan()
324 .residual_model()
325 .map_err(|error| RuntimeError::Data(error.to_string()))?;
326 let residual = if let Some(residual_model) = residual_model {
327 let parameters = ParameterMapping::new(residual_model.params(), model.params())?;
328 let plan = PreparedModel::prepare(&residual_model, execution)?;
329 let dataset = plan.prepare_dataset(execution, dataset)?;
330 Some(GeneralResidual {
331 plan,
332 dataset,
333 parameters,
334 })
335 } else {
336 None
337 };
338 let verification = if execution.normalization_mode() == NormalizationMode::Verify {
339 Some(GeneralResidual {
340 plan: general_plan.clone(),
341 dataset: general_plan.prepare_dataset(execution, dataset)?,
342 parameters: ParameterMapping::new(model.params(), model.params())?,
343 })
344 } else {
345 None
346 };
347 let preparation_passes =
348 1 + usize::from(residual.is_some()) + usize::from(verification.is_some());
349 Ok(Some(Self {
350 evaluator,
351 evaluator_parameters,
352 statistics,
353 residual,
354 verification,
355 stats,
356 diagnostics: PreparedNormalizationDiagnostics {
357 strategy: model.normalization_diagnostics().strategy(),
358 compiler: model.normalization_diagnostics().clone(),
359 retained_bytes,
360 preparation_passes,
361 cache_hit: false,
362 tag_projection_reused_parent: false,
363 },
364 cache_reused: AtomicBool::new(false),
365 _memory_lease: memory_lease,
366 }))
367 }
368
369 pub fn stats(&self) -> &PreparedDatasetStats {
371 &self.stats
372 }
373
374 pub fn diagnostics(&self) -> PreparedNormalizationDiagnostics {
376 let mut diagnostics = self.diagnostics.clone();
377 diagnostics.cache_hit = self.cache_reused.load(Ordering::Relaxed);
378 diagnostics
379 }
380
381 pub fn resident_bytes(&self) -> usize {
383 self.statistics.resident_bytes()
384 }
385
386 pub fn value(&self, params: &ParamValues, execution: &Execution) -> RuntimeResult<f64> {
393 let evaluator_params = self.evaluator_parameters.project(params)?;
394 let mut value = self.evaluator.evaluate(&evaluator_params)?.re;
395 if let Some(residual) = &self.residual {
396 let residual_params = residual.parameters.project(params)?;
397 value += residual.plan.reduce(
398 execution,
399 &residual_params,
400 &residual.dataset,
401 ReductionPlan::weighted_real(),
402 )?;
403 }
404 if let Some(general) = &self.verification {
405 let general_params = general.parameters.project(params)?;
406 let expected = general.plan.reduce(
407 execution,
408 &general_params,
409 &general.dataset,
410 ReductionPlan::weighted_real(),
411 )?;
412 verify_close("normalization value", value, expected, execution)?;
413 }
414 Ok(value)
415 }
416
417 pub fn value_gradient(
424 &self,
425 params: &ParamValues,
426 execution: &Execution,
427 ) -> RuntimeResult<(f64, Vec<f64>)> {
428 let evaluator_params = self.evaluator_parameters.project(params)?;
429 let evaluation = self.evaluator.evaluate_with_gradient(&evaluator_params)?;
430 let mut value = evaluation.value().re;
431 let evaluator_gradient = evaluation
432 .gradient()
433 .iter()
434 .map(|value| value.re)
435 .collect::<Vec<_>>();
436 let mut gradient = vec![0.0; self.evaluator_parameters.parent_free];
437 self.evaluator_parameters
438 .scatter_add(&evaluator_gradient, &mut gradient)?;
439 if let Some(residual) = &self.residual {
440 let residual_params = residual.parameters.project(params)?;
441 let residual_evaluation = residual.plan.reduce_with_gradient(
442 execution,
443 &residual_params,
444 &residual.dataset,
445 ReductionPlan::weighted_real(),
446 )?;
447 value += residual_evaluation.value();
448 residual
449 .parameters
450 .scatter_add(residual_evaluation.gradient(), &mut gradient)?;
451 }
452 if let Some(general) = &self.verification {
453 let general_params = general.parameters.project(params)?;
454 let expected = general.plan.reduce_with_gradient(
455 execution,
456 &general_params,
457 &general.dataset,
458 ReductionPlan::weighted_real(),
459 )?;
460 verify_close("normalization value", value, expected.value(), execution)?;
461 for (index, (actual, expected)) in gradient.iter().zip(expected.gradient()).enumerate()
462 {
463 verify_close(
464 &format!("normalization gradient[{index}]"),
465 *actual,
466 *expected,
467 execution,
468 )?;
469 }
470 }
471 Ok((value, gradient))
472 }
473}
474
475fn accumulate_statistics(
476 plans: &[CpuPlan],
477 params: &[ParamValues],
478 dataset: &Dataset,
479 execution: &Execution,
480) -> RuntimeResult<(Vec<Complex64>, PreparedDatasetStats)> {
481 let mut sums = vec![Complex64::ZERO; plans.len()];
482 let mut corrections = vec![Complex64::ZERO; plans.len()];
483 let mut read_plan: ReadPlan = execution.read_plan(dataset.read_plan());
484 let local_limit = dataset
485 .num_events()
486 .map_err(|error| RuntimeError::Data(error.to_string()))?
487 .and_then(|events| usize::try_from(events).ok())
488 .unwrap_or(usize::MAX);
489 let statistic_bytes = plans.len().saturating_mul(std::mem::size_of::<Complex64>());
490 let decision = MemoryFitRequest {
491 label: "normalization statistics".into(),
492 footprint: MemoryFootprint::from_usize(statistic_bytes, statistic_bytes),
493 available_bytes: execution.host_memory().remaining(),
494 event_limit: local_limit,
495 strategy: "single-pass sufficient statistics".into(),
496 }
497 .evaluate()?;
498 read_plan.chunk_size = Some(
499 read_plan
500 .chunk_size
501 .map_or(decision.chunk_events, |manual| {
502 manual.min(decision.chunk_events)
503 })
504 .max(1),
505 );
506 execution.record_memory_decision(decision);
507 let local = (|| {
508 let mut events = 0usize;
509 let mut batches = 0usize;
510 let mut weight_sum = 0.0;
511 let mut weight_correction = 0.0;
512 for batch in dataset
513 .stream_with_plan(read_plan)
514 .map_err(|error| RuntimeError::Data(error.to_string()))?
515 {
516 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
517 events += batch.len();
518 batches += 1;
519 for row in 0..batch.len() {
520 let weight = batch.weights_at(row);
521 let corrected = weight - weight_correction;
522 let next = weight_sum + corrected;
523 weight_correction = (next - weight_sum) - corrected;
524 weight_sum = next;
525 }
526 for (index, (plan, params)) in plans.iter().zip(params).enumerate() {
527 for (row, value) in plan.evaluate_batch(params, &batch)?.into_iter().enumerate() {
528 let value = value * batch.weights_at(row);
529 let corrected = value - corrections[index];
530 let next = sums[index] + corrected;
531 corrections[index] = (next - sums[index]) - corrected;
532 sums[index] = next;
533 }
534 }
535 }
536 Ok::<_, RuntimeError>((events, batches, weight_sum))
537 })();
538 if !execution.all_succeeded(local.is_ok()) {
539 return local.and(Err(RuntimeError::DistributedPeerFailure));
540 }
541 let (events, batches, weight_sum) = local?;
542 for sum in &mut sums {
543 sum.re = execution.sum_f64(sum.re);
544 sum.im = execution.sum_f64(sum.im);
545 }
546 let stats = PreparedDatasetStats::new(
547 events,
548 execution.sum_usize(events),
549 batches,
550 execution.sum_f64(weight_sum),
551 sums.len() * std::mem::size_of::<Complex64>(),
552 CacheStorage::Resident,
553 );
554 Ok((sums, stats))
555}
556
557fn verify_close(
558 label: &str,
559 actual: f64,
560 expected: f64,
561 execution: &Execution,
562) -> RuntimeResult<()> {
563 let tolerance = match execution.precision() {
564 crate::Precision::F32 => 5.0e-4,
565 crate::Precision::Auto | crate::Precision::F64 => 1.0e-10,
566 } * expected.abs().max(1.0);
567 if (actual - expected).abs() <= tolerance {
568 Ok(())
569 } else {
570 Err(RuntimeError::Data(format!(
571 "{label} verification failed: compiler-native={actual}, general={expected}, tolerance={tolerance}"
572 )))
573 }
574}