Add files using upload-large-folder tool
Browse files- config/experiment/train_fsdp.yaml +54 -0
- config/model/architecture/foldingdit_1.1B.yaml +99 -0
- config/model/architecture/foldingdit_100M.yaml +101 -0
- config/model/architecture/foldingdit_360M.yaml +101 -0
- config/model/architecture/foldingdit_3B.yaml +99 -0
- config/model/architecture/foldingdit_700M.yaml +101 -0
- config/model/sampler/euler_maruyama.yaml +6 -0
- config/paths/default.yaml +20 -0
- examples/minimal.fasta +2 -0
- models/__init__.py +2 -0
- models/simplefold/__init__.py +4 -0
- models/simplefold/__pycache__/__init__.cpython-310.pyc +0 -0
- models/simplefold/__pycache__/__init__.cpython-311.pyc +0 -0
- models/simplefold/__pycache__/flow.cpython-311.pyc +0 -0
- models/simplefold/__pycache__/simplefold.cpython-311.pyc +0 -0
- models/simplefold/flow.py +101 -0
- models/simplefold/mlx/__init__.py +4 -0
- models/simplefold/mlx/__pycache__/architecture.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/blocks.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/confidence_module.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/esm_modules.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/esm_multihead_attention.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/layers.cpython-310.pyc +0 -0
- models/simplefold/mlx/__pycache__/pos_embed.cpython-310.pyc +0 -0
- models/simplefold/mlx/architecture.py +333 -0
- models/simplefold/mlx/blocks.py +100 -0
- models/simplefold/mlx/confidence_module.py +84 -0
- models/simplefold/mlx/esm_modules.py +184 -0
- models/simplefold/mlx/esm_multihead_attention.py +224 -0
- models/simplefold/mlx/esm_network.py +160 -0
- models/simplefold/mlx/esm_rotary_embedding.py +74 -0
- models/simplefold/mlx/layers.py +263 -0
- models/simplefold/mlx/pos_embed.py +204 -0
- models/simplefold/mlx/sampler.py +112 -0
- models/simplefold/simplefold.py +781 -0
- models/simplefold/torch/__init__.py +4 -0
- models/simplefold/torch/architecture.py +304 -0
- models/simplefold/torch/layers.py +313 -0
- models/simplefold/torch/pos_embed.py +177 -0
- scripts/__pycache__/inference.cpython-311.pyc +0 -0
- scripts/__pycache__/train.cpython-310.pyc +0 -0
- scripts/__pycache__/train_fsdp.cpython-310.pyc +0 -0
- scripts/evaluate.py +101 -0
- scripts/inference.py +420 -0
- scripts/preflight.py +153 -0
- scripts/process_data.py +73 -0
- scripts/run_inference.py +41 -0
- scripts/tokenize_data.py +58 -0
- scripts/train.py +142 -0
- scripts/train_fsdp.py +161 -0
config/experiment/train_fsdp.yaml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
# All parameters below will be merged with parameters from default configurations set above
|
| 4 |
+
# this allows you to overwrite only specified parameters
|
| 5 |
+
|
| 6 |
+
# Options for training:
|
| 7 |
+
# - /data: [pdb, pdb_sp, pdb_sp_afesm, pdb_sp_afesme]
|
| 8 |
+
# - /model/architecture:
|
| 9 |
+
# [foldingdit_100M, foldingdit_360M, foldingdit_700M,
|
| 10 |
+
# foldingdit_1.1B, foldingdit_1.6B, foldingdit_3B]
|
| 11 |
+
# - /trainer: [default, fsdp]
|
| 12 |
+
|
| 13 |
+
defaults:
|
| 14 |
+
- override /data: pdb
|
| 15 |
+
- override /model: simplefold
|
| 16 |
+
- override /model/architecture: foldingdit_100M
|
| 17 |
+
- override /model/sampler: euler_maruyama
|
| 18 |
+
- override /model/processor: protein_processor
|
| 19 |
+
- override /callbacks: default
|
| 20 |
+
- override /trainer: fsdp # FSDP training
|
| 21 |
+
- override /logger: tensorboard
|
| 22 |
+
|
| 23 |
+
# load_ckpt_path: [PATH_TO_YOUR_CKPT] # e.g. uncomment to load a checkpoint
|
| 24 |
+
seed: 12345
|
| 25 |
+
|
| 26 |
+
trainer:
|
| 27 |
+
max_steps: 300000
|
| 28 |
+
val_check_interval: 10000 # this controls BOTH checkpoint steps and validation
|
| 29 |
+
accumulate_grad_batches: 1 # always 1 for FSDP
|
| 30 |
+
|
| 31 |
+
data:
|
| 32 |
+
batch_size: 8 # should be the same as the number of GPUs
|
| 33 |
+
num_workers: 16
|
| 34 |
+
|
| 35 |
+
model:
|
| 36 |
+
_target_: models.simplefold.simplefold.SimpleFold
|
| 37 |
+
ema_decay: 0.999
|
| 38 |
+
clip_grad_norm_val: 2.0
|
| 39 |
+
esm_model: "esm2_3B"
|
| 40 |
+
|
| 41 |
+
scheduler:
|
| 42 |
+
_target_: onescience.utils.simplefold.lr_scheduler.LinearWarmup
|
| 43 |
+
_partial_: true
|
| 44 |
+
min_lr: 1e-6
|
| 45 |
+
max_lr: ${model.optimizer.lr}
|
| 46 |
+
warmup_steps: 5000
|
| 47 |
+
|
| 48 |
+
architecture:
|
| 49 |
+
esm_model: ${model.esm_model}
|
| 50 |
+
|
| 51 |
+
processor:
|
| 52 |
+
scale: 16.0
|
| 53 |
+
ref_scale: 5.0
|
| 54 |
+
multiplicity: 16 # number of copies per GPU
|
config/model/architecture/foldingdit_1.1B.yaml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.architecture.FoldingDiT
|
| 2 |
+
|
| 3 |
+
hidden_size: 1280
|
| 4 |
+
num_heads: 20
|
| 5 |
+
atom_num_heads: 6
|
| 6 |
+
output_channels: 3
|
| 7 |
+
use_atom_mask: False
|
| 8 |
+
use_length_condition: True
|
| 9 |
+
esm_dropout_prob: 0.0
|
| 10 |
+
esm_model: esm2_3B
|
| 11 |
+
|
| 12 |
+
time_embedder:
|
| 13 |
+
_target_: models.simplefold.torch.layers.TimestepEmbedder
|
| 14 |
+
hidden_size: 1280
|
| 15 |
+
aminoacid_pos_embedder:
|
| 16 |
+
_target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
|
| 17 |
+
in_dim: 1
|
| 18 |
+
embed_dim: 1280
|
| 19 |
+
include_input: True
|
| 20 |
+
pos_embedder:
|
| 21 |
+
_target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
|
| 22 |
+
in_dim: 3
|
| 23 |
+
include_input: True
|
| 24 |
+
min_freq_log2: 0
|
| 25 |
+
max_freq_log2: 12
|
| 26 |
+
num_freqs: 128
|
| 27 |
+
log_sampling: True
|
| 28 |
+
|
| 29 |
+
trunk:
|
| 30 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 31 |
+
depth: 36
|
| 32 |
+
block:
|
| 33 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 34 |
+
_partial_: True # because in the for loop we create a new module
|
| 35 |
+
hidden_size: 1280
|
| 36 |
+
mlp_ratio: 4.0
|
| 37 |
+
use_swiglu: True # SwiGLU FFN
|
| 38 |
+
self_attention_layer:
|
| 39 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 40 |
+
_partial_: True
|
| 41 |
+
hidden_size: 1280
|
| 42 |
+
num_heads: 20
|
| 43 |
+
qk_norm: True
|
| 44 |
+
pos_embedder:
|
| 45 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 46 |
+
in_dim: 4
|
| 47 |
+
embed_dim: 1280
|
| 48 |
+
num_heads: 20
|
| 49 |
+
base: 100.0
|
| 50 |
+
|
| 51 |
+
atom_hidden_size_enc: 384
|
| 52 |
+
atom_n_queries_enc: 32
|
| 53 |
+
atom_n_keys_enc: 128
|
| 54 |
+
atom_encoder_transformer:
|
| 55 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 56 |
+
depth: 2
|
| 57 |
+
block:
|
| 58 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 59 |
+
_partial_: True # because in the for loop we create a new module
|
| 60 |
+
hidden_size: 384
|
| 61 |
+
mlp_ratio: 4.0
|
| 62 |
+
use_swiglu: True # SwiGLU FFN
|
| 63 |
+
self_attention_layer:
|
| 64 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 65 |
+
_partial_: True
|
| 66 |
+
hidden_size: 384
|
| 67 |
+
num_heads: 6
|
| 68 |
+
qk_norm: True
|
| 69 |
+
pos_embedder:
|
| 70 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 71 |
+
in_dim: 4
|
| 72 |
+
embed_dim: 384
|
| 73 |
+
num_heads: 6
|
| 74 |
+
base: 100.0
|
| 75 |
+
|
| 76 |
+
atom_hidden_size_dec: 384
|
| 77 |
+
atom_n_queries_dec: 32
|
| 78 |
+
atom_n_keys_dec: 128
|
| 79 |
+
atom_decoder_transformer:
|
| 80 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 81 |
+
depth: 2
|
| 82 |
+
block:
|
| 83 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 84 |
+
_partial_: True # because in the for loop we create a new module
|
| 85 |
+
hidden_size: 384
|
| 86 |
+
mlp_ratio: 4.0
|
| 87 |
+
use_swiglu: True # SwiGLU FFN
|
| 88 |
+
self_attention_layer:
|
| 89 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 90 |
+
_partial_: True
|
| 91 |
+
hidden_size: 384
|
| 92 |
+
num_heads: 6
|
| 93 |
+
qk_norm: True
|
| 94 |
+
pos_embedder:
|
| 95 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 96 |
+
in_dim: 4
|
| 97 |
+
embed_dim: 384
|
| 98 |
+
num_heads: 6
|
| 99 |
+
base: 100.0
|
config/model/architecture/foldingdit_100M.yaml
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.architecture.FoldingDiT
|
| 2 |
+
|
| 3 |
+
hidden_size: 768
|
| 4 |
+
num_heads: 12
|
| 5 |
+
atom_num_heads: 4
|
| 6 |
+
output_channels: 3
|
| 7 |
+
use_atom_mask: False
|
| 8 |
+
use_length_condition: True
|
| 9 |
+
esm_dropout_prob: 0.0
|
| 10 |
+
esm_model: esm2_3B
|
| 11 |
+
|
| 12 |
+
time_embedder:
|
| 13 |
+
_target_: models.simplefold.torch.layers.TimestepEmbedder
|
| 14 |
+
hidden_size: 768
|
| 15 |
+
|
| 16 |
+
aminoacid_pos_embedder:
|
| 17 |
+
_target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
|
| 18 |
+
in_dim: 1
|
| 19 |
+
embed_dim: 768
|
| 20 |
+
include_input: True
|
| 21 |
+
|
| 22 |
+
pos_embedder:
|
| 23 |
+
_target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
|
| 24 |
+
in_dim: 3
|
| 25 |
+
include_input: True
|
| 26 |
+
min_freq_log2: 0
|
| 27 |
+
max_freq_log2: 12
|
| 28 |
+
num_freqs: 128
|
| 29 |
+
log_sampling: True
|
| 30 |
+
|
| 31 |
+
trunk:
|
| 32 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 33 |
+
depth: 8
|
| 34 |
+
block:
|
| 35 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 36 |
+
_partial_: True # because in the for loop we create a new module
|
| 37 |
+
hidden_size: 768
|
| 38 |
+
mlp_ratio: 4.0
|
| 39 |
+
use_swiglu: True # SwiGLU FFN
|
| 40 |
+
self_attention_layer:
|
| 41 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 42 |
+
_partial_: True
|
| 43 |
+
hidden_size: 768
|
| 44 |
+
num_heads: 12
|
| 45 |
+
qk_norm: True
|
| 46 |
+
pos_embedder:
|
| 47 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 48 |
+
in_dim: 4
|
| 49 |
+
embed_dim: 768
|
| 50 |
+
num_heads: 12
|
| 51 |
+
base: 100.0
|
| 52 |
+
|
| 53 |
+
atom_hidden_size_enc: 256
|
| 54 |
+
atom_n_queries_enc: 32
|
| 55 |
+
atom_n_keys_enc: 128
|
| 56 |
+
atom_encoder_transformer:
|
| 57 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 58 |
+
depth: 1
|
| 59 |
+
block:
|
| 60 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 61 |
+
_partial_: True # because in the for loop we create a new module
|
| 62 |
+
hidden_size: 256
|
| 63 |
+
mlp_ratio: 4.0
|
| 64 |
+
use_swiglu: True # SwiGLU FFN
|
| 65 |
+
self_attention_layer:
|
| 66 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 67 |
+
_partial_: True
|
| 68 |
+
hidden_size: 256
|
| 69 |
+
num_heads: 4
|
| 70 |
+
qk_norm: True
|
| 71 |
+
pos_embedder:
|
| 72 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 73 |
+
in_dim: 4
|
| 74 |
+
embed_dim: 256
|
| 75 |
+
num_heads: 4
|
| 76 |
+
base: 100.0
|
| 77 |
+
|
| 78 |
+
atom_hidden_size_dec: 256
|
| 79 |
+
atom_n_queries_dec: 32
|
| 80 |
+
atom_n_keys_dec: 128
|
| 81 |
+
atom_decoder_transformer:
|
| 82 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 83 |
+
depth: 1
|
| 84 |
+
block:
|
| 85 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 86 |
+
_partial_: True # because in the for loop we create a new module
|
| 87 |
+
hidden_size: 256
|
| 88 |
+
mlp_ratio: 4.0
|
| 89 |
+
use_swiglu: True # SwiGLU FFN
|
| 90 |
+
self_attention_layer:
|
| 91 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 92 |
+
_partial_: True
|
| 93 |
+
hidden_size: 256
|
| 94 |
+
num_heads: 4
|
| 95 |
+
qk_norm: True
|
| 96 |
+
pos_embedder:
|
| 97 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 98 |
+
in_dim: 4
|
| 99 |
+
embed_dim: 256
|
| 100 |
+
num_heads: 4
|
| 101 |
+
base: 100.0
|
config/model/architecture/foldingdit_360M.yaml
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.architecture.FoldingDiT
|
| 2 |
+
|
| 3 |
+
hidden_size: 1024
|
| 4 |
+
num_heads: 16
|
| 5 |
+
atom_num_heads: 4
|
| 6 |
+
output_channels: 3
|
| 7 |
+
use_atom_mask: False
|
| 8 |
+
use_length_condition: True
|
| 9 |
+
esm_dropout_prob: 0.0
|
| 10 |
+
esm_model: esm2_3B
|
| 11 |
+
|
| 12 |
+
time_embedder:
|
| 13 |
+
_target_: models.simplefold.torch.layers.TimestepEmbedder
|
| 14 |
+
hidden_size: 1024
|
| 15 |
+
|
| 16 |
+
aminoacid_pos_embedder:
|
| 17 |
+
_target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
|
| 18 |
+
in_dim: 1
|
| 19 |
+
embed_dim: 1024
|
| 20 |
+
include_input: True
|
| 21 |
+
|
| 22 |
+
pos_embedder:
|
| 23 |
+
_target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
|
| 24 |
+
in_dim: 3
|
| 25 |
+
include_input: True
|
| 26 |
+
min_freq_log2: 0
|
| 27 |
+
max_freq_log2: 12
|
| 28 |
+
num_freqs: 128
|
| 29 |
+
log_sampling: True
|
| 30 |
+
|
| 31 |
+
trunk:
|
| 32 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 33 |
+
depth: 18
|
| 34 |
+
block:
|
| 35 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 36 |
+
_partial_: True # because in the for loop we create a new module
|
| 37 |
+
hidden_size: 1024
|
| 38 |
+
mlp_ratio: 4.0
|
| 39 |
+
use_swiglu: True # SwiGLU FFN
|
| 40 |
+
self_attention_layer:
|
| 41 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 42 |
+
_partial_: True
|
| 43 |
+
hidden_size: 1024
|
| 44 |
+
num_heads: 16
|
| 45 |
+
qk_norm: True
|
| 46 |
+
pos_embedder:
|
| 47 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 48 |
+
in_dim: 4
|
| 49 |
+
embed_dim: 1024
|
| 50 |
+
num_heads: 16
|
| 51 |
+
base: 100.0
|
| 52 |
+
|
| 53 |
+
atom_hidden_size_enc: 256
|
| 54 |
+
atom_n_queries_enc: 32
|
| 55 |
+
atom_n_keys_enc: 128
|
| 56 |
+
atom_encoder_transformer:
|
| 57 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 58 |
+
depth: 2
|
| 59 |
+
block:
|
| 60 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 61 |
+
_partial_: True # because in the for loop we create a new module
|
| 62 |
+
hidden_size: 256
|
| 63 |
+
mlp_ratio: 4.0
|
| 64 |
+
use_swiglu: True # SwiGLU FFN
|
| 65 |
+
self_attention_layer:
|
| 66 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 67 |
+
_partial_: True
|
| 68 |
+
hidden_size: 256
|
| 69 |
+
num_heads: 4
|
| 70 |
+
qk_norm: True
|
| 71 |
+
pos_embedder:
|
| 72 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 73 |
+
in_dim: 4
|
| 74 |
+
embed_dim: 256
|
| 75 |
+
num_heads: 4
|
| 76 |
+
base: 100.0
|
| 77 |
+
|
| 78 |
+
atom_hidden_size_dec: 256
|
| 79 |
+
atom_n_queries_dec: 32
|
| 80 |
+
atom_n_keys_dec: 128
|
| 81 |
+
atom_decoder_transformer:
|
| 82 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 83 |
+
depth: 2
|
| 84 |
+
block:
|
| 85 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 86 |
+
_partial_: True # because in the for loop we create a new module
|
| 87 |
+
hidden_size: 256
|
| 88 |
+
mlp_ratio: 4.0
|
| 89 |
+
use_swiglu: True # SwiGLU FFN
|
| 90 |
+
self_attention_layer:
|
| 91 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 92 |
+
_partial_: True
|
| 93 |
+
hidden_size: 256
|
| 94 |
+
num_heads: 4
|
| 95 |
+
qk_norm: True
|
| 96 |
+
pos_embedder:
|
| 97 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 98 |
+
in_dim: 4
|
| 99 |
+
embed_dim: 256
|
| 100 |
+
num_heads: 4
|
| 101 |
+
base: 100.0
|
config/model/architecture/foldingdit_3B.yaml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.architecture.FoldingDiT
|
| 2 |
+
|
| 3 |
+
hidden_size: 2048
|
| 4 |
+
num_heads: 32
|
| 5 |
+
atom_num_heads: 10
|
| 6 |
+
output_channels: 3
|
| 7 |
+
use_atom_mask: False
|
| 8 |
+
use_length_condition: True
|
| 9 |
+
esm_dropout_prob: 0.0
|
| 10 |
+
esm_model: esm2_3B
|
| 11 |
+
|
| 12 |
+
time_embedder:
|
| 13 |
+
_target_: models.simplefold.torch.layers.TimestepEmbedder
|
| 14 |
+
hidden_size: 2048
|
| 15 |
+
aminoacid_pos_embedder:
|
| 16 |
+
_target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
|
| 17 |
+
in_dim: 1
|
| 18 |
+
embed_dim: 2048
|
| 19 |
+
include_input: True
|
| 20 |
+
pos_embedder:
|
| 21 |
+
_target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
|
| 22 |
+
in_dim: 3
|
| 23 |
+
include_input: True
|
| 24 |
+
min_freq_log2: 0
|
| 25 |
+
max_freq_log2: 12
|
| 26 |
+
num_freqs: 128
|
| 27 |
+
log_sampling: True
|
| 28 |
+
|
| 29 |
+
trunk:
|
| 30 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 31 |
+
depth: 36
|
| 32 |
+
block:
|
| 33 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 34 |
+
_partial_: True # because in the for loop we create a new module
|
| 35 |
+
hidden_size: 2048
|
| 36 |
+
mlp_ratio: 4.0
|
| 37 |
+
use_swiglu: True # SwiGLU FFN
|
| 38 |
+
self_attention_layer:
|
| 39 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 40 |
+
_partial_: True
|
| 41 |
+
hidden_size: 2048
|
| 42 |
+
num_heads: 32
|
| 43 |
+
qk_norm: True
|
| 44 |
+
pos_embedder:
|
| 45 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 46 |
+
in_dim: 4
|
| 47 |
+
embed_dim: 2048
|
| 48 |
+
num_heads: 32
|
| 49 |
+
base: 100.0
|
| 50 |
+
|
| 51 |
+
atom_hidden_size_enc: 640
|
| 52 |
+
atom_n_queries_enc: 32
|
| 53 |
+
atom_n_keys_enc: 128
|
| 54 |
+
atom_encoder_transformer:
|
| 55 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 56 |
+
depth: 4
|
| 57 |
+
block:
|
| 58 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 59 |
+
_partial_: True # because in the for loop we create a new module
|
| 60 |
+
hidden_size: 640
|
| 61 |
+
mlp_ratio: 4.0
|
| 62 |
+
use_swiglu: True # SwiGLU FFN
|
| 63 |
+
self_attention_layer:
|
| 64 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 65 |
+
_partial_: True
|
| 66 |
+
hidden_size: 640
|
| 67 |
+
num_heads: 10
|
| 68 |
+
qk_norm: True
|
| 69 |
+
pos_embedder:
|
| 70 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 71 |
+
in_dim: 4
|
| 72 |
+
embed_dim: 640
|
| 73 |
+
num_heads: 10
|
| 74 |
+
base: 100.0
|
| 75 |
+
|
| 76 |
+
atom_hidden_size_dec: 640
|
| 77 |
+
atom_n_queries_dec: 32
|
| 78 |
+
atom_n_keys_dec: 128
|
| 79 |
+
atom_decoder_transformer:
|
| 80 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 81 |
+
depth: 4
|
| 82 |
+
block:
|
| 83 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 84 |
+
_partial_: True # because in the for loop we create a new module
|
| 85 |
+
hidden_size: 640
|
| 86 |
+
mlp_ratio: 4.0
|
| 87 |
+
use_swiglu: True # SwiGLU FFN
|
| 88 |
+
self_attention_layer:
|
| 89 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 90 |
+
_partial_: True
|
| 91 |
+
hidden_size: 640
|
| 92 |
+
num_heads: 10
|
| 93 |
+
qk_norm: True
|
| 94 |
+
pos_embedder:
|
| 95 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 96 |
+
in_dim: 4
|
| 97 |
+
embed_dim: 640
|
| 98 |
+
num_heads: 10
|
| 99 |
+
base: 100.0
|
config/model/architecture/foldingdit_700M.yaml
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.architecture.FoldingDiT
|
| 2 |
+
|
| 3 |
+
hidden_size: 1152
|
| 4 |
+
num_heads: 16
|
| 5 |
+
atom_num_heads: 4
|
| 6 |
+
output_channels: 3
|
| 7 |
+
use_atom_mask: False
|
| 8 |
+
use_length_condition: True
|
| 9 |
+
esm_dropout_prob: 0.0
|
| 10 |
+
esm_model: esm2_3B
|
| 11 |
+
|
| 12 |
+
time_embedder:
|
| 13 |
+
_target_: models.simplefold.torch.layers.TimestepEmbedder
|
| 14 |
+
hidden_size: 1152
|
| 15 |
+
|
| 16 |
+
aminoacid_pos_embedder:
|
| 17 |
+
_target_: models.simplefold.torch.pos_embed.AbsolutePositionEncoding
|
| 18 |
+
in_dim: 1
|
| 19 |
+
embed_dim: 1152
|
| 20 |
+
include_input: True
|
| 21 |
+
|
| 22 |
+
pos_embedder:
|
| 23 |
+
_target_: models.simplefold.torch.pos_embed.FourierPositionEncoding
|
| 24 |
+
in_dim: 3
|
| 25 |
+
include_input: True
|
| 26 |
+
min_freq_log2: 0
|
| 27 |
+
max_freq_log2: 12
|
| 28 |
+
num_freqs: 128
|
| 29 |
+
log_sampling: True
|
| 30 |
+
|
| 31 |
+
trunk:
|
| 32 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 33 |
+
depth: 28
|
| 34 |
+
block:
|
| 35 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 36 |
+
_partial_: True # because in the for loop we create a new module
|
| 37 |
+
hidden_size: 1152
|
| 38 |
+
mlp_ratio: 4.0
|
| 39 |
+
use_swiglu: True # SwiGLU FFN
|
| 40 |
+
self_attention_layer:
|
| 41 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 42 |
+
_partial_: True
|
| 43 |
+
hidden_size: 1152
|
| 44 |
+
num_heads: 16
|
| 45 |
+
qk_norm: True
|
| 46 |
+
pos_embedder:
|
| 47 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 48 |
+
in_dim: 4
|
| 49 |
+
embed_dim: 1152
|
| 50 |
+
num_heads: 16
|
| 51 |
+
base: 100.0
|
| 52 |
+
|
| 53 |
+
atom_hidden_size_enc: 256
|
| 54 |
+
atom_n_queries_enc: 32
|
| 55 |
+
atom_n_keys_enc: 128
|
| 56 |
+
atom_encoder_transformer:
|
| 57 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 58 |
+
depth: 2
|
| 59 |
+
block:
|
| 60 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 61 |
+
_partial_: True # because in the for loop we create a new module
|
| 62 |
+
hidden_size: 256
|
| 63 |
+
mlp_ratio: 4.0
|
| 64 |
+
use_swiglu: True # SwiGLU FFN
|
| 65 |
+
self_attention_layer:
|
| 66 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 67 |
+
_partial_: True
|
| 68 |
+
hidden_size: 256
|
| 69 |
+
num_heads: 4
|
| 70 |
+
qk_norm: True
|
| 71 |
+
pos_embedder:
|
| 72 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 73 |
+
in_dim: 4
|
| 74 |
+
embed_dim: 256
|
| 75 |
+
num_heads: 4
|
| 76 |
+
base: 100.0
|
| 77 |
+
|
| 78 |
+
atom_hidden_size_dec: 256
|
| 79 |
+
atom_n_queries_dec: 32
|
| 80 |
+
atom_n_keys_dec: 128
|
| 81 |
+
atom_decoder_transformer:
|
| 82 |
+
_target_: models.simplefold.torch.blocks.HomogenTrunk
|
| 83 |
+
depth: 2
|
| 84 |
+
block:
|
| 85 |
+
_target_: models.simplefold.torch.blocks.DiTBlock
|
| 86 |
+
_partial_: True # because in the for loop we create a new module
|
| 87 |
+
hidden_size: 256
|
| 88 |
+
mlp_ratio: 4.0
|
| 89 |
+
use_swiglu: True # SwiGLU FFN
|
| 90 |
+
self_attention_layer:
|
| 91 |
+
_target_: models.simplefold.torch.layers.EfficientSelfAttentionLayer
|
| 92 |
+
_partial_: True
|
| 93 |
+
hidden_size: 256
|
| 94 |
+
num_heads: 4
|
| 95 |
+
qk_norm: True
|
| 96 |
+
pos_embedder:
|
| 97 |
+
_target_: models.simplefold.torch.pos_embed.AxialRotaryPositionEncoding
|
| 98 |
+
in_dim: 4
|
| 99 |
+
embed_dim: 256
|
| 100 |
+
num_heads: 4
|
| 101 |
+
base: 100.0
|
config/model/sampler/euler_maruyama.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: models.simplefold.torch.sampler.EMSampler
|
| 2 |
+
num_timesteps: 500
|
| 3 |
+
t_start: 1e-4
|
| 4 |
+
tau: 0.3
|
| 5 |
+
log_timesteps: True
|
| 6 |
+
w_cutoff: 0.99
|
config/paths/default.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# path to root directory
|
| 2 |
+
# this requires PROJECT_ROOT environment variable to exist
|
| 3 |
+
# you can replace it with "." if you want the root to be the current working directory
|
| 4 |
+
root_dir: "./"
|
| 5 |
+
|
| 6 |
+
tmp_dir: ${paths.root_dir}tmp/
|
| 7 |
+
|
| 8 |
+
# path to data directory
|
| 9 |
+
data_dir: ${paths.root_dir}data/
|
| 10 |
+
|
| 11 |
+
# path to logging directory
|
| 12 |
+
log_dir: ${paths.root_dir}logs/
|
| 13 |
+
|
| 14 |
+
# artifacts directory
|
| 15 |
+
output_dir: ${paths.root_dir}artifacts/
|
| 16 |
+
|
| 17 |
+
sample_dir: ${.output_dir}/samples/
|
| 18 |
+
|
| 19 |
+
# path to working directory
|
| 20 |
+
work_dir: ${hydra:runtime.cwd}
|
examples/minimal.fasta
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
>simplefold_minimal_example
|
| 2 |
+
GASKLRAVLEKLKLSRDDISTAAGMVKGVVDHLLLRLKCDSAFRGVGLLNTGSYYEHVKISAPNEFDVMFKLEVPRIQLEEYSNTRAYYFVKFKRNPKENPLSQFLEGEILSASKMLSKFRKIIKEEINDDTDVIMKRKRGGSPAVTLLISEKISVDITLALESKSSWPASTQEGLRIQNWLSAKVRKQLRLKPFYLVPKHAEETWRLSFSHIEKEILNNHGKSKTCCENKEEKCCRKDCLKLMKYLLEQLKERFKDKKHLDKFSSYHVKTAFFHVCTQNPQDSQWDRKDLGLCFDNCVTYFLQCLRTEKLENYFIPEFNLFSSNLIDKRSKEFLTKQIEYERNNEFPVFD
|
models/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bundled model modules."""
|
| 2 |
+
|
models/simplefold/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
models/simplefold/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
models/simplefold/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (181 Bytes). View file
|
|
|
models/simplefold/__pycache__/flow.cpython-311.pyc
ADDED
|
Binary file (5.76 kB). View file
|
|
|
models/simplefold/__pycache__/simplefold.cpython-311.pyc
ADDED
|
Binary file (33.6 kB). View file
|
|
|
models/simplefold/flow.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
def right_pad_dims_to(x, t):
|
| 7 |
+
padding_dims = x.ndim - t.ndim
|
| 8 |
+
if padding_dims <= 0:
|
| 9 |
+
return t
|
| 10 |
+
return t.reshape(*t.shape, *((1,) * padding_dims))
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BasePath:
|
| 14 |
+
"""base class for flow matching path"""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
return
|
| 18 |
+
|
| 19 |
+
def compute_alpha_t(self, t):
|
| 20 |
+
"""Compute the data coefficient along the path"""
|
| 21 |
+
return None, None
|
| 22 |
+
|
| 23 |
+
def compute_sigma_t(self, t):
|
| 24 |
+
"""Compute the noise coefficient along the path"""
|
| 25 |
+
return None, None
|
| 26 |
+
|
| 27 |
+
def compute_d_alpha_alpha_ratio_t(self, t):
|
| 28 |
+
"""Compute the ratio between d_alpha and alpha"""
|
| 29 |
+
alpha_t, d_alpha_t = self.compute_alpha_t(t)
|
| 30 |
+
return d_alpha_t / alpha_t
|
| 31 |
+
|
| 32 |
+
def compute_mu_t(self, t, x0, x1):
|
| 33 |
+
"""Compute the mean of time-dependent density p_t"""
|
| 34 |
+
alpha_t, _ = self.compute_alpha_t(t)
|
| 35 |
+
sigma_t, _ = self.compute_sigma_t(t)
|
| 36 |
+
return alpha_t * x1 + sigma_t * x0
|
| 37 |
+
|
| 38 |
+
def compute_xt(self, t, x0, x1):
|
| 39 |
+
"""Sample xt from time-dependent density p_t; rng is required"""
|
| 40 |
+
xt = self.compute_mu_t(t, x0, x1)
|
| 41 |
+
return xt
|
| 42 |
+
|
| 43 |
+
def compute_ut(self, t, x0, x1):
|
| 44 |
+
"""Compute the vector field corresponding to p_t"""
|
| 45 |
+
_, d_alpha_t = self.compute_alpha_t(t)
|
| 46 |
+
_, d_sigma_t = self.compute_sigma_t(t)
|
| 47 |
+
return d_alpha_t * x1 + d_sigma_t * x0
|
| 48 |
+
|
| 49 |
+
def interpolant(self, t, x0, x1):
|
| 50 |
+
t = right_pad_dims_to(x0, t)
|
| 51 |
+
xt = self.compute_xt(t, x0, x1)
|
| 52 |
+
ut = self.compute_ut(t, x0, x1)
|
| 53 |
+
return t, xt, ut
|
| 54 |
+
|
| 55 |
+
def compute_drift(self, x, t):
|
| 56 |
+
"""We always output sde according to score parametrization; """
|
| 57 |
+
t = right_pad_dims_to(x, t)
|
| 58 |
+
alpha_ratio = self.compute_d_alpha_alpha_ratio_t(t)
|
| 59 |
+
sigma_t, d_sigma_t = self.compute_sigma_t(t)
|
| 60 |
+
drift_mean = alpha_ratio * x
|
| 61 |
+
drift_var = alpha_ratio * (sigma_t ** 2) - sigma_t * d_sigma_t
|
| 62 |
+
return -drift_mean, drift_var
|
| 63 |
+
|
| 64 |
+
def compute_score_from_velocity(self, v_t, y_t, t):
|
| 65 |
+
t = right_pad_dims_to(y_t, t)
|
| 66 |
+
alpha_t, d_alpha_t = self.compute_alpha_t(t)
|
| 67 |
+
sigma_t, d_sigma_t = self.compute_sigma_t(t)
|
| 68 |
+
mean = y_t
|
| 69 |
+
reverse_alpha_ratio = alpha_t / d_alpha_t
|
| 70 |
+
var = sigma_t**2 - reverse_alpha_ratio * d_sigma_t * sigma_t
|
| 71 |
+
score = (reverse_alpha_ratio * v_t - mean) / var
|
| 72 |
+
return score
|
| 73 |
+
|
| 74 |
+
def compute_velocity_from_score(self, s_t, y_t, t):
|
| 75 |
+
t = right_pad_dims_to(y_t, t)
|
| 76 |
+
drift_mean, drift_var = self.compute_drift(y_t, t)
|
| 77 |
+
velocity = -drift_mean + drift_var * s_t
|
| 78 |
+
return velocity
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class LinearPath(BasePath):
|
| 82 |
+
"""
|
| 83 |
+
Linear flow process:
|
| 84 |
+
x0: noise, x1: data
|
| 85 |
+
In inference, we sample data from 0 -> 1
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
def __init__(self):
|
| 89 |
+
super().__init__()
|
| 90 |
+
|
| 91 |
+
def compute_alpha_t(self, t):
|
| 92 |
+
"""Compute the data coefficient along the path"""
|
| 93 |
+
return t, 1
|
| 94 |
+
|
| 95 |
+
def compute_sigma_t(self, t):
|
| 96 |
+
"""Compute the noise coefficient along the path"""
|
| 97 |
+
return 1 - t, -1
|
| 98 |
+
|
| 99 |
+
def compute_d_alpha_alpha_ratio_t(self, t):
|
| 100 |
+
"""Compute the ratio between d_alpha and alpha"""
|
| 101 |
+
return 1 / t
|
models/simplefold/mlx/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
models/simplefold/mlx/__pycache__/architecture.cpython-310.pyc
ADDED
|
Binary file (6.66 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/blocks.cpython-310.pyc
ADDED
|
Binary file (2.92 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/confidence_module.cpython-310.pyc
ADDED
|
Binary file (1.92 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/esm_modules.cpython-310.pyc
ADDED
|
Binary file (5.6 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/esm_multihead_attention.cpython-310.pyc
ADDED
|
Binary file (4.84 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/layers.cpython-310.pyc
ADDED
|
Binary file (8.02 kB). View file
|
|
|
models/simplefold/mlx/__pycache__/pos_embed.cpython-310.pyc
ADDED
|
Binary file (5.67 kB). View file
|
|
|
models/simplefold/mlx/architecture.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
import mlx.nn as nn
|
| 8 |
+
import mlx.core as mx
|
| 9 |
+
from models.simplefold.mlx.layers import FinalLayer, ConditionEmbedder
|
| 10 |
+
from onescience.utils.simplefold.esm_utils import esm_model_dict
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# MLX does not have a native one_hot implementation
|
| 14 |
+
def one_hot(indices, num_classes, dtype=None):
|
| 15 |
+
"""
|
| 16 |
+
MLX version of torch.one_hot.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
indices: integer MLX array of any shape, containing class indices in [0, num_classes).
|
| 20 |
+
num_classes: number of classes for the one-hot dimension.
|
| 21 |
+
dtype: output data type (defaults to float32).
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
MLX array of shape indices.shape + (num_classes,) and given dtype.
|
| 25 |
+
"""
|
| 26 |
+
# Default to float32 if no dtype is given
|
| 27 |
+
if dtype is None:
|
| 28 |
+
dtype = mx.float32
|
| 29 |
+
|
| 30 |
+
classes = mx.arange(num_classes, dtype=indices.dtype)
|
| 31 |
+
|
| 32 |
+
# Broadcast-compare: result has shape indices.shape + (num_classes,)
|
| 33 |
+
# For each position, only the matched class index gives True
|
| 34 |
+
mask = indices[..., mx.newaxis] == classes
|
| 35 |
+
|
| 36 |
+
# Cast boolean mask to desired dtype
|
| 37 |
+
return mask.astype(dtype)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class FoldingDiT(nn.Module):
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
trunk,
|
| 44 |
+
time_embedder,
|
| 45 |
+
aminoacid_pos_embedder,
|
| 46 |
+
pos_embedder,
|
| 47 |
+
atom_encoder_transformer,
|
| 48 |
+
atom_decoder_transformer,
|
| 49 |
+
hidden_size=1152,
|
| 50 |
+
num_heads=16,
|
| 51 |
+
atom_num_heads=4,
|
| 52 |
+
output_channels=3,
|
| 53 |
+
atom_hidden_size_enc=256,
|
| 54 |
+
atom_hidden_size_dec=256,
|
| 55 |
+
atom_n_queries_enc=32,
|
| 56 |
+
atom_n_keys_enc=128,
|
| 57 |
+
atom_n_queries_dec=32,
|
| 58 |
+
atom_n_keys_dec=128,
|
| 59 |
+
esm_model="esm2_3B",
|
| 60 |
+
esm_dropout_prob=0.0,
|
| 61 |
+
use_atom_mask=False,
|
| 62 |
+
use_length_condition=True,
|
| 63 |
+
):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.pos_embedder = pos_embedder
|
| 66 |
+
pos_embed_channels = pos_embedder.embed_dim
|
| 67 |
+
self.aminoacid_pos_embedder = aminoacid_pos_embedder
|
| 68 |
+
aminoacid_pos_embed_channels = aminoacid_pos_embedder.embed_dim
|
| 69 |
+
|
| 70 |
+
self.time_embedder = time_embedder
|
| 71 |
+
|
| 72 |
+
self.atom_encoder_transformer = atom_encoder_transformer
|
| 73 |
+
self.atom_decoder_transformer = atom_decoder_transformer
|
| 74 |
+
|
| 75 |
+
self.trunk = trunk
|
| 76 |
+
|
| 77 |
+
self.hidden_size = hidden_size
|
| 78 |
+
self.output_channels = output_channels
|
| 79 |
+
self.num_heads = num_heads
|
| 80 |
+
self.atom_num_heads = atom_num_heads
|
| 81 |
+
self.use_atom_mask = use_atom_mask
|
| 82 |
+
self.esm_dropout_prob = esm_dropout_prob
|
| 83 |
+
self.use_length_condition = use_length_condition
|
| 84 |
+
|
| 85 |
+
esm_s_dim = esm_model_dict[esm_model]["esm_s_dim"]
|
| 86 |
+
esm_num_layers = esm_model_dict[esm_model]["esm_num_layers"]
|
| 87 |
+
|
| 88 |
+
self.atom_hidden_size_enc = atom_hidden_size_enc
|
| 89 |
+
self.atom_hidden_size_dec = atom_hidden_size_dec
|
| 90 |
+
self.atom_n_queries_enc = atom_n_queries_enc
|
| 91 |
+
self.atom_n_keys_enc = atom_n_keys_enc
|
| 92 |
+
self.atom_n_queries_dec = atom_n_queries_dec
|
| 93 |
+
self.atom_n_keys_dec = atom_n_keys_dec
|
| 94 |
+
|
| 95 |
+
atom_feat_dim = pos_embed_channels + aminoacid_pos_embed_channels + 427
|
| 96 |
+
self.atom_feat_proj = nn.Sequential(
|
| 97 |
+
nn.Linear(atom_feat_dim, hidden_size),
|
| 98 |
+
nn.LayerNorm(hidden_size),
|
| 99 |
+
nn.SiLU(),
|
| 100 |
+
)
|
| 101 |
+
self.atom_pos_proj = nn.Linear(pos_embed_channels, hidden_size, bias=False)
|
| 102 |
+
|
| 103 |
+
if self.use_length_condition:
|
| 104 |
+
self.length_embedder = nn.Sequential(
|
| 105 |
+
nn.Linear(1, hidden_size, bias=False),
|
| 106 |
+
nn.LayerNorm(hidden_size),
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
self.atom_in_proj = nn.Linear(hidden_size * 2, hidden_size, bias=False)
|
| 110 |
+
|
| 111 |
+
self.esm_s_combine = mx.zeros(esm_num_layers)
|
| 112 |
+
self.esm_s_proj = ConditionEmbedder(
|
| 113 |
+
input_dim=esm_s_dim,
|
| 114 |
+
hidden_size=hidden_size,
|
| 115 |
+
dropout_prob=0,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
latent_cat_dim = hidden_size * 2
|
| 119 |
+
self.esm_cat_proj = nn.Linear(latent_cat_dim, hidden_size)
|
| 120 |
+
|
| 121 |
+
self.context2atom_proj = nn.Sequential(
|
| 122 |
+
nn.Linear(hidden_size, self.atom_hidden_size_enc),
|
| 123 |
+
nn.LayerNorm(self.atom_hidden_size_enc),
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self.atom_enc_cond_proj = nn.Sequential(
|
| 127 |
+
nn.Linear(hidden_size, self.atom_hidden_size_enc),
|
| 128 |
+
nn.LayerNorm(self.atom_hidden_size_enc),
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
self.atom2latent_proj = nn.Sequential(
|
| 132 |
+
nn.Linear(self.atom_hidden_size_enc, hidden_size),
|
| 133 |
+
nn.LayerNorm(hidden_size),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
self.atom_dec_cond_proj = nn.Sequential(
|
| 137 |
+
nn.Linear(hidden_size, self.atom_hidden_size_dec),
|
| 138 |
+
nn.LayerNorm(self.atom_hidden_size_dec),
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
self.latent2atom_proj = nn.Sequential(
|
| 142 |
+
nn.Linear(hidden_size, hidden_size),
|
| 143 |
+
nn.SiLU(),
|
| 144 |
+
nn.LayerNorm(hidden_size),
|
| 145 |
+
nn.Linear(hidden_size, self.atom_hidden_size_dec),
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
self.final_layer = FinalLayer(
|
| 149 |
+
self.atom_hidden_size_dec, output_channels, c_dim=hidden_size
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def create_local_attn_bias(
|
| 153 |
+
self,
|
| 154 |
+
n: int,
|
| 155 |
+
n_queries: int,
|
| 156 |
+
n_keys: int,
|
| 157 |
+
inf: float = 1e10,
|
| 158 |
+
):
|
| 159 |
+
"""Create local attention bias based on query window n_queries and kv window n_keys.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
n (int): the length of quiries
|
| 163 |
+
n_queries (int): window size of quiries
|
| 164 |
+
n_keys (int): window size of keys/values
|
| 165 |
+
inf (float, optional): the inf to mask attention. Defaults to 1e10.
|
| 166 |
+
device (torch.device, optional): cuda|cpu|None. Defaults to None.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
torch.Tensor: the diagonal-like global attention bias
|
| 170 |
+
"""
|
| 171 |
+
n_trunks = int(math.ceil(n / n_queries))
|
| 172 |
+
padded_n = n_trunks * n_queries
|
| 173 |
+
attn_mask = mx.zeros((padded_n, padded_n))
|
| 174 |
+
for block_index in range(0, n_trunks):
|
| 175 |
+
i = block_index * n_queries
|
| 176 |
+
j1 = max(0, n_queries * block_index - (n_keys - n_queries) // 2)
|
| 177 |
+
j2 = n_queries * block_index + (n_queries + n_keys) // 2
|
| 178 |
+
attn_mask[i : i + n_queries, j1:j2] = 1.0
|
| 179 |
+
attn_bias = (1 - attn_mask) * -inf
|
| 180 |
+
return attn_bias[:n, :n]
|
| 181 |
+
|
| 182 |
+
def create_atom_attn_mask(
|
| 183 |
+
self, feats, natoms, atom_n_queries=None, atom_n_keys=None, inf: float = 1e10
|
| 184 |
+
):
|
| 185 |
+
|
| 186 |
+
if atom_n_queries is not None and atom_n_keys is not None:
|
| 187 |
+
atom_attn_mask = self.create_local_attn_bias(
|
| 188 |
+
n=natoms, n_queries=atom_n_queries, n_keys=atom_n_keys, inf=inf
|
| 189 |
+
)
|
| 190 |
+
else:
|
| 191 |
+
atom_attn_mask = None
|
| 192 |
+
|
| 193 |
+
return atom_attn_mask
|
| 194 |
+
|
| 195 |
+
def __call__(self, noised_pos, t, feats, self_cond=None):
|
| 196 |
+
|
| 197 |
+
B, N, _ = feats["ref_pos"].shape
|
| 198 |
+
M = feats["mol_type"].shape[1]
|
| 199 |
+
atom_to_token = feats["atom_to_token"].astype(mx.float32)
|
| 200 |
+
atom_to_token_idx = feats["atom_to_token_idx"]
|
| 201 |
+
ref_space_uid = feats["ref_space_uid"]
|
| 202 |
+
|
| 203 |
+
# create atom attention masks
|
| 204 |
+
atom_attn_mask_enc = self.create_atom_attn_mask(
|
| 205 |
+
feats,
|
| 206 |
+
natoms=N,
|
| 207 |
+
atom_n_queries=self.atom_n_queries_enc,
|
| 208 |
+
atom_n_keys=self.atom_n_keys_enc,
|
| 209 |
+
)
|
| 210 |
+
atom_attn_mask_dec = self.create_atom_attn_mask(
|
| 211 |
+
feats,
|
| 212 |
+
natoms=N,
|
| 213 |
+
atom_n_queries=self.atom_n_queries_dec,
|
| 214 |
+
atom_n_keys=self.atom_n_keys_dec,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
# create condition embeddings for AdaLN
|
| 218 |
+
c_emb = self.time_embedder(t) # (B, D)
|
| 219 |
+
if self.use_length_condition:
|
| 220 |
+
length = feats["max_num_tokens"].astype(mx.float32)[..., None]
|
| 221 |
+
c_emb = c_emb + self.length_embedder(mx.log(length))
|
| 222 |
+
|
| 223 |
+
mol_type = feats["mol_type"]
|
| 224 |
+
mol_type = one_hot(mol_type, num_classes=4).astype(mx.float32) # [B, M, 4]
|
| 225 |
+
res_type = feats["res_type"].astype(mx.float32) # [B, M, 33]
|
| 226 |
+
pocket_feature = feats["pocket_feature"].astype(mx.float32) # [B, M, 4]
|
| 227 |
+
res_feat = mx.concatenate(
|
| 228 |
+
[mol_type, res_type, pocket_feature], axis=-1
|
| 229 |
+
) # [B, M, 41]
|
| 230 |
+
atom_feat_from_res = mx.matmul(atom_to_token, res_feat) # [B, N, 41]
|
| 231 |
+
atom_res_pos = self.aminoacid_pos_embedder(
|
| 232 |
+
pos=atom_to_token_idx[..., None].astype(mx.float32)
|
| 233 |
+
)
|
| 234 |
+
ref_pos_emb = self.pos_embedder(pos=feats["ref_pos"])
|
| 235 |
+
atom_feat = mx.concatenate(
|
| 236 |
+
[
|
| 237 |
+
ref_pos_emb, # (B, N, PD1)
|
| 238 |
+
atom_feat_from_res, # (B, N, 41)
|
| 239 |
+
atom_res_pos, # (B, N, PD2)
|
| 240 |
+
feats["ref_charge"][..., None], # (B, N, 1)
|
| 241 |
+
feats["atom_pad_mask"][..., None], # (B, N, 1)
|
| 242 |
+
feats["ref_element"], # (B, N, 128)
|
| 243 |
+
feats["ref_atom_name_chars"].reshape(B, N, 4 * 64), # (B, N, 256)
|
| 244 |
+
],
|
| 245 |
+
axis=-1,
|
| 246 |
+
) # (B, N, PD1+PD2+427)
|
| 247 |
+
atom_feat = self.atom_feat_proj(atom_feat) # (B, N, D)
|
| 248 |
+
|
| 249 |
+
atom_coord = self.pos_embedder(pos=noised_pos) # (B, N, PD1)
|
| 250 |
+
atom_coord = self.atom_pos_proj(atom_coord) # (B, N, D)
|
| 251 |
+
|
| 252 |
+
atom_in = mx.concatenate([atom_feat, atom_coord], axis=-1)
|
| 253 |
+
atom_in = self.atom_in_proj(atom_in) # (B, N, D)
|
| 254 |
+
|
| 255 |
+
# position embeddings for Axial RoPE
|
| 256 |
+
atom_pe_pos = mx.concatenate(
|
| 257 |
+
[
|
| 258 |
+
ref_space_uid[..., None].astype(mx.float32), # (B, N, 1)
|
| 259 |
+
feats["ref_pos"], # (B, N, 3)
|
| 260 |
+
],
|
| 261 |
+
axis=-1,
|
| 262 |
+
) # (B, N, 4)
|
| 263 |
+
|
| 264 |
+
token_pe_pos = mx.concatenate(
|
| 265 |
+
[
|
| 266 |
+
feats["residue_index"][..., None].astype(mx.float32), # (B, M, 1)
|
| 267 |
+
feats["entity_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 268 |
+
feats["asym_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 269 |
+
feats["sym_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 270 |
+
],
|
| 271 |
+
axis=-1,
|
| 272 |
+
) # (B, M, 4)
|
| 273 |
+
|
| 274 |
+
atom_c_emb_enc = self.atom_enc_cond_proj(c_emb)
|
| 275 |
+
atom_latent = self.context2atom_proj(atom_in)
|
| 276 |
+
atom_latent = self.atom_encoder_transformer(
|
| 277 |
+
latents=atom_latent,
|
| 278 |
+
c=atom_c_emb_enc,
|
| 279 |
+
attention_mask=atom_attn_mask_enc,
|
| 280 |
+
pos=atom_pe_pos,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
atom_latent = self.atom2latent_proj(atom_latent)
|
| 285 |
+
|
| 286 |
+
# grouping: aggregate atom tokens to residue tokens
|
| 287 |
+
atom_to_token_mean = atom_to_token / (
|
| 288 |
+
atom_to_token.sum(axis=1, keepdims=True) + 1e-6
|
| 289 |
+
)
|
| 290 |
+
latent = mx.matmul(atom_to_token_mean.swapaxes(axis1=1, axis2=2), atom_latent)
|
| 291 |
+
assert latent.shape[1] == M
|
| 292 |
+
|
| 293 |
+
esm_s = (
|
| 294 |
+
mx.softmax(self.esm_s_combine, axis=0)[None, ...] @ feats["esm_s"]
|
| 295 |
+
).squeeze(axis=2)
|
| 296 |
+
|
| 297 |
+
# MLX is only interended for inference, we do not drop any ids
|
| 298 |
+
esm_emb = self.esm_s_proj(esm_s, train=False)
|
| 299 |
+
assert esm_emb.shape[1] == latent.shape[1]
|
| 300 |
+
|
| 301 |
+
latent = self.esm_cat_proj(mx.concatenate([latent, esm_emb], axis=-1))
|
| 302 |
+
|
| 303 |
+
# residue trunk
|
| 304 |
+
latent = self.trunk(
|
| 305 |
+
latents=latent,
|
| 306 |
+
c=c_emb,
|
| 307 |
+
attention_mask=None,
|
| 308 |
+
pos=token_pe_pos,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# ungrouping: broadcast residue tokens to atom tokens
|
| 312 |
+
output = mx.matmul(atom_to_token, latent)
|
| 313 |
+
assert output.shape[1] == N
|
| 314 |
+
|
| 315 |
+
# add skip connection
|
| 316 |
+
output = output + atom_latent
|
| 317 |
+
output = self.latent2atom_proj(output)
|
| 318 |
+
|
| 319 |
+
# atom decoder
|
| 320 |
+
atom_c_emb_dec = self.atom_dec_cond_proj(c_emb)
|
| 321 |
+
output = self.atom_decoder_transformer(
|
| 322 |
+
latents=output,
|
| 323 |
+
c=atom_c_emb_dec,
|
| 324 |
+
attention_mask=atom_attn_mask_dec,
|
| 325 |
+
pos=atom_pe_pos,
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
output = self.final_layer(output, c=c_emb)
|
| 329 |
+
|
| 330 |
+
return {
|
| 331 |
+
"predict_velocity": output,
|
| 332 |
+
"latent": latent,
|
| 333 |
+
}
|
models/simplefold/mlx/blocks.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import mlx.nn as nn
|
| 7 |
+
import mlx.core as mx
|
| 8 |
+
|
| 9 |
+
from .simplefold.mlx.layers import modulate, SwiGLUFeedForward
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class DiTBlock(nn.Module):
|
| 13 |
+
"""
|
| 14 |
+
A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
self_attention_layer,
|
| 20 |
+
hidden_size,
|
| 21 |
+
mlp_ratio=4.0,
|
| 22 |
+
use_swiglu=True,
|
| 23 |
+
):
|
| 24 |
+
super().__init__()
|
| 25 |
+
self.norm1 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
|
| 26 |
+
self.attn = self_attention_layer()
|
| 27 |
+
self.norm2 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
|
| 28 |
+
mlp_hidden_dim = int(hidden_size * mlp_ratio)
|
| 29 |
+
|
| 30 |
+
assert use_swiglu, "Need use_swiglu=True for MLX"
|
| 31 |
+
self.mlp = SwiGLUFeedForward(hidden_size, mlp_hidden_dim)
|
| 32 |
+
|
| 33 |
+
self.adaLN_modulation = nn.Sequential(
|
| 34 |
+
nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
def __call__(
|
| 38 |
+
self,
|
| 39 |
+
latents,
|
| 40 |
+
c,
|
| 41 |
+
**kwargs,
|
| 42 |
+
):
|
| 43 |
+
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
|
| 44 |
+
self.adaLN_modulation(c).split(6, axis=1)
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
_latents, _ = self.attn(
|
| 48 |
+
modulate(self.norm1(latents), shift_msa, scale_msa), **kwargs
|
| 49 |
+
)
|
| 50 |
+
latents = latents + mx.expand_dims(gate_msa, axis=1) * _latents
|
| 51 |
+
|
| 52 |
+
latents = latents + mx.expand_dims(gate_mlp, axis=1) * self.mlp(
|
| 53 |
+
modulate(self.norm2(latents), shift_mlp, scale_mlp)
|
| 54 |
+
)
|
| 55 |
+
return latents
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class TransformerBlock(nn.Module):
|
| 59 |
+
"""
|
| 60 |
+
A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(
|
| 64 |
+
self,
|
| 65 |
+
self_attention_layer,
|
| 66 |
+
hidden_size,
|
| 67 |
+
mlp_ratio=4.0,
|
| 68 |
+
use_swiglu=False,
|
| 69 |
+
):
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.norm1 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
|
| 72 |
+
self.attn = self_attention_layer()
|
| 73 |
+
self.norm2 = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
|
| 74 |
+
mlp_hidden_dim = int(hidden_size * mlp_ratio)
|
| 75 |
+
|
| 76 |
+
assert use_swiglu, "Need use_swiglu=True for MLX"
|
| 77 |
+
self.mlp = SwiGLUFeedForward(hidden_size, mlp_hidden_dim)
|
| 78 |
+
|
| 79 |
+
def __call__(
|
| 80 |
+
self,
|
| 81 |
+
latents,
|
| 82 |
+
**kwargs,
|
| 83 |
+
):
|
| 84 |
+
_latents, _ = self.attn(self.norm1(latents), **kwargs)
|
| 85 |
+
latents = latents + _latents
|
| 86 |
+
latents = latents + self.mlp(self.norm2(latents))
|
| 87 |
+
return latents
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# Homogen trunk, same block applied iteratively
|
| 91 |
+
class HomogenTrunk(nn.Module):
|
| 92 |
+
def __init__(self, block, depth):
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.blocks = [block() for _ in range(depth)]
|
| 95 |
+
|
| 96 |
+
def __call__(self, latents, c, **kwargs):
|
| 97 |
+
for i, block in enumerate(self.blocks):
|
| 98 |
+
kwargs["layer_idx"] = i
|
| 99 |
+
latents = block(latents=latents, c=c, **kwargs)
|
| 100 |
+
return latents
|
models/simplefold/mlx/confidence_module.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import mlx.nn as nn
|
| 7 |
+
import mlx.core as mx
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def compute_aggregated_metric(logits, end=1.0):
|
| 11 |
+
"""Compute the metric from the logits.
|
| 12 |
+
|
| 13 |
+
Parameters
|
| 14 |
+
----------
|
| 15 |
+
logits : torch.Tensor
|
| 16 |
+
The logits of the metric
|
| 17 |
+
end : float
|
| 18 |
+
Max value of the metric, by default 1.0
|
| 19 |
+
|
| 20 |
+
Returns
|
| 21 |
+
-------
|
| 22 |
+
Tensor
|
| 23 |
+
The metric value
|
| 24 |
+
|
| 25 |
+
"""
|
| 26 |
+
num_bins = logits.shape[-1]
|
| 27 |
+
bin_width = end / num_bins
|
| 28 |
+
bounds = mx.arange(start=0.5 * bin_width, stop=end, step=bin_width)
|
| 29 |
+
probs = mx.softmax(logits, axis=-1)
|
| 30 |
+
plddt = mx.sum(
|
| 31 |
+
probs * bounds.reshape(*((1,) * len(probs.shape[:-1])), *bounds.shape),
|
| 32 |
+
axis=-1,
|
| 33 |
+
)
|
| 34 |
+
return plddt
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class ConfidenceModule(nn.Module):
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
hidden_size,
|
| 41 |
+
transformer_blocks=None,
|
| 42 |
+
num_plddt_bins=50,
|
| 43 |
+
):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.transformer_blocks = transformer_blocks
|
| 46 |
+
self.to_plddt_logits = nn.Sequential(
|
| 47 |
+
nn.Linear(hidden_size, hidden_size),
|
| 48 |
+
nn.LayerNorm(hidden_size),
|
| 49 |
+
nn.SiLU(),
|
| 50 |
+
nn.Linear(hidden_size, num_plddt_bins),
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
def __call__(
|
| 54 |
+
self,
|
| 55 |
+
latent,
|
| 56 |
+
feats,
|
| 57 |
+
):
|
| 58 |
+
if self.transformer_blocks is not None:
|
| 59 |
+
token_pe_pos = mx.concatenate(
|
| 60 |
+
[
|
| 61 |
+
feats["residue_index"][..., None].astype(mx.float32), # (B, M, 1)
|
| 62 |
+
feats["entity_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 63 |
+
feats["asym_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 64 |
+
feats["sym_id"][..., None].astype(mx.float32), # (B, M, 1)
|
| 65 |
+
],
|
| 66 |
+
axis=-1,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
latent = self.transformer_blocks(
|
| 70 |
+
latents=latent,
|
| 71 |
+
c=None,
|
| 72 |
+
pos=token_pe_pos,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Compute the pLDDT
|
| 76 |
+
plddt_logits = self.to_plddt_logits(latent)
|
| 77 |
+
|
| 78 |
+
# Compute the aggregated pLDDT
|
| 79 |
+
plddt = compute_aggregated_metric(plddt_logits)
|
| 80 |
+
|
| 81 |
+
return dict(
|
| 82 |
+
plddt=plddt,
|
| 83 |
+
plddt_logits=plddt_logits,
|
| 84 |
+
)
|
models/simplefold/mlx/esm_modules.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
# Started from https://github.com/facebookresearch/esm/tree/main,
|
| 7 |
+
# licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 8 |
+
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
import mlx.core as mx
|
| 12 |
+
import mlx.nn as nn
|
| 13 |
+
|
| 14 |
+
from mlx.nn import LayerNorm as ESM1bLayerNorm
|
| 15 |
+
from mlx.nn import gelu
|
| 16 |
+
from .simplefold.mlx.esm_multihead_attention import MultiheadAttention # noqa
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def symmetrize(x):
|
| 20 |
+
"Make layer symmetric in final two dimensions, used for contact prediction."
|
| 21 |
+
return x + mx.swapaxes(x, axis1=-1, axis2=-2)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def apc(x):
|
| 25 |
+
"Perform average product correct, used for contact prediction."
|
| 26 |
+
a1 = x.sum(-1, keepdims=True)
|
| 27 |
+
a2 = x.sum(-2, keepdims=True)
|
| 28 |
+
a12 = x.sum((-1, -2), keepdims=True)
|
| 29 |
+
|
| 30 |
+
avg = a1 * a2
|
| 31 |
+
avg = mx.divide(avg, a12)
|
| 32 |
+
normalized = x - avg
|
| 33 |
+
return normalized
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ESM1LayerNorm(nn.Module):
|
| 37 |
+
def __init__(self, hidden_size, eps=1e-12, affine=True):
|
| 38 |
+
"""Construct a layernorm layer in the TF style (eps inside the sqrt)."""
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.hidden_size = (
|
| 41 |
+
(hidden_size,) if isinstance(hidden_size, int) else tuple(hidden_size)
|
| 42 |
+
)
|
| 43 |
+
self.eps = eps
|
| 44 |
+
self.affine = bool(affine)
|
| 45 |
+
if self.affine:
|
| 46 |
+
self.weight = mx.array(mx.ones(hidden_size))
|
| 47 |
+
self.bias = mx.array(mx.zeros(hidden_size))
|
| 48 |
+
else:
|
| 49 |
+
self.weight, self.bias = None, None
|
| 50 |
+
|
| 51 |
+
def __call__(self, x):
|
| 52 |
+
dims = tuple(-(i + 1) for i in range(len(self.hidden_size)))
|
| 53 |
+
means = x.mean(dims, keepdims=True)
|
| 54 |
+
x_zeromean = x - means
|
| 55 |
+
variances = x_zeromean.pow(2).mean(dims, keepdims=True)
|
| 56 |
+
x = x_zeromean / mx.sqrt(variances + self.eps)
|
| 57 |
+
if self.affine:
|
| 58 |
+
x = (self.weight * x) + self.bias
|
| 59 |
+
return x
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class TransformerLayer(nn.Module):
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
embed_dim,
|
| 67 |
+
ffn_embed_dim,
|
| 68 |
+
attention_heads,
|
| 69 |
+
add_bias_kv=True,
|
| 70 |
+
use_esm1b_layer_norm=False, # This is true in the implementation
|
| 71 |
+
use_rotary_embeddings: bool = False, # This is true in the implementation
|
| 72 |
+
):
|
| 73 |
+
super().__init__()
|
| 74 |
+
self.embed_dim = embed_dim
|
| 75 |
+
self.ffn_embed_dim = ffn_embed_dim
|
| 76 |
+
self.attention_heads = attention_heads
|
| 77 |
+
self.use_rotary_embeddings = use_rotary_embeddings
|
| 78 |
+
self._init_submodules(add_bias_kv, use_esm1b_layer_norm)
|
| 79 |
+
|
| 80 |
+
def _init_submodules(self, add_bias_kv, use_esm1b_layer_norm):
|
| 81 |
+
BertLayerNorm = ESM1bLayerNorm if use_esm1b_layer_norm else ESM1LayerNorm
|
| 82 |
+
|
| 83 |
+
self.self_attn = MultiheadAttention(
|
| 84 |
+
self.embed_dim,
|
| 85 |
+
self.attention_heads,
|
| 86 |
+
add_bias_kv=add_bias_kv,
|
| 87 |
+
add_zero_attn=False,
|
| 88 |
+
use_rotary_embeddings=self.use_rotary_embeddings,
|
| 89 |
+
)
|
| 90 |
+
self.self_attn_layer_norm = BertLayerNorm(self.embed_dim)
|
| 91 |
+
|
| 92 |
+
self.fc1 = nn.Linear(self.embed_dim, self.ffn_embed_dim)
|
| 93 |
+
self.fc2 = nn.Linear(self.ffn_embed_dim, self.embed_dim)
|
| 94 |
+
|
| 95 |
+
self.final_layer_norm = BertLayerNorm(self.embed_dim)
|
| 96 |
+
|
| 97 |
+
def __call__(
|
| 98 |
+
self,
|
| 99 |
+
x,
|
| 100 |
+
self_attn_mask=None,
|
| 101 |
+
self_attn_padding_mask=None,
|
| 102 |
+
need_head_weights=False,
|
| 103 |
+
):
|
| 104 |
+
residual = x
|
| 105 |
+
x = self.self_attn_layer_norm(x)
|
| 106 |
+
x, attn = self.self_attn(
|
| 107 |
+
query=x,
|
| 108 |
+
key=x,
|
| 109 |
+
value=x,
|
| 110 |
+
key_padding_mask=self_attn_padding_mask,
|
| 111 |
+
need_weights=True,
|
| 112 |
+
need_head_weights=need_head_weights,
|
| 113 |
+
attn_mask=self_attn_mask,
|
| 114 |
+
)
|
| 115 |
+
x = residual + x
|
| 116 |
+
|
| 117 |
+
residual = x
|
| 118 |
+
x = self.final_layer_norm(x)
|
| 119 |
+
x = gelu(self.fc1(x))
|
| 120 |
+
x = self.fc2(x)
|
| 121 |
+
x = residual + x
|
| 122 |
+
|
| 123 |
+
return x, attn
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class RobertaLMHead(nn.Module):
|
| 127 |
+
"""Head for masked language modeling."""
|
| 128 |
+
|
| 129 |
+
def __init__(self, embed_dim, output_dim, weight):
|
| 130 |
+
super().__init__()
|
| 131 |
+
self.dense = nn.Linear(embed_dim, embed_dim)
|
| 132 |
+
self.layer_norm = ESM1bLayerNorm(embed_dim)
|
| 133 |
+
self.weight = weight
|
| 134 |
+
self.bias = mx.array(mx.zeros(output_dim))
|
| 135 |
+
|
| 136 |
+
def __call__(self, features):
|
| 137 |
+
x = self.dense(features)
|
| 138 |
+
x = gelu(x)
|
| 139 |
+
x = self.layer_norm(x)
|
| 140 |
+
# project back to size of vocabulary with bias
|
| 141 |
+
x = mx.matmul(x, mx.swapaxes(self.weight, axis1=0, axis2=1)) + self.bias
|
| 142 |
+
return x
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class ContactPredictionHead(nn.Module):
|
| 146 |
+
"""Performs symmetrization, apc, and computes a logistic regression on the output features"""
|
| 147 |
+
|
| 148 |
+
def __init__(
|
| 149 |
+
self,
|
| 150 |
+
in_features: int,
|
| 151 |
+
prepend_bos: bool,
|
| 152 |
+
append_eos: bool,
|
| 153 |
+
bias=True,
|
| 154 |
+
eos_idx: Optional[int] = None,
|
| 155 |
+
):
|
| 156 |
+
super().__init__()
|
| 157 |
+
self.in_features = in_features
|
| 158 |
+
self.prepend_bos = prepend_bos
|
| 159 |
+
self.append_eos = append_eos
|
| 160 |
+
if append_eos and eos_idx is None:
|
| 161 |
+
raise ValueError(
|
| 162 |
+
"Using an alphabet with eos token, but no eos token was passed in."
|
| 163 |
+
)
|
| 164 |
+
self.eos_idx = eos_idx
|
| 165 |
+
self.regression = nn.Linear(in_features, 1, bias)
|
| 166 |
+
self.activation = nn.Sigmoid()
|
| 167 |
+
|
| 168 |
+
def __call__(self, tokens, attentions):
|
| 169 |
+
# remove eos token attentions
|
| 170 |
+
if self.append_eos:
|
| 171 |
+
eos_mask = tokens.ne(self.eos_idx)
|
| 172 |
+
eos_mask = eos_mask[:, None, ...] * eos_mask[:, :, None, ...]
|
| 173 |
+
attentions = attentions * eos_mask[:, None, None, :, :]
|
| 174 |
+
attentions = attentions[..., :-1, :-1]
|
| 175 |
+
# remove cls token attentions
|
| 176 |
+
if self.prepend_bos:
|
| 177 |
+
attentions = attentions[..., 1:, 1:]
|
| 178 |
+
batch_size, layers, heads, seqlen, _ = attentions.shape
|
| 179 |
+
attentions = attentions.reshape(batch_size, layers * heads, seqlen, seqlen)
|
| 180 |
+
|
| 181 |
+
# features: B x C x T x T
|
| 182 |
+
attentions = apc(symmetrize(attentions))
|
| 183 |
+
attentions = attentions.transpose(0, 2, 3, 1)
|
| 184 |
+
return self.activation(self.regression(attentions).squeeze(3))
|
models/simplefold/mlx/esm_multihead_attention.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
# Started from https://github.com/facebookresearch/esm/tree/main,
|
| 7 |
+
# licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 8 |
+
|
| 9 |
+
import mlx.nn as nn
|
| 10 |
+
import mlx.core as mx
|
| 11 |
+
from .simplefold.mlx.esm_rotary_embedding import RotaryEmbedding
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def utils_softmax(x, dim: int, onnx_trace: bool = False):
|
| 15 |
+
return mx.softmax(x.astype(mx.float32), axis=dim)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def masked_fill_mlx(x, mask, value):
|
| 19 |
+
return mx.where(mask, value, x)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class MultiheadAttention(nn.Module):
|
| 23 |
+
"""Multi-headed attention.
|
| 24 |
+
|
| 25 |
+
See "Attention Is All You Need" for more details.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
embed_dim,
|
| 31 |
+
num_heads,
|
| 32 |
+
kdim=None,
|
| 33 |
+
vdim=None,
|
| 34 |
+
dropout=0.0,
|
| 35 |
+
bias=True,
|
| 36 |
+
add_bias_kv: bool = False,
|
| 37 |
+
add_zero_attn: bool = False,
|
| 38 |
+
self_attention: bool = False,
|
| 39 |
+
encoder_decoder_attention: bool = False,
|
| 40 |
+
use_rotary_embeddings: bool = False,
|
| 41 |
+
):
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.embed_dim = embed_dim
|
| 44 |
+
self.kdim = kdim if kdim is not None else embed_dim
|
| 45 |
+
self.vdim = vdim if vdim is not None else embed_dim
|
| 46 |
+
self.qkv_same_dim = self.kdim == embed_dim and self.vdim == embed_dim
|
| 47 |
+
|
| 48 |
+
self.num_heads = num_heads
|
| 49 |
+
self.dropout = dropout
|
| 50 |
+
self.head_dim = embed_dim // num_heads
|
| 51 |
+
assert (
|
| 52 |
+
self.head_dim * num_heads == self.embed_dim
|
| 53 |
+
), "embed_dim must be divisible by num_heads"
|
| 54 |
+
self.scaling = self.head_dim**-0.5
|
| 55 |
+
|
| 56 |
+
self.self_attention = self_attention
|
| 57 |
+
|
| 58 |
+
self.k_proj = nn.Linear(self.kdim, embed_dim, bias=bias)
|
| 59 |
+
self.v_proj = nn.Linear(self.vdim, embed_dim, bias=bias)
|
| 60 |
+
self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
|
| 61 |
+
|
| 62 |
+
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
|
| 63 |
+
|
| 64 |
+
if add_bias_kv:
|
| 65 |
+
self.bias_k = mx.array(1, 1, embed_dim)
|
| 66 |
+
self.bias_v = mx.array(1, 1, embed_dim)
|
| 67 |
+
else:
|
| 68 |
+
self.bias_k = self.bias_v = None
|
| 69 |
+
|
| 70 |
+
self.add_zero_attn = add_zero_attn
|
| 71 |
+
|
| 72 |
+
self.rot_emb = RotaryEmbedding(dim=self.head_dim)
|
| 73 |
+
|
| 74 |
+
self.enable_torch_version = False
|
| 75 |
+
|
| 76 |
+
def __call__(
|
| 77 |
+
self,
|
| 78 |
+
query,
|
| 79 |
+
key,
|
| 80 |
+
value,
|
| 81 |
+
key_padding_mask=None,
|
| 82 |
+
incremental_state=None,
|
| 83 |
+
need_weights=True,
|
| 84 |
+
static_kv=False,
|
| 85 |
+
attn_mask=None,
|
| 86 |
+
before_softmax=False,
|
| 87 |
+
need_head_weights=False,
|
| 88 |
+
):
|
| 89 |
+
"""Input shape: Time x Batch x Channel
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
key_padding_mask (ByteTensor, optional): mask to exclude
|
| 93 |
+
keys that are pads, of shape `(batch, src_len)`, where
|
| 94 |
+
padding elements are indicated by 1s.
|
| 95 |
+
need_weights (bool, optional): return the attention weights,
|
| 96 |
+
averaged over heads (default: False).
|
| 97 |
+
attn_mask (ByteTensor, optional): typically used to
|
| 98 |
+
implement causal attention, where the mask prevents the
|
| 99 |
+
attention from looking forward in time (default: None).
|
| 100 |
+
before_softmax (bool, optional): return the raw attention
|
| 101 |
+
weights and values before the attention softmax.
|
| 102 |
+
need_head_weights (bool, optional): return the attention
|
| 103 |
+
weights for each head. Implies *need_weights*. Default:
|
| 104 |
+
return the average attention weights over all heads.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
tgt_len, bsz, embed_dim = query.shape
|
| 108 |
+
assert embed_dim == self.embed_dim
|
| 109 |
+
assert list(query.shape) == [tgt_len, bsz, embed_dim]
|
| 110 |
+
|
| 111 |
+
if self.self_attention:
|
| 112 |
+
q = self.q_proj(query)
|
| 113 |
+
k = self.k_proj(query)
|
| 114 |
+
v = self.v_proj(query)
|
| 115 |
+
else:
|
| 116 |
+
assert key is not None and value is not None
|
| 117 |
+
q = self.q_proj(query)
|
| 118 |
+
k = self.k_proj(key)
|
| 119 |
+
v = self.v_proj(value)
|
| 120 |
+
q *= self.scaling
|
| 121 |
+
|
| 122 |
+
if self.bias_k is not None:
|
| 123 |
+
assert self.bias_v is not None
|
| 124 |
+
|
| 125 |
+
# TODO: mlx not support array repeat or new_zeros
|
| 126 |
+
k = mx.concatenate([k, mx.tile(self.bias_k, (1, bsz, 1))])
|
| 127 |
+
v = mx.concatenate([v, mx.tile(self.bias_v, (1, bsz, 1))])
|
| 128 |
+
if attn_mask is not None:
|
| 129 |
+
attn_mask = mx.concatenate(
|
| 130 |
+
[
|
| 131 |
+
attn_mask,
|
| 132 |
+
mx.zeros((attn_mask.shape[0], 1), dtype=attn_mask.dtype),
|
| 133 |
+
],
|
| 134 |
+
axis=1,
|
| 135 |
+
)
|
| 136 |
+
if key_padding_mask is not None:
|
| 137 |
+
key_padding_mask = mx.concatenate(
|
| 138 |
+
[
|
| 139 |
+
key_padding_mask,
|
| 140 |
+
mx.zeros(
|
| 141 |
+
(key_padding_mask.shape[0], 1), dtype=key_padding_mask.dtype
|
| 142 |
+
),
|
| 143 |
+
],
|
| 144 |
+
axis=1,
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
q = mx.swapaxes(
|
| 148 |
+
mx.contiguous(q).reshape(tgt_len, bsz * self.num_heads, self.head_dim),
|
| 149 |
+
axis1=0,
|
| 150 |
+
axis2=1,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if k is not None:
|
| 154 |
+
k = mx.swapaxes(
|
| 155 |
+
mx.contiguous(k).reshape(-1, bsz * self.num_heads, self.head_dim),
|
| 156 |
+
axis1=0,
|
| 157 |
+
axis2=1,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if v is not None:
|
| 161 |
+
v = mx.swapaxes(
|
| 162 |
+
mx.contiguous(v).reshape(-1, bsz * self.num_heads, self.head_dim),
|
| 163 |
+
axis1=0,
|
| 164 |
+
axis2=1,
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
assert k is not None
|
| 168 |
+
src_len = k.shape[1]
|
| 169 |
+
|
| 170 |
+
# This is part of a workaround to get around fork/join parallelism
|
| 171 |
+
# not supporting Optional types.
|
| 172 |
+
|
| 173 |
+
if key_padding_mask is not None and key_padding_mask.ndim == 0:
|
| 174 |
+
key_padding_mask = None
|
| 175 |
+
|
| 176 |
+
if key_padding_mask is not None:
|
| 177 |
+
assert key_padding_mask.shape[0] == bsz
|
| 178 |
+
assert key_padding_mask.shape[1] == src_len
|
| 179 |
+
|
| 180 |
+
if self.rot_emb:
|
| 181 |
+
|
| 182 |
+
q, k = self.rot_emb(q, k)
|
| 183 |
+
|
| 184 |
+
attn_weights = mx.matmul(q, mx.swapaxes(k, axis1=1, axis2=2))
|
| 185 |
+
attn_weights = MultiheadAttention.apply_sparse_mask(
|
| 186 |
+
attn_weights, tgt_len, src_len, bsz
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
assert list(attn_weights.shape) == [bsz * self.num_heads, tgt_len, src_len]
|
| 190 |
+
|
| 191 |
+
if attn_mask is not None:
|
| 192 |
+
attn_mask = attn_mask[None, ...]
|
| 193 |
+
attn_weights += attn_mask
|
| 194 |
+
|
| 195 |
+
if key_padding_mask is not None:
|
| 196 |
+
# don't attend to padding symbols
|
| 197 |
+
attn_weights = attn_weights.reshape(bsz, self.num_heads, tgt_len, src_len)
|
| 198 |
+
attn_weights = masked_fill_mlx(
|
| 199 |
+
attn_weights,
|
| 200 |
+
(key_padding_mask[:, None, None, ...] == 1.0),
|
| 201 |
+
float("-inf"),
|
| 202 |
+
)
|
| 203 |
+
attn_weights = attn_weights.reshape(bsz * self.num_heads, tgt_len, src_len)
|
| 204 |
+
|
| 205 |
+
attn_weights_float = utils_softmax(attn_weights, dim=-1, onnx_trace=False)
|
| 206 |
+
attn_weights = attn_weights_float.astype(attn_weights.dtype)
|
| 207 |
+
|
| 208 |
+
attn_probs = attn_weights.astype(attn_weights.dtype)
|
| 209 |
+
assert v is not None
|
| 210 |
+
attn = mx.matmul(attn_probs, v)
|
| 211 |
+
|
| 212 |
+
assert list(attn.shape) == [bsz * self.num_heads, tgt_len, self.head_dim]
|
| 213 |
+
|
| 214 |
+
attn = mx.contiguous(mx.swapaxes(attn, axis1=0, axis2=1)).reshape(
|
| 215 |
+
tgt_len, bsz, embed_dim
|
| 216 |
+
)
|
| 217 |
+
attn = self.out_proj(attn)
|
| 218 |
+
|
| 219 |
+
attn_weights = None
|
| 220 |
+
|
| 221 |
+
return attn, attn_weights
|
| 222 |
+
|
| 223 |
+
def apply_sparse_mask(attn_weights, tgt_len: int, src_len: int, bsz: int):
|
| 224 |
+
return attn_weights
|
models/simplefold/mlx/esm_network.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
# Started from https://github.com/facebookresearch/esm/tree/main,
|
| 7 |
+
# licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 8 |
+
|
| 9 |
+
from typing import Union
|
| 10 |
+
import mlx.core as mx
|
| 11 |
+
import mlx.nn as nn
|
| 12 |
+
|
| 13 |
+
from .simplefold.mlx.esm_modules import (
|
| 14 |
+
ContactPredictionHead,
|
| 15 |
+
ESM1bLayerNorm,
|
| 16 |
+
RobertaLMHead,
|
| 17 |
+
TransformerLayer,
|
| 18 |
+
)
|
| 19 |
+
from onescience.datapipes.esm import Alphabet
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def masked_fill_mlx(x, mask, value):
|
| 23 |
+
return mx.where(mask, value, x)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ESM2(nn.Module):
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
num_layers: int = 33,
|
| 30 |
+
embed_dim: int = 1280,
|
| 31 |
+
attention_heads: int = 20,
|
| 32 |
+
alphabet: Union[Alphabet, str] = "ESM-1b",
|
| 33 |
+
token_dropout: bool = True,
|
| 34 |
+
):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.num_layers = num_layers
|
| 37 |
+
self.embed_dim = embed_dim
|
| 38 |
+
self.attention_heads = attention_heads
|
| 39 |
+
if not isinstance(alphabet, Alphabet):
|
| 40 |
+
alphabet = Alphabet.from_architecture(alphabet)
|
| 41 |
+
self.alphabet = alphabet
|
| 42 |
+
self.alphabet_size = len(alphabet)
|
| 43 |
+
self.padding_idx = alphabet.padding_idx
|
| 44 |
+
self.mask_idx = alphabet.mask_idx
|
| 45 |
+
self.cls_idx = alphabet.cls_idx
|
| 46 |
+
self.eos_idx = alphabet.eos_idx
|
| 47 |
+
self.prepend_bos = alphabet.prepend_bos
|
| 48 |
+
self.append_eos = alphabet.append_eos
|
| 49 |
+
self.token_dropout = token_dropout
|
| 50 |
+
|
| 51 |
+
self._init_submodules()
|
| 52 |
+
|
| 53 |
+
def _init_submodules(self):
|
| 54 |
+
self.embed_scale = 1
|
| 55 |
+
self.embed_tokens = mx.zeros((self.alphabet_size, self.embed_dim))
|
| 56 |
+
|
| 57 |
+
self.layers = [
|
| 58 |
+
TransformerLayer(
|
| 59 |
+
self.embed_dim,
|
| 60 |
+
4 * self.embed_dim,
|
| 61 |
+
self.attention_heads,
|
| 62 |
+
add_bias_kv=False,
|
| 63 |
+
use_esm1b_layer_norm=True,
|
| 64 |
+
use_rotary_embeddings=True,
|
| 65 |
+
)
|
| 66 |
+
for _ in range(self.num_layers)
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
self.contact_head = ContactPredictionHead(
|
| 70 |
+
self.num_layers * self.attention_heads,
|
| 71 |
+
self.prepend_bos,
|
| 72 |
+
self.append_eos,
|
| 73 |
+
eos_idx=self.eos_idx,
|
| 74 |
+
)
|
| 75 |
+
self.emb_layer_norm_after = ESM1bLayerNorm(self.embed_dim)
|
| 76 |
+
|
| 77 |
+
self.lm_head = RobertaLMHead(
|
| 78 |
+
embed_dim=self.embed_dim,
|
| 79 |
+
output_dim=self.alphabet_size,
|
| 80 |
+
weight=self.embed_tokens,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def __call__(
|
| 84 |
+
self, tokens, repr_layers=[], need_head_weights=False, return_contacts=False
|
| 85 |
+
):
|
| 86 |
+
if return_contacts:
|
| 87 |
+
need_head_weights = True
|
| 88 |
+
|
| 89 |
+
assert tokens.ndim == 2
|
| 90 |
+
padding_mask = mx.equal(tokens, self.padding_idx) # B, T
|
| 91 |
+
|
| 92 |
+
x = self.embed_scale * self.embed_tokens[tokens, :]
|
| 93 |
+
|
| 94 |
+
if self.token_dropout:
|
| 95 |
+
x = masked_fill_mlx(x, (tokens == self.mask_idx)[..., None], 0.0)
|
| 96 |
+
# x: B x T x C
|
| 97 |
+
mask_ratio_train = 0.15 * 0.8
|
| 98 |
+
src_lengths = (~padding_mask).sum(axis=-1)
|
| 99 |
+
mask_ratio_observed = (tokens == self.mask_idx).sum(axis=-1).astype(
|
| 100 |
+
x.dtype
|
| 101 |
+
) / src_lengths
|
| 102 |
+
x = x * (1 - mask_ratio_train) / (1 - mask_ratio_observed)[:, None, None]
|
| 103 |
+
|
| 104 |
+
if padding_mask is not None:
|
| 105 |
+
x = x * (1 - padding_mask[..., None].astype(x.dtype))
|
| 106 |
+
|
| 107 |
+
repr_layers = set(repr_layers)
|
| 108 |
+
hidden_representations = {}
|
| 109 |
+
if 0 in repr_layers:
|
| 110 |
+
hidden_representations[0] = x
|
| 111 |
+
|
| 112 |
+
if need_head_weights:
|
| 113 |
+
attn_weights = []
|
| 114 |
+
|
| 115 |
+
# (B, T, E) => (T, B, E)
|
| 116 |
+
x = mx.swapaxes(x, axis1=0, axis2=1)
|
| 117 |
+
|
| 118 |
+
if not padding_mask.any():
|
| 119 |
+
padding_mask = None
|
| 120 |
+
|
| 121 |
+
for layer_idx, layer in enumerate(self.layers):
|
| 122 |
+
x, attn = layer(
|
| 123 |
+
x,
|
| 124 |
+
self_attn_padding_mask=padding_mask,
|
| 125 |
+
need_head_weights=need_head_weights,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if (layer_idx + 1) in repr_layers:
|
| 129 |
+
hidden_representations[layer_idx + 1] = mx.swapaxes(x, axis1=0, axis2=1)
|
| 130 |
+
if need_head_weights:
|
| 131 |
+
# (H, B, T, T) => (B, H, T, T)
|
| 132 |
+
attn_weights.append(mx.swapaxes(attn, axis1=1, axis2=0))
|
| 133 |
+
|
| 134 |
+
x = self.emb_layer_norm_after(x)
|
| 135 |
+
x = mx.swapaxes(x, axis1=0, axis2=1) # (T, B, E) => (B, T, E)
|
| 136 |
+
|
| 137 |
+
# last hidden representation should have layer norm applied
|
| 138 |
+
if (layer_idx + 1) in repr_layers:
|
| 139 |
+
hidden_representations[layer_idx + 1] = x
|
| 140 |
+
x = self.lm_head(x)
|
| 141 |
+
|
| 142 |
+
result = {"logits": x, "representations": hidden_representations}
|
| 143 |
+
if need_head_weights:
|
| 144 |
+
# attentions: B x L x H x T x T
|
| 145 |
+
attentions = mx.stack(attn_weights, axis=1)
|
| 146 |
+
if padding_mask is not None:
|
| 147 |
+
attention_mask = 1 - padding_mask.astype(attentions.dtype)
|
| 148 |
+
attention_mask = (
|
| 149 |
+
attention_mask[:, None, ...] * attention_mask[:, :, None, ...]
|
| 150 |
+
)
|
| 151 |
+
attentions = attentions * attention_mask[:, None, None, :, :]
|
| 152 |
+
result["attentions"] = attentions
|
| 153 |
+
if return_contacts:
|
| 154 |
+
contacts = self.contact_head(tokens, attentions)
|
| 155 |
+
result["contacts"] = contacts
|
| 156 |
+
|
| 157 |
+
return result
|
| 158 |
+
|
| 159 |
+
def predict_contacts(self, tokens):
|
| 160 |
+
return self(tokens, return_contacts=True)["contacts"]
|
models/simplefold/mlx/esm_rotary_embedding.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
# Started from https://github.com/facebookresearch/esm/tree/main,
|
| 7 |
+
# licensed under MIT License, Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 8 |
+
|
| 9 |
+
import mlx.core as mx
|
| 10 |
+
import mlx.nn as nn
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def rotate_half(x):
|
| 14 |
+
x1, x2 = x.split(2, axis=-1)
|
| 15 |
+
return mx.concatenate([-x2, x1], axis=-1)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def apply_rotary_pos_emb(x, cos, sin):
|
| 19 |
+
cos = cos[:, : x.shape[-2], :]
|
| 20 |
+
sin = sin[:, : x.shape[-2], :]
|
| 21 |
+
|
| 22 |
+
return (x * cos) + (rotate_half(x) * sin)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RotaryEmbedding(nn.Module):
|
| 26 |
+
"""
|
| 27 |
+
The rotary position embeddings from RoFormer_ (Su et. al).
|
| 28 |
+
A crucial insight from the method is that the query and keys are
|
| 29 |
+
transformed by rotation matrices which depend on the relative positions.
|
| 30 |
+
Other implementations are available in the Rotary Transformer repo_ and in
|
| 31 |
+
GPT-NeoX_, GPT-NeoX was an inspiration
|
| 32 |
+
.. _RoFormer: https://arxiv.org/abs/2104.09864
|
| 33 |
+
.. _repo: https://github.com/ZhuiyiTechnology/roformer
|
| 34 |
+
.. _GPT-NeoX: https://github.com/EleutherAI/gpt-neox
|
| 35 |
+
.. warning: Please note that this embedding is not registered on purpose, as it is transformative
|
| 36 |
+
(it does not create the embedding dimension) and will likely be picked up (imported) on a ad-hoc basis
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self, dim: int, *_, **__):
|
| 40 |
+
super().__init__()
|
| 41 |
+
# Generate and save the inverse frequency buffer (non trainable)
|
| 42 |
+
self.inv_freq = mx.array(
|
| 43 |
+
1.0 / (10000 ** (mx.arange(0, dim, 2).astype(mx.float32) / dim))
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
self._seq_len_cached = None
|
| 47 |
+
self._cos_cached = None
|
| 48 |
+
self._sin_cached = None
|
| 49 |
+
|
| 50 |
+
def _update_cos_sin_tables(self, x, seq_dimension=1):
|
| 51 |
+
seq_len = x.shape[seq_dimension]
|
| 52 |
+
|
| 53 |
+
# Reset the tables if the sequence length has changed,
|
| 54 |
+
# or if we're on a new device (possibly due to tracing for instance)
|
| 55 |
+
if seq_len != self._seq_len_cached:
|
| 56 |
+
self._seq_len_cached = seq_len
|
| 57 |
+
t = mx.arange(x.shape[seq_dimension]).astype(self.inv_freq.dtype)
|
| 58 |
+
freqs = mx.einsum("i,j->ij", t, self.inv_freq)
|
| 59 |
+
emb = mx.concatenate([freqs, freqs], axis=-1)
|
| 60 |
+
|
| 61 |
+
self._cos_cached = emb.cos()[None, :, :]
|
| 62 |
+
self._sin_cached = emb.sin()[None, :, :]
|
| 63 |
+
|
| 64 |
+
return self._cos_cached, self._sin_cached
|
| 65 |
+
|
| 66 |
+
def __call__(self, q, k):
|
| 67 |
+
self._cos_cached, self._sin_cached = self._update_cos_sin_tables(
|
| 68 |
+
k, seq_dimension=-2
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return (
|
| 72 |
+
apply_rotary_pos_emb(q, self._cos_cached, self._sin_cached),
|
| 73 |
+
apply_rotary_pos_emb(k, self._cos_cached, self._sin_cached),
|
| 74 |
+
)
|
models/simplefold/mlx/layers.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
from einops.array_api import rearrange
|
| 8 |
+
from operator import __add__
|
| 9 |
+
import mlx.core as mx
|
| 10 |
+
import mlx.nn as nn
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def modulate(x, shift, scale):
|
| 14 |
+
return x * (1 + mx.expand_dims(scale, axis=1)) + mx.expand_dims(shift, axis=1)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
#################################################################################
|
| 18 |
+
# Attention Layers #
|
| 19 |
+
#################################################################################
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class SelfAttentionLayer(nn.Module):
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
hidden_size,
|
| 26 |
+
num_heads=8,
|
| 27 |
+
qkv_bias=False,
|
| 28 |
+
qk_scale=None,
|
| 29 |
+
attn_drop=0.0,
|
| 30 |
+
proj_drop=0.0,
|
| 31 |
+
use_bias=True,
|
| 32 |
+
qk_norm=True,
|
| 33 |
+
pos_embedder=None,
|
| 34 |
+
linear_target: nn.Module = nn.Linear,
|
| 35 |
+
):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.num_heads = num_heads
|
| 38 |
+
head_dim = hidden_size // num_heads
|
| 39 |
+
# NOTE scale factor was wrong in my original version,
|
| 40 |
+
# can set manually to be compat with prev weights
|
| 41 |
+
self.scale = qk_scale or head_dim**-0.5
|
| 42 |
+
|
| 43 |
+
self.qkv = linear_target(hidden_size, hidden_size * 3, bias=qkv_bias)
|
| 44 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 45 |
+
self.proj = linear_target(hidden_size, hidden_size, bias=use_bias)
|
| 46 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 47 |
+
|
| 48 |
+
self.q_norm = nn.RMSNorm(head_dim, eps=1e-8) if qk_norm else nn.Identity()
|
| 49 |
+
self.k_norm = nn.RMSNorm(head_dim, eps=1e-8) if qk_norm else nn.Identity()
|
| 50 |
+
|
| 51 |
+
self.pos_embedder = pos_embedder
|
| 52 |
+
|
| 53 |
+
def __call__(self, x, **kwargs):
|
| 54 |
+
B, N, C = x.shape
|
| 55 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
|
| 56 |
+
pos = kwargs.get("pos")
|
| 57 |
+
|
| 58 |
+
qkv = rearrange(qkv, "b n t h c -> t b h n c")
|
| 59 |
+
q, k, v = (
|
| 60 |
+
qkv[0],
|
| 61 |
+
qkv[1],
|
| 62 |
+
qkv[2],
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 66 |
+
|
| 67 |
+
if self.pos_embedder and pos is not None:
|
| 68 |
+
q, k = self.pos_embedder(q, k, pos)
|
| 69 |
+
|
| 70 |
+
attn = (q @ k.swapaxes(axis1=-2, axis2=-1)) * self.scale
|
| 71 |
+
attn = mx.softmax(attn, axis=-1)
|
| 72 |
+
attn = self.attn_drop(attn)
|
| 73 |
+
|
| 74 |
+
x = (attn @ v).swapaxes(axis1=1, axis2=2).reshape(B, N, C)
|
| 75 |
+
x = self.proj(x)
|
| 76 |
+
x = self.proj_drop(x)
|
| 77 |
+
return x
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class EfficientSelfAttentionLayer(SelfAttentionLayer):
|
| 81 |
+
"""Adapted from https://github.com/facebookresearch/dinov2/blob/main/dinov2/layers/attention.py"""
|
| 82 |
+
|
| 83 |
+
def __init__(
|
| 84 |
+
self,
|
| 85 |
+
*args,
|
| 86 |
+
**kwargs,
|
| 87 |
+
):
|
| 88 |
+
super().__init__(*args, **kwargs)
|
| 89 |
+
|
| 90 |
+
def __call__(self, x, **kwargs):
|
| 91 |
+
B, N, C = x.shape
|
| 92 |
+
attn_mask = kwargs.get("attention_mask")
|
| 93 |
+
pos = kwargs.get("pos")
|
| 94 |
+
|
| 95 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
|
| 96 |
+
qkv = rearrange(qkv, "b n t h c -> t b h n c")
|
| 97 |
+
|
| 98 |
+
q, k, v = (
|
| 99 |
+
qkv[0],
|
| 100 |
+
qkv[1],
|
| 101 |
+
qkv[2],
|
| 102 |
+
)
|
| 103 |
+
if attn_mask is not None:
|
| 104 |
+
attn_mask = attn_mask.astype(q.dtype)
|
| 105 |
+
|
| 106 |
+
# if self.pos_embedder and pos is not None:
|
| 107 |
+
q, k = self.pos_embedder(q, k, pos)
|
| 108 |
+
|
| 109 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 110 |
+
|
| 111 |
+
x = mx.fast.scaled_dot_product_attention(
|
| 112 |
+
q, k, v, mask=attn_mask, scale=1.0 / mx.sqrt(q.shape[-1])
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
x = x.swapaxes(axis1=1, axis2=2).reshape(B, N, C)
|
| 116 |
+
x = self.proj(x)
|
| 117 |
+
x = self.proj_drop(x)
|
| 118 |
+
|
| 119 |
+
return_attn = kwargs.get("return_attn", False)
|
| 120 |
+
if return_attn:
|
| 121 |
+
attn = (q @ k.swapaxes(axis1=-2, axis2=-1)) * self.scale
|
| 122 |
+
attn = attn.softmax(axis=-1)
|
| 123 |
+
return x, attn
|
| 124 |
+
|
| 125 |
+
return x, None
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def exists(val) -> bool:
|
| 129 |
+
"""returns whether val is not none"""
|
| 130 |
+
return val is not None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def default(x, y):
|
| 134 |
+
"""returns x if it exists, otherwise y"""
|
| 135 |
+
return x if exists(x) else y
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
#################################################################################
|
| 139 |
+
# FeedForward Layer #
|
| 140 |
+
#################################################################################
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class SwiGLUFeedForward(nn.Module):
|
| 144 |
+
def __init__(self, dim, hidden_dim, multiple_of=256):
|
| 145 |
+
super().__init__()
|
| 146 |
+
hidden_dim = int(2 * hidden_dim / 3)
|
| 147 |
+
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
|
| 148 |
+
|
| 149 |
+
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
|
| 150 |
+
self.w2 = nn.Linear(hidden_dim, dim, bias=True)
|
| 151 |
+
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
| 152 |
+
|
| 153 |
+
def __call__(self, x):
|
| 154 |
+
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
#################################################################################
|
| 158 |
+
# Utility Layers #
|
| 159 |
+
#################################################################################
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class TimestepEmbedder(nn.Module):
|
| 163 |
+
"""
|
| 164 |
+
Embeds scalar timesteps into vector representations.
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
def __init__(self, hidden_size, frequency_embedding_size=256):
|
| 168 |
+
super().__init__()
|
| 169 |
+
self.mlp = nn.Sequential(
|
| 170 |
+
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
| 171 |
+
nn.SiLU(),
|
| 172 |
+
nn.Linear(hidden_size, hidden_size, bias=True),
|
| 173 |
+
)
|
| 174 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 175 |
+
self.initialize_weights()
|
| 176 |
+
|
| 177 |
+
def initialize_weights(self):
|
| 178 |
+
nn.init.normal(self.mlp.layers[0].weight, std=0.02)
|
| 179 |
+
nn.init.normal(self.mlp.layers[2].weight, std=0.02)
|
| 180 |
+
|
| 181 |
+
@staticmethod
|
| 182 |
+
def timestep_embedding(t, dim, max_period=10000):
|
| 183 |
+
"""
|
| 184 |
+
Create sinusoidal timestep embeddings.
|
| 185 |
+
:param t: a 1-D Tensor of N indices, one per batch element.
|
| 186 |
+
These may be fractional.
|
| 187 |
+
:param dim: the dimension of the output.
|
| 188 |
+
:param max_period: controls the minimum frequency of the embeddings.
|
| 189 |
+
:return: an (N, D) Tensor of positional embeddings.
|
| 190 |
+
"""
|
| 191 |
+
# https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
|
| 192 |
+
half = dim // 2
|
| 193 |
+
freqs = mx.exp(
|
| 194 |
+
-math.log(max_period)
|
| 195 |
+
* mx.arange(start=0, stop=half, dtype=mx.float32)
|
| 196 |
+
/ half
|
| 197 |
+
)
|
| 198 |
+
args = t[:, None].astype(mx.float32) * freqs[None]
|
| 199 |
+
embedding = mx.concatenate([mx.cos(args), mx.sin(args)], axis=-1)
|
| 200 |
+
if dim % 2:
|
| 201 |
+
embedding = mx.concatenate(
|
| 202 |
+
[embedding, mx.zeros_like(embedding[:, :1])], axis=-1
|
| 203 |
+
)
|
| 204 |
+
return embedding
|
| 205 |
+
|
| 206 |
+
def __call__(self, t):
|
| 207 |
+
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
|
| 208 |
+
t_emb = self.mlp(t_freq)
|
| 209 |
+
return t_emb
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class ConditionEmbedder(nn.Module):
|
| 213 |
+
"""
|
| 214 |
+
Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
|
| 215 |
+
"""
|
| 216 |
+
def __init__(self, input_dim, hidden_size, dropout_prob):
|
| 217 |
+
super().__init__()
|
| 218 |
+
self.proj = nn.Sequential(
|
| 219 |
+
nn.Linear(input_dim, hidden_size),
|
| 220 |
+
nn.LayerNorm(hidden_size),
|
| 221 |
+
nn.SiLU(),
|
| 222 |
+
)
|
| 223 |
+
self.dropout_prob = dropout_prob
|
| 224 |
+
self.null_token = mx.zeros(input_dim)
|
| 225 |
+
|
| 226 |
+
def token_drop(self, cond, force_drop_ids=None):
|
| 227 |
+
"""
|
| 228 |
+
cond: (B, N, D)
|
| 229 |
+
Drops conditions to enable classifier-free guidance.
|
| 230 |
+
"""
|
| 231 |
+
if force_drop_ids is None:
|
| 232 |
+
drop_ids = mx.random.uniform(cond.shape[0]) < self.dropout_prob
|
| 233 |
+
else:
|
| 234 |
+
drop_ids = force_drop_ids
|
| 235 |
+
cond[drop_ids] = self.null_token[None, None, :]
|
| 236 |
+
return cond
|
| 237 |
+
|
| 238 |
+
def __call__(self, cond, train, force_drop_ids=None):
|
| 239 |
+
use_dropout = self.dropout_prob > 0
|
| 240 |
+
if (train and use_dropout) or (force_drop_ids is not None):
|
| 241 |
+
cond = self.token_drop(cond, force_drop_ids)
|
| 242 |
+
embeddings = self.proj(cond)
|
| 243 |
+
return embeddings
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
class FinalLayer(nn.Module):
|
| 247 |
+
"""
|
| 248 |
+
The final layer of DiT.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
def __init__(self, hidden_size, out_channels, c_dim=None):
|
| 252 |
+
super().__init__()
|
| 253 |
+
self.norm_final = nn.LayerNorm(hidden_size, affine=False, eps=1e-6)
|
| 254 |
+
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
| 255 |
+
self.adaLN_modulation = nn.Sequential(
|
| 256 |
+
nn.SiLU(), nn.Linear(c_dim, 2 * hidden_size, bias=True)
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def __call__(self, x, c):
|
| 260 |
+
shift, scale = self.adaLN_modulation(c).split(2, axis=1)
|
| 261 |
+
x = modulate(self.norm_final(x), shift, scale)
|
| 262 |
+
x = self.linear(x)
|
| 263 |
+
return x
|
models/simplefold/mlx/pos_embed.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import mlx.core as mx
|
| 7 |
+
import mlx.nn as nn
|
| 8 |
+
import math
|
| 9 |
+
from einops.array_api import rearrange
|
| 10 |
+
import torch
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AbsolutePositionEncoding(nn.Module):
|
| 15 |
+
def __init__(self, in_dim, embed_dim, include_input=False):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.in_dim = in_dim
|
| 18 |
+
self.hidden_dim = embed_dim
|
| 19 |
+
self.include_input = include_input
|
| 20 |
+
assert embed_dim % in_dim == 0, "embed_dim must be divisible by in_dim"
|
| 21 |
+
self.embed_dim = embed_dim + in_dim if include_input else embed_dim
|
| 22 |
+
|
| 23 |
+
def __call__(self, pos):
|
| 24 |
+
pos_embs = []
|
| 25 |
+
for i in range(self.in_dim):
|
| 26 |
+
pe = self.get_1d_pos_embed(pos[..., i])
|
| 27 |
+
pos_embs.append(pe)
|
| 28 |
+
if self.include_input:
|
| 29 |
+
pos_embs.append(pos)
|
| 30 |
+
pos_embs = mx.concatenate(pos_embs, axis=-1)
|
| 31 |
+
return pos_embs
|
| 32 |
+
|
| 33 |
+
def get_1d_pos_embed(self, pos):
|
| 34 |
+
"""
|
| 35 |
+
https://github.com/facebookresearch/DiT/blob/main/models.py#L303
|
| 36 |
+
"""
|
| 37 |
+
embed_dim = self.hidden_dim // (self.in_dim * 2)
|
| 38 |
+
omega = 2 ** mx.linspace(0, math.log(224, 2) - 1, embed_dim).astype(mx.float32)
|
| 39 |
+
omega *= math.pi
|
| 40 |
+
|
| 41 |
+
if len(pos.shape) == 1:
|
| 42 |
+
out = mx.einsum("m,d->md", pos, omega) # (M, D/2), outer product
|
| 43 |
+
elif len(pos.shape) == 2:
|
| 44 |
+
out = mx.einsum("nm,d->nmd", pos, omega)
|
| 45 |
+
|
| 46 |
+
emb_sin = mx.sin(out)
|
| 47 |
+
emb_cos = mx.cos(out) # (*, M, D/2)
|
| 48 |
+
emb = mx.concatenate([emb_sin, emb_cos], axis=-1) # (*, M, D)
|
| 49 |
+
return emb
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class FourierPositionEncoding(nn.Module):
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
in_dim: int,
|
| 56 |
+
include_input: bool = False,
|
| 57 |
+
min_freq_log2: float = 0,
|
| 58 |
+
max_freq_log2: float = 12,
|
| 59 |
+
num_freqs: int = 32,
|
| 60 |
+
log_sampling: bool = True,
|
| 61 |
+
):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.in_dim = in_dim
|
| 64 |
+
self.include_input = include_input
|
| 65 |
+
self.min_freq_log2 = min_freq_log2
|
| 66 |
+
self.max_freq_log2 = max_freq_log2
|
| 67 |
+
self.num_freqs = num_freqs
|
| 68 |
+
self.log_sampling = log_sampling
|
| 69 |
+
self.create_embedding_fn()
|
| 70 |
+
|
| 71 |
+
def create_embedding_fn(self):
|
| 72 |
+
d = self.in_dim
|
| 73 |
+
dim_out = 0
|
| 74 |
+
if self.include_input:
|
| 75 |
+
dim_out += d
|
| 76 |
+
|
| 77 |
+
min_freq = self.min_freq_log2
|
| 78 |
+
max_freq = self.max_freq_log2
|
| 79 |
+
N_freqs = self.num_freqs
|
| 80 |
+
|
| 81 |
+
if self.log_sampling:
|
| 82 |
+
freq_bands = 2.0 ** mx.linspace(min_freq, max_freq, num=N_freqs) # (nf,)
|
| 83 |
+
else:
|
| 84 |
+
freq_bands = mx.linspace(2.0**min_freq, 2.0**max_freq, num=N_freqs) # (nf,)
|
| 85 |
+
|
| 86 |
+
assert mx.isfinite(
|
| 87 |
+
freq_bands
|
| 88 |
+
).all(), f"nan: {mx.isnan(freq_bands).any()} inf: {mx.isinf(freq_bands).any()}"
|
| 89 |
+
|
| 90 |
+
self.freq_bands = freq_bands
|
| 91 |
+
self.embed_dim = dim_out + d * self.freq_bands.size * 2
|
| 92 |
+
|
| 93 |
+
def __call__(
|
| 94 |
+
self,
|
| 95 |
+
pos,
|
| 96 |
+
):
|
| 97 |
+
"""
|
| 98 |
+
Get the positional encoding for each coordinate.
|
| 99 |
+
Args:
|
| 100 |
+
pos:
|
| 101 |
+
(*, in_dim)
|
| 102 |
+
Returns:
|
| 103 |
+
out:
|
| 104 |
+
(*, in_dimitional_encoding)
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
out = []
|
| 108 |
+
if self.include_input:
|
| 109 |
+
out = [pos] # (*, in_dim)
|
| 110 |
+
|
| 111 |
+
pos = pos[..., None] * self.freq_bands # (*b, d, nf)
|
| 112 |
+
|
| 113 |
+
out += [
|
| 114 |
+
mx.sin(pos).flatten(start_axis=-2), # (*b, d*nf)
|
| 115 |
+
mx.cos(pos).flatten(start_axis=-2), # (*b, d*nf)
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
out = mx.concatenate(out, axis=-1) # (*b, 2 * in_dim * nf (+ in_dim))
|
| 119 |
+
return out
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def compute_axial_cis(
|
| 123 |
+
ts,
|
| 124 |
+
in_dim: int,
|
| 125 |
+
dim: int,
|
| 126 |
+
theta: float = 100.0,
|
| 127 |
+
):
|
| 128 |
+
B, N, D = ts.shape
|
| 129 |
+
freqs_all = []
|
| 130 |
+
interval = 2 * in_dim
|
| 131 |
+
for i in range(in_dim):
|
| 132 |
+
freq = 1.0 / (
|
| 133 |
+
theta
|
| 134 |
+
** (
|
| 135 |
+
mx.arange(0, dim, interval)[: (dim // interval)].astype(mx.float32)
|
| 136 |
+
/ dim
|
| 137 |
+
)
|
| 138 |
+
)
|
| 139 |
+
t = ts[..., i].flatten()
|
| 140 |
+
freq_i = mx.outer(t, freq)
|
| 141 |
+
freq_cis_i = polar(mx.ones_like(freq_i), freq_i)
|
| 142 |
+
freq_cis_i = freq_cis_i.reshape((B, N, -1))
|
| 143 |
+
freqs_all.append(freq_cis_i)
|
| 144 |
+
freqs_cis = mx.concatenate(freqs_all, axis=-1)
|
| 145 |
+
return freqs_cis
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def polar(a, b):
|
| 149 |
+
return a * mx.exp(1j * b)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def view_as_complex(x):
|
| 153 |
+
x = x.astype(mx.float32)
|
| 154 |
+
x = x.reshape(*x.shape[:-1], -1, 2) # (..., dim/2, 2)
|
| 155 |
+
return x[..., 0] + 1j * x[..., 1] # real, imag
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def view_as_real(input):
|
| 159 |
+
return mx.stack([input.real, input.imag], axis=-1) # (..., dim)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def apply_rotary_emb(xq: mx.array, xk: mx.array, freqs_cis: mx.array):
|
| 163 |
+
# xq, xk: shape (..., dim)
|
| 164 |
+
# freqs_cis: shape (..., dim // 2, 2) where last dim = [cos, sin]
|
| 165 |
+
xq_ = view_as_complex(xq)
|
| 166 |
+
xk_ = view_as_complex(xk)
|
| 167 |
+
|
| 168 |
+
# Apply complex multiplication
|
| 169 |
+
xq_out = xq_ * freqs_cis
|
| 170 |
+
xk_out = xk_ * freqs_cis
|
| 171 |
+
|
| 172 |
+
# Reconstruct to original shape
|
| 173 |
+
xq_out = view_as_real(xq_out).flatten(start_axis=3)
|
| 174 |
+
xk_out = view_as_real(xk_out).flatten(start_axis=3)
|
| 175 |
+
|
| 176 |
+
return xq_out.astype(xq.dtype), xk_out.astype(xk.dtype)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class AxialRotaryPositionEncoding(nn.Module):
|
| 180 |
+
def __init__(
|
| 181 |
+
self,
|
| 182 |
+
in_dim,
|
| 183 |
+
embed_dim,
|
| 184 |
+
num_heads,
|
| 185 |
+
base=100.0,
|
| 186 |
+
):
|
| 187 |
+
super().__init__()
|
| 188 |
+
self.in_dim = in_dim
|
| 189 |
+
self.num_heads = num_heads
|
| 190 |
+
self.embed_dim = embed_dim // num_heads
|
| 191 |
+
self.base = base
|
| 192 |
+
|
| 193 |
+
def __call__(self, xq, xk, pos):
|
| 194 |
+
"""
|
| 195 |
+
xq: [B, H, N, D]
|
| 196 |
+
xk: [B, H, N, D]
|
| 197 |
+
pos: [B, N, in_dim]
|
| 198 |
+
"""
|
| 199 |
+
if pos.ndim == 2:
|
| 200 |
+
pos = pos[..., None]
|
| 201 |
+
freqs_cis = compute_axial_cis(pos, self.in_dim, self.embed_dim, self.base)
|
| 202 |
+
freqs_cis = mx.expand_dims(freqs_cis, axis=1)
|
| 203 |
+
|
| 204 |
+
return apply_rotary_emb(xq, xk, freqs_cis)
|
models/simplefold/mlx/sampler.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import mlx.core as mx
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from einops.array_api import repeat
|
| 9 |
+
from onescience.utils.simplefold.mlx_utils import center_random_augmentation
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def logspace(start, end, steps, base=10.0, dtype=mx.float32):
|
| 13 |
+
# create a linear space between start and end
|
| 14 |
+
lin = mx.linspace(start, end, steps, dtype=dtype)
|
| 15 |
+
# raise base to that power
|
| 16 |
+
return mx.power(mx.array(base, dtype=dtype), lin)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class EMSampler:
|
| 20 |
+
"""
|
| 21 |
+
A Euler-Maruyama solver for SDEs.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
num_timesteps=500,
|
| 27 |
+
t_start=1e-4,
|
| 28 |
+
tau=0.3,
|
| 29 |
+
log_timesteps=False,
|
| 30 |
+
w_cutoff=0.99,
|
| 31 |
+
):
|
| 32 |
+
self.num_timesteps = num_timesteps
|
| 33 |
+
self.log_timesteps = log_timesteps
|
| 34 |
+
self.t_start = t_start
|
| 35 |
+
self.tau = tau
|
| 36 |
+
self.w_cutoff = w_cutoff
|
| 37 |
+
|
| 38 |
+
if self.log_timesteps:
|
| 39 |
+
t = 1.0 - logspace(-2, 0, steps=self.num_timesteps + 1)[::-1, ...]
|
| 40 |
+
t = t - mx.min(t)
|
| 41 |
+
t = t / mx.max(t)
|
| 42 |
+
self.steps = mx.clip(t, a_min=self.t_start, a_max=1.0)
|
| 43 |
+
else:
|
| 44 |
+
self.steps = mx.linspace(self.t_start, 1.0, num=self.num_timesteps + 1)
|
| 45 |
+
|
| 46 |
+
def diffusion_coefficient(self, t, eps=0.01):
|
| 47 |
+
# determine diffusion coefficient
|
| 48 |
+
w = (1.0 - t) / (t + eps)
|
| 49 |
+
if t >= self.w_cutoff:
|
| 50 |
+
w = 0.0
|
| 51 |
+
return w
|
| 52 |
+
|
| 53 |
+
def euler_maruyama_step(
|
| 54 |
+
self,
|
| 55 |
+
model_fn,
|
| 56 |
+
flow,
|
| 57 |
+
y,
|
| 58 |
+
t,
|
| 59 |
+
t_next,
|
| 60 |
+
batch,
|
| 61 |
+
):
|
| 62 |
+
dt = t_next - t
|
| 63 |
+
eps = mx.random.normal(y.shape)
|
| 64 |
+
|
| 65 |
+
y = center_random_augmentation(
|
| 66 |
+
y,
|
| 67 |
+
batch["atom_pad_mask"],
|
| 68 |
+
augmentation=False,
|
| 69 |
+
centering=True,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
batched_t = repeat(t, " -> b", b=y.shape[0])
|
| 73 |
+
velocity = model_fn(
|
| 74 |
+
noised_pos=y,
|
| 75 |
+
t=batched_t,
|
| 76 |
+
feats=batch,
|
| 77 |
+
)["predict_velocity"]
|
| 78 |
+
|
| 79 |
+
score = flow.compute_score_from_velocity(velocity, y, t)
|
| 80 |
+
|
| 81 |
+
diff_coeff = self.diffusion_coefficient(t)
|
| 82 |
+
drift = velocity + diff_coeff * score
|
| 83 |
+
mean_y = y + drift * dt
|
| 84 |
+
y_sample = mean_y + mx.sqrt(2.0 * dt * diff_coeff * self.tau) * eps
|
| 85 |
+
|
| 86 |
+
return y_sample
|
| 87 |
+
|
| 88 |
+
def sample(self, model_fn, flow, noise, batch):
|
| 89 |
+
sampling_timesteps = self.num_timesteps
|
| 90 |
+
steps = self.steps
|
| 91 |
+
y_sampled = noise
|
| 92 |
+
feats = batch
|
| 93 |
+
|
| 94 |
+
for i in tqdm(
|
| 95 |
+
range(sampling_timesteps),
|
| 96 |
+
desc="Sampling",
|
| 97 |
+
total=sampling_timesteps,
|
| 98 |
+
):
|
| 99 |
+
t = steps[i]
|
| 100 |
+
t_next = steps[i + 1]
|
| 101 |
+
|
| 102 |
+
y_sampled = self.euler_maruyama_step(
|
| 103 |
+
model_fn,
|
| 104 |
+
flow,
|
| 105 |
+
y_sampled,
|
| 106 |
+
t,
|
| 107 |
+
t_next,
|
| 108 |
+
feats,
|
| 109 |
+
)
|
| 110 |
+
mx.eval(y_sampled)
|
| 111 |
+
|
| 112 |
+
return {"denoised_coords": y_sampled}
|
models/simplefold/simplefold.py
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import shutil
|
| 8 |
+
import copy
|
| 9 |
+
import numpy as np
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from einops import repeat
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
from torch.optim.optimizer import Optimizer
|
| 16 |
+
import torch.distributed as dist
|
| 17 |
+
from torch.distributed.fsdp import FullyShardedDataParallel
|
| 18 |
+
from torch.optim.swa_utils import AveragedModel
|
| 19 |
+
from torch.nn.utils import clip_grad_norm_
|
| 20 |
+
|
| 21 |
+
import lightning
|
| 22 |
+
import lightning.pytorch as pl
|
| 23 |
+
from fairscale.nn.data_parallel import FullyShardedDataParallel as FSDP
|
| 24 |
+
from fairscale.nn.wrap import enable_wrap, wrap
|
| 25 |
+
|
| 26 |
+
from onescience.utils.simplefold.esm_utils import _af2_to_esm, esm_registry
|
| 27 |
+
from onescience.datapipes.boltz_data_pipeline.types import Record, Structure
|
| 28 |
+
from onescience.utils.simplefold.boltz_utils import (
|
| 29 |
+
weighted_rigid_align,
|
| 30 |
+
center_random_augmentation,
|
| 31 |
+
process_structure,
|
| 32 |
+
save_structure
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def logit_normal_sample(n=1, m=0.0, s=1.0):
|
| 37 |
+
# Logit-Normal Sampling from https://arxiv.org/pdf/2403.03206.pdf
|
| 38 |
+
u = torch.randn(n) * s + m
|
| 39 |
+
t = 1 / (1 + torch.exp(-u))
|
| 40 |
+
return t
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def lddt_dist(dmat_predicted, dmat_true, mask, cutoff=15.0, per_atom=False):
|
| 44 |
+
# NOTE: the mask is a pairwise mask which should have the identity elements already masked out
|
| 45 |
+
# Compute mask over distances
|
| 46 |
+
dists_to_score = (dmat_true < cutoff).float() * mask
|
| 47 |
+
dist_l1 = torch.abs(dmat_true - dmat_predicted)
|
| 48 |
+
|
| 49 |
+
score = 0.25 * (
|
| 50 |
+
(dist_l1 < 0.5).float()
|
| 51 |
+
+ (dist_l1 < 1.0).float()
|
| 52 |
+
+ (dist_l1 < 2.0).float()
|
| 53 |
+
+ (dist_l1 < 4.0).float()
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# Normalize over the appropriate axes.
|
| 57 |
+
if per_atom:
|
| 58 |
+
mask_no_match = torch.sum(dists_to_score, dim=-1) != 0
|
| 59 |
+
norm = 1.0 / (1e-10 + torch.sum(dists_to_score, dim=-1))
|
| 60 |
+
score = norm * (1e-10 + torch.sum(dists_to_score * score, dim=-1))
|
| 61 |
+
return score, mask_no_match.float()
|
| 62 |
+
else:
|
| 63 |
+
norm = 1.0 / (1e-10 + torch.sum(dists_to_score, dim=(-2, -1)))
|
| 64 |
+
score = norm * (1e-10 + torch.sum(dists_to_score * score, dim=(-2, -1)))
|
| 65 |
+
total = torch.sum(dists_to_score, dim=(-1, -2))
|
| 66 |
+
return score, total
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class SimpleFold(pl.LightningModule):
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
architecture,
|
| 73 |
+
processor,
|
| 74 |
+
loss,
|
| 75 |
+
path,
|
| 76 |
+
sampler,
|
| 77 |
+
optimizer=None,
|
| 78 |
+
scheduler=None,
|
| 79 |
+
plddt_module=None,
|
| 80 |
+
ema_decay=0.999,
|
| 81 |
+
esm_model="esm2_3B",
|
| 82 |
+
aa_bolt_link=None,
|
| 83 |
+
use_rigid_align=True,
|
| 84 |
+
smooth_lddt_loss_weight=1.0,
|
| 85 |
+
lddt_cutoff=15.0,
|
| 86 |
+
clip_grad_norm_val=None,
|
| 87 |
+
lddt_weight_schedule=False,
|
| 88 |
+
plddt_training=False,
|
| 89 |
+
sample_dir='artifacts/',
|
| 90 |
+
):
|
| 91 |
+
super().__init__()
|
| 92 |
+
self.save_hyperparameters(logger=False)
|
| 93 |
+
|
| 94 |
+
self.model = architecture
|
| 95 |
+
self.model_ema = AveragedModel(
|
| 96 |
+
self.model,
|
| 97 |
+
multi_avg_fn=torch.optim.swa_utils.get_ema_multi_avg_fn(
|
| 98 |
+
self.hparams.ema_decay
|
| 99 |
+
),
|
| 100 |
+
use_buffers=True,
|
| 101 |
+
)
|
| 102 |
+
self.model_ema.eval()
|
| 103 |
+
|
| 104 |
+
self.loss = loss
|
| 105 |
+
self.path = path
|
| 106 |
+
self.sampler = sampler
|
| 107 |
+
|
| 108 |
+
self.use_rigid_align = use_rigid_align
|
| 109 |
+
self.lddt_cutoff = lddt_cutoff
|
| 110 |
+
self.smooth_lddt_loss_weight = smooth_lddt_loss_weight
|
| 111 |
+
self.use_smooth_lddt_loss = smooth_lddt_loss_weight > 0.0
|
| 112 |
+
self.lddt_weight_schedule = lddt_weight_schedule
|
| 113 |
+
self.plddt_training = plddt_training
|
| 114 |
+
self.sample_dir = sample_dir
|
| 115 |
+
|
| 116 |
+
self.aa_bolt_link = aa_bolt_link
|
| 117 |
+
self.nval_steps = 0
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
self.t_eps = self.sampler.t_eps
|
| 121 |
+
except AttributeError:
|
| 122 |
+
self.t_eps = 0.0
|
| 123 |
+
|
| 124 |
+
self.use_esm = esm_model is not None
|
| 125 |
+
if self.use_esm:
|
| 126 |
+
self.esm_model, self.esm_dict = esm_registry[esm_model]()
|
| 127 |
+
self.esm_model.eval()
|
| 128 |
+
self.af2_to_esm = _af2_to_esm(self.esm_dict)
|
| 129 |
+
print(f"Using ESM model: {esm_model}")
|
| 130 |
+
else:
|
| 131 |
+
self.esm_model = None
|
| 132 |
+
self.esm_dict = None
|
| 133 |
+
self.af2_to_esm = None
|
| 134 |
+
|
| 135 |
+
self.plddt_module = plddt_module
|
| 136 |
+
if self.plddt_training:
|
| 137 |
+
assert self.plddt_module is not None, "PLDDT module must be provided for PLDDT training"
|
| 138 |
+
self.model.eval()
|
| 139 |
+
|
| 140 |
+
def register(self, name, tensor):
|
| 141 |
+
self.register_buffer(name, tensor.type(torch.float32))
|
| 142 |
+
|
| 143 |
+
def loss_masking(self, loss, atom_mask):
|
| 144 |
+
loss_mask = repeat(atom_mask, "b s -> b s d", d=loss.shape[-1])
|
| 145 |
+
loss *= loss_mask
|
| 146 |
+
|
| 147 |
+
denom = torch.sum(atom_mask, -1, keepdim=True)
|
| 148 |
+
denom = denom.unsqueeze(-1)
|
| 149 |
+
loss = torch.sum(loss, dim=1, keepdim=True) / denom
|
| 150 |
+
return loss
|
| 151 |
+
|
| 152 |
+
def smooth_lddt_loss(
|
| 153 |
+
self,
|
| 154 |
+
pred_coords,
|
| 155 |
+
true_coords,
|
| 156 |
+
# is_nucleotide,
|
| 157 |
+
coords_mask,
|
| 158 |
+
t,
|
| 159 |
+
):
|
| 160 |
+
"""Compute weighted alignment.
|
| 161 |
+
|
| 162 |
+
Parameters
|
| 163 |
+
----------
|
| 164 |
+
pred_coords: torch.Tensor
|
| 165 |
+
The predicted atom coordinates
|
| 166 |
+
true_coords: torch.Tensor
|
| 167 |
+
The ground truth atom coordinates
|
| 168 |
+
coords_mask: torch.Tensor
|
| 169 |
+
The atoms mask
|
| 170 |
+
|
| 171 |
+
"""
|
| 172 |
+
B, N, _ = true_coords.shape
|
| 173 |
+
true_dists = torch.cdist(true_coords, true_coords)
|
| 174 |
+
|
| 175 |
+
mask = (true_dists < self.lddt_cutoff).float()
|
| 176 |
+
mask = mask * (1 - torch.eye(pred_coords.shape[1], device=pred_coords.device))
|
| 177 |
+
mask = mask * (coords_mask.unsqueeze(-1) * coords_mask.unsqueeze(-2))
|
| 178 |
+
|
| 179 |
+
# Compute distances between all pairs of atoms
|
| 180 |
+
pred_dists = torch.cdist(pred_coords, pred_coords)
|
| 181 |
+
dist_diff = torch.abs(true_dists - pred_dists)
|
| 182 |
+
|
| 183 |
+
# Compute epsilon values
|
| 184 |
+
eps = (
|
| 185 |
+
(
|
| 186 |
+
(
|
| 187 |
+
F.sigmoid(0.5 - dist_diff)
|
| 188 |
+
+ F.sigmoid(1.0 - dist_diff)
|
| 189 |
+
+ F.sigmoid(2.0 - dist_diff)
|
| 190 |
+
+ F.sigmoid(4.0 - dist_diff)
|
| 191 |
+
)
|
| 192 |
+
/ 4.0
|
| 193 |
+
)
|
| 194 |
+
.view(B, N, N)
|
| 195 |
+
.mean(dim=0)
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Calculate masked averaging
|
| 199 |
+
num = (eps * mask).sum(dim=(-1, -2))
|
| 200 |
+
den = mask.sum(dim=(-1, -2)).clamp(min=1)
|
| 201 |
+
lddt = num / den
|
| 202 |
+
if self.lddt_weight_schedule:
|
| 203 |
+
t_weight = 1 + 8 * torch.relu(t - 0.5)
|
| 204 |
+
lddt = (1.0 - lddt) * t_weight
|
| 205 |
+
return lddt.mean()
|
| 206 |
+
else:
|
| 207 |
+
return (1.0 - lddt.mean()) * self.smooth_lddt_loss_weight
|
| 208 |
+
|
| 209 |
+
def plddt_loss(
|
| 210 |
+
self,
|
| 211 |
+
pred_lddt,
|
| 212 |
+
pred_atom_coords,
|
| 213 |
+
true_atom_coords,
|
| 214 |
+
true_coords_resolved_mask,
|
| 215 |
+
feats,
|
| 216 |
+
# multiplicity=1,
|
| 217 |
+
):
|
| 218 |
+
"""Compute plddt loss.
|
| 219 |
+
|
| 220 |
+
Parameters
|
| 221 |
+
----------
|
| 222 |
+
pred_lddt: torch.Tensor
|
| 223 |
+
The plddt logits
|
| 224 |
+
pred_atom_coords: torch.Tensor
|
| 225 |
+
The predicted atom coordinates
|
| 226 |
+
true_atom_coords: torch.Tensor
|
| 227 |
+
The atom coordinates after symmetry correction
|
| 228 |
+
true_coords_resolved_mask: torch.Tensor
|
| 229 |
+
The resolved mask after symmetry correction
|
| 230 |
+
feats: Dict[str, torch.Tensor]
|
| 231 |
+
Dictionary containing the model input
|
| 232 |
+
|
| 233 |
+
Returns
|
| 234 |
+
-------
|
| 235 |
+
torch.Tensor
|
| 236 |
+
Plddt loss
|
| 237 |
+
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
# extract necessary features
|
| 241 |
+
atom_mask = true_coords_resolved_mask
|
| 242 |
+
|
| 243 |
+
R_set_to_rep_atom = feats["r_set_to_rep_atom"].float()
|
| 244 |
+
# R_set_to_rep_atom = R_set_to_rep_atom.repeat_interleave(multiplicity, 0).float()
|
| 245 |
+
|
| 246 |
+
token_type = feats["mol_type"]
|
| 247 |
+
# token_type = token_type.repeat_interleave(multiplicity, 0)
|
| 248 |
+
# is_nucleotide_token = (token_type == const.chain_type_ids["DNA"]).float() + (
|
| 249 |
+
# token_type == const.chain_type_ids["RNA"]
|
| 250 |
+
# ).float()
|
| 251 |
+
|
| 252 |
+
B = true_atom_coords.shape[0]
|
| 253 |
+
|
| 254 |
+
# atom_to_token = feats["atom_to_token"].float()
|
| 255 |
+
# atom_to_token = atom_to_token.repeat_interleave(multiplicity, 0)
|
| 256 |
+
|
| 257 |
+
token_to_rep_atom = feats["token_to_rep_atom"].float()
|
| 258 |
+
# token_to_rep_atom = token_to_rep_atom.repeat_interleave(multiplicity, 0)
|
| 259 |
+
|
| 260 |
+
true_token_coords = torch.bmm(token_to_rep_atom, true_atom_coords)
|
| 261 |
+
pred_token_coords = torch.bmm(token_to_rep_atom, pred_atom_coords)
|
| 262 |
+
|
| 263 |
+
# compute true lddt
|
| 264 |
+
true_d = torch.cdist(
|
| 265 |
+
true_token_coords,
|
| 266 |
+
torch.bmm(R_set_to_rep_atom, true_atom_coords),
|
| 267 |
+
)
|
| 268 |
+
pred_d = torch.cdist(
|
| 269 |
+
pred_token_coords,
|
| 270 |
+
torch.bmm(R_set_to_rep_atom, pred_atom_coords),
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# compute mask
|
| 274 |
+
pair_mask = atom_mask.unsqueeze(-1) * atom_mask.unsqueeze(-2)
|
| 275 |
+
pair_mask = (
|
| 276 |
+
pair_mask
|
| 277 |
+
* (1 - torch.eye(pair_mask.shape[1], device=pair_mask.device))[None, :, :]
|
| 278 |
+
)
|
| 279 |
+
pair_mask = torch.einsum("bnm,bkm->bnk", pair_mask, R_set_to_rep_atom)
|
| 280 |
+
pair_mask = torch.bmm(token_to_rep_atom, pair_mask)
|
| 281 |
+
atom_mask = torch.bmm(token_to_rep_atom, atom_mask.unsqueeze(-1).float())
|
| 282 |
+
# is_nucleotide_R_element = torch.bmm(
|
| 283 |
+
# R_set_to_rep_atom, torch.bmm(atom_to_token, is_nucleotide_token.unsqueeze(-1))
|
| 284 |
+
# ).squeeze(-1)
|
| 285 |
+
# cutoff = 15 + 15 * is_nucleotide_R_element.reshape(B, 1, -1).repeat(
|
| 286 |
+
# 1, true_d.shape[1], 1
|
| 287 |
+
# )
|
| 288 |
+
|
| 289 |
+
# compute lddt
|
| 290 |
+
target_lddt, mask_no_match = lddt_dist(
|
| 291 |
+
pred_d, true_d, pair_mask, cutoff=15.0, per_atom=True
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# compute loss
|
| 295 |
+
num_bins = pred_lddt.shape[-1]
|
| 296 |
+
bin_index = torch.floor(target_lddt * num_bins).long()
|
| 297 |
+
bin_index = torch.clamp(bin_index, max=(num_bins - 1))
|
| 298 |
+
lddt_one_hot = F.one_hot(bin_index, num_classes=num_bins)
|
| 299 |
+
errors = -1 * torch.sum(
|
| 300 |
+
lddt_one_hot * F.log_softmax(pred_lddt, dim=-1),
|
| 301 |
+
dim=-1,
|
| 302 |
+
)
|
| 303 |
+
atom_mask = atom_mask.squeeze(-1)
|
| 304 |
+
loss = torch.sum(errors * atom_mask * mask_no_match, dim=-1) / (
|
| 305 |
+
1e-7 + torch.sum(atom_mask * mask_no_match, dim=-1)
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Average over the batch dimension
|
| 309 |
+
loss = torch.mean(loss)
|
| 310 |
+
|
| 311 |
+
self.log(
|
| 312 |
+
"loss/plddt",
|
| 313 |
+
loss.item(),
|
| 314 |
+
on_epoch=True,
|
| 315 |
+
logger=True,
|
| 316 |
+
prog_bar=True,
|
| 317 |
+
rank_zero_only=True,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
return loss
|
| 321 |
+
|
| 322 |
+
def plddt_train_step(self, batch, batch_idx):
|
| 323 |
+
with torch.no_grad():
|
| 324 |
+
batch = self.processor.preprocess_training(
|
| 325 |
+
batch,
|
| 326 |
+
esm_model=self.esm_model,
|
| 327 |
+
esm_dict=self.esm_dict,
|
| 328 |
+
af2_to_esm=self.af2_to_esm,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
noise = torch.randn_like(batch['coords']).to(self.device)
|
| 332 |
+
|
| 333 |
+
out_dict = self.sampler.sample(
|
| 334 |
+
self.model_ema.module.forward, self.path,
|
| 335 |
+
noise, batch
|
| 336 |
+
)
|
| 337 |
+
# out_dict = self.processor.postprocess(out_dict, batch)
|
| 338 |
+
|
| 339 |
+
# denoised_coords = center_of_mass_norm(
|
| 340 |
+
# out_dict["denoised_coords"], batch['atom_pad_mask']
|
| 341 |
+
# )
|
| 342 |
+
# true_coords = center_of_mass_norm(
|
| 343 |
+
# batch['coords'], batch['atom_pad_mask']
|
| 344 |
+
# )
|
| 345 |
+
denoised_coords = center_random_augmentation(
|
| 346 |
+
out_dict["denoised_coords"],
|
| 347 |
+
batch['atom_pad_mask'],
|
| 348 |
+
augmentation=False,
|
| 349 |
+
centering=True,
|
| 350 |
+
)
|
| 351 |
+
true_coords = center_random_augmentation(
|
| 352 |
+
batch['coords'],
|
| 353 |
+
batch['atom_pad_mask'],
|
| 354 |
+
augmentation=False,
|
| 355 |
+
centering=True,
|
| 356 |
+
)
|
| 357 |
+
out_dict["denoised_coords"] = denoised_coords * self.processor.scale
|
| 358 |
+
out_dict["coords"] = true_coords * self.processor.scale
|
| 359 |
+
|
| 360 |
+
out_dict["true_coords_resolved_mask"] = batch["atom_resolved_mask"]
|
| 361 |
+
|
| 362 |
+
t = torch.ones(batch['coords'].shape[0], device=self.device)
|
| 363 |
+
out_feat = self.model(denoised_coords, t, batch) # use unscaled coords
|
| 364 |
+
|
| 365 |
+
# Compute plddt loss
|
| 366 |
+
plddt_out_dict = self.plddt_module(
|
| 367 |
+
out_feat["latent"].detach(),
|
| 368 |
+
batch,
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
plddt_loss = self.plddt_loss(
|
| 372 |
+
plddt_out_dict["plddt_logits"],
|
| 373 |
+
out_dict["denoised_coords"],
|
| 374 |
+
out_dict["coords"],
|
| 375 |
+
out_dict["true_coords_resolved_mask"],
|
| 376 |
+
batch,
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
return plddt_loss
|
| 380 |
+
|
| 381 |
+
def flow_matching_train_step(self, batch, batch_idx):
|
| 382 |
+
batch = self.processor.preprocess_training(
|
| 383 |
+
batch,
|
| 384 |
+
esm_model=self.esm_model,
|
| 385 |
+
esm_dict=self.esm_dict,
|
| 386 |
+
af2_to_esm=self.af2_to_esm,
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
# timestep resampling
|
| 390 |
+
t_size = batch['coords'].shape[0]
|
| 391 |
+
t = 0.98 * logit_normal_sample(n=t_size, m=0.8, s=1.7) + 0.02 * torch.rand(t_size)
|
| 392 |
+
t = t.to(self.device)
|
| 393 |
+
t = t * (1 - 2 * self.t_eps) + self.t_eps
|
| 394 |
+
|
| 395 |
+
noise = torch.randn_like(batch['coords']).to(self.device)
|
| 396 |
+
|
| 397 |
+
_, y_t, v_t = self.path.interpolant(t, noise, batch["coords"])
|
| 398 |
+
|
| 399 |
+
out_dict = self.model(y_t, t, batch)
|
| 400 |
+
|
| 401 |
+
resolved_atom_mask = batch["atom_resolved_mask"].float()
|
| 402 |
+
align_weights = y_t.new_ones(y_t.shape[:2])
|
| 403 |
+
|
| 404 |
+
if self.use_rigid_align:
|
| 405 |
+
with torch.no_grad(), torch.autocast("cuda", enabled=False):
|
| 406 |
+
v_t = out_dict['predict_velocity'].detach().float()
|
| 407 |
+
denoised_coords = y_t + v_t * (1.0 - t[:, None, None])
|
| 408 |
+
coords = batch["coords"].detach().float()
|
| 409 |
+
coords_aligned = weighted_rigid_align(
|
| 410 |
+
coords,
|
| 411 |
+
denoised_coords.detach().float(),
|
| 412 |
+
align_weights.detach().float(),
|
| 413 |
+
mask=resolved_atom_mask.detach().float(),
|
| 414 |
+
)
|
| 415 |
+
_, _, v_t_aligned = self.path.interpolant(t, noise, coords_aligned)
|
| 416 |
+
target = v_t_aligned
|
| 417 |
+
else:
|
| 418 |
+
target = v_t
|
| 419 |
+
|
| 420 |
+
loss = F.mse_loss(out_dict['predict_velocity'], target, reduction='none')
|
| 421 |
+
|
| 422 |
+
loss_mask = resolved_atom_mask * align_weights
|
| 423 |
+
loss = self.loss_masking(loss, loss_mask)
|
| 424 |
+
loss = loss.mean()
|
| 425 |
+
|
| 426 |
+
self.log(
|
| 427 |
+
"loss/mse",
|
| 428 |
+
loss.item(),
|
| 429 |
+
on_epoch=True,
|
| 430 |
+
logger=True,
|
| 431 |
+
prog_bar=True,
|
| 432 |
+
rank_zero_only=True,
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
if self.use_smooth_lddt_loss:
|
| 436 |
+
# one-step Euler to get denoised coordinates
|
| 437 |
+
denoised_coords = y_t + \
|
| 438 |
+
out_dict['predict_velocity'] * (1.0 - t[:, None, None])
|
| 439 |
+
|
| 440 |
+
# rescale coordinates to angstroms
|
| 441 |
+
# denoised_coords = center_of_mass_norm(denoised_coords, batch['atom_pad_mask'])
|
| 442 |
+
# true_coords = center_of_mass_norm(batch['coords'], batch['atom_pad_mask'])
|
| 443 |
+
denoised_coords = center_random_augmentation(
|
| 444 |
+
denoised_coords,
|
| 445 |
+
batch['atom_pad_mask'],
|
| 446 |
+
augmentation=False,
|
| 447 |
+
centering=True,
|
| 448 |
+
)
|
| 449 |
+
true_coords = center_random_augmentation(
|
| 450 |
+
batch['coords'],
|
| 451 |
+
batch['atom_pad_mask'],
|
| 452 |
+
augmentation=False,
|
| 453 |
+
centering=True,
|
| 454 |
+
)
|
| 455 |
+
denoised_coords = denoised_coords * self.processor.scale
|
| 456 |
+
true_coords = true_coords * self.processor.scale
|
| 457 |
+
|
| 458 |
+
smooth_lddt_loss = self.smooth_lddt_loss(
|
| 459 |
+
denoised_coords,
|
| 460 |
+
true_coords,
|
| 461 |
+
resolved_atom_mask,
|
| 462 |
+
t,
|
| 463 |
+
)
|
| 464 |
+
loss += smooth_lddt_loss
|
| 465 |
+
|
| 466 |
+
self.log(
|
| 467 |
+
"loss/smooth_lddt",
|
| 468 |
+
smooth_lddt_loss.item(),
|
| 469 |
+
on_epoch=True,
|
| 470 |
+
logger=True,
|
| 471 |
+
prog_bar=True,
|
| 472 |
+
rank_zero_only=True,
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
self.log(
|
| 476 |
+
"loss/loss",
|
| 477 |
+
loss.item(),
|
| 478 |
+
on_epoch=True,
|
| 479 |
+
logger=True,
|
| 480 |
+
prog_bar=True,
|
| 481 |
+
rank_zero_only=True,
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
self.log(
|
| 485 |
+
"trainer/global_step",
|
| 486 |
+
self.global_step,
|
| 487 |
+
on_epoch=False,
|
| 488 |
+
logger=True,
|
| 489 |
+
prog_bar=False,
|
| 490 |
+
rank_zero_only=True,
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
self.global_training_step = self.trainer.global_step
|
| 494 |
+
self.epoch = self.trainer.current_epoch
|
| 495 |
+
self.world_size = self.trainer.world_size
|
| 496 |
+
return loss
|
| 497 |
+
|
| 498 |
+
def training_step(self, batch, batch_idx):
|
| 499 |
+
if self.plddt_training:
|
| 500 |
+
return self.plddt_train_step(batch, batch_idx)
|
| 501 |
+
else:
|
| 502 |
+
return self.flow_matching_train_step(batch, batch_idx)
|
| 503 |
+
|
| 504 |
+
@torch.no_grad()
|
| 505 |
+
def validation_step(self, batch, batch_idx):
|
| 506 |
+
# we skip validation step in training mode
|
| 507 |
+
return
|
| 508 |
+
|
| 509 |
+
def on_train_start(self):
|
| 510 |
+
global_seed = os.environ.get("PL_GLOBAL_SEED", 42)
|
| 511 |
+
pl.seed_everything(int(global_seed) + self.trainer.global_rank, True)
|
| 512 |
+
return
|
| 513 |
+
|
| 514 |
+
@torch.no_grad()
|
| 515 |
+
def predict_step(self, batch, batch_idx):
|
| 516 |
+
with torch.autocast(device_type='cuda', dtype=torch.float32):
|
| 517 |
+
batch = self.processor.preprocess_inference(
|
| 518 |
+
batch,
|
| 519 |
+
esm_model=self.esm_model,
|
| 520 |
+
esm_dict=self.esm_dict,
|
| 521 |
+
af2_to_esm=self.af2_to_esm,
|
| 522 |
+
)
|
| 523 |
+
num_repeats = batch.get("num_repeats", torch.tensor(1, device=self.device)).item()
|
| 524 |
+
multiplicity = batch["mol_type"].shape[0]
|
| 525 |
+
num_iter = np.ceil(num_repeats / multiplicity).astype(int)
|
| 526 |
+
print(f"Generating {num_repeats} samples with num_iter: {num_iter}, multiplicity: {multiplicity}")
|
| 527 |
+
|
| 528 |
+
# num_repeats is the total number of samples to generate for one protein
|
| 529 |
+
# multiplicity is the number of samples to generate at once
|
| 530 |
+
|
| 531 |
+
curr_idx = 0
|
| 532 |
+
for i in range(num_iter):
|
| 533 |
+
batch_in = copy.deepcopy(batch)
|
| 534 |
+
noise = torch.randn_like(batch_in['coords']).to(self.device)
|
| 535 |
+
|
| 536 |
+
out_dict = self.sampler.sample(
|
| 537 |
+
self.model_ema.module.forward, self.path,
|
| 538 |
+
noise, batch_in
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
if self.plddt_module is not None:
|
| 542 |
+
denoised_coords = center_random_augmentation(
|
| 543 |
+
out_dict["denoised_coords"],
|
| 544 |
+
batch['atom_pad_mask'],
|
| 545 |
+
augmentation=False,
|
| 546 |
+
centering=True,
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
t = torch.ones(batch['coords'].shape[0], device=self.device)
|
| 550 |
+
out_feat = self.model(denoised_coords, t, batch) # use unscaled coords
|
| 551 |
+
|
| 552 |
+
plddt_out_dict = self.plddt_module(
|
| 553 |
+
out_feat["latent"],
|
| 554 |
+
batch_in,
|
| 555 |
+
)
|
| 556 |
+
plddts = plddt_out_dict["plddt"] * 100.0
|
| 557 |
+
else:
|
| 558 |
+
plddts = None
|
| 559 |
+
|
| 560 |
+
out_dict = self.processor.postprocess(out_dict, batch_in)
|
| 561 |
+
|
| 562 |
+
record = Record(**batch_in['record'][0])
|
| 563 |
+
gt_coord = out_dict['coords']
|
| 564 |
+
sampled_coord = out_dict['denoised_coords']
|
| 565 |
+
pad_mask = batch_in['atom_pad_mask']
|
| 566 |
+
|
| 567 |
+
if num_repeats - curr_idx < multiplicity:
|
| 568 |
+
curr_num_copies = num_repeats - curr_idx
|
| 569 |
+
gt_coord = gt_coord[:curr_num_copies]
|
| 570 |
+
sampled_coord = sampled_coord[:curr_num_copies]
|
| 571 |
+
pad_mask = pad_mask[:curr_num_copies]
|
| 572 |
+
plddts = plddts[:curr_num_copies] if plddts is not None else None
|
| 573 |
+
else:
|
| 574 |
+
curr_num_copies = multiplicity
|
| 575 |
+
|
| 576 |
+
data_dir = self.trainer.datamodule.predict_dataloader().dataset.target_dir
|
| 577 |
+
sample_dir = self.sample_dir
|
| 578 |
+
path = Path(data_dir) / "structures" / f"{record.id}.npz"
|
| 579 |
+
structure: Structure = Structure.load(path)
|
| 580 |
+
|
| 581 |
+
for j in range(curr_num_copies):
|
| 582 |
+
file_id = j + curr_idx
|
| 583 |
+
try:
|
| 584 |
+
sampled_structure = copy.deepcopy(structure)
|
| 585 |
+
sampled_structure = process_structure(
|
| 586 |
+
sampled_structure, sampled_coord[j], pad_mask[j], record
|
| 587 |
+
)
|
| 588 |
+
# save mmcif structure
|
| 589 |
+
sampled_struct_dir = Path(sample_dir)
|
| 590 |
+
outname = f"{record.id}_sampled_{str(file_id)}"
|
| 591 |
+
save_structure(
|
| 592 |
+
sampled_structure, sampled_struct_dir, outname,
|
| 593 |
+
plddts=plddts[j] if plddts is not None else None,
|
| 594 |
+
output_format="mmcif",
|
| 595 |
+
)
|
| 596 |
+
# save pdb structure
|
| 597 |
+
save_structure(
|
| 598 |
+
sampled_structure, sampled_struct_dir, outname,
|
| 599 |
+
plddts=plddts[j] if plddts is not None else None,
|
| 600 |
+
output_format="pdb",
|
| 601 |
+
)
|
| 602 |
+
except:
|
| 603 |
+
print(f"Error processing {record.id}")
|
| 604 |
+
continue
|
| 605 |
+
|
| 606 |
+
curr_idx += curr_num_copies
|
| 607 |
+
|
| 608 |
+
self.world_size = self.trainer.world_size
|
| 609 |
+
|
| 610 |
+
return
|
| 611 |
+
|
| 612 |
+
def on_predict_epoch_end(self):
|
| 613 |
+
dist.barrier() # wait for all processes to finish
|
| 614 |
+
return
|
| 615 |
+
|
| 616 |
+
def reset_esm(self, esm_model: str):
|
| 617 |
+
self.esm_model, self.esm_dict = esm_registry[esm_model]()
|
| 618 |
+
self.esm_model.eval()
|
| 619 |
+
self.af2_to_esm = _af2_to_esm(self.esm_dict)
|
| 620 |
+
self.esm_model = self.esm_model.to(self.device)
|
| 621 |
+
self.af2_to_esm = self.af2_to_esm.to(self.device)
|
| 622 |
+
print(f"Successfully reset ESM model {esm_model}")
|
| 623 |
+
|
| 624 |
+
def setup(self, stage: str) -> None:
|
| 625 |
+
"""Lightning hook that is called at the beginning of fit (train + validate), validate,
|
| 626 |
+
test, or predict.
|
| 627 |
+
|
| 628 |
+
This is a good hook when you need to build models dynamically or adjust something about
|
| 629 |
+
them. This hook is called on every process when using DDP.
|
| 630 |
+
|
| 631 |
+
:param stage: Either `"fit"`, `"validate"`, `"test"`, or `"predict"`.
|
| 632 |
+
"""
|
| 633 |
+
self.processor = self.hparams.processor(device=self.device)
|
| 634 |
+
|
| 635 |
+
if self.use_esm:
|
| 636 |
+
if stage == "fit" and not isinstance(
|
| 637 |
+
self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
|
| 638 |
+
):
|
| 639 |
+
# initialize the model with FSDP wrapper
|
| 640 |
+
fsdp_params = dict(
|
| 641 |
+
mixed_precision=True,
|
| 642 |
+
flatten_parameters=True,
|
| 643 |
+
state_dict_device=torch.device("cpu"), # reduce GPU mem usage
|
| 644 |
+
cpu_offload=True, # enable cpu offloading
|
| 645 |
+
fp32_reduce_scatter=True, # use fp32 reduce scatter
|
| 646 |
+
)
|
| 647 |
+
with enable_wrap(wrapper_cls=FSDP, **fsdp_params):
|
| 648 |
+
self.esm_model.eval()
|
| 649 |
+
# Wrap each layer in FSDP separately
|
| 650 |
+
for name, child in self.esm_model.named_children():
|
| 651 |
+
if name == "layers":
|
| 652 |
+
for layer_name, layer in child.named_children():
|
| 653 |
+
wrapped_layer = wrap(layer)
|
| 654 |
+
setattr(child, layer_name, wrapped_layer)
|
| 655 |
+
self.esm_model = wrap(self.esm_model)
|
| 656 |
+
self.af2_to_esm = self.af2_to_esm.to(self.device)
|
| 657 |
+
else:
|
| 658 |
+
self.esm_model = self.esm_model.to(self.device)
|
| 659 |
+
self.af2_to_esm = self.af2_to_esm.to(self.device)
|
| 660 |
+
|
| 661 |
+
if stage == "fit":
|
| 662 |
+
self.training_gpus = self.trainer.world_size
|
| 663 |
+
self.hparams["training_gpus"] = self.training_gpus
|
| 664 |
+
|
| 665 |
+
batch = next(iter(self.trainer.datamodule.train_dataloader()))
|
| 666 |
+
|
| 667 |
+
batch = self.processor.preprocess_training(
|
| 668 |
+
batch,
|
| 669 |
+
self.esm_model,
|
| 670 |
+
self.esm_dict,
|
| 671 |
+
self.af2_to_esm,
|
| 672 |
+
)
|
| 673 |
+
y = batch["coords"]
|
| 674 |
+
t = torch.zeros((y.shape[0])).cuda()
|
| 675 |
+
|
| 676 |
+
def on_train_batch_end(self, outputs, batch, batch_idx):
|
| 677 |
+
optimizer = self.optimizers()
|
| 678 |
+
self.log(
|
| 679 |
+
"trainer/lr",
|
| 680 |
+
optimizer.param_groups[0]["lr"],
|
| 681 |
+
on_epoch=True,
|
| 682 |
+
logger=True,
|
| 683 |
+
prog_bar=True,
|
| 684 |
+
rank_zero_only=True,
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
def on_before_optimizer_step(self, optimizer: Optimizer) -> None:
|
| 688 |
+
|
| 689 |
+
if isinstance(
|
| 690 |
+
self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
|
| 691 |
+
):
|
| 692 |
+
|
| 693 |
+
with FullyShardedDataParallel.summon_full_params(
|
| 694 |
+
self.trainer.strategy.model, with_grads=True
|
| 695 |
+
):
|
| 696 |
+
clip_grad_norm_(
|
| 697 |
+
self.trainer.strategy.model.model.parameters(),
|
| 698 |
+
self.hparams.clip_grad_norm_val,
|
| 699 |
+
norm_type=2.0,
|
| 700 |
+
error_if_nonfinite=True,
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
else:
|
| 704 |
+
clip_grad_norm_(
|
| 705 |
+
self.model.parameters(),
|
| 706 |
+
self.hparams.clip_grad_norm_val,
|
| 707 |
+
norm_type=2.0,
|
| 708 |
+
error_if_nonfinite=True,
|
| 709 |
+
)
|
| 710 |
+
return
|
| 711 |
+
|
| 712 |
+
def on_before_zero_grad(self, optimizer: Optimizer) -> None:
|
| 713 |
+
# if self.eval_ema:
|
| 714 |
+
if isinstance(
|
| 715 |
+
self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
|
| 716 |
+
):
|
| 717 |
+
|
| 718 |
+
with FullyShardedDataParallel.summon_full_params(
|
| 719 |
+
self.trainer.strategy.model
|
| 720 |
+
):
|
| 721 |
+
self.trainer.strategy.model.model_ema.update_parameters(
|
| 722 |
+
self.trainer.strategy.model.model
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
else:
|
| 726 |
+
self.model_ema.update_parameters(self.model)
|
| 727 |
+
return
|
| 728 |
+
|
| 729 |
+
def on_save_checkpoint(self, checkpoint) -> None:
|
| 730 |
+
|
| 731 |
+
if not isinstance(
|
| 732 |
+
self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
|
| 733 |
+
):
|
| 734 |
+
layers_to_delete = []
|
| 735 |
+
for k in checkpoint["state_dict"].keys():
|
| 736 |
+
if k.startswith("esm_model._fsdp_wrapped_module"):
|
| 737 |
+
layers_to_delete.append(k)
|
| 738 |
+
|
| 739 |
+
for k in layers_to_delete:
|
| 740 |
+
del checkpoint["state_dict"][k]
|
| 741 |
+
|
| 742 |
+
else:
|
| 743 |
+
try:
|
| 744 |
+
del checkpoint["hyper_parameters"]
|
| 745 |
+
except:
|
| 746 |
+
print("No hyper_parameters in checkpoint")
|
| 747 |
+
|
| 748 |
+
return super().on_save_checkpoint(checkpoint)
|
| 749 |
+
|
| 750 |
+
def on_load_checkpoint(self, checkpoint) -> None:
|
| 751 |
+
|
| 752 |
+
if not isinstance(
|
| 753 |
+
self.trainer.strategy, lightning.pytorch.strategies.fsdp.FSDPStrategy
|
| 754 |
+
):
|
| 755 |
+
self.training_gpus = checkpoint["hyper_parameters"]["training_gpus"]
|
| 756 |
+
self.fwd_flops = checkpoint["hyper_parameters"]["fwd_flops"]
|
| 757 |
+
|
| 758 |
+
if checkpoint["loops"] is not None:
|
| 759 |
+
|
| 760 |
+
self.trainer.fit_loop.load_state_dict(checkpoint["loops"]["fit_loop"])
|
| 761 |
+
self.trainer.validate_loop.load_state_dict(
|
| 762 |
+
checkpoint["loops"]["validate_loop"]
|
| 763 |
+
)
|
| 764 |
+
return super().on_load_checkpoint(checkpoint)
|
| 765 |
+
|
| 766 |
+
def configure_optimizers(self):
|
| 767 |
+
optimizer = self.hparams.optimizer(
|
| 768 |
+
params=self.trainer.model.parameters()
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
if self.hparams.scheduler is not None:
|
| 772 |
+
scheduler = self.hparams.scheduler(optimizer=optimizer)
|
| 773 |
+
return {
|
| 774 |
+
"optimizer": optimizer,
|
| 775 |
+
"lr_scheduler": {
|
| 776 |
+
"scheduler": scheduler,
|
| 777 |
+
"interval": "step",
|
| 778 |
+
"frequency": 1,
|
| 779 |
+
},
|
| 780 |
+
}
|
| 781 |
+
return {"optimizer": optimizer}
|
models/simplefold/torch/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
models/simplefold/torch/architecture.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
|
| 11 |
+
from .layers import FinalLayer, ConditionEmbedder
|
| 12 |
+
from onescience.utils.simplefold.esm_utils import esm_model_dict
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class FoldingDiT(nn.Module):
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
trunk,
|
| 19 |
+
time_embedder,
|
| 20 |
+
aminoacid_pos_embedder,
|
| 21 |
+
pos_embedder,
|
| 22 |
+
atom_encoder_transformer,
|
| 23 |
+
atom_decoder_transformer,
|
| 24 |
+
hidden_size=1152,
|
| 25 |
+
num_heads=16,
|
| 26 |
+
atom_num_heads=4,
|
| 27 |
+
output_channels=3,
|
| 28 |
+
atom_hidden_size_enc=256,
|
| 29 |
+
atom_hidden_size_dec=256,
|
| 30 |
+
atom_n_queries_enc=32,
|
| 31 |
+
atom_n_keys_enc=128,
|
| 32 |
+
atom_n_queries_dec=32,
|
| 33 |
+
atom_n_keys_dec=128,
|
| 34 |
+
esm_model="esm2_3B",
|
| 35 |
+
esm_dropout_prob=0.0,
|
| 36 |
+
use_atom_mask=False,
|
| 37 |
+
use_length_condition=True,
|
| 38 |
+
):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.pos_embedder = pos_embedder
|
| 41 |
+
pos_embed_channels = pos_embedder.embed_dim
|
| 42 |
+
self.aminoacid_pos_embedder = aminoacid_pos_embedder
|
| 43 |
+
aminoacid_pos_embed_channels = aminoacid_pos_embedder.embed_dim
|
| 44 |
+
|
| 45 |
+
self.time_embedder = time_embedder
|
| 46 |
+
|
| 47 |
+
self.atom_encoder_transformer = atom_encoder_transformer
|
| 48 |
+
self.atom_decoder_transformer = atom_decoder_transformer
|
| 49 |
+
|
| 50 |
+
self.trunk = trunk
|
| 51 |
+
|
| 52 |
+
self.hidden_size = hidden_size
|
| 53 |
+
self.output_channels = output_channels
|
| 54 |
+
self.num_heads = num_heads
|
| 55 |
+
self.atom_num_heads = atom_num_heads
|
| 56 |
+
self.use_atom_mask = use_atom_mask
|
| 57 |
+
self.esm_dropout_prob = esm_dropout_prob
|
| 58 |
+
self.use_length_condition = use_length_condition
|
| 59 |
+
|
| 60 |
+
esm_s_dim = esm_model_dict[esm_model]["esm_s_dim"]
|
| 61 |
+
esm_num_layers = esm_model_dict[esm_model]["esm_num_layers"]
|
| 62 |
+
|
| 63 |
+
self.atom_hidden_size_enc = atom_hidden_size_enc
|
| 64 |
+
self.atom_hidden_size_dec = atom_hidden_size_dec
|
| 65 |
+
self.atom_n_queries_enc = atom_n_queries_enc
|
| 66 |
+
self.atom_n_keys_enc = atom_n_keys_enc
|
| 67 |
+
self.atom_n_queries_dec = atom_n_queries_dec
|
| 68 |
+
self.atom_n_keys_dec = atom_n_keys_dec
|
| 69 |
+
|
| 70 |
+
atom_feat_dim = pos_embed_channels + aminoacid_pos_embed_channels + 427
|
| 71 |
+
self.atom_feat_proj = nn.Sequential(
|
| 72 |
+
nn.Linear(atom_feat_dim, hidden_size),
|
| 73 |
+
nn.LayerNorm(hidden_size),
|
| 74 |
+
nn.SiLU(),
|
| 75 |
+
)
|
| 76 |
+
self.atom_pos_proj = nn.Linear(pos_embed_channels, hidden_size, bias=False)
|
| 77 |
+
|
| 78 |
+
if self.use_length_condition:
|
| 79 |
+
self.length_embedder = nn.Sequential(
|
| 80 |
+
nn.Linear(1, hidden_size, bias=False),
|
| 81 |
+
nn.LayerNorm(hidden_size),
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
self.atom_in_proj = nn.Linear(hidden_size * 2, hidden_size, bias=False)
|
| 85 |
+
|
| 86 |
+
self.esm_s_combine = nn.Parameter(torch.zeros(esm_num_layers))
|
| 87 |
+
self.esm_s_proj = ConditionEmbedder(
|
| 88 |
+
input_dim=esm_s_dim,
|
| 89 |
+
hidden_size=hidden_size,
|
| 90 |
+
dropout_prob=self.esm_dropout_prob,
|
| 91 |
+
)
|
| 92 |
+
latent_cat_dim = hidden_size * 2
|
| 93 |
+
self.esm_cat_proj = nn.Linear(latent_cat_dim, hidden_size)
|
| 94 |
+
|
| 95 |
+
self.context2atom_proj = nn.Sequential(
|
| 96 |
+
nn.Linear(hidden_size, self.atom_hidden_size_enc),
|
| 97 |
+
nn.LayerNorm(self.atom_hidden_size_enc),
|
| 98 |
+
)
|
| 99 |
+
self.atom2latent_proj = nn.Sequential(
|
| 100 |
+
nn.Linear(self.atom_hidden_size_enc, hidden_size),
|
| 101 |
+
nn.LayerNorm(hidden_size),
|
| 102 |
+
)
|
| 103 |
+
self.atom_enc_cond_proj = nn.Sequential(
|
| 104 |
+
nn.Linear(hidden_size, self.atom_hidden_size_enc),
|
| 105 |
+
nn.LayerNorm(self.atom_hidden_size_enc),
|
| 106 |
+
)
|
| 107 |
+
self.atom_dec_cond_proj = nn.Sequential(
|
| 108 |
+
nn.Linear(hidden_size, self.atom_hidden_size_dec),
|
| 109 |
+
nn.LayerNorm(self.atom_hidden_size_dec),
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
self.latent2atom_proj = nn.Sequential(
|
| 113 |
+
nn.Linear(hidden_size, hidden_size),
|
| 114 |
+
nn.SiLU(),
|
| 115 |
+
nn.LayerNorm(hidden_size),
|
| 116 |
+
nn.Linear(hidden_size, self.atom_hidden_size_dec),
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
self.final_layer = FinalLayer(
|
| 120 |
+
self.atom_hidden_size_dec,
|
| 121 |
+
output_channels,
|
| 122 |
+
c_dim=hidden_size
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def create_local_attn_bias(
|
| 126 |
+
self, n: int, n_queries: int, n_keys: int, inf: float = 1e10, device: torch.device = None
|
| 127 |
+
) -> torch.Tensor:
|
| 128 |
+
"""Create local attention bias based on query window n_queries and kv window n_keys.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
n (int): the length of quiries
|
| 132 |
+
n_queries (int): window size of quiries
|
| 133 |
+
n_keys (int): window size of keys/values
|
| 134 |
+
inf (float, optional): the inf to mask attention. Defaults to 1e10.
|
| 135 |
+
device (torch.device, optional): cuda|cpu|None. Defaults to None.
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
torch.Tensor: the diagonal-like global attention bias
|
| 139 |
+
"""
|
| 140 |
+
n_trunks = int(math.ceil(n / n_queries))
|
| 141 |
+
padded_n = n_trunks * n_queries
|
| 142 |
+
attn_mask = torch.zeros(padded_n, padded_n, device=device)
|
| 143 |
+
for block_index in range(0, n_trunks):
|
| 144 |
+
i = block_index * n_queries
|
| 145 |
+
j1 = max(0, n_queries * block_index - (n_keys - n_queries) // 2)
|
| 146 |
+
j2 = n_queries * block_index + (n_queries + n_keys) // 2
|
| 147 |
+
attn_mask[i : i + n_queries, j1:j2] = 1.0
|
| 148 |
+
attn_bias = (1 - attn_mask) * -inf
|
| 149 |
+
return attn_bias.to(device=device)[:n, :n]
|
| 150 |
+
|
| 151 |
+
def create_atom_attn_mask(
|
| 152 |
+
self,
|
| 153 |
+
feats,
|
| 154 |
+
natoms,
|
| 155 |
+
atom_n_queries=None,
|
| 156 |
+
atom_n_keys=None,
|
| 157 |
+
inf: float = 1e10
|
| 158 |
+
) -> torch.Tensor:
|
| 159 |
+
if atom_n_queries is not None and atom_n_keys is not None:
|
| 160 |
+
atom_attn_mask = self.create_local_attn_bias(
|
| 161 |
+
n=natoms,
|
| 162 |
+
n_queries=atom_n_queries,
|
| 163 |
+
n_keys=atom_n_keys,
|
| 164 |
+
device=feats["ref_pos"].device,
|
| 165 |
+
inf=inf,
|
| 166 |
+
)
|
| 167 |
+
else:
|
| 168 |
+
atom_attn_mask = None
|
| 169 |
+
|
| 170 |
+
return atom_attn_mask
|
| 171 |
+
|
| 172 |
+
def forward(self, noised_pos, t, feats, self_cond=None):
|
| 173 |
+
B, N, _ = feats["ref_pos"].shape
|
| 174 |
+
M = feats["mol_type"].shape[1]
|
| 175 |
+
atom_to_token = feats["atom_to_token"].float() # [B, N, M]
|
| 176 |
+
atom_to_token_idx = feats["atom_to_token_idx"]
|
| 177 |
+
ref_space_uid = feats["ref_space_uid"]
|
| 178 |
+
|
| 179 |
+
# create atom attention masks
|
| 180 |
+
atom_attn_mask_enc = self.create_atom_attn_mask(
|
| 181 |
+
feats,
|
| 182 |
+
natoms=N,
|
| 183 |
+
atom_n_queries=self.atom_n_queries_enc,
|
| 184 |
+
atom_n_keys=self.atom_n_keys_enc,
|
| 185 |
+
)
|
| 186 |
+
atom_attn_mask_dec = self.create_atom_attn_mask(
|
| 187 |
+
feats,
|
| 188 |
+
natoms=N,
|
| 189 |
+
atom_n_queries=self.atom_n_queries_dec,
|
| 190 |
+
atom_n_keys=self.atom_n_keys_dec,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# create condition embeddings for AdaLN
|
| 194 |
+
c_emb = self.time_embedder(t) # (B, D)
|
| 195 |
+
if self.use_length_condition:
|
| 196 |
+
length = feats["max_num_tokens"].float().unsqueeze(-1)
|
| 197 |
+
c_emb = c_emb + self.length_embedder(torch.log(length))
|
| 198 |
+
|
| 199 |
+
# create atom features
|
| 200 |
+
mol_type = feats["mol_type"]
|
| 201 |
+
mol_type = F.one_hot(mol_type, num_classes=4).float() # [B, M, 4]
|
| 202 |
+
res_type = feats["res_type"].float() # [B, M, 33]
|
| 203 |
+
pocket_feature = feats["pocket_feature"].float() # [B, M, 4]
|
| 204 |
+
res_feat = torch.cat(
|
| 205 |
+
[mol_type, res_type, pocket_feature],
|
| 206 |
+
dim=-1) # [B, M, 41]
|
| 207 |
+
atom_feat_from_res = torch.bmm(atom_to_token, res_feat) # [B, N, 41]
|
| 208 |
+
atom_res_pos = self.aminoacid_pos_embedder(
|
| 209 |
+
pos=atom_to_token_idx.unsqueeze(-1).float()
|
| 210 |
+
)
|
| 211 |
+
ref_pos_emb = self.pos_embedder(pos=feats["ref_pos"])
|
| 212 |
+
atom_feat = torch.cat(
|
| 213 |
+
[
|
| 214 |
+
ref_pos_emb, # (B, N, PD1)
|
| 215 |
+
atom_feat_from_res, # (B, N, 41)
|
| 216 |
+
atom_res_pos, # (B, N, PD2)
|
| 217 |
+
feats["ref_charge"].unsqueeze(-1), # (B, N, 1)
|
| 218 |
+
feats["atom_pad_mask"].unsqueeze(-1), # (B, N, 1)
|
| 219 |
+
feats["ref_element"], # (B, N, 128)
|
| 220 |
+
feats["ref_atom_name_chars"].reshape(B, N, 4 * 64), # (B, N, 256)
|
| 221 |
+
],
|
| 222 |
+
dim=-1,
|
| 223 |
+
) # (B, N, PD1+PD2+427)
|
| 224 |
+
atom_feat = self.atom_feat_proj(atom_feat) # (B, N, D)
|
| 225 |
+
|
| 226 |
+
atom_coord = self.pos_embedder(pos=noised_pos) # (B, N, PD1)
|
| 227 |
+
atom_coord = self.atom_pos_proj(atom_coord) # (B, N, D)
|
| 228 |
+
|
| 229 |
+
atom_in = torch.cat([atom_feat, atom_coord], dim=-1)
|
| 230 |
+
atom_in = self.atom_in_proj(atom_in) # (B, N, D)
|
| 231 |
+
|
| 232 |
+
# position embeddings for Axial RoPE
|
| 233 |
+
atom_pe_pos = torch.cat(
|
| 234 |
+
[
|
| 235 |
+
ref_space_uid.unsqueeze(-1).float(), # (B, N, 1)
|
| 236 |
+
feats["ref_pos"], # (B, N, 3)
|
| 237 |
+
],
|
| 238 |
+
dim=-1,
|
| 239 |
+
) # (B, N, 4)
|
| 240 |
+
token_pe_pos = torch.cat(
|
| 241 |
+
[
|
| 242 |
+
feats["residue_index"].unsqueeze(-1).float(), # (B, M, 1)
|
| 243 |
+
feats["entity_id"].unsqueeze(-1).float(), # (B, M, 1)
|
| 244 |
+
feats["asym_id"].unsqueeze(-1).float(), # (B, M, 1)
|
| 245 |
+
feats["sym_id"].unsqueeze(-1).float(), # (B, M, 1)
|
| 246 |
+
],
|
| 247 |
+
dim=-1,
|
| 248 |
+
) # (B, M, 4)
|
| 249 |
+
|
| 250 |
+
# atom encoder
|
| 251 |
+
atom_c_emb_enc = self.atom_enc_cond_proj(c_emb)
|
| 252 |
+
atom_latent = self.context2atom_proj(atom_in)
|
| 253 |
+
atom_latent = self.atom_encoder_transformer(
|
| 254 |
+
latents=atom_latent,
|
| 255 |
+
c=atom_c_emb_enc,
|
| 256 |
+
attention_mask=atom_attn_mask_enc,
|
| 257 |
+
pos=atom_pe_pos,
|
| 258 |
+
)
|
| 259 |
+
atom_latent = self.atom2latent_proj(atom_latent)
|
| 260 |
+
|
| 261 |
+
# grouping: aggregate atom tokens to residue tokens
|
| 262 |
+
atom_to_token_mean = atom_to_token / (
|
| 263 |
+
atom_to_token.sum(dim=1, keepdim=True) + 1e-6
|
| 264 |
+
)
|
| 265 |
+
latent = torch.bmm(atom_to_token_mean.transpose(1, 2), atom_latent)
|
| 266 |
+
assert latent.shape[1] == M
|
| 267 |
+
|
| 268 |
+
esm_s = (self.esm_s_combine.softmax(0).unsqueeze(0) @ feats['esm_s']).squeeze(2)
|
| 269 |
+
force_drop_ids = feats.get("force_drop_ids", None)
|
| 270 |
+
esm_emb = self.esm_s_proj(esm_s, self.training, force_drop_ids)
|
| 271 |
+
assert esm_emb.shape[1] == latent.shape[1]
|
| 272 |
+
|
| 273 |
+
latent = self.esm_cat_proj(torch.cat([latent, esm_emb], dim=-1))
|
| 274 |
+
|
| 275 |
+
# residue trunk
|
| 276 |
+
latent = self.trunk(
|
| 277 |
+
latents=latent,
|
| 278 |
+
c=c_emb,
|
| 279 |
+
attention_mask=None,
|
| 280 |
+
pos=token_pe_pos,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# ungrouping: broadcast residue tokens to atom tokens
|
| 284 |
+
output = torch.bmm(atom_to_token, latent)
|
| 285 |
+
assert output.shape[1] == N
|
| 286 |
+
|
| 287 |
+
# add skip connection
|
| 288 |
+
output = output + atom_latent
|
| 289 |
+
output = self.latent2atom_proj(output)
|
| 290 |
+
|
| 291 |
+
# atom decoder
|
| 292 |
+
atom_c_emb_dec = self.atom_dec_cond_proj(c_emb)
|
| 293 |
+
output = self.atom_decoder_transformer(
|
| 294 |
+
latents=output,
|
| 295 |
+
c=atom_c_emb_dec,
|
| 296 |
+
attention_mask=atom_attn_mask_dec,
|
| 297 |
+
pos=atom_pe_pos,
|
| 298 |
+
)
|
| 299 |
+
output = self.final_layer(output, c=c_emb)
|
| 300 |
+
|
| 301 |
+
return {
|
| 302 |
+
"predict_velocity": output,
|
| 303 |
+
"latent": latent,
|
| 304 |
+
}
|
models/simplefold/torch/layers.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def modulate(x, shift, scale):
|
| 14 |
+
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
#################################################################################
|
| 18 |
+
# Attention Layers #
|
| 19 |
+
#################################################################################
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class SelfAttentionLayer(nn.Module):
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
hidden_size,
|
| 26 |
+
num_heads=8,
|
| 27 |
+
qkv_bias=False,
|
| 28 |
+
qk_scale=None,
|
| 29 |
+
attn_drop=0.0,
|
| 30 |
+
proj_drop=0.0,
|
| 31 |
+
use_bias=True,
|
| 32 |
+
qk_norm=True,
|
| 33 |
+
pos_embedder=None,
|
| 34 |
+
linear_target: nn.Module = nn.Linear,
|
| 35 |
+
):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.num_heads = num_heads
|
| 38 |
+
head_dim = hidden_size // num_heads
|
| 39 |
+
self.scale = qk_scale or head_dim**-0.5
|
| 40 |
+
|
| 41 |
+
self.qkv = linear_target(hidden_size, hidden_size * 3, bias=qkv_bias)
|
| 42 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 43 |
+
self.proj = linear_target(hidden_size, hidden_size, bias=use_bias)
|
| 44 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 45 |
+
|
| 46 |
+
self.q_norm = RMSNorm(head_dim) if qk_norm else nn.Identity()
|
| 47 |
+
self.k_norm = RMSNorm(head_dim) if qk_norm else nn.Identity()
|
| 48 |
+
|
| 49 |
+
self.pos_embedder = pos_embedder
|
| 50 |
+
|
| 51 |
+
def forward(self, x, **kwargs):
|
| 52 |
+
B, N, C = x.shape
|
| 53 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
|
| 54 |
+
pos = kwargs.get("pos")
|
| 55 |
+
|
| 56 |
+
qkv = rearrange(qkv, "b n t h c -> t b h n c")
|
| 57 |
+
q, k, v = (
|
| 58 |
+
qkv[0],
|
| 59 |
+
qkv[1],
|
| 60 |
+
qkv[2],
|
| 61 |
+
) # make torchscript happy (cannot use tensor as tuple)
|
| 62 |
+
|
| 63 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 64 |
+
|
| 65 |
+
if self.pos_embedder and pos is not None:
|
| 66 |
+
q, k = self.pos_embedder(q, k, pos)
|
| 67 |
+
|
| 68 |
+
attn = (q @ k.transpose(-2, -1)) * self.scale
|
| 69 |
+
attn = attn.softmax(dim=-1)
|
| 70 |
+
attn = self.attn_drop(attn)
|
| 71 |
+
|
| 72 |
+
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
|
| 73 |
+
x = self.proj(x)
|
| 74 |
+
x = self.proj_drop(x)
|
| 75 |
+
return x
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class EfficientSelfAttentionLayer(SelfAttentionLayer):
|
| 79 |
+
"""Started from https://github.com/facebookresearch/dinov2/blob/main/dinov2/layers/attention.py"""
|
| 80 |
+
|
| 81 |
+
def __init__(
|
| 82 |
+
self,
|
| 83 |
+
*args,
|
| 84 |
+
**kwargs,
|
| 85 |
+
):
|
| 86 |
+
super().__init__(*args, **kwargs)
|
| 87 |
+
|
| 88 |
+
def forward(self, x, **kwargs):
|
| 89 |
+
B, N, C = x.shape
|
| 90 |
+
attn_mask = kwargs.get("attention_mask")
|
| 91 |
+
pos = kwargs.get("pos")
|
| 92 |
+
|
| 93 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
|
| 94 |
+
qkv = rearrange(qkv, "b n t h c -> t b h n c")
|
| 95 |
+
q, k, v = qkv.unbind(0)
|
| 96 |
+
|
| 97 |
+
if attn_mask is not None:
|
| 98 |
+
attn_mask = attn_mask.to(dtype=q.dtype)
|
| 99 |
+
|
| 100 |
+
if self.pos_embedder and pos is not None:
|
| 101 |
+
q, k = self.pos_embedder(q, k, pos)
|
| 102 |
+
|
| 103 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 104 |
+
v1 = v.to(dtype = q.dtype)
|
| 105 |
+
x = nn.functional.scaled_dot_product_attention(q, k, v1, attn_mask=attn_mask)
|
| 106 |
+
|
| 107 |
+
x = x.transpose(1, 2).reshape(B, N, C)
|
| 108 |
+
x = self.proj(x)
|
| 109 |
+
x = self.proj_drop(x)
|
| 110 |
+
|
| 111 |
+
return x
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
#################################################################################
|
| 115 |
+
# FeedForward Layer #
|
| 116 |
+
#################################################################################
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class SwiGLUFeedForward(nn.Module):
|
| 120 |
+
def __init__(self, dim, hidden_dim, multiple_of=256):
|
| 121 |
+
super().__init__()
|
| 122 |
+
hidden_dim = int(2 * hidden_dim / 3)
|
| 123 |
+
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
|
| 124 |
+
|
| 125 |
+
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
|
| 126 |
+
self.w2 = nn.Linear(hidden_dim, dim, bias=True)
|
| 127 |
+
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
| 128 |
+
|
| 129 |
+
self.reset_parameters()
|
| 130 |
+
|
| 131 |
+
def reset_parameters(self):
|
| 132 |
+
torch.nn.init.xavier_uniform_(self.w1.weight)
|
| 133 |
+
torch.nn.init.xavier_uniform_(self.w2.weight)
|
| 134 |
+
torch.nn.init.xavier_uniform_(self.w3.weight)
|
| 135 |
+
if self.w1.bias is not None:
|
| 136 |
+
torch.nn.init.constant_(self.w1.bias, 0)
|
| 137 |
+
if self.w2.bias is not None:
|
| 138 |
+
torch.nn.init.constant_(self.w2.bias, 0)
|
| 139 |
+
if self.w3.bias is not None:
|
| 140 |
+
torch.nn.init.constant_(self.w3.bias, 0)
|
| 141 |
+
|
| 142 |
+
def forward(self, x):
|
| 143 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
#################################################################################
|
| 147 |
+
# Utility Layers #
|
| 148 |
+
#################################################################################
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class TimestepEmbedder(nn.Module):
|
| 152 |
+
"""
|
| 153 |
+
Embeds scalar timesteps into vector representations.
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
def __init__(self, hidden_size, frequency_embedding_size=256):
|
| 157 |
+
super().__init__()
|
| 158 |
+
self.mlp = nn.Sequential(
|
| 159 |
+
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
| 160 |
+
nn.SiLU(),
|
| 161 |
+
nn.Linear(hidden_size, hidden_size, bias=True),
|
| 162 |
+
)
|
| 163 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 164 |
+
self.initialize_weights()
|
| 165 |
+
|
| 166 |
+
def initialize_weights(self):
|
| 167 |
+
nn.init.normal_(self.mlp[0].weight, std=0.02)
|
| 168 |
+
nn.init.normal_(self.mlp[2].weight, std=0.02)
|
| 169 |
+
|
| 170 |
+
@staticmethod
|
| 171 |
+
def timestep_embedding(t, dim, max_period=10000):
|
| 172 |
+
"""
|
| 173 |
+
Create sinusoidal timestep embeddings.
|
| 174 |
+
:param t: a 1-D Tensor of N indices, one per batch element.
|
| 175 |
+
These may be fractional.
|
| 176 |
+
:param dim: the dimension of the output.
|
| 177 |
+
:param max_period: controls the minimum frequency of the embeddings.
|
| 178 |
+
:return: an (N, D) Tensor of positional embeddings.
|
| 179 |
+
"""
|
| 180 |
+
# https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
|
| 181 |
+
half = dim // 2
|
| 182 |
+
freqs = torch.exp(
|
| 183 |
+
-math.log(max_period)
|
| 184 |
+
* torch.arange(start=0, end=half, dtype=torch.float32)
|
| 185 |
+
/ half
|
| 186 |
+
).to(device=t.device)
|
| 187 |
+
args = t[:, None].float() * freqs[None]
|
| 188 |
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 189 |
+
if dim % 2:
|
| 190 |
+
embedding = torch.cat(
|
| 191 |
+
[embedding, torch.zeros_like(embedding[:, :1])], dim=-1
|
| 192 |
+
)
|
| 193 |
+
return embedding
|
| 194 |
+
|
| 195 |
+
def forward(self, t):
|
| 196 |
+
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
|
| 197 |
+
t_emb = self.mlp(t_freq)
|
| 198 |
+
return t_emb
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class ConditionEmbedder(nn.Module):
|
| 202 |
+
"""
|
| 203 |
+
Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
|
| 204 |
+
"""
|
| 205 |
+
def __init__(self, input_dim, hidden_size, dropout_prob):
|
| 206 |
+
super().__init__()
|
| 207 |
+
self.proj = nn.Sequential(
|
| 208 |
+
nn.Linear(input_dim, hidden_size),
|
| 209 |
+
nn.LayerNorm(hidden_size),
|
| 210 |
+
nn.SiLU(),
|
| 211 |
+
)
|
| 212 |
+
self.dropout_prob = dropout_prob
|
| 213 |
+
self.null_token = nn.Parameter(torch.randn(input_dim), requires_grad=True)
|
| 214 |
+
|
| 215 |
+
def token_drop(self, cond, force_drop_ids=None):
|
| 216 |
+
"""
|
| 217 |
+
cond: (B, N, D)
|
| 218 |
+
Drops conditions to enable classifier-free guidance.
|
| 219 |
+
"""
|
| 220 |
+
if force_drop_ids is None:
|
| 221 |
+
drop_ids = torch.rand(cond.shape[0], device=cond.device) < self.dropout_prob
|
| 222 |
+
else:
|
| 223 |
+
drop_ids = force_drop_ids
|
| 224 |
+
cond[drop_ids] = self.null_token[None, None, :]
|
| 225 |
+
return cond
|
| 226 |
+
|
| 227 |
+
def forward(self, cond, train, force_drop_ids=None):
|
| 228 |
+
use_dropout = self.dropout_prob > 0
|
| 229 |
+
if (train and use_dropout) or (force_drop_ids is not None):
|
| 230 |
+
cond = self.token_drop(cond, force_drop_ids)
|
| 231 |
+
embeddings = self.proj(cond)
|
| 232 |
+
return embeddings
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class FinalLayer(nn.Module):
|
| 236 |
+
"""
|
| 237 |
+
The final layer of DiT.
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
def __init__(self, hidden_size, out_channels, c_dim=None):
|
| 241 |
+
super().__init__()
|
| 242 |
+
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 243 |
+
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
| 244 |
+
self.adaLN_modulation = nn.Sequential(
|
| 245 |
+
nn.SiLU(), nn.Linear(c_dim, 2 * hidden_size, bias=True)
|
| 246 |
+
)
|
| 247 |
+
self.initialize_weights()
|
| 248 |
+
|
| 249 |
+
def initialize_weights(self):
|
| 250 |
+
# Initialize transformer layers:
|
| 251 |
+
def _basic_init(module):
|
| 252 |
+
if isinstance(module, nn.Linear):
|
| 253 |
+
torch.nn.init.xavier_uniform_(module.weight)
|
| 254 |
+
if module.bias is not None:
|
| 255 |
+
nn.init.constant_(module.bias, 0)
|
| 256 |
+
|
| 257 |
+
self.apply(_basic_init)
|
| 258 |
+
|
| 259 |
+
# Zero-out output layers:
|
| 260 |
+
nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
|
| 261 |
+
nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
|
| 262 |
+
nn.init.constant_(self.linear.weight, 0)
|
| 263 |
+
nn.init.constant_(self.linear.bias, 0)
|
| 264 |
+
|
| 265 |
+
def forward(self, x, c):
|
| 266 |
+
shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
|
| 267 |
+
x = modulate(self.norm_final(x), shift, scale)
|
| 268 |
+
x = self.linear(x)
|
| 269 |
+
return x
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
class RMSNorm(nn.Module):
|
| 273 |
+
def __init__(self, d, p=-1.0, eps=1e-8, bias=False):
|
| 274 |
+
"""
|
| 275 |
+
Root Mean Square Layer Normalization
|
| 276 |
+
:param d: model size
|
| 277 |
+
:param p: partial RMSNorm, valid value [0, 1], default -1.0 (disabled)
|
| 278 |
+
:param eps: epsilon value, default 1e-8
|
| 279 |
+
:param bias: whether use bias term for RMSNorm, disabled by
|
| 280 |
+
default because RMSNorm doesn't enforce re-centering invariance.
|
| 281 |
+
"""
|
| 282 |
+
super(RMSNorm, self).__init__()
|
| 283 |
+
|
| 284 |
+
self.eps = eps
|
| 285 |
+
self.d = d
|
| 286 |
+
self.p = p
|
| 287 |
+
self.bias = bias
|
| 288 |
+
|
| 289 |
+
self.scale = nn.Parameter(torch.ones(d))
|
| 290 |
+
self.register_parameter("scale", self.scale)
|
| 291 |
+
|
| 292 |
+
if self.bias:
|
| 293 |
+
self.offset = nn.Parameter(torch.zeros(d))
|
| 294 |
+
self.register_parameter("offset", self.offset)
|
| 295 |
+
|
| 296 |
+
def forward(self, x):
|
| 297 |
+
if self.p < 0.0 or self.p > 1.0:
|
| 298 |
+
norm_x = x.norm(2, dim=-1, keepdim=True, dtype=x.dtype)
|
| 299 |
+
d_x = self.d
|
| 300 |
+
else:
|
| 301 |
+
partial_size = int(self.d * self.p)
|
| 302 |
+
partial_x, _ = torch.split(x, [partial_size, self.d - partial_size], dim=-1)
|
| 303 |
+
|
| 304 |
+
norm_x = partial_x.norm(2, dim=-1, keepdim=True, dtype=x.dtype)
|
| 305 |
+
d_x = partial_size
|
| 306 |
+
|
| 307 |
+
rms_x = norm_x * d_x ** (-1.0 / 2)
|
| 308 |
+
x_normed = x / (rms_x + self.eps)
|
| 309 |
+
|
| 310 |
+
if self.bias:
|
| 311 |
+
return self.scale * x_normed + self.offset
|
| 312 |
+
|
| 313 |
+
return self.scale * x_normed
|
models/simplefold/torch/pos_embed.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AbsolutePositionEncoding(nn.Module):
|
| 12 |
+
def __init__(self, in_dim, embed_dim, include_input=False):
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.in_dim = in_dim
|
| 15 |
+
self.hidden_dim = embed_dim
|
| 16 |
+
self.include_input = include_input
|
| 17 |
+
assert embed_dim % in_dim == 0, "embed_dim must be divisible by in_dim"
|
| 18 |
+
self.embed_dim = embed_dim + in_dim if include_input else embed_dim
|
| 19 |
+
|
| 20 |
+
def forward(self, pos):
|
| 21 |
+
pos_embs = []
|
| 22 |
+
for i in range(self.in_dim):
|
| 23 |
+
pe = self.get_1d_pos_embed(pos[..., i])
|
| 24 |
+
pos_embs.append(pe)
|
| 25 |
+
if self.include_input:
|
| 26 |
+
pos_embs.append(pos)
|
| 27 |
+
pos_embs = torch.cat(pos_embs, dim=-1)
|
| 28 |
+
return pos_embs
|
| 29 |
+
|
| 30 |
+
def get_1d_pos_embed(self, pos):
|
| 31 |
+
"""
|
| 32 |
+
https://github.com/facebookresearch/DiT/blob/main/models.py#L303
|
| 33 |
+
"""
|
| 34 |
+
embed_dim = self.hidden_dim // (self.in_dim * 2)
|
| 35 |
+
omega = 2 ** torch.linspace(0, math.log(224, 2) - 1, embed_dim).to(pos.device)
|
| 36 |
+
omega *= torch.pi
|
| 37 |
+
|
| 38 |
+
if len(pos.shape) == 1:
|
| 39 |
+
out = torch.einsum("m,d->md", pos, omega) # (M, D/2), outer product
|
| 40 |
+
elif len(pos.shape) == 2:
|
| 41 |
+
out = torch.einsum("nm,d->nmd", pos, omega)
|
| 42 |
+
|
| 43 |
+
emb_sin = torch.sin(out) # (*, M, D/2)
|
| 44 |
+
emb_cos = torch.cos(out) # (*, M, D/2)
|
| 45 |
+
emb = torch.cat([emb_sin, emb_cos], dim=-1) # (*, M, D)
|
| 46 |
+
return emb
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class FourierPositionEncoding(torch.nn.Module):
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
in_dim: int,
|
| 53 |
+
include_input: bool = False,
|
| 54 |
+
min_freq_log2: float = 0,
|
| 55 |
+
max_freq_log2: float = 12,
|
| 56 |
+
num_freqs: int = 32,
|
| 57 |
+
log_sampling: bool = True,
|
| 58 |
+
):
|
| 59 |
+
super().__init__()
|
| 60 |
+
self.in_dim = in_dim
|
| 61 |
+
self.include_input = include_input
|
| 62 |
+
self.min_freq_log2 = min_freq_log2
|
| 63 |
+
self.max_freq_log2 = max_freq_log2
|
| 64 |
+
self.num_freqs = num_freqs
|
| 65 |
+
self.log_sampling = log_sampling
|
| 66 |
+
self.create_embedding_fn()
|
| 67 |
+
|
| 68 |
+
def create_embedding_fn(self):
|
| 69 |
+
d = self.in_dim
|
| 70 |
+
dim_out = 0
|
| 71 |
+
if self.include_input:
|
| 72 |
+
dim_out += d
|
| 73 |
+
|
| 74 |
+
min_freq = self.min_freq_log2
|
| 75 |
+
max_freq = self.max_freq_log2
|
| 76 |
+
N_freqs = self.num_freqs
|
| 77 |
+
|
| 78 |
+
if self.log_sampling:
|
| 79 |
+
freq_bands = 2.0 ** torch.linspace(
|
| 80 |
+
min_freq, max_freq, steps=N_freqs
|
| 81 |
+
) # (nf,)
|
| 82 |
+
else:
|
| 83 |
+
freq_bands = torch.linspace(
|
| 84 |
+
2.0**min_freq, 2.0**max_freq, steps=N_freqs
|
| 85 |
+
) # (nf,)
|
| 86 |
+
|
| 87 |
+
assert (
|
| 88 |
+
freq_bands.isfinite().all()
|
| 89 |
+
), f"nan: {freq_bands.isnan().any()} inf: {freq_bands.isinf().any()}"
|
| 90 |
+
|
| 91 |
+
self.register_buffer("freq_bands", freq_bands) # (nf,)
|
| 92 |
+
self.embed_dim = dim_out + d * self.freq_bands.numel() * 2
|
| 93 |
+
|
| 94 |
+
def forward(
|
| 95 |
+
self,
|
| 96 |
+
pos: torch.Tensor,
|
| 97 |
+
):
|
| 98 |
+
"""
|
| 99 |
+
Get the positional encoding for each coordinate.
|
| 100 |
+
Args:
|
| 101 |
+
pos:
|
| 102 |
+
(*, in_dim)
|
| 103 |
+
Returns:
|
| 104 |
+
out:
|
| 105 |
+
(*, in_dimitional_encoding)
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
out = []
|
| 109 |
+
if self.include_input:
|
| 110 |
+
out = [pos] # (*, in_dim)
|
| 111 |
+
|
| 112 |
+
pos = pos.unsqueeze(-1) * self.freq_bands # (*b, d, nf)
|
| 113 |
+
|
| 114 |
+
out += [
|
| 115 |
+
torch.sin(pos).flatten(start_dim=-2), # (*b, d*nf)
|
| 116 |
+
torch.cos(pos).flatten(start_dim=-2), # (*b, d*nf)
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
out = torch.cat(out, dim=-1) # (*b, 2 * in_dim * nf (+ in_dim))
|
| 120 |
+
return out
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def compute_axial_cis(
|
| 124 |
+
ts: torch.Tensor,
|
| 125 |
+
in_dim: int,
|
| 126 |
+
dim: int,
|
| 127 |
+
theta: float = 100.0,
|
| 128 |
+
):
|
| 129 |
+
B, N, D = ts.shape
|
| 130 |
+
freqs_all = []
|
| 131 |
+
interval = 2 * in_dim
|
| 132 |
+
for i in range(in_dim):
|
| 133 |
+
freq = 1.0 / (
|
| 134 |
+
theta ** (torch.arange(0, dim, interval)[: (dim // interval)].float() / dim)
|
| 135 |
+
).to(ts.device)
|
| 136 |
+
t = ts[..., i].flatten()
|
| 137 |
+
freq_i = torch.outer(t, freq)
|
| 138 |
+
freq_cis_i = torch.polar(torch.ones_like(freq_i), freq_i)
|
| 139 |
+
freq_cis_i = freq_cis_i.view(B, N, -1)
|
| 140 |
+
freqs_all.append(freq_cis_i)
|
| 141 |
+
freqs_cis = torch.cat(freqs_all, dim=-1)
|
| 142 |
+
return freqs_cis
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor):
|
| 146 |
+
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
|
| 147 |
+
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
|
| 148 |
+
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
|
| 149 |
+
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
|
| 150 |
+
return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class AxialRotaryPositionEncoding(nn.Module):
|
| 154 |
+
def __init__(
|
| 155 |
+
self,
|
| 156 |
+
in_dim,
|
| 157 |
+
embed_dim,
|
| 158 |
+
num_heads,
|
| 159 |
+
base=100.0,
|
| 160 |
+
):
|
| 161 |
+
super().__init__()
|
| 162 |
+
self.in_dim = in_dim
|
| 163 |
+
self.num_heads = num_heads
|
| 164 |
+
self.embed_dim = embed_dim // num_heads
|
| 165 |
+
self.base = base
|
| 166 |
+
|
| 167 |
+
def forward(self, xq, xk, pos):
|
| 168 |
+
"""
|
| 169 |
+
xq: [B, H, N, D]
|
| 170 |
+
xk: [B, H, N, D]
|
| 171 |
+
pos: [B, N, in_dim]
|
| 172 |
+
"""
|
| 173 |
+
if pos.ndim == 2:
|
| 174 |
+
pos = pos.unsqueeze(-1)
|
| 175 |
+
freqs_cis = compute_axial_cis(pos, self.in_dim, self.embed_dim, self.base)
|
| 176 |
+
freqs_cis = freqs_cis.unsqueeze(1)
|
| 177 |
+
return apply_rotary_emb(xq, xk, freqs_cis.to(xq.device))
|
scripts/__pycache__/inference.cpython-311.pyc
ADDED
|
Binary file (22.4 kB). View file
|
|
|
scripts/__pycache__/train.cpython-310.pyc
ADDED
|
Binary file (3.82 kB). View file
|
|
|
scripts/__pycache__/train_fsdp.cpython-310.pyc
ADDED
|
Binary file (4.29 kB). View file
|
|
|
scripts/evaluate.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import hydra
|
| 7 |
+
import torch
|
| 8 |
+
from omegaconf import OmegaConf
|
| 9 |
+
import lightning.pytorch as pl
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from lightning.pytorch import (
|
| 13 |
+
LightningDataModule,
|
| 14 |
+
LightningModule
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
if str(ROOT) not in sys.path:
|
| 19 |
+
sys.path.insert(0, str(ROOT))
|
| 20 |
+
|
| 21 |
+
from onescience.utils.simplefold.utils import extras, create_folders, task_wrapper
|
| 22 |
+
from onescience.utils.simplefold.instantiators import instantiate_callbacks
|
| 23 |
+
from onescience.utils.simplefold.logging_utils import log_hyperparameters
|
| 24 |
+
from onescience.utils.simplefold.pylogger import RankedLogger
|
| 25 |
+
|
| 26 |
+
torch.set_float32_matmul_precision("medium")
|
| 27 |
+
log = RankedLogger(__name__, rank_zero_only=True)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@task_wrapper
|
| 31 |
+
def test(cfg):
|
| 32 |
+
load_ckpt_path = cfg.get("load_ckpt_path", None)
|
| 33 |
+
assert load_ckpt_path != None
|
| 34 |
+
|
| 35 |
+
log.info(f"Instantiating model <{cfg.model._target_}>")
|
| 36 |
+
model: LightningModule = hydra.utils.instantiate(cfg.model)
|
| 37 |
+
|
| 38 |
+
checkpoint = torch.load(load_ckpt_path, map_location="cpu", weights_only=False)
|
| 39 |
+
|
| 40 |
+
# TODO: add load FSDP checkpoint
|
| 41 |
+
try:
|
| 42 |
+
# checkpoint in our official release only contains weights of EMA model
|
| 43 |
+
# therefore, we by default load EMA model weight to LightningModule for inference
|
| 44 |
+
for key in model.model_ema.state_dict().keys():
|
| 45 |
+
if key.startswith("module."):
|
| 46 |
+
model.model_ema.state_dict()[key].copy_(
|
| 47 |
+
checkpoint[key.replace("module.", "")]
|
| 48 |
+
)
|
| 49 |
+
print("Loaded weights of EMA model successfully.")
|
| 50 |
+
except:
|
| 51 |
+
# if using checkpoint from your own training, load weights of LightningModule directly
|
| 52 |
+
model.load_state_dict(
|
| 53 |
+
checkpoint["state_dict"], strict=False
|
| 54 |
+
)
|
| 55 |
+
print("Loaded weights of LightningModule successfully.")
|
| 56 |
+
|
| 57 |
+
# reset ESM model to avoid issues in loading FSDP checkpoint
|
| 58 |
+
model.reset_esm(cfg.model.esm_model)
|
| 59 |
+
|
| 60 |
+
seed = cfg.get("seed", 42)
|
| 61 |
+
pl.seed_everything(seed, workers=True)
|
| 62 |
+
|
| 63 |
+
log.info(f"Instantiating datamodule <{cfg.data._target_}>")
|
| 64 |
+
datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
|
| 65 |
+
|
| 66 |
+
log.info("Instantiating callbacks...")
|
| 67 |
+
callbacks = instantiate_callbacks(cfg.get("callbacks"))
|
| 68 |
+
|
| 69 |
+
log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
|
| 70 |
+
trainer = hydra.utils.instantiate(
|
| 71 |
+
cfg.trainer, callbacks=callbacks, logger=[], plugins=[]
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
object_dict = {
|
| 75 |
+
"cfg": cfg,
|
| 76 |
+
"datamodule": datamodule,
|
| 77 |
+
"model": model,
|
| 78 |
+
"callbacks": callbacks,
|
| 79 |
+
"logger": [],
|
| 80 |
+
"trainer": trainer,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if log:
|
| 84 |
+
log.info("Logging hyperparameters!")
|
| 85 |
+
log_hyperparameters(object_dict)
|
| 86 |
+
|
| 87 |
+
log.info("Starting evaluation!")
|
| 88 |
+
trainer.predict(model=model, datamodule=datamodule, ckpt_path=None)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@hydra.main(version_base="1.3", config_path="../config", config_name="base_eval.yaml")
|
| 92 |
+
def submit_run(cfg):
|
| 93 |
+
OmegaConf.resolve(cfg)
|
| 94 |
+
extras(cfg)
|
| 95 |
+
create_folders(cfg)
|
| 96 |
+
test(cfg)
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
submit_run()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import torch
|
| 9 |
+
import hydra
|
| 10 |
+
import omegaconf
|
| 11 |
+
import argparse
|
| 12 |
+
import numpy as np
|
| 13 |
+
from copy import deepcopy
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from itertools import starmap
|
| 16 |
+
import lightning.pytorch as pl
|
| 17 |
+
|
| 18 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 19 |
+
CONFIG_DIR = ROOT / "config"
|
| 20 |
+
WEIGHT_DIR = ROOT / "weight"
|
| 21 |
+
|
| 22 |
+
if str(ROOT) not in sys.path:
|
| 23 |
+
sys.path.insert(0, str(ROOT))
|
| 24 |
+
|
| 25 |
+
from models.simplefold.flow import LinearPath
|
| 26 |
+
from models.simplefold.torch.sampler import EMSampler
|
| 27 |
+
|
| 28 |
+
from onescience.datapipes.simplefold.processor.protein_processor import ProteinDataProcessor
|
| 29 |
+
from onescience.utils.simplefold.datamodule_utils import process_one_inference_structure
|
| 30 |
+
from onescience.utils.simplefold.esm_utils import _af2_to_esm, esm_registry
|
| 31 |
+
import models.esm.pretrained as esm_pretrained
|
| 32 |
+
from onescience.utils.simplefold.boltz_utils import process_structure, save_structure
|
| 33 |
+
from onescience.utils.simplefold.fasta_utils import process_fastas, check_fasta_inputs
|
| 34 |
+
from onescience.datapipes.boltz_data_pipeline.feature.featurizer import BoltzFeaturizer
|
| 35 |
+
from onescience.datapipes.boltz_data_pipeline.tokenize.boltz_protein import BoltzTokenizer
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
import mlx.core as mx
|
| 39 |
+
from mlx.utils import tree_unflatten, tree_flatten
|
| 40 |
+
from models.simplefold.mlx.sampler import EMSampler as EMSamplerMLX
|
| 41 |
+
from models.simplefold.mlx.esm_network import ESM2 as ESM2MLX
|
| 42 |
+
from onescience.utils.simplefold.mlx_utils import map_torch_to_mlx, map_plddt_torch_to_mlx
|
| 43 |
+
MLX_AVAILABLE = True
|
| 44 |
+
except:
|
| 45 |
+
MLX_AVAILABLE = False
|
| 46 |
+
print("MLX not installed, skip importing MLX related packages.")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
SUPPORTED_MODELS = {
|
| 50 |
+
"simplefold_100M",
|
| 51 |
+
"simplefold_360M",
|
| 52 |
+
"simplefold_700M",
|
| 53 |
+
"simplefold_1.1B",
|
| 54 |
+
"simplefold_1.6B",
|
| 55 |
+
"simplefold_3B",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
|
| 59 |
+
MIN_REAL_WEIGHT_BYTES = 1024
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _resolve_path(path: str | os.PathLike[str], base: Path = ROOT) -> Path:
|
| 63 |
+
path = Path(path)
|
| 64 |
+
return path if path.is_absolute() else base / path
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _require_local_file(path: Path, description: str, min_bytes: int = MIN_REAL_WEIGHT_BYTES) -> Path:
|
| 68 |
+
if not path.exists():
|
| 69 |
+
raise FileNotFoundError(
|
| 70 |
+
f"Missing {description}: {path}\n"
|
| 71 |
+
f"Please place the file under {WEIGHT_DIR} or pass the corresponding environment/CLI path."
|
| 72 |
+
)
|
| 73 |
+
if path.stat().st_size < min_bytes:
|
| 74 |
+
raise RuntimeError(
|
| 75 |
+
f"{description} looks like a link/placeholder rather than a real weight file: {path}\n"
|
| 76 |
+
"Replace it with the real file before running inference."
|
| 77 |
+
)
|
| 78 |
+
return path
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def load_local_or_hub_esm2_3b():
|
| 82 |
+
candidate_paths = []
|
| 83 |
+
|
| 84 |
+
esm_model_path = os.getenv("SIMPLEFOLD_ESM2_MODEL_PATH")
|
| 85 |
+
if esm_model_path:
|
| 86 |
+
candidate_paths.append(esm_model_path)
|
| 87 |
+
|
| 88 |
+
candidate_paths.extend(
|
| 89 |
+
[
|
| 90 |
+
WEIGHT_DIR / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
|
| 91 |
+
WEIGHT_DIR / f"{ESM2_MODEL_NAME}.pt",
|
| 92 |
+
]
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
seen_paths = set()
|
| 96 |
+
for candidate_path in candidate_paths:
|
| 97 |
+
if candidate_path in seen_paths:
|
| 98 |
+
continue
|
| 99 |
+
seen_paths.add(candidate_path)
|
| 100 |
+
|
| 101 |
+
candidate_path = Path(candidate_path)
|
| 102 |
+
regression_path = candidate_path.with_name(
|
| 103 |
+
f"{candidate_path.stem}-contact-regression.pt"
|
| 104 |
+
)
|
| 105 |
+
if candidate_path.exists() and regression_path.exists():
|
| 106 |
+
_require_local_file(candidate_path, "ESM-2 3B model weight")
|
| 107 |
+
_require_local_file(regression_path, "ESM-2 contact regression weight")
|
| 108 |
+
print(f"Loading ESM-2 3B weights from local path: {candidate_path}")
|
| 109 |
+
return esm_pretrained.load_model_and_alphabet(str(candidate_path))
|
| 110 |
+
|
| 111 |
+
raise FileNotFoundError(
|
| 112 |
+
"Local ESM-2 3B weights were not found. Put both files here:\n"
|
| 113 |
+
f" {WEIGHT_DIR / 'esm_models' / f'{ESM2_MODEL_NAME}.pt'}\n"
|
| 114 |
+
f" {WEIGHT_DIR / 'esm_models' / f'{ESM2_MODEL_NAME}-contact-regression.pt'}\n"
|
| 115 |
+
"or set SIMPLEFOLD_ESM2_MODEL_PATH to the .pt file. Remote download is disabled in this package."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def resolve_ccd_path(cache: Path) -> Path:
|
| 120 |
+
candidate_paths = []
|
| 121 |
+
|
| 122 |
+
ccd_path = os.getenv("SIMPLEFOLD_CCD_PATH")
|
| 123 |
+
if ccd_path:
|
| 124 |
+
candidate_paths.append(Path(ccd_path))
|
| 125 |
+
|
| 126 |
+
candidate_paths.append(WEIGHT_DIR / "ccd.pkl")
|
| 127 |
+
|
| 128 |
+
seen_paths = set()
|
| 129 |
+
for candidate_path in candidate_paths:
|
| 130 |
+
resolved_path = str(candidate_path)
|
| 131 |
+
if resolved_path in seen_paths:
|
| 132 |
+
continue
|
| 133 |
+
seen_paths.add(resolved_path)
|
| 134 |
+
|
| 135 |
+
if candidate_path.exists():
|
| 136 |
+
_require_local_file(candidate_path, "CCD dictionary")
|
| 137 |
+
print(f"Using local CCD dictionary: {candidate_path}")
|
| 138 |
+
return candidate_path
|
| 139 |
+
|
| 140 |
+
raise FileNotFoundError(
|
| 141 |
+
f"Missing CCD dictionary. Put ccd.pkl under {WEIGHT_DIR} or set SIMPLEFOLD_CCD_PATH."
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def get_config_path(relative_path):
|
| 146 |
+
"""Resolve a SimpleFold config from the bundled config directory."""
|
| 147 |
+
config_subpath = relative_path.replace("configs/", "")
|
| 148 |
+
config_path = CONFIG_DIR / config_subpath
|
| 149 |
+
if not config_path.is_file():
|
| 150 |
+
raise FileNotFoundError(f"Could not find config file: {config_path}")
|
| 151 |
+
return str(config_path)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def initialize_folding_model(args):
|
| 156 |
+
# define folding model
|
| 157 |
+
simplefold_model = args.simplefold_model
|
| 158 |
+
if simplefold_model not in SUPPORTED_MODELS:
|
| 159 |
+
raise ValueError(f"Unsupported model {simplefold_model!r}. Choose one of {sorted(SUPPORTED_MODELS)}")
|
| 160 |
+
|
| 161 |
+
# create checkpoint directory
|
| 162 |
+
ckpt_dir = _resolve_path(args.ckpt_dir)
|
| 163 |
+
ckpt_path = os.path.join(ckpt_dir, f"{simplefold_model}.ckpt")
|
| 164 |
+
|
| 165 |
+
# create folding model
|
| 166 |
+
ckpt_path = os.path.join(ckpt_dir, f"{simplefold_model}.ckpt")
|
| 167 |
+
_require_local_file(Path(ckpt_path), f"{simplefold_model} checkpoint")
|
| 168 |
+
cfg_path = get_config_path(f"configs/model/architecture/foldingdit_{simplefold_model[11:]}.yaml")
|
| 169 |
+
|
| 170 |
+
checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 171 |
+
|
| 172 |
+
# load model checkpoint
|
| 173 |
+
if args.backend == 'torch':
|
| 174 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 175 |
+
model_config = omegaconf.OmegaConf.load(cfg_path)
|
| 176 |
+
model = hydra.utils.instantiate(model_config)
|
| 177 |
+
model.load_state_dict(checkpoint, strict=True)
|
| 178 |
+
model = model.to(device)
|
| 179 |
+
elif args.backend == 'mlx':
|
| 180 |
+
device = "cpu"
|
| 181 |
+
# replace torch implementations with mlx
|
| 182 |
+
with open(cfg_path, "r") as f:
|
| 183 |
+
yaml_str = f.read()
|
| 184 |
+
yaml_str = yaml_str.replace('torch', 'mlx')
|
| 185 |
+
|
| 186 |
+
model_config = omegaconf.OmegaConf.create(yaml_str)
|
| 187 |
+
model = hydra.utils.instantiate(model_config)
|
| 188 |
+
mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, checkpoint.items()) if k is not None}
|
| 189 |
+
model.update(tree_unflatten(list(mlx_state_dict.items())))
|
| 190 |
+
print(f"Folding model {simplefold_model} loaded.")
|
| 191 |
+
print(f"Using device: {device}.")
|
| 192 |
+
|
| 193 |
+
model.eval()
|
| 194 |
+
return model, device
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def initialize_plddt_module(args, device):
|
| 198 |
+
if not args.plddt:
|
| 199 |
+
return None, None
|
| 200 |
+
|
| 201 |
+
# load pLDDT module if specified
|
| 202 |
+
ckpt_dir = _resolve_path(args.ckpt_dir)
|
| 203 |
+
plddt_ckpt_path = ckpt_dir / "plddt.ckpt"
|
| 204 |
+
if not plddt_ckpt_path.exists():
|
| 205 |
+
plddt_ckpt_path = ckpt_dir / "plddt_module_1.6B.ckpt"
|
| 206 |
+
_require_local_file(plddt_ckpt_path, "pLDDT checkpoint")
|
| 207 |
+
|
| 208 |
+
plddt_module_path = get_config_path("configs/model/architecture/plddt_module.yaml")
|
| 209 |
+
plddt_checkpoint = torch.load(plddt_ckpt_path, map_location="cpu", weights_only=False)
|
| 210 |
+
|
| 211 |
+
if args.backend == "torch":
|
| 212 |
+
plddt_config = omegaconf.OmegaConf.load(plddt_module_path)
|
| 213 |
+
plddt_out_module = hydra.utils.instantiate(plddt_config)
|
| 214 |
+
plddt_out_module.load_state_dict(plddt_checkpoint, strict=True)
|
| 215 |
+
plddt_out_module = plddt_out_module.to(device)
|
| 216 |
+
elif args.backend == "mlx":
|
| 217 |
+
# replace torch implementations with mlx
|
| 218 |
+
with open(plddt_module_path, "r") as f:
|
| 219 |
+
yaml_str = f.read()
|
| 220 |
+
yaml_str = yaml_str.replace('torch', 'mlx')
|
| 221 |
+
|
| 222 |
+
plddt_config = omegaconf.OmegaConf.create(yaml_str)
|
| 223 |
+
plddt_out_module = hydra.utils.instantiate(plddt_config)
|
| 224 |
+
|
| 225 |
+
mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_plddt_torch_to_mlx, plddt_checkpoint.items()) if k is not None}
|
| 226 |
+
plddt_out_module.update(tree_unflatten(list(mlx_state_dict.items())))
|
| 227 |
+
|
| 228 |
+
plddt_out_module.eval()
|
| 229 |
+
print(f"pLDDT output module loaded with {args.backend} backend.")
|
| 230 |
+
|
| 231 |
+
plddt_latent_ckpt_path = ckpt_dir / "simplefold_1.6B.ckpt"
|
| 232 |
+
_require_local_file(plddt_latent_ckpt_path, "SimpleFold 1.6B pLDDT latent checkpoint")
|
| 233 |
+
|
| 234 |
+
plddt_latent_config_path = get_config_path("configs/model/architecture/foldingdit_1.6B.yaml")
|
| 235 |
+
plddt_latent_checkpoint = torch.load(plddt_latent_ckpt_path, map_location="cpu", weights_only=False)
|
| 236 |
+
|
| 237 |
+
if args.backend == "torch":
|
| 238 |
+
plddt_latent_config = omegaconf.OmegaConf.load(plddt_latent_config_path)
|
| 239 |
+
plddt_latent_module = hydra.utils.instantiate(plddt_latent_config)
|
| 240 |
+
plddt_latent_module.load_state_dict(plddt_latent_checkpoint, strict=True)
|
| 241 |
+
plddt_latent_module = plddt_latent_module.to(device)
|
| 242 |
+
elif args.backend == "mlx":
|
| 243 |
+
# replace torch implementations with mlx
|
| 244 |
+
with open(plddt_latent_config_path, "r") as f:
|
| 245 |
+
yaml_str = f.read()
|
| 246 |
+
yaml_str = yaml_str.replace('torch', 'mlx')
|
| 247 |
+
|
| 248 |
+
plddt_latent_config = omegaconf.OmegaConf.create(yaml_str)
|
| 249 |
+
plddt_latent_module = hydra.utils.instantiate(plddt_latent_config)
|
| 250 |
+
mlx_state_dict = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, plddt_latent_checkpoint.items()) if k is not None}
|
| 251 |
+
plddt_latent_module.update(tree_unflatten(list(mlx_state_dict.items())))
|
| 252 |
+
|
| 253 |
+
plddt_latent_module.eval()
|
| 254 |
+
print(f"pLDDT latent module loaded with {args.backend} backend.")
|
| 255 |
+
|
| 256 |
+
return plddt_latent_module, plddt_out_module
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def initialize_esm_model(args, device):
|
| 260 |
+
# load ESM2 model
|
| 261 |
+
esm_model, esm_dict = load_local_or_hub_esm2_3b()
|
| 262 |
+
af2_to_esm = _af2_to_esm(esm_dict)
|
| 263 |
+
|
| 264 |
+
if args.backend == 'torch':
|
| 265 |
+
esm_model = esm_model.to(device)
|
| 266 |
+
af2_to_esm = af2_to_esm.to(device)
|
| 267 |
+
elif args.backend == 'mlx':
|
| 268 |
+
esm_model_mlx = ESM2MLX(num_layers=36, embed_dim=2560, attention_heads=40)
|
| 269 |
+
esm_state_dict_torch = esm_model.cpu().state_dict()
|
| 270 |
+
|
| 271 |
+
esm_state_dict_torch = {k: mx.array(v) for k, v in starmap(map_torch_to_mlx, esm_state_dict_torch.items()) if k is not None}
|
| 272 |
+
esm_model_mlx.update(tree_unflatten(list(esm_state_dict_torch.items())))
|
| 273 |
+
esm_model = esm_model_mlx
|
| 274 |
+
print(f"pLM ESM-3B loaded with {args.backend} backend.")
|
| 275 |
+
|
| 276 |
+
esm_model.eval()
|
| 277 |
+
return esm_model, esm_dict, af2_to_esm
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def initialize_others(args, device):
|
| 281 |
+
# prepare data tokenizer, featurizer, and processor
|
| 282 |
+
tokenizer = BoltzTokenizer()
|
| 283 |
+
featurizer = BoltzFeaturizer()
|
| 284 |
+
processor = ProteinDataProcessor(
|
| 285 |
+
device=device,
|
| 286 |
+
scale=16.0,
|
| 287 |
+
ref_scale=5.0,
|
| 288 |
+
multiplicity=1,
|
| 289 |
+
inference_multiplicity=args.nsample_per_protein,
|
| 290 |
+
backend=args.backend,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# define flow process and sampler
|
| 294 |
+
flow = LinearPath()
|
| 295 |
+
|
| 296 |
+
if args.backend == "torch":
|
| 297 |
+
sampler_cls = EMSampler
|
| 298 |
+
elif args.backend == "mlx":
|
| 299 |
+
sampler_cls = EMSamplerMLX
|
| 300 |
+
|
| 301 |
+
sampler = sampler_cls(
|
| 302 |
+
num_timesteps=args.num_steps,
|
| 303 |
+
t_start=1e-4,
|
| 304 |
+
tau=args.tau,
|
| 305 |
+
log_timesteps=True,
|
| 306 |
+
w_cutoff=0.99,
|
| 307 |
+
)
|
| 308 |
+
return tokenizer, featurizer, processor, flow, sampler
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def generate_structure(
|
| 312 |
+
args, batch, sampler, flow, processor,
|
| 313 |
+
model, plddt_latent_module, plddt_out_module, device
|
| 314 |
+
):
|
| 315 |
+
# run inference for target protein
|
| 316 |
+
if args.backend == "torch":
|
| 317 |
+
noise = torch.randn_like(batch['coords']).to(device)
|
| 318 |
+
elif args.backend == "mlx":
|
| 319 |
+
noise = mx.random.normal(batch['coords'].shape)
|
| 320 |
+
out_dict = sampler.sample(model, flow, noise, batch)
|
| 321 |
+
|
| 322 |
+
if args.plddt:
|
| 323 |
+
if args.backend == "torch":
|
| 324 |
+
t = torch.ones(batch['coords'].shape[0], device=device)
|
| 325 |
+
# use unscaled coords to extract latent for pLDDT prediction
|
| 326 |
+
out_feat = plddt_latent_module(
|
| 327 |
+
out_dict["denoised_coords"].detach(), t, batch)
|
| 328 |
+
plddt_out_dict = plddt_out_module(
|
| 329 |
+
out_feat["latent"].detach(),
|
| 330 |
+
batch,
|
| 331 |
+
)
|
| 332 |
+
elif args.backend == "mlx":
|
| 333 |
+
t = mx.ones(batch['coords'].shape[0])
|
| 334 |
+
# use unscaled coords to extract latent for pLDDT prediction
|
| 335 |
+
out_feat = plddt_latent_module(
|
| 336 |
+
out_dict["denoised_coords"], t, batch)
|
| 337 |
+
plddt_out_dict = plddt_out_module(
|
| 338 |
+
out_feat["latent"],
|
| 339 |
+
batch,
|
| 340 |
+
)
|
| 341 |
+
# scale pLDDT to [0, 100]
|
| 342 |
+
plddts = plddt_out_dict["plddt"] * 100.0
|
| 343 |
+
else:
|
| 344 |
+
plddts = None
|
| 345 |
+
|
| 346 |
+
out_dict = processor.postprocess(out_dict, batch)
|
| 347 |
+
# sampled_coord = out_dict['denoised_coords'].detach()
|
| 348 |
+
if args.backend == "torch":
|
| 349 |
+
sampled_coord = out_dict['denoised_coords'].detach()
|
| 350 |
+
else:
|
| 351 |
+
sampled_coord = out_dict['denoised_coords']
|
| 352 |
+
|
| 353 |
+
pad_mask = batch['atom_pad_mask']
|
| 354 |
+
return sampled_coord, pad_mask, plddts
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def predict_structures_from_fastas(args):
|
| 358 |
+
# create output directories
|
| 359 |
+
output_dir = _resolve_path(args.output_dir)
|
| 360 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 361 |
+
prediction_dir = output_dir / f"predictions_{args.simplefold_model}"
|
| 362 |
+
prediction_dir.mkdir(parents=True, exist_ok=True)
|
| 363 |
+
cache = output_dir / "cache"
|
| 364 |
+
cache.mkdir(parents=True, exist_ok=True)
|
| 365 |
+
|
| 366 |
+
# set random seed for reproducibility
|
| 367 |
+
pl.seed_everything(args.seed, workers=True)
|
| 368 |
+
|
| 369 |
+
if args.backend == "mlx" and not MLX_AVAILABLE:
|
| 370 |
+
args.backend = "torch"
|
| 371 |
+
print("MLX not available, switch to torch backend.")
|
| 372 |
+
|
| 373 |
+
# initialize models
|
| 374 |
+
model, device = initialize_folding_model(args)
|
| 375 |
+
plddt_latent_module, plddt_out_module = initialize_plddt_module(args, device)
|
| 376 |
+
esm_model, esm_dict, af2_to_esm = initialize_esm_model(args, device)
|
| 377 |
+
|
| 378 |
+
# initialize other components
|
| 379 |
+
tokenizer, featurizer, processor, flow, sampler = initialize_others(args, device)
|
| 380 |
+
|
| 381 |
+
# process fasta files to input format
|
| 382 |
+
ccd_path = resolve_ccd_path(cache)
|
| 383 |
+
data = check_fasta_inputs(_resolve_path(args.fasta_path))
|
| 384 |
+
if not data:
|
| 385 |
+
raise ValueError("No valid input files found. Please check the input directory.")
|
| 386 |
+
process_fastas(
|
| 387 |
+
data=data,
|
| 388 |
+
out_dir=output_dir,
|
| 389 |
+
ccd_path=ccd_path,
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
for struct_file in output_dir.glob("structures/*.npz"):
|
| 393 |
+
record_file = output_dir / "records" / f"{struct_file.stem}.json"
|
| 394 |
+
|
| 395 |
+
# prepare the target protein data for inference
|
| 396 |
+
batch, structure, record = process_one_inference_structure(
|
| 397 |
+
struct_file, record_file,
|
| 398 |
+
tokenizer, featurizer, processor,
|
| 399 |
+
esm_model, esm_dict, af2_to_esm,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
sampled_coord, pad_mask, plddts = generate_structure(
|
| 403 |
+
args, batch, sampler, flow, processor,
|
| 404 |
+
model, plddt_latent_module, plddt_out_module, device
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
for i in range(args.nsample_per_protein):
|
| 408 |
+
sampled_coord_i = sampled_coord[i]
|
| 409 |
+
pad_mask_i = pad_mask[i]
|
| 410 |
+
|
| 411 |
+
# save the generated structure
|
| 412 |
+
structure_save = process_structure(
|
| 413 |
+
deepcopy(structure), sampled_coord_i, pad_mask_i, record, backend=args.backend
|
| 414 |
+
)
|
| 415 |
+
outname = f"{record.id}_sampled_{i}"
|
| 416 |
+
save_structure(
|
| 417 |
+
structure_save, prediction_dir, outname,
|
| 418 |
+
output_format=args.output_format,
|
| 419 |
+
plddts=plddts[i] if plddts is not None else None
|
| 420 |
+
)
|
scripts/preflight.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Preflight checks for the packaged SimpleFold ModelScope layout."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import importlib
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
if str(ROOT) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(ROOT))
|
| 14 |
+
|
| 15 |
+
REQUIRED_FILES = [
|
| 16 |
+
"README.md",
|
| 17 |
+
"configuration.json",
|
| 18 |
+
"requirements_dcu.txt",
|
| 19 |
+
"env_install.sh",
|
| 20 |
+
"config/train.yaml",
|
| 21 |
+
"config/base_train.yaml",
|
| 22 |
+
"config/base_eval.yaml",
|
| 23 |
+
"config/model/architecture/foldingdit_100M.yaml",
|
| 24 |
+
"config/model/architecture/foldingdit_1.6B.yaml",
|
| 25 |
+
"config/model/architecture/plddt_module.yaml",
|
| 26 |
+
"scripts/run_inference.py",
|
| 27 |
+
"scripts/inference.py",
|
| 28 |
+
"scripts/train.py",
|
| 29 |
+
"scripts/train_fsdp.py",
|
| 30 |
+
"scripts/evaluate.py",
|
| 31 |
+
"scripts/process_data.py",
|
| 32 |
+
"scripts/tokenize_data.py",
|
| 33 |
+
"examples/minimal.fasta",
|
| 34 |
+
"modules/models/simplefold/flow.py",
|
| 35 |
+
"modules/datapipes/simplefold/processor/protein_processor.py",
|
| 36 |
+
"modules/utils/simplefold/esm_utils.py",
|
| 37 |
+
"modules/datapipes/boltz_data_pipeline/types.py",
|
| 38 |
+
"modules/models/esm/pretrained.py",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
REQUIRED_WEIGHTS = [
|
| 42 |
+
"simplefold_100M.ckpt",
|
| 43 |
+
"simplefold_360M.ckpt",
|
| 44 |
+
"simplefold_700M.ckpt",
|
| 45 |
+
"simplefold_1.1B.ckpt",
|
| 46 |
+
"simplefold_1.6B.ckpt",
|
| 47 |
+
"simplefold_3B.ckpt",
|
| 48 |
+
"plddt.ckpt",
|
| 49 |
+
"plddt_module_1.6B.ckpt",
|
| 50 |
+
"ccd.pkl",
|
| 51 |
+
"boltz1_conf.ckpt",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
OPTIONAL_ESM_WEIGHTS = [
|
| 55 |
+
"esm_models/esm2_t36_3B_UR50D.pt",
|
| 56 |
+
"esm_models/esm2_t36_3B_UR50D-contact-regression.pt",
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def ok(message: str) -> None:
|
| 61 |
+
print(f"[OK] {message}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def warn(message: str) -> None:
|
| 65 |
+
print(f"[WARN] {message}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def fail(message: str) -> None:
|
| 69 |
+
print(f"[FAIL] {message}")
|
| 70 |
+
raise SystemExit(1)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_files() -> None:
|
| 74 |
+
missing = [path for path in REQUIRED_FILES if not (ROOT / path).exists()]
|
| 75 |
+
if missing:
|
| 76 |
+
fail("Missing required files: " + ", ".join(missing))
|
| 77 |
+
ok("Required package files are present")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def check_weights(strict: bool) -> None:
|
| 81 |
+
missing = []
|
| 82 |
+
placeholders = []
|
| 83 |
+
for name in REQUIRED_WEIGHTS:
|
| 84 |
+
path = ROOT / "weight" / name
|
| 85 |
+
if not path.exists():
|
| 86 |
+
missing.append(name)
|
| 87 |
+
elif path.stat().st_size < 1024:
|
| 88 |
+
placeholders.append(name)
|
| 89 |
+
if missing:
|
| 90 |
+
fail("Missing weight files: " + ", ".join(missing))
|
| 91 |
+
if placeholders:
|
| 92 |
+
message = (
|
| 93 |
+
"These files look like link/placeholder files and must be replaced "
|
| 94 |
+
"before real inference/training: " + ", ".join(placeholders)
|
| 95 |
+
)
|
| 96 |
+
if strict:
|
| 97 |
+
fail(message)
|
| 98 |
+
warn(message)
|
| 99 |
+
else:
|
| 100 |
+
ok("Required SimpleFold weights look like real files")
|
| 101 |
+
|
| 102 |
+
missing_esm = [name for name in OPTIONAL_ESM_WEIGHTS if not (ROOT / "weight" / name).exists()]
|
| 103 |
+
if missing_esm:
|
| 104 |
+
warn(
|
| 105 |
+
"ESM-2 3B local weights are not present. Put them under weight/esm_models "
|
| 106 |
+
"before running inference: " + ", ".join(missing_esm)
|
| 107 |
+
)
|
| 108 |
+
else:
|
| 109 |
+
ok("Local ESM-2 3B files are present")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def check_imports(strict: bool) -> None:
|
| 113 |
+
modules = [
|
| 114 |
+
"modules.models.simplefold.flow",
|
| 115 |
+
"modules.models.simplefold.torch.architecture",
|
| 116 |
+
"modules.datapipes.simplefold.processor.protein_processor",
|
| 117 |
+
"modules.utils.simplefold.esm_utils",
|
| 118 |
+
"modules.datapipes.boltz_data_pipeline.types",
|
| 119 |
+
"modules.models.esm.pretrained",
|
| 120 |
+
]
|
| 121 |
+
missing_dependencies = []
|
| 122 |
+
for module_name in modules:
|
| 123 |
+
try:
|
| 124 |
+
importlib.import_module(module_name)
|
| 125 |
+
except ModuleNotFoundError as exc:
|
| 126 |
+
missing_dependencies.append(f"{module_name}: {exc.name}")
|
| 127 |
+
if missing_dependencies:
|
| 128 |
+
message = (
|
| 129 |
+
"Some imports need external Python dependencies. Install requirements_dcu.txt "
|
| 130 |
+
"and rerun with --strict-imports for a full check: "
|
| 131 |
+
+ "; ".join(missing_dependencies)
|
| 132 |
+
)
|
| 133 |
+
if strict:
|
| 134 |
+
fail(message)
|
| 135 |
+
warn(message)
|
| 136 |
+
else:
|
| 137 |
+
ok("Bundled Python modules import successfully")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def main() -> None:
|
| 141 |
+
parser = argparse.ArgumentParser(description="Check packaged SimpleFold runtime files.")
|
| 142 |
+
parser.add_argument("--strict-weights", action="store_true")
|
| 143 |
+
parser.add_argument("--strict-imports", action="store_true")
|
| 144 |
+
args = parser.parse_args()
|
| 145 |
+
|
| 146 |
+
check_files()
|
| 147 |
+
check_weights(strict=args.strict_weights)
|
| 148 |
+
check_imports(strict=args.strict_imports)
|
| 149 |
+
ok("SimpleFold package preflight finished")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
main()
|
scripts/process_data.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
#
|
| 3 |
+
# For licensing see accompanying LICENSE file.
|
| 4 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import multiprocessing
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
if str(ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
from modules.datapipes.simplefold.process_mmcif import process
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
if __name__ == "__main__":
|
| 20 |
+
parser = argparse.ArgumentParser(description="Process MMCIF data.")
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--data_dir",
|
| 23 |
+
type=Path,
|
| 24 |
+
required=True,
|
| 25 |
+
help="The directory containing the MMCIF files.",
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--out_dir",
|
| 29 |
+
type=Path,
|
| 30 |
+
default="data",
|
| 31 |
+
help="The output directory.",
|
| 32 |
+
)
|
| 33 |
+
parser.add_argument(
|
| 34 |
+
"--num-processes",
|
| 35 |
+
type=int,
|
| 36 |
+
default=multiprocessing.cpu_count(),
|
| 37 |
+
help="The number of processes.",
|
| 38 |
+
)
|
| 39 |
+
parser.add_argument(
|
| 40 |
+
"--redis-host",
|
| 41 |
+
type=str,
|
| 42 |
+
default="localhost",
|
| 43 |
+
help="The Redis host, only used with --use-redis.",
|
| 44 |
+
)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--redis-port",
|
| 47 |
+
type=int,
|
| 48 |
+
default=7777,
|
| 49 |
+
help="The Redis port, only used with --use-redis.",
|
| 50 |
+
)
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--use-redis",
|
| 53 |
+
action="store_true",
|
| 54 |
+
help="Use an external Redis CCD resource instead of local weight/ccd.pkl.",
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--ccd-path",
|
| 58 |
+
type=Path,
|
| 59 |
+
default=ROOT / "weight" / "ccd.pkl",
|
| 60 |
+
help="Path to the local CCD pickle file.",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--use-assembly",
|
| 64 |
+
action="store_true",
|
| 65 |
+
help="Whether to use assembly 1.",
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--max-file-size",
|
| 69 |
+
type=int,
|
| 70 |
+
default=None,
|
| 71 |
+
)
|
| 72 |
+
args = parser.parse_args()
|
| 73 |
+
process(args)
|
scripts/run_inference.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run SimpleFold structure prediction from the packaged ModelScope layout."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
if str(ROOT) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(ROOT))
|
| 13 |
+
|
| 14 |
+
from inference import predict_structures_from_fastas
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 18 |
+
parser = argparse.ArgumentParser(description="Run SimpleFold structure prediction.")
|
| 19 |
+
parser.add_argument("--simplefold_model", default="simplefold_100M")
|
| 20 |
+
parser.add_argument("--ckpt_dir", default="weight")
|
| 21 |
+
parser.add_argument("--output_dir", default="outputs/minimal_inference")
|
| 22 |
+
parser.add_argument("--num_steps", type=int, default=10)
|
| 23 |
+
parser.add_argument("--tau", type=float, default=0.01)
|
| 24 |
+
parser.add_argument("--no_log_timesteps", action="store_true")
|
| 25 |
+
parser.add_argument("--fasta_path", default="examples/minimal.fasta")
|
| 26 |
+
parser.add_argument("--nsample_per_protein", type=int, default=1)
|
| 27 |
+
parser.add_argument("--plddt", action="store_true")
|
| 28 |
+
parser.add_argument("--output_format", default="mmcif", choices=["pdb", "mmcif"])
|
| 29 |
+
parser.add_argument("--backend", default="torch", choices=["torch", "mlx"])
|
| 30 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 31 |
+
return parser
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main() -> None:
|
| 35 |
+
args = build_parser().parse_args()
|
| 36 |
+
predict_structures_from_fastas(args)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
main()
|
| 41 |
+
|
scripts/tokenize_data.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
#
|
| 3 |
+
# For licensing see accompanying LICENSE file.
|
| 4 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
if str(ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
from onescience.datapipes.boltz_data_pipeline.tokenize.boltz_protein import BoltzTokenizer
|
| 17 |
+
from onescience.datapipes.boltz_data_pipeline.types import Manifest
|
| 18 |
+
from onescience.datapipes.simplefold.process_structure import tokenize_structure, finalize
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
parser = argparse.ArgumentParser(description="Tokenize structure data.")
|
| 23 |
+
parser.add_argument(
|
| 24 |
+
"--target_dir",
|
| 25 |
+
type=str,
|
| 26 |
+
required=True,
|
| 27 |
+
help="Directory containing the processed structure data.",
|
| 28 |
+
)
|
| 29 |
+
parser.add_argument(
|
| 30 |
+
"--token_dir",
|
| 31 |
+
type=str,
|
| 32 |
+
required=True,
|
| 33 |
+
help="Directory to save the tokenized data.",
|
| 34 |
+
)
|
| 35 |
+
args = parser.parse_args()
|
| 36 |
+
|
| 37 |
+
target_dir = Path(args.target_dir)
|
| 38 |
+
manifest_path = target_dir / "manifest.json"
|
| 39 |
+
manifest: Manifest = Manifest.load(manifest_path)
|
| 40 |
+
tokenizer = BoltzTokenizer()
|
| 41 |
+
records = manifest.records
|
| 42 |
+
print(f"Number of records after filtering: {len(records)}")
|
| 43 |
+
|
| 44 |
+
save_token_dir = Path(args.token_dir) / "tokens"
|
| 45 |
+
save_token_record_dir = Path(args.token_dir) / "records"
|
| 46 |
+
save_token_dir.mkdir(parents=True, exist_ok=True)
|
| 47 |
+
save_token_record_dir.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
for record in tqdm(records):
|
| 50 |
+
tokenize_structure(
|
| 51 |
+
record,
|
| 52 |
+
tokenizer,
|
| 53 |
+
target_dir,
|
| 54 |
+
str(save_token_dir),
|
| 55 |
+
save_token_record_dir,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
finalize(Path(args.token_dir))
|
scripts/train.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import lightning.pytorch as pl
|
| 8 |
+
from lightning.pytorch import LightningDataModule, LightningModule
|
| 9 |
+
import hydra
|
| 10 |
+
from omegaconf import OmegaConf
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
if str(ROOT) not in sys.path:
|
| 17 |
+
sys.path.insert(0, str(ROOT))
|
| 18 |
+
|
| 19 |
+
from onescience.utils.simplefold.utils import (
|
| 20 |
+
extras,
|
| 21 |
+
create_folders,
|
| 22 |
+
task_wrapper,
|
| 23 |
+
)
|
| 24 |
+
from onescience.utils.simplefold.instantiators import (
|
| 25 |
+
instantiate_callbacks,
|
| 26 |
+
instantiate_loggers,
|
| 27 |
+
instantiate_trainer,
|
| 28 |
+
)
|
| 29 |
+
from onescience.utils.simplefold.logging_utils import log_hyperparameters
|
| 30 |
+
from onescience.utils.simplefold.pylogger import RankedLogger
|
| 31 |
+
|
| 32 |
+
log = RankedLogger(__name__, rank_zero_only=True)
|
| 33 |
+
|
| 34 |
+
ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
|
| 35 |
+
|
| 36 |
+
torch.set_float32_matmul_precision("medium")
|
| 37 |
+
torch.backends.cuda.matmul.allow_tf32 = True # This flag defaults to False
|
| 38 |
+
torch.backends.cudnn.allow_tf32 = True # This flag defaults to True
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def configure_local_esm_registry(esm_model: str | None) -> None:
|
| 42 |
+
if esm_model != "esm2_3B":
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
model_path = Path(
|
| 46 |
+
os.getenv(
|
| 47 |
+
"SIMPLEFOLD_ESM2_MODEL_PATH",
|
| 48 |
+
ROOT / "weight" / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
+
regression_path = model_path.with_name(f"{model_path.stem}-contact-regression.pt")
|
| 52 |
+
|
| 53 |
+
if not model_path.exists() or not regression_path.exists():
|
| 54 |
+
raise FileNotFoundError(
|
| 55 |
+
"Local ESM-2 3B weights were not found. Expected files:\n"
|
| 56 |
+
f" {model_path}\n"
|
| 57 |
+
f" {regression_path}\n"
|
| 58 |
+
"Set SIMPLEFOLD_ESM2_MODEL_PATH to the local .pt file if it lives elsewhere."
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
import models.esm.pretrained as local_esm_pretrained
|
| 62 |
+
import onescience.utils.simplefold.esm_utils as esm_utils
|
| 63 |
+
|
| 64 |
+
def load_local_esm2_3b():
|
| 65 |
+
print(f"Loading ESM-2 3B weights from local path: {model_path}")
|
| 66 |
+
return local_esm_pretrained.load_model_and_alphabet(str(model_path))
|
| 67 |
+
|
| 68 |
+
esm_utils.esm_registry["esm2_3B"] = load_local_esm2_3b
|
| 69 |
+
simplefold_module = sys.modules.get("models.simplefold.simplefold")
|
| 70 |
+
if simplefold_module is not None:
|
| 71 |
+
simplefold_module.esm_registry["esm2_3B"] = load_local_esm2_3b
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@task_wrapper
|
| 75 |
+
def train(cfg):
|
| 76 |
+
seed = cfg.get("seed", 42)
|
| 77 |
+
pl.seed_everything(seed, workers=True)
|
| 78 |
+
configure_local_esm_registry(cfg.model.get("esm_model", None))
|
| 79 |
+
|
| 80 |
+
log.info(f"Instantiating model <{cfg.model._target_}>")
|
| 81 |
+
model: LightningModule = hydra.utils.instantiate(cfg.model)
|
| 82 |
+
load_ckpt_path = cfg.get("load_ckpt_path", None)
|
| 83 |
+
|
| 84 |
+
if load_ckpt_path is not None:
|
| 85 |
+
# load existing ckpt
|
| 86 |
+
log.info(f"Resuming from checkpoint <{cfg.load_ckpt_path}>...")
|
| 87 |
+
model.strict_loading = False
|
| 88 |
+
|
| 89 |
+
# manually reset these variables in case of fine-tuning
|
| 90 |
+
model.lddt_weight_schedule = cfg.model.get("lddt_weight_schedule", False)
|
| 91 |
+
model.plddt_training = cfg.model.get("plddt_training", False)
|
| 92 |
+
|
| 93 |
+
# reset ESM model to avoid issues in loading FSDP checkpoint
|
| 94 |
+
model.reset_esm(cfg.model.esm_model)
|
| 95 |
+
|
| 96 |
+
log.info(f"Instantiating datamodule <{cfg.data._target_}>")
|
| 97 |
+
datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
|
| 98 |
+
|
| 99 |
+
log.info("Instantiating callbacks...")
|
| 100 |
+
callbacks = instantiate_callbacks(cfg.get("callbacks"))
|
| 101 |
+
|
| 102 |
+
log.info("Instantiating loggers...")
|
| 103 |
+
OmegaConf.set_struct(cfg.logger, True)
|
| 104 |
+
loggers = instantiate_loggers(cfg.get("logger"))
|
| 105 |
+
|
| 106 |
+
log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
|
| 107 |
+
trainer = instantiate_trainer(
|
| 108 |
+
cfg.trainer, callbacks=callbacks, logger=loggers, plugins=None
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
object_dict = {
|
| 112 |
+
"cfg": cfg,
|
| 113 |
+
"datamodule": datamodule,
|
| 114 |
+
"model": model,
|
| 115 |
+
"callbacks": callbacks,
|
| 116 |
+
"logger": loggers,
|
| 117 |
+
"trainer": trainer,
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
if log:
|
| 121 |
+
log.info("Logging hyperparameters!")
|
| 122 |
+
log_hyperparameters(object_dict)
|
| 123 |
+
|
| 124 |
+
log.info("Starting training!")
|
| 125 |
+
trainer.fit(
|
| 126 |
+
model=model,
|
| 127 |
+
datamodule=datamodule,
|
| 128 |
+
ckpt_path=load_ckpt_path,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@hydra.main(version_base="1.3", config_path="../config", config_name="train.yaml")
|
| 133 |
+
def submit_run(cfg):
|
| 134 |
+
OmegaConf.resolve(cfg)
|
| 135 |
+
extras(cfg)
|
| 136 |
+
create_folders(cfg)
|
| 137 |
+
train(cfg)
|
| 138 |
+
return
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
submit_run()
|
scripts/train_fsdp.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# For licensing see accompanying LICENSE file.
|
| 3 |
+
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import hydra
|
| 8 |
+
import torch
|
| 9 |
+
import functools
|
| 10 |
+
from omegaconf import OmegaConf
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 15 |
+
if str(ROOT) not in sys.path:
|
| 16 |
+
sys.path.insert(0, str(ROOT))
|
| 17 |
+
|
| 18 |
+
import lightning.pytorch as pl
|
| 19 |
+
from lightning.pytorch import LightningDataModule, LightningModule
|
| 20 |
+
from lightning.pytorch.strategies import FSDPStrategy
|
| 21 |
+
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
|
| 22 |
+
|
| 23 |
+
from models.simplefold.torch.blocks import DiTBlock
|
| 24 |
+
from onescience.utils.simplefold.utils import (
|
| 25 |
+
extras,
|
| 26 |
+
create_folders,
|
| 27 |
+
task_wrapper,
|
| 28 |
+
)
|
| 29 |
+
from onescience.utils.simplefold.instantiators import (
|
| 30 |
+
instantiate_callbacks,
|
| 31 |
+
instantiate_loggers,
|
| 32 |
+
)
|
| 33 |
+
from onescience.utils.simplefold.logging_utils import log_hyperparameters
|
| 34 |
+
from onescience.utils.simplefold.pylogger import RankedLogger
|
| 35 |
+
|
| 36 |
+
log = RankedLogger(__name__, rank_zero_only=True)
|
| 37 |
+
ESM2_MODEL_NAME = "esm2_t36_3B_UR50D"
|
| 38 |
+
torch.set_float32_matmul_precision("medium")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def configure_local_esm_registry(esm_model: str | None) -> None:
|
| 42 |
+
if esm_model != "esm2_3B":
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
model_path = Path(
|
| 46 |
+
os.getenv(
|
| 47 |
+
"SIMPLEFOLD_ESM2_MODEL_PATH",
|
| 48 |
+
ROOT / "weight" / "esm_models" / f"{ESM2_MODEL_NAME}.pt",
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
+
regression_path = model_path.with_name(f"{model_path.stem}-contact-regression.pt")
|
| 52 |
+
|
| 53 |
+
if not model_path.exists() or not regression_path.exists():
|
| 54 |
+
raise FileNotFoundError(
|
| 55 |
+
"Local ESM-2 3B weights were not found. Expected files:\n"
|
| 56 |
+
f" {model_path}\n"
|
| 57 |
+
f" {regression_path}\n"
|
| 58 |
+
"Set SIMPLEFOLD_ESM2_MODEL_PATH to the local .pt file if it lives elsewhere."
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
import models.esm.pretrained as local_esm_pretrained
|
| 62 |
+
import onescience.utils.simplefold.esm_utils as esm_utils
|
| 63 |
+
|
| 64 |
+
def load_local_esm2_3b():
|
| 65 |
+
print(f"Loading ESM-2 3B weights from local path: {model_path}")
|
| 66 |
+
return local_esm_pretrained.load_model_and_alphabet(str(model_path))
|
| 67 |
+
|
| 68 |
+
esm_utils.esm_registry["esm2_3B"] = load_local_esm2_3b
|
| 69 |
+
simplefold_module = sys.modules.get("models.simplefold.simplefold")
|
| 70 |
+
if simplefold_module is not None:
|
| 71 |
+
simplefold_module.esm_registry["esm2_3B"] = load_local_esm2_3b
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@task_wrapper
|
| 75 |
+
def train(cfg):
|
| 76 |
+
seed = cfg.get("seed", 42)
|
| 77 |
+
pl.seed_everything(seed, workers=True)
|
| 78 |
+
configure_local_esm_registry(cfg.model.get("esm_model", None))
|
| 79 |
+
|
| 80 |
+
log.info(f"Instantiating model <{cfg.model._target_}>")
|
| 81 |
+
model: LightningModule = hydra.utils.instantiate(cfg.model)
|
| 82 |
+
load_ckpt_path = cfg.get("load_ckpt_path", None)
|
| 83 |
+
|
| 84 |
+
if load_ckpt_path is not None:
|
| 85 |
+
# load existing ckpt
|
| 86 |
+
log.info(f"Resuming from checkpoint <{cfg.load_ckpt_path}>...")
|
| 87 |
+
model.strict_loading = False
|
| 88 |
+
|
| 89 |
+
# manually reset these variables in case of fine-tuning
|
| 90 |
+
model.lddt_weight_schedule = cfg.model.get("lddt_weight_schedule", False)
|
| 91 |
+
model.plddt_training = cfg.model.get("plddt_training", False)
|
| 92 |
+
|
| 93 |
+
log.info(f"Instantiating datamodule <{cfg.data._target_}>")
|
| 94 |
+
datamodule: LightningDataModule = hydra.utils.instantiate(cfg.data)
|
| 95 |
+
|
| 96 |
+
log.info("Instantiating callbacks...")
|
| 97 |
+
callbacks = instantiate_callbacks(cfg.get("callbacks"))
|
| 98 |
+
|
| 99 |
+
log.info("Instantiating loggers...")
|
| 100 |
+
OmegaConf.set_struct(cfg.logger, True)
|
| 101 |
+
loggers = instantiate_loggers(cfg.get("logger"))
|
| 102 |
+
|
| 103 |
+
# When using FSDP, we need to manually specify the wrap policy
|
| 104 |
+
# and activation checkpointing policy for transformer layers.
|
| 105 |
+
|
| 106 |
+
log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
|
| 107 |
+
|
| 108 |
+
esm_layer_class = model.esm_model.layers[0].__class__
|
| 109 |
+
|
| 110 |
+
transformer_auto_wrapper_policy = functools.partial(
|
| 111 |
+
transformer_auto_wrap_policy,
|
| 112 |
+
transformer_layer_cls={DiTBlock, esm_layer_class},
|
| 113 |
+
)
|
| 114 |
+
strategy = FSDPStrategy(
|
| 115 |
+
auto_wrap_policy=transformer_auto_wrapper_policy,
|
| 116 |
+
activation_checkpointing_policy={DiTBlock, esm_layer_class},
|
| 117 |
+
use_orig_params=True,
|
| 118 |
+
state_dict_type="sharded",
|
| 119 |
+
limit_all_gathers=True,
|
| 120 |
+
cpu_offload=False
|
| 121 |
+
)
|
| 122 |
+
trainer = hydra.utils.instantiate(
|
| 123 |
+
cfg.trainer,
|
| 124 |
+
strategy=strategy,
|
| 125 |
+
callbacks=callbacks,
|
| 126 |
+
logger=loggers,
|
| 127 |
+
plugins=None
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
object_dict = {
|
| 131 |
+
"cfg": cfg,
|
| 132 |
+
"datamodule": datamodule,
|
| 133 |
+
"model": model,
|
| 134 |
+
"callbacks": callbacks,
|
| 135 |
+
"logger": loggers,
|
| 136 |
+
"trainer": trainer,
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if log:
|
| 140 |
+
log.info("Logging hyperparameters!")
|
| 141 |
+
log_hyperparameters(object_dict)
|
| 142 |
+
|
| 143 |
+
log.info("Starting training!")
|
| 144 |
+
trainer.fit(
|
| 145 |
+
model=model,
|
| 146 |
+
datamodule=datamodule,
|
| 147 |
+
ckpt_path=load_ckpt_path,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@hydra.main(version_base="1.3", config_path="../config", config_name="base_train.yaml")
|
| 152 |
+
def submit_run(cfg):
|
| 153 |
+
OmegaConf.resolve(cfg)
|
| 154 |
+
extras(cfg)
|
| 155 |
+
create_folders(cfg)
|
| 156 |
+
train(cfg)
|
| 157 |
+
return
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
submit_run()
|