Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/scripts/airtable_issue_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
F_DATE = "Submission Date"
F_PRODUCT = "Tool or Product"
F_SOURCE = "Issue Source"
F_TITLE = "Issue Title"
F_DESCRIPTION = "Issue Description"
F_GITHUB_ID = "Github Username"
F_ORIGINAL_Q = "Original Q Location"
Expand Down Expand Up @@ -342,7 +343,8 @@ def build_row(schema, existing, issue, repo_full_name, action):
row.own(F_GITHUB_ID, login)

row.fill(F_DATE, issue.get("created_at"))
row.fill(F_DESCRIPTION, truncate(f"{title}\n\n{body}", url))
row.fill(F_TITLE, title)
row.fill(F_DESCRIPTION, truncate(body, url))
row.fill(F_DETAILS, details_block(issue, repo_full_name))
row.fill(F_TYPE, request_types(issue))
row.fill(F_PRODUCT, products(issue))
Expand Down
66 changes: 66 additions & 0 deletions cookbook/foldcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# foldcp — ESMFold2 folding examples

This cookbook shows how to run Biohub's ESMFold2 structure-prediction model with
[NVIDIA BioNeMo libraries and methods](https://github.com/NVIDIA-BioNeMo). It
covers single-GPU accelerated kernel backends for faster inference and context
parallelism (CP) for inputs whose `L x L` pair representation does not fit on one
GPU.

Tested on 1x and 4x H100 80GB GPUs.

| Script | Input | GPUs | Result |
| --- | --- | --- | --- |
| `esmfold2-none.py` | 7ysz (1 protein chain ×2) | 1 | Reference single-GPU path |
| `esmfold2-cueq.py` | 7ysz (1 protein chain ×2) | 1 | cuEquivariance backend |
| `esmfold2-fused.py` | 7ysz (1 protein chain ×2) | 1 | Triton fused backend |
| `esmfold2-cp.py` | 7ysz (1 protein chain ×2) | 4 | Same fold API with CP setup |
| `cuequivariance_cp.py` | 5xgo (1 protein chain ×12, CL ligand) | 4 | Larger input via CP |
| `will_fail.py` | 5xgo (same as CP) | 1 | **OOMs** — too large for 1xH100 |

## Environment setup

Start from the NVIDIA PyTorch container, then install the dependencies:

```bash
docker run --gpus all -it --rm nvcr.io/nvidia/pytorch:26.03-py3 bash -l

# Main ESM package
pip install "git+https://github.com/Biohub/esm.git@main"
# fused dependency
pip install "esm[fused] @ git+https://github.com/Biohub/esm.git@main"
# cuEquivariance dependency (cueq12 also exists)
pip install "esm[cueq13] @ git+https://github.com/Biohub/esm.git@main"
# fold-cp dependency for larger inputs
pip install "esm[fold-cp] @ git+https://github.com/Biohub/esm.git@main"
```

## Single-GPU accelerated backends

[single-gpu.md](single-gpu.md) compares the three single-GPU backend choices:
`None` for the pure PyTorch reference path, `"cuequivariance"` for NVIDIA
cuEquivariance triangle-multiplication kernels, and `"fused"` for Triton kernels
that fuse several ESMFold2 hot-path operations. These backends keep the same model
weights and outputs while trading dependencies, warm-up behavior, and speed.

```bash
python esmfold2-none.py
python esmfold2-cueq.py
python esmfold2-fused.py
```

## Fold-CP for larger inputs

[fold-cp.md](fold-cp.md) documents how `wrap_model_with_cp(model, dm, ...)`
spreads one ESMFold2 fold across a square grid of GPUs using the [Fold-CP methodology](https://github.com/NVIDIA-BioNeMo/boltz-cp).
CP shards the large `L x L` pair activations so longer proteins and complexes fit in memory; it is a
capability feature rather than a general speedup.

Launch CP examples with `torchrun` on a perfect-square number of GPUs.

```bash
torchrun --nproc-per-node=4 esmfold2-cp.py

# Larger 5xgo example.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
torchrun --nproc-per-node=4 cuequivariance_cp.py
```
95 changes: 95 additions & 0 deletions cookbook/foldcp/cuequivariance_cp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import gc
import math
import os
from collections import OrderedDict
from time import time

import torch

from esm.models.esmfold2 import (
ESMFold2InputBuilder,
EsmFold2Model,
LigandInput,
MolecularComplexResult,
ProteinInput,
StructurePredictionInput,
)
from esm.models.esmfold2.distributed import DistributedManager, wrap_model_with_cp

spi = StructurePredictionInput( # 5xgo
sequences=[
ProteinInput(
id=["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1", "I1", "J1", "K1", "L1"],
sequence=(
"MAHHHHHHVDDDDKMSTAKLVKSKATNLLYTRNDVSDSEKKATVELLNRQVIQFIDLSLITKQAHWNMRG"
"ANFIAVHEMLDGFRTALIDHLDTMAERAVQLGGVALGTTQVINSKTPLKSYPLDIHNVQDHLKELADRYA"
"IVANDVRKAIGEAKDDDTADILTAASRDLDKFLWFIECNLDLIQKMGLQNYLQAQIREEG"
),
),
LigandInput(id=["M1", "N1"], ccd=["CL"]),
]
)

# torchrun entrypoint: torchrun --nproc_per_node=4 cuequivariance_cp.py
# torchrun sets LOCAL_RANK / WORLD_SIZE / RANK / MASTER_ADDR / MASTER_PORT.
# world_size must be a perfect square (the CP grid is n×n).
local_rank = int(os.environ["LOCAL_RANK"])
world_size = int(os.environ["WORLD_SIZE"])

os.environ["RANK"] = str(local_rank)
torch.cuda.set_device(local_rank)
torch.cuda.reset_peak_memory_stats()

n = math.isqrt(world_size)
DistributedManager.initialize(
grid_group_sizes=OrderedDict([("dp", 1), ("cp", (n, n))]),
device_type="cuda",
backend="nccl",
)
dm = DistributedManager()

model = (
EsmFold2Model.from_pretrained("biohub/ESMFold2", esmc_precision="bf16")
.cuda()
.eval()
)
# Shard BOTH the folding trunk and the MSA encoder across the n×n CP grid.
# (Trunk-only sharding leaves the MSA encoder running the full L×L pair on
# every rank, which OOMs for large complexes.)
#
# Defaults applied here (override via args): bf16=True (bf16 distributed trunk)
# and offload_esmc=True (move the ~12 GB ESM-C LM to CPU after its one-shot use
# so the trunk reuses that memory). Together: ~2.14x faster, ~18.5 GB lower
# peak vs fp32, quality-neutral. comm="gather" (bit-exact) | "ring" (n>=3).
wrap_model_with_cp(model, dm, comm="ring")
# Fused kernels accelerate the modules *outside* the CP region (notably the
# diffusion sampler / structure head). The CP-wrapped trunk and MSA encoder
# no-op this call, so it never touches the distributed ring math.
model.set_kernel_backend("cuequivariance")

start = time()
with torch.inference_mode():
result = ESMFold2InputBuilder().fold(
model, spi, num_loops=10, num_sampling_steps=50, num_diffusion_samples=1, seed=0
)
end = time()
peak_mib = torch.cuda.max_memory_allocated() / (1024**2)

# num_diffusion_samples=1 returns a single result with confidence heads populated.
assert isinstance(result, MolecularComplexResult)
assert result.plddt is not None and result.ptm is not None and result.iptm is not None

if local_rank == 0:
print(
f"pLDDT mean: {float(result.plddt.mean()):.3f}, "
f"pTM: {float(result.ptm):.3f}, ipTM: {float(result.iptm):.3f}"
)
print(f"Elapsed: {end - start} sec")
print(f"Max VRAM: {peak_mib} MB")
with open("esmfold2_cp_output.cif", "w") as f:
f.write(result.complex.to_mmcif())

DistributedManager.cleanup()
DistributedManager._state.clear()
gc.collect()
torch.cuda.empty_cache()
74 changes: 74 additions & 0 deletions cookbook/foldcp/esmfold2-cp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import math
import os
from collections import OrderedDict
from time import time

import torch

from esm.models.esmfold2 import (
ESMFold2InputBuilder,
EsmFold2Model,
MolecularComplexResult,
ProteinInput,
StructurePredictionInput,
)
from esm.models.esmfold2.distributed import DistributedManager, wrap_model_with_cp

spi = StructurePredictionInput( # 7ysz
sequences=[
ProteinInput(
id=["A1", "B1"],
sequence=(
"MDNFDNYEQVASIKVIGIGGAGNNAVNRMIEAGVQGVEFIVANTDAQIISVSKSKNKIVLGKETSKGLGA"
"GANPDVGRQAAIESAEEIKDALKGADMVFVAAGMGGGTGTGAAPIIAKLAREQGALTVGIITTPFSFEGR"
"ARNSYAIQGTEELRKHVDSLIIISNDRLLEVIGGVPLKDSFKEADNILRQGVQTITDLIAVPSLINLDFA"
"DIKTVMKNKGNALFGIGIGSGKDKAIEAANKAIISPLLEASIRGARDAIINVTGGNTLTLNDANDAVDIV"
"KQAIGGEVNIIFGTAVNEHLDDEMIVTVIATGFDGSHHHHHH"
),
)
]
)

local_rank = int(os.environ["LOCAL_RANK"])
world_size = int(os.environ["WORLD_SIZE"])
n = math.isqrt(world_size)
assert n * n == world_size, "CP requires a square number of GPUs: 1, 4, 9, 16, ..."

torch.cuda.set_device(local_rank)
torch.cuda.reset_peak_memory_stats()

DistributedManager.initialize(
grid_group_sizes=OrderedDict([("dp", 1), ("cp", (n, n))]),
device_type="cuda",
backend="nccl",
)
dm = DistributedManager()

model = (
EsmFold2Model.from_pretrained("biohub/ESMFold2", esmc_precision="bf16")
.cuda()
.eval()
)
wrap_model_with_cp(model, dm, comm="ring")

start = time()
with torch.inference_mode():
result = ESMFold2InputBuilder().fold(
model, spi, num_loops=10, num_sampling_steps=50, num_diffusion_samples=1, seed=0
)
end = time()
peak_mib = torch.cuda.max_memory_allocated() / (1024**2)

# num_diffusion_samples=1 returns a single result with confidence heads populated.
assert isinstance(result, MolecularComplexResult)
assert result.plddt is not None and result.ptm is not None and result.iptm is not None

if local_rank == 0:
print(
f"pLDDT mean: {float(result.plddt.mean()):.3f}, "
f"pTM: {float(result.ptm):.3f}, ipTM: {float(result.iptm):.3f}"
)
print(f"Elapsed: {end - start:.2f} sec")
print(f"Max VRAM: {peak_mib:.1f} MB")

DistributedManager.cleanup()
54 changes: 54 additions & 0 deletions cookbook/foldcp/esmfold2-cueq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from time import time

import torch

from esm.models.esmfold2 import (
ESMFold2InputBuilder,
EsmFold2Model,
MolecularComplexResult,
ProteinInput,
StructurePredictionInput,
)

spi = StructurePredictionInput( # 7ysz
sequences=[
ProteinInput(
id=["A1", "B1"],
sequence=(
"MDNFDNYEQVASIKVIGIGGAGNNAVNRMIEAGVQGVEFIVANTDAQIISVSKSKNKIVLGKETSKGLGA"
"GANPDVGRQAAIESAEEIKDALKGADMVFVAAGMGGGTGTGAAPIIAKLAREQGALTVGIITTPFSFEGR"
"ARNSYAIQGTEELRKHVDSLIIISNDRLLEVIGGVPLKDSFKEADNILRQGVQTITDLIAVPSLINLDFA"
"DIKTVMKNKGNALFGIGIGSGKDKAIEAANKAIISPLLEASIRGARDAIINVTGGNTLTLNDANDAVDIV"
"KQAIGGEVNIIFGTAVNEHLDDEMIVTVIATGFDGSHHHHHH"
),
)
]
)

torch.cuda.reset_peak_memory_stats()

model = (
EsmFold2Model.from_pretrained("biohub/ESMFold2", esmc_precision="bf16")
.cuda()
.eval()
)
model.set_kernel_backend("cuequivariance")

start = time()
with torch.inference_mode():
result = ESMFold2InputBuilder().fold(
model, spi, num_loops=10, num_sampling_steps=50, num_diffusion_samples=1, seed=0
)
end = time()
peak_mib = torch.cuda.max_memory_allocated() / (1024**2)

# num_diffusion_samples=1 returns a single result with confidence heads populated.
assert isinstance(result, MolecularComplexResult)
assert result.plddt is not None and result.ptm is not None and result.iptm is not None

print(
f"pLDDT mean: {float(result.plddt.mean()):.3f}, "
f"pTM: {float(result.ptm):.3f}, ipTM: {float(result.iptm):.3f}"
)
print(f"Elapsed: {end - start:.2f} sec")
print(f"Max VRAM: {peak_mib:.1f} MB")
54 changes: 54 additions & 0 deletions cookbook/foldcp/esmfold2-fused.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from time import time

import torch

from esm.models.esmfold2 import (
ESMFold2InputBuilder,
EsmFold2Model,
MolecularComplexResult,
ProteinInput,
StructurePredictionInput,
)

spi = StructurePredictionInput( # 7ysz
sequences=[
ProteinInput(
id=["A1", "B1"],
sequence=(
"MDNFDNYEQVASIKVIGIGGAGNNAVNRMIEAGVQGVEFIVANTDAQIISVSKSKNKIVLGKETSKGLGA"
"GANPDVGRQAAIESAEEIKDALKGADMVFVAAGMGGGTGTGAAPIIAKLAREQGALTVGIITTPFSFEGR"
"ARNSYAIQGTEELRKHVDSLIIISNDRLLEVIGGVPLKDSFKEADNILRQGVQTITDLIAVPSLINLDFA"
"DIKTVMKNKGNALFGIGIGSGKDKAIEAANKAIISPLLEASIRGARDAIINVTGGNTLTLNDANDAVDIV"
"KQAIGGEVNIIFGTAVNEHLDDEMIVTVIATGFDGSHHHHHH"
),
)
]
)

torch.cuda.reset_peak_memory_stats()

model = (
EsmFold2Model.from_pretrained("biohub/ESMFold2", esmc_precision="bf16")
.cuda()
.eval()
)
model.set_kernel_backend("fused")

start = time()
with torch.inference_mode():
result = ESMFold2InputBuilder().fold(
model, spi, num_loops=10, num_sampling_steps=50, num_diffusion_samples=1, seed=0
)
end = time()
peak_mib = torch.cuda.max_memory_allocated() / (1024**2)

# num_diffusion_samples=1 returns a single result with confidence heads populated.
assert isinstance(result, MolecularComplexResult)
assert result.plddt is not None and result.ptm is not None and result.iptm is not None

print(
f"pLDDT mean: {float(result.plddt.mean()):.3f}, "
f"pTM: {float(result.ptm):.3f}, ipTM: {float(result.iptm):.3f}"
)
print(f"Elapsed: {end - start:.2f} sec")
print(f"Max VRAM: {peak_mib:.1f} MB")
Loading
Loading