model: Support DeepSeek-V4-Flash-Vision-Exp - #28150
Conversation
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
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. |
|
chat template part is already redundant to start with, and among some more redundant things that I spotted so far:
if you generated this whole PR with just one single |
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? |
|
a good instinct is to ask your agent: why other models don't need this patch? |
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
Requirements