1mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod cgroup;
14mod gpu;
15mod stats;
16use cgroup::CgroupReader;
17
18use std::collections::{HashMap, HashSet};
19use std::ffi::{c_int, c_ulong};
20use std::mem::MaybeUninit;
21use std::sync::atomic::AtomicBool;
22use std::sync::atomic::Ordering;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use anyhow::bail;
27use anyhow::Context;
28use anyhow::Result;
29use clap::Parser;
30use crossbeam::channel::RecvTimeoutError;
31use libbpf_rs::MapCore;
32use libbpf_rs::MapFlags;
33use libbpf_rs::OpenObject;
34use libbpf_rs::ProgramInput;
35use log::{debug, info, warn};
36use nvml_wrapper::bitmasks::InitFlags;
37use nvml_wrapper::Nvml;
38use scx_stats::prelude::*;
39use scx_utils::build_id;
40use scx_utils::compat;
41use scx_utils::get_primary_cpus;
42use scx_utils::libbpf_clap_opts::LibbpfOpts;
43use scx_utils::perf::parse_perf_event;
44use scx_utils::perf::setup_perf_events;
45use scx_utils::perf::PerfEventSpec;
46use scx_utils::scx_ops_attach;
47use scx_utils::scx_ops_load;
48use scx_utils::scx_ops_open;
49use scx_utils::try_set_rlimit_infinity;
50use scx_utils::uei_exited;
51use scx_utils::uei_report;
52use scx_utils::GpuIndex;
53use scx_utils::Powermode;
54use scx_utils::Topology;
55use scx_utils::UserExitInfo;
56use scx_utils::NR_CPU_IDS;
57use stats::Metrics;
58
59const SCHEDULER_NAME: &str = "scx_cosmos";
60
61#[derive(Debug, clap::Parser)]
62#[command(
63 name = "scx_cosmos",
64 version,
65 disable_version_flag = true,
66 about = "Lightweight scheduler optimized for preserving task-to-CPU locality."
67)]
68struct Opts {
69 #[clap(long, default_value = "0")]
71 exit_dump_len: u32,
72
73 #[clap(short = 's', long, default_value = "1000")]
75 slice_us: u64,
76
77 #[clap(short = 'l', long, default_value = "20000")]
79 slice_lag_us: u64,
80
81 #[clap(short = 'c', long, default_value = "0")]
105 cpu_busy_thresh: u64,
106
107 #[clap(short = 'p', long, default_value = "0")]
114 polling_ms: u64,
115
116 #[clap(short = 'm', long)]
128 primary_domain: Option<String>,
129
130 #[clap(long, default_value = "1000")]
141 primary_overload_us: u64,
142
143 #[clap(short = 'e', long, default_value = "0x0", value_parser = parse_perf_event)]
146 perf_config: PerfEventSpec,
147
148 #[clap(short = 'E', default_value = "0", long)]
150 perf_threshold: u64,
151
152 #[clap(short = 'y', long, default_value = "0x0", value_parser = parse_perf_event)]
155 perf_sticky: PerfEventSpec,
156
157 #[clap(short = 'Y', default_value = "0", long)]
159 perf_sticky_threshold: u64,
160
161 #[clap(short = 'g', long, action = clap::ArgAction::SetTrue)]
163 gpu: bool,
164
165 #[clap(long, default_value = "0", value_parser = clap::value_parser!(u32).range(0..=100))]
170 gpu_util_threshold: u32,
171
172 #[clap(short = 'n', long, action = clap::ArgAction::SetTrue)]
174 disable_numa: bool,
175
176 #[clap(short = 'f', long, action = clap::ArgAction::SetTrue)]
178 disable_cpufreq: bool,
179
180 #[arg(short = 'i', long, action = clap::ArgAction::SetTrue)]
185 flat_idle_scan: bool,
186
187 #[clap(short = 'P', long, action = clap::ArgAction::SetTrue)]
192 preferred_idle_scan: bool,
193
194 #[clap(long, action = clap::ArgAction::SetTrue)]
199 disable_smt: bool,
200
201 #[clap(short = 'S', long, action = clap::ArgAction::SetTrue)]
203 avoid_smt: bool,
204
205 #[clap(short = 'N', long, action = clap::ArgAction::SetTrue)]
211 no_early_clear: bool,
212
213 #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
220 no_wake_sync: bool,
221
222 #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
224 no_deferred_wakeup: bool,
225
226 #[clap(long, action = clap::ArgAction::SetTrue)]
232 time_preemption: bool,
233
234 #[clap(short = 'a', long, action = clap::ArgAction::SetTrue)]
241 mm_affinity: bool,
242
243 #[clap(long)]
245 stats: Option<f64>,
246
247 #[clap(long)]
250 monitor: Option<f64>,
251
252 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
254 verbose: bool,
255
256 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
258 version: bool,
259
260 #[clap(long)]
262 help_stats: bool,
263
264 #[clap(flatten, next_help_heading = "Libbpf Options")]
265 pub libbpf: LibbpfOpts,
266}
267
268pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
269 let mut cpus = Vec::new();
270 let mut seen = HashSet::new();
271
272 if let Some(mode) = match optarg {
274 "powersave" => Some(Powermode::Powersave),
275 "performance" => Some(Powermode::Performance),
276 "turbo" => Some(Powermode::Turbo),
277 "all" => Some(Powermode::Any),
278 _ => None,
279 } {
280 return get_primary_cpus(mode).map_err(|e| e.to_string());
281 }
282
283 if optarg
285 .chars()
286 .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
287 {
288 return Err("Invalid character in CPU list".to_string());
289 }
290
291 let cleaned = optarg.replace(' ', "\t");
293
294 for token in cleaned.split(',') {
295 let token = token.trim_matches(|c: char| c.is_whitespace());
296
297 if token.is_empty() {
298 continue;
299 }
300
301 if let Some((start_str, end_str)) = token.split_once('-') {
302 let start = start_str
303 .trim()
304 .parse::<usize>()
305 .map_err(|_| "Invalid range start")?;
306 let end = end_str
307 .trim()
308 .parse::<usize>()
309 .map_err(|_| "Invalid range end")?;
310
311 if start > end {
312 return Err(format!("Invalid CPU range: {}-{}", start, end));
313 }
314
315 for i in start..=end {
316 if cpus.len() >= *NR_CPU_IDS {
317 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
318 }
319 if seen.insert(i) {
320 cpus.push(i);
321 }
322 }
323 } else {
324 let cpu = token
325 .parse::<usize>()
326 .map_err(|_| format!("Invalid CPU: {}", token))?;
327 if cpus.len() >= *NR_CPU_IDS {
328 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
329 }
330 if seen.insert(cpu) {
331 cpus.push(cpu);
332 }
333 }
334 }
335
336 Ok(cpus)
337}
338
339const DYNAMIC_THRESHOLD_INIT_VALUE: u64 = 1000;
341
342const DYNAMIC_THRESHOLD_MIN_VALUE: u64 = 10;
344
345const DYNAMIC_THRESHOLD_RATE_HIGH: f64 = 4000.0;
347
348const DYNAMIC_THRESHOLD_RATE_LOW: f64 = 2000.0;
350
351const DYNAMIC_THRESHOLD_HYSTERESIS: f64 = 0.1;
354
355const DYNAMIC_THRESHOLD_EMA_ALPHA: f64 = 0.3;
358
359const DYNAMIC_THRESHOLD_SCALE_MIN: f64 = 0.0001;
361
362const DYNAMIC_THRESHOLD_SCALE_MAX: f64 = 1000.0;
364
365const DYNAMIC_THRESHOLD_SLOPE_HIGH: f64 = 0.35;
368
369const DYNAMIC_THRESHOLD_SLOPE_LOW: f64 = 0.58;
371
372const GPU_SYNC_INTERVAL: Duration = Duration::from_secs(1);
375
376#[derive(Debug, Clone)]
382struct DynamicThresholdState {
383 threshold: u64,
385 smoothed_rate: f64,
387 prev_counter: u64,
389 initialized: bool,
391 adjustment_direction: Option<bool>,
395}
396
397impl DynamicThresholdState {
398 fn new(initial_threshold: u64) -> Self {
400 Self {
401 threshold: initial_threshold,
402 smoothed_rate: 0.0,
403 prev_counter: 0,
404 initialized: false,
405 adjustment_direction: None,
406 }
407 }
408
409 fn update(
412 &mut self,
413 counter: u64,
414 elapsed_secs: f64,
415 verbose: bool,
416 name: &str,
417 ) -> Option<u64> {
418 if elapsed_secs <= 0.0 {
419 return None;
420 }
421
422 let delta = counter.saturating_sub(self.prev_counter);
424 self.prev_counter = counter;
425 let raw_rate = delta as f64 / elapsed_secs;
426
427 if self.initialized {
429 self.smoothed_rate = DYNAMIC_THRESHOLD_EMA_ALPHA * raw_rate
430 + (1.0 - DYNAMIC_THRESHOLD_EMA_ALPHA) * self.smoothed_rate;
431 } else {
432 self.smoothed_rate = raw_rate;
434 self.initialized = true;
435 }
436
437 let rate = self.smoothed_rate;
439 let old_threshold = self.threshold;
440
441 let (effective_high, effective_low) = match self.adjustment_direction {
443 Some(true) => {
444 (
446 DYNAMIC_THRESHOLD_RATE_HIGH,
447 DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
448 )
449 }
450 Some(false) => {
451 (
453 DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
454 DYNAMIC_THRESHOLD_RATE_LOW,
455 )
456 }
457 None => {
458 (
460 DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
461 DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
462 )
463 }
464 };
465
466 let new_direction = if rate > effective_high {
468 Some(true) } else if rate < effective_low && rate >= 0.0 {
470 Some(false) } else {
472 if self.adjustment_direction.is_some() {
474 if rate >= DYNAMIC_THRESHOLD_RATE_LOW && rate <= DYNAMIC_THRESHOLD_RATE_HIGH {
476 None } else {
478 self.adjustment_direction }
480 } else {
481 None }
483 };
484
485 if let Some(raising) = new_direction {
487 let scale = Self::compute_scale(rate, raising);
488 let factor = if raising { 1.0 + scale } else { 1.0 - scale };
489 let new_threshold = ((self.threshold as f64) * factor).round() as u64;
490 self.threshold = new_threshold.clamp(DYNAMIC_THRESHOLD_MIN_VALUE, u64::MAX);
491 }
492
493 self.adjustment_direction = new_direction;
494
495 if self.threshold != old_threshold {
497 if verbose {
498 info!(
499 "{}: {} -> {} (smoothed rate {:.1}/s, raw {:.1}/s, dir {:?})",
500 name,
501 old_threshold,
502 self.threshold,
503 self.smoothed_rate,
504 raw_rate,
505 self.adjustment_direction
506 );
507 }
508 Some(self.threshold)
509 } else {
510 None
511 }
512 }
513
514 fn compute_scale(rate: f64, too_high: bool) -> f64 {
517 if too_high {
518 let excess = ((rate / DYNAMIC_THRESHOLD_RATE_HIGH) - 1.0).max(0.0);
519 let scale =
520 DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_HIGH * excess.min(4.0);
521 scale.min(DYNAMIC_THRESHOLD_SCALE_MAX)
522 } else {
523 if rate <= 0.0 {
524 return DYNAMIC_THRESHOLD_SCALE_MAX;
525 }
526 let deficit = (DYNAMIC_THRESHOLD_RATE_LOW - rate) / DYNAMIC_THRESHOLD_RATE_LOW;
527 let t = deficit.clamp(0.0, 1.0);
528 DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_LOW * t
529 }
530 }
531}
532
533struct Scheduler<'a> {
534 skel: BpfSkel<'a>,
535 opts: &'a Opts,
536 struct_ops: Option<libbpf_rs::Link>,
537 stats_server: StatsServer<(), Metrics>,
538 gpu_index_to_node: Option<HashMap<u32, u32>>,
540 previous_gpu_pids: Option<HashMap<u32, u32>>,
542 nvml: Option<Nvml>,
544 gpu_cgroup_reader: Option<CgroupReader>,
546 perf_threshold_state: Option<DynamicThresholdState>,
548 perf_sticky_threshold_state: Option<DynamicThresholdState>,
550}
551
552impl<'a> Scheduler<'a> {
553 fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
554 try_set_rlimit_infinity();
555
556 let topo = Topology::new().unwrap();
558
559 let smt_enabled = !opts.disable_smt && topo.smt_enabled;
561
562 let nr_nodes = topo
564 .nodes
565 .values()
566 .filter(|node| !node.all_cpus.is_empty())
567 .count();
568 info!("NUMA nodes: {}", nr_nodes);
569
570 let numa_enabled = !opts.disable_numa && nr_nodes > 1;
572 if !numa_enabled {
573 info!("Disabling NUMA optimizations");
574 }
575
576 info!(
577 "{} {} {}",
578 SCHEDULER_NAME,
579 build_id::full_version(env!("CARGO_PKG_VERSION")),
580 if smt_enabled { "SMT on" } else { "SMT off" }
581 );
582
583 info!(
585 "scheduler options: {}",
586 std::env::args().collect::<Vec<_>>().join(" ")
587 );
588
589 let mut skel_builder = BpfSkelBuilder::default();
591 skel_builder.obj_builder.debug(opts.verbose);
592 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
593 let mut skel = scx_ops_open!(skel_builder, open_object, cosmos_ops, open_opts)?;
594
595 skel.struct_ops.cosmos_ops_mut().exit_dump_len = opts.exit_dump_len;
596
597 let rodata = skel.maps.rodata_data.as_mut().unwrap();
599 rodata.slice_ns = opts.slice_us * 1000;
600 rodata.slice_lag = opts.slice_lag_us * 1000;
601 rodata.cpufreq_enabled = !opts.disable_cpufreq;
602 rodata.flat_idle_scan = opts.flat_idle_scan;
603 rodata.smt_enabled = smt_enabled;
604 rodata.numa_enabled = numa_enabled;
605 rodata.nr_node_ids = topo.nodes.len() as u32;
606 rodata.no_wake_sync = opts.no_wake_sync;
607 rodata.no_early_clear = opts.no_early_clear;
608 rodata.time_preemption = opts.time_preemption;
609 rodata.mm_affinity = opts.mm_affinity;
610
611 rodata.perf_config = opts.perf_config.event_id;
613 rodata.perf_sticky = opts.perf_sticky.event_id;
614
615 rodata.busy_threshold = opts.cpu_busy_thresh * 1024 / 100;
617
618 rodata.overload_thresh_ns = opts.primary_overload_us * 1000;
620
621 let mut cpus: Vec<_> = topo.all_cpus.values().collect();
623 cpus.sort_by_key(|cpu| std::cmp::Reverse(cpu.cpu_capacity));
624 let max_cap = cpus.first().map(|c| c.cpu_capacity).unwrap_or(1).max(1);
626 for (i, cpu) in cpus.iter().enumerate() {
627 let normalized = (cpu.cpu_capacity * 1024 / max_cap).clamp(1, 1024);
628 rodata.cpu_capacity[cpu.id] = normalized as c_ulong;
629 rodata.preferred_cpus[i] = cpu.id as u64;
630 }
631 rodata.all_cpus_same_capacity = cpus.iter().all(|cpu| cpu.cpu_capacity == max_cap);
632 if opts.preferred_idle_scan {
633 info!(
634 "Preferred CPUs: {:?}",
635 &rodata.preferred_cpus[0..cpus.len()]
636 );
637 }
638 rodata.preferred_idle_scan = opts.preferred_idle_scan;
639
640 let primary_cpus = if let Some(ref domain) = opts.primary_domain {
642 match parse_cpu_list(domain) {
643 Ok(cpus) => cpus,
644 Err(e) => bail!("Error parsing primary domain: {}", e),
645 }
646 } else {
647 (0..*NR_CPU_IDS).collect()
648 };
649 if primary_cpus.len() < *NR_CPU_IDS {
650 info!("Primary CPUs: {:?}", primary_cpus);
651 rodata.primary_all = false;
652 } else {
653 rodata.primary_all = true;
654 }
655
656 let (gpu_index_to_node, previous_gpu_pids, nvml) = if opts.gpu && numa_enabled {
659 match Nvml::init_with_flags(InitFlags::NO_GPUS) {
660 Ok(nvml) => {
661 info!("NVIDIA GPU-aware scheduling enabled (NVML PID sync)");
662 rodata.gpu_enabled = true;
663 let mut idx_to_node = HashMap::new();
664 for (id, gpu) in topo.gpus() {
665 let GpuIndex::Nvidia { nvml_id } = id;
666 idx_to_node.insert(nvml_id, gpu.node_id as u32);
667 }
668 (Some(idx_to_node), Some(HashMap::new()), Some(nvml))
669 }
670 Err(e) => {
671 warn!("NVML init failed, disabling GPU-aware scheduling: {}", e);
672 rodata.gpu_enabled = false;
673 (None, None, None)
674 }
675 }
676 } else {
677 rodata.gpu_enabled = false;
678 (None, None, None)
679 };
680
681 let gpu_cgroup_reader = if nvml.is_some() && opts.gpu_util_threshold == 0 {
682 match CgroupReader::discover() {
683 Ok(reader) => {
684 info!("NVIDIA GPU workload discovery enabled (cgroup v2)");
685 Some(reader)
686 }
687 Err(error) => {
688 warn!(
689 "GPU workload discovery unavailable, using NVML process scope: {error:#}"
690 );
691 None
692 }
693 }
694 } else if nvml.is_some() {
695 info!(
696 "NVIDIA GPU workload discovery requires --gpu-util-threshold=0; using NVML process scope"
697 );
698 None
699 } else {
700 None
701 };
702
703 skel.struct_ops.cosmos_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
705 | *compat::SCX_OPS_ENQ_LAST
706 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
707 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
708 | if numa_enabled {
709 *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
710 } else {
711 0
712 };
713
714 info!(
715 "scheduler flags: {:#x}",
716 skel.struct_ops.cosmos_ops_mut().flags
717 );
718
719 let mut skel = scx_ops_load!(skel, cosmos_ops, uei)?;
721
722 let bss = skel.maps.bss_data.as_mut().unwrap();
725 if opts.perf_config.event_id > 0 {
726 bss.perf_threshold = if opts.perf_threshold == 0 {
727 DYNAMIC_THRESHOLD_INIT_VALUE
728 } else {
729 opts.perf_threshold
730 };
731 }
732 if opts.perf_sticky.event_id > 0 {
733 bss.perf_sticky_threshold = if opts.perf_sticky_threshold == 0 {
734 DYNAMIC_THRESHOLD_INIT_VALUE
735 } else {
736 opts.perf_sticky_threshold
737 };
738 }
739
740 for node in topo.nodes.values() {
742 for cpu in node.all_cpus.values() {
743 if opts.verbose {
744 info!("CPU{} -> node{}", cpu.id, node.id);
745 }
746 skel.maps.cpu_node_map.update(
747 &(cpu.id as u32).to_ne_bytes(),
748 &(node.id as u32).to_ne_bytes(),
749 MapFlags::ANY,
750 )?;
751 }
752 }
753
754 let nr_cpus = *NR_CPU_IDS;
758 info!("Setting up performance counters for {} CPUs...", nr_cpus);
759 let mut perf_available = true;
760 let sticky_counter_idx = if opts.perf_config.event_id > 0 { 1 } else { 0 };
761 for cpu in 0..nr_cpus {
762 if opts.perf_config.event_id > 0 {
763 if let Err(e) =
764 setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
765 {
766 if cpu == 0 {
767 let err_str = e.to_string();
768 if err_str.contains("errno 2") || err_str.contains("os error 2") {
769 warn!("Performance counters not available on this CPU architecture");
770 warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_config.display_name);
771 } else {
772 warn!("Failed to setup perf events: {}", e);
773 }
774 perf_available = false;
775 break;
776 }
777 }
778 }
779 if opts.perf_sticky.event_id > 0 {
780 if let Err(e) = setup_perf_events(
781 &skel.maps.scx_pmu_map,
782 cpu as i32,
783 &opts.perf_sticky,
784 sticky_counter_idx,
785 ) {
786 if cpu == 0 {
787 let err_str = e.to_string();
788 if err_str.contains("errno 2") || err_str.contains("os error 2") {
789 warn!("Performance counters not available on this CPU architecture");
790 warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_sticky.display_name);
791 } else {
792 warn!("Failed to setup perf events: {}", e);
793 }
794 perf_available = false;
795 break;
796 }
797 }
798 }
799 }
800 if perf_available {
801 info!("Performance counters configured successfully for all CPUs");
802 }
803
804 if opts.gpu && numa_enabled {
806 for (id, gpu) in topo.gpus() {
807 let GpuIndex::Nvidia { nvml_id } = id;
808 if opts.verbose {
809 info!("GPU{} -> node{}", nvml_id, gpu.node_id);
810 }
811 skel.maps.gpu_node_map.update(
812 &(nvml_id as u32).to_ne_bytes(),
813 &(gpu.node_id as u32).to_ne_bytes(),
814 MapFlags::ANY,
815 )?;
816 }
817 }
818
819 if primary_cpus.len() < *NR_CPU_IDS {
821 for cpu in primary_cpus {
822 if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
823 bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
824 }
825 }
826 }
827
828 if smt_enabled {
830 Self::init_smt_domains(&mut skel, &topo)?;
831 }
832
833 let struct_ops = Some(scx_ops_attach!(skel, cosmos_ops)?);
835 let stats_server = StatsServer::new(stats::server_data()).launch()?;
836
837 let perf_threshold_state = if opts.perf_config.event_id > 0 && opts.perf_threshold == 0 {
839 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
840 } else {
841 None
842 };
843 let perf_sticky_threshold_state =
844 if opts.perf_sticky.event_id > 0 && opts.perf_sticky_threshold == 0 {
845 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
846 } else {
847 None
848 };
849
850 Ok(Self {
851 skel,
852 opts,
853 struct_ops,
854 stats_server,
855 gpu_index_to_node,
856 previous_gpu_pids,
857 nvml,
858 gpu_cgroup_reader,
859 perf_threshold_state,
860 perf_sticky_threshold_state,
861 })
862 }
863
864 fn sync_gpu_pids(&mut self) -> Result<()> {
869 let gpu_index_to_node = match &self.gpu_index_to_node {
870 Some(m) => m,
871 None => return Ok(()),
872 };
873 let nvml = match &self.nvml {
874 Some(n) => n,
875 None => return Ok(()),
876 };
877 let threshold = self.opts.gpu_util_threshold;
878 let mut pid_to_nodes: HashMap<u32, HashSet<u32>> = HashMap::new();
880 let mut snapshot_complete = true;
881
882 let count = nvml.device_count().context("NVML device count")?;
885 for i in 0..count {
886 let node = match gpu_index_to_node.get(&i) {
887 Some(&n) => n,
888 None => {
889 snapshot_complete = false;
890 continue;
891 }
892 };
893 let device = match nvml.device_by_index(i) {
894 Ok(device) => device,
895 Err(error) => {
896 debug!("NVML device {i} lookup failed: {error:#}");
897 return Err(error).context(format!("NVML device {i} lookup failed"));
898 }
899 };
900
901 if threshold > 0 {
902 match device.process_utilization_stats(None::<u64>) {
904 Ok(samples) => {
905 for sample in samples {
906 let util = sample.sm_util.max(sample.mem_util);
907 if util >= threshold {
908 pid_to_nodes.entry(sample.pid).or_default().insert(node);
909 }
910 }
911 }
912 Err(_) => {
913 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
915 .with_context(|| {
916 format!("NVML device {i} process snapshot is incomplete")
917 })?;
918 }
919 }
920 } else {
921 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
922 .with_context(|| format!("NVML device {i} process snapshot is incomplete"))?;
923 }
924 }
925
926 let mut direct = gpu::direct_gpu_processes(&pid_to_nodes);
927 direct.remove(&std::process::id());
928 let mut current = if snapshot_complete {
929 self.gpu_cgroup_reader
930 .as_ref()
931 .map(|reader| gpu::expand_gpu_processes(&pid_to_nodes, reader))
932 .unwrap_or_else(|| direct.clone())
933 } else {
934 direct.clone()
935 };
936 current.remove(&std::process::id());
937 let max_entries = self.skel.maps.gpu_pid_map.max_entries() as usize;
938
939 if current.len() > max_entries {
942 warn!(
943 "GPU workload has {} processes, exceeding gpu_pid_map capacity {}; using {} direct NVML processes",
944 current.len(),
945 max_entries,
946 direct.len()
947 );
948 }
949 if direct.len() > max_entries {
950 warn!(
951 "{} direct NVML processes exceed gpu_pid_map capacity {}; clearing GPU process hints",
952 direct.len(),
953 max_entries
954 );
955 }
956 current = gpu::fit_gpu_processes(current, &direct, max_entries);
957
958 self.reconcile_gpu_pid_hints(¤t)
959 }
960
961 fn reconcile_gpu_pid_hints(&mut self, desired: &HashMap<u32, u32>) -> Result<()> {
963 let previous = self.previous_gpu_pids.as_ref().unwrap().clone();
964 let map = &self.skel.maps.gpu_pid_map;
965
966 let mut applied = previous.clone();
969 let mut first_error = None;
970
971 for pid in previous.keys() {
974 if desired.contains_key(pid) {
975 continue;
976 }
977 match map.delete(&pid.to_ne_bytes()).context("gpu_pid_map delete") {
978 Ok(()) => {
979 applied.remove(pid);
980 }
981 Err(error) => {
982 if first_error.is_none() {
983 first_error = Some(error);
984 }
985 }
986 }
987 }
988 for (pid, node) in desired {
989 match map
990 .update(&pid.to_ne_bytes(), &node.to_ne_bytes(), MapFlags::ANY)
991 .context("gpu_pid_map update")
992 {
993 Ok(()) => {
994 applied.insert(*pid, *node);
995 }
996 Err(error) => {
997 if first_error.is_none() {
998 first_error = Some(error);
999 }
1000 }
1001 }
1002 }
1003 *self.previous_gpu_pids.as_mut().unwrap() = applied;
1004 if let Some(error) = first_error {
1005 return Err(error);
1006 }
1007 Ok(())
1008 }
1009
1010 fn add_running_gpu_processes_to_set(
1012 device: &nvml_wrapper::Device<'_>,
1013 node: u32,
1014 pid_to_nodes: &mut HashMap<u32, HashSet<u32>>,
1015 ) -> Result<()> {
1016 let mut errors = Vec::new();
1017
1018 match device.running_compute_processes() {
1019 Ok(processes) => {
1020 for process in processes {
1021 pid_to_nodes.entry(process.pid).or_default().insert(node);
1022 }
1023 }
1024 Err(error) => errors.push(format!("compute process query failed: {error}")),
1025 }
1026 match device.running_graphics_processes() {
1027 Ok(processes) => {
1028 for process in processes {
1029 pid_to_nodes.entry(process.pid).or_default().insert(node);
1030 }
1031 }
1032 Err(error) => errors.push(format!("graphics process query failed: {error}")),
1033 }
1034
1035 if errors.is_empty() {
1036 Ok(())
1037 } else {
1038 bail!(errors.join("; "))
1039 }
1040 }
1041
1042 fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
1043 let prog = &mut skel.progs.enable_primary_cpu;
1044 let mut args = cpu_arg {
1045 cpu_id: cpu as c_int,
1046 };
1047 let input = ProgramInput {
1048 context_in: Some(unsafe {
1049 std::slice::from_raw_parts_mut(
1050 &mut args as *mut _ as *mut u8,
1051 std::mem::size_of_val(&args),
1052 )
1053 }),
1054 ..Default::default()
1055 };
1056 let out = prog.test_run(input).unwrap();
1057 if out.return_value != 0 {
1058 return Err(out.return_value);
1059 }
1060
1061 Ok(())
1062 }
1063
1064 fn enable_sibling_cpu(
1065 skel: &mut BpfSkel<'_>,
1066 cpu: usize,
1067 sibling_cpu: usize,
1068 ) -> Result<(), u32> {
1069 let prog = &mut skel.progs.enable_sibling_cpu;
1070 let mut args = domain_arg {
1071 cpu_id: cpu as c_int,
1072 sibling_cpu_id: sibling_cpu as c_int,
1073 };
1074 let input = ProgramInput {
1075 context_in: Some(unsafe {
1076 std::slice::from_raw_parts_mut(
1077 &mut args as *mut _ as *mut u8,
1078 std::mem::size_of_val(&args),
1079 )
1080 }),
1081 ..Default::default()
1082 };
1083 let out = prog.test_run(input).unwrap();
1084 if out.return_value != 0 {
1085 return Err(out.return_value);
1086 }
1087
1088 Ok(())
1089 }
1090
1091 fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
1092 let smt_siblings = topo.sibling_cpus();
1093
1094 info!("SMT sibling CPUs: {:?}", smt_siblings);
1095 for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
1096 Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
1097 }
1098
1099 Ok(())
1100 }
1101
1102 fn get_metrics(&self) -> Metrics {
1103 let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
1104 Metrics {
1105 nr_event_dispatches: bss_data.nr_event_dispatches,
1106 nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
1107 nr_gpu_dispatches: bss_data.nr_gpu_dispatches,
1108 nr_overload_events: bss_data.nr_overload_events,
1109 }
1110 }
1111
1112 pub fn exited(&mut self) -> bool {
1113 uei_exited!(&self.skel, uei)
1114 }
1115
1116 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1117 let (res_ch, req_ch) = self.stats_server.channels();
1118
1119 let polling_time = Duration::from_millis(self.opts.polling_ms).min(Duration::from_secs(1));
1121 let mut last_update = Instant::now();
1122 let mut last_gpu_sync = Instant::now();
1123
1124 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1125 if !polling_time.is_zero() && last_update.elapsed() >= polling_time {
1126 let elapsed_secs = last_update.elapsed().as_secs_f64();
1128
1129 if let Some(ref mut state) = self.perf_threshold_state {
1131 let nr_event = self
1132 .skel
1133 .maps
1134 .bss_data
1135 .as_ref()
1136 .unwrap()
1137 .nr_event_dispatches;
1138 if let Some(new_thresh) =
1139 state.update(nr_event, elapsed_secs, self.opts.verbose, "perf_threshold")
1140 {
1141 self.skel.maps.bss_data.as_mut().unwrap().perf_threshold = new_thresh;
1142 }
1143 }
1144
1145 if let Some(ref mut state) = self.perf_sticky_threshold_state {
1147 let nr_sticky = self
1148 .skel
1149 .maps
1150 .bss_data
1151 .as_ref()
1152 .unwrap()
1153 .nr_ev_sticky_dispatches;
1154 if let Some(new_thresh) = state.update(
1155 nr_sticky,
1156 elapsed_secs,
1157 self.opts.verbose,
1158 "perf_sticky_threshold",
1159 ) {
1160 self.skel
1161 .maps
1162 .bss_data
1163 .as_mut()
1164 .unwrap()
1165 .perf_sticky_threshold = new_thresh;
1166 }
1167 }
1168
1169 last_update = Instant::now();
1170 }
1171
1172 if self.gpu_index_to_node.is_some() && last_gpu_sync.elapsed() >= GPU_SYNC_INTERVAL {
1174 if let Err(e) = self.sync_gpu_pids() {
1175 debug!("GPU PID sync: {}", e);
1176 }
1177 last_gpu_sync = Instant::now();
1178 }
1179
1180 let timeout = if polling_time.is_zero() {
1182 Duration::from_secs(1)
1183 } else {
1184 polling_time
1185 };
1186 match req_ch.recv_timeout(timeout) {
1187 Ok(()) => res_ch.send(self.get_metrics())?,
1188 Err(RecvTimeoutError::Timeout) => {}
1189 Err(e) => Err(e)?,
1190 }
1191 }
1192
1193 let _ = self.struct_ops.take();
1194 uei_report!(&self.skel, uei)
1195 }
1196}
1197
1198impl Drop for Scheduler<'_> {
1199 fn drop(&mut self) {
1200 info!("Unregister {SCHEDULER_NAME} scheduler");
1201 }
1202}
1203
1204fn main() -> Result<()> {
1205 let opts = Opts::parse();
1206
1207 if opts.version {
1208 println!(
1209 "{} {}",
1210 SCHEDULER_NAME,
1211 build_id::full_version(env!("CARGO_PKG_VERSION"))
1212 );
1213 return Ok(());
1214 }
1215
1216 if opts.help_stats {
1217 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1218 return Ok(());
1219 }
1220
1221 let loglevel = simplelog::LevelFilter::Info;
1222
1223 let mut lcfg = simplelog::ConfigBuilder::new();
1224 lcfg.set_time_offset_to_local()
1225 .expect("Failed to set local time offset")
1226 .set_time_level(simplelog::LevelFilter::Error)
1227 .set_location_level(simplelog::LevelFilter::Off)
1228 .set_target_level(simplelog::LevelFilter::Off)
1229 .set_thread_level(simplelog::LevelFilter::Off);
1230 simplelog::TermLogger::init(
1231 loglevel,
1232 lcfg.build(),
1233 simplelog::TerminalMode::Stderr,
1234 simplelog::ColorChoice::Auto,
1235 )?;
1236
1237 let shutdown = Arc::new(AtomicBool::new(false));
1238 let shutdown_clone = shutdown.clone();
1239 ctrlc::set_handler(move || {
1240 shutdown_clone.store(true, Ordering::Relaxed);
1241 })
1242 .context("Error setting Ctrl-C handler")?;
1243
1244 if let Some(intv) = opts.monitor.or(opts.stats) {
1245 let shutdown_copy = shutdown.clone();
1246 let jh = std::thread::spawn(move || {
1247 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
1248 Ok(_) => {
1249 debug!("stats monitor thread finished successfully")
1250 }
1251 Err(error_object) => {
1252 warn!(
1253 "stats monitor thread finished because of an error {}",
1254 error_object
1255 )
1256 }
1257 }
1258 });
1259 if opts.monitor.is_some() {
1260 let _ = jh.join();
1261 return Ok(());
1262 }
1263 }
1264
1265 let mut open_object = MaybeUninit::uninit();
1266 loop {
1267 let mut sched = Scheduler::init(&opts, &mut open_object)?;
1268 if !sched.run(shutdown.clone())?.should_restart() {
1269 break;
1270 }
1271 }
1272
1273 Ok(())
1274}