1use std::collections::HashMap;
46use std::fmt;
47use std::path::Path;
48use std::sync::Arc;
49
50use anyhow::Result;
51use rlx_flow::blocks::{
52 DecodeRopeParamsStage, EmbedScaleStage, GemmaDecodeLayerSpec, GemmaDecodeLayerStage,
53 GemmaLayerStyle, GemmaRmsNormStage, LmHeadStage, LogitSoftcapStage, RopeTablesStage,
54 gemma_attn_spec, gemma_prefill_layer_composed,
55};
56use rlx_flow::{BuiltModel, CompileProfile, FlowStage, ModelFlow, SideOutputs};
57use rlx_ir::dynamic::sym;
58use rlx_ir::hir::HirModule;
59use rlx_ir::shape::Dim;
60use rlx_ir::{DType, Graph, Shape};
61
62use super::config::{GemmaArch, GemmaConfig};
63use super::rope::{build_rope_tables, resolve_inv_freq};
64use rlx_core::flow_bridge::{WeightLoaderSource, load_compile_profile};
65use rlx_core::weight_loader::WeightLoader;
66
67pub const GEMMA_PROFILE_FILE: &str = "gemma.rlx.toml";
69
70pub fn gemma_profile_near_weights(weights: &Path, decode: bool) -> CompileProfile {
72 let default = if decode {
73 CompileProfile::gemma_decode()
74 } else {
75 CompileProfile::gemma_prefill()
76 };
77 let dir = weights.parent().unwrap_or_else(|| Path::new("."));
78 load_compile_profile(&dir.join(GEMMA_PROFILE_FILE), default)
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum GemmaMode {
83 Prefill,
84 Decode,
85}
86
87pub enum GemmaLayerCtx<'a> {
89 Prefill {
90 index: usize,
91 style: GemmaLayerStyle,
92 attn: rlx_flow::blocks::SelfAttnPrefillSpec,
93 kv_sink: &'a SideOutputs,
94 export_kv: bool,
95 head_dim: usize,
96 eps: f32,
97 },
98 Decode {
99 index: usize,
100 spec: GemmaDecodeLayerSpec,
101 kv_out: &'a SideOutputs,
102 aux_in: Option<&'a SideOutputs>,
105 },
106}
107
108impl GemmaLayerCtx<'_> {
109 pub fn index(&self) -> usize {
110 match self {
111 Self::Prefill { index, .. } | Self::Decode { index, .. } => *index,
112 }
113 }
114
115 pub fn default_stage(&self) -> FlowStage {
116 match self {
117 Self::Prefill {
118 index,
119 style,
120 attn,
121 kv_sink,
122 export_kv,
123 head_dim: _,
124 eps,
125 } => gemma_prefill_layer_composed(
126 *index,
127 *style,
128 attn.clone(),
129 *eps,
130 if *export_kv {
131 Some(kv_sink.inner())
132 } else {
133 None
134 },
135 ),
136 Self::Decode {
137 index,
138 spec,
139 kv_out,
140 aux_in,
141 } => {
142 let mut stage = GemmaDecodeLayerStage::layer(*index, spec.clone(), kv_out.inner());
143 if let Some(sink) = aux_in {
144 stage = stage.with_aux_input_tap(sink.inner());
145 }
146 FlowStage::Named {
147 name: format!("layer{index}"),
148 inner: Arc::new(FlowStage::GemmaDecodeLayer(stage)),
149 }
150 }
151 }
152 }
153}
154
155type LayerFn = Arc<dyn Fn(GemmaLayerCtx<'_>) -> FlowStage + Send + Sync>;
156type FlowPatchFn = Arc<dyn Fn(ModelFlow) -> ModelFlow + Send + Sync>;
157
158#[derive(Clone)]
172pub struct GemmaFlow<'a> {
173 cfg: &'a GemmaConfig,
174 mode: GemmaMode,
175 batch: usize,
176 seq: usize,
177 past_seq: usize,
178 dynamic_seq: bool,
179 dynamic_past: bool,
180 with_lm_head: bool,
181 with_kv_outputs: bool,
182 aux_hidden_layer_ids: Vec<usize>,
186 last_logits_only: bool,
187 use_custom_mask: bool,
188 profile: Option<CompileProfile>,
189 before_layers: Vec<FlowStage>,
190 after_layers: Vec<FlowStage>,
191 layer_fn: Option<LayerFn>,
192 flow_patch: Option<FlowPatchFn>,
193 prefill_hidden: bool,
195 media_attn_bias: bool,
197}
198
199impl fmt::Debug for GemmaFlow<'_> {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 f.debug_struct("GemmaFlow")
202 .field("mode", &self.mode)
203 .field("batch", &self.batch)
204 .field("seq", &self.seq)
205 .field("past_seq", &self.past_seq)
206 .field("dynamic_seq", &self.dynamic_seq)
207 .field("dynamic_past", &self.dynamic_past)
208 .field("with_lm_head", &self.with_lm_head)
209 .field("with_kv_outputs", &self.with_kv_outputs)
210 .field("last_logits_only", &self.last_logits_only)
211 .field("use_custom_mask", &self.use_custom_mask)
212 .field("profile", &self.profile)
213 .field("before_layers", &self.before_layers.len())
214 .field("after_layers", &self.after_layers.len())
215 .field("layer_fn", &self.layer_fn.is_some())
216 .field("flow_patch", &self.flow_patch.is_some())
217 .finish_non_exhaustive()
218 }
219}
220
221impl<'a> GemmaFlow<'a> {
222 pub fn new(cfg: &'a GemmaConfig) -> Self {
223 Self {
224 cfg,
225 mode: GemmaMode::Prefill,
226 batch: 1,
227 seq: 128,
228 past_seq: 0,
229 dynamic_seq: false,
230 dynamic_past: false,
231 with_lm_head: false,
232 with_kv_outputs: false,
233 aux_hidden_layer_ids: Vec::new(),
234 last_logits_only: false,
235 use_custom_mask: false,
236 profile: None,
237 before_layers: Vec::new(),
238 after_layers: Vec::new(),
239 layer_fn: None,
240 flow_patch: None,
241 prefill_hidden: false,
242 media_attn_bias: false,
243 }
244 }
245
246 pub fn prefill_from_hidden(mut self) -> Self {
248 self.prefill_hidden = true;
249 self
250 }
251
252 pub fn prefill_media_attn_bias(mut self) -> Self {
254 self.media_attn_bias = true;
255 self
256 }
257
258 pub fn for_prefill(cfg: &'a GemmaConfig, batch: usize, seq: usize) -> Self {
260 Self::new(cfg).prefill().batch(batch).seq(seq)
261 }
262
263 pub fn for_decode(cfg: &'a GemmaConfig, batch: usize, past_seq: usize) -> Self {
265 Self::new(cfg)
266 .decode()
267 .batch(batch)
268 .past(past_seq)
269 .lm_head()
270 }
271
272 pub fn prefill(mut self) -> Self {
273 self.mode = GemmaMode::Prefill;
274 self
275 }
276
277 pub fn decode(mut self) -> Self {
278 self.mode = GemmaMode::Decode;
279 self
280 }
281
282 pub fn batch(mut self, batch: usize) -> Self {
283 self.batch = batch;
284 self
285 }
286
287 pub fn seq(mut self, seq: usize) -> Self {
289 self.seq = seq;
290 self
291 }
292
293 pub fn past(mut self, past_seq: usize) -> Self {
295 self.past_seq = past_seq;
296 self
297 }
298
299 pub fn dynamic_seq(mut self) -> Self {
301 self.dynamic_seq = true;
302 self
303 }
304
305 pub fn dynamic_past(mut self) -> Self {
307 self.dynamic_past = true;
308 self
309 }
310
311 pub fn lm_head(mut self) -> Self {
312 self.with_lm_head = true;
313 self
314 }
315
316 pub fn hidden_only(mut self) -> Self {
318 self.with_lm_head = false;
319 self.last_logits_only = false;
320 self
321 }
322
323 pub fn last_token_logits(mut self) -> Self {
324 self.with_lm_head = true;
325 self.last_logits_only = true;
326 self
327 }
328
329 pub fn export_kv(mut self) -> Self {
330 self.with_kv_outputs = true;
331 self
332 }
333
334 pub fn with_aux_hidden_outputs(mut self, ids: impl IntoIterator<Item = usize>) -> Self {
348 let mut v: Vec<usize> = ids.into_iter().collect();
349 v.sort_unstable();
350 v.dedup();
351 self.aux_hidden_layer_ids = v;
352 self
353 }
354
355 pub fn custom_mask(mut self) -> Self {
356 self.use_custom_mask = true;
357 self
358 }
359
360 pub fn profile(mut self, profile: CompileProfile) -> Self {
361 self.profile = Some(profile);
362 self
363 }
364
365 pub fn profile_prefill(mut self) -> Self {
367 self.profile = Some(CompileProfile::gemma_prefill());
368 self
369 }
370
371 pub fn profile_decode(mut self) -> Self {
372 self.profile = Some(CompileProfile::gemma_decode());
373 self
374 }
375
376 pub fn profile_near(mut self, weights_path: &Path) -> Self {
377 let decode = self.mode == GemmaMode::Decode;
378 self.profile = Some(gemma_profile_near_weights(weights_path, decode));
379 self
380 }
381
382 pub fn before_layers(mut self, stages: impl IntoIterator<Item = FlowStage>) -> Self {
384 self.before_layers.extend(stages);
385 self
386 }
387
388 pub fn after_layers(mut self, stages: impl IntoIterator<Item = FlowStage>) -> Self {
390 self.after_layers.extend(stages);
391 self
392 }
393
394 pub fn layer<F>(mut self, f: F) -> Self
398 where
399 F: Fn(GemmaLayerCtx<'_>) -> FlowStage + Send + Sync + 'static,
400 {
401 self.layer_fn = Some(Arc::new(f));
402 self
403 }
404
405 pub fn patch_flow<F>(mut self, f: F) -> Self
407 where
408 F: Fn(ModelFlow) -> ModelFlow + Send + Sync + 'static,
409 {
410 self.flow_patch = Some(Arc::new(f));
411 self
412 }
413
414 pub fn build(self, weights: &mut dyn WeightLoader) -> Result<BuiltModel> {
415 match self.mode {
416 GemmaMode::Prefill => self.build_prefill(weights),
417 GemmaMode::Decode => self.build_decode(weights),
418 }
419 }
420
421 fn build_prefill(self, weights: &mut dyn WeightLoader) -> Result<BuiltModel> {
422 if self.dynamic_seq && self.batch != 1 {
423 anyhow::bail!("gemma: dynamic_seq prefill requires batch=1");
424 }
425
426 let cfg = self.cfg;
427 let profile = self.profile.unwrap_or_else(CompileProfile::gemma_prefill);
428 let f = DType::F32;
429 let h = cfg.hidden_size;
430 let eps = cfg.rms_norm_eps as f32;
431 let layer_style = cfg.layer_style();
432
433 let hidden_shape = prefill_hidden_shape(self.batch, self.seq, h, self.dynamic_seq, f);
434 let input_shape = prefill_input_shape(self.batch, self.seq, self.dynamic_seq);
435
436 let rope_factors = weights.take("rope_freqs.weight").ok().map(|(data, _)| data);
437 let inv_freq = resolve_inv_freq(cfg, rope_factors.as_deref());
438 let (cos_data, sin_data) = build_rope_tables(&inv_freq, cfg.max_position_embeddings);
439
440 let global_rope =
446 secondary_rope_tables(cfg, cfg.max_position_embeddings, rope_factors.as_deref());
447
448 let kv_sink = SideOutputs::new();
449
450 let mut flow = ModelFlow::new("gemma").with_profile(profile);
451 if self.prefill_hidden {
452 flow = flow.input("prefill_hidden", hidden_shape.clone());
453 } else {
454 flow = flow.input("input_ids", input_shape);
455 }
456
457 if self.dynamic_seq && self.with_lm_head && self.last_logits_only {
458 flow = flow.input("last_token_idx", Shape::new(&[self.batch], DType::F32));
459 }
460
461 if self.media_attn_bias {
462 let nh = cfg.num_attention_heads;
463 if self.dynamic_seq {
464 flow = flow.input(
465 "attn_bias",
466 Shape::from_dims(
467 &[
468 rlx_ir::shape::Dim::Static(self.batch),
469 rlx_ir::shape::Dim::Static(nh),
470 rlx_ir::shape::Dim::Dynamic(rlx_ir::sym::SEQ),
471 rlx_ir::shape::Dim::Dynamic(rlx_ir::sym::SEQ),
472 ],
473 f,
474 ),
475 );
476 } else {
477 flow = flow.input(
478 "attn_bias",
479 Shape::new(&[self.batch, nh, self.seq, self.seq], f),
480 );
481 }
482 }
483
484 flow = flow
485 .rope_tables(RopeTablesStage::param(
486 cfg.max_position_embeddings,
487 inv_freq.len(),
488 cos_data,
489 sin_data,
490 ))
491 .zero_beta_named("gemma.zero_beta.hidden", h);
492
493 if self.prefill_hidden {
494 flow = flow.plugin_named("gemma.prefill_hidden_bind", move |emit, _| {
495 let hidden = emit
496 .flow_input("prefill_hidden")
497 .map_err(|e| anyhow::anyhow!("prefill_hidden input: {e}"))?;
498 let _ = emit.load_param("model.embed_tokens.weight", false)?;
500 Ok(Some(hidden))
501 });
502 } else {
503 flow = flow
504 .token_embed()
505 .raw_stage(FlowStage::EmbedScale(EmbedScaleStage::new(h)));
506 }
507
508 flow = flow.raw_stages(self.before_layers.iter().cloned());
509
510 if let Some(g) = &global_rope {
511 flow = flow.raw_stage(FlowStage::RopeTables(RopeTablesStage::param_named(
512 "global",
513 cfg.max_position_embeddings,
514 g.half_dim,
515 g.cos.clone(),
516 g.sin.clone(),
517 )));
518 }
519
520 let layer_fn = self.layer_fn.clone();
521 let export = self.with_kv_outputs;
522 let media_bias = self.media_attn_bias;
523 let num_heads = cfg.num_attention_heads;
524 let num_layers = cfg.active_num_layers();
525 let layer_attn: Vec<_> = (0..num_layers).map(|i| cfg.layer_attn_options(i)).collect();
526 let is_moe = cfg.is_moe();
531 let moe_num_experts = cfg.num_experts;
532 let moe_top_k = cfg.num_experts_used;
533 let moe_n_embd = cfg.hidden_size;
534 let moe_n_ff = cfg.expert_ffn_dim();
535 let per_layer: Vec<PerLayerAttn> = (0..num_layers)
541 .map(|i| PerLayerAttn {
542 head_dim: cfg.layer_head_dim(i),
543 num_kv_heads: cfg.layer_num_kv_heads(i),
544 n_rot: cfg.layer_n_rot(i),
545 rope_table: if cfg.is_full_attention_layer(i) && global_rope.is_some() {
546 Some("global".to_string())
547 } else {
548 None
549 },
550 k_eq_v: cfg.attention_k_eq_v,
551 })
552 .collect();
553 flow = flow.repeat_layers(num_layers, {
554 let style = layer_style;
555 let sink = kv_sink.clone();
556 move |i| {
557 let (mask, score_scale, softcap) = layer_attn[i];
558 let pl = &per_layer[i];
559 let lh = pl.head_dim;
560 let mut attn = gemma_attn_spec(
561 i,
562 num_heads,
563 pl.head_dim,
564 pl.num_kv_heads,
565 pl.n_rot,
566 mask,
567 score_scale,
568 softcap,
569 );
570 if let Some(name) = pl.rope_table.as_ref() {
571 attn = attn.with_rope_table(name);
572 }
573 if pl.k_eq_v {
574 attn = attn.with_k_eq_v();
575 }
576 if let Some(ref f) = layer_fn {
577 return f(GemmaLayerCtx::Prefill {
578 index: i,
579 style,
580 attn: attn.clone(),
581 kv_sink: &sink,
582 export_kv: export,
583 head_dim: lh,
584 eps,
585 });
586 }
587 if media_bias {
588 return crate::multimodal_flow::multimodal_layer_override(
589 GemmaLayerCtx::Prefill {
590 index: i,
591 style,
592 attn,
593 kv_sink: &sink,
594 export_kv: export,
595 head_dim: lh,
596 eps,
597 },
598 true,
599 );
600 }
601 if is_moe {
602 let prefix = format!("model.layers.{i}");
603 let moe = rlx_flow::blocks::MoeFfnStage::hf(
604 prefix,
605 moe_num_experts,
606 moe_top_k,
607 moe_n_embd,
608 moe_n_ff,
609 );
610 let kv = if export { Some(sink.inner()) } else { None };
611 return rlx_flow::blocks::gemma_moe_prefill_layer_composed(
612 i, style, attn, eps, kv, moe,
613 );
614 }
615 GemmaLayerCtx::Prefill {
616 index: i,
617 style,
618 attn,
619 kv_sink: &sink,
620 export_kv: export,
621 head_dim: lh,
622 eps,
623 }
624 .default_stage()
625 }
626 });
627
628 flow = flow.raw_stages(self.after_layers.iter().cloned());
629
630 if self.with_lm_head && self.last_logits_only {
631 flow = if self.dynamic_seq {
632 flow.gather_last_token_dynamic(self.batch)
633 } else {
634 flow.gather_last_token_at(self.batch, self.seq)
635 };
636 }
637
638 flow = flow.raw_stage(FlowStage::GemmaRmsNorm(GemmaRmsNormStage::hf_layer(
639 "model.norm",
640 eps,
641 )));
642
643 if let Some(patch) = self.flow_patch {
644 flow = patch(flow);
645 }
646
647 let mut built = if self.with_lm_head {
648 let lm = if cfg.tie_word_embeddings {
649 FlowStage::LmHead(LmHeadStage::tied(cfg.vocab_size, h))
650 } else {
651 FlowStage::LmHead(LmHeadStage::separate("lm_head.weight", cfg.vocab_size, h))
652 };
653 flow = flow.raw_stage(lm);
654 if let Some(cap) = cfg.final_logit_softcapping {
655 flow = flow.raw_stage(FlowStage::LogitSoftcap(LogitSoftcapStage::new(cap)));
656 }
657 flow.output("logits")
658 .build(&mut WeightLoaderSource(weights))?
659 } else {
660 flow.output("hidden")
661 .build(&mut WeightLoaderSource(weights))?
662 };
663
664 if self.with_kv_outputs {
665 built = built.with_extra_hir_outputs(kv_sink.drain());
666 }
667 Ok(built)
668 }
669
670 fn build_decode(self, weights: &mut dyn WeightLoader) -> Result<BuiltModel> {
671 let cfg = self.cfg;
672 let profile = self.profile.unwrap_or_else(CompileProfile::gemma_decode);
673 let f = DType::F32;
674 let h = cfg.hidden_size;
675 let eps = cfg.rms_norm_eps as f32;
676 let dh = cfg.head_dim();
677 let half = dh / 2;
678
679 let hidden_shape = Shape::new(&[self.batch, 1, h], f);
680
681 let decode_style = cfg.layer_style();
682 let decode_score_scale = cfg.attn_score_scale();
683 let decode_softcap = cfg.attn_logit_softcapping;
684 let decode_arch = cfg.arch;
685 let decode_sliding = cfg.sliding_window;
686
687 let kv_out = SideOutputs::new();
688 let aux_in = SideOutputs::new();
694 let aux_layer_ids: Vec<usize> = self
695 .aux_hidden_layer_ids
696 .iter()
697 .copied()
698 .filter(|i| *i < cfg.num_hidden_layers)
699 .collect();
700 let aux_in_active = !aux_layer_ids.is_empty();
701 if aux_in_active && cfg.is_moe() {
702 anyhow::bail!(
703 "gemma: with_aux_hidden_outputs is not yet supported on MoE configs \
704 (`is_moe()=true`). The MoE decode layer goes through \
705 `gemma_moe_decode_layer_composed`, which does not yet plumb the tap. \
706 Validate EAGLE3 against the dense Gemma 4 31B target."
707 );
708 }
709
710 let rope_factors = weights.take("rope_freqs.weight").ok().map(|(data, _)| data);
711 let inv_freq = resolve_inv_freq(cfg, rope_factors.as_deref());
712 let (rope_cos, rope_sin) = if self.dynamic_past {
713 (Vec::new(), Vec::new())
714 } else {
715 crate::rope::rope_slice(&inv_freq, self.past_seq)
716 };
717
718 let global_rope_row = if !self.dynamic_past {
723 secondary_rope_row(cfg, self.past_seq, rope_factors.as_deref())
724 } else {
725 None
726 };
727 let global_params = needs_secondary_rope_params(cfg);
728
729 let mut flow = ModelFlow::new("gemma_decode")
730 .with_profile(profile)
731 .input("input_ids", Shape::new(&[self.batch, 1], DType::F32));
732
733 if self.dynamic_past {
734 flow = flow
735 .input("rope_cos", Shape::new(&[1, half], f))
736 .input("rope_sin", Shape::new(&[1, half], f));
737 if let Some(gp) = global_params {
738 let half_global =
739 crate::rope::resolve_global_inv_freq(cfg, rope_factors.as_deref())
740 .map(|v| v.len())
741 .unwrap_or_else(|| crate::rope::default_inv_freq(gp.theta, gp.n_rot).len());
742 flow = flow
743 .input("rope_cos_global", Shape::new(&[1, half_global], f))
744 .input("rope_sin_global", Shape::new(&[1, half_global], f))
745 .raw_stage(FlowStage::Custom(rlx_flow::blocks::CustomStage::named(
746 "gemma.bind_global_decode_rope",
747 |emit, val| {
748 let cos = find_hir_input(emit.hir(), "rope_cos_global")?;
753 let sin = find_hir_input(emit.hir(), "rope_sin_global")?;
754 emit.set_named("global_cos", cos);
755 emit.set_named("global_sin", sin);
756 Ok(val)
757 },
758 )));
759 }
760 }
761
762 if self.use_custom_mask {
763 flow = flow.input("mask", Shape::new(&[self.batch, self.past_seq + 1], f));
764 }
765
766 for layer_idx in 0..cfg.num_hidden_layers {
771 let layer_kv_dim = cfg.layer_num_kv_heads(layer_idx) * cfg.layer_head_dim(layer_idx);
772 let shape = if self.dynamic_past {
773 Shape::from_dims(
774 &[
775 Dim::Static(self.batch),
776 Dim::Dynamic(sym::PAST_SEQ),
777 Dim::Static(layer_kv_dim),
778 ],
779 f,
780 )
781 } else {
782 Shape::new(&[self.batch, self.past_seq, layer_kv_dim], f)
783 };
784 flow = flow
785 .input(format!("past_k_{layer_idx}"), shape.clone())
786 .input(format!("past_v_{layer_idx}"), shape);
787 }
788
789 if !self.dynamic_past {
790 flow = flow.raw_stage(FlowStage::DecodeRopeParams(DecodeRopeParamsStage::new(
791 rope_cos, rope_sin, half,
792 )));
793 if let Some(g) = &global_rope_row {
794 flow = flow.raw_stage(FlowStage::DecodeRopeParams(DecodeRopeParamsStage::named(
795 "global",
796 g.cos.clone(),
797 g.sin.clone(),
798 g.half_dim,
799 )));
800 }
801 }
802
803 flow = flow
804 .bind_decode_inputs(cfg.num_hidden_layers, self.use_custom_mask, true)
805 .zero_beta_named("gemma.zero_beta.hidden", h)
806 .token_embed()
807 .raw_stage(FlowStage::EmbedScale(EmbedScaleStage::new(h)))
808 .raw_stages(self.before_layers.iter().cloned());
809
810 let layer_fn = self.layer_fn.clone();
811 let use_custom_mask = self.use_custom_mask;
812 let num_heads = cfg.num_attention_heads;
813 let num_layers = cfg.active_num_layers();
814 let secondary_rope_active = global_rope_row.is_some();
817 let per_layer_decode: Vec<PerLayerAttn> = (0..num_layers)
818 .map(|i| PerLayerAttn {
819 head_dim: cfg.layer_head_dim(i),
820 num_kv_heads: cfg.layer_num_kv_heads(i),
821 n_rot: cfg.layer_n_rot(i),
822 rope_table: if cfg.is_full_attention_layer(i) && secondary_rope_active {
823 Some("global".to_string())
824 } else {
825 None
826 },
827 k_eq_v: cfg.attention_k_eq_v,
828 })
829 .collect();
830 let is_moe = cfg.is_moe();
832 let moe_num_experts = cfg.num_experts;
833 let moe_top_k = cfg.num_experts_used;
834 let moe_n_embd = cfg.hidden_size;
835 let moe_n_ff = cfg.expert_ffn_dim();
836 flow = flow.repeat_layers(num_layers, {
837 let sink = kv_out.clone();
838 let aux_sink = aux_in.clone();
839 let aux_ids = aux_layer_ids.clone();
840 let hidden_shape = hidden_shape.clone();
841 move |i| {
842 let aux_for_layer: Option<&SideOutputs> = if aux_ids.binary_search(&i).is_ok() {
843 Some(&aux_sink)
844 } else {
845 None
846 };
847 let mask = if use_custom_mask {
848 rlx_ir::op::MaskKind::Causal
849 } else {
850 match (decode_arch, decode_sliding) {
851 (GemmaArch::Gemma2, Some(w)) => rlx_flow::blocks::gemma2_layer_mask(i, w),
852 (GemmaArch::Gemma3 | GemmaArch::Gemma4, Some(w)) => {
856 rlx_flow::blocks::gemma_strided_layer_mask(
857 i,
858 w,
859 decode_arch.sliding_window_stride(),
860 )
861 }
862 _ => rlx_ir::op::MaskKind::Causal,
863 }
864 };
865 let pl = &per_layer_decode[i];
866 let kv_group_size = num_heads / pl.num_kv_heads;
867 let spec = GemmaDecodeLayerSpec {
868 style: decode_style,
869 num_heads,
870 head_dim: pl.head_dim,
871 num_kv_heads: pl.num_kv_heads,
872 kv_group_size,
873 n_rot: pl.n_rot,
874 rope_table: pl.rope_table.clone(),
875 k_eq_v: pl.k_eq_v,
876 eps,
877 use_custom_mask,
878 hidden_shape: hidden_shape.clone(),
879 mask,
880 score_scale: decode_score_scale,
881 attn_logit_softcap: decode_softcap,
882 };
883 if let Some(ref f) = layer_fn {
884 return f(GemmaLayerCtx::Decode {
885 index: i,
886 spec: spec.clone(),
887 kv_out: &sink,
888 aux_in: aux_for_layer,
889 });
890 }
891 if is_moe {
892 let prefix = format!("model.layers.{i}");
893 let moe = rlx_flow::blocks::MoeFfnStage::hf(
894 prefix,
895 moe_num_experts,
896 moe_top_k,
897 moe_n_embd,
898 moe_n_ff,
899 );
900 return rlx_flow::blocks::gemma_moe_decode_layer_composed(
901 i,
902 spec,
903 sink.inner(),
904 moe,
905 );
906 }
907 GemmaLayerCtx::Decode {
908 index: i,
909 spec,
910 kv_out: &sink,
911 aux_in: aux_for_layer,
912 }
913 .default_stage()
914 }
915 });
916
917 flow = flow.raw_stages(self.after_layers.iter().cloned());
918
919 if let Some(patch) = self.flow_patch {
920 flow = patch(flow);
921 }
922
923 let mut flow = flow.raw_stage(FlowStage::GemmaRmsNorm(GemmaRmsNormStage::hf_layer(
924 "model.norm",
925 eps,
926 )));
927 let lm = if cfg.tie_word_embeddings {
928 FlowStage::LmHead(LmHeadStage::tied(cfg.vocab_size, h))
929 } else {
930 FlowStage::LmHead(LmHeadStage::separate("lm_head.weight", cfg.vocab_size, h))
931 };
932 flow = flow.raw_stage(lm);
933 if let Some(cap) = cfg.final_logit_softcapping {
934 flow = flow.raw_stage(FlowStage::LogitSoftcap(LogitSoftcapStage::new(cap)));
935 }
936 let built = flow
940 .output("logits")
941 .build(&mut WeightLoaderSource(weights))?;
942 let mut extra_outputs = kv_out.drain();
943 if aux_in_active {
944 let aux = aux_in.drain();
945 debug_assert_eq!(
946 aux.len(),
947 aux_layer_ids.len(),
948 "aux tap pushed {} hidden states but {} layer ids were requested",
949 aux.len(),
950 aux_layer_ids.len()
951 );
952 extra_outputs.extend(aux);
953 }
954 let built = built.with_extra_hir_outputs(extra_outputs);
955
956 Ok(built)
957 }
958}
959
960fn prefill_hidden_shape(
961 batch: usize,
962 seq: usize,
963 hidden: usize,
964 dynamic: bool,
965 dtype: DType,
966) -> Shape {
967 if dynamic {
968 Shape::from_dims(
969 &[
970 Dim::Static(batch),
971 Dim::Dynamic(sym::SEQ),
972 Dim::Static(hidden),
973 ],
974 dtype,
975 )
976 } else {
977 Shape::new(&[batch, seq, hidden], dtype)
978 }
979}
980
981fn prefill_input_shape(batch: usize, seq: usize, dynamic: bool) -> Shape {
982 if dynamic {
983 Shape::from_dims(&[Dim::Static(batch), Dim::Dynamic(sym::SEQ)], DType::F32)
984 } else {
985 Shape::new(&[batch, seq], DType::F32)
986 }
987}
988
989#[derive(Debug, Clone)]
994struct PerLayerAttn {
995 head_dim: usize,
996 num_kv_heads: usize,
997 n_rot: usize,
998 rope_table: Option<String>,
999 k_eq_v: bool,
1000}
1001
1002#[derive(Debug, Clone)]
1003struct GlobalRopeTables {
1004 cos: Vec<f32>,
1005 sin: Vec<f32>,
1006 half_dim: usize,
1007}
1008
1009fn secondary_rope_tables(
1014 cfg: &GemmaConfig,
1015 max_pos: usize,
1016 factors: Option<&[f32]>,
1017) -> Option<GlobalRopeTables> {
1018 let inv = crate::rope::resolve_global_inv_freq(cfg, factors)?;
1019 let (cos, sin) = crate::rope::build_rope_tables(&inv, max_pos);
1020 Some(GlobalRopeTables {
1021 cos,
1022 sin,
1023 half_dim: inv.len(),
1024 })
1025}
1026
1027fn secondary_rope_row(
1029 cfg: &GemmaConfig,
1030 pos: usize,
1031 factors: Option<&[f32]>,
1032) -> Option<GlobalRopeTables> {
1033 let inv = crate::rope::resolve_global_inv_freq(cfg, factors)?;
1034 let (cos, sin) = crate::rope::rope_slice(&inv, pos);
1035 Some(GlobalRopeTables {
1036 cos,
1037 sin,
1038 half_dim: inv.len(),
1039 })
1040}
1041
1042fn needs_secondary_rope_params(cfg: &GemmaConfig) -> Option<GlobalRopeParams> {
1043 crate::rope::global_rope_params(cfg).map(|(theta, n_rot)| GlobalRopeParams { theta, n_rot })
1044}
1045
1046#[derive(Debug, Clone, Copy)]
1047struct GlobalRopeParams {
1048 theta: f64,
1049 n_rot: usize,
1050}
1051
1052fn find_hir_input(hir: &HirModule, name: &str) -> anyhow::Result<rlx_ir::HirNodeId> {
1053 use rlx_ir::hir::HirOp;
1054 for node in hir.nodes() {
1055 if let HirOp::Input { name: n } = &node.op {
1056 if n == name {
1057 return Ok(node.id);
1058 }
1059 }
1060 }
1061 Err(anyhow::anyhow!("gemma decode flow missing input: {name}"))
1062}
1063
1064impl<'a> GemmaFlow<'a> {
1067 fn from_prefill_opts(cfg: &'a GemmaConfig, o: &GemmaPrefillOpts) -> Self {
1068 let mut f = GemmaFlow::new(cfg).prefill().batch(o.batch).seq(o.seq);
1069 if o.dynamic_seq {
1070 f = f.dynamic_seq();
1071 }
1072 if o.prefill_hidden {
1073 f = f.prefill_from_hidden();
1074 }
1075 if o.media_attn_bias {
1076 f = f.prefill_media_attn_bias();
1077 }
1078 if o.with_lm_head {
1079 f = f.lm_head();
1080 }
1081 if o.with_kv_outputs {
1082 f = f.export_kv();
1083 }
1084 if o.last_logits_only {
1085 f = f.last_token_logits();
1086 }
1087 if let Some(p) = o.profile.clone() {
1088 f = f.profile(p);
1089 }
1090 f
1091 }
1092
1093 fn from_decode_opts(cfg: &'a GemmaConfig, o: &GemmaDecodeOpts) -> Self {
1094 let mut f = GemmaFlow::new(cfg)
1095 .decode()
1096 .batch(o.batch)
1097 .past(o.past_seq)
1098 .lm_head();
1099 if o.dynamic_past {
1100 f = f.dynamic_past();
1101 }
1102 if o.use_custom_mask {
1103 f = f.custom_mask();
1104 }
1105 if let Some(p) = o.profile.clone() {
1106 f = f.profile(p);
1107 }
1108 if !o.aux_hidden_layer_ids.is_empty() {
1109 f = f.with_aux_hidden_outputs(o.aux_hidden_layer_ids.iter().copied());
1110 }
1111 f
1112 }
1113}
1114
1115#[derive(Debug, Clone)]
1117pub struct GemmaPrefillOpts {
1118 pub batch: usize,
1119 pub seq: usize,
1120 pub dynamic_seq: bool,
1121 pub prefill_hidden: bool,
1122 pub media_attn_bias: bool,
1123 pub with_lm_head: bool,
1124 pub with_kv_outputs: bool,
1125 pub last_logits_only: bool,
1126 pub profile: Option<CompileProfile>,
1127}
1128
1129impl GemmaPrefillOpts {
1130 pub fn static_prefill(batch: usize, seq: usize) -> Self {
1131 Self {
1132 batch,
1133 seq,
1134 dynamic_seq: false,
1135 prefill_hidden: false,
1136 media_attn_bias: false,
1137 with_lm_head: false,
1138 with_kv_outputs: false,
1139 last_logits_only: false,
1140 profile: None,
1141 }
1142 }
1143}
1144
1145#[derive(Debug, Clone)]
1147pub struct GemmaDecodeOpts {
1148 pub batch: usize,
1149 pub past_seq: usize,
1150 pub dynamic_past: bool,
1151 pub use_custom_mask: bool,
1152 pub profile: Option<CompileProfile>,
1153 #[doc(alias = "eagle3")]
1158 pub aux_hidden_layer_ids: Vec<usize>,
1159}
1160
1161impl GemmaDecodeOpts {
1162 pub fn new(batch: usize, past_seq: usize) -> Self {
1165 Self {
1166 batch,
1167 past_seq,
1168 dynamic_past: false,
1169 use_custom_mask: false,
1170 profile: None,
1171 aux_hidden_layer_ids: Vec::new(),
1172 }
1173 }
1174
1175 pub fn with_aux_hidden_layer_ids(mut self, ids: impl IntoIterator<Item = usize>) -> Self {
1177 self.aux_hidden_layer_ids = ids.into_iter().collect();
1178 self
1179 }
1180}
1181
1182pub fn build_gemma_prefill_flow(
1183 cfg: &GemmaConfig,
1184 weights: &mut dyn WeightLoader,
1185 opts: &GemmaPrefillOpts,
1186) -> Result<(HirModule, HashMap<String, Vec<f32>>)> {
1187 build_gemma_prefill_built(cfg, weights, opts)?.into_parts()
1188}
1189
1190pub fn build_gemma_prefill_built(
1191 cfg: &GemmaConfig,
1192 weights: &mut dyn WeightLoader,
1193 opts: &GemmaPrefillOpts,
1194) -> Result<BuiltModel> {
1195 GemmaFlow::from_prefill_opts(cfg, opts).build(weights)
1196}
1197
1198pub fn build_gemma_decode_flow(
1199 cfg: &GemmaConfig,
1200 weights: &mut dyn WeightLoader,
1201 opts: &GemmaDecodeOpts,
1202) -> Result<(HirModule, HashMap<String, Vec<f32>>)> {
1203 build_gemma_decode_built(cfg, weights, opts)?.into_parts()
1204}
1205
1206pub fn build_gemma_decode_graph(
1207 cfg: &GemmaConfig,
1208 weights: &mut dyn WeightLoader,
1209 opts: &GemmaDecodeOpts,
1210) -> Result<(Graph, HashMap<String, Vec<f32>>)> {
1211 rlx_core::flow_util::graph_from_built(build_gemma_decode_built(cfg, weights, opts)?)
1212}
1213
1214pub fn build_gemma_decode_built(
1215 cfg: &GemmaConfig,
1216 weights: &mut dyn WeightLoader,
1217 opts: &GemmaDecodeOpts,
1218) -> Result<BuiltModel> {
1219 GemmaFlow::from_decode_opts(cfg, opts).build(weights)
1220}
1221
1222#[cfg(test)]
1223mod gemma4_tests {
1224 use super::*;
1225 use crate::config::{
1226 GemmaArch, GemmaLayerType, GemmaRopeKind, GemmaRopeMap, GemmaRopeParameters,
1227 };
1228
1229 fn gemma4_12b_like() -> GemmaConfig {
1230 let mut cfg = GemmaConfig::tiny_test();
1231 cfg.arch = GemmaArch::Gemma4;
1232 cfg.hidden_size = 3840;
1233 cfg.intermediate_size = 15_360;
1234 cfg.num_hidden_layers = 12; cfg.num_attention_heads = 16;
1236 cfg.num_key_value_heads = 8;
1237 cfg.head_dim = Some(256);
1238 cfg.global_head_dim = Some(512);
1239 cfg.num_global_key_value_heads = Some(1);
1240 cfg.attention_k_eq_v = true;
1241 cfg.sliding_window = Some(1024);
1242 cfg.final_logit_softcapping = Some(30.0);
1243 cfg.tie_word_embeddings = true;
1244 cfg.max_position_embeddings = 4096;
1245 cfg.rope_theta = 10_000.0;
1246 cfg.layer_types = (0..cfg.num_hidden_layers)
1248 .map(|i| {
1249 if (i + 1) % 6 == 0 {
1250 GemmaLayerType::FullAttention
1251 } else {
1252 GemmaLayerType::SlidingAttention
1253 }
1254 })
1255 .collect();
1256 cfg.rope_parameters = GemmaRopeMap {
1257 sliding_attention: Some(GemmaRopeParameters {
1258 rope_theta: Some(10_000.0),
1259 rope_type: Some(GemmaRopeKind::Default),
1260 partial_rotary_factor: None,
1261 }),
1262 full_attention: Some(GemmaRopeParameters {
1263 rope_theta: Some(1_000_000.0),
1264 rope_type: Some(GemmaRopeKind::Proportional),
1265 partial_rotary_factor: Some(0.25),
1266 }),
1267 };
1268 cfg
1269 }
1270
1271 #[test]
1272 fn secondary_rope_emits_distinct_table_for_full_attention() {
1273 let cfg = gemma4_12b_like();
1274 let tables = secondary_rope_tables(&cfg, cfg.max_position_embeddings, None)
1275 .expect("Gemma 4 split rope_parameters should produce a secondary table");
1276 assert_eq!(tables.half_dim, 256);
1280 assert_eq!(tables.cos.len(), cfg.max_position_embeddings * 256);
1281 assert_eq!(tables.sin.len(), tables.cos.len());
1282
1283 assert!((tables.cos[0] - 1.0).abs() < 1e-6);
1285 assert!(tables.sin[0].abs() < 1e-6);
1286 let global_inv = crate::rope::default_inv_freq(1_000_000.0, 512);
1294 let sliding_inv = crate::rope::default_inv_freq(10_000.0, 256);
1295 assert!((global_inv[5] - sliding_inv[5]).abs() > 1e-3);
1296 let global_cos_p1_d5 = (1.0 * global_inv[5]).cos();
1297 let global_sample = tables.cos[256 + 5]; assert!((global_sample as f64 - global_cos_p1_d5).abs() < 1e-5);
1299 assert!((tables.cos[256 + 64] - 1.0).abs() < 1e-6);
1301 }
1302
1303 #[test]
1304 fn per_layer_kv_dims_diverge_on_full_attention() {
1305 let cfg = gemma4_12b_like();
1306 assert_eq!(cfg.layer_num_kv_heads(0) * cfg.layer_head_dim(0), 2048);
1308 assert_eq!(cfg.layer_num_kv_heads(5) * cfg.layer_head_dim(5), 512);
1310 assert_eq!(cfg.layer_num_kv_heads(11) * cfg.layer_head_dim(11), 512);
1311 }
1312
1313 #[test]
1314 fn no_secondary_table_when_params_match() {
1315 let mut cfg = gemma4_12b_like();
1319 cfg.rope_parameters.full_attention = cfg.rope_parameters.sliding_attention;
1320 cfg.global_head_dim = None;
1321 cfg.num_global_key_value_heads = None;
1322 assert!(secondary_rope_tables(&cfg, cfg.max_position_embeddings, None).is_none());
1323 }
1324}