Skip to main content

trueno_graph/gpu/
paging.rs

1//! Graph paging system for handling graphs larger than VRAM
2//!
3//! Implements morsel-driven parallelism and tile-based processing.
4//! Based on Umbra (Neumann & Freitag, CIDR 2020) and Funke et al. (SIGMOD 2018).
5
6use super::cache::TileId;
7use super::memory::GpuMemoryLimits;
8use super::GpuDevice;
9use crate::{CsrGraph, NodeId};
10use anyhow::Result;
11
12/// Graph tile (subset of nodes and their edges)
13#[derive(Debug, Clone)]
14pub struct GraphTile {
15    /// Tile ID
16    pub id: TileId,
17
18    /// Starting node index (inclusive)
19    pub start_node: usize,
20
21    /// Ending node index (exclusive)
22    pub end_node: usize,
23
24    /// Number of nodes in this tile
25    pub num_nodes: usize,
26
27    /// CSR row offsets for this tile
28    pub row_offsets: Vec<u32>,
29    /// CSR column indices (edge targets)
30    pub col_indices: Vec<u32>,
31    /// Edge weights
32    pub edge_weights: Vec<f32>,
33}
34
35impl GraphTile {
36    /// Calculate memory footprint in bytes
37    #[must_use]
38    pub fn size_bytes(&self) -> usize {
39        let row_offsets_size = self.row_offsets.len() * std::mem::size_of::<u32>();
40        let col_indices_size = self.col_indices.len() * std::mem::size_of::<u32>();
41        let weights_size = self.edge_weights.len() * std::mem::size_of::<f32>();
42        row_offsets_size + col_indices_size + weights_size
43    }
44}
45
46/// Paging coordinator for managing graph tiles
47///
48/// Splits large graphs into tiles and manages their lifecycle in GPU memory.
49pub struct PagingCoordinator {
50    /// GPU memory limits
51    limits: GpuMemoryLimits,
52
53    /// All tiles in the graph
54    tiles: Vec<GraphTile>,
55
56    /// Tile size in nodes
57    tile_size: usize,
58}
59
60impl PagingCoordinator {
61    /// Create paging coordinator for a graph
62    ///
63    /// # Errors
64    ///
65    /// Returns error if memory limits cannot be detected
66    #[allow(clippy::cast_possible_truncation)]
67    pub fn new(device: &GpuDevice, graph: &CsrGraph) -> Result<Self> {
68        let limits = GpuMemoryLimits::detect(device)?;
69
70        // Calculate tile size based on VRAM limits
71        let (row_offsets, col_indices, edge_weights) = graph.csr_components();
72        let total_nodes = graph.num_nodes();
73
74        // Estimate bytes per node (amortized)
75        let total_graph_bytes = std::mem::size_of_val(row_offsets)
76            + std::mem::size_of_val(col_indices)
77            + std::mem::size_of_val(edge_weights);
78
79        let bytes_per_node = if total_nodes > 0 {
80            total_graph_bytes / total_nodes
81        } else {
82            1000 // Default estimate
83        };
84
85        let tile_size = limits.recommended_tile_size(bytes_per_node);
86
87        // Split graph into tiles
88        let mut tiles = Vec::new();
89
90        for (tile_id, start_node) in (0..total_nodes).step_by(tile_size).enumerate() {
91            let end_node = (start_node + tile_size).min(total_nodes);
92            let num_nodes = end_node - start_node;
93
94            // Extract CSR data for this tile
95            let (tile_row_offsets, tile_col_indices, tile_edge_weights) =
96                Self::extract_tile_csr(graph, start_node, end_node);
97
98            tiles.push(GraphTile {
99                id: tile_id,
100                start_node,
101                end_node,
102                num_nodes,
103                row_offsets: tile_row_offsets,
104                col_indices: tile_col_indices,
105                edge_weights: tile_edge_weights,
106            });
107        }
108
109        Ok(Self { limits, tiles, tile_size })
110    }
111
112    /// Extract CSR data for a tile (nodes `start_node..end_node`)
113    #[allow(clippy::cast_possible_truncation)]
114    fn extract_tile_csr(
115        graph: &CsrGraph,
116        start_node: usize,
117        end_node: usize,
118    ) -> (Vec<u32>, Vec<u32>, Vec<f32>) {
119        let (graph_row_offsets, graph_col_indices, graph_edge_weights) = graph.csr_components();
120
121        let mut tile_row_offsets = vec![0];
122        let mut tile_col_indices = Vec::new();
123        let mut tile_edge_weights = Vec::new();
124
125        for node_idx in start_node..end_node {
126            let start = graph_row_offsets[node_idx] as usize;
127            let end = graph_row_offsets[node_idx + 1] as usize;
128
129            // Copy edges for this node
130            tile_col_indices.extend_from_slice(&graph_col_indices[start..end]);
131            tile_edge_weights.extend_from_slice(&graph_edge_weights[start..end]);
132
133            // Update row offset
134            tile_row_offsets.push(tile_col_indices.len() as u32);
135        }
136
137        (tile_row_offsets, tile_col_indices, tile_edge_weights)
138    }
139
140    /// Get tile containing a specific node
141    #[must_use]
142    pub fn get_tile_for_node(&self, node: NodeId) -> Option<&GraphTile> {
143        let node_idx = node.0 as usize;
144        self.tiles.iter().find(|tile| node_idx >= tile.start_node && node_idx < tile.end_node)
145    }
146
147    /// Get tile by ID
148    #[must_use]
149    pub fn get_tile(&self, tile_id: TileId) -> Option<&GraphTile> {
150        self.tiles.get(tile_id)
151    }
152
153    /// Get total number of tiles
154    #[must_use]
155    pub fn num_tiles(&self) -> usize {
156        self.tiles.len()
157    }
158
159    /// Check if graph fits entirely in VRAM (no paging needed)
160    #[must_use]
161    pub fn fits_in_vram(&self) -> bool {
162        let total_bytes: usize = self.tiles.iter().map(GraphTile::size_bytes).sum();
163        self.limits.fits_in_vram(total_bytes)
164    }
165
166    /// Get tile size in nodes
167    #[must_use]
168    pub fn tile_size(&self) -> usize {
169        self.tile_size
170    }
171
172    /// Get memory limits
173    #[must_use]
174    pub const fn limits(&self) -> &GpuMemoryLimits {
175        &self.limits
176    }
177
178    /// Get iterator over all tiles
179    pub fn tiles(&self) -> impl Iterator<Item = &GraphTile> {
180        self.tiles.iter()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[tokio::test]
189    async fn test_paging_coordinator_small_graph() {
190        if !GpuDevice::is_gpu_available().await {
191            eprintln!("⚠️  Skipping test_paging_coordinator_small_graph: GPU not available");
192            return;
193        }
194
195        let device = GpuDevice::new().await.unwrap();
196
197        // Create small graph: 0 -> 1 -> 2
198        let mut graph = CsrGraph::new();
199        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
200        graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
201
202        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
203
204        assert!(coordinator.num_tiles() >= 1);
205        assert!(coordinator.fits_in_vram()); // Small graph should fit
206
207        // Check tile for node 0
208        let tile = coordinator.get_tile_for_node(NodeId(0)).unwrap();
209        assert!(tile.start_node <= 0);
210        assert!(tile.end_node > 0);
211    }
212
213    #[tokio::test]
214    async fn test_paging_coordinator_tiling() {
215        if !GpuDevice::is_gpu_available().await {
216            eprintln!("⚠️  Skipping test_paging_coordinator_tiling: GPU not available");
217            return;
218        }
219
220        let device = GpuDevice::new().await.unwrap();
221
222        // Create larger graph
223        let mut graph = CsrGraph::new();
224        for i in 0..100 {
225            graph.add_edge(NodeId(i), NodeId((i + 1) % 100), 1.0).unwrap();
226        }
227
228        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
229
230        println!("Graph nodes: {}", graph.num_nodes());
231        println!("Num tiles: {}", coordinator.num_tiles());
232        println!("Tile size: {}", coordinator.tile_size());
233
234        assert!(coordinator.num_tiles() >= 1);
235
236        // Verify all nodes are covered by tiles
237        for node_id in 0..100 {
238            assert!(
239                coordinator.get_tile_for_node(NodeId(node_id)).is_some(),
240                "Node {} not in any tile",
241                node_id
242            );
243        }
244    }
245
246    #[tokio::test]
247    async fn test_paging_coordinator_get_tile() {
248        if !GpuDevice::is_gpu_available().await {
249            eprintln!("⚠️  Skipping test_paging_coordinator_get_tile: GPU not available");
250            return;
251        }
252
253        let device = GpuDevice::new().await.unwrap();
254
255        let mut graph = CsrGraph::new();
256        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
257        graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
258
259        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
260
261        // Get tile by ID
262        let tile0 = coordinator.get_tile(0);
263        assert!(tile0.is_some());
264
265        // Invalid tile ID
266        let invalid_tile = coordinator.get_tile(999);
267        assert!(invalid_tile.is_none());
268    }
269
270    #[tokio::test]
271    async fn test_paging_coordinator_tiles_iterator() {
272        if !GpuDevice::is_gpu_available().await {
273            eprintln!("⚠️  Skipping test_paging_coordinator_tiles_iterator: GPU not available");
274            return;
275        }
276
277        let device = GpuDevice::new().await.unwrap();
278
279        let mut graph = CsrGraph::new();
280        for i in 0..50 {
281            graph.add_edge(NodeId(i), NodeId(i + 1), 1.0).unwrap();
282        }
283
284        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
285
286        // Count tiles using iterator
287        let tile_count = coordinator.tiles().count();
288        assert_eq!(tile_count, coordinator.num_tiles());
289
290        // Verify all tiles have valid ranges
291        for tile in coordinator.tiles() {
292            assert!(tile.start_node < tile.end_node);
293            assert_eq!(tile.num_nodes, tile.end_node - tile.start_node);
294        }
295    }
296
297    #[tokio::test]
298    async fn test_paging_coordinator_limits() {
299        if !GpuDevice::is_gpu_available().await {
300            eprintln!("⚠️  Skipping test_paging_coordinator_limits: GPU not available");
301            return;
302        }
303
304        let device = GpuDevice::new().await.unwrap();
305
306        let mut graph = CsrGraph::new();
307        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
308
309        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
310
311        let limits = coordinator.limits();
312        assert!(limits.total_vram > 0);
313        assert!(limits.usable_vram > 0);
314        assert!(limits.max_morsels > 0);
315    }
316
317    #[test]
318    fn test_graph_tile_size() {
319        let tile = GraphTile {
320            id: 0,
321            start_node: 0,
322            end_node: 100,
323            num_nodes: 100,
324            row_offsets: vec![0; 101],
325            col_indices: vec![0; 200],
326            edge_weights: vec![0.0; 200],
327        };
328
329        let size = tile.size_bytes();
330        assert!(size > 0);
331
332        // 101 u32 + 200 u32 + 200 f32 = 101*4 + 200*4 + 200*4 = 2004 bytes
333        assert_eq!(size, 101 * 4 + 200 * 4 + 200 * 4);
334    }
335
336    #[tokio::test]
337    async fn test_paging_coordinator_empty_graph() {
338        if !GpuDevice::is_gpu_available().await {
339            eprintln!("⚠️  Skipping test_paging_coordinator_empty_graph: GPU not available");
340            return;
341        }
342
343        let device = GpuDevice::new().await.unwrap();
344        let graph = CsrGraph::new();
345
346        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
347
348        assert_eq!(coordinator.num_tiles(), 0);
349        assert!(coordinator.fits_in_vram()); // Empty graph always fits
350        assert!(coordinator.get_tile_for_node(NodeId(0)).is_none());
351    }
352
353    #[tokio::test]
354    async fn test_paging_with_single_tile() {
355        if !GpuDevice::is_gpu_available().await {
356            eprintln!("⚠️  Skipping test_paging_with_single_tile: GPU not available");
357            return;
358        }
359
360        let device = GpuDevice::new().await.unwrap();
361
362        // Small graph that fits in one tile
363        let mut graph = CsrGraph::new();
364        for i in 0..5 {
365            graph.add_edge(NodeId(i), NodeId(i + 1), 1.0).unwrap();
366        }
367
368        let coordinator = PagingCoordinator::new(&device, &graph).unwrap();
369
370        assert_eq!(coordinator.num_tiles(), 1);
371        let tile = coordinator.get_tile(0).unwrap();
372        assert_eq!(tile.id, 0);
373        assert_eq!(tile.num_nodes, 6);
374    }
375}