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::Arc;
22use std::sync::atomic::AtomicBool;
23use std::sync::atomic::Ordering;
24use std::time::{Duration, Instant};
25
26use anyhow::Context;
27use anyhow::Result;
28use anyhow::bail;
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::Nvml;
37use nvml_wrapper::bitmasks::InitFlags;
38use scx_stats::prelude::*;
39use scx_utils::GpuIndex;
40use scx_utils::NR_CPU_IDS;
41use scx_utils::Powermode;
42use scx_utils::Topology;
43use scx_utils::UserExitInfo;
44use scx_utils::build_id;
45use scx_utils::compat;
46use scx_utils::get_primary_cpus;
47use scx_utils::libbpf_clap_opts::LibbpfOpts;
48use scx_utils::perf::PerfEventSpec;
49use scx_utils::perf::parse_perf_event;
50use scx_utils::perf::setup_perf_events;
51use scx_utils::scx_ops_attach;
52use scx_utils::scx_ops_load;
53use scx_utils::scx_ops_open;
54use scx_utils::try_set_rlimit_infinity;
55use scx_utils::uei_exited;
56use scx_utils::uei_report;
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 (DYNAMIC_THRESHOLD_RATE_LOW..=DYNAMIC_THRESHOLD_RATE_HIGH).contains(&rate) {
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 && let Err(e) =
764 setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
765 && cpu == 0
766 {
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!(
771 "PMU event '{}' not supported - scheduler will run without perf monitoring",
772 opts.perf_config.display_name
773 );
774 } else {
775 warn!("Failed to setup perf events: {}", e);
776 }
777 perf_available = false;
778 break;
779 }
780 if opts.perf_sticky.event_id > 0
781 && let Err(e) = setup_perf_events(
782 &skel.maps.scx_pmu_map,
783 cpu as i32,
784 &opts.perf_sticky,
785 sticky_counter_idx,
786 )
787 && cpu == 0
788 {
789 let err_str = e.to_string();
790 if err_str.contains("errno 2") || err_str.contains("os error 2") {
791 warn!("Performance counters not available on this CPU architecture");
792 warn!(
793 "PMU event '{}' not supported - scheduler will run without perf monitoring",
794 opts.perf_sticky.display_name
795 );
796 } else {
797 warn!("Failed to setup perf events: {}", e);
798 }
799 perf_available = false;
800 break;
801 }
802 }
803 if perf_available {
804 info!("Performance counters configured successfully for all CPUs");
805 }
806
807 if opts.gpu && numa_enabled {
809 for (id, gpu) in topo.gpus() {
810 let GpuIndex::Nvidia { nvml_id } = id;
811 if opts.verbose {
812 info!("GPU{} -> node{}", nvml_id, gpu.node_id);
813 }
814 skel.maps.gpu_node_map.update(
815 &nvml_id.to_ne_bytes(),
816 &(gpu.node_id as u32).to_ne_bytes(),
817 MapFlags::ANY,
818 )?;
819 }
820 }
821
822 if primary_cpus.len() < *NR_CPU_IDS {
824 for cpu in primary_cpus {
825 if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
826 bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
827 }
828 }
829 }
830
831 if smt_enabled {
833 Self::init_smt_domains(&mut skel, &topo)?;
834 }
835
836 let struct_ops = Some(scx_ops_attach!(skel, cosmos_ops)?);
838 let stats_server = StatsServer::new(stats::server_data()).launch()?;
839
840 let perf_threshold_state = if opts.perf_config.event_id > 0 && opts.perf_threshold == 0 {
842 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
843 } else {
844 None
845 };
846 let perf_sticky_threshold_state =
847 if opts.perf_sticky.event_id > 0 && opts.perf_sticky_threshold == 0 {
848 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
849 } else {
850 None
851 };
852
853 Ok(Self {
854 skel,
855 opts,
856 struct_ops,
857 stats_server,
858 gpu_index_to_node,
859 previous_gpu_pids,
860 nvml,
861 gpu_cgroup_reader,
862 perf_threshold_state,
863 perf_sticky_threshold_state,
864 })
865 }
866
867 fn sync_gpu_pids(&mut self) -> Result<()> {
872 let gpu_index_to_node = match &self.gpu_index_to_node {
873 Some(m) => m,
874 None => return Ok(()),
875 };
876 let nvml = match &self.nvml {
877 Some(n) => n,
878 None => return Ok(()),
879 };
880 let threshold = self.opts.gpu_util_threshold;
881 let mut pid_to_nodes: HashMap<u32, HashSet<u32>> = HashMap::new();
883 let mut snapshot_complete = true;
884
885 let count = nvml.device_count().context("NVML device count")?;
888 for i in 0..count {
889 let node = match gpu_index_to_node.get(&i) {
890 Some(&n) => n,
891 None => {
892 snapshot_complete = false;
893 continue;
894 }
895 };
896 let device = match nvml.device_by_index(i) {
897 Ok(device) => device,
898 Err(error) => {
899 debug!("NVML device {i} lookup failed: {error:#}");
900 return Err(error).context(format!("NVML device {i} lookup failed"));
901 }
902 };
903
904 if threshold > 0 {
905 match device.process_utilization_stats(None::<u64>) {
907 Ok(samples) => {
908 for sample in samples {
909 let util = sample.sm_util.max(sample.mem_util);
910 if util >= threshold {
911 pid_to_nodes.entry(sample.pid).or_default().insert(node);
912 }
913 }
914 }
915 Err(_) => {
916 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
918 .with_context(|| {
919 format!("NVML device {i} process snapshot is incomplete")
920 })?;
921 }
922 }
923 } else {
924 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
925 .with_context(|| format!("NVML device {i} process snapshot is incomplete"))?;
926 }
927 }
928
929 let mut direct = gpu::direct_gpu_processes(&pid_to_nodes);
930 direct.remove(&std::process::id());
931 let mut current = if snapshot_complete {
932 self.gpu_cgroup_reader
933 .as_ref()
934 .map(|reader| gpu::expand_gpu_processes(&pid_to_nodes, reader))
935 .unwrap_or_else(|| direct.clone())
936 } else {
937 direct.clone()
938 };
939 current.remove(&std::process::id());
940 let max_entries = self.skel.maps.gpu_pid_map.max_entries() as usize;
941
942 if current.len() > max_entries {
945 warn!(
946 "GPU workload has {} processes, exceeding gpu_pid_map capacity {}; using {} direct NVML processes",
947 current.len(),
948 max_entries,
949 direct.len()
950 );
951 }
952 if direct.len() > max_entries {
953 warn!(
954 "{} direct NVML processes exceed gpu_pid_map capacity {}; clearing GPU process hints",
955 direct.len(),
956 max_entries
957 );
958 }
959 current = gpu::fit_gpu_processes(current, &direct, max_entries);
960
961 self.reconcile_gpu_pid_hints(¤t)
962 }
963
964 fn reconcile_gpu_pid_hints(&mut self, desired: &HashMap<u32, u32>) -> Result<()> {
966 let previous = self.previous_gpu_pids.as_ref().unwrap().clone();
967 let map = &self.skel.maps.gpu_pid_map;
968
969 let mut applied = previous.clone();
972 let mut first_error = None;
973
974 for pid in previous.keys() {
977 if desired.contains_key(pid) {
978 continue;
979 }
980 match map.delete(&pid.to_ne_bytes()).context("gpu_pid_map delete") {
981 Ok(()) => {
982 applied.remove(pid);
983 }
984 Err(error) => {
985 if first_error.is_none() {
986 first_error = Some(error);
987 }
988 }
989 }
990 }
991 for (pid, node) in desired {
992 match map
993 .update(&pid.to_ne_bytes(), &node.to_ne_bytes(), MapFlags::ANY)
994 .context("gpu_pid_map update")
995 {
996 Ok(()) => {
997 applied.insert(*pid, *node);
998 }
999 Err(error) => {
1000 if first_error.is_none() {
1001 first_error = Some(error);
1002 }
1003 }
1004 }
1005 }
1006 *self.previous_gpu_pids.as_mut().unwrap() = applied;
1007 if let Some(error) = first_error {
1008 return Err(error);
1009 }
1010 Ok(())
1011 }
1012
1013 fn add_running_gpu_processes_to_set(
1015 device: &nvml_wrapper::Device<'_>,
1016 node: u32,
1017 pid_to_nodes: &mut HashMap<u32, HashSet<u32>>,
1018 ) -> Result<()> {
1019 let mut errors = Vec::new();
1020
1021 match device.running_compute_processes() {
1022 Ok(processes) => {
1023 for process in processes {
1024 pid_to_nodes.entry(process.pid).or_default().insert(node);
1025 }
1026 }
1027 Err(error) => errors.push(format!("compute process query failed: {error}")),
1028 }
1029 match device.running_graphics_processes() {
1030 Ok(processes) => {
1031 for process in processes {
1032 pid_to_nodes.entry(process.pid).or_default().insert(node);
1033 }
1034 }
1035 Err(error) => errors.push(format!("graphics process query failed: {error}")),
1036 }
1037
1038 if errors.is_empty() {
1039 Ok(())
1040 } else {
1041 bail!(errors.join("; "))
1042 }
1043 }
1044
1045 fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
1046 let prog = &mut skel.progs.enable_primary_cpu;
1047 let mut args = cpu_arg {
1048 cpu_id: cpu as c_int,
1049 };
1050 let input = ProgramInput {
1051 context_in: Some(unsafe {
1052 std::slice::from_raw_parts_mut(
1053 &mut args as *mut _ as *mut u8,
1054 std::mem::size_of_val(&args),
1055 )
1056 }),
1057 ..Default::default()
1058 };
1059 let out = prog.test_run(input).unwrap();
1060 if out.return_value != 0 {
1061 return Err(out.return_value);
1062 }
1063
1064 Ok(())
1065 }
1066
1067 fn enable_sibling_cpu(
1068 skel: &mut BpfSkel<'_>,
1069 cpu: usize,
1070 sibling_cpu: usize,
1071 ) -> Result<(), u32> {
1072 let prog = &mut skel.progs.enable_sibling_cpu;
1073 let mut args = domain_arg {
1074 cpu_id: cpu as c_int,
1075 sibling_cpu_id: sibling_cpu as c_int,
1076 };
1077 let input = ProgramInput {
1078 context_in: Some(unsafe {
1079 std::slice::from_raw_parts_mut(
1080 &mut args as *mut _ as *mut u8,
1081 std::mem::size_of_val(&args),
1082 )
1083 }),
1084 ..Default::default()
1085 };
1086 let out = prog.test_run(input).unwrap();
1087 if out.return_value != 0 {
1088 return Err(out.return_value);
1089 }
1090
1091 Ok(())
1092 }
1093
1094 fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
1095 let smt_siblings = topo.sibling_cpus();
1096
1097 info!("SMT sibling CPUs: {:?}", smt_siblings);
1098 for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
1099 Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
1100 }
1101
1102 Ok(())
1103 }
1104
1105 fn get_metrics(&self) -> Metrics {
1106 let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
1107 Metrics {
1108 nr_event_dispatches: bss_data.nr_event_dispatches,
1109 nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
1110 nr_gpu_dispatches: bss_data.nr_gpu_dispatches,
1111 nr_overload_events: bss_data.nr_overload_events,
1112 }
1113 }
1114
1115 pub fn exited(&mut self) -> bool {
1116 uei_exited!(&self.skel, uei)
1117 }
1118
1119 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1120 let (res_ch, req_ch) = self.stats_server.channels();
1121
1122 let polling_time = Duration::from_millis(self.opts.polling_ms).min(Duration::from_secs(1));
1124 let mut last_update = Instant::now();
1125 let mut last_gpu_sync = Instant::now();
1126
1127 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1128 if !polling_time.is_zero() && last_update.elapsed() >= polling_time {
1129 let elapsed_secs = last_update.elapsed().as_secs_f64();
1131
1132 if let Some(ref mut state) = self.perf_threshold_state {
1134 let nr_event = self
1135 .skel
1136 .maps
1137 .bss_data
1138 .as_ref()
1139 .unwrap()
1140 .nr_event_dispatches;
1141 if let Some(new_thresh) =
1142 state.update(nr_event, elapsed_secs, self.opts.verbose, "perf_threshold")
1143 {
1144 self.skel.maps.bss_data.as_mut().unwrap().perf_threshold = new_thresh;
1145 }
1146 }
1147
1148 if let Some(ref mut state) = self.perf_sticky_threshold_state {
1150 let nr_sticky = self
1151 .skel
1152 .maps
1153 .bss_data
1154 .as_ref()
1155 .unwrap()
1156 .nr_ev_sticky_dispatches;
1157 if let Some(new_thresh) = state.update(
1158 nr_sticky,
1159 elapsed_secs,
1160 self.opts.verbose,
1161 "perf_sticky_threshold",
1162 ) {
1163 self.skel
1164 .maps
1165 .bss_data
1166 .as_mut()
1167 .unwrap()
1168 .perf_sticky_threshold = new_thresh;
1169 }
1170 }
1171
1172 last_update = Instant::now();
1173 }
1174
1175 if self.gpu_index_to_node.is_some() && last_gpu_sync.elapsed() >= GPU_SYNC_INTERVAL {
1177 if let Err(e) = self.sync_gpu_pids() {
1178 debug!("GPU PID sync: {}", e);
1179 }
1180 last_gpu_sync = Instant::now();
1181 }
1182
1183 let timeout = if polling_time.is_zero() {
1185 Duration::from_secs(1)
1186 } else {
1187 polling_time
1188 };
1189 match req_ch.recv_timeout(timeout) {
1190 Ok(()) => res_ch.send(self.get_metrics())?,
1191 Err(RecvTimeoutError::Timeout) => {}
1192 Err(e) => Err(e)?,
1193 }
1194 }
1195
1196 let _ = self.struct_ops.take();
1197 uei_report!(&self.skel, uei)
1198 }
1199}
1200
1201impl Drop for Scheduler<'_> {
1202 fn drop(&mut self) {
1203 info!("Unregister {SCHEDULER_NAME} scheduler");
1204 }
1205}
1206
1207fn main() -> Result<()> {
1208 let opts = Opts::parse();
1209
1210 if opts.version {
1211 println!(
1212 "{} {}",
1213 SCHEDULER_NAME,
1214 build_id::full_version(env!("CARGO_PKG_VERSION"))
1215 );
1216 return Ok(());
1217 }
1218
1219 if opts.help_stats {
1220 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1221 return Ok(());
1222 }
1223
1224 let loglevel = simplelog::LevelFilter::Info;
1225
1226 let mut lcfg = simplelog::ConfigBuilder::new();
1227 lcfg.set_time_offset_to_local()
1228 .expect("Failed to set local time offset")
1229 .set_time_level(simplelog::LevelFilter::Error)
1230 .set_location_level(simplelog::LevelFilter::Off)
1231 .set_target_level(simplelog::LevelFilter::Off)
1232 .set_thread_level(simplelog::LevelFilter::Off);
1233 simplelog::TermLogger::init(
1234 loglevel,
1235 lcfg.build(),
1236 simplelog::TerminalMode::Stderr,
1237 simplelog::ColorChoice::Auto,
1238 )?;
1239
1240 let shutdown = Arc::new(AtomicBool::new(false));
1241 let shutdown_clone = shutdown.clone();
1242 ctrlc::set_handler(move || {
1243 shutdown_clone.store(true, Ordering::Relaxed);
1244 })
1245 .context("Error setting Ctrl-C handler")?;
1246
1247 if let Some(intv) = opts.monitor.or(opts.stats) {
1248 let shutdown_copy = shutdown.clone();
1249 let jh = std::thread::spawn(move || {
1250 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
1251 Ok(_) => {
1252 debug!("stats monitor thread finished successfully")
1253 }
1254 Err(error_object) => {
1255 warn!(
1256 "stats monitor thread finished because of an error {}",
1257 error_object
1258 )
1259 }
1260 }
1261 });
1262 if opts.monitor.is_some() {
1263 let _ = jh.join();
1264 return Ok(());
1265 }
1266 }
1267
1268 let mut open_object = MaybeUninit::uninit();
1269 loop {
1270 let mut sched = Scheduler::init(&opts, &mut open_object)?;
1271 if !sched.run(shutdown.clone())?.should_restart() {
1272 break;
1273 }
1274 }
1275
1276 Ok(())
1277}