Repository navigation
Conversation
Adds esm/models/esmc/mlx_model.py: a self-contained ESMC implementation in Apple MLX — no PyTorch, no CUDA, unified memory, Metal GPU via mx.fast.scaled_dot_product_attention. Architecture matches EsmcForMaskedLM exactly: • pre-LN Transformer, RoPE (base=10000), QK-Norm, SwiGLU FFN • residue scaling: sqrt(n_layers/36) (ESM3 scheme) • weights loaded from the same HF safetensors checkpoint Correctness: max logit MAE = 1.3e-4 vs PyTorch float32 on CPU. Benchmark vs PyTorch MPS on Apple M3: L=140 : 57 ms vs 68 ms → 1.19x L=280 : 104 ms vs 148 ms → 1.41x L=426 : 150 ms vs 233 ms → 1.56x Speedup grows with sequence length (attention is O(L²)). Also adds cookbook/snippets/esmc_mlx.py with a usage example. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full M3 benchmark table for ESMC-300M across opt levels 0-4 and sequence lengths L=76-450. B=12 is sweet spot at 1.75x vs sequential (33.7 seq/s). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the device utils from PR Biohub#409 with: - is_mlx_available(): detects Apple MLX (mlx.core importable) - load_esmc(model_name, device="auto"): top-level factory that selects the optimal ESMC backend for the current hardware: CUDA → EsmcForMaskedLM on CUDA (bfloat16) MLX on M-chip → EsmcMLX (native Metal, no PyTorch) MPS only → EsmcForMaskedLM on MPS (float32) CPU → EsmcForMaskedLM on CPU device="mlx" forces the MLX path explicitly. Both backends share the same __call__(input_ids) -> logits interface so callers need no device-specific branching. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three published ESMC checkpoints share the same pre-LN transformer architecture — only depth and width differ. Adds ESMC-6B to CONFIGS: "biohub/ESMC-6B": hidden=2560, n_heads=40, n_layers=80, intermediate=6912 Memory: ~12 GB bf16, ~3 GB mxfp4. Recommended opt_level=2 on machines with <24 GB RAM (avoids JIT warmup spike); opt_level=3 on M-series Max/Ultra. Also updates load_esmc() docstring and from_pretrained error message to enumerate all three supported repos. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ents Previous table measured MLX in isolation (bench_serial.py) against MPS in isolation — not directly comparable. New table runs both backends in separate subprocesses on the same ProteinGym WT sequences (4 warmup + 5 timed, M3 8GB): Mean speedup: 3.27× (geomean 3.0×) across L=70–536 Range: 1.73× at L=161 → 7.93× at L=428 The earlier 14s outlier at L=211 in the same-process benchmark was Metal compiler contention from MLX + MPS co-resident — not reproducible in isolation (L=211 isolated = 260ms). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings both files into line with the repo's ruff config (E4/E7/E9/F/I): - sorted imports (I001) - split semicolon statements onto separate lines (E702 × 3) - removed unused B_curr variable (F841) - reformatted to ruff style (line length, trailing commas, blank lines) No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Narrow list|None unions with `is not None` guards instead of `if return_hidden_states` so ty can infer the type - Add mlx/mlx.** to ty replace-imports-with-any (optional dep absent from the CPU lint env, same pattern as flash_attn) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches repo convention of full return-type annotations on all public functions. Union EsmcForMaskedLM | EsmcMLX can't be expressed without a circular or conditional import, so Any is the appropriate annotation (same pattern the repo uses for other dynamically-typed factory fns). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Why is MLX faster than PyTorch MPS if both run on Metal?Both backends compile down to Metal shader code on the same GPU cores — the difference is what happens above that layer. The primary mechanism: This is a fused FlashAttention-style Metal kernel written specifically for Apple Silicon by the MLX team. It computes Q@Kᵀ → softmax → @v in a single kernel launch, keeping the L×L attention matrix in threadgroup memory (never written to DRAM). PyTorch MPS SDPA does not have this. Even with For ESMC-300M that's 3 × 30 layers = 90 attention kernel launches vs MLX's 30. Each launch has overhead, and the DRAM round-trips for the L×L matrix grow quadratically with L — which is exactly why the speedup grows with sequence length: The second mechanism: lazy evaluation + PyTorch MPS is eager — every op dispatches a Metal command buffer immediately. MLX builds a computation graph and only executes at Why doesn't PyTorch MPS just do the same? It could in principle — but PyTorch MPS was ported from CUDA PyTorch, so eager dispatch and per-op Metal calls are baked into the architecture. MLX was designed from scratch for Apple Silicon with lazy evaluation as a first-class primitive. Same GPU, different contract with the software above it. The practical upshot: for the masked-marginals scoring hot-path (logits only, no attention maps returned), MLX is the right backend on M-series hardware. @eamag's MPS path (#409) remains the correct fallback when MLX isn't installed. |
What this adds
A native MLX backend for ESMC on Apple Silicon, building on top of #409's device-routing infrastructure.
mlx.coreonly; zero CPU↔GPU copies via unified memorymx.fast.scaled_dot_product_attentionfused Metal kernelmlxis optional — absent MLX silently falls back to feat: add Apple Metal (MPS) support #409's MPS path, no import errorBenchmark · Apple M3 · 8 GB unified memory
Each backend in an isolated subprocess (prevents Metal compiler contention). N=4 warm-up + N=5 timed runs. Sync barriers:
mx.eval(out)/torch.mps.synchronize().Speedup scales with L — MLX's fused Metal kernel (
mx.fast.scaled_dot_product_attention) becomes increasingly efficient over PyTorch's per-op Metal dispatch as sequences grow.Usage
Standards checklist
ruff checkclean (E4/E7/E9/F/I rules)ruff formatappliedty checkclean — addedmlx/mlx.**toreplace-imports-with-anyinpyproject.toml(same pattern asflash_attn)load_esmc()return type annotated asAny(unionEsmcForMaskedLM | EsmcMLXcan't be expressed without a conditional import — same pattern used elsewhere in the repo for dynamic factory fns)mlxis optional dep — absent → no import error, graceful MPS/CPU fallbackBenchmark script + raw JSON logs:
Tyronita/esm2-inference-opt — benchmark/mlx_vs_mps_bench.py🤖 Generated with Claude Code