Saltar a contenido

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-api y bench.
  • 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.

docker compose --profile dev build dev
docker compose --profile dev run --rm dev pytest -q
docker compose --profile dev run --rm dev python tools/doctor.py \
    --probe-imports --probe-vectorta-kernels \
    --output runs/env/doctor_bench_dev.json

Servicio HTTP en :8000 (Swagger en /docs). Sin --reload y con un solo worker: el proceso que sirve es también el que mide.

docker compose --profile bench-api up -d bench-api
curl -s localhost:8000/health
docker compose --profile bench-api down bench-api   # parar SOLO este servicio

One-shot para campañas de benchmarks (exigen ventana silenciosa: ningún otro contenedor ejecutando).

docker compose --profile bench run --rm bench campaign --plan smoke   # ~1 min
docker compose --profile bench run --rm bench campaign --plan full    # ~3,5 h CPU

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.
  • .env fija 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

  1. Escribe el comando para arrancar solo la API de backtesting y análisis y comprobar su salud.
  2. En un servidor de 24 hilos, propón BENCH_CPUSET y BENCH_CPUS para un barrido que no moleste a la API.
  3. ¿Por qué bench-api no usa --reload?