Docker y perfiles¶
Qué vas a aprender¶
- Por qué todo se ejecuta en contenedores y ninguna medida del host vale.
- Los perfiles Compose del banco de backtesting:
dev,bench-apiybench. - Cómo se fija el presupuesto de CPU y memoria (
.env) y por qué afinidad y cuota no son lo mismo. - Los perfiles del runtime causal (API de plataforma y workers) y sus límites de recursos.
Por qué Docker¶
Regla del proyecto
Ninguna medida ni ningún test hecho en el host se acepta. El host no tiene el mismo presupuesto de hilos, ni los mismos límites de memoria, ni necesariamente las mismas versiones (docs/DOCKER.md).
Analogía: si comparas dos coches, lo haces en el mismo circuito, con el mismo combustible y el mismo cronómetro. El contenedor es el circuito: fija versiones, hilos, memoria y datos montados.
La imagen del banco¶
Una sola imagen, finaz/bench, con los dos motores instalados: la paridad exige
tener ambos resultados sobre los mismos arrays en el mismo proceso.
| Etapa | Base | Para qué |
|---|---|---|
rust-toolchain |
rust:1-slim-bookworm |
Transporta rustup/cargo |
builder-rust |
python:3.12-slim-bookworm + toolchain |
Compila VectorTA v0.2.8 con AVX2 (solo para runtime-avx2) |
runtime |
python:3.12-slim-bookworm + uv |
Imagen por defecto (uv.lock --frozen) |
runtime-avx2 |
runtime + wheel propio |
Tag finaz/bench:0.2.8-avx2-x86-64-v3 |
Nunca target-cpu=native
Ataría la imagen a la máquina que la construyó y provocaría SIGILL al moverla. Se
usa x86-64-v3 (AVX2). Y el resultado medido: el AVX2 de este crate no acelera
estas rutas (single ≈1,00; batch 0,87–1,06).
Perfiles del banco de backtesting¶
Un docker compose up sin perfil no arranca nada nuevo: es deliberado, para no
interferir con los servicios heredados (:8001, :8002).
flowchart LR
subgraph P["docker-compose.yml"]
DEV["perfil dev<br/>tests, doctor, shell"]
API["perfil bench-api<br/>FastAPI :8000"]
BEN["perfil bench<br/>campañas one-shot"]
HER["sin perfil<br/>vectorta-service :8001<br/>nautilus-service :8002"]
end
Monta el repo en /srv/bench; editar código no requiere rebuild.
Servicio HTTP en :8000 (Swagger en /docs). Sin --reload y con un solo
worker: el proceso que sirve es también el que mide.
Cuidado con docker compose down sin argumentos
Pararía también los servicios heredados. Nombra siempre el servicio.
Volúmenes¶
| Host | Contenedor | Modo | Contenido |
|---|---|---|---|
./data |
/data |
ro | Parquet canónico (un benchmark no modifica sus datos) |
./cache |
/cache |
rw | Features y checkpoints por hash, caché JIT de numba |
./results |
/results |
rw | Tablas de resultados |
./runs |
/runs |
rw | Artefactos por run_id; runs/env/ sí se versiona |
Presupuesto de CPU y memoria (.env)¶
Se copia .env.example a .env (el .env no se versiona):
| Variable | Defecto | Dimensión |
|---|---|---|
BENCH_CPUSET |
0-3 |
Afinidad: en qué hilos lógicos puede correr |
BENCH_CPUS |
4 |
Cuota: cuánta CPU consume en total |
BENCH_MEM_LIMIT |
4g |
Límite de memoria |
BENCH_WORKERS |
2 |
Procesos worker |
BENCH_IMAGE |
finaz/bench:0.2.0-x86-64-v3 |
Imagen |
Afinidad ≠ cuota
Un contenedor con cpuset de 4 hilos y cuota de 1 CPU se mueve entre esos 4 hilos pero solo consume el equivalente a uno. Hay que fijar y documentar ambas.
El paralelismo va en una sola capa: o pool de procesos, o hilos internos, nunca
ambos. Por eso la imagen fija a 1 POLARS_MAX_THREADS, RAYON_NUM_THREADS,
OMP_NUM_THREADS, OPENBLAS_NUM_THREADS, MKL_NUM_THREADS y NUMBA_NUM_THREADS.
(Medido: el paralelismo interno de rayon pierde 2,2–3,7× frente a procesos.)
Comprobar que los límites se aplican de verdad:
docker compose --profile dev run --rm dev python tools/doctor.py | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d['cgroup_limits'])"
Las medidas no viajan
Cada resultado lleva host_fingerprint; solo se comparan speedups entre corridas con
la misma huella. Entre máquinas, las series se publican por separado.
Perfiles del runtime causal¶
Proyecto Compose finaz-trading-engine, fichero deployment/v2/compose.yaml:
| Perfil | Servicio | Tipo | CPU / memoria |
|---|---|---|---|
| base | api |
servicio | 1 CPU / 1 GiB |
batch |
worker-batch |
servicio | 4 CPU / 8 GiB |
stream |
worker-stream |
servicio | 2 CPU / 4 GiB |
replay-job |
replay |
job one-shot (run --rm) |
— |
quant, opt, neural, chronos, timesfm25 |
job-* |
jobs S7 (restart: "no") |
— |
DC="docker compose -p finaz-trading-engine -f deployment/v2/compose.yaml"
$DC config -q
$DC up -d # base: api
$DC --profile batch up -d # + worker-batch
$DC --profile stream up -d # + worker-stream
$DC --profile replay-job run --rm replay
Las imágenes del runtime son CPU, base python:3.12-slim pineada por digest, usuario finaz
uid 10000, read_only y cap_drop: ALL (ver Seguridad).
Resumen¶
- Todo en contenedores; las medidas del host no valen y las de otra máquina no se mezclan.
- Banco de backtesting:
dev(trabajo),bench-api(API de backtesting y análisis, :8000),bench(campañas); sin perfil no arranca nada nuevo. .envfija afinidad (BENCH_CPUSET), cuota (BENCH_CPUS) y memoria; paralelismo en una sola capa.- Runtime causal: proyecto
finaz-trading-engine, perfiles base/batch/stream/replay-job y jobs S7.
Para practicar¶
- Escribe el comando para arrancar solo la API de backtesting y análisis y comprobar su salud.
- En un servidor de 24 hilos, propón
BENCH_CPUSETyBENCH_CPUSpara un barrido que no moleste a la API. - ¿Por qué
bench-apino usa--reload?