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:
Yu Watanabe
2026-05-21 00:39:47 +09:00
committed by GitHub
10 changed files with 1403 additions and 10 deletions
+9
View File
@@ -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);
}
+1
View File
@@ -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);