package gemma4 import ( "testing" "github.com/ollama/ollama/x/mlxrunner/mlx" ) func TestParseTextConfigE2B(t *testing.T) { skipIfNoMLX(t) data := []byte(`{ "architectures": ["Gemma4ForConditionalGeneration"], "text_config": { "hidden_size": 1536, "num_hidden_layers": 35, "intermediate_size": 6144, "num_attention_heads": 8, "num_key_value_heads": 1, "head_dim": 256, "global_head_dim": 512, "vocab_size": 262144, "rms_norm_eps": 1e-6, "max_position_embeddings": 131072, "sliding_window": 512, "sliding_window_pattern": 5, "final_logit_softcapping": 30.0, "use_double_wide_mlp": true, "num_kv_shared_layers": 20, "hidden_size_per_layer_input": 256, "vocab_size_per_layer_input": 262144, "attention_k_eq_v": false, "tie_word_embeddings": true, "layer_types": [ "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention" ], "rope_parameters": { "full_attention": { "partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional" }, "sliding_attention": { "rope_theta": 10000.0, "rope_type": "default" } } } }`) cfg, err := parseTextConfig(data) if err != nil { t.Fatalf("parseTextConfig failed: %v", err) } // Basic fields. if cfg.HiddenSize != 1536 { t.Errorf("HiddenSize = %d, want 1536", cfg.HiddenSize) } if cfg.NumHiddenLayers != 35 { t.Errorf("NumHiddenLayers = %d, want 35", cfg.NumHiddenLayers) } if cfg.GlobalHeadDim != 512 { t.Errorf("GlobalHeadDim = %d, want 512", cfg.GlobalHeadDim) } if cfg.FinalLogitSoftcapping != 30.0 { t.Errorf("FinalLogitSoftcapping = %f, want 30.0", cfg.FinalLogitSoftcapping) } if cfg.NumKVSharedLayers != 20 { t.Errorf("NumKVSharedLayers = %d, want 20", cfg.NumKVSharedLayers) } if cfg.HiddenSizePerLayer != 256 { t.Errorf("HiddenSizePerLayer = %d, want 256", cfg.HiddenSizePerLayer) } // RoPE settings. if cfg.SlidingRopeDims != 256 { t.Errorf("SlidingRopeDims = %d, want 256", cfg.SlidingRopeDims) } if cfg.FullRopeDims != 512 { t.Errorf("FullRopeDims = %d, want 512 (GlobalHeadDim, partial rotation handled via custom freqs)", cfg.FullRopeDims) } if cfg.SlidingRopeBase != 10000 { t.Errorf("SlidingRopeBase = %f, want 10000", cfg.SlidingRopeBase) } if cfg.FullRopeBase != 1000000 { t.Errorf("FullRopeBase = %f, want 1000000", cfg.FullRopeBase) } // Attention scale. if cfg.SlidingScale == 0 || cfg.FullScale == 0 { t.Error("attention scales should be non-zero") } // KV sharing map. // First shared layer is 35 - 20 = 15. if donor, ok := cfg.KVShareMap[15]; !ok || donor != 13 { t.Errorf("KVShareMap[15] = %d, ok=%v; want 13, true", donor, ok) } if donor, ok := cfg.KVShareMap[19]; !ok || donor != 14 { t.Errorf("KVShareMap[19] = %d, ok=%v; want 14, true (full attn donor)", donor, ok) } if donor, ok := cfg.KVShareMap[34]; !ok || donor != 14 { t.Errorf("KVShareMap[34] = %d, ok=%v; want 14, true (full attn donor)", donor, ok) } // Layer 14 should not be shared. if _, ok := cfg.KVShareMap[14]; ok { t.Error("layer 14 should not be in KVShareMap (non-shared)") } // Donors. if !cfg.KVDonors[13] { t.Error("layer 13 should be a KV donor") } if !cfg.KVDonors[14] { t.Error("layer 14 should be a KV donor") } } func TestParseTextConfig26B(t *testing.T) { skipIfNoMLX(t) data := []byte(`{ "architectures": ["Gemma4ForConditionalGeneration"], "text_config": { "hidden_size": 2816, "num_hidden_layers": 30, "intermediate_size": 2112, "num_attention_heads": 16, "num_key_value_heads": 8, "num_global_key_value_heads": 2, "head_dim": 256, "global_head_dim": 512, "vocab_size": 262144, "rms_norm_eps": 1e-6, "max_position_embeddings": 131072, "sliding_window": 1024, "final_logit_softcapping": 30.0, "use_double_wide_mlp": false, "num_kv_shared_layers": 0, "hidden_size_per_layer_input": null, "attention_k_eq_v": true, "enable_moe_block": true, "num_experts": 128, "top_k_experts": 8, "moe_intermediate_size": 704, "tie_word_embeddings": true, "layer_types": [ "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention" ], "rope_parameters": { "full_attention": { "partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional" }, "sliding_attention": { "rope_theta": 10000.0, "rope_type": "default" } } } }`) cfg, err := parseTextConfig(data) if err != nil { t.Fatalf("parseTextConfig failed: %v", err) } if cfg.HiddenSize != 2816 { t.Errorf("HiddenSize = %d, want 2816", cfg.HiddenSize) } if !cfg.AttentionKEqV { t.Error("AttentionKEqV should be true") } if cfg.NumGlobalKeyValueHeads != 2 { t.Errorf("NumGlobalKeyValueHeads = %d, want 2", cfg.NumGlobalKeyValueHeads) } if !cfg.EnableMoeBlock { t.Error("EnableMoeBlock should be true") } if cfg.NumExperts != 128 { t.Errorf("NumExperts = %d, want 128", cfg.NumExperts) } if cfg.TopKExperts != 8 { t.Errorf("TopKExperts = %d, want 8", cfg.TopKExperts) } if cfg.ExpertIntermediateSize != 704 { t.Errorf("ExpertIntermediateSize = %d, want 704", cfg.ExpertIntermediateSize) } if cfg.HiddenSizePerLayer != 0 { t.Errorf("HiddenSizePerLayer = %d, want 0 (no PLE)", cfg.HiddenSizePerLayer) } } func TestParseTextConfig31B(t *testing.T) { skipIfNoMLX(t) data := []byte(`{ "architectures": ["Gemma4ForConditionalGeneration"], "text_config": { "hidden_size": 5376, "num_hidden_layers": 60, "intermediate_size": 21504, "num_attention_heads": 32, "num_key_value_heads": 16, "num_global_key_value_heads": 4, "head_dim": 256, "global_head_dim": 512, "vocab_size": 262144, "rms_norm_eps": 1e-6, "max_position_embeddings": 131072, "sliding_window": 1024, "final_logit_softcapping": 30.0, "use_double_wide_mlp": false, "num_kv_shared_layers": 0, "hidden_size_per_layer_input": null, "attention_k_eq_v": true, "tie_word_embeddings": true, "layer_types": [ "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention" ], "rope_parameters": { "full_attention": { "partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional" }, "sliding_attention": { "rope_theta": 10000.0, "rope_type": "default" } } } }`) cfg, err := parseTextConfig(data) if err != nil { t.Fatalf("parseTextConfig failed: %v", err) } if cfg.HiddenSize != 5376 { t.Errorf("HiddenSize = %d, want 5376", cfg.HiddenSize) } if cfg.NumHiddenLayers != 60 { t.Errorf("NumHiddenLayers = %d, want 60", cfg.NumHiddenLayers) } if !cfg.AttentionKEqV { t.Error("AttentionKEqV should be true") } if cfg.NumGlobalKeyValueHeads != 4 { t.Errorf("NumGlobalKeyValueHeads = %d, want 4", cfg.NumGlobalKeyValueHeads) } if cfg.NumKeyValueHeads != 16 { t.Errorf("NumKeyValueHeads = %d, want 16", cfg.NumKeyValueHeads) } if cfg.NumKVSharedLayers != 0 { t.Errorf("NumKVSharedLayers = %d, want 0", cfg.NumKVSharedLayers) } if cfg.HiddenSizePerLayer != 0 { t.Errorf("HiddenSizePerLayer = %d, want 0 (no PLE)", cfg.HiddenSizePerLayer) } if cfg.SlidingWindow != 1024 { t.Errorf("SlidingWindow = %d, want 1024", cfg.SlidingWindow) } // KV sharing should be empty (no shared layers). if len(cfg.KVShareMap) != 0 { t.Errorf("KVShareMap should be empty, got %d entries", len(cfg.KVShareMap)) } // Layer types: pattern is 5 sliding + 1 full, repeating 10 times. if !isLayerSliding(0, &cfg) { t.Error("layer 0 should be sliding") } if isLayerSliding(5, &cfg) { t.Error("layer 5 should be full attention") } if !isLayerSliding(6, &cfg) { t.Error("layer 6 should be sliding") } if isLayerSliding(59, &cfg) { t.Error("layer 59 should be full attention") } } func TestParseTextConfigE4B(t *testing.T) { skipIfNoMLX(t) data := []byte(`{ "architectures": ["Gemma4ForConditionalGeneration"], "text_config": { "hidden_size": 2560, "num_hidden_layers": 42, "intermediate_size": 10240, "num_attention_heads": 8, "num_key_value_heads": 2, "head_dim": 256, "global_head_dim": 512, "vocab_size": 262144, "rms_norm_eps": 1e-6, "max_position_embeddings": 131072, "sliding_window": 512, "final_logit_softcapping": 30.0, "use_double_wide_mlp": false, "num_kv_shared_layers": 18, "hidden_size_per_layer_input": 256, "vocab_size_per_layer_input": 262144, "attention_k_eq_v": false, "enable_moe_block": false, "tie_word_embeddings": true, "layer_types": [ "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention", "sliding_attention","sliding_attention","sliding_attention","sliding_attention","sliding_attention","full_attention" ], "rope_parameters": { "full_attention": { "partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional" }, "sliding_attention": { "rope_theta": 10000.0, "rope_type": "default" } } } }`) cfg, err := parseTextConfig(data) if err != nil { t.Fatalf("parseTextConfig failed: %v", err) } if cfg.HiddenSize != 2560 { t.Errorf("HiddenSize = %d, want 2560", cfg.HiddenSize) } if cfg.NumHiddenLayers != 42 { t.Errorf("NumHiddenLayers = %d, want 42", cfg.NumHiddenLayers) } if cfg.IntermediateSize != 10240 { t.Errorf("IntermediateSize = %d, want 10240", cfg.IntermediateSize) } if cfg.NumKeyValueHeads != 2 { t.Errorf("NumKeyValueHeads = %d, want 2", cfg.NumKeyValueHeads) } if cfg.UseDoubleWideMLP { t.Error("UseDoubleWideMLP should be false") } if cfg.NumKVSharedLayers != 18 { t.Errorf("NumKVSharedLayers = %d, want 18", cfg.NumKVSharedLayers) } if cfg.HiddenSizePerLayer != 256 { t.Errorf("HiddenSizePerLayer = %d, want 256 (has PLE)", cfg.HiddenSizePerLayer) } if cfg.AttentionKEqV { t.Error("AttentionKEqV should be false") } if cfg.EnableMoeBlock { t.Error("EnableMoeBlock should be false") } if cfg.SlidingWindow != 512 { t.Errorf("SlidingWindow = %d, want 512", cfg.SlidingWindow) } // Layer types: pattern is 5 sliding + 1 full, repeating 7 times = 42 layers. if !isLayerSliding(0, &cfg) { t.Error("layer 0 should be sliding") } if isLayerSliding(5, &cfg) { t.Error("layer 5 should be full attention") } if !isLayerSliding(6, &cfg) { t.Error("layer 6 should be sliding") } if isLayerSliding(41, &cfg) { t.Error("layer 41 should be full attention") } // KV sharing: first shared = 42 - 18 = 24. // Layer 24 is sliding, its donor should be the last non-shared sliding layer. // Non-shared layers: 0-23. Last sliding in 0-23 is layer 22 (23=full). if donor, ok := cfg.KVShareMap[24]; !ok { t.Error("layer 24 should be in KVShareMap") } else { t.Logf("layer 24 donor = %d", donor) } // Layer 29 is full_attention (5th full), donor should be the last non-shared full layer. // Non-shared full layers: 5, 11, 17, 23. if donor, ok := cfg.KVShareMap[29]; !ok || donor != 23 { t.Errorf("KVShareMap[29] = %d, ok=%v; want 23, true (full attn donor)", donor, ok) } // Layer 23 should NOT be shared (it's the last non-shared layer). if _, ok := cfg.KVShareMap[23]; ok { t.Error("layer 23 should not be in KVShareMap (non-shared)") } } func TestLayerTypeDetection(t *testing.T) { cfg := &TextConfig{ LayerTypes: []string{ "sliding_attention", "sliding_attention", "sliding_attention", "sliding_attention", "full_attention", }, } if !isLayerSliding(0, cfg) { t.Error("layer 0 should be sliding") } if !isLayerSliding(3, cfg) { t.Error("layer 3 should be sliding") } if isLayerSliding(4, cfg) { t.Error("layer 4 should be full attention") } } func TestMTPDraftDefaults(t *testing.T) { tests := []struct { name string cfg *TextConfig wantInitial int wantMax int }{ { name: "nil config", wantInitial: 4, wantMax: 16, }, { name: "31b bf16", cfg: &TextConfig{HiddenSize: 5376, NumHiddenLayers: 60}, wantInitial: 14, wantMax: 16, }, { name: "31b quantized", cfg: &TextConfig{HiddenSize: 5376, NumHiddenLayers: 60, QuantBits: 4}, wantInitial: 14, wantMax: 16, }, { name: "26b-a4b moe", cfg: &TextConfig{HiddenSize: 2816, NumHiddenLayers: 30, EnableMoeBlock: true}, wantInitial: 8, wantMax: 16, }, { name: "generic default", cfg: &TextConfig{HiddenSize: 2560, NumHiddenLayers: 42, HiddenSizePerLayer: 256}, wantInitial: 4, wantMax: 16, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := (&Model{TextConfig: tt.cfg}).MTPDraftDefaults(false) if got.InitialDraftTokens != tt.wantInitial || got.MaxDraftTokens != tt.wantMax || !got.Enabled { t.Fatalf("MTPDraftDefaults() = %+v, want initial=%d max=%d enabled=true", got, tt.wantInitial, tt.wantMax) } }) } } func TestNewCachesOmitsSharedKVLayers(t *testing.T) { m := &Model{ Layers: []*DecoderLayer{ {IsSliding: true, KVShareDonor: -1}, {IsSliding: false, KVShareDonor: -1}, {IsSliding: true, KVShareDonor: 0}, {IsSliding: false, KVShareDonor: 1}, }, TextConfig: &TextConfig{SlidingWindow: 512}, } caches := m.NewCaches() if got, want := len(caches), 2; got != want { t.Fatalf("len(NewCaches()) = %d, want %d", got, want) } } func TestNewCachesIncludesAllNonSharedLayers(t *testing.T) { m := &Model{ Layers: []*DecoderLayer{ {IsSliding: true, KVShareDonor: -1}, {IsSliding: false, KVShareDonor: -1}, {IsSliding: true, KVShareDonor: -1}, }, TextConfig: &TextConfig{SlidingWindow: 512}, } caches := m.NewCaches() if got, want := len(caches), len(m.Layers); got != want { t.Fatalf("len(NewCaches()) = %d, want %d", got, want) } } func TestNewCachesAssistantSharedHistoryOrdering(t *testing.T) { cases := []struct { name string totalLayers int slidingBeforeFull int cacheLayers int }{ {name: "31B", totalLayers: 60, slidingBeforeFull: 5, cacheLayers: 60}, {name: "26B-A4B", totalLayers: 30, slidingBeforeFull: 5, cacheLayers: 30}, {name: "E4B", totalLayers: 42, slidingBeforeFull: 5, cacheLayers: 24}, {name: "E2B", totalLayers: 35, slidingBeforeFull: 4, cacheLayers: 15}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { groupSize := tc.slidingBeforeFull + 1 layers := make([]*DecoderLayer, tc.totalLayers) for i := range layers { donor := int32(-1) if i >= tc.cacheLayers { donor = 0 } layers[i] = &DecoderLayer{ IsSliding: i%groupSize < tc.slidingBeforeFull, KVShareDonor: donor, } } m := &Model{ Layers: layers, TextConfig: &TextConfig{SlidingWindow: 512}, } caches := m.NewCaches() if got := len(caches); got != tc.cacheLayers { t.Fatalf("len(NewCaches()) = %d, want %d", got, tc.cacheLayers) } gotSliding := len(caches) - 2 gotFull := len(caches) - 1 if !m.Layers[gotSliding].IsSliding { t.Fatalf("cache %d should be sliding attention", gotSliding) } if m.Layers[gotFull].IsSliding { t.Fatalf("cache %d should be full attention", gotFull) } }) } } func TestResolveWeightPrefix(t *testing.T) { if err := mlx.CheckInit(); err != nil { t.Skipf("MLX not available: %v", err) } tests := []struct { name string key string wantPfx string }{ {"bare", "embed_tokens.weight", ""}, {"language_model", "model.language_model.embed_tokens.weight", "model.language_model."}, {"with_model", "model.embed_tokens.weight", "model."}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dummy := mlx.FromValue(float32(1.0)) mlx.Eval(dummy) tensors := map[string]*mlx.Array{tt.key: dummy} got := resolveWeightPrefix(tensors) if got != tt.wantPfx { t.Errorf("resolveWeightPrefix(%q) = %q, want %q", tt.key, got, tt.wantPfx) } }) } } func skipIfNoMLX(t *testing.T) { t.Helper() if err := mlx.CheckInit(); err != nil { t.Skipf("MLX not available: %v", err) } }