* [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load
@ 2026-10-24 2:24 Amir Noohi
2026-10-24 2:24 ` [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state() Amir Noohi
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Amir Noohi @ 2026-10-24 2:24 UTC (permalink / raw)
To: Thomas Gleixner, Marc Zyngier, Oliver Upton
Cc: Radu Rendec, Igor Velkov, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Christoffer Dall, kvmarm, linux-arm-kernel,
linux-kernel, Amir Noohi
KVM/arm64 vCPUs slow each other down even when every vCPU has a physical
CPU to itself. On a 32 CPU Ampere Altra (GICv3, VHE) running mainline,
with an in-kernel GICv3 and each vCPU thread pinned to its own otherwise
idle CPU, a vCPU which exits to userspace in a loop manages 320k exits/s
on its own, 120k when 16 vCPUs do the same and 59k with 31. The aggregate
rate stops growing at 8 vCPUs. No two vCPUs share a CPU and nothing is
over-subscribed.
The cause is the interrupt descriptor lock of the host timer interrupts.
Every vCPU load sets their active state with irq_set_irqchip_state()
(twice on VHE: virtual and physical timer). The timer interrupts are per
CPU interrupts, so all CPUs share one descriptor and its lock, while the
state being written is per CPU (a redistributor register on GICv3, a
system register on GICv5). The lock serializes nothing but CPUs updating
their own state. At 8 vCPUs, 99.9% of all lock contention events during
the test have __irq_get_desc_lock() or irq_set_irqchip_state() on the
stack.
Patch 1 adds irq_set_percpu_irqchip_state(), which sets the state of the
calling CPU's instance of a per CPU devid interrupt without taking the
descriptor lock. Patch 2 makes the KVM timer use it. With the series the
same test gets 326k exits/s per vCPU at 31 vCPUs (5.6x), the per vCPU
rate no longer depends on the number of vCPUs, the aggregate rate goes
from 1.8M to 10.1M exits/s, and at 8 vCPUs no lock contention event has
either function on the stack.
The series is based on tip/irq/urgent, as it builds on
__irq_set_irqchip_state() from Igor Velkov's patch 717862eac6ef
("genirq/kexec: Do not take the bus lock of an interrupt chip on the
crash path") [1]. It also applies cleanly to v7.3, current mainline and
kvmarm/next on top of that patch.
I'm aware this arrives during the merge window. There is no urgency on
my side; I'm posting it now so it can be looked at after -rc1, and I'm
happy to rebase and resend then.
Results
-------
Reproducer: a /dev/kvm program (no guest OS) which makes N vCPUs exit to
userspace in a loop, each KVM_RUN being one vCPU load/put, each vCPU
thread pinned to its own otherwise idle CPU and running from its own
guest page, with an in-kernel GICv3 or a userspace irqchip.
The test program is at
https://gist.github.com/amirmnoohi/91560854a63cd3e5fc7a359eb5e17302
32 CPU Ampere Altra (Neoverse-N1, GICv3, VHE), mainline 864c07de0c7d
(v7.4 merge window) with and without the series, same kernel config,
exits/s per vCPU, mean of three 5 second runs:
in-kernel GICv3 userspace irqchip
vCPUs before after before after
1 319694 324584 319670 345088
2 298808 326063 289274 343884
4 294895 328420 284269 343528
8 278579 328736 112423 343984
16 119943 328887 56120 282905
24 77267 327478 36948 253529
31 58722 326056 28114 250586
Aggregate with the in-kernel GICv3: 2.23M exits/s at 8 vCPUs and 1.82M
at 31 before the series, 2.63M and 10.1M with it. With a userspace
irqchip, which also clears the active state after every exit, the rate
with the series still drops to 251k per vCPU at 31 vCPUs (runs varied by
about 8% there); I have not investigated that.
Lock contention events within 5 seconds at 8 vCPUs with
__irq_get_desc_lock() or irq_set_irqchip_state() on the stack: 122800 of
122847 (in-kernel GICv3) and 7865871 of 7865907 (userspace irqchip)
before, none of 49 and 24 with the series.
If all vCPUs instead run from one shared guest page, the aggregate rate
with the series stops at about 3.6M exits/s from 16 vCPUs on this
machine (also with the timer's GIC writes removed altogether, as an
experiment), and the extra time goes to guest context (front-end
stalls) rather than to the host. Measured that way, the series gains
2.0x at 31 vCPUs instead of 5.6x.
A second machine shows the same: on a 384 CPU HiSilicon Kunpeng 950
(GICv3, VHE, 6.6 based distribution kernel, userspace irqchip), 8 vCPUs
on 8 idle CPUs ran 2.4x slower each than a single vCPU, and 69% of the
time spent in irq_set_irqchip_state() was spent waiting for the lock.
Under QEMU TCG (virt, -cpu max), 8 vCPUs, the series removes every
acquisition of the descriptor lock on this path (LOCK_STAT, 10 seconds):
host irqchip irq_desc_lock_class before after
acquisitions contentions
GICv3, VHE in-kernel 393878 36682 none
GICv3, nVHE in-kernel 199654 32080 none
GICv3, nVHE userspace 429928 122152 none
GICv2, VHE userspace 321430 59542 none
Throughput under TCG does not change, so the real hardware numbers
above are the ones that matter.
Testing
-------
- Built for arm64 (defconfig, debug config), x86_64 (defconfig,
allnoconfig) and i386 (defconfig) without new warnings, no new W=1 or
sparse warnings (arch_timer.c has two pre-existing sparse warnings),
checkpatch clean, bisectable.
- On the Ampere Altra, with and without the series: KVM selftests
arch_timer (8 and 31 vCPUs migrating every 2ms), arch_timer_edge_cases,
vgic_irq and vgic_init pass.
- Under QEMU with PROVE_LOCKING, DEBUG_PREEMPT and DEBUG_ATOMIC_SLEEP:
arch_timer, vgic_irq, vgic_init, vgic_lpi_stress, kvm_create_max_vcpus
and steal_time pass on GICv3 VHE and GICv3 nVHE hosts (on a GICv2 host
the vGICv3 based tests skip, the others pass), without warnings or
lockdep splats.
- Not tested: GICv5 (no hardware), hosts with no_hw_deactivation
(Apple), protected mode.
- Not changed: on hosts without a vGIC (e.g. Raspberry Pi 3), KVM
toggles the timer interrupt with enable/disable_percpu_irq(), which
take the same descriptor lock. Those hosts have few CPUs and their
interrupt controllers do not implement irq_set_irqchip_state(), so
the series leaves that path alone.
[1] https://lore.kernel.org/r/20261015-kexec-irq-bus-lock-v2-1-2af15f0757b6@iav.lv
Amir Noohi (2):
genirq/manage: Provide irq_set_percpu_irqchip_state()
KVM: arm64: timer: Avoid the irq descriptor lock on vCPU load
arch/arm64/kvm/arch_timer.c | 2 +-
include/linux/interrupt.h | 2 ++
kernel/irq/manage.c | 36 ++++++++++++++++++++++++++++++++++++
3 files changed, 39 insertions(+), 1 deletion(-)
base-commit: 717862eac6eff0eea8ac30a0c0de0bed84922aab
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state()
2026-10-24 2:24 [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load Amir Noohi
@ 2026-10-24 2:24 ` Amir Noohi
2026-10-24 12:21 ` Marc Zyngier
2026-10-24 2:24 ` [PATCH 2/2] KVM: arm64: timer: Avoid the irq descriptor lock on vCPU load Amir Noohi
2026-10-24 11:52 ` [PATCH 0/2] genirq, KVM: arm64: " Marc Zyngier
2 siblings, 1 reply; 7+ messages in thread
From: Amir Noohi @ 2026-10-24 2:24 UTC (permalink / raw)
To: Thomas Gleixner, Marc Zyngier, Oliver Upton
Cc: Radu Rendec, Igor Velkov, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Christoffer Dall, kvmarm, linux-arm-kernel,
linux-kernel, Amir Noohi
irq_set_irqchip_state() acquires the bus lock and the descriptor lock of
the interrupt. A per CPU devid interrupt has a single descriptor for all
CPUs, while its irqchip state is per CPU, e.g. each GICv3 redistributor
holds the active and pending state of the PPIs of its own CPU and GICv5
keeps that state in per CPU system registers. Callers which modify that
state have to be pinned to the CPU anyway, so the descriptor lock only
serializes CPUs which update their own private state.
KVM on arm64 sets the active state of the host timer interrupts on every
vCPU load, i.e. on every KVM_RUN ioctl and every time a vCPU thread is
scheduled in. When many CPUs load vCPUs at the same time, they all spin
on the descriptor lock of the same interrupt, even when every vCPU has
a physical CPU of its own. A global serialization point on the same path
was removed before in commit 64b6d1d7a845 ("genirq: Get rid of global
lock in irq_do_set_affinity()").
Provide irq_set_percpu_irqchip_state(), which sets the irqchip state of
a per CPU devid interrupt on the calling CPU without acquiring the
descriptor lock. The irqchip callback is still invoked with interrupts
disabled. The caller has to have preemption disabled and has to ensure
that the interrupt is neither freed nor reconfigured concurrently, which
is the case for an interrupt it has requested. Interrupt chips which
require a bus lock are rejected, as the bus lock may sleep and cannot be
taken with preemption disabled.
The function is not exported as its only user, KVM on arm64, cannot be
built as a module.
Signed-off-by: Amir Noohi <amirmnoohi@gmail.com>
---
include/linux/interrupt.h | 2 ++
kernel/irq/manage.c | 36 ++++++++++++++++++++++++++++++++++++
2 files changed, 38 insertions(+)
diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index 3bf969ad8fe0..ab20129615d4 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -507,6 +507,8 @@ extern int irq_get_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
bool *state);
extern int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
bool state);
+int irq_set_percpu_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
+ bool state);
#ifdef CONFIG_IRQ_FORCED_THREADING
# ifdef CONFIG_PREEMPT_RT
diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
index bdb47837c66f..e76fe1cec9fb 100644
--- a/kernel/irq/manage.c
+++ b/kernel/irq/manage.c
@@ -2777,6 +2777,42 @@ int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which, bool v
}
EXPORT_SYMBOL_GPL(irq_set_irqchip_state);
+/**
+ * irq_set_percpu_irqchip_state - Set the irqchip state of a per CPU interrupt
+ * @irq: Linux interrupt number of a per CPU devid interrupt
+ * @which: State to be set (one of IRQCHIP_STATE_*)
+ * @val: Value corresponding to @which
+ *
+ * Variant of irq_set_irqchip_state() which sets the state of the calling
+ * CPU's instance of a per CPU devid interrupt without acquiring the
+ * interrupt descriptor lock. The descriptor and its lock are shared by all
+ * CPUs while the state is not, so acquiring the lock only serializes CPUs
+ * which modify their own state.
+ *
+ * Must be called with preemption disabled on an interrupt which has been
+ * requested, so that the interrupt chip hierarchy cannot change
+ * concurrently. Interrupt chips which require a bus lock are not supported.
+ *
+ * Return: 0 on success, a negative error code otherwise.
+ */
+int irq_set_percpu_irqchip_state(unsigned int irq, enum irqchip_irq_state which, bool val)
+{
+ struct irq_desc *desc = irq_to_desc(irq);
+ struct irq_chip *chip;
+
+ lockdep_assert_preemption_disabled();
+
+ if (WARN_ON_ONCE(!desc || !irq_settings_is_per_cpu_devid(desc)))
+ return -EINVAL;
+
+ chip = irq_desc_get_chip(desc);
+ if (WARN_ON_ONCE(chip->irq_bus_lock || chip->irq_bus_sync_unlock))
+ return -EINVAL;
+
+ guard(irqsave)();
+ return __irq_set_irqchip_state(desc, which, val);
+}
+
/**
* irq_has_action - Check whether an interrupt is requested
* @irq: The linux irq number
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state()
2026-10-24 2:24 ` [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state() Amir Noohi
@ 2026-10-24 12:21 ` Marc Zyngier
2026-10-24 21:28 ` Amir Noohi
0 siblings, 1 reply; 7+ messages in thread
From: Marc Zyngier @ 2026-10-24 12:21 UTC (permalink / raw)
To: Amir Noohi
Cc: Thomas Gleixner, Oliver Upton, Radu Rendec, Igor Velkov,
Fuad Tabba, Joey Gouly, Steffen Eiden, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, Mark Rutland,
Christoffer Dall, kvmarm, linux-arm-kernel, linux-kernel
On Sat, 24 Oct 2026 03:24:49 +0100,
Amir Noohi <amirmnoohi@gmail.com> wrote:
>
> irq_set_irqchip_state() acquires the bus lock and the descriptor lock of
> the interrupt. A per CPU devid interrupt has a single descriptor for all
> CPUs, while its irqchip state is per CPU, e.g. each GICv3 redistributor
> holds the active and pending state of the PPIs of its own CPU and GICv5
> keeps that state in per CPU system registers. Callers which modify that
> state have to be pinned to the CPU anyway, so the descriptor lock only
> serializes CPUs which update their own private state.
>
> KVM on arm64 sets the active state of the host timer interrupts on every
> vCPU load, i.e. on every KVM_RUN ioctl and every time a vCPU thread is
> scheduled in. When many CPUs load vCPUs at the same time, they all spin
> on the descriptor lock of the same interrupt, even when every vCPU has
> a physical CPU of its own. A global serialization point on the same path
> was removed before in commit 64b6d1d7a845 ("genirq: Get rid of global
> lock in irq_do_set_affinity()").
>
> Provide irq_set_percpu_irqchip_state(), which sets the irqchip state of
> a per CPU devid interrupt on the calling CPU without acquiring the
> descriptor lock. The irqchip callback is still invoked with interrupts
> disabled. The caller has to have preemption disabled and has to ensure
> that the interrupt is neither freed nor reconfigured concurrently, which
> is the case for an interrupt it has requested. Interrupt chips which
> require a bus lock are rejected, as the bus lock may sleep and cannot be
> taken with preemption disabled.
>
> The function is not exported as its only user, KVM on arm64, cannot be
> built as a module.
>
> Signed-off-by: Amir Noohi <amirmnoohi@gmail.com>
> ---
> include/linux/interrupt.h | 2 ++
> kernel/irq/manage.c | 36 ++++++++++++++++++++++++++++++++++++
> 2 files changed, 38 insertions(+)
>
> diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
> index 3bf969ad8fe0..ab20129615d4 100644
> --- a/include/linux/interrupt.h
> +++ b/include/linux/interrupt.h
> @@ -507,6 +507,8 @@ extern int irq_get_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
> bool *state);
> extern int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
> bool state);
> +int irq_set_percpu_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
> + bool state);
>
> #ifdef CONFIG_IRQ_FORCED_THREADING
> # ifdef CONFIG_PREEMPT_RT
> diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
> index bdb47837c66f..e76fe1cec9fb 100644
> --- a/kernel/irq/manage.c
> +++ b/kernel/irq/manage.c
> @@ -2777,6 +2777,42 @@ int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which, bool v
> }
> EXPORT_SYMBOL_GPL(irq_set_irqchip_state);
>
> +/**
> + * irq_set_percpu_irqchip_state - Set the irqchip state of a per CPU interrupt
> + * @irq: Linux interrupt number of a per CPU devid interrupt
> + * @which: State to be set (one of IRQCHIP_STATE_*)
> + * @val: Value corresponding to @which
> + *
> + * Variant of irq_set_irqchip_state() which sets the state of the calling
> + * CPU's instance of a per CPU devid interrupt without acquiring the
> + * interrupt descriptor lock. The descriptor and its lock are shared by all
> + * CPUs while the state is not, so acquiring the lock only serializes CPUs
> + * which modify their own state.
> + *
> + * Must be called with preemption disabled on an interrupt which has been
> + * requested, so that the interrupt chip hierarchy cannot change
> + * concurrently. Interrupt chips which require a bus lock are not supported.
> + *
> + * Return: 0 on success, a negative error code otherwise.
> + */
> +int irq_set_percpu_irqchip_state(unsigned int irq, enum irqchip_irq_state which, bool val)
> +{
> + struct irq_desc *desc = irq_to_desc(irq);
> + struct irq_chip *chip;
> +
> + lockdep_assert_preemption_disabled();
> +
> + if (WARN_ON_ONCE(!desc || !irq_settings_is_per_cpu_devid(desc)))
> + return -EINVAL;
> +
> + chip = irq_desc_get_chip(desc);
> + if (WARN_ON_ONCE(chip->irq_bus_lock || chip->irq_bus_sync_unlock))
> + return -EINVAL;
> +
> + guard(irqsave)();
> + return __irq_set_irqchip_state(desc, which, val);
> +}
> +
> /**
> * irq_has_action - Check whether an interrupt is requested
> * @irq: The linux irq number
It really feels like a leap of faith.
There is nothing that says that it is safe to call this without
holding the global lock. For all we know, drivers have been relying on
it for a good decade, and I don't feel confident letting things rip
just because it (mostly) works with the GIC.
At the very least, something exposed by the irqchip (a per-IRQ flag?)
should tell you whether it is safe to elide the lock.
Thanks,
M.
--
Jazz isn't dead. It just smells funny.
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state()
2026-10-24 12:21 ` Marc Zyngier
@ 2026-10-24 21:28 ` Amir Noohi
0 siblings, 0 replies; 7+ messages in thread
From: Amir Noohi @ 2026-10-24 21:28 UTC (permalink / raw)
To: Marc Zyngier
Cc: Thomas Gleixner, Oliver Upton, Radu Rendec, Igor Velkov,
Fuad Tabba, Joey Gouly, Steffen Eiden, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, Mark Rutland,
Christoffer Dall, kvmarm, linux-arm-kernel, linux-kernel
Hi Marc,
On Sat, 24 Oct 2026 13:21:00 +0100, Marc Zyngier <maz@kernel.org> wrote:
> It really feels like a leap of faith.
>
> There is nothing that says that it is safe to call this without
> holding the global lock. For all we know, drivers have been relying on
> it for a good decade, and I don't feel confident letting things rip
> just because it (mostly) works with the GIC.
>
> At the very least, something exposed by the irqchip (a per-IRQ flag?)
> should tell you whether it is safe to elide the lock.
Agreed, and v2 does that. There's a new IRQCHIP_PERCPU_STATE_LOCKLESS
flag, and irq_set_percpu_irqchip_state() only skips the descriptor lock
if the chip implementing irq_set_irqchip_state() has it set. Otherwise,
or if there's a bus lock, it just calls irq_set_irqchip_state(), so
drivers that don't opt in see no change.
Only GICv3 sets it, in a separate patch. For SGIs/PPIs/EPPIs its
callback only touches the local redistributor (gic_data_rdist_sgi_base()
and the RWP poll), nothing shared. FWIW, irq-gic.c, irq-gic-v3.c and
irq-gic-v5.c are the only drivers with both per-CPU devid interrupts and
an irq_set_irqchip_state() callback. KVM's own timer chip (GICv5, no HW
deactivation) doesn't set it either, so those keep the lock.
On GICv2 in QEMU it still takes the lock like mainline does (333k vs
321k acquisitions in lock_stat, within 4%), and on GICv3 it doesn't
take it at all. arch_timer, arch_timer_edge_cases, vgic_irq and
vgic_init pass on the Altra. Under QEMU with lockdep, DEBUG_PREEMPT and
DEBUG_ATOMIC_SLEEP, the selftests pass on GICv3 (VHE and nVHE) and
GICv2 (the vGICv3 ones skip there). No splats.
I made it a chip flag since it's about the callback, but I can make it
per-IRQ if you prefer.
Thanks,
Amir
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/2] KVM: arm64: timer: Avoid the irq descriptor lock on vCPU load
2026-10-24 2:24 [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load Amir Noohi
2026-10-24 2:24 ` [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state() Amir Noohi
@ 2026-10-24 2:24 ` Amir Noohi
2026-10-24 11:52 ` [PATCH 0/2] genirq, KVM: arm64: " Marc Zyngier
2 siblings, 0 replies; 7+ messages in thread
From: Amir Noohi @ 2026-10-24 2:24 UTC (permalink / raw)
To: Thomas Gleixner, Marc Zyngier, Oliver Upton
Cc: Radu Rendec, Igor Velkov, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Christoffer Dall, kvmarm, linux-arm-kernel,
linux-kernel, Amir Noohi
When the host interrupt controller has an active state, every vCPU load
sets the active state of the host timer interrupts with
irq_set_irqchip_state(): once for the virtual timer and, on VHE, once
more for the physical timer. On GICv5 hosts the state is also cleared on
every vCPU put, and with a userspace irqchip it is also cleared after a
guest exit when the virtual timer is not pending.
The timer interrupts are per CPU interrupts, so all CPUs share a single
interrupt descriptor and irq_set_irqchip_state() acquires its lock. CPUs
which load vCPUs concurrently therefore serialize on that lock, although
each of them only writes its own redistributor (and, when setting the
active state on GICv3, reads it back while holding the lock) or, on
GICv5, a system register.
This does not require over-subscription. On a 32 CPU Ampere Altra
(Neoverse-N1, GICv3, VHE) with an in-kernel GICv3, a test which makes N
vCPUs exit to userspace in a loop, each vCPU thread pinned to its own
otherwise idle CPU and running from its own guest page, gets 319694
exits/s from a single vCPU, but only 119943 per vCPU with 16 vCPUs and
58722 with 31. The aggregate rate stops growing at 8 vCPUs. At 8 vCPUs,
122800 of the 122847 lock contention events within 5 seconds had
__irq_get_desc_lock() or irq_set_irqchip_state() on the stack.
Use irq_set_percpu_irqchip_state(), which does not acquire the
descriptor lock. All callers of set_timer_irq_phys_active() run with
preemption disabled, and the timer interrupts are requested in
kvm_timer_hyp_init() and never freed afterwards. On hosts where KVM
stacks its own interrupt chip on top of the timer interrupts, its
irq_set_irqchip_state() callback masks or unmasks the parent interrupt
on the calling CPU, which is equally CPU local.
Same test and kernel configuration, exits/s per vCPU, mean of three 5
second runs:
in-kernel GICv3 userspace irqchip
vCPUs before after before after
1 319694 324584 319670 345088
8 278579 328736 112423 343984
16 119943 328887 56120 282905
31 58722 326056 28114 250586
With an in-kernel GICv3 the per vCPU rate no longer depends on the
number of vCPUs, and at 8 vCPUs no lock contention events have either
function on the stack.
Fixes: b103cc3f10c0 ("KVM: arm/arm64: Avoid timer save/restore in vcpu entry/exit")
Signed-off-by: Amir Noohi <amirmnoohi@gmail.com>
---
arch/arm64/kvm/arch_timer.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/arch/arm64/kvm/arch_timer.c b/arch/arm64/kvm/arch_timer.c
index 6ac3321f4c57..a9103625dea3 100644
--- a/arch/arm64/kvm/arch_timer.c
+++ b/arch/arm64/kvm/arch_timer.c
@@ -679,7 +679,7 @@ static void timer_restore_state(struct arch_timer_context *ctx)
static inline void set_timer_irq_phys_active(struct arch_timer_context *ctx, bool active)
{
int r;
- r = irq_set_irqchip_state(ctx->host_timer_irq, IRQCHIP_STATE_ACTIVE, active);
+ r = irq_set_percpu_irqchip_state(ctx->host_timer_irq, IRQCHIP_STATE_ACTIVE, active);
WARN_ON(r);
}
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load
2026-10-24 2:24 [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load Amir Noohi
2026-10-24 2:24 ` [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state() Amir Noohi
2026-10-24 2:24 ` [PATCH 2/2] KVM: arm64: timer: Avoid the irq descriptor lock on vCPU load Amir Noohi
@ 2026-10-24 11:52 ` Marc Zyngier
2026-10-24 21:23 ` Amir Noohi
2 siblings, 1 reply; 7+ messages in thread
From: Marc Zyngier @ 2026-10-24 11:52 UTC (permalink / raw)
To: Amir Noohi
Cc: Thomas Gleixner, Oliver Upton, Radu Rendec, Igor Velkov,
Fuad Tabba, Joey Gouly, Steffen Eiden, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, Mark Rutland, kvmarm,
linux-arm-kernel, linux-kernel
On Sat, 24 Oct 2026 03:24:48 +0100,
Amir Noohi <amirmnoohi@gmail.com> wrote:
>
> KVM/arm64 vCPUs slow each other down even when every vCPU has a physical
> CPU to itself. On a 32 CPU Ampere Altra (GICv3, VHE) running mainline,
> with an in-kernel GICv3 and each vCPU thread pinned to its own otherwise
> idle CPU, a vCPU which exits to userspace in a loop manages 320k exits/s
> on its own, 120k when 16 vCPUs do the same and 59k with 31. The aggregate
> rate stops growing at 8 vCPUs. No two vCPUs share a CPU and nothing is
> over-subscribed.
>
> The cause is the interrupt descriptor lock of the host timer interrupts.
> Every vCPU load sets their active state with irq_set_irqchip_state()
> (twice on VHE: virtual and physical timer). The timer interrupts are per
> CPU interrupts, so all CPUs share one descriptor and its lock, while the
> state being written is per CPU (a redistributor register on GICv3, a
> system register on GICv5). The lock serializes nothing but CPUs updating
> their own state. At 8 vCPUs, 99.9% of all lock contention events during
> the test have __irq_get_desc_lock() or irq_set_irqchip_state() on the
> stack.
Looking at the test case, this really isn't a representative or
realistic workload. This is a contrived setup that has trapping to
userspace as its only purpose.
I'm not saying that this is useless observation. But it is not
something that represents something exacerbated by "real" VMs.
I'll look at the patches shortly.
Thanks,
M.
--
Jazz isn't dead. It just smells funny.
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load
2026-10-24 11:52 ` [PATCH 0/2] genirq, KVM: arm64: " Marc Zyngier
@ 2026-10-24 21:23 ` Amir Noohi
0 siblings, 0 replies; 7+ messages in thread
From: Amir Noohi @ 2026-10-24 21:23 UTC (permalink / raw)
To: Marc Zyngier
Cc: Thomas Gleixner, Oliver Upton, Radu Rendec, Igor Velkov,
Fuad Tabba, Joey Gouly, Steffen Eiden, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, Mark Rutland, kvmarm,
linux-arm-kernel, linux-kernel
Hi Marc,
On Sat, 24 Oct 2026 12:52:44 +0100, Marc Zyngier <maz@kernel.org> wrote:
> Looking at the test case, this really isn't a representative or
> realistic workload. This is a contrived setup that has trapping to
> userspace as its only purpose.
>
> I'm not saying that this is useless observation. But it is not
> something that represents something exacerbated by "real" VMs.
Fair enough. I checked with real Linux guests running hackbench on the
same Altra (kvmtool, in-kernel GICv3). With one vCPU per CPU, and in
lighter hackbench runs, it makes no measurable difference, but it does
once busy vCPUs share CPUs.
With four 31-vCPU VMs on 31 CPUs (124 vCPUs), all running
"hackbench -g 31 -f 20 -l 2000", each VM takes 76.5s on mainline and
60.4s with the series, so 21% less time (8 runs per kernel, no
overlap). In a profiled run irq_set_irqchip_state() was called ~2.4M
times/s and 47% of those calls hit contention. With the series the
contention on that path drops to ~7 per second.
So the case that matters is overcommitted hosts with busy vCPUs. I'll
make that the main justification in v2 instead of the exit-to-userspace
test.
Thanks,
Amir
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-10-24 21:28 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-24 2:24 [PATCH 0/2] genirq, KVM: arm64: Avoid the irq descriptor lock on vCPU load Amir Noohi
2026-10-24 2:24 ` [PATCH 1/2] genirq/manage: Provide irq_set_percpu_irqchip_state() Amir Noohi
2026-10-24 12:21 ` Marc Zyngier
2026-10-24 21:28 ` Amir Noohi
2026-10-24 2:24 ` [PATCH 2/2] KVM: arm64: timer: Avoid the irq descriptor lock on vCPU load Amir Noohi
2026-10-24 11:52 ` [PATCH 0/2] genirq, KVM: arm64: " Marc Zyngier
2026-10-24 21:23 ` Amir Noohi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®