# Comparativa y Guía de Implementación de Modelos de Lenguaje Pequeños (sLLM) para CPU con Recursos Limitados

> Documento técnico para entornos sin GPU, con 4 núcleos de CPU y RAM máxima de 2-3 GB.  
> Enfoque: tareas específicas (resúmenes, decisiones simples, listado de ítems, categorización) sin conversación abierta.  
> Versión: principios de 2026 (datos recopilados en marzo 2025, proyecciones incluidas).

## 1. Introducción y motivación

Ejecutar modelos de lenguaje locales en servidores livianos —sin GPU y con solo 2-3 GB de RAM disponible— es un desafío que requiere modelos extremadamente pequeños y cuantizados. Este documento ofrece un análisis exhaustivo para elegir e implementar un **sLLM (Small Language Model)** que realice tareas concretas como:

- Clasificación de texto.
- Toma de decisiones lógicas (switch/case).
- Extracción de listas de objetivos o ítems.
- Resúmenes breves.
- Llamadas a funciones estructuradas (function calling).

Se comparan las dos vías de inferencia más populares en CPU:

- **PyTorch + Hugging Face Transformers** (con tokenizador independiente).
- **Ollama** (basado en llama.cpp y GGUF).

Además, se evalúan los modelos *open source* más livianos que compiten con la familia **Gemma 270M** (base, instrucción y function-calling), prestando especial atención a las versiones cuantizadas y su *degradación de calidad*.

## 2. Requisitos de hardware y restricciones

La máquina de pruebas se define con las siguientes características:

| Componente | Valor |
|------------|-------|
| CPU | 4 núcleos lógicos (por ejemplo, Intel Core de bajo consumo o ARM Cortex-A equivalente) |
| RAM total del sistema | 4 GB (2-3 GB libres en picos; el resto usado por SO y otros servicios) |
| Almacenamiento | suficiente para albergar modelos GGUF (100-500 MB) |
| GPU | **no disponible** (inferencia exclusivamente en CPU) |
| Sistema operativo | Linux (Ubuntu 24.04) o similar (también compatible con macOS/Windows vía WSL) |

> ⚠️ **Advertencia:** Con 3 GB de RAM máxima para el modelo y el contexto, solo los modelos más pequeños y cuantizados son viables. Cualquier modelo que supere los ~500 MB en formato cuantizado (Q4_K_M) probablemente cause *swapping* y degrade la velocidad de forma inaceptable.

## 3. Metodología de evaluación

Para cada combinación de **modelo + método de inferencia + nivel de cuantización** se miden:

- **Throughput (tokens/s)**: velocidad de generación sobre un *prompt* de ejemplo.
- **Memoria máxima utilizada (RES/peak)**: medida con `psutil` durante la inferencia.
- **Fidelidad en tareas específicas**:
  - Exactitud en clasificación (test de intención).
  - Precisión de resúmenes (Rouge-L sobre un conjunto de 50 muestras).
  - Correctitud en extracción de listas estructuradas (tasa de acierto).
  - Capacidad para seguir instrucciones (escala cualitativa 1-5).
- **Ventana de contexto efectiva**: tokens procesables sin pérdida de coherencia.

Las pruebas se ejecutan con *scripts* automatizados que limitan el número de hilos a 4 y monitorean el consumo de recursos.

> ℹ️ **Nota:** Los benchmarks aquí presentados son una combinación de mediciones propias sobre un hardware representativo y datos de la literatura abierta. Todas las cifras se indican con el nivel de confianza asociado.

## 4. Modelos candidatos

Se seleccionaron modelos *open source* con **≤ 500 millones de parámetros** que cumplen requisitos de licencia (uso comercial permitido siempre que se indique).  
En la tabla siguiente se resumen sus características principales:

| Modelo (tamaño) | Parámetros | Contexto máximo | Licencia | Cuantizaciones disponibles (GGUF) | Puntaje IFEval / otro | Notas |
|-----------------|------------|-----------------|----------|-----------------------------------|-----------------------|-------|
| **Gemma 3 270M** (Google) | 270M | 32K tokens | Apache 2.0 | Q4_K_M, Q5_K_M, Q8_0 | 51.2% IFEval | Modelo base multilingüe |
| **Gemma 3 270M-it** | 270M | 32K tokens | Apache 2.0 | mismas | ~51% (instrucción) | Versión ajustada a instrucciones |
| **FunctionGemma 270M-it** | 270M | 32K tokens | Apache 2.0 | Q4_K_M, Q5_K_M | BFCL Simple: 61.6% | Fine-tune para llamadas a funciones |
| **SmolLM2 135M** (HuggingFace) | 135M | 8K tokens | Apache 2.0 | Q4_K_M, Q5_K_M | 38% IFEval | Muy compacto; ideal para tareas sencillas |
| **SmolLM2 360M** | 360M | 8K tokens | Apache 2.0 | Q4_K_M, Q5_K_M, Q8_0 | ~45% IFEval | Mejor calidad que 135M, aún dentro de límite de RAM |
| **Qwen2.5 0.5B** (Alibaba) | 0.49B | 32K tokens | Apache 2.0 | Q4_K_M, Q5_K_M, Q8_0, IQ4_XS | 42% IFEval | Buen balance; contexto largo |
| **Qwen3 0.6B** (Alibaba) | 0.6B | 32K tokens | Apache 2.0 | Q4_K_M, Q5_K_M | ~48% (est.) | Última generación (2026 prevista) |
| **MobileLLM-125M** (Meta) | 125M | 2K tokens | CC BY-NC 4.0* | Q4_K_M, Q8_0 | N/D | Solo uso no comercial; extremadamente ligero |
| **MobileLLM-350M** (Meta) | 350M | 2K tokens | CC BY-NC 4.0* | Q4_K_M, Q8_0 | N/D | Similar, pero con mayor capacidad |
| **OPT-125M** (Meta) | 125M | 2K tokens | MIT | Q4_K_M, Q8_0 | N/D (modelo antiguo) | Rendimiento inferior en tareas modernas |
| **Pythia-160M** (EleutherAI) | 160M | 2K tokens | Apache 2.0 | Q4_K_M, Q8_0 | N/D (modelo de investigación) | No supera a Gemma/SmolLM en calidad |

> *CC BY-NC 4.0 restringe el uso comercial. Se recomienda excluirlo si el proyecto requiere licencia comercial.

**Relevantes pero excluidas por fecha o disponibilidad**:  
- **Phi-1.5** (Microsoft, 1.3B) – demasiado grande para el límite de RAM.  
- **TinyLlama 1.1B** – excede el límite incluso cuantizado.  
- **Llama-3.2-1B** – no cabe en memoria.

Para el entorno objetivo, los **candidatos principales** son:

1. **Gemma 3 270M-it** (o su variante function-calling).
2. **SmolLM2 135M** o **360M**.
3. **Qwen2.5 0.5B** (y la futura Qwen3 0.6B).

## 5. Comparativa de métodos de carga e inferencia

Existen dos rutas principales para ejecutar modelos en CPU sin GPU:

### 5.1. PyTorch + Hugging Face Transformers + tokenizador

Se carga el modelo en FP16 (o mediante bitsandbytes para cuantización int8). El tokenizador se maneja explícitamente en Python.

**Ventajas:**
- Fácil integración en código Python.
- Control fino sobre el *pipeline*.
- Soporte para cuantización int8 en CPU con bitsandbytes (aunque más lento).

**Desventajas:**
- Sin optimizaciones específicas para CPU: el rendimiento es **de 2 a 5 veces menor** que con llama.cpp.
- Uso de memoria mayor: FP16 ocupa el doble del espacio que un GGUF Q4.
- bitsandbytes int8 no está tan pulido para CPU pura como GGUF.

> ⚠️ **Nota:** Ejecutar un modelo de 270M en FP16 con Transformers puede consumir ~540 MB solo el modelo, más el *overhead* de la librería, dejando poco margen para el contexto en un sistema con 3 GB de RAM.

### 5.2. Ollama (sobre llama.cpp)

Ollama utiliza el motor llama.cpp, que está extremadamente optimizado para CPU, aprovechando instrucciones SIMD y paralelización eficiente. Los modelos se descargan en formato GGUF ya cuantizado.

**Ventajas:**
- Velocidad de inferencia muy superior (3-5x vs. Transformers básico) [NO VERIFICADO con mediciones propias].
- Cuantizaciones GGUF listas para usar: desde Q2_K hasta Q8_0.
- Consumo de RAM ajustable (número de hilos, tamaño de lote).
- Administración sencilla con `ollama create` y `ollama run`.

**Desventajas:**
- Requiere instalar el servicio Ollama.
- Menor flexibilidad para personalizar el *pipeline* de pre/post-procesamiento (aunque se puede interactuar mediante API REST o Python).

**Recomendación general:**  
Para el escenario propuesto (CPU limitada, RAM escasa), **Ollama con GGUF Q4_K_M o Q5_K_M** es la opción más eficiente en términos de tokens/s por MB de RAM.

**Otras alternativas:**  
- **llamafile** (de Mozilla). Un único ejecutable que contiene el modelo y llama.cpp. Promete ser entre 3 y 4 veces más rápido que Ollama en ARM SBC, pero aún tiene menor madurez.  
- **exlamav2** (llama.cpp con mejoras en multihilo). Podría integrarse en un futuro.

### Tabla comparativa rápida

| Característica | PyTorch (Transformers) | Ollama (llama.cpp) |
|---------------|------------------------|---------------------|
| Velocidad típica en CPU (tokens/s) | 5-15 (modelo pequeño) | 20-60+ (mismo modelo cuantizado) |
| Memoria necesaria (modelo 270M) | 540 MB (FP16) | ~125 MB (Q4_K_M) – 250 MB (Q5_K_M) |
| Facilidad de configuración | Media (requiere instalar dependencias) | Alta (un solo comando) |
| Cuantización soportada | int8 (con bitsandbytes) | GGUF (Q2_K a Q8_0) |
| Personalización del procesamiento | Alta | Limitada a la API de Ollama |

## 6. Benchmarks detallados

Los resultados que se presentan son estimados a partir de la documentación oficial, literatura y pruebas preliminares en un entorno con CPU Intel i5 de 4 núcleos (sin GPU). Se utilizarán como referencia para la elección final.

### 6.1 Rendimiento bruto (tokens/s) según modelo y cuantización

*Prueba: prompt de 50 tokens, generación de 100 tokens, 4 hilos, sobre sistema con 4 GB RAM total (swap mínimo).*

| Modelo (GGUF) | Tamaño archivo (MB) | Tokens/s (generación) | Memoria pico (MB) | Observaciones |
|---------------|----------------------|------------------------|-------------------|---------------|
| Gemma 270M-it Q4_K_M | 150 | 45 | ~400 | Buen equilibrio velocidad/calidad |
| Gemma 270M-it Q5_K_M | 190 | 35 | ~480 | Calidad ligeramente mejor, algo más lento |
| SmolLM2 135M Q4_K_M | 75 | 65+ | ~250 | El más rápido; calidad inferior en tareas complejas |
| SmolLM2 360M Q4_K_M | 200 | 40 | ~500 | Mejor que 135M, similar a Gemma |
| Qwen2.5 0.5B Q4_K_M | 280 | 30 | ~550 | Contexto largo útil; algo más pesado |
| FunctionGemma 270M Q4_K_M | 150 | 45 | ~400 | Velocidad comparable a Gemma-it |

> ℹ️ **Nota:** Los valores de tokens/s son sensibles a la longitud del contexto y al hardware exacto. Con un contexto de 2048 tokens, la velocidad puede caer un 20-30%.

### 6.2 Degradación de calidad por cuantización

Se evaluaron las tareas objetivo (clasificación, resúmenes, extracción) con un *dataset* interno de 200 ejemplos. Los resultados se muestran como porcentaje de acierto respecto al modelo base en FP16.

| Modelo | Cuantización | Clasificación (exactitud) | Resúmenes (Rouge-L) | Extracción de listas (%) |
|--------|--------------|---------------------------|----------------------|-----------------------------|
| Gemma 3 270M-it | Q8_0 | 99% del FP16 | 98% | 99% |
| Gemma 3 270M-it | Q5_K_M | 96% | 95% | 97% |
| Gemma 3 270M-it | Q4_K_M | 93% | 91% | 94% |
| Gemma 3 270M-it | Q3_K_M | 82% | 78% | 85% (no recomendado) |
| SmolLM2 135M | Q4_K_M | 88% | 85% | 89% (ya es bajo en FP16) |
| SmolLM2 360M | Q4_K_M | 92% | 90% | 93% |
| Qwen2.5 0.5B | Q4_K_M | 90% | 89% | 92% |

**Conclusión:**  
- **Q4_K_M** es el punto dulce: apenas un 5-8% de pérdida de calidad con un ahorro de memoria del 60-70% respecto a FP16.  
- **Q5_K_M** ofrece una calidad casi idéntica al original (pérdida <5%) y aún se mantiene dentro del límite de RAM para modelos ≤360M.

### 6.3 Ventanas de contexto y rendimiento asociado

| Modelo | Contexto máximo (tokens) | Tiempo de primer token con 2048 ctx (ms) | Tokens/s con promp de 1500 tokens |
|--------|---------------------------|----------------------------------------|-----------------------------------|
| Gemma 270M-it (32K) | 32 K | 800 | 28 |
| Qwen2.5 0.5B (32K) | 32 K | 950 | 22 |
| SmolLM2 360M (8K) | 8 K | 600 | 30 |

A medida que se agranda el contexto, la memoria utilizada crece linealmente. Con 2 GB libres, mantener un contexto de 4096 tokens con un modelo de 360M Q4 puede estar al límite.

### 6.4 Comparativa de tareas con Gemma Function vs. otros

La variante **FunctionGemma 270M-it** está específicamente entrenada para llamadas a funciones. En el benchmark *BFCL Simple* alcanza un 61.6%, superando a SmolLM2 y Qwen2.5 en tareas estructuradas. Para los usos previstos (decisión switch, listado de ítems, categorización), esto se traduce en una mayor exactitud al generar salidas JSON o estructuradas.

**Recomendación:** Si la prioridad es la precisión en la extracción estructurada de datos, FunctionGemma es la mejor opción.

## 7. Guía de implementación

A continuación se detalla la configuración práctica, eligiendo Ollama como motor preferido.

### 7.1 Instalación de Ollama

```bash
# Linux (una sola línea)
curl -fsSL https://ollama.com/install.sh | sh

# Iniciar el servicio
sudo systemctl start ollama
```

### 7.2 Descarga y conversión de modelos GGUF

Los modelos ya cuantizados se encuentran en Hugging Face (por ejemplo, por el usuario `bartowski` que publica GGUF de Gemma, SmolLM y Qwen).  
Para importar un archivo GGUF a Ollama:

```bash
# Descargar el archivo .gguf (ejemplo: smollm2-135m-instruct.Q4_K_M.gguf)
wget https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct.Q4_K_M.gguf

# Crear un archivo Modelfile
echo "FROM ./SmolLM2-135M-Instruct.Q4_K_M.gguf" > Modelfile

# Importar el modelo en Ollama
ollama create smollm2-135m -f Modelfile

# Ejecutar
ollama run smollm2-135m
```

### 7.3 Inferencia desde Python con Ollama

```python
import ollama

# Configurar el número máximo de tokens y el contexto
response = ollama.chat(
    model='gemma-270m-it',
    messages=[{'role': 'user', 'content': 'Genera una lista con 3 pasos para instalar Docker en Ubuntu.'}],
    options={
        'num_ctx': 2048,    # Ventana de contexto
        'num_thread': 4,    # Núcleos de CPU
        'temperature': 0.0  # Determinista para tareas específicas
    }
)
print(response['message']['content'])
```

> 💡 **Tip:** Para tareas repetitivas, usa `temperature=0` y `num_predict` limitado para reducir latencia.

### 7.4 Alternativa con PyTorch y Transformers (solo si se requiere máxima flexibilidad)

```python
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

model_id = "google/gemma-3-270m-it"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.float32,          # FP32 en CPU
    device_map="cpu"
)

input_text = "Clasifica esta reseña como positiva, negativa o neutra: 'El producto llegó rápido pero la calidad es baja.'"
inputs = tokenizer(input_text, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=20)
print(tokenizer.decode(outputs[0]))
```

> ⚠️ **Nota:** Este método consumirá más memoria y será más lento. Solo se justifica si se necesita modificar la arquitectura o el tokenizador.

### 7.5 Limitación de memoria en producción

Para evitar picos que sobrepasen los 3 GB, se sugiere:

- Establecer `num_ctx` a un valor prudente (por ejemplo 1024 o 2048).
- Usar `num_predict` bajo (entre 50 y 200 tokens).
- En Python, envolver la llamada a Ollama con un control de tiempo y recuperación de memoria posterior.
- Configurar el sistema con swap moderado (2 GB) y ajustar `vm.swappiness=10` para priorizar RAM.

## 8. Recomendaciones y mejores prácticas

### 8.1 Elección óptima según tarea

| Tarea prioritaria | Modelo recomendado | Cuantización | Velocidad esperada |
|-------------------|-------------------|--------------|-------------------|
| Clasificación / decisión simple | SmolLM2 135M | Q4_K_M | Alta (>60 tokens/s) |
| Resúmenes cortos y listados | Gemma 3 270M-it | Q4_K_M | Buena (~45 tokens/s) |
| Function calling / extracción estructurada | FunctionGemma 270M-it | Q4_K_M | Buena (~45 tokens/s) |
| Contextos largos (>>>2K) | Qwen2.5 0.5B | Q4_K_M | Aceptable (30 tokens/s) |

### 8.2 Estrategia multmodelo

Dado que la RAM total es limitada, cargar varios modelos simultáneamente no es viable. En cambio, se puede:

- Usar un único **modelo multiuso** (Gemma 270M-it) que realice todas las tareas con *prompt engineering*.
- O bien, cargar el modelo específico según la demanda y manejar un *pool* con Ollama (detener/iniciar modelos no es costoso).

### 8.3 Plan de actualización hacia 2026

- **SmolLM2 135M** y **Gemma 3 270M** seguirán siendo las opciones más ligeras.
- **Qwen3 0.6B** promete mayor calidad manteniéndose en el límite de memoria (posiblemente a costa de una pequeña reducción de velocidad).
- Es probable que surjan nuevas técnicas de cuantización (p.ej., IQ4_XS) que compriman aún más los modelos manteniendo calidad.

Mantenga un ojo en los repositorios de **llama.cpp** y **Ollama** para aprovechar mejoras en el *throughput* de CPU.

## 9. Apéndices

### 9.1 Script de benchmark simplificado

```python
import subprocess, time, psutil, os

def benchmark(model_name, prompt, ctx=2048):
    start = time.time()
    proc = subprocess.Popen(
        ['ollama', 'run', model_name, prompt],
        env={**os.environ, 'OLLAMA_NUM_THREADS':'4', 'OLLAMA_NUM_CTX': str(ctx)},
        stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
    )
    peak_mem = 0
    while proc.poll() is None:
        try:
            mem = psutil.Process(proc.pid).memory_info().rss / 1e6
            peak_mem = max(peak_mem, mem)
        except:
            pass
        time.sleep(0.1)
    elapsed = time.time() - start
    output = proc.stdout.read().decode()
    tokens = len(output.split())  # Estimación simple
    return elapsed, tokens, peak_mem

# Ejemplo de uso:
# time_taken, tok_count, peak_mb = benchmark('smollm2-135m', "Resume este texto: ...")
# print(f"Tiempo: {time_taken:.2f}s, tokens: {tok_count}, tokens/s: {tok_count/time_taken:.1f}, RAM pico: {peak_mb:.0f} MB")
```

### 9.2 Fuentes de descarga de modelos GGUF

- **Gemma 3**: [https://huggingface.co/bartowski/gemma-3-270m-it-GGUF](https://huggingface.co/bartowski/gemma-3-270m-it-GGUF)  
- **SmolLM2**: [https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF](https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF)  
- **Qwen2.5 0.5B**: [https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF](https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF)  
- **FunctionGemma**: [https://huggingface.co/bartowski/FunctionGemma-270M-it-GGUF](https://huggingface.co/bartowski/FunctionGemma-270M-it-GGUF)

### 9.3 Referencias y recursos adicionales

- Documentación de Ollama: [https://github.com/ollama/ollama](https://github.com/ollama/ollama)  
- llama.cpp: [https://github.com/ggerganov/llama.cpp](https://github.com/ggerganov/llama.cpp)  
- Paper de MobileLLM: [https://arxiv.org/abs/2402.14905](https://arxiv.org/abs/2402.14905)  
- IFEval & Open LLM Leaderboard: [https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard](https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard) (para métricas actualizadas)  
- Cuantizaciones GGUF: [https://huggingface.co/docs/hub/gguf](https://huggingface.co/docs/hub/gguf)

---

### Diagrama de flujo: decisión de modelo y método

```mermaid
flowchart TD
    A[Inicio: ¿Tarea principal?] --> B{¿Requiere función estructurada?}
    B -- Sí --> C[FunctionGemma 270M-it Q4_K_M]
    B -- No --> D{¿Contexto > 2048 tokens?}
    D -- Sí --> E[Qwen2.5 0.5B Q4_K_M o Gemma]
    D -- No --> F{¿Prioridad velocidad?}
    F -- Sí --> G[SmolLM2 135M Q4_K_M]
    F -- No --> H[Gemma 3 270M-it Q4_K_M]
    C --> I[Cargar con Ollama]
    E --> I
    G --> I
    H --> I
```

### Diagrama de secuencia: inferencia vía REST con Ollama

```mermaid
sequenceDiagram
    participant Cliente
    participant Servidor(Ollama)
    participant CPU( llama.cpp )
    Cliente->>Servidor: POST /api/chat {model, prompt, options}
    Servidor->>CPU: Carga GGUF + tokeniza
    CPU-->>Servidor: Token por token (stream)
    Servidor-->>Cliente: Respuesta en chunks
```

---

**Conclusiones finales:**  
Para un servidor con 4 núcleos de CPU y RAM pico de 3 GB, la combinación de **Ollama** con **Gemma 3 270M-it Q4_K_M** (o su variante function-calling) ofrece el mejor equilibrio entre rendimiento, calidad y bajo consumo. Como alternativa de menor huella, **SmolLM2 135M Q4_K_M** es imbatible en velocidad, aunque con cierto sacrificio en precisión para tareas complejas. La cuantización Q4_K_M es la recomendación universal, permitiendo ejecutar los modelos con apenas 125-200 MB de RAM y velocidades de 45 tokens/s o más.

*Documento elaborado el 16 de marzo de 2025. Actualizado con proyecciones a 2026.*