es sencillo. Cargas los pesos, cargas los datos y esperas a que termine. Para la mayoría de los modelos, eso está bien y el único costo real es el tiempo. Cuando la espera se hace demasiado larga, la solución habitual es agregar GPU. Cada uno entrena en una porción diferente de datos en paralelo y el trabajo finaliza más rápido. Nada sobre el estado del modelo cambia; acabas de echarle más manos a la obra.
Esto cubre el caso común, donde el problema es el tiempo. Los modelos grandes añaden un segundo problema que más manos no pueden resolver: el espacio. Durante el año pasado, una parte importante de mi trabajo implicó entrenar y ajustar grandes modelos básicos en diferentes dominios, incluidos modelos unicelulares y modelos transformadores de visión y lenguaje. Una vez que se superan unos cuantos miles de millones de parámetros, el modelo, sus gradientes y sus estados optimizadores ya no caben en una sola GPU. No se puede simplemente replicar el trabajo entre GPU porque una copia no cabe en ninguna de ellas. Tienes que dividir el modelo en sí.
Estos dos problemas se corresponden con dos estrategias. Tiempo de ataque DDP (Datos Distribuidos Paralelos). Cada GPU guarda una copia completa del modelo y entrena en una porción diferente del lote. FSDP (Fully Sharded Data Parallel) ataca el espacio. El modelo se divide en partes para que ninguna GPU tenga que contener todo el modelo.
Sin embargo, en realidad no hay sólo dos opciones. Estos son sólo los dos extremos de un dial. Entre estos se encuentran las etapas ZeRO de DeepSpeed (la biblioteca de capacitación de Microsoft), que le permiten deslizarse de un extremo al otro, paso a paso, intercambiando un poco de memoria por un poco de comunicación en cada parada en lugar de saltar directamente de "todos tienen todo" a "nadie lo tiene".
En la primera parte del artículo se trata dónde configurar ese dial y qué controla realmente. Pero hay un segundo factor que decide qué tan bien funcionan cualquiera de estas estrategias. Cada uno de ellos funciona moviendo datos entre GPU. Y la velocidad a la que se mueven esos datos depende de los cables físicos que los conectan. Esto fue algo en lo que no pensé hasta que el mismo código, en el mismo tipo de GPU, se ejecutó varias veces más lento en un nodo que en otro. Entonces, en la segunda mitad, discutiremos el hardware que se encuentra debajo y cómo el cableado también debe ser algo que usted considere. (Para las personas a las que no les gusta mucho el hardware, créanme, ¡no es tan malo!)
Una nota antes de comenzar. Aquí todo se mide, no sólo la teoría. Las comparaciones de estrategias se realizaron en cuatro GPU A100 de 80 GB; Los experimentos de hardware se realizaron en máquinas H200, incluidas dos que tienen exactamente las mismas GPU conectadas entre sí de diferentes maneras, lo que resulta ser el punto.
Parte 1: El software: lo que contiene cada GPU
Cada estrategia de entrenamiento distribuido es en realidad una respuesta a una pregunta: ¿de qué guarda cada GPU su propia copia? Para que esto tenga sentido, veamos un ejemplo.
Tome Mistral-7B y ajústelo con Adam en precisión mixta BF16. Los parámetros ocupan 14 GB: 7 mil millones de ellos, dos bytes cada uno. Los gradientes ocupan otros 14 GB. Luego tenemos los estados del optimizador, que son la parte más cara. Adam mantiene dos promedios móviles para cada parámetro, impulso y varianza, ambos almacenados en FP32, lo que equivale aproximadamente a 58 GB. Súmelo y llegamos a 87 GB antes de que el modelo haya visto un solo lote o se haya calculado una sola activación.
Para una GPU A100 que tiene 80 GB de VRAM, el modelo no encaja. Entonces distribuimos. Y la pregunta, como se mencionó al inicio de esta sección, es: ¿qué cabe en cada GPU?
Datos distribuidos en paralelo (DDP): todos tienen todo
La respuesta simple es que cada GPU lo contiene todo. Este es DDP, y es el que la mayoría de la gente encuentra por primera vez (que también fue mi caso). Cada GPU guarda una copia completa del modelo: todos los parámetros, todos los gradientes, todos los estados del optimizador. Lo que se divide son los datos. Cada GPU toma una porción diferente del lote, ejecuta su propio paso hacia adelante y hacia atrás y calcula gradientes a partir de su propia porción.
El problema es que esos gradientes son todos diferentes porque cada GPU vio datos diferentes. Si cada GPU aumentara su optimizador ahora, las copias se separarían y ya no tendrías un modelo, tendrías cuatro. Entonces, antes de dar un paso, las GPU promedian sus gradientes. Cada GPU envía sus gradientes, recibe los de todas las demás y todas terminan con el mismo gradiente promediado. Luego realizan un paso de optimización idéntico y las copias permanecen sincronizadas. Ese promedio es la única pieza de comunicación que DDP necesita y ocurre una vez por paso. Se llama reducción total y volveremos a ello más adelante cuando comencemos a contar lo que cuesta cada estrategia.
DDP es rápido y sencillo precisamente porque comunica muy poco. Una reducción total por paso, y PyTorch se superpone incluso con el paso hacia atrás, por lo que con un buen hardware, apenas se nota. Pero su limitación está integrada en el diseño. Cada GPU contiene todo, lo que significa que agregar GPU no soluciona el problema de memoria. Si 87 GB no caben en un A100, tampoco caben en cuatro, porque cada uno de los cuatro todavía tiene capacidad para 87 GB completos. Agregar GPU a DDP aumenta el rendimiento, pero no reduce la memoria que debe contener cada GPU.
Datos paralelos totalmente fragmentados (FSDP): nadie lo tiene todo
Si el problema es que cada GPU contiene todo, la solución obvia es asegurarse de que ninguna GPU lo contenga. Este es el FSDP. En lugar de que cada GPU conserve una copia completa, el modelo se divide en partes y cada GPU posee solo una parte de los parámetros, una parte de los gradientes y una parte de los estados del optimizador. En cuatro GPU, cada una tiene aproximadamente una cuarta parte de los 87 GB, lo que permite que el modelo quepa.
Pero esto plantea un problema inmediato. No se puede ejecutar una capa a través de una GPU que solo tiene una cuarta parte del peso de esa capa. Entonces, FSDP vuelve a ensamblar cada capa justo antes de que sea necesaria y la desmonta nuevamente inmediatamente después. Cuando el pase hacia adelante llega a una capa, las GPU intercambian sus piezas para que cada GPU tenga brevemente los pesos completos de esa capa, la capa se ejecuta y luego cada GPU desecha las piezas que no le pertenecen. Aparece la siguiente capa y vuelve a suceder lo mismo. El modelo completo nunca reside en una sola GPU; sólo una capa lo es, y sólo por el instante está computando. Lo mismo sucede a la inversa durante el pase hacia atrás.
Ese comercio tiene nombres, de la misma manera que los promedios de DDP. Reunir las piezas de todos en una capa completa es un conjunto completo, y distribuir los gradientes sumados a sus propietarios es una reducción-dispersión. Tenga en cuenta este contraste. DDP se comunica una vez por paso. FSDP se comunica constantemente. Una reunión completa antes de cada capa en el pase hacia adelante, otra antes de cada capa en el pase hacia atrás y una reducción-dispersión después. Ese es básicamente el intercambio que haces cuando cambias entre ellos. FSDP compra memoria y la paga en comunicación.
Una aclaración es importante aquí, porque podría resultar un poco confuso. FSDP sigue siendo paralelismo de datos. Cada GPU ejecuta el modelo completo en su propia porción del lote, exactamente como DDP. La única diferencia es que las pesas se almacenan en piezas y se vuelven a ensamblar según sea necesario en lugar de conservarse como una copia completa. Cada GPU todavía calcula todo el paso hacia adelante y hacia atrás de principio a fin.
Esto es diferente del paralelismo de modelos, donde el modelo en sí se divide entre GPU y cada una es responsable de solo una parte del cálculo, un conjunto de capas o una porción de cada capa. En este paradigma, un único pase hacia adelante tiene que saltar entre GPU porque nadie puede terminarlo solo. Además de esto, incluso dentro del paralelismo del modelo, tenemos el paralelismo de capas y tensores, que dictan si dividimos el modelo vertical u horizontalmente entre las capas. Pero antes de divagar demasiado, estas son técnicas diferentes con diferentes patrones de comunicación, y no es de eso de lo que trata este artículo. Todo aquí, DDP, FSDP y las etapas ZeRO intermedias, son datos paralelos. El mismo cálculo en cada GPU, solo que con datos diferentes. Sólo el almacenamiento del modelo difiere de una estrategia a otra.
Volviendo a ello, ahora que hemos discutido la teoría, veamos qué sucede en la práctica. Para ver cómo se comparan estas dos estrategias, realicé experimentos en dos modelos. DINOv2 (un modelo básico de visión) y Mistral-7B (un modelo de lenguaje grande). Ambos experimentos se ejecutaron en cuatro GPU A100 de 80 GB en un solo nodo, utilizando HuggingFace Accelerate con precisión mixta BF16. Lo único que cambió entre carreras fue la estrategia.
DINOV2 sobre Alimentos-101. El primer experimento afina DINOv2 en Food-101, un conjunto de datos de clasificación con 101 categorías de alimentos y alrededor de 101.000 imágenes. La tarea es predecir una categoría de imagen dada una imagen. Probé tanto la red troncal base (parámetros 86M) como la grande (parámetros 304M), usando un tamaño de lote de 32 por GPU para el modelo base y 256 por GPU para el modelo grande. Este es un caso en el que ambas estrategias funcionan ya que el modelo cabe cómodamente en la memoria de la GPU. Pero incluso aquí podemos ver la diferencia de memoria entre los dos enfoques.
FSDP utiliza significativamente menos memoria por GPU: 0,39 GB frente a 1,79 GB para el modelo base y 1,39 GB frente a 6,26 GB para el modelo grande. Eso es aproximadamente 4,5 veces menos memoria con FSDP. Esto importa menos cuando tienes mucho espacio libre, pero nos muestra por qué FSDP será esencial para los modelos más grandes. Entonces vimos la memoria, ¿qué pasa con la velocidad?
Podemos ver que DDP es consistentemente más rápido, alrededor de un 6% más de rendimiento para el modelo base y un 4% para el grande. Esto es de esperarse ya que DDP tiene menos gastos generales de comunicación. Solo tiene una reducción total por paso, frente a la recolección y descarte de FSDP en cada capa. Y cuando vemos el tiempo de entrenamiento:
La conclusión aquí es que cuando el modelo cabe en la memoria, DDP es la mejor opción. Es más sencillo y rápido. Pero observe la brecha de memoria. ¿Qué sucede cuando escalamos a un modelo en el que esa brecha realmente importa?
Mistral-7B en Alpaca. Para la segunda parte, intenté ajustar el modelo Mistral en el conjunto de datos de seguimiento de instrucciones de Alpaca. Con DDP, el trabajo falló inmediatamente debido a un error de falta de memoria. Con FSDP, funcionó sin problemas.
DDP necesitaría aproximadamente 87 GB por GPU para el estado del modelo (parámetros, gradientes, optimizadores), lo que supera los 80 GB disponibles en el A100. FSDP divide esto en 4 GPU, lo que reduce el requisito estimado a aproximadamente 22 GB por GPU. El pico real durante la ejecución fue de aproximadamente 29 GB, lo que incluye activación y otros gastos generales. Por eso existe FSDP. Hace posible el entrenamiento cuando DDP simplemente no puede ajustarse al modelo.
Entonces esa es la regla en la que solía detenerme. ¿El modelo, con sus gradientes y estados optimizadores, cabe en una GPU? En caso afirmativo, utilice DDP. Si no, utilice FSDP. Es una buena regla. Pero en su interior se esconden dos supuestos. La primera es que DDP y FSDP son las dos únicas configuraciones. Al principio ya insinué que no lo son. Hay un dial entre ellos y, en la siguiente parte, analizaremos estas etapas, en las que se fragmenta parte del estado del modelo, pero no todo.
La segunda suposición es que tratamos a todas las máquinas de la misma manera. Estas estrategias no se comportan igual en todas partes. La misma estrategia en el mismo modelo puede ejecutarse varias veces más rápido o más lento dependiendo de cómo estén conectadas las GPU. De eso trata la segunda mitad del artículo. Pero primero, el dial.
ZeRO: el dial entre DDP y FSDP
Para ver las paradas en el dial, comience con un pequeño dato sobre las operaciones de la Parte 1. La reducción total de DDP, la que promedia los gradientes entre las GPU, son en realidad dos operaciones más simples unidas. Primero está la reducción-dispersión, que suma los gradientes de todos pero le da a cada GPU solo su porción del resultado en lugar de darles a todos el resultado completo. Luego, se reúne todo, que reúne esas porciones en una copia completa en cada GPU. Entonces, todo-reducir es básicamente reducir-dispersar más todo-reunir.
Esas son las mismas dos operaciones que utiliza FSDP. Ahora que sabemos que las operaciones se comparten, tiene sentido que la cantidad de fragmentación pueda realizarse por etapas. Entonces, el dial es solo una cuestión de cuánto está dispuesto a mantener fragmentado entre pasos en lugar de volver a ensamblarlo.
Para concretar eso, observe lo que cada GPU almacena por parámetro. En Adam de precisión mixta, hay tres cosas: los parámetros, los gradientes y los estados del optimizador, siendo los estados del optimizador, con diferencia, los más pesados. DDP mantiene copias completas de los tres en cada GPU. Las etapas ZeRO, de DeepSpeed, las quitan una a la vez, las más pesadas primero.
ZeRO-1 fragmenta solo los estados del optimizador. Cada GPU aún conserva todos los parámetros y gradientes completos, pero solo conserva su porción de los estados del optimizador. Dado que esos son los elementos más pesados, esto brinda la mayor ganancia de memoria y cuesta muy poco en comunicación adicional, porque nada de lo que necesita durante el pase hacia adelante y hacia atrás se ha movido.
ZeRO-2 agrega gradientes a los fragmentos. Ahora cada GPU conserva solo su porción de gradientes y su porción de estados del optimizador, pero sigue siendo una copia completa de los parámetros. La comunicación todavía está cerca de la de DDP, ya que los parámetros (lo que realmente calcula) siguen siendo locales.
ZeRO-3 también agrega los parámetros. Ahora, ninguna GPU contiene el modelo completo, por lo que antes de que se pueda ejecutar una capa, sus parámetros deben recopilarse sobre la marcha, usarse y descartarse. ZeRO-3 y FSDP implementan la misma idea básica. Uno está construido por DeepSpeed y el otro es nativo de PyTorch. Entonces, dado que aquí es donde deja de mantener los parámetros locales, también tendrá la comunicación adicional que discutimos anteriormente.
El siguiente diagrama muestra el proceso desde la replicación completa hasta la fragmentación completa:
Vale la pena detenerse en dos cosas de la figura. La primera es la columna de memoria de la derecha. Esos son los picos medidos para Mistral-7B en cuatro GPU, y caen en un patrón limpio a medida que baja el dial. 87 GB en DDP, luego 55, 51, 40 y 37 GB. Pero tenga en cuenta que nunca cae a un cuarto de 87 GB, que es lo que se esperaría si cuatro GPU tuvieran cada una un cuarto de todo. El piso se compone principalmente de activaciones y tiempo de ejecución. Las activaciones todavía se producen de forma independiente en cada GPU, por lo que fragmentar el estado del modelo no las hace desaparecer.
En segundo lugar, cada paso hacia abajo en ese dial compra memoria al gastar comunicación. ZeRO-1 no gasta casi nada. ZeRO-3 y FSDP son los que más gastan. Esa es la vista del software. DDP, ZeRO y FSDP son respuestas diferentes a la misma pregunta: ¿qué debería conservar localmente cada GPU y qué debería obtener de las demás? Avanzar hacia la fragmentación compra memoria, pero también crea más comunicación.
Hasta ahora sólo hemos contado cuántos datos deben moverse. No hemos preguntado qué tan caro es ese movimiento. Eso depende del hardware. Más específicamente, depende de la estructura que conecta las GPU.
Parte 2: El hardware: qué tan rápido pueden hablar las GPU
Los cables entre las GPU
Todas las estrategias de la Parte 1 tenían el mismo costo oculto: la comunicación. DDP lo paga una vez por paso. FSDP lo paga en cada capa. ZeRO te permite elegir cuánto pagar. Pero la comunicación no es un costo abstracto. Son datos que se mueven entre chips y el camino que toman importa.
En este artículo, solo analizo la comunicación dentro de un único nodo. Una vez que el entrenamiento abarca varios servidores, aparece otra capa. Las GPU ahora tienen que comunicarse a través de InfiniBand o Ethernet, lo que genera un conjunto diferente de cuellos de botella. Eso también importa mucho, pero es un artículo aparte.
Dentro de un nodo, cuando las GPU necesitan intercambiar datos, ¿cómo lo hacen? Bueno, hay diferentes caminos que pueden tomar y la velocidad entre ellos puede diferir hasta 10 veces. El mismo código FSDP puede ejecutarse a máxima velocidad en una máquina y rastrearse en otra con exactamente las mismas GPU, simplemente por cómo esas GPU están conectadas entre sí.
Hay dos rutas básicas que el tráfico de GPU puede tomar dentro de un servidor: PCIe y NVLink. PCIe es la ruta predeterminada del sistema a través de la placa base. NVLink es la interconexión directa de GPU a GPU de NVIDIA.
Pero NVLink es sólo el enlace. La topología es igualmente importante. En las máquinas que probé, NVLink apareció en dos disposiciones diferentes. Grupos de puente NVL y NVSwitch. Eso nos da cuatro términos para mantener el orden. PCIe, NVLink, NVL y NVSwitch. Están relacionados, pero no son el mismo tipo de cosas. PCIe y NVLink son los caminos. NVL y NVSwitch son formas de organizar la ruta rápida de NVLink a través de múltiples GPU.
PCIe: la conexión predeterminada
PCIe (PCI Express) es el bus estándar de alta velocidad que conecta los componentes a la placa base de una computadora, las ranuras a las que se conectan la tarjeta gráfica, el SSD y la red. Cada GPU se encuentra en él de forma predeterminada.
Cuando dos GPU necesitan intercambiar datos a través de PCIe, estos pasan a través del sistema: desde una GPU, a través del bus compartido, a menudo a través de la CPU y la memoria del host, y hacia la otra. Funciona entre dos dispositivos cualesquiera, pero es la opción más lenta y todos los dispositivos en el autobús compiten por los mismos carriles. Una ranura PCIe Gen5 x16 mueve alrededor de 64 GB/s en una dirección.
NVLink: una conexión directa de GPU a GPU
NVLink es la interconexión dedicada de NVIDIA. Enlaces que corren de una GPU a otra sin pasar por la CPU o la memoria del sistema. En un H100 o H200, cada GPU tiene 18 conexiones NVLink, que en conjunto suman aproximadamente 450 GB/s en cada dirección, aproximadamente 7 veces lo que proporciona una sola ranura PCIe.
Pero NVLink es sólo el enlace. Lo que importa tanto es cómo se organizan esos enlaces entre las GPU de un servidor. Dos máquinas pueden tener GPU H200 y aun así comportarse de manera muy diferente si una usa NVLink a través de puentes y la otra usa NVSwitch.
NVL: enlaces dentro de grupos, PCIe entre ellos
La disposición más económica conecta NVLink sólo dentro de pequeños grupos de GPU, utilizando puentes físicos entre tarjetas. NVIDIA las vende como tarjetas "NVL", como la H200 NVL. En las máquinas que probé, se unieron ocho tarjetas en dos grupos de cuatro. Dentro de un grupo, cada par de GPU tiene un puente NVLink rápido. Entre grupos, no hay ningún NVLink y el tráfico vuelve a PCIe.
Entonces la tela es desigual. Algunos pares de GPU se comunican a través de NVLink, otros a través de PCIe. Entonces, el par que obtenga depende de en qué GPU físicas se encuentre su trabajo, lo cual lo decide el programador del clúster. En una máquina NVL, la ubicación es importante ya que puede afectar el rendimiento.
NVSwitch: cada GPU conectada entre sí
El acuerdo premium pone un interruptor en el medio. Los chips NVSwitch dedicados conectan cada GPU con todas las demás GPU a la velocidad máxima de NVLink, sin pares rápidos ni pares lentos. Cada GPU está a un salto de distancia de las demás. Esto es lo que hay en las placas base HGX de NVIDIA, el factor de forma SXM, y es el diseño que utilizan los grandes ciclos de entrenamiento.
Para tener una idea de hasta dónde llega esta idea, en Computex 2025, Jensen Huang mostró lo que NVIDIA llama NVLink Spine, una columna de alrededor de 5000 cables que conectan 72 GPU en una única estructura integral y afirmó que mueve alrededor de 130 TB/s. Esto es más que el tráfico máximo de todo Internet. Tome la comparación como quiera, pero el punto subyacente es real. Una estructura NVSwitch es una barra transversal en la que cada GPU se conecta entre sí a máxima velocidad y se escala a un bastidor completo. La máquina que probé es una versión mucho más pequeña de la misma idea. Tiene 8 GPU en lugar de 72, pero conectadas según el mismo principio.
Misma GPU, dos máquinas diferentes
Vale la pena aclarar una confusión que puede ocurrir. Cuando decimos "H200", principalmente nombramos la GPU. Los sistemas H200 se pueden construir en diferentes factores de forma y estructuras: como GPU SXM en una placa base NVSwitch o como tarjetas NVL conectadas mediante puentes. Ambas son GPU de clase H200 con 141 GB de memoria, pero no son el mismo sistema. Los límites de potencia, el diseño de la placa y la refrigeración pueden variar. Pero el punto aquí es que no se puede inferir la estructura de GPU a GPU únicamente a partir del nombre del modelo de GPU. Tienes que mirar la topología. Aquí están las dos máquinas utilizadas para los experimentos a continuación. Puede verificar el cableado de su nodo con nvidia-smi topo -m.
En el nodo SXM, cada par lee NV18. Esto es uniforme, a máxima velocidad, cada GPU es igual. En el nodo NVL, las mismas tarjetas H200 se dividen en dos grupos de cuatro, NVLink rápido dentro de un grupo y un PCIe simple. Un trabajo que aterriza en cuatro GPU dentro de un grupo se ejecuta casi a máxima velocidad. Un trabajo repartido entre grupos desciende a PCIe. La siguiente sección trata sobre exactamente cuánto cuesta eso.
Medir los cables
Tenemos dos tipos de medidas aquí. El primero es un microbenchmark que mide las operaciones colectivas exactas de las que dependen las estrategias. Reducción total para DDP, recopilación total y dispersión reducida para FSDP, e informa el ancho de banda que logra cada uno. El segundo es el entrenamiento de extremo a extremo, el mismo ajuste fino del Mistral-7B de la Parte 1, ahora programado para un rendimiento de estado estable, de modo que podamos ver si las diferencias en el ancho de banda realmente aparecen en el entrenamiento. Todo lo siguiente se ejecutó en las dos máquinas descritas anteriormente.
Comencemos con la comparación más limpia posible. Tome dos GPU H200 SXM y ejecute los mismos colectivos dos veces. Para la primera ejecución, déjeles usar NVLink y, para la segunda, fuercelos a usar PCIe. Mismas GPU, mismo código, solo cambia el cable.
El cable por sí solo vale entre 10 y 11 veces. Se trata del mismo silicio y el mismo funcionamiento, donde la única diferencia era la conexión física por la que viajan los datos.
A continuación, medí cómo cambia el ancho de banda reducido a medida que agrega GPU al trabajo. Para esto, tuve que controlar qué GPU físicas usaba cada ejecución, para poder mantener un trabajo dentro de un solo grupo NVL o forzarlo a cruzar entre grupos.
Podemos ver tres comportamientos aquí. NVSwitch se mantiene plano y rápido. 330 GB/s en dos GPU, aumentando suavemente a 467 en ocho, y nunca importa qué GPU físicas obtenga el trabajo. Cada par es NV18, por lo que la tela se ve igual sin importar cómo la llene el programador. Este es el comportamiento que asumen las grandes ejecuciones de entrenamiento.
El NVL quad es una sorpresa y todo se reduce a cómo se comparten los enlaces NVLink. Cada una de estas GPU tiene 18 enlaces NVLink para distribuir entre sus vecinas. Dentro de un quad de cuatro, cada GPU tiene tres vecinos y proporciona seis enlaces a cada uno, que es el NV6 que verías en la matriz de topología. Seis enlaces por par, dieciocho enlaces en total por GPU.
Ese hecho de compartir es la razón por la que el tamaño del trabajo es tan importante. Un trabajo que utiliza solo dos GPU del cuádruple se comunica a través de solo los seis enlaces que conectan ese par. Los otros 12 enlaces en cada tarjeta van a GPU que no están en funcionamiento, por lo que permanecen inactivas y el par funciona a aproximadamente 114 GB/s, que es un tercio de la capacidad NVLink de la tarjeta. Agregue las otras dos GPU y cada tarjeta ahora se comunicará con sus tres vecinas a la vez, iluminando los 18 enlaces. Esa es la misma cantidad de enlaces que usa un módulo SXM, razón por la cual un cuádruple completo alcanza aproximadamente 322 GB/s, casi igualando a NVSwitch.
Así que el quad NVL nunca fue lento. Simplemente estaba infrautilizado. Un trabajo pequeño deja la mayoría de sus enlaces inactivos. Cuando llenamos el quad, llenamos los enlaces.
La tercera línea es la caja cross quad y nunca sale del territorio PCIe. 28 GB/s en dos GPU, 35 en cuatro, 39 en ocho. Agregar GPU no ayuda porque el problema no es cuántos enlaces están activos; es que el trabajo tiene que cruzar entre quads y no hay ningún NVLink allí. Un colectivo es tan rápido como su salto más lento requerido, por lo que un único enlace PCIe entre los quads arrastra toda la reducción a la velocidad PCIe, sin importar qué tan rápido se ejecuten los demás enlaces. Y dado que un quad tiene solo cuatro GPU de ancho, cualquier trabajo de más de cuatro en esta máquina se ve obligado a cruzar. Ocho GPU no pueden evitarlo.
La vista más clara es el punto de 4 GPU en la figura. Las mismas cuatro tarjetas, en el mismo nodo, funcionan a 322 GB/s dentro de un cuádruple y alrededor de 35 GB/s en dos, una diferencia de aproximadamente 9 veces decidida únicamente por las GPU en las que se realizó el trabajo. En una máquina NVL, la ubicación realmente marca la diferencia.
¿Qué efecto tiene sobre el entrenamiento real?
Las cifras del ancho de banda demuestran que el cable es importante en un microbenchmark. También quería ver si aparece en el entrenamiento real. Entonces ejecuté el mismo ajuste fino de Mistral-7B de la Parte 1 en cuatro GPU, de tres maneras: en NVSwitch, en un quad NVL completo y en un trabajo NVL que abarca deliberadamente dos quads.
Mire primero los dos tejidos rápidos, NVSwitch y el quad NVL completo. Son casi idénticos para cada estrategia. Las cartas nunca fueron el cuello de botella, sino la colocación. (Si parece que FSDP supera al DDP dentro del quad, no le dé demasiada importancia. Las ejecuciones repetidas ponen el ruido en un pequeño porcentaje, por lo que hay un empate. El resumen honesto es que, en cualquiera de las estructuras rápidas, DDP y FSDP son iguales).
El caso de los quads es el costo de una mala colocación. El rendimiento se reduce de 3 a 5 veces. Se trata de un impacto menor que la caída de aproximadamente 10 veces el ancho de banda de la figura 9, y vale la pena comprender la brecha. La formación no es pura comunicación. Cada GPU todavía realiza cálculos reales que no se preocupan por el cable, y esos cálculos ocultan en parte la lenta comunicación detrás de ellos. Así que la penalización total del cable se diluye un poco. Pero entre 3 y 5 veces sigue siendo una diferencia muy significativa, especialmente para trabajos largos.
Y la desaceleración no se da ni siquiera en todas las estrategias. El patrón es exactamente lo que predice la Parte 1. FSDP es el que falla más, porque comunica cada capa y, en un cable lento, esas transferencias por capa no pueden esconderse detrás de la computación. DDP funciona mejor porque se comunica solo una vez por paso. Cuando el cable es rápido, la fragmentación es casi gratuita, por lo que FSDP sigue el ritmo de DDP. Cuando el cable es lento, cada bit adicional de comunicación queda expuesto, por lo que cuanto más se fragmenta una estrategia, más pierde.
Ponlo todo junto. Toda la esfera, en tres cables.
Anteriormente, discutimos que DDP y FSDP son dos extremos de un dial, con ZeRO-1, -2 y -3 como paradas entre ellos. Ese era un diagrama y ahora puedo medirlo. Aquí está el mismo trabajo de Mistral-7B en cuatro GPU, ejecutándose en cada parada del dial, en un cable rápido y en uno lento.
Comience con el panel de memoria a la derecha porque hace exactamente lo que prometía la teoría. Al bajar el dial, la memoria máxima cae en un patrón limpio. 87 GB en DDP, luego 55 en ZeRO-1, 51 en ZeRO-2 y 40 en ZeRO-3. Cada parada fragmenta una cosa más y cada una compra espacio libre. FSDP se encuentra en la parte inferior con 37 GB, pero tenga en cuenta que no es un gran paso hacia abajo con respecto a los 40 de ZeRO-3, porque FSDP y ZeRO-3 fragmentan exactamente las mismas cosas. Como mencioné anteriormente, fragmentan las mismas cosas, pero los marcos implementan y programan las cosas de manera diferente, lo que explica la pequeña brecha que vemos. Esta escalera es la parte que siempre funciona, independientemente del hardware. Si su problema es ajustar el modelo, el dial cumple, como era de esperar.
El panel de rendimiento es interesante porque el dial se comporta como dos cosas diferentes según el cable.
En el cable rápido se pierde la mitad del dial. ZeRO-1 y ZeRO-2 ceden alrededor del 20% del rendimiento de DDP y ZeRO-3 cede más. Pero FSDP, al fragmentar las mismas cosas que ZeRO-3, recupera casi todo ese rendimiento mientras usa aproximadamente la misma memoria. Esto es interesante porque las dos últimas son implementaciones del mismo algoritmo y aterrizan muy separadas en un cable rápido.
La diferencia es la implementación, no el método. Los dos marcos programan y superponen su comunicación de manera diferente, y en un cable rápido, donde la comunicación es barata, esa sobrecarga es lo principal que se mide. (Esta es la configuración DeepSpeed predeterminada de Hugging Face acelera, sin ajuste. Una configuración ajustada probablemente reduciría la brecha; el punto es lo que le brindan los valores predeterminados, no que un marco sea inherentemente más lento).
En un cable lento, el dial se aplana. Todo colapsa hacia el mismo bajo rendimiento y las diferencias entre etapas en su mayoría desaparecen. Lo caro en un cable lento es la comunicación en sí, y una vez que eso domina, la cuestión de qué tensores exactos fragmentar apenas mueve el número. En este caso, el cable marca el ritmo, no la estrategia.
Juntando todo lo anterior, ahora podemos tomar una decisión más informada.
Si estás en NVSwitch (SXM), deja de pensar en el cable. Cada GPU es igualmente rápida y cualquier GPU que le proporcione el programador es tan buena como cualquier otra. Utilice DDP si el modelo se ajusta, FSDP si no. Y dado que FSDP iguala la velocidad de DDP aquí mientras usa mucha menos memoria, es un valor predeterminado razonable incluso cuando el modelo se ajusta.
Si está en NVL y su trabajo encaja dentro de un grupo puente, fíjelo allí y obtendrá una velocidad cercana a NVSwitch. En las máquinas que probé, eso significa cuatro GPU y tú controlas la ubicación con CUDA_VISIBLE_DEVICES. FSDP es un valor predeterminado fuerte en este caso. Obtiene una velocidad casi de clase DDP con aproximadamente la mitad de la memoria.
Si está en NVL y su trabajo tiene que abarcar grupos, espere una desaceleración considerable, peor para las estrategias de comunicación intensa. Tampoco espere que una etapa ZeRO más suave recupere la velocidad, porque el dial se aplana sobre un cable lento. Si el modelo se puede replicar, el DDP simple es la opción menos mala. Si no es así, elija la etapa que encaje en la memoria y simplemente acepte que pagará el precio del cable.
Conclusión
Hemos cubierto mucho terreno. En la primera mitad analizamos las diferentes estrategias. DDP, ZeRO y FSDP. Cada uno de ellos responde a la misma pregunta de forma diferente: ¿qué debería almacenar cada GPU localmente y qué debería obtener de las demás? DDP mantiene todo replicado. FSDP lo fragmenta todo. ZeRO te ofrece paradas intermedias.
En la segunda mitad, analizamos la capa de hardware, el tejido por el que viajan esas solicitudes. En una estructura rápida, la fragmentación puede ser lo suficientemente barata como para que FSDP siga el ritmo de DDP y utilice mucha menos memoria. En un tejido lento, la misma comunicación adicional se vuelve visible y puede ralentizar las cosas significativamente.
Ésa es la lección principal. La estrategia decide cuántos datos se mueven, pero el cable decide qué tan caro es ese movimiento. Entonces, antes de iniciar un trabajo distribuido, ejecute un comando:
nvidia-smi topo -m
Le indica si sus GPU están conectadas mediante NVLink, NVSwitch o una ruta de host más lenta.
¡Gracias por leer y espero que te haya resultado útil!
Referencias y lecturas adicionales