- C 63.9%
- Nix 29.5%
- Makefile 6.6%
Add BPF maps and cascade lookup for per-PID, per-comm, and per-cgroup
scheduling overrides. Comm/PID/cgroup rules all take priority over the
global profile in that order.
BPF (scx_littlebig.bpf.c / scx_littlebig.h):
- struct task_policy { profile, flags } in shared header
- pid_overrides (hash, u32 → task_policy, 4096 entries)
- process_overrides (hash, char[16] → task_policy, 256 entries)
- cgroup_policies (hash, u64 → task_policy, 256 entries)
- get_task_profile_p(): PID → comm → cgroupv2-id → global cascade,
called from both select_cpu and enqueue
Userspace (src/scx_littlebig.c):
- pin_maps() / unpin_maps(): pin override maps to /sys/fs/bpf/scx_littlebig/
so CLI subcommands can open them without running the daemon
- open_pinned_map(): bpf_obj_get() helper for subcommands
- cmd_rule(): rule add/list/remove subcommands
- cmd_pid(): pid <pid> --profile <name>
- cmd_cgroup(): cgroup <path> --profile <name> (reads cgroup.id)
- load_preset_file() / load_preset(): parse .conf format and populate
process_overrides; -P <file> flag loads a preset at daemon start
Presets:
- presets/gaming.conf — performance for cs2, gamescope, wine, steam …
- presets/compile.conf — all-cores for cc1, rustc, cargo, make, ninja …
- presets/background.conf — powersave for baloo, tracker-miner, journald …
Tests (flake.nix):
- TEST 4a: comm rule add — scx_test_task → powersave lands on E-cores
- TEST 4b: rule list shows the active comm rule
- TEST 4c: rule remove restores P-core placement
- TEST 4d: PID override — performance pin in powersave mode
- TEST 4e: -P preset file populates comm rules at startup
- All five subtests pass in the NixOS VM (verified)
|
||
|---|---|---|
| bpf | ||
| presets | ||
| src | ||
| .gitignore | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| Makefile | ||
| README.md | ||
scx_littlebig
A sched_ext BPF scheduler for heterogeneous CPU architectures — Intel P/E-core (Alder Lake+) and ARM big.LITTLE — that optimises power consumption and latency through topology-aware task placement and user-configurable scheduling profiles.
Status: Phase 2 — per-process overrides implemented. Add per-comm, per-PID, and per-cgroup scheduling rules at runtime without reloading the scheduler. Preset files for gaming, compile, and background workloads included.
Why scx_littlebig?
Modern CPUs ship with non-uniform cores:
- Intel 12th gen+ mixes high-frequency P-cores with power-efficient E-cores.
- ARM big.LITTLE (Cortex-A7x + A5x, Snapdragon X, Apple Silicon) has been doing this for years.
The default Linux scheduler (EEVDF) has some topology awareness but offers no user-facing controls for power/performance trade-offs and cannot adapt per-application. scx_littlebig uses the sched_ext framework (Linux ≥ 6.11) to implement a fully topology-aware scheduler with:
- Four switchable profiles — powersave, balanced, performance, all-cores — changeable at runtime without rebooting.
- Per-process overrides — pin specific apps (games, compilers) to a different profile.
- Automatic workload detection — classifies tasks by burst pattern and adjusts placement without configuration.
- Safe fallback — sched_ext guarantees a return to the default kernel scheduler on any error or
Ctrl-C.
Requirements
Runtime
The machine running the scheduler needs:
| Requirement | Minimum |
|---|---|
| Linux kernel | 6.11+ with CONFIG_SCHED_CLASS_EXT=y |
| BTF | /sys/kernel/btf/vmlinux must exist (CONFIG_DEBUG_INFO_BTF=y) |
| Privileges | CAP_SYS_ADMIN (typically: run as root) |
Check your kernel:
uname -r # should be >= 6.11
cat /sys/kernel/sched_ext/state # should exist
ls /sys/kernel/btf/vmlinux # BTF must be available
Ubuntu 24.10, Fedora 41+, and NixOS with linuxPackages_latest all ship sched_ext-enabled kernels.
Build
| Tool | Minimum |
|---|---|
| clang | 16+ (for BPF CO-RE compilation) |
| bpftool | Matching kernel version |
| libbpf | 1.3+ |
| Kernel sched_ext headers | tools/sched_ext/include from kernel source |
| vmlinux with BTF | For generating vmlinux.h at build time |
With Nix all of this is provided automatically — see below.
Building
With Nix (recommended)
The repo ships a flake.nix that pins all build dependencies — kernel with BTF, bpftool, libbpf, clang — so the build is fully reproducible.
nix build
The binary is available at result/bin/scx_littlebig.
For an interactive dev shell (sets VMLINUX_BTF, KERNEL_SCX_INCLUDE, CLANG automatically):
nix develop
make
Without Nix
Requires clang ≥ 16, bpftool, libbpf-dev, and kernel sched_ext headers:
make VMLINUX_BTF=/sys/kernel/btf/vmlinux \
KERNEL_SCX_INCLUDE=/path/to/linux/tools/sched_ext/include
Quick start
# Load with default profile (balanced)
sudo ./result/bin/scx_littlebig
# Powersave — prefer E-cores, spill to P-cores only when saturated
sudo ./result/bin/scx_littlebig -p powersave
# Performance — prefer P-cores for everything
sudo ./result/bin/scx_littlebig -p performance
# All-cores — equal weight, single shared queue (good for compiling)
sudo ./result/bin/scx_littlebig -p all-cores
# Load with gaming rules pre-configured (cs2, gamescope, ... → performance)
sudo ./result/bin/scx_littlebig -P gaming
# Verbose: print per-CPU topology table on startup
sudo ./result/bin/scx_littlebig -v
# Switch profile at runtime without reloading
sudo kill -USR1 $(pidof scx_littlebig)
# Stop: Ctrl-C or SIGTERM — returns to default kernel scheduler
Per-process overrides (Phase 2)
While the daemon is running, control commands update BPF maps directly via
pinned file descriptors under /sys/fs/bpf/scx_littlebig/ — no reload needed.
# Add a per-comm rule: cs2 always uses the performance profile
sudo scx_littlebig rule add performance cs2 gamescope
# List all active comm rules
sudo scx_littlebig rule list
# Remove a rule
sudo scx_littlebig rule remove cs2
# Pin a specific PID to performance regardless of global profile
sudo scx_littlebig pid 12345 --profile performance
# Apply a policy to an entire cgroup (cgroupv2)
sudo scx_littlebig cgroup /sys/fs/cgroup/user.slice --profile powersave
Priority cascade
When selecting a CPU for a task the scheduler walks:
1. pid_overrides keyed by tgid ← highest priority
2. process_overrides keyed by task->comm
3. cgroup_policies keyed by cgroupv2 ID
4. global_profile ← lowest priority
The first match wins; unmatched tasks follow the global profile.
Preset files
Three bundled presets are installed to share/scx_littlebig/presets/:
| Preset | Profile | Processes |
|---|---|---|
gaming |
performance |
cs2, gamescope, mangohud, steam, lutris, wine, proton |
compile |
all-cores |
cc1, cc1plus, rustc, cargo, make, ninja, javac, ld |
background |
powersave |
tracker-miner, baloo, packagekitd, journald |
Load at daemon startup: sudo scx_littlebig -P gaming
You can also write your own .conf files:
# myapp.conf
profile=performance
myapp
myapp-worker
Then: sudo scx_littlebig -P /path/to/myapp.conf
On a hybrid CPU you will see something like:
Topology: 16 CPUs — 8 P-core(s), 8 E-core(s)
scx_littlebig loaded. Profile: balanced (weighted-vtime). Press Ctrl-C to stop.
[balanced] local=1423 pcores=612 ecores=260 shared=0
[balanced] local=2891 pcores=987 ecores=417 shared=0
Profile switched to: powersave
[powersave] local=3102 pcores=12 ecores=1834 shared=0
^C
Profiles
| Profile | P-core policy | E-core policy | Use case |
|---|---|---|---|
balanced |
Interactive tasks | Background/batch | Default daily driver |
powersave |
Overflow only | All tasks | Laptop on battery |
performance |
All tasks | Overflow only | Sustained workloads |
all-cores |
Equal weight | Equal weight | Compiling, rendering |
Architecture
┌─────────────────────────────────────────┐
│ User Space │
│ │
│ scx_littlebig [-f] [-v] [-q] │
│ │ │
│ 1. detect_topology() │
│ read /sys/devices/system/cpu/ │
│ → cpu_capacity per CPU │
│ → classify P-core / E-core │
│ │ │
│ 2. populate core_topology BPF map │
│ │ │
│ 3. SCX_OPS_LOAD / ATTACH │
│ │ │
│ 4. stats loop (sleep 1s, read maps) │
├───────┼──────────────────────────────────┤
│ │ Kernel / BPF │
│ ┌────▼─────────────────────────────┐ │
│ │ scx_littlebig.bpf.c │ │
│ │ │ │
│ │ select_cpu() → idle CPU search │ │
│ │ enqueue() → SHARED_DSQ │ │
│ │ dispatch() → local DSQ pull │ │
│ │ running() → vtime + stats │ │
│ │ stopping() → vtime charge │ │
│ │ enable() → init task vtime │ │
│ │ disable() → cleanup stats │ │
│ └──────────────────────────────────┘ │
│ │
│ BPF Maps: │
│ core_topology [array] cpu → type │
│ task_stats [hash] pid → stats │
│ stats [percpu] counters │
└─────────────────────────────────────────┘
Topology detection
On startup, the userspace binary reads:
/sys/devices/system/cpu/cpuN/cpu_capacity— populated by the kernel on heterogeneous systems for both Intel HFI and ARM big.LITTLE./sys/devices/system/cpu/cpuN/cache/indexM/— to determine LLC sharing domains./sys/devices/system/cpu/cpuN/node*symlinks — for NUMA node assignment.
CPUs with cpu_capacity ≥ 80% of the system maximum are classified as P-cores; those below as E-cores. On uniform systems (all capacities equal or the file absent) all CPUs are marked CORE_TYPE_UNKNOWN and the scheduler behaves like a plain global weighted-vtime scheduler.
The results are written into the core_topology BPF array map before the scheduler attaches, so the BPF program can use them without sysfs access.
Scheduling (Phase 0)
Phase 0 implements the same logic as scx_simple with topology data pre-loaded:
- select_cpu:
scx_bpf_select_cpu_dfl()finds an idle CPU respecting affinity and LLC locality. If found → dispatch to itsSCX_DSQ_LOCAL(zero-copy fast path). - enqueue: tasks not dispatched in
select_cpugo toSHARED_DSQusing weighted virtual time. - dispatch: each CPU pulls from
SHARED_DSQwhen its local queue is empty.
Phase 1 will replace select_cpu with topology-aware core-type selection driven by the active profile.
Roadmap
| Phase | Status | Description |
|---|---|---|
| 0 | Done | Skeleton, topology detection, global weighted-vtime scheduling |
| 1 | Done | Four switchable profiles (powersave / balanced / performance / all-cores) |
| 2 | Done | Per-process overrides (by comm, PID, cgroup); preset configs; pinned BPF maps |
| 3 | Planned | Automatic workload classification (latency-sensitive, throughput, background) |
| 4 | Planned | Polish, packaging (Fedora COPR, AUR, NixOS flake), TUI dashboard |
| 5 | Stretch | GPU-aware CPU placement via DRM fdinfo |
Testing
Boots linuxPackages_latest (which has CONFIG_SCHED_CLASS_EXT and
CONFIG_DEBUG_INFO_BTF) in a QEMU VM, loads the scheduler, runs a brief stress
workload, and verifies /sys/kernel/sched_ext/state transitions — no root on the
host required.
nix build .#checks.x86_64-linux.vm -L
The test is defined in flake.nix under checks.x86_64-linux.vm using the
NixOS testing library.
File structure
scx_littlebig/
├── Makefile Build system
├── README.md This file
├── flake.nix Nix build + dev shell + VM integration tests
├── bpf/
│ ├── scx_littlebig.bpf.c BPF scheduler (profiles + per-task cascade)
│ └── scx_littlebig.h Shared types (BPF ↔ userspace)
├── src/
│ └── scx_littlebig.c Userspace daemon + CLI subcommands
└── presets/
├── gaming.conf performance profile: cs2, gamescope, …
├── compile.conf all-cores profile: cc1, rustc, cargo, …
└── background.conf powersave profile: tracker-miner, baloo, …
References
- sched_ext kernel documentation
- sched-ext/scx schedulers repository
- scx_lavd — heterogeneous-aware gaming scheduler (primary reference for P/E core handling)
- scx_rusty — LLC/NUMA-aware multi-domain scheduler
- LWN: Controlling the CPU scheduler with BPF
- Intel Thread Director / HFI
License
GPL-2.0-only — matching the sched_ext framework and the Linux kernel.