vyre_primitives/graph/exploded/
validation.rs1use super::encoding::fits;
2use super::layout::IfdsCsrLayout;
3
4pub fn validate_ifds_csr_readback(
11 layout: &IfdsCsrLayout,
12 row_ptr: &[u32],
13 col_idx: &[u32],
14 col_len: u32,
15) -> Result<usize, String> {
16 if row_ptr.len() != layout.row_words {
17 return Err(format!(
18 "Fix: exploded IFDS row_ptr readback expected {} word(s), got {}.",
19 layout.row_words,
20 row_ptr.len()
21 ));
22 }
23 if row_ptr.first().copied() != Some(0) {
24 return Err("Fix: exploded IFDS CSR row_ptr[0] must be 0.".to_string());
25 }
26 if col_len > layout.max_col_count {
27 return Err(format!(
28 "Fix: exploded IFDS GPU reported col_len {col_len} above allocated maximum {}.",
29 layout.max_col_count
30 ));
31 }
32 let live_cols = usize::try_from(col_len).map_err(|_| {
33 format!("Fix: exploded IFDS col_len {col_len} cannot be represented as usize.")
34 })?;
35 if live_cols > col_idx.len() {
36 return Err(format!(
37 "Fix: exploded IFDS col_len {col_len} exceeds col_idx readback words {}.",
38 col_idx.len()
39 ));
40 }
41 for (row, window) in row_ptr.windows(2).enumerate() {
42 let start = window[0];
43 let end = window[1];
44 if start > end {
45 return Err(format!(
46 "Fix: exploded IFDS row_ptr is not monotonic at row {row}: {start} > {end}."
47 ));
48 }
49 if end > col_len {
50 return Err(format!(
51 "Fix: exploded IFDS row {row} ends at {end}, beyond live col_len {col_len}."
52 ));
53 }
54 }
55 let final_row = row_ptr.last().copied().unwrap_or(0);
56 if final_row != col_len {
57 return Err(format!(
58 "Fix: exploded IFDS final row_ptr value {final_row} must equal col_len {col_len}."
59 ));
60 }
61 for (index, &column) in col_idx.iter().take(live_cols).enumerate() {
62 if column >= layout.total_nodes {
63 return Err(format!(
64 "Fix: exploded IFDS col_idx[{index}]={column} is outside total_nodes {}.",
65 layout.total_nodes
66 ));
67 }
68 }
69 Ok(live_cols)
70}
71
72#[must_use]
74pub fn ifds_node_count_checked(
75 num_procs: u32,
76 blocks_per_proc: u32,
77 facts_per_proc: u32,
78) -> Option<u32> {
79 num_procs
80 .checked_mul(blocks_per_proc)?
81 .checked_mul(facts_per_proc)
82}
83
84#[must_use]
86pub fn ifds_node_count_saturating(
87 num_procs: u32,
88 blocks_per_proc: u32,
89 facts_per_proc: u32,
90) -> u32 {
91 num_procs
92 .saturating_mul(blocks_per_proc)
93 .saturating_mul(facts_per_proc)
94}
95
96#[must_use]
98pub fn max_ifds_col_count(
99 intra_count: u32,
100 inter_count: u32,
101 gen_count: u32,
102 facts_per_proc: u32,
103) -> Option<u32> {
104 intra_count
105 .checked_mul(facts_per_proc)
106 .and_then(|v| v.checked_add(intra_count.checked_mul(gen_count)?))
107 .and_then(|v| v.checked_add(inter_count.checked_mul(facts_per_proc)?))
108}
109
110pub fn validate_ifds_csr_layout(
112 num_procs: u32,
113 blocks_per_proc: u32,
114 facts_per_proc: u32,
115 intra_count: u32,
116 inter_count: u32,
117 gen_count: u32,
118) -> Result<IfdsCsrLayout, String> {
119 if num_procs == 0 || blocks_per_proc == 0 || facts_per_proc == 0 {
120 return Err(format!(
121 "Fix: exploded IFDS dimensions must be nonzero, got procs={num_procs}, blocks={blocks_per_proc}, facts={facts_per_proc}."
122 ));
123 }
124 if !fits(
125 num_procs.saturating_sub(1),
126 blocks_per_proc.saturating_sub(1),
127 facts_per_proc.saturating_sub(1),
128 ) {
129 return Err(format!(
130 "Fix: exploded IFDS dimensions exceed packed IFDS limits: procs={num_procs}, blocks={blocks_per_proc}, facts={facts_per_proc}."
131 ));
132 }
133 let slots_per_proc = blocks_per_proc.checked_mul(facts_per_proc).ok_or_else(|| {
134 format!(
135 "Fix: exploded IFDS blocks*facts overflows u32: {blocks_per_proc}*{facts_per_proc}."
136 )
137 })?;
138 let total_nodes = num_procs.checked_mul(slots_per_proc).ok_or_else(|| {
139 format!(
140 "Fix: exploded IFDS procs*blocks*facts overflows u32: {num_procs}*{blocks_per_proc}*{facts_per_proc}."
141 )
142 })?;
143 let row_ptr_count = total_nodes.checked_add(1).ok_or_else(|| {
144 format!(
145 "Fix: exploded IFDS total_nodes={total_nodes} overflows row_ptr count. Shard the IFDS graph before GPU dispatch."
146 )
147 })?;
148 let max_col_count = max_ifds_col_count(intra_count, inter_count, gen_count, facts_per_proc)
149 .ok_or_else(|| "Fix: exploded IFDS maximum column count overflows u32.".to_string())?;
150 Ok(IfdsCsrLayout {
151 empty: false,
152 num_procs,
153 blocks_per_proc,
154 facts_per_proc,
155 intra_count,
156 inter_count,
157 gen_count,
158 kill_count: 0,
159 intra_storage_words: (intra_count as usize).max(1),
160 inter_storage_words: (inter_count as usize).max(1),
161 gen_storage_words: (gen_count as usize).max(1),
162 kill_storage_words: 1,
163 slots_per_proc,
164 total_nodes,
165 row_words: row_ptr_count as usize,
166 row_cursor_words: (total_nodes as usize).max(1),
167 killed_words: (total_nodes as usize).max(1),
168 max_col_count,
169 col_buffer_words: (max_col_count as usize).max(1),
170 })
171}
172
173fn checked_rule_count(kind: &str, len: usize) -> Result<u32, String> {
174 u32::try_from(len)
175 .map_err(|_| format!("Fix: exploded IFDS {kind} count {len} exceeds u32 index space."))
176}
177
178pub fn validate_ifds_csr_inputs(
183 num_procs: u32,
184 blocks_per_proc: u32,
185 facts_per_proc: u32,
186 intra_edges: &[(u32, u32, u32)],
187 inter_edges: &[(u32, u32, u32, u32)],
188 flow_gen: &[(u32, u32, u32)],
189 flow_kill: &[(u32, u32, u32)],
190) -> Result<IfdsCsrLayout, String> {
191 let intra_count = checked_rule_count("intra edge", intra_edges.len())?;
192 let inter_count = checked_rule_count("inter edge", inter_edges.len())?;
193 let gen_count = checked_rule_count("GEN", flow_gen.len())?;
194 let kill_count = checked_rule_count("KILL", flow_kill.len())?;
195
196 if num_procs == 0 || blocks_per_proc == 0 || facts_per_proc == 0 {
197 if intra_count == 0 && inter_count == 0 && gen_count == 0 && kill_count == 0 {
198 return Ok(IfdsCsrLayout {
199 empty: true,
200 num_procs,
201 blocks_per_proc,
202 facts_per_proc,
203 intra_count,
204 inter_count,
205 gen_count,
206 kill_count,
207 intra_storage_words: 1,
208 inter_storage_words: 1,
209 gen_storage_words: 1,
210 kill_storage_words: 1,
211 slots_per_proc: 0,
212 total_nodes: 0,
213 row_words: 1,
214 row_cursor_words: 1,
215 killed_words: 1,
216 max_col_count: 0,
217 col_buffer_words: 1,
218 });
219 }
220 return Err(format!(
221 "Fix: exploded IFDS empty dimensions cannot carry rules, got intra={intra_count}, inter={inter_count}, gen={gen_count}, kill={kill_count}."
222 ));
223 }
224
225 let mut layout = validate_ifds_csr_layout(
226 num_procs,
227 blocks_per_proc,
228 facts_per_proc,
229 intra_count,
230 inter_count,
231 gen_count,
232 )?;
233 layout.kill_count = kill_count;
234 layout.kill_storage_words = flow_kill.len().max(1);
235 Ok(layout)
236}