Skip to main content

frink_models/decoder/
ffn_block.rs

1//! The FFN half of one decoder layer, written once per SHAPE: one body
2//! for a row, one for a batch of rows.
3//!
4//! `forward_token`'s CPU arm, its Metal-attention arm and
5//! `forward_token_paged` each spelled out the same six lines -- norm,
6//! gpt-oss or generic FFN, `post_ffn_norm`, residual add -- and the
7//! per-layer seam (`crate::layer_shapes`) needed a seventh fact in all
8//! three: an FFN-free layer (`deci.cpp:147-149`) runs none of it. Three
9//! copies of a branch is how features go missing from one path; this is
10//! the one body, and the callers pass the one thing that differs.
11//!
12//! The batched half had THREE copies too -- the Metal-prefill arm and
13//! the host arm of `forward_hidden_batch_inner`, and
14//! `forward_multi_seq_kv_on_worker` -- and the router-operand seam
15//! (`crate::router_input`) needed an eighth fact in all of them: WHICH
16//! tensor the router reads. Read side by side, the first two differed
17//! by the gpt-oss branch and the FFN-free check (both unreachable on
18//! the Metal arm, both present on the host arm) and the third by
19//! having none of the batched fast paths. [`Decoder::ffn_block_batch`]
20//! is the one body; the multi-sequence caller passes
21//! `BatchedFfnKernels::PerRow` to keep exactly the behaviour it had.
22//!
23//! # What is captured before attention
24//!
25//! Two things the FFN body needs are facts about the hidden state AS
26//! IT ENTERS THE LAYER, which attention has mutated in place by the
27//! time the body runs: what the router reads ([`RouterOperand`];
28//! `smallthinker.cpp:111` reads `inpL`, `arctic.cpp:136` norms `inpSA`)
29//! and, on a PARALLEL-residual layer, what the FFN itself reads
30//! ([`FfnInput`]; `gptneox.cpp:149` norms `inpL` with `ffn_norm`,
31//! `plamo.cpp:97` and `stablelm.cpp:137` hand the FFN the vector
32//! attention read, `crate::parallel_residual`). They travel together
33//! as [`BranchInputs`], and [`Decoder::branch_inputs`] is the ONE
34//! constructor, called where `attn_norm` is applied, before attention.
35//! For every sequential architecture with a router on the FFN input it
36//! answers the two defaults without reading its argument, and the body
37//! computes `ffn_norm(h)` and `router · normed2` as it always did.
38//! There is no `Default` and no second constructor, so a body cannot
39//! be reached with either fact unstated, and a body that took one and
40//! forgot the other cannot be written.
41//!
42//! # The parallel dense FFN
43//!
44//! A layer whose dense FFN is summed with its experts
45//! (`crate::parallel_dense_ffn`: Grok-2, Arctic) has that FFN in the
46//! shared-expert slot and, when the row scales the sum, a
47//! `parallel_sum_scale` on the layer; [`Decoder::apply_parallel_sum_scale`]
48//! multiplies the WHOLE branch output by it right after the combine and
49//! before `down_scale` and the post-FFN norm, where `grok.cpp:180-186`
50//! put it. Beside `apply_down_scale` at every site, so the two cannot
51//! drift.
52
53use frink_core::matmul::rms_norm;
54
55use super::{Decoder, GptOssLayer, LayerWeights};
56use crate::norm::NormOp;
57use crate::router_input::RouterInput;
58use crate::scalar_multipliers::residual_add;
59use crate::skip_stream::SkipStream;
60
61/// What the MoE router multiplies, for one layer of one forward pass.
62///
63/// See the module doc. `Precomputed` holds LOGITS, not the operand:
64/// `smallthinker.cpp:111` computes them before attention, and
65/// computing them at the same point keeps the arithmetic order
66/// llama.cpp's and makes the operand impossible to confuse with the
67/// post-attention residual, which is the same `Vec` mutated in place.
68#[derive(Debug)]
69pub(crate) enum RouterOperand {
70    /// `router · ffn_norm(ffn_inp)`, computed inside the FFN body --
71    /// llama.cpp's `build_moe_ffn` default and every generic-path graph
72    /// but one.
73    FfnInput,
74    /// `router · inpL`, already computed: `[batch, n_experts]`.
75    Precomputed(Vec<f32>),
76    /// Arctic: the routed branch's INPUT, `ffn_norm_exps(inpSA)`,
77    /// `[batch, hidden_dim]`. The body computes `router · x` from it
78    /// and runs the routed experts on it; the dense half still reads
79    /// `ffn_norm(ffn_inp)` (`crate::router_input::RouterInput::
80    /// NormedLayerInput`).
81    BranchInput(Vec<f32>),
82}
83
84/// What the FFN reads: the ordinary pre-FFN norm of the post-attention
85/// residual, computed inside the body, or -- on a parallel-residual
86/// layer -- a norm of the LAYER INPUT, captured before attention
87/// (`crate::parallel_residual`). `[batch, hidden_dim]`, already normed.
88#[derive(Debug)]
89pub(crate) enum FfnInput {
90    PostAttnResidual,
91    LayerInput(Vec<f32>),
92}
93
94/// The two pre-attention facts a layer's FFN body needs, built by
95/// [`Decoder::branch_inputs`] and nothing else.
96#[derive(Debug)]
97pub(crate) struct BranchInputs {
98    pub(crate) router: RouterOperand,
99    pub(crate) ffn: FfnInput,
100}
101
102/// Whether the batched FFN body may take its batched kernels
103/// (`dense_ffn_batch`, `moe_ffn_batch`, the Metal MoE prefill) or must
104/// run every row through the per-position bodies.
105///
106/// `PerRow` is what the multi-sequence body has always done: its rows
107/// are one token from each of N sequences, the batched kernels'
108/// thresholds (4 and 32 rows) were tuned for prefill, and switching
109/// continuous batching onto them is a measurable change with its own
110/// A/B, not something to smuggle in under a seam. A parameter rather
111/// than a second body, so the two cannot drift about anything else.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub(crate) enum BatchedFfnKernels {
114    Prefill,
115    PerRow,
116}
117
118impl Decoder {
119    /// The weights logical layer `l` runs: `layers[l]` for every model
120    /// but a looped one, where it is `layers[l % n_phys]`
121    /// (`crate::layer_loops`). THE mapping; the three host bodies
122    /// iterate `0..config.n_layers` and ask it, so a body cannot index
123    /// `layers` with a logical index by mistake.
124    pub fn layer_for(&self, l: usize) -> &LayerWeights {
125        &self.layers[self.physical_index(l)]
126    }
127
128    /// The physical index behind logical layer `l`: the index into
129    /// `layers`, the gpt-oss side table and the residency plan, all of
130    /// which are sized per PHYSICAL layer.
131    pub(crate) fn physical_index(&self, l: usize) -> usize {
132        match self.config.layer_loops {
133            Some(loops) => loops.physical(l),
134            None => l,
135        }
136    }
137
138    /// A dense layer's single expert on ONE row: `run_expert`, or its
139    /// sub-normed twin when the layer carries BitNet's `ffn_sub_norm`
140    /// (`bitnet.cpp:135-140`, `crate::sub_norms`).
141    ///
142    /// The one place the dense row body decides between the two, so
143    /// that a fused kernel reachable from `run_expert` (the on-device
144    /// SwiGLU, which has no norm between the activation and `down`)
145    /// cannot be reached for a layer that needs the norm.
146    pub(crate) fn run_dense_expert(
147        layer: &LayerWeights,
148        normed2: &[f32],
149        act: frink_moe::GluAct,
150        eps: f32,
151    ) -> Vec<f32> {
152        layer.moe.with_expert(0, |ex| {
153            match (&layer.moe.ffn_sub_norm, &layer.moe.dense_bias) {
154                (None, None) => frink_moe::run_expert(normed2, ex, act),
155                (Some(w), None) => frink_moe::run_expert_sub_normed(normed2, ex, act, w, eps),
156                // `crate::proj_bias`: the biases before the activation
157                // and after `down`. No graph has both a bias and an
158                // inner norm (`bitnet` has neither bias), so the pair
159                // is a loader-refused shape rather than a fourth body.
160                (None, Some(bias)) => frink_moe::run_expert_biased(normed2, ex, act, bias),
161                (Some(_), Some(_)) => {
162                    unreachable!(
163                        "a dense layer with both an inner norm and biases is refused at load"
164                    )
165                }
166            }
167        })
168    }
169
170    /// THE constructor for [`BranchInputs`]. `hidden_before_attn` is
171    /// `[batch_size, hidden_dim]`, the residual stream as it enters the
172    /// layer.
173    pub(crate) fn branch_inputs(
174        &self,
175        layer: &LayerWeights,
176        hidden_before_attn: &[f32],
177        batch_size: usize,
178    ) -> BranchInputs {
179        BranchInputs {
180            router: self.router_operand(layer, hidden_before_attn, batch_size),
181            ffn: self.ffn_input(layer, hidden_before_attn, batch_size),
182        }
183    }
184
185    /// What the FFN reads on a parallel layer: `attn_norm(x)` -- the
186    /// same function on the same vector attention took, so the two
187    /// cannot disagree -- or `ffn_norm(x)`; and the body's own
188    /// `ffn_norm(h)` on a sequential one.
189    fn ffn_input(
190        &self,
191        layer: &LayerWeights,
192        hidden_before_attn: &[f32],
193        batch_size: usize,
194    ) -> FfnInput {
195        use crate::parallel_residual::ParallelNorm;
196        let norm = match layer.moe.parallel {
197            None => return FfnInput::PostAttnResidual,
198            Some(ParallelNorm::SharedNorm) => {
199                debug_assert!(
200                    matches!(layer.moe.norm_weight, NormOp::None),
201                    "a shared-norm parallel layer has no pre-FFN tensor"
202                );
203                &layer.attn.norm_weight
204            }
205            Some(ParallelNorm::TwoNorms) => &layer.moe.norm_weight,
206        };
207        debug_assert_eq!(
208            hidden_before_attn.len(),
209            batch_size * self.config.hidden_dim
210        );
211        let eps = self.config.rms_norm_eps;
212        FfnInput::LayerInput(
213            hidden_before_attn
214                .chunks(self.config.hidden_dim)
215                .flat_map(|row| norm.apply(row, eps))
216                .collect(),
217        )
218    }
219
220    /// The router half of [`Self::branch_inputs`].
221    ///
222    /// Answers `FfnInput` for a dense layer (nothing to route) and for
223    /// gpt-oss (`gpt_oss_ffn` computes its own biased logits from the
224    /// normed input and is the only reader of `router_bias`; no
225    /// gpt-oss graph routes on the layer input, and the debug assert
226    /// pins that the table agrees).
227    fn router_operand(
228        &self,
229        layer: &LayerWeights,
230        hidden_before_attn: &[f32],
231        batch_size: usize,
232    ) -> RouterOperand {
233        match self.config.router_input {
234            RouterInput::NormedFfnInput => RouterOperand::FfnInput,
235            RouterInput::NormedLayerInput => {
236                if Self::is_dense_layer(layer) {
237                    return RouterOperand::FfnInput;
238                }
239                debug_assert_eq!(
240                    hidden_before_attn.len(),
241                    batch_size * self.config.hidden_dim
242                );
243                // REQUIRED at load for this operand (`arctic.cpp:45`);
244                // a layer without it is a loader defect, not a file's.
245                let w = layer
246                    .moe
247                    .exps_norm
248                    .as_deref()
249                    .expect("NormedLayerInput layer loaded without ffn_norm_exps");
250                let eps = self.config.rms_norm_eps;
251                RouterOperand::BranchInput(
252                    hidden_before_attn
253                        .chunks(self.config.hidden_dim)
254                        .flat_map(|row| rms_norm(row, w, eps))
255                        .collect(),
256                )
257            }
258            RouterInput::RawLayerInput => {
259                debug_assert!(
260                    self.gpt_oss.is_none(),
261                    "gpt-oss routes on the normed input; a RawLayerInput gpt-oss model is not a \
262                     shape llama.cpp has"
263                );
264                if Self::is_dense_layer(layer) {
265                    return RouterOperand::FfnInput;
266                }
267                debug_assert_eq!(
268                    hidden_before_attn.len(),
269                    batch_size * self.config.hidden_dim
270                );
271                RouterOperand::Precomputed(if batch_size == 1 {
272                    layer.moe.router.apply(hidden_before_attn)
273                } else {
274                    layer.moe.router.apply_batch(hidden_before_attn, batch_size)
275                })
276            }
277        }
278    }
279
280    /// Runs layer `layer_idx`'s FFN on `hidden` and adds it back, or
281    /// does nothing for a layer whose shape has no FFN.
282    ///
283    /// `hidden` is the post-attention residual; on return it is the
284    /// layer's output.
285    #[allow(clippy::too_many_arguments)]
286    pub(crate) fn ffn_block_row(
287        &self,
288        layer_idx: usize,
289        layer: &LayerWeights,
290        hidden: &mut [f32],
291        oai: Option<&GptOssLayer>,
292        plan: Option<&frink_moe::PlacementPlan>,
293        inputs: BranchInputs,
294        skip: Option<SkipStream<'_>>,
295    ) {
296        if self.config.layer_shape(layer_idx).ffn_dim == 0 {
297            return;
298        }
299        let hidden_dim = self.config.hidden_dim;
300        let BranchInputs {
301            router: operand,
302            ffn,
303        } = inputs;
304        let normed2 = match ffn {
305            FfnInput::PostAttnResidual => self.pre_norm_residual(&layer.moe.norm_weight, hidden, 1),
306            // A parallel layer's FFN reads a norm of the LAYER input,
307            // which `Decoder::branch_inputs` took before attention ran;
308            // nothing there is the residual stream, so there is nothing
309            // for `crate::normed_residual` to adopt. No architecture
310            // has both (`normed_residual::NORMED_RESIDUAL_ARCHITECTURES`
311            // is one row and `minimax-01.cpp:434-440` is sequential).
312            FfnInput::LayerInput(x) => x,
313        };
314        let mut ffn_out = match oai {
315            Some(oai) => Self::gpt_oss_ffn(layer, oai, &normed2, &self.config, hidden_dim),
316            None => Self::run_ffn_block(
317                layer_idx,
318                layer,
319                &normed2,
320                &self.config,
321                hidden_dim,
322                plan,
323                operand,
324            ),
325        };
326        Self::apply_parallel_sum_scale(layer, &mut ffn_out);
327        Self::apply_down_scale(layer, &mut ffn_out);
328        if let Some(post) = &layer.attn.post_ffn_norm {
329            ffn_out = rms_norm(&ffn_out, post, self.config.post_norm_eps());
330        }
331        residual_add(hidden, &ffn_out, self.config.residual_scale);
332        Self::apply_skip_stream(layer, hidden, skip, 1, hidden_dim);
333        self.apply_loop_norm(layer_idx, hidden, 1);
334    }
335
336    /// `ggml_scale(ffn_out + moe_out, s)` (`grok.cpp:180`): the factor
337    /// on the whole branch of a layer whose dense FFN is summed with
338    /// its experts (`crate::parallel_dense_ffn`). Elementwise, so one
339    /// row and a batch of rows are the same call; a no-op for every
340    /// layer without the row's scale.
341    fn apply_parallel_sum_scale(layer: &LayerWeights, ffn_out: &mut [f32]) {
342        if let Some(scale) = layer.moe.parallel_sum_scale {
343            for x in ffn_out.iter_mut() {
344                *x *= scale;
345            }
346        }
347    }
348
349    /// `build_ffn(..., down, down_b, down_s, ...)`: the `{1}` companion
350    /// multiplied onto the FFN output right after `down`, before any
351    /// post-norm (`crate::weight_scales`). Elementwise, so one row and a
352    /// batch of rows are the same call.
353    fn apply_down_scale(layer: &LayerWeights, ffn_out: &mut [f32]) {
354        if let Some(scale) = layer.moe.down_scale {
355            for x in ffn_out.iter_mut() {
356                *x *= scale;
357            }
358        }
359    }
360
361    /// Talkie's second residual (`talkie.cpp:123-126`,
362    /// `crate::skip_stream`): `hidden += skip * out_scale`, row by row,
363    /// after the FFN residual add. A model without the stream passes
364    /// `None` and carries no `out_scale`; the two agree because both
365    /// come from one `ModelConfig::skip_stream`, and the assert says so.
366    fn apply_skip_stream(
367        layer: &LayerWeights,
368        hidden: &mut [f32],
369        skip: Option<SkipStream<'_>>,
370        rows: usize,
371        hidden_dim: usize,
372    ) {
373        match (skip, layer.out_scale) {
374            (Some(skip), Some(scale)) => {
375                debug_assert_eq!(skip.rows.len(), rows * hidden_dim);
376                debug_assert_eq!(hidden.len(), rows * hidden_dim);
377                for (h, s) in hidden.iter_mut().zip(skip.rows.iter()) {
378                    *h += s * scale;
379                }
380            }
381            (None, None) => {}
382            (skip, scale) => unreachable!(
383                "the skip stream and the layer's out_scale come from one config fact; \
384                 got skip={} out_scale={scale:?}",
385                skip.is_some()
386            ),
387        }
388    }
389
390    /// The norm a pass boundary applies (`crate::layer_loops`). Here,
391    /// at the end of BOTH FFN bodies, so every caller of either gets
392    /// it; a model that does not loop never enters the branch.
393    ///
394    /// Two shapes: `nanbeige.cpp:167-175` norms with the model's own
395    /// `output_norm` after every pass but the last, and
396    /// `hrm-text.cpp:162` closes every stack with a WEIGHTLESS RMS --
397    /// which for that architecture is the only final norm there is.
398    fn apply_loop_norm(&self, layer_idx: usize, hidden: &mut [f32], rows: usize) {
399        let Some(loops) = self.config.layer_loops else {
400            return;
401        };
402        let Some(kind) = loops.loop_norm_after(layer_idx) else {
403            return;
404        };
405        let width = self.config.hidden_dim;
406        debug_assert_eq!(hidden.len(), rows * width);
407        for row in hidden.chunks_mut(width) {
408            let normed = match kind {
409                crate::layer_loops::LoopNorm::Output => {
410                    self.final_norm.apply(row, self.config.rms_norm_eps)
411                }
412                crate::layer_loops::LoopNorm::Weightless => {
413                    crate::norm::NormOp::RmsNoParams.apply(row, self.config.rms_norm_eps)
414                }
415            };
416            row.copy_from_slice(&normed);
417        }
418    }
419
420    /// The batched twin of [`Self::ffn_block_row`]: runs layer
421    /// `layer_idx`'s FFN on every row of `hidden_batch`
422    /// (`[batch_size, hidden_dim]`, the post-attention residuals) and
423    /// adds it back, or does nothing for a layer whose shape has no
424    /// FFN.
425    ///
426    /// The fast paths are tried in the order the prefill body always
427    /// tried them -- Metal MoE prefill, the batched dense FFN, the
428    /// bucketed batched MoE -- and each answers `None` for a layer or a
429    /// batch it does not serve, so the per-row bodies are the floor
430    /// every row can reach. gpt-oss runs one position at a time through
431    /// its single validated FFN; none of the batched paths knows about
432    /// its router bias, expert bias or `swiglu_oai`.
433    #[allow(clippy::too_many_arguments)]
434    pub(crate) fn ffn_block_batch(
435        &self,
436        layer_idx: usize,
437        layer: &LayerWeights,
438        hidden_batch: &mut [f32],
439        batch_size: usize,
440        oai: Option<&GptOssLayer>,
441        plan: Option<&frink_moe::PlacementPlan>,
442        inputs: BranchInputs,
443        kernels: BatchedFfnKernels,
444        skip: Option<SkipStream<'_>>,
445    ) {
446        if self.config.layer_shape(layer_idx).ffn_dim == 0 {
447            return;
448        }
449        let hidden_dim = self.config.hidden_dim;
450        let config = &self.config;
451        let BranchInputs {
452            router: operand,
453            ffn,
454        } = inputs;
455        let normed2_batch: Vec<f32> = match ffn {
456            FfnInput::PostAttnResidual => {
457                self.pre_norm_residual(&layer.moe.norm_weight, hidden_batch, batch_size)
458            }
459            FfnInput::LayerInput(x) => {
460                debug_assert_eq!(x.len(), batch_size * hidden_dim);
461                x
462            }
463        };
464
465        if let Some(oai) = oai {
466            for b in 0..batch_size {
467                let normed2 = &normed2_batch[b * hidden_dim..(b + 1) * hidden_dim];
468                let ffn_out = Self::gpt_oss_ffn(layer, oai, normed2, config, hidden_dim);
469                let hidden_row = &mut hidden_batch[b * hidden_dim..(b + 1) * hidden_dim];
470                residual_add(hidden_row, &ffn_out, config.residual_scale);
471            }
472            Self::apply_skip_stream(layer, hidden_batch, skip, batch_size, hidden_dim);
473            self.apply_loop_norm(layer_idx, hidden_batch, batch_size);
474            return;
475        }
476
477        let dense = Self::is_dense_layer(layer);
478        // Skip the batched router matmul entirely for a dense layer --
479        // there is nothing to route (see `is_dense_layer`'s doc
480        // comment), so computing it here just to ignore it below would
481        // waste the one matmul this fast path exists to avoid.
482        let (router_logits_batch, routed_batch): (Vec<f32>, &[f32]) = match &operand {
483            _ if dense => (Vec::new(), normed2_batch.as_slice()),
484            RouterOperand::FfnInput => (
485                layer.moe.router.apply_batch(&normed2_batch, batch_size),
486                normed2_batch.as_slice(),
487            ),
488            RouterOperand::Precomputed(logits) => (logits.clone(), normed2_batch.as_slice()),
489            RouterOperand::BranchInput(x) => {
490                (layer.moe.router.apply_batch(x, batch_size), x.as_slice())
491            }
492        };
493
494        let batched: Option<Vec<f32>> = match kernels {
495            BatchedFfnKernels::PerRow => None,
496            BatchedFfnKernels::Prefill => {
497                #[cfg(feature = "metal")]
498                let metal_ffn = if !dense {
499                    Self::try_metal_moe_prefill_batch(
500                        layer_idx,
501                        layer,
502                        &normed2_batch,
503                        &router_logits_batch,
504                        batch_size,
505                        hidden_dim,
506                        config,
507                    )
508                } else {
509                    None
510                };
511                #[cfg(not(feature = "metal"))]
512                let metal_ffn: Option<Vec<f32>> = None;
513                metal_ffn
514                    // Dense FFN, batched. Without this the FFN -- the
515                    // majority of a dense model's prefill work -- ran one
516                    // position at a time while Q/K/V and the router were
517                    // already batched, which is why `pp512` measured
518                    // about the same as `tg128`.
519                    .or_else(|| {
520                        Self::dense_ffn_batch(layer_idx, layer, &normed2_batch, batch_size, config)
521                    })
522                    .or_else(|| {
523                        Self::moe_ffn_batch(
524                            layer_idx,
525                            layer,
526                            &normed2_batch,
527                            routed_batch,
528                            &router_logits_batch,
529                            batch_size,
530                            config,
531                            plan,
532                        )
533                    })
534            }
535        };
536
537        if let Some(mut ffn_batch) = batched {
538            Self::apply_parallel_sum_scale(layer, &mut ffn_batch);
539            Self::apply_down_scale(layer, &mut ffn_batch);
540            if let Some(post) = &layer.attn.post_ffn_norm {
541                ffn_batch = ffn_batch
542                    .chunks(hidden_dim)
543                    .flat_map(|row| rms_norm(row, post, config.post_norm_eps()))
544                    .collect();
545            }
546            residual_add(hidden_batch, &ffn_batch, config.residual_scale);
547            Self::apply_skip_stream(layer, hidden_batch, skip, batch_size, hidden_dim);
548            self.apply_loop_norm(layer_idx, hidden_batch, batch_size);
549            return;
550        }
551
552        let n_experts = layer.moe.n_experts().max(1);
553        for b in 0..batch_size {
554            let normed2 = &normed2_batch[b * hidden_dim..(b + 1) * hidden_dim];
555            let mut ffn_out = if dense {
556                Self::run_ffn_block(
557                    layer_idx,
558                    layer,
559                    normed2,
560                    config,
561                    hidden_dim,
562                    plan,
563                    RouterOperand::FfnInput,
564                )
565            } else {
566                let router_logits = &router_logits_batch[b * n_experts..(b + 1) * n_experts];
567                Self::combine_ffn_outputs_for_position(
568                    layer_idx,
569                    layer,
570                    normed2,
571                    &routed_batch[b * hidden_dim..(b + 1) * hidden_dim],
572                    router_logits,
573                    config,
574                    hidden_dim,
575                    plan,
576                )
577            };
578            Self::apply_parallel_sum_scale(layer, &mut ffn_out);
579            Self::apply_down_scale(layer, &mut ffn_out);
580            if let Some(post) = &layer.attn.post_ffn_norm {
581                ffn_out = rms_norm(&ffn_out, post, config.post_norm_eps());
582            }
583            let hidden_row = &mut hidden_batch[b * hidden_dim..(b + 1) * hidden_dim];
584            residual_add(hidden_row, &ffn_out, config.residual_scale);
585        }
586        Self::apply_skip_stream(layer, hidden_batch, skip, batch_size, hidden_dim);
587        self.apply_loop_norm(layer_idx, hidden_batch, batch_size);
588    }
589}