Cómo usar modelos enormes con GPUs modestas
Exploro la técnica de la inferencia capa por capa, con precarga en memoria mediante ventanas deslizantes para correr modelos enormes en hardware de andar por casa.
Si quieres estudiar cómo funciona un modelo grande de lenguaje por dentro, lo primero que tienes que hacer es cargarlo. Pero imagina que quieres estudiar un modelo de 122B parámetros. Aclaración 122B sigue la convención anglosajona, donde billion equivale a 109. Es decir, que son 122 mil millones de parámetros. En condiciones normales, ocupa más de 200 gigas en memoria. Así que, en tu tarjeta gráfica no sé, pero en la mía no cabe.
¿Hay alguna alternativa? ¿o nos tenemos que conformar con modelos más pequeños? Te presento Chiquito, un repositorio pedagógico que he creado inspirado por AirLLM, Definición AirLLM es un proyecto de Gavin Li que optimiza el uso de la memoria de inferencia, lo que permite que modelos de lenguaje de 70B se ejecuten en una sola tarjeta GPU de 4 GB sin cuantización, destilación ni poda. que propone cargar los modelos por capas para poder cargar modelos mucho más grandes que la memoria de tu tarjeta gráfica.
El sacrificio
Antes de entrar en detalles, hay que dejar una cosa clara: al cargar un modelo por capas, sacrificas rendimiento. Cada vez que cargamos una capa en la memoria de nuestra tarjeta gráfica estamos realizando una operación que lleva tiempo.
En mis pruebas, con una tarjeta gráfica de 8 GB, generar 20 tokens llevó más de 30 minutos con un modelo de 122B de 4 bits.
Así que no tiene sentido usar esta técnica en producción. Solo tiene sentido cuando quieres examinar en local cómo funciona un modelo que no cabe en la memoria de tu tarjeta gráfica.
Estimador de tiempo de inferencia
Calculadora de tiempo de inferencia LLM: estima cuánto tarda generar tokens pasando capas de un modelo por PCIe desde RAM al GPU.
Parámetros del modelo
Cuantización
Velocidad de transferencia PCIe
Cómo funciona
Una vez has entendido que esta técnica no pretende ser rápida sino resolver el problema de poder ejecutar, aunque sea despacio, modelos grandes en equipos con tarjetas gráficas modestas, podemos empezar a hablar sobre cómo lo consigue.
Los dispositivos meta de PyTorch
Para poder cargar un modelo, necesitas saber qué “forma” tiene. Es decir: cuántas capas tiene, qué tipo de módulos tiene cada una y qué dimensiones tienen sus tensores.
Pero si intentas cargar el modelo de la forma habitual, PyTorch reserva memoria para el modelo entero, con todos sus pesos, de golpe. Y esto es justo lo que queremos evitar.
Aquí es donde entran los dispositivos meta de PyTorch. Definición El dispositivo “meta” es un dispositivo abstracto que denota un tensor que registra únicamente metadatos, pero ningún dato real. Un tensor en el dispositivo meta tiene dimensiones y tiene un tipo de dato asociado pero no ocupa nada en memoria.
import torch
t = torch.empty(10000, 10000, device="meta")
print(t.shape) # torch.Size([10000, 10000])
print(t.device) # meta
Este tensor ocuparía ~400 MB en float32.
En el dispositivo meta ocupa 0 bytes.
Esto permite tener la definición completa de un modelo —con todas sus capas, módulos y dimensiones— antes de meterlo en memoria. La librería accelerate de Hugging Face Definición La librería Accelerate de Hugging Face es una herramienta diseñada para simplificar el entrenamiento y la ejecución de modelos de aprendizaje automático en distintos entornos de hardware (CPU, GPU, múltiples GPUs o TPUs) sin necesidad de cambiar significativamente el código. ofrece un gestor de contexto que hace exactamente esto:
from accelerate import init_empty_weights
from transformers import AutoModelForCausalLM, AutoConfig
config = AutoConfig.from_pretrained(
"Qwen/Qwen2.5-Coder-32B-Instruct"
)
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
model es un objeto completo: puedes recorrer sus capas, inspeccionar sus nombres y consultar las dimensiones de cada parámetro pero no ocupa ningún peso real en memoria.
Chiquito se apoya en este mecanismo en tres momentos:
-
Al iniciarse, crea la estructura del modelo en el dispositivo meta para saber qué capas tiene y cuál es su tamaño. Contexto Al crear el modelo se usa el gestor de contexto de la librería Accelerate que permite cargar el modelo en el dispositivo
meta. -
Durante la inferencia, carga los pesos reales de cada capa en la tarjeta gráfica justo antes de ejecutarla. Contexto Justo antes de usar cada capa para inferencia se usa otro método de la librería Accelerate para mover los pesos al dispositivo GPU.
-
Y, por último, una vez procesada cada capa, la devuelve al dispositivo meta dejando libre la memoria de la tarjeta gráfica. Contexto Una vez se ha usado una capa para inferir y ya no es necesario mantenerla en memoria se mueve el tensor al dispositivo meta liberando así la memoria.
Carga desde HuggingFace
Chiquito usa la API de HuggingFace para descargar los modelos. Y en HuggingFace, los modelos tienen un archivo JSON que describe su arquitectura y que puedes descargar usando la clase AutoConfig:
from transformers import AutoConfig
config = AutoConfig.from_pretrained(
"Qwen/Qwen2.5-Coder-32B-Instruct"
)
| Objeto | Atributo | Valor |
|---|---|---|
| config | num_hidden_layers | 22 |
| config | hidden_size | 2048 |
| config | vocab_size | 32000 |
| config | architectures | [“Qwen2ForCausalLM”] |
Esta operación es ligera porque descarga el archivo de configuración del modelo, sin descargar sus pesos. Chiquito usa la configuración en dos momentos clave:
- Para contar las capas y construir una lista con sus nombres. Contexto Cuando se carga la configuración, se crea el modelo en el dispositivo
metay se recorre el árbol de módulos para contar cuántas capas tiene. - Para detectar la arquitectura del modelo y saber qué clase instanciar. Contexto El campo
config.architectures[0]se usa para seleccionar automáticamente la clase de modelo correcta.
Pero hay una pieza que va antes que el modelo: el tokenizador.
El tokenizador
Los modelos no trabajan con texto sino con secuencias de números enteros. Y el tokenizador es el componente que convierte texto en valores numéricos y viceversa. Puedes probarlo aquí: escribe una frase y observa cómo se descompone en tokens.
Texto de entrada
Tokens
IDs de token
Chiquito carga el tokenizador una sola vez durante la inicialización Contexto Al iniciarse, Chiquito carga el tokenizador y lo deja accesible como model.tokenizer. y lo expone para que puedas codificar tus prompts y decodificar las salidas del modelo. Una vez convertido el texto en identificadores, necesitamos los pesos del modelo para procesarlos. Y esos pesos duermen en archivos.
Los archivos safetensors
En HuggingFace, los pesos del modelo se almacenan en archivos safetensors, que es un formato binario simple, rápido y que permite mapeo en memoria. Definición El formato safetensors fue creado por Hugging Face como alternativa más segura y eficiente al formato pickle que usa PyTorch por defecto. Cada archivo es un mapeo plano de nombre de parámetro a tensor.
El mapa de pesos
Hay modelos pequeños que caben en un único archivo pero los modelos grandes se dividen en más de un archivo. Cuando un modelo está dividido, existe un índice Contexto Los modelos de HuggingFace fragmentados tienen un archivo llamado model.safetensors.index.json que relaciona cada peso con el fichero Safetensors en el que está definido. que mapea cada parámetro con el archivo que lo contiene:
| Peso | Fichero |
|---|---|
model.embed_tokens.weight | model-00001-of-00004.safetensors |
model.layers.0.self_attn.q_proj.weight | model-00001-of-00004.safetensors |
model.layers.31.mlp.up_proj.weight | model-00004-of-00004.safetensors |
lm_head.weight | model-00004-of-00004.safetensors |
Este mapa de pesos es lo que hace posible la carga selectiva de modelos grandes: podemos saber en qué fragmento están los parámetros de cada capa sin tener que cargar el modelo entero. Chiquito usa este mapa para decidir qué archivos necesita en cada momento. Contexto La primera vez que Chiquito carga un modelo, usa este mapa para crear nuevos ficheros Safetensors organizados por capas.
Si el modelo tiene un solo archivo de pesos, no se necesita mapa para dividirlo.
Descarga desde el Hub
HuggingFace proporciona un método Contexto La API de HuggingFace implementa el método snapshot_download que permite descargar un repositorio completo para una revisión dada. para descargar un modelo:
import huggingface_hub
cache_path = huggingface_hub.snapshot_download(
"Qwen/Qwen2.5-Coder-32B-Instruct",
token=None, # necesario si el modelo es privado
ignore_patterns=["*.bin"], # ignorar pesos en formato antiguo
)
La primera vez que llamas al método los pesos del modelo se descargan a una caché local y el método devuelve la ruta en la que los descargó. Las siguientes llamadas devuelven la misma ruta sin volver a descargar nada. Contexto Durante la carga del modelo, Chiquito intenta usar la ruta como directorio local antes de descargarlo.
Con todo esto claro, ya tenemos el contexto necesario para entender cómo Chiquito orquesta la carga capa por capa durante la inferencia.
La arquitectura transformer
Para cargar un modelo capa por capa no hace falta entender en profundidad sus matemáticas internas. Basta con tener claro que un modelo causal de lenguaje es una tubería de cuatro bloques por los que los datos fluyen en orden:
Embeddings
El primero es el embedding, una tabla de búsqueda que convierte los identificadores enteros que produce el tokenizador en vectores densos.
Si el vocabulario tiene 32.000 tokens y la dimensión oculta es 2.048, el embedding es una matriz de forma
(32000, 2048). Recibe un tensor(batch, seq_len)y devuelve un tensor(batch, seq_len, hidden_dim).
Texto de entrada
Tokens
IDs de token
Embeddings (8 dimensiones)
Transformers
A continuación viene el núcleo del modelo: las capas transformer. Cada capa recibe un tensor que representa un “estado oculto” y devuelve otro con la misma forma. Dentro se suceden el mecanismo de autoatención y una red prealimentada, pero para lo que nos ocupa aquí podemos tratarlas como cajas negras.
Lo relevante es que son un bloque que se repite N veces y concentra la mayor parte de los parámetros del modelo.
Un modelo de 1B como TinyLlama tiene 22 capas, uno de 32B puede tener 64. Este es el bloque que más tarda en cargarse y el que justifica toda la técnica.
Normalización
Después viene la normalización final, una capa inocente que estabiliza los estados ocultos antes de la proyección final. Y que también transforma los tensores sin cambiar sus dimensiones.
Capa de salida
Y por último, una proyección lineal lleva los estados ocultos desde la dimensión oculta hasta el tamaño del vocabulario. Su salida es un vector con una puntuación por cada token del vocabulario. El token con la puntuación más alta es, a grandes rasgos, la predicción del modelo para la siguiente posición.
La convención de nombres
Los modelos de HuggingFace siguen un esquema de nombres consistente. Para los modelos tipo Llama —que incluyen Llama, Mistral, Qwen2 y muchos otros— los nombres son:
| Bloque | Ruta del módulo |
|---|---|
| Embedding | model.embed_tokens |
| Capa transformer i | model.layers.i |
| Normalización final | model.norm |
| LM head | lm_head |
Estos nombres son los que aparecen como prefijos en el mapa de pesos del modelo. Contexto Chiquito define los nombres como una variable de clase para poder adaptarlos fácilmente a arquitecturas que usan otra convención.
Recorrer el árbol de módulos
Una vez creado el modelo sobre el dispositivo meta, teniendo en cuenta que tenemos los nombres de cada uno de sus módulos, podemos recorrerlo en un bucle de principio a fin: Contexto Chiquito usa este mecanismo para construir, al arrancar, una lista ordenada con todos los bloques del modelo.
# Modelos tipo Llama
LAYER_NAMES = {
"embed": "model.embed_tokens",
"layer_prefix": "model.layers",
"norm": "model.norm",
"lm_head": "lm_head",
}
for module_name in LAYER_NAMES.values():
module = model
for attr in module_name.split("."):
module = getattr(module, attr)
print(module_name, "> ", module)
Ya tenemos casi todo listo para empezar a procesar nuestro prompt pero nos falta todavía un detalle por resolver. Y es que los transformers miran todos los tokens a la vez, en paralelo. Esto es lo que los hace tan rápidos pero también significa que si no les damos una pista, no saben en qué orden va cada token.
Las Rotary Position Embeddings
Para un transformer, una frase como “el gato se sentó en la alfombra” es indistinguible de otra frase cualquiera con las mismas palabras, como “alfombra la en sentó se gato el”. Y aquí viene una idea clave, el modelo necesita saber en qué posición está cada token dentro de la frase.
La mayoría de los modelos modernos resuelven esto con las Rotary Position Embeddings (RoPE), Definición RoPE codifica la posición aplicando pequeñas rotaciones a los vectores internos del modelo. Para lo que nos ocupa, basta con saber que es un ingrediente extra que las capas reciben junto a los tokens. una forma de representar el número de posición de cada token para que las capas transformer puedan usarlo al procesarlos.
Lo relevante para Chiquito es algo mucho más práctico: estas posiciones se calculan una sola vez al principio de cada inferencia y se reutilizan en todas las capas. Además, el módulo que las calcula no es un parámetro entrenado del modelo sino una tabla derivada de la configuración, así que cabe siempre en memoria y no necesita cargarse y descargarse capa por capa como el resto de los pesos. Contexto Chiquito calcula las embeddings posicionales una vez al principio de cada paso de inferencia y las reutiliza en todas las capas.
Con los cuatro bloques identificados, los nombres mapeados y las posiciones resueltas, Chiquito ya tiene todo lo que necesita para iterar el modelo de principio a fin cargando pesos bajo demanda.
Partición por capas
Hasta aquí ya sabemos que HuggingFace guarda los pesos en uno o varios archivos safetensors y que el mapa de pesos nos dice dónde vive cada tensor. El problema es que los pesos de una misma capa pueden estar repartidos entre varios fragmentos y que cada fragmento, a su vez, mezcla pesos de capas distintas. Si para usar una capa tuviéramos que abrir el fragmento que la contiene y descartar todo lo demás, estaríamos leyendo cientos de megas sobrantes cada vez que pasáramos por esa capa.
Lo que Chiquito hace, la primera vez que trabaja con un modelo, es reorganizar sus pesos en un formato que le resulte cómodo: crea un directorio nuevo y escribe dentro un archivo Safetensors por cada capa del transformer. Contexto La lógica de esta partición de modelos vive en splitter.py. Así, durante la ejecución del modelo, cargar una capa concreta es abrir un único archivo con solo los pesos que esa capa necesita y ninguno de más.
La partición es un coste que se paga una sola vez. La primera vez que Chiquito procesa un modelo nuevo lleva un rato, porque tiene que leer todos los fragmentos originales y escribir los nuevos por capas. Las siguientes veces detecta que ya está hecho y se salta el paso entero.
El marcador .done
Hay un detalle del proceso que parece innecesario pero que tiene una motivación muy concreta. Después de escribir cada archivo Safetensors, Chiquito crea junto a él un segundo archivo vacío con el mismo nombre y la extensión .done.
Esto se hace porque escribir un archivo grande lleva tiempo y si el proceso se interrumpe en mitad de la escritura, lo que queda en disco es un archivo incompleto: existe, pero es inservible. El archivo marcador se crea justo después de que la escritura haya terminado sin problemas, así que su presencia indica que el archivo de esa capa está íntegro. Contexto Si Chiquito se relanza después de una interrupción, comprueba el directorio de partición y solo vuelve a procesar las capas cuyo .done no aparece. Las demás se asumen correctas y no se tocan.
Estado inicial: no se ha escrito nada.
Shards de origen
chiquito_split/
- model.embed_tokens.safetensors
- model.layers.0.safetensors
- model.layers.1.safetensors
- model.norm.safetensors
- lm_head.safetensors
Inferencia por capas
Con el modelo ya repartido en archivos por capas llega la pieza que hace que todo lo anterior tenga sentido: ejecutar el modelo iterativamente capa por capa. Cada vez que el modelo tiene que predecir un token nuevo, el prompt tiene que pasar por todo el modelo. Este proceso se llama inferencia. Contexto De la implementación completa de la inferencia se encarga la clase ChiquitoModel.
El problema ya lo conocemos: no caben todos los pesos a la vez en la memoria de la tarjeta gráfica. Lo que hace Chiquito es convertir esa única llamada en una secuencia de pequeñas cargas y descargas: para cada bloque del modelo carga sus pesos en la tarjeta gráfica, lo ejecuta y los libera antes de pasar al siguiente.
El resultado es que, en cualquier instante, la memoria de la tarjeta gráfica sólo contiene los pesos del bloque que se está ejecutando. Es como una ventana móvil por la que el modelo va desfilando mientras el tensor de estados ocultos lo atraviesa de principio a fin.
Estado inicial. Todos los pesos del modelo están en disco, la VRAM está vacía y los buffers permanentes (como las tablas de RoPE) ya residen en GPU.
Pesos del modelo
- model.layers.0 en disco
- model.layers.1 en disco
- model.norm en disco
- lm_head en disco
VRAM
Generar un texto no es un proceso por lotes: el ciclo
cargar→ejecutar→liberarse repite entero por cada token que se quiere generar. Generar 20 tokens implica recorrer el modelo 20 veces, con todas sus cargas y descargas.
Antes de empezar el recorrido, el proceso de inferencia se asegura de que el modelo arranca desde un estado limpio: todos los parámetros vuelven al dispositivo meta y la memoria de la tarjeta gráfica queda libre. Contexto Durante cada paso de una inferencia, se recorren los parámetros y se mandan al dispositivo meta.
Los buffers del modelo, en cambio, se mueven de vuelta a GPU porque todas las capas los necesitan. La excepción son los buffers del modelo, como las tablas de RoPE que vimos antes: al ser pequeños y usarlos todas las capas, se quedan permanentemente en la memoria.
Atención causal
La forma más sencilla de imaginarse la generación de texto es metiendo los tokens del prompt en el modelo uno a uno: el primero entra solo, el segundo ve al primero, el tercero ve a los dos anteriores, y así hasta el final. Funcionaría, pero sería terriblemente lento.
Lo que se hace en la práctica es meter el prompt entero de golpe y, dentro de cada capa transformer, dejar que la autoatención mire todos los tokens a la vez. Para que el resultado sea idéntico al del recorrido secuencial, hace falta una máscara causal: una matriz de verdadero y falso que le dice a la autoatención qué pares de tokens pueden mirarse, de forma que cada uno sólo ve a los anteriores y a sí mismo. Contexto La máscara triangular sigue esta lógica: si la celda (i, j) es verdadera, el token de la posición i puede atender al de la posición j, si es falsa no.
Esta misma idea se aplica a las dos fases en las que se divide la inferencia, conocidas por convención como prefill y decode:
- Prefill es la primera llamada. El modelo procesa todo el prompt de golpe con una máscara triangular que reproduce, en paralelo, lo que habría ocurrido si los tokens hubieran entrado uno a uno.
- Decode son las llamadas siguientes, una por cada token generado. El modelo recibe un único token nuevo —el último producido— y la máscara se reduce a una sola fila: la del token nuevo, que mira a todo lo anterior y a sí mismo.
[0, 1, 2, 3]
prefill · 4 × 4
Precarga con ventana deslizante
El recorrido capa a capa que vimos en la sección de inferencia lee cada bloque del disco justo antes de ejecutarlo y lo hace además cada vez que la capa aparece en el bucle.
Con 67 capas y 20 tokens por generar son más de 1.000 lecturas de disco por respuesta. La buena noticia es que ese disco se puede cambiar por RAM: si tienes algo de memoria libre, puedes precargar por adelantado los pesos antes de que el bucle de inferencia los utilice. Chiquito ofrece tres estrategias distintas, todas controladas por un parámetro.
preload_to_ram | Dónde viven los pesos | RAM que usa | Velocidad |
|---|---|---|---|
True | RAM del sistema | Modelo entero | La más rápida |
False | Disco | Mínima | La más lenta |
| N | RAM del sistema, N capas | Proporcional a N | Intermedia |
Precargar el modelo entero
En el modo más agresivo, Chiquito vuelca al arrancar todas las capas del modelo a una caché local en RAM. A partir de ese momento la función que carga pesos a CPU deja de tocar el disco. Contexto La carga en bloque ocurre durante la inicialización del modelo y el despacho posterior devuelve el diccionario ya preparado sin tocar el disco.
Es el modo más rápido porque elimina las lecturas de disco durante el bucle de inferencia. A cambio, el modelo entero tiene que caber en RAM: un modelo de 32B en fp16 pide unos 65 GB solo para los pesos.
Leer siempre del disco
En el otro extremo, si la RAM es muy escasa se puede saltar la caché y leer cada capa desde disco cada vez que el bucle la necesita. Es lo más lento pero funciona con prácticamente cualquier configuración: en RAM solo hay una capa a la vez.
Aun así, este modo no es tan ingenuo como parece. Mientras la GPU ejecuta la capa N, un hilo de fondo ya está leyendo la capa N+1 desde disco. Contexto El bucle de inferencia lanza un hilo con un solo trabajador y, al final de cada iteración, le pide que vaya trayendo la siguiente capa en paralelo con la ejecución actual. Es una precarga de un único paso por delante, suficiente para que, si la ejecución de una capa tarda más que leer la siguiente, el coste del disco desaparezca casi por completo.
La ventana deslizante
El punto intermedio es el más interesante y donde Chiquito aplica un patrón clásico de concurrencia. Cuando tienes RAM libre pero no suficiente para el modelo entero, puedes mantener solo N capas en memoria al mismo tiempo y dejar que un hilo de fondo reponga la ventana a medida que el bucle de inferencia la vacía por la cola. Contexto Toda la lógica vive en una clase, que sincroniza productor y consumidor.
Esto se conoce como patrón productor-consumidor:
- el consumidor es el bucle de inferencia, que coge una capa, la ejecuta y la suelta en cuanto termina;
- el productor es un hilo de fondo que, cada vez que se libera un hueco, lo aprovecha para cargar la siguiente capa pendiente.
Antes de arrancar el recorrido propiamente dicho, Chiquito precarga las N primeras capas de forma síncrona. Contexto Cuando empieza la inferencia, se llena la ventana inicial antes de lanzar el hilo de fondo, garantizando que cuando se procese la primera capa los pesos ya estarán disponibles. Solo cuando la ventana está llena echa a andar el hilo productor y cede el control al bucle de inferencia.
Estado inicial. Todas las capas del modelo residen en disco; la ventana en RAM está vacía y el hilo productor aún no se ha puesto en marcha.
Capas del modelo
- model.layers.0 en disco
- model.layers.1 en disco
- model.layers.2 en disco
- model.layers.3 en disco
- model.layers.4 en disco
- model.layers.5 en disco
- model.layers.6 en disco
- model.layers.7 en disco
RAM (ventana)
Hay un detalle técnico pequeño pero crítico en el hilo de fondo: la lectura del archivo se hace fuera del bloqueo. Contexto La carga de una capa sólo bloquea para comprobar cuántos huecos hay libres y elegir la siguiente capa a cargar.
Una vez decidido, termina el bloqueo e inicia la carga de la capa que corresponda.
Cuando el tensor ya está en memoria vuelve a bloquear por un instante para escribirlo en el diccionario de la caché y despertar al consumidor. De otra forma el consumidor se bloquearía durante cada lectura de disco y toda la gracia de tener un hilo de fondo se perdería.
Cuantización
Hasta aquí todas las optimizaciones han sido sobre cuándo y dónde guardar los pesos, no sobre cuánto pesan. La última pieza va justo al contrario: reducir el tamaño de cada peso antes de moverlo. En el bucle capa a capa el cuello de botella no es la tarjeta gráfica calculando sino el bus subiendo datos hasta tarjeta gráfica, así que si cada tensor ocupa la mitad, cada transferencia tarda la mitad.
Chiquito no aporta nada original aquí: se apoya en bitsandbytes, que es la librería estándar para esto. Definición bitsandbytes es una librería que ofrece dos modos de cuantización al vuelo: 8 bits y 4 bits y se encarga de sustituir las capas lineales del modelo por variantes cuantizadas que convierten el tensor en el momento en que se mueve a GPU.
La aritmética es la que cabría esperar. En fp16 cada parámetro ocupa 2 bytes; en 8 bits, 1 byte; en 4 bits, medio byte. Pasar a 4 bits reduce cuatro veces el coste de transferencia, que es el que manda. Cuantización Durante la inicialización del modelo, si el usuario pide cuantización, se crea un
BitsAndBytesConfigy se deja quebitsandbytesreemplace las capas lineales.
Tamaño por capa
| Modelo | fp16 | 8-bit | 4-bit (nf4) |
|---|---|---|---|
TinyLlama/TinyLlama-1.1B-Chat-v1.0 | 84 MB | 42 MB | 21 MB |
Qwen/Qwen2.5-7B | 445 MB | 222 MB | 111 MB |
Qwen/Qwen2.5-32B | 930 MB | 465 MB | 233 MB |
Esta tabla muestra lo que más impacta durante la inferencia: la información que viaja por PCIe en cada paso del bucle. Un Qwen2.5-32B pasa de mover 930 MB por capa en fp16 a mover 233 MB en 4 bits y eso ocurre una vez por capa y por token generado.
Tamaño del modelo completo
| Modelo | fp16 | 8-bit | 4-bit (nf4) |
|---|---|---|---|
TinyLlama/TinyLlama-1.1B-Chat-v1.0 | 2.05 GB | 1.02 GB | 525 MB |
Qwen/Qwen2.5-7B | 14.18 GB | 7.09 GB | 3.55 GB |
Qwen/Qwen2.5-32B | 61.03 GB | 30.51 GB | 15.26 GB |
Esta tabla informa sobre lo que decide si una máquina concreta puede siquiera alojar el modelo entero en RAM: el mismo modelo pide 61 GB en fp16 —inviable en casi cualquier equipo doméstico— pero cabe en 15 GB cuando se cuantiza a 4 bits, que entra cómodamente en los 32 GB de muchos portátiles.
Mezcla de expertos
Hasta aquí hemos dado por supuesto que todas las capas del modelo son iguales y que cada token pasa por ellas entero. Los modelos de “mezcla de expertos” (MoE) rompen esa simetría: dentro de cada capa, la red prealimentada se sustituye por muchas pequeñas redes de “expertos” y un enrutador ligero decide cuáles activar para cada token. Definición Un modelo como Qwen3.5-MoE tiene 128 expertos por capa pero en cada paso solo activa unos pocos. El resto permanece inactivo durante ese token.
Esto le da a Chiquito un margen interesante: si solo se usan unos pocos expertos por token, no hace falta tener todos descomprimidos en memoria a la vez. En su lugar, se dejan los pesos cuantizados en la memoria de la tarjeta gráfica y se descomprime un experto sólo cuando el enrutador lo pide. Contexto Vive en la clase LazyDequantExperts.
| Estrategia | VRAM |
|---|---|
| Descomprimir todos los pesos | ~4.8 GB |
| Empaquetado 4 bits + descomprimir bajo demanda | ~1.2 GB + ~37 MB por experto activo |
Con este truco activado, el resto de Chiquito —partición por capas, bucle de inferencia, ventana deslizante— no nota la diferencia. La fábrica AutoModel detecta la arquitectura en config.json y devuelve un modelo que se comporta igual que los demás.
from chiquito import AutoModel
model = AutoModel.from_pretrained(
"Qwen/Qwen3.5-MoE-A3B-Instruct",
quantization="4bit",
)
Conclusiones
Como hemos visto, se puede cargar un modelo que no cabe en la memoria de una tarjeta gráfica doméstica. Pero al precio de aceptar que la inferencia tardará mucho más. Chiquito no es una optimización para producción, es una herramienta para examinar modelos por dentro en una máquina que, sobre el papel, no debería poder ejecutarlos.
Lo interesante es que las piezas que hacen esto posible no son precisamente innovadoras:
- La inferencia capa por capa es una aplicación directa del patrón de jerarquía de memoria.
- La ventana deslizante es un productor-consumidor de manual con bloqueo.
- La cuantización es una reducción de la precisión en busca de eficiencia.
- En las mezclas de expertos, los pesos se descomprimen sólo cuando el enrutador los pide en un patrón de trabajador perezoso.
Ninguna de las ideas es nueva y esa es precisamente la gracia: con piezas conocidas bien combinadas se puede hacer que donde comen dos, coman tres. ¡Te das Qwen!
Si has llegado hasta aquí con ganas de jugar, clona Chiquito, Contexto Puedes encontrar las fuentes de Chiquito en elcapo/chiquito. apúntalo a un modelo que no te quepa en la GPU y échale paciencia.