1use std::{cmp::Reverse, collections::BinaryHeap};
4
5use crate::backend::accounting::{
6 checked_add_u64_count as checked_add, checked_mul_u64_count as checked_mul,
7 CudaArithmeticOverflow,
8};
9use vyre_driver::megakernel_execution::MegakernelGraphShape;
10use vyre_self_substrate::device_resident_token_fact_graph::DeviceResidentTokenFactGraph;
11
12pub const CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS: usize = 16;
14
15pub const CUDA_TOKEN_FACT_DEGREE_PROFILE_RANKS: [u64; CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS] = [
17 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096, 8_192, 16_384, 32_768,
18];
19
20const CUDA_TOKEN_FACT_DEGREE_PROFILE_MAX_RANK: usize = 32_768;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct CudaTokenFactGraphLayout {
25 pub graph_shape: MegakernelGraphShape,
27 pub max_out_degree: u64,
29 pub top_out_degree_prefix_sums: [u64; CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS],
31 pub node_record_bytes: u64,
33 pub edge_record_bytes: u64,
35 pub node_bytes: u64,
37 pub edge_bytes: u64,
39 pub payload_bytes: u64,
41 pub resident_bytes: u64,
43}
44
45impl CudaTokenFactGraphLayout {
46 #[must_use]
50 pub const fn from_aggregate_fields(
51 graph_shape: MegakernelGraphShape,
52 node_record_bytes: u64,
53 edge_record_bytes: u64,
54 node_bytes: u64,
55 edge_bytes: u64,
56 payload_bytes: u64,
57 resident_bytes: u64,
58 ) -> Self {
59 Self {
60 graph_shape,
61 max_out_degree: graph_shape.edge_count,
62 top_out_degree_prefix_sums: [graph_shape.edge_count;
63 CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS],
64 node_record_bytes,
65 edge_record_bytes,
66 node_bytes,
67 edge_bytes,
68 payload_bytes,
69 resident_bytes,
70 }
71 }
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
76pub enum CudaTokenFactGraphLayoutError {
77 ZeroRecordWidth {
79 field: &'static str,
81 },
82 InvalidCsrShape {
84 field: &'static str,
86 },
87 ByteCountOverflow {
89 field: &'static str,
91 },
92}
93
94impl std::fmt::Display for CudaTokenFactGraphLayoutError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::ZeroRecordWidth { field } => write!(
98 f,
99 "CUDA token/fact graph adapter received zero {field}. Fix: pass the concrete resident ABI record width."
100 ),
101 Self::InvalidCsrShape { field } => write!(
102 f,
103 "CUDA token/fact graph adapter received invalid CSR {field}. Fix: rebuild the token/fact graph through the canonical resident graph planner."
104 ),
105 Self::ByteCountOverflow { field } => write!(
106 f,
107 "CUDA token/fact graph adapter overflowed while computing {field}. Fix: shard the token/fact graph before resident upload."
108 ),
109 }
110 }
111}
112
113impl std::error::Error for CudaTokenFactGraphLayoutError {}
114
115impl CudaArithmeticOverflow for CudaTokenFactGraphLayoutError {
116 fn arithmetic_overflow(field: &'static str) -> Self {
117 Self::ByteCountOverflow { field }
118 }
119}
120
121pub fn adapt_token_fact_graph_to_cuda_layout(
123 graph: &DeviceResidentTokenFactGraph,
124 node_record_bytes: u64,
125 edge_record_bytes: u64,
126) -> Result<CudaTokenFactGraphLayout, CudaTokenFactGraphLayoutError> {
127 if node_record_bytes == 0 {
128 return Err(CudaTokenFactGraphLayoutError::ZeroRecordWidth {
129 field: "node_record_bytes",
130 });
131 }
132 if edge_record_bytes == 0 {
133 return Err(CudaTokenFactGraphLayoutError::ZeroRecordWidth {
134 field: "edge_record_bytes",
135 });
136 }
137 let node_count = u64::try_from(graph.node_ids.len()).map_err(|_| {
138 CudaTokenFactGraphLayoutError::ByteCountOverflow {
139 field: "node count",
140 }
141 })?;
142 let edge_count = u64::try_from(graph.column_indices.len()).map_err(|_| {
143 CudaTokenFactGraphLayoutError::ByteCountOverflow {
144 field: "edge count",
145 }
146 })?;
147 let (max_out_degree, top_out_degree_prefix_sums) = csr_out_degree_profile(graph, edge_count)?;
148 let node_bytes = checked_mul(node_count, node_record_bytes, "node bytes")?;
149 let edge_bytes = checked_mul(edge_count, edge_record_bytes, "edge bytes")?;
150 let resident_without_payload = checked_add(node_bytes, edge_bytes, "node plus edge bytes")?;
151 let resident_bytes = checked_add(
152 resident_without_payload,
153 graph.payload_bytes,
154 "resident bytes",
155 )?;
156
157 Ok(CudaTokenFactGraphLayout {
158 graph_shape: MegakernelGraphShape {
159 node_count,
160 edge_count,
161 },
162 max_out_degree,
163 top_out_degree_prefix_sums,
164 node_record_bytes,
165 edge_record_bytes,
166 node_bytes,
167 edge_bytes,
168 payload_bytes: graph.payload_bytes,
169 resident_bytes,
170 })
171}
172
173fn csr_out_degree_profile(
174 graph: &DeviceResidentTokenFactGraph,
175 edge_count: u64,
176) -> Result<(u64, [u64; CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS]), CudaTokenFactGraphLayoutError> {
177 let expected_row_offsets = graph.node_ids.len().checked_add(1).ok_or(
178 CudaTokenFactGraphLayoutError::ByteCountOverflow {
179 field: "row offset count",
180 },
181 )?;
182 if graph.row_offsets.len() != expected_row_offsets {
183 return Err(CudaTokenFactGraphLayoutError::InvalidCsrShape {
184 field: "row_offsets length",
185 });
186 }
187 let declared_edges = u64::from(*graph.row_offsets.last().ok_or(
188 CudaTokenFactGraphLayoutError::InvalidCsrShape {
189 field: "row_offsets terminator",
190 },
191 )?);
192 if declared_edges != edge_count {
193 return Err(CudaTokenFactGraphLayoutError::InvalidCsrShape {
194 field: "row_offsets edge count",
195 });
196 }
197 let profile_capacity = CUDA_TOKEN_FACT_DEGREE_PROFILE_MAX_RANK.min(graph.node_ids.len());
198 let mut top_degrees = BinaryHeap::with_capacity(profile_capacity);
199 let mut max_out_degree = 0_u64;
200 for row in graph.row_offsets.windows(2) {
201 let start = row[0];
202 let end = row[1];
203 if end < start {
204 return Err(CudaTokenFactGraphLayoutError::InvalidCsrShape {
205 field: "row_offsets ordering",
206 });
207 }
208 let degree = u64::from(end - start);
209 max_out_degree = max_out_degree.max(degree);
210 if top_degrees.len() < profile_capacity {
211 top_degrees.push(Reverse(degree));
212 } else if let Some(mut min_degree) = top_degrees.peek_mut() {
213 if degree > min_degree.0 {
214 *min_degree = Reverse(degree);
215 }
216 }
217 }
218 let mut degrees = top_degrees
219 .into_iter()
220 .map(|Reverse(degree)| degree)
221 .collect::<Vec<_>>();
222 degrees.sort_unstable_by(|lhs, rhs| rhs.cmp(lhs));
223
224 let mut prefix_sum = 0_u64;
225 let mut prefix_sums = [0_u64; CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS];
226 let mut bucket = 0_usize;
227 for (index, degree) in degrees.into_iter().enumerate() {
228 prefix_sum = checked_add(prefix_sum, degree, "top out-degree prefix sum")?;
229 let rank = u64::try_from(index + 1).map_err(|_| {
230 CudaTokenFactGraphLayoutError::ByteCountOverflow {
231 field: "out-degree profile rank",
232 }
233 })?;
234 while bucket < CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS
235 && rank >= CUDA_TOKEN_FACT_DEGREE_PROFILE_RANKS[bucket]
236 {
237 prefix_sums[bucket] = prefix_sum;
238 bucket += 1;
239 }
240 }
241 while bucket < CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS {
242 prefix_sums[bucket] = prefix_sum;
243 bucket += 1;
244 }
245 Ok((max_out_degree, prefix_sums))
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use vyre_driver::megakernel_execution::{
252 plan_megakernel_memory_budget, MegakernelExecutionTopology,
253 };
254 use vyre_self_substrate::device_resident_token_fact_graph::{
255 plan_device_resident_token_fact_graph, TokenFactEdge, TokenFactEdgeKind, TokenFactNode,
256 TokenFactNodeKind,
257 };
258
259 #[test]
260 fn adapter_accounts_for_cuda_resident_token_fact_layout() {
261 let graph = plan_device_resident_token_fact_graph(
262 &[
263 node(1, TokenFactNodeKind::Token, 0, 8),
264 node(2, TokenFactNodeKind::Semantic, 8, 8),
265 node(3, TokenFactNodeKind::Fact, 16, 8),
266 ],
267 &[
268 edge(1, 2, TokenFactEdgeKind::SemanticFact),
269 edge(2, 3, TokenFactEdgeKind::FactDependency),
270 ],
271 24,
272 )
273 .expect("Fix: token/fact graph should pack");
274
275 let cuda = adapt_token_fact_graph_to_cuda_layout(&graph, 32, 16)
276 .expect("Fix: token/fact graph should adapt to CUDA layout");
277
278 assert_eq!(cuda.graph_shape.node_count, 3);
279 assert_eq!(cuda.graph_shape.edge_count, 2);
280 assert_eq!(cuda.max_out_degree, 1);
281 assert_eq!(cuda.top_out_degree_prefix_sums[0], 1);
282 assert_eq!(cuda.top_out_degree_prefix_sums[1], 2);
283 assert_eq!(cuda.top_out_degree_prefix_sums[15], 2);
284 assert_eq!(cuda.node_bytes, 96);
285 assert_eq!(cuda.edge_bytes, 32);
286 assert_eq!(cuda.resident_bytes, 152);
287 let memory = plan_megakernel_memory_budget(
288 MegakernelExecutionTopology::SparseFrontier,
289 cuda.graph_shape,
290 cuda.node_record_bytes,
291 cuda.edge_record_bytes,
292 64,
293 cuda.payload_bytes,
294 16,
295 512,
296 )
297 .expect("Fix: adapted token/fact graph should feed CUDA memory planning");
298 assert_eq!(memory.graph_bytes, 128);
299 }
300
301 #[test]
302 fn adapter_exports_max_out_degree_for_hub_heavy_queue_planning() {
303 let graph = plan_device_resident_token_fact_graph(
304 &[
305 node(1, TokenFactNodeKind::Fact, 0, 4),
306 node(2, TokenFactNodeKind::Fact, 4, 4),
307 node(3, TokenFactNodeKind::Fact, 8, 4),
308 node(4, TokenFactNodeKind::Fact, 12, 4),
309 ],
310 &[
311 edge(1, 2, TokenFactEdgeKind::FactDependency),
312 edge(1, 3, TokenFactEdgeKind::FactDependency),
313 edge(1, 4, TokenFactEdgeKind::FactDependency),
314 edge(2, 3, TokenFactEdgeKind::FactDependency),
315 ],
316 16,
317 )
318 .expect("Fix: hub-heavy token/fact graph should pack");
319
320 let cuda = adapt_token_fact_graph_to_cuda_layout(&graph, 32, 16)
321 .expect("Fix: hub-heavy token/fact graph should adapt to CUDA layout");
322
323 assert_eq!(cuda.graph_shape.edge_count, 4);
324 assert_eq!(cuda.max_out_degree, 3);
325 assert_eq!(cuda.top_out_degree_prefix_sums[0], 3);
326 assert_eq!(cuda.top_out_degree_prefix_sums[1], 4);
327 assert_eq!(cuda.top_out_degree_prefix_sums[2], 4);
328 }
329
330 #[test]
331 fn generated_adapter_profiles_top_out_degree_prefixes() {
332 let mut state = 0x5eec_c0de_f00d_7715_u64;
333 for case_index in 0..4096_u64 {
334 let node_count = 1 + (next_u64(&mut state) % 64) as u32;
335 let nodes = (0..node_count)
336 .map(|index| node(index + 1, TokenFactNodeKind::Fact, u64::from(index) * 4, 4))
337 .collect::<Vec<_>>();
338 let mut edges = Vec::new();
339 if case_index % 4 == 0 {
340 for to in 2..=node_count {
341 edges.push(edge(1, to, TokenFactEdgeKind::FactDependency));
342 }
343 }
344 let attempts = next_u64(&mut state) % (u64::from(node_count) * 5 + 1);
345 for _ in 0..attempts {
346 let from = 1 + (next_u64(&mut state) % u64::from(node_count)) as u32;
347 let to = 1 + (next_u64(&mut state) % u64::from(node_count)) as u32;
348 let kind = if next_u64(&mut state) & 1 == 0 {
349 TokenFactEdgeKind::FactDependency
350 } else {
351 TokenFactEdgeKind::DiagnosticProvenance
352 };
353 edges.push(edge(from, to, kind));
354 }
355 let graph =
356 plan_device_resident_token_fact_graph(&nodes, &edges, u64::from(node_count) * 4)
357 .expect("Fix: generated token/fact graph should pack");
358 let cuda = adapt_token_fact_graph_to_cuda_layout(&graph, 32, 16)
359 .expect("Fix: generated token/fact graph should adapt");
360 let mut degrees = graph
361 .row_offsets
362 .windows(2)
363 .map(|row| u64::from(row[1] - row[0]))
364 .collect::<Vec<_>>();
365 degrees.sort_unstable_by(|lhs, rhs| rhs.cmp(lhs));
366
367 assert_eq!(
368 cuda.max_out_degree,
369 degrees.first().copied().unwrap_or(0),
370 "case {case_index}"
371 );
372 for (bucket, rank) in CUDA_TOKEN_FACT_DEGREE_PROFILE_RANKS.iter().enumerate() {
373 let expected = degrees
374 .iter()
375 .take((*rank as usize).min(degrees.len()))
376 .copied()
377 .sum::<u64>();
378 assert_eq!(
379 cuda.top_out_degree_prefix_sums[bucket], expected,
380 "case {case_index} bucket {bucket}"
381 );
382 }
383 }
384 }
385
386 #[test]
387 fn adapter_profiles_large_graph_with_bounded_top_rank_storage() {
388 let node_count = 32_770_u32;
389 let nodes = (0..node_count)
390 .map(|index| node(index + 1, TokenFactNodeKind::Fact, u64::from(index) * 4, 4))
391 .collect::<Vec<_>>();
392 let mut edges = Vec::with_capacity(32_858);
393 for to in 2..=51 {
394 edges.push(edge(1, to, TokenFactEdgeKind::FactDependency));
395 }
396 for to in 3..=42 {
397 edges.push(edge(2, to, TokenFactEdgeKind::FactDependency));
398 }
399 for from in 3..=node_count {
400 edges.push(edge(from, 1, TokenFactEdgeKind::FactDependency));
401 }
402 let graph =
403 plan_device_resident_token_fact_graph(&nodes, &edges, u64::from(node_count) * 4)
404 .expect("Fix: large skewed token/fact graph should pack");
405
406 let cuda = adapt_token_fact_graph_to_cuda_layout(&graph, 32, 16)
407 .expect("Fix: large skewed token/fact graph should adapt");
408
409 assert_eq!(cuda.graph_shape.node_count, u64::from(node_count));
410 assert_eq!(cuda.graph_shape.edge_count, 32_858);
411 assert_eq!(cuda.max_out_degree, 50);
412 assert_eq!(cuda.top_out_degree_prefix_sums[0], 50);
413 assert_eq!(cuda.top_out_degree_prefix_sums[1], 90);
414 assert_eq!(cuda.top_out_degree_prefix_sums[2], 92);
415 assert_eq!(cuda.top_out_degree_prefix_sums[15], 32_856);
416 }
417
418 #[test]
419 fn aggregate_layout_constructor_preserves_legacy_safe_edge_bound() {
420 let layout = CudaTokenFactGraphLayout::from_aggregate_fields(
421 MegakernelGraphShape {
422 node_count: 4,
423 edge_count: 9,
424 },
425 32,
426 16,
427 128,
428 144,
429 64,
430 336,
431 );
432
433 assert_eq!(layout.max_out_degree, 9);
434 assert_eq!(
435 layout.top_out_degree_prefix_sums,
436 [9; CUDA_TOKEN_FACT_DEGREE_PROFILE_BUCKETS]
437 );
438 assert_eq!(layout.resident_bytes, 336);
439 }
440
441 #[test]
442 fn adapter_rejects_missing_abi_widths() {
443 let graph = plan_device_resident_token_fact_graph(&[], &[], 0)
444 .expect("Fix: empty graph still has a valid resident layout");
445
446 assert_eq!(
447 adapt_token_fact_graph_to_cuda_layout(&graph, 0, 8)
448 .expect_err("zero node record width should fail"),
449 CudaTokenFactGraphLayoutError::ZeroRecordWidth {
450 field: "node_record_bytes",
451 }
452 );
453 assert_eq!(
454 adapt_token_fact_graph_to_cuda_layout(&graph, 8, 0)
455 .expect_err("zero edge record width should fail"),
456 CudaTokenFactGraphLayoutError::ZeroRecordWidth {
457 field: "edge_record_bytes",
458 }
459 );
460 }
461
462 #[test]
463 fn adapter_rejects_public_graphs_with_invalid_csr_rows() {
464 let mut graph = plan_device_resident_token_fact_graph(
465 &[node(1, TokenFactNodeKind::Fact, 0, 4)],
466 &[],
467 4,
468 )
469 .expect("Fix: token/fact graph should pack before adversarial mutation");
470 graph.row_offsets[1] = 1;
471
472 assert_eq!(
473 adapt_token_fact_graph_to_cuda_layout(&graph, 32, 16)
474 .expect_err("invalid CSR row offsets should fail before CUDA planning"),
475 CudaTokenFactGraphLayoutError::InvalidCsrShape {
476 field: "row_offsets edge count",
477 }
478 );
479 }
480
481 fn node(
482 id: u32,
483 kind: TokenFactNodeKind,
484 payload_offset: u64,
485 payload_bytes: u64,
486 ) -> TokenFactNode {
487 TokenFactNode {
488 id,
489 kind,
490 payload_offset,
491 payload_bytes,
492 }
493 }
494
495 fn edge(from: u32, to: u32, kind: TokenFactEdgeKind) -> TokenFactEdge {
496 TokenFactEdge { from, to, kind }
497 }
498
499 fn next_u64(state: &mut u64) -> u64 {
500 *state = state
501 .wrapping_mul(6_364_136_223_846_793_005)
502 .wrapping_add(1_442_695_040_888_963_407);
503 *state
504 }
505}