1use rlx_flow::blocks::{GemmaLayerStyle, gemma_strided_layer_mask, gemma2_layer_mask};
19use rlx_gguf::{GgufFile, MetaValue};
20use rlx_ir::op::MaskKind;
21use serde::Deserialize;
22use std::path::Path;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
25#[serde(rename_all = "lowercase")]
26pub enum GemmaArch {
27 #[default]
28 Gemma,
29 Gemma2,
30 Gemma3,
31 Gemma4,
32}
33
34impl GemmaArch {
35 pub fn sliding_window_stride(self) -> usize {
36 match self {
37 GemmaArch::Gemma3 | GemmaArch::Gemma4 => 6,
38 _ => 0,
39 }
40 }
41
42 fn from_gguf_tag(tag: &str) -> Self {
43 match tag {
44 "gemma2" => GemmaArch::Gemma2,
45 "gemma3" | "gemma3n" => GemmaArch::Gemma3,
46 "gemma4" | "gemma4moe" | "gemma4_unified" | "gemma4_unified_text" => GemmaArch::Gemma4,
47 _ => GemmaArch::Gemma,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum GemmaLayerType {
58 SlidingAttention,
59 FullAttention,
60}
61
62#[derive(Debug, Clone, Copy, Deserialize, Default)]
67pub struct GemmaRopeParameters {
68 #[serde(default)]
69 pub partial_rotary_factor: Option<f32>,
70 #[serde(default)]
71 pub rope_theta: Option<f32>,
72 #[serde(default)]
73 pub rope_type: Option<GemmaRopeKind>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
77#[serde(rename_all = "snake_case")]
78pub enum GemmaRopeKind {
79 #[default]
80 Default,
81 Proportional,
82 Linear,
83 Dynamic,
84}
85
86#[derive(Debug, Clone, Default, Deserialize)]
87pub struct GemmaRopeMap {
88 #[serde(default)]
89 pub sliding_attention: Option<GemmaRopeParameters>,
90 #[serde(default)]
91 pub full_attention: Option<GemmaRopeParameters>,
92}
93
94#[derive(Debug, Clone, Deserialize)]
95pub struct GemmaConfig {
96 #[serde(default)]
97 pub arch: GemmaArch,
98 pub vocab_size: usize,
99 pub hidden_size: usize,
100 pub intermediate_size: usize,
101 pub num_hidden_layers: usize,
102 pub num_attention_heads: usize,
103 pub num_key_value_heads: usize,
104 pub max_position_embeddings: usize,
105 #[serde(default = "default_rms_norm_eps")]
106 pub rms_norm_eps: f64,
107 #[serde(default = "default_rope_theta")]
108 pub rope_theta: f64,
109 #[serde(default)]
110 pub tie_word_embeddings: bool,
111 #[serde(default)]
112 pub attention_bias: bool,
113 #[serde(default)]
114 pub head_dim: Option<usize>,
115 #[serde(default)]
116 pub attn_logit_softcapping: Option<f32>,
117 #[serde(default)]
118 pub final_logit_softcapping: Option<f32>,
119 #[serde(default)]
120 pub sliding_window: Option<usize>,
121 #[serde(default)]
122 pub query_pre_attn_scalar: Option<f32>,
123 #[serde(default)]
124 pub effective_num_layers: Option<usize>,
125 #[serde(default)]
126 pub num_experts: usize,
127 #[serde(default)]
128 pub num_experts_used: usize,
129 #[serde(default)]
130 pub expert_ffn_size: usize,
131 #[serde(default = "default_expert_weights_scale")]
132 pub expert_weights_scale: f32,
133
134 #[serde(default)]
138 pub layer_types: Vec<GemmaLayerType>,
139 #[serde(default)]
141 pub rope_parameters: GemmaRopeMap,
142 #[serde(default)]
146 pub global_head_dim: Option<usize>,
147 #[serde(default)]
150 pub num_global_key_value_heads: Option<usize>,
151 #[serde(default)]
155 pub attention_k_eq_v: bool,
156 #[serde(default)]
159 pub use_bidirectional_attention: Option<String>,
160
161 #[serde(default)]
165 pub hidden_size_per_layer_input: usize,
166 #[serde(default)]
169 pub vocab_size_per_layer_input: usize,
170 #[serde(default)]
174 pub num_kv_shared_layers: usize,
175 #[serde(default)]
178 pub use_double_wide_mlp: bool,
179 #[serde(default)]
182 pub enable_moe_block: bool,
183}
184
185fn default_rms_norm_eps() -> f64 {
186 1e-6
187}
188fn default_rope_theta() -> f64 {
189 10_000.0
190}
191fn default_expert_weights_scale() -> f32 {
192 1.0
193}
194
195impl GemmaConfig {
196 pub fn from_file(path: &Path) -> anyhow::Result<Self> {
197 let data = std::fs::read_to_string(path)?;
198 let value: serde_json::Value = serde_json::from_str(&data)?;
203 let lm_value = match value.get("text_config") {
204 Some(tc) if tc.is_object() => tc.clone(),
205 _ => value.clone(),
206 };
207 let lm_value = normalize_hf_null_usize_fields(lm_value);
208 let mut cfg: Self = serde_json::from_value(lm_value)?;
209 if cfg.arch == GemmaArch::Gemma {
210 cfg.arch = infer_arch_from_json(&data);
211 }
212 Ok(cfg)
213 }
214
215 pub fn from_gguf(raw: &GgufFile) -> anyhow::Result<Self> {
216 gemma_cfg_from_gguf(raw)
217 }
218
219 pub fn head_dim(&self) -> usize {
220 self.head_dim
221 .unwrap_or(self.hidden_size / self.num_attention_heads)
222 }
223
224 pub fn kv_group_size(&self) -> usize {
225 self.num_attention_heads / self.num_key_value_heads
226 }
227
228 pub fn q_proj_dim(&self) -> usize {
229 self.num_attention_heads * self.head_dim()
230 }
231
232 pub fn kv_proj_dim(&self) -> usize {
233 self.num_key_value_heads * self.head_dim()
234 }
235
236 pub fn layer_style(&self) -> GemmaLayerStyle {
237 match self.arch {
238 GemmaArch::Gemma => GemmaLayerStyle::Gemma,
239 GemmaArch::Gemma2 => GemmaLayerStyle::Gemma2,
240 GemmaArch::Gemma3 => GemmaLayerStyle::Gemma3,
241 GemmaArch::Gemma4 => GemmaLayerStyle::Gemma4,
242 }
243 }
244
245 pub fn active_num_layers(&self) -> usize {
246 self.effective_num_layers.unwrap_or(self.num_hidden_layers)
247 }
248
249 pub fn is_moe(&self) -> bool {
250 self.arch == GemmaArch::Gemma4 && self.num_experts > 0
251 }
252
253 pub fn has_ple(&self) -> bool {
259 self.hidden_size_per_layer_input > 0
260 }
261
262 pub fn ple_width(&self) -> usize {
264 self.hidden_size_per_layer_input
265 }
266
267 pub fn ple_vocab_size(&self) -> usize {
269 if self.vocab_size_per_layer_input > 0 {
270 self.vocab_size_per_layer_input
271 } else {
272 self.vocab_size
273 }
274 }
275
276 pub fn first_kv_shared_layer(&self) -> usize {
281 self.num_hidden_layers
282 .saturating_sub(self.num_kv_shared_layers)
283 }
284
285 pub fn is_kv_shared_layer(&self, layer: usize) -> bool {
287 self.num_kv_shared_layers > 0 && layer >= self.first_kv_shared_layer()
288 }
289
290 pub fn kv_source_layer(&self, layer: usize) -> usize {
295 if !self.is_kv_shared_layer(layer) {
296 return layer;
297 }
298 let want_full = self.is_full_attention_layer(layer);
299 let boundary = self.first_kv_shared_layer();
300 (0..boundary)
301 .rev()
302 .find(|&src| self.is_full_attention_layer(src) == want_full)
303 .unwrap_or(layer)
304 }
305
306 pub fn layer_intermediate_size(&self, layer: usize) -> usize {
310 if self.use_double_wide_mlp && self.is_kv_shared_layer(layer) {
311 self.intermediate_size * 2
312 } else {
313 self.intermediate_size
314 }
315 }
316
317 pub fn use_bidirectional_vision(&self) -> bool {
319 self.use_bidirectional_attention.as_deref() == Some("vision")
320 }
321
322 pub fn expert_ffn_dim(&self) -> usize {
323 if self.expert_ffn_size > 0 {
324 self.expert_ffn_size
325 } else {
326 self.intermediate_size
327 }
328 }
329
330 pub fn attn_score_scale(&self) -> Option<f32> {
331 match self.arch {
332 GemmaArch::Gemma => None,
333 GemmaArch::Gemma4 => Some(1.0),
339 GemmaArch::Gemma2 | GemmaArch::Gemma3 => {
340 if let Some(s) = self.query_pre_attn_scalar {
341 Some(1.0 / s)
342 } else {
343 Some(1.0 / (self.head_dim() as f32).sqrt())
344 }
345 }
346 }
347 }
348
349 pub fn layer_attn_options(&self, layer: usize) -> (MaskKind, Option<f32>, Option<f32>) {
359 let scale = self.attn_score_scale();
360 let softcap = self.attn_logit_softcapping;
361 let mask = match (self.arch, self.sliding_window) {
362 (_, None) => MaskKind::Causal,
363 (GemmaArch::Gemma2, Some(w)) => gemma2_layer_mask(layer, w),
364 (GemmaArch::Gemma3 | GemmaArch::Gemma4, Some(w)) => {
365 gemma_strided_layer_mask(layer, w, self.arch.sliding_window_stride())
366 }
367 _ => MaskKind::Causal,
368 };
369 (mask, scale, softcap)
370 }
371
372 #[cfg(test)]
373 pub(crate) fn tiny_test() -> Self {
374 Self {
375 arch: GemmaArch::Gemma,
376 vocab_size: 32,
377 hidden_size: 16,
378 intermediate_size: 32,
379 num_hidden_layers: 2,
380 num_attention_heads: 4,
381 num_key_value_heads: 2,
382 max_position_embeddings: 64,
383 rms_norm_eps: 1e-6,
384 rope_theta: 10_000.0,
385 tie_word_embeddings: true,
386 attention_bias: false,
387 head_dim: None,
388 attn_logit_softcapping: None,
389 final_logit_softcapping: None,
390 sliding_window: None,
391 query_pre_attn_scalar: None,
392 effective_num_layers: None,
393 num_experts: 0,
394 num_experts_used: 0,
395 expert_ffn_size: 0,
396 expert_weights_scale: 1.0,
397 layer_types: Vec::new(),
398 rope_parameters: GemmaRopeMap::default(),
399 global_head_dim: None,
400 num_global_key_value_heads: None,
401 attention_k_eq_v: false,
402 use_bidirectional_attention: None,
403 hidden_size_per_layer_input: 0,
404 vocab_size_per_layer_input: 0,
405 num_kv_shared_layers: 0,
406 use_double_wide_mlp: false,
407 enable_moe_block: false,
408 }
409 }
410
411 pub fn is_full_attention_layer(&self, layer: usize) -> bool {
423 if !self.layer_types.is_empty() {
424 return matches!(
425 self.layer_types.get(layer),
426 Some(GemmaLayerType::FullAttention),
427 );
428 }
429 let stride = self.arch.sliding_window_stride();
430 stride > 1 && (layer + 1).is_multiple_of(stride)
431 }
432
433 pub fn layer_head_dim(&self, layer: usize) -> usize {
437 if self.is_full_attention_layer(layer) {
438 self.global_head_dim.unwrap_or_else(|| self.head_dim())
439 } else {
440 self.head_dim()
441 }
442 }
443
444 pub fn layer_k_eq_v(&self, layer: usize) -> bool {
449 if matches!(self.arch, GemmaArch::Gemma4) {
450 return self.attention_k_eq_v && self.is_full_attention_layer(layer);
455 }
456 self.attention_k_eq_v
457 }
458
459 pub fn layer_num_kv_heads(&self, layer: usize) -> usize {
463 if self.is_full_attention_layer(layer) {
464 self.num_global_key_value_heads
465 .unwrap_or(self.num_key_value_heads)
466 } else {
467 self.num_key_value_heads
468 }
469 }
470
471 pub fn layer_n_rot(&self, layer: usize) -> usize {
475 let dh = self.layer_head_dim(layer);
476 let params = self.layer_rope_parameters(layer);
477 let kind = params
478 .and_then(|p| p.rope_type)
479 .unwrap_or(GemmaRopeKind::Default);
480 let factor = params.and_then(|p| p.partial_rotary_factor);
481 match (kind, factor) {
482 (GemmaRopeKind::Proportional, Some(f)) if f > 0.0 && f < 1.0 => {
483 ((dh as f32) * f).floor() as usize
484 }
485 _ => dh,
486 }
487 }
488
489 pub fn layer_rope_theta(&self, layer: usize) -> f64 {
492 self.layer_rope_parameters(layer)
493 .and_then(|p| p.rope_theta)
494 .map(|t| t as f64)
495 .unwrap_or(self.rope_theta)
496 }
497
498 fn layer_rope_parameters(&self, layer: usize) -> Option<&GemmaRopeParameters> {
499 if self.is_full_attention_layer(layer) {
500 self.rope_parameters.full_attention.as_ref()
501 } else {
502 self.rope_parameters.sliding_attention.as_ref()
503 }
504 }
505}
506
507fn normalize_hf_null_usize_fields(mut value: serde_json::Value) -> serde_json::Value {
509 let Some(obj) = value.as_object_mut() else {
510 return value;
511 };
512 for key in [
513 "num_experts",
514 "num_experts_used",
515 "top_k_experts",
516 "expert_ffn_size",
517 "moe_intermediate_size",
518 "hidden_size_per_layer_input",
519 ] {
520 if obj.get(key).is_some_and(|v| v.is_null()) {
521 obj.insert(key.to_string(), serde_json::Value::from(0usize));
522 }
523 }
524 value
525}
526
527fn infer_arch_from_json(raw: &str) -> GemmaArch {
528 if raw.contains("\"gemma4_unified\"")
533 || raw.contains("\"gemma4_unified_text\"")
534 || raw.contains("\"gemma4\"")
535 || raw.contains("\"gemma4moe\"")
536 || raw.contains("Gemma4UnifiedForConditionalGeneration")
537 || raw.contains("Gemma4ForCausalLM")
538 {
539 return GemmaArch::Gemma4;
540 }
541 if raw.contains("\"model_type\"") {
542 if raw.contains("\"gemma2\"") {
543 return GemmaArch::Gemma2;
544 }
545 if raw.contains("\"gemma3\"") {
546 return GemmaArch::Gemma3;
547 }
548 }
549 GemmaArch::Gemma
550}
551
552fn infer_gemma4_full_partial_rotary(raw: &GgufFile, global_head_dim: usize) -> Option<f32> {
556 if global_head_dim == 0 {
557 return None;
558 }
559 let (factors, _) = raw.dequant_f32("rope_freqs.weight").ok()?;
560 if factors.is_empty() {
561 return None;
562 }
563 const SUPPRESS_THRESH: f32 = 1e20;
564 let rotated_pairs = factors.iter().filter(|&&f| f < SUPPRESS_THRESH).count();
565 if rotated_pairs == 0 || rotated_pairs >= factors.len() {
566 return None;
567 }
568 let n_rot = rotated_pairs * 2;
569 Some(n_rot as f32 / global_head_dim as f32)
570}
571
572fn gemma4_rope_map_from_gguf(
574 raw: &GgufFile,
575 get_f32: &impl Fn(&str) -> Option<f32>,
576 get_u32_opt: &impl Fn(&str) -> Option<u32>,
577 swa_head_dim: Option<usize>,
578 global_head_dim: Option<usize>,
579) -> GemmaRopeMap {
580 let full_theta = get_f32("gemma.rope.freq_base");
581 let swa_theta = get_f32("gemma.rope.freq_base_swa");
582
583 let full_partial = global_head_dim.and_then(|ghd| {
584 infer_gemma4_full_partial_rotary(raw, ghd).or_else(|| {
585 swa_head_dim.filter(|&swa| ghd > swa).map(|_| 0.25)
587 })
588 });
589
590 let swa_partial = match (swa_head_dim, get_u32_opt("gemma.rope.dimension_count_swa")) {
591 (Some(hd), Some(n_rot)) if (n_rot as usize) < hd => Some(n_rot as f32 / hd as f32),
592 _ => None,
593 };
594
595 GemmaRopeMap {
596 sliding_attention: swa_theta.map(|t| GemmaRopeParameters {
597 rope_theta: Some(t),
598 rope_type: Some(if swa_partial.is_some() {
599 GemmaRopeKind::Proportional
600 } else {
601 GemmaRopeKind::Default
602 }),
603 partial_rotary_factor: swa_partial,
604 }),
605 full_attention: full_theta.map(|t| GemmaRopeParameters {
606 rope_theta: Some(t),
607 rope_type: full_partial.map(|_| GemmaRopeKind::Proportional),
608 partial_rotary_factor: full_partial,
609 }),
610 }
611}
612
613pub fn gemma_cfg_from_gguf(raw: &GgufFile) -> anyhow::Result<GemmaConfig> {
614 let arch_tag = raw
615 .metadata
616 .get("general.architecture")
617 .and_then(MetaValue::as_str)
618 .unwrap_or("gemma");
619 let arch_prefix = arch_tag;
620 let arch = GemmaArch::from_gguf_tag(arch_tag);
621
622 let get_meta = |k: &str| -> Option<&MetaValue> {
623 raw.metadata.get(k).or_else(|| {
624 let suffix = k.strip_prefix("gemma.")?;
625 if arch_prefix == "gemma" {
626 None
627 } else {
628 let arch_key = format!("{arch_prefix}.{suffix}");
629 raw.metadata.get(&arch_key)
630 }
631 })
632 };
633 let get_u32 = |k: &str| -> anyhow::Result<u32> {
634 get_meta(k)
635 .and_then(MetaValue::as_u32)
636 .ok_or_else(|| anyhow::anyhow!("missing GGUF metadata key: {k}"))
637 };
638 let get_first_u32 = |k: &str| -> anyhow::Result<u32> {
647 get_meta(k)
648 .and_then(MetaValue::as_first_u32)
649 .ok_or_else(|| anyhow::anyhow!("missing GGUF metadata key: {k}"))
650 };
651 let get_f32 = |k: &str| -> Option<f32> {
652 get_meta(k).and_then(|v| match v {
653 MetaValue::F32(x) => Some(*x),
654 _ => None,
655 })
656 };
657 let get_u32_opt = |k: &str| -> Option<u32> { get_meta(k).and_then(MetaValue::as_u32) };
658 let get_bool = |k: &str| -> Option<bool> {
659 get_meta(k).and_then(|v| match v {
660 MetaValue::Bool(b) => Some(*b),
661 _ => None,
662 })
663 };
664
665 let hidden_size = get_u32("gemma.embedding_length")? as usize;
666 let num_attention_heads = get_first_u32("gemma.attention.head_count")? as usize;
667 let resolved_vocab_size: usize = get_u32("gemma.vocab_size")
672 .ok()
673 .map(|v| v as usize)
674 .or_else(|| {
675 raw.metadata
676 .get("tokenizer.ggml.tokens")
677 .and_then(MetaValue::as_array)
678 .map(|a| a.len())
679 })
680 .unwrap_or(256_000);
681 let head_dim = if matches!(arch, GemmaArch::Gemma4) {
686 get_first_u32("gemma.attention.key_length_swa")
688 .ok()
689 .or_else(|| get_first_u32("gemma.attention.key_length").ok())
690 .map(|v| v as usize)
691 } else {
692 get_first_u32("gemma.attention.key_length")
693 .ok()
694 .or_else(|| get_first_u32("gemma.rope.dimension_count").ok())
695 .map(|v| v as usize)
696 };
697
698 let global_head_dim = if matches!(arch, GemmaArch::Gemma4) {
703 let swa = head_dim.unwrap_or(0);
704 let full = get_first_u32("gemma.attention.key_length")
705 .ok()
706 .map(|v| v as usize);
707 match full {
708 Some(f) if f != swa => Some(f),
709 _ => None,
710 }
711 } else {
712 None
713 };
714 let num_global_key_value_heads = if matches!(arch, GemmaArch::Gemma4) {
715 let sliding_kv = get_first_u32("gemma.attention.head_count_kv")
716 .ok()
717 .map(|v| v as usize);
718 let kv_array = raw
719 .metadata
720 .get(&format!("{arch_prefix}.attention.head_count_kv"))
721 .or_else(|| raw.metadata.get("gemma.attention.head_count_kv"))
722 .and_then(MetaValue::as_array);
723 if let (Some(swa_kv), Some(arr)) = (sliding_kv, kv_array) {
724 arr.iter().find_map(|v| match v {
725 MetaValue::I32(n) if (*n as usize) != swa_kv => Some(*n as usize),
726 MetaValue::U32(n) if (*n as usize) != swa_kv => Some(*n as usize),
727 _ => None,
728 })
729 } else {
730 None
731 }
732 } else {
733 None
734 };
735
736 Ok(GemmaConfig {
737 arch,
738 vocab_size: resolved_vocab_size,
739 hidden_size,
740 intermediate_size: get_u32("gemma.feed_forward_length")? as usize,
741 num_hidden_layers: get_u32("gemma.block_count")? as usize,
742 num_attention_heads,
743 num_key_value_heads: get_first_u32("gemma.attention.head_count_kv")? as usize,
744 max_position_embeddings: get_u32("gemma.context_length").unwrap_or(8192) as usize,
745 rms_norm_eps: get_f32("gemma.attention.layer_norm_rms_epsilon").unwrap_or(1e-6) as f64,
746 rope_theta: get_f32("gemma.rope.freq_base").unwrap_or(10_000.0) as f64,
747 tie_word_embeddings: get_bool("gemma.tie_word_embeddings").unwrap_or(true),
748 attention_bias: get_bool("gemma.attention.bias").unwrap_or(false),
749 head_dim,
750 attn_logit_softcapping: get_f32("gemma.attn_logit_softcapping"),
751 final_logit_softcapping: get_f32("gemma.final_logit_softcapping"),
752 sliding_window: get_u32("gemma.attention.sliding_window")
753 .ok()
754 .map(|v| v as usize),
755 query_pre_attn_scalar: get_f32("gemma.attention.query_pre_attn_scalar"),
756 effective_num_layers: get_u32("gemma.block_count_effective")
757 .ok()
758 .map(|v| v as usize),
759 num_experts: get_u32("gemma.expert_count").unwrap_or(0) as usize,
760 num_experts_used: get_u32("gemma.expert_used_count").unwrap_or(0) as usize,
761 expert_ffn_size: get_u32("gemma.expert_feed_forward_length").unwrap_or(0) as usize,
762 expert_weights_scale: get_f32("gemma.expert_weights_scale").unwrap_or(1.0),
763 layer_types: Vec::new(),
768 rope_parameters: if matches!(arch, GemmaArch::Gemma4) {
777 gemma4_rope_map_from_gguf(raw, &get_f32, &get_u32_opt, head_dim, global_head_dim)
778 } else {
779 GemmaRopeMap::default()
780 },
781 global_head_dim,
782 num_global_key_value_heads,
783 attention_k_eq_v: matches!(arch, GemmaArch::Gemma4),
790 use_bidirectional_attention: None,
791 hidden_size_per_layer_input: get_u32("gemma.embedding_length_per_layer_input").unwrap_or(0)
796 as usize,
797 vocab_size_per_layer_input: 0,
798 num_kv_shared_layers: get_u32("gemma.attention.shared_kv_layers").unwrap_or(0) as usize,
799 use_double_wide_mlp: get_bool("gemma.use_double_wide_mlp").unwrap_or(false),
800 enable_moe_block: get_u32("gemma.expert_count").unwrap_or(0) > 0,
801 })
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807
808 const GEMMA_4_12B_CONFIG: &str = r#"{
813 "architectures": ["Gemma4UnifiedForConditionalGeneration"],
814 "model_type": "gemma4_unified",
815 "tie_word_embeddings": true,
816 "text_config": {
817 "model_type": "gemma4_unified_text",
818 "vocab_size": 262144,
819 "hidden_size": 3840,
820 "intermediate_size": 15360,
821 "num_hidden_layers": 48,
822 "num_attention_heads": 16,
823 "num_key_value_heads": 8,
824 "num_global_key_value_heads": 1,
825 "head_dim": 256,
826 "global_head_dim": 512,
827 "attention_k_eq_v": true,
828 "max_position_embeddings": 131072,
829 "rms_norm_eps": 1e-6,
830 "tie_word_embeddings": true,
831 "attention_bias": false,
832 "final_logit_softcapping": 30.0,
833 "sliding_window": 1024,
834 "layer_types": [
835 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
836 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
837 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
838 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
839 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
840 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
841 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
842 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention"
843 ],
844 "rope_parameters": {
845 "full_attention": { "partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional" },
846 "sliding_attention": { "rope_theta": 10000.0, "rope_type": "default" }
847 }
848 }
849 }"#;
850
851 #[test]
852 fn gemma_4_12b_unified_config_parses_text_subtree() {
853 let dir = std::env::temp_dir();
854 let path = dir.join("rlx_gemma_gemma4_12b_test_config.json");
855 std::fs::write(&path, GEMMA_4_12B_CONFIG).unwrap();
856 let cfg = GemmaConfig::from_file(&path).unwrap();
857 std::fs::remove_file(&path).ok();
858
859 assert_eq!(cfg.arch, GemmaArch::Gemma4);
860 assert_eq!(cfg.vocab_size, 262_144);
861 assert_eq!(cfg.hidden_size, 3840);
862 assert_eq!(cfg.intermediate_size, 15_360);
863 assert_eq!(cfg.num_hidden_layers, 48);
864 assert_eq!(cfg.num_attention_heads, 16);
865 assert_eq!(cfg.num_key_value_heads, 8);
866 assert_eq!(cfg.head_dim(), 256);
867 assert_eq!(cfg.global_head_dim, Some(512));
868 assert_eq!(cfg.num_global_key_value_heads, Some(1));
869 assert!(cfg.attention_k_eq_v);
870 assert_eq!(cfg.sliding_window, Some(1024));
871 assert_eq!(cfg.final_logit_softcapping, Some(30.0));
872 assert!(cfg.tie_word_embeddings);
873 assert_eq!(cfg.layer_types.len(), 48);
874 assert_eq!(cfg.arch.sliding_window_stride(), 6);
876 }
877
878 #[test]
887 fn gemma4_gguf_per_layer_array_and_tokens_vocab() {
888 use rlx_gguf::{GgufFile, GgufWriter, MetaValue};
889 let mut w = GgufWriter::new();
890 w.set_meta("general.architecture", MetaValue::String("gemma4".into()));
892 w.set_meta("gemma4.embedding_length", MetaValue::U32(5376));
893 w.set_meta("gemma4.feed_forward_length", MetaValue::U32(21_504));
894 w.set_meta("gemma4.block_count", MetaValue::U32(60));
895 w.set_meta("gemma4.attention.head_count", MetaValue::U32(4));
897 let layer_kv: Vec<MetaValue> = (0..60)
900 .map(|i| MetaValue::I32(if i == 5 { 4 } else { 16 }))
901 .collect();
902 w.set_meta("gemma4.attention.head_count_kv", MetaValue::Array(layer_kv));
903 let tokens: Vec<MetaValue> = (0..262_144)
906 .map(|_| MetaValue::String(String::new()))
907 .collect();
908 w.set_meta("tokenizer.ggml.tokens", MetaValue::Array(tokens));
909
910 let path = std::env::temp_dir().join("rlx_gemma_gemma4_array_kv_test.gguf");
911 w.write_to_path(&path).unwrap();
912 let raw = GgufFile::from_path(&path).unwrap();
913 std::fs::remove_file(&path).ok();
914
915 let cfg = gemma_cfg_from_gguf(&raw).unwrap();
916 assert_eq!(cfg.arch, GemmaArch::Gemma4);
917 assert_eq!(cfg.vocab_size, 262_144, "vocab from tokens-array length");
918 assert_eq!(cfg.hidden_size, 5376);
919 assert_eq!(cfg.intermediate_size, 21_504);
920 assert_eq!(cfg.num_hidden_layers, 60);
921 assert_eq!(cfg.num_attention_heads, 4);
922 assert_eq!(
924 cfg.num_key_value_heads, 16,
925 "as_first_u32 should pick array[0], not panic on Array variant"
926 );
927 assert!(cfg.attention_k_eq_v, "Gemma 4 should default k_eq_v=true");
929 }
930
931 #[test]
932 fn hf_null_moe_fields_default_to_zero() {
933 let json = r#"{"num_experts": null, "top_k_experts": null}"#;
934 let v = normalize_hf_null_usize_fields(serde_json::from_str(json).unwrap());
935 let obj = v.as_object().unwrap();
936 assert_eq!(obj["num_experts"], 0);
937 assert_eq!(obj["top_k_experts"], 0);
938 }
939
940 #[test]
941 fn infer_gemma4_partial_rotary_from_rope_freqs_pattern() {
942 use rlx_gguf::{GgmlType, GgufFile, GgufWriter, MetaValue};
943 let mut w = GgufWriter::new();
944 w.set_meta("general.architecture", MetaValue::String("gemma4".into()));
945 w.set_meta("gemma4.embedding_length", MetaValue::U32(3840));
946 w.set_meta("gemma4.feed_forward_length", MetaValue::U32(15_360));
947 w.set_meta("gemma4.block_count", MetaValue::U32(48));
948 w.set_meta("gemma4.attention.head_count", MetaValue::U32(16));
949 w.set_meta("gemma4.attention.head_count_kv", MetaValue::U32(8));
950 w.set_meta("gemma4.context_length", MetaValue::U32(8192));
951 w.set_meta("gemma4.rope.freq_base", MetaValue::F32(1_000_000.0));
952 w.set_meta("gemma4.rope.freq_base_swa", MetaValue::F32(10_000.0));
953 w.set_meta("gemma4.rope.dimension_count", MetaValue::U32(512));
954 w.set_meta("gemma4.rope.dimension_count_swa", MetaValue::U32(256));
955 w.set_meta("gemma4.attention.key_length", MetaValue::U32(512));
956 w.set_meta("gemma4.attention.key_length_swa", MetaValue::U32(256));
957 let mut factors = vec![1.0f32; 64];
959 factors.extend(std::iter::repeat_n(1e30f32, 192));
960 let factor_bytes: Vec<u8> = factors.iter().flat_map(|f| f.to_le_bytes()).collect();
961 w.add_tensor_bytes("rope_freqs.weight", vec![256], GgmlType::F32, factor_bytes)
962 .unwrap();
963 let path = std::env::temp_dir().join("rlx_gemma_gemma4_rope_freqs_test.gguf");
964 w.write_to_path(&path).unwrap();
965 let raw = GgufFile::from_path(&path).unwrap();
966 std::fs::remove_file(&path).ok();
967
968 let cfg = gemma_cfg_from_gguf(&raw).unwrap();
969 assert_eq!(cfg.arch, GemmaArch::Gemma4);
970 assert_eq!(cfg.layer_n_rot(5), 128, "full layer p-RoPE from rope_freqs");
971 assert_eq!(cfg.layer_n_rot(0), 256, "swa layer full rotation");
972 let full = cfg.rope_parameters.full_attention.as_ref().unwrap();
973 assert_eq!(full.partial_rotary_factor, Some(0.25));
974 assert_eq!(full.rope_type, Some(GemmaRopeKind::Proportional));
975 }
976
977 #[test]
978 fn gemma_4_12b_per_layer_dispatch() {
979 let dir = std::env::temp_dir();
980 let path = dir.join("rlx_gemma_gemma4_12b_dispatch_config.json");
981 std::fs::write(&path, GEMMA_4_12B_CONFIG).unwrap();
982 let cfg = GemmaConfig::from_file(&path).unwrap();
983 std::fs::remove_file(&path).ok();
984
985 assert!(!cfg.is_full_attention_layer(0));
987 assert_eq!(cfg.layer_head_dim(0), 256);
988 assert_eq!(cfg.layer_num_kv_heads(0), 8);
989 assert_eq!(cfg.layer_n_rot(0), 256);
990 assert!((cfg.layer_rope_theta(0) - 10_000.0).abs() < 1e-3);
991
992 assert!(cfg.is_full_attention_layer(5));
995 assert_eq!(cfg.layer_head_dim(5), 512);
996 assert_eq!(cfg.layer_num_kv_heads(5), 1);
997 assert_eq!(cfg.layer_n_rot(5), 128);
998 assert!((cfg.layer_rope_theta(5) - 1_000_000.0).abs() < 1e-3);
999
1000 assert!(cfg.is_full_attention_layer(47));
1002 }
1003
1004 #[test]
1005 fn pre_gemma4_archs_keep_uniform_layer_shape() {
1006 let mut cfg = GemmaConfig::tiny_test();
1010 cfg.arch = GemmaArch::Gemma3;
1011 cfg.head_dim = Some(64);
1012 cfg.num_key_value_heads = 2;
1013 cfg.rope_theta = 1_000.0;
1014 for i in 0..cfg.num_hidden_layers {
1015 assert_eq!(cfg.layer_head_dim(i), 64);
1016 assert_eq!(cfg.layer_num_kv_heads(i), 2);
1017 assert_eq!(cfg.layer_n_rot(i), 64);
1018 assert!((cfg.layer_rope_theta(i) - 1_000.0).abs() < 1e-3);
1019 }
1020 }
1021
1022 #[test]
1023 fn infer_arch_picks_up_gemma4_markers() {
1024 assert_eq!(
1025 infer_arch_from_json(r#"{"model_type":"gemma4_unified"}"#),
1026 GemmaArch::Gemma4,
1027 );
1028 assert_eq!(
1029 infer_arch_from_json(r#"{"architectures":["Gemma4UnifiedForConditionalGeneration"]}"#),
1030 GemmaArch::Gemma4,
1031 );
1032 assert_eq!(
1033 infer_arch_from_json(r#"{"model_type":"gemma3"}"#),
1034 GemmaArch::Gemma3,
1035 );
1036 }
1037
1038 const GEMMA_4_E2B_CONFIG: &str = r#"{
1042 "model_type": "gemma4",
1043 "text_config": {
1044 "model_type": "gemma4_text",
1045 "vocab_size": 262144,
1046 "hidden_size": 1536,
1047 "intermediate_size": 6144,
1048 "num_hidden_layers": 35,
1049 "num_attention_heads": 8,
1050 "num_key_value_heads": 1,
1051 "head_dim": 256,
1052 "global_head_dim": 512,
1053 "num_kv_shared_layers": 20,
1054 "hidden_size_per_layer_input": 256,
1055 "vocab_size_per_layer_input": 262144,
1056 "use_double_wide_mlp": true,
1057 "enable_moe_block": false,
1058 "max_position_embeddings": 131072,
1059 "rms_norm_eps": 1e-6,
1060 "final_logit_softcapping": 30.0,
1061 "sliding_window": 512,
1062 "tie_word_embeddings": false,
1063 "layer_types": [
1064 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1065 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1066 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1067 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1068 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1069 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention",
1070 "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention"
1071 ],
1072 "rope_parameters": {
1073 "full_attention": {"partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional"},
1074 "sliding_attention": {"rope_theta": 10000.0, "rope_type": "default"}
1075 }
1076 }
1077 }"#;
1078
1079 #[test]
1080 fn gemma_4_e2b_config_parses_ple_and_kv_sharing() {
1081 let cfg: GemmaConfig = {
1082 let value: serde_json::Value = serde_json::from_str(GEMMA_4_E2B_CONFIG).unwrap();
1083 let lm = value.get("text_config").cloned().unwrap();
1084 let mut c: GemmaConfig =
1085 serde_json::from_value(normalize_hf_null_usize_fields(lm)).unwrap();
1086 c.arch = infer_arch_from_json(GEMMA_4_E2B_CONFIG);
1087 c
1088 };
1089 assert_eq!(cfg.arch, GemmaArch::Gemma4);
1090 assert!(cfg.has_ple());
1092 assert_eq!(cfg.ple_width(), 256);
1093 assert_eq!(cfg.ple_vocab_size(), 262144);
1094 assert_eq!(cfg.first_kv_shared_layer(), 15);
1096 for i in 0..15 {
1097 assert!(!cfg.is_kv_shared_layer(i), "layer {i} should be fresh");
1098 assert_eq!(cfg.kv_source_layer(i), i);
1099 }
1100 for i in 15..35 {
1101 assert!(cfg.is_kv_shared_layer(i), "layer {i} should be shared");
1102 }
1103 assert!(cfg.is_full_attention_layer(19));
1106 assert_eq!(
1107 cfg.kv_source_layer(19),
1108 14,
1109 "shared full reuses last fresh full"
1110 );
1111 assert_eq!(cfg.kv_source_layer(34), 14);
1112 assert!(!cfg.is_full_attention_layer(15));
1113 assert_eq!(
1114 cfg.kv_source_layer(15),
1115 13,
1116 "shared sliding reuses last fresh sliding"
1117 );
1118 assert_eq!(cfg.kv_source_layer(20), 13);
1119 assert_eq!(cfg.layer_intermediate_size(0), 6144);
1121 assert_eq!(cfg.layer_intermediate_size(14), 6144);
1122 assert_eq!(cfg.layer_intermediate_size(15), 12288);
1123 assert_eq!(cfg.layer_intermediate_size(34), 12288);
1124 }
1125
1126 #[test]
1127 fn non_e2b_config_has_no_ple_or_sharing() {
1128 let cfg = GemmaConfig::tiny_test();
1129 assert!(!cfg.has_ple());
1130 assert_eq!(cfg.first_kv_shared_layer(), cfg.num_hidden_layers);
1131 assert!(!cfg.is_kv_shared_layer(0));
1132 assert_eq!(cfg.kv_source_layer(0), 0);
1133 assert_eq!(cfg.layer_intermediate_size(0), cfg.intermediate_size);
1134 }
1135}