Skip to main content

candle_mi/diffusion/
mdlm.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! `MDLM` masked-diffusion `DiT` backend.
4//!
5//! A faithful Rust port of `kuleshov-group/mdlm-owt` (Sahoo et al., `NeurIPS`
6//! 2024), validated against the flash-attn-free fp32 reference
7//! `TheQweaker/mdlm-owt-noflash`.  Unlike the causal
8//! `GenericTransformer`, `MDLM` is a
9//! **bidirectional** `DiT`: each block applies `adaLN` modulation (constant
10//! here, since the checkpoint is time-independent), full self-attention with
11//! no causal mask, weight-only `LayerNorm`, and a plain `GELU`-tanh MLP.
12//!
13//! Output logits are raw — the `SUBS` masking (forbidding the `[MASK]` token)
14//! belongs to the diffusion sampler, not the forward pass.
15
16use candle_core::{D, DType, Device, Module, Tensor};
17use candle_nn::{Embedding, LayerNorm, Linear, VarBuilder};
18
19use crate::backend::MIBackend;
20use crate::error::{MIError, Result};
21use crate::hooks::{HookCache, HookPoint, HookSpec};
22
23use super::config::MdlmConfig;
24use super::rope::MdlmRope;
25
26/// Frequency-embedding size of the upstream `TimestepEmbedder` (`sigma_map`).
27const FREQ_EMBED: usize = 256;
28
29// ---------------------------------------------------------------------------
30// Small helpers
31// ---------------------------------------------------------------------------
32
33/// `adaLN` modulation: `x * (1 + scale) + shift`.
34///
35/// `shift` and `scale` are `[1, hidden]` and broadcast over the leading
36/// dimensions of `x` (numpy right-alignment), so this works for both
37/// `[batch, seq, hidden]` block activations and `[batch, hidden]` logit-lens
38/// inputs.
39///
40/// # Shapes
41/// - `x`: `[.., hidden]`
42/// - `shift` / `scale`: `[1, hidden]`
43/// - returns: same shape as `x`
44fn modulate(x: &Tensor, shift: &Tensor, scale: &Tensor) -> Result<Tensor> {
45    let scaled = x.broadcast_mul(&(scale + 1.0)?)?;
46    Ok(scaled.broadcast_add(shift)?)
47}
48
49/// Apply the standard capture-then-intervene hook protocol at `point`.
50///
51/// Mirrors the per-hook-point block used throughout
52/// `GenericTransformer`: the activation is cloned
53/// into the cache when captured, then each registered intervention is applied
54/// in turn (mutating `tensor` in place).
55///
56/// # Errors
57///
58/// Returns [`MIError::Model`] if an intervention's
59/// tensor operation fails.
60// The by-value `HookPoint` lets call sites pass a freshly-built variant without
61// `&`; capturing still needs one clone either way.
62#[allow(clippy::needless_pass_by_value)]
63fn hook_point(
64    tensor: &mut Tensor,
65    point: HookPoint,
66    hooks: &HookSpec,
67    cache: &mut HookCache,
68) -> Result<()> {
69    if hooks.is_captured(&point) {
70        cache.store(point.clone(), tensor.clone());
71    }
72    for intervention in hooks.interventions_at(&point) {
73        *tensor = crate::hooks::apply_intervention(tensor, intervention)?;
74    }
75    Ok(())
76}
77
78/// Load a weight-only `LayerNorm` (no bias, mean-subtracting, `eps`).
79///
80/// `MDLM` stores only the scale (`norm.weight`) and applies
81/// `F.layer_norm(x) * weight` — exactly `candle_nn::LayerNorm::new_no_bias`.
82///
83/// # Errors
84///
85/// Returns [`MIError::Model`] if the weight tensor
86/// cannot be loaded.
87#[allow(clippy::needless_pass_by_value)] // VarBuilder is candle's pass-by-value convention
88fn load_layer_norm(vb: VarBuilder<'_>, hidden: usize, eps: f64) -> Result<LayerNorm> {
89    let weight = vb.get(hidden, "weight")?;
90    Ok(LayerNorm::new_no_bias(weight, eps))
91}
92
93/// Compute the constant `adaLN` conditioning vector `c = silu(sigma_map(0))`.
94///
95/// The released checkpoint is time-independent (`time_conditioning = false`),
96/// so the network zeroes the diffusion time internally and the conditioning
97/// is a single fixed vector.  Evaluating the `TimestepEmbedder` at `t = 0`
98/// (`timestep_embedding(0) = [cos(0)…, sin(0)…] = [1…, 0…]`) and applying
99/// `silu` reproduces it once, at load time.
100///
101/// # Shapes
102/// - returns: `[1, cond_dim]`
103///
104/// # Errors
105///
106/// Returns [`MIError::Model`] on tensor failures.
107#[allow(clippy::needless_pass_by_value)] // VarBuilder is candle's pass-by-value convention
108fn compute_conditioning(
109    vb_sigma: VarBuilder<'_>,
110    cond_dim: usize,
111    device: &Device,
112    dtype: DType,
113) -> Result<Tensor> {
114    let fc1 = candle_nn::linear(FREQ_EMBED, cond_dim, vb_sigma.pp("mlp").pp("0"))?;
115    let fc2 = candle_nn::linear(cond_dim, cond_dim, vb_sigma.pp("mlp").pp("2"))?;
116
117    // timestep_embedding(0, 256) = cat([cos(0)×128, sin(0)×128]) = [1×128, 0×128].
118    let emb: Vec<f32> = (0..FREQ_EMBED)
119        .map(|i| if i < FREQ_EMBED / 2 { 1.0 } else { 0.0 })
120        .collect();
121    let emb = Tensor::from_vec(emb, (1, FREQ_EMBED), device)?.to_dtype(dtype)?;
122
123    let h1 = candle_nn::ops::silu(&fc1.forward(&emb)?)?;
124    let sig = fc2.forward(&h1)?;
125    Ok(candle_nn::ops::silu(&sig)?)
126}
127
128// ---------------------------------------------------------------------------
129// DiTBlock
130// ---------------------------------------------------------------------------
131
132/// A single `MDLM` `DiT` block: `adaLN`-modulated bidirectional attention and
133/// `GELU`-tanh MLP, each gated by a constant conditioning vector.
134struct DiTBlock {
135    /// Pre-attention weight-only `LayerNorm`.
136    norm1: LayerNorm,
137    /// Pre-MLP weight-only `LayerNorm`.
138    norm2: LayerNorm,
139    /// `adaLN` modulation projection: `cond_dim → 6 * hidden` (with bias).
140    adaln: Linear,
141    /// Fused QKV projection: `hidden → 3 * hidden` (no bias).
142    attn_qkv: Linear,
143    /// Attention output projection: `hidden → hidden` (no bias).
144    attn_out: Linear,
145    /// MLP up-projection: `hidden → mlp_ratio * hidden` (with bias).
146    mlp_fc: Linear,
147    /// MLP down-projection: `mlp_ratio * hidden → hidden` (with bias).
148    mlp_proj: Linear,
149    /// Number of attention heads.
150    n_heads: usize,
151    /// Per-head dimension.
152    head_dim: usize,
153    /// Hidden dimension (`n_heads * head_dim`).
154    hidden_dim: usize,
155    /// Attention scale `1 / sqrt(head_dim)`.
156    scale: f64,
157}
158
159impl DiTBlock {
160    /// Load a `DiT` block from the `backbone.blocks.{i}` namespace.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`MIError::Model`] if any weight fails to load.
165    #[allow(clippy::needless_pass_by_value)] // VarBuilder is candle's pass-by-value convention
166    fn load(config: &MdlmConfig, vb: VarBuilder<'_>) -> Result<Self> {
167        let h = config.hidden_dim;
168        let inter = config.mlp_ratio * h;
169        // CAST: usize → f64, head_dim fits in f64 mantissa
170        #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
171        let scale = 1.0 / (config.head_dim as f64).sqrt();
172
173        Ok(Self {
174            norm1: load_layer_norm(vb.pp("norm1"), h, config.norm_eps)?,
175            norm2: load_layer_norm(vb.pp("norm2"), h, config.norm_eps)?,
176            adaln: candle_nn::linear(config.cond_dim, 6 * h, vb.pp("adaLN_modulation"))?,
177            attn_qkv: candle_nn::linear_no_bias(h, 3 * h, vb.pp("attn_qkv"))?,
178            attn_out: candle_nn::linear_no_bias(h, h, vb.pp("attn_out"))?,
179            mlp_fc: candle_nn::linear(h, inter, vb.pp("mlp").pp("0"))?,
180            mlp_proj: candle_nn::linear(inter, h, vb.pp("mlp").pp("2"))?,
181            n_heads: config.n_heads,
182            head_dim: config.head_dim,
183            hidden_dim: h,
184            scale,
185        })
186    }
187
188    /// Bidirectional self-attention sublayer (pre-gate).
189    ///
190    /// Captures `AttnQ`/`AttnK`/`AttnV` (post-reshape, pre-`RoPE`),
191    /// `AttnScores` (post-scale), and `AttnPattern` (post-softmax).  No causal
192    /// mask is applied — attention is full bidirectional.
193    ///
194    /// # Shapes
195    /// - `x`: `[batch, seq, hidden]`
196    /// - returns: `[batch, seq, hidden]`
197    ///
198    /// # Errors
199    ///
200    /// Returns [`MIError::Model`] on tensor failures.
201    fn attention(
202        &self,
203        xs: &Tensor,
204        rope: &MdlmRope,
205        layer_idx: usize,
206        hooks: &HookSpec,
207        cache: &mut HookCache,
208    ) -> Result<Tensor> {
209        let (batch, seq_len, _) = xs.dims3()?;
210        let hidden = self.hidden_dim;
211
212        let qkv = self.attn_qkv.forward(xs)?;
213        let q = qkv.narrow(D::Minus1, 0, hidden)?;
214        let k = qkv.narrow(D::Minus1, hidden, hidden)?;
215        let v = qkv.narrow(D::Minus1, 2 * hidden, hidden)?;
216
217        // [batch, seq, n_heads, head_dim] → [batch, n_heads, seq, head_dim]
218        let mut q = q
219            .reshape((batch, seq_len, self.n_heads, self.head_dim))?
220            .transpose(1, 2)?;
221        let mut k = k
222            .reshape((batch, seq_len, self.n_heads, self.head_dim))?
223            .transpose(1, 2)?;
224        let mut v = v
225            .reshape((batch, seq_len, self.n_heads, self.head_dim))?
226            .transpose(1, 2)?;
227
228        hook_point(&mut q, HookPoint::AttnQ(layer_idx), hooks, cache)?;
229        hook_point(&mut k, HookPoint::AttnK(layer_idx), hooks, cache)?;
230        hook_point(&mut v, HookPoint::AttnV(layer_idx), hooks, cache)?;
231
232        let q = rope.apply(&q)?;
233        let k = rope.apply(&k)?;
234
235        // Attention scores (bidirectional — no causal mask).
236        // CONTIGUOUS: transpose produces non-unit strides; matmul requires contiguous layout
237        let k_t = k.contiguous()?.transpose(2, 3)?;
238        let q = q.contiguous()?;
239        let mut scores = (q.matmul(&k_t)? * self.scale)?;
240        hook_point(&mut scores, HookPoint::AttnScores(layer_idx), hooks, cache)?;
241
242        // Softmax in F32 (no-op promote on the F32 default path; defensive for
243        // lower-precision loads).
244        let original_dtype = scores.dtype();
245        // PROMOTE: softmax over a lower-precision dtype can produce NaN; compute in F32
246        let scores_f32 = if original_dtype == DType::F32 {
247            scores
248        } else {
249            scores.to_dtype(DType::F32)?
250        };
251        let mut pattern = candle_nn::ops::softmax_last_dim(&scores_f32)?;
252        if original_dtype != DType::F32 {
253            pattern = pattern.to_dtype(original_dtype)?;
254        }
255        hook_point(
256            &mut pattern,
257            HookPoint::AttnPattern(layer_idx),
258            hooks,
259            cache,
260        )?;
261
262        // CONTIGUOUS: ensure contiguous layout for the pattern·value matmul
263        let v = v.contiguous()?;
264        let attn = pattern.matmul(&v)?;
265        let attn = attn
266            .transpose(1, 2)?
267            .contiguous()?
268            .reshape((batch, seq_len, hidden))?;
269
270        Ok(self.attn_out.forward(&attn)?)
271    }
272
273    /// Plain `GELU`-tanh MLP: `proj(gelu(fc(x)))`.
274    ///
275    /// # Shapes
276    /// - `x`: `[batch, seq, hidden]`
277    /// - returns: `[batch, seq, hidden]`
278    ///
279    /// # Errors
280    ///
281    /// Returns [`MIError::Model`] on tensor failures.
282    fn mlp(&self, x: &Tensor) -> Result<Tensor> {
283        let up = self.mlp_fc.forward(x)?;
284        let act = up.gelu()?; // tanh approximation, matching nn.GELU(approximate='tanh')
285        Ok(self.mlp_proj.forward(&act)?)
286    }
287
288    /// Run the full block forward with hook support.
289    ///
290    /// Hook semantics: `AttnOut` / `MlpOut` capture the **gated** residual
291    /// contributions (`gate_msa * attn`, `gate_mlp * mlp`), matching the
292    /// `TransformerLens` "added to the residual stream" convention.
293    ///
294    /// # Shapes
295    /// - `hidden_in`: `[batch, seq, hidden]`
296    /// - `cond`: `[1, cond_dim]`
297    /// - returns: `[batch, seq, hidden]`
298    ///
299    /// # Errors
300    ///
301    /// Returns [`MIError::Model`] on tensor failures.
302    fn forward(
303        &self,
304        hidden_in: &Tensor,
305        cond: &Tensor,
306        rope: &MdlmRope,
307        layer_idx: usize,
308        hooks: &HookSpec,
309        cache: &mut HookCache,
310    ) -> Result<Tensor> {
311        let mut hidden = hidden_in.clone();
312        hook_point(&mut hidden, HookPoint::ResidPre(layer_idx), hooks, cache)?;
313        let residual = hidden.clone();
314
315        // adaLN modulation parameters from the constant conditioning vector:
316        // shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp.
317        let mods = self.adaln.forward(cond)?;
318        let hidden_dim = self.hidden_dim;
319        let shift_msa = mods.narrow(D::Minus1, 0, hidden_dim)?;
320        let scale_msa = mods.narrow(D::Minus1, hidden_dim, hidden_dim)?;
321        let gate_msa = mods.narrow(D::Minus1, 2 * hidden_dim, hidden_dim)?;
322        let shift_mlp = mods.narrow(D::Minus1, 3 * hidden_dim, hidden_dim)?;
323        let scale_mlp = mods.narrow(D::Minus1, 4 * hidden_dim, hidden_dim)?;
324        let gate_mlp = mods.narrow(D::Minus1, 5 * hidden_dim, hidden_dim)?;
325
326        // --- attention sublayer ---
327        let normed1 = modulate(&self.norm1.forward(&residual)?, &shift_msa, &scale_msa)?;
328        let attn = self.attention(&normed1, rope, layer_idx, hooks, cache)?;
329        let mut attn_contrib = attn.broadcast_mul(&gate_msa)?;
330        hook_point(
331            &mut attn_contrib,
332            HookPoint::AttnOut(layer_idx),
333            hooks,
334            cache,
335        )?;
336        hidden = (residual + attn_contrib)?;
337        hook_point(&mut hidden, HookPoint::ResidMid(layer_idx), hooks, cache)?;
338
339        // --- MLP sublayer ---
340        let residual2 = hidden.clone();
341        let mut normed2 = modulate(&self.norm2.forward(&hidden)?, &shift_mlp, &scale_mlp)?;
342        hook_point(&mut normed2, HookPoint::MlpPre(layer_idx), hooks, cache)?;
343        let mut mlp_out = self.mlp(&normed2)?;
344        hook_point(&mut mlp_out, HookPoint::MlpPost(layer_idx), hooks, cache)?;
345        let mut mlp_contrib = mlp_out.broadcast_mul(&gate_mlp)?;
346        hook_point(&mut mlp_contrib, HookPoint::MlpOut(layer_idx), hooks, cache)?;
347        hidden = (residual2 + mlp_contrib)?;
348        hook_point(&mut hidden, HookPoint::ResidPost(layer_idx), hooks, cache)?;
349
350        Ok(hidden)
351    }
352}
353
354// ---------------------------------------------------------------------------
355// DDitFinalLayer
356// ---------------------------------------------------------------------------
357
358/// `MDLM` output head: weight-only `LayerNorm`, `adaLN` shift/scale, then the
359/// untied vocabulary projection.
360struct DDitFinalLayer {
361    /// Final weight-only `LayerNorm`.
362    norm_final: LayerNorm,
363    /// `adaLN` modulation projection: `cond_dim → 2 * hidden` (shift + scale).
364    adaln: Linear,
365    /// Untied vocabulary projection: `hidden → vocab_size` (with bias).
366    linear: Linear,
367    /// Hidden dimension (for slicing the modulation parameters).
368    hidden_dim: usize,
369}
370
371impl DDitFinalLayer {
372    /// Load the output head from the `backbone.output_layer` namespace.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`MIError::Model`] if any weight fails to load.
377    #[allow(clippy::needless_pass_by_value)] // VarBuilder is candle's pass-by-value convention
378    fn load(config: &MdlmConfig, vb: VarBuilder<'_>) -> Result<Self> {
379        let h = config.hidden_dim;
380        Ok(Self {
381            norm_final: load_layer_norm(vb.pp("norm_final"), h, config.norm_eps)?,
382            adaln: candle_nn::linear(config.cond_dim, 2 * h, vb.pp("adaLN_modulation"))?,
383            linear: candle_nn::linear(h, config.vocab_size, vb.pp("linear"))?,
384            hidden_dim: h,
385        })
386    }
387
388    /// Modulated, normalized hidden state (pre-projection).
389    ///
390    /// # Shapes
391    /// - `hidden`: `[.., hidden]`
392    /// - `cond`: `[1, cond_dim]`
393    /// - returns: `[.., hidden]`
394    fn modulated(&self, hidden: &Tensor, cond: &Tensor) -> Result<Tensor> {
395        let mods = self.adaln.forward(cond)?;
396        let hidden_dim = self.hidden_dim;
397        let shift = mods.narrow(D::Minus1, 0, hidden_dim)?;
398        let scale = mods.narrow(D::Minus1, hidden_dim, hidden_dim)?;
399        modulate(&self.norm_final.forward(hidden)?, &shift, &scale)
400    }
401
402    /// Full output head with the `FinalNorm` hook (post-modulation, pre-projection).
403    ///
404    /// # Shapes
405    /// - `hidden`: `[batch, seq, hidden]`
406    /// - returns: `[batch, seq, vocab_size]`
407    ///
408    /// # Errors
409    ///
410    /// Returns [`MIError::Model`] on tensor failures.
411    fn forward(
412        &self,
413        hidden: &Tensor,
414        cond: &Tensor,
415        hooks: &HookSpec,
416        cache: &mut HookCache,
417    ) -> Result<Tensor> {
418        let mut xs = self.modulated(hidden, cond)?;
419        hook_point(&mut xs, HookPoint::FinalNorm, hooks, cache)?;
420        Ok(self.linear.forward(&xs)?)
421    }
422
423    /// Logit projection without hooks (for [`MIBackend::project_to_vocab`]).
424    ///
425    /// # Shapes
426    /// - `hidden`: `[batch, hidden]`
427    /// - returns: `[batch, vocab_size]`
428    ///
429    /// # Errors
430    ///
431    /// Returns [`MIError::Model`] on tensor failures.
432    fn project(&self, hidden: &Tensor, cond: &Tensor) -> Result<Tensor> {
433        let xs = self.modulated(hidden, cond)?;
434        Ok(self.linear.forward(&xs)?)
435    }
436}
437
438// ---------------------------------------------------------------------------
439// GenericMdlm
440// ---------------------------------------------------------------------------
441
442/// `MDLM` masked-diffusion backend.
443///
444/// Loads `kuleshov-group/mdlm-owt`-format weights (prefix `backbone.`) and runs
445/// the bidirectional `DiT` forward pass with full hook support, exactly
446/// matching the fp32 reference.
447pub struct GenericMdlm {
448    /// Token embedding (raw `backbone.vocab_embed.embedding` parameter).
449    vocab_embed: Embedding,
450    /// `DiT` blocks.
451    blocks: Vec<DiTBlock>,
452    /// Output head (`backbone.output_layer`).
453    output_layer: DDitFinalLayer,
454    /// Rotary position embedding cache.
455    rope: MdlmRope,
456    /// Constant `adaLN` conditioning vector `c`: `[1, cond_dim]`.
457    cond: Tensor,
458    /// Model configuration.
459    config: MdlmConfig,
460}
461
462impl GenericMdlm {
463    /// Load an `MDLM` model from a [`VarBuilder`].
464    ///
465    /// The caller constructs the `VarBuilder` (buffered or mmap) and provides
466    /// the parsed [`MdlmConfig`].  The constant conditioning vector and the
467    /// `RoPE` cache are pre-computed here.
468    ///
469    /// # Shapes
470    /// - returns: a model whose [`forward`](MIBackend::forward) maps
471    ///   `[batch, seq]` token ids to `[batch, seq, vocab_size]` logits.
472    ///
473    /// # Memory
474    /// Loads the single `mdlm-owt` `safetensors` (~648 MB) through the caller's
475    /// `VarBuilder`: near-zero extra copy with the `mmap` feature, or ~648 MB
476    /// CPU when buffered.  The conditioning vector and `RoPE` cache are negligible.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`MIError::Config`] if the checkpoint sets
481    /// `time_conditioning = true` (unsupported), or
482    /// [`MIError::Model`] if weight loading fails or a
483    /// dimension is inconsistent with the checkpoint.
484    #[allow(clippy::needless_pass_by_value)] // VarBuilder is candle's pass-by-value convention
485    pub fn load(
486        config: MdlmConfig,
487        device: &Device,
488        dtype: DType,
489        vb: VarBuilder<'_>,
490    ) -> Result<Self> {
491        // The conditioning vector is precomputed once at `t = 0`; that is only
492        // correct for a time-independent checkpoint (`time_conditioning = false`,
493        // as in the released `mdlm-owt`).  Reject the time-conditioned case rather
494        // than silently producing wrong logits.
495        if config.time_conditioning {
496            return Err(MIError::Config(
497                "MDLM time_conditioning=true is unsupported (the constant-conditioning \
498                 fast path assumes a time-independent checkpoint)"
499                    .into(),
500            ));
501        }
502
503        let vb_b = vb.pp("backbone");
504
505        let vocab_embed = Embedding::new(
506            vb_b.pp("vocab_embed")
507                .get((config.vocab_size, config.hidden_dim), "embedding")?,
508            config.hidden_dim,
509        );
510
511        let cond = compute_conditioning(vb_b.pp("sigma_map"), config.cond_dim, device, dtype)?;
512
513        let mut blocks = Vec::with_capacity(config.n_blocks);
514        for i in 0..config.n_blocks {
515            blocks.push(DiTBlock::load(&config, vb_b.pp(format!("blocks.{i}")))?);
516        }
517
518        let output_layer = DDitFinalLayer::load(&config, vb_b.pp("output_layer"))?;
519
520        let rope = MdlmRope::new(
521            config.head_dim,
522            config.model_length,
523            config.rope_theta,
524            device,
525            dtype,
526        )?;
527
528        Ok(Self {
529            vocab_embed,
530            blocks,
531            output_layer,
532            rope,
533            cond,
534            config,
535        })
536    }
537
538    /// Access the model configuration.
539    #[must_use]
540    pub const fn config(&self) -> &MdlmConfig {
541        &self.config
542    }
543}
544
545impl MIBackend for GenericMdlm {
546    fn num_layers(&self) -> usize {
547        self.config.n_blocks
548    }
549
550    fn hidden_size(&self) -> usize {
551        self.config.hidden_dim
552    }
553
554    fn vocab_size(&self) -> usize {
555        self.config.vocab_size
556    }
557
558    fn num_heads(&self) -> usize {
559        self.config.n_heads
560    }
561
562    fn forward(&self, input_ids: &Tensor, hooks: &HookSpec) -> Result<HookCache> {
563        let device = input_ids.device();
564        let mut hidden = self.vocab_embed.forward(input_ids)?;
565        let mut cache = HookCache::new(Tensor::zeros(1, DType::F32, device)?);
566
567        hook_point(&mut hidden, HookPoint::Embed, hooks, &mut cache)?;
568
569        for (layer_idx, block) in self.blocks.iter().enumerate() {
570            hidden = block.forward(
571                &hidden, &self.cond, &self.rope, layer_idx, hooks, &mut cache,
572            )?;
573        }
574
575        let logits = self
576            .output_layer
577            .forward(&hidden, &self.cond, hooks, &mut cache)?;
578        cache.set_output(logits);
579        Ok(cache)
580    }
581
582    fn project_to_vocab(&self, hidden: &Tensor) -> Result<Tensor> {
583        self.output_layer.project(hidden, &self.cond)
584    }
585
586    fn embedding_vector(&self, token_id: u32) -> Result<Tensor> {
587        let device = self.vocab_embed.embeddings().device();
588        let ids = Tensor::new(&[token_id], device)?;
589        let emb = self.vocab_embed.forward(&ids)?; // [1, hidden]
590        Ok(emb.squeeze(0)?) // [hidden]
591    }
592}