Research · local models · Gemma 4 settings

Thinking Off, Temperature Zero

What Gemma 4's Settings Actually Do on One RTX 3090

Abstract

A question about the newest Gemma turned into a day of measuring what Gemma 4's serving settings really do. The newest checkpoints that fit a 24 GB card were already installed; the 31B does not fit one RTX 3090 on vLLM at all. What was new was Google's multi-token-prediction drafter, which took the 12B from 78 to about 135 tokens per second at the same scores, once two things were learned: the drafter must come from the same QAT pipeline as the target (the QAT drafter accepts 3.1 tokens per step, the plain one 2.6), and vLLM 0.28.0 only boots it with piecewise CUDA graphs. Then the settings. Rendering the prompt through the server showed that reasoning_effort in the chat-template arguments is a no-op for Gemma 4, so every "thinking" number the suite had recorded for this model was thinking-off. Turned on properly, thinking spent 4,000 to 8,000 tokens on a small parser task and, at temperature 0.2, degenerated into a literal loop on five of five samples. A per-request thinking_token_budget that vLLM implements but Google does not document bounds it; of three budgets tried, 512 cut thoughts short and 1,024 did not. Across seven sampling and thinking variants the only setting with every sample at 8/8 was thinking off at temperature 0, and its five replies were byte-identical. A second bank of 41 prompts across five task types (code with hidden tests, exact-answer math, JSON, tool calls, document extraction) under four recipes then showed the limit of that result: greedy decoding was flawless on 40 prompts and, on four re-runs, wrong the same way on one; Google's default temperature never scored zero on a task across its two samples but slipped on partial cases; budgeted thinking was perfect on math and lowest on code by one partial miss. Greedy is also repeatable only for one request at a time on a warm container: the first reply after every fresh boot differed, and four identical requests sent in parallel returned three different replies. The recipe that shipped is greedy, with a rule: retry a failed check at temperature 1.0, not at temperature 0 again.

Doug (freePerro) with Claude · 2 September 2026 · research note — every number is traceable to a run record under logs/ in the llm-stack repository; the previous note in this series is [A known state for every local model](https://squirrelglitch.com/research/known-state/).

1. Introduction

The starting question was operational: which Gemma is newest, and can it run on the single RTX 3090 that serves local models to an agent pipeline through a swapping proxy. The answer to the first half was quick. Gemma 4 (March 2026) is still the family; its last releases were the 12B Unified model in June, the QAT compressed-tensors checkpoints a day later, DiffusionGemma, and the MTP "assistant" drafters, one of which was refreshed on 20 August. Nothing newer exists under Google's Hugging Face organisation as of 2 September. Two of those were already installed. The one that was not, the 31B, cannot be served on this card by vLLM: Google's own QAT W4A16 checkpoint is 23.3 GB on disk because the embeddings and vision tower stay in bf16, the community 4-bit builds are 19 to 21 GB, and the reference RTX 3090 community repository records a single-card boot OOM on 24 GB Ampere regardless of KV-cache format [8].

So the "newest Gemma" for this hardware was a drafter for a model already in service. Setting it up (§3) was straightforward once its two traps were found. The second half of the day came from a hint: some of the settings might be different than assumed. They were. §4 to §8 report what the chat template, the serving engine and the model actually do under thinking, budgets and sampling, measured with the known-state tools built the previous day [12] plus two new ones. §9 is the broad test, §10 is what changed in the shipped recipe, and §11 compares all of it with what Google's documentation says.

Contributions:

  1. A working, measured MTP lane for Gemma-4-12B on one 3090 (§3): drafter choice by acceptance length, depth by measurement, and the vLLM 0.28.0 boot fault with its workaround.
  2. Ground truth for the thinking controls (§4), obtained by rendering prompts through the server rather than reading documentation.
  3. The thinking-loop failure and its two fixes (§5, §6): a repetition penalty, and vLLM's undocumented-by-Google per-request thinking budget, with the budget size below which it hurts.
  4. A 41-prompt, five-type hammer (§9) on which greedy decoding's one loss was a whole task, sampled decoding's losses were partial cases, and budgeted thinking was perfect on math and lowest on code.

2. Setup

One RTX 3090 (24 GB, Ampere sm_86, power-limited to 250 W) holds only the inference engine; the desktop is rendered by a second card. llama-swap fronts one docker run per model on 127.0.0.1:11440, swapping containers so that one model is resident at a time. Gemma 4 runs on the stock vllm/vllm-openai:v0.28.0 image with --reasoning-parser gemma4 and --tool-call-parser gemma4, the checkpoint's own chat template, a 65,536-token window and four concurrent sequences.

Target. google/gemma-4-12B-it-qat-w4a16-ct, Google's quantization-aware-trained 4-bit checkpoint in compressed-tensors format (group size 32, symmetric), served with --language-model-only so the vision path is not loaded. Weights occupy 9.07 GiB. The KV pool depends on the boot: 10.47 GiB (267,254 tokens) on the first boot, which compiles, and 12.17 GiB (310,753 tokens) on later boots that reuse the compile cache; the undrafted entry gets 12.98 GiB (331,344 tokens) on a warm boot, the drafter's weights and graphs accounting for the difference at the same 41 KiB per token.

Drafter. google/gemma-4-12B-it-qat-q4_0-unquantized-assistant, the QAT-pipeline version of the 4-layer MTP head that shares the target's embeddings and KV cache [3]. It is stored in bf16; what makes it "QAT" is the pipeline it was trained in.

Tools. bench.py (streamed decode speed on a fixed coding prompt), quality.py (hidden pytest sets), modeltest.py and refine.py (the thirteen-probe known-state suite and its recipe selector [12]), and two written for this note: sweep.py, which runs the bake-off code task under named recipe variants, and hammer.py, which runs a bank of 41 prompts across five task types with a scorer per type.

Sampling vocabulary. "Google default" means no sampling parameters in the request, so the checkpoint's generation_config applies: temperature 1.0, top-p 0.95, top-k 64. "House" means the recipe the suite had carried for this model since the previous sweep: temperature 0.2, top-p 0.95, presence penalty 1.0, thinking off, an 8,192-token output cap and a system prompt that demands one fenced code block. "Greedy" is temperature 0.

3. The drafter

Google ships two MTP drafters for the 12B: the plain gemma-4-12B-it-assistant and a QAT-pipeline variant. The QAT model card says a QAT target "must" be paired with a QAT drafter of the same precision [4]; the reference 3090 repository's profile notes record a lower acceptance length when the plain drafter is paired with a QAT 31B target [8]. Both were measured here, at three draft tokens per step, with bench.py on the same coding prompt, three samples each.

Table 1 — decode speed with and without the drafter (250 W, bench.py, three samples)

entrydrafterdecode tok/smean acceptance lengthdraft acceptance
gemma-4-12b (no drafter)—77–79——
gemma-4-12b-mtpQAT assistant, n=3133–1443.1371 %
droppedplain assistant, n=3114–1182.5853 %

The QAT drafter gives about 1.7× decode; the plain one about 1.5×. Rejection sampling preserves the output distribution [14], not the bytes: at temperature 0 the drafted reply on record differs from the undrafted greedy reply in two of the model's own test lines, both scoring 8/8, though a later sequential greedy request on the drafted entry reproduced the undrafted reply exactly, so the variation is numerical rather than the drafter's (§7). The external eight-test scores on the three bench samples at temperature 0.2 were 0/8, 8/8, 8/8, the same bimodal pattern the frozen baseline had recorded for this checkpoint. One caveat belongs to the comparison: the undrafted entry runs full CUDA graphs and the drafted one piecewise (below), so Table 1 bundles the drafter's gain with whatever the piecewise mode costs, which was not isolated. The full known-state suite passed every probe with the drafter, including tools, the Anthropic-style messages endpoint, a 0.16 s cached second turn on a 6,116-token prompt and a 50 s swap.

Draft depth was swept under greedy decoding, four samples per depth, where the replies after the first request of each boot were byte-identical across depths and only the speed changed: end-to-end 117 tok/s at n=2 (acceptance 2.57), 126 at n=3 (3.1–3.2), 128 at n=4 (3.66). n=3 stayed.

The boot fault. With the drafter, vLLM 0.28.0 crashed during CUDA-graph capture of the speculator: Cannot copy between CPU and CUDA tensors during CUDA graph capture unless the CPU tensor is pinned. The piecewise captures had succeeded; the failure came in the speculator's full-graph capture (the log labels the step "prefill CUDA graphs (FULL)"), inside the drafter's compute_logits, which masks the checkpoint's two suppress_tokens (the <image|> and <audio|> end markers) by indexing the logits with a Python list, an unpinned host-to-device copy during capture. The target model applies the same mask with a cached device index, and its logits step runs outside graph capture, so only the drafter trips. Restricting the engine to piecewise graphs with --compilation-config '{"cudagraph_mode":"PIECEWISE"}' avoids the path; every drafted number in this note is measured with that flag. A patch would be a one-line change in gemma4_mtp.py; a flag was preferred to a mounted overlay.

Vision. The same target boots with vision enabled (--limit-mm-per-prompt '{"image":2,"audio":0}' in place of --language-model-only) and the drafter still attached, with no workaround. A 640×320 synthetic test card with two lines of text and two coloured shapes was transcribed exactly and described correctly at 285 prompt tokens. The cost is 10 % of the KV pool (278,589 tokens instead of 310,753 on warm boots) and about 3 % of decode speed (127–141 tok/s against 133–144). The shipped entry stays text-only with the one-line change documented beside it.

4. What the template does with thinking

Gemma 4's thinking is switched on by a <|think|> token at the start of the system turn, which the chat template inserts when enable_thinking is true [1, 2]. The suite's harness had been passing reasoning_effort inside chat_template_kwargs for its "thinking=low/medium/high" variants, the mechanism one other model family honours. Whether Gemma's template honours it can be settled without reading the template: the serving engine's /tokenize endpoint renders the chat template, and /detokenize turns the result back into text.

Table 2 — the rendered prompt for one system + user pair, per chat_template_kwargs

kwargstokensrendered prompt (tail)
{}24<|turn>model\n<|channel>thought\n<channel|>
{"enable_thinking": false}24identical to {}
{"reasoning_effort": "low"}24identical to {}
{"enable_thinking": true}22<|turn>system\n<|think|>\n…<|turn>model\n

Three facts follow. reasoning_effort in the template arguments is a no-op: the prompt is byte-identical to thinking-off, so every "thinking" measurement the suite had recorded for this model was a thinking-off measurement, and the earlier relabelling of its recipe was correct. Thinking is off by default, and when off the template pre-fills an empty thought block <|channel>thought\n<channel|> so the model starts directly on the answer; Google's thinking notebook explains that this was added for the 12B, 26B and 31B "to stabilise model output by suppressing ghost thought channels" [2]. And only enable_thinking: true inserts <|think|>.

There is a second route that does work: vLLM 0.28.0 maps the OpenAI-style top-level reasoning_effort request field to enable_thinking = (effort != "none") in its chat-completion request handling [6]. The levels are not distinguished; low and high render the same prompt. The harness now has an explicit thinking: on mode and passes extra request fields through, so a recipe can say what it means.

5. Thinking, uncapped

With the switch found, the bake-off code task (a garage-door size parser, eight hidden tests, external to anything the model wrote) was run under seven recipe variants, five samples each, at the house 8,192-token output cap.

Table 3 — thinking × sampling on the code task (external tests passed of 8, five samples)

variantmeansamplesmean tokensreasoning tokensfinish
off · house (T 0.2, presence 1.0)6.48, 8, 0, 8, 867505 stop
off · T 0.2, no penalty6.88, 2, 8, 8, 879505 stop
off · Google default (T 1.0)4.80, 8, 0, 8, 881405 stop, one did not parse
off · greedy (T 0)8.08, 8, 8, 8, 852405 stop, byte-identical
on · Google default4.88, 0, 0, 8, 87,2206,7923 stop, 2 length
on · T 0.2, no penalty0.00, 0, 0, 0, 08,1928,1915 length
on · house (T 0.2, presence 1.0)4.88, 8, 0, 0, 86,6526,3203 stop, 2 length
thinking offthinking on
Per-sample external-test scores of the code task under seven thinking and sampling variants 0 1 2 3 4 5 6 7 8 off · house (T 0.2, presence 1.0) off · house (T 0.2, presence 1.0) sample 1: 8/8 (stop) off · house (T 0.2, presence 1.0) sample 2: 8/8 (stop) off · house (T 0.2, presence 1.0) sample 3: 0/8 (stop) off · house (T 0.2, presence 1.0) sample 4: 8/8 (stop) off · house (T 0.2, presence 1.0) sample 5: 8/8 (stop) 675 tok off · T 0.2, no penalty off · T 0.2, no penalty sample 1: 8/8 (stop) off · T 0.2, no penalty sample 2: 2/8 (stop) off · T 0.2, no penalty sample 3: 8/8 (stop) off · T 0.2, no penalty sample 4: 8/8 (stop) off · T 0.2, no penalty sample 5: 8/8 (stop) 795 tok off · Google default (T 1.0) off · Google default (T 1.0) sample 1: 0/8 (stop) off · Google default (T 1.0) sample 2: 8/8 (stop) off · Google default (T 1.0) sample 3: 0/8 (stop) off · Google default (T 1.0) sample 4: 8/8 (stop) off · Google default (T 1.0) sample 5: 8/8 (stop) 814 tok off · greedy (T 0) off · greedy (T 0) sample 1: 8/8 (stop) off · greedy (T 0) sample 2: 8/8 (stop) off · greedy (T 0) sample 3: 8/8 (stop) off · greedy (T 0) sample 4: 8/8 (stop) off · greedy (T 0) sample 5: 8/8 (stop) 524 tok on · Google default on · Google default sample 1: 8/8 (stop) on · Google default sample 2: 0/8 (length) on · Google default sample 3: 0/8 (length) on · Google default sample 4: 8/8 (stop) on · Google default sample 5: 8/8 (stop) 7,220 tok on · T 0.2, no penalty on · T 0.2, no penalty sample 1: 0/8 (length) on · T 0.2, no penalty sample 2: 0/8 (length) on · T 0.2, no penalty sample 3: 0/8 (length) on · T 0.2, no penalty sample 4: 0/8 (length) on · T 0.2, no penalty sample 5: 0/8 (length) 8,192 tok on · house (T 0.2, presence 1.0) on · house (T 0.2, presence 1.0) sample 1: 8/8 (stop) on · house (T 0.2, presence 1.0) sample 2: 8/8 (stop) on · house (T 0.2, presence 1.0) sample 3: 0/8 (length) on · house (T 0.2, presence 1.0) sample 4: 0/8 (length) on · house (T 0.2, presence 1.0) sample 5: 8/8 (stop) 6,652 tok
Figure 1. Five samples per variant at their external-test score out of 8, stacked where equal; the black tick is the mean; a dashed ring marks a sample that hit the 8,192-token cap; mean completion tokens at the right. Only greedy, thinking off, has all five dots at 8; thinking on at temperature 0.2 has all five at 0 and all five dashed.

Two findings. First, on this task thinking off with greedy decoding is the only variant with every sample at 8/8, and its five replies were identical to the byte. The bimodality that the previous sweep had attributed to temperature 0.2 is not the presence penalty's doing: removing the penalty leaves one zero-branch in five, and Google's default temperature drew two, differences that five samples cannot separate.

Second, thinking on is unusable at this cap. At temperature 1.0 the model reasoned for 4,129 to 8,191 tokens about a parser and hit the cap twice in five. At temperature 0.2 it hit the cap five times in five with 8,191 reasoning tokens each. The sweep did not keep reasoning traces, so a sixth sample was run with tracing on to see what fills those tokens: not long deliberation but a degenerate loop. The trace below is its tail, 19,556 characters in which 451 lines contain 336 distinct ones and a single line occurs 29 times:

    Let's use `r"['\"]"`? No.
    Let's use `r"['\"]"`? No.
    Let's use `r"['\"]"`? No.
    Let's use `r"['\"]"`? No.
    Let's use `r

This is not specific to this setup. A public issue on the 12B reports "infinite thinking loops" under llama.cpp [9]; a thread on Google's own developer forum reports coding agents that "almost always devolve into looping thoughts", resolved by the reporter with a repetition penalty of 1.08 over a 4,096-token window [10]; neither has a reply from Google. Applied here as vLLM's per-request repetition_penalty: 1.08 (vLLM's penalty has no window and counts prompt tokens as well as generated ones, so the thread's last-n setting does not transfer), the penalty does what the thread says: thinking at temperature 0.2 finished five of five with scores 8, 8, 7, 8, 8 at 2,049 to 3,266 reasoning tokens, and at temperature 1.0 finished five of five at 8, 8, 0, 6, 8. With thinking off the same penalty scored 0, 8, 0, 8, 8 at temperature 0.2, one more zero-branch than without it, which five samples cannot separate from noise.

6. The thinking budget

vLLM 0.28.0 carries a mechanism Google's documentation does not mention: a per-request thinking_token_budget. A ReasoningConfig derived from the reasoning parser's start and end markers (<|channel> and <channel|> for Gemma 4) tracks each request's reasoning section in the sampler and, once the budget is spent, forces the end marker so the model proceeds to its answer [6]. No server flag is needed beyond --reasoning-parser gemma4: vLLM 0.28.0's argument defaults build the reasoning config from the parser alone, the container log carries no "auto-initialization of reasoning token IDs failed" warning, and the requests confirmed the behaviour.

On a trivial prompt (15 × 37, budget 256, temperature 1.0) three recorded samples reasoned for 207, 150 and 233 tokens and answered 555; an earlier unrecorded trio had reasoned for 159, 256 and 249. On the code task:

Table 4 — thinking with a budget on the code task (external tests of 8)

variantsamplesreasoning tokensnotes
on · Google default · budget 1,024 (sweep.py)8, 8, 8 · 8, 6, 8, 8, 81,024 eachall finished; 1,516–1,704 tokens on seven replies, one 6,485
on · Google default · budget 1,024 (refine.py)5, 0, 8, 8, 82,517–3,067 chars (refine records characters)one answer ran to the 8,192 cap
on · Google default · budget 2,0488, 8, 82,048 eachone answer ran to the cap after a correct block
on · Google default · budget 5127, 8, 0, 0, 8512 eachone did not parse, one hit the cap; too short
on · T 0.2, presence 1.0 · budget 1,0248, 0, 61,024 eachlow temperature still worse
on · T 0.2, no penalty · budget 1,0246, 8, 81,024 each
on · T 0.2, repetition 1.08 · budget 1,0248, 0, 0, 8, 82,428–3,067 charstwo of five hit the cap

Over its thirteen samples the 1,024 budget at Google's default sampling averages 7.0 of 8 and finished in twelve; 512 cuts the thought too early. The characteristic failure is visible in the replies: when the forced <channel|> lands mid-thought, the model sometimes keeps deliberating in the answer channel ("Actually, … Let's re-verify"), producing several code fences instead of one, and occasionally a loop that runs to the cap in the answer rather than the thought.

7. Choosing the recipe by measurement

The suite's recipe selector was rerun on the frozen entry with the control recipe, greedy, budgeted thinking, and budgeted thinking with the repetition penalty, five samples each, under its published rule: best mean wins, but a variant within the noise floor of the best displaces it only if it needs at least 25 % fewer tokens [12].

Table 5 — refine.py, code task, five samples per variant

variantmeansamplescompiletokensreasoning
thinking off · greedy8.08, 8, 8, 8, 81.05240
house recipe (control)7.66, 8, 8, 8, 81.06540
thinking on · Google default · budget 1,0245.85, 0, 8, 8, 80.83,2232,599 chars
thinking on · T 0.2 · repetition 1.08 · budget 1,0244.88, 0, 0, 8, 80.84,2422,784 chars

Greedy won outright and was written into the spec. The re-frozen state: code 8/8 on all five at 135 tok/s median, the whole-file edit task 7/7 on all five, every contract probe passing. One caveat was recorded with it, and it grew under review. Greedy is repeatable only for identical requests sent one at a time to a warm container. In three of three cold boots on record the first reply differed (558 tokens twice, 935 once on the vision-enabled boot) before the 524-token reply appeared on every later sample. The failing cents task of §9, re-run four times sequentially on a warm container, gave the byte-identical reply each time. But four identical greedy requests for the code task sent in parallel returned three different replies (524 tokens twice, 548 and 808 once each), and a sequential request immediately after returned a fourth, byte-identical to the undrafted entry's greedy reply from two days earlier. Temperature 0 removes the sampling noise; it does not remove the numerical noise that batch shape and cache state introduce, and a near-tie between two tokens can go either way.

8. What Google's default temperature is for

Google's model card says to use temperature 1.0, top-p 0.95 and top-k 64 "across all use cases" [1]. On the single code task that setting was the worst thinking-off variant. Before treating that as a verdict, the broad test.

9. Forty-one prompts, four recipes

hammer.py runs a bank of 41 prompts in five types, each with an automatic scorer. Thirteen code tasks (a size parser, phone normalisation, money to cents, interval merging, Roman numerals, RFC 4180 fields, slugs, Luhn, run-length coding, word counts, date parsing, bracket matching, hex colours) each carry six to eight hidden pytest cases and a reference solution; a selftest runs every reference against its tests before any model is asked, so a wrong test cannot be blamed on a model. Twelve math word problems have exact numeric answers, scored on the last number in the reply. Six JSON tasks compare expected keys. Six tool-call tasks check the chosen function and its arguments, including one where the right answer is not to call a tool. Four questions are asked over a 365-word service bulletin. The four recipes were greedy (one sample per task, since its output is deterministic), the house recipe, Google's default, and thinking with the 1,024 budget at Google's default, two samples each: 287 requests, fifteen minutes. Thinking-off replies were capped at 4,096 tokens and thinking-on at 8,192; no thinking-off reply exceeded 1,599.

Table 6 — mean score per task type (1.00 = every hidden check passed)

recipecode (13)math (12)JSON (6)tools (6)extract (4)allloopsat captokensreasoning
greedy · thinking off0.921.001.001.001.000.98001950
Google default · thinking off0.980.961.001.001.000.98002080
house (T 0.2, presence 1.0)0.930.961.001.001.000.97102040
thinking on · budget 1,024 · Google default0.911.001.001.001.000.9711938632
Mean score per task and recipe over the 41-prompt hammer greedy T 1.0 house T 0.2 think+1024 c01_door_size c01_door_size · greedy · 1.00 · r1: 8/8 parses=True c01_door_size · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c01_door_size · house T 0.2 · 0.62 · r1: 8/8 parses=True, r2: 2/8 parses=True 0.62 c01_door_size · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c02_e164 c02_e164 · greedy · 1.00 · r1: 8/8 parses=True c02_e164 · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c02_e164 · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c02_e164 · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c03_cents c03_cents · greedy · 0.00 · r1: 0/8 parses=True 0.00 c03_cents · T 1.0 · 0.94 · r1: 7/8 parses=True, r2: 8/8 parses=True 0.94 c03_cents · house T 0.2 · 0.69 · r1: 4/8 parses=True, r2: 7/8 parses=True 0.69 c03_cents · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c04_merge_intervals c04_merge_intervals · greedy · 1.00 · r1: 6/6 parses=True c04_merge_intervals · T 1.0 · 1.00 · r1: 6/6 parses=True, r2: 6/6 parses=True c04_merge_intervals · house T 0.2 · 1.00 · r1: 6/6 parses=True, r2: 6/6 parses=True c04_merge_intervals · think+1024 · 0.50 · r1: 0/1 parses=False, r2: 6/6 parses=True 0.50 c05_roman c05_roman · greedy · 1.00 · r1: 8/8 parses=True c05_roman · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c05_roman · house T 0.2 · 0.81 · r1: 5/8 parses=True, r2: 8/8 parses=True 0.81 c05_roman · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c06_csv_line c06_csv_line · greedy · 1.00 · r1: 7/7 parses=True c06_csv_line · T 1.0 · 1.00 · r1: 7/7 parses=True, r2: 7/7 parses=True c06_csv_line · house T 0.2 · 1.00 · r1: 7/7 parses=True, r2: 7/7 parses=True c06_csv_line · think+1024 · 0.50 · r1: 0/1 parses=False, r2: 7/7 parses=True 0.50 c07_slugify c07_slugify · greedy · 1.00 · r1: 7/7 parses=True c07_slugify · T 1.0 · 1.00 · r1: 7/7 parses=True, r2: 7/7 parses=True c07_slugify · house T 0.2 · 1.00 · r1: 7/7 parses=True, r2: 7/7 parses=True c07_slugify · think+1024 · 1.00 · r1: 7/7 parses=True, r2: 7/7 parses=True c08_luhn c08_luhn · greedy · 1.00 · r1: 8/8 parses=True c08_luhn · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c08_luhn · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c08_luhn · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c09_rle c09_rle · greedy · 1.00 · r1: 8/8 parses=True c09_rle · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c09_rle · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c09_rle · think+1024 · 0.88 · r1: 6/8 parses=True, r2: 8/8 parses=True 0.88 c10_top_words c10_top_words · greedy · 1.00 · r1: 6/6 parses=True c10_top_words · T 1.0 · 1.00 · r1: 6/6 parses=True, r2: 6/6 parses=True c10_top_words · house T 0.2 · 1.00 · r1: 6/6 parses=True, r2: 6/6 parses=True c10_top_words · think+1024 · 1.00 · r1: 6/6 parses=True, r2: 6/6 parses=True c11_parse_date c11_parse_date · greedy · 1.00 · r1: 8/8 parses=True c11_parse_date · T 1.0 · 0.81 · r1: 5/8 parses=True, r2: 8/8 parses=True 0.81 c11_parse_date · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c11_parse_date · think+1024 · 0.94 · r1: 8/8 parses=True, r2: 7/8 parses=True 0.94 c12_balanced c12_balanced · greedy · 1.00 · r1: 8/8 parses=True c12_balanced · T 1.0 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c12_balanced · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c12_balanced · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c13_hex_rgb c13_hex_rgb · greedy · 1.00 · r1: 8/8 parses=True c13_hex_rgb · T 1.0 · 0.94 · r1: 7/8 parses=True, r2: 8/8 parses=True 0.94 c13_hex_rgb · house T 0.2 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True c13_hex_rgb · think+1024 · 1.00 · r1: 8/8 parses=True, r2: 8/8 parses=True m01_panels m01_panels · greedy · 1.00 · r1: got=4.0 exp=4 m01_panels · T 1.0 · 0.50 · r1: got=404.0 exp=4, r2: got=4.0 exp=4 0.50 m01_panels · house T 0.2 · 1.00 · r1: got=4.0 exp=4, r2: got=4.0 exp=4 m01_panels · think+1024 · 1.00 · r1: got=4.0 exp=4, r2: got=4.0 exp=4 m02_labor m02_labor · greedy · 1.00 · r1: got=316.5 exp=316.5 m02_labor · T 1.0 · 1.00 · r1: got=316.5 exp=316.5, r2: got=316.5 exp=316.5 m02_labor · house T 0.2 · 0.50 · r1: got=316.5 exp=316.5, r2: got=544.0 exp=316.5 0.50 m02_labor · think+1024 · 1.00 · r1: got=316.5 exp=316.5, r2: got=316.5 exp=316.5 m03_minutes m03_minutes · greedy · 1.00 · r1: got=205.0 exp=205 m03_minutes · T 1.0 · 1.00 · r1: got=205.0 exp=205, r2: got=205.0 exp=205 m03_minutes · house T 0.2 · 1.00 · r1: got=205.0 exp=205, r2: got=205.0 exp=205 m03_minutes · think+1024 · 1.00 · r1: got=205.0 exp=205, r2: got=205.0 exp=205 m04_multiples m04_multiples · greedy · 1.00 · r1: got=2318.0 exp=2318 m04_multiples · T 1.0 · 1.00 · r1: got=2318.0 exp=2318, r2: got=2318.0 exp=2318 m04_multiples · house T 0.2 · 1.00 · r1: got=2318.0 exp=2318, r2: got=2318.0 exp=2318 m04_multiples · think+1024 · 1.00 · r1: got=2318.0 exp=2318, r2: got=2318.0 exp=2318 m05_workers m05_workers · greedy · 1.00 · r1: got=6.0 exp=6 m05_workers · T 1.0 · 1.00 · r1: got=6.0 exp=6, r2: got=6.0 exp=6 m05_workers · house T 0.2 · 1.00 · r1: got=6.0 exp=6, r2: got=6.0 exp=6 m05_workers · think+1024 · 1.00 · r1: got=6.0 exp=6, r2: got=6.0 exp=6 m06_percent m06_percent · greedy · 1.00 · r1: got=40.8 exp=40.8 m06_percent · T 1.0 · 1.00 · r1: got=40.8 exp=40.8, r2: got=40.8 exp=40.8 m06_percent · house T 0.2 · 1.00 · r1: got=40.8 exp=40.8, r2: got=40.8 exp=40.8 m06_percent · think+1024 · 1.00 · r1: got=40.8 exp=40.8, r2: got=40.8 exp=40.8 m07_rectangle m07_rectangle · greedy · 1.00 · r1: got=152.25 exp=152.25 m07_rectangle · T 1.0 · 1.00 · r1: got=152.25 exp=152.25, r2: got=152.25 exp=152.25 m07_rectangle · house T 0.2 · 1.00 · r1: got=152.25 exp=152.25, r2: got=152.25 exp=152.25 m07_rectangle · think+1024 · 1.00 · r1: got=152.25 exp=152.25, r2: got=152.25 exp=152.25 m08_distinct m08_distinct · greedy · 1.00 · r1: got=4536.0 exp=4536 m08_distinct · T 1.0 · 1.00 · r1: got=4536.0 exp=4536, r2: got=4536.0 exp=4536 m08_distinct · house T 0.2 · 1.00 · r1: got=4536.0 exp=4536, r2: got=4536.0 exp=4536 m08_distinct · think+1024 · 1.00 · r1: got=4536.0 exp=4536, r2: got=4536.0 exp=4536 m09_modpow m09_modpow · greedy · 1.00 · r1: got=576.0 exp=576 m09_modpow · T 1.0 · 1.00 · r1: got=576.0 exp=576, r2: got=576.0 exp=576 m09_modpow · house T 0.2 · 1.00 · r1: got=576.0 exp=576, r2: got=576.0 exp=576 m09_modpow · think+1024 · 1.00 · r1: got=576.0 exp=576, r2: got=576.0 exp=576 m10_lcm m10_lcm · greedy · 1.00 · r1: got=180.0 exp=180 m10_lcm · T 1.0 · 1.00 · r1: got=180.0 exp=180, r2: got=180.0 exp=180 m10_lcm · house T 0.2 · 1.00 · r1: got=180.0 exp=180, r2: got=180.0 exp=180 m10_lcm · think+1024 · 1.00 · r1: got=180.0 exp=180, r2: got=180.0 exp=180 m11_tank m11_tank · greedy · 1.00 · r1: got=18.0 exp=18 m11_tank · T 1.0 · 1.00 · r1: got=18.0 exp=18, r2: got=18.0 exp=18 m11_tank · house T 0.2 · 1.00 · r1: got=18.0 exp=18, r2: got=18.0 exp=18 m11_tank · think+1024 · 1.00 · r1: got=18.0 exp=18, r2: got=18.0 exp=18 m12_zeros m12_zeros · greedy · 1.00 · r1: got=24.0 exp=24 m12_zeros · T 1.0 · 1.00 · r1: got=24.0 exp=24, r2: got=24.0 exp=24 m12_zeros · house T 0.2 · 1.00 · r1: got=24.0 exp=24, r2: got=24.0 exp=24 m12_zeros · think+1024 · 1.00 · r1: got=24.0 exp=24, r2: got=24.0 exp=24 j01_order j01_order · greedy · 1.00 · r1: 6/6 keys j01_order · T 1.0 · 1.00 · r1: 6/6 keys, r2: 6/6 keys j01_order · house T 0.2 · 1.00 · r1: 6/6 keys, r2: 6/6 keys j01_order · think+1024 · 1.00 · r1: 6/6 keys, r2: 6/6 keys j02_sentiment j02_sentiment · greedy · 1.00 · r1: 3/3 keys j02_sentiment · T 1.0 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j02_sentiment · house T 0.2 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j02_sentiment · think+1024 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j03_items j03_items · greedy · 1.00 · r1: 2/2 keys j03_items · T 1.0 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j03_items · house T 0.2 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j03_items · think+1024 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j04_hours j04_hours · greedy · 1.00 · r1: 3/3 keys j04_hours · T 1.0 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j04_hours · house T 0.2 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j04_hours · think+1024 · 1.00 · r1: 3/3 keys, r2: 3/3 keys j05_palindrome j05_palindrome · greedy · 1.00 · r1: 2/2 keys j05_palindrome · T 1.0 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j05_palindrome · house T 0.2 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j05_palindrome · think+1024 · 1.00 · r1: 2/2 keys, r2: 2/2 keys j06_address j06_address · greedy · 1.00 · r1: 4/4 keys j06_address · T 1.0 · 1.00 · r1: 4/4 keys, r2: 4/4 keys j06_address · house T 0.2 · 1.00 · r1: 4/4 keys, r2: 4/4 keys j06_address · think+1024 · 1.00 · r1: 4/4 keys, r2: 4/4 keys t01_order t01_order · greedy · 1.00 · r1: get_order_status {'order_number': '4821'} t01_order · T 1.0 · 1.00 · r1: get_order_status {'order_number': '4821'}, r2: get_order_status {'order_number': '4821'} t01_order · house T 0.2 · 1.00 · r1: get_order_status {'order_number': '4821'}, r2: get_order_status {'order_number': '4821'} t01_order · think+1024 · 1.00 · r1: get_order_status {'order_number': '4821'}, r2: get_order_status {'order_number': '4821'} t02_schedule t02_schedule · greedy · 1.00 · r1: schedule_install {'date': '2026-09-10', 'window': 'morning'} t02_schedule · T 1.0 · 1.00 · r1: schedule_install {'date': '2026-09-10', 'window': 'morning'}, r2: schedule_install {'date': '2026-09-10', 'window': 'morning'} t02_schedule · house T 0.2 · 1.00 · r1: schedule_install {'date': '2026-09-10', 'window': 'morning'}, r2: schedule_install {'date': '2026-09-10', 'window': 'morning'} t02_schedule · think+1024 · 1.00 · r1: schedule_install {'date': '2026-09-10', 'window': 'morning'}, r2: schedule_install {'date': '2026-09-10', 'window': 'morning'} t03_part t03_part · greedy · 1.00 · r1: lookup_part {'part_number': 'TS-225-2'} t03_part · T 1.0 · 1.00 · r1: lookup_part {'part_number': 'TS-225-2'}, r2: lookup_part {'part_number': 'TS-225-2'} t03_part · house T 0.2 · 1.00 · r1: lookup_part {'part_number': 'TS-225-2'}, r2: lookup_part {'part_number': 'TS-225-2'} t03_part · think+1024 · 1.00 · r1: lookup_part {'part_number': 'TS-225-2'}, r2: lookup_part {'part_number': 'TS-225-2'} t04_sms t04_sms · greedy · 1.00 · r1: send_sms {'message': "we're running 20 minutes late", 'to': '555-123-4567'} t04_sms · T 1.0 · 1.00 · r1: send_sms {'message': "we're running 20 minutes late", 'to': '555-123-4567'}, r2: send_sms {'message': "we're running 20 minutes late", 'to': '555-123-4567'} t04_sms · house T 0.2 · 1.00 · r1: send_sms {'message': "we're running 20 minutes late.", 'to': '555-123-4567'}, r2: send_sms {'message': "We're running 20 minutes late.", 'to': '555-123-4567'} t04_sms · think+1024 · 1.00 · r1: send_sms {'message': "we're running 20 minutes late.", 'to': '555-123-4567'}, r2: send_sms {'message': "we're running 20 minutes late", 'to': '555-123-4567'} t05_no_call t05_no_call · greedy · 1.00 · r1: calls=0 content='Boise' t05_no_call · T 1.0 · 1.00 · r1: calls=0 content='Boise', r2: calls=0 content='Boise' t05_no_call · house T 0.2 · 1.00 · r1: calls=0 content='Boise', r2: calls=0 content='Boise' t05_no_call · think+1024 · 1.00 · r1: calls=0 content='Boise', r2: calls=0 content='Boise' t06_convert t06_convert · greedy · 1.00 · r1: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7} t06_convert · T 1.0 · 1.00 · r1: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7}, r2: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7} t06_convert · house T 0.2 · 1.00 · r1: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7}, r2: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7} t06_convert · think+1024 · 1.00 · r1: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7}, r2: convert_units {'from_unit': 'ft', 'to_unit': 'in', 'value': 7} x01_startup_amps x01_startup_amps · greedy · 1.00 · r1: 4.2 x01_startup_amps · T 1.0 · 1.00 · r1: 4.2, r2: 4.2 x01_startup_amps · house T 0.2 · 1.00 · r1: 4.2, r2: 4.2 x01_startup_amps · think+1024 · 1.00 · r1: 4.2, r2: 4.2 x02_belt_warranty x02_belt_warranty · greedy · 1.00 · r1: 3 x02_belt_warranty · T 1.0 · 1.00 · r1: 3, r2: 3 x02_belt_warranty · house T 0.2 · 1.00 · r1: 3, r2: 3 x02_belt_warranty · think+1024 · 1.00 · r1: 3, r2: 3 x03_error_code x03_error_code · greedy · 1.00 · r1: E27 x03_error_code · T 1.0 · 1.00 · r1: E27, r2: E27 x03_error_code · house T 0.2 · 1.00 · r1: E27, r2: E27 x03_error_code · think+1024 · 1.00 · r1: E27, r2: E27 x04_rail_8ft x04_rail_8ft · greedy · 1.00 · r1: RA-86 x04_rail_8ft · T 1.0 · 1.00 · r1: RA-86, r2: RA-86 x04_rail_8ft · house T 0.2 · 1.00 · r1: RA-86, r2: RA-86 x04_rail_8ft · think+1024 · 1.00 · r1: RA-86, r2: RA-86
Figure 2. Every task of the hammer against every recipe; a cell is the mean score of its samples (green = every hidden check passed, amber ≥ 0.5, red below). Rows are grouped by type: code, math, JSON, tools, extraction. Greedy's single red cell is the cents parser; budgeted thinking's two are the forced cut leaving the model deliberating in its answer. Hover a cell for the per-sample detail.

Every recipe was perfect on JSON, tool calls and extraction, including the no-tool case. The differences are in code and math, and the per-task view (Figure 2) shows their shape.

Greedy's whole loss is one task. On the cents parser it wrote a three-group regular expression and unpacked the match into two names, so every input raises. It was perfect on the other twelve code tasks and on all twenty-eight non-code prompts. The hammer ran greedy once per task, so the reply was re-run four times sequentially on a warm container after review: byte-identical each time. That is what greedy decoding's risk looks like: when the single most likely path is wrong, an identical retry returns the same bug. Only a change in conditions, a parallel neighbour or a cold cache (§7), or a different temperature, gives a different draw.

Google's default never had a task mean of zero across its two samples. Its misses were 7/8 on cents and hex colours, 5/8 on dates, and one math reply that showed the correct working, "84 / 21 = 4", and then printed "404" on its final line. All four fell in the first sample of their task; the second samples were 41 of 41, which two samples per task cannot explain.

Budgeted thinking was the only recipe perfect on math and lowest on code, by one partial miss over greedy: on two code tasks the forced cut left the model deliberating in the answer, once with seven code fences and once looping to the cap, and two more replies scored 6/8 and 7/8. It also costs five times the tokens.

The house recipe had the most partial code misses (2/8, 4/8, 5/8, 7/8) and one arithmetic slip, consistent with the bimodality seen at temperature 0.2 throughout.

10. What shipped

The entry gemma-4-12b-mtp is the QAT 12B with the QAT drafter at depth 3, piecewise graphs, the checkpoint's canonical template, both Gemma 4 parsers, 64K context and four sequences. Its recipe is thinking off at temperature 0 with the house system prompt and an 8,192-token cap.

Table 7 — what to use, by need

needsettingsevidence
a single repeatable callthinking off, temperature 040 of 41 prompts perfect; byte-identical on sequential repeats in a warm container, not under parallel requests
a call that can be retried on a failed checkfirst attempt greedy; retry at Google's default (T 1.0, top-p 0.95, top-k 64)greedy fails identically; T 1.0 never lost a task outright
math or genuine reasoningenable_thinking: true, thinking_token_budget: 1024, Google default sampling12/12 math; one of 82 hammer replies and one of 13 sweep samples hit the cap
code with thinking onavoid0.91 on 13 tasks; the forced cut is the failure mode, at five times the tokens
agent loops with toolsthinking off; if on, keep the budget and echo reasoning_content back on tool-call turnstool calls parse cleanly both ways; Google's multi-turn rule (§11)
imagesswap --language-model-only for a 2-image limitexact transcription; 10 % of the KV pool, 5 % of speed

What not to use, because it measured worse: temperature 0.2 with or without penalties; thinking without a budget; a 512 budget; presence or repetition penalties on thinking-off code; the plain non-QAT drafter. Google's default and greedy tied on the hammer, 0.98 to 0.98; greedy is the pick where a repeatable output matters, and Google's default is the equal choice where a retry is possible.

11. What Google's documentation says

For comparison, the official guidance, page by page. Sampling: the model card's only statement is the standardised configuration of temperature 1.0, top-p 0.95, top-k 64 for all use cases [1]; the technical report has no decoding guidance [3]; Google's own MTP notebook runs its examples with do_sample=False, which is greedy [13]. Thinking: enabled by <|think|> at the top of the system turn; when off, the model still emits the tags with an empty thought block [1]; the thinking notebook adds that the empty block was placed in the template for the three larger models to suppress ghost thought channels [2]. No page mentions a budget, effort levels or any length control; the examples set max_new_tokens=1024. Nothing warns about loops or low temperature; the only loop discussion is on Google's community forum and in a public issue, neither with a staff reply [9, 10]. Multi-turn: strip previous thoughts from history before the next user turn, "with the exception of tool call turns where thinking content should be preserved" [1, 2]; the canonical template enforces this if the client echoes the reasoning back on tool-call turns. MTP: the technical report describes the four-layer drafter that cross-attends to the target's KV cache, with no acceptance rates or recommended depth [3]; the assistant model card promises "up to 3×" [15]; the transformers notebook defaults to four draft tokens with an adaptive draft-length schedule [13]; the QAT card's one hard rule, pair a QAT target with a QAT drafter, is the one §3 confirmed [4]. Serving: Google's Cloud Run guide runs Gemma 4 on vLLM with both gemma4 parsers, --generation-config auto, prefix caching and chunked prefill, and enables thinking per request through chat_template_kwargs [11]; the entry here matches all of that. One page is stale: the prompt-structure reference still documents Gemma 3's turn tokens and says the system role is not supported.

12. Threats to validity

One model, one quantization, one card, one serving engine at one version. The code task in §5 to §7 is a single prompt; its bimodality is real but its ranking of recipes did not survive the 41-prompt test unchanged, which is the reason §9 exists. The hammer bank was written in a day and has two samples per sampled recipe; a 0.02 difference in Table 6 is one partial miss. The hidden tests were proven against reference solutions but the prompts are the authors' phrasing, and a model that reads a specification differently is scored as wrong. Greedy decoding was byte-stable within a warm container but produced a different first reply after every one of three cold boots, and the drafted greedy reply differs from the undrafted one; every determinism claim here was measured one request at a time with a warm prefix cache, while the entry serves an agent pipeline at four concurrent sequences, and the one parallel check made (§7) shows greedy replies diverging under exactly that load. The hidden tests were proven against reference solutions, but the prompts are the authors' phrasing: the cents prompt fixes the order of the dollar sign and the minus sign only by example, and both the house and the Google-default partial misses on that task fail exactly the "-$5.00" case, which is a specification miss scored as a model miss. Table 1 compares an undrafted entry on full CUDA graphs with a drafted entry on piecewise graphs; the piecewise cost was not isolated. The KV-pool figures depend on whether the boot compiled. sweep.py and refine.py disagreed on the budgeted-thinking variant (8, 8, 8 and 8, 6, 8, 8, 8 against 5, 0, 8, 8, 8) at temperature 1.0 with nothing else changed, which is the noise floor of five samples, not a bug found. The loop detector counts a repeated line six times; one house-recipe reply it flagged was a long but correct answer that restated a function. All four Google-default misses on the hammer landed in the first sample of their task, which is either chance at one in sixteen or a first-request effect the two-sample design cannot resolve.

13. Reproducibility

Everything is in the llm-stack repository: config/llama-swap.yaml holds the entry with the flags and the reasons in comments; specs/gemma-4-12b-mtp.yaml is the frozen known state with every finding in its quirks list; bin/sweep.py and bin/hammer.py with hammer/tasks.py are the two new tools, and hammer.py --selftest proves the task bank; PROCESS.md §11 is the day's method log; replies, reasoning traces, summaries and the program output of each run (run.log) are under logs/sweep, logs/hammer and logs/mtp-bench; the drafter, vision and boot figures of §3 are in logs/mtp-bench/decode-*.txt, acceptance-*.txt, boot-*.txt and logs/vision-test-result.json, re-measured after review; the greedy re-runs are logs/hammer/.../greedy_off/c03_cents-rerun.json and logs/mtp-bench/greedy-concurrency.json. The rendered prompts in Table 2 and the budget-256 samples are recorded in logs/gemma-template-render.json, from POST /upstream/<model>/tokenize and /detokenize on the proxy. The drafter fault is in vllm/model_executor/models/gemma4_mtp.py at the suppress_tokens mask; the budget mechanism is vllm/v1/sample/thinking_budget_state.py and vllm/config/reasoning.py.

References

  1. Google DeepMind. Gemma 4 model card. https://ai.google.dev/gemma/docs/core/model_card_4
  2. Google. Thinking mode in Gemma; and the thinking notebook in the Gemma cookbook. https://ai.google.dev/gemma/docs/capabilities/thinking
  3. Gemma Team. Gemma 4 Technical Report. arXiv:2607.02770
  4. Google. Gemma 4 QAT checkpoints (compressed-tensors) model card, "Assistant Compatibility". https://huggingface.co/google/gemma-4-12B-it-qat-w4a16-ct
  5. vLLM. Gemma 4 usage guide (recipes). https://docs.vllm.ai/projects/recipes/en/latest/Google/Gemma4.html
  6. vLLM. Reasoning outputs: automatic enable_thinking activation and the thinking token budget. https://docs.vllm.ai/en/latest/features/reasoning_outputs/
  7. vLLM release v0.28.0. https://github.com/vllm-project/vllm/releases/tag/v0.28.0
  8. club-3090 issue #814, official Gemma 4 31B QAT W4A16 on 4× 3090, and the gemma-4-31b README's single-card note. https://github.com/noonghunna/club-3090/issues/814
  9. google-deepmind/gemma issue #727, gemma-4-12B-it infinite thinking loop. https://github.com/google-deepmind/gemma/issues/727
  10. Google AI developer forum, "Gemma4: how to prevent looping output in thinking?" https://discuss.ai.google.dev/t/gemma4-how-to-prevent-looping-output-in-thinking/173205
  11. Google Cloud. Run agents with Gemma 4 models on Cloud Run. https://docs.cloud.google.com/run/docs/run-gemma-on-cloud-run
  12. Doug (freePerro) with Claude. A known state for every local model. https://squirrelglitch.com/research/known-state/
  13. Google. Gemma cookbook: docs/mtp/mtp.ipynb and docs/capabilities/thinking.ipynb. https://github.com/google-gemma/cookbook
  14. Leviathan, Kalman, Matias. Fast Inference from Transformers via Speculative Decoding. ICML 2023. arXiv:2211.17192
  15. Google. Gemma 4 12B assistant (MTP drafter) model card. https://huggingface.co/google/gemma-4-12B-it-assistant