scx_lavd/cpu_order.rs
1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2025 Valve Corporation.
4// Author: Changwoo Min <changwoo@igalia.com>
5
6// This software may be used and distributed according to the terms of the
7// GNU General Public License version 2.
8
9use anyhow::anyhow;
10use anyhow::Result;
11use itertools::iproduct;
12use itertools::Itertools;
13use scx_utils::CoreType;
14use scx_utils::Cpumask;
15use scx_utils::EnergyModel;
16use scx_utils::PerfDomain;
17use scx_utils::PerfState;
18use scx_utils::Topology;
19use scx_utils::NR_CPU_IDS;
20use std::cell::Cell;
21use std::cell::RefCell;
22use std::collections::BTreeMap;
23use std::collections::BTreeSet;
24use std::collections::HashSet;
25use std::fmt;
26use std::hash::{Hash, Hasher};
27use tracing::debug;
28use tracing::warn;
29
30#[derive(Debug, Clone)]
31pub struct CpuId {
32 // - *_adx: an absolute index within a system scope
33 // - *_rdx: a relative index under a parent
34 //
35 // - numa_adx: a NUMA domain within a system
36 // - pd_adx: a performance domain (CPU frequency domain) within a system
37 // - llc_rdx: an LLC domain (CCX) under a NUMA domain
38 // - llc_kernel_id: physical LLC domain ID provided by the kernel
39 // - core_rdx: a core under a LLC domain
40 // - cpu_rdx: a CPU under a core
41 pub numa_adx: usize,
42 pub pd_adx: usize,
43 pub llc_adx: usize,
44 pub llc_rdx: usize,
45 pub llc_kernel_id: usize,
46 pub core_rdx: usize,
47 pub cpu_rdx: usize,
48 pub cpu_adx: usize,
49 pub smt_level: usize,
50 pub cache_size: usize,
51 pub cpu_cap: usize,
52 pub big_core: bool,
53 pub turbo_core: bool,
54 pub cpu_sibling: usize,
55}
56
57#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
58pub struct ComputeDomainId {
59 pub numa_adx: usize,
60 pub llc_adx: usize,
61 pub llc_rdx: usize,
62 pub llc_kernel_id: usize,
63 pub is_big: bool,
64}
65
66#[derive(Debug, Clone)]
67pub struct ComputeDomain {
68 pub cpdom_id: usize,
69 pub cpdom_alt_id: Cell<usize>,
70 pub cpu_ids: Vec<usize>,
71 pub neighbor_map: RefCell<BTreeMap<usize, RefCell<Vec<usize>>>>,
72}
73
74#[derive(Debug, Clone)]
75#[allow(dead_code)]
76pub struct PerfCpuOrder {
77 pub perf_cap: usize, // performance in capacity
78 pub perf_util: f32, // performance in utilization, [0, 1]
79 pub cpus_perf: RefCell<Vec<usize>>, // CPU adx order within the performance range by @perf_cap
80 pub cpus_ovflw: RefCell<Vec<usize>>, // CPU adx order beyond @perf_cap
81}
82
83#[derive(Debug)]
84#[allow(dead_code)]
85pub struct CpuOrder {
86 pub all_cpus_mask: Cpumask,
87 pub cpuids: Vec<CpuId>,
88 pub perf_cpu_order: BTreeMap<usize, PerfCpuOrder>,
89 pub cpdom_map: BTreeMap<ComputeDomainId, ComputeDomain>,
90 pub nr_cpus: usize,
91 pub nr_cores: usize,
92 pub nr_cpdoms: usize,
93 pub nr_llcs: usize,
94 pub nr_numa: usize,
95 pub smt_enabled: bool,
96 pub has_biglittle: bool,
97 pub has_energy_model: bool,
98}
99
100impl CpuOrder {
101 /// Build a cpu preference order with optional topology configuration.
102 /// When @no_use_em is set, ignore the energy model even if the kernel
103 /// provides one, so the CPU preference order is built as on a machine
104 /// without an energy model.
105 pub fn new(
106 topology_args: Option<&scx_utils::TopologyArgs>,
107 no_use_em: bool,
108 ) -> Result<CpuOrder> {
109 let ctx = CpuOrderCtx::new(topology_args, no_use_em)?;
110 let cpus_pf = ctx.build_topo_order(false).unwrap();
111 let cpus_ps = ctx.build_topo_order(true).unwrap();
112 let cpdom_map = CpuOrderCtx::build_cpdom(&cpus_pf).unwrap();
113 let perf_cpu_order = if ctx.em.is_ok() {
114 let em = ctx.em.unwrap();
115 EnergyModelOptimizer::get_perf_cpu_order_table(&em, &cpus_pf)
116 } else {
117 EnergyModelOptimizer::get_fake_perf_cpu_order_table(&cpus_pf, &cpus_ps)
118 };
119
120 let nr_cpdoms = cpdom_map.len();
121 Ok(CpuOrder {
122 all_cpus_mask: ctx.topo.span,
123 cpuids: cpus_pf,
124 perf_cpu_order,
125 cpdom_map,
126 nr_cpus: ctx.topo.all_cpus.len(),
127 nr_cores: ctx.topo.all_cores.len(),
128 nr_cpdoms,
129 nr_llcs: ctx.topo.all_llcs.len(),
130 nr_numa: ctx.topo.nodes.len(),
131 smt_enabled: ctx.smt_enabled,
132 has_biglittle: ctx.has_biglittle,
133 has_energy_model: ctx.has_energy_model,
134 })
135 }
136}
137
138/// CpuOrderCtx is a helper struct used to build a CpuOrder
139struct CpuOrderCtx {
140 topo: Topology,
141 em: Result<EnergyModel>,
142 smt_enabled: bool,
143 has_biglittle: bool,
144 has_energy_model: bool,
145}
146
147impl CpuOrderCtx {
148 fn new(topology_args: Option<&scx_utils::TopologyArgs>, no_use_em: bool) -> Result<Self> {
149 let topo = match topology_args {
150 Some(args) => Topology::with_args(args)?,
151 None => Topology::new()?,
152 };
153
154 let em = if no_use_em {
155 Err(anyhow!("energy model disabled (--no-use-em)"))
156 } else {
157 EnergyModel::new()
158 };
159 let smt_enabled = topo.smt_enabled;
160 let has_biglittle = topo.has_little_cores();
161 let has_energy_model = em.is_ok();
162
163 debug!("{:#?}", topo);
164 debug!("{:#?}", em);
165
166 Ok(CpuOrderCtx {
167 topo,
168 em,
169 smt_enabled,
170 has_biglittle,
171 has_energy_model,
172 })
173 }
174
175 /// Build a CPU preference order based on its optimization target
176 fn build_topo_order(&self, prefer_powersave: bool) -> Option<Vec<CpuId>> {
177 let mut cpu_ids = Vec::new();
178 let smt_siblings = self.topo.sibling_cpus();
179
180 // Build a vector of cpu ids.
181 for (&numa_adx, node) in self.topo.nodes.iter() {
182 for (llc_rdx, (&llc_adx, llc)) in node.llcs.iter().enumerate() {
183 for (core_rdx, (_core_adx, core)) in llc.cores.iter().enumerate() {
184 for (cpu_rdx, (cpu_adx, cpu)) in core.cpus.iter().enumerate() {
185 let cpu_adx = *cpu_adx;
186 let pd_adx = Self::get_pd_id(&self.em, cpu_adx, llc_adx);
187 let cpu_id = CpuId {
188 numa_adx,
189 pd_adx,
190 llc_adx,
191 llc_rdx,
192 core_rdx,
193 cpu_rdx,
194 cpu_adx,
195 smt_level: cpu.smt_level,
196 cache_size: cpu.cache_size,
197 cpu_cap: cpu.cpu_capacity,
198 big_core: cpu.core_type != CoreType::Little,
199 turbo_core: cpu.core_type == CoreType::Big { turbo: true },
200 cpu_sibling: smt_siblings[cpu_adx] as usize,
201 llc_kernel_id: llc.kernel_id,
202 };
203 cpu_ids.push(RefCell::new(cpu_id));
204 }
205 }
206 }
207 }
208
209 // Convert a vector of RefCell to a vector of plain cpu_ids
210 let mut cpu_ids2 = Vec::new();
211 for cpu_id in cpu_ids.iter() {
212 cpu_ids2.push(cpu_id.borrow().clone());
213 }
214 let mut cpu_ids = cpu_ids2;
215
216 // Sort the cpu_ids
217 match (prefer_powersave, self.has_biglittle) {
218 // 1. powersave, no big/little
219 // * within the same LLC domain
220 // - numa_adx, llc_rdx,
221 // * prefer more capable CPU with higher capacity
222 // and larger cache
223 // - ^cpu_cap (chip binning), ^cache_size,
224 // * prefer the SMT core within the same performance domain
225 // - pd_adx, core_rdx, ^smt_level, cpu_rdx
226 (true, false) => {
227 cpu_ids.sort_by(|a, b| {
228 a.numa_adx
229 .cmp(&b.numa_adx)
230 .then_with(|| a.llc_rdx.cmp(&b.llc_rdx))
231 .then_with(|| b.cpu_cap.cmp(&a.cpu_cap))
232 .then_with(|| b.cache_size.cmp(&a.cache_size))
233 .then_with(|| a.pd_adx.cmp(&b.pd_adx))
234 .then_with(|| a.core_rdx.cmp(&b.core_rdx))
235 .then_with(|| b.smt_level.cmp(&a.smt_level))
236 .then_with(|| a.cpu_rdx.cmp(&b.cpu_rdx))
237 .then_with(|| a.cpu_adx.cmp(&b.cpu_adx))
238 });
239 }
240 // 2. powersave, yes big/little
241 // * within the same LLC domain
242 // - numa_adx, llc_rdx,
243 // * prefer energy-efficient LITTLE CPU with a larger cache
244 // - cpu_cap (big/little), ^cache_size,
245 // * prefer the SMT core within the same performance domain
246 // - pd_adx, core_rdx, ^smt_level, cpu_rdx
247 (true, true) => {
248 cpu_ids.sort_by(|a, b| {
249 a.numa_adx
250 .cmp(&b.numa_adx)
251 .then_with(|| a.llc_rdx.cmp(&b.llc_rdx))
252 .then_with(|| a.cpu_cap.cmp(&b.cpu_cap))
253 .then_with(|| b.cache_size.cmp(&a.cache_size))
254 .then_with(|| a.pd_adx.cmp(&b.pd_adx))
255 .then_with(|| a.core_rdx.cmp(&b.core_rdx))
256 .then_with(|| b.smt_level.cmp(&a.smt_level))
257 .then_with(|| a.cpu_rdx.cmp(&b.cpu_rdx))
258 .then_with(|| a.cpu_adx.cmp(&b.cpu_adx))
259 });
260 }
261 // 3. performance, no big/little
262 // 4. performance, yes big/little
263 // * prefer the non-SMT core
264 // - cpu_rdx,
265 // * fill the same LLC domain first
266 // - numa_adx, llc_rdx,
267 // * prefer more capable CPU with higher capacity
268 // (chip binning or big/little) and larger cache
269 // - ^cpu_cap, ^cache_size, smt_level
270 // * within the same power domain
271 // - pd_adx, core_rdx
272 _ => {
273 cpu_ids.sort_by(|a, b| {
274 a.cpu_rdx
275 .cmp(&b.cpu_rdx)
276 .then_with(|| a.numa_adx.cmp(&b.numa_adx))
277 .then_with(|| a.llc_rdx.cmp(&b.llc_rdx))
278 .then_with(|| b.cpu_cap.cmp(&a.cpu_cap))
279 .then_with(|| b.cache_size.cmp(&a.cache_size))
280 .then_with(|| a.smt_level.cmp(&b.smt_level))
281 .then_with(|| a.pd_adx.cmp(&b.pd_adx))
282 .then_with(|| a.core_rdx.cmp(&b.core_rdx))
283 .then_with(|| a.cpu_adx.cmp(&b.cpu_adx))
284 });
285 }
286 }
287
288 Some(cpu_ids)
289 }
290
291 /// Build a list of compute domains
292 fn build_cpdom(cpu_ids: &Vec<CpuId>) -> Option<BTreeMap<ComputeDomainId, ComputeDomain>> {
293 // Note that building compute domain is independent to CPU order
294 // so it is okay to use any cpus_*.
295
296 // Create a compute domain map, where a compute domain is a CPUs that
297 // are under the same node and LLC (virtual and physical) and have the same core type.
298 let mut cpdom_id = 0;
299 let mut cpdom_map: BTreeMap<ComputeDomainId, ComputeDomain> = BTreeMap::new();
300 let mut cpdom_types: BTreeMap<usize, bool> = BTreeMap::new();
301 for cpu_id in cpu_ids.iter() {
302 let key = ComputeDomainId {
303 numa_adx: cpu_id.numa_adx,
304 llc_adx: cpu_id.llc_adx,
305 llc_rdx: cpu_id.llc_rdx,
306 llc_kernel_id: cpu_id.llc_kernel_id,
307 is_big: cpu_id.big_core,
308 };
309 let value = cpdom_map.entry(key.clone()).or_insert_with(|| {
310 let val = ComputeDomain {
311 cpdom_id,
312 cpdom_alt_id: Cell::new(cpdom_id),
313 cpu_ids: Vec::new(),
314 neighbor_map: RefCell::new(BTreeMap::new()),
315 };
316 cpdom_types.insert(cpdom_id, key.is_big);
317
318 cpdom_id += 1;
319 val
320 });
321 value.cpu_ids.push(cpu_id.cpu_adx);
322 }
323
324 // Build a neighbor map for each compute domain, where neighbors are
325 // ordered by core type, node, and LLC.
326 for ((from_k, from_v), (to_k, to_v)) in iproduct!(cpdom_map.iter(), cpdom_map.iter()) {
327 if from_k == to_k {
328 continue;
329 }
330
331 let d = Self::dist(from_k, to_k);
332 let mut map = from_v.neighbor_map.borrow_mut();
333 match map.get(&d) {
334 Some(v) => {
335 v.borrow_mut().push(to_v.cpdom_id);
336 }
337 None => {
338 map.insert(d, RefCell::new(vec![to_v.cpdom_id]));
339 }
340 }
341 }
342
343 // Circular sort compute domains within the same distance to preserve
344 // proximity between domains.
345 //
346 // Suppose that domains 0, 1, 2, 3, 4, 5, 6, 7 are at the same distance.
347 // 0
348 // 7 1
349 // 6 2
350 // 5 3
351 // 4
352 //
353 // We want to traverse the domains from 0. The circular-sorted order
354 // starting from domain 0 is 0, 1, 7, 2, 6, 3, 5, 4. Similarly,
355 // the order starting from domain 1 is 1, 0, 2, 3, 7, 4, 6, 5.
356 // The one from 7 is 7, 0, 6, 1, 5, 2, 4, 3. As follows, circularly
357 // sorted orders in task stealing preserve proximity between domains
358 // (e.g., 0, 1, 7 in the example), so we can achieve less cacheline
359 // bouncing than with random-ordered task stealing.
360 for (_, cpdom) in cpdom_map.iter() {
361 for (_, neighbors) in cpdom.neighbor_map.borrow_mut().iter() {
362 let mut neighbors_csorted =
363 Self::circular_sort(cpdom.cpdom_id, &neighbors.borrow_mut().to_vec());
364 neighbors.borrow_mut().clear();
365 neighbors.borrow_mut().append(&mut neighbors_csorted);
366 }
367 }
368
369 // Fill up cpdom_alt_id for each compute domain.
370 for (k, v) in cpdom_map.iter() {
371 let mut key = k.clone();
372 key.is_big = !k.is_big;
373
374 if let Some(alt_v) = cpdom_map.get(&key) {
375 // First, try to find an alternative domain
376 // under the same node/LLC.
377 v.cpdom_alt_id.set(alt_v.cpdom_id);
378 } else {
379 // If there is no alternative domain in the same node/LLC,
380 // choose the closest one.
381 //
382 // Note that currently, the idle CPU selection (pick_idle_cpu)
383 // is not optimized for this kind of architecture, where big
384 // and LITTLE cores are in different node/LLCs.
385 'outer: for (_dist, ncpdoms) in v.neighbor_map.borrow().iter() {
386 for ncpdom_id in ncpdoms.borrow().iter() {
387 if let Some(is_big) = cpdom_types.get(ncpdom_id) {
388 if *is_big == key.is_big {
389 v.cpdom_alt_id.set(*ncpdom_id);
390 break 'outer;
391 }
392 }
393 }
394 }
395 }
396 }
397
398 Some(cpdom_map)
399 }
400
401 /// Circular sorting of a list from a starting point
402 fn circular_sort(start: usize, the_rest: &Vec<usize>) -> Vec<usize> {
403 // Create a full list including 'start'
404 let mut list = the_rest.clone();
405 list.push(start);
406 list.sort();
407
408 // Get the index of 'start'
409 let s = list
410 .binary_search(&start)
411 .expect("start must appear exactly once");
412
413 // Get the circularly sorted index list.
414 let n = list.len();
415 let dist = |x: usize| {
416 let d = (x + n - s) % n;
417 d.min(n - d)
418 };
419 let mut order: Vec<usize> = (0..n).collect();
420 order.sort_by_key(|&x| (dist(x), x));
421
422 // Rearrange the full list
423 // according to the circularly sorted index list.
424 let list_csorted: Vec<_> = order.iter().map(|&i| list[i]).collect();
425
426 // Drop 'start' from the rearranged full list.
427 list_csorted[1..].to_vec()
428 }
429
430 /// Get the performance domain (i.e., CPU frequency domain) ID for a CPU.
431 /// If the energy model is not available, use LLC ID instead.
432 fn get_pd_id(em: &Result<EnergyModel>, cpu_adx: usize, llc_adx: usize) -> usize {
433 match em {
434 Ok(em) => em.get_pd_by_cpu_id(cpu_adx).unwrap().id,
435 Err(_) => llc_adx,
436 }
437 }
438
439 /// Calculate distance from two compute domains
440 fn dist(from: &ComputeDomainId, to: &ComputeDomainId) -> usize {
441 let mut d = 0;
442 // core type > numa node > llc
443 if from.is_big != to.is_big {
444 d += 100;
445 }
446 if from.numa_adx != to.numa_adx {
447 d += 10;
448 } else {
449 if from.llc_rdx != to.llc_rdx {
450 d += 1;
451 }
452 if from.llc_kernel_id != to.llc_kernel_id {
453 d += 1;
454 }
455 }
456 d
457 }
458}
459
460#[derive(Debug)]
461struct EnergyModelOptimizer<'a> {
462 // The member performance domains of each equivalence performance domain of
463 // the energy model. Both the equivalence performance domains and their
464 // members are in CPU preference order, so taking N CPUs from an equivalence
465 // performance domain takes the N most preferred ones.
466 eq_pds: Vec<Vec<&'a PerfDomain>>,
467
468 // How many CPUs to take from each equivalence performance domain, for
469 // every combination worth considering. The i-th count belongs to
470 // @eq_pds[i]. It depends only on the CPU count of each equivalence
471 // performance domain, not on the CPU utilization, so it is enumerated once
472 // here.
473 //
474 // For example, when @em has two equivalence performance domains, one of
475 // 2 P-cores and one of 3 E-cores, the (2 + 1) * (3 + 1) - 1 = 11
476 // combinations are:
477 //
478 // [0, 1] -- 1 E-core
479 // [0, 2] -- 2 E-cores
480 // [0, 3] -- 3 E-cores
481 // [1, 0] -- 1 P-core
482 // [1, 1] -- 1 P-core and 1 E-core
483 // ...
484 // [2, 2] -- 2 P-cores and 2 E-cores
485 // [2, 3] -- 2 P-cores and 3 E-cores
486 nr_cpus_combinations: Vec<Vec<usize>>,
487
488 // CPU preference order in a performance mode purely based on topology
489 cpus_topological_order: Vec<usize>,
490
491 // CPU preference order within a performance domain
492 pd_cpu_order: BTreeMap<usize, RefCell<Vec<usize>>>,
493
494 // Total performance capacity of the system
495 tot_perf: usize,
496
497 // All possible combinations of performance domains & states
498 // indexed by performance.
499 pdss_infos: RefCell<BTreeMap<usize, RefCell<HashSet<PDSetInfo<'a>>>>>,
500
501 // Performance domains and states to achieve a certain performance level,
502 // which is derived from @pdss_infos.
503 perf_pdsi: RefCell<BTreeMap<usize, PDSetInfo<'a>>>,
504
505 // CPU orders indexed by performance
506 perf_cpu_order: RefCell<BTreeMap<usize, PerfCpuOrder>>,
507}
508
509#[derive(Debug, Clone, Eq, Hash, Ord, PartialOrd)]
510struct PDS<'a> {
511 pd: &'a PerfDomain,
512 ps: &'a PerfState,
513}
514
515#[derive(Debug, Clone, Eq, Hash, Ord, PartialOrd)]
516struct PDCpu<'a> {
517 pd: &'a PerfDomain, // performance domain
518 cpu_vid: usize, // virtual ID of a CPU on the performance domain
519}
520
521#[derive(Debug, Clone, Eq)]
522struct PDSetInfo<'a> {
523 performance: usize,
524 power: usize,
525 pdcpu_set: BTreeSet<PDCpu<'a>>,
526 pd_id_set: BTreeSet<usize>, // pd:id:0, pd:id:1
527}
528
529const PD_UNIT: usize = 100_000_000;
530const CPU_UNIT: usize = 100_000;
531const LOOKAHEAD_CNT: usize = 10;
532
533/// Upper bound on the number of equivalence performance domain combinations to
534/// consider, to keep the number of combinations manageable. The performance
535/// domains of a processor may not collapse into a few equivalence performance
536/// domains -- per-core binning, for one, could give every core its own
537/// performance table. See <https://github.com/sched-ext/scx/issues/3340>.
538const MAX_EQPD_COMBINATIONS: u128 = 100_000;
539
540impl<'a> EnergyModelOptimizer<'a> {
541 fn new(em: &'a EnergyModel, cpus_pf: &'a Vec<CpuId>) -> EnergyModelOptimizer<'a> {
542 let tot_perf = em.perf_total();
543
544 let eq_pds = Self::sort_eq_pds(em, cpus_pf);
545 let max_nr_cpus: Vec<usize> = eq_pds
546 .iter()
547 .map(|perf_doms| perf_doms.iter().map(|pd| pd.span.weight()).sum())
548 .collect();
549 let nr_cpus_combinations = Self::gen_nr_cpus_combinations(&max_nr_cpus);
550
551 let pdss_infos: BTreeMap<usize, RefCell<HashSet<PDSetInfo<'a>>>> = BTreeMap::new();
552 let pdss_infos = pdss_infos.into();
553
554 let perf_pdsi: BTreeMap<usize, PDSetInfo<'a>> = BTreeMap::new();
555 let perf_pdsi = perf_pdsi.into();
556
557 let mut pd_cpu_order: BTreeMap<usize, RefCell<Vec<usize>>> = BTreeMap::new();
558 let mut cpus_topological_order: Vec<usize> = vec![];
559 for cpuid in cpus_pf.iter() {
560 match pd_cpu_order.get(&cpuid.pd_adx) {
561 Some(v) => {
562 let mut v = v.borrow_mut();
563 v.push(cpuid.cpu_adx);
564 }
565 None => {
566 let v = vec![cpuid.cpu_adx];
567 pd_cpu_order.insert(cpuid.pd_adx, v.into());
568 }
569 }
570 cpus_topological_order.push(cpuid.cpu_adx);
571 }
572
573 let perf_cpu_order: BTreeMap<usize, PerfCpuOrder> = BTreeMap::new();
574 let perf_cpu_order = perf_cpu_order.into();
575
576 debug!("# pd_cpu_order");
577 debug!("{:#?}", pd_cpu_order);
578
579 EnergyModelOptimizer {
580 eq_pds,
581 nr_cpus_combinations,
582 cpus_topological_order,
583 pd_cpu_order,
584 tot_perf,
585 pdss_infos,
586 perf_pdsi,
587 perf_cpu_order,
588 }
589 }
590
591 fn get_perf_cpu_order_table(
592 em: &'a EnergyModel,
593 cpus_pf: &'a Vec<CpuId>,
594 ) -> BTreeMap<usize, PerfCpuOrder> {
595 let emo = EnergyModelOptimizer::new(em, &cpus_pf);
596 emo.gen_perf_cpu_order_table();
597 let perf_cpu_order = emo.perf_cpu_order.borrow().clone();
598
599 perf_cpu_order
600 }
601
602 fn get_fake_perf_cpu_order_table(
603 cpus_pf: &'a Vec<CpuId>,
604 cpus_ps: &'a Vec<CpuId>,
605 ) -> BTreeMap<usize, PerfCpuOrder> {
606 let tot_perf: usize = cpus_pf.iter().map(|cpuid| cpuid.cpu_cap).sum();
607
608 let pco_pf = Self::fake_pco(tot_perf, cpus_pf, false);
609 let pco_ps = Self::fake_pco(tot_perf, cpus_ps, true);
610
611 let mut perf_cpu_order: BTreeMap<usize, PerfCpuOrder> = BTreeMap::new();
612 perf_cpu_order.insert(pco_pf.perf_cap, pco_pf);
613 perf_cpu_order.insert(pco_ps.perf_cap, pco_ps);
614
615 perf_cpu_order
616 }
617
618 fn fake_pco(tot_perf: usize, cpuids: &'a Vec<CpuId>, powersave: bool) -> PerfCpuOrder {
619 let perf_cap;
620
621 if powersave {
622 perf_cap = cpuids[0].cpu_cap;
623 } else {
624 perf_cap = tot_perf;
625 }
626
627 let perf_util: f32 = (perf_cap as f32) / (tot_perf as f32);
628 let cpus: Vec<usize> = cpuids.iter().map(|cpuid| cpuid.cpu_adx).collect();
629 let cpus_perf: Vec<usize> = cpus[..1].iter().map(|&cpuid| cpuid).collect();
630 let cpus_ovflw: Vec<usize> = cpus[1..].iter().map(|&cpuid| cpuid).collect();
631 PerfCpuOrder {
632 perf_cap,
633 perf_util,
634 cpus_perf: cpus_perf.clone().into(),
635 cpus_ovflw: cpus_ovflw.clone().into(),
636 }
637 }
638
639 /// Generate the performance versus CPU preference order table based on
640 /// the system's CPU topology and energy model. The table consists of the
641 /// following information (PerfCpuOrder):
642 ///
643 /// - PerfCpuOrder::perf_cap: The upper bound of the performance
644 /// capacity covered by this tuple.
645 ///
646 /// - PerfCpuOrder::cpus_perf: Primary CPUs to be used is ordered
647 /// by preference.
648 ///
649 /// - PerfCpuOrder::cpus_ovrflw: When the system load goes beyond
650 /// @perf_cap, the list of CPUs to be used is ordered by preference.
651 fn gen_perf_cpu_order_table(&'a self) {
652 // First, generate all possible combinations of CPUs (e.g., two CPUs
653 // in performance domain 0 and three CPUs in performance domain 1) to
654 // achieve the possible performance capacities with minimal energy
655 // consumption. We assume a reasonable load balancer, so the
656 // utilization of the used CPUs is similar.
657 self.gen_all_pds_combinations();
658
659 // Then, from all the possible combinations of performance versus
660 // CPU sets, select a list of combinations that minimize the number of
661 // active performance domains and reduce the number of performance
662 // domain switches when changing performance levels.
663 self.gen_perf_pds_table();
664
665 // Finally, assign CPUs (@cpu_adx) to the virtual CPU ID (@cpu_vid) of
666 // a performance domain.
667 self.assign_cpu_vids();
668 }
669
670 /// Generate a CPU order table for each performance range.
671 fn assign_cpu_vids(&'a self) {
672 // Generate CPU order within the performance range (@cpus_perf).
673 for (&perf_cap, pdsi) in self.perf_pdsi.borrow().iter() {
674 let mut cpus_perf: Vec<usize> = vec![];
675
676 for pdcpu in pdsi.pdcpu_set.iter() {
677 let pd_id = pdcpu.pd.id;
678 let cpu_vid = pdcpu.cpu_vid;
679 let cpu_order = self.pd_cpu_order.get(&pd_id).unwrap().borrow();
680 let cpu_adx = cpu_order[cpu_vid];
681 cpus_perf.push(cpu_adx);
682 }
683
684 let perf_util: f32 = (perf_cap as f32) / (self.tot_perf as f32);
685 let cpus_perf = self.sort_cpus_by_topological_order(&cpus_perf);
686 let cpus_ovflw: Vec<usize> = vec![];
687
688 let mut perf_cpu_order = self.perf_cpu_order.borrow_mut();
689 perf_cpu_order.insert(
690 perf_cap,
691 PerfCpuOrder {
692 perf_cap,
693 perf_util,
694 cpus_perf: cpus_perf.clone().into(),
695 cpus_ovflw: cpus_ovflw.clone().into(),
696 },
697 );
698 }
699
700 // Generate CPU order beyond the performance range (@cpus_ovflw).
701 let perf_cpu_order = self.perf_cpu_order.borrow();
702 let perf_caps: Vec<_> = self.perf_pdsi.borrow().keys().cloned().collect();
703 for o in 1..perf_caps.len() {
704 // Gather all @cpus_perf from the upper performance ranges.
705 let ovrflw_perf_caps = &perf_caps[o..];
706 let mut ovrflw_cpus_all: Vec<usize> = vec![];
707 for perf_cap in ovrflw_perf_caps.iter() {
708 let cpu_order = perf_cpu_order.get(perf_cap).unwrap();
709 let cpus_perf = cpu_order.cpus_perf.borrow();
710 ovrflw_cpus_all.extend(cpus_perf.iter().cloned());
711 }
712
713 // Filter out already taken CPUs from the @ovrflw_cpus_all,
714 // and build @cpus_ovrflw.
715 let mut cpu_set = HashSet::<usize>::new();
716 let perf_cap = perf_caps[o - 1];
717 let cpu_order = perf_cpu_order.get(&perf_cap).unwrap();
718 let cpus_perf = cpu_order.cpus_perf.borrow();
719 for &cpu_adx in cpus_perf.iter() {
720 cpu_set.insert(cpu_adx);
721 }
722
723 let mut cpus_ovflw: Vec<usize> = vec![];
724 for &cpu_adx in ovrflw_cpus_all.iter() {
725 if cpu_set.get(&cpu_adx).is_none() {
726 cpus_ovflw.push(cpu_adx);
727 cpu_set.insert(cpu_adx);
728 }
729 }
730
731 // Inject the constructed @cpus_ovrflw to the table.
732 let mut v = cpu_order.cpus_ovflw.borrow_mut();
733 v.extend(cpus_ovflw.iter().cloned());
734 }
735
736 // Debug print of the generated table
737 debug!("## gen_perf_cpu_order_table");
738 debug!("{:#?}", perf_cpu_order);
739 }
740
741 /// Sort the CPU IDs by topological order (@self.cpus_topological_order).
742 fn sort_cpus_by_topological_order(&'a self, cpus: &Vec<usize>) -> Vec<usize> {
743 let mut sorted: Vec<usize> = vec![];
744 for &cpu_adx in self.cpus_topological_order.iter() {
745 if let Some(_) = cpus.iter().find(|&&x| x == cpu_adx) {
746 sorted.push(cpu_adx);
747 }
748 }
749 sorted
750 }
751
752 /// Generate a table of performance vs. performance domain sets
753 /// (@self.perf_pdss) from all the possible performance domain & state
754 /// combinations (@self.pdss_infos).
755 ///
756 /// An example result is as follows:
757 /// PERF: [_, 300]
758 /// pd:id: 0 -- cpu_vid: 0
759 /// pd:id: 0 -- cpu_vid: 1
760 /// PERF: [_, 1138]
761 /// pd:id: 0 -- cpu_vid: 0
762 /// pd:id: 0 -- cpu_vid: 1
763 /// pd:id: 1 -- cpu_vid: 0
764 /// pd:id: 1 -- cpu_vid: 1
765 /// PERF: [_, 3386]
766 /// pd:id: 1 -- cpu_vid: 0
767 /// pd:id: 1 -- cpu_vid: 1
768 /// pd:id: 1 -- cpu_vid: 2
769 /// pd:id: 2 -- cpu_vid: 0
770 /// pd:id: 2 -- cpu_vid: 1
771 /// PERF: [_, 3977]
772 /// pd:id: 0 -- cpu_vid: 0
773 /// pd:id: 1 -- cpu_vid: 0
774 /// pd:id: 1 -- cpu_vid: 1
775 /// pd:id: 1 -- cpu_vid: 2
776 /// pd:id: 2 -- cpu_vid: 0
777 /// pd:id: 2 -- cpu_vid: 1
778 /// PERF: [_, 4508]
779 /// pd:id: 0 -- cpu_vid: 0
780 /// pd:id: 0 -- cpu_vid: 1
781 /// pd:id: 1 -- cpu_vid: 0
782 /// pd:id: 1 -- cpu_vid: 1
783 /// pd:id: 1 -- cpu_vid: 2
784 /// pd:id: 2 -- cpu_vid: 0
785 /// pd:id: 2 -- cpu_vid: 1
786 /// PERF: [_, 5627]
787 /// pd:id: 0 -- cpu_vid: 0
788 /// pd:id: 0 -- cpu_vid: 1
789 /// pd:id: 1 -- cpu_vid: 0
790 /// pd:id: 1 -- cpu_vid: 1
791 /// pd:id: 1 -- cpu_vid: 2
792 /// pd:id: 2 -- cpu_vid: 0
793 /// pd:id: 2 -- cpu_vid: 1
794 /// pd:id: 3 -- cpu_vid: 0
795 fn gen_perf_pds_table(&'a self) {
796 let utils = vec![0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
797
798 // Find the best performance domains for each system utilization target.
799 for &util in utils.iter() {
800 let mut best_pdsi: Option<PDSetInfo<'a>>;
801 let mut del_pdsi: Option<PDSetInfo<'a>> = None;
802
803 match self.perf_pdsi.borrow().last_key_value() {
804 Some((_, base)) => {
805 best_pdsi = self.find_perf_pds_for(util, Some(base));
806
807 // If the next performance level (@best_pdsi) is subsumed
808 // by the previous level (@base), extend the base to the
809 // next level. To this end, insert the extended base (with
810 // updated performance and power values) and delete the old
811 // base.
812 if let Some(ref best) = best_pdsi {
813 if best.pdcpu_set.is_subset(&base.pdcpu_set) {
814 let ext_pdcpu = PDSetInfo {
815 performance: best.performance,
816 power: best.power,
817 pdcpu_set: base.pdcpu_set.clone(),
818 pd_id_set: base.pd_id_set.clone(),
819 };
820 best_pdsi = Some(ext_pdcpu);
821 del_pdsi = Some(base.clone());
822 }
823 }
824 }
825 None => {
826 best_pdsi = self.find_perf_pds_for(util, None);
827 }
828 };
829
830 if let Some(best_pdsi) = best_pdsi {
831 self.perf_pdsi
832 .borrow_mut()
833 .insert(best_pdsi.performance, best_pdsi);
834 }
835
836 if let Some(del_pdsi) = del_pdsi {
837 self.perf_pdsi.borrow_mut().remove(&del_pdsi.performance);
838 }
839 }
840
841 // Debug print of the generated table
842 debug!("## gen_perf_pds_table");
843 for (perf, pdsi) in self.perf_pdsi.borrow().iter() {
844 debug!("PERF: [_, {}]", perf);
845 for pdcpu in pdsi.pdcpu_set.iter() {
846 debug!(
847 " pd:id: {:?} -- cpu_vid: {}",
848 pdcpu.pd.id, pdcpu.cpu_vid
849 );
850 }
851 }
852 }
853
854 fn find_perf_pds_for(
855 &'a self,
856 util: f32,
857 base: Option<&PDSetInfo<'a>>,
858 ) -> Option<PDSetInfo<'a>> {
859 let target_perf = (util * self.tot_perf as f32) as usize;
860 let mut lookahead = 0;
861 let mut min_dist: usize = usize::MAX;
862 let mut best_pdsi: Option<PDSetInfo<'a>> = None;
863
864 let pdss_infos = self.pdss_infos.borrow();
865 for (&pdsi_perf, pdsi_set) in pdss_infos.iter() {
866 if pdsi_perf >= target_perf {
867 let pdsi_set_ref = pdsi_set.borrow();
868 for pdsi in pdsi_set_ref.iter() {
869 let dist = pdsi.dist(base);
870 if dist < min_dist {
871 min_dist = dist;
872 best_pdsi = Some(pdsi.clone());
873 }
874 }
875 lookahead += 1;
876 if lookahead >= LOOKAHEAD_CNT {
877 break;
878 }
879 }
880 }
881
882 best_pdsi
883 }
884
885 /// Generate all possible performance domain & state combinations,
886 /// @self.pdss_infos. Each combination represents a set of performance
887 /// domains (and their corresponding performance states) that achieve the
888 /// requested performance with minimal power consumption.
889 ///
890 /// We assume a 'reasonable load balancer,' so the CPU utilization of all
891 /// the involved CPUs is similar.
892 ///
893 /// An example result is as follows:
894 ///
895 /// PERF: [_, 5135]
896 /// perf: 5135 -- power: 5475348
897 /// pd:id: 0 -- cpu_vid: 0
898 /// pd:id: 1 -- cpu_vid: 0
899 /// pd:id: 1 -- cpu_vid: 1
900 /// pd:id: 1 -- cpu_vid: 2
901 /// pd:id: 2 -- cpu_vid: 0
902 /// pd:id: 2 -- cpu_vid: 1
903 /// pd:id: 3 -- cpu_vid: 0
904 /// PERF: [_, 5187]
905 /// perf: 5187 -- power: 4844969
906 /// pd:id: 0 -- cpu_vid: 0
907 /// pd:id: 0 -- cpu_vid: 1
908 /// pd:id: 1 -- cpu_vid: 0
909 /// pd:id: 1 -- cpu_vid: 1
910 /// pd:id: 1 -- cpu_vid: 2
911 /// pd:id: 2 -- cpu_vid: 0
912 /// pd:id: 2 -- cpu_vid: 1
913 /// pd:id: 3 -- cpu_vid: 0
914 /// PERF: [_, 5195]
915 /// perf: 5195 -- power: 5924606
916 /// pd:id: 1 -- cpu_vid: 0
917 /// pd:id: 1 -- cpu_vid: 1
918 /// pd:id: 1 -- cpu_vid: 2
919 /// pd:id: 2 -- cpu_vid: 0
920 /// pd:id: 2 -- cpu_vid: 1
921 /// pd:id: 3 -- cpu_vid: 0
922 /// PERF: [_, 5217]
923 /// perf: 5217 -- power: 4894911
924 /// pd:id: 0 -- cpu_vid: 0
925 /// pd:id: 0 -- cpu_vid: 1
926 /// pd:id: 1 -- cpu_vid: 0
927 /// pd:id: 1 -- cpu_vid: 1
928 /// pd:id: 1 -- cpu_vid: 2
929 /// pd:id: 2 -- cpu_vid: 0
930 /// pd:id: 2 -- cpu_vid: 1
931 /// pd:id: 3 -- cpu_vid: 0
932 /// PERF: [_, 5225]
933 /// perf: 5225 -- power: 5665770
934 /// pd:id: 0 -- cpu_vid: 0
935 /// pd:id: 1 -- cpu_vid: 0
936 /// pd:id: 1 -- cpu_vid: 1
937 /// pd:id: 1 -- cpu_vid: 2
938 /// pd:id: 2 -- cpu_vid: 0
939 /// pd:id: 2 -- cpu_vid: 1
940 /// pd:id: 3 -- cpu_vid: 0
941 /// PERF: [_, 5316]
942 /// perf: 5316 -- power: 5860568
943 /// pd:id: 0 -- cpu_vid: 0
944 /// pd:id: 1 -- cpu_vid: 0
945 /// pd:id: 1 -- cpu_vid: 1
946 /// pd:id: 1 -- cpu_vid: 2
947 /// pd:id: 2 -- cpu_vid: 0
948 /// pd:id: 2 -- cpu_vid: 1
949 /// pd:id: 3 -- cpu_vid: 0
950 fn gen_all_pds_combinations(&'a self) {
951 // Start from the min (0%) and max (100%) CPU utilizations
952 let pdsi_vec = self.gen_pds_combinations(0.0);
953 self.insert_pds_combinations(&pdsi_vec);
954
955 let pdsi_vec = self.gen_pds_combinations(100.0);
956 self.insert_pds_combinations(&pdsi_vec);
957
958 // Then dive into the range between the min and max.
959 self.gen_perf_cpuset_table_range(0, 100);
960
961 // Debug print performance table
962 debug!("## gen_all_pds_combinations");
963 for (perf, pdss_info) in self.pdss_infos.borrow().iter() {
964 debug!("PERF: [_, {}]", perf);
965 for pdsi in pdss_info.borrow().iter() {
966 debug!(" perf: {} -- power: {}", pdsi.performance, pdsi.power);
967 for pdcpu in pdsi.pdcpu_set.iter() {
968 debug!(
969 " pd:id: {:?} -- cpu_vid: {}",
970 pdcpu.pd.id, pdcpu.cpu_vid
971 );
972 }
973 }
974 }
975 }
976
977 fn gen_perf_cpuset_table_range(&'a self, low: isize, high: isize) {
978 if low > high {
979 return;
980 }
981
982 // If there is a new performance point in the middle,
983 // let's further explore. Otherwise, stop it here.
984 let mid: isize = low + (high - low) / 2;
985 let pdsi_vec = self.gen_pds_combinations(mid as f32);
986 let found_new = self.insert_pds_combinations(&pdsi_vec);
987 if found_new {
988 self.gen_perf_cpuset_table_range(mid + 1, high);
989 self.gen_perf_cpuset_table_range(low, mid - 1);
990 }
991 }
992
993 /// Rank the performance domains by CPU preference: a performance domain is
994 /// as preferred as its most preferred CPU, which is the first one appearing
995 /// in @cpus_pf. A performance domain with no CPU in @cpus_pf is unranked and
996 /// comes last.
997 fn rank_perf_doms(cpus_pf: &[CpuId]) -> BTreeMap<usize, usize> {
998 let mut ranks = BTreeMap::new();
999
1000 for (rank, cpuid) in cpus_pf.iter().enumerate() {
1001 ranks.entry(cpuid.pd_adx).or_insert(rank);
1002 }
1003
1004 ranks
1005 }
1006
1007 /// Collect the member performance domains of each equivalence performance
1008 /// domain of @em, ordering both the members and the equivalence performance
1009 /// domains by CPU preference. See @EnergyModelOptimizer::eq_pds.
1010 fn sort_eq_pds(em: &'a EnergyModel, cpus_pf: &'a [CpuId]) -> Vec<Vec<&'a PerfDomain>> {
1011 let ranks = Self::rank_perf_doms(cpus_pf);
1012 let rank_of = |pd: &PerfDomain| ranks.get(&pd.id).copied().unwrap_or(usize::MAX);
1013
1014 let mut eq_pds: Vec<Vec<&'a PerfDomain>> = em
1015 .eq_perf_doms
1016 .values()
1017 .map(|eq_pd| {
1018 let mut perf_doms: Vec<&'a PerfDomain> =
1019 eq_pd.perf_doms.iter().map(|pd| pd.as_ref()).collect();
1020 perf_doms.sort_by_key(|pd| rank_of(pd));
1021 perf_doms
1022 })
1023 .collect();
1024
1025 // An equivalence performance domain is as preferred as its most
1026 // preferred member performance domain.
1027 eq_pds.sort_by_key(|perf_doms| perf_doms.iter().map(|pd| rank_of(pd)).min());
1028
1029 eq_pds
1030 }
1031
1032 /// Enumerate how many CPUs to take from each equivalence performance
1033 /// domain, taking at most @max_nr_cpus[i] CPUs from the i-th one. See
1034 /// @EnergyModelOptimizer::nr_cpus_combinations.
1035 fn gen_nr_cpus_combinations(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1036 // The number of all the possible combinations. An equivalence
1037 // performance domain can contribute none, some, or all of its CPUs, so
1038 // it has max_nr_cpus + 1 choices, and the choices of all the
1039 // equivalence performance domains multiply. Subtract one for the
1040 // combination taking no CPU at all. The product saturates instead of
1041 // overflowing when there are hundreds of equivalence performance
1042 // domains.
1043 let nr_combinations = max_nr_cpus
1044 .iter()
1045 .fold(1u128, |nr, &max| nr.saturating_mul(max as u128 + 1))
1046 - 1;
1047 if nr_combinations <= MAX_EQPD_COMBINATIONS {
1048 return Self::gen_all_nr_cpus(max_nr_cpus);
1049 }
1050
1051 let combinations = Self::gen_run_nr_cpus(max_nr_cpus);
1052 warn!(
1053 "{} equivalence performance domains yield {nr_combinations} combinations, \
1054 exceeding the limit of {MAX_EQPD_COMBINATIONS}, so consider only {} of them",
1055 max_nr_cpus.len(),
1056 combinations.len(),
1057 );
1058
1059 combinations
1060 }
1061
1062 /// Enumerate how many CPUs to take from each equivalence performance domain
1063 /// in every possible way. An equivalence performance domain independently
1064 /// takes 0, 1, ... up to all of its CPUs, so a combination picks one count
1065 /// from the range `0..=max_nr_cpus[i]` of every equivalence performance
1066 /// domain. Picking one element from each of several ranges, in all the
1067 /// possible ways, is the cartesian product of those ranges, which
1068 /// `multi_cartesian_product` enumerates one combination at a time. See
1069 /// @EnergyModelOptimizer::nr_cpus_combinations for an example.
1070 fn gen_all_nr_cpus(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1071 max_nr_cpus
1072 .iter()
1073 .map(|&max| 0..=max)
1074 .multi_cartesian_product()
1075 // Drop the one combination taking no CPU at all.
1076 .filter(|nr_cpus| nr_cpus.iter().any(|&nr| nr > 0))
1077 .collect()
1078 }
1079
1080 /// Enumerate how many CPUs to take from each equivalence performance domain
1081 /// when there are too many combinations to consider them all
1082 /// (@MAX_EQPD_COMBINATIONS). Only the runs of equivalence performance
1083 /// domains are considered, where a run takes all the CPUs of consecutive
1084 /// equivalence performance domains and some of the CPUs of the last one:
1085 ///
1086 /// - A forward run grows from the first equivalence performance domain,
1087 /// adding one more equivalence performance domain at a time.
1088 /// - A backward run grows from the last equivalence performance domain
1089 /// in the opposite direction.
1090 /// - A single run takes CPUs from one equivalence performance domain and
1091 /// none from the others.
1092 ///
1093 /// For example, with three equivalence performance domains of 1, 2, and 1
1094 /// CPUs, the runs are:
1095 ///
1096 /// forward: [1, 0, 0]
1097 /// [1, 1, 0], [1, 2, 0]
1098 /// [1, 2, 1]
1099 /// backward: [0, 0, 1]
1100 /// [0, 1, 1], [0, 2, 1]
1101 /// [1, 2, 1]
1102 /// single: [1, 0, 0]
1103 /// [0, 1, 0], [0, 2, 0]
1104 /// [0, 0, 1]
1105 ///
1106 /// which is 3 * nr_cpus = 12 combinations, or 9 once the duplicates are
1107 /// removed. Since there are at most 3 * nr_cpus of them, the runs always
1108 /// fit in @MAX_EQPD_COMBINATIONS.
1109 fn gen_run_nr_cpus(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1110 let nr_eq_pds = max_nr_cpus.len();
1111 let mut combinations = vec![];
1112
1113 // Take 1 to all the CPUs of the @i-th equivalence performance domain,
1114 // which is the last one of a forward run, the last one of a backward
1115 // run, and the only one of a single run.
1116 for i in 0..nr_eq_pds {
1117 let mut forward = vec![0; nr_eq_pds];
1118 forward[..i].copy_from_slice(&max_nr_cpus[..i]);
1119
1120 let mut backward = vec![0; nr_eq_pds];
1121 backward[i + 1..].copy_from_slice(&max_nr_cpus[i + 1..]);
1122
1123 let mut single = vec![0; nr_eq_pds];
1124
1125 for nr in 1..=max_nr_cpus[i] {
1126 forward[i] = nr;
1127 backward[i] = nr;
1128 single[i] = nr;
1129
1130 combinations.push(forward.clone());
1131 combinations.push(backward.clone());
1132 combinations.push(single.clone());
1133 }
1134 }
1135
1136 combinations.sort();
1137 combinations.dedup();
1138
1139 combinations
1140 }
1141
1142 /// Generate the combinations of performance domains and states to consider
1143 /// for a given CPU utilization (@util), one for each combination of
1144 /// per-equivalence performance domain CPU counts.
1145 fn gen_pds_combinations(&'a self, util: f32) -> Vec<PDSetInfo<'a>> {
1146 self.nr_cpus_combinations
1147 .iter()
1148 .map(|nr_cpus| self.gen_pdsi(nr_cpus, util))
1149 .collect()
1150 }
1151
1152 /// Build the performance domains and states taking @nr_cpus[i] CPUs from
1153 /// the i-th equivalence performance domain at the performance state for
1154 /// @util. The CPUs are taken from the member performance domains in order,
1155 /// so the CPUs for a count of N are always a subset of the ones for N + 1.
1156 fn gen_pdsi(&'a self, nr_cpus: &[usize], util: f32) -> PDSetInfo<'a> {
1157 let mut pds_set = vec![];
1158
1159 for (perf_doms, &nr) in self.eq_pds.iter().zip(nr_cpus.iter()) {
1160 // All the member performance domains share one performance table,
1161 // so they are all at the same performance state.
1162 let ps = perf_doms[0].select_perf_state(util).unwrap();
1163 let mut remaining = nr;
1164
1165 for pd in perf_doms.iter() {
1166 if remaining == 0 {
1167 break;
1168 }
1169
1170 // A performance domain contributes at most its own CPUs.
1171 let take = remaining.min(pd.span.weight());
1172 for _ in 0..take {
1173 pds_set.push(PDS::new(pd, ps));
1174 }
1175 remaining -= take;
1176 }
1177 }
1178
1179 PDSetInfo::new(pds_set)
1180 }
1181
1182 fn insert_pds_combinations(&self, new_pdsi_vec: &Vec<PDSetInfo<'a>>) -> bool {
1183 // For the same performance, keep the PDS combinations with the lowest
1184 // power consumption. If there are more than one lowest, keep them all
1185 // to choose one later when assigning CPUs from the selected
1186 // performance domains.
1187 let mut found_new = false;
1188
1189 for new_pdsi in new_pdsi_vec.iter() {
1190 let mut pdss_infos = self.pdss_infos.borrow_mut();
1191 let v = pdss_infos.get(&new_pdsi.performance);
1192 match v {
1193 // There are already PDSetInfo in the list.
1194 Some(v) => {
1195 let mut v = v.borrow_mut();
1196 let pdsi = &v.iter().next().unwrap();
1197 if pdsi.power == new_pdsi.power {
1198 // If the power consumptions are the same, keep both.
1199 if v.insert(new_pdsi.clone()) {
1200 found_new = true;
1201 }
1202 } else if pdsi.power > new_pdsi.power {
1203 // If the new one takes less power, keep the new one.
1204 v.clear();
1205 v.insert(new_pdsi.clone());
1206 found_new = true;
1207 }
1208 }
1209 // This is the first for the performance target.
1210 None => {
1211 // Let's add it and move on.
1212 let mut v: HashSet<PDSetInfo<'a>> = HashSet::new();
1213 v.insert(new_pdsi.clone());
1214 pdss_infos.insert(new_pdsi.performance, v.into());
1215 found_new = true;
1216 }
1217 }
1218 }
1219 found_new
1220 }
1221}
1222
1223impl<'a> PDS<'_> {
1224 fn new(pd: &'a PerfDomain, ps: &'a PerfState) -> PDS<'a> {
1225 PDS { pd, ps }
1226 }
1227}
1228
1229impl PartialEq for PDS<'_> {
1230 fn eq(&self, other: &Self) -> bool {
1231 self.pd == other.pd && self.ps == other.ps
1232 }
1233}
1234
1235impl<'a> PDCpu<'_> {
1236 fn new(pd: &'a PerfDomain, cpu_vid: usize) -> PDCpu<'a> {
1237 PDCpu { pd, cpu_vid }
1238 }
1239}
1240
1241impl PartialEq for PDCpu<'_> {
1242 fn eq(&self, other: &Self) -> bool {
1243 self.pd == other.pd && self.cpu_vid == other.cpu_vid
1244 }
1245}
1246
1247impl fmt::Display for PDS<'_> {
1248 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1249 write!(
1250 f,
1251 "pd:id:{}/pd:weight:{}/ps:cap:{}/ps:power:{}",
1252 self.pd.id,
1253 self.pd.span.weight(),
1254 self.ps.performance,
1255 self.ps.power,
1256 )?;
1257 Ok(())
1258 }
1259}
1260
1261impl<'a> PDSetInfo<'_> {
1262 fn new(pds_set: Vec<PDS<'a>>) -> PDSetInfo<'a> {
1263 // Create a pd_id_set and calculate performance and power.
1264 let mut performance = 0;
1265 let mut power = 0;
1266 let mut pd_id_set: BTreeSet<usize> = BTreeSet::new();
1267
1268 for pds in pds_set.iter() {
1269 performance += pds.ps.performance;
1270 power += pds.ps.power;
1271 pd_id_set.insert(pds.pd.id);
1272 }
1273
1274 // Create a pdcpu_set, so first gather the same PDS entries.
1275 let mut pds_map: BTreeMap<PDS<'a>, RefCell<Vec<PDS<'a>>>> = BTreeMap::new();
1276
1277 for pds in pds_set.iter() {
1278 let v = pds_map.get(&pds);
1279 match v {
1280 Some(v) => {
1281 let mut v = v.borrow_mut();
1282 v.push(pds.clone());
1283 }
1284 None => {
1285 let mut v: Vec<PDS<'a>> = Vec::new();
1286 v.push(pds.clone());
1287 pds_map.insert(pds.clone(), v.into());
1288 }
1289 }
1290 }
1291 // Then assign cpu virtual ids to pdcpu_set.
1292 let mut pdcpu_set: BTreeSet<PDCpu<'a>> = BTreeSet::new();
1293 let pds_map = pds_map;
1294
1295 for (_, v) in pds_map.iter() {
1296 for (cpu_vid, pds) in v.borrow().iter().enumerate() {
1297 let pdcpu = PDCpu::new(pds.pd, cpu_vid);
1298 pdcpu_set.insert(pdcpu);
1299 }
1300 }
1301
1302 PDSetInfo {
1303 performance,
1304 power,
1305 pdcpu_set,
1306 pd_id_set,
1307 }
1308 }
1309
1310 /// Calculate the distance from @base to @self. We minimize the number of
1311 /// performance domains involved to reduce the leakage power consumption.
1312 /// We then maximize the overlap between the previous (i.e., base)
1313 /// performance domains and the new one for a smooth transition to the new
1314 /// cpuset with higher cache locality. Finally, we minimize the number of
1315 /// CPUs involved, thereby reducing the chance of contention for shared
1316 /// hardware resources (e.g., shared cache).
1317 fn dist(&self, base: Option<&PDSetInfo<'a>>) -> usize {
1318 let nr_pds = self.pd_id_set.len();
1319 let nr_pds_overlap = match base {
1320 Some(base) => self.pd_id_set.intersection(&base.pd_id_set).count(),
1321 None => 0,
1322 };
1323 let nr_cpus = self.pdcpu_set.len();
1324
1325 ((nr_pds - nr_pds_overlap) * PD_UNIT) + // # non-overlapping PDs
1326 ((*NR_CPU_IDS - nr_cpus) * CPU_UNIT) + // # of CPUs
1327 (*NR_CPU_IDS - self.pd_id_set.first().unwrap()) // PD ID as a tiebreaker
1328 }
1329}
1330
1331impl PartialEq for PDSetInfo<'_> {
1332 fn eq(&self, other: &Self) -> bool {
1333 self.performance == other.performance
1334 && self.power == other.power
1335 && self.pdcpu_set == other.pdcpu_set
1336 }
1337}
1338
1339impl Hash for PDSetInfo<'_> {
1340 fn hash<H: Hasher>(&self, state: &mut H) {
1341 // We don't need to hash performance, power, and pd_id_set
1342 // since they are a kind of cache for pds_set.
1343 self.pdcpu_set.hash(state);
1344 }
1345}
1346
1347impl PartialEq for PerfCpuOrder {
1348 fn eq(&self, other: &Self) -> bool {
1349 self.perf_cap == other.perf_cap
1350 }
1351}
1352
1353impl fmt::Display for PerfCpuOrder {
1354 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1355 write!(
1356 f,
1357 "capacity bound: {} ({}%)\n",
1358 self.perf_cap,
1359 self.perf_util * 100.0
1360 )?;
1361 write!(f, " primary CPUs: {:?}\n", self.cpus_perf.borrow())?;
1362 write!(f, " overflow CPUs: {:?}", self.cpus_ovflw.borrow())?;
1363 Ok(())
1364 }
1365}
1366
1367/// Tests for enumerating the combinations of equivalence performance domains,
1368/// which used to be enumerated over the individual performance domains and
1369/// blow up on a hybrid processor, where there is one performance domain per
1370/// CPU. See <https://github.com/sched-ext/scx/issues/3340>.
1371#[cfg(test)]
1372mod tests {
1373 use super::*;
1374 use scx_utils::EqPerfDomain;
1375 use std::sync::Arc;
1376
1377 /// Build an energy model whose i-th equivalence performance domain has
1378 /// @eq_pd_nr_cpus[i] CPUs, each CPU in a performance domain of its own as
1379 /// on an Intel hybrid processor.
1380 fn energy_model(eq_pd_nr_cpus: &[usize]) -> EnergyModel {
1381 let mut perf_doms = BTreeMap::new();
1382 let mut eq_perf_doms = BTreeMap::new();
1383 let mut pd_id = 0;
1384
1385 for (eq_pd_id, &nr_cpus) in eq_pd_nr_cpus.iter().enumerate() {
1386 // Give every equivalence performance domain a performance table of
1387 // its own so that they stay distinct.
1388 let performance = 100 * (eq_pd_id + 1);
1389 let ps = PerfState {
1390 cost: performance,
1391 frequency: performance,
1392 inefficient: 0,
1393 performance,
1394 power: performance,
1395 };
1396 let perf_table: BTreeMap<usize, Arc<PerfState>> =
1397 [(performance, ps.into())].into_iter().collect();
1398
1399 let mut members = vec![];
1400 let mut span_bits = 0u64;
1401 for _ in 0..nr_cpus {
1402 let pd: Arc<PerfDomain> = PerfDomain {
1403 id: pd_id,
1404 span: Cpumask::from_vec(vec![1u64 << pd_id]),
1405 perf_table: perf_table.clone(),
1406 }
1407 .into();
1408 span_bits |= 1u64 << pd_id;
1409 perf_doms.insert(pd_id, pd.clone());
1410 members.push(pd);
1411 pd_id += 1;
1412 }
1413
1414 let eq_pd = EqPerfDomain {
1415 id: eq_pd_id,
1416 perf_doms: members,
1417 span: Cpumask::from_vec(vec![span_bits]),
1418 perf_table,
1419 };
1420 eq_perf_doms.insert(eq_pd_id, eq_pd.into());
1421 }
1422
1423 EnergyModel {
1424 perf_doms,
1425 eq_perf_doms,
1426 }
1427 }
1428
1429 /// Build a CPU preference order taking one CPU from each performance domain
1430 /// of @pd_adxs, so the performance domain listed first is the most
1431 /// preferred one.
1432 fn cpu_pref_order(pd_adxs: &[usize]) -> Vec<CpuId> {
1433 pd_adxs
1434 .iter()
1435 .enumerate()
1436 .map(|(core_rdx, &pd_adx)| CpuId {
1437 numa_adx: 0,
1438 pd_adx,
1439 llc_adx: 0,
1440 llc_rdx: 0,
1441 llc_kernel_id: 0,
1442 core_rdx,
1443 cpu_rdx: 0,
1444 cpu_adx: pd_adx,
1445 smt_level: 1,
1446 cache_size: 0,
1447 cpu_cap: 1024,
1448 big_core: true,
1449 turbo_core: false,
1450 cpu_sibling: pd_adx,
1451 })
1452 .collect()
1453 }
1454
1455 /// The CPUs of an equivalence performance domain are taken from its most
1456 /// preferred member performance domain first, not from the one with the
1457 /// lowest id.
1458 #[test]
1459 fn test_members_in_cpu_preference_order() {
1460 // One equivalence performance domain of 4 CPUs, each in a performance
1461 // domain of its own, preferred in the reverse order of their ids.
1462 let em = energy_model(&[4]);
1463 let cpus_pf = cpu_pref_order(&[3, 2, 1, 0]);
1464 let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1465
1466 let pd_ids: Vec<usize> = emo.eq_pds[0].iter().map(|pd| pd.id).collect();
1467 assert_eq!(pd_ids, vec![3, 2, 1, 0]);
1468
1469 // Taking 2 CPUs takes them from the 2 most preferred performance
1470 // domains.
1471 let expected: BTreeSet<usize> = [2, 3].into_iter().collect();
1472 assert_eq!(emo.gen_pdsi(&[2], 100.0).pd_id_set, expected);
1473 }
1474
1475 /// The equivalence performance domains themselves are ordered by CPU
1476 /// preference, so a count belongs to the equivalence performance domain of
1477 /// the same preference.
1478 #[test]
1479 fn test_eq_pds_in_cpu_preference_order() {
1480 // Two equivalence performance domains of 2 CPUs each, preferring the
1481 // CPUs of the second one.
1482 let em = energy_model(&[2, 2]);
1483 let cpus_pf = cpu_pref_order(&[2, 3, 0, 1]);
1484 let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1485
1486 let pd_ids: Vec<Vec<usize>> = emo
1487 .eq_pds
1488 .iter()
1489 .map(|perf_doms| perf_doms.iter().map(|pd| pd.id).collect())
1490 .collect();
1491 assert_eq!(pd_ids, vec![vec![2, 3], vec![0, 1]]);
1492
1493 // The first count belongs to the first equivalence performance domain,
1494 // which is the preferred one.
1495 let expected: BTreeSet<usize> = [2].into_iter().collect();
1496 assert_eq!(emo.gen_pdsi(&[1, 0], 100.0).pd_id_set, expected);
1497 }
1498
1499 /// A 28-thread hybrid processor (8 P-cores, 16 E-cores, and 4 LP-E-cores)
1500 /// collapsing into three equivalence performance domains. Enumerating over
1501 /// its 28 performance domains, one per CPU, would take 2^28 - 1
1502 /// combinations.
1503 #[test]
1504 fn test_hybrid_combinations() {
1505 let em = energy_model(&[8, 16, 4]);
1506 let cpus_pf = vec![];
1507 let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1508
1509 // (8 + 1) * (16 + 1) * (4 + 1) - 1
1510 assert_eq!(emo.nr_cpus_combinations.len(), 764);
1511 assert!(emo.nr_cpus_combinations.contains(&vec![8, 16, 4]));
1512 assert_eq!(emo.gen_pds_combinations(100.0).len(), 764);
1513 }
1514
1515 /// A processor whose performance domains do not collapse at all, as
1516 /// per-core binning could produce. Enumerating all the combinations would
1517 /// take 2^24 - 1 of them, exceeding @MAX_EQPD_COMBINATIONS, so only the
1518 /// runs of equivalence performance domains are considered.
1519 #[test]
1520 fn test_uncollapsed_combinations_fall_back_to_runs() {
1521 let em = energy_model(&[1; 24]);
1522 let cpus_pf = vec![];
1523 let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1524
1525 // 24 forward runs, 24 backward runs, and 24 single runs, of which the
1526 // all-CPU run and the two end single runs are duplicates.
1527 assert_eq!(emo.nr_cpus_combinations.len(), 69);
1528 assert!(emo.nr_cpus_combinations.contains(&vec![1; 24]));
1529
1530 // Every run takes CPUs from consecutive equivalence performance
1531 // domains.
1532 for nr_cpus in emo.nr_cpus_combinations.iter() {
1533 let first = nr_cpus.iter().position(|&nr| nr > 0).unwrap();
1534 let last = nr_cpus.iter().rposition(|&nr| nr > 0).unwrap();
1535 assert!(nr_cpus[first..=last].iter().all(|&nr| nr > 0));
1536 }
1537 }
1538
1539 /// A single equivalence performance domain, so a combination is just how
1540 /// many of its CPUs to take.
1541 #[test]
1542 fn test_uniform_combinations() {
1543 let em = energy_model(&[8]);
1544 let cpus_pf = vec![];
1545 let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1546
1547 assert_eq!(emo.nr_cpus_combinations.len(), 8);
1548 assert_eq!(emo.gen_pds_combinations(100.0).len(), 8);
1549 }
1550}