Ejecutando IA en el lado del cliente
Ejecutando Modelos de Lenguaje en el Navegador con Transformers.js y ONNX Runtime
Introducción
En los últimos años, hemos visto un crecimiento exponencial en el tamaño y la complejidad de los modelos de lenguaje (LLMs). Tradicionalmente, estos modelos requieren servidores potentes con GPUs dedicadas para ejecutarse. Sin embargo, con los avances en cuantización de modelos y las nuevas APIs web como WebGPU y WebAssembly, ahora es posible ejecutar modelos de lenguaje directamente en el navegador del usuario.
En este artículo, exploraremos cómo construir una aplicación web que ejecuta el modelo SmolLM2-135M-Instruct completamente en el cliente, sin necesidad de un backend. Utilizaremos Transformers.js, ONNX Runtime Web, y aprovecharemos WebGPU cuando esté disponible, con un fallback automático a WebAssembly.
Arquitectura de la Solución
graph TB
A[Navegador Web] --> B{WebGPU<br/>Disponible?}
B -->|Sí| C[ONNX Runtime Web<br/>con WebGPU]
B -->|No| D[ONNX Runtime Web<br/>con WebAssembly]
C --> E[Modelo ONNX<br/>SmolLM2-135M-Instruct]
D --> E
E --> F[Transformers.js<br/>Pipeline]
F --> G[Generación de Texto]
H[Servidor HTTP<br/>Nginx] --> I[Archivos Estáticos]
I --> J[index.html]
I --> K[main.js]
I --> L[Modelo ONNX<br/>Local]
style C fill:#4CAF50
style D fill:#FF9800
style E fill:#2196F3
style F fill:#9C27B0
Tecnologías Utilizadas
1. Transformers.js
Transformers.js es una biblioteca JavaScript que permite ejecutar modelos de Hugging Face directamente en el navegador. Está construida sobre ONNX Runtime Web y proporciona una API de alto nivel similar a la biblioteca Python transformers.
Características principales:
- Ejecución 100% en el cliente (sin servidor)
- Soporte para múltiples tareas: text-generation, text-classification, question-answering, etc.
- Optimización automática para WebGPU y WebAssembly
- Caché inteligente de modelos en el navegador
2. ONNX Runtime Web
ONNX Runtime Web es la versión web de ONNX Runtime, un motor de inferencia de alto rendimiento para modelos de machine learning. Soporta múltiples backends:
graph LR
A[ONNX Runtime Web] --> B[WebGPU Backend]
A --> C[WebAssembly Backend]
A --> D[JS Backend]
B --> E[Aceleración GPU<br/>Mayor Rendimiento]
C --> F[Compatibilidad Universal<br/>Rendimiento Medio]
D --> G[Compatibilidad Máxima<br/>Rendimiento Bajo]
style B fill:#4CAF50
style C fill:#FF9800
style D fill:#F44336
Ventajas de ONNX:
- Formato estándar para modelos de ML
- Optimizaciones específicas del navegador
- Soporte para cuantización (INT8, INT4, etc.)
- Interoperabilidad entre frameworks
3. WebGPU vs WebAssembly
WebGPU
- Ventajas:
- Aceleración por GPU (10-100x más rápido que CPU)
- Paralelización masiva
- Acceso directo a recursos de GPU
- Limitaciones:
- Requiere navegadores modernos (Chrome 113+, Edge 113+)
- Necesita drivers de GPU actualizados
- No disponible en todos los dispositivos
WebAssembly (WASM)
- Ventajas:
- Compatibilidad universal
- Rendimiento predecible
- No requiere GPU
- Limitaciones:
- Más lento que WebGPU (pero más rápido que JavaScript puro)
- Limitado por el rendimiento de CPU
4. Cuantización de Modelos
La cuantización reduce la precisión de los pesos del modelo para disminuir el tamaño y aumentar la velocidad de inferencia:
graph TB
A[Modelo Original<br/>FP32 - 540MB] --> B[Cuantización FP16<br/>270MB]
A --> C[Cuantización INT8<br/>137MB]
A --> D[Cuantización INT4<br/>68MB]
A --> E[Cuantización BNB4<br/>175MB]
B --> F[Pérdida Mínima<br/>Rendimiento Alto]
C --> G[Pérdida Moderada<br/>Rendimiento Medio]
D --> H[Pérdida Significativa<br/>Rendimiento Bajo]
E --> I[Pérdida Mínima<br/>Rendimiento Alto<br/>Optimizado]
style E fill:#4CAF50
style B fill:#8BC34A
style C fill:#FFC107
style D fill:#FF9800
En nuestro proyecto, utilizamos BNB4 (BitsAndBytes 4-bit), que ofrece un excelente equilibrio entre tamaño, velocidad y calidad.
Estructura del Proyecto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
LLM-on-browser/
├── index.html # Interfaz de usuario
├── main.js # Lógica principal
├── style.css # Estilos
├── Dockerfile # Configuración Docker
├── docker-compose.yml # Orquestación Docker
├── .dockerignore # Exclusiones Docker
└── SmolLM2-135M-Instruct/ # Modelo local
├── config.json # Configuración del modelo
├── generation_config.json # Configuración de generación
├── tokenizer.json # Tokenizador
├── tokenizer_config.json # Config del tokenizador
└── onnx/
└── model_bnb4.onnx # Modelo ONNX cuantizado (175MB)
Flujo de Ejecución
sequenceDiagram
participant U as Usuario
participant B as Navegador
participant T as Transformers.js
participant O as ONNX Runtime
participant G as GPU/CPU
U->>B: Abre aplicación
B->>T: Inicializa pipeline
T->>B: Detecta dispositivo
B->>T: WebGPU disponible?
alt WebGPU disponible
T->>O: Inicializa con WebGPU
O->>G: Carga modelo en GPU
else WebGPU no disponible
T->>O: Inicializa con WASM
O->>G: Carga modelo en CPU
end
T->>B: Modelo cargado ✓
U->>B: Escribe prompt
U->>B: Click "Generar"
B->>T: Ejecuta generación
T->>O: Inferencia
O->>G: Procesa tokens
G->>O: Resultados
O->>T: Texto generado
T->>B: Muestra resultado
B->>U: Respuesta visible
Implementación Detallada
1. Configuración del Entorno
1
2
3
4
5
6
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0';
// Configurar para usar solo modelos locales
env.allowLocalModels = true;
env.allowRemoteModels = false;
env.localURL = '';
Explicación:
allowLocalModels = true: Permite cargar modelos desde el sistema de archivos localallowRemoteModels = false: Desactiva la descarga desde Hugging Face HublocalURL = '': Configura la ruta base para modelos locales
2. Detección de Dispositivo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function detectDevice() {
let device = 'wasm';
let deviceName = 'WebAssembly';
// Verificar si WebGPU está disponible
if ('gpu' in navigator) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
device = 'webgpu';
deviceName = 'WebGPU';
}
} catch (error) {
console.warn('WebGPU no disponible:', error);
}
}
return device;
}
Flujo de detección:
flowchart TD
A[Inicio] --> B{¿gpu en navigator?}
B -->|No| C[Usar WebAssembly]
B -->|Sí| D[requestAdapter]
D --> E{¿Adapter válido?}
E -->|Sí| F[Usar WebGPU]
E -->|No| C
C --> G[Retornar 'wasm']
F --> H[Retornar 'webgpu']
style F fill:#4CAF50
style C fill:#FF9800
3. Inicialización del Modelo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function initializeModel() {
try {
const device = await detectDevice();
generator = await pipeline(
'text-generation',
'SmolLM2-135M-Instruct',
{
dtype: 'bnb4',
device: device,
}
);
updateStatus('Modelo cargado correctamente ✓', 'success');
} catch (error) {
// Manejo de errores con fallback
}
}
Proceso de carga del modelo:
graph TB
A[Pipeline Request] --> B[Cargar config.json]
B --> C[Cargar tokenizer.json]
C --> D[Cargar generation_config.json]
D --> E{¿Dispositivo?}
E -->|WebGPU| F[Cargar model_bnb4.onnx<br/>en GPU]
E -->|WASM| G[Cargar model_bnb4.onnx<br/>en CPU]
F --> H[Inicializar ONNX Runtime]
G --> H
H --> I[Pipeline Listo]
style F fill:#4CAF50
style G fill:#FF9800
style I fill:#2196F3
4. Generación de Texto
1
2
3
4
5
6
7
8
9
10
11
12
async function generateResponse() {
const prompt = promptInput.value.trim();
const result = await generator(prompt, {
max_new_tokens: 100,
temperature: 0.7,
top_p: 0.9,
do_sample: true,
});
// Procesar y mostrar resultado
}
Parámetros de generación:
max_new_tokens: Número máximo de tokens a generartemperature: Controla la aleatoriedad (0.0 = determinista, 1.0+ = creativo)top_p: Nucleus sampling - considera solo los tokens con probabilidad acumuladado_sample: Habilita muestreo probabilístico vs. greedy decoding
Arquitectura del Modelo SmolLM2
graph TB
A[Input Text] --> B[Tokenizer]
B --> C[Token IDs]
C --> D[Embedding Layer]
D --> E[Transformer Blocks<br/>x30]
E --> F1[Attention Head 1]
E --> F2[Attention Head 2]
E --> F3[Attention Head 3]
E --> F4[Attention Head 4]
E --> F5[Attention Head 5]
E --> F6[Attention Head 6]
E --> F7[Attention Head 7]
E --> F8[Attention Head 8]
E --> F9[Attention Head 9]
F1 --> G[Multi-Head Attention]
F2 --> G
F3 --> G
F4 --> G
F5 --> G
F6 --> G
F7 --> G
F8 --> G
F9 --> G
G --> H[Feed Forward Network]
H --> I[Layer Normalization]
I --> J[Output Layer]
J --> K[Logits]
K --> L[Softmax]
L --> M[Next Token Prediction]
M --> N[Generated Text]
style E fill:#2196F3
style G fill:#4CAF50
style H fill:#FF9800
Especificaciones del modelo:
- Arquitectura: LlamaForCausalLM
- Parámetros: 135M
- Capas: 30
- Heads de atención: 9
- Hidden size: 576
- Vocabulario: ~32,000 tokens
- Contexto máximo: 8,192 tokens
Optimizaciones y Mejoras
1. Caché de Modelos
Los modelos se almacenan en el caché del navegador usando IndexedDB, permitiendo cargas más rápidas en visitas posteriores.
2. Streaming de Respuestas
Aunque no implementado en esta versión, Transformers.js soporta streaming de tokens, permitiendo mostrar resultados mientras se generan.
3. Batching
Para múltiples solicitudes, se puede implementar batching para mejorar el throughput.
Despliegue con Docker
Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
FROM nginx:alpine
# Copiar archivos de la aplicación
COPY index.html /usr/share/nginx/html/
COPY main.js /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
# Copiar el modelo al directorio esperado por Transformers.js
COPY SmolLM2-135M-Instruct/ /usr/share/nginx/html/models/SmolLM2-135M-Instruct/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Estructura en el contenedor:
graph TB
A[Contenedor Nginx] --> B[/usr/share/nginx/html/]
B --> C[index.html]
B --> D[main.js]
B --> E[style.css]
B --> F[/models/]
F --> G[SmolLM2-135M-Instruct/]
G --> H[config.json]
G --> I[tokenizer.json]
G --> J[generation_config.json]
G --> K[onnx/]
K --> L[model_bnb4.onnx<br/>175MB]
style L fill:#4CAF50
Docker Compose
1
2
3
4
5
6
7
services:
web:
build: .
ports:
- "8080:80"
container_name: smollm2-browser
restart: unless-stopped
Consideraciones de Rendimiento
Tiempos de Carga
gantt
title Tiempo de Carga del Modelo
dateFormat X
axisFormat %Ls
section WebGPU
Cargar modelo (GPU) :0, 3000
Inicializar runtime :3000, 1000
Listo para usar :4000, 0
section WebAssembly
Cargar modelo (CPU) :0, 8000
Inicializar runtime :8000, 2000
Listo para usar :10000, 0
Velocidad de Inferencia
- WebGPU: ~20-50 tokens/segundo
- WebAssembly: ~5-15 tokens/segundo
- JavaScript puro: ~1-3 tokens/segundo
Limitaciones y Desafíos
1. Tamaño del Modelo
Aunque 175MB es relativamente pequeño, aún puede ser un desafío para:
- Conexiones lentas
- Dispositivos con almacenamiento limitado
- Navegadores con caché restringido
2. Rendimiento en Dispositivos Móviles
Los dispositivos móviles tienen limitaciones:
- Menor potencia de GPU
- Menos memoria RAM
- Batería limitada
3. Compatibilidad del Navegador
pie title Compatibilidad de WebGPU
"Chrome/Edge 113+" : 45
"Firefox (Experimental)" : 15
"Safari (No soportado)" : 25
"Navegadores antiguos" : 15
Casos de Uso
- Aplicaciones Offline: Funcionamiento sin conexión a internet
- Privacidad: Datos nunca salen del dispositivo del usuario
- Reducción de Costos: No requiere servidores de inferencia
- Baja Latencia: Sin comunicación de red
- Demostraciones: Prototipos rápidos sin infraestructura
Conclusiones
Ejecutar modelos de lenguaje en el navegador es ahora una realidad gracias a:
- Cuantización avanzada que reduce el tamaño de los modelos
- WebGPU que proporciona aceleración por GPU
- WebAssembly que ofrece compatibilidad universal
- Transformers.js que simplifica la integración
Aunque hay limitaciones, esta tecnología abre nuevas posibilidades para aplicaciones web que requieren inteligencia artificial sin depender de servidores externos.
Recursos Adicionales
Código Fuente
El código completo está disponible en el repositorio del proyecto. Para ejecutarlo:
1
2
3
4
5
# Con Docker
docker compose up --build
# O con servidor HTTP local
python -m http.server 8000
Luego abre http://localhost:8080 (o http://localhost:8000) en tu navegador.
Este artículo fue escrito como parte de una exploración de las capacidades de machine learning en el navegador. Las tecnologías web continúan evolucionando, y esperamos ver modelos más grandes y eficientes ejecutándose completamente en el cliente en el futuro cercano.