feat: local MMseqs2 features for AlphaFold 2, with insertions for both backends - #642
Conversation
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>
There was a problem hiding this comment.
💡 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".
| "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", {})), |
There was a problem hiding this comment.
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 👍 / 👎.
…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.
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
MonomericObjectpickle AlphaFold 2 inference reads.finalize_batch_features.py --data_pipeline=alphafold2selects it.--use_mmseqs2keeps 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
--use_hhsearchThe recipe is AlphaFold 2's
reduced_dbsset (no BFD/UniRef30 HHblits arm); the docs say so and say where it cannot be exact.Cache and search correctness
Raising
num_iterationsfailed every RNA shard. Iterative profile search is protein-only; on the pinned build--num-iterations 3on 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_hhsearchrequires--template_pdb70_database_idinstead 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_pickleTests
Current gate on
6c53a24a, run in an isolated worktree on a compute node (SLURM61831784, 2026-09-14):-m external_tools)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:
Xresidues, a UniParc-style header with no species, a peptide, and a homomer, a heteromer and a chopped chain assembled into multimer featuresAgainst the remote ColabFold API. A new opt-in cluster check,
test/cluster/check_alphafold2_predictions.py::TestLocalMmseqsAgainstRemote, builds AF2features for the same chains both ways -- remotely through
--use_mmseqs2, locallythrough this path on a GPU -- and compares them with
compare_msa_backends. It passed at881642c0in1h21 on a GPU node, most of it the search reading the padded databases in full.
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
71eef1c4and workflowdd5b1e7, 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.🤖 Generated with Claude Code