fix(server): SEGV when --mtp-head + --mmproj are both passed#17
Open
WillowOneVision wants to merge 1 commit into
Open
Conversation
When llama-server is launched with both --mtp-head <assistant.gguf> (Gemma 4 MTP speculative decoding) and --mmproj <vision.gguf> (multimodal projector), the server consistently segfaults during slot initialization. Crash signature (gdb backtrace from core): #0 llama_context::n_batch() AtomicBot-ai#1 common_speculative_init(common_params_speculative&, llama_context*) AtomicBot-ai#2 server_context_impl::load_model AtomicBot-ai#3 main Root cause chain: 1. tools/server/server-context.cpp:738 sets params.speculative.type = NONE when mmproj is loaded (the WARN "speculative decoding is not supported by multimodal, it will be disabled"). 2. But params.speculative.mparams_dft.path (set via --mtp-head) is NOT cleared. Stale state. 3. common_speculative_init evaluates has_draft = !params.mparams_dft.path.empty() -> true has_mtp = (params.type == MTP) -> false (overridden) and falls into the legacy DRAFT branch. 4. common_speculative_state_draft ctor at speculative.cpp:227 calls llama_batch_init(llama_n_batch(ctx_dft), ...) where ctx_dft is nullptr (because params.model_dft was zeroed for the MTP case). Null deref. Two-layer defensive fix: - common/speculative.cpp: common_speculative_init returns nullptr early when params.type == COMMON_SPECULATIVE_TYPE_NONE. Defensive against any caller that disables speculative but leaves orphan params. - tools/server/server-context.cpp: at the mmproj-disable site, also clear params_base.speculative.mparams_dft.path and ...model_dft. Removes the orphan state at the source. Either layer alone is sufficient; both together provide defense-in-depth. Verified: - Repro before patch: SEGV on launch. - After patch: server boots cleanly, slots log "speculative decoding context not initialized", text-only and image+text requests both work, no crash. - Regression: same binary without --mmproj still gets MTP speedup (slots log "speculative decoding context initialized" 4/4). This patch prevents the crash but does NOT achieve concurrent MTP+vision operation (per-batch dispatch). It matches the WARN message intent (MTP cleanly disabled when mmproj is loaded). True coexistence is a separate scope (per-batch image-token detection + conditional draft head invocation).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
llama-serverconsistently segfaults during slot initialization when launched with both--mtp-head <assistant.gguf>and--mmproj <vision.gguf>. This PR diagnoses the root cause empirically (gdb backtrace from a core dump) and ships a minimal two-layer defensive fix that lets both flags coexist gracefully — MTP cleanly disabled when mmproj is loaded, no crash, all other paths preserved.Reproducer
./llama-server \ -m gemma-4-E4B-it-Q4_K_M.gguf \ --mtp-head gemma-4-E4B-it-assistant.Q4_K_M.gguf --spec-type mtp \ --draft-block-size 3 \ --mmproj gemma-4-E4B-it-mmproj-F16.gguf \ --host 127.0.0.1 --port 8090 -t 4 -c 4096 \ -ctk turbo4 -ctv turbo4 -ctkd turbo4 -ctvd turbo4Pre-patch log:
Backtrace from core
Root cause chain (10 steps)
common/common.cpp::common_init_from_paramscallsllama_model_load_mtp_from_file()to load the MTP assistant into the target model — no separate draft context exists by design.tools/server/server-context.cpp:668: whenparams_spec.type == COMMON_SPECULATIVE_TYPE_MTP, server-context setsparams_base.speculative.model_dft = nullptr(correct — no draft context).tools/server/server-context.cpp:736-739: whenmctx != nullptr(mmproj loaded), server-context overridesparams_base.speculative.type = COMMON_SPECULATIVE_TYPE_NONEand emits the WARN "speculative decoding is not supported by multimodal, it will be disabled". But it does NOT clearparams_base.speculative.mparams_dft.path(= the--mtp-headGGUF path).common/speculative.cpp:1250::common_speculative_is_compat(ctx_tgt)returns true — it only checks that the target context supports 2-token decode + partial sequence removal, independent of speculative type. Socan_spec = trueat server-context.cpp:777.slot.spec = common_speculative_init(params_base.speculative, slot.ctx).common_speculative_init: line 1293if (params.model_dft && params.type != MTP) { ctx_dft = init_from_model(...); }—model_dftis null (set at step 2), soctx_dftstaysnullptr.has_draft = !params.mparams_dft.path.empty()evaluates totrue(path was set by--mtp-headat CLI; never cleared at step 3).has_mtp = (params.type == MTP)evaluates tofalse(was overridden to NONE at step 3).if (has_draft) { if (has_mtp) {…} else { configs.push_back(DRAFT); } }— pushes aCOMMON_SPECULATIVE_TYPE_DRAFTconfig based on the orphaned path.common_speculative_state_draft(type, ctx_tgt, ctx_dft=nullptr, replacements). The ctor at line 227 callsbatch = llama_batch_init(llama_n_batch(ctx_dft), 0, 1)→llama_n_batch(nullptr)→ SEGV.In one sentence: the server's mmproj-disable cleanup at server-context.cpp:738 only clears
params.speculative.typebut leavesparams.speculative.mparams_dft.pathset, which letscommon_speculative_initpush a DRAFT config whose constructor then dereferences a nullctx_dft.Patch (two-layer defense)
Layer 1 —
common/speculative.cpp::common_speculative_initDefensive early bail when
type == NONE. Any future caller hitting the same conditions also gets protection.Layer 2 —
tools/server/server-context.cpp:736-739Clear the orphan path + pointer at the disable site.
Either layer alone is sufficient to prevent the crash; both together provide redundancy at the producer (server) and consumer (init) ends.
Verification
--mtp-head + --mmproj--mtp-head(no mmproj)The regression test is critical: it confirms the patch does not break the existing MTP-only path. The "initialized" status per slot proves the MTP draft head is loaded and ready to draft when text-only requests come in.
Not a true coexistence patch
This fix prevents the crash but does NOT achieve simultaneous MTP+vision (per-request dispatch). It cleanly disables MTP whenever mmproj is loaded, matching the WARN message's intent. True coexistence would require:
That work is significantly more invasive and is documented as a follow-up.
Test plan
tests/test-speculative-mtp.cppexercising the mmproj-loaded path.