1use laddu_compile::{CompiledModel, ReductionPlan};
2use laddu_data::data::Dataset;
3use laddu_data::data::EventBatch;
4#[cfg(feature = "wgpu")]
5use laddu_data::data::{CacheStorage, MemoryPolicy, accurate::AccurateF64};
6use laddu_expr::parameters::ParamValues;
7use num::complex::Complex64;
8
9use crate::{
10 CpuBackend, CpuPlan, CpuPreparedDataset, Execution, PreparedDatasetStats, ReductionEvaluation,
11 RuntimeError, RuntimeResult,
12};
13
14#[derive(Clone, Debug)]
16pub enum PreparedModel {
17 Cpu(Box<CpuPlan>),
19 #[cfg(feature = "wgpu")]
20 Wgpu(WgpuPlan),
22}
23
24#[derive(Clone, Debug)]
26pub enum PreparedDataset {
27 Cpu(CpuPreparedDataset),
29 #[cfg(feature = "wgpu")]
30 Wgpu(WgpuPreparedDataset),
32}
33
34impl PreparedDataset {
35 pub fn stats(&self) -> &PreparedDatasetStats {
37 match self {
38 Self::Cpu(dataset) => dataset.stats(),
39 #[cfg(feature = "wgpu")]
40 Self::Wgpu(dataset) => dataset.stats(),
41 }
42 }
43}
44
45impl PreparedModel {
46 pub fn evaluate_batch(
53 &self,
54 params: &ParamValues,
55 batch: &EventBatch,
56 ) -> RuntimeResult<Vec<Complex64>> {
57 match self {
58 Self::Cpu(plan) => plan.evaluate_batch(params, batch),
59 #[cfg(feature = "wgpu")]
60 Self::Wgpu(plan) => plan
61 .kernel
62 .evaluate_batch(&plan.context, params, batch)
63 .map(|values| {
64 values
65 .into_iter()
66 .map(|(re, im)| Complex64::new(re, im))
67 .collect()
68 })
69 .map_err(wgpu_error),
70 }
71 }
72
73 pub fn evaluate_batch_with_gradient(
80 &self,
81 params: &ParamValues,
82 batch: &EventBatch,
83 ) -> RuntimeResult<Vec<crate::ValueGradient>> {
84 match self {
85 Self::Cpu(plan) => plan.evaluate_batch_with_gradient(params, batch),
86 #[cfg(feature = "wgpu")]
87 Self::Wgpu(_) => Err(RuntimeError::Wgpu(
88 "event-wise model gradients are not implemented by the WGPU backend".into(),
89 )),
90 }
91 }
92
93 pub fn prepare(model: &CompiledModel, execution: &Execution) -> RuntimeResult<Self> {
100 #[cfg(feature = "wgpu")]
101 if let Some(context) = execution.wgpu_context() {
102 return Ok(Self::Wgpu(WgpuPlan {
103 context: context.clone(),
104 preparation_params: model.params().default_values(),
105 kernel: std::sync::Arc::new(
106 laddu_wgpu::WgpuScalarKernel::compile(context, model).map_err(wgpu_error)?,
107 ),
108 }));
109 }
110 Ok(Self::Cpu(Box::new(
111 CpuBackend.prepare_for_execution(model, execution)?,
112 )))
113 }
114
115 pub fn prepare_dataset(
122 &self,
123 execution: &Execution,
124 dataset: &Dataset,
125 ) -> RuntimeResult<PreparedDataset> {
126 match self {
127 Self::Cpu(plan) => Ok(PreparedDataset::Cpu(
128 plan.prepare_dataset(execution, dataset)?,
129 )),
130 #[cfg(feature = "wgpu")]
131 Self::Wgpu(plan) => plan
132 .prepare_dataset(execution, dataset)
133 .map(PreparedDataset::Wgpu),
134 }
135 }
136
137 pub fn reduce(
144 &self,
145 execution: &Execution,
146 params: &ParamValues,
147 dataset: &PreparedDataset,
148 reduction: ReductionPlan,
149 ) -> RuntimeResult<f64> {
150 #[allow(unreachable_patterns)]
151 match (self, dataset) {
152 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
153 plan.reduce(execution, params, dataset, reduction)
154 }
155 #[cfg(feature = "wgpu")]
156 (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
157 plan.reduce(execution, params, dataset, reduction)
158 }
159 _ => Err(RuntimeError::InvalidShape {
160 index: 0,
161 message: "prepared model and dataset use different backends".into(),
162 }),
163 }
164 }
165
166 pub fn reduce_with_gradient(
173 &self,
174 execution: &Execution,
175 params: &ParamValues,
176 dataset: &PreparedDataset,
177 reduction: ReductionPlan,
178 ) -> RuntimeResult<ReductionEvaluation> {
179 #[allow(unreachable_patterns)]
180 match (self, dataset) {
181 (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
182 plan.reduce_with_gradient(execution, params, dataset, reduction)
183 }
184 #[cfg(feature = "wgpu")]
185 (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
186 plan.reduce_with_gradient(execution, params, dataset, reduction)
187 }
188 _ => Err(RuntimeError::InvalidShape {
189 index: 0,
190 message: "prepared model and dataset use different backends".into(),
191 }),
192 }
193 }
194}
195
196#[cfg(feature = "wgpu")]
197#[derive(Clone)]
199pub struct WgpuPlan {
200 context: std::sync::Arc<laddu_wgpu::WgpuContext>,
201 preparation_params: ParamValues,
202 kernel: std::sync::Arc<laddu_wgpu::WgpuScalarKernel>,
203}
204
205#[cfg(feature = "wgpu")]
206impl std::fmt::Debug for WgpuPlan {
207 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 formatter
209 .debug_struct("WgpuPlan")
210 .field("adapter", &self.context.info().name)
211 .finish_non_exhaustive()
212 }
213}
214
215#[cfg(feature = "wgpu")]
216#[derive(Clone)]
218pub enum WgpuPreparedDataset {
219 Resident {
221 batches: std::sync::Arc<[laddu_wgpu::WgpuPreparedBatch]>,
223 stats: PreparedDatasetStats,
225 memory_lease: crate::MemoryLease,
227 },
228 Streaming {
230 dataset: Dataset,
232 read_plan: laddu_data::io::ReadPlan,
234 workspace: std::sync::Arc<std::sync::Mutex<Option<laddu_wgpu::WgpuPreparedBatch>>>,
236 stats: PreparedDatasetStats,
238 transient_bytes: u64,
240 },
241}
242
243#[cfg(feature = "wgpu")]
244impl std::fmt::Debug for WgpuPreparedDataset {
245 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 formatter
247 .debug_struct("WgpuPreparedDataset")
248 .field("stats", self.stats())
249 .finish_non_exhaustive()
250 }
251}
252
253#[cfg(feature = "wgpu")]
254impl WgpuPreparedDataset {
255 pub fn stats(&self) -> &PreparedDatasetStats {
257 match self {
258 Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
259 }
260 }
261}
262
263#[cfg(feature = "wgpu")]
264impl WgpuPlan {
265 fn prepare_dataset(
266 &self,
267 execution: &Execution,
268 dataset: &Dataset,
269 ) -> RuntimeResult<WgpuPreparedDataset> {
270 let read_plan = execution.read_plan(dataset.read_plan());
271 let mut read_plan = read_plan;
272 let known_local_events = dataset
273 .num_events()
274 .map_err(|error| RuntimeError::Data(error.to_string()))?
275 .and_then(|events| usize::try_from(events).ok());
276 let discovered_events =
277 if known_local_events.is_none() && dataset.memory_policy() != MemoryPolicy::Streaming {
278 let local = (|| {
279 let mut events = 0;
280 for batch in dataset
281 .batches_with_plan(read_plan)
282 .map_err(|error| RuntimeError::Data(error.to_string()))?
283 {
284 events += batch
285 .map_err(|error| RuntimeError::Data(error.to_string()))?
286 .len();
287 }
288 Ok::<_, RuntimeError>(events)
289 })();
290 if !execution.all_succeeded(local.is_ok()) {
291 return local.and(Err(RuntimeError::DistributedPeerFailure));
292 }
293 Some(local?)
294 } else {
295 None
296 };
297 let local_event_limit = known_local_events
298 .or(discovered_events)
299 .unwrap_or(usize::MAX);
300 let fixed = self
301 .kernel
302 .prepared_memory_estimate(&self.preparation_params, 0);
303 let one = self
304 .kernel
305 .prepared_memory_estimate(&self.preparation_params, 1);
306 let per_event = one.saturating_sub(fixed);
307 let schema = dataset
308 .schema()
309 .map_err(|error| RuntimeError::Data(error.to_string()))?;
310 let host_bytes_per_event =
311 (4 * schema.n_p4s() + schema.n_scalars() + usize::from(schema.has_weight()))
312 .saturating_mul(size_of::<f64>())
313 .saturating_mul(2);
314 let host_decision = crate::MemoryDecision::fit(
315 "WGPU host staging",
316 0,
317 u64::try_from(host_bytes_per_event).unwrap_or(u64::MAX),
318 execution.host_memory().remaining(),
319 local_event_limit,
320 "bounded host staging",
321 )?;
322 let host_chunks = local_event_limit
323 .saturating_add(host_decision.chunk_events.saturating_sub(1))
324 / host_decision.chunk_events.max(1);
325 let full = per_event
326 .saturating_mul(local_event_limit)
327 .saturating_add(fixed.saturating_mul(host_chunks));
328 let device_pool = execution
329 .device_memory()
330 .ok_or_else(|| RuntimeError::Wgpu("GPU execution has no device memory pool".into()))?;
331 let storage = match dataset.memory_policy() {
332 MemoryPolicy::Streaming => CacheStorage::Streaming,
333 MemoryPolicy::Resident => {
334 if u64::try_from(full).unwrap_or(u64::MAX) > device_pool.remaining() {
335 return Err(laddu_memory::MemoryError::BudgetExceeded {
336 resource: "device".into(),
337 requested: u64::try_from(full).unwrap_or(u64::MAX),
338 remaining: device_pool.remaining(),
339 }
340 .into());
341 }
342 CacheStorage::Resident
343 }
344 MemoryPolicy::Fastest
345 if u64::try_from(full).unwrap_or(u64::MAX) <= device_pool.remaining() =>
346 {
347 CacheStorage::Resident
348 }
349 MemoryPolicy::Fastest => CacheStorage::Streaming,
350 };
351 let memory_lease = if storage == CacheStorage::Resident {
352 Some(device_pool.reserve(u64::try_from(full).unwrap_or(u64::MAX))?)
353 } else {
354 None
355 };
356 let device_decision = if storage == CacheStorage::Resident {
357 crate::MemoryDecision {
358 label: "WGPU prepared dataset".into(),
359 fixed_bytes: u64::try_from(fixed.saturating_mul(host_chunks)).unwrap_or(u64::MAX),
360 bytes_per_event: u64::try_from(per_event).unwrap_or(u64::MAX),
361 chunk_events: host_decision.chunk_events,
362 estimated_peak_bytes: u64::try_from(full).unwrap_or(u64::MAX),
363 actual_high_water_bytes: None,
364 strategy: "resident".into(),
365 }
366 } else {
367 crate::MemoryDecision::fit(
368 "WGPU prepared dataset",
369 u64::try_from(fixed).unwrap_or(u64::MAX),
370 u64::try_from(per_event).unwrap_or(u64::MAX),
371 device_pool.remaining(),
372 local_event_limit,
373 "streaming",
374 )?
375 };
376 let chunk_events = device_decision
377 .chunk_events
378 .min(host_decision.chunk_events)
379 .max(1);
380 read_plan.chunk_size = Some(
381 read_plan
382 .chunk_size
383 .map_or(chunk_events, |manual| manual.min(chunk_events))
384 .max(1),
385 );
386 execution.record_memory_decision(device_decision.clone());
387 execution.record_memory_decision(host_decision);
388 let local = (|| {
389 let mut batches = Vec::new();
390 let mut events = 0;
391 let mut batch_count = 0;
392 let mut sum_weights = AccurateF64::zero();
393 for batch in dataset
394 .batches_with_plan(read_plan)
395 .map_err(|error| RuntimeError::Data(error.to_string()))?
396 {
397 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
398 events += batch.len();
399 batch_count += 1;
400 for row in 0..batch.len() {
401 sum_weights.push(batch.weights_at(row));
402 }
403 if storage == CacheStorage::Resident {
404 batches.push(
405 self.kernel
406 .prepare_batch(&self.context, &self.preparation_params, &batch)
407 .map_err(wgpu_error)?,
408 );
409 }
410 }
411 Ok::<_, RuntimeError>((batches, events, batch_count, sum_weights.finish()))
412 })();
413 if !execution.all_succeeded(local.is_ok()) {
414 return local.and(Err(RuntimeError::DistributedPeerFailure));
415 }
416 let (batches, events, batch_count, sum_weights) = local?;
417 let resident_bytes = batches
418 .iter()
419 .map(laddu_wgpu::WgpuPreparedBatch::resident_bytes)
420 .sum();
421 let stats = PreparedDatasetStats::new(
422 events,
423 execution.sum_usize(events),
424 batch_count,
425 execution.sum_f64(sum_weights),
426 resident_bytes,
427 storage,
428 );
429 Ok(match storage {
430 CacheStorage::Resident => WgpuPreparedDataset::Resident {
431 batches: batches.into(),
432 stats,
433 memory_lease: memory_lease.ok_or_else(|| {
434 RuntimeError::Wgpu("resident GPU dataset did not reserve device memory".into())
435 })?,
436 },
437 CacheStorage::Streaming => WgpuPreparedDataset::Streaming {
438 dataset: dataset.clone(),
439 read_plan,
440 workspace: Default::default(),
441 stats,
442 transient_bytes: device_decision.estimated_peak_bytes,
443 },
444 })
445 }
446
447 fn reduce(
448 &self,
449 execution: &Execution,
450 params: &ParamValues,
451 dataset: &WgpuPreparedDataset,
452 reduction: ReductionPlan,
453 ) -> RuntimeResult<f64> {
454 let mut total = AccurateF64::zero();
455 match dataset {
456 WgpuPreparedDataset::Resident { batches, .. } => {
457 for batch in batches.iter() {
458 total.push(
459 self.kernel
460 .reduce_prepared_batch(&self.context, params, batch, reduction)
461 .map_err(wgpu_error)?,
462 );
463 }
464 }
465 WgpuPreparedDataset::Streaming {
466 dataset,
467 read_plan,
468 workspace,
469 transient_bytes,
470 ..
471 } => {
472 let _memory = execution
473 .device_memory()
474 .ok_or_else(|| {
475 RuntimeError::Wgpu("GPU execution has no device memory pool".into())
476 })?
477 .reserve(*transient_bytes)?;
478 let mut workspace = workspace.lock().map_err(|_| {
479 RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
480 })?;
481 for batch in dataset
482 .batches_with_plan(*read_plan)
483 .map_err(|error| RuntimeError::Data(error.to_string()))?
484 {
485 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
486 if let Some(prepared) = workspace.as_mut() {
487 if !self
488 .kernel
489 .refresh_batch(
490 &self.context,
491 &self.preparation_params,
492 &batch,
493 prepared,
494 )
495 .map_err(wgpu_error)?
496 {
497 *prepared = self
498 .kernel
499 .prepare_batch(&self.context, &self.preparation_params, &batch)
500 .map_err(wgpu_error)?;
501 }
502 } else {
503 *workspace = Some(
504 self.kernel
505 .prepare_batch(&self.context, &self.preparation_params, &batch)
506 .map_err(wgpu_error)?,
507 );
508 }
509 total.push(
510 self.kernel
511 .reduce_prepared_batch(
512 &self.context,
513 params,
514 workspace
515 .as_ref()
516 .expect("streaming workspace was initialized"),
517 reduction,
518 )
519 .map_err(wgpu_error)?,
520 );
521 }
522 }
523 }
524 Ok(execution.sum_f64(total.finish()))
525 }
526
527 fn reduce_with_gradient(
528 &self,
529 execution: &Execution,
530 params: &ParamValues,
531 dataset: &WgpuPreparedDataset,
532 reduction: ReductionPlan,
533 ) -> RuntimeResult<ReductionEvaluation> {
534 let mut total = AccurateF64::zero();
535 let mut gradient = (0..params.layout().n_free())
536 .map(|_| AccurateF64::zero())
537 .collect::<Vec<_>>();
538 let mut consume = |batch: &laddu_wgpu::WgpuPreparedBatch| -> RuntimeResult<()> {
539 let (value, values) = self
540 .kernel
541 .reduce_prepared_batch_with_gradient(&self.context, params, batch, reduction)
542 .map_err(wgpu_error)?;
543 total.push(value);
544 for (sum, value) in gradient.iter_mut().zip(values) {
545 sum.push(value);
546 }
547 Ok(())
548 };
549 match dataset {
550 WgpuPreparedDataset::Resident { batches, .. } => {
551 for batch in batches.iter() {
552 consume(batch)?;
553 }
554 }
555 WgpuPreparedDataset::Streaming {
556 dataset,
557 read_plan,
558 workspace,
559 transient_bytes,
560 ..
561 } => {
562 let _memory = execution
563 .device_memory()
564 .ok_or_else(|| {
565 RuntimeError::Wgpu("GPU execution has no device memory pool".into())
566 })?
567 .reserve(*transient_bytes)?;
568 let mut workspace = workspace.lock().map_err(|_| {
569 RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
570 })?;
571 for batch in dataset
572 .batches_with_plan(*read_plan)
573 .map_err(|error| RuntimeError::Data(error.to_string()))?
574 {
575 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
576 if let Some(prepared) = workspace.as_mut() {
577 if !self
578 .kernel
579 .refresh_batch(
580 &self.context,
581 &self.preparation_params,
582 &batch,
583 prepared,
584 )
585 .map_err(wgpu_error)?
586 {
587 *prepared = self
588 .kernel
589 .prepare_batch(&self.context, &self.preparation_params, &batch)
590 .map_err(wgpu_error)?;
591 }
592 } else {
593 *workspace = Some(
594 self.kernel
595 .prepare_batch(&self.context, &self.preparation_params, &batch)
596 .map_err(wgpu_error)?,
597 );
598 }
599 consume(
600 workspace
601 .as_ref()
602 .expect("streaming workspace was initialized"),
603 )?;
604 }
605 }
606 }
607 let gradient = gradient
608 .into_iter()
609 .map(|sum| execution.sum_f64(sum.finish()))
610 .collect();
611 Ok(ReductionEvaluation::new(
612 execution.sum_f64(total.finish()),
613 gradient,
614 ))
615 }
616}
617
618#[cfg(feature = "wgpu")]
619fn wgpu_error(error: laddu_wgpu::WgpuError) -> RuntimeError {
620 RuntimeError::Wgpu(error.to_string())
621}
622
623#[cfg(all(test, feature = "wgpu"))]
624mod tests {
625 use std::sync::Arc;
626
627 use laddu_compile::{CompiledModel, ReductionPlan};
628 use laddu_data::{
629 data::{Dataset, EventBatch, OwnedEvent},
630 schema::Schema,
631 };
632 use laddu_expr::{complex, event_scalar, parameter};
633
634 use super::*;
635 use crate::{CpuOptions, Device, ExecutionOptions, GpuBackend, GpuOptions, Precision};
636
637 #[test]
638 #[ignore = "requires a WGPU-compatible hardware adapter"]
639 fn wgpu_resident_and_streaming_reductions_match_f32_cpu() {
640 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
641 let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
642 let x = event_scalar("x");
643 let expression = (x.clone() * scale.clone() + offset.clone()).sin()
644 + complex(scale, offset).norm_sqr()
645 + 2.0;
646 let model = CompiledModel::from_expr(&expression).unwrap();
647 let params = model.params().default_values();
648 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
649 let dataset = Dataset::from_batches(vec![
650 EventBatch::from_events(
651 schema.clone(),
652 [
653 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
654 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
655 ],
656 )
657 .unwrap(),
658 EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![1.25], 2.0)])
659 .unwrap(),
660 ])
661 .unwrap();
662 let wgpu_execution = Execution::local(ExecutionOptions {
663 device: Device::Gpu(GpuOptions {
664 backend: GpuBackend::Wgpu,
665 ..GpuOptions::default()
666 }),
667 memory: crate::MemoryPlan::host_device(
668 crate::MemoryBudget::Auto,
669 crate::MemoryBudget::Bytes(256),
670 ),
671 precision: Precision::F32,
672 ..ExecutionOptions::default()
673 })
674 .unwrap();
675 let cpu_execution = Execution::local(ExecutionOptions {
676 device: Device::Cpu(CpuOptions::default()),
677 precision: Precision::F32,
678 ..ExecutionOptions::default()
679 })
680 .unwrap();
681 let wgpu = PreparedModel::prepare(&model, &wgpu_execution).unwrap();
682 let cpu = PreparedModel::prepare(&model, &cpu_execution).unwrap();
683 let resident = wgpu
684 .prepare_dataset(&wgpu_execution, &dataset.clone().resident())
685 .unwrap();
686 let streaming = wgpu
687 .prepare_dataset(&wgpu_execution, &dataset.clone().streaming())
688 .unwrap();
689 let cpu_data = cpu.prepare_dataset(&cpu_execution, &dataset).unwrap();
690
691 assert_eq!(resident.stats().storage(), CacheStorage::Resident);
692 assert_eq!(streaming.stats().storage(), CacheStorage::Streaming);
693 assert!(resident.stats().resident_bytes() > 0);
694 assert_eq!(streaming.stats().resident_bytes(), 0);
695
696 let cpu_reduction = cpu
697 .reduce_with_gradient(
698 &cpu_execution,
699 ¶ms,
700 &cpu_data,
701 ReductionPlan::weighted_real(),
702 )
703 .unwrap();
704 let resident_reduction = wgpu
705 .reduce_with_gradient(
706 &wgpu_execution,
707 ¶ms,
708 &resident,
709 ReductionPlan::weighted_real(),
710 )
711 .unwrap();
712 let streaming_reduction = wgpu
713 .reduce_with_gradient(
714 &wgpu_execution,
715 ¶ms,
716 &streaming,
717 ReductionPlan::weighted_real(),
718 )
719 .unwrap();
720
721 for actual in [&resident_reduction, &streaming_reduction] {
722 assert!((actual.value() - cpu_reduction.value()).abs() <= 1.0e-4);
723 assert_eq!(actual.gradient().len(), cpu_reduction.gradient().len());
724 for (actual, expected) in actual.gradient().iter().zip(cpu_reduction.gradient()) {
725 assert!((actual - expected).abs() <= 1.0e-4);
726 }
727 }
728 assert!((resident_reduction.value() - streaming_reduction.value()).abs() <= 1.0e-6);
729 for (resident, streaming) in resident_reduction
730 .gradient()
731 .iter()
732 .zip(streaming_reduction.gradient())
733 {
734 assert!((resident - streaming).abs() <= 1.0e-6);
735 }
736 }
737}