1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
//! Reverse CSR frontier expansion over an in-place accumulator bitset.
use std::sync::Arc;
use vyre_foundation::ir::model::expr::Ident;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
use crate::graph::csr_forward_traverse::bitset_words;
use crate::graph::program_graph::{
ProgramGraphShape, BINDING_PRIMITIVE_START, NAME_EDGE_KIND_MASK, NAME_EDGE_OFFSETS,
NAME_EDGE_TARGETS,
};
/// Canonical op id.
pub const OP_ID: &str = "vyre-primitives::graph::csr_backward_or_changed";
/// Source-lane workgroup for reverse in-place CSR expansion.
pub const CSR_BACKWARD_OR_CHANGED_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
/// Dispatch grid for a node-parallel reverse in-place CSR expansion pass.
#[must_use]
pub const fn csr_backward_or_changed_parallel_grid(node_count: u32) -> [u32; 3] {
[
ceil_div_u32(
at_least_one(node_count),
CSR_BACKWARD_OR_CHANGED_WORKGROUP_SIZE[0],
),
1,
1,
]
}
const fn at_least_one(value: u32) -> u32 {
if value == 0 {
1
} else {
value
}
}
const fn ceil_div_u32(value: u32, divisor: u32) -> u32 {
((value - 1) / divisor) + 1
}
/// Parallel in-place reverse expansion program for resident fixed-point drivers.
#[must_use]
pub fn csr_backward_or_changed_parallel(
shape: ProgramGraphShape,
frontier_out: &str,
changed: &str,
edge_kind_mask: u32,
) -> Program {
let src = Expr::InvocationId { axis: 0 };
let words = bitset_words(shape.node_count);
let body = vec![
Node::let_bind("edge_start", Expr::load(NAME_EDGE_OFFSETS, src.clone())),
Node::let_bind(
"edge_end",
Expr::load(NAME_EDGE_OFFSETS, Expr::add(src.clone(), Expr::u32(1))),
),
Node::let_bind("hit", Expr::u32(0)),
Node::loop_for(
"e",
Expr::var("edge_start"),
Expr::var("edge_end"),
vec![Node::if_then(
Expr::eq(Expr::var("hit"), Expr::u32(0)),
vec![
Node::let_bind("kind_mask", Expr::load(NAME_EDGE_KIND_MASK, Expr::var("e"))),
Node::if_then(
Expr::ne(
Expr::bitand(Expr::var("kind_mask"), Expr::u32(edge_kind_mask)),
Expr::u32(0),
),
vec![
Node::let_bind("dst", Expr::load(NAME_EDGE_TARGETS, Expr::var("e"))),
Node::if_then(
Expr::lt(Expr::var("dst"), Expr::u32(shape.node_count)),
vec![
Node::let_bind(
"dst_word",
Expr::load(
frontier_out,
Expr::shr(Expr::var("dst"), Expr::u32(5)),
),
),
Node::let_bind(
"dst_bit",
Expr::shl(
Expr::u32(1),
Expr::bitand(Expr::var("dst"), Expr::u32(31)),
),
),
Node::if_then(
Expr::ne(
Expr::bitand(
Expr::var("dst_word"),
Expr::var("dst_bit"),
),
Expr::u32(0),
),
vec![Node::assign("hit", Expr::u32(1))],
),
],
),
],
),
],
)],
),
Node::if_then(
Expr::eq(Expr::var("hit"), Expr::u32(1)),
vec![
Node::let_bind("src_word_idx", Expr::shr(src.clone(), Expr::u32(5))),
Node::let_bind(
"src_bit",
Expr::shl(Expr::u32(1), Expr::bitand(src.clone(), Expr::u32(31))),
),
Node::let_bind(
"old",
Expr::atomic_or(
frontier_out,
Expr::var("src_word_idx"),
Expr::var("src_bit"),
),
),
Node::if_then(
Expr::eq(
Expr::bitand(Expr::var("old"), Expr::var("src_bit")),
Expr::u32(0),
),
vec![Node::let_bind(
"_changed",
Expr::atomic_or(changed, Expr::u32(0), Expr::u32(1)),
)],
),
],
),
];
let mut buffers = shape.read_only_buffers();
buffers.push(
BufferDecl::storage(
frontier_out,
BINDING_PRIMITIVE_START,
BufferAccess::ReadWrite,
DataType::U32,
)
.with_count(words),
);
buffers.push(
BufferDecl::storage(
changed,
BINDING_PRIMITIVE_START + 1,
BufferAccess::ReadWrite,
DataType::U32,
)
.with_count(1),
);
Program::wrapped(
buffers,
CSR_BACKWARD_OR_CHANGED_WORKGROUP_SIZE,
vec![Node::Region {
generator: Ident::from(OP_ID),
source_region: None,
body: Arc::new(vec![Node::if_then(
Expr::lt(src.clone(), Expr::u32(shape.node_count)),
body,
)]),
}],
)
}
/// CPU reference for one reverse-or-changed expansion pass (snapshot semantics): a
/// source node is added to the frontier when any of its out-neighbors reached by an edge
/// whose kind passes `edge_kind_mask` is present in the INPUT frontier. Returns the
/// updated frontier (input bits are monotonically retained) and `1` iff a new bit was set.
///
/// This reads the pre-pass frontier for the neighbor test, so it is the deterministic
/// single-pass answer. The GPU program [`csr_backward_or_changed_parallel`] reads the
/// live in-place accumulator, so for a multi-hop backward chain a single GPU pass can set
/// MORE bits than one snapshot pass, but both converge to the identical fixed point (the
/// op's contract; proven by the generated backward oracle matrix + fixpoint idempotence).
#[cfg(any(test, feature = "cpu-parity"))]
#[must_use]
pub fn cpu_ref(
node_count: u32,
offsets: &[u32],
targets: &[u32],
masks: &[u32],
frontier: &[u32],
edge_kind_mask: u32,
) -> (Vec<u32>, u32) {
let words = bitset_words(node_count).max(1) as usize;
let mut out = frontier.to_vec();
out.resize(words, 0);
let is_set = |bits: &[u32], node: u32| -> bool {
bits.get((node >> 5) as usize)
.is_some_and(|word| word & (1u32 << (node & 31)) != 0)
};
let mut changed = 0u32;
for src in 0..node_count {
let start = offsets[src as usize] as usize;
let end = offsets[src as usize + 1] as usize;
// Match the program's early-out: stop at the first present, kind-passing out-neighbor.
let mut hit = false;
for edge in start..end {
if masks[edge] & edge_kind_mask != 0 {
let dst = targets[edge];
if dst < node_count && is_set(frontier, dst) {
hit = true;
break;
}
}
}
if hit && !is_set(&out, src) {
out[(src >> 5) as usize] |= 1u32 << (src & 31);
changed = 1;
}
}
(out, changed)
}
/// Iterate [`cpu_ref`] to a fixed point (at most `max_iters` passes): the full set of
/// nodes that can reach an initial-frontier node along kind-passing edges. Returns the
/// converged frontier and `1` iff any pass set a new bit.
#[cfg(any(test, feature = "cpu-parity"))]
#[must_use]
pub fn cpu_ref_closure(
node_count: u32,
offsets: &[u32],
targets: &[u32],
masks: &[u32],
frontier: &[u32],
edge_kind_mask: u32,
max_iters: u32,
) -> (Vec<u32>, u32) {
let words = bitset_words(node_count).max(1) as usize;
let mut out = frontier.to_vec();
out.resize(words, 0);
let mut any_changed = 0u32;
for _ in 0..max_iters {
let (next, changed) = cpu_ref(node_count, offsets, targets, masks, &out, edge_kind_mask);
out = next;
if changed == 0 {
break;
}
any_changed = 1;
}
(out, any_changed)
}
#[cfg(feature = "inventory-registry")]
inventory::submit! {
crate::harness::OpEntry::new(
OP_ID,
|| csr_backward_or_changed_parallel(ProgramGraphShape::new(4, 4), "frontier", "changed", 1),
Some(|| {
// Graph: 0→1, 0→2, 1→3, 2→3. Reverse-or-changed adds a source node when any
// of its out-neighbors is already in the frontier. Seed = {1, 2}: only node 0
// (out-edges to 1 and 2) sees a set out-neighbor, so it is the sole addition.
//
// Deliberately a ONE-HOP fixture. Node 0's read of its out-neighbor bits (1, 2)
// is stable because NO lane writes bits 1 or 2 this pass (the only write is node
// 0's own bit 0, which nobody reads), so the single pass is race-clean and
// order-independent, as the lane-reversal race net and the grid-overfire
// output-invariance net require. A MULTI-HOP backward chain (e.g. seed {3},
// where node 1 is added then node 0 sees 1's just-set bit) is legitimately
// order-dependent in a single node-parallel pass; that only the CONVERGED
// fixed-point is order-independent is the op's real contract, covered by the
// dedicated iterate-to-fixpoint oracle test, not assertable as a single-pass
// fixture here.
let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
vec![vec![
to_bytes(&[0, 0, 0, 0]), // pg_nodes
to_bytes(&[0, 2, 3, 4, 4]), // pg_edge_offsets
to_bytes(&[1, 2, 3, 3]), // pg_edge_targets
to_bytes(&[1, 1, 1, 1]), // pg_edge_kind_mask
to_bytes(&[0, 0, 0, 0]), // pg_node_tags
to_bytes(&[0b0110]), // frontier seed = {1, 2}
to_bytes(&[0]), // changed
]]
}),
Some(|| {
let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
// Node 0 added (its out-neighbors 1, 2 are set); frontier = {0, 1, 2}, changed = 1.
vec![vec![to_bytes(&[0b0111]), to_bytes(&[1])]]
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposes_frontier_and_changed_bindings() {
let program = csr_backward_or_changed_parallel(
ProgramGraphShape::new(4, 3),
"frontier",
"changed",
u32::MAX,
);
let names = program
.buffers()
.iter()
.map(|buffer| buffer.name())
.collect::<Vec<_>>();
assert!(names.contains(&"frontier"));
assert!(names.contains(&"changed"));
assert_eq!(
program.workgroup_size(),
CSR_BACKWARD_OR_CHANGED_WORKGROUP_SIZE
);
}
#[test]
fn parallel_grid_packs_source_lanes_into_blocks() {
assert_eq!(csr_backward_or_changed_parallel_grid(0), [1, 1, 1]);
assert_eq!(csr_backward_or_changed_parallel_grid(1), [1, 1, 1]);
assert_eq!(csr_backward_or_changed_parallel_grid(256), [1, 1, 1]);
assert_eq!(csr_backward_or_changed_parallel_grid(257), [2, 1, 1]);
assert_eq!(csr_backward_or_changed_parallel_grid(513), [3, 1, 1]);
}
}