Image-Text-to-Text
Transformers
Safetensors
deepseek_v4
text-generation
multimodal
vision-language
deepseek-v4
moonvit
nvfp4
blackwell
8-bit precision
fp8
Instructions to use webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4")# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4") model = AutoModelForCausalLM.from_pretrained("webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4
- SGLang
How to use webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4 with Docker Model Runner:
docker model run hf.co/webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4
Promote SGLang MoonViT integration to main
Browse filesPreserves the previous deployment state on pre-sglang-main-2026-08-04. Adds the validated external SGLang model/processor package, routing-aware DeepSeek embedding patch helper, augmented model metadata/index, launch wrapper, smoke client, and deployment documentation.
- config.json +106 -0
- configs/routing/deepseek-v4-flash-60d8d707-palette64.json +66 -0
- configs/serve/sglang-moonvit.yaml +10 -0
- docs/SGLANG_DEPLOYMENT.md +135 -0
- model.safetensors.index.json +2 -2
- scripts/launch_sglang_moonvit.sh +38 -0
- scripts/prepare_sglang_model_repo.py +213 -0
- scripts/smoke_sglang_moonvit.py +68 -0
- sglang_ext/deepseek_vision_sglang/__init__.py +10 -0
- sglang_ext/deepseek_vision_sglang/models/__init__.py +1 -0
- sglang_ext/deepseek_vision_sglang/models/deepseek_v4_moonvit.py +271 -0
- sglang_ext/deepseek_vision_sglang/patch.py +64 -0
- sglang_ext/deepseek_vision_sglang/processors/__init__.py +1 -0
- sglang_ext/deepseek_vision_sglang/processors/moonvit.py +117 -0
- sglang_ext/deepseek_vision_sglang/routing.py +87 -0
config.json
CHANGED
|
@@ -52,6 +52,83 @@
|
|
| 52 |
0
|
| 53 |
],
|
| 54 |
"compress_rope_theta": 160000,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
"eos_token_id": 1,
|
| 56 |
"expert_dtype": "fp4",
|
| 57 |
"hc_eps": 1e-06,
|
|
@@ -60,11 +137,13 @@
|
|
| 60 |
"head_dim": 512,
|
| 61 |
"hidden_act": "silu",
|
| 62 |
"hidden_size": 4096,
|
|
|
|
| 63 |
"index_head_dim": 128,
|
| 64 |
"index_n_heads": 64,
|
| 65 |
"index_topk": 512,
|
| 66 |
"initializer_range": 0.02,
|
| 67 |
"max_position_embeddings": 1048576,
|
|
|
|
| 68 |
"model_type": "deepseek_v4",
|
| 69 |
"moe_intermediate_size": 2048,
|
| 70 |
"n_routed_experts": 256,
|
|
@@ -315,5 +394,32 @@
|
|
| 315 |
"torch_dtype": "bfloat16",
|
| 316 |
"transformers_version": "4.57.1",
|
| 317 |
"use_cache": true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
"vocab_size": 129280
|
| 319 |
}
|
|
|
|
| 52 |
0
|
| 53 |
],
|
| 54 |
"compress_rope_theta": 160000,
|
| 55 |
+
"deepseek_vision": {
|
| 56 |
+
"image_placeholder": "<image>",
|
| 57 |
+
"image_placeholder_token_id": 129280,
|
| 58 |
+
"max_image_tokens": 512,
|
| 59 |
+
"requires_sglang_source_patch": true,
|
| 60 |
+
"routing_palette": [
|
| 61 |
+
0,
|
| 62 |
+
1,
|
| 63 |
+
2,
|
| 64 |
+
8,
|
| 65 |
+
9,
|
| 66 |
+
10,
|
| 67 |
+
12,
|
| 68 |
+
74,
|
| 69 |
+
81,
|
| 70 |
+
110,
|
| 71 |
+
114,
|
| 72 |
+
240,
|
| 73 |
+
17081,
|
| 74 |
+
25312,
|
| 75 |
+
30711,
|
| 76 |
+
58279,
|
| 77 |
+
7637,
|
| 78 |
+
8936,
|
| 79 |
+
45556,
|
| 80 |
+
52073,
|
| 81 |
+
7743,
|
| 82 |
+
8347,
|
| 83 |
+
13203,
|
| 84 |
+
19795,
|
| 85 |
+
44418,
|
| 86 |
+
62970,
|
| 87 |
+
79038,
|
| 88 |
+
6381,
|
| 89 |
+
48025,
|
| 90 |
+
109859,
|
| 91 |
+
29629,
|
| 92 |
+
91213,
|
| 93 |
+
90662,
|
| 94 |
+
121562,
|
| 95 |
+
8570,
|
| 96 |
+
25568,
|
| 97 |
+
3685,
|
| 98 |
+
81916,
|
| 99 |
+
14638,
|
| 100 |
+
50590,
|
| 101 |
+
101211,
|
| 102 |
+
24832,
|
| 103 |
+
75337,
|
| 104 |
+
131,
|
| 105 |
+
15170,
|
| 106 |
+
79723,
|
| 107 |
+
84052,
|
| 108 |
+
20866,
|
| 109 |
+
48327,
|
| 110 |
+
72234,
|
| 111 |
+
15507,
|
| 112 |
+
128,
|
| 113 |
+
16760,
|
| 114 |
+
34135,
|
| 115 |
+
36264,
|
| 116 |
+
59037,
|
| 117 |
+
3839,
|
| 118 |
+
29854,
|
| 119 |
+
109646,
|
| 120 |
+
64,
|
| 121 |
+
23442,
|
| 122 |
+
6584,
|
| 123 |
+
10255,
|
| 124 |
+
17173
|
| 125 |
+
],
|
| 126 |
+
"routing_policy": "palette_cycle",
|
| 127 |
+
"schema_version": 1,
|
| 128 |
+
"sglang_source_commit": "723c2776401987cba3cfee25830db50f3964b83f",
|
| 129 |
+
"tower_model_id": "moonshotai/Kimi-K2.6",
|
| 130 |
+
"tower_revision": "7eb5002f6aadc958aed6a9177b7ed26bb94011bb"
|
| 131 |
+
},
|
| 132 |
"eos_token_id": 1,
|
| 133 |
"expert_dtype": "fp4",
|
| 134 |
"hc_eps": 1e-06,
|
|
|
|
| 137 |
"head_dim": 512,
|
| 138 |
"hidden_act": "silu",
|
| 139 |
"hidden_size": 4096,
|
| 140 |
+
"image_token_id": 129280,
|
| 141 |
"index_head_dim": 128,
|
| 142 |
"index_n_heads": 64,
|
| 143 |
"index_topk": 512,
|
| 144 |
"initializer_range": 0.02,
|
| 145 |
"max_position_embeddings": 1048576,
|
| 146 |
+
"media_placeholder_token_id": 129280,
|
| 147 |
"model_type": "deepseek_v4",
|
| 148 |
"moe_intermediate_size": 2048,
|
| 149 |
"n_routed_experts": 256,
|
|
|
|
| 394 |
"torch_dtype": "bfloat16",
|
| 395 |
"transformers_version": "4.57.1",
|
| 396 |
"use_cache": true,
|
| 397 |
+
"vision_config": {
|
| 398 |
+
"hidden_size": 1152,
|
| 399 |
+
"init_pos_emb_height": 64,
|
| 400 |
+
"init_pos_emb_time": 4,
|
| 401 |
+
"init_pos_emb_width": 64,
|
| 402 |
+
"intermediate_size": 4304,
|
| 403 |
+
"merge_kernel_size": [
|
| 404 |
+
2,
|
| 405 |
+
2
|
| 406 |
+
],
|
| 407 |
+
"merge_type": "sd2_tpool",
|
| 408 |
+
"mm_hidden_size": 1152,
|
| 409 |
+
"mm_projector_type": "patchmerger",
|
| 410 |
+
"model_type": "kimi_k25",
|
| 411 |
+
"num_attention_heads": 16,
|
| 412 |
+
"num_hidden_layers": 27,
|
| 413 |
+
"patch_size": 14,
|
| 414 |
+
"pos_emb_type": "divided_fixed",
|
| 415 |
+
"projector_hidden_act": "gelu",
|
| 416 |
+
"projector_ln_eps": 1e-05,
|
| 417 |
+
"text_hidden_size": 4096,
|
| 418 |
+
"video_attn_type": "spatial_temporal",
|
| 419 |
+
"vt_hidden_size": 1152,
|
| 420 |
+
"vt_intermediate_size": 4304,
|
| 421 |
+
"vt_num_attention_heads": 16,
|
| 422 |
+
"vt_num_hidden_layers": 27
|
| 423 |
+
},
|
| 424 |
"vocab_size": 129280
|
| 425 |
}
|
configs/routing/deepseek-v4-flash-60d8d707-palette64.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
0,
|
| 3 |
+
1,
|
| 4 |
+
2,
|
| 5 |
+
8,
|
| 6 |
+
9,
|
| 7 |
+
10,
|
| 8 |
+
12,
|
| 9 |
+
74,
|
| 10 |
+
81,
|
| 11 |
+
110,
|
| 12 |
+
114,
|
| 13 |
+
240,
|
| 14 |
+
17081,
|
| 15 |
+
25312,
|
| 16 |
+
30711,
|
| 17 |
+
58279,
|
| 18 |
+
7637,
|
| 19 |
+
8936,
|
| 20 |
+
45556,
|
| 21 |
+
52073,
|
| 22 |
+
7743,
|
| 23 |
+
8347,
|
| 24 |
+
13203,
|
| 25 |
+
19795,
|
| 26 |
+
44418,
|
| 27 |
+
62970,
|
| 28 |
+
79038,
|
| 29 |
+
6381,
|
| 30 |
+
48025,
|
| 31 |
+
109859,
|
| 32 |
+
29629,
|
| 33 |
+
91213,
|
| 34 |
+
90662,
|
| 35 |
+
121562,
|
| 36 |
+
8570,
|
| 37 |
+
25568,
|
| 38 |
+
3685,
|
| 39 |
+
81916,
|
| 40 |
+
14638,
|
| 41 |
+
50590,
|
| 42 |
+
101211,
|
| 43 |
+
24832,
|
| 44 |
+
75337,
|
| 45 |
+
131,
|
| 46 |
+
15170,
|
| 47 |
+
79723,
|
| 48 |
+
84052,
|
| 49 |
+
20866,
|
| 50 |
+
48327,
|
| 51 |
+
72234,
|
| 52 |
+
15507,
|
| 53 |
+
128,
|
| 54 |
+
16760,
|
| 55 |
+
34135,
|
| 56 |
+
36264,
|
| 57 |
+
59037,
|
| 58 |
+
3839,
|
| 59 |
+
29854,
|
| 60 |
+
109646,
|
| 61 |
+
64,
|
| 62 |
+
23442,
|
| 63 |
+
6584,
|
| 64 |
+
10255,
|
| 65 |
+
17173
|
| 66 |
+
]
|
configs/serve/sglang-moonvit.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model-path: webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4
|
| 2 |
+
host: 127.0.0.1
|
| 3 |
+
port: 30000
|
| 4 |
+
tensor-parallel-size: 5
|
| 5 |
+
trust-remote-code: true
|
| 6 |
+
enable-multimodal: true
|
| 7 |
+
limit-mm-data-per-request:
|
| 8 |
+
image: 1
|
| 9 |
+
disable-cuda-graph: true
|
| 10 |
+
skip-server-warmup: true
|
docs/SGLANG_DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SGLang deployment status
|
| 2 |
+
|
| 3 |
+
## Short answer
|
| 4 |
+
|
| 5 |
+
The current Hugging Face model directory is **not** a stock-SGLang multimodal model.
|
| 6 |
+
SGLang natively implements the DeepSeek V4 text runtime and Kimi's MoonViT, but it does
|
| 7 |
+
not know that this particular checkpoint combines them. The integration in
|
| 8 |
+
`sglang_ext/deepseek_vision_sglang` is an external SGLang model/processor package and
|
| 9 |
+
requires one narrow source patch. Do not describe it as upstream or zero-code support.
|
| 10 |
+
|
| 11 |
+
The package is pinned to SGLang commit
|
| 12 |
+
`723c2776401987cba3cfee25830db50f3964b83f` (current `main` inspected on 2026-08-04):
|
| 13 |
+
|
| 14 |
+
- SGLang's [external model registry](https://github.com/sgl-project/sglang/blob/723c2776401987cba3cfee25830db50f3964b83f/python/sglang/srt/models/registry.py)
|
| 15 |
+
supports `SGLANG_EXTERNAL_MODEL_PACKAGE`.
|
| 16 |
+
- Its [tokenizer manager](https://github.com/sgl-project/sglang/blob/723c2776401987cba3cfee25830db50f3964b83f/python/sglang/srt/managers/tokenizer_manager.py#L459-L468)
|
| 17 |
+
loads `SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE` through the multimodal processor
|
| 18 |
+
registry.
|
| 19 |
+
- The native [Kimi K2.5 model](https://github.com/sgl-project/sglang/blob/723c2776401987cba3cfee25830db50f3964b83f/python/sglang/srt/models/kimi_k25.py)
|
| 20 |
+
supplies MoonViT and PatchMerger kernels.
|
| 21 |
+
- The native [DeepSeek V4 model](https://github.com/sgl-project/sglang/blob/723c2776401987cba3cfee25830db50f3964b83f/python/sglang/srt/models/deepseek_v4.py#L2347-L2360)
|
| 22 |
+
currently accepts `input_embeds` but ignores it at the embedding site. The checked-in
|
| 23 |
+
patch changes that one assignment while keeping `input_ids` for hash routing.
|
| 24 |
+
|
| 25 |
+
## What the extension does
|
| 26 |
+
|
| 27 |
+
1. The external processor recognizes one literal `<image>` marker and uses the official,
|
| 28 |
+
revision-pinned Kimi K2.6 processor for NaViT resize/normalization/patchification.
|
| 29 |
+
2. SGLang's native MoonViT implementation loads `vision_tower.safetensors`.
|
| 30 |
+
3. The native Kimi PatchMerger shape loads the trained `mm_projector.safetensors` and
|
| 31 |
+
emits 4096-d DeepSeek embeddings.
|
| 32 |
+
4. Image positions are replaced in `inputs_embeds`. A separate tensor preserves every
|
| 33 |
+
text token ID and cycles the checked-in 64-ID route palette over image positions.
|
| 34 |
+
5. The routing phase uses absolute image offsets, so chunked-prefill boundaries do not
|
| 35 |
+
restart the palette.
|
| 36 |
+
|
| 37 |
+
Keeping the architecture string `DeepseekV4ForCausalLM` is intentional. SGLang selects
|
| 38 |
+
its V4 attention, memory-pool, and FP4 expert behavior from that exact name; the external
|
| 39 |
+
registry overwrites only the instantiated model class.
|
| 40 |
+
|
| 41 |
+
## Stage a model directory
|
| 42 |
+
|
| 43 |
+
Download the private model repository to a local directory that contains the backbone
|
| 44 |
+
shards, `vision_tower.safetensors`, and `mm_projector.safetensors`. Then run:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
python scripts/prepare_sglang_model_repo.py /models/deepseek-v4-flash-vision
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
This makes three reviewable packaging changes:
|
| 51 |
+
|
| 52 |
+
- adds `vision_config` and `deepseek_vision` metadata to `config.json`;
|
| 53 |
+
- adds both standalone component files to `model.safetensors.index.json` (SGLang ignores
|
| 54 |
+
safetensor files that are absent from an existing index);
|
| 55 |
+
- copies the standalone extension to `MODEL_DIR/sglang_ext`.
|
| 56 |
+
|
| 57 |
+
Upload that staged directory to the private deployment repository only after reviewing
|
| 58 |
+
the diff. The script does not contact Hugging Face.
|
| 59 |
+
|
| 60 |
+
## Install and launch
|
| 61 |
+
|
| 62 |
+
Use the exact SGLang commit documented above. Set the model snapshot's extension path,
|
| 63 |
+
then launch through the checked-in wrapper:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
export DEEPSEEK_VISION_MODEL_PATH=/models/deepseek-v4-flash-vision
|
| 67 |
+
export DEEPSEEK_VISION_PYTHONPATH="$DEEPSEEK_VISION_MODEL_PATH/sglang_ext"
|
| 68 |
+
export DEEPSEEK_VISION_TP=5
|
| 69 |
+
scripts/launch_sglang_moonvit.sh
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
For a Hugging Face integration branch, download that revision into the local model
|
| 73 |
+
directory and also set `DEEPSEEK_VISION_REVISION` if `DEEPSEEK_VISION_MODEL_PATH` is a
|
| 74 |
+
Hub model ID. For example, the pre-merge smoke branch uses:
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
export DEEPSEEK_VISION_REVISION=sglang-integration
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
The wrapper verifies/applies the one-line SGLang patch before startup and exports all
|
| 81 |
+
three official external-registration variables. It disables CUDA graphs for the first
|
| 82 |
+
correctness gate. Re-enable performance features only after text-only parity and image
|
| 83 |
+
parity pass on the pinned build.
|
| 84 |
+
|
| 85 |
+
## First request
|
| 86 |
+
|
| 87 |
+
The first gate uses SGLang's native `/generate` endpoint. It avoids an upstream OpenAI
|
| 88 |
+
chat-rendering limitation: the current DeepSeek V4 chat encoder explicitly flattens
|
| 89 |
+
parts-list content as text-only before multimodal processing. Calling the OpenAI
|
| 90 |
+
`/v1/chat/completions` route with `image_url` is therefore **not yet supported by this
|
| 91 |
+
extension**.
|
| 92 |
+
|
| 93 |
+
Use the exact prompt shape used for projector training:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
curl http://127.0.0.1:30000/generate \
|
| 97 |
+
-H 'Content-Type: application/json' \
|
| 98 |
+
-d '{
|
| 99 |
+
"text":"<|begin▁of▁sentence|><|User|><image>Describe this image.<|Assistant|></think>",
|
| 100 |
+
"image_data":"data:image/jpeg;base64,...",
|
| 101 |
+
"sampling_params":{"temperature":0,"max_new_tokens":64}
|
| 102 |
+
}'
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
Or use the checked-in smoke client, which builds the data URL and rejects an empty
|
| 106 |
+
generation response:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
python scripts/smoke_sglang_moonvit.py /path/to/probe.jpg
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## Required GPU validation
|
| 113 |
+
|
| 114 |
+
The Mac can validate packaging, routing math, source anchors, and Python syntax, but it
|
| 115 |
+
cannot instantiate the 168 GB NVFP4 checkpoint or CUDA kernels. Before calling this
|
| 116 |
+
deployment ready, run these gates on suitable NVIDIA hardware:
|
| 117 |
+
|
| 118 |
+
1. `python -m deepseek_vision_sglang.patch --check` against the pinned SGLang install.
|
| 119 |
+
2. Loader startup with the staged private model directory.
|
| 120 |
+
3. A text-only prompt compared token-for-token with unmodified SGLang DeepSeek V4.
|
| 121 |
+
4. One cached training image compared against the existing Transformers/BF16 endpoint.
|
| 122 |
+
5. A fresh scene and GUI screenshot, checking image-token count, route-ID count, and
|
| 123 |
+
projector output shape before judging answer quality.
|
| 124 |
+
|
| 125 |
+
## Deliberate limitations
|
| 126 |
+
|
| 127 |
+
- Custom SGLang package plus a one-line source patch; no upstream support claim.
|
| 128 |
+
- Exactly one image per request and at most 512 post-merge image tokens.
|
| 129 |
+
- CUDA image preprocessing only; CPU-only serving is not supported.
|
| 130 |
+
- Tensor parallelism is intended; pipeline parallelism and encoder data parallelism are
|
| 131 |
+
blocked/unvalidated.
|
| 132 |
+
- CUDA graphs, speculative decoding, OpenAI multimodal chat rendering, streaming parity,
|
| 133 |
+
and production concurrency are not validated.
|
| 134 |
+
- This serving integration can fix or expose runtime integration errors. It cannot make
|
| 135 |
+
a weakly trained projector better; quality still needs the same image benchmark.
|
model.safetensors.index.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7cc70b0b7838961ca8cbd1d2ca31394302a6d6e84fea8cb270cd33d52af78457
|
| 3 |
+
size 11227454
|
scripts/launch_sglang_moonvit.sh
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
model_path="${DEEPSEEK_VISION_MODEL_PATH:-webbrain-one/DeepSeek-V4-Flash-Vision-NVFP4}"
|
| 5 |
+
model_revision="${DEEPSEEK_VISION_REVISION:-}"
|
| 6 |
+
tensor_parallel_size="${DEEPSEEK_VISION_TP:-5}"
|
| 7 |
+
host="${DEEPSEEK_VISION_HOST:-127.0.0.1}"
|
| 8 |
+
port="${DEEPSEEK_VISION_PORT:-30000}"
|
| 9 |
+
model_python_path="${DEEPSEEK_VISION_PYTHONPATH:-}"
|
| 10 |
+
|
| 11 |
+
if [[ -z "$model_python_path" ]]; then
|
| 12 |
+
echo "Set DEEPSEEK_VISION_PYTHONPATH to MODEL_DIR/sglang_ext." >&2
|
| 13 |
+
exit 2
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
export PYTHONPATH="${model_python_path}${PYTHONPATH:+:${PYTHONPATH}}"
|
| 17 |
+
export SGLANG_EXTERNAL_MODEL_PACKAGE="deepseek_vision_sglang.models"
|
| 18 |
+
export SGLANG_EXTERNAL_MM_MODEL_ARCH="DeepseekV4ForCausalLM"
|
| 19 |
+
export SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE="deepseek_vision_sglang.processors"
|
| 20 |
+
|
| 21 |
+
python -m deepseek_vision_sglang.patch --apply
|
| 22 |
+
|
| 23 |
+
launch_args=(
|
| 24 |
+
--model-path "$model_path"
|
| 25 |
+
--tp-size "$tensor_parallel_size"
|
| 26 |
+
--host "$host"
|
| 27 |
+
--port "$port"
|
| 28 |
+
--trust-remote-code
|
| 29 |
+
--enable-multimodal
|
| 30 |
+
--limit-mm-data-per-request '{"image":1}'
|
| 31 |
+
--disable-cuda-graph
|
| 32 |
+
--skip-server-warmup
|
| 33 |
+
)
|
| 34 |
+
if [[ -n "$model_revision" ]]; then
|
| 35 |
+
launch_args+=(--revision "$model_revision")
|
| 36 |
+
fi
|
| 37 |
+
|
| 38 |
+
exec python -m sglang.launch_server "${launch_args[@]}"
|
scripts/prepare_sglang_model_repo.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import shutil
|
| 7 |
+
import struct
|
| 8 |
+
import tempfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Iterable
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
SGLANG_SOURCE_COMMIT = "723c2776401987cba3cfee25830db50f3964b83f"
|
| 14 |
+
TOWER_MODEL_ID = "moonshotai/Kimi-K2.6"
|
| 15 |
+
TOWER_REVISION = "7eb5002f6aadc958aed6a9177b7ed26bb94011bb"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def deepseek_vision_config(*, routing_palette: Iterable[int], vocab_size: int) -> dict:
|
| 19 |
+
palette = [int(value) for value in routing_palette]
|
| 20 |
+
if not palette:
|
| 21 |
+
raise ValueError("routing palette cannot be empty")
|
| 22 |
+
if min(palette) < 0 or max(palette) >= vocab_size:
|
| 23 |
+
raise ValueError("routing palette contains an ID outside the model vocabulary")
|
| 24 |
+
return {
|
| 25 |
+
"schema_version": 1,
|
| 26 |
+
"tower_model_id": TOWER_MODEL_ID,
|
| 27 |
+
"tower_revision": TOWER_REVISION,
|
| 28 |
+
"image_placeholder": "<image>",
|
| 29 |
+
"image_placeholder_token_id": vocab_size,
|
| 30 |
+
"max_image_tokens": 512,
|
| 31 |
+
"routing_policy": "palette_cycle",
|
| 32 |
+
"routing_palette": palette,
|
| 33 |
+
"sglang_source_commit": SGLANG_SOURCE_COMMIT,
|
| 34 |
+
"requires_sglang_source_patch": True,
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def vision_config(*, text_hidden_size: int) -> dict:
|
| 39 |
+
return {
|
| 40 |
+
"model_type": "kimi_k25",
|
| 41 |
+
"patch_size": 14,
|
| 42 |
+
"init_pos_emb_height": 64,
|
| 43 |
+
"init_pos_emb_width": 64,
|
| 44 |
+
"init_pos_emb_time": 4,
|
| 45 |
+
"pos_emb_type": "divided_fixed",
|
| 46 |
+
"num_attention_heads": 16,
|
| 47 |
+
"num_hidden_layers": 27,
|
| 48 |
+
"hidden_size": 1152,
|
| 49 |
+
"intermediate_size": 4304,
|
| 50 |
+
# Current SGLang's K2 projector reads the legacy vt_* aliases while
|
| 51 |
+
# MoonViT itself reads the canonical names above. Keep both explicit.
|
| 52 |
+
"vt_num_attention_heads": 16,
|
| 53 |
+
"vt_num_hidden_layers": 27,
|
| 54 |
+
"vt_hidden_size": 1152,
|
| 55 |
+
"vt_intermediate_size": 4304,
|
| 56 |
+
"merge_kernel_size": [2, 2],
|
| 57 |
+
"video_attn_type": "spatial_temporal",
|
| 58 |
+
"merge_type": "sd2_tpool",
|
| 59 |
+
"mm_projector_type": "patchmerger",
|
| 60 |
+
"mm_hidden_size": 1152,
|
| 61 |
+
"projector_hidden_act": "gelu",
|
| 62 |
+
"projector_ln_eps": 1e-5,
|
| 63 |
+
"text_hidden_size": int(text_hidden_size),
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def augment_config(config: dict, routing_palette: Iterable[int]) -> dict:
|
| 68 |
+
architectures = config.get("architectures") or []
|
| 69 |
+
if "DeepseekV4ForCausalLM" not in architectures:
|
| 70 |
+
raise ValueError("model architecture must remain DeepseekV4ForCausalLM")
|
| 71 |
+
vocab_size = int(config["vocab_size"])
|
| 72 |
+
hidden_size = int(config["hidden_size"])
|
| 73 |
+
updated = dict(config)
|
| 74 |
+
updated["vision_config"] = vision_config(text_hidden_size=hidden_size)
|
| 75 |
+
updated["image_token_id"] = vocab_size
|
| 76 |
+
updated["media_placeholder_token_id"] = vocab_size
|
| 77 |
+
updated["deepseek_vision"] = deepseek_vision_config(
|
| 78 |
+
routing_palette=routing_palette,
|
| 79 |
+
vocab_size=vocab_size,
|
| 80 |
+
)
|
| 81 |
+
return updated
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def augment_weight_map(
|
| 85 |
+
index: dict,
|
| 86 |
+
*,
|
| 87 |
+
tower_keys: Iterable[str],
|
| 88 |
+
projector_keys: Iterable[str],
|
| 89 |
+
tower_size: int = 0,
|
| 90 |
+
projector_size: int = 0,
|
| 91 |
+
) -> dict:
|
| 92 |
+
updated = json.loads(json.dumps(index))
|
| 93 |
+
weight_map = updated.setdefault("weight_map", {})
|
| 94 |
+
existing_files = set(weight_map.values())
|
| 95 |
+
additions = {
|
| 96 |
+
**{str(name): "vision_tower.safetensors" for name in tower_keys},
|
| 97 |
+
**{str(name): "mm_projector.safetensors" for name in projector_keys},
|
| 98 |
+
}
|
| 99 |
+
collisions = sorted(
|
| 100 |
+
name
|
| 101 |
+
for name, filename in additions.items()
|
| 102 |
+
if name in weight_map and weight_map[name] != filename
|
| 103 |
+
)
|
| 104 |
+
if collisions:
|
| 105 |
+
raise ValueError(f"component tensor names collide with backbone keys: {collisions[:8]}")
|
| 106 |
+
weight_map.update(additions)
|
| 107 |
+
added_size = sum(
|
| 108 |
+
size
|
| 109 |
+
for filename, size in (
|
| 110 |
+
("vision_tower.safetensors", int(tower_size)),
|
| 111 |
+
("mm_projector.safetensors", int(projector_size)),
|
| 112 |
+
)
|
| 113 |
+
if filename not in existing_files
|
| 114 |
+
)
|
| 115 |
+
if added_size:
|
| 116 |
+
metadata = updated.setdefault("metadata", {})
|
| 117 |
+
metadata["total_size"] = int(metadata.get("total_size", 0)) + added_size
|
| 118 |
+
return updated
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def safetensor_keys(path: Path) -> list[str]:
|
| 122 |
+
with path.open("rb") as handle:
|
| 123 |
+
prefix = handle.read(8)
|
| 124 |
+
if len(prefix) != 8:
|
| 125 |
+
raise ValueError(f"invalid safetensors header prefix: {path}")
|
| 126 |
+
header_size = struct.unpack("<Q", prefix)[0]
|
| 127 |
+
if header_size <= 0 or header_size > 100 * 1024 * 1024:
|
| 128 |
+
raise ValueError(f"invalid safetensors header size {header_size}: {path}")
|
| 129 |
+
header = json.loads(handle.read(header_size))
|
| 130 |
+
return [str(name) for name in header if name != "__metadata__"]
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def write_json_atomic(path: Path, payload: dict) -> None:
|
| 134 |
+
with tempfile.NamedTemporaryFile(
|
| 135 |
+
mode="w",
|
| 136 |
+
encoding="utf-8",
|
| 137 |
+
dir=path.parent,
|
| 138 |
+
prefix=f".{path.name}.",
|
| 139 |
+
suffix=".tmp",
|
| 140 |
+
delete=False,
|
| 141 |
+
) as handle:
|
| 142 |
+
json.dump(payload, handle, indent=2, sort_keys=True)
|
| 143 |
+
handle.write("\n")
|
| 144 |
+
temporary = Path(handle.name)
|
| 145 |
+
temporary.replace(path)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def stage_extension(model_dir: Path, source_root: Path) -> None:
|
| 149 |
+
source = source_root / "sglang_ext" / "deepseek_vision_sglang"
|
| 150 |
+
if not source.is_dir():
|
| 151 |
+
raise ValueError(f"SGLang extension source is missing: {source}")
|
| 152 |
+
target_root = model_dir / "sglang_ext"
|
| 153 |
+
target_root.mkdir(exist_ok=True)
|
| 154 |
+
target = target_root / source.name
|
| 155 |
+
if target.exists():
|
| 156 |
+
shutil.rmtree(target)
|
| 157 |
+
shutil.copytree(source, target, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def prepare(model_dir: Path, palette_path: Path, source_root: Path) -> None:
|
| 161 |
+
required = [
|
| 162 |
+
model_dir / "config.json",
|
| 163 |
+
model_dir / "model.safetensors.index.json",
|
| 164 |
+
model_dir / "vision_tower.safetensors",
|
| 165 |
+
model_dir / "mm_projector.safetensors",
|
| 166 |
+
]
|
| 167 |
+
missing = [str(path) for path in required if not path.is_file()]
|
| 168 |
+
if missing:
|
| 169 |
+
raise ValueError(f"model directory is incomplete: {missing}")
|
| 170 |
+
palette = json.loads(palette_path.read_text(encoding="utf-8"))
|
| 171 |
+
config = json.loads(required[0].read_text(encoding="utf-8"))
|
| 172 |
+
index = json.loads(required[1].read_text(encoding="utf-8"))
|
| 173 |
+
write_json_atomic(required[0], augment_config(config, palette))
|
| 174 |
+
write_json_atomic(
|
| 175 |
+
required[1],
|
| 176 |
+
augment_weight_map(
|
| 177 |
+
index,
|
| 178 |
+
tower_keys=safetensor_keys(required[2]),
|
| 179 |
+
projector_keys=safetensor_keys(required[3]),
|
| 180 |
+
tower_size=required[2].stat().st_size,
|
| 181 |
+
projector_size=required[3].stat().st_size,
|
| 182 |
+
),
|
| 183 |
+
)
|
| 184 |
+
stage_extension(model_dir, source_root)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 188 |
+
parser = argparse.ArgumentParser(
|
| 189 |
+
description="Stage a DeepSeek V4 + MoonViT model directory for the pinned SGLang fork"
|
| 190 |
+
)
|
| 191 |
+
parser.add_argument("model_dir", type=Path)
|
| 192 |
+
parser.add_argument(
|
| 193 |
+
"--palette",
|
| 194 |
+
type=Path,
|
| 195 |
+
default=Path("configs/routing/deepseek-v4-flash-60d8d707-palette64.json"),
|
| 196 |
+
)
|
| 197 |
+
parser.add_argument(
|
| 198 |
+
"--source-root",
|
| 199 |
+
type=Path,
|
| 200 |
+
default=Path(__file__).resolve().parents[1],
|
| 201 |
+
)
|
| 202 |
+
return parser
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def main(argv: list[str] | None = None) -> int:
|
| 206 |
+
args = build_parser().parse_args(argv)
|
| 207 |
+
prepare(args.model_dir.resolve(), args.palette.resolve(), args.source_root.resolve())
|
| 208 |
+
print(f"prepared {args.model_dir.resolve()} for pinned SGLang {SGLANG_SOURCE_COMMIT}")
|
| 209 |
+
return 0
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
raise SystemExit(main())
|
scripts/smoke_sglang_moonvit.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import base64
|
| 6 |
+
import json
|
| 7 |
+
import mimetypes
|
| 8 |
+
import urllib.request
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
DEFAULT_PROMPT = (
|
| 13 |
+
"<|begin▁of▁sentence|><|User|><image>"
|
| 14 |
+
"Describe this image.<|Assistant|></think>"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def image_data_url(path: Path) -> str:
|
| 19 |
+
mime_type, _ = mimetypes.guess_type(path.name)
|
| 20 |
+
if mime_type not in {"image/jpeg", "image/png", "image/webp"}:
|
| 21 |
+
raise ValueError("smoke image must be JPEG, PNG, or WebP")
|
| 22 |
+
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
|
| 23 |
+
return f"data:{mime_type};base64,{encoded}"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def build_payload(path: Path, prompt: str, max_new_tokens: int) -> dict:
|
| 27 |
+
if prompt.count("<image>") != 1:
|
| 28 |
+
raise ValueError("prompt must contain exactly one literal <image> marker")
|
| 29 |
+
return {
|
| 30 |
+
"text": prompt,
|
| 31 |
+
"image_data": image_data_url(path),
|
| 32 |
+
"sampling_params": {
|
| 33 |
+
"temperature": 0,
|
| 34 |
+
"max_new_tokens": int(max_new_tokens),
|
| 35 |
+
},
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 40 |
+
parser = argparse.ArgumentParser(description="Smoke-test the custom SGLang /generate path")
|
| 41 |
+
parser.add_argument("image", type=Path)
|
| 42 |
+
parser.add_argument("--url", default="http://127.0.0.1:30000/generate")
|
| 43 |
+
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 44 |
+
parser.add_argument("--max-new-tokens", type=int, default=64)
|
| 45 |
+
parser.add_argument("--timeout", type=float, default=180.0)
|
| 46 |
+
return parser
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def main(argv: list[str] | None = None) -> int:
|
| 50 |
+
args = build_parser().parse_args(argv)
|
| 51 |
+
payload = build_payload(args.image.resolve(), args.prompt, args.max_new_tokens)
|
| 52 |
+
request = urllib.request.Request(
|
| 53 |
+
args.url,
|
| 54 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 55 |
+
headers={"Content-Type": "application/json"},
|
| 56 |
+
method="POST",
|
| 57 |
+
)
|
| 58 |
+
with urllib.request.urlopen(request, timeout=args.timeout) as response:
|
| 59 |
+
result = json.loads(response.read().decode("utf-8"))
|
| 60 |
+
generated = result.get("text")
|
| 61 |
+
if not isinstance(generated, str) or not generated.strip():
|
| 62 |
+
raise RuntimeError(f"SGLang returned no generated text: {result}")
|
| 63 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
| 64 |
+
return 0
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
raise SystemExit(main())
|
sglang_ext/deepseek_vision_sglang/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""External SGLang package for the DeepSeek V4 + MoonViT adapter.
|
| 2 |
+
|
| 3 |
+
This package is intentionally standalone. A staged Hugging Face model directory can
|
| 4 |
+
ship ``sglang_ext/deepseek_vision_sglang`` and expose it through ``PYTHONPATH`` without
|
| 5 |
+
installing the training project.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
SGLANG_SOURCE_COMMIT = "723c2776401987cba3cfee25830db50f3964b83f"
|
| 9 |
+
|
| 10 |
+
__all__ = ["SGLANG_SOURCE_COMMIT"]
|
sglang_ext/deepseek_vision_sglang/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""SGLang external model registry package."""
|
sglang_ext/deepseek_vision_sglang/models/deepseek_v4_moonvit.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, List, Optional, Tuple
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
|
| 8 |
+
from sglang.srt.configs.kimi_k25 import KimiK25VisionConfig
|
| 9 |
+
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
|
| 10 |
+
from sglang.srt.managers.mm_utils import (
|
| 11 |
+
MultiModalityDataPaddingPatternMultimodalTokens,
|
| 12 |
+
general_mm_embed_routine,
|
| 13 |
+
)
|
| 14 |
+
from sglang.srt.managers.schedule_batch import Modality, MultimodalDataItem, MultimodalInputs
|
| 15 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
| 16 |
+
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
| 17 |
+
from sglang.srt.models.deepseek_v4 import (
|
| 18 |
+
DeepseekV4ForCausalLM as SGLangDeepseekV4ForCausalLM,
|
| 19 |
+
)
|
| 20 |
+
from sglang.srt.models.kimi_k25 import (
|
| 21 |
+
K2VLMultiModalProjector,
|
| 22 |
+
MoonViT3dPretrainedModel,
|
| 23 |
+
mm_projection_auto,
|
| 24 |
+
)
|
| 25 |
+
from sglang.srt.multimodal.mm_utils import materialize_multimodal_features
|
| 26 |
+
from sglang.srt.runtime_context import get_mm, get_parallel, get_server_args
|
| 27 |
+
|
| 28 |
+
from deepseek_vision_sglang.routing import ROUTING_IDS_ATTR, build_sglang_routing_ids
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _config_dict(config, name: str) -> dict:
|
| 32 |
+
value = getattr(config, name, None)
|
| 33 |
+
if value is None:
|
| 34 |
+
raise ValueError(f"config.json is missing {name}")
|
| 35 |
+
if isinstance(value, dict):
|
| 36 |
+
return dict(value)
|
| 37 |
+
if hasattr(value, "to_dict"):
|
| 38 |
+
return value.to_dict()
|
| 39 |
+
raise TypeError(f"config.json field {name} must be an object")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class RoutingAwareDeepseekV4ForCausalLM(SGLangDeepseekV4ForCausalLM):
|
| 43 |
+
"""Recover route IDs when SGLang's multimodal routine supplies embeddings."""
|
| 44 |
+
|
| 45 |
+
@torch.no_grad()
|
| 46 |
+
def forward(
|
| 47 |
+
self,
|
| 48 |
+
input_ids: Optional[torch.Tensor],
|
| 49 |
+
positions: torch.Tensor,
|
| 50 |
+
forward_batch: ForwardBatch,
|
| 51 |
+
input_embeds: Optional[torch.Tensor] = None,
|
| 52 |
+
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
| 53 |
+
) -> torch.Tensor:
|
| 54 |
+
if input_ids is None:
|
| 55 |
+
input_ids = getattr(forward_batch, ROUTING_IDS_ATTR, None)
|
| 56 |
+
if input_ids is None:
|
| 57 |
+
raise RuntimeError("DeepSeek V4 routing IDs were not attached to the forward batch")
|
| 58 |
+
return super().forward(
|
| 59 |
+
input_ids=input_ids,
|
| 60 |
+
positions=positions,
|
| 61 |
+
forward_batch=forward_batch,
|
| 62 |
+
input_embeds=input_embeds,
|
| 63 |
+
pp_proxy_tensors=pp_proxy_tensors,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class DeepseekV4ForCausalLM(nn.Module):
|
| 68 |
+
"""MoonViT wrapper that deliberately overwrites SGLang's text-only V4 entry.
|
| 69 |
+
|
| 70 |
+
Keeping the architecture name unchanged is required: SGLang's DeepSeek V4
|
| 71 |
+
attention, FP4 expert-layout, and memory-pool selection all key off this exact
|
| 72 |
+
architecture string. ``SGLANG_EXTERNAL_MODEL_PACKAGE`` performs the overwrite.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(self, config, quant_config=None, prefix: str = "") -> None:
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.config = config
|
| 78 |
+
self.quant_config = quant_config
|
| 79 |
+
adapter = _config_dict(config, "deepseek_vision")
|
| 80 |
+
vision_config = KimiK25VisionConfig(**_config_dict(config, "vision_config"))
|
| 81 |
+
self.route_palette = tuple(int(value) for value in adapter["routing_palette"])
|
| 82 |
+
if not self.route_palette:
|
| 83 |
+
raise ValueError("deepseek_vision.routing_palette cannot be empty")
|
| 84 |
+
if min(self.route_palette) < 0 or max(self.route_palette) >= int(config.vocab_size):
|
| 85 |
+
raise ValueError("deepseek_vision.routing_palette contains an invalid token ID")
|
| 86 |
+
if int(vision_config.text_hidden_size) != int(config.hidden_size):
|
| 87 |
+
raise ValueError("vision projector output size differs from DeepSeek hidden size")
|
| 88 |
+
if get_mm().mm_enable_dp_encoder:
|
| 89 |
+
raise NotImplementedError(
|
| 90 |
+
"DeepSeek V4 + MoonViT has not validated SGLang encoder data parallelism"
|
| 91 |
+
)
|
| 92 |
+
if get_parallel().pp_size > 1:
|
| 93 |
+
raise NotImplementedError(
|
| 94 |
+
"DeepSeek V4 + MoonViT has not validated pipeline parallelism"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
self.vision_tower = MoonViT3dPretrainedModel(
|
| 98 |
+
vision_config,
|
| 99 |
+
use_data_parallel=False,
|
| 100 |
+
prefix="vision_tower",
|
| 101 |
+
)
|
| 102 |
+
self.mm_projector = K2VLMultiModalProjector(vision_config)
|
| 103 |
+
self.language_model = RoutingAwareDeepseekV4ForCausalLM(
|
| 104 |
+
config,
|
| 105 |
+
quant_config,
|
| 106 |
+
prefix=prefix,
|
| 107 |
+
)
|
| 108 |
+
if hasattr(self.language_model, "dtype"):
|
| 109 |
+
target_dtype = self.language_model.dtype
|
| 110 |
+
self.vision_tower = self.vision_tower.to(dtype=target_dtype)
|
| 111 |
+
self.mm_projector = self.mm_projector.to(dtype=target_dtype)
|
| 112 |
+
self._loaded_vision_params: set[str] = set()
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def model(self):
|
| 116 |
+
return self.language_model
|
| 117 |
+
|
| 118 |
+
def __setattr__(self, name, value):
|
| 119 |
+
if name == "model":
|
| 120 |
+
return
|
| 121 |
+
super().__setattr__(name, value)
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def pp_group(self):
|
| 125 |
+
return self.language_model.pp_group
|
| 126 |
+
|
| 127 |
+
@property
|
| 128 |
+
def start_layer(self) -> int:
|
| 129 |
+
return self.language_model.start_layer
|
| 130 |
+
|
| 131 |
+
@property
|
| 132 |
+
def end_layer(self) -> int:
|
| 133 |
+
return self.language_model.end_layer
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def routed_experts_weights_of_layer(self):
|
| 137 |
+
return self.language_model.routed_experts_weights_of_layer
|
| 138 |
+
|
| 139 |
+
def get_input_embeddings(self):
|
| 140 |
+
return self.language_model.get_input_embeddings()
|
| 141 |
+
|
| 142 |
+
def _materialize_image_features(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
| 143 |
+
device = self.vision_tower.device
|
| 144 |
+
dtype = self.vision_tower.patch_embed.proj.weight.dtype
|
| 145 |
+
parallel = get_parallel()
|
| 146 |
+
server_args = get_server_args()
|
| 147 |
+
consumer_count = max(getattr(server_args, "tp_size", parallel.attn_tp_size), 1)
|
| 148 |
+
device_index = device.index
|
| 149 |
+
if device.type == "cuda" and device_index is None:
|
| 150 |
+
device_index = torch.cuda.current_device()
|
| 151 |
+
features = []
|
| 152 |
+
for item in items:
|
| 153 |
+
if device.type == "cuda":
|
| 154 |
+
item.reconstruct(device_index, ipc_consumer_count=consumer_count)
|
| 155 |
+
features.append(item.feature)
|
| 156 |
+
return materialize_multimodal_features(features, device=device, dtype=dtype)
|
| 157 |
+
|
| 158 |
+
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
| 159 |
+
grid_thws = []
|
| 160 |
+
for item in items:
|
| 161 |
+
grid = item.model_specific_data.get("image_grid_thw")
|
| 162 |
+
if grid is None:
|
| 163 |
+
grid = item.model_specific_data["grid_thws"]
|
| 164 |
+
grid_thws.append(grid)
|
| 165 |
+
grid_thws = torch.concat(grid_thws, dim=0)
|
| 166 |
+
pixel_values = self._materialize_image_features(items)
|
| 167 |
+
image_features = self.vision_tower(pixel_values, grid_thws)
|
| 168 |
+
return mm_projection_auto(self.mm_projector, image_features)
|
| 169 |
+
|
| 170 |
+
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
|
| 171 |
+
return MultiModalityDataPaddingPatternMultimodalTokens().pad_input_tokens(
|
| 172 |
+
input_ids, mm_inputs
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def forward(
|
| 176 |
+
self,
|
| 177 |
+
input_ids: torch.Tensor,
|
| 178 |
+
positions: torch.Tensor,
|
| 179 |
+
forward_batch: ForwardBatch,
|
| 180 |
+
get_embedding: bool = False,
|
| 181 |
+
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
| 182 |
+
):
|
| 183 |
+
if get_embedding:
|
| 184 |
+
raise NotImplementedError("embedding mode is not supported by this generative adapter")
|
| 185 |
+
routing_ids = build_sglang_routing_ids(input_ids, forward_batch, self.route_palette)
|
| 186 |
+
setattr(forward_batch, ROUTING_IDS_ATTR, routing_ids)
|
| 187 |
+
return general_mm_embed_routine(
|
| 188 |
+
input_ids=input_ids,
|
| 189 |
+
forward_batch=forward_batch,
|
| 190 |
+
language_model=self.language_model,
|
| 191 |
+
multimodal_model=self,
|
| 192 |
+
data_embedding_funcs={Modality.IMAGE: self.get_image_feature},
|
| 193 |
+
positions=positions,
|
| 194 |
+
pp_proxy_tensors=pp_proxy_tensors,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
| 198 |
+
params = dict(self.named_parameters(remove_duplicate=False))
|
| 199 |
+
|
| 200 |
+
def load_component(name: str, loaded_weight: torch.Tensor) -> bool:
|
| 201 |
+
tower_name = name.removeprefix("vision_tower.")
|
| 202 |
+
tower_name = tower_name.replace("wqkv.", "attn.qkv_proj.").replace(
|
| 203 |
+
"wo.", "attn.proj."
|
| 204 |
+
)
|
| 205 |
+
tower_param = f"vision_tower.{tower_name}"
|
| 206 |
+
projector_name = name.removeprefix("mm_projector.")
|
| 207 |
+
projector_name = projector_name.replace("proj.0", "linear_1").replace(
|
| 208 |
+
"proj.2", "linear_2"
|
| 209 |
+
)
|
| 210 |
+
projector_param = f"mm_projector.{projector_name}"
|
| 211 |
+
target = None
|
| 212 |
+
if tower_param in params:
|
| 213 |
+
target = tower_param
|
| 214 |
+
elif projector_param in params:
|
| 215 |
+
target = projector_param
|
| 216 |
+
if target is None:
|
| 217 |
+
return False
|
| 218 |
+
param = params[target]
|
| 219 |
+
loader = getattr(param, "weight_loader", default_weight_loader)
|
| 220 |
+
loader(param, loaded_weight)
|
| 221 |
+
self._loaded_vision_params.add(target)
|
| 222 |
+
return True
|
| 223 |
+
|
| 224 |
+
def language_weights():
|
| 225 |
+
for name, loaded_weight in weights:
|
| 226 |
+
if load_component(name, loaded_weight):
|
| 227 |
+
continue
|
| 228 |
+
yield name.removeprefix("language_model."), loaded_weight
|
| 229 |
+
|
| 230 |
+
self.language_model.load_weights(language_weights())
|
| 231 |
+
|
| 232 |
+
required = {
|
| 233 |
+
name
|
| 234 |
+
for name in params
|
| 235 |
+
if name.startswith("vision_tower.") or name.startswith("mm_projector.")
|
| 236 |
+
}
|
| 237 |
+
missing = sorted(required - self._loaded_vision_params)
|
| 238 |
+
if missing:
|
| 239 |
+
sample = ", ".join(missing[:8])
|
| 240 |
+
raise RuntimeError(
|
| 241 |
+
f"vision_tower.safetensors/mm_projector.safetensors did not initialize "
|
| 242 |
+
f"{len(missing)} parameter(s): {sample}"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
def post_load_weights(self):
|
| 246 |
+
self.language_model.post_load_weights()
|
| 247 |
+
|
| 248 |
+
@property
|
| 249 |
+
def stacked_params_mapping(self):
|
| 250 |
+
return getattr(self.language_model, "stacked_params_mapping", [])
|
| 251 |
+
|
| 252 |
+
@property
|
| 253 |
+
def expert_params_mapping(self):
|
| 254 |
+
return getattr(self.language_model, "expert_params_mapping", [])
|
| 255 |
+
|
| 256 |
+
def mutate_weight_preload(self, name):
|
| 257 |
+
return self.language_model.mutate_weight_preload(name)
|
| 258 |
+
|
| 259 |
+
def custom_scale_remap(self, name):
|
| 260 |
+
return self.language_model.custom_scale_remap(name)
|
| 261 |
+
|
| 262 |
+
@classmethod
|
| 263 |
+
def get_model_config_for_expert_location(cls, config):
|
| 264 |
+
return ModelConfigForExpertLocation(
|
| 265 |
+
num_layers=config.num_hidden_layers,
|
| 266 |
+
num_logical_experts=config.n_routed_experts,
|
| 267 |
+
num_groups=None,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
EntryClass = [DeepseekV4ForCausalLM]
|
sglang_ext/deepseek_vision_sglang/patch.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import importlib.util
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from . import SGLANG_SOURCE_COMMIT
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
OLD = "hidden_states = self.embed_tokens(input_ids)"
|
| 11 |
+
NEW = "hidden_states = input_embeds if input_embeds is not None else self.embed_tokens(input_ids)"
|
| 12 |
+
FORWARD_ANCHOR = "class DeepseekV4Model(nn.Module):"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def resolve_sglang_deepseek_v4_source() -> Path:
|
| 16 |
+
spec = importlib.util.find_spec("sglang.srt.models.deepseek_v4")
|
| 17 |
+
if spec is None or spec.origin is None:
|
| 18 |
+
raise RuntimeError("could not locate sglang.srt.models.deepseek_v4")
|
| 19 |
+
return Path(spec.origin).resolve()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def patch_deepseek_v4_source(path: str | Path, *, check_only: bool = False) -> bool:
|
| 23 |
+
"""Apply the one-line routing-aware embedding patch.
|
| 24 |
+
|
| 25 |
+
Returns ``True`` when the source needed a patch and ``False`` when it was already
|
| 26 |
+
patched. ``check_only`` validates the exact source anchor without writing.
|
| 27 |
+
"""
|
| 28 |
+
source = Path(path)
|
| 29 |
+
text = source.read_text(encoding="utf-8")
|
| 30 |
+
if FORWARD_ANCHOR not in text:
|
| 31 |
+
raise RuntimeError("not a recognized SGLang DeepSeek V4 model source")
|
| 32 |
+
if text.count(NEW) == 1:
|
| 33 |
+
return False
|
| 34 |
+
count = text.count(OLD)
|
| 35 |
+
if count != 1:
|
| 36 |
+
raise RuntimeError(
|
| 37 |
+
"expected exactly one SGLang DeepSeek V4 embedding site; "
|
| 38 |
+
f"found {count}. The extension is pinned to {SGLANG_SOURCE_COMMIT}."
|
| 39 |
+
)
|
| 40 |
+
if not check_only:
|
| 41 |
+
source.write_text(text.replace(OLD, NEW), encoding="utf-8")
|
| 42 |
+
return True
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 46 |
+
parser = argparse.ArgumentParser(description="Patch the pinned SGLang DeepSeek V4 loader")
|
| 47 |
+
mode = parser.add_mutually_exclusive_group(required=True)
|
| 48 |
+
mode.add_argument("--check", action="store_true")
|
| 49 |
+
mode.add_argument("--apply", action="store_true")
|
| 50 |
+
parser.add_argument("source", nargs="?", help="deepseek_v4.py; auto-detected when omitted")
|
| 51 |
+
return parser
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def main(argv: list[str] | None = None) -> int:
|
| 55 |
+
args = build_parser().parse_args(argv)
|
| 56 |
+
source = Path(args.source).resolve() if args.source else resolve_sglang_deepseek_v4_source()
|
| 57 |
+
needed = patch_deepseek_v4_source(source, check_only=args.check)
|
| 58 |
+
state = "patchable" if needed and args.check else "patched" if needed else "already-patched"
|
| 59 |
+
print(f"{source}: {state}; pinned SGLang commit {SGLANG_SOURCE_COMMIT}")
|
| 60 |
+
return 0
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
raise SystemExit(main())
|
sglang_ext/deepseek_vision_sglang/processors/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""SGLang external multimodal processor registry package."""
|
sglang_ext/deepseek_vision_sglang/processors/moonvit.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Dict, List, Union
|
| 5 |
+
|
| 6 |
+
from transformers import AutoProcessor
|
| 7 |
+
|
| 8 |
+
from sglang.srt.managers.schedule_batch import MultimodalProcessorOutput
|
| 9 |
+
from sglang.srt.multimodal.processors.base_processor import (
|
| 10 |
+
BaseMultimodalProcessor,
|
| 11 |
+
MultimodalSpecialTokens,
|
| 12 |
+
)
|
| 13 |
+
from sglang.srt.multimodal.processors.kimi_common import KimiGridMMDataMixin
|
| 14 |
+
from sglang.srt.multimodal.processors.kimi_k25 import KimiGPUProcessorWrapper
|
| 15 |
+
|
| 16 |
+
from deepseek_vision_sglang.models.deepseek_v4_moonvit import DeepseekV4ForCausalLM
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _adapter_config(hf_config) -> dict:
|
| 20 |
+
value = getattr(hf_config, "deepseek_vision", None)
|
| 21 |
+
if isinstance(value, dict):
|
| 22 |
+
return value
|
| 23 |
+
if hasattr(value, "to_dict"):
|
| 24 |
+
return value.to_dict()
|
| 25 |
+
raise ValueError("config.json is missing deepseek_vision")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class DeepseekV4MoonViTProcessor(KimiGridMMDataMixin, BaseMultimodalProcessor):
|
| 29 |
+
models = [DeepseekV4ForCausalLM]
|
| 30 |
+
prefer_tokenized_input = False
|
| 31 |
+
gpu_image_decode = True
|
| 32 |
+
precompute_hash_before_cpu_transfer = True
|
| 33 |
+
preserve_processor_input_ids = True
|
| 34 |
+
supports_mm_processor_concurrency = False
|
| 35 |
+
|
| 36 |
+
def __init__(self, hf_config, server_args, text_processor, *args, **kwargs):
|
| 37 |
+
adapter = _adapter_config(hf_config)
|
| 38 |
+
text_tokenizer = getattr(text_processor, "tokenizer", text_processor)
|
| 39 |
+
tower_processor = AutoProcessor.from_pretrained(
|
| 40 |
+
adapter["tower_model_id"],
|
| 41 |
+
revision=adapter["tower_revision"],
|
| 42 |
+
trust_remote_code=True,
|
| 43 |
+
use_fast=True,
|
| 44 |
+
)
|
| 45 |
+
tower_processor.tokenizer = text_tokenizer
|
| 46 |
+
media_cfg = tower_processor.media_processor.media_proc_cfg
|
| 47 |
+
image_token = adapter.get("image_placeholder", "<image>")
|
| 48 |
+
image_token_id = int(adapter.get("image_placeholder_token_id", hf_config.vocab_size))
|
| 49 |
+
mm_tokens = MultimodalSpecialTokens(
|
| 50 |
+
image_token=image_token,
|
| 51 |
+
image_token_id=image_token_id,
|
| 52 |
+
image_token_regex=re.compile(re.escape(image_token)),
|
| 53 |
+
).build(tower_processor)
|
| 54 |
+
max_image_tokens = int(adapter.get("max_image_tokens", 512))
|
| 55 |
+
merge_h, merge_w = media_cfg["merge_kernel_size"]
|
| 56 |
+
processor = KimiGPUProcessorWrapper(
|
| 57 |
+
tower_processor,
|
| 58 |
+
image_token=mm_tokens.image_token,
|
| 59 |
+
image_token_id=mm_tokens.image_token_id,
|
| 60 |
+
patch_size=media_cfg["patch_size"],
|
| 61 |
+
merge_kernel_size=media_cfg["merge_kernel_size"],
|
| 62 |
+
in_patch_limit=max_image_tokens * int(merge_h) * int(merge_w),
|
| 63 |
+
patch_limit_on_one_side=media_cfg["patch_limit_on_one_side"],
|
| 64 |
+
fixed_output_tokens=media_cfg.get("fixed_output_tokens"),
|
| 65 |
+
image_mean=media_cfg["image_mean"],
|
| 66 |
+
image_std=media_cfg["image_std"],
|
| 67 |
+
)
|
| 68 |
+
super().__init__(hf_config, server_args, processor, *args, **kwargs)
|
| 69 |
+
self.mm_tokens = mm_tokens
|
| 70 |
+
|
| 71 |
+
def _encode_prompt(self, prompt: str, image_count: int) -> list[int]:
|
| 72 |
+
if not isinstance(prompt, str):
|
| 73 |
+
raise ValueError("MoonViT SGLang processor requires a string prompt")
|
| 74 |
+
parts = prompt.split(self.mm_tokens.image_token)
|
| 75 |
+
if len(parts) - 1 != image_count:
|
| 76 |
+
raise ValueError(
|
| 77 |
+
f"prompt contains {len(parts) - 1} image placeholder(s), "
|
| 78 |
+
f"but request contains {image_count} image(s)"
|
| 79 |
+
)
|
| 80 |
+
input_ids: list[int] = []
|
| 81 |
+
for index, part in enumerate(parts):
|
| 82 |
+
input_ids.extend(self._tokenizer.encode(part, add_special_tokens=False))
|
| 83 |
+
if index < image_count:
|
| 84 |
+
input_ids.append(int(self.mm_tokens.image_token_id))
|
| 85 |
+
return input_ids
|
| 86 |
+
|
| 87 |
+
async def process_mm_data_async(
|
| 88 |
+
self,
|
| 89 |
+
image_data: List[Union[str, bytes, Dict]],
|
| 90 |
+
input_text,
|
| 91 |
+
request_obj,
|
| 92 |
+
*args,
|
| 93 |
+
**kwargs,
|
| 94 |
+
):
|
| 95 |
+
images = image_data or []
|
| 96 |
+
if len(images) != 1:
|
| 97 |
+
raise ValueError("DeepSeek V4 + MoonViT currently accepts exactly one image")
|
| 98 |
+
input_ids = self._encode_prompt(input_text, len(images))
|
| 99 |
+
base_output = await self.fast_load_mm_data(
|
| 100 |
+
prompt=input_text,
|
| 101 |
+
image_data=images,
|
| 102 |
+
multimodal_tokens=self.mm_tokens,
|
| 103 |
+
input_ids=input_ids,
|
| 104 |
+
)
|
| 105 |
+
mm_items, expanded_ids, _ = await self.process_and_combine_mm_data_async(
|
| 106 |
+
base_output,
|
| 107 |
+
self.mm_tokens,
|
| 108 |
+
sglang_original_input_ids=input_ids,
|
| 109 |
+
)
|
| 110 |
+
return MultimodalProcessorOutput(
|
| 111 |
+
input_ids=expanded_ids.tolist(),
|
| 112 |
+
mm_items=mm_items,
|
| 113 |
+
im_token_id=self.mm_tokens.image_token_id,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
__all__ = ["DeepseekV4MoonViTProcessor"]
|
sglang_ext/deepseek_vision_sglang/routing.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Sequence
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ROUTING_IDS_ATTR = "_deepseek_vision_routing_ids"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _as_int_list(values: Any) -> list[int]:
|
| 11 |
+
if values is None:
|
| 12 |
+
return []
|
| 13 |
+
if hasattr(values, "tolist"):
|
| 14 |
+
values = values.tolist()
|
| 15 |
+
return [int(value) for value in values]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _item_is_image(item: Any) -> bool:
|
| 19 |
+
modality = getattr(item, "modality", None)
|
| 20 |
+
name = getattr(modality, "name", modality)
|
| 21 |
+
return str(name).upper() == "IMAGE"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _item_offsets(item: Any) -> list[tuple[int, int]]:
|
| 25 |
+
offsets = getattr(item, "offsets", None) or []
|
| 26 |
+
return [(int(start), int(end)) for start, end in offsets]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def routing_replacements(
|
| 30 |
+
*,
|
| 31 |
+
extend_prefix_lens: Sequence[int],
|
| 32 |
+
extend_seq_lens: Sequence[int],
|
| 33 |
+
mm_inputs: Sequence[Any],
|
| 34 |
+
palette: Sequence[int],
|
| 35 |
+
) -> list[tuple[int, int]]:
|
| 36 |
+
"""Return flattened ``(position, route_id)`` replacements for image slots.
|
| 37 |
+
|
| 38 |
+
SGLang flattens each request's current extend chunk into one token vector. An
|
| 39 |
+
item's offsets remain absolute, inclusive prompt offsets. Basing the palette
|
| 40 |
+
phase on ``absolute_position - image_start`` keeps routing identical when a long
|
| 41 |
+
image prefix is split across chunked-prefill batches.
|
| 42 |
+
"""
|
| 43 |
+
if not palette:
|
| 44 |
+
raise ValueError("routing palette cannot be empty")
|
| 45 |
+
prefixes = _as_int_list(extend_prefix_lens)
|
| 46 |
+
lengths = _as_int_list(extend_seq_lens)
|
| 47 |
+
inputs = list(mm_inputs or [])
|
| 48 |
+
if not (len(prefixes) == len(lengths) == len(inputs)):
|
| 49 |
+
raise ValueError("SGLang request metadata lengths differ")
|
| 50 |
+
|
| 51 |
+
replacements: list[tuple[int, int]] = []
|
| 52 |
+
flat_request_start = 0
|
| 53 |
+
for prefix_len, seq_len, request_mm in zip(prefixes, lengths, inputs, strict=True):
|
| 54 |
+
chunk_start = prefix_len
|
| 55 |
+
chunk_end = prefix_len + seq_len
|
| 56 |
+
items = getattr(request_mm, "mm_items", None) if request_mm is not None else None
|
| 57 |
+
for item in items or []:
|
| 58 |
+
if not _item_is_image(item):
|
| 59 |
+
continue
|
| 60 |
+
for image_start, image_end_inclusive in _item_offsets(item):
|
| 61 |
+
overlap_start = max(chunk_start, image_start)
|
| 62 |
+
overlap_end = min(chunk_end, image_end_inclusive + 1)
|
| 63 |
+
for absolute_position in range(overlap_start, overlap_end):
|
| 64 |
+
flat_position = flat_request_start + absolute_position - chunk_start
|
| 65 |
+
palette_index = (absolute_position - image_start) % len(palette)
|
| 66 |
+
replacements.append((flat_position, int(palette[palette_index])))
|
| 67 |
+
flat_request_start += seq_len
|
| 68 |
+
return replacements
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_sglang_routing_ids(input_ids: Any, forward_batch: Any, palette: Sequence[int]):
|
| 72 |
+
"""Clone SGLang token IDs and replace only image slots with palette IDs."""
|
| 73 |
+
if input_ids is None:
|
| 74 |
+
raise ValueError("input_ids are required to build DeepSeek routing IDs")
|
| 75 |
+
result = input_ids.clone() if hasattr(input_ids, "clone") else list(input_ids)
|
| 76 |
+
mm_inputs = getattr(forward_batch, "mm_inputs", None)
|
| 77 |
+
if not mm_inputs:
|
| 78 |
+
return result
|
| 79 |
+
replacements = routing_replacements(
|
| 80 |
+
extend_prefix_lens=getattr(forward_batch, "extend_prefix_lens_cpu", None),
|
| 81 |
+
extend_seq_lens=getattr(forward_batch, "extend_seq_lens_cpu", None),
|
| 82 |
+
mm_inputs=mm_inputs,
|
| 83 |
+
palette=palette,
|
| 84 |
+
)
|
| 85 |
+
for position, route_id in replacements:
|
| 86 |
+
result[position] = route_id
|
| 87 |
+
return result
|