A latency-optimized Linux CPU scheduler built on sched_ext. Keeps interactive tasks (UI, audio, input handling) feeling snappy by separating them from batch and background work, with memory/I/O pressure awareness, thermal throttling, and live TUI + GUI monitoring.
Developed by Aniketh T S
Tasks are classified into three dispatch tiers:
| Tier | Slice | CPU freq | DSQ |
|---|---|---|---|
| rt-like | 2 ms | 100% | Global, checked first |
| interactive | 5 ms (10 ms burst) | 100% | Per-CPU, vtime-ordered |
| batch | 20 ms | 50% (adaptive) | Global, vtime-ordered |
A task is interactive if its nice value is ≤ 0 (configurable), or after a few wakeup cycles it spends more than 50% of wall time sleeping — the fingerprint of I/O-bound, latency-sensitive work. The ratio is tracked with an EWMA so it adapts as workloads change. Kernel threads always go to batch.
A task is rt-like (ultra-interactive) if its nice value is ≤ −10 (configurable via --nice-rt-max). These tasks get a dedicated global DSQ checked before all per-CPU queues, so any idle CPU picks them up immediately.
Low-weight cgroup tasks (weight < 50) are forced to batch regardless of sleep ratio, catching background services that happen to sleep often.
- Per-CPU interactive DSQs — each CPU owns an interactive DSQ. Wakeup tasks kick their target CPU with
SCX_KICK_PREEMPTand wake up to 4 nearby idle CPUs to steal the task immediately (push-based load balancing). - Vtime ordering — both interactive and batch DSQs are vtime-ordered. Interactive:
vtime = now − (100−util) × 0.5 ms; tasks that sleep more get priority within their CPU's queue. Batch: tasks accumulate vruntime as CPU time consumed; a 100 ms lag window prevents starvation. - Burst detection — interactive tasks that consistently saturate their slice (util ≥ 90%) earn up to 3 burst credits; each credit grants a 10 ms slice instead of 5 ms, giving transient CPU-hungry phases (JIT compilation, video decode) extra headroom without misclassifying the task.
- Wakeup boost — any task waking from sleep > 50 ms gets a one-shot interactive dispatch. I/O-bound batch tasks (low utilisation) also get this boost after just 5 ms sleep.
- Work stealing — idle CPUs steal from busy CPUs in three passes: same LLC → same NUMA node / different LLC → cross-NUMA. Cache-topology maps (
cpu_llc,cpu_node) are populated from sysfs at startup; transparent no-op on single-socket machines.
- Interactive and rt-like tasks run at 100% CPU freq.
- Batch tasks run at a configurable base frequency (default 50%).
- The base scales up linearly as the batch queue depth grows (up to 100% at 16+ queued tasks).
- Hysteresis:
scx_bpf_cpuperf_setis skipped unless the new value differs by > 5%, avoiding micro-adjustments on stable workloads.
- Tasks performing direct memory reclaim (
PF_MEMALLOC) are immediately forced to the front of the batch vtime queue. - A per-task slice-utilisation EWMA tracks actual CPU usage. Tasks with EWMA < 40% are memory-stall-bound and capped at batch CPU frequency.
- PSI memory pressure (
--mem-pressure-pct): when system-wide memory stall exceeds the threshold, batch CPU freq is reduced proportionally — from 75% of base at the threshold down to 25% at 3× the threshold. A 3-sample hysteresis prevents oscillation. - PSI I/O pressure (
--io-pressure-pct): same graduated throttle applied to I/O stall. Both sources are combined; the most restrictive wins. - Thermal throttling (
--thermal-limit): reads CPU package temperature from sysfs; applies the same graduated throttle when temperature exceeds the configured limit.
Short runs (< 300 µs) while still runnable are counted as suspected RT preemptions (nr_rt_preempt), visible in the TUI and JSON output.
The BPF struct_ops link is tied to the userspace process file descriptor. If scx_quicksched exits or crashes, the kernel automatically reverts all tasks to CFS. timeout_ms = 30000 provides an additional safeguard.
| Kernel | Status |
|---|---|
| < 6.12 | Not supported (no sched_ext) |
| 6.12 – 6.16 | Fully supported |
| 6.17+ | Supported |
- Linux kernel ≥ 6.12 with
CONFIG_SCHED_CLASS_EXT=y clang,gcc,bpftool,libbpf,ncurses
Install all dependencies automatically:
sudo scripts/install-deps.shOr manually:
| Distro | Command |
|---|---|
| Ubuntu / Debian | sudo apt install clang gcc libbpf-dev libncurses-dev linux-tools-$(uname -r) |
| Fedora | sudo dnf install clang gcc libbpf-devel ncurses-devel bpftool |
| RHEL / AlmaLinux | sudo dnf install epel-release && sudo dnf install clang gcc libbpf-devel ncurses-devel bpftool |
| Arch Linux | sudo pacman -S clang gcc libbpf ncurses bpf |
| openSUSE | sudo zypper install clang gcc libbpf-devel ncurses-devel bpftool |
makemake clean # remove build artefacts, preserve vmlinux.h cache
make clean-full # remove everything including the vmlinux.h cachesudo make install # installs to /usr/local/bin
sudo make install PREFIX=/usr # installs to /usr/bin
sudo make uninstallInstalls scx_quicksched, qs, quicksched-gui, the man page, and the systemd unit file.
sudo ./scx_quickschedPress q to quit, c to open the live settings panel.
qs # start in background (no TUI), logs to /var/log/scx_quicksched.log
qs --status # show PID and uptime
qs stop # stop the background instance
qs [flags...] # pass flags through to interactive TUI modepython3 gui/quicksched-gui # or: quicksched-gui (after make install)Requires python3-gi gir1.2-gtk-4.0:
sudo apt install python3-gi gir1.2-gtk-4.0Shows live wakeup latency sparklines, PSI pressure, dispatch mix bar, and all tunable settings with Gaming / Desktop / Server / Battery presets.
sudo systemctl enable --now scx_quicksched
sudo systemctl status scx_quickschedThe unit restarts automatically on failure. It will not start on kernels that do not expose /sys/kernel/sched_ext.
sudo ./scx_quicksched --jsonEmits one JSON line per stats interval to stdout. Fields include interactive, batch, rt_like, burst, rt_preempt, lat_p50_us, lat_p99_us, psi_mem_some, psi_io_some, cpu_temp_c, and more.
sudo ./scx_quicksched --dry-runLoads and JIT-compiles the BPF object and exits without attaching — useful for CI and pre-flight checks.
-i --interactive-slice-us N interactive slice in µs (default: 5000)
-b --batch-slice-us N batch slice in µs (default: 20000)
-n --nice-interactive-max N nice threshold for interactive (default: 0)
--nice-rt-max N nice threshold for rt-like DSQ (default: -10)
-p --interactive-sleep-pct N min sleep % for interactive class (default: 50)
--batch-cpuperf-pct N batch CPU freq % (default: 50)
--no-cpuperf disable CPU freq scaling
--mem-pressure-pct N throttle batch when PSI mem > N% (default: 0=off)
--io-pressure-pct N throttle batch when PSI io > N% (default: 0=off)
--thermal-limit N throttle batch when CPU temp > N°C (default: 0=off)
--no-tui plain text output
--json machine-readable JSON output
--dry-run load and verify BPF; exit without attaching
--slo-us N alert when p99 latency exceeds N µs (default: 0=off)
--pidfile PATH write PID to file (used by qs)
-s --stats-interval N stats interval in seconds (default: 1, 0=off)
-v --verbose verbose libbpf output
-V --version print version and exit
-h --help
Refreshes every stats interval. Press c to open the settings panel, q to quit.
| Row | Description |
|---|---|
rt-like |
Ultra-interactive dispatches (nice ≤ nice-rt-max) |
interactive |
Normal interactive dispatches |
batch |
Batch dispatches |
idle-fast |
Tasks dispatched directly to idle CPU in select_cpu |
stolen |
Work-stolen from another CPU's interactive DSQ |
woke-boost |
Batch tasks granted one-shot interactive dispatch on wake |
burst |
Interactive tasks dispatched with 2× burst slice |
rt-preempt |
Suspected RT preemptions (short run, still runnable) |
| Row | Description |
|---|---|
mem psi |
Memory stall some/full avg10 — [throttled] when active |
io psi |
I/O stall some/full avg10 |
CPU N°C |
CPU package temperature (header, colour-coded) |
memalloc |
PF_MEMALLOC tasks demoted to batch |
preempted |
Tasks stopped before exhausting their slice |
mem-stall |
Dispatches where slice_util_ewma < 40% (RAM-bound) |
Per-CPU dispatch rate bars; idle CPUs labelled; header shows active/total.
avg / p50 / p99 and a log-scale histogram (< 1 µs → ~256 ms).
| Setting | Presets |
|---|---|
| mem-pressure-pct | Off / 5% / 10% / 20% / 30% |
| io-pressure-pct | Off / 5% / 10% / 20% / 30% |
| nice-rt-max | Default / −5 / −10 / −15 / Disabled |
| batch-cpuperf-pct | Default / 25% / 50% / 75% / 100% |
Navigate with UP/DN, adjust with LT/RT, close with c.
| Preset | mem-pct | io-pct | nice-rt-max | batch-cpuperf |
|---|---|---|---|---|
| Gaming | 10% | 5% | −5 | 25% |
| Desktop | Off | Off | −10 | 50% |
| Server | 20% | 15% | −15 | 75% |
| Battery | 5% | 5% | −15 | 25% |
src/
scx_quicksched.c userspace: TUI, settings panel, JSON output, BPF lifecycle
bpf/
scx_quicksched.bpf.c BPF scheduler (classification, vtime dispatch, LLC steal)
scx_quicksched.h shared types, constants, and map definitions
gui/
quicksched-gui GTK4 Python GUI (sparklines, presets, start/stop)
scripts/
install-deps.sh auto-detects distro and installs build dependencies
man/
scx_quicksched.1 man page
packaging/
scx_quicksched.service systemd unit file
scx_quicksched.spec RPM spec (Fedora / RHEL / openSUSE)
PKGBUILD Arch Linux package build script
qs background manager (start / --status / stop)
.github/workflows/
ci.yml CI: Ubuntu, Fedora, Arch, sanitizers, clang-format
Makefile