trueno_graph/gpu/
paging.rs1use super::cache::TileId;
7use super::memory::GpuMemoryLimits;
8use super::GpuDevice;
9use crate::{CsrGraph, NodeId};
10use anyhow::Result;
11
12#[derive(Debug, Clone)]
14pub struct GraphTile {
15 pub id: TileId,
17
18 pub start_node: usize,
20
21 pub end_node: usize,
23
24 pub num_nodes: usize,
26
27 pub row_offsets: Vec<u32>,
29 pub col_indices: Vec<u32>,
31 pub edge_weights: Vec<f32>,
33}
34
35impl GraphTile {
36 #[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
46pub struct PagingCoordinator {
50 limits: GpuMemoryLimits,
52
53 tiles: Vec<GraphTile>,
55
56 tile_size: usize,
58}
59
60impl PagingCoordinator {
61 #[allow(clippy::cast_possible_truncation)]
67 pub fn new(device: &GpuDevice, graph: &CsrGraph) -> Result<Self> {
68 let limits = GpuMemoryLimits::detect(device)?;
69
70 let (row_offsets, col_indices, edge_weights) = graph.csr_components();
72 let total_nodes = graph.num_nodes();
73
74 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 };
84
85 let tile_size = limits.recommended_tile_size(bytes_per_node);
86
87 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 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 #[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 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 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 #[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 #[must_use]
149 pub fn get_tile(&self, tile_id: TileId) -> Option<&GraphTile> {
150 self.tiles.get(tile_id)
151 }
152
153 #[must_use]
155 pub fn num_tiles(&self) -> usize {
156 self.tiles.len()
157 }
158
159 #[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 #[must_use]
168 pub fn tile_size(&self) -> usize {
169 self.tile_size
170 }
171
172 #[must_use]
174 pub const fn limits(&self) -> &GpuMemoryLimits {
175 &self.limits
176 }
177
178 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 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()); 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 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 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 let tile0 = coordinator.get_tile(0);
263 assert!(tile0.is_some());
264
265 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 let tile_count = coordinator.tiles().count();
288 assert_eq!(tile_count, coordinator.num_tiles());
289
290 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 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()); 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 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}