Instructions to use google/gemma-4-12B-it with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use google/gemma-4-12B-it with Transformers:
# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("google/gemma-4-12B-it") model = AutoModelForMultimodalLM.from_pretrained("google/gemma-4-12B-it", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- AMD Developer Cloud
Bug Report: MAJOR issues with parallel generation and left-padding (different response from normal generatiln)
Title: Gemma 4 Unified: left-padding a multimodal prompt by a specific
amount (pad=7 here) corrupts prefill logits — argmax flips to <audio|>;
neighboring pad lengths are fine
System Info
transformersversion: 5.14.1 (output below is from 5.13.1; the two
versions produce bit-identical results for this script, corruption
included)- Platform: Linux-6.17.0-35-generic-x86_64-with-glibc2.39
- Python version: 3.12.13
- Huggingface_hub version: 1.18.0
- Safetensors version: 0.8.0
- Accelerate version: 1.14.0
- Accelerate config: not found
- DeepSpeed version: not installed
- PyTorch version (accelerator?): 2.12.0+cu130 (CUDA)
- Using distributed or parallel set-up in script?: No
- Using GPU in script?: Yes (single GPU,
device_map="auto") - GPU type: NVIDIA RTX PRO 6000 Blackwell Workstation Edition
Who can help?
@zucchini-nlp
Reproduction
With Gemma4UnifiedForConditionalGeneration (google/gemma-4-12B-it, bf16,
SDPA), left-padding a multimodal prompt (282 tokens, one image) by exactly
7 positions corrupts the prefill logits: max next-token logit delta vs the
unpadded forward jumps from ~0.5 (bf16 wobble) to 40, and the argmax
flips from 'The'/'I' to the audio special token <audio|> — on an
image prompt. Every other pad length in the sweep (1–6, 8, 9, 15, 16, 63,
64) stays within wobble, so this is not gradual numerical noise; it is a
discrete failure at a specific offset:
B. padded, batch=1, pad=6 max|dLogit|= 0.5000 mean=0.13692 argmax_flipped=False
B. padded, batch=1, pad=7 max|dLogit|= 40.1875 mean=11.57746 argmax_flipped=True
solo top5: 'I':27.25 'The':27.25 'You':22.62 'There':21.12 'In':21.00
this top5: '<audio|>':25.25 'a':11.50 '-':9.69 'I':8.94 'T':8.81
B. padded, batch=1, pad=8 max|dLogit|= 0.5312 mean=0.12108 argmax_flipped=False
Notes:
- Batch size is irrelevant: batch=1 with pad=7 corrupts identically to a
batch of 2 where the shorter row is padded by 7 (the standard left-pad
collation for variable-length batchedgenerate). The unpadded row of
that batch stays clean (max delta 0.55). - Not a collation artifact: every sequence-aligned tensor (
input_ids,attention_mask, token type ids) is padded in lockstep, the padded row's
suffix is asserted byte-identical to its solo encoding, andposition_idsfollow the attention mask. - Text-only prompts are unaffected at every pad length tried (max delta
~0.5–0.66); the image input is required. - Reproduces bit-identically on transformers 5.13.1 and 5.14.1.
- Ambiguity we did not resolve: for this prompt, pad=7 is also the pad that
makes the total padded length 289 — we have not varied the prompt to
distinguish "pad length 7" from "total length 289" as the trigger. The
script below makes that a two-minute experiment. - User-visible symptom: decoding strips the special token, so greedy
generateon a padded-by-7 row silently returns degraded caption-style
text instead of an instruction-following reply — it looks like sampling
variance, not a broken forward:
greedy solo : 'The image contains red, yellow, green, and blue squares on a light gray background.'
greedy batched: 'a yellow rectangle, a red rectangle, a green rectangle, and a blue rectangle.'
Standalone script (no external assets; draws its own image, sweeps pad
lengths at batch=1, then runs the batch-of-2 and greedy comparisons; exits
1 when the bug fires):
from __future__ import annotations
import argparse
import sys
import tempfile
from pathlib import Path
import torch
from PIL import Image, ImageDraw
from transformers import AutoModelForMultimodalLM, AutoProcessor
SHORT_Q = "In one short sentence, what colors do you see?"
LONG_Q = ("Answer briefly: is the grid mostly empty? Explain in one "
"sentence why you think so.")
def make_image(path: Path, size: int = 96) -> Path:
"""Deterministic grid of colored squares on light gray."""
img = Image.new("RGB", (size, size), (235, 235, 235))
draw = ImageDraw.Draw(img)
cell = size // 6
colors = [(200, 40, 40), (40, 160, 40), (220, 180, 30), (60, 60, 200)]
for i, (gx, gy) in enumerate([(0, 1), (2, 3), (3, 0), (4, 4), (1, 5),
(5, 2), (2, 0), (0, 4)]):
draw.rectangle(
[gx * cell, gy * cell, (gx + 1) * cell - 1, (gy + 1) * cell - 1],
fill=colors[i % 4],
)
img.save(path)
return path
def encode(processor, image_path: Path, text: str) -> dict:
messages = [{"role": "user", "content": [
{"type": "image", "url": str(image_path)},
{"type": "text", "text": text},
]}]
return processor.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors="pt",
)
def left_pad(enc: dict, pad_len: int, pad_token_id: int) -> dict:
"""Left-pad EVERY sequence-aligned tensor in lockstep with input_ids."""
seq_len = enc["input_ids"].shape[1]
out = {}
for k, v in enc.items():
if not isinstance(v, torch.Tensor):
continue
if (not v.dtype.is_floating_point and v.dim() == 2
and v.shape[1] == seq_len):
fill = pad_token_id if k == "input_ids" else 0
out[k] = torch.cat([v.new_full((1, pad_len), fill), v], dim=1)
else:
out[k] = v
# The padded row's suffix must be byte-identical to the solo encoding.
assert torch.equal(out["input_ids"][0, pad_len:], enc["input_ids"][0])
assert out["attention_mask"][0, :pad_len].sum() == 0
return out
def last_logits(model, enc: dict) -> torch.Tensor:
device = next(model.parameters()).device
dtype = next(model.parameters()).dtype
inputs = {}
for k, v in enc.items():
if isinstance(v, torch.Tensor):
v = v.to(device)
inputs[k] = v.to(dtype) if v.dtype.is_floating_point else v
# Same position_ids convention generate() uses for left-padded prefill.
mask = inputs["attention_mask"]
inputs["position_ids"] = (mask.long().cumsum(-1) - 1).clamp(min=0)
with torch.inference_mode():
return model(**inputs).logits[:, -1].float().cpu()
def top5(tok, logits: torch.Tensor) -> str:
vals, ids = logits.topk(5)
return " ".join(f"{tok.decode([int(i)])!r}:{float(v):.2f}"
for v, i in zip(vals, ids))
def report(tok, label: str, solo: torch.Tensor, other: torch.Tensor) -> bool:
delta = (other - solo).abs()
flipped = int(other.argmax()) != int(solo.argmax())
print(f"{label:<28s} max|dLogit|={float(delta.max()):8.4f} "
f"mean={float(delta.mean()):.5f} argmax_flipped={flipped}")
if flipped:
print(f" solo top5: {top5(tok, solo)}")
print(f" this top5: {top5(tok, other)}")
return flipped
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="google/gemma-4-12B-it")
args = ap.parse_args()
import transformers
print(f"transformers=={transformers.__version__} "
f"torch=={torch.__version__}")
processor = AutoProcessor.from_pretrained(args.model, padding_side="left")
model = AutoModelForMultimodalLM.from_pretrained(
args.model, dtype="auto", attn_implementation="sdpa",
device_map="auto",
).eval()
tok = getattr(processor, "tokenizer", processor)
pad_id = tok.pad_token_id
with tempfile.TemporaryDirectory() as tmp:
img = make_image(Path(tmp) / "grid.png")
enc_s = encode(processor, img, SHORT_Q)
enc_l = encode(processor, img, LONG_Q)
len_s, len_l = enc_s["input_ids"].shape[1], enc_l["input_ids"].shape[1]
assert len_l > len_s, (len_s, len_l)
print(f"prompt lengths: short={len_s} long={len_l} tokens")
solo_s = last_logits(model, enc_s)[0]
solo_l = last_logits(model, enc_l)[0]
padded_s = left_pad(enc_s, len_l - len_s, pad_id)
print(f"\nsolo short top5: {top5(tok, solo_s)}")
# B: same row, alone in the batch, over a sweep of pad lengths --
# the corruption tracks the pad length (alignment), not batch size.
corrupted = False
for pad_len in (1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 63, 64):
corrupted |= report(
tok, f"B. padded, batch=1, pad={pad_len}",
solo_s, last_logits(model, left_pad(enc_s, pad_len, pad_id))[0],
)
# C: same padded row, batch of 2 with the (unpadded) long prompt.
stacked = {k: torch.cat([padded_s[k], enc_l[k]], dim=0)
for k in padded_s if isinstance(padded_s[k], torch.Tensor)}
both = last_logits(model, stacked)
corrupted |= report(tok, "C. padded, batch=2", solo_s, both[0])
report(tok, " unpadded control row", solo_l, both[1])
# User-visible symptom: greedy generate, solo vs the same batch of 2.
gen_kwargs = dict(max_new_tokens=40, do_sample=False)
device = next(model.parameters()).device
def to_dev(d):
return {k: (v.to(device) if isinstance(v, torch.Tensor) else v)
for k, v in d.items()}
with torch.inference_mode():
g_solo = model.generate(**to_dev(enc_s), **gen_kwargs)
g_batch = model.generate(**to_dev(stacked), **gen_kwargs)
solo_text = processor.decode(g_solo[0][len_s:], skip_special_tokens=True)
batch_text = processor.decode(g_batch[0][len_l:], skip_special_tokens=True)
print(f"\ngreedy solo : {solo_text.strip()!r}")
print(f"greedy batched: {batch_text.strip()!r}")
print("\nExpected: every condition within max|dLogit| <~ 1 of solo "
"(bf16 wobble). BUG: a padded condition shows max|dLogit| in "
"the tens with the argmax flipped to a modality special token, "
"and the batched greedy text diverges from solo.")
return 1 if corrupted else 0
if __name__ == "__main__":
sys.exit(main())
Full output (transformers 5.13.1; 5.14.1 output is bit-identical):
transformers==5.13.1 torch==2.12.0+cu130
prompt lengths: short=282 long=289 tokens
solo short top5: 'I':27.25 'The':27.25 'You':22.62 'There':21.12 'In':21.00
B. padded, batch=1, pad=1 max|dLogit|= 0.5156 mean=0.13077 argmax_flipped=False
B. padded, batch=1, pad=2 max|dLogit|= 0.5625 mean=0.12874 argmax_flipped=False
B. padded, batch=1, pad=3 max|dLogit|= 0.5820 mean=0.14670 argmax_flipped=False
B. padded, batch=1, pad=4 max|dLogit|= 0.5469 mean=0.12501 argmax_flipped=False
B. padded, batch=1, pad=5 max|dLogit|= 0.4453 mean=0.10723 argmax_flipped=False
B. padded, batch=1, pad=6 max|dLogit|= 0.5000 mean=0.13692 argmax_flipped=False
B. padded, batch=1, pad=7 max|dLogit|= 40.1875 mean=11.57746 argmax_flipped=True
solo top5: 'I':27.25 'The':27.25 'You':22.62 'There':21.12 'In':21.00
this top5: '<audio|>':25.25 'a':11.50 '-':9.69 'I':8.94 'T':8.81
B. padded, batch=1, pad=8 max|dLogit|= 0.5312 mean=0.12108 argmax_flipped=False
B. padded, batch=1, pad=9 max|dLogit|= 0.6875 mean=0.07407 argmax_flipped=False
B. padded, batch=1, pad=15 max|dLogit|= 0.7031 mean=0.12159 argmax_flipped=False
B. padded, batch=1, pad=16 max|dLogit|= 0.4648 mean=0.08500 argmax_flipped=False
B. padded, batch=1, pad=63 max|dLogit|= 0.5000 mean=0.08344 argmax_flipped=False
B. padded, batch=1, pad=64 max|dLogit|= 0.4062 mean=0.07283 argmax_flipped=False
C. padded, batch=2 max|dLogit|= 40.1875 mean=11.60843 argmax_flipped=True
solo top5: 'I':27.25 'The':27.25 'You':22.62 'There':21.12 'In':21.00
this top5: '<audio|>':25.25 'a':11.25 '-':9.38 'I':9.00 'T':8.88
unpadded control row max|dLogit|= 0.5469 mean=0.16452 argmax_flipped=False
greedy solo : 'The image contains red, yellow, green, and blue squares on a light gray background.'
greedy batched: 'a yellow rectangle, a red rectangle, a green rectangle, and a blue rectangle.'
Expected behavior
A left-padded multimodal row should produce (numerically close to) the same
logits as the identical row unpadded, at every pad length — as it already
does at 12 of the 13 pad lengths tried and for text-only inputs at all of
them. A discrete corruption at one specific offset, flipping the argmax to
an audio token on an image prompt, suggests an alignment/stride
assumption in the padded multimodal path (vision-block attention mask or
image-feature placement). Practical impact: standard left-padded batchedgenerate over variable-length multimodal prompts silently degrades
whichever rows happen to land on a bad pad offset.
UPDATE: I've narrowed down the problem. It only occurs when the total padded length == 1 mod 32. So "7" is not the magic number, "1 mod 32" is the magic condition.
Hi @atbolsh ,
Thanks for reporting the issue and providing the details. We are able to reproduce the issue and have escalated this issue to our internal team for further investigation.