1use candle_core::{DType, Device, IndexOp, Result, Tensor, D};
18use candle_nn::{
19 embedding, layer_norm, layer_norm_no_bias, linear, linear_no_bias, ops::softmax_last_dim,
20 Embedding, LayerNorm, LayerNormConfig, Linear, Module, VarBuilder,
21};
22use std::sync::Arc;
23
24use crate::config::{AgentConfig, EncoderConfig, FULL_ATTENTION};
25
26const MASKED_LOGIT: f64 = -1e4;
28
29#[derive(Debug)]
33pub struct Rotary {
34 cos: Tensor,
35 sin: Tensor,
36}
37
38impl Rotary {
39 fn new(
40 head_dim: usize,
41 max_positions: usize,
42 base: f64,
43 dtype: DType,
44 device: &Device,
45 ) -> Result<Self> {
46 let inv_freq: Vec<f32> = (0..head_dim)
48 .step_by(2)
49 .map(|index| (1.0 / base.powf(index as f64 / head_dim as f64)) as f32)
50 .collect();
51 let width = inv_freq.len();
52 let inv_freq = Tensor::from_vec(inv_freq, (1, width), device)?;
53 let positions = Tensor::arange(0u32, max_positions as u32, device)?
54 .to_dtype(DType::F32)?
55 .reshape((max_positions, 1))?;
56 let freqs = positions.matmul(&inv_freq)?;
57 Ok(Self {
58 cos: freqs.cos()?.to_dtype(dtype)?,
59 sin: freqs.sin()?.to_dtype(dtype)?,
60 })
61 }
62
63 fn apply(&self, q: &Tensor, k: &Tensor, seq_len: usize) -> Result<(Tensor, Tensor)> {
65 let cos = self.cos.narrow(0, 0, seq_len)?;
66 let sin = self.sin.narrow(0, 0, seq_len)?;
67 let q = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
71 let k = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
72 Ok((q, k))
73 }
74}
75
76#[derive(Debug, Clone)]
82pub struct AttentionMasks {
83 pub full: Tensor,
85 pub sliding: Tensor,
87}
88
89impl AttentionMasks {
90 pub fn build(attention_mask: &Tensor, window: usize, dtype: DType) -> Result<Self> {
92 let device = attention_mask.device();
93 let (batch, seq_len) = attention_mask.dims2()?;
94 let valid = attention_mask.to_dtype(DType::U8)?;
95
96 let neg_inf = f32::NEG_INFINITY as f64;
97 let zeros = Tensor::zeros((batch, 1, 1, seq_len), DType::F32, device)?;
98 let blocked = Tensor::full(neg_inf as f32, (batch, 1, 1, seq_len), device)?;
99 let key_valid = valid.reshape((batch, 1, 1, seq_len))?;
100 let full = key_valid.where_cond(&zeros, &blocked)?;
101
102 let half = (window / 2) as i64;
108 let positions = Tensor::arange(0i64, seq_len as i64, device)?;
109 let distance = positions
110 .reshape((seq_len, 1))?
111 .broadcast_sub(&positions.reshape((1, seq_len))?)?;
112 let in_window = distance
113 .le(half)?
114 .mul(&distance.ge(-half)?)?
115 .reshape((1, 1, seq_len, seq_len))?;
116
117 let query_is_pad = valid.reshape((batch, 1, seq_len, 1))?.eq(0u8)?;
119 let allowed = in_window
120 .broadcast_as((batch, 1, seq_len, seq_len))?
121 .maximum(&query_is_pad.broadcast_as((batch, 1, seq_len, seq_len))?)?
122 .mul(&key_valid.broadcast_as((batch, 1, seq_len, seq_len))?)?;
123
124 let zeros = Tensor::zeros((batch, 1, seq_len, seq_len), DType::F32, device)?;
125 let blocked = Tensor::full(neg_inf as f32, (batch, 1, seq_len, seq_len), device)?;
126 let sliding = allowed.where_cond(&zeros, &blocked)?;
127
128 Ok(Self {
129 full: full.to_dtype(dtype)?,
130 sliding: sliding.to_dtype(dtype)?,
131 })
132 }
133
134 fn for_kind(&self, kind: &str) -> &Tensor {
135 if kind == FULL_ATTENTION {
136 &self.full
137 } else {
138 &self.sliding
139 }
140 }
141}
142
143fn attention(q: &Tensor, k: &Tensor, v: &Tensor, scale: f64, mask: &Tensor) -> Result<Tensor> {
145 let scores = (q.matmul(&k.transpose(D::Minus2, D::Minus1)?)? * scale)?;
146 let scores = scores.broadcast_add(mask)?;
147 softmax_last_dim(&scores)?.matmul(v)
148}
149
150#[derive(Debug)]
153struct Embeddings {
154 tok_embeddings: Embedding,
155 norm: LayerNorm,
156}
157
158impl Embeddings {
159 fn load(vb: VarBuilder, cfg: &EncoderConfig) -> Result<Self> {
160 Ok(Self {
161 tok_embeddings: embedding(cfg.vocab_size, cfg.hidden_size, vb.pp("tok_embeddings"))?,
162 norm: norm(cfg.hidden_size, cfg.norm_eps, cfg.norm_bias, vb.pp("norm"))?,
163 })
164 }
165
166 fn forward(&self, ids: &Tensor) -> Result<Tensor> {
167 self.norm.forward(&self.tok_embeddings.forward(ids)?)
168 }
169}
170
171fn norm(size: usize, eps: f64, bias: bool, vb: VarBuilder) -> Result<LayerNorm> {
173 if bias {
174 layer_norm(
175 size,
176 LayerNormConfig {
177 eps,
178 ..Default::default()
179 },
180 vb,
181 )
182 } else {
183 layer_norm_no_bias(size, eps, vb)
184 }
185}
186
187fn maybe_biased_linear(
189 in_dim: usize,
190 out_dim: usize,
191 bias: bool,
192 vb: VarBuilder,
193) -> Result<Linear> {
194 if bias {
195 linear(in_dim, out_dim, vb)
196 } else {
197 linear_no_bias(in_dim, out_dim, vb)
198 }
199}
200
201#[derive(Debug)]
202struct EncoderAttention {
203 wqkv: Linear,
204 wo: Linear,
205 num_heads: usize,
206 head_dim: usize,
207 rotary: Arc<Rotary>,
208}
209
210impl EncoderAttention {
211 fn load(vb: VarBuilder, cfg: &EncoderConfig, rotary: Arc<Rotary>) -> Result<Self> {
212 Ok(Self {
213 wqkv: maybe_biased_linear(
214 cfg.hidden_size,
215 3 * cfg.hidden_size,
216 cfg.attention_bias,
217 vb.pp("Wqkv"),
218 )?,
219 wo: maybe_biased_linear(
220 cfg.hidden_size,
221 cfg.hidden_size,
222 cfg.attention_bias,
223 vb.pp("Wo"),
224 )?,
225 num_heads: cfg.num_attention_heads,
226 head_dim: cfg.head_dim(),
227 rotary,
228 })
229 }
230
231 fn forward(&self, xs: &Tensor, mask: &Tensor) -> Result<Tensor> {
232 let (batch, seq_len, hidden) = xs.dims3()?;
233 let qkv = self
234 .wqkv
235 .forward(xs)?
236 .reshape((batch, seq_len, 3, self.num_heads, self.head_dim))?
237 .permute((2, 0, 3, 1, 4))?;
238 let q = qkv.i(0)?;
239 let k = qkv.i(1)?;
240 let v = qkv.i(2)?.contiguous()?;
241
242 let (q, k) = self.rotary.apply(&q, &k, seq_len)?;
243 let scale = (self.head_dim as f64).powf(-0.5);
244 let out = attention(&q, &k, &v, scale, mask)?;
245 self.wo
246 .forward(&out.transpose(1, 2)?.reshape((batch, seq_len, hidden))?)
247 }
248}
249
250#[derive(Debug)]
251struct EncoderMlp {
252 wi: Linear,
253 wo: Linear,
254}
255
256impl EncoderMlp {
257 fn load(vb: VarBuilder, cfg: &EncoderConfig) -> Result<Self> {
258 Ok(Self {
259 wi: maybe_biased_linear(
260 cfg.hidden_size,
261 2 * cfg.intermediate_size,
262 cfg.mlp_bias,
263 vb.pp("Wi"),
264 )?,
265 wo: maybe_biased_linear(
266 cfg.intermediate_size,
267 cfg.hidden_size,
268 cfg.mlp_bias,
269 vb.pp("Wo"),
270 )?,
271 })
272 }
273}
274
275impl Module for EncoderMlp {
276 fn forward(&self, xs: &Tensor) -> Result<Tensor> {
277 let projected = self.wi.forward(xs)?;
278 let halves = projected.chunk(2, D::Minus1)?;
279 self.wo.forward(&(halves[0].gelu_erf()? * &halves[1])?)
281 }
282}
283
284#[derive(Debug)]
285struct EncoderLayer {
286 attention_type: String,
287 attn_norm: Option<LayerNorm>,
289 attn: EncoderAttention,
290 mlp_norm: LayerNorm,
291 mlp: EncoderMlp,
292}
293
294impl EncoderLayer {
295 fn load(
296 vb: VarBuilder,
297 cfg: &EncoderConfig,
298 index: usize,
299 rotary: Arc<Rotary>,
300 ) -> Result<Self> {
301 let attn_norm = if index == 0 {
302 None
303 } else {
304 Some(norm(
305 cfg.hidden_size,
306 cfg.norm_eps,
307 cfg.norm_bias,
308 vb.pp("attn_norm"),
309 )?)
310 };
311 Ok(Self {
312 attention_type: cfg.layer_types()[index].clone(),
313 attn_norm,
314 attn: EncoderAttention::load(vb.pp("attn"), cfg, rotary)?,
315 mlp_norm: norm(
316 cfg.hidden_size,
317 cfg.norm_eps,
318 cfg.norm_bias,
319 vb.pp("mlp_norm"),
320 )?,
321 mlp: EncoderMlp::load(vb.pp("mlp"), cfg)?,
322 })
323 }
324
325 fn forward(&self, xs: &Tensor, masks: &AttentionMasks) -> Result<Tensor> {
326 let normed = match &self.attn_norm {
327 Some(norm) => norm.forward(xs)?,
328 None => xs.clone(),
329 };
330 let xs = (xs
331 + self
332 .attn
333 .forward(&normed, masks.for_kind(&self.attention_type))?)?;
334 let normed = self.mlp_norm.forward(&xs)?;
335 &xs + self.mlp.forward(&normed)?
336 }
337}
338
339#[derive(Debug)]
340pub struct ModernBert {
341 embeddings: Embeddings,
342 layers: Vec<EncoderLayer>,
343 final_norm: LayerNorm,
344 local_attention: usize,
345}
346
347impl ModernBert {
348 pub fn load(
349 vb: VarBuilder,
350 cfg: &EncoderConfig,
351 dtype: DType,
352 device: &Device,
353 ) -> Result<Self> {
354 let global = Arc::new(Rotary::new(
356 cfg.head_dim(),
357 cfg.max_position_embeddings,
358 cfg.rope_base(FULL_ATTENTION),
359 dtype,
360 device,
361 )?);
362 let local = Arc::new(Rotary::new(
363 cfg.head_dim(),
364 cfg.max_position_embeddings,
365 cfg.rope_base(crate::config::SLIDING_ATTENTION),
366 dtype,
367 device,
368 )?);
369
370 let layers_vb = vb.pp("layers");
371 let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
372 for index in 0..cfg.num_hidden_layers {
373 let rotary = if cfg.layer_types()[index] == FULL_ATTENTION {
374 Arc::clone(&global)
375 } else {
376 Arc::clone(&local)
377 };
378 layers.push(EncoderLayer::load(
379 layers_vb.pp(index.to_string()),
380 cfg,
381 index,
382 rotary,
383 )?);
384 }
385
386 Ok(Self {
387 embeddings: Embeddings::load(vb.pp("embeddings"), cfg)?,
388 layers,
389 final_norm: norm(
390 cfg.hidden_size,
391 cfg.norm_eps,
392 cfg.norm_bias,
393 vb.pp("final_norm"),
394 )?,
395 local_attention: cfg.local_attention,
396 })
397 }
398
399 pub fn forward(&self, input_ids: &Tensor, attention_mask: &Tensor) -> Result<Tensor> {
400 let mut xs = self.embeddings.forward(input_ids)?;
401 let masks = AttentionMasks::build(attention_mask, self.local_attention, xs.dtype())?;
402 for layer in &self.layers {
403 xs = layer.forward(&xs, &masks)?;
404 }
405 self.final_norm.forward(&xs)
406 }
407}
408
409#[derive(Debug)]
412struct HeadAttention {
413 in_proj: Linear,
414 out_proj: Linear,
415 num_heads: usize,
416 head_dim: usize,
417}
418
419impl HeadAttention {
420 fn load(vb: VarBuilder, dims: usize) -> Result<Self> {
421 let num_heads = std::cmp::max(1, dims / 64);
422 if dims % num_heads != 0 {
423 candle_core::bail!("Decision head dimensions must be divisible by its head count");
424 }
425 Ok(Self {
426 in_proj: linear(dims, 3 * dims, vb.pp("in_proj"))?,
427 out_proj: linear(dims, dims, vb.pp("out_proj"))?,
428 num_heads,
429 head_dim: dims / num_heads,
430 })
431 }
432
433 fn forward(&self, xs: &Tensor, mask: &Tensor) -> Result<Tensor> {
434 let (batch, seq_len, dims) = xs.dims3()?;
435 let qkv = self
436 .in_proj
437 .forward(xs)?
438 .reshape((batch, seq_len, 3, self.num_heads, self.head_dim))?
439 .permute((2, 0, 3, 1, 4))?;
440 let q = qkv.i(0)?.contiguous()?;
441 let k = qkv.i(1)?.contiguous()?;
442 let v = qkv.i(2)?.contiguous()?;
443 let scale = (self.head_dim as f64).powf(-0.5);
444 let out = attention(&q, &k, &v, scale, mask)?;
445 self.out_proj
446 .forward(&out.transpose(1, 2)?.reshape((batch, seq_len, dims))?)
447 }
448}
449
450#[derive(Debug)]
451struct HeadLayer {
452 self_attn: HeadAttention,
453 norm1: LayerNorm,
454 norm2: LayerNorm,
455 linear1: Linear,
456 linear2: Linear,
457}
458
459impl HeadLayer {
460 fn load(vb: VarBuilder, dims: usize) -> Result<Self> {
461 Ok(Self {
462 self_attn: HeadAttention::load(vb.pp("self_attn"), dims)?,
463 norm1: layer_norm(dims, LayerNormConfig::default(), vb.pp("norm1"))?,
465 norm2: layer_norm(dims, LayerNormConfig::default(), vb.pp("norm2"))?,
466 linear1: linear(dims, 4 * dims, vb.pp("linear1"))?,
467 linear2: linear(4 * dims, dims, vb.pp("linear2"))?,
468 })
469 }
470
471 fn forward(&self, xs: &Tensor, mask: &Tensor) -> Result<Tensor> {
472 let normed = self.norm1.forward(xs)?;
473 let xs = (xs + self.self_attn.forward(&normed, mask)?)?;
474 let normed = self.norm2.forward(&xs)?;
475 let ff = self
478 .linear2
479 .forward(&self.linear1.forward(&normed)?.relu()?)?;
480 &xs + ff
481 }
482}
483
484#[derive(Debug)]
488pub struct DecisionModel {
489 encoder: ModernBert,
490 head: Vec<HeadLayer>,
491 type_emb: Embedding,
492 scorer_norm: LayerNorm,
493 scorer_in: Linear,
494 scorer_out: Linear,
495 act_in: Linear,
496 act_out: Linear,
497 act_dtype: DType,
498}
499
500#[derive(Debug, Clone)]
502pub struct ModelOutput {
503 pub logits: Tensor,
505 pub action: Tensor,
507}
508
509impl DecisionModel {
510 pub fn load(
511 vb: VarBuilder,
512 encoder_cfg: &EncoderConfig,
513 agent_cfg: &AgentConfig,
514 dtype: DType,
515 device: &Device,
516 ) -> Result<Self> {
517 let dims = encoder_cfg.hidden_size;
518 let head_vb = vb.pp("head").pp("layers");
519 let mut head = Vec::with_capacity(agent_cfg.head_layers);
520 for index in 0..agent_cfg.head_layers {
521 head.push(HeadLayer::load(head_vb.pp(index.to_string()), dims)?);
522 }
523
524 let scorer = vb.pp("scorer").pp("layers");
525 let act = vb.pp("act_head").pp("layers");
526
527 Ok(Self {
528 encoder: ModernBert::load(vb.pp("encoder"), encoder_cfg, dtype, device)?,
529 head,
530 type_emb: embedding(3, dims, vb.pp("type_emb"))?,
531 scorer_norm: layer_norm(dims, LayerNormConfig::default(), scorer.pp("0"))?,
533 scorer_in: linear(dims, dims, scorer.pp("1"))?,
534 scorer_out: linear(dims, 1, scorer.pp("3"))?,
535 act_in: linear(dims + 4, 256, act.pp("0"))?,
537 act_out: linear(256, agent_cfg.act_classes, act.pp("2"))?,
538 act_dtype: dtype,
539 })
540 }
541
542 pub fn forward(
548 &self,
549 input_ids: &Tensor,
550 attention_mask: &Tensor,
551 marker_pos: &Tensor,
552 marker_mask: &Tensor,
553 qtype: &Tensor,
554 ) -> Result<ModelOutput> {
555 let hidden = self.encoder.forward(input_ids, attention_mask)?;
556 let (batch, seq_len, dims) = hidden.dims3()?;
557
558 let type_bias = self.type_emb.forward(qtype)?.reshape((batch, 1, dims))?;
560 let mut xs = hidden.broadcast_add(&type_bias)?;
561
562 let head_mask = {
564 let device = attention_mask.device();
565 let valid = attention_mask
566 .to_dtype(DType::U8)?
567 .reshape((batch, 1, 1, seq_len))?;
568 let zeros = Tensor::zeros((batch, 1, 1, seq_len), DType::F32, device)?;
569 let blocked = Tensor::full(f32::NEG_INFINITY, (batch, 1, 1, seq_len), device)?;
570 valid.where_cond(&zeros, &blocked)?.to_dtype(xs.dtype())?
571 };
572 for layer in &self.head {
573 xs = layer.forward(&xs, &head_mask)?;
574 }
575
576 let slots = marker_pos.dim(1)?;
578 let indices = marker_pos
579 .clamp(0i64, i64::MAX)?
580 .to_dtype(DType::U32)?
581 .reshape((batch, slots, 1))?
582 .broadcast_as((batch, slots, dims))?
583 .contiguous()?;
584 let markers = xs.gather(&indices, 1)?;
585
586 let scored = self.scorer_out.forward(
587 &self
588 .scorer_in
589 .forward(&self.scorer_norm.forward(&markers)?)?
590 .gelu_erf()?,
591 )?;
592 let logits = scored.squeeze(D::Minus1)?.to_dtype(DType::F32)?;
593
594 let masked_fill = Tensor::full(MASKED_LOGIT as f32, (batch, slots), logits.device())?;
596 let logits = marker_mask
597 .to_dtype(DType::U8)?
598 .where_cond(&logits, &masked_fill)?;
599
600 let probabilities = softmax_last_dim(&logits)?;
601 let counts = marker_mask
603 .to_dtype(DType::F32)?
604 .sum(D::Minus1)?
605 .clamp(2f32, f32::MAX)?;
606
607 let clamped = probabilities.clamp(1e-9f32, f32::MAX)?;
608 let entropy = probabilities
609 .mul(&clamped.log()?)?
610 .sum(D::Minus1)?
611 .neg()?
612 .div(&counts.log()?)?;
613
614 let (sorted, _) = probabilities.sort_last_dim(true)?;
615 let top1 = sorted.i((.., slots - 1))?;
616 let top2 = sorted.i((.., slots - 2))?;
617 let features = Tensor::stack(
618 &[top1.clone(), (&top1 - &top2)?, entropy, (counts / 255.0)?],
619 D::Minus1,
620 )?;
621
622 let pooled = Tensor::cat(&[xs.i((.., 0))?.to_dtype(DType::F32)?, features], D::Minus1)?;
623 let action = self
624 .act_out
625 .forward(
626 &self
627 .act_in
628 .forward(&pooled.to_dtype(self.act_dtype)?)?
629 .gelu_erf()?,
630 )?
631 .to_dtype(DType::F32)?;
632
633 Ok(ModelOutput { logits, action })
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 fn sliding(device: &Device, seq_len: usize, window: usize) -> Vec<Vec<f32>> {
643 let attention = Tensor::ones((1, seq_len), DType::U8, device).unwrap();
644 let masks = AttentionMasks::build(&attention, window, DType::F32).unwrap();
645 masks
646 .sliding
647 .reshape((seq_len, seq_len))
648 .unwrap()
649 .to_vec2::<f32>()
650 .unwrap()
651 }
652
653 #[test]
654 fn sliding_mask_allows_exactly_the_local_window() {
655 let (seq_len, window) = (8usize, 6usize);
656 let half = (window / 2) as i64;
657 let rows = sliding(&Device::Cpu, seq_len, window);
658
659 for (i, row) in rows.iter().enumerate() {
660 for (j, value) in row.iter().enumerate() {
661 let inside = (i as i64 - j as i64).abs() <= half;
662 if inside {
663 assert_eq!(*value, 0.0, "({i}, {j}) is inside the window");
664 } else {
665 assert!(value.is_infinite() && *value < 0.0, "({i}, {j}) is outside");
666 }
667 }
668 }
669 }
670
671 #[cfg(feature = "metal")]
674 #[test]
675 fn sliding_mask_builds_on_metal_and_matches_cpu() {
676 let Ok(metal) = Device::new_metal(0) else {
677 return;
678 };
679 let (seq_len, window) = (8usize, 6usize);
680 assert_eq!(
681 sliding(&metal, seq_len, window),
682 sliding(&Device::Cpu, seq_len, window)
683 );
684 }
685}