Skip to content

Fix DeepSeek Vision-Exp image inference with SSD streaming on Metal - #937

Open
cropduster wants to merge 1 commit into
antirez:mainfrom
cropduster:fix/vision-ssd-streaming-views
Open

Fix DeepSeek Vision-Exp image inference with SSD streaming on Metal#937
cropduster wants to merge 1 commit into
antirez:mainfrom
cropduster:fix/vision-ssd-streaming-views

Conversation

@cropduster

Copy link
Copy Markdown

Fix DeepSeek Vision-Exp image inference with SSD streaming on Metal

Closes the failure mode reported in #928 (image input → HTTP 400 "DeepSeek V4 vision inference failed").

Summary

Two coupled bugs broke every image request when serving the Vision-Exp MXFP4 checkpoint with --ssd-streaming --vision <encoder> on Metal. Text inference was unaffected.

  1. The SSD-streaming span remap cleared the vision sidecar's Metal views.
    With SSD streaming, the main model map starts as token-embedding-only (ds4.c, ds4_gpu_set_model_map_spans at startup), and the vision encoder sidecar map is registered additionally at startup → vision_map_ready = true. The first prefill installs runtime spans via ds4_gpu_set_model_map_spans, whose rebuild path calls a global ds4_gpu_model_views_clear() — destroying ALL views, including the encoder's — and rebuilds only main-model spans. The encode path re-maps lazily only under if (e->metal_ready && !e->vision_map_ready) (ds4.c:63887); the flag is stale true, so no re-map happens. ds4_gpu_wrap_model_range then fails:

    ds4: Metal model range 0.87..0.87 GiB is not covered by mapped model views
    

    (0.868 GiB = exactly the encoder GGUF size) → ds4_gpu_deepseek4_vision_encode returns 0 → HTTP 400.

  2. Registering the sidecar via ds4_gpu_set_model_map_range overwrote the primary map globals.
    Found while verifying fix attempt Fix spelling typos in README #1 live: the naive re-map path (using ds4_gpu_set_model_map_range for the vision map) made the following main-model prefill fail with 0.10 GiB not covered. Enriched diagnostics + GGUF header parse identified the missed read as layers.0.ffn.gate.bias_vl (vision sentinel tensor) on the vision map, read during the main prefill — because on Metal ds4_gpu_set_model_map_range rewrites g_model_map_ptr/g_model_map_size etc. with the sidecar map, so the next span remap cleared everything again and the prefill resolved the wrong map context. ds4_gpu_set_aux_model_map_range was declared in ds4_gpu.h as "secondary GGUF mapping without replacing the primary model mapping" but never implemented on Metal. This PR implements it (append-only, no global update) and uses it for both vision registration sites.

Changes

  • ds4_metal.m
    • New global g_model_views_generation, bumped in ds4_gpu_model_views_clear(); accessor ds4_gpu_model_views_generation().
    • New ds4_gpu_model_views_clear_for_map(map, size): drops only one mapping's views, preserves other mappings' views (sidecar), bumps generation only when something was removed.
    • ds4_gpu_set_model_map_spans now clears only its own mapping's views instead of all views.
    • ds4_gpu_set_aux_model_map_range implemented: coverage check → append one page-aligned view set for the aux map; updates g_last_aux_map but never the primary globals.
    • ds4_gpu_wrap_model_range failure message extended with diagnostic fields (map ptr, primary/aux flags, offset, length, view count).
  • ds4.c
    • Engine struct: new uint64_t vision_map_generation.
    • Both vision map registration sites (startup + lazy path) record the generation on success.
    • Lazy guard now: (!vision_map_ready || vision_map_generation != ds4_gpu_model_views_generation()) → re-map via ds4_gpu_set_aux_model_map_range.
    • Vision map failure message names the actual vision kind ("DeepSeek" vs "GLM-5.3").
  • ds4_gpu.h: declares ds4_gpu_model_views_generation().
  • ds4_cuda.cu / rocm/ds4_rocm_runtime.cuh: constant-zero generation stubs (these backends never clear model views).

Diff: 5 files, +132/−15.

Verification (M4 Max 128 GiB, Vision-Exp MXFP4, --ssd-streaming --vision, ds4-server on 0.0.0.0:8008, switcher menu option 4)

Before the fix (commit 110afdd, both bugs present):

HTTP 400 {"error":{"message":"DeepSeek V4 vision inference failed"}}
ds4: Metal model range 0.87..0.87 GiB is not covered by mapped model views

Every image request failed; text worked.

After fix attempt #1 (generation counter only — documented to justify the second fix):

IMAGE: 500 {"error":{"message":"metal prefill failed"}}
ds4: Metal model range 0.10..0.10 GiB is not covered by mapped model views (map=0x109610000 primary=0 aux=1 off=0x68117a0 len=0x400 views=4)
ds4: gpu layer 0 ffn batch encode failed
ds4: gpu layer-major prefill layer 0 encode failed

GGUF header parse: offset 0x68117a0 on the vision map = layers.0.ffn.gate.bias_vl (256×F32 = 0x400 bytes) — the vision sentinel read during main prefill, proving the sidecar map was being cleared mid-request and the primary globals had been annexed.

After the final fix (both bugs addressed):

Cold start via switcher menu option 4 → first request is an image (8×8 PNG, data URI):

IMAGE: 200 | 37.9s
0901 23:18:09 chat ctx=0..131:131 prompt start
0901 23:18:35 chat ctx=0..131:131 prompt done 25.589s
0901 23:18:44 chat ctx=131..181:50 gen=50 THINKING decoding chunk=5.65 t/s
0901 23:18:47 chat ctx=0..131:131 gen=96 THINKING finish=length 37.723s

Second image request, content check (64×64 green PNG, 400 max tokens):

IMAGE2: 200 | 56.2s | finish: stop
content: Das Bild ist grün.

Text regression (correctness + speed):

TEXT: 200 | 19.8s
content: 391. Das Ergebnis erhält man, indem man 17 mit 20 multipliziert (340) und 17 mit 3 multipliziert (51) ...
decoding chunk=18.83 t/s avg=17.95 t/s
decoding chunk=19.99 t/s avg=18.33 t/s
decoding chunk=20.28 t/s avg=18.62 t/s

Log check across all post-fix requests: zero "not covered" lines (previously 2 per image request).

Notes

  • CUDA/ROCm are untouched functionally; they get constant-zero generation stubs since they never clear views.
  • The ds4_gpu_wrap_model_range diagnostic extension is deliberately conservative (one fprintf); happy to split it out if you prefer a minimal diff.
  • Resident (non-streaming) runs were never affected — this only manifests with --ssd-streaming.
  • GLM-5.3 vision + SSD streaming shares the auxiliary-map path and should benefit; I haven't re-tested that combination yet.

Two coupled bugs broke DeepSeek Vision-Exp image inference with
--ssd-streaming on Metal:

1. The SSD-streaming span remap cleared ALL global Metal model
   views, including the vision encoder sidecar view registered at
   startup. Because vision_map_ready stayed true, the lazy re-map
   in the vision encode path never ran and every image request
   failed with 'Metal model range 0.87..0.87 GiB is not covered'.
   Fix: track a model-views generation counter; the encode path
   re-registers the sidecar whenever the generation moved.

2. Registering the sidecar via ds4_gpu_set_model_map_range on
   Metal overwrote the primary model map globals, breaking the
   following main-model prefill ('0.10 GiB not covered'). Fix:
   implement ds4_gpu_set_aux_model_map_range on Metal as a true
   append-only secondary registration and make
   ds4_gpu_set_model_map_spans clear only its own mapping's views,
   preserving sidecar views. Also make the vision map failure
   message name the actual vision kind and include diagnostic
   offsets.

CUDA/ROCm expose a constant zero generation (they never clear
views). Verified on M4 Max 128 GiB, Vision-Exp MXFP4:
first image request after cold start returns 200 with correct
content, zero 'not covered' log lines, text regression at ~20 t/s.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant