<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CPU Offloading on kenji.blog</title><link>http://kenji.blog/es/tags/cpu-offloading/</link><description>Recent content in CPU Offloading on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>es</language><copyright>kenjinote</copyright><lastBuildDate>Fri, 11 Sep 2026 01:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/es/tags/cpu-offloading/index.xml" rel="self" type="application/rss+xml"/><item><title>Técnicas de resolución para la escasez de memoria de GPU en el desarrollo de IA (Descarga de CPU, etc.)</title><link>http://kenji.blog/es/p/ai-gpu-vram-optimization-cpu-offloading/</link><pubDate>Fri, 11 Sep 2026 01:00:00 +0900</pubDate><guid>http://kenji.blog/es/p/ai-gpu-vram-optimization-cpu-offloading/</guid><description>&lt;img src="http://kenji.blog/p/ai-gpu-vram-optimization-cpu-offloading/img/eyecatch.jpg" alt="Featured image of post Técnicas de resolución para la escasez de memoria de GPU en el desarrollo de IA (Descarga de CPU, etc.)" />&lt;h1 id="introducción-el-desarrollo-de-ia-y-el-muro-de-la-vram">Introducción: El desarrollo de IA y el &amp;ldquo;Muro de la VRAM&amp;rdquo;
&lt;/h1>&lt;p>En los últimos años, tecnologías de IA generativa como los Grandes Modelos de Lenguaje (LLM) y los Modelos de Difusión (Diffusion Models) han experimentado un rápido desarrollo. Sin embargo, al entrenar (ajuste fino / fine-tuning) o ejecutar la inferencia (Inference) de estos modelos de IA de vanguardia en entornos locales, muchos desarrolladores e investigadores se enfrentan a una barrera extremadamente física: &lt;strong>la falta de memoria de GPU (VRAM)&lt;/strong>.&lt;/p>
&lt;p>Incluso con las GPUs de gama alta para consumidores, como la NVIDIA GeForce RTX 4090, la VRAM máxima es de 24 GB, lo que hace completamente imposible cargar directamente modelos gigantescos como Llama 3 70B. Las opciones orientadas a centros de datos, como la H100 (80 GB) o la B200 (192 GB), son extremadamente costosas y no están fácilmente al alcance de individuos o equipos pequeños. Si no se puede atravesar este &amp;ldquo;Muro de la VRAM (The Wall of VRAM)&amp;rdquo;, ni siquiera será posible experimentar con los modelos más avanzados.&lt;/p>
&lt;p>En este artículo, explicaremos exhaustivamente desde la perspectiva tanto de la inferencia como del entrenamiento, las técnicas avanzadas para superar esta restricción física de la VRAM mediante ingenios en la arquitectura de software y hardware. Profundizaremos usando fórmulas matemáticas e ilustraciones en temas como la descarga de CPU, la optimización de la caché KV, los puntos de control de gradiente (Gradient Checkpointing) y las arquitecturas más recientes de memoria unificada (Unified Memory). Al leer este artículo, comprenderás profundamente el comportamiento de la VRAM y adquirirás conocimientos prácticos para manejar modelos gigantescos con recursos limitados.&lt;/p>
&lt;hr>
&lt;h1 id="1-anatomía-del-consumo-de-vram-de-los-modelos-de-ia-inferencia-y-entrenamiento">1. Anatomía del consumo de VRAM de los modelos de IA (Inferencia y Entrenamiento)
&lt;/h1>&lt;p>El primer paso para resolver la escasez de VRAM es comprender con precisión &amp;ldquo;qué&amp;rdquo; y &amp;ldquo;cuánta&amp;rdquo; memoria se está consumiendo desde una perspectiva microscópica. Si en lugar de tratarlo como una caja negra podemos estimarlo con precisión utilizando fórmulas matemáticas, podremos seleccionar los métodos de optimización adecuados.&lt;/p>
&lt;h2 id="11-cálculo-de-memoria-de-los-parámetros-del-modelo-pesos">1.1 Cálculo de memoria de los parámetros del modelo (pesos)
&lt;/h2>&lt;p>La cantidad básica de memoria consumida por los parámetros (Weights) que componen un modelo de IA se determina por el número total de parámetros del modelo y el tipo de datos (Precision: precisión) utilizado para representarlos.&lt;/p>
&lt;p>Los tipos de datos comúnmente utilizados en el aprendizaje profundo y el número de bytes por parámetro ($B$) son los siguientes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>FP32 (Punto flotante de precisión simple):&lt;/strong> 4 bytes (precisión estándar durante el entrenamiento)&lt;/li>
&lt;li>&lt;strong>FP16 / BF16 (Punto flotante de media precisión):&lt;/strong> 2 bytes (inferencia general y entrenamiento de precisión mixta)&lt;/li>
&lt;li>&lt;strong>INT8 (Entero de 8 bits):&lt;/strong> 1 byte (modelos cuantizados)&lt;/li>
&lt;li>&lt;strong>INT4 (Cuantización de enteros de 4 bits):&lt;/strong> 0.5 bytes (cuantización extrema como GPTQ, AWQ, GGUF)&lt;/li>
&lt;/ul>
&lt;p>Si el número de parámetros de todo el modelo es $P$, la cantidad base de memoria $M_{weights}$ ocupada por los pesos en sí se expresa con la siguiente fórmula:&lt;/p>
$$ M_{weights} = P \times B $$&lt;p>Por ejemplo, si cargamos el modelo &amp;ldquo;Llama 3 8B&amp;rdquo; (aproximadamente 8 mil millones de parámetros) publicado por Meta en FP16 (media precisión), el cálculo sería el siguiente:&lt;/p>
$$ M_{weights} = 8,000,000,000 \times 2 \text{ bytes} \approx 16,000,000,000 \text{ bytes} \approx 16 \text{ GB} $$&lt;p>Es decir, simplemente cargar los pesos del modelo en la GPU consume 16 GB de VRAM. En una RTX 3060 (12 GB), se produciría un error de Out of Memory (OOM) en este punto. Sin embargo, si cuantizamos el modelo a INT4, pasaría a ser de $8 \times 0.5 = 4 \text{ GB}$, lo que permitiría cargarlo sin problemas.&lt;/p>
&lt;h2 id="12-consumo-de-memoria-durante-la-inferencia-aumento-de-la-caché-kv">1.2 Consumo de memoria durante la inferencia: Aumento de la caché KV
&lt;/h2>&lt;p>En la inferencia de LLMs (especialmente en la generación de texto autorregresiva), lo que presiona la VRAM con tanta o mayor intensidad que los pesos es la &lt;strong>caché KV (Key-Value Cache)&lt;/strong>.
En la arquitectura Transformer, para evitar recalcular la información de los tokens generados y procesados en el pasado, los tensores Key y Value de cada capa de atención se mantienen en caché en la VRAM. Esto mejora la velocidad de cálculo (Compute), pero a medida que la longitud del contexto (longitud del prompt de entrada + longitud de generación) aumenta, el consumo de memoria crece linealmente de forma explosiva.&lt;/p>
&lt;p>La cantidad de memoria de la caché KV consumida al procesar 1 token, $M_{kv\_token}$, se calcula estrictamente con la siguiente fórmula, basándose en la arquitectura del modelo:&lt;/p>
$$ M_{kv\_token} = 2 \times N_{layers} \times N_{heads\_kv} \times D_{head} \times B $$&lt;p>Aquí, cada variable tiene el siguiente significado:&lt;/p>
&lt;ul>
&lt;li>$2$ : Porque existen dos tensores, Key y Value&lt;/li>
&lt;li>$N_{layers}$ : Número de capas (layers) del Transformer&lt;/li>
&lt;li>$N_{heads\_kv}$ : Número de cabezales de atención KV (en el caso de GQA: Grouped Query Attention, será menor que el número habitual de cabezales)&lt;/li>
&lt;li>$D_{head}$ : Dimensionalidad de cada cabezal (generalmente, la dimensión de la capa oculta $D_{model} / N_{heads}$)&lt;/li>
&lt;li>$B$ : Número de bytes del tipo de datos (2 en el caso de FP16)&lt;/li>
&lt;/ul>
&lt;p>La cantidad total de caché KV, $M_{kv\_total}$, es el resultado de multiplicar esto por la longitud de la secuencia ($L_{seq}$) y el tamaño del lote ($BatchSize$).&lt;/p>
$$ M_{kv\_total} = M_{kv\_token} \times L_{seq} \times BatchSize $$&lt;p>&lt;strong>Ejemplo concreto: En el caso de Llama 2 7B&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>$N_{layers} = 32$&lt;/li>
&lt;li>$N_{heads\_kv} = 32$ (En el caso de MHA)&lt;/li>
&lt;li>$D_{head} = 128$&lt;/li>
&lt;li>FP16 ($B=2$)&lt;/li>
&lt;li>Tamaño del lote 1, longitud de secuencia 8192 (contexto de 8K)&lt;/li>
&lt;/ul>
$$ M_{kv\_total} = 2 \times 32 \times 32 \times 128 \times 2 \times 8192 \times 1 = 4,294,967,296 \text{ bytes} \approx 4 \text{ GB} $$&lt;p>Si ampliáramos el contexto a 32K (32768 tokens), consumiría aproximadamente 16 GB solo en la caché KV. Si aumentáramos el tamaño del lote a 4, serían 64 GB. El hecho de que empiece a exigir mucha más VRAM que el propio tamaño del modelo es uno de los grandes retos durante la inferencia.&lt;/p>
&lt;h2 id="13-consumo-de-memoria-durante-el-entrenamiento-optimizador-gradientes-y-activaciones">1.3 Consumo de memoria durante el entrenamiento: Optimizador, gradientes y activaciones
&lt;/h2>&lt;p>En comparación con la inferencia, el entrenamiento de un modelo (preentrenamiento o ajuste fino) consume mucha más VRAM. Esto se debe a que es necesario retener no solo información del paso hacia adelante (propagación hacia adelante), sino también de la propagación hacia atrás (backpropagation). La memoria durante el entrenamiento se compone principalmente de los siguientes cuatro elementos:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Pesos del modelo (Model Weights):&lt;/strong> Similar a la inferencia, pero en el entrenamiento de precisión mixta, se pueden retener tanto los de FP16 como los de FP32 (pesos maestros).&lt;/li>
&lt;li>&lt;strong>Gradientes (Gradients):&lt;/strong> Gradientes calculados en la retropropagación por cada parámetro. En el caso de FP16, son 2 bytes por parámetro.&lt;/li>
&lt;li>&lt;strong>Estados del optimizador (Optimizer States):&lt;/strong> Optimizadores avanzados como AdamW mantienen un primer momento (Momentum) y un segundo momento (Variance) para cada parámetro. Para mantener la estabilidad del entrenamiento, estos se almacenan generalmente en FP32 (4 bytes). Es decir, consumen $4 + 4 = 8$ bytes/parámetro por los dos momentos.&lt;/li>
&lt;li>&lt;strong>Activaciones (Activations):&lt;/strong> Para el cálculo de los gradientes de retropropagación, es necesario conservar en la memoria la salida (estado intermedio) de cada capa obtenida durante la propagación hacia adelante. Esto depende enormemente del tamaño del lote y de la longitud de la secuencia, y puede volverse muy grande.&lt;/li>
&lt;/ol>
&lt;p>En resumen, en el entrenamiento de precisión mixta (Mixed Precision Training) utilizando el optimizador estándar de Adam, se requieren &lt;strong>aproximadamente entre 16 y 20 bytes&lt;/strong> por parámetro (4 de peso maestro + 2 de peso FP16 + 2 de gradiente + 8 del optimizador + α).&lt;/p>
$$ M_{train\_param} \approx P \times 16 \text{ bytes} $$&lt;p>Para entrenar un modelo de 7B (7 mil millones de parámetros), solo en aspectos relacionados con los parámetros se necesitarían $7B \times 16 = 112 \text{ GB}$, y al sumarle las activaciones, el cálculo arroja un sorprendente requerimiento de más de 140 GB de VRAM. Para ejecutar esto en una VRAM de 24 GB, es indispensable utilizar las agresivas técnicas de optimización que se explican a partir del próximo capítulo.&lt;/p>
&lt;hr>
&lt;h1 id="2-técnicas-para-el-ahorro-de-vram-en-la-inferencia">2. Técnicas para el ahorro de VRAM en la inferencia
&lt;/h1>&lt;p>Se han desarrollado muchas técnicas de software que traspasan los límites del hardware como enfoque para ejecutar modelos gigantescos durante la inferencia.&lt;/p>
&lt;h2 id="21-descarga-de-cpu-cpu-offloading-y-división-de-capas">2.1 Descarga de CPU (CPU Offloading) y división de capas
&lt;/h2>&lt;p>Cuando un modelo masivo no cabe en una o varias GPUs, la técnica de colocar una parte del modelo en la memoria del sistema (CPU RAM) y avanzar en los cálculos transfiriendo a la GPU solo cuando es necesario se conoce como &lt;strong>descarga de CPU (CPU Offloading)&lt;/strong>. Herramientas como &lt;code>llama.cpp&lt;/code> y &lt;code>Accelerate&lt;/code> de Hugging Face soportan esta funcionalidad.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;RAM del Sistema (DDR4 / DDR5)&amp;#34;] --&amp;gt; B[&amp;#34;VRAM de la GPU (GDDR6X)&amp;#34;]
B[&amp;#34;VRAM de la GPU (GDDR6X)&amp;#34;] --&amp;gt; C[&amp;#34;Núcleos Tensor (Cálculo)&amp;#34;]
subgraph &amp;#34;División de Capas y Descarga&amp;#34;
D[&amp;#34;Capas Inferiores 1-15 (Fijadas en GPU)&amp;#34;]
E[&amp;#34;Capas Superiores 16-32 (Descargadas en CPU)&amp;#34;]
end
E[&amp;#34;Capas Superiores 16-32 (Descargadas en CPU)&amp;#34;] -.-&amp;gt; B[&amp;#34;VRAM de la GPU (GDDR6X)&amp;#34;]
&lt;/pre>
&lt;p>&lt;strong>Mecanismo y desafíos:&lt;/strong>
Dado que los modelos Transformer tienen una estructura donde las capas (layers) se apilan en serie, el cálculo de una capa no comienza hasta que termina el de la capa anterior. Aprovechando esto, solo las capas que caben en la GPU (por ejemplo, de la capa 1 a la 15) se mantienen residentes (fijadas) en la VRAM, mientras que el resto de las capas (de la 16 a la 32) se alojan en la RAM de la CPU, que es de gran capacidad pero más lenta. Durante la inferencia, al terminar los cálculos hasta la capa 15, los pesos de la capa 16 se transfieren (copian) desde la CPU a la GPU a través del bus PCIe, y el cálculo se ejecuta en la GPU.&lt;/p>
&lt;p>Sin embargo, &lt;strong>el ancho de banda (Bandwidth) del PCIe se convierte en un enorme cuello de botella&lt;/strong>. El ancho de banda máximo teórico del PCIe 4.0 x16 es de 32 GB/s (unidireccional), pero en comparación con el ancho de banda interno de la VRAM de las GPUs más recientes (por ejemplo, 1008 GB/s en la GDDR6X de la RTX 4090 y más de 3 TB/s en la HBM3 de la H100), es dos órdenes de magnitud más lento, por lo que el uso intensivo de la descarga de CPU reduce drásticamente la velocidad de inferencia (Tokens por Segundo).
Para minimizar la pérdida de velocidad, el punto práctico clave es colocar tantas capas como sea posible en la GPU (maximizar las capas de GPU) y reducir al mínimo las capas descargadas.&lt;/p>
&lt;h2 id="22-cuantización-de-la-caché-kv-y-pagedattention">2.2 Cuantización de la caché KV y PagedAttention
&lt;/h2>&lt;p>Contra la caché KV, principal culpable del consumo de VRAM durante la inferencia, también se aplican dos potentes optimizaciones.&lt;/p>
&lt;p>&lt;strong>1. Cuantización de la caché KV (KV Cache Quantization):&lt;/strong>
Es una técnica donde no solo los pesos del modelo, sino también la propia caché KV generada dinámicamente en tiempo de ejecución, se cuantiza a INT8, INT4 o FP8 para almacenarla en la VRAM. Esto permite reducir el tamaño de la caché KV entre la mitad y la cuarta parte. Los motores de inferencia más recientes (vLLM, llama.cpp) incorporan esta funcionalidad, logrando un ahorro significativo de VRAM mientras mantienen al mínimo la degradación de la precisión.&lt;/p>
&lt;p>&lt;strong>2. PagedAttention:&lt;/strong>
La aplicación del concepto de &amp;ldquo;paginación&amp;rdquo; de la memoria virtual de los sistemas operativos a la caché KV es lo que se conoce como &lt;strong>PagedAttention&lt;/strong>, introducido por el motor de inferencia vLLM. En los motores de inferencia convencionales, se reservaba de antemano un área continua de VRAM (Preasignación) de acuerdo con la longitud máxima de secuencia configurada. Por ello, cuando la entrada real era más corta, se producía fragmentación y un desperdicio de la memoria no utilizada, llegando a derrochar más del 60% de la VRAM.&lt;/p>
&lt;p>PagedAttention permite dividir la caché KV en bloques (páginas) de tamaño fijo y distribuirlos y almacenarlos en espacios no contiguos de memoria física. Esto reduce casi a cero el desperdicio de memoria (limitándolo solo a la fragmentación interna) y permite aumentar significativamente el tamaño del lote con la misma capacidad de VRAM.&lt;/p>
&lt;pre class="mermaid">
graph LR
A[&amp;#34;Caché KV Lógica&amp;#34;] --&amp;gt; B[&amp;#34;Bloques Físicos de VRAM&amp;#34;]
A1[&amp;#34;Token 1, 2, 3, 4&amp;#34;] --&amp;gt; B3[&amp;#34;Bloque 3 (Asignado)&amp;#34;]
A2[&amp;#34;Token 5, 6, 7, 8&amp;#34;] --&amp;gt; B1[&amp;#34;Bloque 1 (Asignado)&amp;#34;]
A3[&amp;#34;Tokens Futuros...&amp;#34;] -.-&amp;gt; B2[&amp;#34;Bloque 2 (Libre)&amp;#34;]
&lt;/pre>
&lt;h2 id="23-flashattention-superando-la-complejidad-de-memoria-en-el-cálculo-de-atención">2.3 FlashAttention: Superando la complejidad de memoria en el cálculo de atención
&lt;/h2>&lt;p>La escasez de VRAM no solo se produce por la cantidad de memoria para almacenar datos, sino también por la falta de un &amp;ldquo;espacio de trabajo temporal&amp;rdquo; durante los cálculos. El mecanismo de Self-Attention estándar del Transformer requiere materializar (instanciar) una matriz de atención gigante de $N \times N$ en la VRAM para una longitud de secuencia $N$. Esto resulta en una complejidad de memoria de $O(N^2)$, siendo la causa principal del OOM en contextos extensos.&lt;/p>
&lt;p>Quien resolvió esto fue &lt;strong>FlashAttention&lt;/strong> (y FlashAttention-2, 3).
FlashAttention es un algoritmo diseñado teniendo en cuenta la arquitectura de hardware de la GPU (la estructura jerárquica de la enorme pero lenta HBM y la diminuta pero ultrarrápida SRAM). Utiliza una técnica llamada mosaico (Tiling) que carga los datos en la SRAM por bloques para completar los cálculos de atención, evitando por completo el proceso de escribir la matriz de $N \times N$ en la HBM (VRAM).&lt;/p>
&lt;p>Gracias a esto, la complejidad de memoria en las capas de atención se redujo drásticamente de $O(N^2)$ a $O(N)$ (proporcional a la longitud de la secuencia), lo que alivió significativamente los límites en la longitud del contexto.&lt;/p>
&lt;h2 id="24-el-ascenso-de-la-memoria-unificada-unified-memory-y-apple-silicon">2.4 El ascenso de la Memoria Unificada (Unified Memory) y Apple Silicon
&lt;/h2>&lt;p>Quienes están abordando este problema desde las raíces de la arquitectura del PC son aquellos que han adoptado la &lt;strong>Arquitectura de Memoria Unificada (Unified Memory Architecture: UMA)&lt;/strong>, como Apple Silicon (las series Max y Ultra de M1/M2/M3/M4) y algunas APU recientes (como AMD Strix Point).&lt;/p>
&lt;p>En estas arquitecturas, la CPU y la GPU en la placa base comparten exactamente la misma memoria física (por ejemplo, hasta 192 GB de LPDDR5). Por lo tanto, el concepto de una &amp;ldquo;transferencia lenta de datos desde la CPU a la GPU a través del PCIe&amp;rdquo; simplemente no existe físicamente.&lt;/p>
&lt;pre class="mermaid">
graph TD
subgraph &amp;#34;Arquitectura de Memoria Unificada (ej. Apple Silicon)&amp;#34;
A[&amp;#34;Núcleos de CPU&amp;#34;] &amp;lt;--&amp;gt; C[&amp;#34;Controlador de Memoria Compartida&amp;#34;]
B[&amp;#34;Núcleos de GPU / Motor Neuronal&amp;#34;] &amp;lt;--&amp;gt; C[&amp;#34;Controlador de Memoria Compartida&amp;#34;]
C[&amp;#34;Controlador de Memoria Compartida&amp;#34;] &amp;lt;--&amp;gt; D[&amp;#34;Fondo de Memoria Unificada (ej. 192GB)&amp;#34;]
end
&lt;/pre>
&lt;p>La mayor ventaja de esta arquitectura es que no hay un límite estricto de VRAM, lo que permite utilizar casi toda la memoria del sistema directamente para cargar LLMs masivos. Con una Mac Studio que cuenta con 192 GB de memoria unificada, es posible cargar modelos gigantescos de la clase 70B o superiores (como Grok-1) sin cuantizar en un solo dispositivo e inferir rápidamente. El ancho de banda de acceso a memoria también alcanza los 800 GB/s en el M2 Ultra, presumiendo de velocidades comparables a las de las GPUs discretas para consumidores. Es un enfoque extremadamente poderoso que resuelve el dilema entre &amp;ldquo;capacidad de memoria&amp;rdquo; y &amp;ldquo;ancho de banda&amp;rdquo; a nivel de hardware.&lt;/p>
&lt;hr>
&lt;h1 id="3-técnicas-para-el-ahorro-de-vram-en-el-entrenamiento-ajuste-fino">3. Técnicas para el ahorro de VRAM en el entrenamiento (Ajuste fino)
&lt;/h1>&lt;p>Durante el entrenamiento (Training), que requiere aún más VRAM que la inferencia, también han surgido numerosos avances. Para realizar el ajuste fino con recursos limitados, es fundamental combinar las siguientes tecnologías.&lt;/p>
&lt;h2 id="31-puntos-de-control-de-gradiente-gradient-checkpointing">3.1 Puntos de control de gradiente (Gradient Checkpointing)
&lt;/h2>&lt;p>En la retropropagación (backpropagation) del aprendizaje profundo, es necesario retener en la memoria las salidas intermedias (Activations) de todas las capas de la propagación hacia adelante para poder calcular los gradientes. Cuando la longitud de la secuencia o el tamaño del lote aumentan, esta memoria de activaciones comienza a dominar la VRAM.&lt;/p>
&lt;p>&lt;strong>Los Puntos de control de gradiente (Gradient Checkpointing / Activation Recomputation)&lt;/strong> son una técnica genial que aprovecha el equilibrio entre la capacidad de memoria y el tiempo de cálculo (Compute).
En lugar de guardar todas las salidas intermedias en la memoria, solo se guardan las salidas de capas específicas (puntos de control). Durante la retropropagación, si se necesita un valor intermedio que no fue guardado en un punto de control, &lt;strong>se vuelve a calcular la propagación hacia adelante (recálculo) desde el punto de control guardado más cercano para restaurar ese valor&lt;/strong>.&lt;/p>
&lt;p>Aunque la cantidad de cálculo aumenta en aproximadamente un 20 a 30% y el tiempo total de entrenamiento se alarga, se logra reducir drásticamente el consumo de VRAM debido a las activaciones, pasando de $O(N)$ (donde $N$ es el número de capas) a $O(\sqrt{N})$. En el entrenamiento de grandes modelos en la actualidad, es un parámetro de configuración tan imprescindible que se podría decir que no se puede empezar sin él.&lt;/p>
&lt;h2 id="32-lora-y-qlora-adaptación-de-bajo-rango">3.2 LoRA y QLoRA (Adaptación de Bajo Rango)
&lt;/h2>&lt;p>El principal artífice que resolvió el problema de la escasez de VRAM de raíz es &lt;strong>LoRA&lt;/strong>, la técnica más representativa de PEFT (Ajuste Fino Eficiente en Parámetros / Parameter-Efficient Fine-Tuning).&lt;/p>
&lt;p>Se congela (Frozen) la enorme matriz de pesos original del modelo $W_0 \in \mathbb{R}^{d \times k}$, de modo que no sea entrenada. En su lugar, se introducen en paralelo dos matrices de bajo rango muy pequeñas, $A \in \mathbb{R}^{r \times k}$ y $B \in \mathbb{R}^{d \times r}$, y se entrena únicamente a $A$ y $B$. (Aquí, el rango $r$ es un valor pequeño tal que $r \ll d, k$).&lt;/p>
$$ W_{adapted} = W_0 + \Delta W = W_0 + B A $$&lt;p>Con esto, la cantidad de parámetros a entrenar se reduce a menos del 1% (a veces menos del 0.1%) del original y, consecuentemente, los &amp;ldquo;gradientes&amp;rdquo; y los &amp;ldquo;estados del optimizador&amp;rdquo; que devoraban memoria también caen de manera drástica a menos del 1%.&lt;/p>
&lt;p>Además, &lt;strong>QLoRA (Quantized LoRA)&lt;/strong> lleva esto a su máxima evolución.
En QLoRA, los pesos del modelo base $W_0$ se cuantizan al extremo en 4 bits (formato NF4: NormalFloat4) para cargarse en la VRAM. Luego, las pequeñas matrices $A, B$ de LoRA se entrenan en BF16 (16 bits) para conservar la precisión de los cálculos.&lt;/p>
&lt;p>Mientras que la cuantización de 4 bits reduce el tamaño en VRAM del modelo base a un cuarto del original, se emplea una técnica llamada &lt;strong>Paged Optimizers&lt;/strong> (optimizadores paginados) que, en caso de que la VRAM esté a punto de agotarse, automáticamente resguarda temporalmente (descarga) el estado del optimizador en la RAM de la CPU. De este modo, incluso con una única GPU de 24 GB de VRAM (como la RTX 4090), se hizo posible el ajuste fino de modelos supermasivos como Llama 3 70B.&lt;/p>
&lt;h2 id="33-deepspeed-zero-y-la-descarga-offloading">3.3 DeepSpeed ZeRO y la Descarga (Offloading)
&lt;/h2>&lt;p>En entornos donde se usan múltiples GPUs (Multi-GPU), la mera paralelización de datos (Data Parallelism) no resuelve el problema de la VRAM. Esto se debe a que, como cada GPU conserva una copia entera del modelo, no se puede superar el límite individual de la capacidad de cada VRAM.&lt;/p>
&lt;p>&lt;strong>ZeRO (Zero Redundancy Optimizer)&lt;/strong>, de la librería &lt;strong>DeepSpeed&lt;/strong> desarrollada por Microsoft, es una técnica que divide (fragmenta) exhaustivamente los parámetros, gradientes y estados del optimizador del modelo a lo largo de varias GPUs. Esto permite tratar la &amp;ldquo;suma total&amp;rdquo; de la VRAM de múltiples GPUs como un único y gigantesco fondo de memoria.&lt;/p>
&lt;pre class="mermaid">
graph TD
subgraph &amp;#34;Etapa 3 de ZeRO (Particionamiento de Parámetros)&amp;#34;
A[&amp;#34;GPU 0&amp;#34;] --&amp;gt; D[&amp;#34;Partición 0 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;]
B[&amp;#34;GPU 1&amp;#34;] --&amp;gt; E[&amp;#34;Partición 1 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;]
C[&amp;#34;GPU 2&amp;#34;] --&amp;gt; F[&amp;#34;Partición 2 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;]
end
D[&amp;#34;Partición 0 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;] &amp;lt;--&amp;gt; E[&amp;#34;Partición 1 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;]
E[&amp;#34;Partición 1 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;] &amp;lt;--&amp;gt; F[&amp;#34;Partición 2 (Almacena 1/3 de Pesos/Gradientes/Opts)&amp;#34;]
&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>Etapa 1 de ZeRO:&lt;/strong> Se dividen los estados del optimizador en cada GPU&lt;/li>
&lt;li>&lt;strong>Etapa 2 de ZeRO:&lt;/strong> También se dividen los gradientes en cada GPU&lt;/li>
&lt;li>&lt;strong>Etapa 3 de ZeRO:&lt;/strong> Los propios parámetros del modelo (pesos) también se dividen en cada GPU&lt;/li>
&lt;/ul>
&lt;p>Además, al emplear una función llamada &lt;strong>ZeRO-Offload&lt;/strong>, se puede &lt;strong>descargar (offload) en la memoria de la CPU&lt;/strong> el cálculo de actualización de los gradientes y el estado del optimizador particionado por ZeRO, de forma que los ejecute la CPU anfitriona en lugar de la GPU. Gracias a esto, la carga de la VRAM de la GPU se reduce al mínimo, permitiendo el entrenamiento de modelos masivos incluso en entornos con GPUs limitadas. Puesto que los cálculos se realizan en la CPU y los resultados se devuelven a la GPU a través de PCIe, la velocidad de entrenamiento disminuye, pero así se evita la peor situación posible: que el &amp;ldquo;entrenamiento colapse por falta de memoria&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h1 id="4-ejemplos-de-implementación-accelerate-de-hugging-face-y-deepspeed">4. Ejemplos de implementación: Accelerate de Hugging Face y DeepSpeed
&lt;/h1>&lt;p>Para concluir, mostraremos un ejemplo sencillo de cómo implementar realmente la descarga de CPU y la optimización de VRAM mediante código en Python.&lt;/p>
&lt;h2 id="41-descarga-automática-con-device_mapauto-en-hugging-face">4.1 Descarga automática con &lt;code>device_map=&amp;quot;auto&amp;quot;&lt;/code> en Hugging Face
&lt;/h2>&lt;p>Con las librerías &lt;code>transformers&lt;/code> y &lt;code>accelerate&lt;/code> de Hugging Face, al cargar un modelo, las capas se pueden dividir automáticamente entre la GPU y la CPU.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">transformers&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">AutoTokenizer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">model_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;meta-llama/Llama-2-13b-hf&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Mediante device_map=&amp;#34;auto&amp;#34;, lo que no quepa en la VRAM se descarga en la RAM de la CPU&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Con load_in_8bit=True, se cuantizan los pesos a 8 bits, permitiendo mayor ahorro de memoria&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">model&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">AutoModelForCausalLM&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_pretrained&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model_id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device_map&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">load_in_8bit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">offload_folder&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;offload_dir&amp;#34;&lt;/span> &lt;span class="c1"># Si hace falta, es posible descargar incluso en disco (SSD)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Al ejecutar este código, la librería &lt;code>accelerate&lt;/code> en segundo plano analizará el espacio libre disponible en la VRAM del sistema y en la RAM de la CPU, acomodando (Dispatch) las capas de la forma más óptima.&lt;/p>
&lt;h2 id="42-configuración-de-descarga-de-cpu-en-deepspeed-zero-2">4.2 Configuración de descarga de CPU en DeepSpeed (ZeRO-2)
&lt;/h2>&lt;p>A continuación, un ejemplo de un archivo de configuración (JSON) para activar la descarga de CPU con DeepSpeed durante el entrenamiento.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fp16&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;zero_optimization&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;stage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;offload_optimizer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;device&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cpu&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pin_memory&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;allgather_partitions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;allgather_bucket_size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2e8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;overlap_comm&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;reduce_scatter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;reduce_bucket_size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2e8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contiguous_gradients&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;train_batch_size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gradient_accumulation_steps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Con esta configuración, al especificar &lt;code>&amp;quot;cpu&amp;quot;&lt;/code> en &lt;code>offload_optimizer&lt;/code>, se obliga a que el mantenimiento del estado y los cálculos de actualización del optimizador (como Adam), que consumen una inmensa cantidad de VRAM, sean ejecutados por la CPU del sistema. Esto permite que la VRAM de la GPU se dedique en exclusiva a la tarea más importante: los cálculos de propagación hacia adelante y hacia atrás del modelo. Al establecer &lt;code>pin_memory: true&lt;/code>, se previenen los fallos de página (page faults), acelerando al máximo posible las transferencias de PCIe entre la CPU y la GPU.&lt;/p>
&lt;hr>
&lt;h1 id="resumen">Resumen
&lt;/h1>&lt;p>La escasez de memoria de GPU (Out of Memory) en el desarrollo de IA será un desafío eterno que seguirá acompañando a los desarrolladores a medida que los modelos aumenten de escala. Sin embargo, al combinar de forma adecuada una profunda comprensión del hardware (arquitectura) con técnicas de optimización a nivel algorítmico y de software como las explicadas en este artículo, se hace posible la inferencia y el entrenamiento de modelos masivos en un entorno local, algo que a primera vista podría parecer imposible.&lt;/p>
&lt;p>&lt;strong>Resumen de contramedidas durante la inferencia:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cuantización (INT4 / INT8 / FP8):&lt;/strong> Comprime drásticamente el tamaño del modelo en sí y reduce la ocupación de VRAM.&lt;/li>
&lt;li>&lt;strong>Descarga de CPU (CPU Offloading):&lt;/strong> Traslada a la memoria del sistema las capas que no caben en la VRAM (asumiendo un compromiso con la pérdida de velocidad debida al ancho de banda del PCIe).&lt;/li>
&lt;li>&lt;strong>Optimización de la caché KV:&lt;/strong> Se utiliza la paginación (PagedAttention), la cuantización de caché y FlashAttention para asegurar la longitud de contexto (Context Length).&lt;/li>
&lt;li>&lt;strong>Aprovechamiento de la memoria unificada:&lt;/strong> Se utiliza la UMA de arquitecturas como Apple Silicon para emplear grandes capacidades de memoria de forma directa en la inferencia.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Resumen de contramedidas durante el entrenamiento:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>PEFT (LoRA / QLoRA):&lt;/strong> Se limita el número de parámetros a entrenar y se cuantiza el modelo base al máximo.&lt;/li>
&lt;li>&lt;strong>Puntos de control de gradiente (Gradient Checkpointing):&lt;/strong> Se desechan las salidas intermedias de la propagación hacia adelante y se recalculan durante la retropropagación, conteniendo así el consumo de VRAM a cambio de un mayor tiempo de cálculo.&lt;/li>
&lt;li>&lt;strong>ZeRO y descarga de CPU (DeepSpeed):&lt;/strong> Supera los límites de VRAM dividiendo el estado del optimizador y los gradientes en múltiples GPUs, o descargándolos en la memoria de la CPU.&lt;/li>
&lt;/ol>
&lt;p>Al aprovechar al máximo estas tecnologías avanzadas, logremos extraer el mayor rendimiento posible en el desarrollo de IA dentro de unos recursos de hardware limitados. En este campo, que avanza a pasos agigantados día con día, es de esperar que en el futuro sigan apareciendo nuevos algoritmos para ahorrar memoria. La clave estará en revisar periódicamente las novedades en las últimas librerías y adoptarlas en nuestras implementaciones.&lt;/p></description></item></channel></rss>