ktstr/
gauntlet.rs

1//! Gauntlet topology presets.
2//!
3//! See the [Gauntlet](https://ktstr.dev/guide/running-tests/gauntlet.html)
4//! chapter of the guide.
5
6use crate::vmm::Topology;
7
8/// A gauntlet topology preset.
9///
10/// Each preset defines a specific CPU topology for matrix testing.
11/// See [`gauntlet_presets()`] for the full set.
12pub struct TopoPreset {
13    pub name: &'static str,
14    /// Human-readable description; read by preset-audit tests only.
15    #[allow(dead_code)]
16    pub description: &'static str,
17    pub topology: Topology,
18    /// Memory budget for this preset's VM; read by preset-audit tests only.
19    #[allow(dead_code)]
20    pub memory_mib: usize,
21}
22
23/// Topology presets used by gauntlet mode.
24///
25/// Covers topologies from `tiny-1llc` (4 CPUs) up to the
26/// `max-cpu` / `max-cpu-nosmt` presets (252 CPUs, near the KVM
27/// vCPU limit), spanning SMT, non-SMT (`-nosmt`), and multi-NUMA
28/// (`numa2-*`, `numa4-*`) families. Returned in `defs` / `numa_defs`
29/// declaration order, not by size.
30///
31/// The full set is returned unconditionally; the only filter applied
32/// here is the aarch64 retain below, which drops SMT presets
33/// (`threads_per_core > 1`) because ARM64 CPUs have no SMT (the
34/// non-SMT medium/large/max presets keep ARM64's topology scale
35/// coverage). The multi-NUMA presets are *not* filtered here: the
36/// default [`crate::test_support::TopologyConstraints`]
37/// (`max_numa_nodes: Some(1)`) excludes them at test-selection time
38/// (via `accepts` / `accepts_no_perf_mode`) unless a test raises
39/// the bound.
40pub fn gauntlet_presets() -> Vec<TopoPreset> {
41    let defs: &[(&str, &str, u32, u32, u32, usize)] = &[
42        ("tiny-1llc", "4 CPUs, 1 LLC", 1, 4, 1, 2048),
43        ("tiny-2llc", "4 CPUs, 2 LLCs", 2, 2, 1, 2048),
44        ("odd-3llc", "9 CPUs, 3 LLCs (odd)", 3, 3, 1, 2048),
45        ("odd-5llc", "15 CPUs, 5 LLCs (prime)", 5, 3, 1, 2048),
46        ("odd-7llc", "14 CPUs, 7 LLCs (prime)", 7, 2, 1, 2048),
47        ("smt-2llc", "8 CPUs, 2 LLCs with SMT", 2, 2, 2, 2048),
48        ("smt-3llc", "12 CPUs, 3 LLCs with SMT", 3, 2, 2, 2048),
49        ("medium-4llc", "32 CPUs, 4 LLCs", 4, 4, 2, 2048),
50        ("medium-8llc", "64 CPUs, 8 LLCs", 8, 4, 2, 2048),
51        ("large-4llc", "128 CPUs, 4 LLCs", 4, 16, 2, 2048),
52        ("large-8llc", "128 CPUs, 8 LLCs", 8, 8, 2, 2048),
53        (
54            "near-max-llc",
55            "240 CPUs, 15 LLCs (near max)",
56            15,
57            8,
58            2,
59            2048,
60        ),
61        (
62            "max-cpu",
63            "252 CPUs, 14 LLCs (near KVM vCPU limit)",
64            14,
65            9,
66            2,
67            4096,
68        ),
69        // Non-SMT medium/large/max presets for ARM64 coverage.
70        // These also run on x86_64 to test non-SMT topologies at scale.
71        (
72            "medium-4llc-nosmt",
73            "32 CPUs, 4 LLCs (no SMT)",
74            4,
75            8,
76            1,
77            2048,
78        ),
79        (
80            "medium-8llc-nosmt",
81            "64 CPUs, 8 LLCs (no SMT)",
82            8,
83            8,
84            1,
85            2048,
86        ),
87        (
88            "large-4llc-nosmt",
89            "128 CPUs, 4 LLCs (no SMT)",
90            4,
91            32,
92            1,
93            2048,
94        ),
95        (
96            "large-8llc-nosmt",
97            "128 CPUs, 8 LLCs (no SMT)",
98            8,
99            16,
100            1,
101            2048,
102        ),
103        (
104            "near-max-llc-nosmt",
105            "240 CPUs, 15 LLCs (no SMT)",
106            15,
107            16,
108            1,
109            2048,
110        ),
111        (
112            "max-cpu-nosmt",
113            "252 CPUs, 14 LLCs (no SMT, near KVM vCPU limit)",
114            14,
115            18,
116            1,
117            4096,
118        ),
119    ];
120    let numa_defs: &[(&str, &str, u32, u32, u32, u32, usize)] = &[
121        (
122            "numa2-4llc",
123            "16 CPUs, 2 NUMA nodes, 4 LLCs",
124            2,
125            4,
126            4,
127            1,
128            2048,
129        ),
130        (
131            "numa2-8llc",
132            "128 CPUs, 2 NUMA nodes, 8 LLCs",
133            2,
134            8,
135            8,
136            2,
137            2048,
138        ),
139        (
140            "numa2-8llc-nosmt",
141            "128 CPUs, 2 NUMA nodes, 8 LLCs (no SMT)",
142            2,
143            8,
144            16,
145            1,
146            2048,
147        ),
148        (
149            "numa4-8llc",
150            "32 CPUs, 4 NUMA nodes, 8 LLCs",
151            4,
152            8,
153            4,
154            1,
155            2048,
156        ),
157        (
158            "numa4-12llc",
159            "192 CPUs, 4 NUMA nodes, 12 LLCs",
160            4,
161            12,
162            8,
163            2,
164            4096,
165        ),
166    ];
167
168    let mut presets: Vec<TopoPreset> = defs
169        .iter()
170        .map(|&(n, d, s, c, t, m)| TopoPreset {
171            name: n,
172            description: d,
173            topology: Topology {
174                llcs: s,
175                cores_per_llc: c,
176                threads_per_core: t,
177                numa_nodes: 1,
178                nodes: None,
179                distances: None,
180            },
181            memory_mib: m,
182        })
183        .chain(numa_defs.iter().map(|&(n, d, nn, s, c, t, m)| TopoPreset {
184            name: n,
185            description: d,
186            topology: Topology {
187                llcs: s,
188                cores_per_llc: c,
189                threads_per_core: t,
190                numa_nodes: nn,
191                nodes: None,
192                distances: None,
193            },
194            memory_mib: m,
195        }))
196        .collect();
197
198    // ARM64 has no SMT -- exclude presets with threads_per_core > 1.
199    if cfg!(target_arch = "aarch64") {
200        presets.retain(|p| p.topology.threads_per_core <= 1);
201    }
202
203    presets
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn gauntlet_presets_unique_names() {
212        let p = gauntlet_presets();
213        let names: Vec<&str> = p.iter().map(|p| p.name).collect();
214        let unique: std::collections::HashSet<&&str> = names.iter().collect();
215        assert_eq!(names.len(), unique.len());
216    }
217
218    #[test]
219    fn gauntlet_presets_total_cpus_match() {
220        for p in &gauntlet_presets() {
221            let cpus = p.topology.total_cpus();
222            assert!(
223                p.description.contains(&cpus.to_string()),
224                "{}: description '{}' doesn't mention {} CPUs",
225                p.name,
226                p.description,
227                cpus
228            );
229        }
230    }
231
232    #[test]
233    fn gauntlet_presets_memory_sane() {
234        for p in &gauntlet_presets() {
235            assert!(
236                p.memory_mib >= 512,
237                "{} has too little memory: {}MiB",
238                p.name,
239                p.memory_mib
240            );
241            let cpus = p.topology.total_cpus() as usize;
242            assert!(
243                p.memory_mib >= cpus * 8,
244                "{} has {}MiB for {} CPUs",
245                p.name,
246                p.memory_mib,
247                cpus
248            );
249        }
250    }
251
252    #[test]
253    fn gauntlet_presets_topology_pinned() {
254        // (name, expected LLCs, expected total CPUs)
255        let expected: &[(&str, u32, u32)] = &[
256            ("tiny-1llc", 1, 4),
257            ("tiny-2llc", 2, 4),
258            ("odd-3llc", 3, 9),
259            ("odd-5llc", 5, 15),
260            ("odd-7llc", 7, 14),
261            #[cfg(not(target_arch = "aarch64"))]
262            ("smt-2llc", 2, 8),
263            #[cfg(not(target_arch = "aarch64"))]
264            ("smt-3llc", 3, 12),
265            #[cfg(not(target_arch = "aarch64"))]
266            ("medium-4llc", 4, 32),
267            #[cfg(not(target_arch = "aarch64"))]
268            ("medium-8llc", 8, 64),
269            #[cfg(not(target_arch = "aarch64"))]
270            ("large-4llc", 4, 128),
271            #[cfg(not(target_arch = "aarch64"))]
272            ("large-8llc", 8, 128),
273            #[cfg(not(target_arch = "aarch64"))]
274            ("near-max-llc", 15, 240),
275            #[cfg(not(target_arch = "aarch64"))]
276            ("max-cpu", 14, 252),
277            ("medium-4llc-nosmt", 4, 32),
278            ("medium-8llc-nosmt", 8, 64),
279            ("large-4llc-nosmt", 4, 128),
280            ("large-8llc-nosmt", 8, 128),
281            ("near-max-llc-nosmt", 15, 240),
282            ("max-cpu-nosmt", 14, 252),
283            ("numa2-4llc", 4, 16),
284            #[cfg(not(target_arch = "aarch64"))]
285            ("numa2-8llc", 8, 128),
286            ("numa2-8llc-nosmt", 8, 128),
287            ("numa4-8llc", 8, 32),
288            #[cfg(not(target_arch = "aarch64"))]
289            ("numa4-12llc", 12, 192),
290        ];
291        let presets = gauntlet_presets();
292        assert_eq!(
293            expected.len(),
294            presets.len(),
295            "pinned list and preset list have different lengths"
296        );
297        for &(name, llcs, cpus) in expected {
298            let p = presets.iter().find(|p| p.name == name).unwrap();
299            assert_eq!(
300                p.topology.num_llcs(),
301                llcs,
302                "{}: expected {} LLCs, got {}",
303                name,
304                llcs,
305                p.topology.num_llcs()
306            );
307            assert_eq!(
308                p.topology.total_cpus(),
309                cpus,
310                "{}: expected {} CPUs, got {}",
311                name,
312                cpus,
313                p.topology.total_cpus()
314            );
315        }
316    }
317
318    #[test]
319    fn gauntlet_presets_topology_valid() {
320        for p in &gauntlet_presets() {
321            p.topology
322                .validate()
323                .unwrap_or_else(|e| panic!("{}: {e}", p.name));
324        }
325    }
326
327    #[test]
328    fn gauntlet_presets_max_cpu_near_limit() {
329        let presets = gauntlet_presets();
330        let max_presets: Vec<_> = presets
331            .iter()
332            .filter(|p| p.name.starts_with("max-cpu"))
333            .collect();
334        assert!(
335            !max_presets.is_empty(),
336            "at least one max-cpu preset must exist"
337        );
338        for p in &max_presets {
339            let cpus = p.topology.total_cpus();
340            assert!(
341                cpus <= 255,
342                "{} has {} CPUs, exceeds KVM vCPU limit",
343                p.name,
344                cpus
345            );
346            assert!(
347                cpus >= 200,
348                "{} should be near the limit: {} CPUs",
349                p.name,
350                cpus
351            );
352        }
353    }
354
355    #[test]
356    fn topology_single_cpu() {
357        let t = Topology {
358            llcs: 1,
359            cores_per_llc: 1,
360            threads_per_core: 1,
361            numa_nodes: 1,
362            nodes: None,
363            distances: None,
364        };
365        assert_eq!(t.total_cpus(), 1);
366        assert_eq!(t.num_llcs(), 1);
367    }
368
369    #[test]
370    #[cfg(not(target_arch = "aarch64"))]
371    fn gauntlet_presets_smt_presets_have_threads() {
372        let presets = gauntlet_presets();
373        for p in &presets {
374            if p.name.starts_with("smt-") {
375                assert_eq!(
376                    p.topology.threads_per_core, 2,
377                    "{} should have 2 threads per core",
378                    p.name
379                );
380            }
381        }
382    }
383
384    #[test]
385    fn gauntlet_presets_odd_presets_are_odd() {
386        let presets = gauntlet_presets();
387        for p in &presets {
388            if p.name.starts_with("odd-") {
389                assert!(
390                    p.topology.llcs % 2 != 0,
391                    "{}: odd-* presets must have odd LLC count, got {} LLCs",
392                    p.name,
393                    p.topology.llcs
394                );
395            }
396        }
397    }
398
399    #[test]
400    fn gauntlet_presets_numa_presets_have_correct_nodes() {
401        for p in &gauntlet_presets() {
402            if p.name.starts_with("numa2") {
403                assert_eq!(
404                    p.topology.numa_nodes, 2,
405                    "{}: expected 2 NUMA nodes",
406                    p.name
407                );
408            } else if p.name.starts_with("numa4") {
409                assert_eq!(
410                    p.topology.numa_nodes, 4,
411                    "{}: expected 4 NUMA nodes",
412                    p.name
413                );
414            }
415        }
416    }
417
418    #[test]
419    fn gauntlet_presets_description_non_empty() {
420        for p in &gauntlet_presets() {
421            assert!(
422                !p.description.is_empty(),
423                "{} has empty description",
424                p.name
425            );
426        }
427    }
428}