1mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::collections::{HashMap, HashSet};
15use std::ffi::{c_int, c_ulong};
16use std::fs;
17use std::fs::File;
18use std::io::{BufRead, BufReader};
19use std::mem::MaybeUninit;
20use std::path::Path;
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")]
99 cpu_busy_thresh: u64,
100
101 #[clap(short = 'p', long, default_value = "0")]
112 polling_ms: u64,
113
114 #[clap(short = 'm', long)]
126 primary_domain: Option<String>,
127
128 #[clap(short = 'e', long, default_value = "0x0", value_parser = parse_perf_event)]
131 perf_config: PerfEventSpec,
132
133 #[clap(short = 'E', default_value = "0", long)]
135 perf_threshold: u64,
136
137 #[clap(short = 'y', long, default_value = "0x0", value_parser = parse_perf_event)]
140 perf_sticky: PerfEventSpec,
141
142 #[clap(short = 'Y', default_value = "0", long)]
144 perf_sticky_threshold: u64,
145
146 #[clap(short = 'g', long, action = clap::ArgAction::SetTrue)]
148 gpu: bool,
149
150 #[clap(long, default_value = "0", value_parser = clap::value_parser!(u32).range(0..=100))]
155 gpu_util_threshold: u32,
156
157 #[clap(short = 'n', long, action = clap::ArgAction::SetTrue)]
159 disable_numa: bool,
160
161 #[clap(short = 'f', long, action = clap::ArgAction::SetTrue)]
163 disable_cpufreq: bool,
164
165 #[arg(short = 'i', long, action = clap::ArgAction::SetTrue)]
170 flat_idle_scan: bool,
171
172 #[clap(short = 'P', long, action = clap::ArgAction::SetTrue)]
177 preferred_idle_scan: bool,
178
179 #[clap(long, action = clap::ArgAction::SetTrue)]
184 disable_smt: bool,
185
186 #[clap(short = 'S', long, action = clap::ArgAction::SetTrue)]
188 avoid_smt: bool,
189
190 #[clap(short = 'N', long, action = clap::ArgAction::SetTrue)]
196 no_early_clear: bool,
197
198 #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
205 no_wake_sync: bool,
206
207 #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
209 no_deferred_wakeup: bool,
210
211 #[clap(long, action = clap::ArgAction::SetTrue)]
217 time_preemption: bool,
218
219 #[clap(short = 'a', long, action = clap::ArgAction::SetTrue)]
226 mm_affinity: bool,
227
228 #[clap(long)]
230 stats: Option<f64>,
231
232 #[clap(long)]
235 monitor: Option<f64>,
236
237 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
239 verbose: bool,
240
241 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
243 version: bool,
244
245 #[clap(long)]
247 help_stats: bool,
248
249 #[clap(flatten, next_help_heading = "Libbpf Options")]
250 pub libbpf: LibbpfOpts,
251}
252
253pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
254 let mut cpus = Vec::new();
255 let mut seen = HashSet::new();
256
257 if let Some(mode) = match optarg {
259 "powersave" => Some(Powermode::Powersave),
260 "performance" => Some(Powermode::Performance),
261 "turbo" => Some(Powermode::Turbo),
262 "all" => Some(Powermode::Any),
263 _ => None,
264 } {
265 return get_primary_cpus(mode).map_err(|e| e.to_string());
266 }
267
268 if optarg
270 .chars()
271 .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
272 {
273 return Err("Invalid character in CPU list".to_string());
274 }
275
276 let cleaned = optarg.replace(' ', "\t");
278
279 for token in cleaned.split(',') {
280 let token = token.trim_matches(|c: char| c.is_whitespace());
281
282 if token.is_empty() {
283 continue;
284 }
285
286 if let Some((start_str, end_str)) = token.split_once('-') {
287 let start = start_str
288 .trim()
289 .parse::<usize>()
290 .map_err(|_| "Invalid range start")?;
291 let end = end_str
292 .trim()
293 .parse::<usize>()
294 .map_err(|_| "Invalid range end")?;
295
296 if start > end {
297 return Err(format!("Invalid CPU range: {}-{}", start, end));
298 }
299
300 for i in start..=end {
301 if cpus.len() >= *NR_CPU_IDS {
302 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
303 }
304 if seen.insert(i) {
305 cpus.push(i);
306 }
307 }
308 } else {
309 let cpu = token
310 .parse::<usize>()
311 .map_err(|_| format!("Invalid CPU: {}", token))?;
312 if cpus.len() >= *NR_CPU_IDS {
313 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
314 }
315 if seen.insert(cpu) {
316 cpus.push(cpu);
317 }
318 }
319 }
320
321 Ok(cpus)
322}
323
324const DYNAMIC_THRESHOLD_INIT_VALUE: u64 = 1000;
326
327const DYNAMIC_THRESHOLD_MIN_VALUE: u64 = 10;
329
330const DYNAMIC_THRESHOLD_RATE_HIGH: f64 = 4000.0;
332
333const DYNAMIC_THRESHOLD_RATE_LOW: f64 = 2000.0;
335
336const DYNAMIC_THRESHOLD_HYSTERESIS: f64 = 0.1;
339
340const DYNAMIC_THRESHOLD_EMA_ALPHA: f64 = 0.3;
343
344const DYNAMIC_THRESHOLD_SCALE_MIN: f64 = 0.0001;
346
347const DYNAMIC_THRESHOLD_SCALE_MAX: f64 = 1000.0;
349
350const DYNAMIC_THRESHOLD_SLOPE_HIGH: f64 = 0.35;
353
354const DYNAMIC_THRESHOLD_SLOPE_LOW: f64 = 0.58;
356
357const GPU_SYNC_INTERVAL: Duration = Duration::from_secs(1);
360
361#[derive(Debug, Clone)]
367struct DynamicThresholdState {
368 threshold: u64,
370 smoothed_rate: f64,
372 prev_counter: u64,
374 initialized: bool,
376 adjustment_direction: Option<bool>,
380}
381
382impl DynamicThresholdState {
383 fn new(initial_threshold: u64) -> Self {
385 Self {
386 threshold: initial_threshold,
387 smoothed_rate: 0.0,
388 prev_counter: 0,
389 initialized: false,
390 adjustment_direction: None,
391 }
392 }
393
394 fn update(
397 &mut self,
398 counter: u64,
399 elapsed_secs: f64,
400 verbose: bool,
401 name: &str,
402 ) -> Option<u64> {
403 if elapsed_secs <= 0.0 {
404 return None;
405 }
406
407 let delta = counter.saturating_sub(self.prev_counter);
409 self.prev_counter = counter;
410 let raw_rate = delta as f64 / elapsed_secs;
411
412 if self.initialized {
414 self.smoothed_rate = DYNAMIC_THRESHOLD_EMA_ALPHA * raw_rate
415 + (1.0 - DYNAMIC_THRESHOLD_EMA_ALPHA) * self.smoothed_rate;
416 } else {
417 self.smoothed_rate = raw_rate;
419 self.initialized = true;
420 }
421
422 let rate = self.smoothed_rate;
424 let old_threshold = self.threshold;
425
426 let (effective_high, effective_low) = match self.adjustment_direction {
428 Some(true) => {
429 (
431 DYNAMIC_THRESHOLD_RATE_HIGH,
432 DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
433 )
434 }
435 Some(false) => {
436 (
438 DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
439 DYNAMIC_THRESHOLD_RATE_LOW,
440 )
441 }
442 None => {
443 (
445 DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
446 DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
447 )
448 }
449 };
450
451 let new_direction = if rate > effective_high {
453 Some(true) } else if rate < effective_low && rate >= 0.0 {
455 Some(false) } else {
457 if self.adjustment_direction.is_some() {
459 if rate >= DYNAMIC_THRESHOLD_RATE_LOW && rate <= DYNAMIC_THRESHOLD_RATE_HIGH {
461 None } else {
463 self.adjustment_direction }
465 } else {
466 None }
468 };
469
470 if let Some(raising) = new_direction {
472 let scale = Self::compute_scale(rate, raising);
473 let factor = if raising { 1.0 + scale } else { 1.0 - scale };
474 let new_threshold = ((self.threshold as f64) * factor).round() as u64;
475 self.threshold = new_threshold.clamp(DYNAMIC_THRESHOLD_MIN_VALUE, u64::MAX);
476 }
477
478 self.adjustment_direction = new_direction;
479
480 if self.threshold != old_threshold {
482 if verbose {
483 info!(
484 "{}: {} -> {} (smoothed rate {:.1}/s, raw {:.1}/s, dir {:?})",
485 name,
486 old_threshold,
487 self.threshold,
488 self.smoothed_rate,
489 raw_rate,
490 self.adjustment_direction
491 );
492 }
493 Some(self.threshold)
494 } else {
495 None
496 }
497 }
498
499 fn compute_scale(rate: f64, too_high: bool) -> f64 {
502 if too_high {
503 let excess = ((rate / DYNAMIC_THRESHOLD_RATE_HIGH) - 1.0).max(0.0);
504 let scale =
505 DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_HIGH * excess.min(4.0);
506 scale.min(DYNAMIC_THRESHOLD_SCALE_MAX)
507 } else {
508 if rate <= 0.0 {
509 return DYNAMIC_THRESHOLD_SCALE_MAX;
510 }
511 let deficit = (DYNAMIC_THRESHOLD_RATE_LOW - rate) / DYNAMIC_THRESHOLD_RATE_LOW;
512 let t = deficit.clamp(0.0, 1.0);
513 DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_LOW * t
514 }
515 }
516}
517
518#[derive(Debug, Clone, Copy)]
519struct CpuTimes {
520 user: u64,
521 nice: u64,
522 total: u64,
523}
524
525struct Scheduler<'a> {
526 skel: BpfSkel<'a>,
527 opts: &'a Opts,
528 struct_ops: Option<libbpf_rs::Link>,
529 stats_server: StatsServer<(), Metrics>,
530 gpu_index_to_node: Option<HashMap<u32, u32>>,
532 previous_gpu_pids: Option<HashMap<u32, u32>>,
534 nvml: Option<Nvml>,
536 perf_threshold_state: Option<DynamicThresholdState>,
538 perf_sticky_threshold_state: Option<DynamicThresholdState>,
540}
541
542impl<'a> Scheduler<'a> {
543 fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
544 try_set_rlimit_infinity();
545
546 let topo = Topology::new().unwrap();
548
549 let smt_enabled = !opts.disable_smt && topo.smt_enabled;
551
552 let nr_nodes = topo
554 .nodes
555 .values()
556 .filter(|node| !node.all_cpus.is_empty())
557 .count();
558 info!("NUMA nodes: {}", nr_nodes);
559
560 let numa_enabled = !opts.disable_numa && nr_nodes > 1;
562 if !numa_enabled {
563 info!("Disabling NUMA optimizations");
564 }
565
566 info!(
567 "{} {} {}",
568 SCHEDULER_NAME,
569 build_id::full_version(env!("CARGO_PKG_VERSION")),
570 if smt_enabled { "SMT on" } else { "SMT off" }
571 );
572
573 info!(
575 "scheduler options: {}",
576 std::env::args().collect::<Vec<_>>().join(" ")
577 );
578
579 let mut skel_builder = BpfSkelBuilder::default();
581 skel_builder.obj_builder.debug(opts.verbose);
582 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
583 let mut skel = scx_ops_open!(skel_builder, open_object, cosmos_ops, open_opts)?;
584
585 skel.struct_ops.cosmos_ops_mut().exit_dump_len = opts.exit_dump_len;
586
587 let rodata = skel.maps.rodata_data.as_mut().unwrap();
589 rodata.slice_ns = opts.slice_us * 1000;
590 rodata.slice_lag = opts.slice_lag_us * 1000;
591 rodata.cpufreq_enabled = !opts.disable_cpufreq;
592 rodata.flat_idle_scan = opts.flat_idle_scan;
593 rodata.smt_enabled = smt_enabled;
594 rodata.numa_enabled = numa_enabled;
595 rodata.nr_node_ids = topo.nodes.len() as u32;
596 rodata.no_wake_sync = opts.no_wake_sync;
597 rodata.no_early_clear = opts.no_early_clear;
598 rodata.time_preemption = opts.time_preemption;
599 rodata.mm_affinity = opts.mm_affinity;
600
601 rodata.perf_config = opts.perf_config.event_id;
603 rodata.perf_sticky = opts.perf_sticky.event_id;
604
605 rodata.busy_threshold = opts.cpu_busy_thresh * 1024 / 100;
607
608 let mut cpus: Vec<_> = topo.all_cpus.values().collect();
610 cpus.sort_by_key(|cpu| std::cmp::Reverse(cpu.cpu_capacity));
611 let max_cap = cpus.first().map(|c| c.cpu_capacity).unwrap_or(1).max(1);
613 for (i, cpu) in cpus.iter().enumerate() {
614 let normalized = (cpu.cpu_capacity * 1024 / max_cap).clamp(1, 1024);
615 rodata.cpu_capacity[cpu.id] = normalized as c_ulong;
616 rodata.preferred_cpus[i] = cpu.id as u64;
617 }
618 rodata.all_cpus_same_capacity = cpus.iter().all(|cpu| cpu.cpu_capacity == max_cap);
619 if opts.preferred_idle_scan {
620 info!(
621 "Preferred CPUs: {:?}",
622 &rodata.preferred_cpus[0..cpus.len()]
623 );
624 }
625 rodata.preferred_idle_scan = opts.preferred_idle_scan;
626
627 let primary_cpus = if let Some(ref domain) = opts.primary_domain {
629 match parse_cpu_list(domain) {
630 Ok(cpus) => cpus,
631 Err(e) => bail!("Error parsing primary domain: {}", e),
632 }
633 } else {
634 (0..*NR_CPU_IDS).collect()
635 };
636 if primary_cpus.len() < *NR_CPU_IDS {
637 info!("Primary CPUs: {:?}", primary_cpus);
638 rodata.primary_all = false;
639 } else {
640 rodata.primary_all = true;
641 }
642
643 let (gpu_index_to_node, previous_gpu_pids, nvml) = if opts.gpu && numa_enabled {
646 match Nvml::init_with_flags(InitFlags::NO_GPUS) {
647 Ok(nvml) => {
648 info!("NVIDIA GPU-aware scheduling enabled (NVML PID sync)");
649 rodata.gpu_enabled = true;
650 let mut idx_to_node = HashMap::new();
651 for (id, gpu) in topo.gpus() {
652 let GpuIndex::Nvidia { nvml_id } = id;
653 idx_to_node.insert(nvml_id, gpu.node_id as u32);
654 }
655 (Some(idx_to_node), Some(HashMap::new()), Some(nvml))
656 }
657 Err(e) => {
658 warn!("NVML init failed, disabling GPU-aware scheduling: {}", e);
659 rodata.gpu_enabled = false;
660 (None, None, None)
661 }
662 }
663 } else {
664 rodata.gpu_enabled = false;
665 (None, None, None)
666 };
667
668 skel.struct_ops.cosmos_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
670 | *compat::SCX_OPS_ENQ_LAST
671 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
672 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
673 | if numa_enabled {
674 *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
675 } else {
676 0
677 };
678
679 info!(
680 "scheduler flags: {:#x}",
681 skel.struct_ops.cosmos_ops_mut().flags
682 );
683
684 let mut skel = scx_ops_load!(skel, cosmos_ops, uei)?;
686
687 let bss = skel.maps.bss_data.as_mut().unwrap();
690 if opts.perf_config.event_id > 0 {
691 bss.perf_threshold = if opts.perf_threshold == 0 {
692 DYNAMIC_THRESHOLD_INIT_VALUE
693 } else {
694 opts.perf_threshold
695 };
696 }
697 if opts.perf_sticky.event_id > 0 {
698 bss.perf_sticky_threshold = if opts.perf_sticky_threshold == 0 {
699 DYNAMIC_THRESHOLD_INIT_VALUE
700 } else {
701 opts.perf_sticky_threshold
702 };
703 }
704
705 for node in topo.nodes.values() {
707 for cpu in node.all_cpus.values() {
708 if opts.verbose {
709 info!("CPU{} -> node{}", cpu.id, node.id);
710 }
711 skel.maps.cpu_node_map.update(
712 &(cpu.id as u32).to_ne_bytes(),
713 &(node.id as u32).to_ne_bytes(),
714 MapFlags::ANY,
715 )?;
716 }
717 }
718
719 let nr_cpus = *NR_CPU_IDS;
723 info!("Setting up performance counters for {} CPUs...", nr_cpus);
724 let mut perf_available = true;
725 let sticky_counter_idx = if opts.perf_config.event_id > 0 { 1 } else { 0 };
726 for cpu in 0..nr_cpus {
727 if opts.perf_config.event_id > 0 {
728 if let Err(e) =
729 setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
730 {
731 if cpu == 0 {
732 let err_str = e.to_string();
733 if err_str.contains("errno 2") || err_str.contains("os error 2") {
734 warn!("Performance counters not available on this CPU architecture");
735 warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_config.display_name);
736 } else {
737 warn!("Failed to setup perf events: {}", e);
738 }
739 perf_available = false;
740 break;
741 }
742 }
743 }
744 if opts.perf_sticky.event_id > 0 {
745 if let Err(e) = setup_perf_events(
746 &skel.maps.scx_pmu_map,
747 cpu as i32,
748 &opts.perf_sticky,
749 sticky_counter_idx,
750 ) {
751 if cpu == 0 {
752 let err_str = e.to_string();
753 if err_str.contains("errno 2") || err_str.contains("os error 2") {
754 warn!("Performance counters not available on this CPU architecture");
755 warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_sticky.display_name);
756 } else {
757 warn!("Failed to setup perf events: {}", e);
758 }
759 perf_available = false;
760 break;
761 }
762 }
763 }
764 }
765 if perf_available {
766 info!("Performance counters configured successfully for all CPUs");
767 }
768
769 if opts.gpu && numa_enabled {
771 for (id, gpu) in topo.gpus() {
772 let GpuIndex::Nvidia { nvml_id } = id;
773 if opts.verbose {
774 info!("GPU{} -> node{}", nvml_id, gpu.node_id);
775 }
776 skel.maps.gpu_node_map.update(
777 &(nvml_id as u32).to_ne_bytes(),
778 &(gpu.node_id as u32).to_ne_bytes(),
779 MapFlags::ANY,
780 )?;
781 }
782 }
783
784 if primary_cpus.len() < *NR_CPU_IDS {
786 for cpu in primary_cpus {
787 if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
788 bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
789 }
790 }
791 }
792
793 if smt_enabled {
795 Self::init_smt_domains(&mut skel, &topo)?;
796 }
797
798 let struct_ops = Some(scx_ops_attach!(skel, cosmos_ops)?);
800 let stats_server = StatsServer::new(stats::server_data()).launch()?;
801
802 let perf_threshold_state = if opts.perf_config.event_id > 0 && opts.perf_threshold == 0 {
804 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
805 } else {
806 None
807 };
808 let perf_sticky_threshold_state =
809 if opts.perf_sticky.event_id > 0 && opts.perf_sticky_threshold == 0 {
810 Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
811 } else {
812 None
813 };
814
815 Ok(Self {
816 skel,
817 opts,
818 struct_ops,
819 stats_server,
820 gpu_index_to_node,
821 previous_gpu_pids,
822 nvml,
823 perf_threshold_state,
824 perf_sticky_threshold_state,
825 })
826 }
827
828 fn sync_gpu_pids(&mut self) -> Result<()> {
832 let gpu_index_to_node = match &self.gpu_index_to_node {
833 Some(m) => m,
834 None => return Ok(()),
835 };
836 let nvml = match &self.nvml {
837 Some(n) => n,
838 None => return Ok(()),
839 };
840 let threshold = self.opts.gpu_util_threshold;
841 let previous = self.previous_gpu_pids.as_ref().unwrap();
842 let mut pid_to_nodes: HashMap<u32, HashSet<u32>> = HashMap::new();
844
845 let count = nvml.device_count().context("NVML device count")?;
846 for i in 0..count {
847 let node = match gpu_index_to_node.get(&i) {
848 Some(&n) => n,
849 None => continue,
850 };
851 let device = nvml.device_by_index(i).context("NVML device_by_index")?;
852
853 if threshold > 0 {
854 match device.process_utilization_stats(None::<u64>) {
856 Ok(samples) => {
857 for sample in samples {
858 let util = sample.sm_util.max(sample.mem_util);
859 if util >= threshold {
860 pid_to_nodes.entry(sample.pid).or_default().insert(node);
861 }
862 }
863 }
864 Err(_) => {
865 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes);
867 }
868 }
869 } else {
870 Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes);
871 }
872 }
873
874 let mut current: HashMap<u32, u32> = HashMap::new();
876 for (tgid, nodes) in pid_to_nodes {
877 if nodes.len() == 1 {
878 let node = nodes.into_iter().next().unwrap();
879 current.insert(tgid, node);
880 for tid in Self::task_tids(tgid) {
881 current.insert(tid, node);
882 }
883 }
884 }
885
886 let map = &self.skel.maps.gpu_pid_map;
887 for (pid, node) in ¤t {
888 map.update(&pid.to_ne_bytes(), &node.to_ne_bytes(), MapFlags::ANY)
889 .context("gpu_pid_map update")?;
890 }
891 for pid in previous.keys() {
892 if !current.contains_key(pid) {
893 let _ = map.delete(&pid.to_ne_bytes());
894 }
895 }
896 *self.previous_gpu_pids.as_mut().unwrap() = current;
897 Ok(())
898 }
899
900 fn add_running_gpu_processes_to_set(
902 device: &nvml_wrapper::Device<'_>,
903 node: u32,
904 pid_to_nodes: &mut HashMap<u32, HashSet<u32>>,
905 ) {
906 for proc in device
907 .running_compute_processes()
908 .unwrap_or_default()
909 .into_iter()
910 .chain(device.running_graphics_processes().unwrap_or_default())
911 {
912 pid_to_nodes.entry(proc.pid).or_default().insert(node);
913 }
914 }
915
916 fn task_tids(pid: u32) -> Vec<u32> {
918 let task_dir = format!("/proc/{}/task", pid);
919 let Ok(entries) = fs::read_dir(Path::new(&task_dir)) else {
920 return Vec::new();
921 };
922 entries
923 .filter_map(|e| e.ok())
924 .filter_map(|e| e.file_name().to_str().and_then(|s| s.parse::<u32>().ok()))
925 .collect()
926 }
927
928 fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
929 let prog = &mut skel.progs.enable_primary_cpu;
930 let mut args = cpu_arg {
931 cpu_id: cpu as c_int,
932 };
933 let input = ProgramInput {
934 context_in: Some(unsafe {
935 std::slice::from_raw_parts_mut(
936 &mut args as *mut _ as *mut u8,
937 std::mem::size_of_val(&args),
938 )
939 }),
940 ..Default::default()
941 };
942 let out = prog.test_run(input).unwrap();
943 if out.return_value != 0 {
944 return Err(out.return_value);
945 }
946
947 Ok(())
948 }
949
950 fn enable_sibling_cpu(
951 skel: &mut BpfSkel<'_>,
952 cpu: usize,
953 sibling_cpu: usize,
954 ) -> Result<(), u32> {
955 let prog = &mut skel.progs.enable_sibling_cpu;
956 let mut args = domain_arg {
957 cpu_id: cpu as c_int,
958 sibling_cpu_id: sibling_cpu as c_int,
959 };
960 let input = ProgramInput {
961 context_in: Some(unsafe {
962 std::slice::from_raw_parts_mut(
963 &mut args as *mut _ as *mut u8,
964 std::mem::size_of_val(&args),
965 )
966 }),
967 ..Default::default()
968 };
969 let out = prog.test_run(input).unwrap();
970 if out.return_value != 0 {
971 return Err(out.return_value);
972 }
973
974 Ok(())
975 }
976
977 fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
978 let smt_siblings = topo.sibling_cpus();
979
980 info!("SMT sibling CPUs: {:?}", smt_siblings);
981 for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
982 Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
983 }
984
985 Ok(())
986 }
987
988 fn get_metrics(&self) -> Metrics {
989 let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
990 Metrics {
991 nr_event_dispatches: bss_data.nr_event_dispatches,
992 nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
993 nr_gpu_dispatches: bss_data.nr_gpu_dispatches,
994 }
995 }
996
997 pub fn exited(&mut self) -> bool {
998 uei_exited!(&self.skel, uei)
999 }
1000
1001 fn compute_user_cpu_pct(prev: &CpuTimes, curr: &CpuTimes) -> Option<u64> {
1002 let user_diff = (curr.user + curr.nice).saturating_sub(prev.user + prev.nice);
1004 let total_diff = curr.total.saturating_sub(prev.total);
1005
1006 if total_diff > 0 {
1007 let user_ratio = user_diff as f64 / total_diff as f64;
1008 Some((user_ratio * 1024.0).round() as u64)
1009 } else {
1010 None
1011 }
1012 }
1013
1014 fn parse_per_cpu_cpu_times<R: BufRead>(
1017 reader: R,
1018 nr_cpus: usize,
1019 ) -> Option<Vec<Option<CpuTimes>>> {
1020 let mut result = vec![None; nr_cpus];
1021
1022 for line in reader.lines() {
1023 let line = line.ok()?;
1024 let line = line.trim();
1025 if !line.starts_with("cpu") {
1026 continue;
1027 }
1028 let rest = line.strip_prefix("cpu")?;
1029 if rest.starts_with(' ') {
1030 continue;
1032 }
1033 let cpu_id: usize = rest.split_whitespace().next()?.parse().ok()?;
1034 if cpu_id >= nr_cpus {
1035 continue;
1036 }
1037 let fields: Vec<&str> = line.split_whitespace().collect();
1038 if fields.len() < 5 {
1039 return None;
1040 }
1041 let user: u64 = fields[1].parse().ok()?;
1042 let nice: u64 = fields[2].parse().ok()?;
1043 let total: u64 = fields
1044 .iter()
1045 .skip(1)
1046 .take(8)
1047 .filter_map(|v| v.parse::<u64>().ok())
1048 .sum();
1049 result[cpu_id] = Some(CpuTimes { user, nice, total });
1050 }
1051
1052 result.iter().any(Option::is_some).then_some(result)
1053 }
1054
1055 fn read_per_cpu_cpu_times(nr_cpus: usize) -> Option<Vec<Option<CpuTimes>>> {
1057 let file = File::open("/proc/stat").ok()?;
1058 Self::parse_per_cpu_cpu_times(BufReader::new(file), nr_cpus)
1059 }
1060
1061 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1062 let (res_ch, req_ch) = self.stats_server.channels();
1063
1064 let polling_time = Duration::from_millis(self.opts.polling_ms).min(Duration::from_secs(1));
1069 let nr_cpus = *NR_CPU_IDS as usize;
1070 let mut prev_cputime = Self::read_per_cpu_cpu_times(nr_cpus).unwrap_or_else(|| {
1071 warn!("Failed to read initial per-CPU stats; starting with zero CPU utilization");
1072 vec![None; nr_cpus]
1073 });
1074 let mut last_update = Instant::now();
1075 let mut last_gpu_sync = Instant::now();
1076
1077 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1078 if !polling_time.is_zero() && last_update.elapsed() >= polling_time {
1080 if let Some(curr_cputime) = Self::read_per_cpu_cpu_times(nr_cpus) {
1081 let map = &self.skel.maps.cpu_util_map;
1082 for cpu in 0..nr_cpus {
1083 let util = match (&prev_cputime[cpu], &curr_cputime[cpu]) {
1084 (Some(prev), Some(curr)) => Self::compute_user_cpu_pct(prev, curr),
1085 _ => Some(0),
1086 };
1087
1088 if let Some(util) = util {
1089 let _ = map.update(
1090 &(cpu as u32).to_ne_bytes(),
1091 &util.to_ne_bytes(),
1092 MapFlags::ANY,
1093 );
1094 }
1095 }
1096 prev_cputime = curr_cputime;
1097 }
1098
1099 let elapsed_secs = last_update.elapsed().as_secs_f64();
1101
1102 if let Some(ref mut state) = self.perf_threshold_state {
1104 let nr_event = self
1105 .skel
1106 .maps
1107 .bss_data
1108 .as_ref()
1109 .unwrap()
1110 .nr_event_dispatches;
1111 if let Some(new_thresh) =
1112 state.update(nr_event, elapsed_secs, self.opts.verbose, "perf_threshold")
1113 {
1114 self.skel.maps.bss_data.as_mut().unwrap().perf_threshold = new_thresh;
1115 }
1116 }
1117
1118 if let Some(ref mut state) = self.perf_sticky_threshold_state {
1120 let nr_sticky = self
1121 .skel
1122 .maps
1123 .bss_data
1124 .as_ref()
1125 .unwrap()
1126 .nr_ev_sticky_dispatches;
1127 if let Some(new_thresh) = state.update(
1128 nr_sticky,
1129 elapsed_secs,
1130 self.opts.verbose,
1131 "perf_sticky_threshold",
1132 ) {
1133 self.skel
1134 .maps
1135 .bss_data
1136 .as_mut()
1137 .unwrap()
1138 .perf_sticky_threshold = new_thresh;
1139 }
1140 }
1141
1142 last_update = Instant::now();
1143 }
1144
1145 if self.gpu_index_to_node.is_some() && last_gpu_sync.elapsed() >= GPU_SYNC_INTERVAL {
1147 if let Err(e) = self.sync_gpu_pids() {
1148 debug!("GPU PID sync: {}", e);
1149 }
1150 last_gpu_sync = Instant::now();
1151 }
1152
1153 let timeout = if polling_time.is_zero() {
1155 Duration::from_secs(1)
1156 } else {
1157 polling_time
1158 };
1159 match req_ch.recv_timeout(timeout) {
1160 Ok(()) => res_ch.send(self.get_metrics())?,
1161 Err(RecvTimeoutError::Timeout) => {}
1162 Err(e) => Err(e)?,
1163 }
1164 }
1165
1166 let _ = self.struct_ops.take();
1167 uei_report!(&self.skel, uei)
1168 }
1169}
1170
1171impl Drop for Scheduler<'_> {
1172 fn drop(&mut self) {
1173 info!("Unregister {SCHEDULER_NAME} scheduler");
1174 }
1175}
1176
1177fn main() -> Result<()> {
1178 let opts = Opts::parse();
1179
1180 if opts.version {
1181 println!(
1182 "{} {}",
1183 SCHEDULER_NAME,
1184 build_id::full_version(env!("CARGO_PKG_VERSION"))
1185 );
1186 return Ok(());
1187 }
1188
1189 if opts.help_stats {
1190 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1191 return Ok(());
1192 }
1193
1194 let loglevel = simplelog::LevelFilter::Info;
1195
1196 let mut lcfg = simplelog::ConfigBuilder::new();
1197 lcfg.set_time_offset_to_local()
1198 .expect("Failed to set local time offset")
1199 .set_time_level(simplelog::LevelFilter::Error)
1200 .set_location_level(simplelog::LevelFilter::Off)
1201 .set_target_level(simplelog::LevelFilter::Off)
1202 .set_thread_level(simplelog::LevelFilter::Off);
1203 simplelog::TermLogger::init(
1204 loglevel,
1205 lcfg.build(),
1206 simplelog::TerminalMode::Stderr,
1207 simplelog::ColorChoice::Auto,
1208 )?;
1209
1210 let shutdown = Arc::new(AtomicBool::new(false));
1211 let shutdown_clone = shutdown.clone();
1212 ctrlc::set_handler(move || {
1213 shutdown_clone.store(true, Ordering::Relaxed);
1214 })
1215 .context("Error setting Ctrl-C handler")?;
1216
1217 if let Some(intv) = opts.monitor.or(opts.stats) {
1218 let shutdown_copy = shutdown.clone();
1219 let jh = std::thread::spawn(move || {
1220 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
1221 Ok(_) => {
1222 debug!("stats monitor thread finished successfully")
1223 }
1224 Err(error_object) => {
1225 warn!(
1226 "stats monitor thread finished because of an error {}",
1227 error_object
1228 )
1229 }
1230 }
1231 });
1232 if opts.monitor.is_some() {
1233 let _ = jh.join();
1234 return Ok(());
1235 }
1236 }
1237
1238 let mut open_object = MaybeUninit::uninit();
1239 loop {
1240 let mut sched = Scheduler::init(&opts, &mut open_object)?;
1241 if !sched.run(shutdown.clone())?.should_restart() {
1242 break;
1243 }
1244 }
1245
1246 Ok(())
1247}