Skip to content

feat: local MMseqs2 features for AlphaFold 2, with insertions for both backends - #642

Merged
DimaMolod merged 17 commits into
mainfrom
feat/af2-local-mmseqs
Sep 14, 2026
Merged

DimaMolod merged 17 commits into
mainfrom
feat/af2-local-mmseqs

Conversation

@DimaMolod

@DimaMolod DimaMolod commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Local MMseqs2 features for AlphaFold 2 — and insertions for both backends

The local MMseqs2 feature path (GPU or CPU search, then CPU finalization) was AlphaFold 3 only. The search stage is backend-neutral — one MSA bundle per chain — so AlphaFold 2 support is a second finalizer that turns a bundle into the MonomericObject pickle AlphaFold 2 inference reads. finalize_batch_features.py --data_pipeline=alphafold2 selects it. --use_mmseqs2 keeps meaning the remote ColabFold API.

A defect this fixes for AlphaFold 3 as well

Every local MMseqs2 alignment had an all-zero deletion matrix. result2msa --msa-format-mode 2, the only format the stage ran, keeps full database headers but drops every column the query does not span, so no insertion ever reached AlphaFold. Measured against a real 586-residue query: 98/120 small BFD hits (81.7%) and 712/824 UniProt hits (86.4%) carry insertions — 13 035 residues on UniProt alone. The code assumed mode 2 delivered insertions as query-gap columns and converted them; the pinned build never emits one, so that path never ran on real data.

Mode 5 keeps insertions but cuts the header to the accession (sp|P83570|GWA_SEPOF … → P83570), and AlphaFold reads the species it pairs chains by from the long form. So each search result is now formatted both ways and joined row by row — and the join is verified, not trusted: stripping insertions from every mode-5 row must reproduce its mode-2 row, and a disagreement fails the request rather than labelling one hit with another's header. Cost: ~16 ms per hit (2 s against a 947 s search on small BFD; 13 s against 3739 s on UniProt).

This also removes the search stage's last AlphaFold 3 dependency (alphafold3.cpp.msa_conversion), which the AlphaFold 2 image lacks: the stage imported there and died on its first result.

Bundle provenance moves, so no bundle written before this is reused.

What the AlphaFold 2 finalizer reproduces from native AlphaFold 2

  • per-database caps counting the query (uniref90 10 000, MGnify 501), merge order UniRef90 → BFD → MGnify
  • templates searched from UniRef90 alone (bundles now record each database's row span, so this is possible), with hmmsearch or --use_hhsearch
  • pairing features from the independent UniProt search, species parsed from its headers — not a copy of the unpaired features, which is what the remote path uses
  • accession identifiers filled offline from the headers; nothing on this path queries UniProt

The recipe is AlphaFold 2's reduced_dbs set (no BFD/UniRef30 HHblits arm); the docs say so and say where it cannot be exact.

Cache and search correctness

  • Raising num_iterations failed every RNA shard. Iterative profile search is protein-only; on the pinned build --num-iterations 3 on a nucleotide search exits 1. It now reaches protein searches only. RNA provenance never recorded it, so no cached RNA MSA changes identity. The real-binary RNA contract test now runs at 1 and 3 iterations, and fails on the previous code.

  • A bundle AlphaFold 2 cannot slice was never rebuilt. A bundle whose per-database row spans are missing, name missing or duplicate databases, or don't add up was reported as a failure and left in place, where the workflow's shard completion kept validating it, so every retry failed the same way. It is now deleted like any other damaged bundle, which schedules a repair.

  • HHsearch feature reuse now includes PDB70 identity. --use_hhsearch requires --template_pdb70_database_id instead of the unused seqres identity. A changed PDB70 build rebuilds features while reusing MSA bundles; unchanged IDs reuse features, and unused databases do not invalidate them. The companion workflow PR forwards and keys the same selected identity. Operators must change the immutable ID when rebuilding a database, including at the same path. HMMsearch and AF3 retain their cache identities.

  • Invalid bundles are removed at the shared reader. Complete, unique database roles and a protein pairing alignment are checked before a finalizer consumes a bundle. RNA retains its valid empty paired alignment. A downstream template-search failure preserves valid MSA data.

Also

  • --mmseqs_db_load_mode (memory only, deliberately outside the cache identity)
  • compare_msa_backends.py --artifact_format=af2_pickle
  • the template stack is built from explicit settings, and the legacy script delegates to the same builder

Tests

Current gate on 6c53a24a, run in an isolated worktree on a compute node (SLURM 61831784, 2026-09-14):

Suite Passed Skipped / deselected
unit, AlphaFold 2 environment 589 10 skipped
unit, AlphaFold 3 environment 641 1 skipped
integration, AlphaFold 2 environment 107 27 deselected
integration, AlphaFold 3 environment 107 27 deselected
real MMseqs2 binary (-m external_tools) 14 0

Unit-only coverage was compared against the PR base: all 38 new executable functions are exercised, including nested helpers. The protocol method declaration has no implementation and is excluded. New tests cover both template factories, resolved settings, metadata selection, AF2/AF3 CLI dispatch and validation, PDB70 invalidation/reuse, and malformed-bundle repair. The three integration tests broken by relocated template imports now patch the actual dependency modules and retain their constructor assertions.

The architecture review retained the existing search → bundle → finalizer separation and persistent shard-schedule module. Bundle usability validation now finishes inside the reader's delete-on-invalid operation.

The real-binary tests cover:

  • the command contract for protein and RNA, with the two-pass join checked on real output
  • an AlphaFold 2 pickle built end to end from a real search, whose template comes from 3L4Q
  • ten edge cases on purpose-built databases: a deep MSA that hits AlphaFold 2's MGnify cap at exactly 551 rows, an orphan with nothing anywhere, no templates, X residues, a UniParc-style header with no species, a peptide, and a homomer, a heteromer and a chopped chain assembled into multimer features

Against the remote ColabFold API. A new opt-in cluster check,
test/cluster/check_alphafold2_predictions.py::TestLocalMmseqsAgainstRemote, builds AF2
features for the same chains both ways -- remotely through --use_mmseqs2, locally
through this path on a GPU -- and compares them with compare_msa_backends. It passed at 881642c0 in
1h21 on a GPU node, most of it the search reading the padded databases in full.

chain remote depth / Neff local depth / Neff local / remote
P61626, human lysozyme 5111 / 3023 3354 / 1886 0.66 / 0.62
O31718 1517 / 634 1264 / 594 0.83 / 0.94
a random 90-mer 1 / 1 1 / 1 neither finds a family

Both sides carry insertions (2425 rows remotely and 1317 locally for lysozyme). Local
pairs from its own UniProt search, with 677 species for lysozyme and 149 for O31718,
where the remote path copies its unpaired rows into the paired features. Local finds 20
templates per chain against the remote path's single placeholder. Because the two search
different databases, the test asserts ranges, not equality: at least 100 rows on both
sides, local within 0.2-5x of remote on depth and Neff, insertions present, species to
pair by, a real template for lysozyme, and at most 20 rows for the orphan.

GPU inference smoke test. Those edge-case pickles, folded with AlphaFold 2 (4 monomers and 4 multimers), all completed: 8/8 on a Blackwell MIG slice. The later H100 invocation reused completed predictions.

Snakemake end to end (companion PR KosinskiLab/AlphaPulldownSnakemake#55): the historical 2026-09-11 run used core 71eef1c4 and workflow dd5b1e7, and completed all 35 steps on the cluster. That covered one search shard for five proteins, five finalizations, nine AF2-multimer folds and their analysis. The folds include three edge cases on the real databases, alone and paired: thioredoxin (a deep MSA), a random 90-residue orphan with no homologs or templates, and a 26-residue peptide. A second invocation found nothing to do. This run used hmmsearch and predates the PDB70 cache correction; current fixes are covered by the gate above.

Benchmark against native AlphaFold 2

The set is 32 monomers spread over the quartiles of their native MSA depth, plus 12 heterodimers released after AF2-multimer's training cutoff. Each was featurized with native jackhmmer (reduced_dbs), with this path using GPU and CPU search, and with the remote ColabFold API, all against the same databases and with templates up to 2021-09-30. Each heterodimer was then folded with the same five AF2-multimer models and seed per feature set, and every model scored by DockQ against the experimental structure.

median, unless stated native local, GPU local, CPU
MSA depth, shallowest / deepest quartile 52 / 12 247 29 / 11 756 28 / 5 181
Neff, all monomers 778 627 572
unpaired hits recovered vs native — 0.76 0.47
templates per chain (overlap with native, Jaccard) 16 16 (0.67) 15 (0.64)
top-ranked DockQ, mean of 12 0.59 0.56 0.56
acceptable interfaces (DockQ ≥ 0.23) 9 / 12 9 / 12 9 / 12
  • Structure quality is close to native. Paired over the 12 heterodimers, local features scored 0.028 lower on average. One interface, 9HMX, lost about 0.3 (0.70 → 0.40, although its best model reached 0.67). The other eleven moved by less than 0.1.
  • The MSAs are shallower, most on the shallowest families (recovery 0.50 in the lowest quartile), as already measured for AlphaFold 3. Template overlap with native rises from 0.30 on the shallowest quartile to 0.90 on the deepest, because the templates are searched from the UniRef90 alignment.
  • A CPU search finds far fewer distant hits than the GPU prefilter on deep families, at MMseqs2's default sensitivity, yet DockQ is identical here. Raising CPU sensitivity is a possible follow-up; this PR doesn't change it.
  • Cost: the GPU search of all 56 chains in one shard took 0.9 h and the CPU search 4.25 h, both peaking at 146 GB. An earlier GPU attempt timed out at 4 h while it shared the NFS server with the native jobs and the CPU search. The GPU search is I/O-bound on network storage: the H200 sat at 0% while the padded databases streamed at ~150 MB/s. AlphaFold 2 finalization is dominated by template featurization, not the MSA: median ~1 GB and 2 min per chain, peak 18.8 GB and 87 min, set by which structures the templates come from. The workflow's defaults now follow these numbers.

🤖 Generated with Claude Code

DimaMolod and others added 10 commits September 11, 2026 09:02
MMseqs2 reads the whole target database into RSS unless told otherwise; mode 2
memory-maps it instead. The workflow config documented db_load_mode but nothing
read it, and there was no core flag to receive it.

It reaches both commands, since both read the target database. It is a
process-level setting on the adapter rather than part of MsaBatchSettings,
because it changes memory behaviour and never the alignment: two runs differing
only here must keep reusing each other's bundles, so it is kept out of the
cache signature by construction, and a test asserts that on two real adapters.

Unset, nothing is passed and MMseqs2 chooses as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every local MMseqs2 MSA had an all-zero deletion matrix. result2msa mode 2,
the only format we ran, keeps full database headers but drops every column the
query does not span, so no insertion ever reached AlphaFold. Against a real
586-residue query that was 98 of 120 small_bfd hits (81.7%) and 712 of 824
uniprot hits (86.4%), 13035 residues on uniprot alone. The code assumed mode 2
delivered insertions as query-gap columns and converted them; the pinned build
never emits one, so that path never ran on real data.

Mode 5 keeps the insertions but cuts the header to the database key, so
sp|P83570|GWA_SEPOF becomes P83570 -- and AlphaFold reads the species it pairs
chains by out of the long form. Neither format alone is usable, so each result
is formatted both ways and joined row by row. The join is verified, not
trusted: stripping insertions from every mode-5 row must reproduce its mode-2
row, and a disagreement fails the request instead of labelling one hit with
another's header. The second pass costs about 16 ms a hit (2 s against a 947 s
search on small_bfd, 13 s against 3739 s on uniprot). RNA takes the same route.

This also removes the last AlphaFold 3 dependency from the search stage. It
converted rows with alphafold3.cpp.msa_conversion, which the AlphaFold 2 image
does not have, so the stage imported there and died on its first result.

The bundle, now schema 3, also records how many unpaired rows each database
contributed. AlphaFold 3 does not need it; an AlphaFold 2 consumer does, since
it searches templates from uniref90 alone and caps each database separately. A
bundle whose counts do not add up is searched again rather than sliced wrongly.

Provenance moves (protein schema 5, RNA 2, plus msa_format), so no bundle
written before this is reused. The only workflow cache that existed held two
bundles of e2e test data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The local MMseqs2 search stage was AlphaFold 3 only. It is backend-neutral --
one MSA bundle per chain -- so AlphaFold 2 support is a second finalizer that
turns a bundle into a pickled MonomericObject, the form AF2 inference reads.
Both CPU and GPU search feed it. finalize_batch_features.py now takes
--data_pipeline=alphafold2 and selects it; --use_mmseqs2 keeps meaning the
remote ColabFold API.

It builds features as alphafold.data.pipeline.DataPipeline.process does from
jackhmmer: uniref90 capped at 10000 and mgnify at 501, counting the query as AF2
does; merge order uniref90, BFD, MGnify (row order decides what AF2 samples);
templates searched from uniref90 alone, never the merged alignment, via the
bundle's per-database row spans; the paired UniProt alignment truncated to 50000
and exposed as *_all_seq. The recipe is AF2's reduced_dbs set -- there is no
BFD/UniRef30 HHblits arm -- and the module says where it cannot be exact.

The paired features come from an independent UniProt search whose headers carry
the species AF2 pairs chains by. The remote path has no such search and copies
the unpaired features into *_all_seq instead. Accession identifiers are parsed
offline from the headers; nothing on this path queries UniProt over the network.

A local pickle carries the 21 keys a native one does plus the two accession
arrays. AF2 pairing takes its key set from the first chain, so an extra key
crashes in one order only (#619); pair_and_merge backfills identifier arrays
first, and a test pairs a local chain with an older pickle in both orders.
Another asserts which rows pair -- two chains' HUMAN hits on one row -- through
AF2's own pairing code.

The template stack is built from explicit settings rather than global FLAGS,
and the legacy script now delegates to the same builder. Metadata records only
the template resources that ran: the jackhmmer and HHblits flags default to
whatever is on PATH, and recording them would claim tools that never touched
this MSA. make_features' template defaults are extracted and shared, so the two
paths cannot drift.

An end-to-end test runs nothing faked: real MMseqs2 through create_batch_msas,
then this finalizer with real hmmsearch against a seqres backed by the real
3L4Q mmCIF, and checks the pickle for the template, the recovered insertion and
the parsed species. Its query is an NS1 homolog, not NS1: AF2 discards a
template identical to its query without a warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hopped

Covers the multimer paths the heteromer test does not: identical entities merged
into a dense MSA, pair_msa=False block-diagonalising instead of pairing, and a
chopped local chain after a full one -- issue #619's order -- which needs the
accession identifiers carried through the slice, row-aligned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both READMEs said AlphaFold 2 feature generation was untouched by the local
MMseqs2 path; it now has a finalizer. Document how to run it, what it reproduces
from native AlphaFold 2, and where it differs: the reduced_dbs recipe, caps after
deduplication, a template profile without insert columns, accessions filled in.

Also say that alignments now keep their insertions and that earlier bundles are
searched again, and that AlphaFold 2 features are not yet benchmarked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four separate MMseqs2 databases for UniRef90, MGnify, small BFD and UniProt, one
real pass through both CLIs, nothing faked. Covers the cases that fail silently:
600 MGnify hits against AlphaFold 2's 501 cap (it bites: exactly 551 rows), an
orphan with no hits anywhere, a chain with no template, a homolog that finds the
real 3L4Q template, X residues, one sequence under two names, UniProt hits whose
headers carry no species, and a 20-residue peptide -- all with the same keys.

build_edge_case_features is importable, so the same pickles can be written
somewhere durable and folded on a GPU.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
compare_msa_backends.py read only AlphaFold 3 JSON. --artifact_format=af2_pickle
reads MonomericObject pickles and measures what each gives the model: unpaired
depth, coverage and Neff, rows that carry insertions, paired depth, distinct
species available to pairing, and real templates (AlphaFold 2's empty-template
placeholder is not counted).

No homolog overlap for pickles: they store alignments as integer rows without
headers, so there is no accession to match on, and residue strings are not a
substitute. Compare raw alignments for that.

Decodes with the alphabet af2_to_af3_msa already spells out, so it runs without
AlphaFold installed; the AlphaFold 3 mode is unchanged apart from an
artifactFormat field in the report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Iterative profile search is protein-only. On the pinned build,
--num-iterations 3 on a nucleotide search exits 1, so raising num_iterations
failed every shard holding an RNA chain. The option now reaches protein
searches only; RNA provenance already never recorded it, so no cached RNA MSA
changes identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AlphaFold 2 slices the unpaired alignment by database, so a bundle whose
per-database row spans are missing or do not add up is unusable to it. The
finalizer reported that and left the bundle in place, where the workflow's
shard completion kept validating it: no repair was ever scheduled and every
retry failed the same way. Such a bundle is now treated as damaged and
deleted, the way read_msa_bundle already treats every other defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace "not yet benchmarked" with the benchmark: 32 monomers over native
MSA-depth quartiles and 12 heterodimers released after AF2-multimer's cutoff,
native reduced_dbs against local GPU and CPU search. Top-ranked DockQ averaged
0.56 against 0.59, 9 of 12 interfaces acceptable either way; the MSAs are
shallower, most on the shallowest families, and a CPU search finds far fewer
distant hits than the GPU prefilter on deep ones. Finalization cost follows the
template hits, not chain length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DimaMolod
DimaMolod marked this pull request as ready for review September 11, 2026 19:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae838f9a30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +367 to +371
"max_template_date": self._settings.max_template_date,
"pdb_seqres_database_id": self._settings.template_seqres_database_id,
"mmcif_database_id": self._settings.template_mmcif_database_id,
"template_searcher": self._settings.template_searcher,
"software": dict(self._settings.base_metadata.get("software", {})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the PDB70 identity in the AF2 cache key

When --use_hhsearch is selected, the template stack actually searches pdb70_database_path, but this signature still keys reuse only on the PDB seqres and mmCIF identifiers. Thus replacing or updating PDB70 while those two IDs remain unchanged causes _read_matching_artifact to reuse pickles whose template hits came from the old PDB70 database; there is also no separate PDB70 identifier flag to force regeneration.

Useful? React with 👍 / 👎.

DimaMolod and others added 6 commits September 11, 2026 21:59
…bFold API

An opt-in cluster check that builds AlphaFold 2 features for the same chains
both ways -- remotely through --use_mmseqs2, locally through create_batch_msas
and finalize_batch_features -- and compares them with compare_af2_directories.

The two search different databases, so it asserts agreement, not equality, with
bands from the 32-monomer benchmark: for two real families (human lysozyme and
O31718 from the benchmark) both sides find at least 100 rows, local is within
0.2-5x of remote on depth and Neff, both carry insertions, and local has species
to pair by; lysozyme gets a template; and for a random orphan neither side finds
more than 20 rows. The measured report is printed into the job log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ires

The first cluster run failed before searching: create_batch_msas requires
--mmseqs_batch_max_sequences and --mmseqs_batch_max_residues and they have no
default. Pass limits that put all three chains in one shard, as the workflow
would. Both local scripts were checked against flag parsing with these exact
arguments before rerunning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st on PATH

The second cluster run searched fine and then failed every finalization with
"expected str, bytes or os.PathLike object, not NoneType". The scripts default
kalign, hmmsearch and hmmbuild to whatever is first on PATH, and the job had
been launched from a shell whose first environment has no kalign, so template
realignment got None. Reproduced on the login node with that PATH, and fixed by
putting the AlphaPulldown environment's bin first. Do that in the test itself,
so the check works from any shell rather than depending on how it was launched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Require and persist the PDB70 build identity for HHsearch while retaining the seqres identity for hmmsearch. Validate complete bundle database roles before reuse so unusable bundles are removed for repair. Update the relocated template-stack test patch targets and cover all new executable functions through unit tests.
@DimaMolod
DimaMolod merged commit 9124e4d into main Sep 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant