Skip to content

Fixes CP support#4

Open
zyndagj wants to merge 4 commits into
Biohub:mainfrom
zyndagj:zyndagj/foldcp_support
Open

Fixes CP support#4
zyndagj wants to merge 4 commits into
Biohub:mainfrom
zyndagj:zyndagj/foldcp_support

Conversation

@zyndagj

@zyndagj zyndagj commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Distributes the entire ESMFold2 forward pass across a context-parallel mesh so that no rank ever materializes a full L×L tensor. This enables inference on sequences that would OOM on a single GPU.

What's distributed:

  • MSA encoder (outer product mean, pair averaging, triangular multiplications)
  • Recycle loop
  • Diffusion module (conditioning, transformer, atom-to-token pooling)
  • Confidence head (distogram, row-attention pooling, pLDDT/pTM)
  • LM→pair projection and ESM-C backbone (tensor-parallel sharding + CPU offload)
  • Pair initialization — every O(L²) tensor is now sharded at creation; no full pair tensor is ever gathered

Supporting fixes:

  • GPU cache cleared after ESM-C offload to reclaim HBM
  • SVD falls back to CPU when GPU SVD fails
  • Distributed manager: NVLS disabled by default (opt-in for NVSwitch/Fabric Manager systems), lazy NCCL init fallback, get_coordinate() normalization across PyTorch versions

Validation

  • Spawn-based 2×2 bit-exact unit tests per component (tests/models/esmfold2/)
  • Real-checkpoint parity: pLDDT/pTM spread <1e-3 across mesh
    • ring comm equivalent to gather comm
  • Pair-init sharding verified bit-exact vs. serial construction including non-divisible-L padding (pair_init_cp_parity_test.py)
  • Serial (non-CP) path is guarded with isinstance DTensor checks and unchanged

root and others added 4 commits June 15, 2026 08:28
…edups

  Extends ESMFold2's 2D context-parallel (CP) path beyond the FoldingTrunk to the
  MSA encoder, and adds memory/precision optimizations that make the 2402-residue
  5xgo complex fold ~2x faster and well under the 80 GB limit on a 2x2 grid.

  MSA encoder CP (forward-only inference; the encoder runs the full LxL pair every
  recycling loop and previously OOMed on every rank):
  - OuterProductMeanDistributed: transpose-based (MSA depth replicated), bit-exact.
  - MSAPairWeightedAveragingDistributed: comm="gather" (default, bit-exact) or
    "ring" (boltz-style online-softmax).
  - MSAEncoderDistributed / MSAEncoderCPWrapper: drop-in for the serial MSAEncoder.
  - wrap_model_with_cp(): convenience wrapper for trunk + MSA encoder.

  Trunk performance (2x2 grid, 5xgo, vs fp32 baseline 1359 s / 73.8 GB):
  - bf16 distributed trunk: cast params + pair to bf16, output back to caller dtype
    (boltz-cp runs its CP trunk pair in bf16). Quality-neutral: pLDDT 0.643 vs 0.639.
  - ESM-C offload: move the ~12 GB ESM-C 6B LM to CPU after its one-shot
    hidden-state computation (restored next call), freeing it for the trunk +
    diffusion. This was the dominant cost — resident-but-idle ESM-C kept the
    allocator at ~84% occupancy, whose cudaMalloc stalls drove rank desync and ring
    spin-wait. Combined with bf16: 636 s / 55.3 GB (2.14x faster, -18.5 GB peak),
    pLDDT unchanged (0.640).
  - Inference fast path in the replicated Linear/LayerNorm: skip the autograd
    Function + ctx bookkeeping and cache replicated weight locals when grad is off.

  API: bf16 and ESM-C offload are wrapper arguments, not env vars:
    wrap_model_with_cp(model, dm, comm="gather", bf16=True, offload_esmc=True)
    wrap_model_with_cp_trunks(model, dm, bf16=True)
  offload_esmc sets model._offload_esmc (instance attr, default False on the base
  model; opted in by the CP wrapper).

  Distributed manager: disable NCCL NVLS by default (NCCL_NVLS_ENABLE=0; opt back in
  on NVSwitch + Fabric Manager systems), fall back to lazy init when device_id is
  unsupported or eager NCCL connect fails, and normalize get_coordinate() across
  PyTorch versions. Fix vendored import paths (projects.huggingface.transformers ->
  transformers).
…ence, LM→pair, ESM-C)

Extend 2-D context parallelism from the MSA encoder + trunk to the rest of the
model so per-rank peak memory DROPS as the CP mesh (P×P) grows. Previously only
the Pairformer trunk and MSA encoder were sharded; every other stage ran serial
at full length L on every rank and re-gathered the full L×L pair at its
boundaries, so adding GPUs past 4 did not raise (and could lower) the max input
length. The pair now stays sharded (Shard(0),Shard(1),Shard(2)) end to end.

New distributed components (installed by wrap_model_with_cp via CP-agnostic seams
in modeling_esmfold2.py; serial path unchanged when unwrapped):

- recycle.py (CPRecycleEngine): sharded recycle loop, no per-iteration full-L×L
  gather; handles per-loop LM dropout and the parcae tail.
- structure_wrapper.py + diffusion_{transformer,conditioning}.py +
  attention_pair_bias.py + atom_to_token.py: distributed diffusion structure head
  (conditioned-z + token attention sharded; gather mode, num_diffusion_samples==1).
- single_to_pair.py: sharded LM→pair builder — lm_z built directly as a sharded
  DTensor (no full L×L), shard-local per-loop dropout.
- esmc_tp.py: opt-in (tp_esmc=True) TE-native tensor parallelism for the ESM-C
  SwiGLU MLP across the CP ranks.
- confidence_wrapper.py + confidence_zbase.py + row_attention_pooling.py:
  distributed confidence head (z_base + nested trunk + row-pool + PAE/PDE sharded;
  pTM/ipTM reductions reuse a shared serial _finish).
- distogram_wrapper.py: distributed distogram head (symmetrize via transpose comm).

modeling_esmfold2.py gains the CP-tail seams and a behavior-preserving
ConfidenceHead._finish refactor. Validation: spawn-based 2x2 bit-exact unit tests
per component + real-checkpoint fold pLDDT/pTM parity (tests/models/esmfold2/).
Numerically faithful — cross-mesh pLDDT spread <1e-3, ring≡gather.
-SVD fallsback to CPU
The initial pair tensors (z_init, relative-position encoding, token-bond
encoding, the initial pair state, and the pair attention mask) were built
full [B, L, L, *] on every rank and only sharded at the recycle-loop
boundary via distribute_tensor. That made the per-rank init peak scale like
a single GPU — several full L×L tensors resident at once, plus rel_pos /
token_bonds held full through the whole forward — so CP gave no memory
relief for the init phase and OOM'd at short L.

Build each of these directly as a 2-D-sharded (Shard0,Shard1,Shard2) DTensor
by computing only this rank's (row, col) block, mirroring evolutionaryscale's
_sharded_rel_pos_encoding + sliced z_init. Per-token inputs stay replicated;
only their O(L²) outer combinations are sharded.

- pair_init.py (new): PairInitDistributed builds z_init/rel_pos/token_bonds/
  initial-state blocks; build_sharded_pair_mask and build_sharded_distogram_bins
  helpers (block outer-product / block cdist).
- recycle.py: run_loop / parcae_finish accept already-sharded z / z_init /
  pair_mask DTensors (+ explicit n_orig); no distribute_tensor of a full tensor.
- structure_wrapper.py: diffusion conditioning consumes the sharded rel_pos
  DTensor directly (removes the per-diffusion-step full-rel_pos floor).
- msa_wrapper.py: install _cp_pair_init; MSA per-iteration pair mask via the
  shared build_sharded_pair_mask (was a full [B,L,L] every recycle step).
- confidence_wrapper.py / confidence_zbase.py: consume sharded rel_pos /
  token_bonds / distogram_bins (block cdist) and build the nested-trunk pair
  mask sharded — removes the end-of-forward gather.
- modeling_esmfold2.py: route pair-init + pair mask through the CP seam; drop
  the transient confidence-boundary gather. Serial path unchanged (isinstance
  DTensor guards).

Every O(L²) tensor on the CP inference path is now sharded at creation.
Block builders verified bit-exact vs the serial full construction on a 2×2
grid (pair_init_cp_parity_test.py), including the padded (non-divisible-L) case.
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