Skip to main content

celox_analysis/
lanes.rs

1//! Static assignment of a weighted dependency DAG to execution lanes.
2//!
3//! A lane is a sequential instruction stream executed by one worker. Work
4//! items are listed in a topological order; each lane executes its items in
5//! that order. Cross-lane dependencies are satisfied by explicit waits between
6//! *tasks*, the maximal runs of one lane's items that need no new wait.
7//!
8//! [`assign_lanes`] is an earliest-start list scheduler with a fixed priority
9//! (the given order). A dependency whose producer runs in another lane costs
10//! an additional synchronization delay, so chains stay in one lane unless
11//! another lane can start the item sufficiently earlier. An item also stays
12//! in the lane of its latest producer, or else of the previous item, while
13//! that lane starts it at most a locality slack later than the best lane:
14//! neighbouring items usually touch neighbouring state, and runs of them in
15//! one lane keep cache lines from being shared between cores. Items in one
16//! ownership group always share a lane: callers use groups for work which
17//! writes the same state object, so the object has exactly one writer lane.
18//!
19//! [`form_lane_tasks`] cuts each lane's sequence into tasks and records, for
20//! every task, the minimal set of `(lane, completed task count)` waits that
21//! satisfies every cross-lane dependency. A wait for a producer also implies
22//! everything that producer task (transitively) waited for; those implied
23//! waits are omitted. Every task only waits for tasks created before it, so
24//! executing the lanes concurrently cannot deadlock.
25//!
26//! For `N` items, `E` dependencies, and `L` lanes, assignment costs
27//! `O(N * L + E)` time and task formation `O(E + T * L)` for `T` tasks.
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum LaneError {
31    /// Input slices do not describe the same number of items or groups.
32    Shape,
33    /// A dependency does not point to an earlier item.
34    NotTopological { item: usize, predecessor: usize },
35    /// A group or lane identifier is outside its declared range.
36    InvalidIdentifier,
37}
38
39/// Inputs describing one lane assignment problem.
40#[derive(Debug, Clone, Copy)]
41pub struct LaneAssignmentInput<'a> {
42    /// Estimated execution cost of each item.
43    pub costs: &'a [u64],
44    /// Dependencies of each item. Every predecessor precedes the item.
45    pub predecessors: &'a [Vec<usize>],
46    /// Ownership group of each item. Items in one group share a lane.
47    pub groups: &'a [usize],
48    /// Lane fixed in advance for each group, if any.
49    pub group_lanes: &'a [Option<u32>],
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct LaneAssignmentOptions {
54    /// Number of available lanes. Zero is treated as one.
55    pub lanes: u32,
56    /// Estimated delay for an item to observe a producer in another lane,
57    /// in the same unit as item costs.
58    pub synchronization_cost: u64,
59    /// Largest delay accepted to keep an item in its producer's or its
60    /// predecessor's lane, in the same unit as item costs.
61    pub locality_slack: u64,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LaneAssignment {
66    /// Lane of each item.
67    pub item_lanes: Vec<u32>,
68    /// Estimated completion time of the schedule.
69    pub estimated_makespan: u64,
70    /// Sum of all item costs, i.e. the estimated single-lane execution time.
71    pub total_cost: u64,
72}
73
74/// Assign every item to a lane.
75pub fn assign_lanes(
76    input: LaneAssignmentInput<'_>,
77    options: LaneAssignmentOptions,
78) -> Result<LaneAssignment, LaneError> {
79    let count = input.costs.len();
80    if input.predecessors.len() != count || input.groups.len() != count {
81        return Err(LaneError::Shape);
82    }
83    let lane_count = options.lanes.max(1) as usize;
84    for &lane in input.group_lanes.iter().flatten() {
85        if lane as usize >= lane_count {
86            return Err(LaneError::InvalidIdentifier);
87        }
88    }
89    let mut group_lanes = input.group_lanes.to_vec();
90    let mut item_lanes = vec![0u32; count];
91    let mut finish = vec![0u64; count];
92    let mut lane_available = vec![0u64; lane_count];
93    // Latest producer finish time per lane for the item being placed. The
94    // touched list keeps resetting this proportional to the item's fan-in.
95    let mut producer_finish = vec![0u64; lane_count];
96    let mut touched = Vec::new();
97    let mut total_cost = 0u64;
98    let sync = options.synchronization_cost;
99
100    let mut previous_lane = None::<usize>;
101    for item in 0..count {
102        let group = input.groups[item];
103        let Some(fixed) = group_lanes.get(group).copied() else {
104            return Err(LaneError::InvalidIdentifier);
105        };
106        let mut latest_producer = None::<(u64, usize)>;
107        for &predecessor in &input.predecessors[item] {
108            if predecessor >= item {
109                return Err(LaneError::NotTopological { item, predecessor });
110            }
111            let lane = item_lanes[predecessor] as usize;
112            if producer_finish[lane] == 0 {
113                touched.push(lane);
114            }
115            producer_finish[lane] = producer_finish[lane].max(finish[predecessor].max(1));
116            if latest_producer.is_none_or(|(time, _)| finish[predecessor] > time) {
117                latest_producer = Some((finish[predecessor], lane));
118            }
119        }
120        // The best and second-best producer lanes give every lane's remote
121        // ready time without scanning all predecessors once per lane.
122        let mut best = (0u64, usize::MAX);
123        let mut second = 0u64;
124        for &lane in &touched {
125            let time = producer_finish[lane];
126            if time > best.0 {
127                second = best.0;
128                best = (time, lane);
129            } else if time > second {
130                second = time;
131            }
132        }
133        let ready_in = |lane: usize| {
134            let remote = if best.1 == lane { second } else { best.0 };
135            let remote = if remote == 0 {
136                0
137            } else {
138                remote.saturating_add(sync)
139            };
140            producer_finish[lane].max(remote)
141        };
142        let lane = match fixed {
143            Some(lane) => lane as usize,
144            None => {
145                let mut selected = 0usize;
146                let mut selected_key = (u64::MAX, u64::MAX);
147                for (lane, &available) in lane_available.iter().enumerate() {
148                    let start = available.max(ready_in(lane));
149                    // Prefer the earliest start, then the least loaded lane so
150                    // independent work spreads instead of piling onto lane 0.
151                    let key = (start, available);
152                    if key < selected_key {
153                        selected_key = key;
154                        selected = lane;
155                    }
156                }
157                let local = latest_producer.map(|(_, lane)| lane).or(previous_lane);
158                match local {
159                    Some(lane)
160                        if lane_available[lane].max(ready_in(lane))
161                            <= selected_key.0.saturating_add(options.locality_slack) =>
162                    {
163                        lane
164                    }
165                    _ => selected,
166                }
167            }
168        };
169        let start = lane_available[lane].max(ready_in(lane));
170        let cost = input.costs[item];
171        finish[item] = start.saturating_add(cost);
172        lane_available[lane] = finish[item];
173        item_lanes[item] = lane as u32;
174        group_lanes[group] = Some(lane as u32);
175        total_cost = total_cost.saturating_add(cost);
176        previous_lane = Some(lane);
177        for lane in touched.drain(..) {
178            producer_finish[lane] = 0;
179        }
180    }
181
182    Ok(LaneAssignment {
183        item_lanes,
184        estimated_makespan: lane_available.into_iter().max().unwrap_or(0),
185        total_cost,
186    })
187}
188
189/// A lane assignment together with an execution order.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct LaneSchedulePlan {
192    /// Lane of each item.
193    pub item_lanes: Vec<u32>,
194    /// Items in a topological order in which each lane runs its items.
195    pub order: Vec<usize>,
196    /// Estimated completion time of the schedule.
197    pub estimated_makespan: u64,
198}
199
200/// Assign items to lanes and order them by critical-path priority.
201///
202/// Unlike [`assign_lanes`], which places items in their given order, this
203/// list scheduler places the ready item with the longest remaining path to
204/// the end of the DAG first. Work that other lanes wait for therefore runs
205/// early in its lane, which matters most when lanes are fixed in advance (for
206/// example one lane per instance subtree). Lane choice, ownership groups,
207/// synchronization and locality follow [`assign_lanes`].
208pub fn list_schedule_lanes(
209    input: LaneAssignmentInput<'_>,
210    options: LaneAssignmentOptions,
211) -> Result<LaneSchedulePlan, LaneError> {
212    use std::cmp::Reverse;
213    use std::collections::BinaryHeap;
214
215    let count = input.costs.len();
216    if input.predecessors.len() != count || input.groups.len() != count {
217        return Err(LaneError::Shape);
218    }
219    let lane_count = options.lanes.max(1) as usize;
220    for &lane in input.group_lanes.iter().flatten() {
221        if lane as usize >= lane_count {
222            return Err(LaneError::InvalidIdentifier);
223        }
224    }
225    let mut successors = vec![Vec::new(); count];
226    for (item, predecessors) in input.predecessors.iter().enumerate() {
227        for &predecessor in predecessors {
228            if predecessor >= item {
229                return Err(LaneError::NotTopological { item, predecessor });
230            }
231            successors[predecessor].push(item);
232        }
233    }
234    if input
235        .groups
236        .iter()
237        .any(|&group| group >= input.group_lanes.len())
238    {
239        return Err(LaneError::InvalidIdentifier);
240    }
241    // Longest path from each item to the end of the DAG, itself included.
242    // An edge between two lanes fixed in advance also costs a
243    // synchronization, so work feeding another lane ranks higher.
244    let fixed_lane = |item: usize| input.group_lanes[input.groups[item]];
245    let mut bottom_level = vec![0u64; count];
246    for item in (0..count).rev() {
247        let tail = successors[item]
248            .iter()
249            .map(|&successor| {
250                let crossing = matches!(
251                    (fixed_lane(item), fixed_lane(successor)),
252                    (Some(from), Some(to)) if from != to
253                );
254                bottom_level[successor].saturating_add(if crossing {
255                    options.synchronization_cost
256                } else {
257                    0
258                })
259            })
260            .max()
261            .unwrap_or(0);
262        bottom_level[item] = tail.saturating_add(input.costs[item]);
263    }
264
265    let mut group_lanes = input.group_lanes.to_vec();
266    let mut item_lanes = vec![0u32; count];
267    let mut start = vec![0u64; count];
268    let mut finish = vec![0u64; count];
269    let mut rank = vec![0usize; count];
270    let mut lane_available = vec![0u64; lane_count];
271    let mut producer_finish = vec![0u64; lane_count];
272    let mut touched = Vec::new();
273    let mut unresolved = input.predecessors.iter().map(Vec::len).collect::<Vec<_>>();
274    let mut ready = (0..count)
275        .filter(|&item| unresolved[item] == 0)
276        .map(|item| (bottom_level[item], Reverse(item)))
277        .collect::<BinaryHeap<_>>();
278    let sync = options.synchronization_cost;
279    let mut previous_lane = None::<usize>;
280    let mut next_rank = 0;
281    while let Some((_, Reverse(item))) = ready.pop() {
282        let mut latest_producer = None::<(u64, usize)>;
283        for &predecessor in &input.predecessors[item] {
284            let lane = item_lanes[predecessor] as usize;
285            if producer_finish[lane] == 0 {
286                touched.push(lane);
287            }
288            producer_finish[lane] = producer_finish[lane].max(finish[predecessor].max(1));
289            if latest_producer.is_none_or(|(time, _)| finish[predecessor] > time) {
290                latest_producer = Some((finish[predecessor], lane));
291            }
292        }
293        let mut best = (0u64, usize::MAX);
294        let mut second = 0u64;
295        for &lane in &touched {
296            let time = producer_finish[lane];
297            if time > best.0 {
298                second = best.0;
299                best = (time, lane);
300            } else if time > second {
301                second = time;
302            }
303        }
304        let ready_in = |lane: usize| {
305            let remote = if best.1 == lane { second } else { best.0 };
306            let remote = if remote == 0 {
307                0
308            } else {
309                remote.saturating_add(sync)
310            };
311            producer_finish[lane].max(remote)
312        };
313        let group = input.groups[item];
314        let lane = match group_lanes[group] {
315            Some(lane) => lane as usize,
316            None => {
317                let mut selected = 0usize;
318                let mut selected_key = (u64::MAX, u64::MAX);
319                for (lane, &available) in lane_available.iter().enumerate() {
320                    let key = (available.max(ready_in(lane)), available);
321                    if key < selected_key {
322                        selected_key = key;
323                        selected = lane;
324                    }
325                }
326                match latest_producer.map(|(_, lane)| lane).or(previous_lane) {
327                    Some(lane)
328                        if lane_available[lane].max(ready_in(lane))
329                            <= selected_key.0.saturating_add(options.locality_slack) =>
330                    {
331                        lane
332                    }
333                    _ => selected,
334                }
335            }
336        };
337        start[item] = lane_available[lane].max(ready_in(lane));
338        finish[item] = start[item].saturating_add(input.costs[item]);
339        lane_available[lane] = finish[item];
340        item_lanes[item] = lane as u32;
341        group_lanes[group] = Some(lane as u32);
342        rank[item] = next_rank;
343        next_rank += 1;
344        previous_lane = Some(lane);
345        for lane in touched.drain(..) {
346            producer_finish[lane] = 0;
347        }
348        for &successor in &successors[item] {
349            unresolved[successor] -= 1;
350            if unresolved[successor] == 0 {
351                ready.push((bottom_level[successor], Reverse(successor)));
352            }
353        }
354    }
355    // Start times respect every dependency, and ties keep the placement
356    // order, in which producers precede their consumers.
357    let mut order = (0..count).collect::<Vec<_>>();
358    order.sort_unstable_by_key(|&item| (start[item], rank[item]));
359    Ok(LaneSchedulePlan {
360        item_lanes,
361        order,
362        estimated_makespan: lane_available.into_iter().max().unwrap_or(0),
363    })
364}
365
366/// One wait in a task prologue: the lane must have completed at least
367/// `completed_tasks` tasks of the current execution.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
369pub struct LaneWait {
370    pub lane: u32,
371    pub completed_tasks: u32,
372}
373
374/// A maximal run of one lane's items that starts after its waits complete.
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct LaneTask {
377    pub lane: u32,
378    /// Position of this task in its lane's task sequence.
379    pub sequence: u32,
380    /// Items in execution order.
381    pub items: Vec<usize>,
382    /// Non-redundant waits, sorted by lane.
383    pub waits: Vec<LaneWait>,
384}
385
386/// Cut lane sequences into tasks with minimal cross-lane waits.
387///
388/// The returned tasks are listed in creation order, which is also a valid
389/// sequential execution order of all items.
390pub fn form_lane_tasks(
391    lane_count: u32,
392    item_lanes: &[u32],
393    predecessors: &[Vec<usize>],
394) -> Result<Vec<LaneTask>, LaneError> {
395    form_lane_tasks_separated(lane_count, item_lanes, predecessors, &[])
396}
397
398/// [`form_lane_tasks`] with items that must not share a task.
399///
400/// `classes` holds a two-bit class per item (or is empty). An item of class
401/// 1 and an item of class 2 never share a task; class 0 is compatible with
402/// both, and an item of class 3 shares a task with nothing. Callers use this
403/// when a task becomes one generated function whose code generation depends
404/// on the combination of its items.
405pub fn form_lane_tasks_separated(
406    lane_count: u32,
407    item_lanes: &[u32],
408    predecessors: &[Vec<usize>],
409    classes: &[u8],
410) -> Result<Vec<LaneTask>, LaneError> {
411    form_lane_tasks_with_options(
412        lane_count,
413        item_lanes,
414        predecessors,
415        classes,
416        TaskFormationOptions::default(),
417    )
418}
419
420/// Optional cuts that let consumers observe completed work sooner.
421#[derive(Debug, Clone, Copy, Default)]
422pub struct TaskFormationOptions<'a> {
423    /// Cost of each indivisible item; required when `max_cost` is nonzero.
424    pub costs: &'a [u64],
425    /// Maximum accumulated task cost. Zero leaves task size unbounded.
426    /// An indivisible item larger than the limit remains a single task.
427    pub max_cost: u64,
428    /// Close a task immediately after these items, before unrelated work joins.
429    pub publish_after: &'a [bool],
430}
431
432/// Form dependency-safe tasks with optional cost and publication boundaries.
433pub fn form_lane_tasks_with_options(
434    lane_count: u32,
435    item_lanes: &[u32],
436    predecessors: &[Vec<usize>],
437    classes: &[u8],
438    options: TaskFormationOptions<'_>,
439) -> Result<Vec<LaneTask>, LaneError> {
440    let count = item_lanes.len();
441    if predecessors.len() != count
442        || (!classes.is_empty() && classes.len() != count)
443        || (!options.costs.is_empty() && options.costs.len() != count)
444        || (options.max_cost != 0 && options.costs.len() != count)
445        || (!options.publish_after.is_empty() && options.publish_after.len() != count)
446    {
447        return Err(LaneError::Shape);
448    }
449    let class_of = |item: usize| classes.get(item).copied().unwrap_or(0) & 3;
450    let mut task_classes: Vec<u8> = Vec::new();
451    let mut task_costs: Vec<u64> = Vec::new();
452    let lane_count = lane_count.max(1) as usize;
453    let mut tasks: Vec<LaneTask> = Vec::new();
454    // Vector clock known at the start of each task: for every lane, how many
455    // of its tasks have completed.
456    let mut task_clocks: Vec<Vec<u32>> = Vec::new();
457    let mut lane_tasks: Vec<Vec<usize>> = vec![Vec::new(); lane_count];
458    let mut open = vec![None::<usize>; lane_count];
459    let mut lane_clock = vec![vec![0u32; lane_count]; lane_count];
460    let mut task_of_item = vec![usize::MAX; count];
461    let mut required = vec![0u32; lane_count];
462    let mut touched = Vec::new();
463
464    for item in 0..count {
465        let cost = options.costs.get(item).copied().unwrap_or(0);
466        let publish = options.publish_after.get(item).copied().unwrap_or(false);
467        let lane = item_lanes[item] as usize;
468        if lane >= lane_count {
469            return Err(LaneError::InvalidIdentifier);
470        }
471        for &predecessor in &predecessors[item] {
472            if predecessor >= item {
473                return Err(LaneError::NotTopological { item, predecessor });
474            }
475            let producer_lane = item_lanes[predecessor] as usize;
476            if producer_lane == lane {
477                continue;
478            }
479            let producer_task = task_of_item[predecessor];
480            let completed = tasks[producer_task].sequence + 1;
481            if completed > lane_clock[lane][producer_lane] {
482                if required[producer_lane] == 0 {
483                    touched.push(producer_lane);
484                }
485                required[producer_lane] = required[producer_lane].max(completed);
486            }
487        }
488
489        if touched.is_empty()
490            && let Some(task) = open[lane]
491            && (task_classes[task] | class_of(item)) != 3
492            && (options.max_cost == 0 || task_costs[task].saturating_add(cost) <= options.max_cost)
493        {
494            tasks[task].items.push(item);
495            task_classes[task] |= class_of(item);
496            task_costs[task] = task_costs[task].saturating_add(cost);
497            if publish {
498                open[lane] = None;
499            }
500            task_of_item[item] = task;
501            continue;
502        }
503
504        touched.sort_unstable();
505        let mut clock = lane_clock[lane].clone();
506        let mut waits = Vec::with_capacity(touched.len());
507        for &producer_lane in &touched {
508            let completed = required[producer_lane];
509            let producer_task = lane_tasks[producer_lane][completed as usize - 1];
510            // Later items of the producer lane must not join a task that a
511            // consumer already waits for.
512            if open[producer_lane] == Some(producer_task) {
513                open[producer_lane] = None;
514            }
515            for (known, &implied) in clock.iter_mut().zip(&task_clocks[producer_task]) {
516                *known = (*known).max(implied);
517            }
518            waits.push(LaneWait {
519                lane: producer_lane as u32,
520                completed_tasks: completed,
521            });
522        }
523        for &producer_lane in &touched {
524            clock[producer_lane] = clock[producer_lane].max(required[producer_lane]);
525        }
526        // Drop waits implied by another wait's transitive knowledge.
527        let direct = waits
528            .iter()
529            .copied()
530            .filter(|wait| {
531                !waits.iter().any(|other| {
532                    other.lane != wait.lane && {
533                        let other_task =
534                            lane_tasks[other.lane as usize][other.completed_tasks as usize - 1];
535                        task_clocks[other_task][wait.lane as usize] >= wait.completed_tasks
536                    }
537                })
538            })
539            .collect::<Vec<_>>();
540        for producer_lane in touched.drain(..) {
541            required[producer_lane] = 0;
542        }
543
544        let task = tasks.len();
545        let sequence = lane_tasks[lane].len() as u32;
546        tasks.push(LaneTask {
547            lane: lane as u32,
548            sequence,
549            items: vec![item],
550            waits: direct,
551        });
552        task_classes.push(class_of(item));
553        task_costs.push(cost);
554        lane_clock[lane] = clock.clone();
555        task_clocks.push(clock);
556        lane_tasks[lane].push(task);
557        open[lane] = (!publish).then_some(task);
558        task_of_item[item] = task;
559    }
560    Ok(tasks)
561}
562
563/// Estimated completion time of `tasks` when every task costs the sum of its
564/// items plus `task_overhead`, and a wait completes `synchronization_cost`
565/// after the producer task finishes.
566///
567/// Unlike the estimate of [`assign_lanes`], this charges the task boundaries
568/// that the waits actually create.
569pub fn estimate_task_makespan(
570    lane_count: u32,
571    tasks: &[LaneTask],
572    item_costs: &[u64],
573    synchronization_cost: u64,
574    task_overhead: u64,
575) -> u64 {
576    let lane_count = lane_count.max(1) as usize;
577    let mut lane_time = vec![0u64; lane_count];
578    let mut finished = vec![Vec::<u64>::new(); lane_count];
579    for task in tasks {
580        let lane = (task.lane as usize).min(lane_count - 1);
581        let mut start = lane_time[lane];
582        for wait in &task.waits {
583            let producer = finished
584                .get(wait.lane as usize)
585                .and_then(|times| times.get((wait.completed_tasks as usize).checked_sub(1)?))
586                .copied()
587                .unwrap_or(0);
588            start = start.max(producer.saturating_add(synchronization_cost));
589        }
590        let cost = task
591            .items
592            .iter()
593            .filter_map(|&item| item_costs.get(item))
594            .fold(task_overhead, |total, cost| total.saturating_add(*cost));
595        let finish = start.saturating_add(cost);
596        lane_time[lane] = finish;
597        finished[lane].push(finish);
598    }
599    lane_time.into_iter().max().unwrap_or(0)
600}
601
602/// Check that `tasks` honour every dependency of `predecessors`.
603///
604/// Returns the first violated `(predecessor, item)` edge. This is an
605/// independent oracle for [`form_lane_tasks`] and for callers that rewrite a
606/// task list after formation.
607pub fn verify_lane_tasks(
608    lane_count: u32,
609    tasks: &[LaneTask],
610    predecessors: &[Vec<usize>],
611) -> Result<(), (usize, usize)> {
612    let lane_count = lane_count.max(1) as usize;
613    let count = predecessors.len();
614    let mut position = vec![(usize::MAX, usize::MAX); count];
615    let mut lane_sequences: Vec<Vec<usize>> = vec![Vec::new(); lane_count];
616    for (index, task) in tasks.iter().enumerate() {
617        let lane = task.lane as usize;
618        if lane >= lane_count || task.sequence as usize != lane_sequences[lane].len() {
619            return Err((usize::MAX, usize::MAX));
620        }
621        lane_sequences[lane].push(index);
622        for (offset, &item) in task.items.iter().enumerate() {
623            if item >= count || position[item].0 != usize::MAX {
624                return Err((usize::MAX, item.min(count)));
625            }
626            position[item] = (index, offset);
627        }
628    }
629    if position.iter().any(|(task, _)| *task == usize::MAX) {
630        return Err((usize::MAX, usize::MAX));
631    }
632    // Transitive knowledge at each task start, computed independently of
633    // the formation algorithm.
634    let mut clocks: Vec<Vec<u32>> = Vec::with_capacity(tasks.len());
635    let mut lane_previous: Vec<Option<usize>> = vec![None; lane_count];
636    for (index, task) in tasks.iter().enumerate() {
637        let lane = task.lane as usize;
638        let mut clock = lane_previous[lane]
639            .map(|previous| {
640                let mut clock: Vec<u32> = clocks[previous].clone();
641                clock[lane] = clock[lane].max(tasks[previous].sequence + 1);
642                clock
643            })
644            .unwrap_or_else(|| vec![0; lane_count]);
645        for wait in &task.waits {
646            let waited_lane = wait.lane as usize;
647            if waited_lane >= lane_count || waited_lane == lane || wait.completed_tasks == 0 {
648                return Err((usize::MAX, usize::MAX));
649            }
650            let Some(&waited) = lane_sequences[waited_lane].get(wait.completed_tasks as usize - 1)
651            else {
652                return Err((usize::MAX, usize::MAX));
653            };
654            if waited >= index {
655                return Err((usize::MAX, usize::MAX));
656            }
657            for (known, &implied) in clock.iter_mut().zip(&clocks[waited]) {
658                *known = (*known).max(implied);
659            }
660            clock[waited_lane] = clock[waited_lane].max(wait.completed_tasks);
661        }
662        lane_previous[lane] = Some(index);
663        clocks.push(clock);
664    }
665    for (item, item_predecessors) in predecessors.iter().enumerate() {
666        let (task, offset) = position[item];
667        for &predecessor in item_predecessors {
668            let (producer_task, producer_offset) = position[predecessor];
669            let ordered = if tasks[producer_task].lane == tasks[task].lane {
670                (producer_task, producer_offset) < (task, offset)
671            } else {
672                clocks[task][tasks[producer_task].lane as usize] > tasks[producer_task].sequence
673            };
674            if !ordered {
675                return Err((predecessor, item));
676            }
677        }
678    }
679    Ok(())
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    fn assign(costs: &[u64], predecessors: &[Vec<usize>], lanes: u32, sync: u64) -> LaneAssignment {
687        let groups = (0..costs.len()).collect::<Vec<_>>();
688        let group_lanes = vec![None; costs.len()];
689        assign_lanes(
690            LaneAssignmentInput {
691                costs,
692                predecessors,
693                groups: &groups,
694                group_lanes: &group_lanes,
695            },
696            LaneAssignmentOptions {
697                lanes,
698                synchronization_cost: sync,
699                locality_slack: 0,
700            },
701        )
702        .unwrap()
703    }
704
705    #[test]
706    fn publication_does_not_hold_a_consumer_behind_unrelated_work() {
707        let lanes = [0, 0, 1];
708        let predecessors = vec![vec![], vec![], vec![0]];
709        let costs = [2, 100, 100];
710        let original = form_lane_tasks(2, &lanes, &predecessors).unwrap();
711        assert_eq!(estimate_task_makespan(2, &original, &costs, 0, 0), 202);
712        for options in [
713            TaskFormationOptions {
714                costs: &costs,
715                max_cost: 50,
716                ..TaskFormationOptions::default()
717            },
718            TaskFormationOptions {
719                publish_after: &[true, false, false],
720                ..TaskFormationOptions::default()
721            },
722        ] {
723            let tasks =
724                form_lane_tasks_with_options(2, &lanes, &predecessors, &[], options).unwrap();
725            assert_eq!(estimate_task_makespan(2, &tasks, &costs, 0, 0), 102);
726            verify_lane_tasks(2, &tasks, &predecessors).unwrap();
727            assert_eq!(
728                tasks[2].waits,
729                [LaneWait {
730                    lane: 0,
731                    completed_tasks: 1
732                }]
733            );
734        }
735    }
736
737    #[test]
738    fn publication_and_budget_keep_sparse_classes_separate() {
739        let predecessors = vec![vec![], vec![0], vec![1], vec![2]];
740        let tasks = form_lane_tasks_with_options(
741            1,
742            &[0; 4],
743            &predecessors,
744            &[1, 0, 2, 2],
745            TaskFormationOptions {
746                costs: &[1; 4],
747                max_cost: 100,
748                publish_after: &[false, true, false, false],
749            },
750        )
751        .unwrap();
752        assert_eq!(
753            tasks.iter().map(|t| t.items.clone()).collect::<Vec<_>>(),
754            [vec![0, 1], vec![2, 3]]
755        );
756        verify_lane_tasks(1, &tasks, &predecessors).unwrap();
757        assert_eq!(
758            form_lane_tasks_with_options(
759                1,
760                &[0],
761                &[vec![]],
762                &[],
763                TaskFormationOptions {
764                    max_cost: 1,
765                    ..TaskFormationOptions::default()
766                }
767            ),
768            Err(LaneError::Shape)
769        );
770    }
771
772    #[test]
773    fn independent_items_spread_across_lanes() {
774        let costs = vec![10; 8];
775        let predecessors = vec![Vec::new(); 8];
776        let result = assign(&costs, &predecessors, 4, 100);
777        let mut per_lane = [0; 4];
778        for lane in &result.item_lanes {
779            per_lane[*lane as usize] += 1;
780        }
781        assert_eq!(per_lane, [2, 2, 2, 2]);
782        assert_eq!(result.estimated_makespan, 20);
783        assert_eq!(result.total_cost, 80);
784    }
785
786    #[test]
787    fn expensive_synchronization_keeps_a_chain_in_one_lane() {
788        let costs = vec![10; 4];
789        let predecessors = vec![vec![], vec![0], vec![1], vec![2]];
790        let result = assign(&costs, &predecessors, 4, 1000);
791        assert!(
792            result
793                .item_lanes
794                .iter()
795                .all(|lane| *lane == result.item_lanes[0])
796        );
797        assert_eq!(result.estimated_makespan, 40);
798    }
799
800    #[test]
801    fn locality_slack_keeps_runs_of_independent_items_together() {
802        let costs = vec![10; 8];
803        let predecessors = vec![Vec::new(); 8];
804        let groups = (0..8).collect::<Vec<_>>();
805        let group_lanes = vec![None; 8];
806        let result = assign_lanes(
807            LaneAssignmentInput {
808                costs: &costs,
809                predecessors: &predecessors,
810                groups: &groups,
811                group_lanes: &group_lanes,
812            },
813            LaneAssignmentOptions {
814                lanes: 2,
815                synchronization_cost: 100,
816                locality_slack: 30,
817            },
818        )
819        .unwrap();
820        // A lane keeps taking items until it is more than the slack ahead.
821        assert_eq!(result.item_lanes, vec![0, 0, 0, 0, 1, 1, 1, 1]);
822        assert_eq!(result.estimated_makespan, 40);
823    }
824
825    #[test]
826    fn ownership_groups_share_a_lane_and_pins_are_respected() {
827        let costs = vec![10, 10, 10, 10];
828        let predecessors = vec![Vec::new(); 4];
829        let groups = vec![0, 1, 0, 2];
830        let group_lanes = vec![None, Some(3), None];
831        let result = assign_lanes(
832            LaneAssignmentInput {
833                costs: &costs,
834                predecessors: &predecessors,
835                groups: &groups,
836                group_lanes: &group_lanes,
837            },
838            LaneAssignmentOptions {
839                lanes: 4,
840                synchronization_cost: 1,
841                locality_slack: 0,
842            },
843        )
844        .unwrap();
845        assert_eq!(result.item_lanes[0], result.item_lanes[2]);
846        assert_eq!(result.item_lanes[1], 3);
847    }
848
849    #[test]
850    fn non_topological_input_is_rejected() {
851        let costs = vec![1, 1];
852        let predecessors = vec![vec![1], vec![]];
853        let groups = vec![0, 1];
854        let group_lanes = vec![None, None];
855        assert_eq!(
856            assign_lanes(
857                LaneAssignmentInput {
858                    costs: &costs,
859                    predecessors: &predecessors,
860                    groups: &groups,
861                    group_lanes: &group_lanes,
862                },
863                LaneAssignmentOptions {
864                    lanes: 2,
865                    synchronization_cost: 0,
866                    locality_slack: 0,
867                },
868            ),
869            Err(LaneError::NotTopological {
870                item: 0,
871                predecessor: 1
872            })
873        );
874    }
875
876    #[test]
877    fn tasks_split_only_at_new_cross_lane_dependencies() {
878        // lane 0: 0 -> 2 -> 4 -> 6, lane 1: 1 -> 3 (depends on 0) -> 5
879        // (depends on 2, already covered) -> 7 (depends on 6).
880        let lanes = [0, 1, 0, 1, 0, 1, 0, 1];
881        let predecessors = vec![
882            vec![],
883            vec![],
884            vec![0],
885            vec![1, 0],
886            vec![2],
887            vec![3, 2],
888            vec![4],
889            vec![5, 6],
890        ];
891        let tasks = form_lane_tasks(2, &lanes, &predecessors).unwrap();
892        verify_lane_tasks(2, &tasks, &predecessors).unwrap();
893        let lane0 = tasks
894            .iter()
895            .filter(|task| task.lane == 0)
896            .collect::<Vec<_>>();
897        // The consumer of item 0 closes the task holding items 0 and 2.
898        assert_eq!(lane0[0].items, vec![0, 2]);
899        assert_eq!(lane0[1].items, vec![4, 6]);
900        let lane1 = tasks
901            .iter()
902            .filter(|task| task.lane == 1)
903            .collect::<Vec<_>>();
904        assert_eq!(lane1.len(), 3);
905        assert_eq!(lane1[0].items, vec![1]);
906        assert!(lane1[0].waits.is_empty());
907        assert_eq!(lane1[1].items, vec![3, 5]);
908        assert_eq!(
909            lane1[1].waits,
910            vec![LaneWait {
911                lane: 0,
912                completed_tasks: 1
913            }]
914        );
915        assert_eq!(lane1[2].items, vec![7]);
916        assert_eq!(
917            lane1[2].waits,
918            vec![LaneWait {
919                lane: 0,
920                completed_tasks: 2
921            }]
922        );
923    }
924
925    #[test]
926    fn list_scheduling_runs_critical_work_first() {
927        // Item 0 is independent; item 1 feeds a chain on another lane whose
928        // synchronization makes it the longer path. With fixed lanes, lane 0
929        // should run 1 before 0.
930        let costs = vec![6, 1, 1, 1];
931        let predecessors = vec![vec![], vec![], vec![1], vec![2]];
932        let groups = vec![0, 0, 1, 1];
933        let group_lanes = vec![Some(0), Some(1)];
934        let plan = list_schedule_lanes(
935            LaneAssignmentInput {
936                costs: &costs,
937                predecessors: &predecessors,
938                groups: &groups,
939                group_lanes: &group_lanes,
940            },
941            LaneAssignmentOptions {
942                lanes: 2,
943                synchronization_cost: 5,
944                locality_slack: 0,
945            },
946        )
947        .unwrap();
948        assert_eq!(plan.item_lanes, vec![0, 0, 1, 1]);
949        let position = |item| plan.order.iter().position(|&x| x == item).unwrap();
950        assert!(position(1) < position(0));
951        // Lane 0 runs 1 then 0 (finishing at 7); lane 1 starts the chain
952        // after the synchronization and finishes at 1 + 5 + 2.
953        assert_eq!(plan.estimated_makespan, 8);
954        for (item, predecessors) in predecessors.iter().enumerate() {
955            for &predecessor in predecessors {
956                assert!(position(predecessor) < position(item));
957            }
958        }
959    }
960
961    #[test]
962    fn task_makespan_charges_task_boundaries_and_waits() {
963        let tasks = vec![
964            LaneTask {
965                lane: 0,
966                sequence: 0,
967                items: vec![0],
968                waits: Vec::new(),
969            },
970            LaneTask {
971                lane: 1,
972                sequence: 0,
973                items: vec![1, 2],
974                waits: vec![LaneWait {
975                    lane: 0,
976                    completed_tasks: 1,
977                }],
978            },
979        ];
980        // Lane 1 starts at 10 + 5 (wait) and runs 2 + 3 + 1 (overhead).
981        assert_eq!(estimate_task_makespan(2, &tasks, &[9, 2, 3], 5, 1), 21);
982    }
983
984    #[test]
985    fn transitive_waits_are_omitted() {
986        // 0 (lane 0) -> 1 (lane 1) -> 2 (lane 2); 2 also depends on 0.
987        let lanes = [0, 1, 2];
988        let predecessors = vec![vec![], vec![0], vec![1, 0]];
989        let tasks = form_lane_tasks(3, &lanes, &predecessors).unwrap();
990        verify_lane_tasks(3, &tasks, &predecessors).unwrap();
991        let last = tasks.iter().find(|task| task.lane == 2).unwrap();
992        assert_eq!(
993            last.waits,
994            vec![LaneWait {
995                lane: 1,
996                completed_tasks: 1
997            }]
998        );
999    }
1000
1001    #[test]
1002    fn separated_classes_never_share_a_task() {
1003        let lanes = [0, 0, 0, 0];
1004        let predecessors = vec![vec![], vec![], vec![], vec![]];
1005        let tasks = form_lane_tasks_separated(1, &lanes, &predecessors, &[1, 0, 2, 2]).unwrap();
1006        verify_lane_tasks(1, &tasks, &predecessors).unwrap();
1007        assert_eq!(
1008            tasks
1009                .iter()
1010                .map(|task| task.items.clone())
1011                .collect::<Vec<_>>(),
1012            vec![vec![0, 1], vec![2, 3]]
1013        );
1014    }
1015
1016    #[test]
1017    fn verification_rejects_a_missing_wait() {
1018        let lanes = [0, 1];
1019        let predecessors = vec![vec![], vec![0]];
1020        let mut tasks = form_lane_tasks(2, &lanes, &predecessors).unwrap();
1021        for task in &mut tasks {
1022            task.waits.clear();
1023        }
1024        assert_eq!(verify_lane_tasks(2, &tasks, &predecessors), Err((0, 1)));
1025    }
1026
1027    #[test]
1028    fn randomized_graphs_form_valid_tasks() {
1029        // Deterministic pseudo-random DAGs; the verifier is the oracle.
1030        let mut state = 0x9e37_79b9_7f4a_7c15u64;
1031        let mut next = || {
1032            state ^= state << 13;
1033            state ^= state >> 7;
1034            state ^= state << 17;
1035            state
1036        };
1037        for _ in 0..200 {
1038            let count = (next() % 40) as usize + 1;
1039            let lane_count = (next() % 5) as u32 + 1;
1040            let mut predecessors = vec![Vec::new(); count];
1041            for (item, row) in predecessors.iter_mut().enumerate() {
1042                for predecessor in 0..item {
1043                    if next() % 5 == 0 {
1044                        row.push(predecessor);
1045                    }
1046                }
1047            }
1048            let costs = (0..count).map(|_| next() % 50 + 1).collect::<Vec<_>>();
1049            let assignment = assign(&costs, &predecessors, lane_count, next() % 40);
1050            let tasks = form_lane_tasks(lane_count, &assignment.item_lanes, &predecessors).unwrap();
1051            verify_lane_tasks(lane_count, &tasks, &predecessors).unwrap();
1052            let random_lanes = (0..count)
1053                .map(|_| (next() % u64::from(lane_count)) as u32)
1054                .collect::<Vec<_>>();
1055            let tasks = form_lane_tasks(lane_count, &random_lanes, &predecessors).unwrap();
1056            verify_lane_tasks(lane_count, &tasks, &predecessors).unwrap();
1057            let publish = (0..count).map(|_| next() % 3 == 0).collect::<Vec<_>>();
1058            let classes = (0..count).map(|_| (next() % 4) as u8).collect::<Vec<_>>();
1059            let tasks = form_lane_tasks_with_options(
1060                lane_count,
1061                &random_lanes,
1062                &predecessors,
1063                &classes,
1064                TaskFormationOptions {
1065                    costs: &costs,
1066                    max_cost: next() % 100 + 1,
1067                    publish_after: &publish,
1068                },
1069            )
1070            .unwrap();
1071            verify_lane_tasks(lane_count, &tasks, &predecessors).unwrap();
1072            let mut covered = tasks
1073                .iter()
1074                .flat_map(|t| t.items.iter().copied())
1075                .collect::<Vec<_>>();
1076            covered.sort_unstable();
1077            assert_eq!(covered, (0..count).collect::<Vec<_>>());
1078            for task in &tasks {
1079                assert!(
1080                    task.items.len() == 1
1081                        || task
1082                            .items
1083                            .iter()
1084                            .fold(0, |class, &item| class | classes[item])
1085                            != 3
1086                );
1087            }
1088
1089            // Priority list scheduling: a topological order that keeps
1090            // ownership groups and fixed lanes.
1091            let groups = (0..count)
1092                .map(|_| (next() % count as u64) as usize)
1093                .collect::<Vec<_>>();
1094            let group_lanes = (0..count)
1095                .map(|_| (next() % 3 == 0).then(|| (next() % u64::from(lane_count)) as u32))
1096                .collect::<Vec<_>>();
1097            let plan = list_schedule_lanes(
1098                LaneAssignmentInput {
1099                    costs: &costs,
1100                    predecessors: &predecessors,
1101                    groups: &groups,
1102                    group_lanes: &group_lanes,
1103                },
1104                LaneAssignmentOptions {
1105                    lanes: lane_count,
1106                    synchronization_cost: next() % 40,
1107                    locality_slack: next() % 20,
1108                },
1109            )
1110            .unwrap();
1111            let mut position = vec![usize::MAX; count];
1112            for (index, &item) in plan.order.iter().enumerate() {
1113                assert_eq!(position[item], usize::MAX, "item listed twice");
1114                position[item] = index;
1115            }
1116            for (item, row) in predecessors.iter().enumerate() {
1117                assert!(
1118                    row.iter()
1119                        .all(|&predecessor| position[predecessor] < position[item])
1120                );
1121                if let Some(lane) = group_lanes[groups[item]] {
1122                    assert_eq!(plan.item_lanes[item], lane);
1123                }
1124                let first = groups
1125                    .iter()
1126                    .position(|&group| group == groups[item])
1127                    .unwrap();
1128                assert_eq!(plan.item_lanes[item], plan.item_lanes[first]);
1129            }
1130        }
1131    }
1132}