Optimisation du noyau Linux pour serveurs IA : ce qui fait vraiment la différence
Le noyau Ubuntu par défaut d'une pile HWE 6.8 ou 6.11 suffit pour environ 80 % des charges de travail IA. Cet article s'adresse aux 20 % restants : un EPYC multi-socket qui pousse huit GPU, une machine de service vLLM qui se heurte au mur vm.max_map_count sous charge, ou un serveur d'inférence dont la latence de queue est grignotée par les transitions d'états C du CPU.
La plupart des conseils publics de « réglage Linux pour l'IA » sont des recommandations pour bases de données recyclées depuis 2015, et une part non négligeable d'entre eux dégrade discrètement la latence d'inférence. Voici l'ensemble plus restreint et plus honnête de modifications qui aident réellement sur une machine Kentino AI de classe Kentino (4 à 8 GPU sur Xeon ou EPYC, Ubuntu 22.04 / 24.04, noyau 6.x). Le pilote et la pile CUDA sont supposés installés conformément à L01 et L02.
La hiérarchie honnête des gains
Avant toute modification sysctl, mesurez d'abord l'ampleur de l'enjeu :
| Modification | Gain typique en inférence / entraînement |
|---|---|
| Placement des processus tenant compte du NUMA | 10–30 % sur les machines multi-socket |
Gouverneur CPU performance (depuis powersave) |
5–15 % sur les machines de service, TTFT plus bas |
| Désactivation des états C profonds | Quelques microsecondes de moins sur le P99, +30–50 W au repos / socket |
THP réglé sur madvise
|
1–5 % sur PyTorch, moins de blocages en forte activité mémoire |
vm.max_map_count, ulimit -n
|
Évite que le service ne s'effondre sous charge |
| Affinité des IRQ au nœud NUMA local | 5–15 % sur les chemins DataLoader réseau / RDMA |
Dimensionnement des tampons TCP (rmem_max, wmem_max) |
Ne compte que pour le stockage / streaming >25 GbE |
| Tout le reste | 0–3 %, souvent du bruit |
Si vous ne retenez qu'une chose de cet article : la prise en compte du NUMA et le gouverneur CPU sont les deux grands gains. Tout le reste relève de l'erreur d'arrondi tant que vous n'avez pas mesuré.
Gouverneur CPU : les 5–15 % les moins chers du marché
Ubuntu utilise par défaut powersave (intel_pstate) ou ondemand (acpi_cpufreq) selon le pilote. Les deux font monter les fréquences de façon réactive, ce qui coûte 5–15 % sur le TTFT d'inférence et sur la préparation côté CPU autour d'une passe avant vLLM : tokenisation, ordonnancement, échantillonnage des logits.
Pour une machine de service, réglez sur performance et n'y pensez plus :
sudo apt install -y cpufrequtils
echo 'GOVERNOR="performance"' | sudo tee /etc/default/cpufrequtils
sudo systemctl restart cpufrequtils
# Vérification
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do cat $c; done | sort -u
Sur EPYC Genoa / Turin avec un noyau 6.5+, le pilote amd-pstate remplace acpi-cpufreq. Même principe : réglez scaling_governor sur performance. Le mode plus récent amd-pstate-epp conserve les performances tout en autorisant le boost par cœur ; laissez EPP sur performance, et non sur balance_performance.
Nuance : sur une machine d'entraînement à ~100 % d'utilisation GPU soutenue, les CPU sont de toute façon chargés et le gouverneur compte moins. Le gros gain concerne les serveurs d'inférence qui restent au repos entre les requêtes et doivent monter en régime instantanément.
NUMA : ce qui sépare une machine EPYC rapide d'une machine médiocre
Les serveurs Xeon ou EPYC mono-socket n'ont rien à se demander : un seul nœud NUMA, tous les accès mémoire sont locaux. C'est sur les machines bi-socket que se jouent les 10–30 %.
L'ordonnanceur par défaut placera volontiers un worker vLLM sur le socket 0 et le fera défauter sur de la mémoire allouée au socket 1 : chaque chargement devient un saut inter-socket UPI / Infinity Fabric. Pour un modèle dont des centaines de Go de poids sont parcourus à chaque token, l'effet est bien réel.
Inspectez :
numactl --hardware # nombre de nœuds, tailles, matrice de distances
nvidia-smi topo -m # affinité NUMA GPU↔CPU
cat /sys/class/net/<iface>/device/numa_node # affinité NUMA de la carte réseau
Le schéma recherché — et que NCCL détecte automatiquement — est le GPU N épinglé au nœud NUMA qui héberge son complexe racine PCIe, et non à l'autre socket. Pour un processus d'inférence lancé manuellement, épinglez à la fois le CPU et la mémoire sur le nœud local :
# vLLM sur un EPYC bi-socket, GPU 0–3 sur le nœud 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
Deux réglages connexes :
-
kernel.numa_balancingmigre les pages vers le CPU qui les touche. Utile pour des charges de travail générales, parfois contre-productif pour l'IA : un prefill parcourt les poids une fois, le noyau migre les pages, la requête suivante les ramène. Laissez activé par défaut (1) ; si la gigue est corrélée ànuma_pte_updatesdans/proc/vmstat, essayezsysctl kernel.numa_balancing=0. -
NCCL en bi-socket — réglez
NCCL_SOCKET_IFNAMEsur la carte réseau d'administration etNCCL_IB_HCAsur la carte réseau située sur le même nœud NUMA que les GPU. Une mauvaise carte divise silencieusement par deux le débit inter-nœuds.
Serveur EPYC 9004/9005 mono-socket : ignorez toute cette section.
Transparent Huge Pages : madvise, pas always
Le THP regroupe opportunément les pages de 4 Ko en pages de 2 Mo, ce qui réduit la pression sur le TLB. PyTorch et le pilote CUDA en profitent modestement. Le piège, c'est le mode always en cas de forte activité mémoire : le noyau bloque un thread pendant 50–100 ms pour compacter la mémoire, ce qui torpille n'importe quel SLO de latence.
Le bon réglage est madvise : les applications qui savent ce qu'elles font appellent madvise(MADV_HUGEPAGE) sur les allocations de longue durée et obtiennent le THP ; les autres non. À définir via /etc/default/grub :
GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=madvise"
update-grub && reboot. Le defrag doit suivre : defer+madvise laisse le compactage se faire en arrière-plan plutôt que dans le chemin du thread qui alloue :
echo defer+madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
Pour vLLM / SGLang / TensorRT-LLM en production, madvise est le bon réglage par défaut. PyTorch avec la mémoire unifiée CUDA (code de recherche) profite parfois de always — mesurez avant de basculer.
Hugepages explicites de 1 Go : seulement si vous avez mesuré
Les hugepages explicites réservées au démarrage via hugetlbfs apportent un petit gain supplémentaire sur de très gros poids de LLM en éliminant les défauts de TLB lors du balayage des poids. Le revers : la mémoire est réservée d'avance et indisponible pour le reste du système, l'application doit être conçue pour les utiliser, et le gain est de 1–3 % pour la plupart des charges de service.
GRUB_CMDLINE_LINUX_DEFAULT="... default_hugepagesz=1G hugepagesz=1G hugepages=64"
Cela vaut le coup pour une machine TensorRT-LLM dont le moteur tient en mémoire verrouillée et où vous courez après le dernier pourcent de TTFT. Cela n'en vaut pas la peine pour un serveur vLLM générique qui change de modèle, ni pour une machine d'entraînement. À écarter lors d'une première installation ; à revoir seulement si perf stat -e dTLB-load-misses révèle une pression sur le TLB et que vous avez de la RAM en réserve.
vm.max_map_count et nofile : les murs que vLLM rencontre à grande échelle
Par défaut, vm.max_map_count vaut 65530 sur Ubuntu : c'est le nombre de mappages mémoire distincts qu'un processus peut détenir. vLLM servant un gros modèle avec une forte concurrence, ou tout framework utilisant de nombreux fragments safetensors en mmap, dépasse largement ce seuil et meurt avec Cannot allocate memory. La valeur de 262144 héritée d'Elasticsearch est le strict minimum ; pour un vLLM servant du 70B+ avec des centaines de séquences simultanées, montez à 1048576. Cela ne coûte rien : c'est une limite souple, pas une réservation.
Par défaut, ulimit -n vaut 1024 sur Ubuntu. D'un ridicule achevé : vLLM, Triton, les workers DataLoader de PyTorch, NCCL et la couche gRPC qui les relie ouvrent chacun des centaines de descripteurs de fichiers. Atteignez la limite et vous obtenez EMFILE: too many open files ainsi qu'un processus qui se bloque silencieusement.
# /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 et systemctl daemon-reexec. Vérifiez avec ulimit -n et cat /proc/<pid>/limits.
Affinité des IRQ : épingler les interruptions de la carte réseau sur les CPU locaux
Sur une carte réseau ConnectX-6/7 100 GbE qui soutient plus de 10 Go/s de streaming de jeux de données ou de trafic RDMA, les interruptions doivent arriver sur des CPU (a) situés sur le même nœud NUMA que la carte, et (b) différents des cœurs qui exécutent les workers DataLoader. L'irqbalance par défaut fait un travail acceptable ; sous forte charge, non.
Le correctif le plus propre est le set_irq_affinity.sh de Mellanox, fourni avec mlnx-tools :
sudo systemctl disable --now irqbalance
sudo /usr/sbin/set_irq_affinity.sh enp1s0f0 # tous les cœurs locaux à la carte
sudo /usr/sbin/set_irq_affinity_cpulist.sh 4-11 enp1s0f0 # épingler sur des cœurs précis
La mauvaise manœuvre consiste à laisser irqbalance tourner sur une machine où vous avez aussi épinglé manuellement : les deux se disputent. Choisissez. Machine de service avec une ou deux cartes RDMA : épinglez manuellement et désactivez irqbalance. Machine polyvalente avec de nombreuses interfaces : laissez irqbalance actif avec --banirq pour exclure la carte critique.
Tampons réseau du noyau : uniquement pour les chemins de stockage rapide / streaming
Pour un serveur d'inférence mono-nœud qui dialogue avec ses clients en TCP classique à des débits modestes, la valeur par défaut de net.core.rmem_max / wmem_max, soit 208 Ko, convient. Pour un nœud qui tire ses données d'entraînement d'un NFS 100 GbE ou d'un stockage objet, ou pour un frontend vLLM derrière un répartiteur de charge à fort RPS, ces valeurs par défaut plafonnent votre produit bande passante–délai. Un jeu de départ pour une machine raccordée en 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
Un maximum de 256 Mo est surdimensionné pour du 100 GbE en rack (le BDP tourne autour de 1.25 Mo) mais reste sans danger : l'auto-ajustement TCP n'agrandit les tampons qu'au besoin, jusqu'au plafond. En RDMA (RoCE / IB) sur le chemin de données, ces réglages n'ont aucune importance : RDMA contourne la pile TCP du noyau. Ils comptent toujours pour le plan d'administration, le NFS et les téléchargements de modèles.
États C : latence contre consommation au repos
Pour une machine de service sensible à la latence, l'amélioration de latence de queue la moins chère après le gouverneur CPU est la désactivation des états C profonds. L'entrée et la sortie du C6 se comptent en dizaines de microsecondes ; pour une requête censée répondre en 30 ms, un CPU qui sort du C6 pour gérer l'échantillonnage après token ajoute une gigue perceptible.
GRUB_CMDLINE_LINUX_DEFAULT="... intel_idle.max_cstate=1 processor.max_cstate=1"
update-grub && reboot. Vérifiez avec cpupower idle-info : seuls C0 et C1 doivent être disponibles. Coût : 30–50 W de consommation au repos par socket parce que les cœurs n'entrent jamais en sommeil profond. Sur un serveur bi-socket à 8 GPU, cela fait 60–100 W de surcoût permanent — négligeable face aux 3.5–4.5 kW tirés en continu par les GPU en charge.
À appliquer aux serveurs d'inférence sensibles à la latence et aux chaînes robotiques temps réel. À éviter sur les machines d'entraînement (la latence n'y compte pas et la consommation au repos s'accumule sur des mois) et sur les traitements par lots.
Réglages noyau liés au système de fichiers (aperçu de L04)
Une poignée de sysctls fs.* comptent :
fs.aio-max-nr = 1048576 # 65536 par défaut, trop bas pour les chargeurs de poids vLLM
fs.inotify.max_user_watches = 524288 # outils qui surveillent les dossiers de points de contrôle
fs.aio-max-nr est celui qui mord : les frameworks qui font des E/S asynchrones sur de nombreux fragments (DALI, chargeurs de poids vLLM) épuisent la valeur par défaut de 65536 sur les gros modèles. Le choix du système de fichiers lui-même (XFS, ZFS ou ext4), les options de montage et la sémantique O_DIRECT sont traités dans L04.
Désactiver les modules noyau inutiles
Une machine de service sans écran n'a aucune raison de charger des modules pour le bluetooth, le son, la webcam, le Wi-Fi, le joystick ou le serveur d'impression. Chacun représente du code supplémentaire dans la surface d'attaque et un léger retard au démarrage. Négligeable pris isolément ; collectivement, une machine plus propre est plus facile à appréhender.
# /etc/modprobe.d/blacklist-ai-server.conf
blacklist bluetooth
blacklist btusb
blacklist snd_hda_intel
blacklist uvcvideo
blacklist joydev
# En complément
sudo systemctl disable --now bluetooth.service cups.service avahi-daemon.service \
ModemManager.service whoopsie.service apport.service
Le démarrage passe d'environ 30 s à environ 10 s sur une configuration EPYC typique et lsmod redevient lisible. Aucun impact sur les performances d'inférence, et une réduction modeste de la surface d'attaque.
Deux profils : inférence et entraînement
La forme du réglage est véritablement différente. Esquisses prêtes à l'emploi :
Inférence (/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
À ajouter sur la ligne de commande du noyau : transparent_hugepage=madvise intel_idle.max_cstate=1 processor.max_cstate=1. Gouverneur : performance. IRQ de la carte réseau épinglées sur des cœurs NUMA locaux, irqbalance désactivé.
Entraînement (/etc/sysctl.d/99-ai-training.conf)
vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 0 # migrer des pages en pleine époque n'est que du brassage inutile
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
Ligne de commande du noyau : transparent_hugepage=madvise (sans épinglage des états C : l'entraînement est limité par le débit et la consommation au repos s'accumule sur des semaines). Gouverneur : performance si vous avez le budget électrique. NCCL épinglé sur le NUMA local via NCCL_IB_HCA.
Le piège du « j'ai tout réglé et c'est devenu plus lent »
Le mode d'échec le plus courant est le copier-coller dogmatique : récupérer une centaine de sysctls dans un article de blog, redémarrer, constater de moins bonnes performances et n'avoir aucune idée du réglage à annuler. La méthode qui fonctionne :
- Établissez une référence —
nccl-tests all_reduce_perf, lebenchmark_throughput.pyde vLLM, le temps par étape d'entraînement. Notez les chiffres. - Changez une seule chose.
- Relancez le même benchmark, trois fois pour lisser le bruit.
- Conservez si la médiane est nettement meilleure. Revenez en arrière sinon.
- Documentez la modification et sa raison, en gestion de versions, à côté du fichier sysctl.
Nous avons vu des machines de production avec 40 lignes de réglages sysctl qui étaient, au total, 2 % plus lentes que les valeurs par défaut d'Ubuntu — chaque modification prise isolément neutre ou néfaste, mais l'exploitant était convaincu que « le réglage aide ».
Le tuned 2.27 de Red Hat embarque des profils ai-inference et ai-training explicites et constitue un raccourci crédible sur RHEL / Rocky. Ubuntu ne le fournit pas ; apt install tuned fonctionne mais est moins abouti. Sur Ubuntu, des fichiers maison déposés dans /etc/sysctl.d/ et une ligne de commande GRUB propre sont plus faciles à auditer et vous savez exactement ce qui est défini.
Que faire ensuite
Une séquence de réglage noyau raisonnable pour une nouvelle configuration Kentino Kentino AI, par ordre de priorité :
-
Réglez le gouverneur CPU sur
performance. Vérifiez aveccpupower frequency-info. Le plus gros gain sans frais. -
Lancez
numactl --hardwareetnvidia-smi topo -m. Comprenez la topologie avant d'épingler quoi que ce soit. Sur les machines bi-socket, prévoyez quels GPU vont avec quel nœud NUMA. -
Définissez les limites
vm.max_map_countetnofile. Elles évitent des pannes, pas de la lenteur. Faites-le avant la première mise en production. -
Définissez
transparent_hugepage=madvisesur la ligne de commande du noyau. -
Épinglez les IRQ de la carte réseau sur le nœud NUMA local de la carte avec
set_irq_affinity.sh. Désactivezirqbalancesi vous l'avez fait. -
Pour les machines d'inférence uniquement : désactivez les états C profonds via
intel_idle.max_cstate=1 processor.max_cstate=1. - N'ajustez les tampons réseau que si vous avez mesuré un goulet d'étranglement sur un chemin de stockage ou de streaming 25/100 GbE.
- Laissez de côté les hugepages explicites de 1 Go sauf si un benchmark montre une pression sur le TLB.
- Mesurez avant et après chaque modification. Une chose à la fois. Documentez.
Références croisées : L01 pour le pilote et l'épinglage du noyau, L02 pour la pile CUDA / conteneurs, L04 pour la couche système de fichiers, L05 pour la supervision.
Sous Linux moderne, le réglage du noyau relève surtout d'une configuration faite une fois, et non d'une partie d'optimisation sans fin. Réglez correctement le gouverneur, le placement NUMA et les limites. Laissez le reste de côté, sauf si votre benchmark dit le contraire.
Cet article fait partie du Kentino Wiki, une série de référence sur le calcul IA, la robotique et les systèmes qui les relient. Commentaires et corrections bienvenus à info@kentino.com.