webbrain-one commited on
Commit
6bfbcbe
·
verified ·
1 Parent(s): e2c1eaf

Promote SGLang MoonViT integration to main

Browse files

Preserves 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 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:2d83d58754cff11724f117d20d95e31803a48512d29f8e00463b2501905d6d72
3
- size 11205914
 
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