Skip to content

Add support for Windows Energy Meter Interface (EMI) - #1263

Open
benoit-cty wants to merge 11 commits into
masterfrom
windows_support
Open

Add support for Windows Energy Meter Interface (EMI)#1263
benoit-cty wants to merge 11 commits into
masterfrom
windows_support

Conversation

@benoit-cty

Copy link
Copy Markdown
Contributor

Description

Adds a new CPU power-measurement backend for Windows: the built-in Windows Energy Meter Interface (EMI), available since Windows 11. EMI exposes the same RAPL energy counters CodeCarbon already reads on Linux (/sys/class/powercap), but through a standard OS device interface — no third-party driver, no administrator rights, and no extra Python dependency.

Changes:

  • New codecarbon/core/windows_emi.py: a stdlib-only ctypes implementation (WindowsEMI class + is_emi_available() probe) that enumerates GUID_DEVICE_ENERGY_METER devices, reads their metadata/channels, and computes energy/power deltas from the cumulative picowatt-hour counters. Mirrors the existing IntelRAPL semantics: package channels are preferred over PP0/PP1 subdomains to avoid double-counting, with an opt-in rapl_include_dram flag.
  • codecarbon/external/hardware.py: new "windows_emi" CPU mode, treated as an energy-delta backend alongside "intel_rapl".
  • codecarbon/core/resource_tracker.py: on Windows, CodeCarbon now tries EMI first, then falls back to Intel Power Gadget (kept for backward compatibility on machines where it's already installed), then to CPU-load estimation.
  • codecarbon/core/hardware_cache.py: clears the EMI availability cache alongside the other hardware caches.
  • docs/explanation/methodology.md: documents the new Windows measurement path and its limitations.
  • New tests in tests/test_windows_emi.py (parsing, channel selection, energy/power computation, error handling, availability probe), plus updates to tests/test_resource_tracker.py, tests/test_cpu_load.py, and tests/test_tracking_inference.py to reflect the new backend priority.

Related Issue

Resolves #457

Motivation and Context

Intel Power Gadget — CodeCarbon's only real (non-estimated) CPU power source on Windows — was discontinued by Intel in December 2023 and can no longer be downloaded, so in practice every Windows user has been silently falling back to TDP × CPU-load estimation for over two years.

The issue originally proposed Intel PCM as a replacement, but it's Intel-only and notoriously hard to install (kernel driver, admin rights, no pip package) — unlikely to ever be installed by an actual CodeCarbon user. A follow-up comment proposed LibreHardwareMonitor / PyHardwareMonitor, which does work for both Intel and AMD, but still requires administrator privileges and ships the WinRing0 kernel driver (which has known CVEs and is increasingly blocked by AV/EDR and Microsoft's vulnerable-driver blocklist), plus a pythonnet + .NET Framework dependency.

Windows 11 already exposes the same RAPL counters natively through the Energy Meter Interface — the same mechanism Firefox's power profiler switched to after Power Gadget's deprecation (Mozilla bug 1774844). It works for Intel and AMD, requires no admin rights, no kernel driver, and no extra dependency (validated: reads succeed from an unelevated process). This lets CodeCarbon get real hardware measurements on Windows again with zero added friction for users.

Limitations, documented in the methodology page: EMI requires Windows 11 on bare metal (Windows 10 only reports on devices with dedicated metering hardware, e.g. Surface Book); it won't work inside VMs, same as RAPL on Linux. Where it isn't available, CodeCarbon transparently falls back to Power Gadget (if installed) and then CPU-load estimation, so there's no regression for any existing setup.

How Has This Been Tested?

  • Added tests/test_windows_emi.py: unit tests for EMI metadata/channel parsing (V1 and V2), measurement parsing, package-channel selection logic (including multi-package and DRAM-inclusion cases), WindowsEMI setup/energy computation (including negative-counter-delta and zero-time-delta edge cases), and is_emi_available() caching behavior — all via mocks, no real hardware access required.
  • Updated tests/test_resource_tracker.py, tests/test_cpu_load.py, and tests/test_tracking_inference.py for the new Windows backend priority (EMI before Power Gadget) and to account for a coarser counter-refresh granularity (<= instead of < for one assertion).
  • Manually validated against real hardware on a Windows 11 bare-metal machine (Intel Core Ultra 7 265H): a standalone probe script confirmed EMI device discovery and readable RAPL_Package0_PKG/DRAM/PP0/PP1 channels while running unelevated (~4 W idle). Then ran a full EmissionsTracker session under synthetic CPU load and confirmed CPU Tracking Method: Windows EMI was selected automatically, with power readings around 15 W and correctly computed emissions.
  • Ran the full existing test suite (pytest tests, excluding modules gated on unrelated optional dependencies not installed in this environment — dash, joserfc) to confirm no regressions: all passing.
  • Verified lint/formatting with black, isort, and flake8 per the project's pre-commit config.

Screenshots (if appropriate):

N/A (backend/library change, no UI impact).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

AI Usage Disclosure

Please refer to docs/how-to/ai-policy.md for detailed guidelines on how to disclose AI usage in your PR. Accurately completing this section is mandatory.

  • 🟥 AI-vibecoded: You cannot explain the logic. Car analogy : the car drive by itself, you are outside it and just tell it where to go.
  • 🟠 AI-generated: Car analogy : the car drive by itself, you are inside and give instructions.
  • ⭐ AI-assisted. Car analogy : you drive the car, AI help you find your way.
  • ♻️ No AI used. Car analogy : you drive the car.

Checklist:

Go over all the following points, and put an x in all the boxes that apply.

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the docs/how-to/contributing.md document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@benoit-cty
benoit-cty requested a review from a team as a code owner July 5, 2026 17:34
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.91018% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.39%. Comparing base (663db8f) to head (495d735).

Files with missing lines Patch % Lines
codecarbon/core/windows_emi.py 94.49% 17 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1263      +/-   ##
==========================================
+ Coverage   89.70%   90.39%   +0.69%     
==========================================
  Files          48       49       +1     
  Lines        4778     5103     +325     
==========================================
+ Hits         4286     4613     +327     
+ Misses        492      490       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread deploy/deploy.py Dismissed
@benoit-cty

Copy link
Copy Markdown
Contributor Author

Tested on Windows 11:

[codecarbon WARNING @ 11:24:34] Multiple instances of codecarbon are allowed to run at the same time.
[codecarbon DEBUG @ 11:24:37] Detected 1 physical sockets on Windows.
[codecarbon INFO @ 11:24:37] [setup] RAM Tracking...
[codecarbon INFO @ 11:24:37] [setup] CPU Tracking...
[codecarbon INFO @ 11:24:37] [setup] GPU Tracking...
[codecarbon INFO @ 11:24:37] No GPU found.
[codecarbon INFO @ 11:24:37]    EMI - Monitoring channel 'RAPL_Package0_PKG' of device \\?\ACPI#GenuineIntel_-_Intel64_Family_6_Model_197_-_Intel(R)_Core(TM)_Ultra_7_265H#0#{45bd8344-7ed6-49cf-a440-c276c933b053}
[codecarbon INFO @ 11:24:37] Tracking CPU via the Windows Energy Meter Interface (RAPL)
[codecarbon INFO @ 11:24:39]    EMI - Monitoring channel 'RAPL_Package0_PKG' of device \\?\ACPI#GenuineIntel_-_Intel64_Family_6_Model_197_-_Intel(R)_Core(TM)_Ultra_7_265H#0#{45bd8344-7ed6-49cf-a440-c276c933b053}
[codecarbon INFO @ 11:24:39] The below tracking methods have been set up:
                RAM Tracking Method: RAM power estimation model
                CPU Tracking Method: Windows EMI
                GPU Tracking Method: Unspecified
            
[codecarbon INFO @ 11:24:39] >>> Tracker's metadata:
[codecarbon INFO @ 11:24:39]   Platform system: Windows-11-10.0.26200-SP0
[codecarbon INFO @ 11:24:39]   Python version: 3.14.0
[codecarbon INFO @ 11:24:39]   CodeCarbon version: 3.2.8
[codecarbon INFO @ 11:24:39]   Available RAM : 31.458 GB
[codecarbon INFO @ 11:24:39]   CPU count: 16 thread(s) in 1 physical CPU(s)
[codecarbon INFO @ 11:24:39]   CPU model: Intel(R) Core(TM) Ultra 7 265H
[codecarbon INFO @ 11:24:39]   GPU count: 0
[codecarbon INFO @ 11:24:39]   GPU model: 
[codecarbon DEBUG @ 11:24:39] RAM power estimation: 20.00W for 31.46GB
[codecarbon INFO @ 11:24:39] Energy consumed for RAM : 0.000000 kWh. RAM Power : 20.0 W
[codecarbon DEBUG @ 11:24:39] Done measure for RAM - measurement time: 0.0017 s - last call 0.00 s
[codecarbon DEBUG @ 11:24:39] get_cpu_details {'Processor Energy Delta_0(kWh)': 3.6938890000000005e-09, 'Processor Power Delta_0(kWh)': 4.590107486797142}
[codecarbon DEBUG @ 11:24:39] _get_power_from_cpus - DONT MATCH Processor Energy Delta_0(kWh) : 3.6938890000000005e-09
[codecarbon DEBUG @ 11:24:39] _get_power_from_cpus - MATCH Processor Power Delta_0(kWh) : 4.590107486797142
[codecarbon INFO @ 11:24:39] Delta energy consumed for CPU with windows_emi : 0.000000 kWh, power : 4.590107486797142 W
[codecarbon INFO @ 11:24:39] Energy consumed for All CPU : 0.000000 kWh
[codecarbon DEBUG @ 11:24:39] Done measure for CPU - measurement time: 0.0021 s - last call 0.00 s
[codecarbon INFO @ 11:24:39] 0.000000 kWh of electricity and 0.000000 L of water were used since the beginning.
[codecarbon DEBUG @ 11:24:39] last_duration=0.0010105000001203734

@benoit-cty

Copy link
Copy Markdown
Contributor Author

New test done on a desktop computer with a smart plug:

[codecarbon INFO @ 12:14:25]   Platform system: Windows-11-10.0.22631-SP0
[codecarbon INFO @ 12:14:25]   Python version: 3.12.4
[codecarbon INFO @ 12:14:25]   CodeCarbon version: 3.2.9
[codecarbon INFO @ 12:14:25]   Available RAM : 31.877 GB
[codecarbon INFO @ 12:14:25]   CPU count: 32 thread(s) in 1 physical CPU(s)
[codecarbon INFO @ 12:14:25]   CPU model: AMD Ryzen Threadripper 1950X 16-Core Processor
[codecarbon INFO @ 12:14:25]   GPU count: 1
[codecarbon INFO @ 12:14:25]   GPU model: 1 x NVIDIA GeForce GTX 1080 Ti
[codecarbon DEBUG @ 12:14:25] RAM power estimation: 20.00W for 31.88GB
  • Idle:
    • Smart plug: 190W
    • CPU reported by EMI: 45W
    • GPU reported by Nvml: 85W
  • Under load: 510W
    • CPU reported by EMI: 278W note: AMD Ryzen Threadripper 1950X TDP: 180 W, far below 278W
    • GPU reported by Nvml: 230W note: TDP: 250 W

Delta smart plug: 510-190=320W
Smart-plug - GPU : 320-230=90W left for the CPU

So I think we have the same problem of double counting that we have under Linux with RAPL with this CPU.

rapl_include_dram for EMI — hardware_cache.py:132 only reads the DRAM/psys settings when hw._mode == "intel_rapl". A cached windows_emi CPU is rebuilt with rapl_include_dram=False, silently dropping the user's setting on the second run. Also, WindowsEMI stores it as emi_include_dram, so even adding the mode wouldn't match the getattr(intel, "rapl_include_dram") lookup.
… windows_emi.py

find_mirrored_channels returns a {channel: mirrored_reference} mapping instead of a bare list, so each candidate keeps a link to the channel it duplicates.
start() only flags candidates (_mirrored_candidates) and leaves them out of the reported energy; it no longer prunes _devices.
_confirm_mirrored_channels() runs at the start of each get_cpu_details(), before the deltas are computed:
equal deltas over the interval → permanently dropped (_drop_channels);
diverging deltas → the channel is a real independent meter and is restored (logged at INFO);
both deltas zero → inconclusive, stays pending for the next interval.
Net effect: a dual-socket machine whose two counters happen to coincide at the initial snapshot recovers its second socket after one interval, while multi-die duplicates are still excluded from the very first measurement (no over-count regression).
@benoit-cty

Copy link
Copy Markdown
Contributor Author

On my CPU it was not just double-counting, but multiple-counting:

Windows exposes 16 EMI devices on this CPU, one per core:

select_channel_indices() made its decision per device. The device holding RAPL_Package0_PKG correctly ignored its Core0_CORE companion, but each of the other 15 devices exposes a single channel with no pkg in its name, so the len(channel_names) == 1 fallback selected it. The result was PKG (154 W) + 15 × ~7.3 W = 265 W — your 278/279 W.

The mirrored-counter detection never fired because these are genuinely distinct counters. That hypothesis was wrong for Windows (it stays valid for the Linux per-die case, which is unrelated).

Fix
select_channel_indices is replaced by select_channels() in windows_emi.py, which looks at all devices at once: if a package channel exists anywhere on the machine, only package channels are kept, everywhere. CORE/PP0/PP1 channels are subdomains of the package and are dropped regardless of which device carries them.

Measured on my machine, at 100 % CPU load:

Before After
emi_channels.py sum 265.06 W 152.03 W
Tracker log 279.5 W 151.6 W

151 W under the 180 W TDP, instead of 1.55× above it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intel® Power Gadget is being deprecated

2 participants