1use super::CuContract;
12use super::CuMissionPlan;
13use super::CuPlan;
14use super::CuPlanPlacement;
15use super::CuPlanThread;
16use super::CuPlanWorker;
17use super::CuProfile;
18use crate::config::CuConfig;
19use crate::config::MAX_RT_PRIORITY;
20use crate::config::SchedulingPolicy;
21use crate::config::TaskKind;
22use alloc::collections::BTreeMap;
23use alloc::collections::BTreeSet;
24use alloc::collections::VecDeque;
25use alloc::format;
26use alloc::string::String;
27use alloc::string::ToString;
28use alloc::vec;
29use alloc::vec::Vec;
30use cu29_traits::CuError;
31use cu29_traits::CuResult;
32use serde::Deserialize;
33use serde::Serialize;
34
35const RESPONSE_CAP_PERIODS: u64 = 20;
37const PATIENCE: usize = 120;
39const KICK: usize = 3;
41const MIN_SEPARATION: f64 = 0.01;
44const RATE_TOLERANCE: f64 = 0.005;
46const RATE_STEP: f64 = 0.02;
48const SEARCH_SEED: u64 = 20260914;
50#[cfg(not(test))]
52const SEARCH_MOVES: usize = 2000;
53#[cfg(test)]
54const SEARCH_MOVES: usize = 300;
55#[cfg(not(test))]
57const SEARCH_RESTARTS: usize = 8;
58#[cfg(test)]
59const SEARCH_RESTARTS: usize = 3;
60
61#[derive(Clone, Debug)]
63pub struct ProposeRequest<'a> {
64 pub config: &'a CuConfig,
65 pub mission: &'a str,
66 pub contract: &'a CuContract,
67 pub profile: &'a CuProfile,
68 pub candidates: usize,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74pub struct CuCandidate {
75 pub plan: CuPlan,
76 pub prediction: CuPrediction,
77}
78
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct CuPrediction {
83 pub score: Vec<f64>,
85 pub chains: BTreeMap<String, CuChainPrediction>,
86 pub workers: BTreeMap<String, CuWorkerPrediction>,
87 pub sources: BTreeMap<String, f64>,
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct CuChainPrediction {
94 pub latency_ns: u64,
95 pub deadline_ns: u64,
96 pub ratio: f64,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct CuWorkerPrediction {
103 pub cpu: usize,
104 pub load: f64,
107 pub response_ns: Option<u64>,
109 pub rate: f64,
111}
112
113struct Model {
120 inventory: CuMissionPlan,
121 units: Vec<Vec<usize>>,
123 cost: Vec<u64>,
125 fired_cost: Vec<u64>,
127 preds: Vec<Vec<usize>>,
129 copperlist_ns: u64,
131 cycle_ns: u64,
133 chains: Vec<Vec<(usize, usize)>>,
135 chain_deadlines: Vec<u32>,
136 sources: Vec<Vec<usize>>,
138 short: Vec<bool>,
141 short_paths: Vec<Vec<usize>>,
143 fires: Vec<Vec<bool>>,
146 cpus: Vec<usize>,
147 max_in_flight: u32,
149 window_cost: Vec<Vec<u64>>,
152 policy: SchedulingPolicy,
153 headroom: f64,
154}
155
156#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
160struct Assignment {
161 lanes: Vec<Vec<usize>>,
162}
163
164struct Evaluation {
165 score: Vec<f64>,
166 chain_latency: Vec<u64>,
168 response: Vec<Option<u64>>,
169 rate: Vec<f64>,
170 load: Vec<f64>,
171 cycle_rate: f64,
172}
173
174impl Model {
175 fn new(
176 request: &ProposeRequest<'_>,
177 inventory: CuMissionPlan,
178 max_in_flight: u32,
179 ) -> CuResult<Self> {
180 let contract = request.contract;
181 let profile = request.profile;
182 let copperlist_ns = match request
183 .config
184 .runtime
185 .as_ref()
186 .and_then(|runtime| runtime.rate_target_hz)
187 {
188 Some(rate) => 1_000_000_000 / rate.max(1),
189 None => profile
190 .window_ns
191 .checked_div(profile.copperlists.max(1))
192 .unwrap_or(0)
193 .max(1),
194 };
195 let background: BTreeSet<&str> = inventory
196 .background
197 .iter()
198 .map(|entry| entry.task.as_str())
199 .collect();
200 let mut units: Vec<Vec<usize>> = Vec::new();
204 let mut unit_of = vec![usize::MAX; inventory.steps.len()];
205 for (index, step) in inventory.steps.iter().enumerate() {
206 if is_refine(&step.key) {
207 let base = inventory.steps.iter().position(|other| {
208 other.copperlist == step.copperlist
209 && !is_refine(&other.key)
210 && task_of_key(&other.key) == task_of_key(&step.key)
211 });
212 let Some(base) = base.map(|b| unit_of[b]).filter(|&u| u != usize::MAX) else {
213 return Err(CuError::from(format!(
214 "Refine step '{}' has no base step before it",
215 step.key
216 )));
217 };
218 units[base].push(index);
219 unit_of[index] = base;
220 } else {
221 unit_of[index] = units.len();
222 units.push(vec![index]);
223 }
224 }
225 let mut cost = Vec::with_capacity(units.len());
226 let mut fired_cost = Vec::with_capacity(units.len());
227 let mut firing = Vec::with_capacity(units.len());
228 for unit in &units {
229 let step = &inventory.steps[unit[0]];
230 let task = task_of_key(&step.key);
231 if task.is_some_and(|task| background.contains(task)) {
232 cost.push(0);
235 fired_cost.push(0);
236 firing.push(None);
237 continue;
238 }
239 let operation = profile.operations.get(&step.key).ok_or_else(|| {
240 CuError::from(format!("The profile has no measurement for '{}'", step.key))
241 })?;
242 let fired = (operation.fired.samples as f64 / profile.copperlists.max(1) as f64)
246 .clamp(0.0, 1.0);
247 let expected =
248 operation.fired.mean_ns * fired + operation.skipped.mean_ns * (1.0 - fired);
249 cost.push(expected as u64);
250 fired_cost.push(operation.fired.mean_ns.max(operation.skipped.mean_ns) as u64);
251 firing.push(
252 operation
253 .firing
254 .clone()
255 .map(|pattern| (pattern, operation.fired.mean_ns, operation.skipped.mean_ns)),
256 );
257 }
258 let copperlists_per_cycle = inventory.copperlists_per_cycle;
261 let window_ns = contract
262 .sources
263 .iter()
264 .map(|source| u64::from(source.period_ms) * 1_000_000)
265 .max()
266 .unwrap_or(copperlist_ns)
267 .max(copperlist_ns);
268 let window_cycles = window_ns.div_ceil(copperlist_ns * u64::from(copperlists_per_cycle));
269 let window_cls = (window_cycles * u64::from(copperlists_per_cycle)) as u32;
270 let probability = |cycle: u64, unit: usize| {
271 firing[unit].as_ref().map(|(pattern, _, _)| {
272 let offset = cycle as u32 * copperlists_per_cycle
273 + inventory.steps[units[unit][0]].copperlist;
274 pattern.probability(window_cls, offset)
275 })
276 };
277 let window_cost: Vec<Vec<u64>> = (0..window_cycles)
278 .map(|cycle| {
279 (0..units.len())
280 .map(|unit| match (probability(cycle, unit), &firing[unit]) {
281 (Some(p), Some((_, fired_ns, skipped_ns))) => {
282 (fired_ns * p + skipped_ns * (1.0 - p)) as u64
283 }
284 _ => cost[unit],
285 })
286 .collect()
287 })
288 .collect();
289 let fires: Vec<Vec<bool>> = (0..window_cycles)
290 .map(|cycle| {
291 (0..units.len())
292 .map(|unit| probability(cycle, unit).is_none_or(|p| p > 0.0))
293 .collect()
294 })
295 .collect();
296 let mut preds: Vec<Vec<usize>> = vec![Vec::new(); units.len()];
297 for edge in &inventory.dependencies {
298 let (from, to) = (unit_of[edge.from as usize], unit_of[edge.to as usize]);
299 if edge.cycle_lag == 0 && from != to && !preds[to].contains(&from) {
300 preds[to].push(from);
301 }
302 }
303 if let Some(unit) = first_unit_on_a_cycle(&preds) {
307 return Err(CuError::from(format!(
308 "Step '{}' and its refinements cannot run together: an edge leads out of one of its phases and back into an earlier one through another component",
309 inventory.steps[units[unit][0]].key
310 )));
311 }
312 let units_of = |task: &str| -> Vec<usize> {
314 (0..inventory.copperlists_per_cycle)
315 .filter_map(|offset| {
316 inventory
317 .steps
318 .iter()
319 .position(|step| {
320 step.copperlist == offset
321 && !is_refine(&step.key)
322 && task_of_key(&step.key) == Some(task)
323 })
324 .map(|index| unit_of[index])
325 })
326 .collect()
327 };
328 let mut chains = Vec::new();
329 for chain in &contract.chains {
330 let sources = units_of(&chain.source);
331 let sinks = units_of(&chain.sink);
332 if sources.len() != inventory.copperlists_per_cycle as usize
333 || sinks.len() != sources.len()
334 {
335 return Err(CuError::from(format!(
336 "Chain '{}' names a task without a whole-phase step",
337 chain.id
338 )));
339 }
340 chains.push(sources.into_iter().zip(sinks).collect());
341 }
342 let sources = contract
343 .sources
344 .iter()
345 .map(|source| units_of(&source.task))
346 .collect();
347 let mut succs: Vec<Vec<usize>> = vec![Vec::new(); units.len()];
350 for (to, from) in preds.iter().enumerate() {
351 for &from in from {
352 succs[from].push(to);
353 }
354 }
355 let mut short = vec![false; units.len()];
356 let mut short_paths = Vec::new();
357 for (chain, pairs) in chains.iter().enumerate() {
358 let spec = &contract.chains[chain];
359 let period = contract
360 .sources
361 .iter()
362 .find(|source| source.task == spec.source)
363 .map(|source| source.period_ms);
364 if period.is_some_and(|period| spec.deadline_ms <= period) {
365 for &(source, sink) in pairs {
366 let (forward, backward) = (reachable(&succs, source), reachable(&preds, sink));
367 let path: Vec<usize> = (0..units.len())
368 .filter(|&u| forward[u] && backward[u])
369 .collect();
370 for &unit in &path {
371 short[unit] = true;
372 }
373 short_paths.push(path);
374 }
375 }
376 }
377 Ok(Self {
378 cycle_ns: copperlist_ns * u64::from(copperlists_per_cycle),
379 inventory,
380 units,
381 cost,
382 fired_cost,
383 preds,
384 copperlist_ns,
385 chains,
386 chain_deadlines: contract
387 .chains
388 .iter()
389 .map(|chain| chain.deadline_ms)
390 .collect(),
391 sources,
392 short,
393 short_paths,
394 fires,
395 cpus: contract.cpus.clone(),
396 max_in_flight: max_in_flight.max(copperlists_per_cycle),
397 window_cost,
398 policy: contract.worker_policy,
399 headroom: contract.headroom,
400 })
401 }
402
403 fn tiers(&self) -> usize {
406 match self.policy {
407 SchedulingPolicy::Fifo { priority } | SchedulingPolicy::RoundRobin { priority }
408 if priority < MAX_RT_PRIORITY =>
409 {
410 2
411 }
412 _ => 1,
413 }
414 }
415
416 fn lane_cpu(&self, lane: usize) -> usize {
417 self.cpus[lane % self.cpus.len()]
418 }
419
420 fn lane_tier(&self, lane: usize) -> usize {
421 lane / self.cpus.len()
422 }
423
424 fn worker_id(&self, lane: usize) -> String {
425 let cpu = self.lane_cpu(lane);
426 if self.lane_tier(lane) == 0 {
427 format!("cpu{cpu}")
428 } else {
429 format!("cpu{cpu}-hi")
430 }
431 }
432
433 fn worker_policy(&self, lane: usize) -> SchedulingPolicy {
434 match self.policy {
435 SchedulingPolicy::Fifo { priority } if self.lane_tier(lane) > 0 => {
436 SchedulingPolicy::Fifo {
437 priority: priority + 1,
438 }
439 }
440 SchedulingPolicy::RoundRobin { priority } if self.lane_tier(lane) > 0 => {
441 SchedulingPolicy::RoundRobin {
442 priority: priority + 1,
443 }
444 }
445 policy => policy,
446 }
447 }
448
449 fn dispatcher(&self) -> CuPlanThread {
450 let policy = match self.policy {
451 SchedulingPolicy::Fifo { priority } => SchedulingPolicy::Fifo {
452 priority: priority + 2,
453 },
454 SchedulingPolicy::RoundRobin { priority } => SchedulingPolicy::RoundRobin {
455 priority: priority + 2,
456 },
457 policy => policy,
458 };
459 CuPlanThread { cpu: None, policy }
460 }
461
462 fn higher_load(&self, lane: usize, window_load: &[u64]) -> u64 {
464 let cpus = self.cpus.len();
465 (0..window_load.len())
466 .filter(|&h| h % cpus == lane % cpus && h / cpus > lane / cpus)
467 .map(|h| window_load[h])
468 .sum()
469 }
470
471 fn is_valid(&self, assignment: &Assignment) -> bool {
479 let n = self.units.len();
480 let lanes = assignment.lanes.len();
481 let mut lane_of = vec![usize::MAX; n];
482 for (l, lane) in assignment.lanes.iter().enumerate() {
483 for &o in lane {
484 lane_of[o] = l;
485 }
486 }
487 if lane_of.contains(&usize::MAX) {
488 return false;
489 }
490 if self.tiers() > 1 {
491 if (0..n).any(|o| self.lane_tier(lane_of[o]) > 0 && !self.short[o]) {
492 return false;
493 }
494 if self.short_paths.iter().any(|path| {
495 path.iter()
496 .any(|&o| self.lane_tier(lane_of[o]) != self.lane_tier(lane_of[path[0]]))
497 }) {
498 return false;
499 }
500 }
501 let mut incoming = vec![0usize; n];
502 let mut succs: Vec<Vec<usize>> = vec![Vec::new(); n];
503 for (to, preds) in self.preds.iter().enumerate() {
504 for &from in preds {
505 succs[from].push(to);
506 incoming[to] += 1;
507 }
508 }
509 for lane in &assignment.lanes {
510 for pair in lane.windows(2) {
511 succs[pair[0]].push(pair[1]);
512 incoming[pair[1]] += 1;
513 }
514 }
515 if kahn_count(&succs, incoming) != n {
516 return false;
517 }
518 let mut lane_succs: Vec<Vec<usize>> = vec![Vec::new(); lanes];
519 let mut lane_incoming = vec![0usize; lanes];
520 for (to, preds) in self.preds.iter().enumerate() {
521 for &from in preds {
522 let (a, b) = (lane_of[from], lane_of[to]);
523 if a != b && !lane_succs[a].contains(&b) {
524 lane_succs[a].push(b);
525 lane_incoming[b] += 1;
526 }
527 }
528 }
529 kahn_count(&lane_succs, lane_incoming) == lanes
530 }
531
532 fn evaluate(&self, assignment: &Assignment) -> Option<Evaluation> {
535 let lanes = &assignment.lanes;
536 let g = self.copperlist_ns;
537 let period = self.cycle_ns;
538 let lane_cost: Vec<u64> = lanes
539 .iter()
540 .map(|lane| lane.iter().map(|&o| self.cost[o]).sum())
541 .collect();
542 let window_ns = period * self.window_cost.len() as u64;
545 let window_load: Vec<u64> = lanes
546 .iter()
547 .map(|lane| {
548 lane.iter()
549 .map(|&o| self.window_cost.iter().map(|cycle| cycle[o]).sum::<u64>())
550 .sum()
551 })
552 .collect();
553 let mut response = Vec::with_capacity(lanes.len());
554 let mut rate = Vec::with_capacity(lanes.len());
555 let mut load = Vec::with_capacity(lanes.len());
556 let mut inflate = Vec::with_capacity(lanes.len());
557 for (l, &cost) in lane_cost.iter().enumerate() {
558 let r = fixpoint(cost, &[], g, RESPONSE_CAP_PERIODS * period);
559 response.push(r);
560 let higher = self.higher_load(l, &window_load);
561 let busy = (window_load[l] + higher) as f64 / window_ns as f64;
562 let headroom = ((1.0 - self.headroom) / busy.max(1e-9)).min(1.0);
566 rate.push(
567 match r {
568 Some(r) if r <= period => 1.0,
569 Some(r) => period as f64 / (r + g / 2) as f64,
570 None => 0.0,
571 }
572 .min(headroom),
573 );
574 load.push(busy);
575 inflate.push(if higher < window_ns {
578 (window_ns as f64 / (window_ns - higher) as f64).min(RESPONSE_CAP_PERIODS as f64)
579 } else {
580 RESPONSE_CAP_PERIODS as f64
581 });
582 }
583 let (starts, ends) = self.timeline(lanes, &self.fired_cost, &inflate)?;
586 let (_, expected_ends) = self.timeline(lanes, &self.cost, &inflate)?;
587 let per_window = self.window_cost.len();
591 let n = self.units.len();
592 let (window_starts, window_ends) = self.window_timeline(lanes, &inflate, 2)?;
593 let last_end = |lane: &[usize], window: usize| {
594 let cycle = window * per_window + per_window - 1;
595 lane.iter()
596 .map(|&o| window_ends[cycle * n + o])
597 .max()
598 .unwrap_or(0)
599 };
600 for (l, lane) in lanes.iter().enumerate() {
601 let span = last_end(lane, 1) - last_end(lane, 0);
602 if span > window_ns {
603 rate[l] = rate[l].min(window_ns as f64 / span as f64);
604 }
605 }
606 let mut chain_ratio = Vec::with_capacity(self.chains.len());
607 let mut chain_latency = Vec::with_capacity(self.chains.len());
608 for (chain, pairs) in self.chains.iter().enumerate() {
609 let deadline = u64::from(self.contract_deadline(chain)) * 1_000_000;
610 let latency = pairs
611 .iter()
612 .map(|&(source, sink)| {
613 let steady = (per_window..2 * per_window)
614 .filter(|&cycle| self.fires[cycle % per_window][source])
615 .map(|cycle| {
616 window_ends[cycle * n + sink]
617 .saturating_sub(window_starts[cycle * n + source])
618 })
619 .max()
620 .unwrap_or(0);
621 ends[sink].saturating_sub(starts[source]).max(steady)
622 })
623 .max()
624 .unwrap_or(0);
625 chain_ratio.push(latency as f64 / deadline as f64);
626 chain_latency.push(latency);
627 }
628 let makespan = expected_ends.iter().copied().max().unwrap_or(0);
633 let cycles_in_flight =
634 f64::from(self.max_in_flight) / f64::from(self.inventory.copperlists_per_cycle.max(1));
635 let pipelined = makespan as f64 / cycles_in_flight;
636 let cycle_rate =
637 rate.iter()
638 .copied()
639 .fold(1.0f64, f64::min)
640 .min(if pipelined <= period as f64 {
641 1.0
642 } else {
643 period as f64 / (pipelined + g as f64 / 2.0)
644 });
645 let source_rate: Vec<f64> = self.sources.iter().map(|_| cycle_rate).collect();
646 let rate_deficit: f64 = source_rate.iter().map(|&r| rate_deficit(r)).sum();
647 let sum: f64 = chain_ratio.iter().sum();
648 let max_load = load.iter().copied().fold(0.0f64, f64::max);
649 let score = vec![
650 round6(rate_deficit),
651 chain_ratio.iter().filter(|&&r| r > 1.0).count() as f64,
652 chain_ratio
653 .iter()
654 .filter(|&&r| r > 1.0 - self.headroom)
655 .count() as f64,
656 round6(sum),
657 round6(max_load),
658 ];
659 Some(Evaluation {
660 score,
661 chain_latency,
662 response,
663 rate,
664 load,
665 cycle_rate,
666 })
667 }
668
669 fn timeline(
673 &self,
674 lanes: &[Vec<usize>],
675 cost: &[u64],
676 inflate: &[f64],
677 ) -> Option<(Vec<u64>, Vec<u64>)> {
678 let n = self.units.len();
679 let mut starts = vec![0u64; n];
680 let mut ends = vec![0u64; n];
681 let mut done = vec![false; n];
682 let mut lane_free = vec![0u64; lanes.len()];
683 let mut progressed = true;
684 let mut remaining = n;
685 while remaining > 0 && progressed {
686 progressed = false;
687 for (l, lane) in lanes.iter().enumerate() {
688 let Some(&o) = lane.iter().find(|&&o| !done[o]) else {
689 continue;
690 };
691 if self.preds[o].iter().any(|&p| !done[p]) {
692 continue;
693 }
694 let release = self.preds[o].iter().map(|&p| ends[p]).max().unwrap_or(0);
695 let start = lane_free[l].max(release);
696 starts[o] = start;
697 ends[o] = start + (cost[o] as f64 * inflate[l]) as u64;
698 lane_free[l] = ends[o];
699 done[o] = true;
700 remaining -= 1;
701 progressed = true;
702 }
703 }
704 (remaining == 0).then_some((starts, ends))
705 }
706
707 fn window_timeline(
713 &self,
714 lanes: &[Vec<usize>],
715 inflate: &[f64],
716 windows: usize,
717 ) -> Option<(Vec<u64>, Vec<u64>)> {
718 let n = self.units.len();
719 let per_window = self.window_cost.len();
720 let cycles = per_window * windows;
721 let ring =
722 (self.max_in_flight / self.inventory.copperlists_per_cycle.max(1)).max(1) as usize;
723 let mut starts = vec![0u64; n * cycles];
724 let mut ends = vec![0u64; n * cycles];
725 let mut done = vec![false; n * cycles];
726 let mut remaining = vec![n; cycles];
727 let mut cycle_end = vec![0u64; cycles];
728 let mut admission = vec![0u64; cycles];
729 let mut lane_free = vec![0u64; lanes.len()];
730 let mut position = vec![0usize; lanes.len()];
731 let mut progressed = true;
732 while progressed {
733 progressed = false;
734 for (l, lane) in lanes.iter().enumerate() {
735 while position[l] < lane.len() * cycles {
736 let (cycle, o) = (position[l] / lane.len(), lane[position[l] % lane.len()]);
737 let at = |unit: usize| cycle * n + unit;
738 if cycle >= ring && remaining[cycle - ring] > 0 {
739 break;
740 }
741 if self.preds[o].iter().any(|&p| !done[at(p)]) {
742 break;
743 }
744 if admission[cycle] == 0 {
745 admission[cycle] = (cycle as u64 * self.cycle_ns)
746 .max(if cycle >= ring {
747 cycle_end[cycle - ring]
748 } else {
749 0
750 })
751 .max(if cycle > 0 { admission[cycle - 1] } else { 0 });
752 }
753 let release = self.preds[o]
754 .iter()
755 .map(|&p| ends[at(p)])
756 .fold(admission[cycle], u64::max);
757 let start = lane_free[l].max(release);
758 starts[at(o)] = start;
759 ends[at(o)] = start
760 + (self.window_cost[cycle % per_window][o] as f64 * inflate[l]) as u64;
761 lane_free[l] = ends[at(o)];
762 cycle_end[cycle] = cycle_end[cycle].max(ends[at(o)]);
763 done[at(o)] = true;
764 remaining[cycle] -= 1;
765 position[l] += 1;
766 progressed = true;
767 }
768 }
769 }
770 remaining.iter().all(|&r| r == 0).then_some((starts, ends))
771 }
772
773 fn contract_deadline(&self, chain: usize) -> u32 {
774 self.chain_deadlines[chain]
775 }
776}
777
778fn round6(value: f64) -> f64 {
779 (value * 1e6 + 0.5) as u64 as f64 / 1e6
780}
781
782fn reachable(adj: &[Vec<usize>], start: usize) -> Vec<bool> {
784 let mut seen = vec![false; adj.len()];
785 let mut stack = vec![start];
786 seen[start] = true;
787 while let Some(u) = stack.pop() {
788 for &v in &adj[u] {
789 if !seen[v] {
790 seen[v] = true;
791 stack.push(v);
792 }
793 }
794 }
795 seen
796}
797
798fn components(preds: &[Vec<usize>]) -> Vec<usize> {
800 let n = preds.len();
801 let mut succs: Vec<Vec<usize>> = vec![Vec::new(); n];
802 for (to, from) in preds.iter().enumerate() {
803 for &from in from {
804 succs[from].push(to);
805 }
806 }
807 let mut component = vec![usize::MAX; n];
808 let mut count = 0;
809 for start in 0..n {
810 if component[start] != usize::MAX {
811 continue;
812 }
813 let mut stack = vec![start];
814 component[start] = count;
815 while let Some(u) = stack.pop() {
816 for &v in preds[u].iter().chain(&succs[u]) {
817 if component[v] == usize::MAX {
818 component[v] = count;
819 stack.push(v);
820 }
821 }
822 }
823 count += 1;
824 }
825 component
826}
827
828fn kahn_count(succs: &[Vec<usize>], mut incoming: Vec<usize>) -> usize {
831 let mut ready: VecDeque<usize> = (0..succs.len()).filter(|&i| incoming[i] == 0).collect();
832 let mut seen = 0;
833 while let Some(node) = ready.pop_front() {
834 seen += 1;
835 for &next in &succs[node] {
836 incoming[next] -= 1;
837 if incoming[next] == 0 {
838 ready.push_back(next);
839 }
840 }
841 }
842 seen
843}
844
845fn fixpoint(c: u64, hp: &[(u64, u64)], g: u64, cap: u64) -> Option<u64> {
847 let mut x = c;
848 loop {
849 let next = c + hp
850 .iter()
851 .map(|&(cost, period)| (x + g).div_ceil(period.max(1)) * cost)
852 .sum::<u64>();
853 if next == x {
854 return Some(x);
855 }
856 if next > cap {
857 return None;
858 }
859 x = next;
860 }
861}
862
863fn task_of_key(key: &str) -> Option<&str> {
865 key.split('|').find_map(|part| part.strip_prefix("task:"))
866}
867
868fn is_refine(key: &str) -> bool {
869 key.contains("|phase:refine:")
870}
871
872fn first_unit_on_a_cycle(preds: &[Vec<usize>]) -> Option<usize> {
874 let mut incoming: Vec<usize> = preds.iter().map(Vec::len).collect();
875 let mut succs: Vec<Vec<usize>> = vec![Vec::new(); preds.len()];
876 for (to, from) in preds.iter().enumerate() {
877 for &from in from {
878 succs[from].push(to);
879 }
880 }
881 let mut ready: VecDeque<usize> = (0..preds.len()).filter(|&i| incoming[i] == 0).collect();
882 while let Some(unit) = ready.pop_front() {
883 for &next in &succs[unit] {
884 incoming[next] -= 1;
885 if incoming[next] == 0 {
886 ready.push_back(next);
887 }
888 }
889 }
890 let mut left: Vec<bool> = incoming.iter().map(|&n| n > 0).collect();
894 loop {
895 let stripped = (0..preds.len()).find(|&i| left[i] && succs[i].iter().all(|&s| !left[s]));
896 match stripped {
897 Some(i) => left[i] = false,
898 None => break,
899 }
900 }
901 (0..preds.len()).find(|&i| left[i])
902}
903
904pub(super) fn rate_deficit(rate: f64) -> f64 {
908 let deficit = (1.0 - RATE_TOLERANCE - rate).max(0.0);
909 (deficit / RATE_STEP).ceil() * RATE_STEP
910}
911
912struct Rng(u64);
914
915impl Rng {
916 fn next(&mut self) -> u64 {
917 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
918 let mut z = self.0;
919 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
920 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
921 z ^ (z >> 31)
922 }
923
924 fn below(&mut self, n: usize) -> usize {
925 (self.next() % n.max(1) as u64) as usize
926 }
927}
928
929impl Model {
930 fn start_plan(&self) -> Assignment {
935 let n = self.units.len();
936 let cpus = self.cpus.len();
937 let tiers = self.tiers();
938 let mut incoming = vec![0usize; n];
939 let mut succs: Vec<Vec<usize>> = vec![Vec::new(); n];
940 for (to, preds) in self.preds.iter().enumerate() {
941 for &from in preds {
942 succs[from].push(to);
943 incoming[to] += 1;
944 }
945 }
946 let mut ready: VecDeque<usize> = (0..n).filter(|&i| incoming[i] == 0).collect();
947 let mut order = Vec::with_capacity(n);
948 while let Some(o) = ready.pop_front() {
949 order.push(o);
950 for &next in &succs[o] {
951 incoming[next] -= 1;
952 if incoming[next] == 0 {
953 ready.push_back(next);
954 }
955 }
956 }
957 let mut asap = vec![0u64; n];
958 let mut position = vec![0usize; n];
959 for (i, &o) in order.iter().enumerate() {
960 position[o] = i;
961 asap[o] = self.preds[o]
962 .iter()
963 .map(|&p| asap[p] + self.cost[p])
964 .max()
965 .unwrap_or(0);
966 }
967 let component = components(&self.preds);
968 let count = component.iter().max().map_or(0, |&c| c + 1);
969 let unit_load = |o: usize| self.window_cost.iter().map(|cycle| cycle[o]).sum::<u64>();
970 let mut members: Vec<Vec<usize>> = vec![Vec::new(); count];
971 let mut deadline = vec![u64::MAX; count];
972 for o in 0..n {
973 members[component[o]].push(o);
974 }
975 for (chain, pairs) in self.chains.iter().enumerate() {
976 for &(_, sink) in pairs {
977 let c = &mut deadline[component[sink]];
978 *c = (*c).min(u64::from(self.chain_deadlines[chain]) * 1_000_000);
979 }
980 }
981 let mut components: Vec<usize> = (0..count).collect();
982 for c in &components {
983 members[*c].sort_by_key(|&o| (asap[o], position[o]));
984 }
985 let comp_load: Vec<u64> = members
986 .iter()
987 .map(|units| units.iter().map(|&o| unit_load(o)).sum())
988 .collect();
989 components.sort_by_key(|&c| (deadline[c], core::cmp::Reverse(comp_load[c]), c));
990 let share = comp_load.iter().sum::<u64>() / cpus as u64;
991 let mut lanes = vec![Vec::new(); cpus * tiers];
992 let mut lane_load = vec![0u64; cpus * tiers];
993 let mut low = Vec::new();
994 for &c in &components {
995 if tiers > 1 && members[c].iter().all(|&o| self.short[o]) {
996 let lane = cpus
997 + (0..cpus)
998 .min_by_key(|&i| (lane_load[cpus + i], i))
999 .expect("at least one CPU");
1000 lanes[lane].extend(&members[c]);
1001 lane_load[lane] += comp_load[c];
1002 } else {
1003 low.extend(&members[c]);
1004 }
1005 }
1006 let capacity = |stage: usize| {
1007 share.saturating_sub(if tiers > 1 {
1008 lane_load[cpus + stage]
1009 } else {
1010 0
1011 })
1012 };
1013 let (mut stage, mut acc, mut boundary) = (0, 0u64, capacity(0));
1014 for o in low {
1015 let w = unit_load(o);
1016 if stage + 1 < cpus && acc + w / 2 > boundary {
1017 stage += 1;
1018 boundary += capacity(stage);
1019 }
1020 lanes[stage].push(o);
1021 acc += w;
1022 }
1023 Assignment { lanes }
1024 }
1025
1026 fn neighbour(&self, assignment: &Assignment, rng: &mut Rng) -> Assignment {
1030 let mut lanes = assignment.lanes.clone();
1031 let occupied: Vec<usize> = (0..lanes.len()).filter(|&l| !lanes[l].is_empty()).collect();
1032 if occupied.is_empty() {
1033 return assignment.clone();
1034 }
1035 if rng.below(2) == 0 || lanes.len() == 1 {
1036 let from = occupied[rng.below(occupied.len())];
1037 let at = rng.below(lanes[from].len());
1038 let o = lanes[from].remove(at);
1039 let tier = self.lane_tier(from);
1041 let cpus = self.cpus.len();
1042 let to = tier * cpus + rng.below(cpus);
1043 let position = rng.below(lanes[to].len() + 1);
1044 lanes[to].insert(position, o);
1045 } else {
1046 let wide: Vec<usize> = occupied
1047 .iter()
1048 .copied()
1049 .filter(|&l| lanes[l].len() > 1)
1050 .collect();
1051 if wide.is_empty() {
1052 return assignment.clone();
1053 }
1054 let lane = wide[rng.below(wide.len())];
1055 let at = rng.below(lanes[lane].len() - 1);
1056 lanes[lane].swap(at, at + 1);
1057 }
1058 Assignment { lanes }
1059 }
1060
1061 fn search(&self, seed: u64, moves: usize, restarts: usize) -> Vec<(Vec<f64>, Assignment)> {
1064 let mut seen: BTreeMap<Assignment, Option<Vec<f64>>> = BTreeMap::new();
1065 let score = |assignment: &Assignment,
1066 seen: &mut BTreeMap<Assignment, Option<Vec<f64>>>|
1067 -> Option<Vec<f64>> {
1068 if let Some(known) = seen.get(assignment) {
1069 return known.clone();
1070 }
1071 let value = if self.is_valid(assignment) {
1072 self.evaluate(assignment).map(|evaluation| evaluation.score)
1073 } else {
1074 None
1075 };
1076 seen.insert(assignment.clone(), value.clone());
1077 value
1078 };
1079 let start = self.start_plan();
1080 for attempt in 0..restarts.max(1) {
1081 let mut rng = Rng(seed.wrapping_add(attempt as u64));
1082 let mut current = if attempt == 0 {
1083 start.clone()
1084 } else {
1085 let mut random = start.clone();
1086 for _ in 0..self.units.len() * 2 {
1087 let candidate = self.neighbour(&random, &mut rng);
1088 if self.is_valid(&candidate) {
1089 random = candidate;
1090 }
1091 }
1092 random
1093 };
1094 let mut current_score = score(¤t, &mut seen);
1095 let mut best = current.clone();
1096 let mut best_score = current_score.clone();
1097 let mut stall = 0;
1098 for _ in 0..moves {
1099 let candidate = self.neighbour(¤t, &mut rng);
1100 let candidate_score = score(&candidate, &mut seen);
1101 let better = match (&candidate_score, ¤t_score) {
1102 (Some(c), Some(cur)) => better_score(c, cur),
1103 (Some(_), None) => true,
1104 _ => false,
1105 };
1106 if better {
1107 current = candidate;
1108 current_score = candidate_score;
1109 stall = 0;
1110 if best_score
1111 .as_ref()
1112 .is_none_or(|b| better_score(current_score.as_ref().unwrap(), b))
1113 {
1114 best = current.clone();
1115 best_score = current_score.clone();
1116 }
1117 } else {
1118 stall += 1;
1119 if stall >= PATIENCE {
1120 current = best.clone();
1121 for _ in 0..KICK {
1122 let kicked = self.neighbour(¤t, &mut rng);
1123 if self.is_valid(&kicked) {
1124 current = kicked;
1125 }
1126 }
1127 current_score = score(¤t, &mut seen);
1128 stall = 0;
1129 }
1130 }
1131 }
1132 }
1133 let mut scored: Vec<(Vec<f64>, Assignment)> = seen
1134 .into_iter()
1135 .filter_map(|(assignment, score)| score.map(|score| (score, assignment)))
1136 .collect();
1137 scored.sort_by(|a, b| {
1138 if better_score(&a.0, &b.0) {
1139 core::cmp::Ordering::Less
1140 } else if better_score(&b.0, &a.0) {
1141 core::cmp::Ordering::Greater
1142 } else {
1143 a.1.cmp(&b.1)
1144 }
1145 });
1146 scored
1147 }
1148
1149 fn materialize(&self, assignment: &Assignment, max_in_flight: u32) -> CuMissionPlan {
1151 let mut mission = self.inventory.clone();
1152 mission.max_in_flight = max_in_flight.max(mission.copperlists_per_cycle);
1153 mission.workers = assignment
1154 .lanes
1155 .iter()
1156 .enumerate()
1157 .filter(|(_, lane)| !lane.is_empty())
1158 .map(|(l, lane)| CuPlanWorker {
1159 id: self.worker_id(l),
1160 placement: CuPlanPlacement::Thread {
1161 cpu: Some(self.lane_cpu(l)),
1162 policy: self.worker_policy(l),
1163 },
1164 steps: lane
1165 .iter()
1166 .flat_map(|&u| self.units[u].iter().map(|&o| o as u32))
1167 .collect(),
1168 })
1169 .collect();
1170 mission.dispatcher = Some(self.dispatcher());
1171 mission
1172 }
1173
1174 fn prediction(&self, assignment: &Assignment, contract: &CuContract) -> CuPrediction {
1175 let evaluation = self
1176 .evaluate(assignment)
1177 .expect("a ranked assignment evaluates");
1178 let chains = contract
1179 .chains
1180 .iter()
1181 .zip(&self.chains)
1182 .enumerate()
1183 .map(|(index, (chain, _))| {
1184 let deadline_ns = u64::from(chain.deadline_ms) * 1_000_000;
1185 let latency_ns = evaluation.chain_latency[index];
1186 (
1187 chain.id.clone(),
1188 CuChainPrediction {
1189 latency_ns,
1190 deadline_ns,
1191 ratio: latency_ns as f64 / deadline_ns as f64,
1192 },
1193 )
1194 })
1195 .collect();
1196 let workers = assignment
1197 .lanes
1198 .iter()
1199 .enumerate()
1200 .filter(|(_, lane)| !lane.is_empty())
1201 .map(|(l, _)| {
1202 (
1203 self.worker_id(l),
1204 CuWorkerPrediction {
1205 cpu: self.lane_cpu(l),
1206 load: evaluation.load[l],
1207 response_ns: evaluation.response[l],
1208 rate: evaluation.rate[l],
1209 },
1210 )
1211 })
1212 .collect();
1213 let sources = contract
1214 .sources
1215 .iter()
1216 .map(|source| (source.task.clone(), evaluation.cycle_rate))
1217 .collect();
1218 CuPrediction {
1219 score: evaluation.score,
1220 chains,
1221 workers,
1222 sources,
1223 }
1224 }
1225}
1226
1227fn better_score(a: &[f64], b: &[f64]) -> bool {
1229 for (x, y) in a.iter().zip(b) {
1230 if x < y {
1231 return true;
1232 }
1233 if x > y {
1234 return false;
1235 }
1236 }
1237 false
1238}
1239
1240fn same_candidate(a: &[f64], b: &[f64], sum_tier: usize) -> bool {
1243 a[..sum_tier] == b[..sum_tier]
1244 && (a[sum_tier] - b[sum_tier]).abs() <= MIN_SEPARATION * b[sum_tier].abs().max(1e-9)
1245}
1246
1247pub fn propose(request: &ProposeRequest<'_>) -> CuResult<Vec<CuCandidate>> {
1252 if request.candidates == 0 {
1253 return Err(CuError::from(
1254 "At least one placement candidate is required",
1255 ));
1256 }
1257 request
1258 .contract
1259 .validate(request.config, Some(request.mission))?;
1260 let graph = request.config.get_graph(Some(request.mission))?;
1261 let signature = super::graph_signature(graph, Some(request.mission));
1262 if request.profile.config_signature != signature {
1263 return Err(CuError::from(format!(
1264 "The profile was recorded on another graph ({}); this config's mission '{}' is {signature}",
1265 request.profile.config_signature, request.mission
1266 )));
1267 }
1268 let baseline = CuPlan::from_config(request.config)?;
1269 let mut cycles = vec![1];
1270 if request.contract.max_in_flight >= 2
1271 && graph
1272 .get_all_nodes()
1273 .iter()
1274 .any(|(_, node)| node.get_declared_task_kind() == Some(TaskKind::Stateless))
1275 {
1276 cycles.push(2);
1277 }
1278 let mut candidates = Vec::new();
1279 for copperlists_per_cycle in cycles {
1280 for max_in_flight in copperlists_per_cycle..=request.contract.max_in_flight {
1281 candidates.extend(propose_shape(
1282 request,
1283 copperlists_per_cycle,
1284 max_in_flight,
1285 &baseline.concurrent_resources,
1286 )?);
1287 }
1288 }
1289 candidates.sort_by(|left, right| {
1290 if better_score(&left.prediction.score, &right.prediction.score) {
1291 core::cmp::Ordering::Less
1292 } else if better_score(&right.prediction.score, &left.prediction.score) {
1293 core::cmp::Ordering::Greater
1294 } else {
1295 core::cmp::Ordering::Equal
1296 }
1297 });
1298 let mut chosen: Vec<CuCandidate> = Vec::new();
1299 for candidate in candidates {
1300 if chosen.len() >= request.candidates {
1301 break;
1302 }
1303 if chosen
1304 .iter()
1305 .any(|known| same_candidate(&candidate.prediction.score, &known.prediction.score, 3))
1306 {
1307 continue;
1308 }
1309 chosen.push(candidate);
1310 }
1311 if chosen.is_empty() {
1312 return Err(CuError::from(
1313 "No valid plan was found on this contract's CPUs",
1314 ));
1315 }
1316 Ok(chosen)
1317}
1318
1319fn propose_shape(
1320 request: &ProposeRequest<'_>,
1321 copperlists_per_cycle: u32,
1322 max_in_flight: u32,
1323 concurrent_resources: &[String],
1324) -> CuResult<Vec<CuCandidate>> {
1325 let exported = CuPlan::from_config_cyclic_with_resources(
1326 request.config,
1327 copperlists_per_cycle,
1328 concurrent_resources,
1329 )?;
1330 let inventory = exported
1331 .missions
1332 .get(request.mission)
1333 .ok_or_else(|| CuError::from(format!("Unknown mission '{}'", request.mission)))?
1334 .clone();
1335 let model = Model::new(request, inventory, max_in_flight)?;
1336 let seed = SEARCH_SEED ^ (u64::from(copperlists_per_cycle) << 32) ^ u64::from(max_in_flight);
1337 let scored = model.search(seed, SEARCH_MOVES, SEARCH_RESTARTS);
1338 let mut chosen: Vec<(Vec<f64>, Assignment)> = Vec::new();
1339 for (score, assignment) in scored {
1340 if chosen.len() >= request.candidates {
1341 break;
1342 }
1343 if chosen
1344 .iter()
1345 .any(|(known, _)| same_candidate(&score, known, 3))
1346 {
1347 continue;
1348 }
1349 chosen.push((score, assignment));
1350 }
1351 let mut candidates = Vec::with_capacity(chosen.len());
1352 for (_, assignment) in chosen {
1353 let mission = model.materialize(&assignment, max_in_flight);
1354 let mut plan = exported.clone();
1355 plan.missions.insert(request.mission.to_string(), mission);
1356 let mut prepared = request.config.clone();
1357 plan.provide_capacity(&mut prepared);
1358 if plan.validate(&prepared).is_err() {
1359 continue;
1360 }
1361 candidates.push(CuCandidate {
1362 prediction: model.prediction(&assignment, request.contract),
1363 plan,
1364 });
1365 }
1366 Ok(candidates)
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371 use super::*;
1372 use crate::planner::CuChain;
1373 use crate::planner::CuCostStats;
1374 use crate::planner::CuOperationProfile;
1375 use crate::planner::CuSourceRate;
1376
1377 fn config() -> CuConfig {
1378 CuConfig::deserialize_ron(
1379 r#"(
1380 runtime: (rate_target_hz: 100),
1381 logging: (copperlist_count: 4),
1382 tasks: [(id: "src", type: "Source"), (id: "left", type: "Left"),
1383 (id: "right", type: "Right", kind: stateless_task), (id: "sink", type: "Sink")],
1384 cnx: [(src: "src", dst: "left", msg: "u32"), (src: "src", dst: "right", msg: "u32"),
1385 (src: "left", dst: "sink", msg: "u32"), (src: "right", dst: "sink", msg: "u32")],
1386 )"#,
1387 )
1388 .unwrap()
1389 }
1390
1391 fn contract(cpus: Vec<usize>) -> CuContract {
1392 CuContract {
1393 chains: vec![CuChain {
1394 id: "hot".into(),
1395 source: "src".into(),
1396 sink: "sink".into(),
1397 deadline_ms: 10,
1398 }],
1399 sources: vec![CuSourceRate {
1400 task: "src".into(),
1401 period_ms: 10,
1402 }],
1403 cpus,
1404 max_in_flight: 2,
1405 headroom: 0.2,
1406 worker_policy: SchedulingPolicy::Fair,
1407 }
1408 }
1409
1410 fn profile() -> CuProfile {
1412 let graph = config().get_graph(Some("default")).unwrap().clone();
1413 let mut profile = CuProfile::new(
1414 crate::planner::graph_signature(&graph, Some("default")),
1415 "default".into(),
1416 );
1417 profile.copperlists = 100;
1418 profile.window_ns = 1_000_000_000;
1419 for (task, cost) in [
1420 ("src", 100_000),
1421 ("left", 3_000_000),
1422 ("right", 3_000_000),
1423 ("sink", 100_000),
1424 ] {
1425 let mut samples = vec![cost; 100];
1426 profile.operations.insert(
1427 format!("mission:default|task:{task}|phase:whole"),
1428 CuOperationProfile {
1429 fired: CuCostStats::from_samples(&mut samples),
1430 skipped: CuCostStats::default(),
1431 firing_rate_hz: 100.0,
1432 firing: None,
1433 },
1434 );
1435 }
1436 profile
1437 }
1438
1439 fn request<'a>(
1440 config: &'a CuConfig,
1441 contract: &'a CuContract,
1442 profile: &'a CuProfile,
1443 ) -> ProposeRequest<'a> {
1444 ProposeRequest {
1445 config,
1446 mission: "default",
1447 contract,
1448 profile,
1449 candidates: 3,
1450 }
1451 }
1452
1453 #[test]
1454 fn parallel_branches_land_on_two_cpus_and_shorten_the_chain() {
1455 let config = config();
1456 let profile = profile();
1457 let one = contract(vec![0]);
1458 let serial = propose(&request(&config, &one, &profile)).unwrap();
1459 let two = contract(vec![0, 1]);
1460 let parallel = propose(&request(&config, &two, &profile)).unwrap();
1461 let serial_latency = serial[0].prediction.chains["hot"].latency_ns;
1462 let parallel_latency = parallel[0].prediction.chains["hot"].latency_ns;
1463 assert_eq!(serial_latency, 6_200_000);
1464 assert_eq!(parallel_latency, 3_200_000);
1465 assert_eq!(parallel[0].plan.missions["default"].workers.len(), 2);
1466 for candidate in serial.iter().chain(¶llel) {
1467 candidate.plan.validate(&config).unwrap();
1468 assert_eq!(candidate.prediction.sources["src"], 1.0);
1469 }
1470 let again = propose(&request(&config, &two, &profile)).unwrap();
1472 assert_eq!(again, parallel);
1473 }
1474
1475 #[test]
1476 fn a_chain_is_timed_in_a_cycle_where_its_steps_fire() {
1477 let config = config();
1478 let mut profile = profile();
1479 let left = profile
1482 .operations
1483 .get_mut("mission:default|task:left|phase:whole")
1484 .unwrap();
1485 left.fired = CuCostStats::from_samples(&mut [3_000_000; 10]);
1486 left.skipped = CuCostStats::from_samples(&mut vec![1_000; 90]);
1487 let one = contract(vec![0]);
1488 let serial = propose(&request(&config, &one, &profile)).unwrap();
1489 assert_eq!(serial[0].prediction.chains["hot"].latency_ns, 6_200_000);
1490 let load = serial[0].prediction.workers["cpu0"].load;
1491 assert!((0.32..0.36).contains(&load), "{load}");
1492 }
1493
1494 #[test]
1495 fn two_copperlists_per_cycle_let_the_stateless_task_overlap() {
1496 let config = config();
1497 let profile = profile();
1498 let two = contract(vec![0, 1, 2]);
1499 let candidates = propose(&request(&config, &two, &profile)).unwrap();
1500 let best = &candidates[0];
1501 let mission = &best.plan.missions["default"];
1502 assert_eq!(
1503 (mission.copperlists_per_cycle, mission.max_in_flight),
1504 (2, 2)
1505 );
1506 best.plan.validate(&config).unwrap();
1507 assert!(best.prediction.chains["hot"].latency_ns <= 6_200_000);
1509 assert!(best.prediction.workers.values().all(|w| w.load < 1.0));
1510 }
1511
1512 #[test]
1513 fn anytime_phases_stay_together_on_one_worker() {
1514 let config = CuConfig::deserialize_ron(
1515 r#"(
1516 runtime: (rate_target_hz: 100),
1517 logging: (copperlist_count: 2),
1518 tasks: [(id: "src", type: "Source"),
1519 (id: "refiner", type: "Refiner", anytime: (max_refines: 2)),
1520 (id: "sink", type: "Sink")],
1521 cnx: [(src: "src", dst: "refiner", msg: "u32"), (src: "refiner", dst: "sink", msg: "u32")],
1522 )"#,
1523 )
1524 .unwrap();
1525 let graph = config.get_graph(Some("default")).unwrap();
1526 let mut profile = CuProfile::new(
1527 crate::planner::graph_signature(graph, Some("default")),
1528 "default".into(),
1529 );
1530 profile.copperlists = 100;
1531 profile.window_ns = 1_000_000_000;
1532 for (key, cost) in [
1533 ("mission:default|task:src|phase:whole", 100_000),
1534 ("mission:default|task:refiner|phase:base", 3_000_000),
1535 ("mission:default|task:sink|phase:whole", 100_000),
1536 ] {
1537 let mut samples = vec![cost; 100];
1538 profile.operations.insert(
1539 key.into(),
1540 CuOperationProfile {
1541 fired: CuCostStats::from_samples(&mut samples),
1542 skipped: CuCostStats::default(),
1543 firing_rate_hz: 100.0,
1544 firing: None,
1545 },
1546 );
1547 }
1548 let mut contract = contract(vec![0, 1]);
1549 contract.chains[0].sink = "sink".into();
1550 contract.max_in_flight = 1;
1551 let candidates = propose(&request(&config, &contract, &profile)).unwrap();
1552 for candidate in &candidates {
1553 let mission = &candidate.plan.missions["default"];
1554 let refine = |key: &str| {
1555 mission
1556 .steps
1557 .iter()
1558 .position(|s| s.key.contains(key))
1559 .unwrap() as u32
1560 };
1561 let (base, one, two) = (refine("phase:base"), refine("refine:1"), refine("refine:2"));
1562 let worker = mission
1563 .workers
1564 .iter()
1565 .find(|w| w.steps.contains(&base))
1566 .unwrap();
1567 let at = |step| worker.steps.iter().position(|&s| s == step).unwrap();
1568 assert_eq!((at(one), at(two)), (at(base) + 1, at(base) + 2));
1569 }
1570 assert_eq!(candidates[0].prediction.chains["hot"].latency_ns, 3_200_000);
1571 }
1572
1573 #[test]
1574 fn fifo_workers_come_in_two_tiers_and_lanes_form_a_pipeline() {
1575 let config = config();
1576 let profile = profile();
1577 let mut contract = contract(vec![0, 1]);
1578 contract.worker_policy = SchedulingPolicy::Fifo { priority: 60 };
1579 let candidates = propose(&request(&config, &contract, &profile)).unwrap();
1580 for candidate in &candidates {
1581 let mission = &candidate.plan.missions["default"];
1582 let mut lane_of = BTreeMap::new();
1583 for (l, worker) in mission.workers.iter().enumerate() {
1584 let priority = match worker.placement {
1585 CuPlanPlacement::Thread {
1586 policy: SchedulingPolicy::Fifo { priority },
1587 ..
1588 } => priority,
1589 _ => panic!("{}: not a FIFO thread", worker.id),
1590 };
1591 assert_eq!(priority, if worker.id.ends_with("-hi") { 61 } else { 60 });
1592 assert!(worker.id.ends_with("-hi"), "{}", worker.id);
1595 for &step in &worker.steps {
1596 lane_of.insert(step, l);
1597 }
1598 }
1599 let mut edges: BTreeSet<(usize, usize)> = mission
1601 .dependencies
1602 .iter()
1603 .filter(|edge| edge.cycle_lag == 0)
1604 .map(|edge| (lane_of[&edge.from], lane_of[&edge.to]))
1605 .filter(|(a, b)| a != b)
1606 .collect();
1607 let mut lanes: BTreeSet<usize> = lane_of.values().copied().collect();
1608 while let Some(&first) = lanes.iter().find(|&&l| !edges.iter().any(|&(_, b)| b == l)) {
1609 lanes.remove(&first);
1610 edges.retain(|&(a, _)| a != first);
1611 }
1612 assert!(lanes.is_empty(), "lanes wait on each other: {edges:?}");
1613 assert_eq!(
1614 mission.dispatcher.as_ref().unwrap().policy,
1615 SchedulingPolicy::Fifo { priority: 62 }
1616 );
1617 }
1618 assert_eq!(candidates[0].prediction.chains["hot"].latency_ns, 3_200_000);
1619 }
1620
1621 #[test]
1622 fn a_missing_measurement_is_an_error() {
1623 let config = config();
1624 let mut profile = profile();
1625 profile
1626 .operations
1627 .remove("mission:default|task:left|phase:whole");
1628 let one = contract(vec![0]);
1629 assert!(propose(&request(&config, &one, &profile)).is_err());
1630 }
1631}