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¶
- Lanza la campaña
smokey localiza ensamples.parquetlas columnasorder_seedyposition_in_pairde una pareja. - ¿Por qué la fila
B01 sma vectorta/scalar 10.000.000con PARITY_FAIL no puede entrar en un speedup aunque su tiempo sea correcto? - 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.
- Enumera tres formas de contaminar una campaña y qué regla del arnés protege de cada una.