1use std::sync::atomic::{AtomicU64, Ordering};
5
6use super::{
7 AdmissionKind, CandidateView, RouteCandidate, RouteContext, RouteDecision, RouteDevice,
8 RoutePolicy, RouteTarget,
9};
10
11#[derive(Debug)]
12pub(crate) struct RoutePicker {
13 policy: RoutePolicy,
14 round_robin_cursor: AtomicU64,
15}
16
17#[derive(Debug)]
23pub struct BuiltinRoutePicker {
24 inner: RoutePicker,
25}
26
27impl BuiltinRoutePicker {
28 pub const fn round_robin() -> Self {
29 Self {
30 inner: RoutePicker::new(RoutePolicy::RoundRobin),
31 }
32 }
33
34 pub const fn random() -> Self {
35 Self {
36 inner: RoutePicker::new(RoutePolicy::Random),
37 }
38 }
39
40 pub const fn power_of_two_choices() -> Self {
41 Self {
42 inner: RoutePicker::new(RoutePolicy::PowerOfTwoChoices),
43 }
44 }
45
46 pub const fn least_loaded() -> Self {
47 Self {
48 inner: RoutePicker::new(RoutePolicy::LeastLoaded),
49 }
50 }
51
52 pub fn select_worker(&self, worker_ids: &[u64], load: impl Fn(u64) -> u64) -> Option<u64> {
54 self.inner
55 .select(
56 CandidateView::Workers(worker_ids),
57 RouteContext::default(),
58 load,
59 )
60 .map(|decision| decision.target.worker_id)
61 }
62
63 pub fn peek_worker(&self, worker_ids: &[u64], load: impl Fn(u64) -> u64) -> Option<u64> {
65 self.inner
66 .peek(
67 CandidateView::Workers(worker_ids),
68 RouteContext::default(),
69 load,
70 )
71 .map(|decision| decision.target.worker_id)
72 }
73}
74
75impl RoutePicker {
76 pub(crate) const fn new(policy: RoutePolicy) -> Self {
77 Self {
78 policy,
79 round_robin_cursor: AtomicU64::new(0),
80 }
81 }
82
83 pub(crate) const fn policy(&self) -> RoutePolicy {
84 self.policy
85 }
86
87 #[inline(always)]
88 pub(crate) fn peek(
89 &self,
90 candidates: CandidateView<'_>,
91 context: RouteContext,
92 load: impl Fn(u64) -> u64,
93 ) -> Option<RouteDecision> {
94 let mut samples = RandomSamples;
95 self.choose_with_samples(candidates, context, &load, false, &mut samples)
96 }
97
98 #[inline(always)]
99 pub(crate) fn select(
100 &self,
101 candidates: CandidateView<'_>,
102 context: RouteContext,
103 load: impl Fn(u64) -> u64,
104 ) -> Option<RouteDecision> {
105 let mut samples = RandomSamples;
106 self.choose_with_samples(candidates, context, &load, true, &mut samples)
107 }
108
109 #[inline(always)]
110 fn choose_stateless_index(
111 &self,
112 candidate_count: usize,
113 commit: bool,
114 samples: &mut impl SampleSource,
115 ) -> Option<usize> {
116 if candidate_count == 0 {
117 return None;
118 }
119
120 match self.policy {
121 RoutePolicy::RoundRobin => {
122 let cursor = if commit {
123 self.round_robin_cursor.fetch_add(1, Ordering::Relaxed)
124 } else {
125 self.round_robin_cursor.load(Ordering::Relaxed)
126 };
127 Some(cursor as usize % candidate_count)
128 }
129 RoutePolicy::Random => Some(samples.index(candidate_count)),
130 RoutePolicy::PowerOfTwoChoices
131 | RoutePolicy::LeastLoaded
132 | RoutePolicy::DeviceAwareWeighted => None,
133 }
134 }
135
136 #[inline(always)]
137 fn choose_with_samples(
138 &self,
139 candidates: CandidateView<'_>,
140 context: RouteContext,
141 load: &impl Fn(u64) -> u64,
142 commit: bool,
143 samples: &mut impl SampleSource,
144 ) -> Option<RouteDecision> {
145 if candidates.is_empty() {
146 return None;
147 }
148
149 match self.policy {
150 RoutePolicy::RoundRobin | RoutePolicy::Random => Some(RouteDecision {
151 target: candidates.target(self.choose_stateless_index(
152 candidates.len(),
153 commit,
154 samples,
155 )?),
156 admission: AdmissionKind::None,
157 }),
158 RoutePolicy::PowerOfTwoChoices => {
159 let first = samples.index(candidates.len());
160 if candidates.len() == 1 {
161 return Some(RouteDecision {
162 target: candidates.target(first),
163 admission: AdmissionKind::Occupancy,
164 });
165 }
166 let second_offset = 1 + samples.index(candidates.len() - 1);
167 let second = (first + second_offset) % candidates.len();
168 let first_target = candidates.target(first);
169 let second_target = candidates.target(second);
170 let target = if load(first_target.worker_id) <= load(second_target.worker_id) {
171 first_target
172 } else {
173 second_target
174 };
175 Some(RouteDecision {
176 target,
177 admission: AdmissionKind::Occupancy,
178 })
179 }
180 RoutePolicy::LeastLoaded => {
181 lowest_load(candidates, load, samples).map(|target| RouteDecision {
182 target,
183 admission: AdmissionKind::Occupancy,
184 })
185 }
186 RoutePolicy::DeviceAwareWeighted => {
187 let CandidateView::DeviceAware(candidates) = candidates else {
188 return None;
189 };
190 device_aware(candidates, context, load, samples)
191 }
192 }
193 }
194}
195
196trait SampleSource {
197 fn index(&mut self, upper: usize) -> usize;
198}
199
200struct RandomSamples;
201
202impl SampleSource for RandomSamples {
203 #[inline(always)]
204 fn index(&mut self, upper: usize) -> usize {
205 fastrand::usize(..upper)
206 }
207}
208
209#[inline(always)]
210fn lowest_load(
211 candidates: CandidateView<'_>,
212 load: &impl Fn(u64) -> u64,
213 samples: &mut impl SampleSource,
214) -> Option<RouteTarget> {
215 match candidates {
216 CandidateView::Workers(workers) => {
217 let mut best = None;
218 let mut best_load = u64::MAX;
219 let mut ties = 0usize;
220 for &worker_id in workers {
221 let target_load = load(worker_id);
222 if target_load < best_load {
223 best = Some(RouteTarget::worker(worker_id));
224 best_load = target_load;
225 ties = 1;
226 } else if target_load == best_load {
227 ties += 1;
228 if samples.index(ties) == 0 {
229 best = Some(RouteTarget::worker(worker_id));
230 }
231 }
232 }
233 best
234 }
235 CandidateView::DeviceAware(candidates) => {
236 let mut best = None;
237 let mut best_load = u64::MAX;
238 let mut ties = 0usize;
239 for candidate in candidates {
240 let target_load = load(candidate.target.worker_id);
241 if target_load < best_load {
242 best = Some(candidate.target);
243 best_load = target_load;
244 ties = 1;
245 } else if target_load == best_load {
246 ties += 1;
247 if samples.index(ties) == 0 {
248 best = Some(candidate.target);
249 }
250 }
251 }
252 best
253 }
254 }
255}
256
257#[derive(Default)]
258struct DeviceGroup {
259 count: u64,
260 total_load: u64,
261 best: Option<RouteTarget>,
262 best_load: u64,
263 best_ties: usize,
264}
265
266impl DeviceGroup {
267 #[inline(always)]
268 fn consider(&mut self, target: RouteTarget, target_load: u64, samples: &mut impl SampleSource) {
269 self.count += 1;
270 self.total_load = self.total_load.saturating_add(target_load);
271 if self.best.is_none() || target_load < self.best_load {
272 self.best = Some(target);
273 self.best_load = target_load;
274 self.best_ties = 1;
275 } else if target_load == self.best_load {
276 self.best_ties += 1;
277 if samples.index(self.best_ties) == 0 {
278 self.best = Some(target);
279 }
280 }
281 }
282}
283
284#[inline(always)]
285fn device_aware(
286 candidates: &[RouteCandidate],
287 context: RouteContext,
288 load: &impl Fn(u64) -> u64,
289 samples: &mut impl SampleSource,
290) -> Option<RouteDecision> {
291 let mut cpu = DeviceGroup {
292 best_load: u64::MAX,
293 ..Default::default()
294 };
295 let mut accelerator = DeviceGroup {
296 best_load: u64::MAX,
297 ..Default::default()
298 };
299 let mut full_cache = DeviceGroup {
300 best_load: u64::MAX,
301 ..Default::default()
302 };
303
304 for candidate in candidates {
305 let target_load = load(candidate.target.worker_id);
306 match candidate.device {
307 RouteDevice::Cpu => cpu.consider(candidate.target, target_load, samples),
308 RouteDevice::Accelerator => {
309 accelerator.consider(candidate.target, target_load, samples)
310 }
311 }
312 if context.required_cache_hits > 0 && candidate.cache_hits >= context.required_cache_hits {
313 full_cache.consider(candidate.target, target_load, samples);
314 }
315 }
316
317 if let Some(target) = full_cache.best {
318 return Some(RouteDecision {
319 target,
320 admission: AdmissionKind::None,
321 });
322 }
323
324 let target = match (cpu.best, accelerator.best) {
325 (None, None) => return None,
326 (Some(target), None) => target,
327 (None, Some(target)) => target,
328 (Some(cpu_target), Some(accelerator_target)) => {
329 let ratio = context.non_cpu_to_cpu_ratio.max(1) as u64;
330 let allowed_cpu = accelerator.total_load.saturating_mul(cpu.count)
331 / ratio.saturating_mul(accelerator.count);
332 if cpu.total_load < allowed_cpu {
333 cpu_target
334 } else {
335 accelerator_target
336 }
337 }
338 };
339
340 Some(RouteDecision {
341 target,
342 admission: AdmissionKind::Occupancy,
343 })
344}
345
346#[cfg(test)]
347mod tests {
348 use std::cell::Cell;
349
350 use super::*;
351
352 struct ScriptedSamples {
353 values: Vec<usize>,
354 consumed: usize,
355 }
356
357 impl ScriptedSamples {
358 fn new(values: impl Into<Vec<usize>>) -> Self {
359 Self {
360 values: values.into(),
361 consumed: 0,
362 }
363 }
364 }
365
366 impl SampleSource for ScriptedSamples {
367 fn index(&mut self, upper: usize) -> usize {
368 let value = self.values[self.consumed];
369 self.consumed += 1;
370 value % upper
371 }
372 }
373
374 #[test]
375 fn round_robin_peek_does_not_advance_selection() {
376 let picker = RoutePicker::new(RoutePolicy::RoundRobin);
377 let candidates = CandidateView::Workers(&[10, 20, 30, 40]);
378 for _ in 0..16 {
379 assert_eq!(
380 picker
381 .peek(candidates, RouteContext::default(), |_| 0)
382 .unwrap()
383 .target
384 .worker_id,
385 10
386 );
387 }
388 let selected = (0..4)
389 .map(|_| {
390 picker
391 .select(candidates, RouteContext::default(), |_| 0)
392 .unwrap()
393 .target
394 .worker_id
395 })
396 .collect::<Vec<_>>();
397 assert_eq!(selected, [10, 20, 30, 40]);
398 }
399
400 #[test]
401 fn random_peek_uses_one_independent_sample() {
402 let picker = RoutePicker::new(RoutePolicy::Random);
403 let candidates = CandidateView::Workers(&[10, 20, 30, 40]);
404 let mut samples = ScriptedSamples::new(vec![2]);
405 let decision = picker
406 .choose_with_samples(
407 candidates,
408 RouteContext::default(),
409 &|_| 0,
410 false,
411 &mut samples,
412 )
413 .unwrap();
414 assert_eq!(decision.target.worker_id, 30);
415 assert_eq!(samples.consumed, 1);
416 }
417
418 #[test]
419 fn p2c_uses_exactly_two_samples_and_two_load_reads() {
420 let picker = RoutePicker::new(RoutePolicy::PowerOfTwoChoices);
421 let candidates = CandidateView::Workers(&[10, 20, 30, 40]);
422 let mut samples = ScriptedSamples::new(vec![1, 1]);
423 let reads = Cell::new(0);
424 let decision = picker
425 .choose_with_samples(
426 candidates,
427 RouteContext::default(),
428 &|worker| {
429 reads.set(reads.get() + 1);
430 if worker == 20 { 5 } else { 1 }
431 },
432 true,
433 &mut samples,
434 )
435 .unwrap();
436 assert_eq!(decision.target.worker_id, 40);
437 assert_eq!(samples.consumed, 2);
438 assert_eq!(reads.get(), 2);
439 }
440
441 #[test]
442 fn least_loaded_ties_follow_scripted_reservoir_samples() {
443 let picker = RoutePicker::new(RoutePolicy::LeastLoaded);
444 let candidates = CandidateView::Workers(&[10, 20, 30]);
445 let mut samples = ScriptedSamples::new(vec![1, 0]);
446 let decision = picker
447 .choose_with_samples(
448 candidates,
449 RouteContext::default(),
450 &|_| 3,
451 true,
452 &mut samples,
453 )
454 .unwrap();
455 assert_eq!(decision.target.worker_id, 30);
456 assert_eq!(samples.consumed, 2);
457 }
458
459 #[test]
460 fn device_aware_scans_each_candidate_once_and_full_hits_skip_admission() {
461 let picker = RoutePicker::new(RoutePolicy::DeviceAwareWeighted);
462 let candidates = [
463 RouteCandidate {
464 target: RouteTarget::worker(10),
465 device: RouteDevice::Cpu,
466 cache_hits: 0,
467 },
468 RouteCandidate {
469 target: RouteTarget::worker(20),
470 device: RouteDevice::Accelerator,
471 cache_hits: 2,
472 },
473 RouteCandidate {
474 target: RouteTarget::worker(30),
475 device: RouteDevice::Accelerator,
476 cache_hits: 1,
477 },
478 ];
479 let reads = Cell::new(0);
480 let decision = picker
481 .peek(
482 CandidateView::DeviceAware(&candidates),
483 RouteContext {
484 required_cache_hits: 2,
485 non_cpu_to_cpu_ratio: 8,
486 },
487 |worker| {
488 reads.set(reads.get() + 1);
489 worker
490 },
491 )
492 .unwrap();
493 assert_eq!(decision.target.worker_id, 20);
494 assert_eq!(decision.admission, AdmissionKind::None);
495 assert_eq!(reads.get(), candidates.len());
496 }
497}