Repository navigation
feat(gemma4): serve the E2B and E4B releases - #59
mkornreich wants to merge 1 commit into
Conversation
|
Triaged while assembling a merged deployment branch for a 2× RTX 6000 Ada / 2× Xeon Gold 6526Y Linux box serving DeepSeek-V4-Flash with offloaded experts, to benchmark the open PRs together. Gemma-4 E-series. Different model family, not merged or tested here. Flagging only so the absence of a report from me is not read as a problem found — I merged and benchmarked #30, #48, #56, #69, #70, #71 and #81, and left this one out deliberately. |
|
@gdevenyi Are you planning on trying to test support for that model? I made the change since it is the only way to get Gemma-4 fully running on my AMD Radeon 890M GPU and not spill into the CPU |
|
@mkornreich I had a spare H100 box and It generates correctly with CUDA graphs off. ft serve --model <gemma-4-E2B-it snapshot> --served-model-name gemma4-e2b \
--host 127.0.0.1 --port 21941 --cuda-graph-max-bs 0boots to With graphs enabled (the default) it fails during capture, in the per-layer-embedding path:
Separately, the double-wide layer split matches what you implemented. On Tested on: H100 80GB HBM3 (sm_90), driver 580.126.16, Xeon Platinum 8462Y+, Linux 5.15.0-157-generic, nvcc 13.1, torch 2.11.0+cu130, Python 3.12.13, PR branch at 6486907 on top of Thanks for putting this together — happy to re-test if you push a fix for the capture path. |
|
@rakhimovv thanks for the H100 run and the precise trace — that pinpointed it exactly. You're right that capture dies on the host round-trip in the per-layer-embedding path. One thing worth noting: pinning the Pushed
Verified on an 8 GB RTX 5070:
Would appreciate a re-test of the default (no-env) path on the H100. And thanks for independently confirming the double-wide split lines up with the 20 KV-shared layers. |
|
Re-tested Default path, no env, graphs on — captures and serves. Greedy output is byte-identical to the The per-layer inputs are live under replay, not frozen at capture. Since a stale PLE was the failure mode you were guarding against, I sent four unrelated greedy prompts through the captured decode graph rather than relying on the one:
All correct and all prompt-specific, which a capture-time-frozen table could not produce. Opt-in host path also behaves as described. Same environment as before: H100 80GB HBM3 (sm_90), driver 580.126.16, Xeon Platinum 8462Y+, Linux 5.15.0-157-generic, nvcc 13.1, torch 2.11.0+cu130, Python 3.12.13, checkpoint No blockers left from my side on NVIDIA hardware. |
… sharing, double-wide mlp) Alternative to upstream PR FlashML-org#59: rebuilt on today's code; shared layers read the source layer's paged KV instead of keeping a copy, and the PLE table reuses --embed-device cpu instead of disabling CUDA graphs. Co-authored-by: mk <menachemkornreich@gmail.com> Assisted-by: Claude Opus 5.5
|
Hi @mkornreich, still working on this? The branch has been quiet since August and main has What changed: dense Gemma-4 GGUF landed in #706 (your dense parser is superseded), and the Pointers: per-layer FFN width wants a per-layer override, not a widened If you don't have time, I'm happy to pick it up with co-author credit to you and finish the |
|
@void-mckenzie I'll check it soon |
E2B and E4B add three things to the gemma4 decoder: a per-layer input embedding table, KV-shared layers, and (E2B) a double-wide MLP on those layers. Both checkpoint formats are served; the GGUF path no longer refuses these files. - Per-layer inputs: embed_tokens_per_layer plus a per-layer projection and norm, one more residual step before layer_scalar. The table defaults to the GPU. --embed-weights host now moves it to pinned host RAM, which is how these models fit an 8 GB GPU, and CUDA graphs stay on. A table tied to the LM head stays on the GPU. - KV sharing: the last num_kv_shared_layers layers project q only and read k, v from the last earlier layer of their attention type, handed over in a dict that lives for one forward. Each shared layer still stores its own KV copy; reading the source's slab can be a follow-up. - MLP width: ModelConfig.layer_intermediate_sizes is a per-layer override, read from HF's use_double_wide_mlp or GGUF's feed_forward_length list. intermediate_size stays the base width. The E-series vision tower applies clipped linears, which the tower does not implement, so these checkpoints serve text only with a warning instead of refusing to start. An image request gets a 400. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abd3b14 to
6697b9d
Compare
|
@void-mckenzie Rebuilt on current main, on top of #706, with your review items addressed: the per-layer MLP width is now a per-layer override, the PLE table defaults to the GPU with host RAM behind |
Summary
Serves Gemma-4 E2B and E4B, from both HF safetensors and GGUF, on current
main. The GGUF refusal inmodels/gemma4/gguf.pyis gone. Rebuilt on top of #706 and the GGUF refactor rather than rebased: the dense parser this PR used to carry is superseded, so what is left is the E-series delta.E2B and E4B add three things to the gemma4 decoder: a per-layer input embedding table, KV-shared layers, and (E2B only) a double-wide MLP on those layers.
Changes
models/gemma4/model.py):embed_tokens_per_layerplus a per-layer projection and norm, one more residual step beforelayer_scalar.engine/engine.py): the table defaults to the GPU.--embed-weights hostnow moves it to pinned host RAM, replacing the oldFREETOKEN_GEMMA4_PLE_CPUenv var. A table tied to the LM head stays on the GPU, andhostis still refused when that is the only table. CUDA graphs stay on, which the old host path could not do.models/gemma4/attention.py): the lastnum_kv_shared_layerslayers project q only and read k, v from the last earlier layer of their attention type. They are handed over in a dict living for one forward, which removes the old persistent stash and its fixed 8192-token size. Each shared layer still stores its own KV copy.models/config.py,models/blocks.py):ModelConfig.layer_intermediate_sizesis a per-layer override, read from HF'suse_double_wide_mlpor GGUF'sfeed_forward_lengthlist.intermediate_sizestays the base width.models/gemma4/weight.py,models/gemma4/gguf.py,models/gguf/tensors.py): drop the k, v and k_norm tensors that files ship for shared layers, and accepthead_count_kvandfeed_forward_lengthas either one value or a per-layer list. The table is read without an extra copy on both paths, so loading a 5.6 GB BF16 table does not double the peak RAM, and the safetensors reader never touches VRAM for it under--embed-weights host.models/register.py,engine/config.py): the E-series tower applies clipped linears, which the tower does not implement. These checkpoints now serve text only with a warning, through a smallEncoderSpec.unservedhook, instead of refusing to start. An image request gets a 400.Three commits left this PR: the Llama GGUF loader (
mainhas its own now), fp8 KV (it has its own PR, #126), andrun-freetoken.sh(a local convenience).Tested models
RTX 5070 Laptop 8 GB, driver 595.99, torch 2.14.1, transformers 5.18, Python 3.13. CUDA graphs on throughout, bs=1, 400 tokens.
--embed-weights host--embed-weights hostgemma4:latest, with a corrected tokenizer section)--embed-weights hostE4B with the BF16 table takes 6.7 GB of VRAM under
--embed-weights host, leaving about 78k KV tokens. By the numbers the bf16 safetensors cannot fit an 8 GB card with the table on the GPU (4.4 GiB table plus 4.6 GiB of other weights against 7.3 GiB free), so I did not run that case here. Its default path was last exercised in the H100 runs earlier in this thread, on the old branch.Results
Au,51,Guten Morgen,Jupiter), so the per-layer inputs are live under replay. E2B's answer to the France prompt is byte-identical to the earlier H100 run in this thread.tok_compare): 0 differing texts on E2B and E4B against the HFtokenizer.json.gguf_check, with the kernel table), run one base commit earlier:ft checkpointon the E2B safetensors converts in 8 s, loads in 20 s with--embed-weights host, and matches the safetensors' greedy output.tests/models/test_gemma4_e_series.py(19 tests).tests/models tests/moe tests/kernels/test_ggml.py tests/server tests/engine, one base commit earlier: 1441 passed, 221 skipped, 3 failed. The same 3 fail on cleanmainon this machine:test_scheme_for_agrees_with_the_stored_tensorsover non-LLM repos in my local HF cache. On the final commit the gemma4, loader, registry and ggml tests give 110 passed, 4 skipped, and the serving, staggered and greedy-answer checks above were repeated.Not covered
UD-Q2_K_XLon both models:embedding TQ2_0: no get_rows kernel.ik_llama.cppfiles with fork-only ggml types.gemma4-assistantMTP draft files (not a served architecture).ft checkpointconverts a GGUF, but serving the result fails atmodel.embed_tokens, becausesource_metadata.ggufhas no tensor table forGGUFConfig. That lookup is not touched here and is not specific to these models, so I left it.One observation outside this PR: after a ggml source change the first server start compiles the GGML kernels during graph capture, with the model already loaded. That compile alone peaked at 11.6 GB of RAM here (24 jobs), which is a lot to add to a loaded model on a consumer machine.
🤖 Generated with Claude Code