Skip to content

Commit dbd1792

Browse files
polinabinder1claude
andcommitted
evo2-sae dashboard: single-container serve (static frontend baked into image)
Make the dashboard + API deployable as ONE container so a public-repo user (or coworker) can launch it with a single 'docker run', no Node and no second process: * Dockerfile: add a Node build stage that compiles feature_explorer -> static dist/, COPY it into the runtime image, and set DASHBOARD_DIST so server.py serves it at / (Node lives only in the build stage, never in the runtime GPU image). * vite.config.js: drop the /api rewrite — proxy /api straight through to :8001/api. Dev now hits the same paths as production (the server serves the API under /api), so there's no dev/prod drift. * move the /gene_embed endpoint onto the /api router (rebased onto the new server.py); frontend already calls ${BACKEND}/gene_embed (= /api/gene_embed), so no frontend code change. * re-point the gene_embed contract test to /api/gene_embed. Dev mode (vite + launch_dashboard.py) is unchanged for UI iteration; the baked-in static build is the deploy path. Frontend compile is exercised by the image build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Polina Binder <pbinder@nvidia.com>
1 parent d0e4d4c commit dbd1792

3 files changed

Lines changed: 32 additions & 6 deletions

File tree

interpretability/sparse_autoencoders/recipes/evo2/Dockerfile

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,31 @@
44
# (the mbridge evo2_megatron recipe), so .ci_build.sh delegates to evo2_megatron's OWN build
55
# (inheriting its pinned megatron-bridge / causal-conv1d / TransformerEngine versions), then
66
# installs the sae library + this recipe on top. We don't reimplement the megatron build here.
7+
# A Node build stage compiles the dashboard to static files baked into the image, so ONE
8+
# container serves both the UI (at /) and the API (at /api) — no Node at runtime.
79
#
810
# Build from the REPO ROOT (the context must include the recipes/evo2_megatron sibling):
911
# docker build -f interpretability/sparse_autoencoders/recipes/evo2/Dockerfile -t evo2-sae .
10-
# Run (needs a GPU + checkpoints; see the recipe README):
12+
# Serve the dashboard + API on one port (needs a GPU + checkpoints; see the recipe README):
13+
# docker run --gpus all -p 8001:8001 \
14+
# -e EVO2_CKPT_DIR=/ckpt/evo2 -e SAE_CKPT_PATH=/ckpt/sae.pt -e EMBEDDING_LAYER=26 \
15+
# -v /my/checkpoints:/ckpt evo2-sae scripts/launch_inference.sh serve
16+
# # then open http://localhost:8001
17+
# Or run the tests:
1118
# docker run --gpus all -it evo2-sae bash -lc "source .ci_test_env.sh && pytest tests/"
19+
20+
# Stage 1 — build the dashboard frontend to static files. Node lives ONLY in this stage; the
21+
# runtime image below gets just the compiled dist/, never npm/vite.
22+
FROM node:20-slim AS frontend
23+
WORKDIR /frontend
24+
# Copy manifests first so `npm ci` is cached unless dependencies change.
25+
COPY interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/package.json \
26+
interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/package-lock.json ./
27+
RUN npm ci
28+
COPY interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/ ./
29+
RUN npm run build # -> /frontend/dist
30+
31+
# Stage 2 — the runnable GPU image.
1232
ARG BASE_IMAGE=nvcr.io/nvidia/pytorch:26.04-py3
1333
FROM ${BASE_IMAGE}
1434

@@ -31,6 +51,12 @@ COPY interpretability/sparse_autoencoders interpretability/sparse_autoencoders
3151
WORKDIR /workspace/interpretability/sparse_autoencoders/recipes/evo2
3252
RUN bash .ci_build.sh install
3353

54+
# 3. Bake in the prebuilt dashboard and point the server at it (DASHBOARD_DIST), so the one
55+
# container serves the UI at / and the API at /api. Only this layer changes on a frontend edit.
56+
COPY --from=frontend /frontend/dist \
57+
/workspace/interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/dist
58+
ENV DASHBOARD_DIST=/workspace/interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/dist
59+
3460
# Default to the built venv so `docker run … pytest tests/` (or importing Evo2SAE) just works.
3561
ENV VIRTUAL_ENV=/workspace/recipes/evo2_megatron/.venv
3662
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

interpretability/sparse_autoencoders/recipes/evo2/feature_explorer/vite.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ export default defineConfig({
1111
host: '0.0.0.0',
1212
port: 5176,
1313
strictPort: true,
14-
// The live backend (server.py) runs on :8001. Proxying it under
15-
// /api means only the Vite port needs to be tunneled — the browser talks
16-
// to Vite, Vite talks to the backend, both on the pod.
14+
// The live backend (server.py) runs on :8001 and serves the API under /api. Proxy /api
15+
// straight through (NO rewrite) so dev hits the same paths as production: in dev the browser
16+
// talks to Vite and Vite forwards /api/* to :8001/api/*; in the single-container build the
17+
// browser hits /api/* on the backend directly. Same frontend code, identical paths both ways.
1718
proxy: {
1819
'/api': {
1920
target: 'http://localhost:8001',
2021
changeOrigin: true,
21-
rewrite: (path) => path.replace(/^\/api/, ''),
2222
},
2323
},
2424
},

interpretability/sparse_autoencoders/recipes/evo2/tests/test_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def test_gene_embed_returns_decodable_matrix(client):
122122
import numpy as np
123123

124124
genes = [{"symbol": "g1", "sequence": "ACGTACGT"}, {"symbol": "g2", "sequence": "TTTTGGGG"}]
125-
b = client.post("/gene_embed", json={"genes": genes, "min_firing": 1}).json()
125+
b = client.post("/api/gene_embed", json={"genes": genes, "min_firing": 1}).json()
126126
assert {"G_b64", "Gmax_b64", "n_features", "n_genes", "genes", "feature_ids"} <= set(b)
127127
assert b["n_genes"] == 2 and len(b["genes"]) == 2
128128
# only firing columns are shipped: feature_ids maps column -> real SAE feature id

0 commit comments

Comments
 (0)