@@ -1886,9 +1886,11 @@ def get_model_spec(self, model):
18861886 )
18871887 if sliding_window_pattern is not None :
18881888 layer_types = [
1889- "full_attention"
1890- if (i + 1 ) % sliding_window_pattern == 0
1891- else "sliding_attention"
1889+ (
1890+ "full_attention"
1891+ if (i + 1 ) % sliding_window_pattern == 0
1892+ else "sliding_attention"
1893+ )
18921894 for i in range (num_layers )
18931895 ]
18941896
@@ -2058,6 +2060,8 @@ def set_decoder(self, spec, module, quant_type=common_spec.Quantization.CT2):
20582060 gc .collect ()
20592061
20602062
2063+ @register_loader ("Gemma4UnifiedTextConfig" )
2064+ @register_loader ("Gemma4UnifiedConfig" )
20612065@register_loader ("Gemma4TextConfig" )
20622066@register_loader ("Gemma4Config" )
20632067class Gemma4Loader (ModelLoader ):
@@ -2068,6 +2072,8 @@ def architecture_name(self):
20682072 def get_model_class (self , config , default_class ):
20692073 if config .__class__ .__name__ == "Gemma4Config" :
20702074 return transformers .Gemma4ForConditionalGeneration
2075+ if config .__class__ .__name__ == "Gemma4UnifiedConfig" :
2076+ return transformers .Gemma4UnifiedForConditionalGeneration
20712077 return default_class
20722078
20732079 def get_model_spec (self , model ):
@@ -2109,18 +2115,28 @@ def get_model_spec(self, model):
21092115 rope_local_base_freq = float (sliding_rope .get ("rope_theta" , 10_000 ))
21102116 rope_theta = float (global_rope .get ("rope_theta" , 1_000_000 ))
21112117
2112- # Proportional RoPE: only a fraction of global_head_dim uses RoPE
2118+ # Proportional RoPE (HF `rope_type="proportional"`, currently Gemma4-only):
2119+ # halves on full head_dim and zero-pads trailing freqs, unlike GPT-NeoX-style
2120+ # partial RoPE which halves on rotary_dim. HF: `1 / rope_theta^(2i/head_dim)`;
2121+ # CT2's RotaryEmbeddings: `1 / base^(2i/rotary_dim)`. Rescale base to match.
21132122 global_partial_factor = float (global_rope .get ("partial_rotary_factor" , 1.0 ))
21142123 global_rotary_dim = int (global_head_dim * global_partial_factor )
2124+ global_rope_base = (
2125+ rope_theta ** (global_rotary_dim / global_head_dim )
2126+ if 0 < global_rotary_dim < global_head_dim
2127+ else rope_theta
2128+ )
21152129
21162130 sliding_window = getattr (text_config , "sliding_window" , 512 )
21172131 layer_types = getattr (text_config , "layer_types" , None )
21182132 if layer_types is None :
21192133 sliding_window_pattern = 6
21202134 layer_types = [
2121- "sliding_attention"
2122- if bool ((i + 1 ) % sliding_window_pattern )
2123- else "full_attention"
2135+ (
2136+ "sliding_attention"
2137+ if bool ((i + 1 ) % sliding_window_pattern )
2138+ else "full_attention"
2139+ )
21242140 for i in range (num_layers )
21252141 ]
21262142
@@ -2140,7 +2156,7 @@ def get_model_spec(self, model):
21402156 quant_group_size = None
21412157 quant_bits = None
21422158
2143- # Build spec with sliding-attention defaults; global layers overridden per-layer below
2159+ # Build spec with sliding-attention defaults; global layers overridden per-layer below.
21442160 spec = transformer_spec .TransformerDecoderModelSpec .from_config (
21452161 num_layers ,
21462162 num_heads ,
@@ -2166,8 +2182,14 @@ def get_model_spec(self, model):
21662182 v_norm = True ,
21672183 )
21682184
2185+ # Set it to 0 so the decoder processes all tokens at once; per-layer sliding_window
2186+ # set below handles KV-cache trimming for sliding-attention layers.
2187+ spec .decoder .sliding_window = np .dtype ("int32" ).type (0 )
2188+
21692189 self ._layer_types = layer_types
21702190 self ._attention_k_eq_v = attention_k_eq_v
2191+ self ._global_head_dim = global_head_dim
2192+ self ._global_rotary_dim = global_rotary_dim
21712193
21722194 # Per-layer overrides for full-attention layers
21732195 for i , layer_type in enumerate (layer_types ):
@@ -2178,7 +2200,9 @@ def get_model_spec(self, model):
21782200 layer .self_attention .rotary_dim = np .dtype ("int32" ).type (
21792201 global_rotary_dim
21802202 )
2181- layer .self_attention .rotary_base = np .dtype ("float32" ).type (rope_theta )
2203+ layer .self_attention .rotary_base = np .dtype ("float32" ).type (
2204+ global_rope_base
2205+ )
21822206 layer .self_attention .sliding_window = np .dtype ("int32" ).type (0 )
21832207 layer .self_attention .head_dim = np .dtype ("int32" ).type (global_head_dim )
21842208 if num_global_kv_heads is not None :
@@ -2226,7 +2250,10 @@ def set_config(self, config, model, tokenizer):
22262250 and isinstance (tokenizer .chat_template , str )
22272251 and tokenizer .chat_template .strip ()
22282252 ):
2229- config .eos_token = "<end_of_turn>"
2253+ if "<turn|>" in tokenizer .chat_template :
2254+ config .eos_token = "<turn|>"
2255+ else :
2256+ config .eos_token = "<end_of_turn>"
22302257 else :
22312258 config .eos_token = tokenizer .eos_token
22322259
@@ -2241,6 +2268,18 @@ def set_decoder(self, spec, module, quant_type=common_spec.Quantization.CT2):
22412268 self .set_layer_norm (spec .layer_norm , module .norm )
22422269
22432270 attention_k_eq_v = getattr (self , "_attention_k_eq_v" , False )
2271+ ghd = getattr (self , "_global_head_dim" , None )
2272+ grd = getattr (self , "_global_rotary_dim" , None )
2273+ # HF's proportional partial-RoPE pairs channels [0:R/2]↔[HD/2:HD/2+R/2];
2274+ # CT2's RotaryEmbeddings pairs [0:R/2]↔[R/2:R]. Permute Q/K accordingly.
2275+ partial_perm = None
2276+ if ghd and grd and 0 < grd < ghd :
2277+ partial_perm = (
2278+ list (range (0 , grd // 2 ))
2279+ + list (range (ghd // 2 , ghd // 2 + grd // 2 ))
2280+ + list (range (grd // 2 , ghd // 2 ))
2281+ + list (range (ghd // 2 + grd // 2 , ghd ))
2282+ )
22442283
22452284 for layer_spec , layer in zip (spec .layer , module .layers ):
22462285 self .set_layer_norm (layer_spec .input_layer_norm , layer .input_layernorm )
@@ -2294,6 +2333,22 @@ def set_decoder(self, spec, module, quant_type=common_spec.Quantization.CT2):
22942333 layer_spec .self_attention .linear [0 ], split_layers , cc_dim
22952334 )
22962335
2336+ # Apply the partial-RoPE permutation to Q/K rows of the fused QKV (and
2337+ # the norm gammas); V rows are left untouched (V is not RoPE-rotated).
2338+ if is_full_attn and partial_perm is not None :
2339+ fused = layer_spec .self_attention .linear [0 ].weight
2340+ q_rows = split_layers [0 ].weight .shape [0 ]
2341+ k_rows = split_layers [1 ].weight .shape [0 ]
2342+ qk = fused [: q_rows + k_rows ].view (- 1 , ghd , fused .shape [1 ])
2343+ fused [: q_rows + k_rows ] = qk [:, partial_perm , :].reshape (
2344+ q_rows + k_rows , fused .shape [1 ]
2345+ )
2346+ for norm_spec in (
2347+ layer_spec .self_attention .q_norm ,
2348+ layer_spec .self_attention .k_norm ,
2349+ ):
2350+ norm_spec .gamma = norm_spec .gamma [partial_perm ]
2351+
22972352 self .set_linear (
22982353 layer_spec .self_attention .linear [1 ],
22992354 layer .self_attn .o_proj ,
0 commit comments