Skip to content

Commit e23a925

Browse files
authored
Support for gemma4 12b dense model (#2060)
1 parent 54a546c commit e23a925

2 files changed

Lines changed: 71 additions & 16 deletions

File tree

docs/guides/transformers.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,19 +227,19 @@ print(tok.convert_tokens_to_string(res[0].sequences[0]))
227227
[Gemma 4](https://ai.google.dev/gemma/docs/gemma4) is Google's next generation of lightweight open-weight models, featuring a hybrid attention architecture with interleaved global and sliding-window attention layers.
228228

229229
```{note}
230-
Only the 31B dense model (`gemma-4-31B`) is currently supported. The MoE variants (E2B, E4B) are not supported.
230+
Only the 31B and 12B dense models are currently supported. The MoE variants (E2B, E4B) are not supported.
231231
```
232232

233-
Gemma 4 models come in two flavors: instruction tuned (it) models and base models.
233+
Gemma 4 models come in two flavors: instruction tuned (it) models and pre-trained models.
234234

235235
Instruction tuned models use the same [prompt template format](https://ai.google.dev/gemma/docs/core/prompt-structure) as Gemma 3.
236236

237237
When converting an instruction-tuned model, CTranslate2 sets `<end_of_turn>` as the default end-of-sequence token.
238238

239-
To convert the 31B instruction-tuned model:
239+
To convert the 12B instruction-tuned model:
240240

241241
```bash
242-
ct2-transformers-converter --model google/gemma-4-31B-it --quantization float16 --output_dir gemma-4-31b-it
242+
ct2-transformers-converter --model google/gemma-4-12B-it --quantization float16 --output_dir gemma-4-12b-it
243243
```
244244

245245
Usage sample:
@@ -248,8 +248,8 @@ Usage sample:
248248
from transformers import AutoTokenizer
249249
import ctranslate2
250250

251-
tok = AutoTokenizer.from_pretrained("google/gemma-4-31B-it")
252-
gen = ctranslate2.Generator("gemma-4-31b-it", device="auto")
251+
tok = AutoTokenizer.from_pretrained("google/gemma-4-12B-it")
252+
gen = ctranslate2.Generator("gemma-4-12b-it", device="auto")
253253

254254
prompt = "<start_of_turn>user\nGenerate a 200 word text talking about George Orwell.<end_of_turn>\n<start_of_turn>model\n"
255255
tokens = tok.convert_ids_to_tokens(tok.encode(prompt))

python/ctranslate2/converters/transformers.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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")
20632067
class 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

Comments
 (0)