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}