A Lilu plugin that lets an Intel Core Ultra 200HX (Arrow Lake-HX) run at its real clocks and power under macOS Tahoe. Two things hold it back on a Hackintosh, and the plugin fixes both:
- A hidden 30 W power limit. The CPU obeys two package power limits: MSR 0x610 and a copy in the memory controller hub (MCHBAR + 0x59A0). On the Lenovo Legion 5 15IAX10 the firmware sets MSR 0x610 to 120 W / 168 W but leaves the MCHBAR copy at 30 W / 30 W. Lenovo's Windows software raises it after boot; nothing does that under macOS, so every core shares 30 W. The plugin copies MSR 0x610's limits into MCHBAR, unless the firmware has locked it.
- Clocks capped by the wrong HWP units. macOS (XCPM) programs Speed Shift (IA32_HWP_REQUEST) with maximum 54, meaning 5.4 GHz in 100 MHz steps. On Arrow Lake's hybrid cores the HWP fields are in per-core performance units (about 61 MHz on the P-cores and 70 MHz on the E-cores of a 275HX), so 54 stops the P-cores at 3.3 GHz and the E-cores at 3.8 GHz. The plugin takes the HWP request over and gives each core its own lowest-to-highest range.
It also controls the fans of the Legion 5 15IAX10 (full speed, a low curve or your own curve) through Lenovo's own firmware methods, with a menu bar app, LegionFan, to switch between them.
Lenovo Legion 5 15IAX10 (Core Ultra 9 275HX), macOS 26.7.1 (25G241), OpenCore 1.0.8, Lilu 1.7.2, on AC. The load is
tools/spin.c (integer work per thread), measured with tools/cpuload.py:
| load | before | with the plugin | change |
|---|---|---|---|
| 1 thread | 310 Mops/s, P-core ratio max 33 | 411-424 Mops/s, P-core ratio max 47 | +33-37 % |
| 8 threads | 2426 Mops/s, 26 W | 3377 Mops/s, 60 W | +39 % |
| 24 threads | 5666 Mops/s, 30 W | 8882 Mops/s, 83 W | +57 % |
On this laptop heat is now the limit: with all cores busy the package reaches 100-104 C and the CPU's own thermal throttling takes over before 120 W or 5.4 GHz is reached. Better cooling (a stand, full-speed fans, below) gives more.
- Intel Arrow Lake: CPUID family 6, model 0xC5 or 0xC6. The plugin reads the model with the CPUID instruction, so an
OpenCore
Cpuid1Dataspoof does not hide it. On any other CPU it loads and does nothing. - macOS Tahoe (Darwin 25). The HWP fix needs the kernel symbol
_xcpm_hwp_enabled; if it is missing, that fix stays off. - XCPM active (
sysctl machdep.xcpm.mode= 1), withplugin-type= 1 on the first CPU (SSDT-PLUG or SSDT-PLUG-ALT). - Lilu 1.7.x.
- Download
LegionCpuPower.kextfrom the releases (or build it, below) and put it inEFI/OC/Kexts. - Add it to
Kernel > Addafter Lilu, withMinKernel25.0.0 andMaxKernel25.99.99. - Reboot. Check it with
sysctl debug.legioncpu.status:
cpu arrow lake, xcpm hwp flag 0, hwp 1 (rewrites 24), pl 1 (heat holds 0), epp 32, pkg 61 C, mchbar mapped
If it says MCHBAR power limit locked, your firmware locked the copy and only the HWP fix applies.
Both fixes are on at boot. They can be changed at runtime, and a reboot always returns to the boot defaults.
| sysctl | meaning |
|---|---|
debug.legioncpu.hwp |
1 = per-core HWP requests from this plugin, 0 = back to XCPM |
debug.legioncpu.pl |
1 = MCHBAR limit follows MSR 0x610, 0 = the firmware's value |
debug.legioncpu.epp |
energy/performance hint written with hwp=1: 0 performance ... 255 power saving (default 32) |
debug.legioncpu.guard |
1 = heat guard (below), 0 = only the CPU's own thermal throttling |
debug.legioncpu.fan |
0 auto (firmware curve), 1 full speed, 2 custom curve, 3 low |
debug.legioncpu.fancurve |
the ten points of the custom curve, fan levels 1-10, e.g. 2,2,3,4,5,6,7,8,9,10 |
debug.legioncpu.status |
read-only state |
Example: sudo sysctl -w debug.legioncpu.pl=0.
Boot-args: -legioncpunohwp, -legioncpunopl and -legioncpunofan turn one part off, -legioncpuoff disables the
plugin.
Heat guard. After a minute at 100 C or more, the plugin caps the power limit at the package power it measured minus
10 W (never below 45 W), and again after every further minute that hot. Each 30 s below 90 C raises the cap by 5 W
until it is back at the firmware's limits. It never touches TjMax or any thermal register; the CPU's own thermal
protection stays in charge. Set debug.legioncpu.guard=0 for benchmarks.
Fan modes go through Lenovo's WMI methods on the \_SB.GZFD ACPI device, the same calls Lenovo Vantage makes on
Windows: full speed is feature 0x04020000 of WMAE, the curve is WMAB methods 5 and 6 (ten fan levels in EC RAM).
On this EC both only take effect in Lenovo's Custom power mode, so modes 1-3 switch the laptop to Custom (WMAA 0x2C
with 0xFF, on AC only) and auto puts the previous mode back. The plugin waits two minutes after boot before changing
the power mode: the firmware notifies the GPU on a mode change, and doing that while the NVIDIA driver was bringing up
the display left it without acceleration. In modes 2 and 3, 10 s at 95 C or more switches to full speed until the
package has been below 80 C for a minute.
Measured: full speed 5500-5700 rpm, the firmware curve 3300-4300 rpm at the same temperatures.
LegionFan (legionfan/) is a menu bar app for this: Auto, Low, Full speed, Custom curve and a ten-slider curve
editor, with the fan speeds in the menu. A root LaunchDaemon applies the saved choice at boot, before login; the app
starts at login. legionfan/build.sh, then sudo legionfan/install.sh (--remove uninstalls).
-
MCHBAR + 0x59A0 (package power limit 1/2): set to MSR 0x610's value without the lock bit. Restored on
pl=0. -
IA32_HWP_REQUEST (MSR 0x774) on every core: min = lowest, max = highest from IA32_HWP_CAPABILITIES (0x771), desired 0 (hardware decides), EPP from the sysctl. Checked once a second and written again if something changed it.
-
The kernel variable
_xcpm_hwp_enabledset to 0 while hwp=1, so XCPM stops writing HWP requests (it writes IA32_PERF_CTL instead, which the CPU ignores while HWP is enabled). Set back to 1 onhwp=0. -
With a fan mode other than auto: Lenovo's power mode (Custom), the EC's full-speed bit and its custom fan table, through the firmware's own ACPI methods. Auto restores the previous power mode and the fan table found at boot.
Nothing is written to disk or NVRAM.
probe/ is LegionCpuProbe, a read-only kext used to find all this. Every 2 s it publishes the relevant MSRs per core
(HWP capabilities and requests, current ratio, temperature, limit reasons), the package MSRs (power limits, energy,
TjMax, turbo ratios) and the MCHBAR power limit to the IORegistry (ioreg -r -c LegionCpuProbe). tools/cpuload.py
reads it while tools/spin runs and prints throughput, package watts, per-core-type clocks and limit reasons.
tools/build-mac.sh # out/LegionCpuPower.kext (clones Lilu 1.7.2 into ./Lilu on first run)
probe/build-mac.sh # probe/out/LegionCpuProbe.kext
cc -O2 -o tools/spin tools/spin.c
Command Line Tools only, no Xcode project.
docs/HOW-IT-WORKS.md has the measurements, the XCPM disassembly that showed where the request is built, and why the plugin turns XCPM's HWP path off instead of patching it.
ISC, see LICENSE. Uses Lilu (BSD-3-Clause) at build and run time.