feat(sdk)!: generalize llama_cpp speculative decoding to all types - #1195
Conversation
cf697fb to
88428ed
Compare
Test evidenceSnapdragon X Elite, Windows on ARM64. Build: Models
Prompt for every run: CommandsSet once per shell: Baseline (no spec): Ngram-mod (no draft model): Draft-mtp (needs the paired drafter): Results (verbatim
|
| device | run | [ok] line |
|---|---|---|
| CPU | baseline | ngl=0 ttft=11.8ms prefill=1211.5tps decode=116.5tps gen=20 tok |
| CPU | ngram-mod | ngl=0 ttft=22.7ms prefill=625.1tps decode=115.1tps gen=30 tok |
| GPU (Adreno/OpenCL) | baseline | ngl=-1 ttft=72.8ms prefill=192.8tps decode=73.2tps gen=20 tok |
| GPU | ngram-mod | ngl=-1 ttft=75.3ms prefill=186.9tps decode=72.4tps gen=30 tok |
| HTP0 | baseline | ngl=-1 ttft=104.5ms prefill=134.2tps decode=42.1tps gen=20 tok |
| HTP0 | ngram-mod | ngl=-1 ttft=107.7ms prefill=130.5tps decode=42.1tps gen=30 tok |
Draft-mtp, Gemma-4-12B-Q4_K_M + paired drafter, GPU:
[ok ] cell plugin=llama_cpp device=gpu(id=GPUOpenCL) ngl=-1 ttft=693.3ms prefill=20.2tps decode=1.7tps gen=30 tok
[spec] draft acceptance = 0.06667 (5 accepted / 75 generated)
What the numbers say
- No regression: baseline decode on all three devices matches upstream
llama-clibuilt from the same b9775 (local check: ~72–75 t/s GPU on this model). - ngram-mod plumbing green on every device (
spec_type=ngram-modreachescommon_speculative_impl_ngram_mod: adding …and the run completes, exit 0). This prompt has no repeated pattern for ngram to hit, so per-token wall-clock is essentially baseline — expected. The value ofngram-*is on code / summary / repetitive text, not one-shot Q&A. - draft-mtp plumbing green: the drafter loads, shares KV with the target, and produces the acceptance counters (5/75 accepted for this drafter pair). Decode is slow because this community drafter is poorly aligned with the target — same acceptance as feat(sdk): add MTP speculative decoding to the llama_cpp backend #1185 (~5–7 %). Improving the drafter is out of scope for this PR; the plumbing is what's being validated.
Negative paths
--spec-type draft-mtpwithout--draft-model→ clean error, no crash, andsetup_speculativelogs--spec-type 'draft-mtp' requires a draft model (--draft-model).- Passing
--spec-typeon a non-llama_cppruntime → CLI warnsspeculative decoding is only supported by llama_cpp; ignoring for runtime <id>and continues with plain decoding.
337dca3 to
9942157
Compare
Test evidence after submodule bumpSame X Elite host, same models, same commands as the previous evidence comment. Branch tip: Results (verbatim
|
| device | run | [ok] line |
|---|---|---|
| CPU | baseline | ngl=0 ttft=15.4ms prefill=925.4tps decode=80.9tps gen=20 tok |
| CPU | ngram-mod | ngl=0 ttft=27.3ms prefill=519.8tps decode=117.5tps gen=30 tok |
| GPU (Adreno/OpenCL) | baseline | ngl=-1 ttft=72.0ms prefill=195.0tps decode=75.8tps gen=20 tok |
| GPU | ngram-mod | ngl=-1 ttft=74.0ms prefill=189.9tps decode=76.1tps gen=30 tok |
| HTP0 | baseline | ngl=-1 ttft=37.9ms prefill=371.8tps decode=56.4tps gen=20 tok |
| HTP0 | ngram-mod | ngl=-1 ttft=44.2ms prefill=320.1tps decode=55.9tps gen=30 tok |
Draft-mtp, Gemma-4-12B-Q4_K_M + paired drafter, GPU:
[ok ] cell plugin=llama_cpp device=gpu(id=GPUOpenCL) ngl=-1 ttft=565.0ms prefill=24.8tps decode=1.8tps gen=30 tok
[spec] draft acceptance = 0.06667 (5 accepted / 75 generated)
Delta vs previous b9775 evidence
| device | b9775 decode | b10056 decode | Δ |
|---|---|---|---|
| CPU baseline | 80.8 | 80.9 | ~ |
| GPU baseline | 73.2 | 75.8 | +4 % |
| HTP0 baseline | 42.1 | 56.4 | +34 % |
| CPU ngram-mod | 115.1 | 117.5 | +2 % |
| GPU ngram-mod | 72.4 | 76.1 | +5 % |
| HTP0 ngram-mod | 42.1 | 55.9 | +33 % |
| GPU draft-mtp | 1.7 (acc 5/75) | 1.8 (acc 5/75) | ~ |
Prefill: HTP prefill 134 → 372 tok/s (~3×). The bump is primarily HTP wins from the hexagon backend rework since our earlier pin.
The hexagon backend rework since our previous pin (b9775) is now stable on Windows-ARM64 and materially faster on HTP0 — verified end-to-end on Snapdragon X Elite (see PR evidence comment): HTP decode 42 -> 56 tok/s (+34%), prefill 134 -> 372 tok/s. GPU / CPU baselines unchanged. draft-mtp and ngram-mod paths remain green. Companion tweaks: - Refresh sdk/patches/llama-hexagon-release-sessions.patch anchor line numbers against the new tip; content unchanged. - sdk/plugins/llama_cpp/src/vlm.cpp: init the new mtmd_input_text.text_len field (upstream added it; leaving it uninitialised silently corrupts VLM tokenisation). Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Replace the MTP-only spec_draft_model/spec_n_draft fields on geniex_ModelConfig with a generalized speculative config: spec_type (comma-separated llama.cpp type names) + spec_draft_model + spec_n_max/spec_n_min/spec_p_min. The plugin's setup_speculative now parses spec_type via common_speculative_types_from_names and covers all nine types on b9775 — draft-model (mtp / eagle3 / draft-simple, needs a draft GGUF) and self-speculative (ngram-simple / ngram-map-k / ngram-map-k4v / ngram-mod / ngram-cache, no draft model). The draft context path keeps our device-selection pinning so a Hexagon target target does not lose the drafter to default placement. geniex-bench mirrors the flags. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Wire the generalized speculative fields through geniex infer. --spec-type accepts one or a comma-separated set of llama.cpp type names; --draft-model resolves either a local GGUF path or a catalogue name (auto-pulling via the model manager, matching the main-model argument's behaviour). Non-llama_cpp runtimes warn and drop the spec flags rather than erroring. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
reacquire_before_load() calls the HTP FastRPC session-reacquire proc unconditionally, which can crash on CPU/GPU inference paths when the ADSP domain is in a bad state. Only reacquire when the target device is actually HTP (npu). cpu / gpu targets no longer depend on the ADSP domain's health. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
…droid) Mirror geniex_ModelConfig's new spec_type / spec_draft_model / spec_n_max / spec_n_min / spec_p_min and geniex_ProfileData's draft_n_total / draft_n_accepted across the three FFI mirrors per CONTRIBUTING §4. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
9942157 to
8ab8bfc
Compare
Draft only ever runs n_max tokens per verify step, but was inheriting the target's n_batch=2048/n_ubatch=1024, allocating ~2.3 GiB of HTP0 scratch and OOMing on X2 Elite. Cap both at max(64, n_max). Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
HTP + draft-mtp on Snapdragon X2 Elite (SC8480XP)Same target/draft/prompt as the upstream tutorial. Bench was
Upstream Also fixed here ( Commands (target = X2 Elite, models pulled via |
Adapt to upstream API break: llama_model_params.use_mmap /
use_mlock booleans were replaced by a single load_mode enum
(LLAMA_LOAD_MODE_{NONE,MMAP,MLOCK,MMAP_MLOCK,DIRECT_IO}).
Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
The rationale ("drafter grabs HTP0 and breaks the MTP graph") no
longer applies — upstream now takes device selection via
params.speculative.draft.devices.
Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Two bugs were preventing correct multi-turn inference: 1. prefix-match rollback passed p1 = n_past - match_len to llama_memory_seq_rm, which removed only a small window and left stale KV past match_len. Use -1 (to end) instead so the tail is fully evicted before the new turn's prefill. 2. spec prefill went through llama_batch_get_one and skipped common_speculative_process, so the MTP hook's pending_h stayed at turn 1's last h_nextn while the target KV advanced into turn 2 - the first draft() then failed with llama_decode(ctx_dft) = -1. When spec is enabled, prefill now builds an explicit batch and feeds it through common_speculative_process too. Also drop the manual llama_set_embeddings toggle in setup_speculative: the MTP hook uses the staging llama_set_embeddings_nextn API and does not need the main embeddings output enabled; keeping it on suppressed the target's logits and produced empty completions on turn 2. Verified on X2 Elite (SC8480XP) with gemma-4-26B target + assistant draft, two-turn "My name is Alice / What is my name?" recall. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
geniex infer --spec-type / --draft-model / --draft-tokens / --draft-min / --draft-p-min already worked, but the same flags on `geniex run` were parsed and silently dropped: the client didn't put them on the HTTP request, and the server didn't read them off ChatCompletionRequest. Wire the full path: - ChatCompletionRequest: add spec_type / spec_draft_model / spec_n_max / spec_n_min / spec_p_min JSON fields. - ModelParam: carry the same knobs so the keepalive cache keys on them (switching spec settings now correctly rebuilds the model). - ResolveModelParam: accept a SpecParam struct; zero for non-llama_cpp runtimes so the qairt plugin's param-guard is not tripped. - keepAliveGet (LLM): plumb the spec knobs into geniex_sdk.ModelConfig, and resolve --draft-model via a new resolveDraftModelPath helper that treats an existing filesystem path as-is and pulls a catalogue name (with optional :precision suffix) through geniex_sdk.ModelPull on first use - matching what `geniex infer` already does. - run.go: append the five spec fields to both the warm-up and the streaming /chat/completions requests. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
The three FIX-tagged patches in LlamaLlm::create (HTP reacquire, gpt-oss tensor override, HTP guard marking) were already in braced blocks; brace the fourth (npu reacquire gate) too. The device-selection block stays flat because 'selection' must outlive mpar.devices, which points into its buffer. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
keepAliveGet already assumes the target model is cached (via geniex_sdk.ModelGetPaths); apply the same rule to the draft. Users must `geniex pull` the draft repo beforehand — the server errors on a missing cache entry rather than downloading in the middle of a request. `geniex infer --draft-model` still auto-pulls in the CLI side; only the serve path is trimmed. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Add spec_type / spec_draft_model / spec_n_max / spec_n_min / spec_p_min to the ChatCompletionRequest schema; documents the enum values and notes that the server does not auto-pull the draft model. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Add three multi-turn "recall Alice" tests to test_llama_cpp_llm.py: - test_multi_turn_recalls_prior_turn (cpu/npu/gpu): baseline, no speculative; covers apply_chat_template + prefix-match KV rollback. - test_multi_turn_with_ngram_simple (cpu/npu/gpu): self-speculative variant that reuses the LLM fixture (Qwen3-4B Q4_0) - no MTP model required. - test_multi_turn_with_draft_mtp (npu only): draft-mtp against the gemma-4-A4B target + RachidAR assistant draft (added to _models.py as LLAMA_CPP_MTP_*). NPU-only because the target is ~15GB. Also fixes a null-deref exposed by the ngram-simple test: decode_speculative() called llama_get_memory(this->draft_ctx) and llama_memory_seq_rm(mem_dft, ...) unconditionally, but ngram-* types are self-speculative and leave draft_ctx null - the calls now guard on draft_ctx / mem_dft. Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Signed-off-by: Mengsheng Wu <mengshen@qti.qualcomm.com>
Summary
Extend the
llama_cppbackend from MTP-only (#1185) to all speculative types llama.cpp exposes upstream.CLI
--spec-type(comma-separated for chaining, llama.cpp picks in fixed priority):draft-mtp,draft-eagle3,draft-simplengram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache--draft-modeltakes a catalogue name (auto-pulled like the main model) or a local GGUF path.Examples (verified on Snapdragon X Elite,
GENIEX_PLUGIN_QAIRT=OFF):FFI (breaking)
geniex_ModelConfig:spec_draft_model+spec_n_draft→spec_type+spec_draft_model+spec_n_max+spec_n_min+spec_p_min. Mirrored across Go, Python, Android per CONTRIBUTING §4.geniex_ProfileDatakeepsdraft_n_total/draft_n_accepted.llama.cpp submodule
Bumped to master (b10056-38,
6d5a910c5). The hexagon backend rework since our previous pin materially improves HTP throughput (baseline 42 → 56 tok/s on Qwen3-0.6B, +34 %). Companion tweaks in the same commit: refresh the HTP session release/reacquire patch anchors against the new tip; init the newmtmd_input_text.text_lenfield upstream added.Supersedes #1185.
Test plan
Snapdragon X Elite,
GENIEX_PLUGIN_QAIRT=OFF, Qwen3-0.6B-Q8_0 + Gemma-4-12B-MTP.--spec-type) — decode 80.9 / 75.8 / 56.4 tok/s--spec-type ngram-mod— no draft model, no crashes, ngram counters populate--spec-type draft-mtp --draft-model <drafter>— draft acceptance reported (5/75 for this drafter pair; matches feat(sdk): add MTP speculative decoding to the llama_cpp backend #1185)--draft-modelerrors cleanly, non-llama_cpp runtime warns and drops the flags