Skip to content

Commit 9bedf2b

Browse files
polinabinder1claude
andcommitted
evo2 SAE: inference engine + steering, CI lane, and tests (on migrated layout)
Re-lands #1622 onto the post-#1633 top-level layout (interpretability/sparse_autoencoders/). A prior `merge main` had orphaned all of #1622's added files at the old bionemo-recipes/ path while the rest of the tree migrated; this replays the net delta at the correct paths. - Evo2SAE inference engine + steering hook (src/evo2_sae/, sae/src/sae/steering.py) - dedicated path-gated GPU CI lane + .ci_build.sh (reuses evo2_megatron's build, no fork) + .ci_test_env.sh - conftest 1B-load fixture (bionemo_load -> run_nemo2_to_mbridge + synth tiny SAE, memory-gated) - engine unit tests (test_core.py, CPU) + steering/encode GPU tests (test_steering.py) Validated on the 1B: test_core 7 passed (CPU), test_steering 9 passed (GPU). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Polina Binder <pbinder@nvidia.com>
1 parent d52b0b9 commit 9bedf2b

12 files changed

Lines changed: 1158 additions & 5 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: "BioNeMo Interpretability Recipes CI"
2+
3+
# CI for the SAE interpretability recipes (interpretability/sparse_autoencoders).
4+
# Uses the same squashed megatron image + L4 GPU as the mbridge lane, but the interpretability
5+
# tree is a single uv workspace (not N *megatron recipes), so there is no per-recipe matrix —
6+
# just a path-gated GPU job. Currently scoped to the evo2 recipe, whose engine builds on the
7+
# mbridge bionemo.evo2 recipe, so the job also checks out recipes/evo2_megatron (which pulls its
8+
# bionemo-core / bionemo-recipeutils deps from git).
9+
10+
on:
11+
push:
12+
branches:
13+
- "pull-request/[0-9]+"
14+
- "dependabot/**"
15+
paths:
16+
- "interpretability/sparse_autoencoders/**"
17+
- "recipes/evo2_megatron/**" # engine builds on bionemo.evo2
18+
- ".github/workflows/unit-tests-interpretability-recipes.yaml"
19+
merge_group:
20+
types: [checks_requested]
21+
schedule:
22+
- cron: "0 9 * * *" # Runs at 9 AM UTC daily (2 AM MST)
23+
24+
defaults:
25+
run:
26+
shell: bash -x -e -u -o pipefail {0}
27+
28+
concurrency:
29+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
unit-tests:
34+
runs-on: linux-amd64-gpu-l4-latest-1
35+
name: "interpretability-unit-tests (evo2)"
36+
container:
37+
image: svcbionemo023/bionemo-framework:pytorch26.04-py3-squashed
38+
options: --shm-size=16G
39+
env:
40+
CI: true
41+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
42+
HF_HOME: /cache/huggingface
43+
44+
steps:
45+
- name: Show GPU info
46+
run: nvidia-smi
47+
- name: Setup proxy cache
48+
uses: nv-gha-runners/setup-proxy-cache@main
49+
50+
- name: Checkout repository
51+
uses: actions/checkout@v4
52+
with:
53+
# The evo2 recipe builds on bionemo.evo2 (recipes/evo2_megatron); evo2_megatron pulls
54+
# its bionemo-core / bionemo-recipeutils deps from git, so only these two are needed.
55+
sparse-checkout: |
56+
interpretability/sparse_autoencoders
57+
recipes/evo2_megatron
58+
sparse-checkout-cone-mode: false
59+
60+
- name: Cache Hugging Face models
61+
uses: actions/cache@v4
62+
with:
63+
path: /cache/huggingface
64+
key: ${{ runner.os }}-huggingface-interp-evo2-${{ github.sha }}
65+
restore-keys: |
66+
${{ runner.os }}-huggingface-interp-evo2-
67+
${{ runner.os }}-huggingface-
68+
69+
- name: Install dependencies
70+
working-directory: interpretability/sparse_autoencoders/recipes/evo2
71+
run: bash .ci_build.sh
72+
73+
- name: Run tests
74+
working-directory: interpretability/sparse_autoencoders/recipes/evo2
75+
run: |
76+
source .ci_test_env.sh
77+
pytest -v tests/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
# Build the Evo2-SAE recipe environment for CI.
3+
#
4+
# The inference engine imports `bionemo.evo2` — which lives ONLY in the mbridge
5+
# `recipes/evo2_megatron` recipe (there is no prebaked image that ships it). So we build
6+
# that environment with evo2_megatron's OWN CI build script, inheriting its pinned
7+
# megatron-bridge / causal-conv1d / TransformerEngine handling verbatim (no fork that
8+
# could drift), then install the SAE library + this recipe on top of the same venv.
9+
#
10+
# The CI checkout must also provide the sibling recipe (recipes/evo2_megatron); it pulls its
11+
# own bionemo-core / bionemo-recipeutils deps from git, so nothing else is required locally.
12+
set -euo pipefail
13+
14+
HERE="$(cd "$(dirname "$0")" && pwd)"
15+
SAE_LIB="$HERE/../../sae" # sparse_autoencoders/sae
16+
MEGATRON="$HERE/../../../../recipes/evo2_megatron"
17+
18+
# Fail loudly if the sibling recipe isn't checked out, rather than emitting a cryptic path
19+
# error deep inside the build. In CI this means the workflow's sparse-checkout MUST include
20+
# recipes/evo2_megatron.
21+
if [[ ! -f "$MEGATRON/.ci_build.sh" ]]; then
22+
echo "ERROR: evo2_megatron not found at $MEGATRON" >&2
23+
echo " evo2-sae builds on the mbridge bionemo.evo2 recipe; ensure the checkout" >&2
24+
echo " includes recipes/evo2_megatron." >&2
25+
exit 1
26+
fi
27+
28+
# 1. Build the bionemo.evo2 (mbridge) environment. Creates $MEGATRON/.venv with the
29+
# system-site torch/TE plus the full megatron stack.
30+
( cd "$MEGATRON" && bash .ci_build.sh )
31+
32+
# 2. Add the generic SAE library + this recipe into that venv. PIP_CONSTRAINT= clears the
33+
# TE pin constraint evo2_megatron sets (it must not block our pure-Python installs).
34+
source "$MEGATRON/.venv/bin/activate"
35+
PIP_CONSTRAINT= pip install -e "$SAE_LIB"
36+
PIP_CONSTRAINT= pip install -e "$HERE"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Sourced by CI before running pytest (mirrors evo2_megatron/.ci_test_env.sh).
2+
# The recipe shares evo2_megatron's venv (see .ci_build.sh), which lives in the sibling
3+
# recipe dir rather than here.
4+
source ../../../../recipes/evo2_megatron/.venv/bin/activate

interpretability/sparse_autoencoders/recipes/evo2/pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ dependencies = [
1515
"pyarrow>=23.0.0",
1616
]
1717

18-
# No package code lives here yet — the recipe is just an entry-point for
19-
# scripts/ that depends on the shared `sae` workspace package. Declare no
20-
# packages so setuptools doesn't try to discover anything.
21-
[tool.setuptools]
22-
packages = []
18+
# The `evo2_sae` package (src/) holds the live inference engine + steering hook;
19+
# scripts/ (extract, train) are standalone entry points alongside it. The FastAPI
20+
# server + CLI (and their fastapi/uvicorn deps) are added by the serve PR (#1637).
21+
[tool.setuptools.packages.find]
22+
where = ["src"]
2323

2424
[tool.uv.sources]
2525
sae = { workspace = true }
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: LicenseRef-Apache2
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Evo2 + SAE inference engine — reused by the live server, the batch CLI, and the viz backend."""
17+
18+
from typing import TYPE_CHECKING
19+
20+
21+
if TYPE_CHECKING: # for type checkers / ruff F822 — runtime access goes through __getattr__ below
22+
from .core import DEFAULT_ORGANISM_TAGS, Evo2SAE, clean_dna
23+
24+
25+
__all__ = ["DEFAULT_ORGANISM_TAGS", "Evo2SAE", "clean_dna"]
26+
27+
28+
def __getattr__(name: str):
29+
"""Lazily pull the heavy engine symbols from ``.core`` (importing ``.core`` loads torch).
30+
31+
Keeps ``import evo2_sae`` (and lightweight submodules like ``evo2_sae.fasta``) cheap so
32+
callers that only need the helpers don't drag in torch, while ``from evo2_sae import
33+
Evo2SAE`` still works.
34+
"""
35+
if name in __all__:
36+
from . import core
37+
38+
return getattr(core, name)
39+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

0 commit comments

Comments
 (0)