Skip to main content

fso_graph/
sssp.rs

1use crate::prelude::*;
2
3use atomic_float::AtomicF32;
4use log::info;
5use rayon::prelude::*;
6
7use std::{
8    sync::atomic::{AtomicUsize, Ordering},
9    time::Instant,
10};
11
12const INF: f32 = f32::MAX;
13const NO_BIN: usize = usize::MAX;
14const BIN_SIZE_THRESHOLD: usize = 1000;
15
16const BATCH_SIZE: usize = 64;
17
18#[derive(Copy, Clone, Debug)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "clap", derive(clap::Args))]
21pub struct DeltaSteppingConfig {
22    /// The node for which to compute distances to all reachable nodes.
23    #[cfg_attr(feature = "clap", clap(long))]
24    pub start_node: usize,
25
26    /// Defines the "bucket width". A bucket maintains nodes with the
27    /// same tentative distance to the start node.
28    #[cfg_attr(feature = "clap", clap(long))]
29    pub delta: f32,
30}
31
32impl DeltaSteppingConfig {
33    pub fn new(start_node: usize, delta: f32) -> Self {
34        Self { start_node, delta }
35    }
36}
37
38pub fn delta_stepping<NI, G>(graph: &G, config: DeltaSteppingConfig) -> Vec<AtomicF32>
39where
40    NI: Idx,
41    G: Graph<NI> + DirectedNeighborsWithValues<NI, f32> + Sync,
42{
43    let start = Instant::now();
44
45    let DeltaSteppingConfig { start_node, delta } = config;
46
47    let node_count = graph.node_count().index();
48    let thread_count = rayon::current_num_threads();
49
50    let mut distance: Vec<AtomicF32> = Vec::with_capacity(node_count);
51    distance.resize_with(node_count, || AtomicF32::new(INF));
52    distance[start_node.index()].store(0.0, Ordering::Release);
53
54    let mut frontier = vec![NI::zero(); graph.edge_count().index()];
55    frontier[0] = NI::new(start_node);
56    let frontier_idx = AtomicUsize::new(0);
57    let mut frontier_len = 1;
58
59    let mut local_bins = Vec::with_capacity(thread_count);
60    local_bins.resize_with(thread_count, ThreadLocalBins::<NI>::new);
61
62    let mut curr_bin = 0;
63
64    while curr_bin != NO_BIN {
65        frontier_idx.store(0, Ordering::Relaxed);
66
67        let next_bin = local_bins
68            .par_iter_mut()
69            .map(|local_bins| {
70                process_shared_bin(
71                    local_bins,
72                    curr_bin,
73                    graph,
74                    (&frontier, &frontier_idx, frontier_len),
75                    &distance,
76                    delta,
77                )
78            })
79            .map(|local_bins| process_local_bins(local_bins, curr_bin, graph, &distance, delta))
80            .map(|local_bins| min_non_empty_bin(local_bins, curr_bin))
81            .min_by(|x, y| x.cmp(y))
82            .unwrap_or(NO_BIN);
83
84        // copy next local bins into shared global bin
85        frontier_len = frontier_slices(&mut frontier, &local_bins, next_bin)
86            .par_iter_mut()
87            .zip(local_bins.par_iter_mut())
88            .filter(|(_, local_bins)| local_bins.contains(next_bin))
89            .map(|(slice, local_bins)| {
90                slice.copy_from_slice(local_bins.slice(next_bin));
91                local_bins.clear(next_bin);
92                slice.len()
93            })
94            .sum();
95
96        curr_bin = next_bin;
97    }
98
99    info!("Computed SSSP in {:?}", start.elapsed());
100
101    distance
102}
103
104fn process_shared_bin<'bins, NI, G>(
105    bins: &'bins mut ThreadLocalBins<NI>,
106    curr_bin: usize,
107    graph: &G,
108    (frontier, frontier_idx, frontier_len): (&[NI], &AtomicUsize, usize),
109    distance: &[AtomicF32],
110    delta: f32,
111) -> &'bins mut ThreadLocalBins<NI>
112where
113    NI: Idx,
114    G: Graph<NI> + DirectedNeighborsWithValues<NI, f32> + Sync,
115{
116    loop {
117        let offset = frontier_idx.fetch_add(BATCH_SIZE, Ordering::AcqRel);
118
119        if offset >= frontier_len {
120            break;
121        }
122
123        let limit = usize::min(offset + BATCH_SIZE, frontier_len);
124
125        for node in frontier[offset..limit].iter() {
126            if distance[node.index()].load(Ordering::Acquire) >= delta * curr_bin as f32 {
127                relax_edges(graph, distance, bins, *node, delta);
128            }
129        }
130    }
131    bins
132}
133
134fn process_local_bins<'bins, NI, G>(
135    bins: &'bins mut ThreadLocalBins<NI>,
136    curr_bin: usize,
137    graph: &G,
138    distance: &[AtomicF32],
139    delta: f32,
140) -> &'bins mut ThreadLocalBins<NI>
141where
142    NI: Idx,
143    G: Graph<NI> + DirectedNeighborsWithValues<NI, f32> + Sync,
144{
145    while curr_bin < bins.len()
146        && !bins.is_empty(curr_bin)
147        && bins.bin_len(curr_bin) < BIN_SIZE_THRESHOLD
148    {
149        let current_bin_copy = bins.clone(curr_bin);
150        bins.clear(curr_bin);
151
152        for node in current_bin_copy {
153            relax_edges(graph, distance, bins, node, delta);
154        }
155    }
156    bins
157}
158
159fn min_non_empty_bin<NI: Idx>(local_bins: &mut ThreadLocalBins<NI>, curr_bin: usize) -> usize {
160    let mut next_local_bin = NO_BIN;
161    for bin in curr_bin..local_bins.len() {
162        if !local_bins.is_empty(bin) {
163            next_local_bin = bin;
164            break;
165        }
166    }
167    next_local_bin
168}
169
170fn relax_edges<NI, G>(
171    graph: &G,
172    distances: &[AtomicF32],
173    local_bins: &mut ThreadLocalBins<NI>,
174    node: NI,
175    delta: f32,
176) where
177    NI: Idx,
178    G: Graph<NI> + DirectedNeighborsWithValues<NI, f32> + Sync,
179{
180    for Target { target, value } in graph.out_neighbors_with_values(node) {
181        let mut old_distance = distances[target.index()].load(Ordering::Acquire);
182        let new_distance = distances[node.index()].load(Ordering::Acquire) + value;
183
184        while new_distance < old_distance {
185            match distances[target.index()].compare_exchange_weak(
186                old_distance,
187                new_distance,
188                Ordering::Release,
189                Ordering::Relaxed,
190            ) {
191                Ok(_) => {
192                    let dest_bin = (new_distance / delta) as usize;
193                    if dest_bin >= local_bins.len() {
194                        local_bins.resize(dest_bin + 1);
195                    }
196                    local_bins.push(dest_bin, *target);
197                    break;
198                }
199                // CAX failed -> retry with new min distance
200                Err(min_distance) => old_distance = min_distance,
201            }
202        }
203    }
204}
205
206fn frontier_slices<'a, NI: Idx>(
207    frontier: &'a mut [NI],
208    bins: &[ThreadLocalBins<NI>],
209    next_bin: usize,
210) -> Vec<&'a mut [NI]> {
211    let mut slices = Vec::with_capacity(bins.len());
212    let mut tail = frontier;
213
214    for local_bins in bins.iter() {
215        if local_bins.contains(next_bin) {
216            let (head, remainder) = tail.split_at_mut(local_bins.bin_len(next_bin));
217            slices.push(head);
218            tail = remainder;
219        } else {
220            slices.push(&mut []);
221        }
222    }
223
224    slices
225}
226
227#[derive(Debug)]
228struct ThreadLocalBins<T> {
229    bins: Vec<Vec<T>>,
230}
231
232impl<T> ThreadLocalBins<T>
233where
234    T: Clone,
235{
236    fn new() -> Self {
237        Self { bins: vec![vec![]] }
238    }
239
240    fn contains(&self, bin: usize) -> bool {
241        self.len() > bin
242    }
243
244    fn len(&self) -> usize {
245        self.bins.len()
246    }
247
248    fn bin_len(&self, bin: usize) -> usize {
249        self.bins[bin].len()
250    }
251
252    fn is_empty(&self, bin: usize) -> bool {
253        self.bins[bin].is_empty()
254    }
255
256    fn clone(&self, bin: usize) -> Vec<T> {
257        self.bins[bin].clone()
258    }
259
260    fn clear(&mut self, bin: usize) {
261        self.bins[bin].clear();
262    }
263
264    fn slice(&self, bin: usize) -> &[T] {
265        &self.bins[bin]
266    }
267
268    fn resize(&mut self, new_len: usize) {
269        self.bins.resize_with(new_len, Vec::new)
270    }
271
272    fn push(&mut self, bin: usize, val: T) {
273        self.bins[bin].push(val);
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::prelude::{CsrLayout, GraphBuilder};
281
282    #[test]
283    fn test_sssp() {
284        let gdl = "(a:A)
285                        (b:B)
286                        (c:C)
287                        (d:D)
288                        (e:E)
289                        (f:F)
290                        (a)-[{cost:  4.0 }]->(b)
291                        (a)-[{cost:  2.0 }]->(c)
292                        (b)-[{cost:  5.0 }]->(c)
293                        (b)-[{cost: 10.0 }]->(d)
294                        (c)-[{cost:  3.0 }]->(e)
295                        (d)-[{cost: 11.0 }]->(f)
296                        (e)-[{cost:  4.0 }]->(d)";
297
298        let graph: DirectedCsrGraph<usize, (), f32> = GraphBuilder::new()
299            .csr_layout(CsrLayout::Deduplicated)
300            .gdl_str::<usize, _>(gdl)
301            .build()
302            .unwrap();
303
304        let config = DeltaSteppingConfig::new(0, 3.0);
305
306        let actual: Vec<f32> = delta_stepping(&graph, config)
307            .into_iter()
308            .map(|d| d.load(Ordering::Relaxed))
309            .collect();
310        let expected: Vec<f32> = vec![0.0, 4.0, 2.0, 9.0, 5.0, 20.0];
311
312        assert_eq!(actual, expected);
313    }
314}