dflash: zero-fill draft-cache holes left by mtmd chunks and reused prefixes - #1
dflash: zero-fill draft-cache holes left by mtmd chunks and reused prefixes#1dagnarf wants to merge 2 commits into
Conversation
…efixes Multimodal (mtmd) image chunks are decoded into the target context by the mtmd helper and never pass through common_speculative_process(), so the dflash draft cache is left with a positional hole. The next injection then fails the KV cache's consecutive-position check: init: the tokens of sequence 0 in the input batch have inconsistent sequence positions: ... X = 62 ... Y = 86 ... Y = X + 1 llama_decode(ctx_dft) failed rc=-1 srv update_slots: decode() failed: failed to process speculative batch and the server returns HTTP 500 for any request whose prompt contains an image. The same hole appears when the target reuses a cached prompt prefix the draft cache never saw (e.g. after a server restart with --cache-reuse). Detect the per-sequence gap at the top of process() and seed it with zero-feature encoder injections. Drafted tokens remain verified by the target, so this degrades post-image draft acceptance only - output remains distribution-exact (verified: greedy OCR output byte-identical between draft-dflash and a non-speculative run of the same request). A fuller fix would extract real target features for mtmd chunks (see the existing TODO referencing ggml-org#24669); this is the minimal change that stops the crash without touching the mtmd path.
Large images break the previous path: a 4000x3000 photo produces a 2961-row image-embedding chunk whose rows carry M-RoPE positions. The first ubatch happens to pass the draft KV cache's checks, but the second fails the consecutive-position rule: process: llama_decode(ctx_dft) failed rc=-1 (n_tokens=913, offset=2048) slot operator(): failed to decode mtmd chunk, idx = 1755, res = 1 srv send_error: failed to process mtmd chunk and even single-ubatch chunks inject rows at bogus draft positions (the M-RoPE temporal component, constant across the image), polluting the draft sliding window. Skip embedding batches entirely; the positional hole they leave is covered by the zero-fill pass when the next token batch arrives. Verified: 4000x3000 photo request now returns 200 with greedy output byte-identical to a non-speculative run; small-image OCR and rasterized-PDF transcription remain exact.
|
Pushed a second commit ( A 4000×3000 photo produces a ~3000-row image-embedding chunk that does reach Smaller images "worked" only because their chunk fit in one ubatch — and even then the injected image rows sat at wrong positions inside the draft's sliding window. The second commit skips embedding batches in Re-validated on Qwen3.8-27B BF16 + BF16 mmproj (M5 Max): the 4000×3000 photo returns 200 with greedy output byte-identical to a non-speculative run; small-image OCR and rasterized-PDF transcription remain exact; text-only behavior unchanged. If a target ever provides linear positions for image chunks (or after ggml-org#24669 lands), the embedding path could be re-enabled behind a position-linearity check — but skip-plus-zero-fill is the correct conservative default for M-RoPE targets today. 🤖 Generated with Claude Code |
Fixes the multimodal breakage in DFlash2 speculative decoding (reported on ggml-org#27342: vision requests failing while native MTP works).
Repro (M5 Max, Metal,
dflash2head5ecbe1ac1)Any
/v1/chat/completionsrequest containing an image returns HTTP 500:Root cause
mtmd image chunks are decoded into the target context by the mtmd helper, so they never pass through
common_speculative_process(). The dflash draft cache is left with a positional hole spanning the image chunk, and the first text ubatch after the image fails the draft KV cache's consecutive-position check. (Theprocess()comment already anticipates this — "skipping the embedding batches leaves a hole in the draft's cache" — but the batches aren't skipped, they simply never arrive.) The same hole can appear when the target reuses a cached prompt prefix the draft cache never saw.Fix
Detect the per-sequence gap at the top of the dflash
process()and seed it with zero-feature encoder injections. Drafted tokens are still verified by the target, so this only degrades draft acceptance right after an image — output stays distribution-exact.A fuller fix would extract real target features for mtmd chunks (per the existing TODO referencing ggml-org#24669); this is the minimal change that stops the 500 without touching the mtmd path. The
LOG_WRNon each hole could arguably beLOG_DBGfor cache-reuse-heavy serving — happy to adjust.Validation (Qwen3.8-27B BF16 + BF16 mmproj +
Qwen3.8-27B-DFlash2-BF16, M5 Max)🤖 Generated with Claude Code