Strojenie jądra Linux pod serwery AI: co naprawdę robi różnicę
Domyślne jądro Ubuntu na stosie HWE 6.8 lub 6.11 w zupełności wystarcza dla mniej więcej 80% obciążeń AI. Ten artykuł dotyczy pozostałych 20% — wielogniazdowych platform EPYC obsługujących osiem GPU, serwera vLLM, który pod obciążeniem uderza w ścianę vm.max_map_count, albo serwera inferencyjnego, któremu opóźnienia ogonowe zjadają przejścia między stanami C procesora.
Większość publicznie dostępnych porad o "strojeniu Linuksa pod AI" to odgrzane zalecenia dla baz danych z 2015 roku, a niemała ich część po cichu pogarsza opóźnienia inferencji. Poniżej znajduje się węższy, uczciwy zestaw zmian, które naprawdę pomagają na maszynie Kentino AI klasy Kentino (4–8 GPU na Xeonie lub EPYC, Ubuntu 22.04 / 24.04, jądro 6.x). Zakładamy, że sterownik i stos CUDA zostały zainstalowane zgodnie z L01 i L02.
Uczciwa hierarchia zysków
Przed jakimikolwiek zmianami w sysctl warto zakotwiczyć się w skali możliwej do wygrania stawki:
| Zmiana | Typowy zysk przy inferencji / treningu |
|---|---|
| Rozmieszczanie procesów ze świadomością NUMA | 10–30% na maszynach wielogniazdowych |
Governor CPU performance (zamiast powersave) |
5–15% na serwerach obsługujących zapytania, niższy TTFT |
| Wyłączenie głębokich stanów C | Mikrosekundy mniej w P99, +30–50 W na gniazdo w spoczynku |
THP ustawione na madvise
|
1–5% w PyTorch, mniej zacięć przy dużej rotacji pamięci |
vm.max_map_count, ulimit -n
|
Chroni usługę przed wywróceniem się pod obciążeniem |
| Powinowactwo IRQ do lokalnego węzła NUMA | 5–15% na sieciowych ścieżkach DataLoader / RDMA |
Dobór rozmiaru buforów TCP (rmem_max, wmem_max) |
Ma znaczenie wyłącznie przy magazynie / strumieniowaniu >25 GbE |
| Cała reszta | 0–3%, zwykle w granicach szumu |
Jeśli z tego artykułu miałoby zostać tylko jedno zdanie: świadomość NUMA i governor CPU to dwa wielkie zyski. Cała reszta jest błędem zaokrąglenia, o ile nie zostało to zmierzone.
Governor CPU: najtańsze 5–15% do wzięcia
Ubuntu domyślnie ustawia powersave (intel_pstate) albo ondemand (acpi_cpufreq), zależnie od sterownika. Oba podnoszą taktowanie reaktywnie, co kosztuje 5–15% na TTFT inferencji oraz na przygotowaniu po stronie CPU wokół przebiegu vLLM — tokenizacji, szeregowaniu, próbkowaniu logitów.
Na serwerze obsługującym zapytania warto ustawić performance i zapomnieć o sprawie:
sudo apt install -y cpufrequtils
echo 'GOVERNOR="performance"' | sudo tee /etc/default/cpufrequtils
sudo systemctl restart cpufrequtils
# Weryfikacja
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do cat $c; done | sort -u
Na EPYC Genoa / Turin z jądrem 6.5+ sterownik amd-pstate zastępuje acpi-cpufreq. Zasada jest ta sama: ustawić scaling_governor na performance. Nowszy tryb amd-pstate-epp utrzymuje wydajność, dopuszczając boost per rdzeń; EPP należy zostawić na performance, nie na balance_performance.
Zastrzeżenie: na maszynie treningowej przy trwałym wykorzystaniu GPU bliskim 100% procesory i tak są obciążone, więc governor ma mniejsze znaczenie. Największy zysk pojawia się na serwerach inferencyjnych, które między żądaniami stoją bezczynnie i muszą natychmiast nabrać obrotów.
NUMA: różnica między szybką a przeciętną maszyną EPYC
W serwerach jednogniazdowych Xeon lub EPYC nie ma nad czym się zastanawiać — jeden węzeł NUMA, każdy dostęp do pamięci jest lokalny. To na maszynach dwugniazdowych leży owe 10–30%.
Domyślny planista chętnie umieści proces roboczy vLLM na gnieździe 0 i każe mu sięgać do pamięci przydzielonej na gnieździe 1 — każde odczytanie to przeskok przez UPI / Infinity Fabric. Przy modelu o setkach GB wag przechodzonych raz na token ma to realne znaczenie.
Kontrola:
numactl --hardware # liczba węzłów, rozmiary, macierz odległości
nvidia-smi topo -m # powinowactwo NUMA GPU↔CPU
cat /sys/class/net/<iface>/device/numa_node # powinowactwo NUMA karty sieciowej
Pożądany układ — wykrywany automatycznie przez NCCL — to GPU N przypięte do węzła NUMA, na którym znajduje się jego kompleks główny PCIe, a nie do drugiego gniazda. Przy ręcznie uruchamianym procesie inferencyjnym należy przypiąć do lokalnego węzła zarówno CPU, jak i pamięć:
# vLLM na dwugniazdowym EPYC, GPU 0–3 na węźle NUMA 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
Dwa powiązane pokrętła:
-
kernel.numa_balancingmigruje strony w stronę CPU, który ich dotyka. Pomaga przy ogólnych obciążeniach, przy AI bywa przeciwskuteczne: prefill przechodzi wagi raz, jądro migruje strony, a kolejne żądanie ściąga je z powrotem. Domyślnie warto zostawić włączone (1); jeśli wahania korelują znuma_pte_updatesw/proc/vmstat, można spróbowaćsysctl kernel.numa_balancing=0. -
NCCL na maszynie dwugniazdowej —
NCCL_SOCKET_IFNAMEnależy ustawić na kartę zarządzającą, aNCCL_IB_HCAna kartę w tym samym węźle NUMA co GPU. Niewłaściwa karta po cichu obcina przepustowość między węzłami o połowę.
Serwer jednogniazdowy EPYC 9004/9005: cały ten rozdział można pominąć.
Transparent Huge Pages: madvise, nie always
THP składa strony 4 KB w strony 2 MB tam, gdzie to możliwe, zmniejszając presję na TLB. PyTorch i sterownik CUDA zyskują na tym umiarkowanie. Pułapką jest tryb always przy dużej rotacji pamięci — jądro wstrzymuje wątek na 50–100 ms, kompaktując pamięć, i torpeduje każdy cel SLO dotyczący opóźnień.
Właściwym ustawieniem jest madvise: aplikacje, które wiedzą, co robią, wywołują madvise(MADV_HUGEPAGE) na długowiecznych alokacjach i dostają THP; żadne inne go nie dostają. Ustawia się to w /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=madvise"
update-grub && reboot. Defragmentacja powinna być spójna — defer+madvise pozwala prowadzić kompakcję w tle, a nie na ścieżce wątku alokującego:
echo defer+madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
Dla vLLM / SGLang / TensorRT-LLM na produkcji madvise jest właściwą wartością domyślną. PyTorch z pamięcią ujednoliconą CUDA (kod badawczy) czasem zyskuje na always — przed przełączeniem trzeba to zmierzyć.
Jawne hugepages 1 GB: tylko po pomiarach
Jawne hugepages rezerwowane przy starcie przez hugetlbfs dają niewielki dodatkowy zysk przy bardzo dużych wagach LLM, eliminując chybienia TLB podczas ich przeglądania. Haczyk: pamięć jest rezerwowana z góry i niedostępna dla reszty systemu, aplikacja musi być zbudowana tak, by z niej korzystać, a zysk dla większości obciążeń usługowych wynosi 1–3%.
GRUB_CMDLINE_LINUX_DEFAULT="... default_hugepagesz=1G hugepagesz=1G hugepages=64"
Warto się tym zająć przy maszynie z TensorRT-LLM, gdzie silnik mieści się w pamięci przypiętej i trwa pogoń za ostatnim procentem TTFT. Nie warto przy ogólnym serwerze vLLM, który wymienia modele, ani przy maszynie treningowej. Przy pierwszej konfiguracji należy to pominąć; wracać tylko wtedy, gdy perf stat -e dTLB-load-misses pokazuje presję na TLB i jest zapas RAM.
vm.max_map_count i nofile: ściany, w które vLLM uderza przy skali
Domyślna wartość vm.max_map_count w Ubuntu to 65530 — liczba odrębnych mapowań pamięci, jakie może utrzymywać proces. vLLM obsługujący duży model przy wysokiej współbieżności, albo dowolny framework intensywnie korzystający z fragmentów safetensors mapowanych przez mmap, przekracza ten limit i kończy pracę błędem Cannot allocate memory. Wartość 262144, zapożyczona z Elasticsearcha, to absolutne minimum; przy vLLM obsługującym modele 70B+ z setkami równoległych sekwencji warto podnieść ją do 1048576. Nic to nie kosztuje — to limit miękki, nie rezerwacja.
Domyślny ulimit -n w Ubuntu wynosi 1024. Komicznie mało: vLLM, Triton, procesy robocze PyTorch DataLoader, NCCL i warstwa gRPC między nimi otwierają po setki deskryptorów każdy. Po osiągnięciu limitu pojawia się EMFILE: too many open files, a proces po cichu się zacina.
# /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
Następnie sudo sysctl --system i systemctl daemon-reexec. Weryfikacja przez ulimit -n oraz cat /proc/<pid>/limits.
Powinowactwo IRQ: przypinanie przerwań karty sieciowej do lokalnych CPU
Na karcie 100 GbE ConnectX-6/7 utrzymującej 10+ GB/s strumienia danych lub ruchu RDMA przerwania muszą trafiać na CPU, które (a) leżą w tym samym węźle NUMA co karta oraz (b) nie są tymi samymi rdzeniami, na których działają procesy robocze DataLoader. Domyślny irqbalance radzi sobie znośnie; przy dużym obciążeniu — już nie.
Najczystszym rozwiązaniem jest skrypt set_irq_affinity.sh Mellanoxa z pakietu mlnx-tools:
sudo systemctl disable --now irqbalance
sudo /usr/sbin/set_irq_affinity.sh enp1s0f0 # wszystkie rdzenie lokalne dla karty
sudo /usr/sbin/set_irq_affinity_cpulist.sh 4-11 enp1s0f0 # przypięcie do wskazanych rdzeni
Błędem jest pozostawienie działającego irqbalance na maszynie, na której przypięto przerwania ręcznie — będą ze sobą walczyć. Trzeba wybrać jedno. Serwer usługowy z jedną lub dwiema kartami RDMA: przypiąć ręcznie, irqbalance wyłączyć. Maszyna ogólnego przeznaczenia z wieloma interfejsami: zostawić irqbalance włączony z opcją --banirq wykluczającą krytyczną kartę.
Bufory sieciowe jądra: tylko dla szybkich ścieżek magazynu / strumieniowania
Dla jednowęzłowego serwera inferencyjnego rozmawiającego z klientami po zwykłym TCP przy umiarkowanych przepływnościach domyślne net.core.rmem_max / wmem_max na poziomie 208 KB w zupełności wystarczają. Dla węzła pobierającego dane treningowe z NFS 100 GbE lub z magazynu obiektowego, albo dla frontendu vLLM za load balancerem o wysokim RPS, wartości domyślne stanowią sufit przepustowości wynikający z iloczynu pasma i opóźnienia. Punkt wyjścia dla maszyny podłączonej przez 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
Maksimum 256 MB to przesada w obrębie szafy przy 100 GbE (BDP wynosi ~1.25 MB), ale jest nieszkodliwe — automatyczne strojenie TCP powiększa bufory tylko w razie potrzeby, do zadanego pułapu. Przy RDMA (RoCE / IB) na ścieżce danych te pokrętła nie mają znaczenia — RDMA omija stos TCP jądra. Nadal liczą się jednak dla płaszczyzny zarządzania, NFS i pobierania modeli.
Stany C: opóźnienia kontra pobór mocy w spoczynku
Na serwerze usługowym wrażliwym na opóźnienia najtańszą poprawą opóźnień ogonowych — zaraz po governorze CPU — jest wyłączenie głębokich stanów C. Wejście i wyjście ze stanu C6 zajmuje dziesiątki mikrosekund; przy żądaniu, które powinno dostać odpowiedź w 30 ms, procesor wybudzający się z C6 do obsługi próbkowania po tokenie dodaje zauważalne wahania.
GRUB_CMDLINE_LINUX_DEFAULT="... intel_idle.max_cstate=1 processor.max_cstate=1"
update-grub && reboot. Weryfikacja przez cpupower idle-info — dostępne powinny być wyłącznie C0 i C1. Koszt: 30–50 W mocy w spoczynku na gniazdo, bo rdzenie nigdy nie zapadają w głęboki sen. Na dwugniazdowym serwerze z ośmioma GPU daje to 60–100 W stałego narzutu — rzecz bez znaczenia wobec 3.5–4.5 kW trwałego poboru GPU pod obciążeniem.
Warto stosować na serwerach inferencyjnych wrażliwych na opóźnienia i na ścieżkach robotyki czasu rzeczywistego. Pominąć na maszynach treningowych (opóźnienia nie mają znaczenia, a pobór mocy w spoczynku sumuje się przez miesiące) i przy zadaniach wsadowych.
Strojenie jądra związane z systemem plików (zapowiedź L04)
Znaczenie ma garść ustawień fs.*:
fs.aio-max-nr = 1048576 # domyślne 65536 to za mało dla modułów ładujących wagi vLLM
fs.inotify.max_user_watches = 524288 # narzędzia obserwujące katalogi z punktami kontrolnymi
Najbardziej daje się we znaki fs.aio-max-nr — frameworki prowadzące asynchroniczne operacje we/wy na wielu fragmentach (DALI, moduły ładujące wagi vLLM) przy dużych modelach wyczerpują domyślne 65536. Sam wybór systemu plików (XFS kontra ZFS kontra ext4), opcje montowania i semantyka O_DIRECT to temat L04.
Wyłączanie zbędnych modułów jądra
Bezgłowy serwer usługowy nie ma po co ładować modułów bluetooth, dźwięku, kamery internetowej, sieci bezprzewodowej, joysticka czy serwera druku. Każdy z nich to dodatkowy kod w powierzchni ataku i drobne opóźnienie startu. Osobno rzecz błaha; łącznie — o czystszej maszynie łatwiej myśleć.
# /etc/modprobe.d/blacklist-ai-server.conf
blacklist bluetooth
blacklist btusb
blacklist snd_hda_intel
blacklist uvcvideo
blacklist joydev
# Dodatkowo
sudo systemctl disable --now bluetooth.service cups.service avahi-daemon.service \
ModemManager.service whoopsie.service apport.service
Czas startu spada z ~30 s do ~10 s na typowej konfiguracji EPYC, a lsmod staje się czytelny. Zerowy wpływ na wydajność inferencji, umiarkowane zmniejszenie powierzchni ataku.
Dwa profile: inferencja kontra trening
Kształt strojenia jest tu naprawdę inny. Szkice plików drop-in:
Inferencja (/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
Dodatkowo w wierszu poleceń jądra: transparent_hugepage=madvise intel_idle.max_cstate=1 processor.max_cstate=1. Governor: performance. Przerwania karty sieciowej przypięte do rdzeni lokalnych dla NUMA, irqbalance wyłączony.
Trening (/etc/sysctl.d/99-ai-training.conf)
vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 0 # migracja stron w trakcie epoki to sama jałowa praca
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
Wiersz poleceń jądra: transparent_hugepage=madvise (bez blokowania stanów C — trening jest ograniczony przepustowością, a pobór mocy w spoczynku sumuje się przez tygodnie). Governor: performance, jeśli pozwala na to budżet mocy. NCCL przypięty do lokalnego węzła NUMA przez NCCL_IB_HCA.
Pułapka "nastroiłem wszystko i zrobiło się wolniej"
Najczęstszym scenariuszem porażki jest zrzut w stylu kultu cargo: skopiować sto ustawień sysctl z wpisu na blogu, zrestartować, zaobserwować gorszą wydajność i nie mieć pojęcia, które pokrętło wycofać. Metoda, która działa:
- Ustalić punkt odniesienia —
nccl-tests all_reduce_perf, vLLMbenchmark_throughput.py, czas kroku treningowego. Zapisać liczby. - Zmienić jedną rzecz.
- Powtórzyć ten sam benchmark, trzykrotnie ze względu na szum.
- Zachować, jeśli mediana jest wyraźnie lepsza. W przeciwnym razie wycofać.
- Udokumentować zmianę i jej powód, w systemie kontroli wersji obok pliku sysctl.
Widzieliśmy produkcyjne maszyny z 40 liniami strojenia sysctl, które łącznie były o 2% wolniejsze od ustawień domyślnych Ubuntu — każda pojedyncza zmiana neutralna albo szkodliwa, a mimo to operator był pewien, że "strojenie pomaga".
Red Hat w tuned 2.27 dostarcza jawne profile ai-inference i ai-training i jest to wiarygodny skrót na RHEL / Rocky. Ubuntu go nie dostarcza; apt install tuned działa, ale jest mniej dopracowany. Na Ubuntu samodzielnie przygotowane pliki drop-in w /etc/sysctl.d/ oraz czysty wiersz poleceń GRUB-a łatwiej skontrolować i dokładnie wiadomo, co zostało ustawione.
Co zrobić dalej
Rozsądna kolejność strojenia jądra dla świeżej konfiguracji Kentino Kentino AI, według priorytetów:
-
Ustawić governor CPU na
performance. Zweryfikować poleceniemcpupower frequency-info. Największy zysk bez żadnych kosztów. -
Uruchomić
numactl --hardwareoraznvidia-smi topo -m. Przed jakimkolwiek przypinaniem trzeba zrozumieć topologię. Na maszynach dwugniazdowych należy zaplanować, które GPU idą z którym węzłem NUMA. -
Ustawić
vm.max_map_counti limitynofile. Zapobiegają awariom, nie spowolnieniom. Zrobić to przed pierwszym uruchomieniem produkcyjnym. -
Ustawić
transparent_hugepage=madvisew wierszu poleceń jądra. -
Przypiąć przerwania karty sieciowej do jej lokalnego węzła NUMA przy pomocy
set_irq_affinity.sh. Po wykonaniu tego wyłączyćirqbalance. -
Wyłącznie na serwerach inferencyjnych: wyłączyć głębokie stany C przez
intel_idle.max_cstate=1 processor.max_cstate=1. - Stroić bufory sieciowe tylko wtedy, gdy zmierzono wąskie gardło na ścieżce magazynu lub strumieniowania 25/100 GbE.
- Pominąć jawne hugepages 1 GB, o ile benchmark nie wykaże presji na TLB.
- Wykonywać pomiary przed każdą zmianą i po niej. Po jednej rzeczy naraz. Dokumentować.
Odsyłacze: L01 w sprawie sterownika i przypinania jądra, L02 w sprawie stosu CUDA / kontenerów, L04 w sprawie warstwy systemu plików, L05 w sprawie monitoringu.
Strojenie jądra we współczesnym Linuksie to w większości konfiguracja jednorazowa, a nie ciągła gra w optymalizację. Trzeba dobrze ustawić governor, rozmieszczenie NUMA i limity. Resztę pominąć, o ile benchmark nie mówi czego innego.
To część Kentino Wiki, serii referencyjnej o obliczeniach AI, robotyce i systemach, które je łączą. Komentarze i sprostowania mile widziane pod adresem info@kentino.com.