"The scheduler does not ask how important your job is. It asks what you declared."
Two Clusters, Then Three Names
Every Apple silicon chip through the M4 generation carries two kinds of CPU core: performance cores — the wide ones from the previous lesson — and efficiency cores, smaller, slower, and far cheaper to run. macOS reports them as two performance levels: hw.perflevel0 (24 performance cores on an M3 Ultra) and hw.perflevel1 (8 efficiency cores). The M5 generation renamed the ladder. Apple's M5 Pro and Max announcement describes "six of the highest-performing core design, now called super cores" alongside "12 all-new performance cores, optimized for power-efficient, multithreaded workloads", and lists no efficiency cores for the Pro, Max and Ultra. The fleet's M5 laptop confirms the naming from the shell: hw.perflevel0.name is Super (6) and hw.perflevel1.name is Performance (12), with no third level. The base M5 keeps efficiency cores; the pro chips, on this evidence, have a fast tier and an efficient-multithread tier and nothing below it.
How Work Finds a Core
Nothing chooses a core by hand. A thread carries a quality-of-service class — user-interactive, user-initiated, default, utility, background — declared by the code that created it or clamped onto the whole process from outside, and the scheduler uses the class to decide which cluster to prefer, how high to clock it, and who wins when the die is contended. User-interactive and user-initiated work leans hard toward the performance cluster; background work is placed on the efficiency cluster and, importantly, run at a restrained clock so it stays cheap. Threads migrate between clusters as load changes; the class is a preference and a budget, not a pin.
You can see the budget from the shell. taskpolicy -c background clamps a process to the background class; the code block runs the same Python loop under each clamp on office:
| Clamp | 20M-iteration loop | Ratio | Evidence |
|---|---|---|---|
| default | 1.31 s | 1.0x | measured, office, 2026-09-15 |
| utility | 1.26 s | 1.0x | measured |
| maintenance | 7.75 s | 5.9x slower | measured |
| background | 8.81 s | 6.7x slower | measured |
The background loop is not merely on a slower core; a 6.7x ratio is more than the gap between an efficiency core and a performance core. It is a slower core and a restrained clock and a lower claim on the die — the budget, all three parts of it. That is exactly what you want from a job that must never be noticed.
Why a Fleet Runs on This
The household's Macs run dozens of long-lived services — engines, sync jobs, a media worker, a transcoding pipeline — beside the foreground apps someone is actually using. They coexist on one die because those services declare themselves: a launchd job with ProcessType set to Background is scheduled like the background row of the table, and a worker that leases jobs from a control plane runs its heavy loop at utility or background class on purpose. The result is a machine that is both a server and a desk without a second machine. The rule, and the trap, is the same sentence: the scheduler honours what you declared, not what you meant. A background job that forgets to declare itself competes with the user's editor at full clock; a foreground app that spawns work at background class wonders why its progress bar crawls.
One more thing the clusters buy, which matters to inference: while a model decodes on the GPU, the CPU-side loop that feeds it (sampling, tokenization, the Python driver) is one thread at user-initiated class on one performance core, and everything else on the machine has the rest of the performance cluster (twenty-three cores on office, seven on an eight-core Max) and the whole efficiency cluster to live on. The GPU and the CPU share a memory pool, not a scheduler.