The Energy Model EM Framework
The Energy Model (EM) framework is the kernel’s shared power price-list. It sits as an abstraction layer between drivers that know how much power a device draws at each performance level (from device-tree data, firmware, or measurement) and the subsystems that want that information to make energy-aware decisions — chiefly the scheduler’s Energy-Aware Scheduling (EAS) and the thermal Intelligent Power Allocator. Rather than have each consumer re-implement support for every possible power-data source, the EM standardizes the format: a per-performance-domain table of
<frequency, capacity, power>triples, plus a precomputedcostcoefficient used in the scheduler’s hot path (energy-model.rst §1). The framework lives inkernel/power/energy_model.cand is gated byCONFIG_ENERGY_MODEL. This note walks the two core structures (struct em_perf_domainandstruct em_perf_state), how a domain is registered (em_dev_register_perf_domain()plus the driver’s->active_powercallback, or DT/cpufreq auto-registration), how the cost is precomputed, the two consumers and their differing needs, and the crucial distinction between real (micro-Watt) and artificial (abstract-scale) power numbers.
This note is pinned to Linux 6.12 LTS (2024-11-17). The two structures, the cost formula, and the registration API were re-verified against 6.18 LTS (2025-11-30) and are byte-for-byte identical across the two LTS lines.
Mental Model
Picture the Energy Model as a lookup table per CPU cluster (per performance domain, in the jargon — a group of CPUs whose frequency scales together, usually one cpufreq policy). Each row of the table is one operating performance point (OPP — see Dynamic Voltage and Frequency Scaling DVFS): a frequency, the capacity (throughput) the CPU delivers there, and the power it draws there in micro-Watts. A consumer that wants to know “if this cluster runs at the frequency needed to satisfy load L, how much power does it burn?” maps L to a row and reads the power. The framework’s one piece of cleverness is that it also stores, per row, a derived cost coefficient that lets the scheduler compute a whole domain’s energy with a single multiply, avoiding a division on every wakeup.
The framework is firmware-neutral by construction. The same table abstraction serves an Arm board that gets its power numbers from device-tree opp-microwatt properties, an Arm SCMI platform that gets them from firmware, and an x86 part that estimates them with a math formula. Drivers supply a callback; the framework calls it once per performance state at registration and freezes the table.
flowchart TB subgraph SRC["Power-data sources (driver supplies a callback)"] DT["Device Tree<br/>opp-microwatt"] FW["Firmware<br/>(SCMI, etc.)"] EST["Math estimate<br/>C * V^2 * f"] end DT --> DRV["cpufreq driver / OPP core"] FW --> DRV EST --> DRV DRV -->|"em_dev_register_perf_domain()<br/>->active_power() per state"| EMF["Energy Model framework<br/>kernel/power/energy_model.c"] EMF --> TBL["struct em_perf_domain<br/>+ em_perf_state[]<br/>(freq, performance, power, cost)"] TBL -->|"em_cpu_energy()<br/>cost * sum_util"| EAS["Scheduler (EAS)<br/>needs only relative energy"] TBL -->|"em_perf_state.power<br/>(micro-Watts)"| IPA["Thermal IPA / power_allocator<br/>needs REAL micro-Watts"]
Figure 1 — the Energy Model as a hub between power sources and consumers. What it shows: drivers translate whatever source they have (DT, firmware, a formula) into <freq, power> tuples via the ->active_power callback; the framework builds one frozen table per performance domain, precomputing capacity and a cost coefficient. Two consumers read it differently — EAS reads the cost-based em_cpu_energy() and only needs relative numbers, while the thermal IPA reads em_perf_state.power and needs real micro-Watts. The insight to take: the EM unifies the supply side (many sources, one format) and serves two demand sides whose accuracy requirements differ, which is exactly why the “real vs. artificial” power-number distinction matters.
The Two Core Structures
Everything in the EM hangs off two structs in include/linux/energy_model.h (energy_model.h, v6.12).
A performance state is one row of the table:
struct em_perf_state {
unsigned long performance; /* CPU capacity at this frequency (0..1024-ish) */
unsigned long frequency; /* frequency in kHz, for consistency with cpufreq */
unsigned long power; /* power consumed at this level (micro-Watts), may be static+dynamic */
unsigned long cost; /* precomputed: power * max_freq / freq (see below) */
unsigned long flags; /* e.g. EM_PERF_STATE_INEFFICIENT */
};Walking the fields: frequency is the kHz value (matching cpufreq’s units); performance is the capacity the CPU delivers at that frequency, on the scheduler’s 0–1024 scale; power is the active power draw in micro-Watts (it may be a total — static leakage plus dynamic — depending on how the driver sourced it); cost is a derived coefficient (next section); and flags carries EM_PERF_STATE_INEFFICIENT, set when a higher-frequency state in the same domain has a lower-or-equal cost, making this state pointless to ever use (energy_model.h, v6.12).
A performance domain owns the table:
struct em_perf_domain {
struct em_perf_table __rcu *em_table; /* the runtime-modifiable state[] table */
int nr_perf_states; /* number of rows */
unsigned long flags; /* EM_PERF_DOMAIN_MICROWATTS / _ARTIFICIAL / _SKIP_INEFFICIENCIES */
unsigned long cpus[]; /* cpumask of the domain's CPUs (CPU devices only) */
};The states themselves live in a separate struct em_perf_table — { rcu, kref, em_perf_state state[] } — so the table can be swapped at runtime under RCU while readers hold the old copy. The kref reference-counts owners; when the last owner drops it, an RCU callback frees the old table (energy_model.c em_release_table_kref, v6.12). The cpus[] cumask is embedded for cache locality during the scheduler’s energy calculations. The three flags are the key metadata: EM_PERF_DOMAIN_MICROWATTS (power values are real µW), EM_PERF_DOMAIN_ARTIFICIAL (power values are abstract, not µW), and EM_PERF_DOMAIN_SKIP_INEFFICIENCIES (set when inefficient states should be skipped) (energy_model.h, v6.12).
There is a hard size limit: EM_MAX_NUM_CPUS is 4096 on 64-bit and only 16 on 32-bit, and EM_MAX_POWER is 64,000,000 µW (64 W) — both safety nets to prevent the cost * sum_util and total-power multiplications from overflowing on 32-bit machines (energy_model.h, v6.12).
Registration: Three Ways In
A driver registers a performance domain with one API (energy-model.rst §2.2; energy_model.c em_dev_register_perf_domain, v6.12):
int em_dev_register_perf_domain(struct device *dev, unsigned int nr_states,
struct em_data_callback *cb, cpumask_t *cpus,
bool microwatts);The driver supplies an ->active_power(dev, *power, *freq) callback. The framework calls it once per performance state, and the callback must ceil the requested frequency to the lowest real OPP at or above it and fill in *power and *freq for that OPP. The framework iterates this to build the table, validates that frequencies strictly increase and that each power is in (0, EM_MAX_POWER], then calls em_init_performance() to fill the performance (capacity) column and em_compute_costs() to fill the cost column (energy_model.c em_create_perf_table, v6.12). For CPU devices the cpus mask is mandatory; for non-CPU devices it must be NULL. Registration is serialized by a mutex (em_pd_mutex) so the driver callback may sleep, and re-registering a device that already has an EM returns -EEXIST (energy_model.c, v6.12).
The documentation distinguishes four registration styles on top of that one API (energy-model.rst §2.2):
- Advanced EM — the driver provides a precise per-state power model via
->active_power, setmicrowatts = true. Preferred when static (leakage) power matters, because the driver can reflect real measurements rather than a formula. - DT-based EM — power values come from the device tree’s
operating-points-v2table, each OPP extended with anopp-microwattproperty carrying real (static + dynamic) micro-Watts, typically from measurement. Plugged in through the OPP framework. - Artificial EM — for platforms that know only relative efficiency between CPU types, not absolute power. The driver sets
microwatts = falseand provides a->get_cost()callback supplying thecostvalues directly. The framework flags the domainEM_PERF_DOMAIN_ARTIFICIAL. This is the escape hatch when even an abstract power model is hard to fit into the µW range. - Simple EM — registered via the helper
cpufreq_register_em_with_opp(), which synthesizes power from the textbook dynamic-power formulaPower = C × V² × f(capacitance times voltage-squared times frequency). Cheap, but it ignores static leakage, so it can misrepresent real silicon.
For CPUs, the common path is automatic: a cpufreq driver implements the cpufreq_driver::register_em() callback, and the cpufreq core invokes it at the right point in policy setup, so the EM appears without explicit board code (energy-model.rst §3.1). The example driver from the docs shows the pattern:
static void foo_cpufreq_register_em(struct cpufreq_policy *policy)
{
struct em_data_callback em_cb = EM_DATA_CB(est_power); /* wraps ->active_power */
struct device *cpu_dev = get_cpu_device(cpumask_first(policy->cpus));
int nr_opp = foo_get_nr_opp(policy);
/* last arg 'true' = power values are real micro-Watts */
em_dev_register_perf_domain(cpu_dev, nr_opp, &em_cb, policy->cpus, true);
}The microwatts flag is load-bearing: the framework rejects a registration with microwatts = false and no ->get_cost callback with “EM: only supports uW power values” — you must either supply real µW or explicitly declare an artificial model (energy_model.c lines 610–624, v6.12).
How the Cost Coefficient Is Precomputed
The cost column is the EM’s optimization for the scheduler’s hot path. The header comment defines it as power × max_frequency / frequency, but the actual code computes the mathematically-equivalent power × 10 / performance (with the ×10 only to add precision before the integer division) (energy_model.c em_compute_costs, v6.12):
/* in em_compute_costs(), iterating states from highest down to lowest */
power_res = table[i].power * 10; /* boost resolution before integer divide */
cost = power_res / table[i].performance;
table[i].cost = cost;Walking it: table[i].power is the µW at state i; table[i].performance is that state’s capacity. Because performance is proportional to frequency, dividing power by performance is equivalent to dividing power by frequency and scaling — so cost ends up proportional to power / frequency × max_freq, a constant per state. The reason this matters is the consumer side: the scheduler’s em_cpu_energy() then computes a whole performance domain’s energy as simply cost × Σ util — one multiply, no per-CPU division. The full algebra (why cost × sum_util equals the domain energy) is derived in Energy-Aware Scheduling EAS; the point here is that the framework front-loads the division at registration so the runtime path is cheap.
The same loop sets the inefficiency flag. Iterating from the highest state down, it tracks prev_cost; if a (lower-frequency) state’s cost is not lower than a higher-frequency state’s cost, that state is marked EM_PERF_STATE_INEFFICIENT — there is no reason to run there when a faster state costs the same or less (energy_model.c lines 267–273, v6.12). The framework then pushes these inefficient frequencies into cpufreq via cpufreq_table_set_inefficient() and sets EM_PERF_DOMAIN_SKIP_INEFFICIENCIES on the domain so both layers skip them (energy_model.c em_cpufreq_update_efficiencies, v6.12).
Runtime Modification — Static Power and Chip Binning
Originally the EM was a single static table for the system’s whole runtime. Modern kernels (the 6.x line) support runtime modification to reflect things like temperature-dependent leakage and per-chip voltage binning (energy-model.rst §1, §2.4). A driver wanting to update the table allocates a fresh one with em_table_alloc(), fills the new em_perf_state rows (frequencies copied from the existing table), recomputes costs with em_dev_compute_costs(), and swaps it in with em_dev_update_perf_domain(), which does an RCU pointer swap under em_pd_mutex and drops a kref on the old table (energy_model.c em_dev_update_perf_domain, v6.12). Readers (EAS, thermal) access the live table inside an RCU read section via em_perf_state_from_pd(), so the swap never tears a reader. Because updates take a mutex, the updating driver must run in sleepable context.
A specific built-in updater is em_dev_update_chip_binning(): after the OPP framework learns a chip’s actual voltages (chip binning — silicon from the same line runs at slightly different voltages), this re-reads power via dev_pm_opp_calc_power() and rebuilds the table so the EM reflects the specific part (energy_model.c em_dev_update_chip_binning, v6.12). Separately, em_check_capacity_update() runs after boot to fix up the performance column once the final CPU capacities are known (at registration time arch_scale_cpu_capacity() may not yet be calibrated) (energy_model.c em_check_capacity_update, v6.12).
The Two Consumers — and Why Real vs. Artificial Matters
The EM has two principal in-tree clients, and the difference in what they need is the single most important practical fact about the framework.
EAS (scheduler) reads the EM through the inline em_cpu_energy(pd, max_util, sum_util, allowed_cpu_cap), which returns ps->cost × sum_util after mapping max_util to a performance state (energy_model.h, v6.12). EAS only ever compares energy estimates between candidate CPUs, so it needs the numbers to be internally consistent, not physically accurate. Micro-Watts or an “abstract scale” both work for EAS — which is exactly why the artificial-EM escape hatch exists at all (sched-energy.rst §6.2).
The thermal Intelligent Power Allocator (IPA / power_allocator governor) is different. IPA’s job is to keep a thermal zone at a target temperature by budgeting real power across cooling devices, so it needs power expressed in real milli/micro-Watts (or at least a single consistent abstract scale across all cooling devices in the zone) — it is estimating power actually used, not breaking a relative tie (power_allocator.rst “Energy Model requirements”; energy-model.rst §1). The IPA’s CPU power actor, cpufreq_cooling, reads em_perf_state.power directly (converting µW → mW) to map between a frequency and its power draw (cpufreq_cooling.c cpu_power_to_freq, v6.12). And it explicitly refuses an artificial EM: the power-cooling registration bails out if em_is_artificial(em) is true, because abstract-scale numbers are meaningless for a power budget (cpufreq_cooling.c, v6.12).
So the microwatts flag and the EM_PERF_DOMAIN_ARTIFICIAL flag are not bookkeeping — they tell a consumer whether it can trust the numbers. A subsystem that needs real power must check the flag and refuse (or warn) on an artificial model, which is precisely what cpufreq_cooling does. With an abstract scale, deriving real energy in Joules is simply impossible (energy-model.rst §1).
Configuration and Observability
The framework requires CONFIG_ENERGY_MODEL=y. With CONFIG_DEBUG_FS, every registered domain appears under /sys/kernel/debug/energy_model/<device>/, with a cpus file, a flags file, and one sub-directory per performance state exposing frequency, power, cost, performance, and inefficient (energy_model.c em_debug_create_pd, v6.12):
# Inspect the Energy Model the kernel actually built
ls /sys/kernel/debug/energy_model/
# e.g. cpu0 cpu4 (one dir per performance domain's first CPU device)
cat /sys/kernel/debug/energy_model/cpu0/cpus # which CPUs share this PD
cat /sys/kernel/debug/energy_model/cpu0/flags # 0x1=µW, 0x2=skip-inefficient, 0x4=artificial
cat /sys/kernel/debug/energy_model/cpu0/ps:1200000/power # µW at the 1.2 GHz state
cat /sys/kernel/debug/energy_model/cpu0/ps:1200000/cost # precomputed cost coefficientReading the flags value tells you immediately whether the platform registered a real (0x1 MICROWATTS) or artificial (0x4) model — the single most useful diagnostic when EAS or IPA behaves oddly. At registration the kernel also logs EM: created perf domain (and, under sched_debug, the per-domain nr_pstate line from the scheduler side).
Failure Modes and Common Misunderstandings
“Power numbers look like capacities, not Watts.” Likely an artificial EM (flags & 0x4): the values are an abstract efficiency scale, not micro-Watts, and you cannot interpret them as power. Any consumer needing real power (thermal IPA) will refuse this model.
“em_dev_register_perf_domain() returned -EINVAL.” The common causes: a non-increasing frequency from the ->active_power callback, a power value of 0 or above EM_MAX_POWER (64 W), microwatts = false with no ->get_cost callback, or — on a 32-bit kernel — a domain with more than EM_MAX_NUM_CPUS (16) CPUs (energy_model.c, v6.12).
“Different CPUs in one domain disagree on capacity.” Registration enforces that all CPUs of a CPU performance domain share the same arch_scale_cpu_capacity() (same micro-architecture), failing with “EMs CPUs … must have the same capacity” otherwise — the table is shared, so heterogeneous cores must be in separate domains (energy_model.c lines 587–607, v6.12).
“The simple C·V²·f model gives EAS bad placements.” The simple EM ignores static leakage, which on modern nodes is significant. If energy decisions matter, prefer an advanced/DT EM sourced from real measurements (the opp-microwatt route) over the formula (energy-model.rst §2.2).
Alternatives and When to Choose Them
There is no real alternative abstraction in-tree — the EM is the single kernel interface for per-OPP power data, and both EAS and thermal IPA depend on it. The choices are at the sourcing layer: real measured power via DT opp-microwatt (best fidelity), firmware-supplied power (SCMI), the C·V²·f simple model (cheapest, leakage-blind), or an artificial relative-efficiency model (when absolute power is unknown — usable by EAS, not by thermal IPA). A platform that needs both EAS and power-budget thermal control must supply real µW; one that only runs EAS can get away with an artificial model.
Production Notes
On Android SoCs the EM is typically populated from per-board opp-microwatt device-tree values derived from silicon power characterization, giving both EAS and IPA real micro-Watts to work with. The runtime-modification machinery (added to the 6.x line) lets vendor thermal drivers adjust the model as leakage changes with temperature, and em_dev_update_chip_binning() lets the model track per-chip voltage binning — both reflecting a shift the documentation calls out explicitly: from a “single static EM” to “a single EM that can change during runtime according to workload” (energy-model.rst §1). The recurring deployment gotcha is the real-vs-artificial flag: a board that registers an artificial EM (to get EAS working with only relative efficiency data) and then expects the power_allocator thermal governor to budget power will find IPA silently refusing the cooling device, because abstract numbers cannot anchor a Watt budget.
See Also
- Energy-Aware Scheduling EAS — the scheduler consumer that reads the EM via
em_cpu_energy()(cost × sum_util); its sibling note on the PM side - Cooling Devices and Thermal Governors — the thermal Intelligent Power Allocator (IPA) consumer that needs real micro-Watts and refuses artificial EMs
- Dynamic Voltage and Frequency Scaling DVFS — the operating performance points (OPPs) the EM tabulates frequency/voltage/power for
- The schedutil Governor — the governor whose “frequency follows utilization” behavior makes the EM’s util→state mapping valid for EAS
- The cpufreq Subsystem — performance domains map 1-to-1 to cpufreq policies;
cpufreq_driver::register_em()auto-registers the EM - Linux Power Management MOC — §8, the parent map