Skip to content
Open
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
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()
Binary file added docs/benchmarks/mlx_vs_mps_latency.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/benchmarks/mlx_vs_mps_speedup_bar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/benchmarks/mlx_vs_mps_summary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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