1use laddu_compile::{CompiledModel, ReductionPlan};
2#[cfg(feature = "wgpu")]
3use laddu_data::BatchLayout;
4use laddu_data::data::Dataset;
5use laddu_data::data::EventBatch;
6#[cfg(feature = "wgpu")]
7use laddu_data::data::{CacheStorage, MemoryPolicy, accurate::AccurateF64};
8#[cfg(feature = "wgpu")]
9use laddu_data::schema::Precision as DataPrecision;
10use laddu_expr::parameters::ParamValues;
11#[cfg(feature = "wgpu")]
12use laddu_memory::{MemoryDecision, MemoryFootprint};
13use num::complex::Complex64;
14use std::sync::Arc;
15
16#[cfg(feature = "wgpu")]
17use crate::preparation::{DatasetPreparation, DatasetStatsAccumulator, RuntimePreparationPlan};
18use crate::{
19 CpuBackend, CpuPlan, CpuPreparedDataset, Execution, PreparedDatasetStats, ReductionEvaluation,
20 RuntimeError, RuntimeResult,
21};
22
23#[derive(Clone, Debug)]
25pub enum PreparedModel {
26 Cpu(Arc<CpuPlan>),
28 #[cfg(feature = "wgpu")]
29 Wgpu(WgpuPlan),
31}
32
33#[derive(Clone, Debug)]
35pub enum PreparedDataset {
36 Cpu(CpuPreparedDataset),
38 #[cfg(feature = "wgpu")]
39 Wgpu(WgpuPreparedDataset),
41}
42
43impl PreparedDataset {
44 pub fn stats(&self) -> &PreparedDatasetStats {
46 match self {
47 Self::Cpu(dataset) => dataset.stats(),
48 #[cfg(feature = "wgpu")]
49 Self::Wgpu(dataset) => dataset.stats(),
50 }
51 }
52}
53
54impl PreparedModel {
55 pub fn required_event_scalars(&self) -> &[String] {
59 match self {
60 Self::Cpu(plan) => plan.required_event_scalars(),
61 #[cfg(feature = "wgpu")]
62 Self::Wgpu(plan) => &plan.required_event_scalars,
63 }
64 }
65
66 pub fn evaluate_batch(
73 &self,
74 params: &ParamValues,
75 batch: &EventBatch,
76 ) -> RuntimeResult<Vec<Complex64>> {
77 match self {
78 Self::Cpu(plan) => plan.evaluate_batch(params, batch),
79 #[cfg(feature = "wgpu")]
80 Self::Wgpu(plan) => plan
81 .kernel
82 .evaluate_batch(&plan.context, params, batch)
83 .map(|values| {
84 values
85 .into_iter()
86 .map(|(re, im)| Complex64::new(re, im))
87 .collect()
88 })
89 .map_err(wgpu_error),
90 }
91 }
92
93 pub(crate) fn evaluate_batch_outputs(
97 &self,
98 params: &ParamValues,
99 batch: &EventBatch,
100 outputs: &[laddu_expr::ExprId],
101 ) -> RuntimeResult<Vec<Vec<Complex64>>> {
102 match self {
103 Self::Cpu(plan) => plan.evaluate_batch_outputs(params, batch, outputs),
104 #[cfg(feature = "wgpu")]
105 Self::Wgpu(_) => Err(RuntimeError::Wgpu(
106 "multi-output query evaluation is not implemented by the WGPU backend".into(),
107 )),
108 }
109 }
110
111 pub fn evaluate_batch_with_gradient(
118 &self,
119 params: &ParamValues,
120 batch: &EventBatch,
121 ) -> RuntimeResult<Vec<crate::ValueGradient>> {
122 match self {
123 Self::Cpu(plan) => plan.evaluate_batch_with_gradient(params, batch),
124 #[cfg(feature = "wgpu")]
125 Self::Wgpu(_) => Err(RuntimeError::Wgpu(
126 "event-wise model gradients are not implemented by the WGPU backend".into(),
127 )),
128 }
129 }
130
131 pub fn prepare(model: &CompiledModel, execution: &Execution) -> RuntimeResult<Self> {
138 #[cfg(feature = "wgpu")]
139 if let Some(context) = execution.wgpu_context() {
140 return Ok(Self::Wgpu(WgpuPlan {
141 context: context.clone(),
142 preparation_params: model.params().default_values(),
143 kernel: std::sync::Arc::new(
144 laddu_wgpu::WgpuScalarKernel::compile(context, model).map_err(wgpu_error)?,
145 ),
146 required_event_scalars: crate::required_event_scalars(model),
147 }));
148 }
149 Ok(Self::Cpu(
150 CpuBackend.prepare_shared_for_execution(model, execution)?,
151 ))
152 }
153
154 pub fn prepare_dataset(
161 &self,
162 execution: &Execution,
163 dataset: &Dataset,
164 ) -> RuntimeResult<PreparedDataset> {
165 match self {
166 Self::Cpu(plan) => Ok(PreparedDataset::Cpu(
167 plan.prepare_dataset(execution, dataset)?,
168 )),
169 #[cfg(feature = "wgpu")]
170 Self::Wgpu(plan) => plan
171 .prepare_dataset(execution, dataset)
172 .map(PreparedDataset::Wgpu),
173 }
174 }
175
176 pub fn evaluate_prepared(
187 &self,
188 execution: &Execution,
189 params: &ParamValues,
190 dataset: &PreparedDataset,
191 source: &Dataset,
192 ) -> RuntimeResult<Vec<Complex64>> {
193 let mut output =
194 self.evaluate_prepared_many(execution, std::slice::from_ref(params), dataset, source)?;
195 Ok(output.pop().unwrap_or_default())
196 }
197
198 pub fn evaluate_prepared_many(
207 &self,
208 execution: &Execution,
209 params: &[ParamValues],
210 dataset: &PreparedDataset,
211 source: &Dataset,
212 ) -> RuntimeResult<Vec<Vec<Complex64>>> {
213 #[cfg(not(feature = "wgpu"))]
214 let _ = source;
215 #[allow(unreachable_patterns)]
216 match (self, dataset) {
217 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
218 plan.evaluate_prepared_dataset_many(execution, params, dataset)
219 }
220 #[cfg(feature = "wgpu")]
221 (Self::Wgpu(_), PreparedDataset::Wgpu(_)) => {
222 evaluate_source_many(self, execution, params, source, None)
223 .map(|(values, _)| values)
224 }
225 _ => Err(RuntimeError::InvalidShape {
226 index: 0,
227 message: "prepared model and dataset use different backends".into(),
228 }),
229 }
230 }
231
232 #[doc(hidden)]
235 pub fn visit_prepared_many<F>(
236 &self,
237 execution: &Execution,
238 parameter_sets: &[(&ParamValues, &str)],
239 dataset: &PreparedDataset,
240 source: &Dataset,
241 reduction: Option<ReductionPlan>,
242 consume: F,
243 ) -> RuntimeResult<Vec<f64>>
244 where
245 F: FnMut(usize, usize, &[Complex64]) -> RuntimeResult<()>,
246 {
247 #[cfg(not(feature = "wgpu"))]
248 let _ = source;
249 #[allow(unreachable_patterns)]
250 match (self, dataset) {
251 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => plan.visit_prepared_dataset_many(
252 execution,
253 parameter_sets,
254 dataset,
255 reduction,
256 false,
257 consume,
258 ),
259 #[cfg(feature = "wgpu")]
260 (Self::Wgpu(_), PreparedDataset::Wgpu(_)) => {
261 visit_source_many(self, execution, parameter_sets, source, reduction, consume)
262 }
263 _ => Err(RuntimeError::InvalidShape {
264 index: 0,
265 message: "prepared model and dataset use different backends".into(),
266 }),
267 }
268 }
269
270 #[doc(hidden)]
272 pub fn visit_prepared_many_parallel<F>(
273 &self,
274 execution: &Execution,
275 parameter_sets: &[(&ParamValues, &str)],
276 dataset: &PreparedDataset,
277 source: &Dataset,
278 reduction: Option<ReductionPlan>,
279 consume: F,
280 ) -> RuntimeResult<Vec<f64>>
281 where
282 F: FnMut(usize, usize, &[Complex64]) -> RuntimeResult<()> + Send,
283 {
284 #[cfg(not(feature = "wgpu"))]
285 let _ = source;
286 #[allow(unreachable_patterns)]
287 match (self, dataset) {
288 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => execution.install(|| {
289 plan.visit_prepared_dataset_many(
290 execution,
291 parameter_sets,
292 dataset,
293 reduction,
294 true,
295 consume,
296 )
297 }),
298 #[cfg(feature = "wgpu")]
299 (Self::Wgpu(_), PreparedDataset::Wgpu(_)) => {
300 visit_source_many(self, execution, parameter_sets, source, reduction, consume)
301 }
302 _ => Err(RuntimeError::InvalidShape {
303 index: 0,
304 message: "prepared model and dataset use different backends".into(),
305 }),
306 }
307 }
308
309 pub fn evaluate_prepared_many_with_reduction(
316 &self,
317 execution: &Execution,
318 params: &[ParamValues],
319 dataset: &PreparedDataset,
320 source: &Dataset,
321 reduction: ReductionPlan,
322 ) -> RuntimeResult<(Vec<Vec<Complex64>>, Vec<f64>)> {
323 #[cfg(not(feature = "wgpu"))]
324 let _ = source;
325 #[allow(unreachable_patterns)]
326 match (self, dataset) {
327 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => plan
328 .evaluate_prepared_dataset_many_with_reduction(
329 execution, params, dataset, reduction,
330 ),
331 #[cfg(feature = "wgpu")]
332 (Self::Wgpu(_), PreparedDataset::Wgpu(_)) => {
333 let (values, sums) =
334 evaluate_source_many(self, execution, params, source, Some(reduction))?;
335 Ok((values, sums.unwrap_or_default()))
336 }
337 _ => Err(RuntimeError::InvalidShape {
338 index: 0,
339 message: "prepared model and dataset use different backends".into(),
340 }),
341 }
342 }
343
344 pub fn reduce(
351 &self,
352 execution: &Execution,
353 params: &ParamValues,
354 dataset: &PreparedDataset,
355 reduction: ReductionPlan,
356 ) -> RuntimeResult<f64> {
357 #[allow(unreachable_patterns)]
358 match (self, dataset) {
359 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
360 plan.reduce(execution, params, dataset, reduction)
361 }
362 #[cfg(feature = "wgpu")]
363 (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
364 plan.reduce(execution, params, dataset, reduction)
365 }
366 _ => Err(RuntimeError::InvalidShape {
367 index: 0,
368 message: "prepared model and dataset use different backends".into(),
369 }),
370 }
371 }
372
373 pub fn reduce_with_gradient(
380 &self,
381 execution: &Execution,
382 params: &ParamValues,
383 dataset: &PreparedDataset,
384 reduction: ReductionPlan,
385 ) -> RuntimeResult<ReductionEvaluation> {
386 #[allow(unreachable_patterns)]
387 match (self, dataset) {
388 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
389 plan.reduce_with_gradient(execution, params, dataset, reduction)
390 }
391 #[cfg(feature = "wgpu")]
392 (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
393 plan.reduce_with_gradient(execution, params, dataset, reduction)
394 }
395 _ => Err(RuntimeError::InvalidShape {
396 index: 0,
397 message: "prepared model and dataset use different backends".into(),
398 }),
399 }
400 }
401}
402
403#[cfg(feature = "wgpu")]
404type PreparedValuesWithSums = (Vec<Vec<Complex64>>, Option<Vec<f64>>);
405
406#[cfg(feature = "wgpu")]
407fn evaluate_source_many(
408 model: &PreparedModel,
409 execution: &Execution,
410 params: &[ParamValues],
411 source: &Dataset,
412 reduction: Option<ReductionPlan>,
413) -> RuntimeResult<PreparedValuesWithSums> {
414 let local = (|| {
415 let mut output = params.iter().map(|_| Vec::new()).collect::<Vec<_>>();
416 let mut sums = reduction.map(|_| {
417 params
418 .iter()
419 .map(|_| AccurateF64::zero())
420 .collect::<Vec<_>>()
421 });
422 for batch in source
423 .batches()
424 .map_err(|error| RuntimeError::Data(error.to_string()))?
425 {
426 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
427 for (index, (parameters, values)) in params.iter().zip(&mut output).enumerate() {
428 let batch_values = model.evaluate_batch(parameters, &batch)?;
429 if let (Some(reduction), Some(sums)) = (reduction, sums.as_mut()) {
430 for (row, value) in batch_values.iter().enumerate() {
431 sums[index].push(batch.weights_at(row) * reduction.apply(*value)?.value());
432 }
433 }
434 values.extend(batch_values);
435 }
436 }
437 Ok((
438 output,
439 sums.map(|sums| sums.into_iter().map(AccurateF64::finish).collect()),
440 ))
441 })();
442 if !execution.all_succeeded(local.is_ok()) {
443 return local.and(Err(RuntimeError::DistributedPeerFailure));
444 }
445 let (output, sums) = local?;
446 Ok((
447 output,
448 sums.map(|sums: Vec<f64>| sums.into_iter().map(|sum| execution.sum_f64(sum)).collect()),
449 ))
450}
451
452#[cfg(feature = "wgpu")]
453fn visit_source_many<F>(
454 model: &PreparedModel,
455 execution: &Execution,
456 parameter_sets: &[(&ParamValues, &str)],
457 source: &Dataset,
458 reduction: Option<ReductionPlan>,
459 mut consume: F,
460) -> RuntimeResult<Vec<f64>>
461where
462 F: FnMut(usize, usize, &[Complex64]) -> RuntimeResult<()>,
463{
464 let local = (|| {
465 let mut offset = 0;
466 let mut sums = reduction.map(|_| {
467 parameter_sets
468 .iter()
469 .map(|_| AccurateF64::zero())
470 .collect::<Vec<_>>()
471 });
472 for batch in source
473 .batches()
474 .map_err(|error| RuntimeError::Data(error.to_string()))?
475 {
476 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
477 for (index, &(parameters, context)) in parameter_sets.iter().enumerate() {
478 let values = model.evaluate_batch(parameters, &batch).map_err(|error| {
479 RuntimeError::Parameter(format!("{context} evaluation failed: {error}"))
480 })?;
481 if let (Some(reduction), Some(sums)) = (reduction, sums.as_mut()) {
482 for (row, value) in values.iter().enumerate() {
483 let reduced = reduction.apply(*value).map_err(|error| {
484 RuntimeError::Parameter(format!("{context} reduction failed: {error}"))
485 })?;
486 sums[index].push(batch.weights_at(row) * reduced.value());
487 }
488 }
489 consume(offset, index, &values)?;
490 }
491 offset += batch.len();
492 }
493 Ok(sums
494 .unwrap_or_default()
495 .into_iter()
496 .map(AccurateF64::finish)
497 .collect::<Vec<_>>())
498 })();
499 if !execution.all_succeeded(local.is_ok()) {
500 return local.and(Err(RuntimeError::DistributedPeerFailure));
501 }
502 Ok(local?
503 .into_iter()
504 .map(|sum| execution.sum_f64(sum))
505 .collect())
506}
507
508#[cfg(feature = "wgpu")]
509#[derive(Clone)]
511pub struct WgpuPlan {
512 context: std::sync::Arc<laddu_wgpu::WgpuContext>,
513 preparation_params: ParamValues,
514 kernel: std::sync::Arc<laddu_wgpu::WgpuScalarKernel>,
515 required_event_scalars: Vec<String>,
516}
517
518#[cfg(feature = "wgpu")]
519#[derive(Clone, Debug)]
520struct WgpuDatasetPlan {
521 read_plan: laddu_data::io::ReadPlan,
522 preparation_plan: RuntimePreparationPlan,
523 device_decision: MemoryDecision,
524}
525
526#[cfg(feature = "wgpu")]
527impl WgpuDatasetPlan {
528 fn resolve(
529 read_plan: laddu_data::io::ReadPlan,
530 memory_policy: MemoryPolicy,
531 local_event_limit: usize,
532 host_footprint: MemoryFootprint,
533 prepared_footprint: MemoryFootprint,
534 host_available: u64,
535 device_available: Option<u64>,
536 ) -> RuntimeResult<Self> {
537 let mut preparation_plan = RuntimePreparationPlan::new(read_plan, local_event_limit);
538 let host_decision = preparation_plan.fit_staging(
539 "WGPU host staging",
540 host_footprint,
541 host_available,
542 "bounded host staging",
543 )?;
544 let host_chunks = local_event_limit
545 .saturating_add(host_decision.chunk_events.saturating_sub(1))
546 / host_decision.chunk_events.max(1);
547 let resident_footprint = MemoryFootprint::fixed(prepared_footprint.fixed_bytes)
548 .checked_scale_usize(host_chunks)
549 .and_then(|fixed| {
550 fixed.checked_add(MemoryFootprint::per_event(
551 prepared_footprint.bytes_per_event,
552 ))
553 })
554 .map_err(|error| RuntimeError::Data(format!("GPU working-set overflow: {error}")))?;
555 let resident_peak = resident_footprint.peak_bytes(local_event_limit);
556 let device_available = device_available
557 .ok_or_else(|| RuntimeError::Wgpu("GPU execution has no device memory pool".into()))?;
558 preparation_plan.select_storage(
559 memory_policy,
560 "device",
561 resident_peak <= device_available,
562 resident_peak,
563 device_available,
564 )?;
565 let device_decision = if preparation_plan.storage() == CacheStorage::Resident {
566 preparation_plan.fit_resident(
567 "WGPU prepared dataset",
568 resident_footprint,
569 device_available,
570 "resident",
571 host_decision.chunk_events,
572 )?
573 } else {
574 preparation_plan.fit_staging(
575 "WGPU prepared dataset",
576 prepared_footprint,
577 device_available,
578 "streaming",
579 )?
580 };
581 let chunk_events = device_decision
582 .chunk_events
583 .min(host_decision.chunk_events)
584 .max(1);
585 preparation_plan.clamp_read_plan(read_plan.chunk_size, chunk_events);
586 Ok(Self {
587 read_plan: preparation_plan.read_plan(),
588 preparation_plan,
589 device_decision,
590 })
591 }
592
593 fn reserve_storage(
594 &mut self,
595 pool: Option<&crate::MemoryPool>,
596 ) -> RuntimeResult<Option<crate::MemoryLease>> {
597 self.preparation_plan.reserve_storage(pool, || {
598 RuntimeError::Wgpu("GPU execution has no device memory pool".into())
599 })?;
600 Ok(self.preparation_plan.take_memory_lease())
601 }
602}
603
604#[cfg(feature = "wgpu")]
605impl std::fmt::Debug for WgpuPlan {
606 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 formatter
608 .debug_struct("WgpuPlan")
609 .field("adapter", &self.context.info().name)
610 .finish_non_exhaustive()
611 }
612}
613
614#[cfg(feature = "wgpu")]
615#[derive(Clone)]
617pub enum WgpuPreparedDataset {
618 Resident {
620 batches: std::sync::Arc<[laddu_wgpu::WgpuPreparedBatch]>,
622 stats: PreparedDatasetStats,
624 memory_lease: crate::MemoryLease,
626 },
627 Streaming {
629 dataset: Dataset,
631 read_plan: laddu_data::io::ReadPlan,
633 workspace: std::sync::Arc<std::sync::Mutex<Option<laddu_wgpu::WgpuPreparedBatch>>>,
635 stats: PreparedDatasetStats,
637 transient_bytes: u64,
639 },
640}
641
642#[cfg(feature = "wgpu")]
643impl std::fmt::Debug for WgpuPreparedDataset {
644 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645 formatter
646 .debug_struct("WgpuPreparedDataset")
647 .field("stats", self.stats())
648 .finish_non_exhaustive()
649 }
650}
651
652#[cfg(feature = "wgpu")]
653impl WgpuPreparedDataset {
654 pub fn stats(&self) -> &PreparedDatasetStats {
656 match self {
657 Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
658 }
659 }
660
661 fn try_for_each_prepared_batch<F>(
662 &self,
663 execution: &Execution,
664 context: &laddu_wgpu::WgpuContext,
665 kernel: &laddu_wgpu::WgpuScalarKernel,
666 preparation_params: &ParamValues,
667 mut consume: F,
668 ) -> RuntimeResult<()>
669 where
670 F: FnMut(&laddu_wgpu::WgpuPreparedBatch) -> RuntimeResult<()>,
671 {
672 match self {
673 Self::Resident { batches, .. } => {
674 for batch in batches.iter() {
675 consume(batch)?;
676 }
677 }
678 Self::Streaming {
679 dataset,
680 read_plan,
681 workspace,
682 transient_bytes,
683 ..
684 } => {
685 let _memory = execution
686 .device_memory()
687 .ok_or_else(|| {
688 RuntimeError::Wgpu("GPU execution has no device memory pool".into())
689 })?
690 .reserve(*transient_bytes)?;
691 let mut workspace = workspace.lock().map_err(|_| {
692 RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
693 })?;
694 for batch in dataset
695 .stream_with_plan(*read_plan)
696 .map_err(|error| RuntimeError::Data(error.to_string()))?
697 {
698 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
699 if let Some(prepared) = workspace.as_mut() {
700 if !kernel
701 .refresh_batch(context, preparation_params, &batch, prepared)
702 .map_err(wgpu_error)?
703 {
704 *prepared = kernel
705 .prepare_batch(context, preparation_params, &batch)
706 .map_err(wgpu_error)?;
707 }
708 } else {
709 *workspace = Some(
710 kernel
711 .prepare_batch(context, preparation_params, &batch)
712 .map_err(wgpu_error)?,
713 );
714 }
715 consume(
716 workspace
717 .as_ref()
718 .expect("streaming workspace was initialized"),
719 )?;
720 }
721 }
722 }
723 Ok(())
724 }
725}
726
727#[cfg(feature = "wgpu")]
728impl WgpuPlan {
729 fn prepare_dataset(
730 &self,
731 execution: &Execution,
732 dataset: &Dataset,
733 ) -> RuntimeResult<WgpuPreparedDataset> {
734 let preparation = DatasetPreparation::new(execution, dataset);
735 let preparation_plan = preparation.runtime_plan()?;
736 let read_plan = preparation_plan.read_plan();
737 let local_event_limit = preparation_plan.event_limit();
738 let prepared_footprint = self
739 .kernel
740 .prepared_memory_footprint(&self.preparation_params)
741 .map_err(|error| RuntimeError::Data(format!("GPU working-set overflow: {error}")))?;
742 let schema = dataset
743 .schema()
744 .map_err(|error| RuntimeError::Data(error.to_string()))?;
745 let host_footprint = BatchLayout::from_schema(&schema)
746 .schema_working_set(DataPrecision::F64, 2)
747 .map_err(|error| RuntimeError::Data(format!("host working-set overflow: {error}")))?;
748 let mut plan = WgpuDatasetPlan::resolve(
749 read_plan,
750 dataset.memory_policy(),
751 local_event_limit,
752 host_footprint,
753 prepared_footprint,
754 execution.host_memory().remaining(),
755 execution.device_memory().map(|pool| pool.remaining()),
756 )?;
757 let memory_lease = plan.reserve_storage(execution.device_memory())?;
758 for decision in plan.preparation_plan.take_decisions().into_iter().rev() {
759 execution.record_memory_decision(decision);
760 }
761 let local = (|| {
762 let mut batches = Vec::new();
763 let mut stats = DatasetStatsAccumulator::new();
764 for batch in dataset
765 .stream_with_plan(plan.read_plan)
766 .map_err(|error| RuntimeError::Data(error.to_string()))?
767 {
768 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
769 stats.observe(&batch);
770 if plan.preparation_plan.storage() == CacheStorage::Resident {
771 batches.push(
772 self.kernel
773 .prepare_batch(&self.context, &self.preparation_params, &batch)
774 .map_err(wgpu_error)?,
775 );
776 }
777 }
778 Ok::<_, RuntimeError>((batches, stats.finish()))
779 })();
780 let (batches, local_stats) = preparation.coordinate(local)?;
781 let resident_bytes = batches
782 .iter()
783 .map(laddu_wgpu::WgpuPreparedBatch::resident_bytes)
784 .sum();
785 let stats =
786 preparation.finish_stats(local_stats, resident_bytes, plan.preparation_plan.storage());
787 Ok(match plan.preparation_plan.storage() {
788 CacheStorage::Resident => WgpuPreparedDataset::Resident {
789 batches: batches.into(),
790 stats,
791 memory_lease: memory_lease.ok_or_else(|| {
792 RuntimeError::Wgpu("resident GPU dataset did not reserve device memory".into())
793 })?,
794 },
795 CacheStorage::Streaming => WgpuPreparedDataset::Streaming {
796 dataset: dataset.clone(),
797 read_plan: plan.read_plan,
798 workspace: Default::default(),
799 stats,
800 transient_bytes: plan.device_decision.estimated_peak_bytes,
801 },
802 })
803 }
804
805 fn reduce(
806 &self,
807 execution: &Execution,
808 params: &ParamValues,
809 dataset: &WgpuPreparedDataset,
810 reduction: ReductionPlan,
811 ) -> RuntimeResult<f64> {
812 let mut total = AccurateF64::zero();
813 dataset.try_for_each_prepared_batch(
814 execution,
815 &self.context,
816 &self.kernel,
817 &self.preparation_params,
818 |batch| {
819 total.push(
820 self.kernel
821 .reduce_prepared_batch(&self.context, params, batch, reduction)
822 .map_err(wgpu_error)?,
823 );
824 Ok(())
825 },
826 )?;
827 Ok(execution.sum_f64(total.finish()))
828 }
829
830 fn reduce_with_gradient(
831 &self,
832 execution: &Execution,
833 params: &ParamValues,
834 dataset: &WgpuPreparedDataset,
835 reduction: ReductionPlan,
836 ) -> RuntimeResult<ReductionEvaluation> {
837 let mut total = AccurateF64::zero();
838 let mut gradient = (0..params.layout().n_free())
839 .map(|_| AccurateF64::zero())
840 .collect::<Vec<_>>();
841 let mut consume = |batch: &laddu_wgpu::WgpuPreparedBatch| -> RuntimeResult<()> {
842 let (value, values) = self
843 .kernel
844 .reduce_prepared_batch_with_gradient(&self.context, params, batch, reduction)
845 .map_err(wgpu_error)?;
846 total.push(value);
847 for (sum, value) in gradient.iter_mut().zip(values) {
848 sum.push(value);
849 }
850 Ok(())
851 };
852 dataset.try_for_each_prepared_batch(
853 execution,
854 &self.context,
855 &self.kernel,
856 &self.preparation_params,
857 &mut consume,
858 )?;
859 let gradient = gradient
860 .into_iter()
861 .map(|sum| execution.sum_f64(sum.finish()))
862 .collect();
863 Ok(ReductionEvaluation::new(
864 execution.sum_f64(total.finish()),
865 gradient,
866 ))
867 }
868}
869
870#[cfg(feature = "wgpu")]
871fn wgpu_error(error: laddu_wgpu::WgpuError) -> RuntimeError {
872 RuntimeError::Wgpu(error.to_string())
873}
874
875#[cfg(test)]
876mod prepared_evaluation_tests {
877 use std::{cell::RefCell, rc::Rc, sync::Arc};
878
879 use laddu_compile::{CompiledModel, ReductionPlan};
880 use laddu_data::{
881 data::{Dataset, EventBatch, OwnedEvent},
882 schema::Schema,
883 };
884 use laddu_expr::{event_scalar, parameter};
885 use num::complex::Complex64;
886
887 use super::PreparedModel;
888 use crate::{CpuOptions, Device, Execution, ExecutionOptions, JitPolicy, ThreadPolicy};
889
890 fn dataset(streaming: bool) -> Dataset {
891 let schema = Arc::new(
892 Schema::new(std::iter::empty::<&str>(), ["x"], false)
893 .expect("test schema should be valid"),
894 );
895 let first_batch = EventBatch::from_events(
896 schema.clone(),
897 [
898 OwnedEvent::new(vec![], vec![1.0]),
899 OwnedEvent::new(vec![], vec![2.0]),
900 ],
901 )
902 .expect("first test batch should be valid");
903 let second_batch = EventBatch::from_events(schema, [OwnedEvent::new(vec![], vec![3.0])])
904 .expect("second test batch should be valid");
905 let dataset = Dataset::from_batches(vec![first_batch, second_batch])
906 .expect("test dataset should be valid");
907 if streaming {
908 dataset.streaming()
909 } else {
910 dataset.fastest()
911 }
912 }
913
914 #[test]
915 fn prepared_evaluation_preserves_event_order_for_resident_and_streaming_data() {
916 let model =
917 CompiledModel::from_expr(&(event_scalar("x") + parameter!("offset", initial: 0.5)))
918 .expect("test model should compile");
919 let execution = Execution::default();
920 let prepared_model =
921 PreparedModel::prepare(&model, &execution).expect("model should prepare");
922 let parameters = [
923 model.params().values(&[0.5]).expect("first parameters"),
924 model.params().values(&[1.5]).expect("second parameters"),
925 ];
926 let expected = [
927 vec![
928 Complex64::new(1.5, 0.0),
929 Complex64::new(2.5, 0.0),
930 Complex64::new(3.5, 0.0),
931 ],
932 vec![
933 Complex64::new(2.5, 0.0),
934 Complex64::new(3.5, 0.0),
935 Complex64::new(4.5, 0.0),
936 ],
937 ];
938
939 for streaming in [false, true] {
940 let source = dataset(streaming);
941 let prepared = prepared_model
942 .prepare_dataset(&execution, &source)
943 .expect("dataset should prepare");
944 let actual = prepared_model
945 .evaluate_prepared_many(&execution, ¶meters, &prepared, &source)
946 .expect("prepared dataset should evaluate");
947 assert_eq!(actual, expected);
948 let (actual, sums) = prepared_model
949 .evaluate_prepared_many_with_reduction(
950 &execution,
951 ¶meters,
952 &prepared,
953 &source,
954 ReductionPlan::weighted_positive_real(),
955 )
956 .expect("prepared dataset should evaluate and reduce");
957 assert_eq!(actual, expected);
958 assert_eq!(sums, [7.5, 10.5]);
959 let mut visited = vec![Vec::new(), Vec::new()];
960 let parameter_sets = [
961 (¶meters[0], "central"),
962 (¶meters[1], "ensemble draw 0"),
963 ];
964 let sums = prepared_model
965 .visit_prepared_many(
966 &execution,
967 ¶meter_sets,
968 &prepared,
969 &source,
970 Some(ReductionPlan::weighted_positive_real()),
971 |_, parameter_index, values| {
972 visited[parameter_index].extend_from_slice(values);
973 Ok(())
974 },
975 )
976 .expect("prepared blocks should be visited and reduced");
977 assert_eq!(visited, expected);
978 assert_eq!(sums, [7.5, 10.5]);
979 }
980 }
981
982 #[test]
983 fn prepared_block_visitors_run_inside_the_execution_owned_pool() {
984 let model =
985 CompiledModel::from_expr(&event_scalar("x")).expect("test model should compile");
986 let execution = Execution::local(ExecutionOptions {
987 device: Device::Cpu(CpuOptions {
988 threads: ThreadPolicy::Fixed(2),
989 jit: JitPolicy::Disabled,
990 }),
991 ..ExecutionOptions::default()
992 })
993 .expect("fixed-thread execution should build");
994 let prepared_model =
995 PreparedModel::prepare(&model, &execution).expect("model should prepare");
996 let source = dataset(false);
997 let prepared = prepared_model
998 .prepare_dataset(&execution, &source)
999 .expect("dataset should prepare");
1000 let parameters = model.params().values(&[]).expect("parameters should build");
1001 let mut visitor_pool_threads = Vec::new();
1002
1003 prepared_model
1004 .visit_prepared_many_parallel(
1005 &execution,
1006 &[(¶meters, "central")],
1007 &prepared,
1008 &source,
1009 None,
1010 |_, _, _| {
1011 visitor_pool_threads.push(rayon::current_num_threads());
1012 Ok(())
1013 },
1014 )
1015 .expect("prepared blocks should be visited");
1016
1017 assert!(!visitor_pool_threads.is_empty());
1018 assert!(visitor_pool_threads.iter().all(|&threads| threads == 2));
1019 }
1020
1021 #[test]
1022 fn existing_prepared_visitor_accepts_non_send_callbacks() {
1023 let model =
1024 CompiledModel::from_expr(&event_scalar("x")).expect("test model should compile");
1025 let execution = Execution::default();
1026 let prepared_model =
1027 PreparedModel::prepare(&model, &execution).expect("model should prepare");
1028 let source = dataset(false);
1029 let prepared = prepared_model
1030 .prepare_dataset(&execution, &source)
1031 .expect("dataset should prepare");
1032 let parameters = model.params().values(&[]).expect("parameters should build");
1033 let visited = Rc::new(RefCell::new(Vec::new()));
1034 let captured = Rc::clone(&visited);
1035
1036 prepared_model
1037 .visit_prepared_many(
1038 &execution,
1039 &[(¶meters, "central")],
1040 &prepared,
1041 &source,
1042 None,
1043 move |_, _, values| {
1044 captured.borrow_mut().extend_from_slice(values);
1045 Ok(())
1046 },
1047 )
1048 .expect("non-Send visitor should remain supported");
1049
1050 assert_eq!(visited.borrow().len(), 3);
1051 }
1052}
1053
1054#[cfg(all(test, feature = "wgpu"))]
1055mod tests {
1056 use std::sync::Arc;
1057
1058 use laddu_compile::{CompiledModel, ReductionPlan};
1059 use laddu_data::{
1060 data::{Dataset, EventBatch, OwnedEvent},
1061 schema::Schema,
1062 };
1063 use laddu_expr::{complex, event_scalar, parameter};
1064
1065 use super::*;
1066 use crate::{CpuOptions, Device, ExecutionOptions, GpuBackend, GpuOptions, Precision};
1067
1068 #[test]
1069 fn wgpu_dataset_plan_resolves_storage_and_chunk_limits_without_hardware() {
1070 let mut read_plan = laddu_data::io::ReadPlan::serial();
1071 read_plan.chunk_size = Some(3);
1072 let plan = WgpuDatasetPlan::resolve(
1073 read_plan,
1074 MemoryPolicy::Fastest,
1075 100,
1076 MemoryFootprint::new(100, 8),
1077 MemoryFootprint::new(200, 4),
1078 1_000,
1079 Some(10_000),
1080 )
1081 .unwrap();
1082
1083 assert_eq!(plan.preparation_plan.storage(), CacheStorage::Resident);
1084 assert_eq!(plan.read_plan.chunk_size, Some(3));
1085 assert_eq!(plan.device_decision.chunk_events, 100);
1086 assert_eq!(plan.device_decision.estimated_peak_bytes, 600);
1087 }
1088
1089 #[test]
1090 fn wgpu_dataset_plan_falls_back_to_streaming_when_resident_does_not_fit() {
1091 let plan = WgpuDatasetPlan::resolve(
1092 laddu_data::io::ReadPlan::serial(),
1093 MemoryPolicy::Fastest,
1094 100,
1095 MemoryFootprint::new(100, 8),
1096 MemoryFootprint::new(500, 10),
1097 1_000,
1098 Some(1_000),
1099 )
1100 .unwrap();
1101
1102 assert_eq!(plan.preparation_plan.storage(), CacheStorage::Streaming);
1103 assert_eq!(plan.read_plan.chunk_size, Some(50));
1104 assert_eq!(plan.device_decision.chunk_events, 50);
1105 assert_eq!(plan.device_decision.estimated_peak_bytes, 1_000);
1106 }
1107
1108 #[test]
1109 fn wgpu_dataset_plan_reports_host_failure_before_missing_device_pool() {
1110 let error = WgpuDatasetPlan::resolve(
1111 laddu_data::io::ReadPlan::serial(),
1112 MemoryPolicy::Fastest,
1113 1,
1114 MemoryFootprint::new(100, 8),
1115 MemoryFootprint::new(200, 4),
1116 100,
1117 None,
1118 )
1119 .unwrap_err();
1120
1121 assert!(matches!(
1122 error,
1123 RuntimeError::Memory(laddu_memory::MemoryError::BudgetExceeded { resource, .. })
1124 if resource == "WGPU host staging"
1125 ));
1126 }
1127
1128 #[test]
1129 #[ignore = "requires a WGPU-compatible hardware adapter"]
1130 fn wgpu_resident_and_streaming_reductions_match_f32_cpu() {
1131 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
1132 let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
1133 let x = event_scalar("x");
1134 let expression = (x.clone() * scale.clone() + offset.clone()).sin()
1135 + complex(scale, offset).norm_sqr()
1136 + 2.0;
1137 let model = CompiledModel::from_expr(&expression).unwrap();
1138 let params = model.params().default_values();
1139 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
1140 let dataset = Dataset::from_batches(vec![
1141 EventBatch::from_events(
1142 schema.clone(),
1143 [
1144 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
1145 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
1146 ],
1147 )
1148 .unwrap(),
1149 EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![1.25], 2.0)])
1150 .unwrap(),
1151 ])
1152 .unwrap();
1153 let wgpu_execution = Execution::local(ExecutionOptions {
1154 device: Device::Gpu(GpuOptions {
1155 backend: GpuBackend::Wgpu,
1156 ..GpuOptions::default()
1157 }),
1158 memory: crate::MemoryPlan::host_device(
1159 crate::MemoryBudget::Auto,
1160 crate::MemoryBudget::Bytes(256),
1161 ),
1162 precision: Precision::F32,
1163 ..ExecutionOptions::default()
1164 })
1165 .unwrap();
1166 let cpu_execution = Execution::local(ExecutionOptions {
1167 device: Device::Cpu(CpuOptions::default()),
1168 precision: Precision::F32,
1169 ..ExecutionOptions::default()
1170 })
1171 .unwrap();
1172 let wgpu = PreparedModel::prepare(&model, &wgpu_execution).unwrap();
1173 let cpu = PreparedModel::prepare(&model, &cpu_execution).unwrap();
1174 let resident = wgpu
1175 .prepare_dataset(&wgpu_execution, &dataset.clone().resident())
1176 .unwrap();
1177 let streaming = wgpu
1178 .prepare_dataset(&wgpu_execution, &dataset.clone().streaming())
1179 .unwrap();
1180 let cpu_data = cpu.prepare_dataset(&cpu_execution, &dataset).unwrap();
1181
1182 assert_eq!(resident.stats().storage(), CacheStorage::Resident);
1183 assert_eq!(streaming.stats().storage(), CacheStorage::Streaming);
1184 assert!(resident.stats().resident_bytes() > 0);
1185 assert_eq!(streaming.stats().resident_bytes(), 0);
1186
1187 let cpu_reduction = cpu
1188 .reduce_with_gradient(
1189 &cpu_execution,
1190 ¶ms,
1191 &cpu_data,
1192 ReductionPlan::weighted_real(),
1193 )
1194 .unwrap();
1195 let resident_reduction = wgpu
1196 .reduce_with_gradient(
1197 &wgpu_execution,
1198 ¶ms,
1199 &resident,
1200 ReductionPlan::weighted_real(),
1201 )
1202 .unwrap();
1203 let streaming_reduction = wgpu
1204 .reduce_with_gradient(
1205 &wgpu_execution,
1206 ¶ms,
1207 &streaming,
1208 ReductionPlan::weighted_real(),
1209 )
1210 .unwrap();
1211
1212 for actual in [&resident_reduction, &streaming_reduction] {
1213 assert!((actual.value() - cpu_reduction.value()).abs() <= 1.0e-4);
1214 assert_eq!(actual.gradient().len(), cpu_reduction.gradient().len());
1215 for (actual, expected) in actual.gradient().iter().zip(cpu_reduction.gradient()) {
1216 assert!((actual - expected).abs() <= 1.0e-4);
1217 }
1218 }
1219 assert!((resident_reduction.value() - streaming_reduction.value()).abs() <= 1.0e-6);
1220 for (resident, streaming) in resident_reduction
1221 .gradient()
1222 .iter()
1223 .zip(streaming_reduction.gradient())
1224 {
1225 assert!((resident - streaming).abs() <= 1.0e-6);
1226 }
1227 }
1228}