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