Instructions to use google/gemma-4-26B-A4B-it with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use google/gemma-4-26B-A4B-it with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="google/gemma-4-26B-A4B-it") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("google/gemma-4-26B-A4B-it") model = AutoModelForMultimodalLM.from_pretrained("google/gemma-4-26B-A4B-it", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Inference
- HuggingChat
- Notebooks
- Google Colab
- Kaggle
- AMD Developer Cloud
- Local Apps Settings
- vLLM
How to use google/gemma-4-26B-A4B-it with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "google/gemma-4-26B-A4B-it" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "google/gemma-4-26B-A4B-it", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/google/gemma-4-26B-A4B-it
- SGLang
How to use google/gemma-4-26B-A4B-it with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "google/gemma-4-26B-A4B-it" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "google/gemma-4-26B-A4B-it", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "google/gemma-4-26B-A4B-it" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "google/gemma-4-26B-A4B-it", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use google/gemma-4-26B-A4B-it with Docker Model Runner:
docker model run hf.co/google/gemma-4-26B-A4B-it
fix(chat_template): emit tool_response multimodal placeholders inside the block
Problem
Multimodal placeholders in tool messages are emitted after format_tool_response_block() has already closed the block with <tool_response|>:
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}} {# text goes inside #}
{%- for part in tool_body -%}
{%- if part.get('type') in ['image', 'image_url'] -%}
{{- '<|image|>' -}} {# image lands outside #}
When the tool message is the final message, the rendered prompt therefore ends with a bare multimodal token:
... <|"|>}<tool_response|><|image|>
In the tested setup, generation from this prompt shape is unstable. The observed failure rate depends on the prompt and image: the minimal reproduction below fails in 49/60 runs with a dark checkerboard and 24/60 with a light checkerboard. In a longer application prompt with a realistic tool response, I observe a failure rate of approximately 30%.
Moving the placeholder inside the tool-response block eliminated all observed failures in these tests.
Fix
Move content-type dispatch into format_tool_response_block() and emit multimodal placeholders before the closing tag:
before <|tool_response>response:search{value:<|"|>text<|"|>}<tool_response|><|image|>
after <|tool_response>response:search{value:<|"|>text<|"|>}<|image|><tool_response|>
This preserves the template’s current text-first ordering. The same tokens are emitted; only the position of <|image|> relative to <tool_response|> changes.
No turn-level logic changes. Content normalization and placeholder emission are instead centralized in format_tool_response_block(), reducing the call site to a single invocation.
Emitting placeholders before the text or interleaving them in content-part order also produced zero failures. Appending them after the text was chosen because it is closest to the current rendered output and therefore minimizes the behavioral change.
Reproduction
The reproduction is self-contained, generates a synthetic image, and requires only Pillow. It pins pick_category using tool_choice.
For this reproduction, a run is considered successful only when the pinned tool call is emitted with no additional text. A run is counted as incorrect if the tool call is missing or any extra text is generated.
vllm serve google/gemma-4-26B-A4B-it \
--enable-auto-tool-choice --tool-call-parser gemma4 --reasoning-parser gemma4 \
--trust-request-chat-template
python repro_gemma4_tool_image.py --model google/gemma-4-26B-A4B-it -n 60
python repro_gemma4_tool_image.py --model google/gemma-4-26B-A4B-it -n 60 --invert
python repro_gemma4_tool_image.py --model google/gemma-4-26B-A4B-it -n 60 --no-tool-image
python repro_gemma4_tool_image.py --model google/gemma-4-26B-A4B-it -n 60 --chat-template patched.jinja
Results on vLLM 0.26.0, temperature 1.0, n=60 per cell:
| Condition | Current template | This PR |
|---|---|---|
tool-message image, dark board (--invert) |
49/60 (81.7%) | 0/60 |
| tool-message image, light board | 24/60 (40.0%) | 0/60 |
| no tool-message image (control) | 0/60 | — |
Also reproduced on vLLM 0.20.2rc1 (58/60 with the dark board), so this is not tied to a particular vLLM version.
Representative failures — degenerate loops, or the pinned tool call followed by garbage:
- de de de de de de de de de de de de de de de de de de ...
-undercut-undercut-undercut-undercut-undercut-undercut- ...
- nothing is being searched for. nothing is being searched for. ...
repro_gemma4_tool_image.py
#!/usr/bin/env python3
"""Reproduce: a prompt ending in a bare multimodal token destabilises gemma-4.
The chat template emits multimodal placeholders from `tool` messages *after*
`format_tool_response_block()` has closed the block with `<tool_response|>`.
When the `tool` message is the last one, the prompt ends with a bare `<|image|>`.
This script pins a tool call with `tool_choice`, so the only correct output is
that tool call with no prose. It reports how often the model does something else.
vllm serve google/gemma-4-26B-A4B-it \
--enable-auto-tool-choice --tool-call-parser gemma4 --reasoning-parser gemma4 --trust-request-chat-template
python repro_gemma4_tool_image.py --base http://localhost:8000/v1 \
--model google/gemma-4-26B-A4B-it -n 100
# and with a patched template:
python repro_gemma4_tool_image.py ... --chat-template patched.jinja
Requires Pillow.
"""
import argparse
import base64
import io
import json
import urllib.error
import urllib.request
from collections import Counter
from PIL import Image, ImageDraw
def make_image_data_url(size=896, cells=8, invert=False):
"""A plain checkerboard.
Any image reproduces the problem, but the *rate* depends on the image:
a dark-background board fails ~82% here, a light-background one ~40%.
Use --invert to switch.
"""
bg, fg = ((40, 40, 40), (235, 235, 235)) if invert else ((235, 235, 235), (40, 40, 40))
img = Image.new("RGB", (size, size), bg)
draw = ImageDraw.Draw(img)
step = size // cells
for row in range(cells):
for col in range(cells):
if (row + col) % 2:
draw.rectangle(
[col * step, row * step, (col + 1) * step - 1, (row + 1) * step - 1],
fill=fg)
buf = io.BytesIO()
img.save(buf, format="PNG")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
TOOL = {
"type": "function",
"function": {
"name": "pick_category",
"description": "Pick the category that best matches the request.",
"parameters": {
"type": "object",
"properties": {
"reason": {"type": "string", "description": "Why this category."},
"category": {"type": "string",
"enum": ["food", "place", "product", "plant", "other"]},
},
"required": ["reason", "category"],
},
},
}
def build_messages(data_url, with_image):
"""[system, user(image), assistant(tool_call), tool(text [+ image])]"""
tool_content = [{"type": "text", "text":
"# search results\n\n[1] first result\n[2] second result\n[3] third result"}]
if with_image:
tool_content.append({"type": "image_url", "image_url": {"url": data_url}})
return [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": [{"type": "image_url", "image_url": {"url": data_url}}]},
{"role": "assistant", "tool_calls": [{
"id": "call_1", "type": "function",
"function": {"name": "search", "arguments": json.dumps({"q": "x"})}}]},
{"role": "tool", "tool_call_id": "call_1", "name": "search", "content": tool_content},
]
def stream_once(url, body, api_key):
"""SSE request; returns (content, tool_call_seen) or raises."""
req = urllib.request.Request(
url, data=json.dumps({**body, "stream": True}).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + api_key})
content, called = [], False
with urllib.request.urlopen(req, timeout=300) as r:
for raw in r:
line = raw.decode("utf-8", "replace").strip()
if not line.startswith("data:"):
continue
payload = line[5:].strip()
if payload == "[DONE]":
break
try:
chunk = json.loads(payload)
except json.JSONDecodeError:
continue
for ch in chunk.get("choices") or []:
delta = ch.get("delta") or {}
if delta.get("content"):
content.append(delta["content"])
for tc in delta.get("tool_calls") or []:
if (tc.get("function") or {}).get("name") == "pick_category":
called = True
return "".join(content).strip(), called
def classify(text, called):
"""Correct = the pinned tool call, and nothing else."""
if called:
return "ok" if not text else "tool_call + text"
return "empty" if not text else "text, no tool call"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="http://localhost:8000/v1")
ap.add_argument("--model", required=True)
ap.add_argument("--api-key", default="EMPTY")
ap.add_argument("-n", type=int, default=100)
ap.add_argument("--temperature", type=float, default=1.0)
ap.add_argument("--chat-template", help="path to a .jinja file (needs --trust-request-chat-template)")
ap.add_argument("--no-tool-image", action="store_true", help="control: omit the tool-message image")
ap.add_argument("--invert", action="store_true", help="dark-background checkerboard (higher failure rate)")
args = ap.parse_args()
data_url = make_image_data_url(invert=args.invert)
body = {
"model": args.model,
"messages": build_messages(data_url, not args.no_tool_image),
"tools": [TOOL],
"tool_choice": {"type": "function", "function": {"name": "pick_category"}},
"temperature": args.temperature,
"max_completion_tokens": 512,
"stream_options": {"include_usage": True},
}
if args.chat_template:
with open(args.chat_template, encoding="utf-8") as f:
body["chat_template"] = f.read()
url = args.base.rstrip("/") + "/chat/completions"
counts, samples = Counter(), []
for i in range(args.n):
try:
text, called = stream_once(url, body, args.api_key)
kind = classify(text, called)
except urllib.error.HTTPError as e:
kind, text = f"HTTP {e.code}", e.read().decode()[:160]
counts[kind] += 1
if kind != "ok" and len(samples) < 5:
samples.append((kind, text[:160]))
print(f"\r {i + 1}/{args.n} ok={counts['ok']}", end="", flush=True)
n = sum(counts.values())
bad = n - counts["ok"]
print(f"\n\n tool image: {'omitted' if args.no_tool_image else ('dark' if args.invert else 'light')}"
f" template: {args.chat_template or 'server default'}")
print(f" incorrect: {bad}/{n} ({100 * bad / n:.1f}%)")
for k, c in counts.most_common():
if k != "ok":
print(f" {k:20s} {c:4d} ({100 * c / n:.1f}%)")
for kind, text in samples:
print(f" [{kind}] {text!r}")
main()
Also fixes a crash
In Jinja a dict is also is sequence. The current call site tests tool_body is string and then is sequence, so a tool message whose content is a dict falls into the content-parts branch; iterating the dict yields its keys (strings) and part.get() raises UndefinedError: 'str object' has no attribute 'get'. The flat dispatch tests is mapping first, so this is handled.
tool content |
current | this PR |
|---|---|---|
| string | same | same |
| mapping | render error | renders correctly |
| parts (text + image) | image outside the block | image inside the block |
| parts (text only) | same | same |
Rendering was diffed against the current template across those four shapes and 15 real prompts; output is identical except for the two intended changes.