1use super::types::{
6 BufferBinding, BufferHandle, BufferId, BufferUsage, PipelineBarrier, WarpDivergenceRecord,
7};
8
9pub trait ComputeBackend {
11 fn name(&self) -> &str;
13 fn create_buffer(&self, size: usize) -> BufferHandle;
15 fn write_buffer(&self, handle: BufferHandle, data: &[f64]);
17 fn read_buffer(&self, handle: BufferHandle) -> Vec<f64>;
19 fn dispatch(&self, kernel: &dyn ComputeKernel, work_size: usize);
21}
22pub trait ComputeKernel {
24 fn name(&self) -> &str;
26 fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], work_size: usize);
32}
33pub fn compute_num_workgroups(total_items: u32, workgroup_size: u32) -> u32 {
35 total_items.div_ceil(workgroup_size)
36}
37pub fn compute_num_workgroups_3d(total: [u32; 3], workgroup_size: [u32; 3]) -> [u32; 3] {
39 [
40 total[0].div_ceil(workgroup_size[0]),
41 total[1].div_ceil(workgroup_size[1]),
42 total[2].div_ceil(workgroup_size[2]),
43 ]
44}
45pub fn required_barrier(
50 pass_a_outputs: &[BufferId],
51 pass_b_inputs: &[BufferId],
52) -> PipelineBarrier {
53 let overlap = pass_a_outputs.iter().any(|out| pass_b_inputs.contains(out));
54 if overlap {
55 PipelineBarrier::StorageReadAfterWrite
56 } else {
57 PipelineBarrier::None
58 }
59}
60pub fn detect_aliasing(bindings: &[BufferBinding]) -> Vec<(u32, u32)> {
65 let mut conflicts = Vec::new();
66 for i in 0..bindings.len() {
67 for j in (i + 1)..bindings.len() {
68 if bindings[i].buffer_id == bindings[j].buffer_id {
69 let write_i = matches!(
70 bindings[i].usage,
71 BufferUsage::WriteOnly | BufferUsage::ReadWrite
72 );
73 let read_j = matches!(
74 bindings[j].usage,
75 BufferUsage::ReadOnly | BufferUsage::ReadWrite
76 );
77 let write_j = matches!(
78 bindings[j].usage,
79 BufferUsage::WriteOnly | BufferUsage::ReadWrite
80 );
81 let read_i = matches!(
82 bindings[i].usage,
83 BufferUsage::ReadOnly | BufferUsage::ReadWrite
84 );
85 if write_i && read_j || write_j && read_i {
86 conflicts.push((bindings[i].binding, bindings[j].binding));
87 }
88 }
89 }
90 }
91 conflicts
92}
93pub fn analyse_warp_divergence(predicates: &[bool], warp_size: usize) -> WarpDivergenceRecord {
98 if predicates.is_empty() || warp_size == 0 {
99 return WarpDivergenceRecord::default();
100 }
101 let mut total = 0u64;
102 let mut divergent = 0u64;
103 let n_warps = predicates.len().div_ceil(warp_size);
104 for w in 0..n_warps {
105 let start = w * warp_size;
106 let end = (start + warp_size).min(predicates.len());
107 let slice = &predicates[start..end];
108 total += 1;
109 let all_true = slice.iter().all(|&v| v);
110 let all_false = slice.iter().all(|&v| !v);
111 if !all_true && !all_false {
112 divergent += 1;
113 }
114 }
115 WarpDivergenceRecord {
116 total_branches: total,
117 divergent_branches: divergent,
118 }
119}
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::CpuBackend;
124 use crate::compute::ComputeDispatcher;
125 use crate::compute::ComputePass;
126 use crate::compute::GpuBuffer;
127 use crate::compute::GpuCommand;
128 use crate::compute::GpuCommandEncoder;
129 use crate::compute::GpuError;
130 use crate::compute::KernelSpec;
131 use crate::compute::MemoryBandwidthModel;
132 use crate::compute::OccupancyModel;
133 use crate::compute::ResourceLifecycle;
134 use crate::compute::TimelineSemaphore;
135 #[test]
136 fn cpu_backend_buffer_roundtrip() {
137 let backend = CpuBackend::new();
138 let buf = backend.create_buffer(4);
139 backend.write_buffer(buf, &[1.0, 2.0, 3.0, 4.0]);
140 let data = backend.read_buffer(buf);
141 assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
142 }
143 #[test]
144 fn dispatcher_buffer_write_read_roundtrip() {
145 let mut d = ComputeDispatcher::new();
146 let id = d.create_buffer(5, None);
147 d.write_buffer(id, &[1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
148 let out = d.read_buffer(id).unwrap();
149 assert_eq!(out, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
150 }
151 #[test]
152 fn dispatcher_buffer_initial_data() {
153 let mut d = ComputeDispatcher::new();
154 let id = d.create_buffer(3, Some(&[10.0, 20.0, 30.0]));
155 let out = d.read_buffer(id).unwrap();
156 assert_eq!(out, vec![10.0, 20.0, 30.0]);
157 }
158 #[test]
159 fn dispatcher_invalid_buffer_read_errors() {
160 let d = ComputeDispatcher::new();
161 let bad_id = BufferId(99);
162 assert_eq!(d.read_buffer(bad_id), Err(GpuError::InvalidBuffer(bad_id)));
163 }
164 #[test]
165 fn dispatch_map_identity() {
166 let mut d = ComputeDispatcher::new();
167 let src = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
168 let dst = d.create_buffer(4, None);
169 d.dispatch_map(src, dst, |x| x).unwrap();
170 assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
171 }
172 #[test]
173 fn dispatch_map_scale_by_two() {
174 let mut d = ComputeDispatcher::new();
175 let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
176 let dst = d.create_buffer(3, None);
177 d.dispatch_map(src, dst, |x| x * 2.0).unwrap();
178 assert_eq!(d.read_buffer(dst).unwrap(), vec![2.0, 4.0, 6.0]);
179 }
180 #[test]
181 fn dispatch_reduce_sum() {
182 let mut d = ComputeDispatcher::new();
183 let id = d.create_buffer(5, Some(&[1.0, 2.0, 3.0, 4.0, 5.0]));
184 let sum = d.dispatch_reduce(id, |a, b| a + b).unwrap();
185 assert!((sum - 15.0).abs() < 1e-12);
186 }
187 #[test]
188 fn dispatch_reduce_max() {
189 let mut d = ComputeDispatcher::new();
190 let id = d.create_buffer(5, Some(&[3.0, 1.0, 7.0, 2.0, 5.0]));
191 let max = d.dispatch_reduce(id, f64::max).unwrap();
192 assert!((max - 7.0).abs() < 1e-12);
193 }
194 #[test]
195 fn dispatch_reduce_empty_errors() {
196 let mut d = ComputeDispatcher::new();
197 let id = d.create_buffer(0, None);
198 assert_eq!(
199 d.dispatch_reduce(id, |a, b| a + b),
200 Err(GpuError::EmptyBuffer)
201 );
202 }
203 #[test]
204 fn sph_density_single_particle_self_contribution_positive() {
205 let mut d = ComputeDispatcher::new();
206 let pos = d.create_buffer(3, Some(&[0.0, 0.0, 0.0]));
207 let mass = d.create_buffer(1, Some(&[1.0]));
208 let out = d.create_buffer(1, None);
209 d.dispatch_sph_density(pos, mass, 1.0, out).unwrap();
210 let density = d.read_buffer(out).unwrap();
211 assert_eq!(density.len(), 1);
212 assert!((density[0] - 1.0).abs() < 1e-12);
213 }
214 #[test]
215 fn sph_density_two_particles_within_kernel_positive() {
216 let mut d = ComputeDispatcher::new();
217 let pos = d.create_buffer(6, Some(&[0.0, 0.0, 0.0, 0.5, 0.0, 0.0]));
218 let mass = d.create_buffer(2, Some(&[1.0, 1.0]));
219 let out = d.create_buffer(2, None);
220 d.dispatch_sph_density(pos, mass, 2.0, out).unwrap();
221 let density = d.read_buffer(out).unwrap();
222 assert_eq!(density.len(), 2);
223 assert!(
224 density[0] > 0.0,
225 "density[0] should be positive: {}",
226 density[0]
227 );
228 assert!(
229 density[1] > 0.0,
230 "density[1] should be positive: {}",
231 density[1]
232 );
233 }
234 #[test]
235 fn sph_density_particles_outside_kernel_zero_cross_contribution() {
236 let mut d = ComputeDispatcher::new();
237 let pos = d.create_buffer(6, Some(&[0.0, 0.0, 0.0, 100.0, 0.0, 0.0]));
238 let mass = d.create_buffer(2, Some(&[1.0, 1.0]));
239 let out = d.create_buffer(2, None);
240 d.dispatch_sph_density(pos, mass, 1.0, out).unwrap();
241 let density = d.read_buffer(out).unwrap();
242 assert!((density[0] - 1.0).abs() < 1e-12);
243 assert!((density[1] - 1.0).abs() < 1e-12);
244 }
245 #[test]
246 fn kernel_spec_creation() {
247 let b0 = BufferId(0);
248 let b1 = BufferId(1);
249 let spec = KernelSpec::new("sph_density", 64, vec![b0, b1]);
250 assert_eq!(spec.name, "sph_density");
251 assert_eq!(spec.workgroup_size, [64, 1, 1]);
252 assert_eq!(spec.buffer_bindings.len(), 2);
253 }
254 #[test]
255 fn gpu_buffer_new_zeros() {
256 let buf = GpuBuffer::new(8);
257 assert_eq!(buf.size, 8);
258 assert!(buf.data.iter().all(|&v| v == 0.0));
259 }
260 #[test]
261 fn test_buffer_binding_shorthands() {
262 let id = BufferId(5);
263 let br = BufferBinding::read(0, id);
264 assert_eq!(br.usage, BufferUsage::ReadOnly);
265 let bw = BufferBinding::write(1, id);
266 assert_eq!(bw.usage, BufferUsage::WriteOnly);
267 let brw = BufferBinding::read_write(2, id);
268 assert_eq!(brw.usage, BufferUsage::ReadWrite);
269 let bu = BufferBinding::uniform(3, id);
270 assert_eq!(bu.usage, BufferUsage::Uniform);
271 }
272 #[test]
273 fn test_kernel_spec_3d_workgroup() {
274 let spec = KernelSpec::with_workgroup_3d("test", [8, 8, 4], vec![]);
275 assert_eq!(spec.workgroup_size, [8, 8, 4]);
276 assert_eq!(spec.threads_per_workgroup(), 256);
277 }
278 #[test]
279 fn test_kernel_spec_num_workgroups() {
280 let spec = KernelSpec::new("test", 64, vec![]);
281 assert_eq!(spec.num_workgroups_x(100), 2);
282 assert_eq!(spec.num_workgroups_x(64), 1);
283 assert_eq!(spec.num_workgroups_x(65), 2);
284 }
285 #[test]
286 fn test_gpu_buffer_fill_and_clear() {
287 let mut buf = GpuBuffer::new(5);
288 buf.fill(42.0);
289 assert!(buf.data.iter().all(|&v| (v - 42.0).abs() < 1e-12));
290 buf.clear();
291 assert!(buf.data.iter().all(|&v| v == 0.0));
292 }
293 #[test]
294 fn test_gpu_buffer_byte_size() {
295 let buf = GpuBuffer::new(10);
296 assert_eq!(buf.byte_size(), 80);
297 }
298 #[test]
299 fn test_gpu_buffer_as_slice() {
300 let buf = GpuBuffer::from_data(vec![1.0, 2.0, 3.0]);
301 assert_eq!(buf.as_slice(), &[1.0, 2.0, 3.0]);
302 }
303 #[test]
304 fn test_cpu_backend_num_buffers() {
305 let backend = CpuBackend::new();
306 assert_eq!(backend.num_buffers(), 0);
307 backend.create_buffer(10);
308 assert_eq!(backend.num_buffers(), 1);
309 backend.create_buffer(5);
310 assert_eq!(backend.num_buffers(), 2);
311 }
312 #[test]
313 fn test_cpu_backend_total_elements() {
314 let backend = CpuBackend::new();
315 backend.create_buffer(10);
316 backend.create_buffer(5);
317 assert_eq!(backend.total_elements(), 15);
318 }
319 #[test]
320 fn test_dispatcher_num_buffers() {
321 let mut d = ComputeDispatcher::new();
322 assert_eq!(d.num_buffers(), 0);
323 d.create_buffer(5, None);
324 assert_eq!(d.num_buffers(), 1);
325 }
326 #[test]
327 fn test_dispatcher_has_buffer() {
328 let mut d = ComputeDispatcher::new();
329 let id = d.create_buffer(5, None);
330 assert!(d.has_buffer(id));
331 assert!(!d.has_buffer(BufferId(999)));
332 }
333 #[test]
334 fn test_dispatcher_buffer_size() {
335 let mut d = ComputeDispatcher::new();
336 let id = d.create_buffer(7, None);
337 assert_eq!(d.buffer_size(id).unwrap(), 7);
338 }
339 #[test]
340 fn test_dispatcher_destroy_buffer() {
341 let mut d = ComputeDispatcher::new();
342 let id = d.create_buffer(5, None);
343 assert!(d.has_buffer(id));
344 d.destroy_buffer(id).unwrap();
345 assert!(!d.has_buffer(id));
346 }
347 #[test]
348 fn test_dispatcher_destroy_invalid_buffer_errors() {
349 let mut d = ComputeDispatcher::new();
350 assert_eq!(
351 d.destroy_buffer(BufferId(42)),
352 Err(GpuError::InvalidBuffer(BufferId(42)))
353 );
354 }
355 #[test]
356 fn test_dispatcher_copy_buffer() {
357 let mut d = ComputeDispatcher::new();
358 let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
359 let dst = d.create_buffer(3, None);
360 d.copy_buffer(src, dst).unwrap();
361 assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0]);
362 }
363 #[test]
364 fn test_dispatcher_copy_buffer_size_mismatch() {
365 let mut d = ComputeDispatcher::new();
366 let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
367 let dst = d.create_buffer(5, None);
368 assert!(d.copy_buffer(src, dst).is_err());
369 }
370 #[test]
371 fn test_dispatch_map_indexed() {
372 let mut d = ComputeDispatcher::new();
373 let src = d.create_buffer(4, Some(&[10.0, 20.0, 30.0, 40.0]));
374 let dst = d.create_buffer(4, None);
375 d.dispatch_map_indexed(src, dst, |i, x| x + i as f64)
376 .unwrap();
377 assert_eq!(d.read_buffer(dst).unwrap(), vec![10.0, 21.0, 32.0, 43.0]);
378 }
379 #[test]
380 fn test_dispatch_zip_map() {
381 let mut d = ComputeDispatcher::new();
382 let a = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
383 let b = d.create_buffer(3, Some(&[10.0, 20.0, 30.0]));
384 let out = d.create_buffer(3, None);
385 d.dispatch_zip_map(a, b, out, |x, y| x + y).unwrap();
386 assert_eq!(d.read_buffer(out).unwrap(), vec![11.0, 22.0, 33.0]);
387 }
388 #[test]
389 fn test_compute_pass_recording() {
390 let mut pass = ComputePass::new();
391 assert_eq!(pass.num_commands(), 0);
392 pass.dispatch("density", 1000);
393 pass.dispatch("force", 1000);
394 pass.dispatch("integrate", 1000);
395 assert_eq!(pass.num_commands(), 3);
396 assert_eq!(pass.total_work_items(), 3000);
397 assert_eq!(pass.commands()[0].0, "density");
398 assert_eq!(pass.commands()[1].1, 1000);
399 }
400 #[test]
401 fn test_compute_pass_clear() {
402 let mut pass = ComputePass::new();
403 pass.dispatch("test", 100);
404 assert_eq!(pass.num_commands(), 1);
405 pass.clear();
406 assert_eq!(pass.num_commands(), 0);
407 }
408 #[test]
409 fn test_resource_lifecycle_tracking() {
410 let mut lifecycle = ResourceLifecycle::new();
411 assert!(lifecycle.is_empty());
412 let id = BufferId(0);
413 lifecycle.record_create(id, 100);
414 lifecycle.record_write(id);
415 lifecycle.record_write(id);
416 lifecycle.record_read(id);
417 assert_eq!(lifecycle.len(), 4);
418 assert_eq!(lifecycle.count_writes(id), 2);
419 assert_eq!(lifecycle.count_reads(id), 1);
420 }
421 #[test]
422 fn test_resource_lifecycle_clear() {
423 let mut lifecycle = ResourceLifecycle::new();
424 lifecycle.record_create(BufferId(0), 10);
425 lifecycle.clear();
426 assert!(lifecycle.is_empty());
427 }
428 #[test]
429 fn test_compute_num_workgroups() {
430 assert_eq!(compute_num_workgroups(100, 64), 2);
431 assert_eq!(compute_num_workgroups(64, 64), 1);
432 assert_eq!(compute_num_workgroups(1, 64), 1);
433 }
434 #[test]
435 fn test_compute_num_workgroups_3d() {
436 let wg = compute_num_workgroups_3d([100, 100, 100], [8, 8, 8]);
437 assert_eq!(wg, [13, 13, 13]);
438 }
439 #[test]
440 fn test_gpu_error_display() {
441 let e = GpuError::InvalidBuffer(BufferId(5));
442 assert!(format!("{e}").contains("5"));
443 let e2 = GpuError::SizeMismatch {
444 expected: 10,
445 got: 5,
446 };
447 assert!(format!("{e2}").contains("10"));
448 let e3 = GpuError::EmptyBuffer;
449 assert!(format!("{e3}").contains("empty"));
450 let e4 = GpuError::NotFound("test".to_string());
451 assert!(format!("{e4}").contains("test"));
452 }
453 #[test]
454 fn test_command_encoder_basic() {
455 let mut enc = GpuCommandEncoder::new("test_pass");
456 assert_eq!(enc.label(), "test_pass");
457 assert_eq!(enc.command_count(), 0);
458 enc.dispatch_compute("density", [64, 1, 1]);
459 enc.dispatch_compute("force", [64, 1, 1]);
460 enc.insert_barrier(PipelineBarrier::StorageReadAfterWrite);
461 assert_eq!(enc.command_count(), 3);
462 }
463 #[test]
464 fn test_command_encoder_reset() {
465 let mut enc = GpuCommandEncoder::new("enc");
466 enc.dispatch_compute("k", [1, 1, 1]);
467 enc.reset();
468 assert_eq!(enc.command_count(), 0);
469 }
470 #[test]
471 fn test_command_encoder_submit_copies() {
472 let mut enc = GpuCommandEncoder::new("enc");
473 let mut d = ComputeDispatcher::new();
474 let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
475 let dst = d.create_buffer(3, None);
476 enc.copy_buffer(src, dst, 3);
477 enc.submit(&mut d).unwrap();
478 assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0]);
479 }
480 #[test]
481 fn test_command_encoder_push_constant() {
482 let mut enc = GpuCommandEncoder::new("enc");
483 enc.push_constant("dt", 0.001);
484 assert_eq!(enc.command_count(), 1);
485 match &enc.commands()[0] {
486 GpuCommand::PushConstant { name, value } => {
487 assert_eq!(name, "dt");
488 assert!((value - 0.001).abs() < 1e-15);
489 }
490 _ => panic!("expected PushConstant"),
491 }
492 }
493 #[test]
494 fn test_required_barrier_overlap() {
495 let a_out = vec![BufferId(0), BufferId(1)];
496 let b_in = vec![BufferId(1), BufferId(2)];
497 let barrier = required_barrier(&a_out, &b_in);
498 assert_eq!(barrier, PipelineBarrier::StorageReadAfterWrite);
499 }
500 #[test]
501 fn test_required_barrier_no_overlap() {
502 let a_out = vec![BufferId(0)];
503 let b_in = vec![BufferId(5)];
504 let barrier = required_barrier(&a_out, &b_in);
505 assert_eq!(barrier, PipelineBarrier::None);
506 }
507 #[test]
508 fn test_detect_aliasing_conflict() {
509 let bindings = vec![
510 BufferBinding::write(0, BufferId(10)),
511 BufferBinding::read(1, BufferId(10)),
512 ];
513 let conflicts = detect_aliasing(&bindings);
514 assert!(!conflicts.is_empty(), "should detect aliasing conflict");
515 }
516 #[test]
517 fn test_detect_aliasing_no_conflict() {
518 let bindings = vec![
519 BufferBinding::read(0, BufferId(10)),
520 BufferBinding::read(1, BufferId(11)),
521 ];
522 let conflicts = detect_aliasing(&bindings);
523 assert!(conflicts.is_empty(), "no conflict expected");
524 }
525 #[test]
526 fn test_detect_aliasing_same_buffer_two_reads() {
527 let bindings = vec![
528 BufferBinding::read(0, BufferId(5)),
529 BufferBinding::read(1, BufferId(5)),
530 ];
531 let conflicts = detect_aliasing(&bindings);
532 assert!(conflicts.is_empty());
533 }
534 #[test]
535 fn test_timeline_semaphore_signal_and_wait() {
536 let mut sem = TimelineSemaphore::new();
537 assert_eq!(sem.current_value(), 0);
538 sem.signal(1);
539 assert_eq!(sem.current_value(), 1);
540 assert!(sem.wait(1));
541 assert!(!sem.wait(2));
542 sem.signal(3);
543 assert!(sem.wait(3));
544 assert_eq!(sem.signal_count(), 2);
545 }
546 #[test]
547 fn test_timeline_semaphore_default() {
548 let sem = TimelineSemaphore::default();
549 assert_eq!(sem.current_value(), 0);
550 }
551 #[test]
552 fn test_occupancy_full_when_unconstrained() {
553 let model = OccupancyModel::mid_range();
554 let occ = model.estimate_occupancy(64, 0, 32);
555 assert!(
556 occ > 0.5,
557 "occupancy should be high for small workgroup, got {occ}"
558 );
559 }
560 #[test]
561 fn test_occupancy_limited_by_shared_memory() {
562 let model = OccupancyModel::mid_range();
563 let occ = model.estimate_occupancy(64, model.shared_mem_per_cu, 1);
564 let occ_limited = model.estimate_occupancy(64, model.shared_mem_per_cu / 2, 1);
565 assert!(
566 occ <= occ_limited,
567 "more smem usage should give lower or equal occupancy"
568 );
569 }
570 #[test]
571 fn test_occupancy_bounded_to_one() {
572 let model = OccupancyModel::mid_range();
573 let occ = model.estimate_occupancy(1, 0, 0);
574 assert!((0.0..=1.0).contains(&occ));
575 }
576 #[test]
577 fn test_peak_gflops_positive() {
578 let model = OccupancyModel::mid_range();
579 let gflops = model.peak_gflops(1500.0);
580 assert!(gflops > 0.0);
581 }
582 #[test]
583 fn test_warp_divergence_none() {
584 let predicates = vec![true; 32];
585 let rec = analyse_warp_divergence(&predicates, 32);
586 assert_eq!(rec.divergent_branches, 0);
587 assert!((rec.divergence_rate()).abs() < 1e-12);
588 }
589 #[test]
590 fn test_warp_divergence_full() {
591 let predicates: Vec<bool> = (0..32).map(|i| i % 2 == 0).collect();
592 let rec = analyse_warp_divergence(&predicates, 32);
593 assert_eq!(rec.divergent_branches, 1);
594 assert!((rec.divergence_rate() - 1.0).abs() < 1e-12);
595 }
596 #[test]
597 fn test_warp_divergence_penalty() {
598 let rec = WarpDivergenceRecord {
599 total_branches: 10,
600 divergent_branches: 5,
601 };
602 let penalty = rec.performance_penalty(32);
603 assert!(
604 penalty > 1.0 && penalty < 2.0,
605 "penalty should be > 1, got {penalty}"
606 );
607 }
608 #[test]
609 fn test_warp_divergence_empty() {
610 let rec = analyse_warp_divergence(&[], 32);
611 assert_eq!(rec.total_branches, 0);
612 assert!((rec.divergence_rate()).abs() < 1e-12);
613 }
614 #[test]
615 fn test_memory_bandwidth_arithmetic_intensity() {
616 let intensity = MemoryBandwidthModel::arithmetic_intensity(1000.0, 100.0);
617 assert!((intensity - 10.0).abs() < 1e-12);
618 }
619 #[test]
620 fn test_memory_bandwidth_zero_bytes() {
621 let intensity = MemoryBandwidthModel::arithmetic_intensity(100.0, 0.0);
622 assert!(intensity.is_infinite());
623 }
624 #[test]
625 fn test_roofline_bandwidth_bound() {
626 let model = MemoryBandwidthModel::mid_range();
627 let perf = model.roofline_performance(0.1);
628 let expected = 0.1 * model.peak_bandwidth_gbs;
629 assert!(
630 (perf - expected).abs() < 1e-6,
631 "bandwidth-bound perf mismatch"
632 );
633 }
634 #[test]
635 fn test_roofline_compute_bound() {
636 let model = MemoryBandwidthModel::mid_range();
637 let perf = model.roofline_performance(1e9);
638 assert!((perf - model.peak_compute_gflops).abs() < 1e-6);
639 }
640 #[test]
641 fn test_is_bandwidth_bound() {
642 let model = MemoryBandwidthModel::mid_range();
643 let ridge = model.peak_compute_gflops / model.peak_bandwidth_gbs;
644 assert!(model.is_bandwidth_bound(ridge * 0.5));
645 assert!(!model.is_bandwidth_bound(ridge * 2.0));
646 }
647 #[test]
648 fn test_estimated_runtime_ms_positive() {
649 let model = MemoryBandwidthModel::mid_range();
650 let t = model.estimated_runtime_ms(1e12, 1e9);
651 assert!(t > 0.0 && t.is_finite());
652 }
653 #[test]
654 fn test_reduction_tree_sum() {
655 let mut d = ComputeDispatcher::new();
656 let buf = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
657 let result = d.dispatch_reduction_tree(buf).unwrap();
658 assert!(
659 (result - 10.0).abs() < 1e-12,
660 "sum should be 10, got {result}"
661 );
662 }
663 #[test]
664 fn test_reduction_tree_empty() {
665 let mut d = ComputeDispatcher::new();
666 let buf = d.create_buffer(0, Some(&[]));
667 let result = d.dispatch_reduction_tree(buf).unwrap();
668 assert_eq!(result, 0.0);
669 }
670 #[test]
671 fn test_reduction_tree_single_element() {
672 let mut d = ComputeDispatcher::new();
673 let buf = d.create_buffer(1, Some(&[42.0]));
674 let result = d.dispatch_reduction_tree(buf).unwrap();
675 assert!((result - 42.0).abs() < 1e-12);
676 }
677 #[test]
678 fn test_reduction_tree_power_of_two() {
679 let data: Vec<f64> = (1..=8).map(|x| x as f64).collect();
680 let mut d = ComputeDispatcher::new();
681 let buf = d.create_buffer(8, Some(&data));
682 let result = d.dispatch_reduction_tree(buf).unwrap();
683 assert!((result - 36.0).abs() < 1e-12, "1+2+…+8=36, got {result}");
684 }
685 #[test]
686 fn test_inclusive_scan_basic() {
687 let mut d = ComputeDispatcher::new();
688 let buf_in = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
689 let buf_out = d.create_buffer(4, None);
690 d.dispatch_inclusive_scan(buf_in, buf_out).unwrap();
691 let result = d.read_buffer(buf_out).unwrap();
692 let expected = [1.0, 3.0, 6.0, 10.0];
693 for (a, b) in result.iter().zip(expected.iter()) {
694 assert!((a - b).abs() < 1e-12, "mismatch: {a} vs {b}");
695 }
696 }
697 #[test]
698 fn test_inclusive_scan_single() {
699 let mut d = ComputeDispatcher::new();
700 let buf_in = d.create_buffer(1, Some(&[7.0]));
701 let buf_out = d.create_buffer(1, None);
702 d.dispatch_inclusive_scan(buf_in, buf_out).unwrap();
703 let result = d.read_buffer(buf_out).unwrap();
704 assert!((result[0] - 7.0).abs() < 1e-12);
705 }
706 #[test]
707 fn test_radix_sort_basic() {
708 let data = vec![5.0, 1.0, 3.0, 2.0, 4.0];
709 let mut d = ComputeDispatcher::new();
710 let buf = d.create_buffer(5, Some(&data));
711 let sorted = d.dispatch_radix_sort(buf).unwrap();
712 for w in sorted.windows(2) {
713 assert!(w[0] <= w[1], "not sorted: {} > {}", w[0], w[1]);
714 }
715 }
716 #[test]
717 fn test_radix_sort_empty() {
718 let mut d = ComputeDispatcher::new();
719 let buf = d.create_buffer(0, Some(&[]));
720 let sorted = d.dispatch_radix_sort(buf).unwrap();
721 assert!(sorted.is_empty());
722 }
723 #[test]
724 fn test_radix_sort_already_sorted() {
725 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
726 let mut d = ComputeDispatcher::new();
727 let buf = d.create_buffer(5, Some(&data));
728 let sorted = d.dispatch_radix_sort(buf).unwrap();
729 assert_eq!(sorted, data);
730 }
731 #[test]
732 fn test_radix_sort_length_preserved() {
733 let data: Vec<f64> = (0..16).map(|i| (16 - i) as f64).collect();
734 let mut d = ComputeDispatcher::new();
735 let buf = d.create_buffer(16, Some(&data));
736 let sorted = d.dispatch_radix_sort(buf).unwrap();
737 assert_eq!(sorted.len(), 16);
738 }
739}