Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9463223
Add Gemma 4 model support with tokenizer, configuration, and inferenc…
mikepapadim Jun 7, 2026
f5f7edf
Merge to resolve conflicts with main
orionpapadakis Jun 25, 2026
77f5640
Introduce Gemma4 Q8_0 transformer layers with logits soft-capping and…
orionpapadakis Jun 26, 2026
ffe4344
Merge branch 'gemma4-pr120' into feat/gemma4-batched-decode
mikepapadim Jul 11, 2026
11dc501
Gemma 4 on the batched-decode branch: merge PR #120, verify stock GPU…
mikepapadim Jul 11, 2026
0e65d1a
README: add batched decode & vLLM-style serving section + Gemma 4 mod…
mikepapadim Jul 11, 2026
7ad85d7
Gemma4 batched decode (1/N): sliding-window/full decode attention ker…
mikepapadim Jul 11, 2026
18ff705
Gemma4 batched decode (2/N): NEOX RoPE (own-KV + Q-only) + per-head Q…
mikepapadim Jul 11, 2026
da83dea
Gemma4 batched decode (3/N): GeGLU + batched RMSNorm-apply + norm+res…
mikepapadim Jul 11, 2026
ae2d08f
Gemma4 batched decode (4/N): PLE kernels validated - all 10 batched k…
mikepapadim Jul 11, 2026
db34c36
Gemma4 batched decode: refactor attention KV addressing to (slotStrid…
mikepapadim Jul 12, 2026
d79d0e4
Gemma4 batched decode: handle headDim=512 (4-acc attention), add F32/…
mikepapadim Jul 12, 2026
ca75b2f
Gemma4 plan: kernels complete + assembly-ready (E2B dims probed, head…
mikepapadim Jul 12, 2026
b48d183
Add Mistral-7B batched decode: generalize decode layer graph to base …
mikepapadim Jul 12, 2026
250cd02
Gemma4 batched engine (WIP): assembles + runs end-to-end (35-layer Q8…
mikepapadim Jul 12, 2026
7021146
Gemma4 engine WIP: diagnostics - logits finite but wrong direction (n…
mikepapadim Jul 12, 2026
b64e4c1
Add Q8_0 support to copyEmbeddingRow (FloatTensor variant) - unblocks…
mikepapadim Jul 12, 2026
191c268
Gemma4 engine WIP: layer-wise wrapX dumps show sane magnitudes throug…
mikepapadim Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions BATCHED_DECODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ capped lower):
| Llama-3.2-1B | 16 · 2048 · 128256 | 101 tok/s | 128 | 4175 tok/s | 41× |
| Qwen3-1.7B | 28 · 2048 · 151936 | 48 tok/s | 64 | 1433 tok/s | 30× |
| Qwen3-4B | 36 · 2560 · 151936 | 39 tok/s | 32 | 405 tok/s | 10× |
| Mistral-7B-v0.3 | 32 · 4096 · 32768 | 27 tok/s | 32 | 331 tok/s | 12× |

**Supported models: LLaMA, Qwen3, Mistral (FP16).** Mistral runs on the LLaMA decode path with
no new kernels — the only generalizations were typing the layer graph to the base
`Configuration` and parameterizing the RoPE theta (`config.ropeTheta()`, e.g. Mistral 1e6). Any
LLaMA-family model (RMSNorm + SwiGLU + GQA + RoPE, no QK-norm, no sliding window) drops in the
same way; the full serving stack (continuous + paging + prefix + on-device sampling) is
model-agnostic and applies unchanged (Mistral-7B: 128-request continuous+paged+prefix →
prefix-consistent, ~11.6× less KV, 82.8% fewer prefill tokens).

All bit-exact vs the single-stream greedy reference (`all B streams identical: true`)
and coherent.
Expand Down
79 changes: 79 additions & 0 deletions GEMMA4_BATCHED.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Gemma 4 on the batched-decode branch — findings

This branch merges **Gemma 4 support (PR #120, `gemma4-new`)** onto the vLLM-style
batched-decode work (`feat/static-batched-decode`, PR #129) and evaluates how much of the
batched-decode / serving stack can be applied to Gemma 4 to squeeze decode performance.

Model: `unsloth/gemma-4-E2B-it-GGUF` → `gemma-4-E2B-it-Q8_0.gguf` (5.0 GB), RTX 4090,
TornadoVM **CUDA backend**, JDK 21.

## Status

- ✅ **Merge is clean** — Gemma 4 (model, loader, tokenizer, kernels, FP16/Q8_0 layers)
coexists with all batched-decode features; the tree builds and runs.
- ✅ **Stock Gemma 4 GPU decode works** — coherent output:
`What is the capital of France? → "The capital of France is **Paris**."`
- ✅ **Perf squeeze applied (CUDA graphs, model-agnostic):**

| Gemma-4-E2B Q8_0 | tok/s | speedup |
|------------------|------:|--------:|
| single-token, no CUDA graphs | 22.3 (short) / 11.5 (200-tok) | 1.0× |
| single-token, **CUDA graphs** | **25.3** / **12.3** | **+13.6% / +6.5%** |

Enable with `JAVA_TOOL_OPTIONS="-Dllama.cudaGraphs=true"` (or `--cuda-graphs`). Free, output
unchanged. The gain shrinks with longer context as kernel work grows relative to launch overhead.

## Why full batched decode is a larger follow-up

The batched-decode MMA engine (`BatchedDecodeEngine`) assumes a **uniform** transformer layer
(fixed head/FFN dims, global causal attention, SiLU, adjacent- or split-half RoPE, FP16 weights).
Gemma 4 (a Gemma-3n-class MatFormer) breaks nearly all of these — from `Gemma4Configuration` +
`Gemma4Kernels`:

| Gemma 4 feature | impact on a batched-decode layer graph |
|-----------------|----------------------------------------|
| **Per-layer** head dims (`headDimSwa`/`headDimFull`) + per-layer FFN (`feedForwardLength[]`) | per-layer GEMM shapes — the per-layer graph already supports this, so it's fine |
| **Alternating sliding-window / full attention** (`slidingWindowPattern[]`, `slidingWindowSize`) | needs a **windowed** batched-decode attention kernel (attend `[max(0,pos-W+1), pos]`) |
| **Shared-KV layers** (`sharedKvLayers`) | some layers reuse an earlier layer's KV — cache/block addressing differs per layer |
| **Per-layer embeddings (PLE)** + AltUp/Laurel | embedding path is not a single table lookup |
| **Pre + post norms** around attn and FFN (4/layer) + per-head Q/K RMSNorm + query scaling | extra RMS tasks + Gemma-specific norm kernels |
| **GeGLU** (gelu) not SiLU | fork the packed SwiGLU kernel to GeGLU |
| **NEOX RoPE**, two thetas (`ropeTheta` full / `ropeThetaSwa`) | fork the decode/paged RoPE (NEOX pairing, per-layer theta) |
| **Final logit softcapping** (tanh) | greedy on-device argmax stays valid (softcap is monotonic); temperature needs the softcap applied |
| **BF16 / Q8_0 weights** (no plain FP16) | the MMA path is FP16 (`HalfFloatArray`) — needs a BF16-MMA path or on-load FP16 conversion |

So a Gemma 4 batched-decode layer graph needs, at minimum: a **windowed** decode attention
kernel, a **GeGLU** packed FFN kernel, a **NEOX** decode/paged RoPE kernel, Gemma pre/post-norm
+ Q/K-norm + query-scale tasks, shared-KV addressing, a PLE embedding path, and a BF16-or-FP16
weight story. That is a dedicated multi-file build (comparable to the whole Llama/Qwen3 decode
path), not a small fork like Qwen3 was.

## What already transfers

The **engine-level** serving features are model-agnostic — once a Gemma 4 batched forward
exists, they apply unchanged:

- **On-device sampling** (GPU argmax) — Gemma 4 has a large vocab (~262 k), so the per-step D2H
logits copy is even bigger than Llama's; on-device argmax would help more. Greedy is valid
through the tanh softcap (monotonic).
- **Continuous batching**, **PagedAttention**, **prefix caching** — pure scheduling / KV
addressing, independent of the layer internals (paging must account for shared-KV layers).
- **Logits-skip** on pure-prefill steps.

## Reproduce

```bash
# model
huggingface-cli download unsloth/gemma-4-E2B-it-GGUF gemma-4-E2B-it-Q8_0.gguf --local-dir .
# stock GPU decode + CUDA graphs
JAVA_TOOL_OPTIONS="-Dllama.cudaGraphs=true" \
python3 llama-tornado --gpu --cuda --model gemma-4-E2B-it-Q8_0.gguf \
--prompt "What is the capital of France?" --instruct -n 64
```

## Next

1. Windowed batched-decode attention + GeGLU + NEOX RoPE kernels (the Gemma 4 decode layer graph).
2. FP16 (or BF16-MMA) weight path for the tensor-core GEMMs.
3. Then the model-agnostic engine features (continuous / paging / prefix / on-device sampling)
drop in — tracked in the roadmap issue (#130).
87 changes: 87 additions & 0 deletions GEMMA4_BATCHED_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Gemma 4 batched-decode — port plan & status

Porting the batched-decode engine to Gemma 4 (Q8_0 path, so the existing Q8 tensor-core GEMMs
`gemmMMAQ8` / `gemmMMAQKVQ8` / `gemmMMAGateUpQ8` handle the projections). Every non-projection
op in the single-token `Gemma4Q8_0FFNLayers` (~25 tasks/layer) needs a **batched** variant that
processes B rows. Each kernel is **microbench-validated bit-exact vs its single-token reference**
before assembly (the safe path — the full forward isn't testable until near-complete).

## Per-layer op sequence to batch (from `Gemma4Q8_0FFNLayers`)

```
attn pre-norm (reduce+apply) → wrapXb
q_proj (Q8 GEMM) → q_norm (per-head RMS)
[own-KV layers] k_proj→k_norm, v_proj→v_norm, rope_and_cache (NEOX + KV write)
[reuse-KV layers] rope_q_only
attention (sliding-window / full, scale 1.0)
wo_proj (Q8 GEMM) → post-attn norm+residual → wrapX
ffn pre-norm → ffn_gate_up (GeGLU Q8) → ffn_down (Q8 GEMM) → post-ffn norm+residual
PLE: ple_gate_proj → gelu·mul → ple_proj → post-ple norm+residual
[optional] layer_output_scale
layer-0 setup: scale_embedding(√dim), ple_model_proj, ple_proj_scale_norm, ple_merge
final: RMS + logits GEMM + logit softcap (tanh)
```

## Kernel checklist (`Gemma4BatchDecodeKernels`)

- [x] **batched sliding-window / full attention**, scale 1.0, per-slot KV, FP16 out —
`batchedGemmaDecodeAttentionFP16Out` (validated bit-exact, windowed + full; `GemmaBatchedAttentionBench`)
- [x] batched **NEOX RoPE + per-slot KV write** (`batchedGemmaDecodeRopeNeox`) + **RoPE-Q-only**
(`batchedGemmaDecodeRopeQOnly`) — validated (Q/K/V maxRel 7e-5; `GemmaBatchedRopeNormBench`)
- [x] batched **per-head Q/K RMSNorm** (`batchedGemmaPerHeadRmsNorm`) + **V norm**
(`batchedGemmaPerHeadRmsNormNoWeight`) — validated bit-exact
- [x] batched **pre-norm apply** (`batchedGemmaApplyRmsNorm`) + **norm+residual**
(`batchedGemmaRmsNormApplyWithResidual`) — validated; reduce via existing `batchedRmsReduceParallel`
- [x] batched **GeGLU** gate/up (`batchedGemmaGeGLUPacked`, gelu over packed Q8 gate/up) — validated
- [x] elementwise `scaleInPlace` / `addAndScale` / `scaleInPlaceFromTensor` — reuse `Gemma4Kernels` as-is (flat, size = B·dim)
- [x] batched **PLE** tasks: `batchedGemmaPleGateGeluMul` + `batchedGemmaPleProjScaleAndNormalize` — validated bit-exact
- [x] **logit softcap** — skipped for greedy (softcap is monotonic → argmax invariant; on-device argmax unaffected)

Projections reuse the existing Q8 MMA GEMMs (`gemmMMAQ8`, `gemmMMAQKVQ8`, `gemmMMAGateUpQ8`).

## Structural handling

- **Per-layer head/FFN dims** — one TaskGraph per layer already bakes each layer's dims as
constants (as the single-token path does); no extra work.
- **Sliding-window vs full** — the attention kernel takes `windowSize` (full layers pass
`≥ contextLength`); the per-layer graph passes the layer's value.
- **Shared-KV layers** — reuse-KV layers skip K/V proj + KV write and RoPE-Q-only; their
attention reads the KV region of `kvReuseLayer(layer)`. Batched: pass that layer's per-slot
KV base instead of the current layer's.
- **PLE** — per-layer-embedding contribution mixed in each layer; `perLayerInputs` computed once
at layer 0 (host-gathers the per-token per-layer-embedding row into a batch buffer).
- **Weights** — Q8_0 for the main projections; PLE projections may be F32/F16 (dispatch per tensor).

## Engine

Once the kernels land: a `Gemma4Q8LayersBatchDecodeMMA` layer graph (mirrors the single-token
task order, batched) + `Gemma4State`-backed batch buffers + dispatch in `BatchedDecodeEngine`
(`config instanceof Gemma4Configuration`). The serving features (continuous / paging / prefix /
on-device sampling) are model-agnostic and then apply unchanged.

## Status

**All 10 new batched kernels ported + validated bit-exact** (attention, NEOX RoPE ×2,
per-head norm ×2, GeGLU, RMSNorm-apply, norm+residual, PLE ×2); projections reuse the existing
Q8 MMA GEMMs; elementwise ops reuse `Gemma4Kernels` as-is; softcap skipped for greedy. Each has
a passing microbench (`GemmaBatched{Attention,RopeNorm,FFN,Ple}Bench`).

**Kernels are complete and assembly-ready.** The target model's shape drove two extra kernel
fixes, both done + validated:
- **E2B geometry** (probed): dim 1536, 35 layers, nHeads 8, **nHeadKv 1**, headDim **256 (swa) /
512 (full)**, 20 shared-KV layers, sliding window 512, nEmbdPerLayer 256, ffn 6144, vocab
**262144**. Attention rewritten to handle **headDim ≤ 512** (4 register accumulators, 8×512
tiles) — re-validated bit-exact at 256 and 512.
- **PLE weights are F32** (`perLayerInpGate`/`perLayerProj`) and F16 (`perLayerModelProj`); the
main projections + `wcls` are Q8_0. Added `batchedMatVecF32` / `batchedMatVecF16` for the PLE
projections (no MMA path for F32) and `batchedGemmaApplyRmsNormFP16` for the Q8-GEMM inputs.
- **KV addressing** known: per-slot stride = `totalCacheElements` (single-seq, dedup'd), per-layer
base = `cacheLayerBaseOffset[l]` (reuse layers alias their source); attention/rope take
`(slotStride, layerBaseOff)`.

**Remaining = engine assembly (the first end-to-end test point):** a `Gemma4BatchedDecodeEngine`
mirroring the single-token task order batched — embed·√dim + layer-0 PLE setup (host-gather the
per-token per-layer-embedding rows into a batch buffer), 35 layer graphs (Q8 MMA q/k/v/o/gate-up/
down + the validated batched kernels; own-KV vs reuse-KV branch; swa/full freq tables + window),
final RMS + Q8 logits GEMM + on-device argmax — then debug greedy vs the single-token reference.
This is dense integration but every kernel it needs is validated; it is de-risked, not open-ended.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ We are at the early stages of Java entering the AI world with features added to

TornadoVM 4.0 includes a native [Metal](https://developer.apple.com/metal/) backend, enabling GPU-accelerated inference on Apple Silicon.

-----------
## ⚡ Batched decode & vLLM-style serving (experimental)

An experimental GPU **batched-decode engine** (`bench/BatchedDecodeEngine`, CUDA backend) decodes
**B independent sequences at once** — turning the bandwidth-bound single-token matvecs of decode
into compute-bound tensor-core GEMMs (one weight read amortized across B tokens). On an RTX 4090
(Llama-3.2-1B FP16) it reaches **~4200 tok/s aggregate at B=128 (≈41× single-stream)**, output
verified bit-exact against the single-stream greedy reference. It ships the vLLM serving stack:

| Feature | Effect |
|---------|--------|
| **Continuous (iteration-level) batching** | evict-on-stop + admit-from-queue; +20% throughput / +24% utilization vs static waves |
| **PagedAttention** | block-pool KV + per-slot block table; ~10.7× less KV memory at ~1% overhead |
| **Prefix caching** | shared prompt prefix prefilled once into pinned blocks; +85% throughput |
| **On-device sampling** | GPU argmax → transfer B token ids not the 65 MB logits tensor; +30% throughput |
| **CUDA graphs**, **logits-skip** | per-step launch-overhead + pure-prefill logits GEMM removed |

Supported on **LLaMA** and **Qwen3** (FP16). See **[`BATCHED_DECODE.md`](BATCHED_DECODE.md)** for
the design (with diagrams), per-model numbers, and exact reproduction prompts/flags;
**[`GEMMA4_BATCHED.md`](GEMMA4_BATCHED.md)** for the Gemma 4 evaluation; and the
[vLLM features roadmap](https://github.com/beehive-lab/GPULlama3.java/issues/130) for the
feature/commit checklist.

-----------
## 📦 Maven Dependency

Expand Down Expand Up @@ -282,6 +305,9 @@ jbang LlamaTornadoCli.java -m beehive-llama-3.2-1b-instruct-fp16.gguf \
### Qwen 3 Collection
[https://huggingface.co/collections/beehive-lab/llama3-gpullama3java](https://huggingface.co/collections/beehive-lab/qwen-3-gpullama3java)

### Gemma 4 Collection (BF16 / Q8_0)
[https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF](https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF)

### Phi-3 Collection
[https://huggingface.co/collections/beehive-lab/llama3-gpullama3java](https://huggingface.co/collections/beehive-lab/phi-3-gpullama3java)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ public static void main(String[] args) throws Exception {
} else {
LlamaFP16LayersBatchDecodeMMA l = paged
? new LlamaFP16LayersBatchDecodeMMA((LlamaState) state, (LlamaTornadoWeights) weights,
(LlamaConfiguration) config, B, decodeCtx, keyCacheBatch, valueCacheBatch, seqPositions,
config, B, decodeCtx, keyCacheBatch, valueCacheBatch, seqPositions,
blockTable, blockSize, maxBlocksPerSlot)
: new LlamaFP16LayersBatchDecodeMMA((LlamaState) state, (LlamaTornadoWeights) weights,
(LlamaConfiguration) config, B, decodeCtx, keyCacheBatch, valueCacheBatch, seqPositions);
config, B, decodeCtx, keyCacheBatch, valueCacheBatch, seqPositions);
layerITGs = l.getLayerImmutableTaskGraphs();
lastLayerId = l.getLastLayerTaskGraphID();
updateLayerSched = l::updateGridScheduler;
Expand Down
Loading