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