Ladění linuxového jádra pro AI servery: co skutečně zabírá

Výchozí jádro Ubuntu na stacku HWE 6.8 nebo 6.11 je pro zhruba 80 % AI workloadů dostačující. Tento článek je pro zbývajících 20 % — vícesoketový EPYC pohánějící osm GPU, vLLM serving stroj, který pod zátěží narazí na limit vm.max_map_count, nebo inference server, jemuž tail latency ukusují přechody mezi C-stavy CPU.

Většina veřejných rad na „ladění Linuxu pro AI“ je recyklované databázové doporučení z roku 2015 a nemalá část z nich inference latenci tiše zhoršuje. Níže je menší, poctivá množina změn, které na stroji třídy Kentino AI (4–8 GPU na Xeonu nebo EPYC, Ubuntu 22.04 / 24.04, jádro 6.x) skutečně pomáhají. Předpokládáme, že ovladač i CUDA stack jsou nainstalované podle L01 a L02.

Poctivá hierarchie přínosů

Než sáhnete na jakýkoli sysctl, ujasněte si, o jak velkou výhru jde:

Změna Typický přínos pro inferenci / trénink
NUMA-aware umístění procesů 10–30 % na vícesoketových strojích
Governor CPU performance (z powersave) 5–15 % na serving strojích, nižší TTFT
Vypnutí hlubokých C-stavů Mikrosekundy dolů z P99, +30–50 W v idle / soket
THP nastavené na madvise 1–5 % na PyTorchi, méně stallů při fluktuaci paměti
vm.max_map_count, ulimit -n Zabrání pádu serving vrstvy pod zátěží
IRQ afinita na lokální NUMA uzel 5–15 % na síťových cestách DataLoaderu / RDMA
Dimenzování TCP bufferů (rmem_max, wmem_max) Důležité jen pro >25GbE úložiště / streaming
Vše ostatní 0–3 %, často jen šum

Pokud si z článku odnesete jedinou věc: Dva velké přínosy jsou NUMA a governor CPU. Všechno ostatní je zaokrouhlovací chyba, dokud si to nezměříte.

Governor CPU: nejlevnějších 5–15 %

Ubuntu má ve výchozím stavu powersave (intel_pstate) nebo ondemand (acpi_cpufreq) podle ovladače. Oba zvyšují takty reaktivně, což stojí 5–15 % na TTFT inference a na přípravě na straně CPU kolem forward passu vLLM — tokenizace, plánování, sampling logitů.

Na serving stroji nastavte performance a zapomeňte na to:

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

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

Na EPYC Genoa / Turin s jádrem 6.5+ nahrazuje ovladač amd-pstate ovladač acpi-cpufreq. Stejný princip, nastavte scaling_governor na performance. Novější režim amd-pstate-epp udržuje výkon a zároveň umožňuje boostování jednotlivých jader; EPP nechte na performance, ne na balance_performance.

Výhrada: na trénovacím stroji s trvalým ~100% vytížením GPU jsou procesory zatížené tak jako tak a na governoru tolik nezáleží. Velká výhra je u inference serverů, které mezi požadavky zahálejí a musí naskočit okamžitě.

NUMA: rozdíl mezi rychlým a průměrným EPYC strojem

U jednosoketových serverů s Xeonem nebo EPYC není co řešit — jeden NUMA uzel, každý přístup do paměti je lokální. Těch 10–30 % se skrývá u dvousoketových strojů.

Výchozí plánovač klidně umístí vLLM worker na soket 0 a nechá ho page-faultovat do paměti alokované na soketu 1 — každé čtení je skok přes UPI / Infinity Fabric mezi sokety. U modelu se stovkami GB vah, které se procházejí jednou na token, je to znát.

Zkontrolujte:

numactl --hardware                            # node count, sizes, distance matrix
nvidia-smi topo -m                            # GPU↔CPU NUMA affinity
cat /sys/class/net/<iface>/device/numa_node   # NIC NUMA affinity

Vzorec, který chcete — a který NCCL detekuje automaticky — je GPU N připnuté k NUMA uzlu hostujícímu jeho PCIe root complex, ne k druhému soketu. U ručně spuštěného inference procesu připněte k lokálnímu uzlu CPU i paměť:

# vLLM on a 2-socket EPYC, GPUs 0–3 on NUMA node 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

Dva související parametry:

  • kernel.numa_balancing migruje stránky blíž k CPU, které s nimi pracuje. U běžných workloadů pomáhá, u AI je občas kontraproduktivní: prefill projde váhy jednou, jádro stránky přemigruje a další požadavek si je stáhne zpět. Ve výchozím stavu nechte zapnuté (1); pokud jitter koreluje s numa_pte_updates v /proc/vmstat, zkuste sysctl kernel.numa_balancing=0.
  • NCCL na dvousoketu — nastavte NCCL_SOCKET_IFNAME na management NIC a NCCL_IB_HCA na NIC na stejném NUMA uzlu jako GPU. Špatná NIC tiše sníží propustnost mezi uzly na polovinu.

Jednosoketový server EPYC 9004/9005: celou tuto sekci ignorujte.

Transparent Huge Pages: madvise, ne always

THP oportunisticky slučuje 4KB stránky do 2MB stránek a snižuje tak tlak na TLB. PyTorch i ovladač CUDA z toho mírně profitují. Past je režim always při vysoké fluktuaci paměti — jádro zastaví vlákno na 50–100 ms kvůli kompakci paměti a torpéduje tím jakékoli latenční SLO.

Správné nastavení je madvise: aplikace, které vědí, co dělají, zavolají nad dlouhožijícími alokacemi madvise(MADV_HUGEPAGE) a THP dostanou; nic jiného ne. Nastavíte přes /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=madvise"

update-grub && reboot. Defrag by měl odpovídat — defer+madvise nechá kompakci běžet na pozadí, nikoli v cestě alokujícího vlákna:

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

Pro vLLM / SGLang / TensorRT-LLM v produkci je madvise správná výchozí volba. PyTorch s CUDA Unified Memory (výzkumný kód) občas těží z always — před přepnutím si to změřte.

Explicitní 1GB hugepages: jen pokud jste měřili

Explicitní hugepages rezervované při bootu přes hugetlbfs přinesou u velmi velkých vah LLM malý přínos navíc tím, že odstraní výpadky TLB při procházení vah. Háček: paměť se rezervuje předem a je pro zbytek systému nedostupná, aplikace s nimi musí umět pracovat a u většiny serving workloadů je zisk 1–3 %.

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

Vyplatí se u TensorRT-LLM stroje, kde se engine vejde do pinned paměti a Vy honíte poslední procento TTFT. Nevyplatí se u běžného vLLM serveru, který přepíná modely, ani u trénovacího stroje. Při prvním buildu je přeskočte; vraťte se k nim, jen když perf stat -e dTLB-load-misses ukáže tlak na TLB a máte rezervu RAM.

vm.max_map_count a nofile: zdi, do kterých vLLM při škálování naráží

Výchozí vm.max_map_count je na Ubuntu 65530 — počet různých mapování paměti, která proces může držet. vLLM obsluhující velký model s vysokou souběžností, nebo jakýkoli framework s množstvím safetensors shardů načítaných přes mmap, tuto hranici překročí a spadne s Cannot allocate memory. Hodnota 262144 převzatá z Elasticsearche je naprosté minimum; pro vLLM obsluhující 70B+ se stovkami souběžných sekvencí jděte na 1048576. Nic to nestojí — jde o soft limit, ne o rezervaci.

Výchozí ulimit -n je na Ubuntu 1024. Komicky málo: vLLM, Triton, workeři PyTorch DataLoaderu, NCCL a gRPC vrstva mezi nimi otevírají každý stovky FD. Jakmile narazíte na limit, dostanete EMFILE: too many open files a proces, který se tiše zasekne.

# /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 a systemctl daemon-reexec. Ověřte přes ulimit -n a cat /proc/<pid>/limits.

IRQ afinita: připnutí přerušení NIC na lokální CPU

Na 100GbE NIC ConnectX-6/7, která drží 10+ GB/s streamování datasetu nebo RDMA provozu, musí přerušení dopadat na CPU (a) na stejném NUMA uzlu jako NIC a (b) ne na stejná jádra, kde běží workeři DataLoaderu. Výchozí irqbalance si vede obstojně; pod velkou zátěží už ne.

Nejčistší řešení je skript set_irq_affinity.sh od Mellanoxu z balíčku mlnx-tools:

sudo systemctl disable --now irqbalance
sudo /usr/sbin/set_irq_affinity.sh enp1s0f0                  # all NIC-local cores
sudo /usr/sbin/set_irq_affinity_cpulist.sh 4-11 enp1s0f0     # pin to specific cores

Chyba je nechat běžet irqbalance na stroji, kde jste zároveň pinovali ručně — perou se spolu. Vyberte si jedno. Serving stroj s jednou nebo dvěma RDMA NIC: pinujte ručně a vypněte irqbalance. Univerzální stroj s mnoha rozhraními: nechte irqbalance zapnutý s --banirq pro vyloučení kritické NIC.

Síťové buffery jádra: jen pro rychlé úložiště / streaming

U jednouzlového inference serveru, který komunikuje s klienty přes běžné TCP při mírných rychlostech, výchozí net.core.rmem_max / wmem_max o velikosti 208 KB stačí. U uzlu, který tahá trénovací data ze 100GbE NFS nebo object storu, nebo u vLLM frontendu za load balancerem s vysokým RPS jsou výchozí hodnoty stropem Vašeho bandwidth-delay produktu. Výchozí sada pro stroj připojený na 100 GbE:

# /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

Maximum 256 MB je pro 100 GbE v rámci racku přehnané (BDP je ~1.25 MB), ale neškodí — automatické ladění TCP zvětšuje buffery jen podle potřeby až do stropu. Na RDMA (RoCE / IB) na datové cestě tyto parametry nehrají roli — RDMA obchází TCP stack jádra. Pro management rovinu, NFS a stahování modelů ale platí dál.

C-stavy: latence versus klidová spotřeba

U latenčně citlivého serving stroje je po governoru CPU nejlevnějším zlepšením tail latency vypnutí hlubokých C-stavů. Vstup do C6 a výstup z něj trvá desítky mikrosekund; u požadavku, který má odpovědět za 30 ms, přidá CPU probouzející se z C6 kvůli samplingu po tokenu znatelný jitter.

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

update-grub && reboot. Ověřte přes cpupower idle-info — k dispozici by měly být jen C0 a C1. Cena: 30–50 W klidové spotřeby na soket, protože jádra nikdy nepřejdou do hlubokého spánku. Na dvousoketovém serveru s osmi GPU jde o 60–100 W trvalé režie — zanedbatelné proti 3.5–4.5 kW trvalého odběru GPU pod zátěží.

Používejte u latenčně citlivých inference serverů a real-time robotických cest. Přeskočte u trénovacích strojů (latence nehraje roli, klidová spotřeba se za měsíce nasčítá) a u dávkových úloh.

Ladění jádra kolem souborového systému (náhled na L04)

Důležitých je několik sysctlů fs.*:

fs.aio-max-nr = 1048576              # default 65536 too low for vLLM weight loaders
fs.inotify.max_user_watches = 524288 # tooling that watches checkpoint dirs

fs.aio-max-nr je ten, který opravdu zabolí — frameworky provádějící asynchronní I/O proti mnoha shardům (DALI, načítání vah vLLM) u velkých modelů výchozích 65536 vyčerpají. Samotná volba souborového systému (XFS vs ZFS vs ext4), parametry připojení a sémantika O_DIRECT jsou tématem L04.

Vypnutí nepotřebných modulů jádra

Bezhlavý serving stroj nemá důvod načítat moduly pro bluetooth, zvuk, webkameru, wi-fi, joystick nebo tiskový server. Každý z nich je další kód v útočné ploše a malé zdržení při bootu. Jednotlivě triviální; dohromady se o čistším stroji lépe přemýšlí.

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

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

Boot se u typického EPYC buildu zkrátí z ~30 s na ~10 s a výstup lsmod se dá číst. Nulový dopad na výkon inference, mírné zmenšení útočné plochy.

Dva profily: inference versus trénink

Tvar ladění se skutečně liší. Náčrty k přímému použití:

Inference (/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

Navíc na příkazový řádek jádra: transparent_hugepage=madvise intel_idle.max_cstate=1 processor.max_cstate=1. Governor: performance. IRQ NIC připnutá na NUMA-lokální jádra, irqbalance vypnutý.

Trénink (/etc/sysctl.d/99-ai-training.conf)

vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 0       # page migration mid-epoch is just churn
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

Příkazový řádek jádra: transparent_hugepage=madvise (vynechte pinování C-stavů — trénink je omezený propustností a klidová spotřeba se za týdny nasčítá). Governor: performance, pokud na to máte energetický rozpočet. NCCL připnuté na lokální NUMA přes NCCL_IB_HCA.

Past „všechno jsem vyladil a je to pomalejší“

Nejčastější způsob selhání je cargo-cult výsyp: zkopírovat sto sysctlů z blogu, restartovat, naměřit horší výkon a netušit, který parametr vrátit zpět. Postup, který funguje:

  1. Změřte výchozí stav — nccl-tests all_reduce_perf, vLLM benchmark_throughput.py, čas trénovacího kroku. Čísla si zapište.
  2. Změňte jednu věc.
  3. Spusťte stejný benchmark znovu, třikrát kvůli šumu.
  4. Nechte, pokud je medián znatelně lepší. Jinak vraťte zpět.
  5. Změnu i její důvod zdokumentujte ve verzovacím systému vedle souboru sysctl.

Viděli jsme produkční stroje se 40 řádky sysctl ladění, které byly dohromady o 2 % pomalejší než výchozí Ubuntu — každá jednotlivá změna neutrální nebo horší, ale operátor byl přesvědčený, že „to ladění pomáhá“.

Nástroj tuned 2.27 od Red Hatu přináší hotové profily ai-inference a ai-training a na RHEL / Rocky je to seriózní zkratka. Ubuntu jej nedodává; apt install tuned funguje, ale je méně dotažený. Na Ubuntu se ručně psané drop-in soubory v /etc/sysctl.d/ a čistý příkazový řádek GRUBu snáze auditují a přesně víte, co je nastavené.

Co dělat dál

Rozumná posloupnost ladění jádra pro čerstvý build Kentino AI, podle priority:

  1. Nastavte governor CPU na performance. Ověřte přes cpupower frequency-info. Největší výhra zdarma.
  2. Spusťte numactl --hardware a nvidia-smi topo -m. Než cokoli připnete, pochopte topologii. U dvousoketových strojů naplánujte, které GPU patří ke kterému NUMA uzlu.
  3. Nastavte vm.max_map_count a nofile limity. Ty zabraňují selháním, ne pomalosti. Udělejte je před prvním produkčním během.
  4. Nastavte transparent_hugepage=madvise na příkazovém řádku jádra.
  5. Připněte IRQ NIC k lokálnímu NUMA uzlu dané NIC pomocí set_irq_affinity.sh. Vypněte irqbalance, pokud jste tak učinili.
  6. Pouze pro inference stroje: vypněte hluboké C-stavy přes intel_idle.max_cstate=1 processor.max_cstate=1.
  7. Vyladěte síťové buffery jen pokud jste naměřili úzké hrdlo na 25/100GbE cestě k úložišti nebo streamingu.
  8. Přeskočte explicitní 1GB hugepages, pokud nemáte benchmark ukazující tlak na TLB.
  9. Před každou změnou i po ní měřte. Vždy jednu věc. Dokumentujte.

Křížové odkazy: L01 k ovladači a pinování jádra, L02 ke stacku CUDA / kontejnerů, L04 k vrstvě souborového systému, L05 k monitoringu.

Ladění jádra na moderním Linuxu je většinou jednorázová konfigurace, ne nekonečná hra na optimalizaci. Nastavte správně governor, umístění NUMA a limity. Zbytek přeskočte, pokud Váš benchmark neříká jinak.


Toto je součást Kentino Wiki, referenční série o AI výpočtech, robotice a systémech, které je propojují. Komentáře a opravy uvítáme na info@kentino.com.