Summary
Running a Qwen3.8-27B dense GGUF with speculative MTP (--spec-type draft-mtp --spec-draft-adaptive) on the Strix Halo (gfx1151, native Vulkan build) crashes the server with SIGSEGV after completing one request, when a second request starts decoding.
The crash frame is llama_ple_disk::page_cached(), but that function is 5 instructions and cannot fault itself — the this pointer it receives is garbage (it contains string bytes), so this is heap corruption upstream of the call. This started with the PLE-on-disk / lazy-mode work merged in 77024dd95 --ngram-on-disk and lazymode merge (#29).
Environment
- Build: strix-official
/home/gsq/strix-official, HEAD = origin/master 8c1c282ec (build 11124), clean tree (git status empty). Local == GitHub master, so it is upstream code as-is.
- HW/OS: AMD Strix Halo APU (Radeon 8060S gfx1151), Arch / Omarchy, 64 GB unified memory, Vulkan/RADV backend.
- Reproduces on two different models (unsloth Qwen3.8-27B and huihui Qwen3.8-27B-abliterated) → not a model-file issue.
Reproduction
Any normal llama-server invocation of the 27B dense GGUF, e.g.:
llama-server -m <qwen3.8-27b-dense-q4gguf> --device Vulkan0 -c 98304 -ngl 999 \
--load-mode none -fa on --cache-type-k q8_0 --cache-type-v q8_0 \
--batch-size 4096 --ubatch-size 512 -t 16 --parallel 1 --jinja --cache-prompt \
--spec-type draft-mtp --spec-draft-adaptive --spec-draft-n-min 3 --spec-draft-n-max 4 \
... --mmproj <mmproj.gguf>
No --lazy-mode / --ngram-on-disk flag is passed, and no LLAMA_ARG_LAZY_MODE/LLAMA_ARG_NGRAM_ON_DISK env is set. The crash is deterministic: 7/7 runs die with SIGSEGV at the same frame.
Evidence
-
Repeated identical crash — coredumpctl list: every run is SIGSEGV (si_code: SI_KERNEL), e.g. PIDs 66933, 67449, 67868, 71127, 74154, 74406, 80400 on the same day.
#0 llama_ple_disk::page_cached() const
#1 qwen4exp_ple_prefetch(llama_model const&, ...)
#2 llama_context::decode(llama_batch const&)
#3 llama_decode
#.. server decode loop / update_slots / start_loop
-
The faulting instruction is the first of page_cached() (a load of the vtable/object pointer), and rdi (this) holds string bytes — captured with gdb from the core:
=> page_cached()+0 : mov (%rdi),%rax ; SIGSEGV here
rdi = 0x22202c22676e6964
rdi as a little-endian byte sequence is "ding, 2" — i.e. the object pointer was overwritten with text data. A valid call would have a heap pointer in rdi.
-
The process was mid-request when it died. Server log: task 0 completes (prompt eval + token output + MTP draft acceptance), slot released, then task 2 starts → crash. So the server handled one request fine and corrupts on the subsequent decode — consistent with a use-after-free / buffer overrun in the PLE path rather than at model load.
-
Not OOM (64 GB machine; no oom-kill in journal) and not a missing-flag issue: no PLE n-gram table stays on disk: log line appears anywhere, confirming the ple-disk object was not intentionally built by lazy-mode for these runs.
Source cross-reference (HEAD 8c1c282ec)
// src/models/qwen4exp.cpp:1748 qwen4exp_ple_prefetch(...)
if (!tokens || n_tokens < 4096) return;
const auto & pmodel = static_cast<const llama_model_qwen4exp &>(model_base);
if (!pmodel.ple_disk || !pmodel.ple_disk->page_cached()) return; // <- this line
// src/llama-ple-disk.cpp:391
bool llama_ple_disk::page_cached() const { return !pimpl->direct; } // no pimpl null-check
At minimum page_cached() should be defensive (pimpl &&), but that would only mask the corruption. The real defect is the corrupted ple_disk/PLE object being present and reachable in decode despite lazy-mode not having built it for this model — pointing at lifetime / shared_ptr handling of ple_disk introduced with the PLE-on-disk merge (77024dd95).
Suggested next steps
- Add an assert/null-check + log in
qwen4exp_ple_prefetch and page_cached() to make this fail loudly instead of segfaulting.
- Audit the lifetime/ownership of
llama_model_qwen4exp::ple_disk (shared_ptr reset timing) around decode with speculative MTP — especially whether it can be observed concurrently while being destroyed/reset, or whether PLE scratch state is shared between slots without a barrier.
Question for maintainers
Is the combination dense Qwen3.x + --spec-type draft-mtp --spec-draft-adaptive on Vulkan supported in this build? If MTP on dense Qwen should avoid the PLE/prefetch path, I can also verify with MTP disabled as a fallback.
Summary
Running a Qwen3.8-27B dense GGUF with speculative MTP (
--spec-type draft-mtp --spec-draft-adaptive) on the Strix Halo (gfx1151, native Vulkan build) crashes the server with SIGSEGV after completing one request, when a second request starts decoding.The crash frame is
llama_ple_disk::page_cached(), but that function is 5 instructions and cannot fault itself — thethispointer it receives is garbage (it contains string bytes), so this is heap corruption upstream of the call. This started with the PLE-on-disk / lazy-mode work merged in77024dd95 --ngram-on-disk and lazymode merge (#29).Environment
/home/gsq/strix-official, HEAD = origin/master8c1c282ec(build 11124), clean tree (git statusempty). Local == GitHub master, so it is upstream code as-is.Reproduction
Any normal
llama-serverinvocation of the 27B dense GGUF, e.g.:No
--lazy-mode/--ngram-on-diskflag is passed, and noLLAMA_ARG_LAZY_MODE/LLAMA_ARG_NGRAM_ON_DISKenv is set. The crash is deterministic: 7/7 runs die with SIGSEGV at the same frame.Evidence
Repeated identical crash —
coredumpctl list: every run isSIGSEGV (si_code: SI_KERNEL), e.g. PIDs 66933, 67449, 67868, 71127, 74154, 74406, 80400 on the same day.The faulting instruction is the first of
page_cached()(a load of the vtable/object pointer), andrdi(this) holds string bytes — captured with gdb from the core:rdias a little-endian byte sequence is"ding, 2"— i.e. the object pointer was overwritten with text data. A valid call would have a heap pointer inrdi.The process was mid-request when it died. Server log: task 0 completes (prompt eval + token output + MTP draft acceptance), slot released, then task 2 starts → crash. So the server handled one request fine and corrupts on the subsequent decode — consistent with a use-after-free / buffer overrun in the PLE path rather than at model load.
Not OOM (64 GB machine; no oom-kill in journal) and not a missing-flag issue: no
PLE n-gram table stays on disk:log line appears anywhere, confirming the ple-disk object was not intentionally built by lazy-mode for these runs.Source cross-reference (HEAD
8c1c282ec)At minimum
page_cached()should be defensive (pimpl &&), but that would only mask the corruption. The real defect is the corruptedple_disk/PLE object being present and reachable in decode despite lazy-mode not having built it for this model — pointing at lifetime / shared_ptr handling ofple_diskintroduced with the PLE-on-disk merge (77024dd95).Suggested next steps
qwen4exp_ple_prefetchandpage_cached()to make this fail loudly instead of segfaulting.llama_model_qwen4exp::ple_disk(shared_ptr reset timing) around decode with speculative MTP — especially whether it can be observed concurrently while being destroyed/reset, or whether PLE scratch state is shared between slots without a barrier.Question for maintainers
Is the combination dense Qwen3.x +
--spec-type draft-mtp --spec-draft-adaptiveon Vulkan supported in this build? If MTP on dense Qwen should avoid the PLE/prefetch path, I can also verify with MTP disabled as a fallback.