Skip to main content

trueno_graph/gpu/
bfs.rs

1//! GPU BFS (Breadth-First Search) implementation
2//!
3//! Frontier-based BFS using WGSL compute shaders.
4//! Based on Gunrock (Wang et al., ACM `ToPC` 2017).
5
6use super::{GpuCsrBuffers, GpuDevice};
7use crate::NodeId;
8use anyhow::{Context, Result};
9
10/// BFS parameters for GPU shader
11#[repr(C)]
12#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
13struct BfsParams {
14    num_nodes: u32,
15    current_level: u32,
16    source_node: u32,
17    _padding: u32,
18}
19
20/// GPU BFS result
21#[derive(Debug, Clone)]
22pub struct GpuBfsResult {
23    /// Distance from source to each node (`u32::MAX` for unreachable)
24    pub distances: Vec<u32>,
25
26    /// Number of nodes visited
27    pub visited_count: usize,
28}
29
30impl GpuBfsResult {
31    /// Get distance to a specific node
32    #[must_use]
33    pub fn distance(&self, node: NodeId) -> Option<u32> {
34        self.distances.get(node.0 as usize).copied().filter(|&d| d != u32::MAX)
35    }
36
37    /// Check if node is reachable from source
38    #[must_use]
39    pub fn is_reachable(&self, node: NodeId) -> bool {
40        self.distance(node).is_some()
41    }
42}
43
44/// Helper: Read single u32 from GPU buffer
45async fn read_buffer_u32(device: &GpuDevice, buffer: &wgpu::Buffer) -> Result<u32> {
46    let staging_buffer = device.create_buffer(
47        "Staging Buffer",
48        4,
49        wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
50    )?;
51
52    let mut encoder =
53        device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
54    encoder.copy_buffer_to_buffer(buffer, 0, &staging_buffer, 0, 4);
55    device.queue().submit(Some(encoder.finish()));
56
57    let buffer_slice = staging_buffer.slice(..);
58    let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
59
60    buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
61        let _ = tx.send(result);
62    });
63
64    device.device().poll(wgpu::Maintain::Wait);
65    rx.receive().await.context("Failed to receive map result")?.context("Buffer mapping failed")?;
66
67    let data = buffer_slice.get_mapped_range();
68    let value = u32::from_ne_bytes(data[0..4].try_into()?);
69    drop(data);
70    staging_buffer.unmap();
71
72    Ok(value)
73}
74
75/// Helper: Read distances array from GPU buffer
76async fn read_distances(
77    device: &GpuDevice,
78    distances_buffer: &wgpu::Buffer,
79    num_nodes: usize,
80) -> Result<Vec<u32>> {
81    let size = (num_nodes * std::mem::size_of::<u32>()) as u64;
82    let staging_buffer = device.create_buffer(
83        "Distances Staging",
84        size,
85        wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
86    )?;
87
88    let mut encoder =
89        device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
90    encoder.copy_buffer_to_buffer(distances_buffer, 0, &staging_buffer, 0, size);
91    device.queue().submit(Some(encoder.finish()));
92
93    let buffer_slice = staging_buffer.slice(..);
94    let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
95
96    buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
97        let _ = tx.send(result);
98    });
99
100    device.device().poll(wgpu::Maintain::Wait);
101    rx.receive().await.context("Failed to receive map result")?.context("Buffer mapping failed")?;
102
103    let data = buffer_slice.get_mapped_range();
104    let distances: Vec<u32> = bytemuck::cast_slice(&data).to_vec();
105    drop(data);
106    staging_buffer.unmap();
107
108    Ok(distances)
109}
110
111/// Run GPU BFS from source node
112///
113/// # Errors
114///
115/// Returns error if:
116/// - GPU shader compilation fails
117/// - Buffer creation fails
118/// - Shader dispatch fails
119/// - Result readback fails
120///
121/// # Example
122///
123/// ```ignore
124/// # use trueno_graph::gpu::{GpuDevice, GpuCsrBuffers, gpu_bfs};
125/// # use trueno_graph::{CsrGraph, NodeId};
126/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
127/// let device = GpuDevice::new().await?;
128/// let mut graph = CsrGraph::new();
129/// graph.add_edge(NodeId(0), NodeId(1), 1.0)?;
130/// graph.add_edge(NodeId(1), NodeId(2), 1.0)?;
131///
132/// let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph)?;
133/// let result = gpu_bfs(&device, &buffers, NodeId(0)).await?;
134///
135/// assert_eq!(result.distance(NodeId(0)), Some(0));
136/// assert_eq!(result.distance(NodeId(1)), Some(1));
137/// assert_eq!(result.distance(NodeId(2)), Some(2));
138/// # Ok(())
139/// # }
140/// ```
141#[allow(clippy::too_many_lines)]
142#[allow(clippy::cast_possible_truncation)]
143pub async fn gpu_bfs(
144    device: &GpuDevice,
145    buffers: &GpuCsrBuffers,
146    source: NodeId,
147) -> Result<GpuBfsResult> {
148    // Step 1: Load WGSL shader
149    const SHADER: &str = include_str!("shaders/bfs_simple.wgsl");
150    let shader_module = device.device().create_shader_module(wgpu::ShaderModuleDescriptor {
151        label: Some("BFS Shader"),
152        source: wgpu::ShaderSource::Wgsl(SHADER.into()),
153    });
154
155    // Step 2: Create bind group layout
156    let bind_group_layout =
157        device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
158            label: Some("BFS Bind Group Layout"),
159            entries: &[
160                // @binding(0): uniform params
161                wgpu::BindGroupLayoutEntry {
162                    binding: 0,
163                    visibility: wgpu::ShaderStages::COMPUTE,
164                    ty: wgpu::BindingType::Buffer {
165                        ty: wgpu::BufferBindingType::Uniform,
166                        has_dynamic_offset: false,
167                        min_binding_size: None,
168                    },
169                    count: None,
170                },
171                // @binding(1): storage row_offsets (read)
172                wgpu::BindGroupLayoutEntry {
173                    binding: 1,
174                    visibility: wgpu::ShaderStages::COMPUTE,
175                    ty: wgpu::BindingType::Buffer {
176                        ty: wgpu::BufferBindingType::Storage { read_only: true },
177                        has_dynamic_offset: false,
178                        min_binding_size: None,
179                    },
180                    count: None,
181                },
182                // @binding(2): storage col_indices (read)
183                wgpu::BindGroupLayoutEntry {
184                    binding: 2,
185                    visibility: wgpu::ShaderStages::COMPUTE,
186                    ty: wgpu::BindingType::Buffer {
187                        ty: wgpu::BufferBindingType::Storage { read_only: true },
188                        has_dynamic_offset: false,
189                        min_binding_size: None,
190                    },
191                    count: None,
192                },
193                // @binding(3): storage distances (read_write, atomic)
194                wgpu::BindGroupLayoutEntry {
195                    binding: 3,
196                    visibility: wgpu::ShaderStages::COMPUTE,
197                    ty: wgpu::BindingType::Buffer {
198                        ty: wgpu::BufferBindingType::Storage { read_only: false },
199                        has_dynamic_offset: false,
200                        min_binding_size: None,
201                    },
202                    count: None,
203                },
204                // @binding(4): storage updated (read_write, atomic)
205                wgpu::BindGroupLayoutEntry {
206                    binding: 4,
207                    visibility: wgpu::ShaderStages::COMPUTE,
208                    ty: wgpu::BindingType::Buffer {
209                        ty: wgpu::BufferBindingType::Storage { read_only: false },
210                        has_dynamic_offset: false,
211                        min_binding_size: None,
212                    },
213                    count: None,
214                },
215            ],
216        });
217
218    // Step 3: Create compute pipeline
219    let pipeline_layout = device.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
220        label: Some("BFS Pipeline Layout"),
221        bind_group_layouts: &[&bind_group_layout],
222        push_constant_ranges: &[],
223    });
224
225    let compute_pipeline =
226        device.device().create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
227            label: Some("BFS Pipeline"),
228            layout: Some(&pipeline_layout),
229            module: &shader_module,
230            entry_point: "bfs_level",
231            compilation_options: wgpu::PipelineCompilationOptions::default(),
232            cache: None,
233        });
234
235    // Step 4: Create auxiliary buffers
236    let num_nodes = buffers.num_nodes();
237
238    // Params buffer (uniform)
239    let params_buffer = device.create_buffer_init(
240        "BFS Params",
241        bytemuck::bytes_of(&BfsParams {
242            num_nodes: num_nodes as u32,
243            current_level: 0,
244            source_node: source.0,
245            _padding: 0,
246        }),
247        wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
248    )?;
249
250    // Distances buffer (storage, atomic<u32>)
251    let mut initial_distances = vec![u32::MAX; num_nodes];
252    if (source.0 as usize) < num_nodes {
253        initial_distances[source.0 as usize] = 0;
254    }
255
256    let distances_buffer = device.create_buffer_init(
257        "BFS Distances",
258        bytemuck::cast_slice(&initial_distances),
259        wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
260    )?;
261
262    // Updated flag buffer (storage, atomic<u32>)
263    let updated_buffer = device.create_buffer_init(
264        "BFS Updated Flag",
265        bytemuck::bytes_of(&0u32),
266        wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
267    )?;
268
269    // Step 5: Create bind group
270    let bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
271        label: Some("BFS Bind Group"),
272        layout: &bind_group_layout,
273        entries: &[
274            wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() },
275            wgpu::BindGroupEntry { binding: 1, resource: buffers.row_offsets.as_entire_binding() },
276            wgpu::BindGroupEntry { binding: 2, resource: buffers.col_indices.as_entire_binding() },
277            wgpu::BindGroupEntry { binding: 3, resource: distances_buffer.as_entire_binding() },
278            wgpu::BindGroupEntry { binding: 4, resource: updated_buffer.as_entire_binding() },
279        ],
280    });
281
282    // Step 6: BFS dispatch loop
283    let workgroup_size = 256;
284    let num_workgroups = (num_nodes as u32).div_ceil(workgroup_size).max(1);
285
286    for level in 0..num_nodes {
287        // Reset updated flag to 0
288        device.queue().write_buffer(&updated_buffer, 0, bytemuck::bytes_of(&0u32));
289
290        // Update params with current level
291        device.queue().write_buffer(
292            &params_buffer,
293            0,
294            bytemuck::bytes_of(&BfsParams {
295                num_nodes: num_nodes as u32,
296                current_level: level as u32,
297                source_node: source.0,
298                _padding: 0,
299            }),
300        );
301
302        // Create command encoder
303        let mut encoder = device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
304            label: Some("BFS Command Encoder"),
305        });
306
307        {
308            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
309                label: Some("BFS Compute Pass"),
310                timestamp_writes: None,
311            });
312
313            compute_pass.set_pipeline(&compute_pipeline);
314            compute_pass.set_bind_group(0, &bind_group, &[]);
315            compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
316        }
317
318        // Submit commands
319        device.queue().submit(Some(encoder.finish()));
320
321        // Wait for GPU to ensure correctness (future optimization: async polling)
322        device.device().poll(wgpu::Maintain::Wait);
323
324        // Read updated flag
325        let updated_value = read_buffer_u32(device, &updated_buffer).await?;
326
327        // If no updates, BFS is complete
328        if updated_value == 0 {
329            break;
330        }
331    }
332
333    // Step 7: Read back results
334    let distances = read_distances(device, &distances_buffer, num_nodes).await?;
335    let visited_count = distances.iter().filter(|&&d| d != u32::MAX).count();
336
337    Ok(GpuBfsResult { distances, visited_count })
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::CsrGraph;
344
345    #[tokio::test]
346    async fn test_gpu_bfs_simple_chain() {
347        if !GpuDevice::is_gpu_available().await {
348            eprintln!("⚠️  Skipping test_gpu_bfs_simple_chain: GPU not available");
349            return;
350        }
351
352        let device = GpuDevice::new().await.unwrap();
353
354        // Create chain: 0 -> 1 -> 2
355        let mut graph = CsrGraph::new();
356        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
357        graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
358
359        let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph).unwrap();
360        let result = gpu_bfs(&device, &buffers, NodeId(0)).await.unwrap();
361
362        // Verify BFS distances
363        assert_eq!(result.distance(NodeId(0)), Some(0));
364        assert_eq!(result.distance(NodeId(1)), Some(1));
365        assert_eq!(result.distance(NodeId(2)), Some(2));
366    }
367
368    #[tokio::test]
369    async fn test_gpu_bfs_disconnected() {
370        if !GpuDevice::is_gpu_available().await {
371            eprintln!("⚠️  Skipping test_gpu_bfs_disconnected: GPU not available");
372            return;
373        }
374
375        let device = GpuDevice::new().await.unwrap();
376
377        // Create disconnected: 0 -> 1, 2 (isolated)
378        let mut graph = CsrGraph::new();
379        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
380        graph.add_edge(NodeId(2), NodeId(2), 1.0).unwrap(); // Self-loop
381
382        let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph).unwrap();
383        let result = gpu_bfs(&device, &buffers, NodeId(0)).await.unwrap();
384
385        assert_eq!(result.distance(NodeId(0)), Some(0));
386        assert!(!result.is_reachable(NodeId(2))); // Should be unreachable
387    }
388
389    #[test]
390    fn test_gpu_bfs_result_api() {
391        let result = GpuBfsResult { distances: vec![0, 1, u32::MAX], visited_count: 2 };
392
393        assert_eq!(result.distance(NodeId(0)), Some(0));
394        assert_eq!(result.distance(NodeId(1)), Some(1));
395        assert_eq!(result.distance(NodeId(2)), None); // Unreachable
396
397        assert!(result.is_reachable(NodeId(0)));
398        assert!(result.is_reachable(NodeId(1)));
399        assert!(!result.is_reachable(NodeId(2)));
400    }
401}