use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
use crate::fixpoint::persistent_fixpoint::{
persistent_fixpoint, persistent_fixpoint_grid, PERSISTENT_FIXPOINT_WORKGROUP_SIZE,
};
pub const OP_ID: &str = "vyre-primitives::math::bellman_shortest_path";
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn bellman_shortest_path(
src: &str,
dst: &str,
weight: &str,
dist: &str,
next_dist: &str,
changed: &str,
n_nodes: u32,
n_edges: u32,
max_iterations: u32,
) -> Program {
if n_nodes == 0 {
return crate::invalid_output_program(
OP_ID,
dist,
DataType::U32,
format!("Fix: bellman_shortest_path requires n_nodes > 0, got {n_nodes}."),
);
}
if max_iterations == 0 {
return crate::invalid_output_program(
OP_ID,
dist,
DataType::U32,
format!(
"Fix: bellman_shortest_path requires max_iterations > 0, got {max_iterations}."
),
);
}
let transfer_body = bellman_transfer_body(src, dst, weight, dist, next_dist, n_nodes, n_edges);
let dispatch_elements = n_nodes.max(n_edges);
let needs_grid_sync = dispatch_elements > PERSISTENT_FIXPOINT_WORKGROUP_SIZE[0];
let inner = if needs_grid_sync {
persistent_fixpoint_grid(
transfer_body,
dist,
next_dist,
changed,
n_nodes,
max_iterations,
)
} else {
persistent_fixpoint(
transfer_body,
dist,
next_dist,
changed,
n_nodes,
max_iterations,
)
};
let changed_words = if needs_grid_sync {
max_iterations.max(1)
} else {
1
};
bellman_wrap(
&inner,
src,
dst,
weight,
dist,
next_dist,
changed,
n_nodes,
n_edges,
changed_words,
)
}
fn bellman_transfer_body(
src: &str,
dst: &str,
weight: &str,
dist: &str,
next_dist: &str,
n_nodes: u32,
n_edges: u32,
) -> Vec<Node> {
let t = Expr::InvocationId { axis: 0 };
vec![Node::if_then(
Expr::lt(t.clone(), Expr::u32(n_edges)),
vec![
Node::let_bind("u", Expr::load(src, t.clone())),
Node::let_bind("v", Expr::load(dst, t.clone())),
Node::let_bind("w", Expr::load(weight, t.clone())),
Node::if_then(
Expr::and(
Expr::lt(Expr::var("u"), Expr::u32(n_nodes)),
Expr::lt(Expr::var("v"), Expr::u32(n_nodes)),
),
vec![
Node::let_bind("du", Expr::load(dist, Expr::var("u"))),
Node::if_then(
Expr::ne(Expr::var("du"), Expr::u32(u32::MAX)),
vec![
Node::let_bind(
"alt",
Expr::select(
Expr::gt(
Expr::var("w"),
Expr::sub(Expr::u32(u32::MAX), Expr::var("du")),
),
Expr::u32(u32::MAX),
Expr::add(Expr::var("du"), Expr::var("w")),
),
),
Node::let_bind(
"_relax",
Expr::atomic_min(next_dist, Expr::var("v"), Expr::var("alt")),
),
],
),
],
),
],
)]
}
#[allow(clippy::too_many_arguments)]
fn bellman_wrap(
inner: &Program,
src: &str,
dst: &str,
weight: &str,
dist: &str,
next_dist: &str,
changed: &str,
n_nodes: u32,
n_edges: u32,
changed_words: u32,
) -> Program {
super::wrap_fixpoint_program(
OP_ID,
inner,
vec![
BufferDecl::storage(dist, 0, BufferAccess::ReadWrite, DataType::U32)
.with_count(n_nodes),
BufferDecl::storage(next_dist, 1, BufferAccess::ReadWrite, DataType::U32)
.with_count(n_nodes),
BufferDecl::storage(changed, 2, BufferAccess::ReadWrite, DataType::U32)
.with_count(changed_words),
BufferDecl::storage(src, 3, BufferAccess::ReadOnly, DataType::U32).with_count(n_edges),
BufferDecl::storage(dst, 4, BufferAccess::ReadOnly, DataType::U32).with_count(n_edges),
BufferDecl::storage(weight, 5, BufferAccess::ReadOnly, DataType::U32)
.with_count(n_edges),
],
)
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
fn bellman_single_word_harness(
src: &str,
dst: &str,
weight: &str,
dist: &str,
next_dist: &str,
changed: &str,
n_nodes: u32,
n_edges: u32,
max_iterations: u32,
) -> Program {
let transfer_body = bellman_transfer_body(src, dst, weight, dist, next_dist, n_nodes, n_edges);
let inner = persistent_fixpoint(
transfer_body,
dist,
next_dist,
changed,
n_nodes,
max_iterations,
);
bellman_wrap(
&inner, src, dst, weight, dist, next_dist, changed, n_nodes, n_edges, 1,
)
}
#[cfg(any(test, feature = "cpu-parity"))]
#[must_use]
pub fn cpu_ref(
src: &[u32],
dst: &[u32],
weight: &[u32],
dist: &[u32],
n_nodes: u32,
max_iterations: u32,
) -> (Vec<u32>, u32) {
let mut current = Vec::new();
let mut next = Vec::new();
let iters = cpu_ref_into(
src,
dst,
weight,
dist,
n_nodes,
max_iterations,
&mut current,
&mut next,
);
(current, iters)
}
#[cfg(any(test, feature = "cpu-parity"))]
#[allow(clippy::too_many_arguments)]
pub fn cpu_ref_into(
src: &[u32],
dst: &[u32],
weight: &[u32],
dist: &[u32],
n_nodes: u32,
max_iterations: u32,
current: &mut Vec<u32>,
next: &mut Vec<u32>,
) -> u32 {
let n = n_nodes as usize;
let edge_count = src.len().min(dst.len()).min(weight.len());
current.clear();
current.resize(n, u32::MAX);
for (out, &value) in current.iter_mut().zip(dist.iter()) {
*out = value;
}
next.clear();
next.extend_from_slice(current);
for iter in 0..max_iterations {
for i in 0..edge_count {
let u = src[i] as usize;
let v = dst[i] as usize;
if u >= n || v >= n {
continue;
}
let w = weight[i];
let du = current[u];
if du != u32::MAX {
let alt = du.saturating_add(w);
next[v] = next[v].min(alt);
}
}
if next.as_slice() == current.as_slice() {
return iter;
}
current.copy_from_slice(&next);
}
max_iterations
}
#[cfg(feature = "inventory-registry")]
inventory::submit! {
vyre_foundation::operation::OperationRegistration::primitive(
OP_ID,
|| bellman_shortest_path("src", "dst", "weight", "dist", "next_dist", "changed", 4, 4, 10),
Some(|| {
let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
vec![vec![
to_bytes(&[0, u32::MAX, u32::MAX, u32::MAX]), to_bytes(&[0, u32::MAX, u32::MAX, u32::MAX]), to_bytes(&[0]), to_bytes(&[0, 1, 2, 0]), to_bytes(&[1, 2, 3, 3]), to_bytes(&[10, 20, 30, 100]), ]]
}),
Some(|| {
let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
vec![vec![
to_bytes(&[0, 10, 30, 60]), to_bytes(&[0, 10, 30, 60]), to_bytes(&[0]), ]]
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use vyre_foundation::MemoryOrdering;
#[test]
fn test_cpu_ref_trivial() {
let src = vec![0];
let dst = vec![1];
let weight = vec![5];
let dist = vec![0, u32::MAX];
let (final_dist, iters) = cpu_ref(&src, &dst, &weight, &dist, 2, 10);
assert_eq!(final_dist, vec![0, 5]);
assert_eq!(iters, 1);
}
#[test]
fn test_cpu_ref_single_node() {
let dist = vec![0];
let (final_dist, iters) = cpu_ref(&[], &[], &[], &dist, 1, 10);
assert_eq!(final_dist, vec![0]);
assert_eq!(iters, 0);
}
#[test]
fn test_cpu_ref_cycle() {
let src = vec![0, 1, 2];
let dst = vec![1, 2, 0];
let weight = vec![10, 10, 10];
let dist = vec![0, u32::MAX, u32::MAX];
let (final_dist, _) = cpu_ref(&src, &dst, &weight, &dist, 3, 10);
assert_eq!(final_dist, vec![0, 10, 20]);
}
#[test]
fn test_cpu_ref_large_line() {
let n = 50;
let mut src = Vec::new();
let mut dst = Vec::new();
let mut weight = Vec::new();
for i in 0..n - 1 {
src.push(i as u32);
dst.push((i + 1) as u32);
weight.push(1);
}
let mut dist = vec![u32::MAX; n];
dist[0] = 0;
let (final_dist, iters) = cpu_ref(&src, &dst, &weight, &dist, n as u32, n as u32 * 2);
assert_eq!(final_dist[n - 1], (n - 1) as u32);
assert_eq!(iters, (n - 1) as u32);
}
#[test]
fn test_cpu_ref_asymmetric() {
let src = vec![0, 0, 1, 2];
let dst = vec![1, 3, 3, 3];
let weight = vec![10, 100, 20, 5];
let dist = vec![0, u32::MAX, u32::MAX, u32::MAX];
let (final_dist, _) = cpu_ref(&src, &dst, &weight, &dist, 4, 10);
assert_eq!(final_dist[3], 30);
}
#[test]
fn test_cpu_ref_ignores_malformed_edges_and_pads_distances() {
let src = vec![0, 9, 1];
let dst = vec![1, 2];
let weight = vec![5, 99, 7];
let (final_dist, _) = cpu_ref(&src, &dst, &weight, &[0], 3, 10);
assert_eq!(final_dist, vec![0, 5, u32::MAX]);
}
#[test]
fn cpu_ref_into_reuses_current_and_next_buffers() {
let src = vec![0, 1, 2, 0];
let dst = vec![1, 2, 3, 3];
let weight = vec![10, 20, 30, 100];
let dist = vec![0, u32::MAX, u32::MAX, u32::MAX];
let mut current = Vec::with_capacity(16);
let mut next = Vec::with_capacity(16);
current.extend_from_slice(&[99, 98, 97, 96, 95, 94]);
next.extend_from_slice(&[77, 76, 75, 74, 73, 72]);
let current_capacity = current.capacity();
let next_capacity = next.capacity();
let iters = cpu_ref_into(&src, &dst, &weight, &dist, 4, 10, &mut current, &mut next);
assert_eq!(current, vec![0, 10, 30, 60]);
assert!(iters <= 4);
assert_eq!(current.capacity(), current_capacity);
assert_eq!(next.capacity(), next_capacity);
let iters = cpu_ref_into(&[], &[], &[], &[0], 1, 10, &mut current, &mut next);
assert_eq!(current, vec![0]);
assert_eq!(next, vec![0]);
assert_eq!(iters, 0);
assert_eq!(current.capacity(), current_capacity);
assert_eq!(next.capacity(), next_capacity);
}
#[test]
fn test_parity_small_graph() {
let src = vec![0, 1, 2, 0];
let dst = vec![1, 2, 3, 3];
let weight = vec![10, 20, 30, 100];
let dist_init = vec![0, u32::MAX, u32::MAX, u32::MAX];
let p = bellman_shortest_path(
"src",
"dst",
"weight",
"dist",
"next_dist",
"changed",
4,
4,
10,
);
let (expected_dist, _) = cpu_ref(&src, &dst, &weight, &dist_init, 4, 10);
use vyre_reference::reference_eval;
use vyre_reference::value::Value;
let to_value = |data: &[u32]| {
let bytes = crate::wire::pack_u32_slice(data);
Value::Bytes(Arc::from(bytes))
};
let inputs = vec![
to_value(&dist_init),
to_value(&dist_init),
to_value(&[0]),
to_value(&src),
to_value(&dst),
to_value(&weight),
];
let results = reference_eval(&p, &inputs).expect("Fix: interpreter failed");
let actual_bytes = results[0].to_bytes();
let actual_dist: Vec<u32> = actual_bytes
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(actual_dist, expected_dist);
}
#[test]
fn program_declares_six_buffers() {
let p = bellman_shortest_path("s", "d", "w", "di", "nd", "c", 4, 4, 10);
assert_eq!(p.buffers().len(), 6);
}
#[test]
fn rejects_zero_nodes_with_trap() {
let p = bellman_shortest_path("s", "d", "w", "di", "nd", "c", 0, 4, 10);
assert!(p.stats().trap());
}
#[test]
fn rejects_zero_max_iterations_with_trap() {
let p = bellman_shortest_path("s", "d", "w", "di", "nd", "c", 4, 4, 0);
assert!(p.stats().trap());
}
fn required_workgroups(program: &Program) -> u32 {
let elements = program
.buffers()
.iter()
.map(|buffer| buffer.count())
.max()
.unwrap_or(1);
elements.div_ceil(program.workgroup_size()[0])
}
fn changed_words(program: &Program) -> u32 {
program
.buffers()
.iter()
.find(|buffer| buffer.name() == "c")
.expect("Fix: bellman_shortest_path must declare its convergence-flag buffer.")
.count()
}
#[test]
fn multi_workgroup_bellman_never_shares_one_cleared_convergence_word() {
let program = bellman_shortest_path("s", "d", "w", "di", "nd", "c", 257, 256, 8);
assert_eq!(
required_workgroups(&program),
2,
"Fix: 257 nodes over a 256-wide workgroup must need two workgroups."
);
assert_eq!(
changed_words(&program),
8,
"Fix: a multi-workgroup bellman dispatch must use the per-iteration convergence-word protocol, not one shared cleared word."
);
}
fn count_grid_sync(nodes: &[Node]) -> usize {
nodes
.iter()
.map(|node| match node {
Node::Barrier {
ordering: MemoryOrdering::GridSync,
} => 1,
Node::If {
then, otherwise, ..
} => count_grid_sync(then) + count_grid_sync(otherwise),
Node::Loop { body, .. } | Node::Block(body) => count_grid_sync(body),
Node::Region { body, .. } => count_grid_sync(body),
_ => 0,
})
.sum()
}
#[test]
fn routing_threshold_is_the_declared_workgroup_width() {
let width = PERSISTENT_FIXPOINT_WORKGROUP_SIZE[0];
let at_width = bellman_shortest_path("s", "d", "w", "di", "nd", "c", width, width, 8);
assert_eq!(
at_width.workgroup_size(),
PERSISTENT_FIXPOINT_WORKGROUP_SIZE
);
assert_eq!(required_workgroups(&at_width), 1);
assert_eq!(
changed_words(&at_width),
1,
"Fix: a single-workgroup launch must keep the compact one-word convergence flag."
);
let past_width = bellman_shortest_path("s", "d", "w", "di", "nd", "c", width + 1, width, 8);
assert_eq!(required_workgroups(&past_width), 2);
assert_eq!(
changed_words(&past_width),
8,
"Fix: one node past the workgroup width already needs the per-iteration convergence words."
);
}
#[test]
fn wide_edge_list_with_tiny_node_set_still_routes_to_the_grid_form() {
let width = PERSISTENT_FIXPOINT_WORKGROUP_SIZE[0];
let program = bellman_shortest_path("s", "d", "w", "di", "nd", "c", 4, width + 1, 8);
assert_eq!(
required_workgroups(&program),
2,
"Fix: 257 edges over a 256-wide workgroup must need two workgroups even with 4 nodes."
);
assert_eq!(
changed_words(&program),
8,
"Fix: the routing threshold must be the dispatch span (max declared buffer), not n_nodes."
);
assert!(
count_grid_sync(program.entry()) > 0,
"Fix: a two-workgroup dispatch must be grid-synchronized whichever buffer widened it."
);
}
#[test]
fn grid_route_fences_the_grid_and_single_workgroup_route_does_not() {
let width = PERSISTENT_FIXPOINT_WORKGROUP_SIZE[0];
let single = bellman_shortest_path("s", "d", "w", "di", "nd", "c", width, width, 4);
assert_eq!(
count_grid_sync(single.entry()),
0,
"Fix: a single-workgroup bellman program must not force a cooperative grid launch."
);
let grid = bellman_shortest_path("s", "d", "w", "di", "nd", "c", width + 1, width, 4);
assert_eq!(
count_grid_sync(grid.entry()),
8,
"Fix: the grid form must fence each of its 4 waves twice, once after the transfer step and once after the compare."
);
}
#[test]
fn grid_route_sizes_changed_to_one_word_per_iteration() {
let width = PERSISTENT_FIXPOINT_WORKGROUP_SIZE[0];
for max_iterations in [1_u32, 2, 8, 64] {
let program = bellman_shortest_path(
"s",
"d",
"w",
"di",
"nd",
"c",
width + 1,
width,
max_iterations,
);
let harness =
persistent_fixpoint_grid(Vec::new(), "di", "nd", "c", width + 1, max_iterations);
assert_eq!(
changed_words(&program),
max_iterations,
"Fix: the grid route needs one convergence word per iteration; {max_iterations} iterations need {max_iterations} words."
);
assert_eq!(
changed_words(&program),
changed_words(&harness),
"Fix: this wrapper's `changed` declaration must match persistent_fixpoint_grid's own."
);
}
}
fn run_bellman(
program: &Program,
reversed: bool,
dist: &[u32],
src: &[u32],
dst: &[u32],
weight: &[u32],
changed_words: u32,
) -> (Vec<u32>, Vec<u32>) {
use vyre_reference::value::Value;
let to_value = |data: &[u32]| Value::Bytes(Arc::from(crate::wire::pack_u32_slice(data)));
let inputs = vec![
to_value(dist),
to_value(dist),
to_value(&vec![0_u32; changed_words as usize]),
to_value(src),
to_value(dst),
to_value(weight),
];
let results = if reversed {
vyre_reference::reference_eval_lane_reversed(program, &inputs)
} else {
vyre_reference::reference_eval(program, &inputs)
}
.expect("Fix: the reference interpreter must execute the bellman program.");
let decode = |value: &vyre_reference::value::Value| -> Vec<u32> {
value
.to_bytes()
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
.collect()
};
(decode(&results[0]), decode(&results[2]))
}
#[test]
fn single_word_harness_returns_wrong_distances_above_one_workgroup() {
let n_nodes = 257_u32;
let n_edges = 1_u32;
let src = vec![0_u32];
let dst = vec![256_u32];
let weight = vec![5_u32];
let mut dist = vec![u32::MAX; n_nodes as usize];
dist[0] = 0;
let max_iterations = 4_u32;
let (expected, _) = cpu_ref(&src, &dst, &weight, &dist, n_nodes, max_iterations);
assert_eq!(
expected[256], 5,
"Fix: the CPU oracle must relax node 256 to 5 over the single edge."
);
let unsound = bellman_single_word_harness(
"s",
"d",
"w",
"di",
"nd",
"c",
n_nodes,
n_edges,
max_iterations,
);
let (reversed, reversed_flag) = run_bellman(&unsound, true, &dist, &src, &dst, &weight, 1);
assert_eq!(
reversed[256],
u32::MAX,
"Fix: this test exists to record the OBSERVED wrong value the racing shared flag produces; if the single-word harness stops diverging here, re-derive the defect before deleting this test."
);
assert_ne!(
reversed[256], expected[256],
"Fix: the pre-routing single-word harness diverges from the CPU oracle at node 256."
);
assert_eq!(
reversed_flag[0], 0,
"Fix: the shared flag must be observed claiming convergence while the state is unconverged, which is what makes the wrong answer silent."
);
let (forward, _) = run_bellman(&unsound, false, &dist, &src, &dst, &weight, 1);
assert_eq!(
forward[256], 5,
"Fix: stepping group 0 first must expose the SAME program as correct, proving the divergence is cross-workgroup ordering."
);
}
#[test]
fn grid_routed_bellman_is_order_independent_where_single_word_diverges() {
let n_nodes = 257_u32;
let n_edges = 1_u32;
let src = vec![0_u32];
let dst = vec![256_u32];
let weight = vec![5_u32];
let mut dist = vec![u32::MAX; n_nodes as usize];
dist[0] = 0;
let max_iterations = 4_u32;
let (expected, _) = cpu_ref(&src, &dst, &weight, &dist, n_nodes, max_iterations);
let routed = bellman_shortest_path(
"s",
"d",
"w",
"di",
"nd",
"c",
n_nodes,
n_edges,
max_iterations,
);
assert_eq!(
changed_words(&routed),
max_iterations,
"Fix: this size must route to the grid harness."
);
for reversed in [false, true] {
let (actual, _) = run_bellman(
&routed,
reversed,
&dist,
&src,
&dst,
&weight,
max_iterations,
);
assert_eq!(
actual, expected,
"Fix: the grid-routed bellman program must match the CPU oracle in both workgroup orders (reversed={reversed})."
);
}
}
#[test]
fn single_word_harness_loses_far_edge_relaxations_when_edges_exceed_one_workgroup() {
let n_nodes = 4_u32;
let n_edges = 257_u32;
let mut src = vec![0_u32; n_edges as usize];
let mut dst = vec![1_u32; n_edges as usize];
let mut weight = vec![1_u32; n_edges as usize];
src[256] = 0;
dst[256] = 3;
weight[256] = 7;
let mut dist = vec![u32::MAX; n_nodes as usize];
dist[0] = 0;
let max_iterations = 4_u32;
let (expected, _) = cpu_ref(&src, &dst, &weight, &dist, n_nodes, max_iterations);
assert_eq!(
expected,
vec![0, 1, u32::MAX, 7],
"Fix: the CPU oracle must reach node 3 at cost 7 through edge 256 and leave node 2 unreachable."
);
let unsound = bellman_single_word_harness(
"s",
"d",
"w",
"di",
"nd",
"c",
n_nodes,
n_edges,
max_iterations,
);
assert_eq!(
required_workgroups(&unsound),
2,
"Fix: 257 edges must still span two workgroups even though there are only 4 nodes."
);
let (forward, forward_flag) = run_bellman(&unsound, false, &dist, &src, &dst, &weight, 1);
let (reversed, _) = run_bellman(&unsound, true, &dist, &src, &dst, &weight, 1);
assert_eq!(
forward,
vec![0, 1, u32::MAX, u32::MAX],
"Fix: this test records the OBSERVED wrong distances the racing shared flag produces in the canonical order; if the single-word harness stops diverging here, re-derive the defect before deleting this test."
);
assert_eq!(
forward_flag[0], 0,
"Fix: the shared flag must be observed claiming convergence while node 3 is unreachable, which is what makes the wrong answer silent."
);
assert_eq!(
reversed, expected,
"Fix: stepping group 1 first must match the oracle, proving the forward-order divergence is cross-workgroup ordering and not a wrong fixture."
);
}
#[test]
fn grid_routed_bellman_publishes_far_edge_relaxations_in_both_orders() {
let n_nodes = 4_u32;
let n_edges = 257_u32;
let mut src = vec![0_u32; n_edges as usize];
let mut dst = vec![1_u32; n_edges as usize];
let mut weight = vec![1_u32; n_edges as usize];
src[256] = 0;
dst[256] = 3;
weight[256] = 7;
let mut dist = vec![u32::MAX; n_nodes as usize];
dist[0] = 0;
let max_iterations = 4_u32;
let (expected, _) = cpu_ref(&src, &dst, &weight, &dist, n_nodes, max_iterations);
let routed = bellman_shortest_path(
"s",
"d",
"w",
"di",
"nd",
"c",
n_nodes,
n_edges,
max_iterations,
);
assert_eq!(
changed_words(&routed),
max_iterations,
"Fix: 257 edges must route to the grid harness even at 4 nodes."
);
for reversed in [false, true] {
let (actual, _) = run_bellman(
&routed,
reversed,
&dist,
&src,
&dst,
&weight,
max_iterations,
);
assert_eq!(
actual, expected,
"Fix: the grid-routed program must publish edge 256's relaxation in both workgroup orders (reversed={reversed})."
);
}
}
}