Skip to main content

laya/
model.rs

1//! ModernBERT encoder and Laya decision heads, in candle.
2//!
3//! Architecture follows upstream Laya and Hugging Face ModernBERT. Four
4//! details are easy to get wrong and are called out where they occur:
5//!
6//! 1. The encoder and scoring head use **exact** (erf) GELU, so `gelu_erf` —
7//!    candle's `gelu` is the tanh approximation.
8//! 2. The decision-head feed-forward uses **ReLU**, because upstream builds it
9//!    from `torch.nn.TransformerEncoderLayer`, whose default activation is ReLU
10//!    even though everything around it is GELU.
11//! 3. Padded *queries* are allowed to attend to valid keys. Without that a
12//!    fully-masked softmax row produces NaN. Padded positions are never used as
13//!    keys or pooled outputs, so valid-token results are unchanged.
14//! 4. RoPE tables are built in f32 and cast afterwards; building them directly
15//!    in f16 loses precision at long positions.
16
17use 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
26/// Logit assigned to marker slots that do not correspond to a real option.
27const MASKED_LOGIT: f64 = -1e4;
28
29// ---------------------------------------------------------------- rotary
30
31/// Precomputed RoPE tables for one theta.
32#[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        // Built in f32 regardless of the model dtype, then cast once.
47        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    /// Apply rotary embeddings to `(batch, heads, seq, head_dim)` tensors.
64    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        // `rope` is the half-split (NeoX) form, matching MLX's
68        // `fast.rope(..., traditional=False)`. `rope_i` would be the
69        // interleaved variant and is *not* what upstream uses.
70        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// ---------------------------------------------------------------- masks
77
78/// Additive attention masks: `0` where attention is allowed, `-inf` elsewhere.
79///
80/// Mirrors upstream's boolean masks, which MLX's SDPA turns into exactly this.
81#[derive(Debug, Clone)]
82pub struct AttentionMasks {
83    /// `(batch, 1, 1, seq)` — key validity.
84    pub full: Tensor,
85    /// `(batch, 1, seq, seq)` — key validity intersected with the local window.
86    pub sliding: Tensor,
87}
88
89impl AttentionMasks {
90    /// `attention_mask` is `(batch, seq)` with 1 for real tokens.
91    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        // Local window: |i - j| <= window / 2, inclusive, as upstream computes it.
103        // Bounded from both sides rather than via abs(): candle's Metal backend
104        // has no uabs kernel for I64, and this mask is on the unconditional path,
105        // so abs() here made every predict fail on Apple GPUs. Staying in I64
106        // also avoids an f32 round trip on long sequences.
107        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        // A padded *query* may see every valid key, so no row is fully masked.
118        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
143/// Scaled dot-product attention with an additive mask.
144fn 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// ---------------------------------------------------------------- encoder
151
152#[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
171/// LayerNorm honouring the checkpoint's `norm_bias` flag.
172fn 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
187/// Linear honouring an optional bias flag.
188fn 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        // Exact GELU on the value half, gated by the second half.
280        self.wo.forward(&(halves[0].gelu_erf()? * &halves[1])?)
281    }
282}
283
284#[derive(Debug)]
285struct EncoderLayer {
286    attention_type: String,
287    /// `None` at layer 0, where upstream uses `nn.Identity`.
288    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        // Only two thetas exist, so the tables are built once and shared.
355        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// ------------------------------------------------------------ decision head
410
411#[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            // These norms carry a bias, matching torch's TransformerEncoderLayer.
464            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        // ReLU, not GELU: torch's TransformerEncoderLayer defaults to ReLU even
476        // though the encoder and scoring head use GELU.
477        let ff = self
478            .linear2
479            .forward(&self.linear1.forward(&normed)?.relu()?)?;
480        &xs + ff
481    }
482}
483
484// ---------------------------------------------------------- decision model
485
486/// The full Laya decision model: encoder, decision head, scorer and action head.
487#[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/// One forward pass: masked scoring logits and action probabilities' logits.
501#[derive(Debug, Clone)]
502pub struct ModelOutput {
503    /// `(batch, slots)` in f32, already masked to `-1e4` on unused slots.
504    pub logits: Tensor,
505    /// `(batch, act_classes)` in f32.
506    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            // Sequential(LayerNorm, Linear, GELU, Linear): index 2 has no parameters.
532            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            // Sequential(Linear, GELU, Linear).
536            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    /// Run a prepared batch.
543    ///
544    /// * `input_ids`, `attention_mask`: `(batch, seq)`
545    /// * `marker_pos`, `marker_mask`: `(batch, slots)`
546    /// * `qtype`: `(batch,)`
547    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        // Add the question-type embedding to every position.
559        let type_bias = self.type_emb.forward(qtype)?.reshape((batch, 1, dims))?;
560        let mut xs = hidden.broadcast_add(&type_bias)?;
561
562        // The head sees key validity only; padded queries may attend freely.
563        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        // Gather the hidden state at each option's marker position.
577        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        // Unused slots are pushed far below any real score.
595        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        // `k` is the real option count, floored at 2 to match the padded layout.
602        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    /// The `(seq, seq)` sliding mask for an all-valid attention mask.
642    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    /// Regression test for the `uabs I64 not implemented` Metal failure: the
672    /// mask must build on the GPU and agree with the CPU bit for bit.
673    #[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}