1use super::{GpuCsrBuffers, GpuDevice};
7use anyhow::{Context, Result};
8
9#[repr(C)]
11#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
12struct PageRankParams {
13 num_nodes: u32,
14 damping: f32,
15 iteration: u32,
16 dangling_sum: f32,
17}
18
19#[derive(Debug, Clone)]
21pub struct GpuPageRankResult {
22 pub scores: Vec<f32>,
24
25 pub iterations: usize,
27}
28
29impl GpuPageRankResult {
30 #[must_use]
32 pub fn score(&self, node_id: usize) -> Option<f32> {
33 self.scores.get(node_id).copied()
34 }
35}
36
37async fn read_scores(
39 device: &GpuDevice,
40 scores_buffer: &wgpu::Buffer,
41 num_nodes: usize,
42) -> Result<Vec<f32>> {
43 let size = (num_nodes * std::mem::size_of::<f32>()) as u64;
44 let staging_buffer = device.create_buffer(
45 "Scores Staging",
46 size,
47 wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
48 )?;
49
50 let mut encoder =
51 device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
52 encoder.copy_buffer_to_buffer(scores_buffer, 0, &staging_buffer, 0, size);
53 device.queue().submit(Some(encoder.finish()));
54
55 let buffer_slice = staging_buffer.slice(..);
56 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
57
58 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
59 let _ = tx.send(result);
60 });
61
62 device.device().poll(wgpu::Maintain::Wait);
63 rx.receive().await.context("Failed to receive map result")?.context("Buffer mapping failed")?;
64
65 let data = buffer_slice.get_mapped_range();
66 let scores: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
67 drop(data);
68 staging_buffer.unmap();
69
70 Ok(scores)
71}
72
73#[allow(clippy::too_many_lines)]
112#[allow(clippy::cast_possible_truncation)]
113#[allow(clippy::cast_precision_loss)]
114pub async fn gpu_pagerank(
115 device: &GpuDevice,
116 buffers: &GpuCsrBuffers,
117 out_degrees: &[u32],
118 max_iterations: usize,
119 damping: f32,
120) -> Result<GpuPageRankResult> {
121 const SHADER: &str = include_str!("shaders/pagerank.wgsl");
123 let shader_module = device.device().create_shader_module(wgpu::ShaderModuleDescriptor {
124 label: Some("PageRank Shader"),
125 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
126 });
127
128 let bind_group_layout =
130 device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
131 label: Some("PageRank Bind Group Layout"),
132 entries: &[
133 wgpu::BindGroupLayoutEntry {
135 binding: 0,
136 visibility: wgpu::ShaderStages::COMPUTE,
137 ty: wgpu::BindingType::Buffer {
138 ty: wgpu::BufferBindingType::Uniform,
139 has_dynamic_offset: false,
140 min_binding_size: None,
141 },
142 count: None,
143 },
144 wgpu::BindGroupLayoutEntry {
146 binding: 1,
147 visibility: wgpu::ShaderStages::COMPUTE,
148 ty: wgpu::BindingType::Buffer {
149 ty: wgpu::BufferBindingType::Storage { read_only: true },
150 has_dynamic_offset: false,
151 min_binding_size: None,
152 },
153 count: None,
154 },
155 wgpu::BindGroupLayoutEntry {
157 binding: 2,
158 visibility: wgpu::ShaderStages::COMPUTE,
159 ty: wgpu::BindingType::Buffer {
160 ty: wgpu::BufferBindingType::Storage { read_only: true },
161 has_dynamic_offset: false,
162 min_binding_size: None,
163 },
164 count: None,
165 },
166 wgpu::BindGroupLayoutEntry {
168 binding: 3,
169 visibility: wgpu::ShaderStages::COMPUTE,
170 ty: wgpu::BindingType::Buffer {
171 ty: wgpu::BufferBindingType::Storage { read_only: true },
172 has_dynamic_offset: false,
173 min_binding_size: None,
174 },
175 count: None,
176 },
177 wgpu::BindGroupLayoutEntry {
179 binding: 4,
180 visibility: wgpu::ShaderStages::COMPUTE,
181 ty: wgpu::BindingType::Buffer {
182 ty: wgpu::BufferBindingType::Storage { read_only: false },
183 has_dynamic_offset: false,
184 min_binding_size: None,
185 },
186 count: None,
187 },
188 wgpu::BindGroupLayoutEntry {
190 binding: 5,
191 visibility: wgpu::ShaderStages::COMPUTE,
192 ty: wgpu::BindingType::Buffer {
193 ty: wgpu::BufferBindingType::Storage { read_only: true },
194 has_dynamic_offset: false,
195 min_binding_size: None,
196 },
197 count: None,
198 },
199 ],
200 });
201
202 let pipeline_layout = device.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
204 label: Some("PageRank Pipeline Layout"),
205 bind_group_layouts: &[&bind_group_layout],
206 push_constant_ranges: &[],
207 });
208
209 let compute_pipeline =
210 device.device().create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
211 label: Some("PageRank Pipeline"),
212 layout: Some(&pipeline_layout),
213 module: &shader_module,
214 entry_point: "pagerank_iteration",
215 compilation_options: wgpu::PipelineCompilationOptions::default(),
216 cache: None,
217 });
218
219 let num_nodes = buffers.num_nodes();
221
222 let initial_score = 1.0 / num_nodes as f32;
224
225 let dangling_count = out_degrees.iter().filter(|&&d| d == 0).count();
227 let initial_dangling_sum = dangling_count as f32 * initial_score;
228
229 let params_buffer = device.create_buffer_init(
231 "PageRank Params",
232 bytemuck::bytes_of(&PageRankParams {
233 num_nodes: num_nodes as u32,
234 damping,
235 iteration: 0,
236 dangling_sum: initial_dangling_sum,
237 }),
238 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
239 )?;
240 let initial_scores = vec![initial_score; num_nodes];
241
242 let current_scores_buffer = device.create_buffer_init(
244 "PageRank Current Scores",
245 bytemuck::cast_slice(&initial_scores),
246 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
247 )?;
248
249 let next_scores_buffer = device.create_buffer_init(
251 "PageRank Next Scores",
252 bytemuck::cast_slice(&initial_scores),
253 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
254 )?;
255
256 let out_degrees_buffer = device.create_buffer_init(
258 "PageRank Out Degrees",
259 bytemuck::cast_slice(out_degrees),
260 wgpu::BufferUsages::STORAGE,
261 )?;
262
263 let workgroup_size = 256;
265 let num_workgroups = (num_nodes as u32).div_ceil(workgroup_size).max(1);
266
267 for iteration in 0..max_iterations {
269 let current_scores = read_scores(device, ¤t_scores_buffer, num_nodes).await?;
271 let dangling_sum: f32 =
272 (0..num_nodes).filter(|&i| out_degrees[i] == 0).map(|i| current_scores[i]).sum();
273
274 device.queue().write_buffer(
276 ¶ms_buffer,
277 0,
278 bytemuck::bytes_of(&PageRankParams {
279 num_nodes: num_nodes as u32,
280 damping,
281 iteration: iteration as u32,
282 dangling_sum,
283 }),
284 );
285
286 let bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
288 label: Some("PageRank Bind Group"),
289 layout: &bind_group_layout,
290 entries: &[
291 wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() },
292 wgpu::BindGroupEntry {
293 binding: 1,
294 resource: buffers.row_offsets.as_entire_binding(),
295 },
296 wgpu::BindGroupEntry {
297 binding: 2,
298 resource: buffers.col_indices.as_entire_binding(),
299 },
300 wgpu::BindGroupEntry {
301 binding: 3,
302 resource: current_scores_buffer.as_entire_binding(),
303 },
304 wgpu::BindGroupEntry {
305 binding: 4,
306 resource: next_scores_buffer.as_entire_binding(),
307 },
308 wgpu::BindGroupEntry {
309 binding: 5,
310 resource: out_degrees_buffer.as_entire_binding(),
311 },
312 ],
313 });
314
315 let mut encoder = device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
317 label: Some("PageRank Command Encoder"),
318 });
319
320 {
321 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
322 label: Some("PageRank Compute Pass"),
323 timestamp_writes: None,
324 });
325
326 compute_pass.set_pipeline(&compute_pipeline);
327 compute_pass.set_bind_group(0, &bind_group, &[]);
328 compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
329 }
330
331 device.queue().submit(Some(encoder.finish()));
333
334 device.device().poll(wgpu::Maintain::Wait);
336
337 let mut encoder = device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
339 label: Some("PageRank Buffer Swap"),
340 });
341
342 let buffer_size = (num_nodes * std::mem::size_of::<f32>()) as u64;
343 encoder.copy_buffer_to_buffer(
344 &next_scores_buffer,
345 0,
346 ¤t_scores_buffer,
347 0,
348 buffer_size,
349 );
350
351 device.queue().submit(Some(encoder.finish()));
352 device.device().poll(wgpu::Maintain::Wait);
353 }
354
355 let scores = read_scores(device, ¤t_scores_buffer, num_nodes).await?;
357
358 Ok(GpuPageRankResult { scores, iterations: max_iterations })
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::{CsrGraph, NodeId};
365
366 #[tokio::test]
367 #[allow(clippy::cast_possible_truncation)]
368 async fn test_gpu_pagerank_simple_chain() {
369 if !GpuDevice::is_gpu_available().await {
370 eprintln!("⚠️ Skipping test_gpu_pagerank_simple_chain: GPU not available");
371 return;
372 }
373
374 let device = GpuDevice::new().await.unwrap();
375
376 let mut graph = CsrGraph::new();
378 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
379 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
380
381 let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph).unwrap();
382 let out_degrees: Vec<u32> = (0..graph.num_nodes())
383 .map(|i| graph.outgoing_neighbors(NodeId(i as u32)).unwrap().len() as u32)
384 .collect();
385 let result = gpu_pagerank(&device, &buffers, &out_degrees, 20, 0.85).await.unwrap();
386
387 let score_0 = result.score(0).unwrap();
389 let score_1 = result.score(1).unwrap();
390 let score_2 = result.score(2).unwrap();
391
392 println!("GPU PageRank scores: node0={score_0}, node1={score_1}, node2={score_2}");
393
394 assert!(score_0 > 0.0, "Score 0 should be positive");
396 assert!(score_1 > 0.0, "Score 1 should be positive");
397 assert!(score_2 > 0.0, "Score 2 should be positive");
398
399 assert!(score_2 > score_1);
401 assert!(score_1 > score_0);
402
403 let sum = score_0 + score_1 + score_2;
405 println!("Sum: {sum}");
406 assert!((sum - 1.0).abs() < 0.1, "Sum should be approximately 1.0, got {sum}");
407 }
408
409 #[test]
410 fn test_gpu_pagerank_result_api() {
411 let result = GpuPageRankResult { scores: vec![0.1, 0.3, 0.6], iterations: 20 };
412
413 assert_eq!(result.score(0), Some(0.1));
414 assert_eq!(result.score(1), Some(0.3));
415 assert_eq!(result.score(2), Some(0.6));
416 assert_eq!(result.score(3), None); }
418}