Ajuste del kernel de Linux para servidores de IA: lo que de verdad marca la diferencia
El kernel de Ubuntu por defecto sobre una pila HWE 6.8 o 6.11 es suficiente para alrededor del 80 % de las cargas de trabajo de IA. Este artículo es para el 20 % restante: EPYC multisocket moviendo ocho GPU, una máquina de servicio con vLLM que choca contra el muro de vm.max_map_count bajo carga, o un servidor de inferencia cuya latencia de cola se la comen las transiciones de estados C de la CPU.
La mayoría de los consejos públicos sobre «ajuste de Linux para IA» son recomendaciones de bases de datos recicladas de 2015 y una parte nada despreciable de ellos empeora en silencio la latencia de inferencia. A continuación tiene el conjunto más reducido y honesto de cambios que de verdad ayudan en un equipo Kentino AI de clase Kentino (4–8 GPU sobre Xeon o EPYC, Ubuntu 22.04 / 24.04, kernel 6.x). Se da por supuesto que el driver y la pila CUDA están instalados según L01 y L02.
La jerarquía honesta de las mejoras
Antes de tocar ningún sysctl, calibre el tamaño del premio:
| Cambio | Mejora típica en inferencia / entrenamiento |
|---|---|
| Colocación de procesos consciente de NUMA | 10–30 % en equipos multisocket |
Gobernador de CPU performance (desde powersave) |
5–15 % en máquinas de servicio, menor TTFT |
| Desactivar los estados C profundos | Microsegundos menos en el P99, +30–50 W en reposo por socket |
THP en madvise
|
1–5 % en PyTorch, menos bloqueos con memoria muy movida |
vm.max_map_count, ulimit -n
|
Evita que el servicio se caiga bajo carga |
| Afinidad de IRQ al nodo NUMA local | 5–15 % en rutas de DataLoader en red / RDMA |
Dimensionado de búferes TCP (rmem_max, wmem_max) |
Solo importa por encima de >25 GbE en almacenamiento / streaming |
| Todo lo demás | 0–3 %, a menudo ruido |
Si no se queda con nada más de este artículo, quédese con esto: la conciencia de NUMA y el gobernador de CPU son las dos grandes mejoras. Todo lo demás es error de redondeo salvo que lo haya medido.
Gobernador de CPU: el 5–15 % más barato que hay sobre la mesa
Ubuntu usa por defecto powersave (intel_pstate) u ondemand (acpi_cpufreq) según el driver. Ambos suben las frecuencias de forma reactiva, lo que cuesta un 5–15 % en el TTFT de inferencia y en la preparación del lado de la CPU alrededor de una pasada hacia delante de vLLM: tokenización, planificación, muestreo de logits.
En una máquina de servicio, ponga performance y olvídese:
sudo apt install -y cpufrequtils
echo 'GOVERNOR="performance"' | sudo tee /etc/default/cpufrequtils
sudo systemctl restart cpufrequtils
# Verificar
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do cat $c; done | sort -u
En EPYC Genoa / Turin con kernel 6.5+, el driver amd-pstate sustituye a acpi-cpufreq. Misma idea: ponga scaling_governor en performance. El modo más reciente amd-pstate-epp mantiene el rendimiento permitiendo el boost por núcleo; deje el EPP en performance, no en balance_performance.
Matiz: en una máquina de entrenamiento con un uso de GPU sostenido cercano al 100 %, las CPU están cargadas de todos modos y el gobernador importa menos. La gran mejora está en los servidores de inferencia que quedan en reposo entre peticiones y necesitan subir de frecuencia al instante.
NUMA: la diferencia entre que un equipo EPYC sea rápido o mediocre
Los servidores Xeon o EPYC de un solo socket no dan nada que pensar: un único nodo NUMA, todos los accesos a memoria son locales. Es en los equipos de doble socket donde está ese 10–30 %.
El planificador por defecto colocará tan ricamente un worker de vLLM en el socket 0 y hará que provoque fallos de página contra memoria reservada en el socket 1: cada lectura es un salto entre sockets por UPI / Infinity Fabric. Para un modelo con cientos de GB de pesos que se recorren una vez por token, esto es real.
Inspeccione:
numactl --hardware # número de nodos, tamaños, matriz de distancias
nvidia-smi topo -m # afinidad NUMA GPU↔CPU
cat /sys/class/net/<iface>/device/numa_node # afinidad NUMA de la NIC
El patrón que quiere, y que NCCL detecta automáticamente, es la GPU N anclada al nodo NUMA que aloja su complejo raíz PCIe, no al otro socket. Para un proceso de inferencia lanzado a mano, ancle tanto la CPU como la memoria al nodo local:
# vLLM en un EPYC de 2 sockets, GPU 0–3 en el nodo 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
Dos parámetros relacionados:
-
kernel.numa_balancingmigra páginas hacia la CPU que las toca. Útil para cargas generales, a veces contraproducente en IA: un prefill recorre los pesos una vez, el kernel migra las páginas y la siguiente petición vuelve a traerlas. Déjelo activado por defecto (1); si el jitter se correlaciona connuma_pte_updatesen/proc/vmstat, pruebesysctl kernel.numa_balancing=0. -
NCCL en doble socket: ponga
NCCL_SOCKET_IFNAMEapuntando a la NIC de gestión yNCCL_IB_HCAa la NIC del mismo nodo NUMA que las GPU. Una NIC equivocada reduce a la mitad, y sin avisar, el rendimiento entre nodos.
Servidor EPYC 9004/9005 de un solo socket: sáltese esta sección entera.
Transparent Huge Pages: madvise, no always
THP agrupa páginas de 4 KB en páginas de 2 MB de forma oportunista, reduciendo la presión sobre la TLB. PyTorch y el driver CUDA se benefician moderadamente. La trampa es el modo always cuando hay mucho movimiento de memoria: el kernel bloquea un hilo durante 50–100 ms compactando memoria y manda al traste cualquier SLO de latencia.
El ajuste correcto es madvise: las aplicaciones que saben lo que hacen llaman a madvise(MADV_HUGEPAGE) sobre sus reservas de larga vida y obtienen THP; el resto, no. Se configura en /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=madvise"
update-grub && reboot. La desfragmentación debe ir en la misma línea: defer+madvise deja que la compactación ocurra en segundo plano y no en la ruta del hilo que reserva memoria:
echo defer+madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
Para vLLM / SGLang / TensorRT-LLM en producción, madvise es el valor correcto por defecto. PyTorch con memoria unificada de CUDA (código de investigación) a veces se beneficia de always; mida antes de cambiarlo.
Hugepages explícitas de 1 GB: solo si lo ha medido
Las hugepages explícitas reservadas en el arranque mediante hugetlbfs aportan una pequeña mejora adicional con pesos de LLM muy grandes, al eliminar fallos de TLB durante el recorrido de los pesos. El inconveniente: la memoria se reserva de entrada y queda fuera del alcance del resto del sistema, la aplicación tiene que estar compilada para usarlas y la ganancia es del 1–3 % en la mayoría de cargas de servicio.
GRUB_CMDLINE_LINUX_DEFAULT="... default_hugepagesz=1G hugepagesz=1G hugepages=64"
Merece la pena en una máquina con TensorRT-LLM donde el motor cabe en memoria fijada y va detrás del último punto porcentual de TTFT. No merece la pena en un servidor vLLM genérico que cambia de modelo ni en ninguna máquina de entrenamiento. Sáltelo en una primera instalación; vuelva a valorarlo solo si perf stat -e dTLB-load-misses muestra presión en la TLB y le sobra RAM.
vm.max_map_count y nofile: los muros contra los que choca vLLM a escala
El valor por defecto de vm.max_map_count en Ubuntu es 65530, el número de mapeos de memoria distintos que puede mantener un proceso. vLLM sirviendo un modelo grande con mucha concurrencia, o cualquier framework que use muchos shards de safetensors mapeados con mmap, se pasa de ese límite y muere con Cannot allocate memory. El valor de 262144 heredado de Elasticsearch es el mínimo imprescindible; para vLLM sirviendo modelos de 70B o más con cientos de secuencias simultáneas, súbalo a 1048576. No cuesta nada: es un límite flexible, no una reserva.
El ulimit -n por defecto en Ubuntu es 1024. Ridículamente bajo: vLLM, Triton, los workers del DataLoader de PyTorch, NCCL y la capa gRPC entre ellos abren cientos de descriptores cada uno. Al llegar al límite recibirá EMFILE: too many open files y un proceso que se atasca en silencio.
# /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 y systemctl daemon-reexec. Compruébelo con ulimit -n y cat /proc/<pid>/limits.
Afinidad de IRQ: anclar las interrupciones de la NIC a las CPU locales
En una NIC ConnectX-6/7 de 100 GbE que sostiene más de 10 GB/s de streaming de datasets o de tráfico RDMA, las interrupciones tienen que aterrizar en CPU que (a) estén en el mismo nodo NUMA que la NIC y (b) no sean los mismos núcleos que ejecutan los workers del DataLoader. El irqbalance por defecto cumple aceptablemente; bajo carga intensa, no.
La solución más limpia es el set_irq_affinity.sh de Mellanox, incluido en mlnx-tools:
sudo systemctl disable --now irqbalance
sudo /usr/sbin/set_irq_affinity.sh enp1s0f0 # todos los núcleos locales a la NIC
sudo /usr/sbin/set_irq_affinity_cpulist.sh 4-11 enp1s0f0 # anclar a núcleos concretos
El error es dejar irqbalance en marcha en una máquina donde además ha anclado a mano: se pelean entre sí. Elija uno. Máquina de servicio con una o dos NIC RDMA: ancle a mano y desactive irqbalance. Máquina de propósito general con muchas interfaces: deje irqbalance activo con --banirq para excluir la NIC crítica.
Búferes de red del kernel: solo para rutas de almacenamiento rápido / streaming
Para un servidor de inferencia de un solo nodo que habla con clientes por TCP plano a tasas modestas, los 208 KB por defecto de net.core.rmem_max / wmem_max están bien. Para un nodo que tira datos de entrenamiento de un NFS a 100 GbE o de un almacén de objetos, o para un frontend de vLLM detrás de un balanceador con muchas peticiones por segundo, esos valores por defecto son el techo de su producto ancho de banda × retardo. Un conjunto de partida para una máquina conectada a 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 máximo de 256 MB es excesivo para 100 GbE dentro del rack (el BDP ronda 1.25 MB), pero inofensivo: el autoajuste de TCP hace crecer los búferes solo según los necesita, hasta el tope. En RDMA (RoCE / IB), para la ruta de datos estos parámetros no importan: RDMA se salta la pila TCP del kernel. Siguen importando para el plano de gestión, NFS y las descargas de modelos.
Estados C: latencia frente a consumo en reposo
En una máquina de servicio sensible a la latencia, la mejora más barata de la latencia de cola, después del gobernador de CPU, es desactivar los estados C profundos. Entrar y salir de C6 lleva decenas de microsegundos; para una petición que debería responder en 30 ms, una CPU saliendo de C6 para encargarse del muestreo posterior al token añade un jitter apreciable.
GRUB_CMDLINE_LINUX_DEFAULT="... intel_idle.max_cstate=1 processor.max_cstate=1"
update-grub && reboot. Compruébelo con cpupower idle-info: solo deberían estar disponibles C0 y C1. Coste: 30–50 W de consumo en reposo por socket, porque los núcleos nunca entran en sueño profundo. En un servidor de doble socket con 8 GPU, entre 60 y 100 W de sobrecoste permanente: nada frente a los 3.5–4.5 kW de consumo sostenido de las GPU bajo carga.
Aplíquelo a servidores de inferencia sensibles a la latencia y a rutas de robótica en tiempo real. Sáltelo en máquinas de entrenamiento (la latencia da igual y el consumo en reposo se acumula durante meses) y en trabajos por lotes.
Ajustes del kernel relacionados con el sistema de archivos (anticipo de L04)
Un puñado de sysctls fs.* importan:
fs.aio-max-nr = 1048576 # el valor por defecto 65536 es muy bajo para los cargadores de pesos de vLLM
fs.inotify.max_user_watches = 524288 # herramientas que vigilan directorios de checkpoints
fs.aio-max-nr es el que muerde: los frameworks que hacen E/S asíncrona contra muchos shards (DALI, los cargadores de pesos de vLLM) agotan los 65536 por defecto con modelos grandes. La elección del sistema de archivos en sí (XFS frente a ZFS o ext4), las opciones de montaje y la semántica de O_DIRECT se tratan en L04.
Desactivar módulos del kernel innecesarios
Una máquina de servicio sin monitor no tiene por qué cargar módulos de bluetooth, sonido, webcam, wifi, joystick o servidor de impresión. Cada uno es más código en la superficie de ataque y un pequeño retraso en el arranque. Por separado son triviales; en conjunto, una máquina más limpia es más fácil de razonar.
# /etc/modprobe.d/blacklist-ai-server.conf
blacklist bluetooth
blacklist btusb
blacklist snd_hda_intel
blacklist uvcvideo
blacklist joydev
# Además
sudo systemctl disable --now bluetooth.service cups.service avahi-daemon.service \
ModemManager.service whoopsie.service apport.service
El arranque baja de unos 30 s a unos 10 s en una instalación EPYC típica y lsmod se vuelve legible. Cero impacto en el rendimiento de inferencia y una reducción modesta de la superficie de ataque.
Dos perfiles: inferencia frente a entrenamiento
La forma del ajuste es realmente distinta. Esquemas listos para usar:
Inferencia (/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
Además, en la línea de comandos del kernel: transparent_hugepage=madvise intel_idle.max_cstate=1 processor.max_cstate=1. Gobernador: performance. IRQ de la NIC ancladas a núcleos NUMA locales e irqbalance desactivado.
Entrenamiento (/etc/sysctl.d/99-ai-training.conf)
vm.max_map_count = 1048576
vm.swappiness = 1
vm.overcommit_memory = 1
kernel.numa_balancing = 0 # migrar páginas a mitad de época es puro trasiego
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
Línea de comandos del kernel: transparent_hugepage=madvise (sáltese el anclaje de estados C: el entrenamiento está limitado por rendimiento y el consumo en reposo se acumula durante semanas). Gobernador: performance si tiene presupuesto eléctrico. NCCL anclado al NUMA local mediante NCCL_IB_HCA.
La trampa del «lo he ajustado todo y va más lento»
El fallo más habitual con diferencia es el volcado por imitación: copiar cien sysctls de una entrada de blog, reiniciar, observar peor rendimiento y no tener ni idea de qué parámetro revertir. El método que funciona:
- Establezca una línea base:
nccl-tests all_reduce_perf,benchmark_throughput.pyde vLLM, tiempo por paso de entrenamiento. Apunte los números. - Cambie una sola cosa.
- Repita el mismo benchmark, tres veces para filtrar el ruido.
- Consérvelo si la mediana mejora de forma apreciable. Reviértalo si no.
- Documente el cambio y el motivo, en control de versiones junto al archivo de sysctl.
Hemos visto máquinas en producción con 40 líneas de ajustes sysctl que en conjunto eran un 2 % más lentas que Ubuntu por defecto: cada cambio individual era neutro o peor, pero el operador estaba convencido de que «el ajuste ayuda».
El tuned 2.27 de Red Hat incluye perfiles explícitos ai-inference y ai-training y es un atajo creíble en RHEL / Rocky. Ubuntu no lo trae; apt install tuned funciona, pero está menos pulido. En Ubuntu, los archivos propios en /etc/sysctl.d/ y una línea de comandos de GRUB limpia son más fáciles de auditar y sabe exactamente qué hay configurado.
Qué hacer a continuación
Una secuencia razonable de ajuste del kernel para una instalación Kentino AI nueva, por orden de prioridad:
-
Ponga el gobernador de CPU en
performance. Compruébelo concpupower frequency-info. La mayor mejora sin coste. -
Ejecute
numactl --hardwareynvidia-smi topo -m. Entienda la topología antes de anclar nada. En equipos de doble socket, planifique qué GPU van con cada nodo NUMA. -
Configure
vm.max_map_county los límites denofile. Evitan fallos, no lentitud. Hágalo antes de la primera ejecución en producción. -
Ponga
transparent_hugepage=madviseen la línea de comandos del kernel. -
Ancle las IRQ de la NIC al nodo NUMA local de la NIC con
set_irq_affinity.sh. Desactiveirqbalancesi lo ha hecho. -
Solo en máquinas de inferencia: desactive los estados C profundos con
intel_idle.max_cstate=1 processor.max_cstate=1. - Ajuste los búferes de red solo si ha medido un cuello de botella en una ruta de almacenamiento o streaming a 25/100 GbE.
- Sáltese las hugepages explícitas de 1 GB salvo que tenga un benchmark que demuestre presión en la TLB.
- Mida antes y después de cada cambio. De uno en uno. Documente.
Referencias cruzadas: L01 para el driver y el anclaje de versión del kernel, L02 para la pila CUDA / de contenedores, L04 para la capa de sistema de archivos y L05 para la monitorización.
En el Linux moderno, el ajuste del kernel es sobre todo configuración de una sola vez, no un juego de optimización continua. Acierte con el gobernador, la colocación NUMA y los límites. Sáltese el resto salvo que su benchmark diga lo contrario.
Esto forma parte de la Kentino Wiki, una serie de referencia sobre computación de IA, robótica y los sistemas que las conectan. Comentarios y correcciones son bienvenidos en info@kentino.com.