Views
No views yet
google/gemma-4-12B-it,
plus a reproducible recipe for running it with DeepSeek DSpark
speculative decoding (draft head: deepseek-ai/dspark_gemma4_12b_block7)
— on a single 32 GB Blackwell GPU (validated on an RTX 5090).| recipe | speed | context | notes |
|---|---|---|---|
| Fast (torch.compile max-autotune) | ~150 tok/s on code (~2× a plain bf16 12B) | ~32 k | short/medium chat + code |
| Long-context (windowed KV cache) | ~40–55 tok/s | 128 k = 26.6 GB, 256 k = 28.7 GB, in-VRAM | full 256 k on 32 GB |
recipe/)._scaled_mm kernel. On Blackwell (sm_120) _scaled_mm fp8 matmul is ~2.5× a bf16 matmul; the default
KernelPreference.AUTO instead tries a cutlass kernel that doesn't load on sm_120/py3.12 and silently
falls back to a slow dequant path — so KernelPreference.TORCH is essential.1import torch
2from transformers import AutoModelForCausalLM, AutoTokenizer
3
4# Load-and-go: the fp8 quantization_config is baked into config.json — no config needed at load.
5model = AutoModelForCausalLM.from_pretrained(
6 "skibare87/gemma-4-12B-it-FP8-DSpark",
7 dtype=torch.bfloat16, device_map="cuda", attn_implementation="sdpa",
8).eval()
9tok = AutoTokenizer.from_pretrained("skibare87/gemma-4-12B-it-FP8-DSpark")google/gemma-4-12B-it yourself instead of using these weights:1from transformers import AutoModelForCausalLM, TorchAoConfig
2from torchao.quantization import Float8DynamicActivationFloat8WeightConfig, PerRow
3from torchao.quantization.quantize_.common.kernel_preference import KernelPreference
4
5cfg = Float8DynamicActivationFloat8WeightConfig(
6 granularity=PerRow(), kernel_preference=KernelPreference.TORCH, # native _scaled_mm, not AUTO
7)
8model = AutoModelForCausalLM.from_pretrained(
9 "google/gemma-4-12B-it", quantization_config=TorchAoConfig(cfg),
10 dtype=torch.bfloat16, device_map="cuda", attn_implementation="sdpa",
11).eval()
12# model.save_pretrained("gemma-4-12B-it-FP8") # <- produces the checkpoint in this repotorch.compile(mode= "max-autotune-no-cudagraphs", dynamic=True) on the target, which fuses the fp8 activation-quant +
_scaled_mm into proper triton kernels.TORCHINDUCTOR_CACHE_DIR off /tmp, and use torch
2.11's mega-cache (torch.compiler.save_cache_artifacts() / load_cache_artifacts()) so restarts
are a cache hit (~4 min) instead of a recompile. torch._dynamo.config.caching_precompile does not
work with torchao fp8 (it can't serialize Float8Tensor guards).DynamicCache stores the sliding layers full-length, so 256 k KV would be ~90 GB. Windowing the
sliding layers makes 256 k KV ~5 GB. The pieces (patches in recipe/, applied to a
DeepSpec checkout):windowed_cache.py — SpecSlidingLayer: a crop-safe sliding cache (stores window + pad so a
speculative reject never eats into the real window; get_mask_sizes reports the true stored length so
gemma's sliding mask stays aligned). Validated logit-exact vs the full forward past 1024 tokens,
through crop cycles.base_evaluator.patch): prefill long prompts in chunks and keep only the draft's
target hidden-state layers, with a rolling window so target_hidden_states never materializes
full-length. This was the real memory lever (128 k: 38.7 → 26.6 GB).evaluator.patch): the draft only proposes, so its context is windowed
(DSPARK_DRAFT_CTX_WINDOW, default 16384); a cumulative-offset trick keeps absolute positions correct.head_dim=512; flash-attn caps at 256, so
force torch.nn.attention.sdpa_kernel([EFFICIENT_ATTENTION, MATH]) — the math backend uses 32 GB for
one such attention, efficient uses 4.4 GB.DynamicCache(config=...) (and get_head_shapes) do layer_types[:-num_kv_shared_layers]; gemma-4-12B
has num_kv_shared_layers = 0, so [:-0] is an empty list and the sliding-window cache layers are
silently never created (everything becomes full-storage). The workaround (build the cache layers
manually) is in windowed_cache.py; details + a minimal repro in
recipe/transformers-num-kv-shared-layers-bug.md.recipe/server.py is a self-contained OpenAI-compatible /v1/chat/completions
shim wrapping DeepSpec's Gemma4DSparkEvaluator, with env knobs for both recipes
(DSPARK_COMPILE=1 → fast path; DSPARK_DRAFT_CTX_WINDOW / DSPARK_PREFILL_CHUNK → long context).stream_callback added to DSpark's generate loop
pushes accepted tokens as speculation commits them — see the patches) and thinking: gemma4 reasons
in a <|channel>thought … <channel|> channel, which the shim exposes as OpenAI-style reasoning_content
(streamed separately from the answer content; toggle with DSPARK_THINKING=0). Works through a
LiteLLM gateway into Open WebUI.image_url) and the shim runs it through the processor, then passes pixel_values (+ mm_token_type_ids,
image_position_ids) into the DSpark prefill via a prefill_mm param threaded through
generate_decoding_sample. The target embeds the image, the KV cache carries it, and the draft
speculates over image-aware hidden states — vision on the same speculative loop as text (accept-len
~3.2, ~32 tok/s), and it reasons about the image when thinking is on.-it (instruct) variant. The base pattern-completes and never stops; -it ships its chat
template and eos_token_id: [1, 106, 50]. gemma-4 uses a <|channel|>/<|think|> (harmony-style)
format, not <start_of_turn>.attn_implementation="sdpa" (not flash — head_dim 512).deepseek-ai/dspark_gemma4_12b_block7,
DeepSpec.num_kv_shared_layers interactions.