Skip to content

Add MPS support, upgrade PyTorch, refactor packaging, and fix CI + correctness/security bugs#66

Open
fnachon wants to merge 29 commits into
generatebio:mainfrom
fnachon:main
Open

Add MPS support, upgrade PyTorch, refactor packaging, and fix CI + correctness/security bugs#66
fnachon wants to merge 29 commits into
generatebio:mainfrom
fnachon:main

Conversation

@fnachon

@fnachon fnachon commented Jul 3, 2026

Copy link
Copy Markdown

Summary

This PR brings this fork's main branch up to date with a series of changes made since diverging from upstream: Apple Silicon (MPS) support, a PyTorch upgrade, a packaging/test refactor, and a round of correctness/security bug fixes found during a manual code review.

Apple Silicon (MPS) support

  • Adds a Mac-specific conda environment file and packaging updates (environment_Mac.yaml, pyproject.toml) to install/run on Apple Silicon.
  • Adds MPS device support throughout the model/data/layer code, with CPU fallback for ops MPS doesn't support (notably Cholesky decomposition, which MPS lacks).

Upgrade PyTorch to 2.12.1

  • Bumps the pinned PyTorch version and adjusts any code needed for compatibility.

Packaging, tests, and locked environments refactor

  • Reorganizes packaging/test layout and adds locked environment files for reproducible installs.
  • Test fixtures under tests/resources/*.cif were unintentionally excluded by a blanket *.cif rule in .gitignore (left over from before these tests existed); added an explicit exception and committed the fixtures so CI can actually run these tests.

Correctness bugs, security issues, and dead code (found via code review)

Verified with targeted reproductions plus the existing test suite:

  • Attention: fixed double-scaling of logits in the gated Attention class (divided by d_k instead of sqrt(d_k)), and fixed a crash in the gate path (torch.sigmoid doesn't take a dim kwarg).
  • chroma/data/system.py: AtomLocationView.defined() checked is not None, but undefined locations are marked with NaN, so it always returned True and let NaN coordinates leak into exported CIF files; to_XCS() crashed past ~62 chains because a fallback chain-letter computation ran unconditionally even when unused; ResidueView.get_backbone() passed atom-type strings where integer indices were expected; _read_pdb's options argument was silently dropped due to a positional/keyword mismatch; ARG NH1/NH2 disambiguation renamed a location object instead of the atom (added AtomView.rename to fix); closed file handles in from_CIF/from_PDB/to_CIF/to_PDB; replaced an O(n * chains) per-residue lookup in to_XCS with a single linear pass.
  • chroma/models/chroma.py: _protein_list_to_XCS ignored its device argument; removed _design_ar, a dead method referencing a never-assigned attribute.
  • chroma/models/graph_design.py: fixed a tuple/None bug that crashed loss() whenever sidechains=False.
  • chroma/layers/structure/symmetry.py: subsample() returned a chain map that didn't correspond to the sampled coordinates.
  • chroma/layers/structure/mvn.py: added the missing RRt_clamp_inverse buffer needed by multiply_inverse_covariance.
  • chroma/models/procap.py: a single padded sample in a batch silently dropped sequence conditioning for the whole batch instead of just that sample; re-enabled a swallowed IndexError in context reordering.
  • chroma/layers/structure/geometry.py: removed a byte-identical duplicate VirtualAtomsCA class.
  • chroma/utility/api.py (security): the registered access token was sent as a query parameter to any URL passed as a weights path, with no host check; added a host allowlist. Weight downloads were written directly to a predictable, world-writable /tmp path with no atomic rename; downloads now stream to a temp file and os.replace into place, plus a request timeout was added. register_key wrote the token to a world-readable config.json; now created with 0600 permissions.
  • chroma/utility/model.py: save_model raised NotImplementedError for s3: paths only after already serializing the full state dict; now checks first, avoiding wasted work and an orphaned temp file.

Test plan

  • pytest -m "not integration and not slow" across tests/data, tests/models, tests/layers, tests/utility (163 passed)
  • tests/utility/test_api.py::test_api (integration, full network download + token flow)
  • Targeted reproductions for each bug fix (documented in commit messages)
  • CI green on this branch: https://github.com/fnachon/chroma/actions/runs/28652681771

fnachon and others added 29 commits April 7, 2025 22:58
To simplify the installation
add mps as valid device
add mps device
switch from setup.py to .toml file
torch.linalg.cholesky is not implemented for mps. Do the calculation on the cpu and move the resulting tensor back to the gpu
torch.linalg.det is not implemented for mps. do the calculation on the cpu
torch.linalg.cummax and torch.linalg.cummin not implemented for mps. Move the calculation to the cpu then move the result back to the gpu.
torch.linalg.eigh not implemented for mps. Do in cpu
Update for running on Apple Silicon GPU with mps
- Apply attention mask before softmax (not after) so weights sum to 1;
  add nan_to_num guard for all-masked rows (ghost chains in mixed batches)
- call_with_explicit_mps_cpu_fallback now catches RuntimeError in addition
  to UserWarning so linalg.svd on MPS falls back to CPU instead of crashing
- Cache key includes tensor shape to prevent wrong MPS→CPU routing across
  different-sized calls
- Unify three independent tensor-tree traversals into one (_iter_tensors)
- Extract _with_mps_fallback_cache shared by both public MPS helpers
- Add get_default_device() to utility/torch.py; Chroma.__init__ uses it
  instead of an inline ternary
- Remove dead self.attention from MultiHeadAttention.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumps the pinned torch version and regenerates both conda lockfiles.
MPS remains available on osx-arm64 with the conda-forge cpu_generic build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The blanket *.cif rule in .gitignore prevented tests/resources fixtures
from ever being committed, so CI (which checks out a clean clone) failed
with FileNotFoundError across the data-utility, layers, and models jobs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_proclass_conditioner needs a registered API token to download
proprietary model weights, which isn't available in CI; mark it
integration/slow like the equivalent tests/models/test_chroma.py does
so it's excluded from the quick-test run.

test_multi_head_attention_matches_legacy_path compares a batched-einsum
projection against a legacy per-head loop; they're only equivalent up
to floating-point reduction order, which differs across BLAS backends
and made the default torch.allclose tolerance too strict on CI's runner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found via a systematic review of chroma/layers, chroma/models, chroma/data,
and chroma/utility. Each fix was verified with a targeted reproduction plus
the existing test suite.

Correctness:
- Attention: fix double-scaling of logits in the gated Attention class
  (divided by d_k instead of sqrt(d_k)), and fix a crash in the gate path
  (torch.sigmoid doesn't take a dim kwarg).
- system.py: AtomLocationView.defined() checked `is not None` but undefined
  locations are marked with NaN, so it always returned True and let NaN
  coordinates leak into exported CIF files; to_XCS() crashed past ~62 chains
  because an unused chain-letter fallback was computed unconditionally;
  ResidueView.get_backbone() passed atom-type strings where integer indices
  were expected; _read_pdb's `options` argument was silently dropped due to a
  positional/keyword mismatch; ARG NH1/NH2 disambiguation renamed a location
  object instead of the atom (added AtomView.rename to fix); closed file
  handles in from_CIF/from_PDB/to_CIF/to_PDB.
- chroma.py: _protein_list_to_XCS ignored its device argument; removed
  _design_ar, a dead method referencing a never-assigned attribute.
- graph_design.py: fixed a tuple/None bug that crashed loss() whenever
  sidechains=False.
- symmetry.py: subsample() returned a chain map that didn't correspond to
  the sampled coordinates.
- mvn.py: added the missing RRt_clamp_inverse buffer needed by
  multiply_inverse_covariance.
- procap.py: a single padded sample in a batch silently dropped sequence
  conditioning for the whole batch instead of just that sample; re-enabled
  a swallowed IndexError in context reordering.
- geometry.py: removed a byte-identical duplicate VirtualAtomsCA class.

Security (chroma/utility/api.py):
- The registered access token was sent as a query param to any URL passed
  as a weights path; added a host allowlist.
- Weight downloads were written directly to a predictable, world-writable
  /tmp path with no atomic rename; now stream to a temp file and os.replace
  into place, and add a request timeout.
- register_key wrote the token to a world-readable config.json; now created
  with 0600 permissions.

Performance:
- model.py: save_model raised NotImplementedError for s3: paths after
  already serializing the full state_dict; now checks first.
- system.py: to_XCS called get_residue(i) in a loop, which re-walks the
  chain list from the start each time; now iterates residues() once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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