1use std::time::{Duration, Instant};
29
30use crate::lbm_gpu::{LbmConfig, LbmSimulation};
31use crate::sph_gpu::{SphConfig, SphSimulation};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum BackendKind {
38 Cpu,
40 Wgpu,
42 Cuda,
44}
45
46impl std::fmt::Display for BackendKind {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Cpu => write!(f, "CPU"),
50 Self::Wgpu => write!(f, "wgpu"),
51 Self::Cuda => write!(f, "CUDA"),
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
60pub struct GpuBenchReport {
61 pub name: String,
63 pub backend: BackendKind,
65 pub n: usize,
67 pub iterations: u32,
69 pub total: Duration,
71 pub mean: Duration,
73 pub mflops: Option<f64>,
75}
76
77impl std::fmt::Display for GpuBenchReport {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(
80 f,
81 "[{:<5} {:>20}] n={:>6} mean={:.3}µs",
82 self.backend,
83 self.name,
84 self.n,
85 self.mean.as_secs_f64() * 1e6
86 )?;
87 if let Some(mf) = self.mflops {
88 write!(f, " {:.1} MFLOPs", mf)?;
89 }
90 Ok(())
91 }
92}
93
94pub struct GpuBenchHarness {
98 pub warmup: u32,
100 pub iterations: u32,
102 pub reports: Vec<GpuBenchReport>,
104}
105
106impl GpuBenchHarness {
107 pub fn new() -> Self {
109 Self {
110 warmup: 2,
111 iterations: 5,
112 reports: Vec::new(),
113 }
114 }
115
116 pub fn available_backends() -> Vec<BackendKind> {
122 let mut out = vec![BackendKind::Cpu];
123
124 if crate::compute::WgpuBackend::try_new().is_ok() {
126 out.push(BackendKind::Wgpu);
127 }
128
129 if crate::compute::cuda_backend::CudaBackend::try_new(0).is_ok() {
131 out.push(BackendKind::Cuda);
132 }
133
134 out
135 }
136
137 pub fn bench_sph_density(&mut self, n: usize) -> Vec<GpuBenchReport> {
144 let cfg = SphConfig {
145 n_particles: n,
146 smoothing_h: 0.1,
147 rest_density: 1000.0,
148 gravity: 0.0, domain_min: [-10.; 3],
150 domain_max: [10.; 3],
151 ..SphConfig::default()
152 };
153
154 let mut out = Vec::new();
155
156 {
158 let mut sim = SphSimulation::new(cfg.clone());
160 let side = (n as f64).cbrt().ceil() as usize + 1;
162 for (idx, i) in (0..n).enumerate() {
163 let x = (idx % side) as f64 * 0.1 - 5.0;
164 let y = ((idx / side) % side) as f64 * 0.1;
165 let z = (idx / (side * side)) as f64 * 0.1;
166 sim.state.pos_x[i] = x;
167 sim.state.pos_y[i] = y;
168 sim.state.pos_z[i] = z;
169 }
170
171 for _ in 0..self.warmup {
173 sim.step(1.0 / 60.0);
174 }
175
176 let t0 = Instant::now();
177 for _ in 0..self.iterations {
178 sim.step(1.0 / 60.0);
179 }
180 let total = t0.elapsed();
181
182 let flops = 10.0 * n as f64 * n as f64;
183 let mflops = flops / (total.as_secs_f64() / self.iterations as f64) / 1e6;
184
185 let r = GpuBenchReport {
186 name: "sph_density".to_string(),
187 backend: BackendKind::Cpu,
188 n,
189 iterations: self.iterations,
190 total,
191 mean: total / self.iterations,
192 mflops: Some(mflops),
193 };
194 out.push(r.clone());
195 self.reports.push(r);
196 }
197
198 if crate::compute::WgpuBackend::try_new().is_ok() {
200 let mut sim = SphSimulation::new(cfg.clone());
201 let side = (n as f64).cbrt().ceil() as usize + 1;
202 for (idx, i) in (0..n).enumerate() {
203 sim.state.pos_x[i] = (idx % side) as f64 * 0.1 - 5.0;
204 sim.state.pos_y[i] = ((idx / side) % side) as f64 * 0.1;
205 sim.state.pos_z[i] = (idx / (side * side)) as f64 * 0.1;
206 }
207
208 for _ in 0..self.warmup {
209 sim.step(1.0 / 60.0);
210 }
211 let t0 = Instant::now();
212 for _ in 0..self.iterations {
213 sim.step(1.0 / 60.0);
214 }
215 let total = t0.elapsed();
216
217 let backend = if sim.has_gpu() {
218 BackendKind::Wgpu
219 } else {
220 BackendKind::Cpu
221 };
222 let flops = 10.0 * n as f64 * n as f64;
223 let mflops = flops / (total.as_secs_f64() / self.iterations as f64) / 1e6;
224
225 let r = GpuBenchReport {
226 name: "sph_density".to_string(),
227 backend,
228 n,
229 iterations: self.iterations,
230 total,
231 mean: total / self.iterations,
232 mflops: Some(mflops),
233 };
234 out.push(r.clone());
235 self.reports.push(r);
236 }
237
238 out
239 }
240
241 pub fn bench_lbm_step(&mut self, nx: usize, ny: usize, nz: usize) -> Vec<GpuBenchReport> {
247 let cfg = LbmConfig {
248 nx,
249 ny,
250 nz,
251 tau: 0.6,
252 rho0: 1.0,
253 force_x: 0.0,
254 force_y: 0.0,
255 force_z: 0.0,
256 };
257 let nc = nx * ny * nz;
258 let mut out = Vec::new();
259
260 {
262 let mut sim = LbmSimulation::new(cfg.clone());
263 sim.set_lid_velocity(0.1, 0.0, 0.0);
264
265 for _ in 0..self.warmup {
266 sim.step();
267 }
268 let t0 = Instant::now();
269 for _ in 0..self.iterations {
270 sim.step();
271 }
272 let total = t0.elapsed();
273
274 let flops = 120.0 * nc as f64;
275 let mflops = flops / (total.as_secs_f64() / self.iterations as f64) / 1e6;
276
277 let r = GpuBenchReport {
278 name: format!("lbm_bgk_{}x{}x{}", nx, ny, nz),
279 backend: BackendKind::Cpu,
280 n: nc,
281 iterations: self.iterations,
282 total,
283 mean: total / self.iterations,
284 mflops: Some(mflops),
285 };
286 out.push(r.clone());
287 self.reports.push(r);
288 }
289
290 out
291 }
292
293 pub fn bench_parallel_scan(&mut self, n: usize) -> GpuBenchReport {
299 let data: Vec<f64> = (0..n).map(|i| i as f64 + 1.0).collect();
300
301 for _ in 0..self.warmup {
302 let _ = inclusive_scan_cpu(&data);
303 }
304 let t0 = Instant::now();
305 let mut result = Vec::new();
306 for _ in 0..self.iterations {
307 result = inclusive_scan_cpu(&data);
308 }
309 let total = t0.elapsed();
310 let _ = result;
311
312 let flops = 2.0 * n as f64;
313 let mflops = flops / (total.as_secs_f64() / self.iterations as f64) / 1e6;
314
315 let r = GpuBenchReport {
316 name: "parallel_scan".to_string(),
317 backend: BackendKind::Cpu,
318 n,
319 iterations: self.iterations,
320 total,
321 mean: total / self.iterations,
322 mflops: Some(mflops),
323 };
324 self.reports.push(r.clone());
325 r
326 }
327
328 pub fn run_full_suite(&mut self) -> String {
339 self.bench_sph_density(64);
340 self.bench_sph_density(256);
341 self.bench_lbm_step(8, 8, 8);
342 self.bench_lbm_step(16, 16, 4);
343 self.bench_parallel_scan(1024);
344 self.bench_parallel_scan(65536);
345
346 let mut out = format!("{} benchmarks\n", self.reports.len());
347 for r in &self.reports {
348 out.push_str(&format!(" {}\n", r));
349 }
350 out
351 }
352
353 pub fn cpu_vs_wgpu_comparison(&mut self, n: usize) -> Vec<GpuBenchReport> {
370 let mut out = Vec::new();
371
372 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
374 for _ in 0..self.warmup {
375 let _ = inclusive_scan_cpu(&data);
376 }
377 let t0 = std::time::Instant::now();
378 for _ in 0..self.iterations {
379 let _ = inclusive_scan_cpu(&data);
380 }
381 let total_cpu = t0.elapsed();
382 let mean_cpu = total_cpu / self.iterations;
383 let flops = 2.0 * n as f64;
384 let mflops_cpu = flops / (total_cpu.as_secs_f64() / self.iterations as f64) / 1e6;
385
386 let cpu_report = GpuBenchReport {
387 name: "cpu_copy_scan".to_string(),
388 backend: BackendKind::Cpu,
389 n,
390 iterations: self.iterations,
391 total: total_cpu,
392 mean: mean_cpu,
393 mflops: Some(mflops_cpu),
394 };
395 out.push(cpu_report.clone());
396 self.reports.push(cpu_report);
397
398 #[cfg(feature = "wgpu-backend")]
400 {
401 use crate::compute::wgpu_backend::real::WgpuBackendReal;
402
403 let backend_result = WgpuBackendReal::try_new();
404 if let Ok(mut backend) = backend_result {
405 const COPY_WGSL: &str = r#"
406@group(0) @binding(0) var<storage, read> in_buf: array<f32>;
407@group(0) @binding(1) var<storage, read_write> out_buf: array<f32>;
408
409@compute @workgroup_size(64)
410fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
411 let i = gid.x;
412 if (i < arrayLength(&in_buf)) {
413 out_buf[i] = in_buf[i];
414 }
415}
416"#;
417 let in_buf = backend.create_buffer_f64(n);
418 let out_buf = backend.create_buffer_f64(n);
419 backend.write_buffer_f64(in_buf, &data);
420
421 let workgroups = WgpuBackendReal::dispatch_count_for(n, 64);
422
423 for _ in 0..self.warmup {
425 let _ = backend.dispatch_wgsl(
426 COPY_WGSL,
427 "main",
428 &[
429 (in_buf, wgpu::BufferBindingType::Storage { read_only: true }),
430 (
431 out_buf,
432 wgpu::BufferBindingType::Storage { read_only: false },
433 ),
434 ],
435 workgroups,
436 );
437 }
438
439 let t0 = std::time::Instant::now();
440 for _ in 0..self.iterations {
441 let _ = backend.dispatch_wgsl(
442 COPY_WGSL,
443 "main",
444 &[
445 (in_buf, wgpu::BufferBindingType::Storage { read_only: true }),
446 (
447 out_buf,
448 wgpu::BufferBindingType::Storage { read_only: false },
449 ),
450 ],
451 workgroups,
452 );
453 }
454 let total_wgpu = t0.elapsed();
455 let mean_wgpu = total_wgpu / self.iterations;
456 let mflops_wgpu = flops / (total_wgpu.as_secs_f64() / self.iterations as f64) / 1e6;
457
458 let wgpu_report = GpuBenchReport {
459 name: "wgpu_copy_dispatch".to_string(),
460 backend: BackendKind::Wgpu,
461 n,
462 iterations: self.iterations,
463 total: total_wgpu,
464 mean: mean_wgpu,
465 mflops: Some(mflops_wgpu),
466 };
467 out.push(wgpu_report.clone());
468 self.reports.push(wgpu_report);
469 }
470 }
471
472 out
473 }
474
475 pub fn cpu_vs_wgpu_sph(&mut self, n: usize) -> Vec<GpuBenchReport> {
491 let cfg = SphConfig {
492 n_particles: n,
493 smoothing_h: 0.1,
494 rest_density: 1000.0,
495 gravity: 0.0,
496 domain_min: [-10.; 3],
497 domain_max: [10.; 3],
498 ..SphConfig::default()
499 };
500
501 let mut out = Vec::new();
502
503 {
505 let mut sim = SphSimulation::new(cfg.clone());
506 let side = (n as f64).cbrt().ceil() as usize + 1;
507 for (idx, i) in (0..n).enumerate() {
508 let x = (idx % side) as f64 * 0.1 - 5.0;
509 let y = ((idx / side) % side) as f64 * 0.1;
510 let z = (idx / (side * side)) as f64 * 0.1;
511 sim.state.pos_x[i] = x;
512 sim.state.pos_y[i] = y;
513 sim.state.pos_z[i] = z;
514 }
515 for _ in 0..self.warmup {
516 sim.step(1.0 / 60.0);
517 }
518 let t0 = Instant::now();
519 for _ in 0..self.iterations {
520 sim.step(1.0 / 60.0);
521 }
522 let total = t0.elapsed();
523
524 let r = GpuBenchReport {
525 name: "sph_density_cpu".to_string(),
526 backend: BackendKind::Cpu,
527 n,
528 iterations: self.iterations,
529 total,
530 mean: total / self.iterations,
531 mflops: None,
532 };
533 out.push(r.clone());
534 self.reports.push(r);
535 }
536
537 if crate::compute::WgpuBackend::try_new().is_ok() {
539 let mut sim = SphSimulation::new(cfg.clone());
540 let side = (n as f64).cbrt().ceil() as usize + 1;
541 for (idx, i) in (0..n).enumerate() {
542 sim.state.pos_x[i] = (idx % side) as f64 * 0.1 - 5.0;
543 sim.state.pos_y[i] = ((idx / side) % side) as f64 * 0.1;
544 sim.state.pos_z[i] = (idx / (side * side)) as f64 * 0.1;
545 }
546
547 for _ in 0..self.warmup {
548 sim.step(1.0 / 60.0);
549 }
550 let t0 = Instant::now();
551 for _ in 0..self.iterations {
552 sim.step(1.0 / 60.0);
553 }
554 let total = t0.elapsed();
555
556 let backend = if sim.has_gpu() {
557 BackendKind::Wgpu
558 } else {
559 BackendKind::Cpu
560 };
561
562 let r = GpuBenchReport {
563 name: "sph_density_wgpu".to_string(),
564 backend,
565 n,
566 iterations: self.iterations,
567 total,
568 mean: total / self.iterations,
569 mflops: None,
570 };
571 out.push(r.clone());
572 self.reports.push(r);
573 }
574
575 out
576 }
577
578 pub fn cpu_vs_cuda_sph(&mut self, n: usize) -> Vec<GpuBenchReport> {
596 let cfg = crate::sph_gpu::SphConfig {
597 n_particles: n,
598 smoothing_h: 0.1,
599 rest_density: 1000.0,
600 gravity: 0.0,
601 domain_min: [-10.; 3],
602 domain_max: [10.; 3],
603 ..crate::sph_gpu::SphConfig::default()
604 };
605 let mut out = Vec::new();
606
607 {
609 let mut sim = crate::sph_gpu::SphSimulation::new(cfg.clone());
610 let side = (n as f64).cbrt().ceil() as usize + 1;
611 for idx in 0..n {
612 let x = (idx % side) as f64 * 0.1 - 5.0;
613 let y = ((idx / side) % side) as f64 * 0.1;
614 let z = (idx / (side * side)) as f64 * 0.1;
615 sim.state.pos_x[idx] = x;
616 sim.state.pos_y[idx] = y;
617 sim.state.pos_z[idx] = z;
618 }
619 for _ in 0..self.warmup {
620 sim.step(1.0 / 60.0);
621 }
622 let t0 = Instant::now();
623 for _ in 0..self.iterations {
624 sim.step(1.0 / 60.0);
625 }
626 let total = t0.elapsed();
627
628 let r = GpuBenchReport {
629 name: "cuda_sph_density_cpu".to_string(),
630 backend: BackendKind::Cpu,
631 n,
632 iterations: self.iterations,
633 total,
634 mean: total / self.iterations,
635 mflops: None,
636 };
637 out.push(r.clone());
638 self.reports.push(r);
639 }
640
641 #[cfg(feature = "cuda-backend")]
643 {
644 use crate::compute::cuda_backend::{CUDA_SPH_DENSITY_SRC, CudaBackend};
645
646 if let Ok(mut backend) = CudaBackend::try_new(0) {
647 let compiled =
649 backend.compile_and_register("sph_density_kernel", CUDA_SPH_DENSITY_SRC);
650 if compiled.is_ok() {
651 let side = (n as f64).cbrt().ceil() as usize + 1;
653 let mut positions = vec![0.0_f64; n * 3];
654 for idx in 0..n {
655 positions[3 * idx] = (idx % side) as f64 * 0.1 - 5.0;
656 positions[3 * idx + 1] = ((idx / side) % side) as f64 * 0.1;
657 positions[3 * idx + 2] = (idx / (side * side)) as f64 * 0.1;
658 }
659
660 let pos_buf = backend.create_buffer(n * 3);
661 let den_buf = backend.create_buffer(n);
662 backend.write_buffer(pos_buf, &positions);
663
664 let block_x: u32 = 256;
665 let grid_x = (n as u32).div_ceil(block_x);
666
667 let n_i32 = [n as i32];
672 let scalars_f64 = [
673 cfg.smoothing_h,
674 if cfg.particle_mass > 0.0 {
679 cfg.particle_mass
680 } else {
681 1.0
682 },
683 ];
684
685 for _ in 0..self.warmup {
687 backend.launch_with_scalars(
688 "sph_density_kernel",
689 &[pos_buf, den_buf],
690 &n_i32,
691 &scalars_f64,
692 grid_x,
693 block_x,
694 );
695 backend.synchronize();
696 }
697
698 let t0 = Instant::now();
699 for _ in 0..self.iterations {
700 backend.launch_with_scalars(
701 "sph_density_kernel",
702 &[pos_buf, den_buf],
703 &n_i32,
704 &scalars_f64,
705 grid_x,
706 block_x,
707 );
708 backend.synchronize();
709 }
710 let total = t0.elapsed();
711
712 let r = GpuBenchReport {
713 name: "cuda_sph_density_gpu".to_string(),
714 backend: BackendKind::Cuda,
715 n,
716 iterations: self.iterations,
717 total,
718 mean: total / self.iterations,
719 mflops: None,
720 };
721 out.push(r.clone());
722 self.reports.push(r);
723 }
724 }
725 }
726
727 out
728 }
729
730 pub fn print_comparison(&self) {
732 println!("\n{:=<75}", "");
733 println!(
734 "{:<5} {:<22} {:>8} {:>12} {:>10}",
735 "Back", "Kernel", "N", "Mean (µs)", "MFLOPs"
736 );
737 println!("{:=<75}", "");
738 for r in &self.reports {
739 let mf = r.mflops.map_or("—".to_string(), |m| format!("{:.1}", m));
740 println!(
741 "{:<5} {:<22} {:>8} {:>12.3} {:>10}",
742 r.backend,
743 r.name,
744 r.n,
745 r.mean.as_secs_f64() * 1e6,
746 mf
747 );
748 }
749 println!("{:=<75}", "");
750 }
751}
752
753impl Default for GpuBenchHarness {
754 fn default() -> Self {
755 Self::new()
756 }
757}
758
759#[derive(Debug, Clone)]
763pub struct SpeedupReport {
764 pub cpu_mean: Duration,
766 pub wgpu_mean: Option<Duration>,
768 pub speedup: Option<f64>,
770}
771
772pub fn compute_speedup(reports: &[GpuBenchReport]) -> SpeedupReport {
787 let cpu_mean = reports.first().map(|r| r.mean).unwrap_or(Duration::ZERO);
788 let wgpu_mean = reports.get(1).map(|r| r.mean);
789 let speedup = wgpu_mean.map(|wm| {
790 if wm.as_secs_f64() > 0.0 {
791 cpu_mean.as_secs_f64() / wm.as_secs_f64()
792 } else {
793 f64::INFINITY
794 }
795 });
796 SpeedupReport {
797 cpu_mean,
798 wgpu_mean,
799 speedup,
800 }
801}
802
803#[derive(Debug, Clone)]
807pub struct CudaSpeedupReport {
808 pub cpu_mean: Duration,
810 pub cuda_mean: Option<Duration>,
812 pub speedup: Option<f64>,
814}
815
816pub fn compute_cuda_speedup(reports: &[GpuBenchReport]) -> CudaSpeedupReport {
832 let cpu_mean = reports.first().map(|r| r.mean).unwrap_or(Duration::ZERO);
833 let cuda_mean = reports.get(1).map(|r| r.mean);
834 let speedup = cuda_mean.map(|cm| {
835 if cm.as_secs_f64() > 0.0 {
836 cpu_mean.as_secs_f64() / cm.as_secs_f64()
837 } else {
838 f64::INFINITY
839 }
840 });
841 CudaSpeedupReport {
842 cpu_mean,
843 cuda_mean,
844 speedup,
845 }
846}
847
848pub fn inclusive_scan_cpu(data: &[f64]) -> Vec<f64> {
854 let mut out = Vec::with_capacity(data.len());
855 let mut acc = 0.0_f64;
856 for &v in data {
857 acc += v;
858 out.push(acc);
859 }
860 out
861}
862
863#[cfg(test)]
866mod tests {
867 use super::*;
868
869 #[test]
870 fn test_available_backends_has_cpu() {
871 let b = GpuBenchHarness::available_backends();
872 assert!(b.contains(&BackendKind::Cpu));
873 }
874
875 #[test]
876 fn test_inclusive_scan() {
877 let data = vec![1.0, 2.0, 3.0, 4.0];
878 let out = inclusive_scan_cpu(&data);
879 assert_eq!(out, vec![1.0, 3.0, 6.0, 10.0]);
880 }
881
882 #[test]
883 fn test_bench_sph_density_returns_at_least_cpu() {
884 let mut h = GpuBenchHarness {
885 warmup: 0,
886 iterations: 1,
887 reports: Vec::new(),
888 };
889 let reports = h.bench_sph_density(8);
890 assert!(!reports.is_empty());
891 assert_eq!(reports[0].backend, BackendKind::Cpu);
892 }
893
894 #[test]
895 fn test_bench_lbm_step() {
896 let mut h = GpuBenchHarness {
897 warmup: 0,
898 iterations: 1,
899 reports: Vec::new(),
900 };
901 let reports = h.bench_lbm_step(4, 4, 4);
902 assert_eq!(reports.len(), 1);
903 assert_eq!(reports[0].n, 64);
904 }
905
906 #[test]
907 fn test_bench_parallel_scan() {
908 let mut h = GpuBenchHarness {
909 warmup: 0,
910 iterations: 1,
911 reports: Vec::new(),
912 };
913 let r = h.bench_parallel_scan(100);
914 assert_eq!(r.n, 100);
915 assert!(r.mflops.is_some());
916 }
917
918 #[test]
919 fn test_run_full_suite() {
920 let mut h = GpuBenchHarness {
921 warmup: 0,
922 iterations: 1,
923 reports: Vec::new(),
924 };
925 let summary = h.run_full_suite();
926 assert!(summary.contains("benchmarks"));
927 }
928
929 #[test]
930 fn test_backend_display() {
931 assert_eq!(format!("{}", BackendKind::Cpu), "CPU");
932 assert_eq!(format!("{}", BackendKind::Wgpu), "wgpu");
933 assert_eq!(format!("{}", BackendKind::Cuda), "CUDA");
934 }
935}