Conversation
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Adds the MTP head's own hyper-connection mixer tensor names and lists the NextN tensors under the qwen4exp architecture.
Adds --spec-type draft-mtp support for Qwen3.8-Flash-Next. The MTP head folds the next token's embedding into the trunk's wide hyper-connection residual, runs one trunk-style block (dense attention + MoE) over it, and collapses the result with its own mixer before reusing the trunk's LM head. - read nextn_predict_layers so n_layer() excludes the MTP block - load the trailing block through the existing trunk path: is_recr() and is_ple() are already false past the trunk, so it needs no special casing - eh_proj fuses the checkpoint's fc_embedding and fc_hidden side by side, so one matmul computes fc_embedding@e + fc_hidden@h - the head carries its own hyper-connection mixer, mirroring the trunk's hc_head_*, which stands in for the output norm qwen4exp does not have - export the wide pre-collapse residual as t_h_nextn from both graphs, so the driver can feed it back for the next draft step - route MTP contexts to a plain KV cache filtered to the trailing layer The draft block attends densely for now: the trunk's QSA only prunes context past a 2048-token budget, so dense is a numerical superset and drafts are verified either way. Indexer tensors are still loaded.
The MTP block is one trunk-shaped block (dense attention + MoE wrapped in hyper-connections) plus a head-level combiner, so once _QwenMtpMixin renames mtp.layers.0.* to the trailing block index its tensors ride the existing qwen4exp mappings unchanged. Two head-level pieces need handling: - fc_embedding and fc_hidden fuse into the eh_proj the shared NextN code expects, since W_e@e + W_h@h == [W_e|W_h] @ concat(e, h) - mtp.hyper_connection_mixer.* is the head's own copy of the trunk's hc_head_* output mixer, unindexed in the checkpoint and per-block in the GGUF compress_ratios is read with length block_count, so it gains a trailing 0 for the MTP block, which attends densely. --no-nextn drops the head; --mtp exports it on its own.
A NextN/MTP draft exported with --mtp carries the token embeddings, output norm and lm head so it can be loaded as a standalone model. For every current sidecar those three tensors are most of the file: ggml-org/Qwen3.8-27B-GGUF mtp-Qwen3.8-27B-Q4_0.gguf is 1.565 GiB, of which 1.332 GiB (85%) is the copy, against 0.223 GiB for the MTP block itself. Add an opt-in --mtp-shared-embd that leaves them out and marks the file with nextn_shared_target_tensors. The loader then resolves those names against the already loaded target model. The graph side needs no change: the nextn blocks of twelve archs already fall back to model.tok_embd and model.output. The borrow is gated on the new key, so a sidecar published before this change cannot reach it and keeps its current behaviour. Shapes are checked against the target and a mismatch is refused, as is loading such a file on its own.
The graph cache is keyed on cgraph->nodes[0] alone, so two evaluations that share a first node but differ in shape collide on one entry. Warmup needs two consecutive calls with unchanged node properties, so a workload whose batch shape varies resets warmup on nearly every call and falls back to eager launch. Speculative decoding is exactly that workload. The qwen4exp verify batch is distributed 2:13 percent, 3:11 percent, 4:75 percent as the accepted count varies, where qwen35 sits at 4:98 percent and is effectively constant. Host launch time for the qwen4exp target decode was 1.52 ms with the draft head disabled and 12.35 ms with it enabled, while GPU time was unchanged, so the regression was entirely host side. The key now mixes the first node, the last node and the node count. This is O(1) rather than a walk over every node: the existing uid early return fires on 127 of 128 decodes, so the hot path must not touch node data. An earlier all-nodes hash reintroduced exactly the per-node walk a CUDA graph exists to avoid. Measured overhead against the previous key is 0.2 to 0.6 percent, with both variants built into one binary to avoid comparing across runs. Capture churn on Qwen3.8-27B UD-Q2_K_XL drops from 52 captures and 50 destroys to 4 and 0, with identical output md5 and an unchanged speculative ratio. Across 14 distinct prefill shapes the cache instantiates 16 entries against 14 before, with no destroys and no growth, and is capped at 64 by LRU on top of the existing sweep. test-backend-ops passes 13646 of 13646 on CUDA0, and Llama-3.2-1B-Instruct Q8_0 is byte identical with no throughput change.
_QwenMtpMixin is not a ModelBase subclass, so it re-declares the attributes it reads off cls for the type checker. filter_tensors reads cls.mtp_shared_embd without a matching declaration, which ty reports as unresolved-attribute. The declaration is a bare annotation, matching no_mtp and mtp_only above it. That creates no class attribute, so it cannot shadow ModelBase.mtp_shared_embd even though the mixin precedes the model class in the MRO; a default value here would have. Assisted-by: Claude
The previous key returned cgraph->nodes[0] without dereferencing it, so an empty graph was harmless. The shape-aware key reads nodes[0]->ne[], which is not, and neither call site checks n_nodes. Assisted-by: Claude
A draft-only export declares the full block count but ships the MTP block alone, so the trunk tensors load as null and only the MTP graph is buildable. Context reservation builds the trunk graph, which walked those nulls and segfaulted. A shared-embedding draft is caught earlier by the borrow check, since it has no token_embd of its own. A self-contained draft keeps one, so it passed that check and reached here. Assisted-by: Claude
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Claude Opus 5
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
* server: refactor subproc handling * fix Windows build * download: keep concurrent downloads of one blob apart Every process writes the same path + .downloadInProgress, so a second download of the same blob finds that file, takes it for its own partial transfer and asks for the bytes after it, which produces a corrupt result. The in-progress file now carries the pid of the process writing it. std::rename also replaces an existing destination on POSIX but fails on Windows, so a download whose blob appeared in the meantime is dropped after every retry and an etag rewrite silently keeps the old value. std::filesystem::rename has the POSIX behaviour everywhere, and the error now carries the reason reported by the system. * Revert "download: keep concurrent downloads of one blob apart" This reverts commit 917b83f. * tests: serialize the router tests that download the same model Parallel workers share one cache, so the two tests fetch the same blob into the same in-progress file and race to rename it. They now take a file lock around the download, like the session fixture does for the preset models. * Revert "tests: serialize the router tests that download the same model" This reverts commit c368a4a. --------- Co-authored-by: Pascal <admin@serveurperso.com>
Assisted-by: Codex # Conflicts: # src/CMakeLists.txt
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Advise read-mostly + prefetch to the allocating device for unified-memory buffers so lazily-migrated pages settle on the owning GPU. Co-Authored-By: opencode <noreply@opencode.ai> (cherry picked from commit ba2c46f)
Defer queued requests when resident-ctx-sum + candidate prompt would reach the threshold; deferred tasks retry on slot release. Auto-slot path only (explicit id_slot bypasses). Vanilla slot accounting: max(prompt-cache tokens, full task prompt length) per processing slot. Env: LLAMA_ARG_PARALLEL_CTX_THRESHOLD. Co-Authored-By: opencode <noreply@opencode.ai> (cherry picked from commit d065374)
Mirror the defer SRV_INF with an admit line (resident + candidate < threshold) so boundary tests can show the exact accounting either way. Co-Authored-By: opencode <noreply@opencode.ai> (cherry picked from commit 0ed2ac3)
… exceeding threshold alone The gate deferred any request with resident + candidate >= threshold, including resident == 0. A lone request bigger than the threshold on an otherwise idle pool then defers forever: nothing is resident, so no slot release ever retries it. The threshold guards combined oversubscription between concurrent requests, not a single request's own size (that is bounded by the per-slot cap). Defer now requires resident > 0; a lone request always admits. Admit log distinguishes the lone case so the printed comparison stays accurate. Co-Authored-By: opencode <noreply@opencode.ai> (cherry picked from commit 3ab0fde)
… one task A release event re-posted a single deferred task (FIFO head, or one explicitly requesting the slot). If that task re-deferred - e.g. the hydra#747 threshold gate deferring a large candidate - the release was burned and every other waiter stayed queued even with a slot idle, while brand-new arrivals kept getting served from the main queue (priority inversion, silent client hangs). Re-post the entire deferred FIFO in order (explicit-slot matches first) to the front of the main queue so every waiter is re-evaluated against the freed capacity before newer arrivals; tasks that still cannot proceed re-defer to the back, preserving FIFO order. Deterministic repro + production case: hydra#747 task-12239 hang (docs/investigations/740-results-report.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 1d3c4a8)
…t head Reconciles cdd1102's hand-rolled qwen4exp NextN/MTP draft head with the gs/qwen4exp-mtp lineage already carried by this branch. Findings: the two NextN/MTP draft-head implementations are the same code (same LLM_TENSOR_NEXTN_HC_HEAD_* enum/names, same llama_layer_nextn.hc_head_* fields, same graph_mtp / no_build_t shape, same qwen4exp.cpp graph). The only real difference is how a draft borrows the target's token embeddings and LM head: * gs lineage: at model-load time, opt-in via the GGUF key {arch}.nextn_shared_target_tensors + llama_model_params.model_shared, resolved in llama_model_loader::borrow_shared_tensor(). Graph side just asserts the tensor exists. Wired end to end (converter --mtp-shared-embd, common passes mparams.model_shared = model_tgt). * cdd1102: at graph-build time, qwen4exp-only, via qwen4exp_shared_model(cparams.ctx_other, ...), with token_embd marked TENSOR_NOT_REQUIRED for mtp_only files. Resolution: keep the gs loader-level mechanism everywhere. Rationale below. Conflicts resolved to the gs side: * src/llama-model.h comment only (hc_head_* fields already identical) * src/models/models.h comment only (no_build_t / graph_mtp already identical) * src/models/qwen4exp.cpp token_embd flags (loader guarantees it is present or borrowed, so flags=0 is correct); n_ff_exp(il) (gs API); borrow paths (handled by the loader, not the graph) Auto-merged cdd1102 deltas reverted to the gs side: * common/speculative.cpp is_mem_shared gemma4 gate. Unnecessary in gs: llama_context resets cparams.ctx_other to null and only sets it for Gemma4Assistant/EAGLE3/DFlash, so llama_get_ctx_other(ctx_dft) is null for qwen4exp and is_mem_shared is already false. The gate existed only to counteract cdd's own ctx_other use for graph-time borrow. * src/llama-context.cpp QWEN4EXP added to the EAGLE3/DFLASH ctx_other check. Inert with loader-level borrow (qwen4exp tok_embd/output are never null), so it is dropped rather than carried as dead relaxation. cdd1102's remaining changes (llama-model.cpp mtp_on_hybrid_qwen QWEN4EXP, llama-arch.* NEXTN_HC_HEAD_*) are already present unchanged in the gs lineage. Result: the merge tree is byte-identical to efd26f2; no unique cdd code is lost and no cdd divergence is retained.
Empirical E2E of the resolved tree — 2026-09-12Rig freed, production stopped. Built the resolved tree ( 1) Production-shaped self-contained model — PASS
2) qwen4exp shared-draft borrow + MTP — PASSReal qwen4exp pairing:
Correction to my own earlier readingWhile setting up I briefly concluded from VerdictNo PR-blocking finding. Both flagged risk assumptions hold empirically. The only substantive observation is that Hydra's production model is Stack shut down cleanly; both GPUs back to 1 MiB used. |
Fix two -Werror build failures hit by the CUDA CI job (gcc 13, LLAMA_FATAL_WARNINGS=ON) on the qwen4exp lineage: - ggml_cuda_mul_mat_id_grouped_host_staged: cast ids->ne[0] to size_t before comparing against the SIZE_MAX bound (sign-compare). - ggml_cuda_try_fuse: reserve the topk-moe op vector up front. gcc 13 flags the growing std::vector<ggml_op> insert with a -Wstringop-overflow false positive. Assisted-by: opencode
CI status (honest)Head updated to
Verified locally by compiling Remaining red (blocks merge)
Cause is Reproduced deterministically (4/4) on Other CI failures (not caused by this change)
Do not merge until #121 is resolved and the CI matrix is green. |
opened cdd11021b's hand-rolled qwen4exp NextN/MTP draft head and the gs qwen4exp-mtp lineage are the same code; only the embedding/LM-head borrow mechanism differs. Resolved to gs's loader-level mechanism; merge tree is byte-identical to efd26f235. PR ddvnguyen/llama.cpp#120 open against baseline-flash-next (review only, not merged). Refs #763
Rig window (production stopped, GPUs 1 MiB): - qwen35 production model + same-file MTP: PASS, 41.5% acceptance, 32.57 tok/s, coherent. Note production is qwen35 (not qwen4exp). - qwen4exp apex-mini + shared draft sidecar (nextn_shared_target_tensors): PASS. Standalone-refused, paired-loads -> loader borrow confirmed; 63.9% acceptance, coherent -> is_mem_shared false / no corruption. No PR-blocking finding. Detail in ddvnguyen/llama.cpp#120 comment. Stack shut down; GPUs back to 1 MiB. Refs #763
The target-verification intent was validated for every batch. But the plain kv-cache splitter (split_simple) describes each row as its own sequence, so grouped MoE target verification failed for single-stream models with "ubatch does not match the validated execution intent". Give the batch allocr a verification span hint. When the span is an atomic target verification, split_simple groups the rows by sequence, matching split_equal. Only require the grouped intent and certificate when grouped MoE execution is active, so non-grouped backends keep the plain split. Fixes #121 Assisted-by: opencode
|
#121 fixed in Verification on the rig:
Fix groups the target-verification ubatch by sequence only when grouped MoE execution is active ( |
Corrected qwen4exp MTP benchmark — the earlier 0.79x was a UM config errorRoot cause: the previous run set Common config (all arms): fork
RPC-split = Findings
Harness finding (for ggml-org#765)
|
Deep + concurrent arm: qwen4exp apex at ~74.5K depth x 2 slotsProduction-relevant shape (per CLAUDE.md multi-agent design): Fit / config (verified before committing to the run)Load (single-process dual-GPU fit, no RPC): n_slots=2, n_ctx_slot=98304, kv_unified=false. Method (PR ggml-org#765 checkpoint-seed harness)Grow 2 concurrent sessions to depth via Actual resident depth reached: 74,525 / 74,641 tokens (target 80K; harness word->token ratio ran ~7% short). Both slots ≈74.5K. Decode results (aggregate = sum of both concurrent slots' tokens / run span)
Per-slot (200 tok): no-draft 6.11 + 6.11; MTP 7.92 + 8.88. MTP acceptance over the run: 1323/1879 draft tokens = 70.4%. Notes
|
Deep + concurrent arm — production KV pin (K=q8_0, V=q5_1)Re-run of the ~74.5K x 2 /
vs q8_0/q8_0 (previous comment)
No-draft essentially unchanged (~+2%), MTP aggregate +14% and acceptance +5 pts with V=q5_1 at this depth/concurrency. MTP remains strongly net-positive at production shape. No OOM/paging; UM unset throughout. |
|
--moe-expert-cache-size |
tok/s (mean) | vs off |
|---|---|---|
| 0 (disabled) | 20.40 | — |
| 2 | 9.11 | -55.3% |
| 4 | 9.70 | -52.4% |
| 8 | 10.13 | -50.3% |
| 16 | 13.62 | -33.2% |
| 32 | 16.09 | -21.1% |
Per-prompt: N0 [19.96, 20.57, 20.67], N2 [8.97, 9.30, 9.04], N4 [9.71, 9.81, 9.58], N8 [10.18, 10.29, 9.94], N16 [13.94, 13.92, 13.00], N32 [16.68, 15.95, 15.63].
Finding: on this 2-GPU rig the expert cache is a net loss at this shape. The fit solver keeps experts resident, so enabling the cache routes them through the PCIe LRU path and costs ~55% at N=2; it recovers monotonically with size but never reaches the no-cache baseline up to N=32. Recommendation: leave --moe-expert-cache-size off for apex at these shapes and use it only for memory-constrained profiles (larger ctx / higher parallelism) that would otherwise evict experts.
Correction: an earlier "MTP 10.15 -> 16.68 tok/s" comparison was not MTP. With --spec-type draft-mtp there was 0 draft acceptance (no draft acceptance log line; no draft fields in the responses) and the main graph hit required grouped execution failed: grouped plan unavailable 63-95x before falling back. The two points were just this cache curve at a non-default --fit-target, inside the slow cache regime. Proper apex plain-decode baseline is 20.4 tok/s. The MTP interaction is tracked separately (see linked issue).
Assisted-by: opencode
|
Follow-up to the cache-size sweep above: the MTP interaction (no draft acceptance + repeated Assisted-by: opencode |
APEX model memory layout: 28.8 GB of the 78.7 GB is a sparse PLE n-gram table that stays on diskVerification while looking at the ~20 tok/s decode ceiling. The model is 78.7 GB on disk but the real working set is ~50 GB. GGUF tensor split (all 6 shards): What the 28.8 GB tensor is: a qwen4exp PLE (per-layer embedding) n-gram table. Metadata 16 rows x 160 dims x ~90 bytes/row = ~1.4 KB/token, random access. Live measurement (mmap default, t8, fit-target 1536,3072, c8192): Consequences:
Assisted-by: opencode |
Update 2026-09-12: #121 fixed in
f6301d4eed659bcefaadded a target-verification validator that requires a span-grouped ubatch(
n_seq_tokens == verification_span). The server marks that span for every spec method, but theplain
llama_kv_cachepath splits withsplit_simple, which reports one sequence per row. Sosingle-stream models failed
ubatch does not match the validated execution intentand the serverreturned HTTP 500. The grouped CUDA certificate imposes the same grouped-row shape.
Fix (localized, no qwen4exp-specific branches):
src/llama-batch.{h,cpp}: add averification_spanhint to the batch allocr.split_simpledelegates to
split_equalwhen the batch is an atomic target-verification span, keeping the rowsgrouped by sequence (the same shape the hybrid memory path already produces).
src/llama-context.cpp: set the span hint and require/forward the MAIN target-verification intentand certificate only when grouped MoE execution is active
(
required_grouped_execution_flags(moe_expert_cache_slots, moe_required_grouped_execution_supported) != NONE).Non-grouped backends keep the plain pre-
d659bcefasplit. DRAFT/MTP intents are unchanged.Blast radius: only grouped MoE execution changes; non-grouped backends behave exactly as before.
Unrelated observation
A local
LLAMA_FATAL_WARNINGS=ONbuild (GCC 15) trips pre-existing warnings inggml/src/ggml-cuda/moe-cache.cuandstaged-input.cu; the CI CUDA job uses a GCC 13 container.Not caused by this change; worth a separate issue if CI reproduces it.
What this is
baseline-flash-nextand this branch (fork/763-qwen4exp-mtp, tipefd26f235) are two competing answers to the same question: how to give qwen4exp (Qwen3.8-Flash-Next) a working NextN/MTP speculative draft head for the baseline-flash-next line.baseline-flash-next(cdd11021b) = the old Hydra Simplify to include lower-case windows.h always, fix compile on mingw32 ggml-org/llama.cpp#747 chain plus one hand-rolledfeat(qwen4exp): add NextN/MTP draft-head graph with shared-tensor borrowingcommit.GenerelSchwerz/llama.cppqwen4exp-mtplineage (CUDA MoE expert cache, grouped MoE drafting, Flash-Next MTP, upstream merges) plus the same 5 Hydra Simplify to include lower-case windows.h always, fix compile on mingw32 ggml-org/llama.cpp#747 commits replayed, plus this reconciliation merge.This PR reconciles the two and proposes the gs
qwen4exp-mtplineage as the basis.Decision: primary basis = the gs
qwen4exp-mtplineageDiff findings
The two draft-head implementations are the same code:
LLM_TENSOR_NEXTN_HC_HEAD_{NORM,DOWN,UP}enum values and"blk.%d.nextn.hc_head_*"names,llama_layer_nextn.hc_head_{norm,down,up}fields,llama_model_qwen4exp::graphshape (protected,no_build_ttag ctor,graph_mtp : public graph),qwen4exp.cppdraft graph is the same.The only real difference is how a draft borrows the target's token embeddings / LM head:
cdd11021b{arch}.nextn_shared_target_tensors+llama_model_params.model_shared, resolved byllama_model_loader::borrow_shared_tensor()qwen4exp_shared_model(cparams.ctx_other, ...), qwen4exp-onlytoken_embd,output,output_normctx_other;token_embdmarked optional formtp_only--mtp-shared-embd(convert) +mparams.model_shared = model_tgt(common)What was ported from
cdd11021b: nothingEvery cdd delta is either already present in gs or superseded by it:
src/llama-model.cppmtp_on_hybrid_qwenQWEN4EXP and theNEXTN_HC_HEAD_*arch changes are already in gs unchanged.src/llama-model.h,src/models/models.h) or gs-correct (src/models/qwen4exp.cpp:token_embdflags=0,n_ff_exp(il), loader-based borrow).common/speculative.cppis_mem_sharedgemma gate andsrc/llama-context.cppQWEN4EXPctx_otherregistration are workarounds for cdd's graph-time borrow and are not carried.Why the
is_mem_sharedgate is not needed here. gs resetscparams.ctx_other = nullptrinllama_contextand only sets it forGemma4Assistant/EAGLE3/DFlash(src/llama-context.cpp:791-808). For qwen4exp it stays null, sollama_get_ctx_other(ctx_dft) == ctx_tgtis false andis_mem_sharedis already correct. cdd's gate exists only because cdd does setctx_otherfor qwen4exp (to power its graph-time borrow) and then has to gateis_mem_sharedback off. Carrying the gate into gs would be redundant and would change behavior for gemma4/eagle3/dflash.Resulting tree
git rev-parse HEAD^{tree}==git rev-parse efd26f235^{tree}(df72c2736). The merge takes the gs side in full;cdd11021bis recorded as an ancestor (not silently dropped) but contributes no tree change.Behavior differences a reviewer should check
baseline-flash-next's draft head with the gs lineage".nextn_shared_target_tensors(convert_hf_to_gguf.py --mtp-shared-embd) for a draft that does not shiptoken_embd/output. A self-contained draft (Hydra's currentQwen3.8-27B-*-MTP.ggufshiptoken_embd) loads unchanged. cdd's auto-detecting graph-time fallback is not carried, so any existing draft-only GGUF without the flag is now refused at load instead of silently borrowed.is_mem_shared=falsesingle-head path. If anyone expected cdd's gated behavior, please confirm.cdd11021b's graph-time borrow entirely.Not fully confident in / did not verify
nextn_shared_target_tensors) is gs's and is untested by me against Hydra's GGUF exports.is_mem_sharedjudgment comes from reading gs'sctx_otherlifecycle, not a runtime test. If a reviewer knows of a qwen4exp path wherectx_otheris non-null in gs, revisit.Verification done
efd26f235(CUDA 13.2.2,CMAKE_CUDA_ARCHITECTURES=86;120):llama-serverlinks;--helpshows--moe-expert-cache-size,--spec-draft-moe-expert-cache-size,--load-mode,--parallel-ctx-threshold.git merge-treeconflict probe + fullgit mergeinspection (3 conflicts, all resolved to gs).CI status
c101306effixes-Werror=sign-compare(ggml/src/ggml-cuda/ggml-cuda.cu:3053) and-Wstringop-overflow(ggml/src/ggml-cuda/ggml-cuda.cu:5509); verified by compilingggml-cuda.cuwith the exact CUDA CI flags (gcc 13,LLAMA_FATAL_WARNINGS=ON) -> 0 errors.test_speculative.py::test_with_and_without_draft(HTTP 500) - blocked by [engine] draft-simple 500: d659bcefac target-verification intent rejects split_simple ubatch (blocks PR #120) #121.429rate-limit while downloading test models.