Skip to main content

ferritin_plms/esmfold2/layers/
lm_encoder.rs

1//! LM encoder adapter: projects ESMC-6B hidden states → single representation.
2//!
3//! Four standard pre-norm transformer blocks (self-attention + SwiGLU FFN)
4//! running at d_model=2560, followed by a LayerNorm and a linear projection
5//! to d_single=384.
6//!
7//! Weight layout (rooted at `lm_encoder`):
8//! ```text
9//! blocks.{0..3}.attn.layernorm_qkv.0.*  — pre-attn LayerNorm
10//! blocks.{0..3}.attn.layernorm_qkv.1.*  — QKV linear (no bias)
11//! blocks.{0..3}.attn.out_proj.*         — output projection (no bias)
12//! blocks.{0..3}.attn.q_ln.*             — per-head Q LayerNorm (no bias)
13//! blocks.{0..3}.attn.k_ln.*             — per-head K LayerNorm (no bias)
14//! blocks.{0..3}.ffn.0.*                 — pre-FFN LayerNorm
15//! blocks.{0..3}.ffn.1.*                 — gate+up projection (no bias)
16//! blocks.{0..3}.ffn.3.*                 — down projection (no bias)
17//! norm.*                                — final LayerNorm
18//! proj.*                                — linear d_in → d_out (no bias)
19//! ```
20
21use candle_core::{D, Result, Tensor};
22use candle_nn::{self as nn, LayerNorm, LayerNormConfig, Module, VarBuilder};
23
24// ── Attention ─────────────────────────────────────────────────────────────
25
26struct LMAttention {
27    layernorm_qkv: nn::Sequential,
28    out_proj: nn::Linear,
29    q_ln: LayerNorm,
30    k_ln: LayerNorm,
31    n_heads: usize,
32    d_head: usize,
33}
34
35impl LMAttention {
36    fn load(vb: VarBuilder, d_model: usize, n_heads: usize) -> Result<Self> {
37        let d_head = d_model / n_heads;
38        let norm = nn::layer_norm(
39            d_model,
40            LayerNormConfig::from(1e-5),
41            vb.pp("layernorm_qkv.0"),
42        )?;
43        let qkv = nn::linear_no_bias(d_model, d_model * 3, vb.pp("layernorm_qkv.1"))?;
44        let layernorm_qkv = nn::seq().add(norm).add(qkv);
45        let out_proj = nn::linear_no_bias(d_model, d_model, vb.pp("out_proj"))?;
46
47        let q_ln = LayerNorm::new_no_bias(vb.pp("q_ln").get((d_model,), "weight")?, 1e-5);
48        let k_ln = LayerNorm::new_no_bias(vb.pp("k_ln").get((d_model,), "weight")?, 1e-5);
49
50        Ok(Self {
51            layernorm_qkv,
52            out_proj,
53            q_ln,
54            k_ln,
55            n_heads,
56            d_head,
57        })
58    }
59
60    fn forward(&self, x: &Tensor) -> Result<Tensor> {
61        let (b, l, _) = x.dims3()?;
62
63        // Pre-norm + QKV projection: [B, L, 3*d_model]
64        let qkv = self.layernorm_qkv.forward(x)?;
65        let chunks = qkv.chunk(3, D::Minus1)?;
66        let (q, k, v) = (&chunks[0], &chunks[1], &chunks[2]);
67
68        // Per-head LayerNorms on full d_model before reshape
69        let q = self.q_ln.forward(q)?;
70        let k = self.k_ln.forward(k)?;
71
72        // [B, L, d_model] → [B, n_heads, L, d_head] → [B*n_heads, L, d_head]
73        let reshape_heads = |t: &Tensor| -> Result<Tensor> {
74            t.reshape((b, l, self.n_heads, self.d_head))?
75                .transpose(1, 2)?
76                .reshape((b * self.n_heads, l, self.d_head))
77        };
78        let q = reshape_heads(&q)?;
79        let k = reshape_heads(&k)?;
80        let v = reshape_heads(v)?;
81
82        // Scaled dot-product attention
83        let scale = (self.d_head as f64).sqrt();
84        let scores = (q.matmul(&k.transpose(D::Minus2, D::Minus1)?)? / scale)?;
85        let weights = nn::ops::softmax(&scores, D::Minus1)?;
86        let out = weights.matmul(&v)?; // [B*n_heads, L, d_head]
87
88        // [B*n_heads, L, d_head] → [B, L, d_model]
89        let out = out
90            .reshape((b, self.n_heads, l, self.d_head))?
91            .transpose(1, 2)?
92            .reshape((b, l, self.n_heads * self.d_head))?;
93
94        self.out_proj.forward(&out)
95    }
96}
97
98// ── SwiGLU FFN ────────────────────────────────────────────────────────────
99
100struct LMFfn {
101    norm: LayerNorm,
102    gate_up_proj: nn::Linear,
103    down_proj: nn::Linear,
104}
105
106impl LMFfn {
107    fn load(vb: VarBuilder, d_model: usize) -> Result<Self> {
108        // SwiGLU hidden dim: nearest multiple of 256 of (d_model × 8/3)
109        let hidden = ((8.0 / 3.0 * d_model as f64 + 255.0) / 256.0).floor() as usize * 256;
110        Ok(Self {
111            norm: nn::layer_norm(d_model, LayerNormConfig::from(1e-5), vb.pp("0"))?,
112            gate_up_proj: nn::linear_no_bias(d_model, hidden * 2, vb.pp("1"))?,
113            down_proj: nn::linear_no_bias(hidden, d_model, vb.pp("3"))?,
114        })
115    }
116
117    fn forward(&self, x: &Tensor) -> Result<Tensor> {
118        let x = self.norm.forward(x)?;
119        let gate_up = self.gate_up_proj.forward(&x)?;
120        let chunks = gate_up.chunk(2, D::Minus1)?;
121        let hidden = (chunks[0].silu()? * &chunks[1])?;
122        self.down_proj.forward(&hidden)
123    }
124}
125
126// ── Transformer block ──────────────────────────────────────────────────────
127
128struct LMBlock {
129    attn: LMAttention,
130    ffn: LMFfn,
131}
132
133impl LMBlock {
134    fn load(vb: VarBuilder, d_model: usize, n_heads: usize) -> Result<Self> {
135        Ok(Self {
136            attn: LMAttention::load(vb.pp("attn"), d_model, n_heads)?,
137            ffn: LMFfn::load(vb.pp("ffn"), d_model)?,
138        })
139    }
140
141    fn forward(&self, x: &Tensor) -> Result<Tensor> {
142        let x = (x + &self.attn.forward(x)?)?;
143        &x + &self.ffn.forward(&x)?
144    }
145}
146
147// ── LMEncoder ─────────────────────────────────────────────────────────────
148
149/// 4-layer transformer adapter that maps ESMC-6B hidden states
150/// (`[B, L, d_in=2560]`) to the ESMFold2 single representation (`[B, L, d_out=384]`).
151pub struct LMEncoder {
152    blocks: Vec<LMBlock>,
153    norm: LayerNorm,
154    proj: nn::Linear,
155}
156
157impl LMEncoder {
158    /// Load the LM encoder from a `VarBuilder` rooted at `lm_encoder.*`.
159    ///
160    /// `n_heads` is derived as `d_in / 64` (standard head size; 2560/64 = 40 for ESMC-6B).
161    pub fn load(vb: VarBuilder, d_in: usize, d_out: usize, n_layers: usize) -> Result<Self> {
162        let n_heads = d_in / 64; // 40 for d_in=2560
163        let blocks = (0..n_layers)
164            .map(|i| LMBlock::load(vb.pp(format!("blocks.{i}")), d_in, n_heads))
165            .collect::<Result<Vec<_>>>()?;
166        let norm = nn::layer_norm(d_in, LayerNormConfig::from(1e-5), vb.pp("norm"))?;
167        let proj = nn::linear_no_bias(d_in, d_out, vb.pp("proj"))?;
168        Ok(Self { blocks, norm, proj })
169    }
170
171    /// Project ESMC-6B hidden states to the single representation.
172    ///
173    /// - Input:  `[B, L, d_in=2560]`
174    /// - Output: `[B, L, d_out=384]`
175    pub fn forward(&self, x: &Tensor) -> Result<Tensor> {
176        let mut x = x.clone();
177        for block in &self.blocks {
178            x = block.forward(&x)?;
179        }
180        self.proj.forward(&self.norm.forward(&x)?)
181    }
182}
183
184// ── Tests ──────────────────────────────────────────────────────────────────
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use candle_core::{DType, Device, Tensor};
190
191    #[test]
192    fn test_lm_encoder_output_shape() {
193        let device = Device::Cpu;
194        let vb = VarBuilder::zeros(DType::F32, &device);
195        let encoder = LMEncoder::load(vb, 2560, 384, 4).unwrap();
196        let x = Tensor::zeros(&[1, 16, 2560], DType::F32, &device).unwrap();
197        let out = encoder.forward(&x).unwrap();
198        assert_eq!(out.dims(), &[1, 16, 384]);
199    }
200
201    #[test]
202    fn test_lm_encoder_batch_invariant_shape() {
203        let device = Device::Cpu;
204        let vb = VarBuilder::zeros(DType::F32, &device);
205        let encoder = LMEncoder::load(vb, 2560, 384, 4).unwrap();
206        let x = Tensor::zeros(&[2, 8, 2560], DType::F32, &device).unwrap();
207        let out = encoder.forward(&x).unwrap();
208        assert_eq!(out.dims(), &[2, 8, 384]);
209    }
210
211    #[test]
212    fn test_lm_ffn_hidden_dim() {
213        // For d_model=2560, SwiGLU hidden = nearest-256 of (2560 * 8/3) = 6912
214        let expected = ((8.0_f64 / 3.0 * 2560.0 + 255.0) / 256.0).floor() as usize * 256;
215        assert_eq!(expected, 6912);
216    }
217}