Introducción del proyecto
AirLLM es una herramienta en Python orientada a la inferencia de grandes modelos de lenguaje. Su idea central no consiste en cargar el modelo completo de una sola vez en la VRAM, sino en dividirlo en capas jerárquicas más finas y, durante la inferencia, cargar únicamente lo que se necesita. De este modo, podemos probar modelos como 70B, 235B, 671B e incluso modelos MoE aún más extravagantes como Kimi K3 en entornos con una VRAM muy limitada.
Kimi K3 ha estado muy en el foco recientemente por una razón bastante directa: se trata de un modelo MoE de escala enorme. Su número de parámetros es muy grande, pero en la práctica cada token solo se enruta a una parte de los expertos. AirLLM aprovecha exactamente esto mediante una carga en streaming a nivel de expertos, evitando tener que meter el modelo completo en la VRAM de una sola vez.
En este artículo armaremos juntos un proyecto mínimo: primero ejecutaremos la inferencia de Kimi K3 y luego lo encapsularemos en un sencillo API HTTP. Al final también comentaremos cuándo conviene la inferencia local con baja VRAM y cuándo es mejor usar directamente una interfaz compatible como Defapi para hacerlo más fácil.
Dificultad: Intermedia | Duración: 30-60 minutos | Resultados: Comprender el flujo de inferencia con baja VRAM de AirLLM y poner en marcha un servicio mínimo de inferencia para Kimi K3
Perfil de destinatarios
- Desarrolladores que quieren experimentar con grandes modelos en una sola tarjeta y en máquinas con poca VRAM
- Ingenieros que quieren evaluar el coste de inferencia de modelos como Kimi K3, DeepSeek-V3 y Qwen3-235B
- Personas que están creando prototipos de aplicaciones de IA y quieren validar el rendimiento del modelo localmente primero
- Desarrolladores que necesitan encapsular el modelo local como API y luego conectarlo a Agentes, RAG o sistemas de negocio
Dependencias clave y entorno
Kimi K3 es bastante exigente con el entorno. Primero aclaremos el límite de dependencias:
| Dependencia | Versión recomendada | Descripción |
|---|---|---|
| Python | 3.10+ | Se recomienda usar un entorno virtual para aislar dependencias |
| NVIDIA Driver | Compatible con CUDA 12 | Debe poder ejecutar PyTorch en una versión que soporte CUDA 12 |
| PyTorch | Construido para CUDA 12 | La cadena de dependencias de Kimi K3 suele encajar mejor con CUDA 12 |
| transformers | 4.56.x | El código remoto del modelo Kimi K3 tiene requisitos sobre la versión |
| airllm | Última versión | Usa AutoModel para cargar el modelo de forma unificada |
| compressed-tensors | Última versión | Los formatos de pesos relacionados con Kimi K3 se usarán aquí |
| flash-attn | Compatible con CUDA 12 | El código del modelo Kimi K3 normalmente requiere flash attention |
WARNING
AirLLM puede reducir el uso de VRAM, pero no hace que los archivos del modelo se vuelvan más pequeños. La primera ejecución descargará y dividirá el modelo; el espacio en disco y la calidad de la red siguen siendo cruciales. Los modelos como Kimi K3 son enormes: se recomienda preparar suficiente espacio de caché antes.
Estructura completa del proyecto
Primero prepararemos un proyecto mínimo, manteniendo la estructura simple:
airllm-kimi-k3-demo/
├── .env.example
├── requirements.txt
├── run_kimi_k3.py
├── server.py
└── README.md
1. Crear un entorno virtual de Python
Crea el directorio y el entorno virtual:
mkdir airllm-kimi-k3-demo
cd airllm-kimi-k3-demo
python -m venv .venv
source .venv/bin/activate
En Windows PowerShell, activa así:
mkdir airllm-kimi-k3-demo
cd airllm-kimi-k3-demo
python -m venv .venv
.venv\Scripts\Activate.ps1
Verifica la versión de Python:
python --version
Se recomienda que el resultado sea al menos Python 3.10.x. Si en tu máquina hay varias versiones de Python, puedes especificar explícitamente con py -3.10 -m venv .venv.
2. Instalar AirLLM y dependencias de Kimi K3
Crea requirements.txt:
airllm
accelerate
sentencepiece
safetensors
compressed-tensors
python-dotenv
fastapi
uvicorn[standard]
Instala primero PyTorch. Como ejemplo con CUDA 12.1:
pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
Luego instala las dependencias principales:
pip install -r requirements.txt
pip install "transformers>=4.56,<4.57"
Por último, instala flash-attn:
pip install flash-attn --no-build-isolation
TIP
Si flash-attn compila muy lento, primero confirma que el entorno actual puede hacer import torch correctamente y que torch.version.cuda muestra que es la familia de CUDA 12. Kimi K3 tiene dependencias más orientadas a un entorno de ingeniería; no se recomienda instalarlo directamente en un entorno global de Python desordenado.
3. Configurar permisos del modelo y directorio de caché
Algunos modelos de Hugging Face requieren inicio de sesión o autorización. Preparamos un .env.example:
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
HF_HOME=/data/huggingface
AIRLLM_MODEL_ID=moonshotai/Kimi-K3
Al usarlo de verdad, copia una copia:
cp .env.example .env
En Windows PowerShell:
Copy-Item .env.example .env
Luego completa tu token de Hugging Face. El ID del modelo debe coincidir con el nombre del repositorio publicado oficialmente; si usas un repositorio espejo o uno sincronizado internamente, también puedes cambiarlo por el repo id correspondiente.
WARNING
No envíes .env a un repositorio Git. El token del modelo es información sensible; si se filtra, podría llevar a un abuso de los permisos del modelo privado.
4. Escribir el script mínimo de inferencia de Kimi K3
Crea run_kimi_k3.py:
import os
from dotenv import load_dotenv
from airllm import AutoModel
def main() -> None:
"""Carga Kimi K3 y ejecuta una generación mínima de texto."""
load_dotenv()
model_id = os.getenv("AIRLLM_MODEL_ID", "moonshotai/Kimi-K3")
hf_token = os.getenv("HF_TOKEN")
# Controla la longitud de entrada: primero usa un prompt corto para comprobar el flujo y evitar esperas largas en la validación inicial.
max_length = 256
# AutoModel detecta automáticamente el tipo de modelo; hf_token se usa para acceder a repositorios que requieren autorización.
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=True,
)
prompt = "Explica en tres frases por qué AirLLM puede reducir la VRAM para la inferencia de grandes modelos."
# El tokenizer sigue el estilo de Hugging Face, por lo que el coste de migración es bajo.
input_tokens = model.tokenizer(
[prompt],
return_tensors="pt",
return_attention_mask=False,
truncation=True,
max_length=max_length,
padding=False,
)
# En la inferencia, AirLLM carga pesos por capas; establece max_new_tokens un poco más bajo para validar rápidamente.
generation_output = model.generate(
input_tokens["input_ids"].cuda(),
max_new_tokens=128,
use_cache=True,
return_dict_in_generate=True,
)
output = model.tokenizer.decode(
generation_output.sequences[0],
skip_special_tokens=True,
)
print(output)
if __name__ == "__main__":
main()
Ejecución:
python run_kimi_k3.py
La primera ejecución suele ser más lenta, porque AirLLM debe descargar el modelo, analizar los archivos de pesos y dividirlo en un formato que luego se pueda cargar por capas. Verás que la segunda ejecución será mucho más fluida, ya que la caché del modelo y los resultados de la división ya estarán listos.
5. Observar el uso de VRAM y el consumo de disco
Abre otra terminal para vigilar la VRAM:
nvidia-smi -l 1
Observa dos indicadores:
Memory-Usage
GPU-Util
Si todo va bien, la VRAM no se disparará instantáneamente como en transformers.from_pretrained() tradicional, donde el modelo completo se carga de golpe. El enfoque de AirLLM es más parecido a “ir tomando lo necesario a medida que avanza”: carga en memoria la capa y la parte de expertos correspondientes cuando se necesita y libera cuando ya no se usan.
Luego revisa el disco:
du -sh "$HF_HOME"
En Windows PowerShell puedes usar:
Get-ChildItem $env:HF_HOME -Recurse | Measure-Object Length -Sum
TIP
La inferencia con baja VRAM no es un almuerzo gratis. AirLLM traslada la carga de la VRAM hacia la E/S del disco, la memoria de CPU y la caché del modelo; por tanto, la velocidad del SSD impactará directamente la experiencia.
6. Habilitar la compresión de parámetros
AirLLM permite reducir el tamaño de la carga de pesos mediante el parámetro compression. Podemos añadir 4bit al inicializar el modelo:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
compression="4bit", # Usa compresión de pesos en 4bit para reducir el volumen de carga y mejorar la velocidad en algunos casos
profiling_mode=True,
)
Si quieres ser más conservador, prueba primero 8bit:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
compression="8bit", # 8bit es más conservador; equilibra mejor velocidad y precisión
profiling_mode=True,
)
Normalmente elegimos así:
| Escenario | Recomendación |
|---|---|
| Solo probar si Kimi K3 puede ejecutarse | Sin compresión, primero confirma el flujo |
| La E/S de disco es un cuello de botella | Prueba 8bit |
| Prioridad en velocidad y validación barata | Prueba 4bit |
| Hacer una evaluación seria | Fija los parámetros y compara salidas en múltiples rondas |
WARNING
La compresión cambia la forma en que se cargan los pesos. Aunque en muchos casos el impacto en la inferencia es pequeño, si estás haciendo evaluación de modelos, inferencia con textos largos o validación de negocio sensible, asegúrate de conservar la versión sin comprimir como referencia (baseline).
7. Encapsularlo en un sencillo API HTTP
Después de que el script local funcione, normalmente querrás integrarlo en una aplicación. A continuación usaremos FastAPI para construir un servicio mínimo.
Crea server.py:
import os
from typing import List, Literal
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from airllm import AutoModel
class ChatMessage(BaseModel):
"""Estructura de mensajes estilo OpenAI, útil para reemplazarlo por una API compatible más adelante."""
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
"""Solicitud de chat mínima: conserva solo los campos necesarios para el tutorial."""
messages: List[ChatMessage]
max_tokens: int = 256
class ChatResponse(BaseModel):
"""Devuelve un resultado en texto; mantenlo simple para facilitar el debugging."""
text: str
load_dotenv()
app = FastAPI(title="AirLLM Kimi K3 Demo")
model_id = os.getenv("AIRLLM_MODEL_ID", "moonshotai/Kimi-K3")
hf_token = os.getenv("HF_TOKEN")
# Carga el modelo al iniciar el servicio; en un entorno real de producción se puede añadir health check y carga perezosa.
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=False,
)
def build_prompt(messages: List[ChatMessage]) -> str:
"""Une mensajes en múltiples turnos en un prompt simple; luego se puede reemplazar por la plantilla oficial de chat del modelo."""
lines: list[str] = []
for message in messages:
lines.append(f"{message.role}: {message.content}")
lines.append("assistant:")
return "\n".join(lines)
@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest) -> ChatResponse:
"""Ejecuta una generación de texto para Kimi K3."""
prompt = build_prompt(request.messages)
input_tokens = model.tokenizer(
[prompt],
return_tensors="pt",
return_attention_mask=False,
truncation=True,
max_length=1024,
padding=False,
)
generation_output = model.generate(
input_tokens["input_ids"].cuda(),
max_new_tokens=request.max_tokens,
use_cache=True,
return_dict_in_generate=True,
)
text = model.tokenizer.decode(
generation_output.sequences[0],
skip_special_tokens=True,
)
return ChatResponse(text=text)
Iniciar el servicio:
uvicorn server:app --host 0.0.0.0 --port 8000
Prueba de llamada:
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Explica en chino por qué los modelos MoE como Kimi K3 son adecuados para la carga en streaming por expertos."
}
],
"max_tokens": 200
}'
Ahora ya tenemos un API mínimo. Aún no es de nivel producción, pero es suficiente para experimentos locales, prototipos de Agent, depuración de prompts y comparación del rendimiento del modelo.
8. Cómo elegir entre inferencia local y la interfaz de Defapi
El valor de AirLLM es muy claro: nos permite tocar los grandes modelos en máquinas con poca VRAM, especialmente para aprender, validar, evaluar y hacer experimentos offline. Pero en producción también hay que considerar el throughput, la latencia, la concurrencia, las actualizaciones del modelo, la recuperación ante fallos y el coste operativo.
Si solo quieres integrar rápidamente las capacidades del modelo en una aplicación, o necesitas una API unificada con estilo OpenAI/Anthropic/Gemini, Defapi encaja mejor. Su ventaja es que el precio suele ser aproximadamente la mitad del oficial y que los modelos comunes suelen ser compatibles con estos protocolos:
v1/chat/completionsv1/messagesv1beta/models/
Por ejemplo, podemos abstraer la llamada local /chat en un cliente unificado. En la fase de desarrollo, usamos AirLLM local; en el entorno online, cambiamos a Defapi:
import os
import requests
def ask_model(prompt: str) -> str:
"""Elige inferencia local o una interfaz compatible con Defapi según variables de entorno."""
provider = os.getenv("MODEL_PROVIDER", "local")
if provider == "defapi":
response = requests.post(
"https://api.defapi.org/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.environ['DEFAPI_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": os.getenv("DEFAPI_MODEL", "anthropic/claude-sonnet-4.5"),
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.7,
},
timeout=60,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
response = requests.post(
"http://localhost:8000/chat",
json={
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 256,
},
timeout=300,
)
response.raise_for_status()
return response.json()["text"]
La ventaja de este enfoque es que las fronteras quedan muy claras: el modelo local se encarga de explorar y validar, mientras que Defapi se encarga de integrar modelos online de forma estable, unificada y a bajo coste. Para OpenClaw, servicios RAG y Agentes automatizados, este esquema de doble modo es muy útil.
Solución de problemas frecuentes
1. ¿Qué hacer si falla la instalación de flash-attn?
Primero confirma que PyTorch reconozca CUDA correctamente:
python -c "import torch; print(torch.__version__); print(torch.version.cuda); print(torch.cuda.is_available())"
Si torch.cuda.is_available() es False, no te precipites a instalar flash-attn. Primero debes corregir PyTorch, el driver y las versiones de CUDA. Para modelos como Kimi K3, se recomienda usar una compilación para CUDA 12, para evitar fallos de compilación causados por la falta de paquetes precompilados adecuados en CUDA 13.
2. ¿Qué hacer si transformers da un error de versión?
Fija la versión en 4.56.x:
pip uninstall -y transformers
pip install "transformers>=4.56,<4.57"
Luego vuelve a ejecutar:
python run_kimi_k3.py
Si ves que falla la carga del código remoto del modelo, normalmente es porque la versión de transformers no coincide con el código del modelo.
3. ¿Qué hacer si falla la descarga desde Hugging Face?
Primero confirma si el token funciona:
huggingface-cli whoami
Si no estás logueado:
huggingface-cli login
También puedes usar solo HF_TOKEN dentro de .env. Si el modelo requiere permisos de acceso, primero debes aceptar los términos en la página del modelo.
4. ¿Qué hacer si el espacio en disco es insuficiente?
La primera ejecución de AirLLM descargará los pesos originales y también generará los archivos del modelo dividido. Puedes mover el directorio de caché a un disco más grande:
export HF_HOME=/mnt/models/huggingface
O configurarlo en .env:
HF_HOME=/mnt/models/huggingface
Si el disco sigue justo, puedes probar delete_original=True de AirLLM para conservar solo los archivos convertidos:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
delete_original=True, # Elimina los pesos originales tras la conversión para ahorrar espacio en disco
)
5. ¿Por qué la primera ejecución es especialmente lenta?
Es un comportamiento normal. La primera ejecución incluye pasos como descargar pesos, leer índices, dividir el modelo y escribir en la caché. La segunda ejecución reutiliza la caché, por lo que será mucho más rápida.
Puedes habilitar el profiling:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=True, # Muestra el tiempo de carga e inferencia para ayudar a localizar cuellos de botella
)
Si el tiempo se gasta principalmente en lectura de disco, cambiar a un SSD NVMe más rápido suele ser más efectivo que aumentar la VRAM.
6. ¿La VRAM no baja a lo esperado?
Primero confirma que tu código no mezcla el método tradicional de carga:
from airllm import AutoModel
model = AutoModel.from_pretrained(model_id)
No mezcles con el método tradicional de carga:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(model_id)
Este último seguirá la ruta de carga tradicional y es fácil que llene la VRAM de inmediato.
7. ¿Por qué el servicio de API se bloquea cuando aumentan las concurrencias?
La inferencia local con poca VRAM es más adecuada para experimentos en serie y no para asumir directamente llamadas de alta concurrencia. La solución más simple es limitar la concurrencia:
uvicorn server:app --host 0.0.0.0 --port 8000 --workers 1
Si necesitas llamadas online más estables, puedes derivar las solicitudes a Defapi en línea, dejando AirLLM local para evaluación y depuración.
Lecturas adicionales / Direcciones avanzadas
- Comparar el tiempo de carga, la VRAM y la calidad de salida de Kimi K3, DeepSeek-V3 y Qwen3-235B bajo AirLLM.
- Convertir el servicio FastAPI en una interfaz compatible con OpenAI tipo
/v1/chat/completions. - Integrar Defapi y usar
v1/chat/completionsov1/messagespara gestionar llamadas a modelos online de forma unificada. - Añadir una cola al servicio de AirLLM para evitar que varias solicitudes compitan a la vez por el disco y la GPU.
- En OpenClaw u otros marcos de Agent, configurar modelos locales y modelos en la nube para seleccionar dinámicamente según la complejidad de la tarea.