Skip to main content

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::Result;
10use anyhow::anyhow;
11use itertools::Itertools;
12use itertools::iproduct;
13use scx_utils::CoreType;
14use scx_utils::Cpumask;
15use scx_utils::EnergyModel;
16use scx_utils::NR_CPU_IDS;
17use scx_utils::PerfDomain;
18use scx_utils::PerfState;
19use scx_utils::Topology;
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.values() {
361            for neighbors in cpdom.neighbor_map.borrow_mut().values() {
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 ncpdoms in v.neighbor_map.borrow().values() {
386                    for ncpdom_id in ncpdoms.borrow().iter() {
387                        if let Some(is_big) = cpdom_types.get(ncpdom_id)
388                            && *is_big == key.is_big
389                        {
390                            v.cpdom_alt_id.set(*ncpdom_id);
391                            break 'outer;
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#[allow(clippy::upper_case_acronyms)]
510#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
511struct PDS<'a> {
512    pd: &'a PerfDomain,
513    ps: &'a PerfState,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
517struct PDCpu<'a> {
518    pd: &'a PerfDomain, // performance domain
519    cpu_vid: usize,     // virtual ID of a CPU on the performance domain
520}
521
522#[derive(Debug, Clone, Eq)]
523struct PDSetInfo<'a> {
524    performance: usize,
525    power: usize,
526    pdcpu_set: BTreeSet<PDCpu<'a>>,
527    pd_id_set: BTreeSet<usize>, // pd:id:0, pd:id:1
528}
529
530const PD_UNIT: usize = 100_000_000;
531const CPU_UNIT: usize = 100_000;
532const LOOKAHEAD_CNT: usize = 10;
533
534/// Upper bound on the number of equivalence performance domain combinations to
535/// consider, to keep the number of combinations manageable. The performance
536/// domains of a processor may not collapse into a few equivalence performance
537/// domains -- per-core binning, for one, could give every core its own
538/// performance table. See <https://github.com/sched-ext/scx/issues/3340>.
539const MAX_EQPD_COMBINATIONS: u128 = 100_000;
540
541impl<'a> EnergyModelOptimizer<'a> {
542    fn new(em: &'a EnergyModel, cpus_pf: &'a Vec<CpuId>) -> EnergyModelOptimizer<'a> {
543        let tot_perf = em.perf_total();
544
545        let eq_pds = Self::sort_eq_pds(em, cpus_pf);
546        let max_nr_cpus: Vec<usize> = eq_pds
547            .iter()
548            .map(|perf_doms| perf_doms.iter().map(|pd| pd.span.weight()).sum())
549            .collect();
550        let nr_cpus_combinations = Self::gen_nr_cpus_combinations(&max_nr_cpus);
551
552        let pdss_infos: BTreeMap<usize, RefCell<HashSet<PDSetInfo<'a>>>> = BTreeMap::new();
553        let pdss_infos = pdss_infos.into();
554
555        let perf_pdsi: BTreeMap<usize, PDSetInfo<'a>> = BTreeMap::new();
556        let perf_pdsi = perf_pdsi.into();
557
558        let mut pd_cpu_order: BTreeMap<usize, RefCell<Vec<usize>>> = BTreeMap::new();
559        let mut cpus_topological_order: Vec<usize> = vec![];
560        for cpuid in cpus_pf.iter() {
561            match pd_cpu_order.get(&cpuid.pd_adx) {
562                Some(v) => {
563                    let mut v = v.borrow_mut();
564                    v.push(cpuid.cpu_adx);
565                }
566                None => {
567                    let v = vec![cpuid.cpu_adx];
568                    pd_cpu_order.insert(cpuid.pd_adx, v.into());
569                }
570            }
571            cpus_topological_order.push(cpuid.cpu_adx);
572        }
573
574        let perf_cpu_order: BTreeMap<usize, PerfCpuOrder> = BTreeMap::new();
575        let perf_cpu_order = perf_cpu_order.into();
576
577        debug!("# pd_cpu_order");
578        debug!("{:#?}", pd_cpu_order);
579
580        EnergyModelOptimizer {
581            eq_pds,
582            nr_cpus_combinations,
583            cpus_topological_order,
584            pd_cpu_order,
585            tot_perf,
586            pdss_infos,
587            perf_pdsi,
588            perf_cpu_order,
589        }
590    }
591
592    fn get_perf_cpu_order_table(
593        em: &'a EnergyModel,
594        cpus_pf: &'a Vec<CpuId>,
595    ) -> BTreeMap<usize, PerfCpuOrder> {
596        let emo = EnergyModelOptimizer::new(em, cpus_pf);
597        emo.gen_perf_cpu_order_table();
598
599        emo.perf_cpu_order.borrow().clone()
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 = if powersave {
620            cpuids[0].cpu_cap
621        } else {
622            tot_perf
623        };
624
625        let perf_util: f32 = (perf_cap as f32) / (tot_perf as f32);
626        let cpus: Vec<usize> = cpuids.iter().map(|cpuid| cpuid.cpu_adx).collect();
627        let cpus_perf: Vec<usize> = cpus[..1].to_vec();
628        let cpus_ovflw: Vec<usize> = cpus[1..].to_vec();
629        PerfCpuOrder {
630            perf_cap,
631            perf_util,
632            cpus_perf: cpus_perf.clone().into(),
633            cpus_ovflw: cpus_ovflw.clone().into(),
634        }
635    }
636
637    /// Generate the performance versus CPU preference order table based on
638    /// the system's CPU topology and energy model. The table consists of the
639    /// following information (PerfCpuOrder):
640    ///
641    ///   - PerfCpuOrder::perf_cap: The upper bound of the performance
642    ///     capacity covered by this tuple.
643    ///
644    ///   - PerfCpuOrder::cpus_perf: Primary CPUs to be used is ordered
645    ///     by preference.
646    ///
647    ///   - PerfCpuOrder::cpus_ovrflw: When the system load goes beyond
648    ///     @perf_cap, the list of CPUs to be used is ordered by preference.
649    fn gen_perf_cpu_order_table(&'a self) {
650        // First, generate all possible combinations of CPUs (e.g., two CPUs
651        // in performance domain 0 and three CPUs in performance domain 1) to
652        // achieve the possible performance capacities with minimal energy
653        // consumption. We assume a reasonable load balancer, so the
654        // utilization of the used CPUs is similar.
655        self.gen_all_pds_combinations();
656
657        // Then, from all the possible combinations of performance versus
658        // CPU sets, select a list of combinations that minimize the number of
659        // active performance domains and reduce the number of performance
660        // domain switches when changing performance levels.
661        self.gen_perf_pds_table();
662
663        // Finally, assign CPUs (@cpu_adx) to the virtual CPU ID (@cpu_vid) of
664        // a performance domain.
665        self.assign_cpu_vids();
666    }
667
668    /// Generate a CPU order table for each performance range.
669    fn assign_cpu_vids(&'a self) {
670        // Generate CPU order within the performance range (@cpus_perf).
671        for (&perf_cap, pdsi) in self.perf_pdsi.borrow().iter() {
672            let mut cpus_perf: Vec<usize> = vec![];
673
674            for pdcpu in pdsi.pdcpu_set.iter() {
675                let pd_id = pdcpu.pd.id;
676                let cpu_vid = pdcpu.cpu_vid;
677                let cpu_order = self.pd_cpu_order.get(&pd_id).unwrap().borrow();
678                let cpu_adx = cpu_order[cpu_vid];
679                cpus_perf.push(cpu_adx);
680            }
681
682            let perf_util: f32 = (perf_cap as f32) / (self.tot_perf as f32);
683            let cpus_perf = self.sort_cpus_by_topological_order(&cpus_perf);
684            let cpus_ovflw: Vec<usize> = vec![];
685
686            let mut perf_cpu_order = self.perf_cpu_order.borrow_mut();
687            perf_cpu_order.insert(
688                perf_cap,
689                PerfCpuOrder {
690                    perf_cap,
691                    perf_util,
692                    cpus_perf: cpus_perf.clone().into(),
693                    cpus_ovflw: cpus_ovflw.clone().into(),
694                },
695            );
696        }
697
698        // Generate CPU order beyond the performance range (@cpus_ovflw).
699        let perf_cpu_order = self.perf_cpu_order.borrow();
700        let perf_caps: Vec<_> = self.perf_pdsi.borrow().keys().cloned().collect();
701        for o in 1..perf_caps.len() {
702            // Gather all @cpus_perf from the upper performance ranges.
703            let ovrflw_perf_caps = &perf_caps[o..];
704            let mut ovrflw_cpus_all: Vec<usize> = vec![];
705            for perf_cap in ovrflw_perf_caps.iter() {
706                let cpu_order = perf_cpu_order.get(perf_cap).unwrap();
707                let cpus_perf = cpu_order.cpus_perf.borrow();
708                ovrflw_cpus_all.extend(cpus_perf.iter().cloned());
709            }
710
711            // Filter out already taken CPUs from the @ovrflw_cpus_all,
712            // and build @cpus_ovrflw.
713            let mut cpu_set = HashSet::<usize>::new();
714            let perf_cap = perf_caps[o - 1];
715            let cpu_order = perf_cpu_order.get(&perf_cap).unwrap();
716            let cpus_perf = cpu_order.cpus_perf.borrow();
717            for &cpu_adx in cpus_perf.iter() {
718                cpu_set.insert(cpu_adx);
719            }
720
721            let mut cpus_ovflw: Vec<usize> = vec![];
722            for &cpu_adx in ovrflw_cpus_all.iter() {
723                if cpu_set.get(&cpu_adx).is_none() {
724                    cpus_ovflw.push(cpu_adx);
725                    cpu_set.insert(cpu_adx);
726                }
727            }
728
729            // Inject the constructed @cpus_ovrflw to the table.
730            let mut v = cpu_order.cpus_ovflw.borrow_mut();
731            v.extend(cpus_ovflw.iter().cloned());
732        }
733
734        // Debug print of the generated table
735        debug!("## gen_perf_cpu_order_table");
736        debug!("{:#?}", perf_cpu_order);
737    }
738
739    /// Sort the CPU IDs by topological order (@self.cpus_topological_order).
740    fn sort_cpus_by_topological_order(&'a self, cpus: &Vec<usize>) -> Vec<usize> {
741        let mut sorted: Vec<usize> = vec![];
742        for &cpu_adx in self.cpus_topological_order.iter() {
743            if cpus.iter().find(|&&x| x == cpu_adx).is_some() {
744                sorted.push(cpu_adx);
745            }
746        }
747        sorted
748    }
749
750    /// Generate a table of performance vs. performance domain sets
751    /// (@self.perf_pdss) from all the possible performance domain & state
752    /// combinations (@self.pdss_infos).
753    ///
754    /// An example result is as follows:
755    ///     PERF: [_, 300]
756    ///             pd:id: 0 -- cpu_vid: 0
757    ///             pd:id: 0 -- cpu_vid: 1
758    ///     PERF: [_, 1138]
759    ///             pd:id: 0 -- cpu_vid: 0
760    ///             pd:id: 0 -- cpu_vid: 1
761    ///             pd:id: 1 -- cpu_vid: 0
762    ///             pd:id: 1 -- cpu_vid: 1
763    ///     PERF: [_, 3386]
764    ///             pd:id: 1 -- cpu_vid: 0
765    ///             pd:id: 1 -- cpu_vid: 1
766    ///             pd:id: 1 -- cpu_vid: 2
767    ///             pd:id: 2 -- cpu_vid: 0
768    ///             pd:id: 2 -- cpu_vid: 1
769    ///     PERF: [_, 3977]
770    ///             pd:id: 0 -- cpu_vid: 0
771    ///             pd:id: 1 -- cpu_vid: 0
772    ///             pd:id: 1 -- cpu_vid: 1
773    ///             pd:id: 1 -- cpu_vid: 2
774    ///             pd:id: 2 -- cpu_vid: 0
775    ///             pd:id: 2 -- cpu_vid: 1
776    ///     PERF: [_, 4508]
777    ///             pd:id: 0 -- cpu_vid: 0
778    ///             pd:id: 0 -- cpu_vid: 1
779    ///             pd:id: 1 -- cpu_vid: 0
780    ///             pd:id: 1 -- cpu_vid: 1
781    ///             pd:id: 1 -- cpu_vid: 2
782    ///             pd:id: 2 -- cpu_vid: 0
783    ///             pd:id: 2 -- cpu_vid: 1
784    ///     PERF: [_, 5627]
785    ///             pd:id: 0 -- cpu_vid: 0
786    ///             pd:id: 0 -- cpu_vid: 1
787    ///             pd:id: 1 -- cpu_vid: 0
788    ///             pd:id: 1 -- cpu_vid: 1
789    ///             pd:id: 1 -- cpu_vid: 2
790    ///             pd:id: 2 -- cpu_vid: 0
791    ///             pd:id: 2 -- cpu_vid: 1
792    ///             pd:id: 3 -- cpu_vid: 0
793    fn gen_perf_pds_table(&'a self) {
794        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];
795
796        // Find the best performance domains for each system utilization target.
797        for &util in utils.iter() {
798            let mut best_pdsi: Option<PDSetInfo<'a>>;
799            let mut del_pdsi: Option<PDSetInfo<'a>> = None;
800
801            match self.perf_pdsi.borrow().last_key_value() {
802                Some((_, base)) => {
803                    best_pdsi = self.find_perf_pds_for(util, Some(base));
804
805                    // If the next performance level (@best_pdsi) is subsumed
806                    // by the previous level (@base), extend the base to the
807                    // next level. To this end, insert the extended base (with
808                    // updated performance and power values) and delete the old
809                    // base.
810                    if let Some(ref best) = best_pdsi
811                        && best.pdcpu_set.is_subset(&base.pdcpu_set)
812                    {
813                        let ext_pdcpu = PDSetInfo {
814                            performance: best.performance,
815                            power: best.power,
816                            pdcpu_set: base.pdcpu_set.clone(),
817                            pd_id_set: base.pd_id_set.clone(),
818                        };
819                        best_pdsi = Some(ext_pdcpu);
820                        del_pdsi = Some(base.clone());
821                    }
822                }
823                None => {
824                    best_pdsi = self.find_perf_pds_for(util, None);
825                }
826            };
827
828            if let Some(best_pdsi) = best_pdsi {
829                self.perf_pdsi
830                    .borrow_mut()
831                    .insert(best_pdsi.performance, best_pdsi);
832            }
833
834            if let Some(del_pdsi) = del_pdsi {
835                self.perf_pdsi.borrow_mut().remove(&del_pdsi.performance);
836            }
837        }
838
839        // Debug print of the generated table
840        debug!("## gen_perf_pds_table");
841        for (perf, pdsi) in self.perf_pdsi.borrow().iter() {
842            debug!("PERF: [_, {}]", perf);
843            for pdcpu in pdsi.pdcpu_set.iter() {
844                debug!(
845                    "        pd:id: {:?} -- cpu_vid: {}",
846                    pdcpu.pd.id, pdcpu.cpu_vid
847                );
848            }
849        }
850    }
851
852    fn find_perf_pds_for(
853        &'a self,
854        util: f32,
855        base: Option<&PDSetInfo<'a>>,
856    ) -> Option<PDSetInfo<'a>> {
857        let target_perf = (util * self.tot_perf as f32) as usize;
858        let mut lookahead = 0;
859        let mut min_dist: usize = usize::MAX;
860        let mut best_pdsi: Option<PDSetInfo<'a>> = None;
861
862        let pdss_infos = self.pdss_infos.borrow();
863        for (&pdsi_perf, pdsi_set) in pdss_infos.iter() {
864            if pdsi_perf >= target_perf {
865                let pdsi_set_ref = pdsi_set.borrow();
866                for pdsi in pdsi_set_ref.iter() {
867                    let dist = pdsi.dist(base);
868                    if dist < min_dist {
869                        min_dist = dist;
870                        best_pdsi = Some(pdsi.clone());
871                    }
872                }
873                lookahead += 1;
874                if lookahead >= LOOKAHEAD_CNT {
875                    break;
876                }
877            }
878        }
879
880        best_pdsi
881    }
882
883    /// Generate all possible performance domain & state combinations,
884    /// @self.pdss_infos. Each combination represents a set of performance
885    /// domains (and their corresponding performance states) that achieve the
886    /// requested performance with minimal power consumption.
887    ///
888    /// We assume a 'reasonable load balancer,' so the CPU utilization of all
889    /// the involved CPUs is similar.
890    ///
891    /// An example result is as follows:
892    ///
893    ///     PERF: [_, 5135]
894    ///         perf: 5135 -- power: 5475348
895    ///             pd:id: 0 -- cpu_vid: 0
896    ///             pd:id: 1 -- cpu_vid: 0
897    ///             pd:id: 1 -- cpu_vid: 1
898    ///             pd:id: 1 -- cpu_vid: 2
899    ///             pd:id: 2 -- cpu_vid: 0
900    ///             pd:id: 2 -- cpu_vid: 1
901    ///             pd:id: 3 -- cpu_vid: 0
902    ///     PERF: [_, 5187]
903    ///         perf: 5187 -- power: 4844969
904    ///             pd:id: 0 -- cpu_vid: 0
905    ///             pd:id: 0 -- cpu_vid: 1
906    ///             pd:id: 1 -- cpu_vid: 0
907    ///             pd:id: 1 -- cpu_vid: 1
908    ///             pd:id: 1 -- cpu_vid: 2
909    ///             pd:id: 2 -- cpu_vid: 0
910    ///             pd:id: 2 -- cpu_vid: 1
911    ///             pd:id: 3 -- cpu_vid: 0
912    ///     PERF: [_, 5195]
913    ///         perf: 5195 -- power: 5924606
914    ///             pd:id: 1 -- cpu_vid: 0
915    ///             pd:id: 1 -- cpu_vid: 1
916    ///             pd:id: 1 -- cpu_vid: 2
917    ///             pd:id: 2 -- cpu_vid: 0
918    ///             pd:id: 2 -- cpu_vid: 1
919    ///             pd:id: 3 -- cpu_vid: 0
920    ///     PERF: [_, 5217]
921    ///         perf: 5217 -- power: 4894911
922    ///             pd:id: 0 -- cpu_vid: 0
923    ///             pd:id: 0 -- cpu_vid: 1
924    ///             pd:id: 1 -- cpu_vid: 0
925    ///             pd:id: 1 -- cpu_vid: 1
926    ///             pd:id: 1 -- cpu_vid: 2
927    ///             pd:id: 2 -- cpu_vid: 0
928    ///             pd:id: 2 -- cpu_vid: 1
929    ///             pd:id: 3 -- cpu_vid: 0
930    ///     PERF: [_, 5225]
931    ///         perf: 5225 -- power: 5665770
932    ///             pd:id: 0 -- cpu_vid: 0
933    ///             pd:id: 1 -- cpu_vid: 0
934    ///             pd:id: 1 -- cpu_vid: 1
935    ///             pd:id: 1 -- cpu_vid: 2
936    ///             pd:id: 2 -- cpu_vid: 0
937    ///             pd:id: 2 -- cpu_vid: 1
938    ///             pd:id: 3 -- cpu_vid: 0
939    ///     PERF: [_, 5316]
940    ///         perf: 5316 -- power: 5860568
941    ///             pd:id: 0 -- cpu_vid: 0
942    ///             pd:id: 1 -- cpu_vid: 0
943    ///             pd:id: 1 -- cpu_vid: 1
944    ///             pd:id: 1 -- cpu_vid: 2
945    ///             pd:id: 2 -- cpu_vid: 0
946    ///             pd:id: 2 -- cpu_vid: 1
947    ///             pd:id: 3 -- cpu_vid: 0
948    fn gen_all_pds_combinations(&'a self) {
949        // Start from the min (0%) and max (100%) CPU utilizations
950        let pdsi_vec = self.gen_pds_combinations(0.0);
951        self.insert_pds_combinations(&pdsi_vec);
952
953        let pdsi_vec = self.gen_pds_combinations(100.0);
954        self.insert_pds_combinations(&pdsi_vec);
955
956        // Then dive into the range between the min and max.
957        self.gen_perf_cpuset_table_range(0, 100);
958
959        // Debug print performance table
960        debug!("## gen_all_pds_combinations");
961        for (perf, pdss_info) in self.pdss_infos.borrow().iter() {
962            debug!("PERF: [_, {}]", perf);
963            for pdsi in pdss_info.borrow().iter() {
964                debug!("    perf: {} -- power: {}", pdsi.performance, pdsi.power);
965                for pdcpu in pdsi.pdcpu_set.iter() {
966                    debug!(
967                        "        pd:id: {:?} -- cpu_vid: {}",
968                        pdcpu.pd.id, pdcpu.cpu_vid
969                    );
970                }
971            }
972        }
973    }
974
975    fn gen_perf_cpuset_table_range(&'a self, low: isize, high: isize) {
976        if low > high {
977            return;
978        }
979
980        // If there is a new performance point in the middle,
981        // let's further explore. Otherwise, stop it here.
982        let mid: isize = low + (high - low) / 2;
983        let pdsi_vec = self.gen_pds_combinations(mid as f32);
984        let found_new = self.insert_pds_combinations(&pdsi_vec);
985        if found_new {
986            self.gen_perf_cpuset_table_range(mid + 1, high);
987            self.gen_perf_cpuset_table_range(low, mid - 1);
988        }
989    }
990
991    /// Rank the performance domains by CPU preference: a performance domain is
992    /// as preferred as its most preferred CPU, which is the first one appearing
993    /// in @cpus_pf. A performance domain with no CPU in @cpus_pf is unranked and
994    /// comes last.
995    fn rank_perf_doms(cpus_pf: &[CpuId]) -> BTreeMap<usize, usize> {
996        let mut ranks = BTreeMap::new();
997
998        for (rank, cpuid) in cpus_pf.iter().enumerate() {
999            ranks.entry(cpuid.pd_adx).or_insert(rank);
1000        }
1001
1002        ranks
1003    }
1004
1005    /// Collect the member performance domains of each equivalence performance
1006    /// domain of @em, ordering both the members and the equivalence performance
1007    /// domains by CPU preference. See @EnergyModelOptimizer::eq_pds.
1008    fn sort_eq_pds(em: &'a EnergyModel, cpus_pf: &'a [CpuId]) -> Vec<Vec<&'a PerfDomain>> {
1009        let ranks = Self::rank_perf_doms(cpus_pf);
1010        let rank_of = |pd: &PerfDomain| ranks.get(&pd.id).copied().unwrap_or(usize::MAX);
1011
1012        let mut eq_pds: Vec<Vec<&'a PerfDomain>> = em
1013            .eq_perf_doms
1014            .values()
1015            .map(|eq_pd| {
1016                let mut perf_doms: Vec<&'a PerfDomain> =
1017                    eq_pd.perf_doms.iter().map(|pd| pd.as_ref()).collect();
1018                perf_doms.sort_by_key(|pd| rank_of(pd));
1019                perf_doms
1020            })
1021            .collect();
1022
1023        // An equivalence performance domain is as preferred as its most
1024        // preferred member performance domain.
1025        eq_pds.sort_by_key(|perf_doms| perf_doms.iter().map(|pd| rank_of(pd)).min());
1026
1027        eq_pds
1028    }
1029
1030    /// Enumerate how many CPUs to take from each equivalence performance
1031    /// domain, taking at most @max_nr_cpus[i] CPUs from the i-th one. See
1032    /// @EnergyModelOptimizer::nr_cpus_combinations.
1033    fn gen_nr_cpus_combinations(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1034        // The number of all the possible combinations. An equivalence
1035        // performance domain can contribute none, some, or all of its CPUs, so
1036        // it has max_nr_cpus + 1 choices, and the choices of all the
1037        // equivalence performance domains multiply. Subtract one for the
1038        // combination taking no CPU at all. The product saturates instead of
1039        // overflowing when there are hundreds of equivalence performance
1040        // domains.
1041        let nr_combinations = max_nr_cpus
1042            .iter()
1043            .fold(1u128, |nr, &max| nr.saturating_mul(max as u128 + 1))
1044            - 1;
1045        if nr_combinations <= MAX_EQPD_COMBINATIONS {
1046            return Self::gen_all_nr_cpus(max_nr_cpus);
1047        }
1048
1049        let combinations = Self::gen_run_nr_cpus(max_nr_cpus);
1050        warn!(
1051            "{} equivalence performance domains yield {nr_combinations} combinations, \
1052             exceeding the limit of {MAX_EQPD_COMBINATIONS}, so consider only {} of them",
1053            max_nr_cpus.len(),
1054            combinations.len(),
1055        );
1056
1057        combinations
1058    }
1059
1060    /// Enumerate how many CPUs to take from each equivalence performance domain
1061    /// in every possible way. An equivalence performance domain independently
1062    /// takes 0, 1, ... up to all of its CPUs, so a combination picks one count
1063    /// from the range `0..=max_nr_cpus[i]` of every equivalence performance
1064    /// domain. Picking one element from each of several ranges, in all the
1065    /// possible ways, is the cartesian product of those ranges, which
1066    /// `multi_cartesian_product` enumerates one combination at a time. See
1067    /// @EnergyModelOptimizer::nr_cpus_combinations for an example.
1068    fn gen_all_nr_cpus(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1069        max_nr_cpus
1070            .iter()
1071            .map(|&max| 0..=max)
1072            .multi_cartesian_product()
1073            // Drop the one combination taking no CPU at all.
1074            .filter(|nr_cpus| nr_cpus.iter().any(|&nr| nr > 0))
1075            .collect()
1076    }
1077
1078    /// Enumerate how many CPUs to take from each equivalence performance domain
1079    /// when there are too many combinations to consider them all
1080    /// (@MAX_EQPD_COMBINATIONS). Only the runs of equivalence performance
1081    /// domains are considered, where a run takes all the CPUs of consecutive
1082    /// equivalence performance domains and some of the CPUs of the last one:
1083    ///
1084    ///   - A forward run grows from the first equivalence performance domain,
1085    ///     adding one more equivalence performance domain at a time.
1086    ///   - A backward run grows from the last equivalence performance domain
1087    ///     in the opposite direction.
1088    ///   - A single run takes CPUs from one equivalence performance domain and
1089    ///     none from the others.
1090    ///
1091    /// For example, with three equivalence performance domains of 1, 2, and 1
1092    /// CPUs, the runs are:
1093    ///
1094    ///     forward:  [1, 0, 0]
1095    ///               [1, 1, 0], [1, 2, 0]
1096    ///               [1, 2, 1]
1097    ///     backward: [0, 0, 1]
1098    ///               [0, 1, 1], [0, 2, 1]
1099    ///               [1, 2, 1]
1100    ///     single:   [1, 0, 0]
1101    ///               [0, 1, 0], [0, 2, 0]
1102    ///               [0, 0, 1]
1103    ///
1104    /// which is 3 * nr_cpus = 12 combinations, or 9 once the duplicates are
1105    /// removed. Since there are at most 3 * nr_cpus of them, the runs always
1106    /// fit in @MAX_EQPD_COMBINATIONS.
1107    fn gen_run_nr_cpus(max_nr_cpus: &[usize]) -> Vec<Vec<usize>> {
1108        let nr_eq_pds = max_nr_cpus.len();
1109        let mut combinations = vec![];
1110
1111        // Take 1 to all the CPUs of the @i-th equivalence performance domain,
1112        // which is the last one of a forward run, the last one of a backward
1113        // run, and the only one of a single run.
1114        for i in 0..nr_eq_pds {
1115            let mut forward = vec![0; nr_eq_pds];
1116            forward[..i].copy_from_slice(&max_nr_cpus[..i]);
1117
1118            let mut backward = vec![0; nr_eq_pds];
1119            backward[i + 1..].copy_from_slice(&max_nr_cpus[i + 1..]);
1120
1121            let mut single = vec![0; nr_eq_pds];
1122
1123            for nr in 1..=max_nr_cpus[i] {
1124                forward[i] = nr;
1125                backward[i] = nr;
1126                single[i] = nr;
1127
1128                combinations.push(forward.clone());
1129                combinations.push(backward.clone());
1130                combinations.push(single.clone());
1131            }
1132        }
1133
1134        combinations.sort();
1135        combinations.dedup();
1136
1137        combinations
1138    }
1139
1140    /// Generate the combinations of performance domains and states to consider
1141    /// for a given CPU utilization (@util), one for each combination of
1142    /// per-equivalence performance domain CPU counts.
1143    fn gen_pds_combinations(&'a self, util: f32) -> Vec<PDSetInfo<'a>> {
1144        self.nr_cpus_combinations
1145            .iter()
1146            .map(|nr_cpus| self.gen_pdsi(nr_cpus, util))
1147            .collect()
1148    }
1149
1150    /// Build the performance domains and states taking @nr_cpus[i] CPUs from
1151    /// the i-th equivalence performance domain at the performance state for
1152    /// @util. The CPUs are taken from the member performance domains in order,
1153    /// so the CPUs for a count of N are always a subset of the ones for N + 1.
1154    fn gen_pdsi(&'a self, nr_cpus: &[usize], util: f32) -> PDSetInfo<'a> {
1155        let mut pds_set = vec![];
1156
1157        for (perf_doms, &nr) in self.eq_pds.iter().zip(nr_cpus.iter()) {
1158            // All the member performance domains share one performance table,
1159            // so they are all at the same performance state.
1160            let ps = perf_doms[0].select_perf_state(util).unwrap();
1161            let mut remaining = nr;
1162
1163            for pd in perf_doms.iter() {
1164                if remaining == 0 {
1165                    break;
1166                }
1167
1168                // A performance domain contributes at most its own CPUs.
1169                let take = remaining.min(pd.span.weight());
1170                for _ in 0..take {
1171                    pds_set.push(PDS::new(pd, ps));
1172                }
1173                remaining -= take;
1174            }
1175        }
1176
1177        PDSetInfo::new(pds_set)
1178    }
1179
1180    fn insert_pds_combinations(&self, new_pdsi_vec: &Vec<PDSetInfo<'a>>) -> bool {
1181        // For the same performance, keep the PDS combinations with the lowest
1182        // power consumption. If there are more than one lowest, keep them all
1183        // to choose one later when assigning CPUs from the selected
1184        // performance domains.
1185        let mut found_new = false;
1186
1187        for new_pdsi in new_pdsi_vec.iter() {
1188            let mut pdss_infos = self.pdss_infos.borrow_mut();
1189            let v = pdss_infos.get(&new_pdsi.performance);
1190            match v {
1191                // There are already PDSetInfo in the list.
1192                Some(v) => {
1193                    let mut v = v.borrow_mut();
1194                    let pdsi = &v.iter().next().unwrap();
1195                    if pdsi.power == new_pdsi.power {
1196                        // If the power consumptions are the same, keep both.
1197                        if v.insert(new_pdsi.clone()) {
1198                            found_new = true;
1199                        }
1200                    } else if pdsi.power > new_pdsi.power {
1201                        // If the new one takes less power, keep the new one.
1202                        v.clear();
1203                        v.insert(new_pdsi.clone());
1204                        found_new = true;
1205                    }
1206                }
1207                // This is the first for the performance target.
1208                None => {
1209                    // Let's add it and move on.
1210                    let mut v: HashSet<PDSetInfo<'a>> = HashSet::new();
1211                    v.insert(new_pdsi.clone());
1212                    pdss_infos.insert(new_pdsi.performance, v.into());
1213                    found_new = true;
1214                }
1215            }
1216        }
1217        found_new
1218    }
1219}
1220
1221impl<'a> PDS<'_> {
1222    fn new(pd: &'a PerfDomain, ps: &'a PerfState) -> PDS<'a> {
1223        PDS { pd, ps }
1224    }
1225}
1226
1227impl<'a> PDCpu<'_> {
1228    fn new(pd: &'a PerfDomain, cpu_vid: usize) -> PDCpu<'a> {
1229        PDCpu { pd, cpu_vid }
1230    }
1231}
1232
1233impl fmt::Display for PDS<'_> {
1234    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1235        write!(
1236            f,
1237            "pd:id:{}/pd:weight:{}/ps:cap:{}/ps:power:{}",
1238            self.pd.id,
1239            self.pd.span.weight(),
1240            self.ps.performance,
1241            self.ps.power,
1242        )?;
1243        Ok(())
1244    }
1245}
1246
1247impl<'a> PDSetInfo<'_> {
1248    fn new(pds_set: Vec<PDS<'a>>) -> PDSetInfo<'a> {
1249        // Create a pd_id_set and calculate performance and power.
1250        let mut performance = 0;
1251        let mut power = 0;
1252        let mut pd_id_set: BTreeSet<usize> = BTreeSet::new();
1253
1254        for pds in pds_set.iter() {
1255            performance += pds.ps.performance;
1256            power += pds.ps.power;
1257            pd_id_set.insert(pds.pd.id);
1258        }
1259
1260        // Create a pdcpu_set, so first gather the same PDS entries.
1261        let mut pds_map: BTreeMap<PDS<'a>, RefCell<Vec<PDS<'a>>>> = BTreeMap::new();
1262
1263        for pds in pds_set.iter() {
1264            let v = pds_map.get(pds);
1265            match v {
1266                Some(v) => {
1267                    let mut v = v.borrow_mut();
1268                    v.push(pds.clone());
1269                }
1270                None => {
1271                    let mut v: Vec<PDS<'a>> = Vec::new();
1272                    v.push(pds.clone());
1273                    pds_map.insert(pds.clone(), v.into());
1274                }
1275            }
1276        }
1277        // Then assign cpu virtual ids to pdcpu_set.
1278        let mut pdcpu_set: BTreeSet<PDCpu<'a>> = BTreeSet::new();
1279        let pds_map = pds_map;
1280
1281        for v in pds_map.values() {
1282            for (cpu_vid, pds) in v.borrow().iter().enumerate() {
1283                let pdcpu = PDCpu::new(pds.pd, cpu_vid);
1284                pdcpu_set.insert(pdcpu);
1285            }
1286        }
1287
1288        PDSetInfo {
1289            performance,
1290            power,
1291            pdcpu_set,
1292            pd_id_set,
1293        }
1294    }
1295
1296    /// Calculate the distance from @base to @self. We minimize the number of
1297    /// performance domains involved to reduce the leakage power consumption.
1298    /// We then maximize the overlap between the previous (i.e., base)
1299    /// performance domains and the new one for a smooth transition to the new
1300    /// cpuset with higher cache locality. Finally, we minimize the number of
1301    /// CPUs involved, thereby reducing the chance of contention for shared
1302    /// hardware resources (e.g., shared cache).
1303    fn dist(&self, base: Option<&PDSetInfo<'a>>) -> usize {
1304        let nr_pds = self.pd_id_set.len();
1305        let nr_pds_overlap = match base {
1306            Some(base) => self.pd_id_set.intersection(&base.pd_id_set).count(),
1307            None => 0,
1308        };
1309        let nr_cpus = self.pdcpu_set.len();
1310
1311        ((nr_pds - nr_pds_overlap) * PD_UNIT) +         // # non-overlapping PDs
1312        ((*NR_CPU_IDS - nr_cpus) * CPU_UNIT) +          // # of CPUs
1313        (*NR_CPU_IDS - self.pd_id_set.first().unwrap()) // PD ID as a tiebreaker
1314    }
1315}
1316
1317impl PartialEq for PDSetInfo<'_> {
1318    fn eq(&self, other: &Self) -> bool {
1319        self.performance == other.performance
1320            && self.power == other.power
1321            && self.pdcpu_set == other.pdcpu_set
1322    }
1323}
1324
1325impl Hash for PDSetInfo<'_> {
1326    fn hash<H: Hasher>(&self, state: &mut H) {
1327        // We don't need to hash performance, power, and pd_id_set
1328        // since they are a kind of cache for pds_set.
1329        self.pdcpu_set.hash(state);
1330    }
1331}
1332
1333impl PartialEq for PerfCpuOrder {
1334    fn eq(&self, other: &Self) -> bool {
1335        self.perf_cap == other.perf_cap
1336    }
1337}
1338
1339impl fmt::Display for PerfCpuOrder {
1340    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1341        writeln!(
1342            f,
1343            "capacity bound:  {} ({}%)",
1344            self.perf_cap,
1345            self.perf_util * 100.0
1346        )?;
1347        writeln!(f, "  primary CPUs:  {:?}", self.cpus_perf.borrow())?;
1348        write!(f, "  overflow CPUs: {:?}", self.cpus_ovflw.borrow())?;
1349        Ok(())
1350    }
1351}
1352
1353/// Tests for enumerating the combinations of equivalence performance domains,
1354/// which used to be enumerated over the individual performance domains and
1355/// blow up on a hybrid processor, where there is one performance domain per
1356/// CPU. See <https://github.com/sched-ext/scx/issues/3340>.
1357#[cfg(test)]
1358mod tests {
1359    use super::*;
1360    use scx_utils::EqPerfDomain;
1361    use std::sync::Arc;
1362
1363    /// Build an energy model whose i-th equivalence performance domain has
1364    /// @eq_pd_nr_cpus[i] CPUs, each CPU in a performance domain of its own as
1365    /// on an Intel hybrid processor.
1366    fn energy_model(eq_pd_nr_cpus: &[usize]) -> EnergyModel {
1367        let mut perf_doms = BTreeMap::new();
1368        let mut eq_perf_doms = BTreeMap::new();
1369        let mut pd_id = 0;
1370
1371        for (eq_pd_id, &nr_cpus) in eq_pd_nr_cpus.iter().enumerate() {
1372            // Give every equivalence performance domain a performance table of
1373            // its own so that they stay distinct.
1374            let performance = 100 * (eq_pd_id + 1);
1375            let ps = PerfState {
1376                cost: performance,
1377                frequency: performance,
1378                inefficient: 0,
1379                performance,
1380                power: performance,
1381            };
1382            let perf_table: BTreeMap<usize, Arc<PerfState>> =
1383                [(performance, ps.into())].into_iter().collect();
1384
1385            let mut members = vec![];
1386            let mut span_bits = 0u64;
1387            for _ in 0..nr_cpus {
1388                let pd: Arc<PerfDomain> = PerfDomain {
1389                    id: pd_id,
1390                    span: Cpumask::from_vec(vec![1u64 << pd_id]),
1391                    perf_table: perf_table.clone(),
1392                }
1393                .into();
1394                span_bits |= 1u64 << pd_id;
1395                perf_doms.insert(pd_id, pd.clone());
1396                members.push(pd);
1397                pd_id += 1;
1398            }
1399
1400            let eq_pd = EqPerfDomain {
1401                id: eq_pd_id,
1402                perf_doms: members,
1403                span: Cpumask::from_vec(vec![span_bits]),
1404                perf_table,
1405            };
1406            eq_perf_doms.insert(eq_pd_id, eq_pd.into());
1407        }
1408
1409        EnergyModel {
1410            perf_doms,
1411            eq_perf_doms,
1412        }
1413    }
1414
1415    /// Build a CPU preference order taking one CPU from each performance domain
1416    /// of @pd_adxs, so the performance domain listed first is the most
1417    /// preferred one.
1418    fn cpu_pref_order(pd_adxs: &[usize]) -> Vec<CpuId> {
1419        pd_adxs
1420            .iter()
1421            .enumerate()
1422            .map(|(core_rdx, &pd_adx)| CpuId {
1423                numa_adx: 0,
1424                pd_adx,
1425                llc_adx: 0,
1426                llc_rdx: 0,
1427                llc_kernel_id: 0,
1428                core_rdx,
1429                cpu_rdx: 0,
1430                cpu_adx: pd_adx,
1431                smt_level: 1,
1432                cache_size: 0,
1433                cpu_cap: 1024,
1434                big_core: true,
1435                turbo_core: false,
1436                cpu_sibling: pd_adx,
1437            })
1438            .collect()
1439    }
1440
1441    /// The CPUs of an equivalence performance domain are taken from its most
1442    /// preferred member performance domain first, not from the one with the
1443    /// lowest id.
1444    #[test]
1445    fn test_members_in_cpu_preference_order() {
1446        // One equivalence performance domain of 4 CPUs, each in a performance
1447        // domain of its own, preferred in the reverse order of their ids.
1448        let em = energy_model(&[4]);
1449        let cpus_pf = cpu_pref_order(&[3, 2, 1, 0]);
1450        let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1451
1452        let pd_ids: Vec<usize> = emo.eq_pds[0].iter().map(|pd| pd.id).collect();
1453        assert_eq!(pd_ids, vec![3, 2, 1, 0]);
1454
1455        // Taking 2 CPUs takes them from the 2 most preferred performance
1456        // domains.
1457        let expected: BTreeSet<usize> = [2, 3].into_iter().collect();
1458        assert_eq!(emo.gen_pdsi(&[2], 100.0).pd_id_set, expected);
1459    }
1460
1461    /// The equivalence performance domains themselves are ordered by CPU
1462    /// preference, so a count belongs to the equivalence performance domain of
1463    /// the same preference.
1464    #[test]
1465    fn test_eq_pds_in_cpu_preference_order() {
1466        // Two equivalence performance domains of 2 CPUs each, preferring the
1467        // CPUs of the second one.
1468        let em = energy_model(&[2, 2]);
1469        let cpus_pf = cpu_pref_order(&[2, 3, 0, 1]);
1470        let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1471
1472        let pd_ids: Vec<Vec<usize>> = emo
1473            .eq_pds
1474            .iter()
1475            .map(|perf_doms| perf_doms.iter().map(|pd| pd.id).collect())
1476            .collect();
1477        assert_eq!(pd_ids, vec![vec![2, 3], vec![0, 1]]);
1478
1479        // The first count belongs to the first equivalence performance domain,
1480        // which is the preferred one.
1481        let expected: BTreeSet<usize> = [2].into_iter().collect();
1482        assert_eq!(emo.gen_pdsi(&[1, 0], 100.0).pd_id_set, expected);
1483    }
1484
1485    /// A 28-thread hybrid processor (8 P-cores, 16 E-cores, and 4 LP-E-cores)
1486    /// collapsing into three equivalence performance domains. Enumerating over
1487    /// its 28 performance domains, one per CPU, would take 2^28 - 1
1488    /// combinations.
1489    #[test]
1490    fn test_hybrid_combinations() {
1491        let em = energy_model(&[8, 16, 4]);
1492        let cpus_pf = vec![];
1493        let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1494
1495        // (8 + 1) * (16 + 1) * (4 + 1) - 1
1496        assert_eq!(emo.nr_cpus_combinations.len(), 764);
1497        assert!(emo.nr_cpus_combinations.contains(&vec![8, 16, 4]));
1498        assert_eq!(emo.gen_pds_combinations(100.0).len(), 764);
1499    }
1500
1501    /// A processor whose performance domains do not collapse at all, as
1502    /// per-core binning could produce. Enumerating all the combinations would
1503    /// take 2^24 - 1 of them, exceeding @MAX_EQPD_COMBINATIONS, so only the
1504    /// runs of equivalence performance domains are considered.
1505    #[test]
1506    fn test_uncollapsed_combinations_fall_back_to_runs() {
1507        let em = energy_model(&[1; 24]);
1508        let cpus_pf = vec![];
1509        let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1510
1511        // 24 forward runs, 24 backward runs, and 24 single runs, of which the
1512        // all-CPU run and the two end single runs are duplicates.
1513        assert_eq!(emo.nr_cpus_combinations.len(), 69);
1514        assert!(emo.nr_cpus_combinations.contains(&vec![1; 24]));
1515
1516        // Every run takes CPUs from consecutive equivalence performance
1517        // domains.
1518        for nr_cpus in emo.nr_cpus_combinations.iter() {
1519            let first = nr_cpus.iter().position(|&nr| nr > 0).unwrap();
1520            let last = nr_cpus.iter().rposition(|&nr| nr > 0).unwrap();
1521            assert!(nr_cpus[first..=last].iter().all(|&nr| nr > 0));
1522        }
1523    }
1524
1525    /// A single equivalence performance domain, so a combination is just how
1526    /// many of its CPUs to take.
1527    #[test]
1528    fn test_uniform_combinations() {
1529        let em = energy_model(&[8]);
1530        let cpus_pf = vec![];
1531        let emo = EnergyModelOptimizer::new(&em, &cpus_pf);
1532
1533        assert_eq!(emo.nr_cpus_combinations.len(), 8);
1534        assert_eq!(emo.gen_pds_combinations(100.0).len(), 8);
1535    }
1536}