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}