Skip to main content

vyre_primitives/graph/exploded/
validation.rs

1use super::encoding::fits;
2use super::layout::IfdsCsrLayout;
3
4/// Validate CSR data returned by an exploded IFDS backend.
5///
6/// # Errors
7///
8/// Returns an actionable diagnostic when row pointers, live column length, or
9/// live column indices do not satisfy the primitive CSR contract.
10pub 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/// Checked exploded-supergraph node count.
73#[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/// Saturating exploded-supergraph node count for capacity planning UIs.
85#[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/// Maximum column count needed by the deterministic IFDS CSR builder.
97#[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
110/// Validate dimensions/counts and return the exact dispatch buffer layout.
111pub 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
178/// Validate the full IFDS CSR dispatch contract from caller-owned rule slices.
179///
180/// Returns the exact primitive dispatch layout so consumers do not narrow rule
181/// counts or decide padded input-buffer widths locally.
182pub 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}