systemd-udevd: configure NIC IRQs CPU affinity (#40304)
# Context #40195 defines the initial proposal and the motivation behind this PR. This PR introduces 3 new options for `.link` files `[Link]` section: - `IRQAffinityPolicy=` - `IRQAffinity=` - `IRQAffinityNUMA=` The purpose is to allow `systemd-udevd` to configure a NIC's IRQs affinity to specific CPU(s). `IRQAffinityPolicy=` supports two policies: - `single`: assign all the NIC IRQs to CPU 0, or the first CPU in the CPU set resulting from the union of `IRQAffinity=` and `IRQAffinityNUMA=`. - `spread`: assign all the NIC IRQs to all the CPUs (or the union of `IRQAffinity=` and `IRQAffinityNUMA=` if defined) in a round-robin fashion while optimizing for cache locally while spreading apart queues on CPUs as much as possible. Both `IRQAffinity=` and `IRQAffinityNUMA=` behaves as filters to reduce the CPU set to assign IRQs to, and are only valid if `IRQAffinityPolicy=` is defined. # Spreading IRQs This section describes the algorithm responsible for spreading IRQs over different CPUs to maximize performance. ## 1. Discover CPU topology Read from `/sys/devices/system/cpu/cpu*/topology` to identify: - L3 cache domains (dies) - Physical cores VS hyperthreads - NUMA nodes - Core ordering within each die ``` Example: Dual-socket server with 2 dies per socket, 4 cores per die ┌─────────────────────────────────────────────────────────────────┐ │ NUMA Node 0 │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │ │ Die 0 (L3 Cache) │ │ Die 1 (L3 Cache) │ │ │ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │ │ │ │ C0 ││ C1 ││ C2 ││ C3 │ │ │ C4 ││ C5 ││ C6 ││ C7 │ │ │ │ │0,16││1,17││2,18││3,19│ │ │4,20││5,21││6,22││7,23│ │ │ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │ │ └─────────────────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ NUMA Node 1 │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │ │ Die 2 (L3 Cache) │ │ Die 3 (L3 Cache) │ │ │ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │ │ │ │ C8 ││ C9 ││C10 ││C11 │ │ │C12 ││C13 ││C14 ││C15 │ │ │ │ │8,24││9,25││10,2││11,2│ │ │12,2││13,2││14,3││15,3│ │ │ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │ │ └─────────────────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ (Numbers show CPU IDs: first HT, second HT) ``` ## 2. Filter the first hyperthread only Use only the first hyperthread of each physical core to avoid SMT contention. Two IRQs on sibling HTs contend for ALU/cache without cache benefit. ``` Before: CPUs 0-31 (16 cores × 2 HTs) After: CPUs 0-15 (first HT of each core) ``` ## 3. Equidistant permutations Reorder dies and CPUs so consecutive selections are maximally spread apart. ``` Original order: [0, 1, 2, 3] (adjacent dies/CPUs) | v Equidistant: [0, 2, 1, 3] (spread apart) ``` This ensures that even if only 2 IRQs are assigned, they land on dies 0 and 2 (not 0 and 1), maximizing physical distance. The permutation is also applied within each die: ``` Die permutation: Die0 -> Die2 -> Die1 -> Die3 Within each die: ┌───────────────────────────────────────────────┐ │ Die 0: [C0,C1,C2,C3] -> [C0,C2,C1,C3] │ │ Die 1: [C4,C5,C6,C7] -> [C4,C6,C5,C7] │ │ Die 2: [C8,C9,C10,C11] -> [C8,C10,C9,C11] │ │ Die 3: [C12,C13,C14,C15] -> [C12,C14,C13,C15] │ └───────────────────────────────────────────────┘ ``` ## 4. Round-robin selection across dies Pick one CPU from each die in rotation, following permuted order. ``` Round 1: Die0->C0 Die2->C8 Die1->C4 Die3->C12 | | | | v v v v IRQs: [IRQ0] [IRQ1] [IRQ2] [IRQ3] Round 2: Die0->C2 Die2->C10 Die1->C6 Die3->C14 | | | | v v v v IRQs: [IRQ4] [IRQ5] [IRQ6] [IRQ7] ``` If there are more IRQs than physical cores, this logic wraps around and reuse CPUs. Only the first hyperthread of each core is used to avoid cache line contention between queues. Closes #40195.
This commit is contained in:
@@ -81,3 +81,12 @@ int cmp_uint16(const uint16_t *a, const uint16_t *b) {
|
||||
|
||||
return CMP(*a, *b);
|
||||
}
|
||||
|
||||
int cmp_unsigned(const unsigned *a, const unsigned *b) {
|
||||
/* This is called from qsort()s inner loops. Correctly implemented qsort will never pass NULL so we
|
||||
just suppress the check via POINTER_MAY_BE_NULL instead of assert() to avoid the runtime cost. */
|
||||
POINTER_MAY_BE_NULL(a);
|
||||
POINTER_MAY_BE_NULL(b);
|
||||
|
||||
return CMP(*a, *b);
|
||||
}
|
||||
|
||||
@@ -44,3 +44,4 @@ void qsort_r_safe(void *base, size_t nmemb, size_t size, comparison_userdata_fn_
|
||||
|
||||
int cmp_int(const int *a, const int *b);
|
||||
int cmp_uint16(const uint16_t *a, const uint16_t *b);
|
||||
int cmp_unsigned(const unsigned *a, const unsigned *b);
|
||||
|
||||
Reference in New Issue
Block a user