Skip to content
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ from esm.models.esmc import EsmcForMaskedLM, EsmcTokenizer
# example GFP sequence
sequences = ["MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTFSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK"]

model = EsmcForMaskedLM.from_pretrained("biohub/ESMC-6B", device="cuda").eval()
model = EsmcForMaskedLM.from_pretrained("biohub/ESMC-6B", device="auto").eval()
tokenizer = EsmcTokenizer()

inputs = tokenizer(sequences, return_tensors="pt", padding=True)
Expand Down Expand Up @@ -170,7 +170,7 @@ from esm.models.esmc import EsmcForMaskedLM, EsmcSaeModel, EsmcTokenizer

sequence = "MGSNKSKPKDASQRRRSLEPAENVHGAGGGAFPASQTPSKPASADGHRGPSAAFAPAAAEPKLFGGFNSSDTVTSPQRAGPLAGGVTTFVALYDYESRTETDLSFKKGERLQIVNNTEGDWWLAHSLSTGQTGYIPSNYVAPSDSIQAEEWYFGKITRRESERLLLNAENPRGTFLVRESETTKGAYCLSVSDFDNAKGLNVKHYKIRKLDSGGFYITSRTQFNSLQQLVAYYSKHADGLCHRLTTVCPTSKPQTQGLAKDAWEIPRESLRLEVKLGQGCFGEVWMGTWNGTTRVAIKTLKPGTMSPEAFLQEAQVMKKLRHEKLVQLYAVVSEEPIYIVTEYMSKGSLLDFLKGETGKYLRLPQLVDMAAQIASGMAYVERMNYVHRDLRAANILVGENLVCKVADFGLARLIEDNEYTARQGAKFPIKWTAPEAALYGRFTIKSDVWSFGILLTELTTKGRVPYPGMVNREVLDQVERGYRMPCPPECPESLHDLMCQCWRKEPEERPTFEYLQAFLEDYFTSTEPQYQPGENL"

model = EsmcForMaskedLM.from_pretrained("biohub/ESMC-6B", device="cuda").eval()
model = EsmcForMaskedLM.from_pretrained("biohub/ESMC-6B", device="auto").eval()
tokenizer = EsmcTokenizer()
sae = EsmcSaeModel.from_pretrained(
"biohub/ESMC-6B-sae-k64-codebook16384",
Expand Down Expand Up @@ -237,7 +237,7 @@ HHAI_SEQ = (
"YKVHPSTSQAYKQFGNSVVINVLQYIAYNIGSSLNFKPY"
)

model = EsmFold2Model.from_pretrained("biohub/ESMFold2", device="cuda").eval()
model = EsmFold2Model.from_pretrained("biohub/ESMFold2", device="auto").eval()

spi = StructurePredictionInput(
sequences=[
Expand Down
2 changes: 1 addition & 1 deletion _assets/ESM3_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ from esm.sdk.api import ESM3InferenceClient, ESMProtein, GenerationConfig
login()

# This will download the model weights and instantiate the model on your machine.
model: ESM3InferenceClient = ESM3.from_pretrained("esm3-sm-open-v1").to("cuda") # or "cpu"
model: ESM3InferenceClient = ESM3.from_pretrained("esm3-sm-open-v1").to("auto")

# Generate a completion for a partial Carbonic Anhydrase (2vvb)
prompt = "___________________________________________________DQATSLRILNNGHAFNVEFDDSQDKAVLKGGPLDGTYRLIQFHFHWGSLDGQGSEHTVDKKKYAAELHLVHWNTKYGDFGKAVQQPDGLAVLGIFLKVGSAKPGLQKVVDVLDSIKTKGKSADFTNFDPRGLLPESLDYWTYPGSLTTPP___________________________________________________________"
Expand Down
29 changes: 14 additions & 15 deletions cookbook/local/raw_forwards.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@
InterProQuantizedTokenizer as EsmFunctionTokenizer,
)
from esm.tokenization.sequence_tokenizer import EsmSequenceTokenizer
from esm.utils.device import empty_cache, resolve_device
from esm.utils.structure.protein_chain import ProteinChain
from esm.utils.types import FunctionAnnotation

device = resolve_device("auto")


# The open weights are bf16, so calling `forward` directly needs autocast; the SDK
# generate/logits paths set it up for you.
@torch.no_grad()
@torch.autocast("cuda", dtype=torch.bfloat16)
def inverse_folding_example():
tokenizer = EsmSequenceTokenizer()
encoder = ESM3_structure_encoder_v0("cuda")
model = ESM3_sm_open_v0("cuda")
encoder = ESM3_structure_encoder_v0(device)
model = ESM3_sm_open_v0(device)

chain = ProteinChain.from_rcsb("1utn", "A")
coords, plddt, residue_index = chain.to_structure_encoder_inputs()
coords = coords.cuda()
plddt = plddt.cuda()
residue_index = residue_index.cuda()
coords = coords.to(device)
plddt = plddt.to(device)
residue_index = residue_index.to(device)
_, structure_tokens = encoder.encode(coords, residue_index=residue_index)

# Add BOS/EOS padding
Expand All @@ -50,11 +50,10 @@ def inverse_folding_example():


@torch.no_grad()
@torch.autocast("cuda", dtype=torch.bfloat16)
def conditioned_prediction_example():
tokenizers = get_esm3_model_tokenizers()

model = ESM3_sm_open_v0("cuda")
model = ESM3_sm_open_v0(device)

# PDB 1UTN
sequence = "MKTFIFLALLGAAVAFPVDDDDKIVGGYTCGANTVPYQVSLNSGYHFCGGSLINSQWVVSAAHCYKSGIQVRLGEDNINVVEGNEQFISASKSIVHPSYNSNTLNNDIMLIKLKSAASLNSRVASISLPTSCASAGTQCLISGWGNTKSSGTSYPDVLKCLKAPILSDSSCKSAYPGQITSNMFCAGYLEGGKDSCQGDSGGPVVCSGKLQGIVSWGSGCAQKNKPGVYTKVCNYVSWIKQTIASN"
Expand All @@ -80,8 +79,8 @@ def conditioned_prediction_example():
function_tokens = tokenizers.function.tokenize(function_annotations, len(sequence))
function_tokens = tokenizers.function.encode(function_tokens)

function_tokens = function_tokens.cuda().unsqueeze(0)
sequence_tokens = sequence_tokens.cuda().unsqueeze(0)
function_tokens = function_tokens.to(device).unsqueeze(0)
sequence_tokens = sequence_tokens.to(device).unsqueeze(0)

output = model.forward(
sequence_tokens=sequence_tokens, function_tokens=function_tokens
Expand All @@ -92,8 +91,8 @@ def conditioned_prediction_example():
@torch.no_grad()
def decode(sequence, output, sequence_tokens):
# To save on VRAM, we load these in separate functions
decoder = ESM3_structure_decoder_v0("cuda")
function_decoder = ESM3_function_decoder_v0("cuda")
decoder = ESM3_structure_decoder_v0(device)
function_decoder = ESM3_function_decoder_v0(device)
function_tokenizer = EsmFunctionTokenizer()

# Generally not recommended to just argmax the logits, decode iteratively!
Expand Down Expand Up @@ -150,6 +149,6 @@ def decode(sequence, output, sequence_tokens):
inverse_folding_example()

sequence, output, sequence_tokens = conditioned_prediction_example()
torch.cuda.empty_cache()
empty_cache()
# And then decode from tokenized representation to outputs:
decode(sequence, output, sequence_tokens)
98 changes: 98 additions & 0 deletions cookbook/snippets/esmc_mlx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""ESMC-300M / 600M inference in Apple MLX (Apple Silicon, no PyTorch).

Requirements:
pip install mlx safetensors huggingface_hub esm

Benchmark on Apple M3 (20 passes, bfloat16 weights):
L=140 : MLX 57 ms vs PyTorch MPS 68 ms → 1.19x
L=280 : MLX 104 ms vs PyTorch MPS 148 ms → 1.41x
L=426 : MLX 150 ms vs PyTorch MPS 233 ms → 1.56x
"""

import mlx.core as mx

from esm.models.esmc import EsmcTokenizer
from esm.models.esmc.mlx_model import EsmcMLX


def score_sequence_mlx(
model: EsmcMLX,
tokenizer: EsmcTokenizer,
sequence: str,
) -> mx.array:
"""Compute per-position log-probabilities for *sequence*.

Returns
-------
mx.array
Shape ``(L, 64)`` — log-softmax over the 64-token ESMC vocabulary
at each position (BOS/EOS stripped).
"""
enc = tokenizer(sequence, return_tensors="pt")
input_ids = mx.array(enc["input_ids"].numpy()) # (1, L+2)
logits = model(input_ids) # (1, L+2, 64)
log_probs = mx.log_softmax(logits[0], axis=-1) # (L+2, 64)
return log_probs[1:-1] # strip BOS/EOS → (L, 64)


def main():
# ------------------------------------------------------------------
# Load model and tokenizer
# ------------------------------------------------------------------
print("Loading biohub/ESMC-300M in MLX ...")
model = EsmcMLX.from_pretrained("biohub/ESMC-300M")
tokenizer = EsmcTokenizer.from_pretrained("biohub/ESMC-300M")

# ------------------------------------------------------------------
# Score a sequence
# ------------------------------------------------------------------
sequence = "MDVFMKGLSKAKEGVVAAAEKTKQGVAEAAGKTKEGVLYVGSK"
log_probs = score_sequence_mlx(model, tokenizer, sequence)
mx.eval(log_probs)

print(f"Sequence length : {len(sequence)}")
print(f"Log-prob shape : {log_probs.shape}") # (43, 64)
print(f"Top token at pos 0 : id={log_probs[0].argmax().item()}")

# ------------------------------------------------------------------
# Masked-marginals variant score
# ------------------------------------------------------------------
# The ESMC vocabulary maps amino acids to token ids 4-23.
# Use score_sequence_mlx on the wild-type; no masking needed for
# a single-pass approximation.

ref_seq = "MDVFMKGLSKAKEGVVAAAEKTKQGVAEAAGKTKEGVLYVGSK"
mut_seq = "MDVFMKGLSKAKEGVVAAAEKTKQGVAEAAGKTAEGVLYVGSK" # K30A

lp_ref = score_sequence_mlx(model, tokenizer, ref_seq)
lp_mut = score_sequence_mlx(model, tokenizer, mut_seq)
mx.eval(lp_ref, lp_mut)

# Amino acid token ids: A=4, C=5, D=6, ... (see EsmcTokenizer vocab)
# Position 29 (0-indexed): K→A substitution
pos, wt_id, mut_id = 29, 14, 4 # K=14, A=4 in ESMC vocab
score = float(lp_ref[pos, mut_id] - lp_ref[pos, wt_id])
print(f"\nK30A variant score (log-LR): {score:.4f}")

# ------------------------------------------------------------------
# Speed benchmark
# ------------------------------------------------------------------
import time

seq_long = ref_seq * 6 # L=258
enc = tokenizer(seq_long, return_tensors="pt")
ids = mx.array(enc["input_ids"].numpy())

model(ids); mx.eval(model(ids)) # warm-up Metal shaders

N = 20
t0 = time.perf_counter()
for _ in range(N):
out = model(ids)
mx.eval(out)
ms = (time.perf_counter() - t0) / N * 1000
print(f"\nMLX throughput L={len(seq_long)}: {ms:.1f} ms/pass")


if __name__ == "__main__":
main()
54 changes: 45 additions & 9 deletions esm/layers/attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,43 @@
import torch.nn.functional as F
from torch import nn

from esm.layers import rotary
from esm.layers.rotary import RotaryEmbedding, TritonRotaryEmbedding
from esm.utils.device import is_cuda_available

try:
from flash_attn import flash_attn_varlen_qkvpacked_func
except (ImportError, RuntimeError):
flash_attn_varlen_qkvpacked_func = None # ty:ignore[invalid-assignment]


def _eager_scaled_dot_product_attention(
query_BHLD: torch.Tensor,
key_BHLD: torch.Tensor,
value_BHLD: torch.Tensor,
mask_BHLL: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
scale = query_BHLD.shape[-1] ** -0.5
attn_scores = torch.einsum("bhld,bhsd->bhls", query_BHLD, key_BHLD) * scale
if mask_BHLL is not None:
attn_scores = attn_scores.masked_fill(~mask_BHLL, float("-inf"))
attn_weights = torch.softmax(attn_scores, dim=-1)
context_BHLD = torch.einsum("bhls,bhsd->bhld", attn_weights, value_BHLD)
return context_BHLD, attn_weights


def _triton_rotary_available() -> bool:
return is_cuda_available() and rotary.apply_triton_rotary is not None


def _flash_attention_available(x: torch.Tensor, rotary_module: nn.Module) -> bool:
return (
flash_attn_varlen_qkvpacked_func is not None
and x.is_cuda
and isinstance(rotary_module, TritonRotaryEmbedding)
)


class MultiHeadAttention(nn.Module):
def __init__(
self, d_model: int, n_heads: int, bias: bool = False, qk_layernorm: bool = True
Expand Down Expand Up @@ -72,13 +101,10 @@ def forward(
mask_BHLL = None

if output_attentions:
scale = self.d_head**-0.5
attn_scores = torch.einsum("bhld,bhsd->bhls", query_BHLD, key_BHLD) * scale
if mask_BHLL is not None:
attn_scores = attn_scores.masked_fill(~mask_BHLL, float("-inf"))
attn_weights = torch.softmax(attn_scores, dim=-1)
context_BHLD = torch.einsum("bhls,bhsd->bhld", attn_weights, value_BHLD)
else:
context_BHLD, attn_weights = _eager_scaled_dot_product_attention(
query_BHLD, key_BHLD, value_BHLD, mask_BHLL
)
elif query_BHLD.is_cuda:
attn_weights = None
if mask_BHLL is None:
# Shortcut, if we don't use attention biases then torch
Expand All @@ -90,6 +116,11 @@ def forward(
context_BHLD = F.scaled_dot_product_attention(
query_BHLD, key_BHLD, value_BHLD, mask_BHLL
)
else:
attn_weights = None
context_BHLD, _ = _eager_scaled_dot_product_attention(
query_BHLD, key_BHLD, value_BHLD, mask_BHLL
)

context_BLD = einops.rearrange(context_BHLD, "b h s d -> b s (h d)")

Expand All @@ -104,12 +135,17 @@ def __init__(
d_model=d_model, n_heads=n_heads, bias=bias, qk_layernorm=qk_layernorm
)

# Flash attention rotary.
self.rotary = TritonRotaryEmbedding(d_model // n_heads)
if _triton_rotary_available():
self.rotary = TritonRotaryEmbedding(d_model // n_heads)
else:
self.rotary = RotaryEmbedding(d_model // n_heads)

def forward(
self, x, seq_id, output_attentions: bool = False
) -> tuple[torch.Tensor, torch.Tensor | None]:
if not _flash_attention_available(x, self.rotary):
return super().forward(x, seq_id, output_attentions=output_attentions)

if output_attentions:
raise ValueError(
"FlashMultiHeadAttention does not support output_attentions=True. "
Expand Down
Loading