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 precomputed cost coefficient used in the scheduler’s hot path (energy-model.rst §1). The framework lives in kernel/power/energy_model.c and is gated by CONFIG_ENERGY_MODEL. This note walks the two core structures (struct em_perf_domain and struct em_perf_state), how a domain is registered (em_dev_register_perf_domain() plus the driver’s ->active_power callback, 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, set microwatts = 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-v2 table, each OPP extended with an opp-microwatt property 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 = false and provides a ->get_cost() callback supplying the cost values directly. The framework flags the domain EM_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 formula Power = 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 coefficient

Reading 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