Linux-Kernel-Tuning für KI-Server: Was wirklich etwas bringt

Der Standard-Ubuntu-Kernel auf einem 6.8- oder 6.11-HWE-Stack genügt für rund 80% aller KI-Workloads. Dieser Artikel richtet sich an die anderen 20% – Multi-Socket-EPYC mit acht GPUs, eine vLLM-Serving-Box, die unter Last gegen die vm.max_map_count -Wand läuft, oder ein Inferenz-Server, dessen Tail-Latenz von CPU-C-State-Übergängen aufgefressen wird.

Die meisten öffentlich verfügbaren Ratschläge zum Thema „Linux-Tuning für KI“ sind recycelte Datenbank-Empfehlungen aus 2015 – und ein nicht unerheblicher Teil davon verschlechtert die Inferenzlatenz still und leise. Nachfolgend der kleinere, ehrliche Satz an Änderungen, die auf einer Kentino AI Box dieser Klasse (4–8 GPUs auf Xeon oder EPYC, Ubuntu 22.04 / 24.04, Kernel 6.x) tatsächlich etwas bringen. Treiber und CUDA-Stack als installiert vorausgesetzt, gemäß L01 und L02.

Die ehrliche Rangfolge der Gewinne

Vor jeder sysctl-Änderung sollten Sie wissen, wie groß der Preis überhaupt ist:

Änderung Typischer Gewinn bei Inferenz / Training
NUMA-bewusste Prozessplatzierung 10–30% auf Multi-Socket-Boxen
CPU-Governor performance (statt powersave) 5–15% auf Serving-Boxen, niedrigere TTFT
Tiefe C-States deaktivieren Mikrosekunden weniger beim P99, +30–50 W Leerlauf je Sockel
THP auf madvise 1–5% bei PyTorch, weniger Stalls unter Speicherdruck
vm.max_map_count, ulimit -n Verhindert, dass das Serving unter Last umkippt
IRQ-Affinität zum lokalen NUMA-Knoten 5–15% auf vernetzten DataLoader- / RDMA-Pfaden
Dimensionierung der TCP-Puffer (rmem_max, wmem_max) Relevant nur bei Storage / Streaming über >25 GbE
Alles Übrige 0–3%, meist Rauschen

Wenn Sie nur eines aus diesem Artikel mitnehmen: NUMA-Bewusstsein und der CPU-Governor sind die beiden großen Hebel. Alles andere ist Rundungsfehler, solange Sie nicht gemessen haben.

CPU-Governor: die billigsten 5–15%, die es gibt

Ubuntu nutzt je nach Treiber standardmäßig powersave (intel_pstate) oder ondemand (acpi_cpufreq). Beide takten reaktiv hoch, was 5–15% bei der Inferenz-TTFT und bei der CPU-seitigen Vorbereitung rund um einen vLLM-Forward-Pass kostet – Tokenisierung, Scheduling, Sampling der Logits.

Für eine Serving-Box setzen Sie performance und vergessen es:

sudo apt install -y cpufrequtils
echo 'GOVERNOR="performance"' | sudo tee /etc/default/cpufrequtils
sudo systemctl restart cpufrequtils

# Prüfen
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do cat $c; done | sort -u

Auf EPYC Genoa / Turin mit Kernel 6.5+ ersetzt der Treiber amd-pstate den Treiber acpi-cpufreq. Dieselbe Idee: Setzen Sie scaling_governor auf performance. Der neuere Modus amd-pstate-epp erhält die Performance und erlaubt zugleich Boosting je Kern; belassen Sie EPP auf performance, nicht auf balance_performance.

Einschränkung: Auf einer Trainings-Box bei dauerhaft ~100% GPU-Auslastung sind die CPUs ohnehin ausgelastet, und der Governor fällt weniger ins Gewicht. Der große Gewinn liegt bei Inferenz-Servern, die zwischen Anfragen im Leerlauf sind und sofort hochtakten müssen.

NUMA: der Unterschied zwischen einer schnellen und einer mittelmäßigen EPYC-Box

Bei Single-Socket-Servern mit Xeon oder EPYC gibt es nichts zu überlegen – ein NUMA-Knoten, jeder Speicherzugriff ist lokal. Bei Dual-Socket-Boxen liegen die 10–30%.

Der Standard-Scheduler platziert einen vLLM-Worker klaglos auf Sockel 0 und lässt ihn in Speicher hineinlaufen, der auf Sockel 1 alloziert wurde – jeder Ladevorgang ist ein Sprung über UPI bzw. Infinity Fabric. Bei einem Modell mit Hunderten GB Gewichten, die pro Token einmal durchlaufen werden, ist das spürbar.

Prüfen:

numactl --hardware                            # Knotenzahl, Größen, Distanzmatrix
nvidia-smi topo -m                            # GPU↔CPU-NUMA-Affinität
cat /sys/class/net/<iface>/device/numa_node   # NUMA-Affinität der NIC

Das gewünschte Muster – und das, was NCCL automatisch erkennt – ist: GPU N ist an den NUMA-Knoten gebunden, der ihren PCIe-Root-Complex beherbergt, nicht an den anderen Sockel. Bei einem manuell gestarteten Inferenzprozess binden Sie CPU und Speicher an den lokalen Knoten:

# vLLM auf einem EPYC mit 2 Sockeln, GPUs 0–3 auf NUMA-Knoten 0
numactl --cpunodebind=0 --membind=0 \
  python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.3-70B-Instruct --tensor-parallel-size 4

Zwei verwandte Stellschrauben:

  • kernel.numa_balancing verschiebt Speicherseiten in Richtung der CPU, die sie anfasst. Für allgemeine Workloads hilfreich, für KI gelegentlich kontraproduktiv: Ein Prefill läuft die Gewichte einmal durch, der Kernel migriert die Seiten, die nächste Anfrage holt sie zurück. Standardmäßig aktiviert lassen (1); korreliert der Jitter mit numa_pte_updates in /proc/vmstat, probieren Sie sysctl kernel.numa_balancing=0.
  • NCCL auf Dual-Socket – setzen Sie NCCL_SOCKET_IFNAME auf die Management-NIC und NCCL_IB_HCA auf die NIC im selben NUMA-Knoten wie die GPUs. Die falsche NIC halbiert den Durchsatz zwischen den Knoten stillschweigend.

Single-Socket-Server mit EPYC 9004/9005: Überspringen Sie diesen ganzen Abschnitt.

Transparent Huge Pages: madvise, nicht always

THP fasst 4-KB-Seiten opportunistisch zu 2-MB-Seiten zusammen und senkt so den TLB-Druck. PyTorch und der CUDA-Treiber profitieren moderat. Die Falle ist der Modus always bei Speicherdruck – der Kernel blockiert einen Thread 50–100 ms lang zur Speicherkompaktierung und torpediert damit jedes Latenz-SLO.

Die richtige Einstellung ist madvise: Anwendungen, die wissen, was sie tun, rufen madvise(MADV_HUGEPAGE) für langlebige Allokationen auf und bekommen THP; alle anderen nicht. Einstellung über /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=madvise"

update-grub && reboot. Defrag sollte dazu passen – defer+madvise lässt die Kompaktierung im Hintergrund laufen statt im Pfad des allozierenden Threads:

echo defer+madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

Für vLLM / SGLang / TensorRT-LLM im Produktivbetrieb ist madvise der richtige Standard. PyTorch mit CUDA Unified Memory (Forschungscode) profitiert manchmal von always – messen Sie, bevor Sie umschalten.

Explizite 1-GB-Hugepages: nur, wenn Sie gemessen haben

Explizit beim Booten über hugetlbfs reservierte Hugepages bringen bei sehr großen LLM-Gewichten einen kleinen zusätzlichen Gewinn, weil sie TLB-Misses beim Durchlaufen der Gewichte eliminieren. Der Haken: Der Speicher wird vorab reserviert und steht dem restlichen System nicht zur Verfügung, die Anwendung muss dafür gebaut sein, und der Gewinn liegt bei den meisten Serving-Workloads bei 1–3%.

GRUB_CMDLINE_LINUX_DEFAULT="... default_hugepagesz=1G hugepagesz=1G hugepages=64"

Lohnt sich bei einer TensorRT-LLM-Box, deren Engine in gepinnten Speicher passt und bei der Sie dem letzten Prozent TTFT hinterherjagen. Lohnt sich nicht bei einem allgemeinen vLLM-Server, der Modelle wechselt, oder bei irgendeiner Trainings-Box. Beim ersten Aufbau überspringen; nur dann erneut ansehen, wenn perf stat -e dTLB-load-misses TLB-Druck zeigt und Sie RAM-Reserven haben.

vm.max_map_count und nofile: die Wände, gegen die vLLM bei Skalierung läuft

Der Standardwert von vm.max_map_count unter Ubuntu ist 65530 – die Zahl der einzelnen Speicher-Mappings, die ein Prozess halten kann. vLLM, das ein großes Modell mit hoher Parallelität bedient, oder jedes Framework, das viele per mmapeingebundene safetensors-Shards nutzt, überschreitet das und stirbt mit Cannot allocate memory. Der von Elasticsearch stammende Wert 262144 ist das absolute Minimum; für vLLM mit 70B+ und Hunderten paralleler Sequenzen sollten Sie auf 1048576 gehen. Kostet nichts – ein Soft-Limit, keine Reservierung.

Der Standardwert von ulimit -n unter Ubuntu ist 1024. Lachhaft niedrig: vLLM, Triton, PyTorch-DataLoader-Worker, NCCL und die gRPC-Schicht dazwischen öffnen jeweils Hunderte von File-Deskriptoren. Wer das Limit erreicht, bekommt EMFILE: too many open files und einen Prozess, der sich stillschweigend aufhängt.

# /etc/sysctl.d/99-ai-server.conf
vm.max_map_count = 1048576

# /etc/security/limits.d/99-ai-server.conf
*    soft    nofile  1048576
*    hard    nofile  1048576

# /etc/systemd/system.conf.d/99-limits.conf
[Manager]
DefaultLimitNOFILE=1048576

sudo sysctl --system und systemctl daemon-reexec. Prüfen mit ulimit -n und cat /proc/<pid>/limits.

IRQ-Affinität: NIC-Interrupts an lokale CPUs binden

Auf einer 100-GbE-NIC vom Typ ConnectX-6/7, die dauerhaft 10+ GB/s Datensatz-Streaming oder RDMA-Traffic bewältigt, müssen die Interrupts auf CPUs landen, die (a) im selben NUMA-Knoten wie die NIC liegen und (b) nicht dieselben Kerne sind, auf denen DataLoader-Worker laufen. Der Standard irqbalance macht das passabel; unter hoher Last nicht mehr.

Die sauberste Lösung ist das Skript set_irq_affinity.sh von Mellanox aus mlnx-tools:

sudo systemctl disable --now irqbalance
sudo /usr/sbin/set_irq_affinity.sh enp1s0f0                  # alle NIC-lokalen Kerne
sudo /usr/sbin/set_irq_affinity_cpulist.sh 4-11 enp1s0f0     # an bestimmte Kerne binden

Falsch ist es, irqbalance auf einer Box weiterlaufen zu lassen, auf der Sie zusätzlich manuell gepinnt haben – die beiden arbeiten gegeneinander. Entscheiden Sie sich. Serving-Box mit einer oder zwei RDMA-NICs: manuell pinnen, irqbalanceabschalten. Allzweck-Box mit vielen Interfaces: irqbalance anlassen und mit --banirq die kritische NIC ausnehmen.

Netzwerkpuffer im Kernel: nur für schnelle Storage- / Streaming-Pfade

Für einen Single-Node-Inferenzserver, der über einfaches TCP mit moderaten Raten mit Clients spricht, reichen die Standardwerte von net.core.rmem_max / wmem_max mit 208 KB aus. Für einen Knoten, der Trainingsdaten von 100-GbE-NFS oder aus einem Object Store zieht, oder für ein vLLM-Frontend hinter einem Load Balancer mit hoher Anfragerate sind die Standardwerte die Obergrenze Ihres Bandwidth-Delay-Produkts. Ein Startpunkt für eine Box mit 100-GbE-Anbindung:

# /etc/sysctl.d/99-ai-network.conf
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 65535

Maximal 256 MB ist für 100 GbE innerhalb des Racks übertrieben (das BDP liegt bei ~1.25 MB), aber harmlos – das TCP-Auto-Tuning vergrößert die Puffer nur nach Bedarf bis zur Obergrenze. Bei RDMA (RoCE / IB) auf dem Datenpfad spielen diese Stellschrauben keine Rolle – RDMA umgeht den TCP-Stack des Kernels. Für die Management-Ebene, NFS und Modell-Downloads bleiben sie relevant.

C-States: Latenz gegen Leerlaufleistung

Für eine latenzsensible Serving-Box ist das Abschalten tiefer C-States nach dem CPU-Governor die günstigste Verbesserung der Tail-Latenz. Ein- und Austritt aus C6 liegen im Bereich zehner Mikrosekunden; bei einer Anfrage, die in 30 ms antworten soll, erzeugt eine CPU, die für das Sampling nach dem Token aus C6 aufwacht, spürbaren Jitter.

GRUB_CMDLINE_LINUX_DEFAULT="... intel_idle.max_cstate=1 processor.max_cstate=1"

update-grub && reboot. Prüfen mit cpupower idle-info – nur C0 und C1 sollten verfügbar sein. Preis: 30–50 W Leerlaufleistung je Sockel , weil die Kerne nie in den Tiefschlaf gehen. Auf einem Dual-Socket-Server mit 8 GPUs sind das 60–100 W permanenter Overhead – belanglos gegenüber 3.5–4.5 kW dauerhafter GPU-Aufnahme unter Last.

Anwenden auf latenzsensible Inferenzserver und Echtzeit-Robotikpfade. Überspringen bei Trainings-Boxen (Latenz spielt keine Rolle, Leerlaufleistung summiert sich über Monate) und bei Batch-Jobs.

Dateisystembezogenes Kernel-Tuning (Vorschau auf L04)

Eine Handvoll fs.* -sysctls ist relevant:

fs.aio-max-nr = 1048576              # Standard 65536 ist für vLLM-Weight-Loader zu niedrig
fs.inotify.max_user_watches = 524288 # Tooling, das Checkpoint-Verzeichnisse überwacht

fs.aio-max-nr ist der Wert, der wirklich beißt – Frameworks mit asynchronem I/O über viele Shards (DALI, vLLM-Weight-Loader) sprengen den Standardwert 65536 bei großen Modellen. Die Wahl des Dateisystems selbst (XFS vs. ZFS vs. ext4), die Mount-Optionen und die O_DIRECT -Semantik behandelt L04.

Nicht benötigte Kernel-Module deaktivieren

Eine headless betriebene Serving-Box hat keinen Grund, Module für Bluetooth, Sound, Webcam, WLAN, Joystick oder den Druckserver zu laden. Jedes davon ist zusätzlicher Code in der Angriffsfläche und eine kleine Bootverzögerung. Einzeln trivial; zusammengenommen ist eine aufgeräumte Box leichter zu durchschauen.

# /etc/modprobe.d/blacklist-ai-server.conf
blacklist bluetooth
blacklist btusb
blacklist snd_hda_intel
blacklist uvcvideo
blacklist joydev

# Zusätzlich
sudo systemctl disable --now bluetooth.service cups.service avahi-daemon.service \
                              ModemManager.service whoopsie.service apport.service

Der Bootvorgang sinkt bei einem typischen EPYC-Aufbau von ~30 s auf ~10 s, und lsmod wird wieder lesbar. Keinerlei Einfluss auf die Inferenzleistung, moderate Reduktion der Angriffsfläche.

Zwei Profile: Inferenz gegen Training

Die Tuning-Form unterscheidet sich tatsächlich. Vorlagen zum Übernehmen:

Inferenz (/etc/sysctl.d/99-ai-inference.conf)

vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 1
fs.aio-max-nr = 1048576
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 65535

Zusätzlich auf der Kernel-Kommandozeile: transparent_hugepage=madvise intel_idle.max_cstate=1 processor.max_cstate=1. Governor: performance. NIC-IRQs an NUMA-lokale Kerne gepinnt, irqbalance aus.

Training (/etc/sysctl.d/99-ai-training.conf)

vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 0       # Seitenmigration mitten in der Epoche ist nur Unruhe
fs.aio-max-nr = 1048576
fs.inotify.max_user_watches = 524288
net.core.rmem_max = 536870912
net.core.wmem_max = 536870912
net.ipv4.tcp_rmem = 4096 131072 536870912
net.ipv4.tcp_wmem = 4096 131072 536870912

Kernel-Kommandozeile: transparent_hugepage=madvise (C-State-Pinning weglassen – Training ist durchsatzgebunden, Leerlaufleistung summiert sich über Wochen). Governor: performance , sofern Ihr Leistungsbudget das hergibt. NCCL über NCCL_IB_HCAan den lokalen NUMA-Knoten gebunden.

Die Falle „Ich habe alles getunt, und es wurde langsamer“

Der mit Abstand häufigste Fehlermodus ist der Cargo-Cult-Dump: hundert sysctls aus einem Blogbeitrag kopieren, neu starten, schlechtere Performance feststellen und keine Ahnung haben, welche Stellschraube man zurücknehmen soll. Methode, die funktioniert:

  1. Basiswert ermitteln – nccl-tests all_reduce_perf, vLLM benchmark_throughput.py, Zeit pro Trainingsschritt. Zahlen aufschreiben.
  2. Genau eine Sache ändern.
  3. Denselben Benchmark erneut laufen lassen, dreimal wegen des Rauschens.
  4. Behalten, wenn der Median spürbar besser ist. Andernfalls zurücknehmen.
  5. Die Änderung und ihre Begründung dokumentieren, in der Versionsverwaltung neben der sysctl-Datei.

Wir haben Produktivsysteme mit 40 Zeilen sysctl-Tuning gesehen, die zusammengenommen 2% langsamer waren als die Ubuntu-Standardwerte – jede einzelne Änderung neutral oder schlechter, der Betreiber aber überzeugt: „Das Tuning hilft.“

Red Hats tuned 2.27 bringt explizite Profile ai-inference und ai-training mit und ist unter RHEL / Rocky eine glaubwürdige Abkürzung. Ubuntu liefert es nicht mit; apt install tuned funktioniert, ist aber weniger ausgefeilt. Unter Ubuntu sind selbst geschriebene Drop-ins in /etc/sysctl.d/ und eine saubere GRUB-Kommandozeile leichter zu prüfen, und Sie wissen genau, was gesetzt ist.

Wie Sie weiter vorgehen

Eine sinnvolle Reihenfolge für das Kernel-Tuning eines frischen Kentino AI Aufbaus, nach Priorität:

  1. CPU-Governor auf performancesetzen. Mit cpupower frequency-infoprüfen. Der größte Einzelgewinn, der nichts kostet.
  2. Führen Sie numactl --hardware und nvidia-smi topo -maus. Verstehen Sie die Topologie, bevor Sie irgendetwas pinnen. Planen Sie auf Dual-Socket-Boxen, welche GPUs zu welchem NUMA-Knoten gehören.
  3. Limits für vm.max_map_count und nofile setzen. Diese verhindern Ausfälle, keine Langsamkeit. Erledigen Sie sie vor dem ersten Produktivlauf.
  4. Setzen Sie transparent_hugepage=madvise auf der Kernel-Kommandozeile.
  5. NIC-IRQs mit set_irq_affinity.shan den lokalen NUMA-Knoten der NIC binden. irqbalance abschalten, wenn Sie das getan haben.
  6. Nur für Inferenz-Boxen: tiefe C-States über intel_idle.max_cstate=1 processor.max_cstate=1deaktivieren.
  7. Netzwerkpuffer tunen – aber nur, wenn Sie einen Engpass auf einem Storage- oder Streaming-Pfad mit 25/100 GbE gemessen haben.
  8. Explizite 1-GB-Hugepages überspringen , solange kein Benchmark TLB-Druck belegt.
  9. Vor und nach jeder Änderung benchmarken. Immer nur eine Sache auf einmal. Dokumentieren.

Querverweise: L01 für Treiber und Kernel-Pinning, L02 für den CUDA- / Container-Stack, L04 für die Dateisystemebene, L05 für das Monitoring.

Kernel-Tuning unter modernem Linux ist überwiegend eine einmalige Konfiguration, kein fortlaufendes Optimierungsspiel. Bringen Sie Governor, NUMA-Platzierung und Limits in Ordnung. Den Rest lassen Sie weg, sofern Ihr Benchmark nichts anderes sagt.


Dies ist Teil des Kentino Wiki, einer Referenzreihe zu KI-Compute, Robotik und den Systemen, die beides verbinden. Kommentare und Korrekturen willkommen unter info@kentino.com.