Re: [PATCH v2 1/1] PCI: hv: Spread load across CPUs when selecting effective affinity CPU

From: Naman Jain

Date: Mon Oct 26 2026 - 04:57:10 EST




On 10/26/2026 10:29 AM, Michael Kelley wrote:
In a system with multiple devices with the same configuration (such as
NVMe controllers), the affinity masks are the same for all such devices.
Linux typicaly must select a single CPU from each mask to be the effective
affinity. If a fixed algorithm is used, such as picking the first CPU
in the mask, interrupts from multiple devices are assigned to the same
CPUs, while other CPUs are not assigned any interrupts, producing an
interrupt load imbalance.

On x86, the "APIC" IRQ chip avoids this problem by spreading the
interrupts across the CPUs in an affinity mask. It tracks the number
of IRQs assigned to each CPU, and chooses the CPU in the mask that
has the fewest assignments. Because Hyper-V vPCI device IRQs eventually
parent to APIC, the spreading done by APIC accrues to Hyper-V vPCI
devices, so all is good in x86 systems.

On arm64, the "ITS" IRQ chip for the GICv3+ITS does the same kind of
spreading for LPI interrupts. But Hyper-V provides guest VMs only with
a virtual GICv3, not including an ITS or LPI support. IRQs for vPCI
devices on arm64 are mapped to SPIs and parented to the "GICv3" IRQ
chip. The GICv3 IRQ chip always chooses the first CPU in the affinity
mask as the effective affinity, producing an interrupt load imbalance
for Hyper-V vPCI devices in arm64 systems.

The Hyper-V vPCI driver already has an arm64-specific IRQ chip to
allocate and managed SPIs for use by vPCI devices. Solve the imbalance
problem by extending this IRQ chip to implement a "set affinity"
function that tracks SPIs assigned to each CPU, and chooses the
least-loaded CPU as the effective affinity. Then invoke the parent
IRQ chip (GICv3) to actually set that single CPU as the effective
affinity in the virtual GICv3 hardware. As a result, IRQs are spread
across CPUs like on x86 and interrupt load imbalances are avoided.

Signed-off-by: Michael Kelley <mhklinux@xxxxxxxxxxx>

<snip>

+/*
+ * Track the count of SPIs assigned to each CPU. Pick a single CPU from the
+ * affinity mask based on the SPI loading data, and have the parent IRQ
+ * chip (should be "GICv3") set the effective affinity to that single CPU.
+ */
+static int hv_pci_arm64_set_affinity(struct irq_data *d,
+ const struct cpumask *mask_val,
+ bool force)
+{
+ unsigned int cpu, prev_cpu;
+ int ret;
+
+ prev_cpu = cpumask_first(irq_data_get_effective_affinity_mask(d));
+
+ scoped_guard(raw_spinlock_irqsave, &cpu_spi_count_lock) {
+ if (prev_cpu < nr_cpu_ids)
+ hv_pci_dec_spi_count(d, prev_cpu);

Nit: Would it be better to add this nr_cpu_ids check in hv_pci_dec_spi_count()/hv_pci_inc_spi_count() instead? I don't have a strong opinion on it.

+
+ if (force)
+ cpu = hv_pci_arm64_get_min_load(d, mask_val);
+ else
+ cpu = hv_pci_arm64_select_cpu(d, mask_val);
+
+ if (cpu >= nr_cpu_ids) {

At some places like this, you may still need to retain it.

+ if (prev_cpu < nr_cpu_ids)
+ hv_pci_inc_spi_count(d, prev_cpu);
+ return -EINVAL;
+ }
+
+ hv_pci_inc_spi_count(d, cpu);
+ }
+
+ ret = irq_chip_set_affinity_parent(d, cpumask_of(cpu), force);
+ if (ret < 0) {
+ guard(raw_spinlock_irqsave)(&cpu_spi_count_lock);
+
+ if (prev_cpu < nr_cpu_ids)
+ hv_pci_inc_spi_count(d, prev_cpu);
+ if (cpu < nr_cpu_ids)
+ hv_pci_dec_spi_count(d, cpu);
+ }
+
+ return ret;

<snip>

With or without above nit changes, for the patch -

Reviewed-by: Naman Jain <namjain@xxxxxxxxxxxxxxxxxxx>
Tested-by: Naman Jain <namjain@xxxxxxxxxxxxxxxxxxx>

Tested the patch on Azure ARM64 VM and verified the interrupt distribution and also saw soft lockups disappearing after optimal distribution of NVME - PCI interrupts produced by this patch.
Not repeating here, observations are similar to what I mentioned in the review of v1.

Regards,
Naman