* [PATCH 01/13] thermal: intel: Keep directed package interrupts off after syscore shutdown
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 02/13] thermal: intel: Prefix log messages via pr_fmt() Ricardo Neri
` (11 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
On kexec, the syscore shutdown callback disables the directed package-level
thermal interrupt (DPTI) on every package. kernel_kexec() runs the callback
with CPU hotplug disabled, but re-enables CPU hotplug before
machine_shutdown() stops the other CPUs. A CPU hotplug operation in that
window directs the interrupt again: offlining the CPU that handles it
migrates the interrupt to another CPU, and a CPU coming online claims a
package that has no handler. The next kernel inherits the enable bit on
that CPU, never records the CPU as a handler, and never clears it.
Set a flag in the shutdown callback and make the CPU hotplug callbacks
leave DPTI alone once it is set. No lock is needed. The callback runs with
CPU hotplug disabled. Every later CPU hotplug operation takes
cpu_add_remove_lock, which cpu_hotplug_enable() releases after the flag is
set.
Fixes: 1e768cc92bc7 ("thermal: intel: Add a syscore shutdown callback for kexec reboot")
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
Each package's handler is the first of its CPUs to come online, which for
package 0 is CPU0. On x86, CPU0 cannot be offlined. Its interrupt never
migrates: a single-package system never opens the window. The other
packages of a multi-package system can.
The sysfs interface added later in this series makes the window real on any
system: it can move the interrupt off CPU0, and each write programs a CPU
without a CPU hotplug operation.
---
drivers/thermal/intel/therm_throt.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index 59520cac6cc8..1ed604bd70e0 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -578,6 +578,13 @@ static void config_directed_thermal_pkg_intr(void *info)
*/
static unsigned int *directed_intr_handler_cpus;
+/*
+ * Set by the syscore shutdown callback before it disables the directed
+ * interrupt, so that no CPU hotplug operation enables it again on the way to
+ * kexec.
+ */
+static bool directed_pkg_intr_shut_down;
+
static bool directed_thermal_pkg_intr_supported(void)
{
if (!boot_cpu_has(X86_FEATURE_DPTI))
@@ -603,6 +610,10 @@ static void disable_directed_thermal_pkg_intr_all(void)
if (!directed_thermal_pkg_intr_supported())
return;
+ /*
+ * No need to reset the handlers: callers free directed_intr_handler_cpus
+ * or have shut DPTI down.
+ */
for (i = 0; i < topology_max_packages(); i++) {
if (directed_intr_handler_cpus[i] == nr_cpu_ids)
continue;
@@ -625,6 +636,9 @@ static int enable_directed_thermal_pkg_intr(unsigned int cpu)
if (pkg_id >= topology_max_packages())
return -EINVAL;
+ if (directed_pkg_intr_shut_down)
+ return 0;
+
/* Another CPU in this package already handles the directed interrupt. */
if (directed_intr_handler_cpus[pkg_id] != nr_cpu_ids)
return 0;
@@ -662,6 +676,9 @@ static void disable_directed_thermal_pkg_intr(unsigned int cpu)
if (pkg_id >= topology_max_packages())
return;
+ if (directed_pkg_intr_shut_down)
+ return;
+
/* Not the CPU handling the directed interrupt. */
if (directed_intr_handler_cpus[pkg_id] != cpu)
return;
@@ -738,6 +755,8 @@ static int directed_pkg_intr_syscore_suspend(void *data)
static void directed_pkg_intr_syscore_shutdown(void *data)
{
+ directed_pkg_intr_shut_down = true;
+
disable_directed_thermal_pkg_intr_all();
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 02/13] thermal: intel: Prefix log messages via pr_fmt()
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
2026-11-10 3:36 ` [PATCH 01/13] thermal: intel: Keep directed package interrupts off after syscore shutdown Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 03/13] thermal: intel: Move the sysfs device helpers next to their callers Ricardo Neri
` (10 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
therm_throt.c emits several log messages, but no pr_fmt() is defined, so
dmesg carries no subsystem hint about which file produced a given line.
Define pr_fmt() to prepend KBUILD_MODNAME, matching thermal_core.c and
most Intel-thermal neighbors (intel_powerclamp, x86_pkg_temp_thermal,
intel_soc_dts_thermal, etc.).
While at it, drop the trailing periods from the two DPTI messages that
have them. No other message in this file uses one.
No functional change intended beyond the log message text.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index 1ed604bd70e0..afbccd367c0b 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -14,6 +14,9 @@
* Credits: Adapted from Zwane Mwaikambo's original code in mce_intel.c.
* Inspired by Ross Biro's and Al Borchers' counter code.
*/
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
#include <linux/syscore_ops.h>
#include <linux/interrupt.h>
#include <linux/notifier.h>
@@ -717,7 +720,7 @@ static void disable_directed_thermal_pkg_intr(unsigned int cpu)
smp_call_function_single(new_cpu, config_directed_thermal_pkg_intr,
&enable, true);
- pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; reverting to broadcast.\n",
+ pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; reverting to broadcast\n",
cpu, new_cpu);
new_cpu = nr_cpu_ids;
@@ -825,7 +828,7 @@ static int thermal_throttle_online(unsigned int cpu)
intel_hfi_online(cpu);
if (enable_directed_thermal_pkg_intr(cpu)) {
- pr_info_once("Failed to direct package thermal interrupts. All CPUs will receive it.\n");
+ pr_info_once("Failed to direct package thermal interrupts. All CPUs will receive it\n");
cleanup_directed_pkg_thermal_intr();
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 03/13] thermal: intel: Move the sysfs device helpers next to their callers
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
2026-11-10 3:36 ` [PATCH 01/13] thermal: intel: Keep directed package interrupts off after syscore shutdown Ricardo Neri
2026-11-10 3:36 ` [PATCH 02/13] thermal: intel: Prefix log messages via pr_fmt() Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 04/13] thermal: intel: dpti: Adopt the dpti_ prefix and the owner terminology Ricardo Neri
` (9 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
thermal_throttle_add_dev() and thermal_throttle_remove_dev() sit at the
top of the second CONFIG_SYSFS block, more than 300 lines above
thermal_throttle_online() and thermal_throttle_offline(), their only
callers. Anything the add path needs must therefore be declared ahead of
it, even when the definition lives far below.
Wiring a sysfs interface for the directed package-level thermal interrupt
owner into thermal_throttle_add_dev() would need forward declarations.
Move both helpers down, immediately above thermal_throttle_online().
No functional change intended.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 118 ++++++++++++++++++------------------
1 file changed, 59 insertions(+), 59 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index afbccd367c0b..d89362dc5075 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -477,65 +477,6 @@ static int __init int_pln_enable_setup(char *s)
__setup("int_pln_enable", int_pln_enable_setup);
#ifdef CONFIG_SYSFS
-/* Add/Remove thermal_throttle interface for CPU device: */
-static int thermal_throttle_add_dev(struct device *dev, unsigned int cpu)
-{
- int err;
- struct cpuinfo_x86 *c = &cpu_data(cpu);
-
- err = sysfs_create_group(&dev->kobj, &thermal_attr_group);
- if (err)
- return err;
-
- if (cpu_has(c, X86_FEATURE_PLN) && int_pln_enable) {
- err = sysfs_add_file_to_group(&dev->kobj,
- &dev_attr_core_power_limit_count.attr,
- thermal_attr_group.name);
- if (err)
- goto del_group;
- }
-
- if (cpu_has(c, X86_FEATURE_PTS)) {
- err = sysfs_add_file_to_group(&dev->kobj,
- &dev_attr_package_throttle_count.attr,
- thermal_attr_group.name);
- if (err)
- goto del_group;
-
- err = sysfs_add_file_to_group(&dev->kobj,
- &dev_attr_package_throttle_max_time_ms.attr,
- thermal_attr_group.name);
- if (err)
- goto del_group;
-
- err = sysfs_add_file_to_group(&dev->kobj,
- &dev_attr_package_throttle_total_time_ms.attr,
- thermal_attr_group.name);
- if (err)
- goto del_group;
-
- if (cpu_has(c, X86_FEATURE_PLN) && int_pln_enable) {
- err = sysfs_add_file_to_group(&dev->kobj,
- &dev_attr_package_power_limit_count.attr,
- thermal_attr_group.name);
- if (err)
- goto del_group;
- }
- }
-
- return 0;
-
-del_group:
- sysfs_remove_group(&dev->kobj, &thermal_attr_group);
-
- return err;
-}
-
-static void thermal_throttle_remove_dev(struct device *dev)
-{
- sysfs_remove_group(&dev->kobj, &thermal_attr_group);
-}
-
static int check_directed_thermal_pkg_intr_ack(void)
{
unsigned int count = 15000;
@@ -802,6 +743,65 @@ static void cleanup_directed_pkg_thermal_intr(void)
directed_intr_handler_cpus = NULL;
}
+/* Add/Remove thermal_throttle interface for CPU device: */
+static int thermal_throttle_add_dev(struct device *dev, unsigned int cpu)
+{
+ int err;
+ struct cpuinfo_x86 *c = &cpu_data(cpu);
+
+ err = sysfs_create_group(&dev->kobj, &thermal_attr_group);
+ if (err)
+ return err;
+
+ if (cpu_has(c, X86_FEATURE_PLN) && int_pln_enable) {
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_core_power_limit_count.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+ }
+
+ if (cpu_has(c, X86_FEATURE_PTS)) {
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_package_throttle_count.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_package_throttle_max_time_ms.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_package_throttle_total_time_ms.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+
+ if (cpu_has(c, X86_FEATURE_PLN) && int_pln_enable) {
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_package_power_limit_count.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+ }
+ }
+
+ return 0;
+
+del_group:
+ sysfs_remove_group(&dev->kobj, &thermal_attr_group);
+
+ return err;
+}
+
+static void thermal_throttle_remove_dev(struct device *dev)
+{
+ sysfs_remove_group(&dev->kobj, &thermal_attr_group);
+}
+
/* Get notified when a cpu comes on/off. Be hotplug friendly. */
static int thermal_throttle_online(unsigned int cpu)
{
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 04/13] thermal: intel: dpti: Adopt the dpti_ prefix and the owner terminology
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (2 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 03/13] thermal: intel: Move the sysfs device helpers next to their callers Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 05/13] thermal: intel: dpti: Tear down DPTI only during boot Ricardo Neri
` (8 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
The functions and variables that implement the directed package-level
thermal interrupt (DPTI) are spelled four different ways:
directed_thermal_pkg_intr, directed_pkg_thermal_intr, directed_pkg_intr,
and directed_intr. No single spelling covers the whole group, so no single
grep finds it. The longhand also yields names of up to 37 characters, which
crowds the 80-column limit at the call sites.
x86 already uses the short form in the X86_FEATURE_DPTI feature bit and in
the DPTI field names of MSR_IA32_THERM_INTERRUPT and
MSR_IA32_PACKAGE_THERM_STATUS. Reuse it for the C symbols as well: the
namespace becomes uniform, shorter, and greps as one unit.
These names also call the CPU that receives the interrupt the "handler",
which reads as the interrupt service routine. Across the kernel the noun
names a function rather than a CPU. Call that CPU the owner instead. The
handling of the Corrected Machine Check Interrupt already uses this
vocabulary for the same problem: mce_banks_owned tracks which CPU services
each of a package's shared banks, and cmci_rediscover() hands that duty to
another CPU when the owner goes offline.
A planned sysfs attribute, package_interrupt_owner, will use the term too.
No functional change intended.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 129 ++++++++++++++++++------------------
1 file changed, 63 insertions(+), 66 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index d89362dc5075..a170dcc8513f 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -477,7 +477,7 @@ static int __init int_pln_enable_setup(char *s)
__setup("int_pln_enable", int_pln_enable_setup);
#ifdef CONFIG_SYSFS
-static int check_directed_thermal_pkg_intr_ack(void)
+static int dpti_wait_for_ack(void)
{
unsigned int count = 15000;
u64 msr_val;
@@ -500,7 +500,7 @@ static int check_directed_thermal_pkg_intr_ack(void)
return 0;
}
-static void config_directed_thermal_pkg_intr(void *info)
+static void dpti_config(void *info)
{
bool enable = *((bool *)info);
u64 msr_val;
@@ -520,21 +520,21 @@ static void config_directed_thermal_pkg_intr(void *info)
* hotplug is inactive: the init and cleanup paths as well as syscore callbacks.
* No extra locking needed.
*/
-static unsigned int *directed_intr_handler_cpus;
+static unsigned int *dpti_owner_cpus;
/*
* Set by the syscore shutdown callback before it disables the directed
* interrupt, so that no CPU hotplug operation enables it again on the way to
* kexec.
*/
-static bool directed_pkg_intr_shut_down;
+static bool dpti_shut_down;
-static bool directed_thermal_pkg_intr_supported(void)
+static bool dpti_supported(void)
{
if (!boot_cpu_has(X86_FEATURE_DPTI))
return false;
- if (!directed_intr_handler_cpus)
+ if (!dpti_owner_cpus)
return false;
return true;
@@ -546,53 +546,52 @@ static bool directed_thermal_pkg_intr_supported(void)
* deadlocks in SMP function calls. The syscore shutdown callback also calls
* this function, but runs with CPU hotplug disabled (and interrupts enabled).
*/
-static void disable_directed_thermal_pkg_intr_all(void)
+static void dpti_clear_all(void)
{
bool enable = false;
int i;
- if (!directed_thermal_pkg_intr_supported())
+ if (!dpti_supported())
return;
/*
- * No need to reset the handlers: callers free directed_intr_handler_cpus
- * or have shut DPTI down.
+ * No need to reset the owners: callers free dpti_owner_cpus or have shut
+ * DPTI down.
*/
for (i = 0; i < topology_max_packages(); i++) {
- if (directed_intr_handler_cpus[i] == nr_cpu_ids)
+ if (dpti_owner_cpus[i] == nr_cpu_ids)
continue;
- smp_call_function_single(directed_intr_handler_cpus[i],
- config_directed_thermal_pkg_intr,
+ smp_call_function_single(dpti_owner_cpus[i], dpti_config,
&enable, true);
}
}
-static int enable_directed_thermal_pkg_intr(unsigned int cpu)
+static int dpti_online(unsigned int cpu)
{
bool enable = true;
u16 pkg_id;
- if (!directed_thermal_pkg_intr_supported())
+ if (!dpti_supported())
return 0;
pkg_id = topology_logical_package_id(cpu);
if (pkg_id >= topology_max_packages())
return -EINVAL;
- if (directed_pkg_intr_shut_down)
+ if (dpti_shut_down)
return 0;
- /* Another CPU in this package already handles the directed interrupt. */
- if (directed_intr_handler_cpus[pkg_id] != nr_cpu_ids)
+ /* Another CPU in this package already owns the directed interrupt. */
+ if (dpti_owner_cpus[pkg_id] != nr_cpu_ids)
return 0;
thermal_clear_package_intr_status(PACKAGE_LEVEL,
PACKAGE_THERM_STATUS_DPTI_ACK);
- config_directed_thermal_pkg_intr(&enable);
- if (!check_directed_thermal_pkg_intr_ack()) {
- directed_intr_handler_cpus[pkg_id] = cpu;
+ dpti_config(&enable);
+ if (!dpti_wait_for_ack()) {
+ dpti_owner_cpus[pkg_id] = cpu;
return 0;
}
@@ -602,29 +601,29 @@ static int enable_directed_thermal_pkg_intr(unsigned int cpu)
* CPUs may come online and would otherwise keep trying.
*/
enable = false;
- config_directed_thermal_pkg_intr(&enable);
+ dpti_config(&enable);
return -ETIMEDOUT;
}
-static void disable_directed_thermal_pkg_intr(unsigned int cpu)
+static void dpti_offline(unsigned int cpu)
{
unsigned int new_cpu;
bool enable;
u16 pkg_id;
- if (!directed_thermal_pkg_intr_supported())
+ if (!dpti_supported())
return;
pkg_id = topology_logical_package_id(cpu);
if (pkg_id >= topology_max_packages())
return;
- if (directed_pkg_intr_shut_down)
+ if (dpti_shut_down)
return;
- /* Not the CPU handling the directed interrupt. */
- if (directed_intr_handler_cpus[pkg_id] != cpu)
+ /* Not the owner of the directed interrupt. */
+ if (dpti_owner_cpus[pkg_id] != cpu)
return;
/*
@@ -646,8 +645,7 @@ static void disable_directed_thermal_pkg_intr(unsigned int cpu)
* but it does not reach this path because all the secondary
* CPUs are offline.
*/
- smp_call_function_single(new_cpu, config_directed_thermal_pkg_intr,
- &enable, true);
+ smp_call_function_single(new_cpu, dpti_config, &enable, true);
}
/*
@@ -657,9 +655,8 @@ static void disable_directed_thermal_pkg_intr(unsigned int cpu)
* receive it.
*/
enable = false;
- if (new_cpu < nr_cpu_ids && check_directed_thermal_pkg_intr_ack()) {
- smp_call_function_single(new_cpu, config_directed_thermal_pkg_intr,
- &enable, true);
+ if (new_cpu < nr_cpu_ids && dpti_wait_for_ack()) {
+ smp_call_function_single(new_cpu, dpti_config, &enable, true);
pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; reverting to broadcast\n",
cpu, new_cpu);
@@ -671,76 +668,76 @@ static void disable_directed_thermal_pkg_intr(unsigned int cpu)
* Clear the directed interrupt on @cpu. Hardware acknowledgment can be
* ignored since @cpu is going offline.
*/
- config_directed_thermal_pkg_intr(&enable);
+ dpti_config(&enable);
- directed_intr_handler_cpus[pkg_id] = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
+ dpti_owner_cpus[pkg_id] = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
}
/*
- * CPU0 may be handling the directed interrupt, but the CPU hotplug callbacks
- * are not called for CPU0 during suspend and resume.
+ * CPU0 may own the directed interrupt, but the CPU hotplug callbacks are not
+ * called for CPU0 during suspend and resume.
*/
-static void directed_pkg_intr_syscore_resume(void *data)
+static void dpti_syscore_resume(void *data)
{
/*
* We can't do anything to handle errors. If direction fails for CPU0,
* another CPU will take over or disable direction entirely during CPU
* hotplug.
*/
- enable_directed_thermal_pkg_intr(0);
+ dpti_online(0);
}
-static int directed_pkg_intr_syscore_suspend(void *data)
+static int dpti_syscore_suspend(void *data)
{
- disable_directed_thermal_pkg_intr(0);
+ dpti_offline(0);
return 0;
}
-static void directed_pkg_intr_syscore_shutdown(void *data)
+static void dpti_syscore_shutdown(void *data)
{
- directed_pkg_intr_shut_down = true;
+ dpti_shut_down = true;
- disable_directed_thermal_pkg_intr_all();
+ dpti_clear_all();
}
-static const struct syscore_ops directed_pkg_intr_pm_ops = {
- .resume = directed_pkg_intr_syscore_resume,
- .suspend = directed_pkg_intr_syscore_suspend,
- .shutdown = directed_pkg_intr_syscore_shutdown,
+static const struct syscore_ops dpti_syscore_ops = {
+ .resume = dpti_syscore_resume,
+ .suspend = dpti_syscore_suspend,
+ .shutdown = dpti_syscore_shutdown,
};
-static struct syscore directed_pkg_intr_pm = {
- .ops = &directed_pkg_intr_pm_ops,
+static struct syscore dpti_syscore = {
+ .ops = &dpti_syscore_ops,
};
-static __init void init_directed_pkg_intr(void)
+static __init void dpti_init(void)
{
int i;
if (!boot_cpu_has(X86_FEATURE_DPTI))
return;
- directed_intr_handler_cpus = kmalloc_objs(*directed_intr_handler_cpus,
- topology_max_packages());
- if (!directed_intr_handler_cpus)
+ dpti_owner_cpus = kmalloc_objs(*dpti_owner_cpus,
+ topology_max_packages());
+ if (!dpti_owner_cpus)
return;
for (i = 0; i < topology_max_packages(); i++)
- directed_intr_handler_cpus[i] = nr_cpu_ids;
+ dpti_owner_cpus[i] = nr_cpu_ids;
- register_syscore(&directed_pkg_intr_pm);
+ register_syscore(&dpti_syscore);
}
-static void cleanup_directed_pkg_thermal_intr(void)
+static void dpti_cleanup(void)
{
- if (!directed_thermal_pkg_intr_supported())
+ if (!dpti_supported())
return;
- unregister_syscore(&directed_pkg_intr_pm);
- disable_directed_thermal_pkg_intr_all();
- kfree(directed_intr_handler_cpus);
- directed_intr_handler_cpus = NULL;
+ unregister_syscore(&dpti_syscore);
+ dpti_clear_all();
+ kfree(dpti_owner_cpus);
+ dpti_owner_cpus = NULL;
}
/* Add/Remove thermal_throttle interface for CPU device: */
@@ -827,9 +824,9 @@ static int thermal_throttle_online(unsigned int cpu)
*/
intel_hfi_online(cpu);
- if (enable_directed_thermal_pkg_intr(cpu)) {
+ if (dpti_online(cpu)) {
pr_info_once("Failed to direct package thermal interrupts. All CPUs will receive it\n");
- cleanup_directed_pkg_thermal_intr();
+ dpti_cleanup();
}
/* Unmask the thermal vector after the above workqueues are initialized. */
@@ -849,7 +846,7 @@ static int thermal_throttle_offline(unsigned int cpu)
l = apic_read(APIC_LVTTHMR);
apic_write(APIC_LVTTHMR, l | APIC_LVT_MASKED);
- disable_directed_thermal_pkg_intr(cpu);
+ dpti_offline(cpu);
intel_hfi_offline(cpu);
@@ -870,7 +867,7 @@ static __init int thermal_throttle_init_device(void)
if (!atomic_read(&therm_throt_en))
return 0;
- init_directed_pkg_intr();
+ dpti_init();
intel_hfi_init();
@@ -880,7 +877,7 @@ static __init int thermal_throttle_init_device(void)
if (ret >= 0)
return 0;
- cleanup_directed_pkg_thermal_intr();
+ dpti_cleanup();
return ret;
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 05/13] thermal: intel: dpti: Tear down DPTI only during boot
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (3 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 04/13] thermal: intel: dpti: Adopt the dpti_ prefix and the owner terminology Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 06/13] thermal: intel: dpti: Introduce struct dpti_pkg for per-package state Ricardo Neri
` (7 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
An unacknowledged DPTI setup on a CPU coming online tears DPTI down on all
packages, at any time. During boot, the hotplug online callback runs on
every online CPU. The teardown keeps each of them from spending its own ack
timeout on the same failure.
Add a flag that tells boot apart from later CPU hotplug. Clear it once the
hotplug callbacks are registered. By then at least one CPU has attempted
the setup. If DPTI is still up, that attempt succeeded. From then on,
assume the hardware works and prevent a complete teardown from other code
paths.
The flag is system-wide. This assumes that if DPTI works on the boot
package it will work on others too. This assumption is reasonable since all
packages are identical in hardware and firmware.
Skipping the teardown after boot is safe because a failure leaves the
hardware in a working state: failures in the migration of the interrupt to
another CPU fall back to broadcasting the interrupt to all CPUs in the
package.
Name the failing package in the message and whether it alone or every
package reverts to broadcast. Raise the message to a warning, like the
offline path's.
cpuhp_setup_state() releases the CPU hotplug lock before returning.
Clearing the flag outside the lock means a CPU could come online in that
window, see the flag still set, and hit the boot-teardown path on failure,
even though the initcall has already finished verifying the CPUs to which
"boot" refers. Use cpuhp_setup_state_cpuslocked() under cpus_read_lock()
so the flag is cleared while no CPU can come online.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 47 +++++++++++++++++++++++++++----------
1 file changed, 34 insertions(+), 13 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index a170dcc8513f..ac719e2407ac 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -529,6 +529,13 @@ static unsigned int *dpti_owner_cpus;
*/
static bool dpti_shut_down;
+/*
+ * Cleared once the hotplug callbacks are registered: at least one CPU
+ * attempted the setup by then, and the hardware is assumed sane from that
+ * point on.
+ */
+static bool dpti_in_boot __ro_after_init = true;
+
static bool dpti_supported(void)
{
if (!boot_cpu_has(X86_FEATURE_DPTI))
@@ -595,11 +602,7 @@ static int dpti_online(unsigned int cpu)
return 0;
}
- /*
- * A failure indicates faulty hardware. Roll back completely so that
- * no other CPU tries. This is especially important during boot as all
- * CPUs may come online and would otherwise keep trying.
- */
+ /* Roll back on failure so the package reverts to broadcasting mode. */
enable = false;
dpti_config(&enable);
@@ -681,8 +684,7 @@ static void dpti_syscore_resume(void *data)
{
/*
* We can't do anything to handle errors. If direction fails for CPU0,
- * another CPU will take over or disable direction entirely during CPU
- * hotplug.
+ * another CPU will take over during CPU hotplug.
*/
dpti_online(0);
}
@@ -825,8 +827,16 @@ static int thermal_throttle_online(unsigned int cpu)
intel_hfi_online(cpu);
if (dpti_online(cpu)) {
- pr_info_once("Failed to direct package thermal interrupts. All CPUs will receive it\n");
- dpti_cleanup();
+ pr_warn_once("Failed to direct the thermal interrupt of package %u; reverting to broadcast on %s\n",
+ topology_logical_package_id(cpu),
+ dpti_in_boot ? "all packages" : "this package");
+
+ /*
+ * A failure during boot indicates faulty hardware, so no other
+ * CPU should try.
+ */
+ if (dpti_in_boot)
+ dpti_cleanup();
}
/* Unmask the thermal vector after the above workqueues are initialized. */
@@ -871,11 +881,22 @@ static __init int thermal_throttle_init_device(void)
intel_hfi_init();
- ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "x86/therm:online",
- thermal_throttle_online,
- thermal_throttle_offline);
- if (ret >= 0)
+ /*
+ * A CPU coming online before dpti_in_boot is cleared would treat a DPTI
+ * failure as faulty hardware and tear DPTI down. Hold the CPU hotplug
+ * lock across the registration and the clear, so that the flag changes
+ * only while no CPU can come online.
+ */
+ guard(cpus_read_lock)();
+
+ ret = cpuhp_setup_state_cpuslocked(CPUHP_AP_ONLINE_DYN,
+ "x86/therm:online",
+ thermal_throttle_online,
+ thermal_throttle_offline);
+ if (ret >= 0) {
+ dpti_in_boot = false;
return 0;
+ }
dpti_cleanup();
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 06/13] thermal: intel: dpti: Introduce struct dpti_pkg for per-package state
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (4 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 05/13] thermal: intel: dpti: Tear down DPTI only during boot Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 07/13] thermal: intel: dpti: Configure the local CPU by value Ricardo Neri
` (6 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
The array dpti_owner_cpus[] tracks the CPU that owns the directed
package-level thermal interrupt (DPTI) for each package.
A sticky, per-package owner CPU that the user selects through sysfs will
need a field of its own. Rather than adding a second parallel array, wrap
the state in a struct. Future members can be added in one place without
churning every call site again.
No functional change intended.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 38 ++++++++++++++++++++++---------------
1 file changed, 23 insertions(+), 15 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index ac719e2407ac..a0a3842e2760 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -515,12 +515,21 @@ static void dpti_config(void *info)
wrmsrq(MSR_IA32_THERM_INTERRUPT, msr_val);
}
+/**
+ * struct dpti_pkg - Per-package DPTI state
+ * @owner: CPU currently programmed to receive the package-level
+ * interrupt, or nr_cpu_ids when the package has no owner.
+ */
+struct dpti_pkg {
+ unsigned int owner;
+};
+
/*
* Accessed from CPU hotplug callbacks and from code that runs while CPU
* hotplug is inactive: the init and cleanup paths as well as syscore callbacks.
* No extra locking needed.
*/
-static unsigned int *dpti_owner_cpus;
+static struct dpti_pkg *dpti_pkgs;
/*
* Set by the syscore shutdown callback before it disables the directed
@@ -541,7 +550,7 @@ static bool dpti_supported(void)
if (!boot_cpu_has(X86_FEATURE_DPTI))
return false;
- if (!dpti_owner_cpus)
+ if (!dpti_pkgs)
return false;
return true;
@@ -562,14 +571,14 @@ static void dpti_clear_all(void)
return;
/*
- * No need to reset the owners: callers free dpti_owner_cpus or have shut
+ * No need to reset the owners: callers free dpti_pkgs[] or have shut
* DPTI down.
*/
for (i = 0; i < topology_max_packages(); i++) {
- if (dpti_owner_cpus[i] == nr_cpu_ids)
+ if (dpti_pkgs[i].owner == nr_cpu_ids)
continue;
- smp_call_function_single(dpti_owner_cpus[i], dpti_config,
+ smp_call_function_single(dpti_pkgs[i].owner, dpti_config,
&enable, true);
}
}
@@ -590,7 +599,7 @@ static int dpti_online(unsigned int cpu)
return 0;
/* Another CPU in this package already owns the directed interrupt. */
- if (dpti_owner_cpus[pkg_id] != nr_cpu_ids)
+ if (dpti_pkgs[pkg_id].owner != nr_cpu_ids)
return 0;
thermal_clear_package_intr_status(PACKAGE_LEVEL,
@@ -598,7 +607,7 @@ static int dpti_online(unsigned int cpu)
dpti_config(&enable);
if (!dpti_wait_for_ack()) {
- dpti_owner_cpus[pkg_id] = cpu;
+ dpti_pkgs[pkg_id].owner = cpu;
return 0;
}
@@ -626,7 +635,7 @@ static void dpti_offline(unsigned int cpu)
return;
/* Not the owner of the directed interrupt. */
- if (dpti_owner_cpus[pkg_id] != cpu)
+ if (dpti_pkgs[pkg_id].owner != cpu)
return;
/*
@@ -673,7 +682,7 @@ static void dpti_offline(unsigned int cpu)
*/
dpti_config(&enable);
- dpti_owner_cpus[pkg_id] = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
+ dpti_pkgs[pkg_id].owner = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
}
/*
@@ -720,13 +729,12 @@ static __init void dpti_init(void)
if (!boot_cpu_has(X86_FEATURE_DPTI))
return;
- dpti_owner_cpus = kmalloc_objs(*dpti_owner_cpus,
- topology_max_packages());
- if (!dpti_owner_cpus)
+ dpti_pkgs = kmalloc_objs(*dpti_pkgs, topology_max_packages());
+ if (!dpti_pkgs)
return;
for (i = 0; i < topology_max_packages(); i++)
- dpti_owner_cpus[i] = nr_cpu_ids;
+ dpti_pkgs[i].owner = nr_cpu_ids;
register_syscore(&dpti_syscore);
}
@@ -738,8 +746,8 @@ static void dpti_cleanup(void)
unregister_syscore(&dpti_syscore);
dpti_clear_all();
- kfree(dpti_owner_cpus);
- dpti_owner_cpus = NULL;
+ kfree(dpti_pkgs);
+ dpti_pkgs = NULL;
}
/* Add/Remove thermal_throttle interface for CPU device: */
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 07/13] thermal: intel: dpti: Configure the local CPU by value
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (5 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 06/13] thermal: intel: dpti: Introduce struct dpti_pkg for per-package state Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 08/13] thermal: intel: dpti: Factor the programming sequence out into a helper Ricardo Neri
` (5 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
dpti_config() enables or disables the directed package-level thermal
interrupt (DPTI) on the CPU that runs it. It takes its argument by
pointer because it also serves as an smp_call_function_single()
callback. A caller that only programs its own CPU therefore has to
declare a scratch bool and track which value it holds at each call site.
Rename it to dpti_config_this_cpu() and let it take the setting by value.
Only the callers that reach another CPU need the callback signature;
unmarshal the argument for them in a thin dpti_config_ipi() trampoline.
The scratch bool then exists only where an SMP function call is made.
This also prepares for factoring the hardware programming sequence into a
helper that runs on the local CPU and configures the interrupt the same
way.
No functional change intended: the MSR read-modify-write is unchanged, and
the scratch bools held a known value at every converted call site.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index a0a3842e2760..36cdbe0eb6c1 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -500,9 +500,8 @@ static int dpti_wait_for_ack(void)
return 0;
}
-static void dpti_config(void *info)
+static void dpti_config_this_cpu(bool enable)
{
- bool enable = *((bool *)info);
u64 msr_val;
rdmsrq(MSR_IA32_THERM_INTERRUPT, msr_val);
@@ -515,6 +514,11 @@ static void dpti_config(void *info)
wrmsrq(MSR_IA32_THERM_INTERRUPT, msr_val);
}
+static void dpti_config_ipi(void *info)
+{
+ dpti_config_this_cpu(*((bool *)info));
+}
+
/**
* struct dpti_pkg - Per-package DPTI state
* @owner: CPU currently programmed to receive the package-level
@@ -578,14 +582,13 @@ static void dpti_clear_all(void)
if (dpti_pkgs[i].owner == nr_cpu_ids)
continue;
- smp_call_function_single(dpti_pkgs[i].owner, dpti_config,
+ smp_call_function_single(dpti_pkgs[i].owner, dpti_config_ipi,
&enable, true);
}
}
static int dpti_online(unsigned int cpu)
{
- bool enable = true;
u16 pkg_id;
if (!dpti_supported())
@@ -605,15 +608,14 @@ static int dpti_online(unsigned int cpu)
thermal_clear_package_intr_status(PACKAGE_LEVEL,
PACKAGE_THERM_STATUS_DPTI_ACK);
- dpti_config(&enable);
+ dpti_config_this_cpu(true);
if (!dpti_wait_for_ack()) {
dpti_pkgs[pkg_id].owner = cpu;
return 0;
}
/* Roll back on failure so the package reverts to broadcasting mode. */
- enable = false;
- dpti_config(&enable);
+ dpti_config_this_cpu(false);
return -ETIMEDOUT;
}
@@ -657,7 +659,8 @@ static void dpti_offline(unsigned int cpu)
* but it does not reach this path because all the secondary
* CPUs are offline.
*/
- smp_call_function_single(new_cpu, dpti_config, &enable, true);
+ smp_call_function_single(new_cpu, dpti_config_ipi, &enable,
+ true);
}
/*
@@ -668,7 +671,8 @@ static void dpti_offline(unsigned int cpu)
*/
enable = false;
if (new_cpu < nr_cpu_ids && dpti_wait_for_ack()) {
- smp_call_function_single(new_cpu, dpti_config, &enable, true);
+ smp_call_function_single(new_cpu, dpti_config_ipi, &enable,
+ true);
pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; reverting to broadcast\n",
cpu, new_cpu);
@@ -680,7 +684,7 @@ static void dpti_offline(unsigned int cpu)
* Clear the directed interrupt on @cpu. Hardware acknowledgment can be
* ignored since @cpu is going offline.
*/
- dpti_config(&enable);
+ dpti_config_this_cpu(false);
dpti_pkgs[pkg_id].owner = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 08/13] thermal: intel: dpti: Factor the programming sequence out into a helper
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (6 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 07/13] thermal: intel: dpti: Configure the local CPU by value Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 09/13] thermal: intel: dpti: Program the directed interrupt on the target CPU Ricardo Neri
` (4 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
The hardware programming sequence for the directed package-level thermal
interrupt (DPTI) is embedded in dpti_online(): clear the acknowledgment
status, enable the interrupt, wait for the hardware to acknowledge, and
roll back on failure. The syscore resume callback reuses it to
unconditionally restore the DPTI on CPU0.
The programming sequence will have to run on a selected owner CPU. CPU0
will no longer always be the boot package's owner. The syscore resume
callback then cannot reuse dpti_online().
Factor the sequence out into dpti_program_this_cpu() and call it from both
the online callback and syscore resume. Its unused argument gives it the
callback signature, so that it can run on a remote CPU via
smp_call_on_cpu(). The syscore callbacks now act only if CPU0 owns the
DPTI.
Add dpti_lookup_pkg_id() to check, at entry points, that a CPU's package
has per-package state, and dpti_pkg_of_cpu_nocheck() for code that runs
after that check. Owner selection and the sysfs interface will reuse them.
No locking is needed in the syscore path: interrupts are disabled and
only the boot CPU is online at this point.
A suspend and resume cycle leaves the same state as before: the DPTI
directed to CPU0, or broadcasting if the hardware does not acknowledge.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 83 ++++++++++++++++++++++++++++++-------
1 file changed, 68 insertions(+), 15 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index 36cdbe0eb6c1..72dabffe3b2f 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -519,6 +519,25 @@ static void dpti_config_ipi(void *info)
dpti_config_this_cpu(*((bool *)info));
}
+/* The unused argument matches the smp_call_on_cpu() callback signature. */
+static int dpti_program_this_cpu(void *unused)
+{
+ int ret;
+
+ thermal_clear_package_intr_status(PACKAGE_LEVEL,
+ PACKAGE_THERM_STATUS_DPTI_ACK);
+
+ dpti_config_this_cpu(true);
+ ret = dpti_wait_for_ack();
+ if (!ret)
+ return 0;
+
+ /* Undo the enable on failure, leaving the package as it was before. */
+ dpti_config_this_cpu(false);
+
+ return ret;
+}
+
/**
* struct dpti_pkg - Per-package DPTI state
* @owner: CPU currently programmed to receive the package-level
@@ -560,6 +579,38 @@ static bool dpti_supported(void)
return true;
}
+static int dpti_lookup_pkg_id(unsigned int cpu, u16 *pkg_id)
+{
+ u16 id;
+
+ if (!dpti_supported())
+ return -ENODEV;
+
+ id = topology_logical_package_id(cpu);
+ if (id >= topology_max_packages())
+ return -ENODEV;
+
+ *pkg_id = id;
+
+ return 0;
+}
+
+/* For callers already past dpti_lookup_pkg_id(). */
+static struct dpti_pkg *dpti_pkg_of_cpu_nocheck(unsigned int cpu)
+{
+ return &dpti_pkgs[topology_logical_package_id(cpu)];
+}
+
+static bool dpti_cpu_is_owner(unsigned int cpu)
+{
+ u16 pkg_id;
+
+ if (dpti_lookup_pkg_id(cpu, &pkg_id))
+ return false;
+
+ return dpti_pkgs[pkg_id].owner == cpu;
+}
+
/*
* Must be called with cpu_hotplug_lock held to prevent CPUs from going offline
* while iterating through packages and interrupts must be enabled to avoid
@@ -590,6 +641,7 @@ static void dpti_clear_all(void)
static int dpti_online(unsigned int cpu)
{
u16 pkg_id;
+ int ret;
if (!dpti_supported())
return 0;
@@ -605,19 +657,13 @@ static int dpti_online(unsigned int cpu)
if (dpti_pkgs[pkg_id].owner != nr_cpu_ids)
return 0;
- thermal_clear_package_intr_status(PACKAGE_LEVEL,
- PACKAGE_THERM_STATUS_DPTI_ACK);
-
- dpti_config_this_cpu(true);
- if (!dpti_wait_for_ack()) {
- dpti_pkgs[pkg_id].owner = cpu;
- return 0;
- }
+ ret = dpti_program_this_cpu(NULL);
+ if (ret)
+ return ret;
- /* Roll back on failure so the package reverts to broadcasting mode. */
- dpti_config_this_cpu(false);
+ dpti_pkgs[pkg_id].owner = cpu;
- return -ETIMEDOUT;
+ return 0;
}
static void dpti_offline(unsigned int cpu)
@@ -695,16 +741,23 @@ static void dpti_offline(unsigned int cpu)
*/
static void dpti_syscore_resume(void *data)
{
+ if (!dpti_cpu_is_owner(0))
+ return;
+
+ if (!dpti_program_this_cpu(NULL))
+ return;
+
/*
- * We can't do anything to handle errors. If direction fails for CPU0,
- * another CPU will take over during CPU hotplug.
+ * Let another CPU take over via CPU hotplug. No lock is needed here:
+ * only CPU0 is online and interrupts are disabled.
*/
- dpti_online(0);
+ dpti_pkg_of_cpu_nocheck(0)->owner = nr_cpu_ids;
}
static int dpti_syscore_suspend(void *data)
{
- dpti_offline(0);
+ if (dpti_cpu_is_owner(0))
+ dpti_config_this_cpu(false);
return 0;
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 09/13] thermal: intel: dpti: Program the directed interrupt on the target CPU
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (7 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 08/13] thermal: intel: dpti: Factor the programming sequence out into a helper Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 10/13] thermal: intel: dpti: Add rank-based owner selection Ricardo Neri
` (3 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
The CPU online and offline paths of the directed package-level thermal
interrupt (DPTI) both perform the same hardware programming sequence: clear
the acknowledgment status, enable the interrupt on a target CPU, wait for
the hardware to acknowledge the setup, and roll back the enable if the
acknowledgment times out.
MSR_IA32_PACKAGE_THERM_STATUS is package-scoped, but both paths clear and
poll it on the CPU that runs them while enabling the interrupt on the
target CPU. This setup works because every caller today targets a CPU in
its own package. A sysfs interface that programs an owner on behalf of any
package will lift that restriction.
Run the full sequence on the target CPU instead.
smp_call_function_single() cannot be used for the sequence: it would wait
for up to 15ms in interrupt context. Instead, use smp_call_on_cpu(), which
can wait and cannot migrate to another CPU.
Keep using smp_call_function_single() to clear the directed interrupt on a
CPU. It does not wait for acknowledgment and can therefore run in interrupt
context. Clearing is then the only remote configuration left. Replace
dpti_config_ipi() with dpti_clear_ipi(), which takes no setting.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 63 +++++++++++++++++--------------------
1 file changed, 29 insertions(+), 34 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index 72dabffe3b2f..e6ce443c0a9d 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -514,9 +514,14 @@ static void dpti_config_this_cpu(bool enable)
wrmsrq(MSR_IA32_THERM_INTERRUPT, msr_val);
}
-static void dpti_config_ipi(void *info)
+static void dpti_clear_ipi(void *unused)
{
- dpti_config_this_cpu(*((bool *)info));
+ dpti_config_this_cpu(false);
+}
+
+static void dpti_clear_on_cpu(unsigned int cpu)
+{
+ smp_call_function_single(cpu, dpti_clear_ipi, NULL, true);
}
/* The unused argument matches the smp_call_on_cpu() callback signature. */
@@ -538,6 +543,18 @@ static int dpti_program_this_cpu(void *unused)
return ret;
}
+/*
+ * MSR_IA32_PACKAGE_THERM_STATUS is package-scoped, so the acknowledgment must
+ * be polled on the same package. Run a worker bound to @cpu: it cannot be
+ * migrated and may wait for the acknowledgment for as long as 15ms.
+ */
+static int dpti_program_on_cpu(unsigned int cpu)
+{
+ lockdep_assert_cpus_held();
+
+ return smp_call_on_cpu(cpu, dpti_program_this_cpu, NULL, false);
+}
+
/**
* struct dpti_pkg - Per-package DPTI state
* @owner: CPU currently programmed to receive the package-level
@@ -619,7 +636,6 @@ static bool dpti_cpu_is_owner(unsigned int cpu)
*/
static void dpti_clear_all(void)
{
- bool enable = false;
int i;
if (!dpti_supported())
@@ -630,11 +646,12 @@ static void dpti_clear_all(void)
* DPTI down.
*/
for (i = 0; i < topology_max_packages(); i++) {
- if (dpti_pkgs[i].owner == nr_cpu_ids)
+ unsigned int owner = dpti_pkgs[i].owner;
+
+ if (owner == nr_cpu_ids)
continue;
- smp_call_function_single(dpti_pkgs[i].owner, dpti_config_ipi,
- &enable, true);
+ dpti_clear_on_cpu(owner);
}
}
@@ -657,7 +674,7 @@ static int dpti_online(unsigned int cpu)
if (dpti_pkgs[pkg_id].owner != nr_cpu_ids)
return 0;
- ret = dpti_program_this_cpu(NULL);
+ ret = dpti_program_on_cpu(cpu);
if (ret)
return ret;
@@ -669,7 +686,6 @@ static int dpti_online(unsigned int cpu)
static void dpti_offline(unsigned int cpu)
{
unsigned int new_cpu;
- bool enable;
u16 pkg_id;
if (!dpti_supported())
@@ -691,35 +707,14 @@ static void dpti_offline(unsigned int cpu)
* offline.
*/
new_cpu = cpumask_any_but(topology_core_cpumask(cpu), cpu);
- if (new_cpu < nr_cpu_ids) {
- enable = true;
- thermal_clear_package_intr_status(PACKAGE_LEVEL,
- PACKAGE_THERM_STATUS_DPTI_ACK);
-
- /*
- * We are here via CPU hotplug. Since we are holding the
- * cpu_hotplug_lock, @new_cpu cannot go offline and interrupts
- * are enabled, so the SMP function call is safe.
- *
- * The syscore suspend callback runs with interrupts disabled,
- * but it does not reach this path because all the secondary
- * CPUs are offline.
- */
- smp_call_function_single(new_cpu, dpti_config_ipi, &enable,
- true);
- }
/*
- * If hardware does not acknowledge the directed interrupt setup on
- * @new_cpu, disable the redirection. Since no other CPU is configured
- * to receive the package-level interrupt, all CPUs in the package will
- * receive it.
+ * dpti_program_on_cpu() undoes a setup that the hardware does not
+ * acknowledge on @new_cpu. With @cpu cleared below, no CPU is then
+ * configured to receive the package-level interrupt: all CPUs in the
+ * package receive it.
*/
- enable = false;
- if (new_cpu < nr_cpu_ids && dpti_wait_for_ack()) {
- smp_call_function_single(new_cpu, dpti_config_ipi, &enable,
- true);
-
+ if (new_cpu < nr_cpu_ids && dpti_program_on_cpu(new_cpu)) {
pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; reverting to broadcast\n",
cpu, new_cpu);
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 10/13] thermal: intel: dpti: Add rank-based owner selection
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (8 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 09/13] thermal: intel: dpti: Program the directed interrupt on the target CPU Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 11/13] thermal: intel: dpti: Serialize updates to the directed interrupt state Ricardo Neri
` (2 subsequent siblings)
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
Prepare for a sysfs interface that lets the user choose a CPU to own a
package's directed package-level thermal interrupt (DPTI). Add the state
and selection functionality for a pinned owner.
Implement a ranking mechanism to identify the best-suited CPU to own the
interrupt. The highest-ranked CPU in a package is the one the user selects
(DPTI_RANK_PINNED). All other CPUs are ranked as DPTI_RANK_ONLINE until
owner selection becomes isolation-aware.
On CPU hotplug events, re-evaluate which CPU owns the DPTI. If a
better-ranked CPU comes online, move the interrupt to that CPU. Likewise,
if the owner goes offline, find the next best-ranked CPU and move the
interrupt there. Both cases can occur if the user-selected CPU changes its
online status.
dpti_select_cpu() also reports the rank of its choice, which the online
path needs for its strict-improvement test. dpti_offline() ignores it until
isolation-aware selection warns about an isolated owner, but passes it
anyway rather than making the argument optional.
Route both hotplug callbacks through dpti_lookup_pkg_id() so that they
validate the package id the same way. dpti_online() needs only the
validation: let dpti_lookup_pkg_id() take a NULL pkg_id. dpti_online() now
returns the lookup's -ENODEV rather than -EINVAL: the CPU comes from the
hotplug core and is valid; what is missing is a per-package slot. The
caller only tests the value against zero.
With all CPUs ranked equally, behavior changes only after a failed
acknowledgment: the package then retries on its lowest-numbered online CPU
rather than on the CPU coming online.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 151 +++++++++++++++++++++++++++++++-----
1 file changed, 130 insertions(+), 21 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index e6ce443c0a9d..97c205dab1b6 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -559,9 +559,13 @@ static int dpti_program_on_cpu(unsigned int cpu)
* struct dpti_pkg - Per-package DPTI state
* @owner: CPU currently programmed to receive the package-level
* interrupt, or nr_cpu_ids when the package has no owner.
+ * @pin: CPU the user requested as the owner, or nr_cpu_ids for
+ * automatic selection. A pinned CPU overrides the automatic
+ * selection whenever it is online.
*/
struct dpti_pkg {
unsigned int owner;
+ unsigned int pin;
};
/*
@@ -596,6 +600,7 @@ static bool dpti_supported(void)
return true;
}
+/* @pkg_id may be NULL for a caller that only validates @cpu. */
static int dpti_lookup_pkg_id(unsigned int cpu, u16 *pkg_id)
{
u16 id;
@@ -607,7 +612,8 @@ static int dpti_lookup_pkg_id(unsigned int cpu, u16 *pkg_id)
if (id >= topology_max_packages())
return -ENODEV;
- *pkg_id = id;
+ if (pkg_id)
+ *pkg_id = id;
return 0;
}
@@ -655,44 +661,145 @@ static void dpti_clear_all(void)
}
}
-static int dpti_online(unsigned int cpu)
+/**
+ * enum dpti_rank - How suitable a CPU is as a DPTI owner
+ * @DPTI_RANK_NONE: No eligible CPU.
+ * @DPTI_RANK_ONLINE: An online CPU of the package (automatic choice).
+ * @DPTI_RANK_PINNED: Explicitly requested by the user; overrides any
+ * automatic choice.
+ *
+ * Higher is better; the selection picks the highest-ranked online CPU in the
+ * package.
+ */
+enum dpti_rank {
+ DPTI_RANK_NONE,
+ DPTI_RANK_ONLINE,
+ DPTI_RANK_PINNED,
+};
+
+static enum dpti_rank dpti_cpu_rank(unsigned int cpu)
{
- u16 pkg_id;
- int ret;
+ /* No concept of isolation yet: every online CPU ranks the same. */
+ return DPTI_RANK_ONLINE;
+}
- if (!dpti_supported())
- return 0;
+/*
+ * Select the owner of the package-level thermal interrupt for @pkg_cpu's
+ * package. @skip is excluded from the selection (e.g., if it is going offline).
+ * @rank receives the rank of the selected CPU.
+ *
+ * A user-pinned CPU takes precedence whenever it is online; otherwise the best
+ * ranked CPU in the package is chosen.
+ */
+static unsigned int dpti_select_cpu(unsigned int pkg_cpu, unsigned int skip,
+ enum dpti_rank *rank)
+{
+ const struct cpumask *pkg_mask = topology_core_cpumask(pkg_cpu);
+ enum dpti_rank best_rank = DPTI_RANK_NONE;
+ unsigned int cpu, pin, best = nr_cpu_ids;
- pkg_id = topology_logical_package_id(cpu);
- if (pkg_id >= topology_max_packages())
- return -EINVAL;
+ lockdep_assert_cpus_held();
+
+ /*
+ * Choose the user-pinned CPU if available: it is online and not going
+ * offline (as indicated by @skip).
+ */
+ pin = dpti_pkg_of_cpu_nocheck(pkg_cpu)->pin;
+ if (pin < nr_cpu_ids && pin != skip &&
+ cpumask_test_cpu(pin, pkg_mask)) {
+ *rank = DPTI_RANK_PINNED;
+ return pin;
+ }
+
+ for_each_cpu(cpu, pkg_mask) {
+ enum dpti_rank cpu_rank;
+
+ if (cpu == skip)
+ continue;
+
+ cpu_rank = dpti_cpu_rank(cpu);
+ if (cpu_rank > best_rank) {
+ best_rank = cpu_rank;
+ best = cpu;
+ /*
+ * Nothing outranks an online CPU in automatic selection.
+ */
+ if (best_rank == DPTI_RANK_ONLINE)
+ break;
+ }
+ }
+
+ *rank = best_rank;
+
+ return best;
+}
+
+/*
+ * Reselect the owner for @cpu's package and migrate to it, but only on a
+ * strict rank improvement.
+ */
+static int dpti_update_owner(unsigned int cpu)
+{
+ struct dpti_pkg *pkg = dpti_pkg_of_cpu_nocheck(cpu);
+ enum dpti_rank new_rank, old_rank;
+ unsigned int new_cpu, old_cpu;
+ int ret;
if (dpti_shut_down)
return 0;
- /* Another CPU in this package already owns the directed interrupt. */
- if (dpti_pkgs[pkg_id].owner != nr_cpu_ids)
+ old_cpu = pkg->owner;
+ new_cpu = dpti_select_cpu(cpu, nr_cpu_ids, &new_rank);
+ if (new_cpu == old_cpu)
return 0;
- ret = dpti_program_on_cpu(cpu);
+ old_rank = DPTI_RANK_NONE;
+ if (old_cpu < nr_cpu_ids)
+ old_rank = dpti_cpu_rank(old_cpu);
+ if (new_rank <= old_rank)
+ return 0;
+
+ ret = dpti_program_on_cpu(new_cpu);
if (ret)
return ret;
- dpti_pkgs[pkg_id].owner = cpu;
+ if (old_cpu < nr_cpu_ids)
+ dpti_clear_on_cpu(old_cpu);
+
+ pkg->owner = new_cpu;
return 0;
}
+static int dpti_online(unsigned int cpu)
+{
+ int ret;
+
+ if (!dpti_supported())
+ return 0;
+
+ /*
+ * Validate @cpu's package id on behalf of dpti_update_owner(), which
+ * indexes dpti_pkgs[] based on @cpu.
+ */
+ ret = dpti_lookup_pkg_id(cpu, NULL);
+ if (ret)
+ return ret;
+
+ /*
+ * A CPU coming online may be a better owner than the one currently
+ * programmed.
+ */
+ return dpti_update_owner(cpu);
+}
+
static void dpti_offline(unsigned int cpu)
{
+ enum dpti_rank new_rank;
unsigned int new_cpu;
u16 pkg_id;
- if (!dpti_supported())
- return;
-
- pkg_id = topology_logical_package_id(cpu);
- if (pkg_id >= topology_max_packages())
+ if (dpti_lookup_pkg_id(cpu, &pkg_id))
return;
if (dpti_shut_down)
@@ -706,7 +813,7 @@ static void dpti_offline(unsigned int cpu)
* The package-level interrupt must remain directed after this CPU goes
* offline.
*/
- new_cpu = cpumask_any_but(topology_core_cpumask(cpu), cpu);
+ new_cpu = dpti_select_cpu(cpu, cpu, &new_rank);
/*
* dpti_program_on_cpu() undoes a setup that the hardware does not
@@ -727,7 +834,7 @@ static void dpti_offline(unsigned int cpu)
*/
dpti_config_this_cpu(false);
- dpti_pkgs[pkg_id].owner = (new_cpu < nr_cpu_ids) ? new_cpu : nr_cpu_ids;
+ dpti_pkgs[pkg_id].owner = new_cpu;
}
/*
@@ -785,8 +892,10 @@ static __init void dpti_init(void)
if (!dpti_pkgs)
return;
- for (i = 0; i < topology_max_packages(); i++)
+ for (i = 0; i < topology_max_packages(); i++) {
dpti_pkgs[i].owner = nr_cpu_ids;
+ dpti_pkgs[i].pin = nr_cpu_ids;
+ }
register_syscore(&dpti_syscore);
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 11/13] thermal: intel: dpti: Serialize updates to the directed interrupt state
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (9 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 10/13] thermal: intel: dpti: Add rank-based owner selection Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 12/13] thermal: intel: dpti: Avoid nohz_full CPUs when possible Ricardo Neri
2026-11-10 3:36 ` [PATCH 13/13] thermal: intel: dpti: Add sysfs control of interrupt owner Ricardo Neri
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
dpti_pkgs[] is written from the CPU hotplug callbacks, from the init and
cleanup paths, and from the syscore callbacks. None of those can run
concurrently with one another: the array needs no lock of its own today.
A sysfs interface to select the owner will access dpti_pkgs[] concurrently
with the CPU hotplug callbacks. Introduce dpti_mutex to serialize them. The
mutex is always taken inside the CPU hotplug lock, never the other way
around.
dpti_shut_down needs the same protection. The syscore shutdown callback
sets it with CPU hotplug disabled, which keeps out the CPU hotplug
callbacks but not a sysfs writer. Set it under the mutex too.
No functional change intended: until the sysfs writer lands, there is only
one class of writer, so the mutex is never contended.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 30 +++++++++++++++++++++++-------
1 file changed, 23 insertions(+), 7 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index 97c205dab1b6..c418471cf8f4 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -26,6 +26,7 @@
#include <linux/export.h>
#include <linux/delay.h>
#include <linux/types.h>
+#include <linux/mutex.h>
#include <linux/init.h>
#include <linux/smp.h>
#include <linux/sysfs.h>
@@ -569,16 +570,17 @@ struct dpti_pkg {
};
/*
- * Accessed from CPU hotplug callbacks and from code that runs while CPU
- * hotplug is inactive: the init and cleanup paths as well as syscore callbacks.
- * No extra locking needed.
+ * Serializes updates to dpti_pkgs[] and dpti_shut_down. To avoid deadlock, a
+ * path that needs both this mutex and the CPU hotplug lock always takes the
+ * hotplug lock first.
*/
+static DEFINE_MUTEX(dpti_mutex);
+
static struct dpti_pkg *dpti_pkgs;
/*
- * Set by the syscore shutdown callback before it disables the directed
- * interrupt, so that no CPU hotplug operation enables it again on the way to
- * kexec.
+ * Set under dpti_mutex by the syscore shutdown callback before it disables
+ * DPTI, so that no owner update enables it again on the way to kexec.
*/
static bool dpti_shut_down;
@@ -624,6 +626,10 @@ static struct dpti_pkg *dpti_pkg_of_cpu_nocheck(unsigned int cpu)
return &dpti_pkgs[topology_logical_package_id(cpu)];
}
+/*
+ * Reads dpti_pkgs[] without dpti_mutex: the syscore callers run with
+ * interrupts disabled on the only online CPU.
+ */
static bool dpti_cpu_is_owner(unsigned int cpu)
{
u16 pkg_id;
@@ -647,6 +653,8 @@ static void dpti_clear_all(void)
if (!dpti_supported())
return;
+ guard(mutex)(&dpti_mutex);
+
/*
* No need to reset the owners: callers free dpti_pkgs[] or have shut
* DPTI down.
@@ -699,6 +707,7 @@ static unsigned int dpti_select_cpu(unsigned int pkg_cpu, unsigned int skip,
unsigned int cpu, pin, best = nr_cpu_ids;
lockdep_assert_cpus_held();
+ lockdep_assert_held(&dpti_mutex);
/*
* Choose the user-pinned CPU if available: it is online and not going
@@ -745,6 +754,8 @@ static int dpti_update_owner(unsigned int cpu)
unsigned int new_cpu, old_cpu;
int ret;
+ lockdep_assert_held(&dpti_mutex);
+
if (dpti_shut_down)
return 0;
@@ -786,6 +797,8 @@ static int dpti_online(unsigned int cpu)
if (ret)
return ret;
+ guard(mutex)(&dpti_mutex);
+
/*
* A CPU coming online may be a better owner than the one currently
* programmed.
@@ -802,6 +815,8 @@ static void dpti_offline(unsigned int cpu)
if (dpti_lookup_pkg_id(cpu, &pkg_id))
return;
+ guard(mutex)(&dpti_mutex);
+
if (dpti_shut_down)
return;
@@ -866,7 +881,8 @@ static int dpti_syscore_suspend(void *data)
static void dpti_syscore_shutdown(void *data)
{
- dpti_shut_down = true;
+ scoped_guard(mutex, &dpti_mutex)
+ dpti_shut_down = true;
dpti_clear_all();
}
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 12/13] thermal: intel: dpti: Avoid nohz_full CPUs when possible
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (10 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 11/13] thermal: intel: dpti: Serialize updates to the directed interrupt state Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
2026-11-10 3:36 ` [PATCH 13/13] thermal: intel: dpti: Add sysfs control of interrupt owner Ricardo Neri
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
A single CPU of each package owns the directed package-level thermal
interrupt (DPTI). Automatic selection ranks every online CPU of the
package equally. Consequently, the package thermal interrupt could be
directed to a CPU isolated via the nohz_full= or isolcpus=nohz
command-line parameters.
Directing the interrupt to a nohz_full CPU degrades its tickless isolation.
Make the automatic ranking isolation-aware: split DPTI_RANK_ONLINE into
DPTI_RANK_HOUSEKEEPING for CPUs in the HK_TYPE_KERNEL_NOISE mask and
DPTI_RANK_ISOLATED for the nohz_full-isolated ones.
The CPU hotplug online callback reselects and migrates the DPTI on rank
improvement: a housekeeping CPU that comes online later takes over from an
isolated one that claimed the package earlier in boot.
A migration that the hardware does not acknowledge leaves the current owner
in place. Warn once about it rather than failing the online callback, whose
message would report the package as broadcasting. This cannot happen during
boot, so the boot-time teardown is unaffected.
Warn once when a package has to fall back to an isolated CPU.
Isolation changes at runtime via cpuset isolated partitions are not
tracked.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
drivers/thermal/intel/therm_throt.c | 47 ++++++++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 9 deletions(-)
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index c418471cf8f4..c8c91a621cf6 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -17,6 +17,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <linux/sched/isolation.h>
#include <linux/syscore_ops.h>
#include <linux/interrupt.h>
#include <linux/notifier.h>
@@ -672,7 +673,8 @@ static void dpti_clear_all(void)
/**
* enum dpti_rank - How suitable a CPU is as a DPTI owner
* @DPTI_RANK_NONE: No eligible CPU.
- * @DPTI_RANK_ONLINE: An online CPU of the package (automatic choice).
+ * @DPTI_RANK_ISOLATED: nohz_full-isolated (last resort).
+ * @DPTI_RANK_HOUSEKEEPING: Not isolated (ideal automatic choice).
* @DPTI_RANK_PINNED: Explicitly requested by the user; overrides any
* automatic choice.
*
@@ -681,14 +683,21 @@ static void dpti_clear_all(void)
*/
enum dpti_rank {
DPTI_RANK_NONE,
- DPTI_RANK_ONLINE,
+ DPTI_RANK_ISOLATED,
+ DPTI_RANK_HOUSEKEEPING,
DPTI_RANK_PINNED,
};
static enum dpti_rank dpti_cpu_rank(unsigned int cpu)
{
- /* No concept of isolation yet: every online CPU ranks the same. */
- return DPTI_RANK_ONLINE;
+ /*
+ * Prefer a HK_TYPE_KERNEL_NOISE CPU to avoid disturbing nohz_full
+ * isolated CPUs. The mask is boot-static. No locking needed.
+ */
+ if (housekeeping_cpu(cpu, HK_TYPE_KERNEL_NOISE))
+ return DPTI_RANK_HOUSEKEEPING;
+
+ return DPTI_RANK_ISOLATED;
}
/*
@@ -731,9 +740,10 @@ static unsigned int dpti_select_cpu(unsigned int pkg_cpu, unsigned int skip,
best_rank = cpu_rank;
best = cpu;
/*
- * Nothing outranks an online CPU in automatic selection.
+ * Nothing outranks a housekeeping CPU in automatic
+ * selection.
*/
- if (best_rank == DPTI_RANK_ONLINE)
+ if (best_rank == DPTI_RANK_HOUSEKEEPING)
break;
}
}
@@ -743,6 +753,13 @@ static unsigned int dpti_select_cpu(unsigned int pkg_cpu, unsigned int skip,
return best;
}
+static void dpti_warn_if_isolated(unsigned int cpu, enum dpti_rank rank)
+{
+ if (rank == DPTI_RANK_ISOLATED)
+ pr_warn_once("Package %u thermal interrupt directed to nohz_full-isolated CPU%u; no housekeeping CPU available\n",
+ topology_logical_package_id(cpu), cpu);
+}
+
/*
* Reselect the owner for @cpu's package and migrate to it, but only on a
* strict rank improvement.
@@ -771,13 +788,18 @@ static int dpti_update_owner(unsigned int cpu)
return 0;
ret = dpti_program_on_cpu(new_cpu);
- if (ret)
+ if (ret) {
+ if (old_cpu < nr_cpu_ids)
+ pr_warn_once("Failed to redirect package thermal interrupt from CPU%u to CPU%u; keeping CPU%u\n",
+ old_cpu, new_cpu, old_cpu);
return ret;
+ }
if (old_cpu < nr_cpu_ids)
dpti_clear_on_cpu(old_cpu);
pkg->owner = new_cpu;
+ dpti_warn_if_isolated(new_cpu, new_rank);
return 0;
}
@@ -801,9 +823,14 @@ static int dpti_online(unsigned int cpu)
/*
* A CPU coming online may be a better owner than the one currently
- * programmed.
+ * programmed. If the hardware does not acknowledge it, the current
+ * owner keeps the interrupt: a valid outcome, already reported.
*/
- return dpti_update_owner(cpu);
+ ret = dpti_update_owner(cpu);
+ if (ret && dpti_pkg_of_cpu_nocheck(cpu)->owner < nr_cpu_ids)
+ return 0;
+
+ return ret;
}
static void dpti_offline(unsigned int cpu)
@@ -841,6 +868,7 @@ static void dpti_offline(unsigned int cpu)
cpu, new_cpu);
new_cpu = nr_cpu_ids;
+ new_rank = DPTI_RANK_NONE;
}
/*
@@ -850,6 +878,7 @@ static void dpti_offline(unsigned int cpu)
dpti_config_this_cpu(false);
dpti_pkgs[pkg_id].owner = new_cpu;
+ dpti_warn_if_isolated(new_cpu, new_rank);
}
/*
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH 13/13] thermal: intel: dpti: Add sysfs control of interrupt owner
2026-11-10 3:36 [PATCH 00/13] thermal: intel: dpti: Direct the package thermal interrupt to a chosen CPU Ricardo Neri
` (11 preceding siblings ...)
2026-11-10 3:36 ` [PATCH 12/13] thermal: intel: dpti: Avoid nohz_full CPUs when possible Ricardo Neri
@ 2026-11-10 3:36 ` Ricardo Neri
12 siblings, 0 replies; 14+ messages in thread
From: Ricardo Neri @ 2026-11-10 3:36 UTC (permalink / raw)
To: Rafael J. Wysocki, Daniel Lezcano, Zhang Rui, Lukasz Luba,
Srinivas Pandruvada
Cc: linux-pm, linux-api, linux-kernel, ricardo.neri, Ricardo Neri
Override automatic selection and let the user choose a CPU to own a
package's directed package-level thermal interrupt (DPTI). Add a boolean
attribute:
/sys/devices/system/cpu/cpuX/thermal_throttle/package_interrupt_owner
Reading it reports whether the CPU owns its package's interrupt. Writing 1
migrates the interrupt to the CPU and pins it there. Writing 0 releases the
pin. Documentation/ABI/testing describes the semantics in full.
The read format matches the other boolean attributes under
/sys/devices/system/cpu. The write side uses kstrtobool(), as
umwait_control/enable_c02 does.
A user who isolates a CPU at runtime, through a cpuset isolated partition,
can move the interrupt away with this attribute. A pin to a
nohz_full-isolated CPU is honored with a warning, rate-limited because the
write is repeatable.
The attribute is created before the hardware is programmed, and a
programming failure during boot tears DPTI down for good. Remove the
attribute from the CPUs that already got it: otherwise it outlives the
feature it controls, failing every access with -ENODEV, while CPUs coming
online later get no such file at all. A failure after boot is different:
DPTI stays up, and the attribute keeps reporting hardware state truthfully.
The other thermal_throttle attributes need no equivalent teardown, as the
CPUID features and boot parameter gating them cannot be withdrawn at
runtime.
Although the owner is a per-package property, keep the implementation
simple and report ownership through the per-CPU thermal_throttle directory.
A CPU's file is a stateless view onto the same per-package state. This
mirrors the package_throttle_count attribute.
Signed-off-by: Ricardo Neri <ricardo.neri-calderon@linux.intel.com>
---
checkpatch warns that the msleep(5) can sleep for up to 20ms and suggests
usleep_range(). The 5ms is taken from lock_device_hotplug_sysfs(), which
answers a contended hotplug lock with the same trylock, sleep and
restart_syscall() sequence. Sleeping longer than asked only delays the
retry of a contended write.
---
Documentation/ABI/testing/sysfs-devices-system-cpu | 40 ++++++
drivers/thermal/intel/therm_throt.c | 139 +++++++++++++++++++++
2 files changed, 179 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-devices-system-cpu b/Documentation/ABI/testing/sysfs-devices-system-cpu
index 545687ebadcc..73dd75c9004a 100644
--- a/Documentation/ABI/testing/sysfs-devices-system-cpu
+++ b/Documentation/ABI/testing/sysfs-devices-system-cpu
@@ -830,3 +830,43 @@ Description:
degradations due to underlying physical CPU contention.
See Documentation/scheduler/sched-paravirt.rst for more details.
+
+What: /sys/devices/system/cpu/cpuX/thermal_throttle/package_interrupt_owner
+Date: November 2026
+Contact: Linux power management list <linux-pm@vger.kernel.org>
+Description:
+ (RW) Boolean. On CPUs supporting the directed package-level
+ thermal interrupt, a single CPU per package is programmed to
+ receive it. This attribute reports and controls whether cpuX is
+ that CPU.
+
+ Reading returns 1 if cpuX currently owns the package thermal
+ interrupt, 0 otherwise. The owner is per package: at most one
+ CPU of a package reads 1, and every CPU of the package reads 0
+ while the package has no owner. A package has no owner until one
+ is programmed, and none if the hardware does not acknowledge
+ one. On Intel processors, a package without an owner broadcasts
+ the thermal interrupt to all of its CPUs. If such a failure
+ happens while the kernel boots, the attribute does not exist on
+ any CPU.
+
+ Writing 1 (also 'y' or 'on') migrates the interrupt to cpuX and
+ pins it there, overriding automatic selection. The request is
+ honored even if cpuX is nohz_full-isolated; a warning is logged
+ in that case, since a directed interrupt disturbs the isolated
+ CPU. The write fails if the hardware does not acknowledge the
+ new owner.
+
+ Writing 0 (also 'n' or 'off') releases a pin on cpuX and returns
+ the package to automatic selection, which prefers CPUs that are
+ not nohz_full-isolated. If cpuX is nohz_full-isolated and such a
+ preferred CPU of the package is online, the interrupt moves
+ there, provided the hardware acknowledges the new owner.
+ Otherwise cpuX remains the owner and keeps reading 1. Writing 0
+ to a CPU that is not pinned has no effect.
+
+ The pin survives offlining. While a pinned CPU is offline this
+ attribute does not exist for it, and automatic selection hands
+ the interrupt to another CPU of the package, which reads 1 in
+ the meantime. The interrupt returns to the pinned CPU as soon as
+ it comes back online.
diff --git a/drivers/thermal/intel/therm_throt.c b/drivers/thermal/intel/therm_throt.c
index c8c91a621cf6..1688f6362f6f 100644
--- a/drivers/thermal/intel/therm_throt.c
+++ b/drivers/thermal/intel/therm_throt.c
@@ -18,6 +18,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/sched/isolation.h>
+#include <linux/sched/signal.h>
#include <linux/syscore_ops.h>
#include <linux/interrupt.h>
#include <linux/notifier.h>
@@ -760,6 +761,13 @@ static void dpti_warn_if_isolated(unsigned int cpu, enum dpti_rank rank)
topology_logical_package_id(cpu), cpu);
}
+static void dpti_warn_if_pinned_isolated(unsigned int cpu)
+{
+ if (dpti_cpu_rank(cpu) == DPTI_RANK_ISOLATED)
+ pr_warn_ratelimited("Package %u thermal interrupt pinned to nohz_full-isolated CPU%u\n",
+ topology_logical_package_id(cpu), cpu);
+}
+
/*
* Reselect the owner for @cpu's package and migrate to it, but only on a
* strict rank improvement.
@@ -881,6 +889,121 @@ static void dpti_offline(unsigned int cpu)
dpti_warn_if_isolated(new_cpu, new_rank);
}
+/*
+ * The DPTI owner is a per-package property, but it is exposed through the
+ * existing per-CPU thermal_throttle sysfs group.
+ *
+ * Reading reports whether @cpu is the owner programmed in hardware, not
+ * whether it is pinned. A released pin therefore still reads 1 until the
+ * placement changes, and while a pinned CPU is offline the remaining CPUs
+ * report the automatically chosen fallback.
+ */
+static ssize_t package_interrupt_owner_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ unsigned int cpu = dev->id;
+ bool is_owner;
+ u16 pkg_id;
+ int ret;
+
+ ret = dpti_lookup_pkg_id(cpu, &pkg_id);
+ if (ret)
+ return ret;
+
+ scoped_guard(mutex, &dpti_mutex)
+ is_owner = dpti_pkgs[pkg_id].owner == cpu;
+
+ return sysfs_emit(buf, "%u\n", is_owner);
+}
+
+static int dpti_pin_owner(unsigned int cpu)
+{
+ struct dpti_pkg *pkg = dpti_pkg_of_cpu_nocheck(cpu);
+ unsigned int old_pin;
+ int ret;
+
+ guard(mutex)(&dpti_mutex);
+
+ /* The pin outranks every automatic choice, so it always migrates. */
+ old_pin = pkg->pin;
+ pkg->pin = cpu;
+
+ /*
+ * A failed write must not change policy or get silently retried on the
+ * next hotplug event.
+ */
+ ret = dpti_update_owner(cpu);
+ if (ret)
+ pkg->pin = old_pin;
+ else
+ dpti_warn_if_pinned_isolated(cpu);
+
+ return ret;
+}
+
+static void dpti_unpin_owner(unsigned int cpu)
+{
+ struct dpti_pkg *pkg = dpti_pkg_of_cpu_nocheck(cpu);
+
+ guard(mutex)(&dpti_mutex);
+
+ if (pkg->pin != cpu)
+ return;
+
+ pkg->pin = nr_cpu_ids;
+
+ /*
+ * Without the pin, a housekeeping CPU of the package outranks an
+ * isolated owner. Migrate rather than wait for a re-evaluation that may
+ * never come. A hardware acknowledgment failure leaves the interrupt in
+ * place, which is still a valid outcome of releasing a pin.
+ */
+ dpti_update_owner(cpu);
+}
+
+static ssize_t package_interrupt_owner_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ unsigned int cpu = dev->id;
+ bool want_pin;
+ int ret;
+
+ ret = dpti_lookup_pkg_id(cpu, NULL);
+ if (ret)
+ return ret;
+
+ ret = kstrtobool(buf, &want_pin);
+ if (ret)
+ return ret;
+
+ /*
+ * The selection reads the package's online CPUs; the programming runs
+ * remotely on the CPU it picks. Both need the CPU hotplug lock, which
+ * this write cannot block on: offlining @cpu holds it while
+ * sysfs_remove_group() waits for this write to release its reference on
+ * the attribute. Restart the syscall instead, which drops that
+ * reference and lets the offline proceed. Avoid busy looping with a 5ms
+ * sleep between tries. lock_device_hotplug_sysfs() follows the same
+ * approach.
+ */
+ if (!cpus_read_trylock()) {
+ msleep(5);
+ return restart_syscall();
+ }
+
+ if (want_pin)
+ ret = dpti_pin_owner(cpu);
+ else
+ dpti_unpin_owner(cpu);
+
+ cpus_read_unlock();
+
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_RW(package_interrupt_owner);
+
/*
* CPU0 may own the directed interrupt, but the CPU hotplug callbacks are not
* called for CPU0 during suspend and resume.
@@ -947,9 +1070,17 @@ static __init void dpti_init(void)
static void dpti_cleanup(void)
{
+ unsigned int cpu;
+
if (!dpti_supported())
return;
+ /* The attribute must not outlive the per-package state. */
+ for_each_online_cpu(cpu)
+ sysfs_remove_file_from_group(&get_cpu_device(cpu)->kobj,
+ &dev_attr_package_interrupt_owner.attr,
+ thermal_attr_group.name);
+
unregister_syscore(&dpti_syscore);
dpti_clear_all();
kfree(dpti_pkgs);
@@ -966,6 +1097,14 @@ static int thermal_throttle_add_dev(struct device *dev, unsigned int cpu)
if (err)
return err;
+ if (dpti_supported()) {
+ err = sysfs_add_file_to_group(&dev->kobj,
+ &dev_attr_package_interrupt_owner.attr,
+ thermal_attr_group.name);
+ if (err)
+ goto del_group;
+ }
+
if (cpu_has(c, X86_FEATURE_PLN) && int_pln_enable) {
err = sysfs_add_file_to_group(&dev->kobj,
&dev_attr_core_power_limit_count.attr,
--
2.43.0
^ permalink raw reply [flat|nested] 14+ messages in thread