Skip to content

model: Support DeepSeek-V4-Flash-Vision-Exp - #28150

Closed
tarruda wants to merge 6 commits into
ggml-org:masterfrom
tarruda:dsv4-vision-exp-support
Closed

model: Support DeepSeek-V4-Flash-Vision-Exp#28150
tarruda wants to merge 6 commits into
ggml-org:masterfrom
tarruda:dsv4-vision-exp-support

Conversation

@tarruda

@tarruda tarruda commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Overview

Adds support for DeepSeek V4 Flash Vision Exp.

Additional information

Not sure if anyone else is already working, but I had GPT/Codex work on this overnight so leaving it here as a draft/reference. I've had it split into digestible commits to simplify reviewing. Hopefully it will be useful to someone working on this.

This required a new chat template, which should be handled automatically in the conversion script.

Test weights/mmproj currently being uploaded here: https://huggingface.co/tarruda/DeepSeek-V4-Flash-Vision-Exp-GGUF

Did some smoke tests on vision and appears to be working well and supports output of bounding boxes in the same format as qwen:

Details image

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes this was 100% generated by GPT 5.6 Sol. Did some manual smoke tests and seems to be correct, with text part being similar to 0731.

Define the GGUF metadata and tensor names shared by conversion and runtime support for DeepSeek V4 vision models.

- Add vision token limits and projector image geometry keys.
- Add visual routing bias and image padding tensor types.
- Map DeepSeek V4 vision encoder tensor names.
- Register the deepseek4_vision projector type.

Assisted-by: Codex
Convert the DeepSeek V4 Vision language model and BF16 projector from the original safetensors while validating the experimental model's fixed tensor and tokenizer layout.

- Validate vision configuration, routing biases, tensor inventory, and tokenizer anchors.
- Separate language and projector tensors and preserve the visual routing bias.
- Convert the vision encoder, aligner, and structural image embeddings.
- Select and embed the dedicated DeepSeek V4 Vision chat template.

Assisted-by: Codex
Load vision-specific language-model metadata and route image embedding batches through the DeepSeek V4 graph without changing text-only behavior.

- Load and preserve the image-token limit and visual expert-routing biases.
- Select visual routing biases and bypass token-hash routing for image embeddings.
- Allow full attention within validated contiguous image blocks.
- Cover the vision metadata and tensors in the synthetic architecture roundtrip.

Assisted-by: Codex
Add the DeepSeek V4 vision encoder, preprocessing, structural-token layout, and prompt-position handling to mtmd.

- Load the projector metadata and tensors and build the 2D-RoPE vision graph.
- Resize and normalize images within the model token and aspect-ratio limits.
- Assemble structural embeddings and align image blocks to four-token boundaries.
- Decode each image block in one ubatch and test the preprocessing geometry.

Assisted-by: Codex
Preserve typed media markers for templates that render them directly so DeepSeek V4 Vision receives image placeholders in the original content order.

- Detect whether a Jinja template preserves media-marker content parts.
- Keep structured media parts only for templates that advertise that capability.
- Test reasoning mode, adjacent images, developer and tool media, and placeholder rejection.

Assisted-by: Codex
Document the single-ubatch requirement for DeepSeek V4 image blocks and provide a server example sized for the model's maximum 384-token image.

Assisted-by: Codex
@github-actions github-actions Bot added documentation Improvements or additions to documentation model Model specific testing Everything test related mtmd Related to multimodal functionality (video/image/audio) jinja parser Issues related to the jinja parser conversion labels Sep 1, 2026
@tarruda

tarruda commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Just saw @ngxson PRs #28133 and #28154, so closing this.

@tarruda tarruda closed this Sep 1, 2026
@ngxson

ngxson commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator
  • Yes this was 100% generated by GPT 5.6 Sol

classic "let's add 10 more machinery" move from gpt, claude would not even suggest that

@tarruda

tarruda commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

classic "let's add 10 more machinery" move from gpt, claude would not even suggest that

Do you mean that a lot of the changes here are not necessary? Other than the chat template, I don't really understand most of the changes in this PR, but I'd appreciate if you could point which parts are you referring to.

@ngxson

ngxson commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

chat template part is already redundant to start with, and among some more redundant things that I spotted so far:

  • reinvented hash inside test cases
  • the round trip mtmd_helper_chunk_required_ubatch_internal, not redundant but not clever either
  • hacks to kv cache

if you generated this whole PR with just one single /goal, this could be a good example of the paper clip optimizer problem - pursuing one goal while giving up all other constraints

@tarruda

tarruda commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

chat template part is already redundant to start with

The chat template is based on the existing template which was largely based on the original encoding.py reference code provided by deepseek, plus some changes to support image input. Here's the diff:

--- models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja   2026-08-31 16:21:09
+++ models/templates/deepseek-ai-DeepSeek-V4-Flash-Vision-Exp.jinja     2026-09-01 06:29:13
@@ -12,6 +12,31 @@
   {%- set drop_thinking = true -%}
 {%- endif -%}
 {%- set dsml_token = '|DSML|' -%}
+{%- set image_placeholder = '<' + (dsml_token | replace('DSML', 'deepseek_image')) + '>' -%}
+{%- macro checked_text(text) -%}
+  {%- if image_placeholder in text -%}
+    {{- raise_exception('Images must be provided as media content blocks.') -}}
+  {%- endif -%}
+  {{- text -}}
+{%- endmacro -%}
+{%- macro render_content(content) -%}
+  {%- if content is string -%}
+    {{- checked_text(content) -}}
+  {%- elif content is not none -%}
+    {%- for part in content -%}
+      {%- if not loop.first -%}
+        {{- '\n\n' -}}
+      {%- endif -%}
+      {%- if part['type'] == 'text' -%}
+        {{- checked_text(part['text'] or '') -}}
+      {%- elif part['type'] == 'media_marker' -%}
+        {{- part['text'] -}}
+      {%- else -%}
+        {{- raise_exception('Unsupported message content type.') -}}
+      {%- endif -%}
+    {%- endfor -%}
+  {%- endif -%}
+{%- endmacro -%}
 {%- set thinking_start_token = '<think>' -%}
 {%- set thinking_end_token = '</think>' -%}
 {%- set reasoning_effort_high = 'Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n' -%}
@@ -24,10 +49,10 @@
 {%- for message in messages -%}
   {%- if message['role'] == 'system' -%}
     {%- if ns.is_first_sp -%}
-      {%- set ns.system_prompt = ns.system_prompt + (message['content'] or '') -%}
+      {%- set ns.system_prompt = ns.system_prompt + render_content(message['content']) -%}
       {%- set ns.is_first_sp = false -%}
     {%- else -%}
-      {%- set ns.system_prompt = ns.system_prompt + '\n\n' + (message['content'] or '') -%}
+      {%- set ns.system_prompt = ns.system_prompt + '\n\n' + render_content(message['content']) -%}
     {%- endif -%}
   {%- endif -%}
 {%- endfor -%}
@@ -78,7 +103,7 @@
       {{- '<|User|>' -}}
       {%- set state.in_user = true -%}
     {%- endif -%}
-    {{- message['content'] or '' -}}
+    {{- render_content(message['content']) -}}
   {%- elif message['role'] == 'tool' -%}
     {%- if state.in_user -%}
       {{- '\n\n' -}}
@@ -86,7 +111,7 @@
       {{- '<|User|>' -}}
       {%- set state.in_user = true -%}
     {%- endif -%}
-    {{- '<tool_result>' + (message['content'] or '') + '</tool_result>' -}}
+    {{- '<tool_result>' + render_content(message['content']) + '</tool_result>' -}}
   {%- elif message['role'] == 'assistant' -%}
     {%- set state.in_user = false -%}
     {{- '<|Assistant|>' -}}
@@ -95,14 +120,14 @@
     {%- if keep_reasoning -%}
       {{- thinking_start_token -}}
       {%- if message['reasoning_content'] is defined and message['reasoning_content'] -%}
-        {{- message['reasoning_content'] -}}
+        {{- checked_text(message['reasoning_content']) -}}
       {%- endif -%}
       {{- thinking_end_token -}}
     {%- else -%}
       {{- thinking_end_token -}}
     {%- endif -%}
     {%- if message['content'] is defined and message['content'] -%}
-      {{- message['content'] -}}
+      {{- render_content(message['content']) -}}
     {%- endif -%}
     {%- if message['tool_calls'] -%}
       {{- '\n\n<' + dsml_token + 'tool_calls>\n' -}}

What is redundant about this?

@ngxson

ngxson commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

a good instinct is to ask your agent: why other models don't need this patch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion documentation Improvements or additions to documentation jinja parser Issues related to the jinja parser model Model specific mtmd Related to multimodal functionality (video/image/audio) testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants