1use crate::esmc::layers::rotary::RotaryEmbedding;
2use crate::esmc::models::esmc::ESMCConfig;
3use candle_core::{Module, Result, Tensor};
4use candle_nn::{self as nn, LayerNorm, LayerNormConfig, VarBuilder};
5pub struct MultiHeadAttention {
8 d_model: usize,
9 n_heads: usize,
10 d_head: usize,
11 layernorm_qkv: nn::Sequential,
12 out_proj: nn::Linear,
13 q_ln: Box<dyn Module>,
14 k_ln: Box<dyn Module>,
15 rotary: RotaryEmbedding,
16}
17
18impl MultiHeadAttention {
19 pub fn load(vb: VarBuilder, config: &ESMCConfig) -> Result<Self> {
49 let ESMCConfig {
50 d_model, n_heads, ..
51 } = config;
52
53 let d_head = d_model / n_heads;
54
55 let layernorm = nn::layer_norm(
57 *d_model,
58 LayerNormConfig::from(1e-5),
59 vb.pp("layernorm_qkv.0"),
60 )?;
61 let linear = nn::linear_no_bias(*d_model, d_model * 3, vb.pp("layernorm_qkv.1"))?;
62 let layernorm_qkv = nn::seq().add(layernorm).add(linear);
63 let out_proj = nn::linear_no_bias(*d_model, *d_model, vb.pp("out_proj"))?;
64
65 let q_ln: Box<dyn Module> = {
67 let w = vb.pp("q_ln").get((*d_model,), "weight")?;
68 Box::new(LayerNorm::new_no_bias(w, 1e-5))
69 };
70 let k_ln: Box<dyn Module> = {
71 let w = vb.pp("k_ln").get((*d_model,), "weight")?;
72 Box::new(LayerNorm::new_no_bias(w, 1e-5))
73 };
74
75 let rotary = RotaryEmbedding::load(vb.pp("rotary"), config)?;
76
77 Ok(Self {
78 d_model: *d_model,
79 n_heads: *n_heads,
80 d_head,
81 layernorm_qkv,
82 out_proj,
83 q_ln,
84 k_ln,
85 rotary,
86 })
87 }
88
89 pub fn forward(&self, x: &Tensor, sequence_id: Option<&Tensor>) -> Result<Tensor> {
90 let (b, l, _) = x.dims3()?;
91 let qkv = self.layernorm_qkv.forward(x)?;
93 let chunks = qkv.chunk(3, candle_core::D::Minus1)?;
94 let (q, k, v) = (&chunks[0], &chunks[1], &chunks[2]);
95
96 let q = self.q_ln.forward(q)?;
98 let k = self.k_ln.forward(k)?;
99
100 let q = q
102 .reshape((b, l, self.n_heads, self.d_head))?
103 .transpose(1, 2)?;
104 let k = k
105 .reshape((b, l, self.n_heads, self.d_head))?
106 .transpose(1, 2)?;
107 let v = v
108 .reshape((b, l, self.n_heads, self.d_head))?
109 .transpose(1, 2)?;
110
111 let (q, k) = self.rotary.forward(&q, &k)?;
113
114 let scale = (self.d_head as f64).sqrt().recip();
118 let k_t = k
119 .transpose(candle_core::D::Minus1, candle_core::D::Minus2)?
120 .contiguous()?;
121 let attn = (q.contiguous()?.matmul(&k_t)? * scale)?;
122
123 let attn = if let Some(seq_id) = sequence_id {
125 let mask = seq_id
127 .unsqueeze(1)?
128 .unsqueeze(1)?
129 .broadcast_as(attn.shape())?;
130 let neg_inf = (Tensor::ones_like(&attn)? * f64::NEG_INFINITY)?;
131 mask.where_cond(&attn, &neg_inf)?
132 } else {
133 attn
134 };
135
136 let attn = candle_nn::ops::softmax(&attn, candle_core::D::Minus1)?;
137
138 let context = attn.matmul(&v.contiguous()?)?; let context = context
141 .transpose(1, 2)?
142 .contiguous()?
143 .reshape((b, l, self.d_model))?;
144
145 self.out_proj.forward(&context)
146 }
147
148 }
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::esmc::models::esmc::ESMCConfig;
187 use candle_core::{DType, Device};
188 use std::collections::HashMap;
189
190 fn tiny_attention(device: &Device) -> Result<(MultiHeadAttention, ESMCConfig)> {
196 let config = ESMCConfig {
197 d_model: 8,
198 n_heads: 2,
199 n_layers: 1,
200 ..ESMCConfig::esmc_300m()
201 };
202 let d = config.d_model;
203
204 let mut state = 0x9E37_79B9_7F4A_7C15u64;
206 let mut rand = |dims: &[usize], device: &Device| -> Result<Tensor> {
207 let n: usize = dims.iter().product();
208 let v: Vec<f32> = (0..n)
209 .map(|_| {
210 state = state
211 .wrapping_mul(6364136223846793005)
212 .wrapping_add(1442695040888963407);
213 ((state >> 33) as f32 / (1u64 << 31) as f32) - 0.5
214 })
215 .collect();
216 Tensor::from_vec(v, dims, device)
217 };
218
219 let mut ts: HashMap<String, Tensor> = HashMap::new();
220 ts.insert(
221 "layernorm_qkv.0.weight".into(),
222 Tensor::ones(d, DType::F32, device)?,
223 );
224 ts.insert(
225 "layernorm_qkv.0.bias".into(),
226 Tensor::zeros(d, DType::F32, device)?,
227 );
228 ts.insert("layernorm_qkv.1.weight".into(), rand(&[d * 3, d], device)?);
229 ts.insert("out_proj.weight".into(), rand(&[d, d], device)?);
230 ts.insert("q_ln.weight".into(), Tensor::ones(d, DType::F32, device)?);
231 ts.insert("k_ln.weight".into(), Tensor::ones(d, DType::F32, device)?);
232
233 let vb = VarBuilder::from_tensors(ts, DType::F32, device);
234 let attn = MultiHeadAttention::load(vb, &config)?;
235 Ok((attn, config))
236 }
237
238 fn max_abs_diff(a: &Tensor, b: &Tensor) -> Result<f32> {
239 let a: Vec<f32> = a.flatten_all()?.to_vec1()?;
240 let b: Vec<f32> = b.flatten_all()?.to_vec1()?;
241 assert_eq!(a.len(), b.len(), "shape mismatch in comparison");
242 Ok(a.iter()
243 .zip(b.iter())
244 .map(|(x, y)| (x - y).abs())
245 .fold(0f32, f32::max))
246 }
247
248 fn inputs(len: usize, padded: usize, d: usize, device: &Device) -> Result<(Tensor, Tensor)> {
251 let real: Vec<f32> = (0..len * d).map(|i| (i as f32 * 0.37).sin()).collect();
252 let mut padded_v = real.clone();
253 padded_v.extend(std::iter::repeat_n(0.9f32, (padded - len) * d));
254 Ok((
255 Tensor::from_vec(real, (1, len, d), device)?,
256 Tensor::from_vec(padded_v, (1, padded, d), device)?,
257 ))
258 }
259
260 fn mask_row(len: usize, padded: usize, device: &Device) -> Result<Tensor> {
261 let mut m = vec![1u8; len];
262 m.resize(padded, 0);
263 Tensor::from_vec(m, (1, padded), device)
264 }
265
266 #[test]
270 fn test_sequence_id_mask_isolates_padded_keys() -> Result<()> {
271 let device = Device::Cpu;
272 let (attn, config) = tiny_attention(&device)?;
273 let (len, padded) = (5usize, 9usize);
274 let (x, x_padded) = inputs(len, padded, config.d_model, &device)?;
275
276 let alone = attn.forward(&x, None)?;
277
278 let mask = mask_row(len, padded, &device)?;
279 let masked = attn.forward(&x_padded, Some(&mask))?.narrow(1, 0, len)?;
280 let diff = max_abs_diff(&alone, &masked)?;
281 assert!(
282 diff < 1e-5,
283 "masked padding changed the real positions: max abs diff {diff}"
284 );
285
286 let unmasked = attn.forward(&x_padded, None)?.narrow(1, 0, len)?;
287 let leak = max_abs_diff(&alone, &unmasked)?;
288 assert!(
289 leak > 1e-4,
290 "padding without sequence_id should perturb the real positions, but max abs diff was {leak}"
291 );
292 Ok(())
293 }
294
295 #[test]
300 fn test_batched_rows_match_single_row_attention() -> Result<()> {
301 let device = Device::Cpu;
302 let (attn, config) = tiny_attention(&device)?;
303 let d = config.d_model;
304 let (long, short) = (7usize, 3usize);
305
306 let (x_long, _) = inputs(long, long, d, &device)?;
307 let (x_short, x_short_padded) = inputs(short, long, d, &device)?;
308
309 let batch = Tensor::cat(&[&x_long, &x_short_padded], 0)?;
310 let mask = Tensor::cat(
311 &[
312 &mask_row(long, long, &device)?,
313 &mask_row(short, long, &device)?,
314 ],
315 0,
316 )?;
317 let batched = attn.forward(&batch, Some(&mask))?;
318
319 for (row, x, len) in [(0usize, &x_long, long), (1usize, &x_short, short)] {
320 let alone = attn.forward(x, None)?;
321 let from_batch = batched.narrow(0, row, 1)?.narrow(1, 0, len)?;
322 let diff = max_abs_diff(&alone, &from_batch)?;
323 assert!(
324 diff < 1e-5,
325 "batch row {row} disagrees with its single-row attention: {diff}"
326 );
327 }
328 Ok(())
329 }
330}