Saltar a contenido

Benchmarks: medir sin engañarse

Medir el tiempo de un programa es fácil; medirlo de forma que la cifra signifique algo es difícil. Otro contenedor que roba CPU, una caché caliente, un orden de ejecución que favorece a uno de los dos, un P95 calculado con cinco muestras… Este capítulo explica el arnés B00–B06, las reglas que convierten un cronómetro en una medida publicable y lo que se aprendió en la campaña completa y en la optimización WP-08b.

Qué vas a aprender

  • Los escenarios B00–B06 y qué pregunta responde cada uno.
  • Las piezas del arnés: ScenarioCase, Sample, plan antes de medir, orden AB/BA, RSS y cgroup, ventana silenciosa.
  • Las nueve puertas que debe pasar un speedup para publicarse.
  • Los resultados de la campaña full-20260917T074854 (233 casos, 2.425 muestras, ~3 h 38 min).
  • Las conclusiones de WP-08b: AVX2 no acelera, rayon pierde ante procesos, ledger en Rust innecesario.
  • Cómo lanzar una campaña.

1. Los escenarios

Id Propósito Qué varía (según catalog/benchmark_plan.json)
B00 Conformidad N = 10, 100, 1000 con el oráculo
B01 Primitivas N = 10⁴, 10⁵, 10⁶, 10⁷; modos batch y streaming; proveedores canónico, VectorTA escalar/batch, Nautilus
B02 Un activo, pipeline completo N = 10⁴, 10⁵, 10⁶ (10⁷ opcional); P01, P02, P04, P06, P11 × adaptadores
B03 Cambio de activo 10 activos pedidos; controles equal_n y same_dates_real_unequal_n
B04 Ventana de fechas fracciones 0,25 / 0,5 / 1,0 y extensión 0,1; reset_flat frente a continue
B05 Marco temporal 1m, 5m, 1h, 1d; controles de fechas fijas y N fijo sintético
B06 Optimización de parámetros K = 10, 100, 1000, 10.000 candidatos distintos, lista persistida antes de ejecutar

(B07–B10 existen en el plan; de ellos, H4 cerró el walk-forward B10 de Q03.)


2. El arnés: reglas que protegen la cifra

flowchart TD
    P[plan.json<br/>todas las ScenarioCase<br/>escritas ANTES de medir] --> O[orden AB/BA<br/>semilla 1729]
    O --> W[3 calentamientos<br/>+ 20 medidas pequeños<br/>0 + 5 grandes]
    W --> S[samples.parquet<br/>reescrito muestra a muestra]
    S --> R[report.md<br/>mediana, IQR, P95 fiable sí/no]
    R --> G{9 puertas}
    G -- todas OK --> PUB[speedup publicado]
    G -- alguna falla --> NO[value = None]
Regla Por qué
Plan antes de medir (plan.json atómico) Una campaña sin plan no es reanudable ni auditable; nadie puede quitar casos incómodos a posteriori.
samples.parquet muestra a muestra Un corte deja siempre un Parquet completo con lo medido. Un caso a medias se re-mide entero.
Orden AB/BA sembrado (semilla 1729) Alterna quién va primero en cada pareja: la deriva térmica o de carga no favorece a nadie.
Repeticiones 3 calentamientos + 20 medidas en casos pequeños; 5 en grandes. P95 fiable solo con ≥ 20 muestras.
Arranque aparte container_startup_ns se mide por fuera y nunca se suma al wall_ns (mediana medida: 574,8 ms).
Memoria RSS del proceso y pico del cgroup; en modo en proceso el RSS es cota superior declarada.
Ventana silenciosa Ningún otro contenedor ejecutando. Si se contamina, se repite entera.
Huella de máquina Solo se comparan medidas con la misma host_fingerprint (x86_64|Linux|py3.12.14).

ScenarioCase (en finazbench/bench/scenarios.py) describe un caso: escenario, estrategia, datos, N, ruta, cache_mode (COLD_PROCESS_FULL, WARM_DATA, INTENT_REPLAY…) y adaptador. Cada Sample lleva su order_seed y su position_in_pair.

Modos de caché

Implementados: COLD_PROCESS_FULL (un proceso hijo por muestra), WARM_DATA (carga cacheada en proceso) e INTENT_REPLAY. Declarados y no implementados: WARM_FEATURES, OPT_FULL, APPEND_CONTINUE (no se aproximan).

2.1 Las nueve puertas de un speedup

Un cociente T_A / T_B solo se publica si todas pasan:

# Puerta
1 parity = PASS
2 mismo data_hash
3 misma formula_version
4 misma lista de candidatos
5 mismo contrato de ejecución
6 misma política de capital
7 mismo cache_mode
8 mismo output_mode
9 misma host_fingerprint

Un speedup sin paridad no es orientativo: es nada

Si una puerta falla, published=False y value=None. Una clave ausente tampoco pasa: no haberla comparado no es haberla encontrado igual.


3. La campaña full-20260917T074854

Dato Valor
Plan full, 233 casos
Muestras 2.425 (filas de samples.parquet; n_samples en campaign.json)
Duración acumulada 13.060,7 s (~3 h 38 min de tiempo de pared observado, elapsed_ns de campaign.json, sumando 4 ejecuciones)
Semilla de orden 1729
Imagen finaz/bench:0.2.0-x86-64-v3
data_hash e8b5fc3612fd944a692af21807706f665fb5bec4f1251d1652fbeaabea6595b0

3.1 Primitivas (B01): un vistazo

Mediana de pared en ms, SYNTH 1min, COLD_PROCESS_FULL:

Primitiva N canónico VectorTA escalar Nautilus
ema 1.000.000 551,4 290,5 881,4 (PARITY_FAIL: semilla)
rsi 1.000.000 4.240,7 2.186,4 2.800,3 (PARITY_FAIL)
sma 1.000.000 44,6 35,2 2.100,3 (PASS)
donchian 1.000.000 2.667,7 1.466,7 2.876,8 (PARITY_FAIL: definición)

Las filas de Nautilus con PARITY_FAIL tienen tiempo pero no speedup: miden otra fórmula.

3.2 Speedups publicados (B02, median_wall_nautilus / median_wall_vta_pipeline)

NVDA 1min, COLD_PROCESS_FULL, todas las puertas OK:

Estrategia N NT_FEATURES NT_ONLINE
P01 10.000 6,3× 6,6×
P01 100.000 18,4× 22,2×
P01 631.735 66,7× 75,4×
P04 631.735 122,6× 124,8×
P06 631.735 37,9× 45,8×
P11 631.735 92,4× 106,8×

Con el analizador de cartera de Nautilus activo (variante full, D-07), P01 en 631.735 barras (NT_ONLINE) llega a 616,3×: por eso se publican las dos cifras por separado.

Cómo leer «66,7×»

Para P01 sobre la serie completa de NVDA 1min, el pipeline VectorTA + ledger propio tarda del orden de medio segundo y NT_FEATURES ~67 veces más, produciendo exactamente los mismos fills. No dice que Nautilus sea «malo»: mide el coste de simular evento a evento lo que el pipeline vectorizado calcula en bloque.

3.3 Declaraciones (no se aproxima lo no implementado)

Escenario Declaración Motivo
B01 RESOURCE_LIMIT algunos 10M sintéticos no caben en BENCH_MEM_LIMIT × 0,65
B03 ASSET_COUNT_LIMITED el plan pide 10 activos; el canónico de fase A tiene NVDA y KO
B04 CONTINUE_NOT_IMPLEMENTED no hay checkpoint causal; solo reset_flat
B06 INSUFFICIENT_DISTINCT_CANDIDATES la rejilla de P01 da 16 distintos (1.000 la ampliada); repetir candidatos para decir 10.000 está prohibido
B02–B06 CACHE_MODES_NOT_IMPLEMENTED tres modos de caché declarados sin implementar

4. WP-08b: optimización medida

Tres preguntas, tres respuestas con cifras (results/bench/wp08b/report.md).

4.1 ¿Acelera el AVX2 del build propio de VectorTA 0.2.8?

No. Sonda ABBA en el mismo proceso, 1.000.000 de barras, 3 + 20 medidas:

Tipo Speedup AVX2 / escalar
single (sma, ema, rma, rsi, stddev, atr, bollinger) 0,996 – 1,027 (≈ 1,00)
batch_grid (32 periodos) 0,865 – 1,056

Todos los digest cruzados son idénticos: los dos kernels producen exactamente los mismos números. Las mejoras aparentes de las tablas de campaña (hasta 2,3× en rsi_grid) aparecen también con el wheel 0.2.8 escalar: son del wheel, no del AVX2.

4.2 ¿Paralelismo interno (rayon) o procesos?

Mismo presupuesto de 4 núcleos: inner = 1 proceso con RAYON_NUM_THREADS=4; outer = 4 procesos con 1 hilo. Barrido sma de 191 periodos sobre 200.000 barras.

Kernel / carga outer / inner Lectura
avx2, 4 barridos 0,425 procesos ~2,35× más rápidos
avx2, 8 barridos 0,451 ~2,2×
scalar, 4 barridos 0,270 ~3,7×

Rayon pierde ante procesos por 2,2–3,7×. Para barridos, reparte candidatos entre procesos.

4.3 ¿Hace falta Nautilus por lotes o un ledger en Rust?

  • Nautilus por lotes (catálogo Parquet): NO APLICA. La memoria está cerca del tope de 2 GiB pero el caso termina sin OOM; el eje que limita es el tiempo (dos órdenes de magnitud).
  • Ledger en Rust: NO HACE FALTA. simulate_ns = 12,6 ms para 12.576 fills (P01), el 2,6 % del pipeline de 487 ms.

5. Cómo lanzar una campaña

cp .env.example .env     # BENCH_CPUSET, BENCH_CPUS, BENCH_MEM_LIMIT

# smoke: ~1 min, produce los cuatro artefactos
docker compose --profile bench run --rm bench campaign --plan smoke

# full: ~3,5 h de CPU, exige ventana silenciosa
docker compose --profile bench run --rm bench campaign --plan full

Salidas en results/bench/<campaign_id>/: plan.json, samples.parquet, report.md y el resumen. Si la campaña se corta, al relanzarla se leen plan.json y samples.parquet y se saltan los casos completos.

Antes de publicar una cifra

Comprueba: misma host_fingerprint, mismo data_hash, paridad PASS, P95 marcado como fiable solo con ≥ 20 muestras, y ventana silenciosa declarada. WP-08b demostró que fijar cpuset (p. ej. BENCH_CPUSET=4-11) separa mesetas, pero hay que declararlo en las condiciones.


Resumen

  • B00–B06 cubren conformidad, primitivas, pipeline, activo, ventana, marco y optimización.
  • El arnés escribe el plan antes de medir, alterna AB/BA con semilla 1729, separa el arranque y exige ventana silenciosa.
  • Un speedup se publica solo si pasan nueve puertas, empezando por la paridad.
  • Campaña full: 233 casos, 2.425 muestras, ~3 h 38 min; Nautilus es entre ~6× y ~125× más lento que el pipeline vectorizado en B02 con los mismos fills.
  • WP-08b: AVX2 ≈ 1,00; rayon pierde 2,2–3,7× ante procesos; ledger Rust innecesario.

Para practicar

  1. Lanza la campaña smoke y localiza en samples.parquet las columnas order_seed y position_in_pair de una pareja.
  2. ¿Por qué la fila B01 sma vectorta/scalar 10.000.000 con PARITY_FAIL no puede entrar en un speedup aunque su tiempo sea correcto?
  3. Con los datos de §4.2, calcula cuánto tardaría un barrido de 1.000 candidatos repartido en 4 procesos frente a 4 hilos de rayon, suponiendo que escala linealmente.
  4. Enumera tres formas de contaminar una campaña y qué regla del arnés protege de cada una.