No description
  • Python 73.4%
  • Shell 26.6%
Find a file
2026-08-24 16:59:11 +01:00
repos-benchmark Endurece los cinco escenarios 2026-08-21 01:06:09 +01:00
scripts-benchmark Documentando planificación 2026-08-23 23:56:08 +01:00
.gitignore Añade .gitignore 2026-07-31 13:31:23 +01:00
benchmark-modelos-ollama.md Añade nuevos test y mejoras 2026-07-31 14:42:42 +01:00
benchmark-planificacion.md Documentos finales 2026-08-24 16:59:11 +01:00
benchmark-thinking-y-agentico.md Documentos finales 2026-08-24 16:59:11 +01:00
CLAUDE.md Documentos finales 2026-08-24 16:59:11 +01:00
modelos-opencode-go.md Arregla los test 2026-07-31 13:55:20 +01:00
README.md Documentos finales 2026-08-24 16:59:11 +01:00

Benchmarks de LLMs

Informes y harness para comparar modelos: los que corren en local sobre Ollama y las opciones de pago. El producto son los informes; los scripts existen para poder rehacerlos.

Informes

Informe Contenido
benchmark-modelos-ollama.md Evaluación de lean-ctx + comparativa de modelos en tareas de código
benchmark-thinking-y-agentico.md Efecto del thinking on/off y capacidad agéntica multi-fichero
benchmark-planificacion.md Capacidad de planificar un cambio antes de tocar código
modelos-opencode-go.md Precios, cupos y estrategia de consumo en OpenCode Go

Qué mide cada harness

Son dos cosas distintas y conviene no confundirlas:

  • Modelo purobench_lib.py (4 tareas de código, 31 casos) y bench_hard.py (6 problemas de razonamiento con respuesta numérica exacta) llaman directamente a la API nativa de Ollama (/api/chat). Es la única vía donde think:false se respeta; por la capa OpenAI-compatible se ignora.
  • Modelo + cliente agéntico — la parte agéntica de run_bench.sh pasa por un cliente, así que el resultado incluye su prompt de sistema, su bucle de tools y su herramienta de edición.

Para que esa segunda medida sea interpretable, cada escenario agéntico se corre con los dos clientes sobre el mismo modelo local: hermes y opencode (CLIENTS en run_bench.sh). Con el modelo fijo, una diferencia de resultado entre clientes es atribuible al cliente. Sin esa comparación no se puede distinguir "el modelo no supo" de "el cliente lo estropeó" — un modelo puede emitir escapes que la herramienta de edición de un cliente corrompe y la de otro no.

opencode usa los modelos locales como ollama/<modelo>. Los modelos de pago (opencode-go/*) quedan fuera de estos harness: son otra pregunta y se miden aparte.

El bucle es por modelo y dentro por cliente, para cargar cada modelo en VRAM una sola vez. Antes de los clientes se hace un warmup, porque si no el primero paga la carga y el segundo no: en granite4.1:30b eso son 47 segundos de diferencia, más que la tarea entera. Cualquier comparación de tiempos sin ese precalentamiento mide el orden del bucle, no el cliente.

Ejecutar

Requiere hermes y opencode en el PATH y un Ollama accesible (por defecto http://condor:11434).

# un modelo suelto, razonamiento con y sin thinking
python3 scripts-benchmark/bench_hard.py qwen3.6:27b true
python3 scripts-benchmark/bench_hard.py qwen3.6:27b false

# suite completa: todos los modelos, escenarios y clientes
bash scripts-benchmark/run_bench.sh

# acotada, para iterar sin gastar una hora de GPU
MODELS="gemma4:26b" SCENARIOS="regresion silencioso" bash scripts-benchmark/run_bench.sh

# pasada de referencia para escribir un informe (~3 h): 3 muestras de todo
REPS=3 AGENT_REPS=3 nohup bash scripts-benchmark/run_bench.sh > /tmp/suite.log 2>&1 &

Variables: OLLAMA_HOST, OUT, MODELS, SCENARIOS, CLIENTS, TIMEOUT, REPS (repeticiones de las tareas de modelo puro) y AGENT_REPS (repeticiones de cada celda agéntica).

El banco corre por defecto 4 modelos: qwen3-coder:30b, gemma4:26b, qwen3.6:27b y laguna-xs-2.1. granite4.1:30b y north-mini-code-1.0 están en EXCLUIDOS — siguen instalados y se reactivan poniéndolos en MODELS, pero no entran en el banco: el primero por lento (varios timeouts de 500s), el segundo por fallar dificil y contextolargo con ambos clientes. Si instalas un modelo nuevo y no lo añades a ninguna de las dos listas, el script avisa al arrancar.

Estados posibles: PASA, FALLA, FALLA_TESTS_MODIFICADOS, FALLA_TEST_NO_REPRODUCE, ERROR_CLIENTE (el cliente ni arrancó: no puntúa como fallo del modelo), NO_INSTALADO, y el sufijo +TIMEOUT cuando el run agotó el reloj (el veredicto vale, el tiempo es un tope).

opencode no descubre los modelos de Ollama solo: hay que declararlos a mano en ~/.config/opencode/opencode.json, dentro de provider.ollama.models. Si falta uno, opencode responde UnknownError en un segundo y el run sale como ERROR_CLIENTE.

Cada run guarda el diff completo de los ficheros tocados, la salida de los tests y las métricas del cliente. Los resultados agénticos salen etiquetados por cliente:

dificil | hermes   | gemma4:26b | r1 | PASA | 24s | turnos=11 | out_tok=1059 | in_tok=206229
dificil | opencode | gemma4:26b | r1 | PASA | 21s | turnos=10 | out_tok=519  | in_tok=88468

Los modelos no instalados se marcan NO_INSTALADO en vez de contarse como fallo, y los errores de red aparecen como ERR/E en vez de 0/N: un problema de infraestructura nunca debe leerse como un fallo de calidad del modelo.

El harness NO es determinista. temperature=0 y seed=42 no bastan. En 16 corridas de bench_lib.py gemma4:26b false, sin tocar nada de la configuración: 12 dieron 26/26 y 4 dieron 24/26 (la diferencia está siempre en basic_calculator, 8/8 u 6/8).

Se descartaron por experimento tres explicaciones: modelos residentes en VRAM, num_ctx por defecto y el orden respecto a bench_hard.py. Lo único que correlaciona es la velocidad: las corridas de 24/26 fueron a 126-130 tok/s y las de 26/26 a ~139, lo que apunta a un estado distinto de la GPU. La correlación no es perfecta (una corrida a 126 tok/s dio 26/26), así que no está confirmada como causa.

Consecuencia práctica: una sola pasada no es un resultado. Diferencias de uno o dos casos entre modelos están dentro del ruido. Para comparar de verdad hay que repetir cada medición y dar el rango, no un número. Cuidado especialmente con el patrón "3 corridas iguales, luego es determinista": eso ya llevó a una conclusión equivocada.

El presupuesto de tokens y NO_CONVERGE

Con think:true el razonamiento se descuenta de num_predict, así que un modelo puede agotarlo entero pensando y devolver cadena vacía. El presupuesto con thinking es 16384 tokens (NUM_PREDICT_THINKING en ambos harness), y no es un número arbitrario: en la 3090 un denso de 27B va a ~40 tok/s, o sea unos 7 minutos de reloj para una sola respuesta. Ese es el techo de lo que tiene sentido esperar de un modelo local.

Agotarlo no es un defecto del banco: es un resultado. Los harness lo detectan por done_reason == "length" y lo marcan NO_CONVERGE(>16384tok), declarando el presupuesto usado. Un modelo que necesita más que eso para responder no es utilizable, y el informe lo dice en vez de esconderlo tras un "sube el presupuesto y repite".

El caso que lo ilustra es calc_parentesis en qwen3.8-27b-ud-q4kxl (tanda del 2026-08-24):

think=OFF | calc_parentesis=10/10
think=ON  | calc_parentesis=0/10 | NO_CONVERGE(>16384tok): #0 #1 #2

La misma tarea que resuelve perfecta sin thinking se vuelve un cero con él, agotando el presupuesto las tres veces. No es que le falte capacidad ni presupuesto: es que razonando no converge, y eso es exactamente lo que hay que reportar. laguna-xs-2.1 hace lo mismo en tareas de código, consumiendo 137 000 caracteres sin emitir una línea.

Contraejemplo que conviene tener presente: subir el techo de 8192 a 16384 rescató las tareas t2 y t5 de razonamiento en ese mismo modelo, que pasaron de truncarse a 3/3. Por eso el umbral se subió antes de fijarlo: a 8192 el banco estaba midiendo su propio límite, a 16384 mide al modelo.

La marca solo se pone cuando el modelo agotó el presupuesto sin entregar: si lo agotó pero el código pasa igual, entregó lo que hacía falta y no se etiqueta.

NUM_PREDICT sigue existiendo como variable de entorno para experimentar, pero pisa los dos modos a la vez (ON y OFF), así que cambiarla rompe la comparabilidad de los tiempos del modo OFF con los informes anteriores.

VRAM y contexto en condor

La GPU son 24 GB y el servidor está configurado con una ventana de 49152 tokens (48K). Ese valor es el techo con laguna-xs-2.1 instalado, que es el modelo más grande (20,4 GB):

num_ctx En VRAM Velocidad
49152 (48K) 100 % 178 tok/s
65536 (64K) 98 % (0,4 GB en RAM)
98304 (96K) 90 % (2,0 GB en RAM) 123,7 tok/s
131072 (128K) 84 % (3,4 GB en RAM)

Subir la ventana cuesta un 30 % de velocidad por cada 2 GB que se salen a RAM. Para comprobarlo: carga el modelo y mira size_vram frente a size en /api/ps (o ollama ps); si no coinciden, hay parte en RAM.

No confundas num_ctx con num_predict. El primero es la ventana de contexto, consume VRAM y lo fija el servidor (48K); los harness no lo tocan. El segundo es el tope de tokens a generar en una respuesta, se controla con NUM_PREDICT y no reserva memoria por sí mismo.

Escenarios

Repos Python con bugs sembrados a propósito, documentados en repos-benchmark/BUGS.md y nunca en el propio código: el harness copia el escenario entero, así que anotar el bug donde está equivale a dárselo resuelto.

La línea que lo separa todo es qué se copia al directorio de trabajo:

repos-benchmark/<escenario>/ — lo ve el modelo repos-benchmark/ — no lo ve
el código con los bugs BUGS.md, dónde está cada uno
TASK.txt, el enunciado oculto/<esc>/test_*.py, la batería oculta
REGLAS.md, el contrato de negocio verificadores/<esc>.sh, cómo se le evalúa
test_*.py, los tests visibles

Dos consecuencias de diseño:

  • El contrato vive en REGLAS.md, no en el docstring de la función que lo incumple. Sigue declarado —si no, el escenario mediría adivinación— pero obliga a cruzar dos sitios.
  • Pasar los tests visibles no basta. El verificador corre después una batería que el modelo no ha visto, así que un parche que satisface literalmente el test que falla no cuela. Dos bugs (el umbral de silencioso y la agregación de contextolargo) no tienen ningún test visible: solo constan en REGLAS.md y en la batería oculta.
Escenario Qué exige Qué distingue
dificil/ Tres bugs en cascada en tres ficheros Tres naturalezas distintas: ValueError → magnitud absurda → orden de cálculo equivocado. El tercero solo aparece con descuento, o sea a partir de tres artículos
regresion/ Arreglar slugify sin tocar truncate truncate tiene un comportamiento contraintuitivo pero correcto, y la batería oculta lo cubre a fondo: quien reescribe el fichero entero lo rompe. El test visible usa tres espacios, así que un parche literal no generaliza
silencioso/ Dos bugs, ninguno lanza excepción El de la media está en un fichero que el test no nombra. El del umbral de aprobado no tiene test visible: solo consta en REGLAS.md, y quien arregle lo que falla y pare ahí suspende
testprimero/ Escribir el test que reproduce el fallo y luego arreglarlo El verificador corre los tests del modelo contra el código original; si también pasan ahí, no reproducen nada. Y un parche solo para 45s, el caso que cita el enunciado, no supera la batería oculta
plan/ No se arregla nada: se escribe un plan. Ver abajo Único escenario sin veredicto automático
contextolargo/ Tres bugs en un proyecto de 17 ficheros y 34 tests Dos se manifiestan en la capa alta pero viven en extremos opuestos del proyecto. El tercero es de interacción y no tiene test visible: los informes aplican el IVA al agregado en vez de sumar totales, y con una sola reserva cuadra. Métrica clave: in_tok, que delata al que se lee el repo entero

Se eliminó el escenario facil/ (un off-by-one): los seis modelos lo pasaban, no ordenaba nada.

Los estados posibles son PASA, FALLA, FALLA_TESTS_MODIFICADOS, FALLA_TEST_NO_REPRODUCE (solo en testprimero/) y NO_INSTALADO. Los devuelve el verificador del escenario como código de salida: 0, 1, 3 y 2 respectivamente.

El escenario plan

Los demás escenarios miden si el modelo arregla. Este mide si sabe planificar, que es una capacidad distinta y no correlaciona: en la primera pasada, el modelo con mejor resultado agéntico (10/10) sacó 0/12 planificando, porque no llegó a leer ni un fichero del proyecto.

Se le pide un plan para añadir cancelaciones con reembolso escalonado al proyecto de contextolargo/, con los bugs ya arreglados — planificar sobre código roto mediría otra cosa. El proyecto se monta solo desde el original, así que no hay copias que diverjan.

bash scripts-benchmark/gen_planes.sh          # genera y anonimiza los planes

Cómo se evalúa

No hay tests que pasen o fallen: hay que juzgar un texto. Para que el juicio no sea impresionista:

  1. La rúbrica está cerrada de antemano en repos-benchmark/plan/RUBRICA.md: 12 criterios, la mayoría objetivos (¿menciona esta_libre? sí/no), más tres penalizaciones.
  2. Se puntúa a ciegas. El script deja los planes en ciego_A.txt…, barajados por hash y sin el nombre del modelo. El mapeo va a mapeo_NO_MIRAR.txt: no lo abras hasta haber puntuado.
  3. Verifica cada afirmación contra el código. Los planes citan funciones, atributos y números de línea, y se equivocan: hay que comprobarlo, no darlo por bueno.

Los dos criterios que de verdad separan son las dependencias ocultas: al cancelar una reserva hay que filtrarla en disponibilidad.esta_libre (o la habitación queda bloqueada para siempre) y en informes (o las cifras quedan infladas). Un plan puede cubrir todo lo obvio y seguir siendo peligroso si omite esos dos.

Lo único automático es la comprobación de que el modelo no editó ficheros, ya que se le pidió explícitamente que no lo hiciera. Desobedecerlo es en sí un dato de fiabilidad.