Skip to main content

scirs2_graph/gpu/
algorithms.rs

1//! Parallel BFS and SSSP algorithms with GPU dispatch.
2//!
3//! All algorithms accept graphs in standard formats (CSR or adjacency lists)
4//! and dispatch to the GPU (wgpu) when the `wgpu` feature is enabled and a
5//! compatible adapter is present.  When no adapter is available, or when the
6//! edge count is below the threshold (4096), the call falls back to the
7//! CPU-parallel implementations in `super::parallel`.
8
9use std::cmp::Reverse;
10use std::collections::BinaryHeap;
11
12use crate::error::{GraphError, Result};
13
14pub(super) use super::parallel::{parallel_bellman_ford_atomic, parallel_bfs_atomic};
15
16/// GPU edge-count threshold below which CPU execution is preferred.
17const GPU_EDGE_THRESHOLD: usize = 4096;
18
19/// Backend selection for GPU-accelerated graph algorithms.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum GpuGraphBackend {
23    /// CPU-parallel execution using atomic operations.
24    CpuParallel,
25    /// GPU backend via wgpu (with CPU fallback when no adapter is found).
26    Gpu,
27}
28
29/// Configuration for GPU/parallel BFS and SSSP algorithms.
30#[derive(Debug, Clone)]
31pub struct GpuBfsConfig {
32    /// Backend to use. Default: [`GpuGraphBackend::CpuParallel`].
33    pub backend: GpuGraphBackend,
34    /// Frontier chunk size for parallel processing. Default: 1024.
35    pub chunk_size: usize,
36}
37
38impl Default for GpuBfsConfig {
39    fn default() -> Self {
40        Self {
41            backend: GpuGraphBackend::CpuParallel,
42            chunk_size: 1024,
43        }
44    }
45}
46
47// ─────────────────────────────────────────────────────────────────────────────
48// BFS
49// ─────────────────────────────────────────────────────────────────────────────
50
51/// BFS from `source` on a CSR graph. Returns distances (usize::MAX = unreachable).
52///
53/// When `config.backend == Gpu` and enough edges are present (≥4096), dispatches
54/// to the wgpu shader. Falls back to CPU-parallel on missing adapter or compile
55/// error. When `config.backend == CpuParallel`, always uses CPU-parallel.
56///
57/// # Errors
58/// Returns [`GraphError::InvalidParameter`] if `source >= n` or CSR arrays are inconsistent.
59pub fn gpu_bfs(
60    row_ptr: &[usize],
61    col_idx: &[usize],
62    source: usize,
63    config: &GpuBfsConfig,
64) -> Result<Vec<usize>> {
65    if row_ptr.len() < 2 {
66        return Err(GraphError::InvalidParameter {
67            param: "row_ptr".to_string(),
68            value: format!("len={}", row_ptr.len()),
69            expected: "at least 2 elements (n+1)".to_string(),
70            context: "gpu_bfs".to_string(),
71        });
72    }
73
74    let n = row_ptr.len() - 1;
75
76    if source >= n {
77        return Err(GraphError::InvalidParameter {
78            param: "source".to_string(),
79            value: format!("{source}"),
80            expected: format!("0..{n}"),
81            context: "gpu_bfs".to_string(),
82        });
83    }
84
85    if let Some(&last) = row_ptr.last() {
86        if last > col_idx.len() {
87            return Err(GraphError::InvalidParameter {
88                param: "row_ptr/col_idx".to_string(),
89                value: format!("row_ptr last={}, col_idx len={}", last, col_idx.len()),
90                expected: "row_ptr[n] <= col_idx.len()".to_string(),
91                context: "gpu_bfs".to_string(),
92            });
93        }
94    }
95
96    match config.backend {
97        GpuGraphBackend::CpuParallel => Ok(parallel_bfs_atomic(row_ptr, col_idx, source)),
98        GpuGraphBackend::Gpu => {
99            #[cfg(feature = "wgpu")]
100            {
101                let n_edges = col_idx.len();
102                if n_edges >= GPU_EDGE_THRESHOLD {
103                    match wgpu_bfs(row_ptr, col_idx, source) {
104                        Ok(dist) => return Ok(dist),
105                        Err(_) => {
106                            // No adapter or dispatch error — fall through to CPU
107                        }
108                    }
109                }
110            }
111            // CPU fallback (below threshold or no adapter)
112            Ok(parallel_bfs_atomic(row_ptr, col_idx, source))
113        }
114    }
115}
116
117// ─────────────────────────────────────────────────────────────────────────────
118// Bellman-Ford SSSP
119// ─────────────────────────────────────────────────────────────────────────────
120
121/// Bellman-Ford SSSP on a CSR graph with edge weights.
122///
123/// Detects negative-weight cycles in the CPU path: returns an error if any
124/// cycle with negative total weight is reachable from `source`.
125///
126/// When `config.backend == Gpu` and enough edges are present (≥4096) and all
127/// weights are non-negative, dispatches to the wgpu edge-parallel shader.
128/// Falls back to CPU sequential on negative weights, missing adapter, or below threshold.
129///
130/// # Errors
131/// Returns [`GraphError::AlgorithmFailure`] on negative cycle detection (CPU path),
132/// [`GraphError::InvalidParameter`] for bad inputs.
133pub fn gpu_sssp_bellman_ford(
134    row_ptr: &[usize],
135    col_idx: &[usize],
136    weights: &[f64],
137    source: usize,
138    config: &GpuBfsConfig,
139) -> Result<Vec<f64>> {
140    if row_ptr.len() < 2 {
141        return Err(GraphError::InvalidParameter {
142            param: "row_ptr".to_string(),
143            value: format!("len={}", row_ptr.len()),
144            expected: "at least 2 elements (n+1)".to_string(),
145            context: "gpu_sssp_bellman_ford".to_string(),
146        });
147    }
148
149    let n = row_ptr.len() - 1;
150
151    if source >= n {
152        return Err(GraphError::InvalidParameter {
153            param: "source".to_string(),
154            value: format!("{source}"),
155            expected: format!("0..{n}"),
156            context: "gpu_sssp_bellman_ford".to_string(),
157        });
158    }
159
160    if col_idx.len() != weights.len() {
161        return Err(GraphError::InvalidParameter {
162            param: "weights".to_string(),
163            value: format!("len={}", weights.len()),
164            expected: format!("same length as col_idx ({})", col_idx.len()),
165            context: "gpu_sssp_bellman_ford".to_string(),
166        });
167    }
168
169    let has_negative = weights.iter().any(|&w| w < 0.0);
170
171    match config.backend {
172        GpuGraphBackend::CpuParallel => {
173            if has_negative {
174                // GPU-parallel trick only works for non-negative; use sequential with cycle detection
175                bellman_ford_sequential(row_ptr, col_idx, weights, source, n)
176            } else {
177                // Convert f64 → f32 for parallel path, then back to f64
178                let weights_f32: Vec<f32> = weights.iter().map(|&w| w as f32).collect();
179                match parallel_bellman_ford_atomic(row_ptr, col_idx, &weights_f32, source) {
180                    Ok(result) => Ok(result.into_iter().map(|v| v as f64).collect()),
181                    Err(_) => bellman_ford_sequential(row_ptr, col_idx, weights, source, n),
182                }
183            }
184        }
185        GpuGraphBackend::Gpu => {
186            #[cfg(feature = "wgpu")]
187            {
188                let n_edges = col_idx.len();
189                if !has_negative && n_edges >= GPU_EDGE_THRESHOLD {
190                    let weights_f32: Vec<f32> = weights.iter().map(|&w| w as f32).collect();
191                    match wgpu_bellman_ford(row_ptr, col_idx, &weights_f32, source, n) {
192                        Ok(dist_f32) => {
193                            return Ok(dist_f32.into_iter().map(|v| v as f64).collect());
194                        }
195                        Err(_) => {
196                            // No adapter or dispatch error — fall through to CPU
197                        }
198                    }
199                }
200            }
201            // CPU sequential fallback (supports negative weights + cycle detection)
202            bellman_ford_sequential(row_ptr, col_idx, weights, source, n)
203        }
204    }
205}
206
207/// Sequential Bellman-Ford with negative-cycle detection (CPU-only fallback).
208fn bellman_ford_sequential(
209    row_ptr: &[usize],
210    col_idx: &[usize],
211    weights: &[f64],
212    source: usize,
213    n: usize,
214) -> Result<Vec<f64>> {
215    let has_negative = weights.iter().any(|&w| w < 0.0);
216
217    let mut dist = vec![f64::INFINITY; n];
218    dist[source] = 0.0;
219
220    for _ in 0..(n.saturating_sub(1)) {
221        let mut changed = false;
222        for u in 0..n {
223            if dist[u] == f64::INFINITY {
224                continue;
225            }
226            let start = row_ptr[u];
227            let end = row_ptr[u + 1];
228            for idx in start..end {
229                let v = col_idx[idx];
230                let w = weights[idx];
231                let new_dist = dist[u] + w;
232                if new_dist < dist[v] {
233                    dist[v] = new_dist;
234                    changed = true;
235                }
236            }
237        }
238        if !changed {
239            break;
240        }
241    }
242
243    // Detect negative cycles
244    if has_negative {
245        for u in 0..n {
246            if dist[u] == f64::INFINITY {
247                continue;
248            }
249            let start = row_ptr[u];
250            let end = row_ptr[u + 1];
251            for idx in start..end {
252                let v = col_idx[idx];
253                let w = weights[idx];
254                if dist[u] + w < dist[v] {
255                    return Err(GraphError::AlgorithmFailure {
256                        algorithm: "gpu_sssp_bellman_ford".to_string(),
257                        reason: "negative weight cycle detected".to_string(),
258                        iterations: n,
259                        tolerance: 0.0,
260                    });
261                }
262            }
263        }
264    }
265
266    Ok(dist)
267}
268
269// ─────────────────────────────────────────────────────────────────────────────
270// Delta-stepping SSSP
271// ─────────────────────────────────────────────────────────────────────────────
272
273/// Delta-stepping SSSP on an adjacency-list graph.
274///
275/// Partitions edges into light (weight ≤ delta) and heavy categories.
276/// When `config.backend == Gpu` and enough edges are present (≥4096), dispatches
277/// true GPU delta-stepping using two WGSL kernels:
278/// - `delta_light`: proposes updates for light edges via `atomicMin` on f32-bits
279/// - `delta_apply`: applies proposals to the distance buffer
280///
281/// Heavy edges are relaxed once per iteration using the Bellman-Ford kernel.
282/// Convergence is detected via a GPU-side flag buffer.
283/// Falls back to sequential Dijkstra for small graphs or when no GPU adapter is found.
284///
285/// The adaptive delta heuristic uses `max_weight / sqrt(n)` when `delta` is the
286/// special sentinel value 0.0 (see `gpu_sssp_delta_stepping_adaptive`).
287///
288/// # Errors
289/// Returns [`GraphError::InvalidParameter`] if `delta <= 0`, `source` is out
290/// of range, or negative edge weights are detected.
291pub fn gpu_sssp_delta_stepping(
292    adj: &[Vec<(usize, f64)>],
293    source: usize,
294    delta: f64,
295    config: &GpuBfsConfig,
296) -> Result<Vec<f64>> {
297    let n = adj.len();
298
299    if n == 0 {
300        return Err(GraphError::InvalidParameter {
301            param: "adj".to_string(),
302            value: "len=0".to_string(),
303            expected: "non-empty graph".to_string(),
304            context: "gpu_sssp_delta_stepping".to_string(),
305        });
306    }
307
308    if source >= n {
309        return Err(GraphError::InvalidParameter {
310            param: "source".to_string(),
311            value: format!("{source}"),
312            expected: format!("0..{n}"),
313            context: "gpu_sssp_delta_stepping".to_string(),
314        });
315    }
316
317    if delta <= 0.0 {
318        return Err(GraphError::InvalidParameter {
319            param: "delta".to_string(),
320            value: format!("{delta}"),
321            expected: "positive value".to_string(),
322            context: "gpu_sssp_delta_stepping".to_string(),
323        });
324    }
325
326    // Reject negative weights
327    for (u, nbrs) in adj.iter().enumerate() {
328        for &(_, w) in nbrs {
329            if w < 0.0 {
330                return Err(GraphError::InvalidParameter {
331                    param: "weights".to_string(),
332                    value: format!("negative weight on edge from {u}"),
333                    expected: "non-negative edge weights for delta-stepping".to_string(),
334                    context: "gpu_sssp_delta_stepping".to_string(),
335                });
336            }
337        }
338    }
339
340    // Build CSR from adjacency list
341    let mut row_ptr = vec![0usize; n + 1];
342    for (u, nbrs) in adj.iter().enumerate() {
343        row_ptr[u + 1] = row_ptr[u] + nbrs.len();
344    }
345    let total_edges = row_ptr[n];
346    let mut col_idx = Vec::with_capacity(total_edges);
347    let mut wts = Vec::with_capacity(total_edges);
348    for nbrs in adj {
349        for &(v, w) in nbrs {
350            col_idx.push(v);
351            wts.push(w);
352        }
353    }
354
355    match config.backend {
356        GpuGraphBackend::CpuParallel => {
357            // Sequential Dijkstra (equivalent, correct for non-negative weights)
358            Ok(dijkstra_sequential(adj, source))
359        }
360        GpuGraphBackend::Gpu => {
361            // True GPU delta-stepping: light-edge proposal + apply + heavy-edge BF pass.
362            #[cfg(feature = "wgpu")]
363            {
364                if total_edges >= GPU_EDGE_THRESHOLD {
365                    match wgpu_delta_stepping(adj, source, delta) {
366                        Ok(dist_f32) => {
367                            return Ok(dist_f32.into_iter().map(|v| v as f64).collect());
368                        }
369                        Err(_) => {
370                            // No adapter or shader error — fall through to CPU
371                        }
372                    }
373                }
374            }
375            Ok(dijkstra_sequential(adj, source))
376        }
377    }
378}
379
380/// Sequential Dijkstra reference implementation.
381fn dijkstra_sequential(adj: &[Vec<(usize, f64)>], src: usize) -> Vec<f64> {
382    let n = adj.len();
383    let mut dist = vec![f64::INFINITY; n];
384    dist[src] = 0.0;
385    let mut heap: BinaryHeap<(Reverse<u64>, usize)> = BinaryHeap::new();
386    heap.push((Reverse(0), src));
387    while let Some((Reverse(d_scaled), u)) = heap.pop() {
388        let d = d_scaled as f64 * 1e-9;
389        if d > dist[u] + 1e-12 {
390            continue;
391        }
392        for &(v, w) in &adj[u] {
393            let nd = dist[u] + w;
394            if nd < dist[v] - 1e-12 {
395                dist[v] = nd;
396                heap.push((Reverse((nd * 1e9) as u64), v));
397            }
398        }
399    }
400    dist
401}
402
403// ─────────────────────────────────────────────────────────────────────────────
404// wgpu dispatch (compiled only when `wgpu` feature + scirs2-core/wgpu available)
405// ─────────────────────────────────────────────────────────────────────────────
406
407#[cfg(feature = "wgpu")]
408fn wgpu_bfs(
409    row_ptr: &[usize],
410    col_idx: &[usize],
411    source: usize,
412) -> std::result::Result<Vec<usize>, String> {
413    use wgpu::{
414        util::DeviceExt as _, Backends, BindGroupDescriptor, BindGroupEntry,
415        BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType,
416        BufferDescriptor, BufferUsages, CommandEncoderDescriptor, ComputePassDescriptor,
417        DeviceDescriptor, Features, Instance, InstanceDescriptor, Limits, PowerPreference,
418        RequestAdapterOptions, ShaderModuleDescriptor, ShaderSource, ShaderStages,
419    };
420
421    let n = row_ptr.len() - 1;
422
423    // Validate sizes fit in u32
424    if n > u32::MAX as usize {
425        return Err("graph too large for u32 indexing".into());
426    }
427    if col_idx.len() > u32::MAX as usize {
428        return Err("edge list too large for u32 indexing".into());
429    }
430
431    // Acquire adapter and device
432    let instance = Instance::new(InstanceDescriptor {
433        backends: Backends::all(),
434        flags: wgpu::InstanceFlags::default(),
435        memory_budget_thresholds: Default::default(),
436        backend_options: Default::default(),
437        display: None,
438    });
439    let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
440        power_preference: PowerPreference::HighPerformance,
441        compatible_surface: None,
442        force_fallback_adapter: false,
443    }))
444    .map_err(|_| "no wgpu adapter available".to_string())?;
445
446    let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
447        label: Some("scirs2-graph-bfs"),
448        required_features: Features::empty(),
449        required_limits: Limits::default(),
450        ..Default::default()
451    }))
452    .map_err(|e| format!("device creation failed: {e}"))?;
453
454    // Convert CSR to u32
455    let row_offsets_u32: Vec<u32> = row_ptr.iter().map(|&x| x as u32).collect();
456    let col_indices_u32: Vec<u32> = col_idx.iter().map(|&x| x as u32).collect();
457
458    // Distances: init all to -1 (unvisited)
459    let mut distances_i32: Vec<i32> = vec![-1i32; n];
460    distances_i32[source] = 0;
461
462    // Max frontier size = n vertices
463    let frontier_capacity = n;
464
465    // Create device buffers
466    let buf_row = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
467        label: Some("bfs-row-offsets"),
468        contents: bytemuck::cast_slice(&row_offsets_u32),
469        usage: BufferUsages::STORAGE,
470    });
471    let buf_col = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
472        label: Some("bfs-col-indices"),
473        contents: bytemuck::cast_slice(&col_indices_u32),
474        usage: BufferUsages::STORAGE,
475    });
476    let buf_dist = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
477        label: Some("bfs-distances"),
478        contents: bytemuck::cast_slice(&distances_i32),
479        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
480    });
481
482    // Frontier in/out buffers (u32, one slot per vertex max)
483    let frontier_bytes = (frontier_capacity * std::mem::size_of::<u32>()) as u64;
484    let buf_frontier_in = device.create_buffer(&BufferDescriptor {
485        label: Some("bfs-frontier-in"),
486        size: frontier_bytes,
487        usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
488        mapped_at_creation: false,
489    });
490    let buf_frontier_out = device.create_buffer(&BufferDescriptor {
491        label: Some("bfs-frontier-out"),
492        size: frontier_bytes,
493        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
494        mapped_at_creation: false,
495    });
496
497    // Frontier count (atomic<u32>) — 4 bytes
498    let buf_frontier_count = device.create_buffer(&BufferDescriptor {
499        label: Some("bfs-frontier-count"),
500        size: 4,
501        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
502        mapped_at_creation: false,
503    });
504
505    // Uniform buffer: { frontier_size: u32, current_level: i32 }
506    // Pad to 16 bytes for uniform alignment
507    let buf_uniforms = device.create_buffer(&BufferDescriptor {
508        label: Some("bfs-uniforms"),
509        size: 16,
510        usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
511        mapped_at_creation: false,
512    });
513
514    // Staging buffers for readback
515    let dist_byte_len = (n * std::mem::size_of::<i32>()) as u64;
516    let buf_dist_staging = device.create_buffer(&BufferDescriptor {
517        label: Some("bfs-dist-staging"),
518        size: dist_byte_len,
519        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
520        mapped_at_creation: false,
521    });
522    let buf_count_staging = device.create_buffer(&BufferDescriptor {
523        label: Some("bfs-count-staging"),
524        size: 4,
525        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
526        mapped_at_creation: false,
527    });
528    let buf_frontier_out_staging = device.create_buffer(&BufferDescriptor {
529        label: Some("bfs-frontier-out-staging"),
530        size: frontier_bytes,
531        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
532        mapped_at_creation: false,
533    });
534
535    // Compile shader
536    use super::wgpu_shaders::BFS_FRONTIER_WGSL;
537    let shader_module = device.create_shader_module(ShaderModuleDescriptor {
538        label: Some("bfs-frontier-shader"),
539        source: ShaderSource::Wgsl(BFS_FRONTIER_WGSL.into()),
540    });
541
542    // Build bind group layout matching shader bindings 0-6
543    let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
544        label: Some("bfs-bgl"),
545        entries: &[
546            bgl_storage_ro(0), // row_offsets
547            bgl_storage_ro(1), // col_indices
548            bgl_storage_rw(2), // distances (atomic)
549            bgl_storage_ro(3), // frontier_in
550            bgl_storage_rw(4), // frontier_out (atomic)
551            bgl_storage_rw(5), // frontier_out_count (atomic)
552            bgl_uniform(6),    // uniforms
553        ],
554    });
555
556    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
557        label: Some("bfs-layout"),
558        bind_group_layouts: &[Some(&bgl)],
559        ..Default::default()
560    });
561
562    let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
563        label: Some("bfs-pipeline"),
564        layout: Some(&pipeline_layout),
565        module: &shader_module,
566        entry_point: Some("bfs_frontier"),
567        compilation_options: Default::default(),
568        cache: None,
569    });
570
571    // Seed: frontier_in = [source], frontier_count = 0
572    let source_u32 = source as u32;
573    let source_bytes = source_u32.to_le_bytes();
574    queue.write_buffer(&buf_frontier_in, 0, &source_bytes);
575    queue.write_buffer(&buf_frontier_count, 0, &0u32.to_le_bytes());
576
577    let mut frontier_size: u32 = 1;
578    let mut current_level: i32 = 0;
579
580    // BFS level loop — one dispatch per frontier level
581    while frontier_size > 0 {
582        // Write uniforms
583        let uniforms_data: [u8; 16] = {
584            let mut buf = [0u8; 16];
585            buf[0..4].copy_from_slice(&frontier_size.to_le_bytes());
586            buf[4..8].copy_from_slice(&current_level.to_le_bytes());
587            buf
588        };
589        queue.write_buffer(&buf_uniforms, 0, &uniforms_data);
590        queue.write_buffer(&buf_frontier_count, 0, &0u32.to_le_bytes());
591
592        // Clear frontier_out (not strictly required but defensive)
593        let zero_frontier: Vec<u8> = vec![0u8; frontier_capacity * 4];
594        queue.write_buffer(&buf_frontier_out, 0, &zero_frontier);
595
596        // Bind group (static for this iteration)
597        let bind_group = device.create_bind_group(&BindGroupDescriptor {
598            label: Some("bfs-bg"),
599            layout: &bgl,
600            entries: &[
601                BindGroupEntry {
602                    binding: 0,
603                    resource: buf_row.as_entire_binding(),
604                },
605                BindGroupEntry {
606                    binding: 1,
607                    resource: buf_col.as_entire_binding(),
608                },
609                BindGroupEntry {
610                    binding: 2,
611                    resource: buf_dist.as_entire_binding(),
612                },
613                BindGroupEntry {
614                    binding: 3,
615                    resource: buf_frontier_in.as_entire_binding(),
616                },
617                BindGroupEntry {
618                    binding: 4,
619                    resource: buf_frontier_out.as_entire_binding(),
620                },
621                BindGroupEntry {
622                    binding: 5,
623                    resource: buf_frontier_count.as_entire_binding(),
624                },
625                BindGroupEntry {
626                    binding: 6,
627                    resource: buf_uniforms.as_entire_binding(),
628                },
629            ],
630        });
631
632        // Dispatch one workgroup per 64 frontier vertices
633        let workgroups = frontier_size.div_ceil(64);
634        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
635        {
636            let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor {
637                label: None,
638                timestamp_writes: None,
639            });
640            pass.set_pipeline(&pipeline);
641            pass.set_bind_group(0, &bind_group, &[]);
642            pass.dispatch_workgroups(workgroups, 1, 1);
643        }
644        // Copy count → staging
645        encoder.copy_buffer_to_buffer(&buf_frontier_count, 0, &buf_count_staging, 0, 4);
646        // Copy frontier_out → staging (we'll read how many needed from count)
647        encoder.copy_buffer_to_buffer(
648            &buf_frontier_out,
649            0,
650            &buf_frontier_out_staging,
651            0,
652            frontier_bytes,
653        );
654        queue.submit([encoder.finish()]);
655
656        // Wait for GPU
657        device
658            .poll(wgpu::PollType::wait_indefinitely())
659            .map_err(|e| format!("GPU poll error: {e:?}"))?;
660
661        // Read back count
662        let next_count = read_u32_from_staging(&device, &buf_count_staging)?;
663        if next_count == 0 {
664            break;
665        }
666
667        // Read back frontier_out (only as many as next_count)
668        let next_frontier =
669            read_u32_vec_from_staging(&device, &buf_frontier_out_staging, next_count as usize)?;
670
671        // Copy frontier_out → frontier_in for next iteration
672        let next_bytes: Vec<u8> = next_frontier
673            .iter()
674            .flat_map(|&v| v.to_le_bytes())
675            .collect();
676        queue.write_buffer(&buf_frontier_in, 0, &next_bytes);
677
678        frontier_size = next_count;
679        current_level += 1;
680    }
681
682    // Read back distances
683    let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
684    encoder.copy_buffer_to_buffer(&buf_dist, 0, &buf_dist_staging, 0, dist_byte_len);
685    queue.submit([encoder.finish()]);
686
687    device
688        .poll(wgpu::PollType::wait_indefinitely())
689        .map_err(|e| format!("GPU poll error during dist readback: {e:?}"))?;
690
691    let dist_i32 = read_i32_vec_from_staging(&device, &buf_dist_staging, n)?;
692
693    // Convert: -1 → usize::MAX, otherwise cast to usize
694    let dist_usize: Vec<usize> = dist_i32
695        .into_iter()
696        .map(|d| if d < 0 { usize::MAX } else { d as usize })
697        .collect();
698
699    Ok(dist_usize)
700}
701
702#[cfg(feature = "wgpu")]
703fn wgpu_bellman_ford(
704    row_ptr: &[usize],
705    col_idx: &[usize],
706    weights: &[f32],
707    source: usize,
708    n: usize,
709) -> std::result::Result<Vec<f32>, String> {
710    use wgpu::{
711        util::DeviceExt as _, Backends, BindGroupDescriptor, BindGroupEntry,
712        BindGroupLayoutDescriptor, BufferDescriptor, BufferUsages, CommandEncoderDescriptor,
713        ComputePassDescriptor, DeviceDescriptor, Features, Instance, InstanceDescriptor, Limits,
714        PowerPreference, RequestAdapterOptions, ShaderModuleDescriptor, ShaderSource,
715    };
716
717    if n > u32::MAX as usize || col_idx.len() > u32::MAX as usize {
718        return Err("graph too large for u32 indexing".into());
719    }
720
721    // Build flat edge list from CSR
722    let n_edges = col_idx.len();
723    let mut edge_src: Vec<u32> = Vec::with_capacity(n_edges);
724    let mut edge_dst: Vec<u32> = Vec::with_capacity(n_edges);
725    let mut edge_wt: Vec<f32> = Vec::with_capacity(n_edges);
726    for u in 0..n {
727        let start = row_ptr[u];
728        let end = row_ptr[u + 1];
729        for i in start..end {
730            edge_src.push(u as u32);
731            edge_dst.push(col_idx[i] as u32);
732            edge_wt.push(weights[i]);
733        }
734    }
735
736    // Acquire adapter and device
737    let instance = Instance::new(InstanceDescriptor {
738        backends: Backends::all(),
739        flags: wgpu::InstanceFlags::default(),
740        memory_budget_thresholds: Default::default(),
741        backend_options: Default::default(),
742        display: None,
743    });
744    let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
745        power_preference: PowerPreference::HighPerformance,
746        compatible_surface: None,
747        force_fallback_adapter: false,
748    }))
749    .map_err(|_| "no wgpu adapter available".to_string())?;
750
751    let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
752        label: Some("scirs2-graph-bf"),
753        required_features: Features::empty(),
754        required_limits: Limits::default(),
755        ..Default::default()
756    }))
757    .map_err(|e| format!("device creation failed: {e}"))?;
758
759    // Init distances: source=0.0, others=INFINITY (as u32 bits)
760    let inf_bits = f32::INFINITY.to_bits();
761    let mut dist_bits: Vec<u32> = vec![inf_bits; n];
762    dist_bits[source] = 0u32;
763
764    // Create buffers
765    let buf_src = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
766        label: Some("bf-edge-src"),
767        contents: bytemuck::cast_slice(&edge_src),
768        usage: BufferUsages::STORAGE,
769    });
770    let buf_dst = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
771        label: Some("bf-edge-dst"),
772        contents: bytemuck::cast_slice(&edge_dst),
773        usage: BufferUsages::STORAGE,
774    });
775    let buf_wt = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
776        label: Some("bf-edge-wt"),
777        contents: bytemuck::cast_slice(&edge_wt),
778        usage: BufferUsages::STORAGE,
779    });
780    let buf_dist = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
781        label: Some("bf-distances"),
782        contents: bytemuck::cast_slice(&dist_bits),
783        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
784    });
785
786    // Uniform: { n_edges: u32, _pad[3] }
787    let uniforms: [u32; 4] = [n_edges as u32, 0, 0, 0];
788    let buf_uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
789        label: Some("bf-uniforms"),
790        contents: bytemuck::cast_slice(&uniforms),
791        usage: BufferUsages::UNIFORM,
792    });
793
794    // Compile shader
795    use super::wgpu_shaders::SSSP_BELLMAN_FORD_WGSL;
796    let shader_module = device.create_shader_module(ShaderModuleDescriptor {
797        label: Some("bf-shader"),
798        source: ShaderSource::Wgsl(SSSP_BELLMAN_FORD_WGSL.into()),
799    });
800
801    let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
802        label: Some("bf-bgl"),
803        entries: &[
804            bgl_storage_ro(0), // edge_src
805            bgl_storage_ro(1), // edge_dst
806            bgl_storage_ro(2), // edge_wt
807            bgl_storage_rw(3), // distances (atomic)
808            bgl_uniform(4),    // uniforms
809        ],
810    });
811
812    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
813        label: Some("bf-layout"),
814        bind_group_layouts: &[Some(&bgl)],
815        ..Default::default()
816    });
817
818    let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
819        label: Some("bf-pipeline"),
820        layout: Some(&pipeline_layout),
821        module: &shader_module,
822        entry_point: Some("sssp_bellman_ford"),
823        compilation_options: Default::default(),
824        cache: None,
825    });
826
827    let bind_group = device.create_bind_group(&BindGroupDescriptor {
828        label: Some("bf-bg"),
829        layout: &bgl,
830        entries: &[
831            BindGroupEntry {
832                binding: 0,
833                resource: buf_src.as_entire_binding(),
834            },
835            BindGroupEntry {
836                binding: 1,
837                resource: buf_dst.as_entire_binding(),
838            },
839            BindGroupEntry {
840                binding: 2,
841                resource: buf_wt.as_entire_binding(),
842            },
843            BindGroupEntry {
844                binding: 3,
845                resource: buf_dist.as_entire_binding(),
846            },
847            BindGroupEntry {
848                binding: 4,
849                resource: buf_uniforms.as_entire_binding(),
850            },
851        ],
852    });
853
854    let workgroups = (n_edges as u32).div_ceil(64);
855
856    // n-1 Bellman-Ford passes
857    for _ in 0..(n.saturating_sub(1)) {
858        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
859        {
860            let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor {
861                label: None,
862                timestamp_writes: None,
863            });
864            pass.set_pipeline(&pipeline);
865            pass.set_bind_group(0, &bind_group, &[]);
866            pass.dispatch_workgroups(workgroups, 1, 1);
867        }
868        queue.submit([encoder.finish()]);
869
870        device
871            .poll(wgpu::PollType::wait_indefinitely())
872            .map_err(|e| format!("GPU poll error: {e:?}"))?;
873    }
874
875    // Readback
876    let dist_byte_len = (n * std::mem::size_of::<u32>()) as u64;
877    let buf_staging = device.create_buffer(&BufferDescriptor {
878        label: Some("bf-staging"),
879        size: dist_byte_len,
880        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
881        mapped_at_creation: false,
882    });
883    let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
884    encoder.copy_buffer_to_buffer(&buf_dist, 0, &buf_staging, 0, dist_byte_len);
885    queue.submit([encoder.finish()]);
886
887    device
888        .poll(wgpu::PollType::wait_indefinitely())
889        .map_err(|e| format!("GPU poll error during readback: {e:?}"))?;
890
891    let result_bits = read_u32_vec_from_staging(&device, &buf_staging, n)?;
892    Ok(result_bits.into_iter().map(f32::from_bits).collect())
893}
894
895// ─────────────────────────────────────────────────────────────────────────────
896// True GPU delta-stepping dispatch (light/heavy partition + atomicMin)
897// ─────────────────────────────────────────────────────────────────────────────
898
899#[cfg(feature = "wgpu")]
900fn wgpu_delta_stepping(
901    adj: &[Vec<(usize, f64)>],
902    source: usize,
903    delta: f64,
904) -> std::result::Result<Vec<f32>, String> {
905    use super::wgpu_shaders::{DELTA_APPLY_WGSL, DELTA_HEAVY_WGSL, DELTA_LIGHT_WGSL};
906    use wgpu::{
907        util::DeviceExt as _, Backends, BindGroupDescriptor, BindGroupEntry,
908        BindGroupLayoutDescriptor, BufferDescriptor, BufferUsages, CommandEncoderDescriptor,
909        ComputePassDescriptor, DeviceDescriptor, Features, Instance, InstanceDescriptor, Limits,
910        PowerPreference, RequestAdapterOptions, ShaderModuleDescriptor, ShaderSource,
911    };
912
913    let n = adj.len();
914    if n > u32::MAX as usize {
915        return Err("graph too large for u32 indexing".into());
916    }
917
918    // ── CPU-side preprocessing: partition edges into light and heavy ──────────
919    let delta_f32 = delta as f32;
920    let mut light_src: Vec<u32> = Vec::new();
921    let mut light_dst: Vec<u32> = Vec::new();
922    let mut light_wt: Vec<f32> = Vec::new();
923    let mut heavy_src: Vec<u32> = Vec::new();
924    let mut heavy_dst: Vec<u32> = Vec::new();
925    let mut heavy_wt: Vec<f32> = Vec::new();
926    for (u, nbrs) in adj.iter().enumerate() {
927        for &(v, w) in nbrs {
928            let w32 = w as f32;
929            if w32 <= delta_f32 {
930                light_src.push(u as u32);
931                light_dst.push(v as u32);
932                light_wt.push(w32);
933            } else {
934                heavy_src.push(u as u32);
935                heavy_dst.push(v as u32);
936                heavy_wt.push(w32);
937            }
938        }
939    }
940
941    // ── Acquire adapter and device ────────────────────────────────────────────
942    let instance = Instance::new(InstanceDescriptor {
943        backends: Backends::all(),
944        flags: wgpu::InstanceFlags::default(),
945        memory_budget_thresholds: Default::default(),
946        backend_options: Default::default(),
947        display: None,
948    });
949    let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
950        power_preference: PowerPreference::HighPerformance,
951        compatible_surface: None,
952        force_fallback_adapter: false,
953    }))
954    .map_err(|_| "no wgpu adapter available".to_string())?;
955
956    let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
957        label: Some("scirs2-graph-delta"),
958        required_features: Features::empty(),
959        required_limits: Limits::default(),
960        ..Default::default()
961    }))
962    .map_err(|e| format!("device creation failed: {e}"))?;
963
964    // ── Init distance buffer ───────────────────────────────────────────────────
965    let inf_bits = f32::INFINITY.to_bits();
966    let mut dist_bits: Vec<u32> = vec![inf_bits; n];
967    dist_bits[source] = 0u32; // distance[source] = 0.0f.bits()
968
969    // ── Create shared distance buffer ─────────────────────────────────────────
970    let buf_dist = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
971        label: Some("ds-distances"),
972        contents: bytemuck::cast_slice(&dist_bits),
973        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
974    });
975
976    // ── Create proposed buffer (inf initially, reset each light phase) ─────────
977    let proposed_init: Vec<u32> = vec![inf_bits; n];
978    let buf_proposed = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
979        label: Some("ds-proposed"),
980        contents: bytemuck::cast_slice(&proposed_init),
981        usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
982    });
983
984    // ── Convergence flag buffer ────────────────────────────────────────────────
985    let buf_changed = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
986        label: Some("ds-changed"),
987        contents: bytemuck::cast_slice(&[0u32]),
988        usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
989    });
990    let buf_changed_staging = device.create_buffer(&BufferDescriptor {
991        label: Some("ds-changed-staging"),
992        size: 4,
993        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
994        mapped_at_creation: false,
995    });
996
997    // ── Light edge buffers ────────────────────────────────────────────────────
998    let n_light = light_src.len();
999    // Handle empty light edge list (create minimal placeholder buffers)
1000    let (buf_light_src, buf_light_dst, buf_light_wt) = if n_light > 0 {
1001        let s = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1002            label: Some("ds-light-src"),
1003            contents: bytemuck::cast_slice(&light_src),
1004            usage: BufferUsages::STORAGE,
1005        });
1006        let d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1007            label: Some("ds-light-dst"),
1008            contents: bytemuck::cast_slice(&light_dst),
1009            usage: BufferUsages::STORAGE,
1010        });
1011        let w = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1012            label: Some("ds-light-wt"),
1013            contents: bytemuck::cast_slice(&light_wt),
1014            usage: BufferUsages::STORAGE,
1015        });
1016        (s, d, w)
1017    } else {
1018        // Placeholder 4-byte buffers so bind group creation doesn't fail
1019        let placeholder: [u32; 1] = [0];
1020        let s = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1021            label: Some("ds-light-src-ph"),
1022            contents: bytemuck::cast_slice(&placeholder),
1023            usage: BufferUsages::STORAGE,
1024        });
1025        let d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1026            label: Some("ds-light-dst-ph"),
1027            contents: bytemuck::cast_slice(&placeholder),
1028            usage: BufferUsages::STORAGE,
1029        });
1030        let w_ph: [f32; 1] = [0.0];
1031        let w = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1032            label: Some("ds-light-wt-ph"),
1033            contents: bytemuck::cast_slice(&w_ph),
1034            usage: BufferUsages::STORAGE,
1035        });
1036        (s, d, w)
1037    };
1038
1039    // ── Heavy edge buffers ────────────────────────────────────────────────────
1040    let n_heavy = heavy_src.len();
1041    let (buf_heavy_src, buf_heavy_dst, buf_heavy_wt) = if n_heavy > 0 {
1042        let s = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1043            label: Some("ds-heavy-src"),
1044            contents: bytemuck::cast_slice(&heavy_src),
1045            usage: BufferUsages::STORAGE,
1046        });
1047        let d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1048            label: Some("ds-heavy-dst"),
1049            contents: bytemuck::cast_slice(&heavy_dst),
1050            usage: BufferUsages::STORAGE,
1051        });
1052        let w = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1053            label: Some("ds-heavy-wt"),
1054            contents: bytemuck::cast_slice(&heavy_wt),
1055            usage: BufferUsages::STORAGE,
1056        });
1057        (s, d, w)
1058    } else {
1059        let placeholder: [u32; 1] = [0];
1060        let s = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1061            label: Some("ds-heavy-src-ph"),
1062            contents: bytemuck::cast_slice(&placeholder),
1063            usage: BufferUsages::STORAGE,
1064        });
1065        let d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1066            label: Some("ds-heavy-dst-ph"),
1067            contents: bytemuck::cast_slice(&placeholder),
1068            usage: BufferUsages::STORAGE,
1069        });
1070        let w_ph: [f32; 1] = [0.0];
1071        let w = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1072            label: Some("ds-heavy-wt-ph"),
1073            contents: bytemuck::cast_slice(&w_ph),
1074            usage: BufferUsages::STORAGE,
1075        });
1076        (s, d, w)
1077    };
1078
1079    // ── Compile shaders ───────────────────────────────────────────────────────
1080    let light_shader = device.create_shader_module(ShaderModuleDescriptor {
1081        label: Some("ds-light-shader"),
1082        source: ShaderSource::Wgsl(DELTA_LIGHT_WGSL.into()),
1083    });
1084    let apply_shader = device.create_shader_module(ShaderModuleDescriptor {
1085        label: Some("ds-apply-shader"),
1086        source: ShaderSource::Wgsl(DELTA_APPLY_WGSL.into()),
1087    });
1088    let heavy_shader = device.create_shader_module(ShaderModuleDescriptor {
1089        label: Some("ds-heavy-shader"),
1090        source: ShaderSource::Wgsl(DELTA_HEAVY_WGSL.into()),
1091    });
1092
1093    // ── Build bind group layouts ──────────────────────────────────────────────
1094    // Light kernel: bindings 0-5 (src, dst, wt, distances_ro, proposed_rw, uniforms)
1095    let light_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1096        label: Some("ds-light-bgl"),
1097        entries: &[
1098            bgl_storage_ro(0), // light_src
1099            bgl_storage_ro(1), // light_dst
1100            bgl_storage_ro(2), // light_wt
1101            bgl_storage_ro(3), // distances (read-only snapshot)
1102            bgl_storage_rw(4), // proposed (atomic write)
1103            bgl_uniform(5),    // uniforms
1104        ],
1105    });
1106
1107    // Apply kernel: bindings 0-3 (proposed_ro, distances_rw, changed_rw, uniforms)
1108    let apply_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1109        label: Some("ds-apply-bgl"),
1110        entries: &[
1111            bgl_storage_ro(0), // proposed
1112            bgl_storage_rw(1), // distances (atomic update)
1113            bgl_storage_rw(2), // changed_flag (atomic)
1114            bgl_uniform(3),    // uniforms
1115        ],
1116    });
1117
1118    // Heavy kernel (DELTA_HEAVY): 0-5 (src, dst, wt, distances_rw, changed_flag_rw, uniforms)
1119    let heavy_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1120        label: Some("ds-heavy-bgl"),
1121        entries: &[
1122            bgl_storage_ro(0), // heavy_src
1123            bgl_storage_ro(1), // heavy_dst
1124            bgl_storage_ro(2), // heavy_wt
1125            bgl_storage_rw(3), // distances (atomic)
1126            bgl_storage_rw(4), // changed_flag (atomic) — tracks heavy-edge updates
1127            bgl_uniform(5),    // uniforms
1128        ],
1129    });
1130
1131    // ── Build pipelines ───────────────────────────────────────────────────────
1132    let light_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1133        label: Some("ds-light-layout"),
1134        bind_group_layouts: &[Some(&light_bgl)],
1135        ..Default::default()
1136    });
1137    let apply_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1138        label: Some("ds-apply-layout"),
1139        bind_group_layouts: &[Some(&apply_bgl)],
1140        ..Default::default()
1141    });
1142    let heavy_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1143        label: Some("ds-heavy-layout"),
1144        bind_group_layouts: &[Some(&heavy_bgl)],
1145        ..Default::default()
1146    });
1147
1148    let light_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1149        label: Some("ds-light-pipeline"),
1150        layout: Some(&light_pl_layout),
1151        module: &light_shader,
1152        entry_point: Some("delta_light"),
1153        compilation_options: Default::default(),
1154        cache: None,
1155    });
1156    let apply_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1157        label: Some("ds-apply-pipeline"),
1158        layout: Some(&apply_pl_layout),
1159        module: &apply_shader,
1160        entry_point: Some("delta_apply"),
1161        compilation_options: Default::default(),
1162        cache: None,
1163    });
1164    let heavy_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1165        label: Some("ds-heavy-pipeline"),
1166        layout: Some(&heavy_pl_layout),
1167        module: &heavy_shader,
1168        entry_point: Some("delta_heavy"),
1169        compilation_options: Default::default(),
1170        cache: None,
1171    });
1172
1173    // ── Uniform buffers ───────────────────────────────────────────────────────
1174    let light_uniforms: [u32; 4] = [n_light as u32, 0, 0, 0];
1175    let buf_light_uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1176        label: Some("ds-light-uniforms"),
1177        contents: bytemuck::cast_slice(&light_uniforms),
1178        usage: BufferUsages::UNIFORM,
1179    });
1180
1181    let apply_uniforms: [u32; 4] = [n as u32, 0, 0, 0];
1182    let buf_apply_uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1183        label: Some("ds-apply-uniforms"),
1184        contents: bytemuck::cast_slice(&apply_uniforms),
1185        usage: BufferUsages::UNIFORM,
1186    });
1187
1188    let heavy_uniforms: [u32; 4] = [n_heavy as u32, 0, 0, 0];
1189    let buf_heavy_uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1190        label: Some("ds-heavy-uniforms"),
1191        contents: bytemuck::cast_slice(&heavy_uniforms),
1192        usage: BufferUsages::UNIFORM,
1193    });
1194
1195    // ── Build bind groups ─────────────────────────────────────────────────────
1196    let light_bg = device.create_bind_group(&BindGroupDescriptor {
1197        label: Some("ds-light-bg"),
1198        layout: &light_bgl,
1199        entries: &[
1200            BindGroupEntry {
1201                binding: 0,
1202                resource: buf_light_src.as_entire_binding(),
1203            },
1204            BindGroupEntry {
1205                binding: 1,
1206                resource: buf_light_dst.as_entire_binding(),
1207            },
1208            BindGroupEntry {
1209                binding: 2,
1210                resource: buf_light_wt.as_entire_binding(),
1211            },
1212            BindGroupEntry {
1213                binding: 3,
1214                resource: buf_dist.as_entire_binding(),
1215            },
1216            BindGroupEntry {
1217                binding: 4,
1218                resource: buf_proposed.as_entire_binding(),
1219            },
1220            BindGroupEntry {
1221                binding: 5,
1222                resource: buf_light_uniforms.as_entire_binding(),
1223            },
1224        ],
1225    });
1226
1227    let apply_bg = device.create_bind_group(&BindGroupDescriptor {
1228        label: Some("ds-apply-bg"),
1229        layout: &apply_bgl,
1230        entries: &[
1231            BindGroupEntry {
1232                binding: 0,
1233                resource: buf_proposed.as_entire_binding(),
1234            },
1235            BindGroupEntry {
1236                binding: 1,
1237                resource: buf_dist.as_entire_binding(),
1238            },
1239            BindGroupEntry {
1240                binding: 2,
1241                resource: buf_changed.as_entire_binding(),
1242            },
1243            BindGroupEntry {
1244                binding: 3,
1245                resource: buf_apply_uniforms.as_entire_binding(),
1246            },
1247        ],
1248    });
1249
1250    let heavy_bg = device.create_bind_group(&BindGroupDescriptor {
1251        label: Some("ds-heavy-bg"),
1252        layout: &heavy_bgl,
1253        entries: &[
1254            BindGroupEntry {
1255                binding: 0,
1256                resource: buf_heavy_src.as_entire_binding(),
1257            },
1258            BindGroupEntry {
1259                binding: 1,
1260                resource: buf_heavy_dst.as_entire_binding(),
1261            },
1262            BindGroupEntry {
1263                binding: 2,
1264                resource: buf_heavy_wt.as_entire_binding(),
1265            },
1266            BindGroupEntry {
1267                binding: 3,
1268                resource: buf_dist.as_entire_binding(),
1269            },
1270            BindGroupEntry {
1271                binding: 4,
1272                resource: buf_changed.as_entire_binding(),
1273            },
1274            BindGroupEntry {
1275                binding: 5,
1276                resource: buf_heavy_uniforms.as_entire_binding(),
1277            },
1278        ],
1279    });
1280
1281    // ── Workgroup counts ──────────────────────────────────────────────────────
1282    let light_wg = (n_light as u32).saturating_add(255) / 256;
1283    let apply_wg = (n as u32).saturating_add(255) / 256;
1284    let heavy_wg = (n_heavy as u32).saturating_add(255) / 256;
1285
1286    // ── Main convergence loop ─────────────────────────────────────────────────
1287    // Maximum iterations bounded by n to prevent infinite loops in edge cases.
1288    let max_iters = n + 1;
1289    for _ in 0..max_iters {
1290        // Reset proposed buffer to INFINITY before each light phase
1291        let inf_reset: Vec<u32> = vec![inf_bits; n];
1292        queue.write_buffer(&buf_proposed, 0, bytemuck::cast_slice(&inf_reset));
1293        // Reset changed flag to 0
1294        queue.write_buffer(&buf_changed, 0, &0u32.to_le_bytes());
1295
1296        // Phase 1: Light edge proposal
1297        if n_light > 0 {
1298            let mut encoder =
1299                device.create_command_encoder(&CommandEncoderDescriptor { label: None });
1300            {
1301                let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor {
1302                    label: None,
1303                    timestamp_writes: None,
1304                });
1305                pass.set_pipeline(&light_pipeline);
1306                pass.set_bind_group(0, &light_bg, &[]);
1307                pass.dispatch_workgroups(light_wg.max(1), 1, 1);
1308            }
1309            queue.submit([encoder.finish()]);
1310            device
1311                .poll(wgpu::PollType::wait_indefinitely())
1312                .map_err(|e| format!("GPU poll (light): {e:?}"))?;
1313        }
1314
1315        // Phase 2: Apply proposals to distances, track convergence
1316        {
1317            let mut encoder =
1318                device.create_command_encoder(&CommandEncoderDescriptor { label: None });
1319            {
1320                let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor {
1321                    label: None,
1322                    timestamp_writes: None,
1323                });
1324                pass.set_pipeline(&apply_pipeline);
1325                pass.set_bind_group(0, &apply_bg, &[]);
1326                pass.dispatch_workgroups(apply_wg.max(1), 1, 1);
1327            }
1328            queue.submit([encoder.finish()]);
1329            device
1330                .poll(wgpu::PollType::wait_indefinitely())
1331                .map_err(|e| format!("GPU poll (apply): {e:?}"))?;
1332        }
1333
1334        // Phase 3: Heavy edge relaxation (one pass, like Bellman-Ford step)
1335        if n_heavy > 0 {
1336            let mut encoder =
1337                device.create_command_encoder(&CommandEncoderDescriptor { label: None });
1338            {
1339                let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor {
1340                    label: None,
1341                    timestamp_writes: None,
1342                });
1343                pass.set_pipeline(&heavy_pipeline);
1344                pass.set_bind_group(0, &heavy_bg, &[]);
1345                pass.dispatch_workgroups(heavy_wg.max(1), 1, 1);
1346            }
1347            // Copy changed flag to staging for readback
1348            encoder.copy_buffer_to_buffer(&buf_changed, 0, &buf_changed_staging, 0, 4);
1349            queue.submit([encoder.finish()]);
1350            device
1351                .poll(wgpu::PollType::wait_indefinitely())
1352                .map_err(|e| format!("GPU poll (heavy): {e:?}"))?;
1353        } else {
1354            // No heavy edges: copy changed flag to staging directly
1355            let mut encoder =
1356                device.create_command_encoder(&CommandEncoderDescriptor { label: None });
1357            encoder.copy_buffer_to_buffer(&buf_changed, 0, &buf_changed_staging, 0, 4);
1358            queue.submit([encoder.finish()]);
1359            device
1360                .poll(wgpu::PollType::wait_indefinitely())
1361                .map_err(|e| format!("GPU poll (no-heavy): {e:?}"))?;
1362        }
1363
1364        // Read convergence flag
1365        let changed = read_u32_from_staging(&device, &buf_changed_staging)?;
1366        if changed == 0 {
1367            break;
1368        }
1369    }
1370
1371    // ── Read back final distances ─────────────────────────────────────────────
1372    let dist_byte_len = (n * std::mem::size_of::<u32>()) as u64;
1373    let buf_staging = device.create_buffer(&BufferDescriptor {
1374        label: Some("ds-dist-staging"),
1375        size: dist_byte_len,
1376        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
1377        mapped_at_creation: false,
1378    });
1379    let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
1380    encoder.copy_buffer_to_buffer(&buf_dist, 0, &buf_staging, 0, dist_byte_len);
1381    queue.submit([encoder.finish()]);
1382    device
1383        .poll(wgpu::PollType::wait_indefinitely())
1384        .map_err(|e| format!("GPU poll (readback): {e:?}"))?;
1385
1386    let result_bits = read_u32_vec_from_staging(&device, &buf_staging, n)?;
1387    Ok(result_bits.into_iter().map(f32::from_bits).collect())
1388}
1389
1390// ─────────────────────────────────────────────────────────────────────────────
1391// Helper functions for bind group layout entries
1392// ─────────────────────────────────────────────────────────────────────────────
1393
1394#[cfg(feature = "wgpu")]
1395fn bgl_storage_ro(binding: u32) -> wgpu::BindGroupLayoutEntry {
1396    wgpu::BindGroupLayoutEntry {
1397        binding,
1398        visibility: wgpu::ShaderStages::COMPUTE,
1399        ty: wgpu::BindingType::Buffer {
1400            ty: wgpu::BufferBindingType::Storage { read_only: true },
1401            has_dynamic_offset: false,
1402            min_binding_size: None,
1403        },
1404        count: None,
1405    }
1406}
1407
1408#[cfg(feature = "wgpu")]
1409fn bgl_storage_rw(binding: u32) -> wgpu::BindGroupLayoutEntry {
1410    wgpu::BindGroupLayoutEntry {
1411        binding,
1412        visibility: wgpu::ShaderStages::COMPUTE,
1413        ty: wgpu::BindingType::Buffer {
1414            ty: wgpu::BufferBindingType::Storage { read_only: false },
1415            has_dynamic_offset: false,
1416            min_binding_size: None,
1417        },
1418        count: None,
1419    }
1420}
1421
1422#[cfg(feature = "wgpu")]
1423fn bgl_uniform(binding: u32) -> wgpu::BindGroupLayoutEntry {
1424    wgpu::BindGroupLayoutEntry {
1425        binding,
1426        visibility: wgpu::ShaderStages::COMPUTE,
1427        ty: wgpu::BindingType::Buffer {
1428            ty: wgpu::BufferBindingType::Uniform,
1429            has_dynamic_offset: false,
1430            min_binding_size: None,
1431        },
1432        count: None,
1433    }
1434}
1435
1436// ─────────────────────────────────────────────────────────────────────────────
1437// GPU buffer readback helpers
1438// ─────────────────────────────────────────────────────────────────────────────
1439
1440#[cfg(feature = "wgpu")]
1441fn read_u32_from_staging(
1442    device: &wgpu::Device,
1443    staging: &wgpu::Buffer,
1444) -> std::result::Result<u32, String> {
1445    let slice = staging.slice(0..4);
1446    let (tx, rx) = std::sync::mpsc::channel();
1447    slice.map_async(wgpu::MapMode::Read, move |r| {
1448        let _ = tx.send(r);
1449    });
1450    device
1451        .poll(wgpu::PollType::wait_indefinitely())
1452        .map_err(|e| format!("poll error: {e:?}"))?;
1453    rx.recv()
1454        .map_err(|_| "channel closed".to_string())?
1455        .map_err(|e| format!("map_async failed: {e:?}"))?;
1456    let mapped = slice.get_mapped_range();
1457    let val = u32::from_le_bytes([mapped[0], mapped[1], mapped[2], mapped[3]]);
1458    drop(mapped);
1459    staging.unmap();
1460    Ok(val)
1461}
1462
1463#[cfg(feature = "wgpu")]
1464fn read_u32_vec_from_staging(
1465    device: &wgpu::Device,
1466    staging: &wgpu::Buffer,
1467    count: usize,
1468) -> std::result::Result<Vec<u32>, String> {
1469    let byte_len = (count * 4) as u64;
1470    let slice = staging.slice(0..byte_len);
1471    let (tx, rx) = std::sync::mpsc::channel();
1472    slice.map_async(wgpu::MapMode::Read, move |r| {
1473        let _ = tx.send(r);
1474    });
1475    device
1476        .poll(wgpu::PollType::wait_indefinitely())
1477        .map_err(|e| format!("poll error: {e:?}"))?;
1478    rx.recv()
1479        .map_err(|_| "channel closed".to_string())?
1480        .map_err(|e| format!("map_async failed: {e:?}"))?;
1481    let mapped = slice.get_mapped_range();
1482    let result = mapped
1483        .chunks_exact(4)
1484        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
1485        .collect();
1486    drop(mapped);
1487    staging.unmap();
1488    Ok(result)
1489}
1490
1491#[cfg(feature = "wgpu")]
1492fn read_i32_vec_from_staging(
1493    device: &wgpu::Device,
1494    staging: &wgpu::Buffer,
1495    count: usize,
1496) -> std::result::Result<Vec<i32>, String> {
1497    let byte_len = (count * 4) as u64;
1498    let slice = staging.slice(0..byte_len);
1499    let (tx, rx) = std::sync::mpsc::channel();
1500    slice.map_async(wgpu::MapMode::Read, move |r| {
1501        let _ = tx.send(r);
1502    });
1503    device
1504        .poll(wgpu::PollType::wait_indefinitely())
1505        .map_err(|e| format!("poll error: {e:?}"))?;
1506    rx.recv()
1507        .map_err(|_| "channel closed".to_string())?
1508        .map_err(|e| format!("map_async failed: {e:?}"))?;
1509    let mapped = slice.get_mapped_range();
1510    let result = mapped
1511        .chunks_exact(4)
1512        .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
1513        .collect();
1514    drop(mapped);
1515    staging.unmap();
1516    Ok(result)
1517}
1518
1519// ─────────────────────────────────────────────────────────────────────────────
1520// Unit tests
1521// ─────────────────────────────────────────────────────────────────────────────
1522
1523#[cfg(test)]
1524mod tests {
1525    use super::*;
1526
1527    fn build_csr(n: usize, edges: &[(usize, usize)]) -> (Vec<usize>, Vec<usize>) {
1528        let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
1529        for &(u, v) in edges {
1530            adj[u].push(v);
1531            adj[v].push(u);
1532        }
1533        let mut row_ptr = vec![0usize; n + 1];
1534        for i in 0..n {
1535            row_ptr[i + 1] = row_ptr[i] + adj[i].len();
1536        }
1537        let col_idx: Vec<usize> = adj.into_iter().flatten().collect();
1538        (row_ptr, col_idx)
1539    }
1540
1541    fn build_csr_directed(
1542        n: usize,
1543        edges: &[(usize, usize, f64)],
1544    ) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
1545        let mut adj: Vec<Vec<(usize, f64)>> = vec![vec![]; n];
1546        for &(u, v, w) in edges {
1547            adj[u].push((v, w));
1548        }
1549        let mut row_ptr = vec![0usize; n + 1];
1550        for i in 0..n {
1551            row_ptr[i + 1] = row_ptr[i] + adj[i].len();
1552        }
1553        let mut col_idx = Vec::new();
1554        let mut weights = Vec::new();
1555        for nbrs in adj {
1556            for (v, w) in nbrs {
1557                col_idx.push(v);
1558                weights.push(w);
1559            }
1560        }
1561        (row_ptr, col_idx, weights)
1562    }
1563
1564    fn dijkstra_ref(adj: &[Vec<(usize, f64)>], src: usize) -> Vec<f64> {
1565        let n = adj.len();
1566        let mut dist = vec![f64::INFINITY; n];
1567        dist[src] = 0.0;
1568        let mut heap: BinaryHeap<(Reverse<u64>, usize)> = BinaryHeap::new();
1569        heap.push((Reverse(0), src));
1570        while let Some((Reverse(d), u)) = heap.pop() {
1571            let d = d as f64 * 1e-9;
1572            if d > dist[u] + 1e-12 {
1573                continue;
1574            }
1575            for &(v, w) in &adj[u] {
1576                let nd = dist[u] + w;
1577                if nd < dist[v] - 1e-12 {
1578                    dist[v] = nd;
1579                    heap.push((Reverse((nd * 1e9) as u64), v));
1580                }
1581            }
1582        }
1583        dist
1584    }
1585
1586    #[test]
1587    fn test_gpu_bfs_path_graph() {
1588        let (rp, ci) = build_csr(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
1589        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1590        assert_eq!(dist, vec![0, 1, 2, 3, 4]);
1591    }
1592
1593    #[test]
1594    fn test_gpu_bfs_connected() {
1595        let edges: Vec<(usize, usize)> = (0..5usize)
1596            .flat_map(|i| ((i + 1)..5).map(move |j| (i, j)))
1597            .collect();
1598        let (rp, ci) = build_csr(5, &edges);
1599        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1600        assert_eq!(dist[0], 0);
1601        for i in 1..5 {
1602            assert_eq!(dist[i], 1);
1603        }
1604    }
1605
1606    #[test]
1607    fn test_gpu_bfs_disconnected() {
1608        let (rp, ci) = build_csr(4, &[(0, 1), (2, 3)]);
1609        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1610        assert_eq!(dist[0], 0);
1611        assert_eq!(dist[1], 1);
1612        assert_eq!(dist[2], usize::MAX);
1613        assert_eq!(dist[3], usize::MAX);
1614    }
1615
1616    #[test]
1617    fn test_gpu_bfs_single_node() {
1618        let rp = vec![0usize, 0];
1619        let ci: Vec<usize> = vec![];
1620        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1621        assert_eq!(dist, vec![0]);
1622    }
1623
1624    #[test]
1625    fn test_gpu_bfs_invalid_source() {
1626        let (rp, ci) = build_csr(4, &[(0, 1)]);
1627        assert!(gpu_bfs(&rp, &ci, 10, &GpuBfsConfig::default()).is_err());
1628    }
1629
1630    #[test]
1631    fn test_gpu_bfs_tree() {
1632        let (rp, ci) = build_csr(7, &[(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]);
1633        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1634        assert_eq!(dist, vec![0, 1, 1, 2, 2, 2, 2]);
1635    }
1636
1637    #[test]
1638    fn test_gpu_bfs_star_graph() {
1639        let edges: Vec<(usize, usize)> = (1..=5).map(|i| (0, i)).collect();
1640        let (rp, ci) = build_csr(6, &edges);
1641        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1642        assert_eq!(dist[0], 0);
1643        for i in 1..=5 {
1644            assert_eq!(dist[i], 1);
1645        }
1646    }
1647
1648    #[test]
1649    fn test_gpu_bfs_cycle() {
1650        let (rp, ci) = build_csr(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
1651        let dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1652        assert_eq!(dist[0], 0);
1653        assert_eq!(dist[1], 1);
1654        assert_eq!(dist[2], 2);
1655        // 0-3 is a direct edge in undirected sense
1656        assert_eq!(dist[3], 1);
1657    }
1658
1659    #[test]
1660    fn test_gpu_sssp_shortest_paths() {
1661        // Triangle: 0->1 (1), 0->2 (4), 1->2 (2) → dist[2]=3
1662        let (rp, ci, w) = build_csr_directed(3, &[(0, 1, 1.0), (0, 2, 4.0), (1, 2, 2.0)]);
1663        let dist =
1664            gpu_sssp_bellman_ford(&rp, &ci, &w, 0, &GpuBfsConfig::default()).expect("sssp failed");
1665        assert!((dist[0] - 0.0).abs() < 1e-10);
1666        assert!((dist[1] - 1.0).abs() < 1e-10);
1667        assert!(
1668            (dist[2] - 3.0).abs() < 1e-10,
1669            "expected 3.0, got {}",
1670            dist[2]
1671        );
1672    }
1673
1674    #[test]
1675    fn test_gpu_sssp_negative_weight_detection() {
1676        // Negative cycle: 0->1 (1), 1->0 (-2)
1677        let (rp, ci, w) = build_csr_directed(3, &[(0, 1, 1.0), (1, 0, -2.0), (0, 2, 5.0)]);
1678        assert!(gpu_sssp_bellman_ford(&rp, &ci, &w, 0, &GpuBfsConfig::default()).is_err());
1679    }
1680
1681    #[test]
1682    fn test_gpu_sssp_unreachable() {
1683        let (rp, ci, w) = build_csr_directed(3, &[(0, 1, 1.0)]);
1684        let dist =
1685            gpu_sssp_bellman_ford(&rp, &ci, &w, 0, &GpuBfsConfig::default()).expect("sssp failed");
1686        assert_eq!(dist[2], f64::INFINITY);
1687    }
1688
1689    #[test]
1690    fn test_gpu_sssp_path_graph() {
1691        let (rp, ci, w) = build_csr_directed(4, &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0)]);
1692        let dist =
1693            gpu_sssp_bellman_ford(&rp, &ci, &w, 0, &GpuBfsConfig::default()).expect("sssp failed");
1694        for i in 0..4usize {
1695            assert!(
1696                (dist[i] - i as f64).abs() < 1e-10,
1697                "dist[{}]={}",
1698                i,
1699                dist[i]
1700            );
1701        }
1702    }
1703
1704    #[test]
1705    fn test_delta_stepping_matches_dijkstra() {
1706        let adj = vec![
1707            vec![(1usize, 2.0f64), (2, 6.0)],
1708            vec![(3usize, 1.0f64), (2, 3.0)],
1709            vec![(4usize, 1.0f64)],
1710            vec![(4usize, 5.0f64)],
1711            vec![],
1712        ];
1713        let delta_dist = gpu_sssp_delta_stepping(&adj, 0, 2.0, &GpuBfsConfig::default())
1714            .expect("delta stepping failed");
1715        let ref_dist = dijkstra_ref(&adj, 0);
1716        for i in 0..5 {
1717            if ref_dist[i] == f64::INFINITY {
1718                assert_eq!(delta_dist[i], f64::INFINITY);
1719            } else {
1720                assert!(
1721                    (ref_dist[i] - delta_dist[i]).abs() < 1e-9,
1722                    "node {}: ref={}, delta={}",
1723                    i,
1724                    ref_dist[i],
1725                    delta_dist[i]
1726                );
1727            }
1728        }
1729    }
1730
1731    #[test]
1732    fn test_delta_stepping_negative_weight_error() {
1733        let adj = vec![vec![(1usize, -1.0f64)], vec![]];
1734        assert!(gpu_sssp_delta_stepping(&adj, 0, 1.0, &GpuBfsConfig::default()).is_err());
1735    }
1736
1737    #[test]
1738    fn test_delta_stepping_invalid_source() {
1739        let adj = vec![vec![(1usize, 1.0f64)], vec![]];
1740        assert!(gpu_sssp_delta_stepping(&adj, 5, 1.0, &GpuBfsConfig::default()).is_err());
1741    }
1742
1743    #[test]
1744    fn test_delta_stepping_invalid_delta() {
1745        let adj = vec![vec![(1usize, 1.0f64)], vec![]];
1746        assert!(gpu_sssp_delta_stepping(&adj, 0, -1.0, &GpuBfsConfig::default()).is_err());
1747        assert!(gpu_sssp_delta_stepping(&adj, 0, 0.0, &GpuBfsConfig::default()).is_err());
1748    }
1749
1750    #[test]
1751    fn test_delta_stepping_disconnected() {
1752        let adj = vec![
1753            vec![(1usize, 1.0f64)],
1754            vec![],
1755            vec![(3usize, 2.0f64)],
1756            vec![],
1757        ];
1758        let dist = gpu_sssp_delta_stepping(&adj, 0, 1.0, &GpuBfsConfig::default()).expect("failed");
1759        assert!((dist[0] - 0.0).abs() < 1e-10);
1760        assert!((dist[1] - 1.0).abs() < 1e-10);
1761        assert_eq!(dist[2], f64::INFINITY);
1762        assert_eq!(dist[3], f64::INFINITY);
1763    }
1764
1765    #[test]
1766    fn test_parallel_bfs_atomic_matches_bfs() {
1767        let (rp, ci) = build_csr(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
1768        let bfs_dist = gpu_bfs(&rp, &ci, 0, &GpuBfsConfig::default()).expect("bfs failed");
1769        let atomic_dist = parallel_bfs_atomic(&rp, &ci, 0);
1770        assert_eq!(bfs_dist, atomic_dist);
1771    }
1772}