Skip to content

Instantly share code, notes, and snippets.

@eSlider
Last active August 21, 2026 19:50
Show Gist options
  • Select an option

  • Save eSlider/f334f33ca5636c798dc4241306e6c013 to your computer and use it in GitHub Desktop.

Select an option

Save eSlider/f334f33ca5636c798dc4241306e6c013 to your computer and use it in GitHub Desktop.
Qwen3.8-2B/4B + Gemma 4 E2B GGUF perf & tool-calling report + llama-server scripts (gfx90c Vulkan; MTP, ngram-map-k spec decode)

Gemma 4 edge GPU notes on local gfx90c (ROCm + Vulkan)

Date: 2026-08-01
Host: AMD Ryzen 7 5800H + Radeon Graphics (gfx90c / RADV RENOIR), ~12 GiB system RAM
Builds (llama.cpp 0005475):

  • TheRock HIP: build-work/llama.cpp/build-therock (GGML_HIP=ON, GPU_TARGETS=gfx90c, GGML_HIP_GRAPHS=OFF) — broken compute
  • Mesa Vulkan: build-work/llama.cpp/build-vulkan (GGML_VULKAN=ON, GGML_HIP=OFF) — working

Helpers: utils/bench-gpu.sh (HIP), utils/bench-vulkan.sh (Vulkan/RADV)

Models tested

Model Source File On-disk size
Gemma 4 E2B IT QAT Q4_0 google/gemma-4-E2B-it-qat-q4_0-gguf gemma-4-E2B_q4_0-it.gguf 3.10 GiB (4.63B params reported)
Gemma 4 E4B IT QAT Q4_0 google/gemma-4-E4B-it-qat-q4_0-gguf gemma-4-E4B_q4_0-it.gguf 4.79 GiB (7.46B params reported)
Qwen3.6 35B-A3B MoE IQ2_M HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive ...-IQ2_M.gguf 10.85 GiB (34.66B total / ~3B active)
Baseline local Qwen3-0.6B-Q4_0.gguf 359 MiB

GGUF weights live under build-work/ (not committed).

Vulkan results (working GPU path)

Device:

ggml_vulkan: 0 = AMD Radeon Graphics (RADV RENOIR) (radv) | uma: 1 | fp16: 1 | warp size: 64
Available devices:
  Vulkan0: AMD Radeon Graphics (RADV RENOIR) (~8700 MiB reported)

llama-bench: -ngl 99 -t 1 -fa off -b 512 -ub 512 -r 2 (helper: utils/bench-vulkan.sh).

Model backend ngl pp128 t/s tg64 t/s
Qwen3 0.6B Q4_0 Vulkan 99 1083.44 ± 12.26 88.75 ± 0.26
Gemma 4 E2B Q4_0 Vulkan 99 312.56 ± 7.30 26.15 ± 0.02
Gemma 4 E4B Q4_0 Vulkan 99 156.73 ± 1.46 12.84 ± 0.02

E2B Vulkan smoke (llama-completion -ngl 99 --jinja -no-cnv): load ~1.3 s; short prompt completes.

Best GPU fit on this host: Gemma 4 E2B on Vulkan/RADV (~26 tg t/s, ~2x decode vs E4B, fits UMA comfortably).

ROCm / HIP status

Device enumeration works; GPU compute fails.

ggml_cuda_init: found 1 ROCm devices (Total VRAM: 6652 MiB):
  Device 0: AMD Radeon Graphics, gfx90c (0x90c), VMM: no, Wave Size: 64
Available devices:
  ROCm0: AMD Radeon Graphics (6652 MiB, ...)

Observed with HIP visible (-ngl > 0):

  1. ROCm error: unspecified launch failure on MUL_MAT (also after GGML_HIP_GRAPHS=OFF).
  2. utils/bench-gpu.sh aborts on warmup prompt; some runs stall until hard timeout.
  3. Repeated failures can wedge the GPU (gpu_busy_percent stuck at 99%, dmesg qcm fence wait loop timeout / device wedged) until reboot.

gfx90c is outside this repo's published HIP targets. Prefer Vulkan here; use HIP on listed dGPU/APU targets (gfx110X / gfx1151 / ...).

Recover after a HIP hang:

sudo build-work/rocm/bin/amd-smi reset -G -g 0

CPU reference (HIP hidden)

Useful when comparing backends; not the recommended deploy path once Vulkan works.

Model pp tg
E2B (pp64/tg32) 11.30 8.48
E4B (pp64/tg32) 5.59 4.40
Qwen3-0.6B (pp32/tg16) 41.44 28.11
Qwen3.6-35B-A3B IQ2_M (pp32/tg16) 3.73 3.21

MoE IQ2_M: mmap CPU smoke only; too large for useful GPU offload on this UMA budget.

Best-fit recommendation

  1. Primary (this host): Gemma 4 E2B QAT Q4_0 via Vulkan (build-vulkan, utils/bench-vulkan.sh).
  2. Secondary: E4B Vulkan if quality > speed (~13 tg t/s).
  3. ROCm/Lemonade production: supported GPU targets + utils/bench-gpu.sh; do not rely on Renoir HIP today.
  4. MoE demo: Qwen3.6-35B-A3B IQ2_M on CPU mmap only here.

MoE / advanced mechanics worth using

Gemma 4 family

  • PLE (Per-Layer Embeddings) on E2B/E4B: large embedding tables, cheap lookups; "effective" params much smaller than total with embeddings. Keep QAT GGUFs; avoid re-quantizing blindly.
  • Hybrid attention: sliding-window local layers interleaved with global layers (p-RoPE / unified KV on globals).
  • QAT Q4_0: prefer Google's official QAT GGUF over post-hoc Q4 when available.
  • MoE 26B A4B: ~25B total / ~3.8B active, 8-of-128 experts + shared MLP (llama.cpp gemma4 MoE path). Needs RAM/VRAM for all experts.
  • MTP / assistant draught + speculative decoding: matching QAT draught models on stronger GPUs.
  • Multimodal: mmproj for vision/audio on E2B/E4B — optional follow-up on Vulkan.

Qwen3.6 35B-A3B MoE

  • Sparse MoE (qwen35moe): ~35B total / ~3B active — memory still tracks total weights.
  • IQ2_M: only near-fit quant on 12 GiB; higher K_P quants need much more RAM.
  • Jinja chat template required (--jinja).

Practical knobs

  • Vulkan: utils/bench-vulkan.sh (-ngl 99); keep TheRock out of LD_LIBRARY_PATH.
  • HIP (supported GPUs): utils/bench-gpu.sh; consider GGML_HIP_GRAPHS=OFF if launch failures appear.
  • Try -fa on after validating flash-attn for the backend/GPU.

Reproduce (Vulkan)

# Build (once)
cmake -S build-work/llama.cpp -B build-work/llama.cpp/build-vulkan -G Ninja \
  -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DGGML_HIP=OFF
cmake --build build-work/llama.cpp/build-vulkan -j"$(nproc)" --target llama-bench llama-completion

# Bench
BENCH_MODEL=$PWD/build-work/gemma-4-E2B_q4_0-it.gguf ./utils/bench-vulkan.sh

Verdict summary

Goal Pick
Edge GPU on this gfx90c host Gemma 4 E2B QAT Q4_0 + Vulkan/RADV (~26 tg t/s)
Stronger edge, still local Gemma 4 E4B QAT Q4_0 + Vulkan (~13 tg t/s)
MoE mechanics demo Qwen3.6-35B-A3B IQ2_M (CPU mmap)
ROCm HIP on this iGPU Blocked — use supported GPU targets instead

Tool-calling agent verification (2026-08-21)

Served with serve-gemma4-e2b.sh (default profile: --jinja, 131072 ctx, SERV_SPEC=ngram-map-k,ngram-cache) on 127.0.0.1:8081 and used as a real agent — not just a synthetic get_weather probe — against a live company knowledge base (hybrid vector+FTS search behind an OpenAI-compatible brain_search tool).

Flow exercised end-to-end:

  1. system+user → model emits a well-formed tool_calls (brain_search {"q":"oo CLI usage and environment variables"}) plus coherent reasoning_content
  2. tool result fed back → grounded natural-language answer, correctly citing the retrieved doc (ONLYOFFICE_URL / ONLYOFFICE_USER / ONLYOFFICE_PASSWORD)
  3. write→read loop: a fact ingested minutes earlier via the KB's MCP server was found and used by the agent without restart

Measured agent-turn rates (chat completions incl. jinja template + reasoning + JSON tool syntax, so lower than raw tg64 bench):

Turn prompt tok gen tok secs gen t/s
decide + emit tool call 95 81 6.5 12.5
grounded answer from result 205 225 14.0 16.1

Verdict: Gemma 4 E2B QAT Q4_0 on gfx90c/Vulkan is genuinely usable as a local ops-agent at ~26 t/s raw decode (~12–16 t/s per agent turn), with reliable tool-call emission at temp 0.2.

gfx90c local LLM inference: master benchmark report

Rounds: 2026-08-01 (Gemma 4 / MoE) · 2026-08-17 (Qwen3.8-2B/4B, runtimes, MTP, tool calling) · 2026-08-18 (Qwen3.8-4B variants) · 2026-08-20 (ngram-map-k speculative decoding) Benchmark method: llama-bench (-ngl 99 -t 1 -fa off -p 128 -n 64 -r 2/3) unless noted; live-server numbers via /v1/chat/completions timings.

Host hardware

CPU

  • AMD Ryzen 7 5800H (Cezanne, Zen 3, 8 cores / 16 threads, up to 4.46 GHz, TDP 45 W mobile)
  • Caches: L1d 256 KiB / L1i 256 KiB (8 inst), L2 4 MiB, L3 16 MiB
  • SIMD: AVX2, FMA3, AVX, SSE4.1/4.2, AES-NI, SHA-NI, VAES, VPCLMULQDQ

GPU (iGPU, shared memory)

  • AMD Radeon Graphics (Vega 8), gfx target gfx90c (Cezanne iGPU)
  • RADV RENOIR driver, Mesa 26.0.3-1ubuntu1 (Vulkan API 1.4.335), llvmpipe fallback present
  • No dedicated VRAM: unified memory (UMA), shares the 12 GiB system RAM (llama-bench reports ~8.7 GiB usable). GPU ~1.9-2.0 GHz, 8 CUs / 512 shaders.
  • Bandwidth-bound: iGPU shares the same DDR4 memory as CPU -> decoding is memory-bandwidth-limited, not compute-limited.

OS / Linux kernel

  • Ubuntu 26.04 LTS (Resolute Raccoon), x86_64
  • Kernel: 7.2.0-070200rc5-generic (pre-release RC build 202607262232, PREEMPT_DYNAMIC); installed variants: 7.0.0-27/28/29-generic, 7.2.0-070200rc5-generic (booted)
  • Mesa 26.0.3-1ubuntu1 Vulkan drivers (RADV), ROCm TheRock HIP present but non-functional on gfx90c

Memory & storage

  • RAM: 12 GiB total (DDR4 dual-channel UMA), 32 GiB swapfile
  • Disk: ~2.7 TiB free; models live in build-work/, /tmp is tmpfs (6.5 GiB)

Model variants tested

Model Repo Params Q4 on-disk Arch Native ctx MTP head
Qwen3 0.6B Q4_0 local (baseline) 0.6B 359 MiB qwen3 32768 no
Qwen3.8-2B Q4_K_M empero-ai/Qwen3.8-2B-GGUF 1.94B 1.21 GiB qwen35 (Gated DeltaNet) 262144 yes (nextn, mtp_layers=1)
Qwen3.8-4B Q4_K_M empero-ai/Qwen3.8-4B-GGUF 4.33B 2.58 GiB qwen35 (Gated DeltaNet) 262144 yes (nextn_predict_layers=1, blk.32.nextn)
Gemma 4 E2B QAT Q4_0 google/gemma-4-E2B-it-qat-q4_0-gguf 4.63B 3.10 GiB gemma4 (PLE, hybrid attn) 131072 no
Gemma 4 E4B QAT Q4_0 google/gemma-4-E4B-it-qat-q4_0-gguf 7.46B 4.79 GiB gemma4 131072 no
Qwen3.6-35B-A3B MoE IQ2_M HauhauCS/... 34.66B / ~3B active 10.85 GiB qwen35moe 262144 no

Runtimes tested

Runtime Version/build Backend Tool API
llama.cpp (stock) commit 0005475 (b10696), build-vulkan2 Vulkan/RADV + CPU yes (OpenAI, verified)
Atomic fork (atomic-llama-cpp-turboquant) commit cd56093 (b10269) Vulkan/RADV yes
Atomic Chat app AppImage 2.0.0 — (empty engine stubs, needs GUI) n/a
Zinc (zolotukhin/zinc) zig 0.15.2 ReleaseFast Vulkan/RADV no (headless)
gguf-runner v0.5.0 (Rust, -C target-cpu=native) CPU yes (REPL --allowed-tools)
ROCm TheRock HIP repo build HIP broken on gfx90c (see below)

Master results (decode tok/s, best on top)

# Model Runtime Backend pp128 t/s tg t/s Tool calling Notes
1 Qwen3-0.6B Q4_0 llama.cpp Vulkan 1083.4 88.75 - tiny baseline
2 Qwen3.8-2B Zinc Vulkan 38-43 26.6-33.9 no fastest decode measured; short-ctx run 33.9
3 Qwen3.8-2B llama.cpp stock Vulkan 382.0 27.2 yes server 256t A/B: 25.15; -fa off
4 Gemma 4 E2B llama.cpp Vulkan 312.6 26.15 yes (best) biggest capable model at full speed; ctx 131072
5 Qwen3.8-2B Atomic fork Vulkan 342.6 24.9 yes turbo4 KV: 22.31
6 Qwen3.8-2B llama.cpp + MTP (draft-mtp n3) Vulkan 179.3 22.7 yes 256t A/B: 14.7 (accept 0.41)
7 Qwen3.8-2B llama.cpp CPU-only CPU 8T 232.1 19.83 no -ngl 0
8 Gemma 4 E4B llama.cpp Vulkan 156.7 12.84 - bigger, half of E2B speed
9 Qwen3.8-2B gguf-runner CPU - 12.4 yes (REPL)
10 Qwen3.8-4B llama.cpp stock Vulkan 141.5 12.14 yes (3/3) reliable tools, 2x slower than E2B
11 Qwen3.8-4B Atomic fork turbo4 Vulkan 28.0 (live) 11.8 yes turbo4 ≈ stock; 128k ctx
12 Qwen3.8-4B Atomic fork turbo4 + draft-mtp n6 Vulkan 21.6 (live) 6.8 yes MTP = net loss (draft=full model)
13 Qwen3.8-2B Atomic fork + MTP + turbo3 Vulkan - 8.6 yes worst (accept 0.11)
14 Gemma 4 E2B llama.cpp CPU CPU 11.3 8.48 - ref
15 Gemma 4 E4B llama.cpp CPU CPU 5.59 4.40 - ref
16 Qwen3.6-35B-A3B MoE llama.cpp CPU mmap CPU 3.73 3.21 - MoE smoke only

Zinc's numbers are from its own timing path (incl. short ctx); everything else is llama-bench tg64 or live-server A/B. In a straight server A/B, Zinc ≈ llama.cpp on decode but llama.cpp prefill is ~10x faster.

Tool calling (verified, llama-server OpenAI API)

Every model ran get_weather (simple) and bash/read/glob with prompt "list dir" (coding tools), streaming, temp 0.2.

Model simple tool (get_weather) coding tools (list dir → bash) Full tool-result loop Verdict for opencode
Qwen3.8-2B yes (5/5) NO (answers in prose/```bash block) no not for tools — plain reasoning / 262k window only
Qwen3.8-4B yes yes (3/3) yes yes — but 12 t/s
Gemma 4 E2B yes yes yes yes — recommended (26 t/s)
Gemma 4 E4B not tested not tested - likely yes (same template)
Zinc n/a n/a - no tool API yet
gguf-runner yes (REPL --allowed-tools) - - CLI only

Gemma 4 E2B re-verified 2026-08-18 at native 131072 ctx, temp 0.2: list dirtool_calls with bash {"command":"ls"}; get_weathertool_calls. It emits a reasoning_content preamble before the call — keep max_tokens high enough (≥200) or the call gets truncated (finish=length, looks like a tool-call failure but isn't). Streaming reports the args in the final delta.

Sample successful response (get_weather):

{ "finish_reason": "tool_calls",
  "message": { "tool_calls": [{ "type": "function",
    "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }] } }

Temperature gotcha (2026-08-17): opencode does not send temperature for reasoning models, so llama-server's default 0.8 was used → the 2B drifted into prose ~1/3 of runs. Fixed with --temp 0.2 in both serve scripts → 5/5 and 3/3.

Context window capacity (llama-server)

Qwen3.8 family native ctx 262144 (Gated DeltaNet → small per-token KV; big windows are cheap).

Model ctx slots tg t/s VmPeak loads
Qwen3.8-2B 81920 (80k) 4 25.4 5.8 GB ~15 s
Qwen3.8-2B 131072 4 24.8 7.4 GB ok
Qwen3.8-2B 262144 (native max) 1 (--parallel 1) 25.7 ~5.7 GB ~6 s
Qwen3.8-4B 131072 1 11.5 ok ~40 s
Qwen3.8-4B 262144 1 OOM (ErrorOutOfDeviceMemory/DeviceLost at load) — only the 2B fits 262k; 4B practical max = 131072
Gemma 4 E2B 8192 (default) 4 23.1 5.5 GB (RSS 383 MiB) ~10 s
Gemma 4 E2B 131072 (native max) 1 22.9 (live) / 26.15 (bench) 10.0 GB (RSS 404 MiB) ~15 s
Gemma 4 E2B >131072 (e.g. 262144) 1 silently capped to native 131072 (slot ctx capped to n_ctx_train)
  • Default 4 slots multiply KV by 4x → use --parallel 1 for the full 256k slot.
  • -c 81920 is the safe multi-user default (5.8 GB peak). KV-quant (q8_0/q4_0) shrinks memory at 256k but does not speed decode here.

The winner

  1. Tool-driven / everyday opencode: llamacpp/gemma4-e2b (Gemma 4 E2B on stock llama.cpp + Vulkan) — reliably calls coding tools, completes tool loops, 26.15 t/s, 131072 ctx. Best capability-per-token.
  2. Fastest decode: Zinc (26.6-33.9 t/s) on Qwen3.8-2B, but headless — no serving/tool API.
  3. Biggest window / plain reasoning: Qwen3.8-2B (27.2 t/s, 262144 ctx) or Qwen3.8-4B (reliable tools but 12.1 t/s).
  4. All "optimizations" lose here: MTP (draft-mtp), TurboQuant KV (turbo2/3/4) and FlashAttention all measured slower than plain -fa off — this iGPU is memory-bandwidth-bound, so draft/quant overheads never pay off.

Round 2026-08-20: ngram-map-k speculative decoding

draft-dflash is unavailable for gemma-4-E2B (no DFlash draft exists for the 2B model; upstream llama.cpp DFlash is Qwen3-backbone-only). Tested the draft-free combo --spec-type ngram-map-k,ngram-cache (--spec-draft-n-max 16, size_n=12 / size_m=48 / min_hits=1).

Method: live llama-server /completion, temp 0.2, ctx 8192; (a) long single gen n_predict 256, best of 2; (b) 4-turn cached agent-style session (repeated tool-schema preamble), turn-4 best-of-3.

Model base t/s mtp t/s ngram-map-k,ngram-cache t/s comment
Gemma 4 E2B 22.9 (long) / 31.5 (warm agent) n/a (no MTP head) 22.1 (long) / 31.9 (warm agent) neutral; server default is now the combo
Qwen3.8-2B 22.2 21.2 21.6 neutral
Qwen3.8-4B 22.5 22.3 22.0 neutral

Workload split (both methods are valid, both show base ≈ ngram):

  • Short tool-call turns in a warm cached session: 31-35 t/s for both base and combo (gemma: base 28.6/31.5/31.8, combo 31.7/32.0/31.9, n=3). An early single-turn reading of 35 t/s was real but not a spec-decode win — it was measured against a ~22 t/s cold baseline, which was an apples-to-oranges comparison; on a warm-vs-warm basis ngram ≈ base.
  • Long 256-token code/prose generation: ~22 t/s for both base and combo (and MTP).

Acceptance in agent-style sessions climbs from ~0.17 (cold) to 0.5-0.68 (warm) but stays below the break-even needed to overcome the draft/verify overhead on this bandwidth-bound iGPU — same conclusion as MTP/TurboQuant: speculative decoding is a wash here, never a win.

Round 1 recap (Gemma 4 / MoE, 2026-08-01)

Full detail in gemma4-edge-rocm.md (also in this gist).

  • ROCm/HIP: enumerates gfx90c (6652 MiB) but compute fails (ROCm error: unspecified launch failure on MUL_MAT; can wedge GPU until reboot). gfx90c is outside this repo's HIP targets — Vulkan only here.
  • Decode scales with active model bytes: 88.75 (0.6B) > 27.19 (1.94B) > 26.15 (E2B) > 12.84 (E4B).

Per-runtime setup used

llama.cpp (stock, Vulkan)

cmake -S build-work/llama.cpp -B build-work/llama.cpp/build-vulkan2 -G Ninja \
  -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DGGML_HIP=OFF
cmake --build build-work/llama.cpp/build-vulkan2 -j"$(nproc)" --target llama-server llama-bench
export LD_LIBRARY_PATH=$PWD/build-work/llama.cpp/build-vulkan2/bin

Measure: llama-bench -m build-work/Qwen3.8-2B-Q4_K_M.gguf -ngl 99 -t 1 -fa off -p 128 -n 64 -r 3

MTP draft head (2B A/B)

python3 convert_hf_to_gguf.py build-work/hf-qwen38-src --mtp \
  --outfile build-work/mtp-Qwen3.8-2B-Q4_K_M.gguf
llama-server -m <model> -md build-work/mtp-Qwen3.8-2B-Q4_K_M.gguf \
  --spec-type draft-mtp --spec-draft-n-max 3 -ngl 99 -ngld 99 --jinja

4B: -md <main-model> uses the baked-in blk.32.nextn head (draft = full model → double memory; crashed once at 128k, recovers).

Zinc (Zig)

git clone https://github.com/zolotukhin/zinc.git && cd zinc && zig build -Doptimize=ReleaseFast
./zig-out/bin/zinc -m <model> --prompt "..." --chat -n 256 -c 4096

gguf-runner (Rust, CPU)

RUSTFLAGS="-C target-cpu=native" cargo install --git https://github.com/apimeister/gguf-runner
gguf-runner --model <model> --prompt "..." --max-tokens 32 --show-timings --think no

Atomic Chat / Atomic fork

AppImage 2.0.0 ships empty 0-byte engine stubs (downloaded at runtime) + needs a GUI — tested its engine fork instead:

git clone https://github.com/AtomicBot-ai/atomic-llama-cpp-turboquant && cd atomic-llama-cpp-turboquant
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DGGML_HIP=OFF
cmake --build build -j"$(nproc)" --target llama-server llama-bench

Flags: -ctk/-ctv turbo2|turbo3|turbo4, --spec-type draft-mtp, --spec-draft-n-max N, -md/--mtp-head.

Serving + opencode

./utils/serve-gemma4-e2b.sh start    # recommended (tools)   -> 127.0.0.1:8081/v1
./utils/serve-qwen38.sh start        # Qwen3.8-2B (262k ctx)
SERV_MODEL=build-work/Qwen3.8-4B-Q4_K_M.gguf ./utils/serve-qwen38.sh start   # Qwen3.8-4B
./utils/serve-qwen38.sh stop|status  # daemon control (pid/log in /tmp/opencode/)
curl http://127.0.0.1:8081/health

opencode models (provider llamacpp, all in ~/.config/opencode/opencode.json): llamacpp/gemma4-e2b (ctx 131072, tools ✔) · llamacpp/qwen3.8-2b (ctx 262144, reasoning) · llamacpp/qwen3.8-4b (ctx 262144, reasoning + tools). Restart opencode after config edits.

#!/usr/bin/env bash
# Serve Gemma 4 E2B QAT Q4_0 (google/gemma-4-E2B-it-qat-q4_0-gguf) via llama.cpp (Vulkan/RADV) on gfx90c.
#
# Measured: 26.15 t/s decode (llama-bench tg64), 312.6 t/s prefill (pp128). Native ctx 131072.
# Tool calling VERIFIED on this host (get_weather -> tool_calls, 2026-08-17).
#
# Default: max native context window (131072, single slot via --parallel 1).
# Uses the same port as opencode's `llamacpp` provider (8081), so stop any other server first.
#
# Usage:
# ./utils/serve-gemma4-e2b.sh start # start in background (daemon), wait for readiness
# ./utils/serve-gemma4-e2b.sh stop # stop the daemon
# ./utils/serve-gemma4-e2b.sh status # show pid/health
# ./utils/serve-gemma4-e2b.sh # foreground (same as `start` but stays attached)
# SERV_CTX=8192 ./utils/serve-gemma4-e2b.sh start # smaller window / multi-user (4 slots)
#
# Speculative decoding:
# SERV_SPEC=ngram-map-k,ngram-cache # default: ngram-map-k + ngram-cache (recommended)
# SERV_SPEC="" # disable speculative decoding entirely
# SERV_SPEC="draft-dflash,ngram-map-k" # dflash + ngram (requires dflash.block_size model metadata —
# # NOT AVAILABLE for gemma-4-E2B/E4B; may error or fall back to ngram only)
# SERV_NGRAM_N=12 SERV_NGRAM_M=48 SERV_NGRAM_MIN_HITS=1 # ngram-map-k tuning
# Note: dflash requires 'dflash.block_size' GGUF metadata — gemma-4-E2B/Q4_0 lacks this.
# If used, llama-server will attempt to load dflash draft but may error/fall back since the
# Gemma E2B model has no dflash.block_size metadata. ngram-only combos work without extra metadata.
#
# opencode: provider `llamacpp` / model `gemma4-e2b` -> http://127.0.0.1:8081/v1,
# configured with limit.context = 131072 to match this server.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD="${LLAMACPP_BUILD:-$ROOT/build-work/llama.cpp/build-vulkan2}"
MODEL="${SERV_MODEL:-$ROOT/build-work/gemma-4-E2B_q4_0-it.gguf}"
BIN="$BUILD/bin"
PORT="${SERV_PORT:-8081}"
HOST="${SERV_HOST:-127.0.0.1}"
CTX="${SERV_CTX:-131072}"
PARALLEL="${SERV_PARALLEL:-1}"
NGL="${SERV_NGL:-99}"
N_THREADS="${SERV_N_THREADS:-1}"
TEMP="${SERV_TEMP:-0.2}"
SPEC="${SERV_SPEC:-ngram-map-k,ngram-cache}"
SPEC_N_MAX="${SERV_SPEC_N_MAX:-16}"
NGRAM_N="${SERV_NGRAM_N:-12}"
NGRAM_M="${SERV_NGRAM_M:-48}"
NGRAM_MIN_HITS="${SERV_NGRAM_MIN_HITS:-1}"
NAME="gemma4-e2b"
PIDFILE="${SERV_PIDFILE:-/tmp/opencode/serve-${NAME}.pid}"
LOGFILE="${SERV_LOGFILE:-/tmp/opencode/serve-${NAME}.log}"
if [[ ! -x "$BIN/llama-server" ]]; then
echo "Missing $BIN/llama-server. Build it first:" >&2
echo " cmake -S $ROOT/build-work/llama.cpp -B $BUILD -G Ninja -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DGGML_HIP=OFF" >&2
echo " cmake --build $BUILD -j\"\$(nproc)\" --target llama-server" >&2
exit 1
fi
if [[ ! -f "$MODEL" ]]; then
echo "Missing model: $MODEL (download first: hf download google/gemma-4-E2B-it-qat-q4_0-gguf gemma-4-E2B_q4_0-it.gguf --local-dir $ROOT/build-work)" >&2
exit 1
fi
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
build_args() {
args=(
-m "$MODEL"
-ngl "$NGL"
-t "$N_THREADS"
-fa off
-c "$CTX"
--parallel "$PARALLEL"
--temp "$TEMP"
--jinja
--host "$HOST"
--port "$PORT"
)
if [ -n "$SPEC" ]; then
args+=(--spec-type "$SPEC")
args+=(--spec-draft-n-max "$SPEC_N_MAX")
if [[ ",$SPEC," == *",ngram-map-k,"* ]]; then
args+=(--spec-ngram-map-k-size-n "$NGRAM_N")
args+=(--spec-ngram-map-k-size-m "$NGRAM_M")
args+=(--spec-ngram-map-k-min-hits "$NGRAM_MIN_HITS")
fi
if [[ ",$SPEC," == *",draft-dflash,"* ]]; then
echo "WARNING: draft-dflash selected but gemma-4-E2B model has no dflash.block_size metadata." >&2
echo " llama-server may error or fall back to ngram-only decode." >&2
fi
fi
}
health() { curl -s -m 2 "http://$HOST:$PORT/health" 2>/dev/null; }
daemon_alive() {
local pid; pid=$(cat "$PIDFILE" 2>/dev/null || true)
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}
cmd_start() {
if daemon_alive; then
echo "$NAME already running (pid $(cat "$PIDFILE")) on http://$HOST:$PORT"
return 0
fi
mkdir -p "$(dirname "$PIDFILE")"
build_args
echo "Starting $NAME on http://$HOST:$PORT (ctx $CTX, parallel $PARALLEL, ngl $NGL)… log: $LOGFILE"
setsid nohup "$BIN/llama-server" "${args[@]}" > "$LOGFILE" 2>&1 < /dev/null &
echo $! > "$PIDFILE"
for _ in $(seq 1 60); do
[ "$(health)" = '{"status":"ok"}' ] && break
sleep 2
done
if [ "$(health)" = '{"status":"ok"}' ]; then
echo "Ready: $(health) (pid $(cat "$PIDFILE"))"
else
echo "WARNING: no healthy response yet — tail $LOGFILE" >&2
exit 1
fi
}
cmd_stop() {
if ! daemon_alive; then
echo "$NAME not running"
rm -f "$PIDFILE"
return 0
fi
echo "Stopping $NAME (pid $(cat "$PIDFILE"))"
kill "$(cat "$PIDFILE")"
sleep 2
if daemon_alive; then
kill -9 "$(cat "$PIDFILE")" 2>/dev/null || true
fi
rm -f "$PIDFILE"
echo "Stopped"
}
cmd_status() {
if daemon_alive; then
echo "$NAME: RUNNING (pid $(cat "$PIDFILE")) — health: $(health)"
else
echo "$NAME: stopped"
fi
}
case "${1:-}" in
start) shift || true; cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
*) cmd_start ;;
esac
#!/usr/bin/env bash
# Serve empero-ai/Qwen3.8-2B-GGUF (Q4_K_M) via llama.cpp (Vulkan/RADV) on gfx90c.
#
# Fastest measured config on this host: stock llama.cpp Vulkan, -fa off, no MTP.
# MTP/speculative decoding is a NET LOSS on this bandwidth-limited iGPU (see docs/qwen38-2b-perf.md).
#
# Default: max native context window (262144 tokens, single slot via --parallel 1).
# Measured: 25.7 t/s decode at 262144 ctx (native max), 25.4 t/s at 81920. Context size
# does not slow decode on this bandwidth-bound iGPU.
#
# temp=0.2: opencode does NOT send temperature for reasoning models (llama-server default
# 0.8 makes this 2B model drift into prose instead of tool_calls). Low temp = reliable tools.
#
# Usage:
# ./utils/serve-qwen38.sh start # start in background (daemon), wait for readiness
# ./utils/serve-qwen38.sh stop # stop the daemon
# ./utils/serve-qwen38.sh status # show pid/health
# ./utils/serve-qwen38.sh # foreground (same as `start` but stays attached)
# SERV_CTX=81920 ./utils/serve-qwen38.sh start # 80k window (4 slots, multi-user)
# ./utils/serve-qwen38.sh start --mtp # also load the MTP draft head (draft-mtp; SLOWER)
# SERV_SPEC=ngram-map-k,ngram-cache ./utils/serve-qwen38.sh start # draft-free ngram spec decode
# SERV_SPEC="" ./utils/serve-qwen38.sh start --mtp # MTP without ngram
# SERV_SPEC_N_MAX=16 SERV_NGRAM_N=12 SERV_NGRAM_M=48 SERV_NGRAM_MIN_HITS=1 # ngram-map-k tuning
#
# opencode: provider `llamacpp` / model `qwen3.8-2b` -> http://127.0.0.1:8081/v1,
# configured with limit.context = 262144 to match this server.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD="${LLAMACPP_BUILD:-$ROOT/build-work/llama.cpp/build-vulkan2}"
MODEL="${SERV_MODEL:-$ROOT/build-work/Qwen3.8-2B-Q4_K_M.gguf}"
MTP_HEAD="${SERV_MTP_HEAD:-$ROOT/build-work/mtp-Qwen3.8-2B-Q4_K_M.gguf}"
BIN="$BUILD/bin"
PORT="${SERV_PORT:-8081}"
HOST="${SERV_HOST:-127.0.0.1}"
CTX="${SERV_CTX:-262144}"
PARALLEL="${SERV_PARALLEL:-1}"
NGL="${SERV_NGL:-99}"
N_THREADS="${SERV_N_THREADS:-1}"
TEMP="${SERV_TEMP:-0.2}"
SPEC="${SERV_SPEC:-}"
SPEC_N_MAX="${SERV_SPEC_N_MAX:-16}"
NGRAM_N="${SERV_NGRAM_N:-12}"
NGRAM_M="${SERV_NGRAM_M:-48}"
NGRAM_MIN_HITS="${SERV_NGRAM_MIN_HITS:-1}"
NAME="qwen3.8-2b"
PIDFILE="${SERV_PIDFILE:-/tmp/opencode/serve-${NAME}.pid}"
LOGFILE="${SERV_LOGFILE:-/tmp/opencode/serve-${NAME}.log}"
if [[ ! -x "$BIN/llama-server" ]]; then
echo "Missing $BIN/llama-server. Build it first:" >&2
echo " cmake -S $ROOT/build-work/llama.cpp -B $BUILD -G Ninja -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DGGML_HIP=OFF" >&2
echo " cmake --build $BUILD -j\"\$(nproc)\" --target llama-server" >&2
exit 1
fi
if [[ ! -f "$MODEL" ]]; then
echo "Missing model: $MODEL (download first: hf download empero-ai/Qwen3.8-2B-GGUF Qwen3.8-2B-Q4_K_M.gguf --local-dir $ROOT/build-work)" >&2
exit 1
fi
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
build_args() {
args=(
-m "$MODEL"
-ngl "$NGL"
-t "$N_THREADS"
-fa off
-c "$CTX"
--parallel "$PARALLEL"
--temp "$TEMP"
--jinja
--host "$HOST"
--port "$PORT"
)
local spec_type="$SPEC"
if [[ "${1:-}" == "--mtp" ]]; then
if [[ ! -f "$MTP_HEAD" ]]; then
echo "Missing MTP head: $MTP_HEAD" >&2
exit 1
fi
args+=(-md "$MTP_HEAD")
spec_type="draft-mtp${SPEC:+,$SPEC}"
echo "Serving with MTP draft ($MTP_HEAD) — NOTE: slower than plain on this GPU." >&2
fi
if [ -n "$spec_type" ]; then
args+=(--spec-type "$spec_type")
args+=(--spec-draft-n-max "$SPEC_N_MAX")
if [[ ",$spec_type," == *",ngram-map-k,"* ]]; then
args+=(--spec-ngram-map-k-size-n "$NGRAM_N")
args+=(--spec-ngram-map-k-size-m "$NGRAM_M")
args+=(--spec-ngram-map-k-min-hits "$NGRAM_MIN_HITS")
fi
fi
}
health() { curl -s -m 2 "http://$HOST:$PORT/health" 2>/dev/null; }
daemon_alive() {
local pid; pid=$(cat "$PIDFILE" 2>/dev/null || true)
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}
cmd_start() {
if daemon_alive; then
echo "$NAME already running (pid $(cat "$PIDFILE")) on http://$HOST:$PORT"
return 0
fi
mkdir -p "$(dirname "$PIDFILE")"
build_args "${1:-}"
echo "Starting $NAME on http://$HOST:$PORT (ctx $CTX, parallel $PARALLEL, ngl $NGL)… log: $LOGFILE"
setsid nohup "$BIN/llama-server" "${args[@]}" > "$LOGFILE" 2>&1 < /dev/null &
echo $! > "$PIDFILE"
for _ in $(seq 1 60); do
[ "$(health)" = '{"status":"ok"}' ] && break
sleep 2
done
if [ "$(health)" = '{"status":"ok"}' ]; then
echo "Ready: $(health) (pid $(cat "$PIDFILE"))"
else
echo "WARNING: no healthy response yet — tail $LOGFILE" >&2
exit 1
fi
}
cmd_stop() {
if ! daemon_alive; then
echo "$NAME not running"
rm -f "$PIDFILE"
return 0
fi
echo "Stopping $NAME (pid $(cat "$PIDFILE"))"
kill "$(cat "$PIDFILE")"
sleep 2
if daemon_alive; then
kill -9 "$(cat "$PIDFILE")" 2>/dev/null || true
fi
rm -f "$PIDFILE"
echo "Stopped"
}
cmd_status() {
if daemon_alive; then
echo "$NAME: RUNNING (pid $(cat "$PIDFILE")) — health: $(health)"
else
echo "$NAME: stopped"
fi
}
case "${1:-}" in
start) shift || true; cmd_start "${1:-}" ;;
stop) cmd_stop ;;
status) cmd_status ;;
*) cmd_start "${1:-}" ;; # no/first-arg==--mtp -> foreground-style start (detached)
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment