danielhanchen commited on
Commit
fa50b55
·
verified ·
1 Parent(s): e697f86

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +5 -0
  3. LICENSE +21 -0
  4. README.md +132 -0
  5. chat_template.jinja +248 -0
  6. config.json +197 -0
  7. encoding/README.md +71 -0
  8. encoding/encoding_dsv4.py +957 -0
  9. encoding/test_encoding_dsv4.py +215 -0
  10. encoding/tests/test_input_1.json +81 -0
  11. encoding/tests/test_input_2.json +24 -0
  12. encoding/tests/test_input_3.json +159 -0
  13. encoding/tests/test_input_4.json +28 -0
  14. encoding/tests/test_output_1.txt +36 -0
  15. encoding/tests/test_output_2.txt +1 -0
  16. encoding/tests/test_output_3.txt +38 -0
  17. encoding/tests/test_output_4.txt +29 -0
  18. generation_config.json +9 -0
  19. inference/README.md +70 -0
  20. inference/config.json +50 -0
  21. inference/convert.py +167 -0
  22. inference/examples/example_vl.txt +1 -0
  23. inference/examples/example_vl_harmony.json +35 -0
  24. inference/examples/images/carrots.jpeg +3 -0
  25. inference/examples/images/corn.jpeg +0 -0
  26. inference/generate.py +209 -0
  27. inference/image_processor.py +184 -0
  28. inference/kernel.py +536 -0
  29. inference/model.py +1046 -0
  30. inference/requirements.txt +8 -0
  31. inference/run.sh +15 -0
  32. inference/vision.py +118 -0
  33. model-00001-of-00048.safetensors +3 -0
  34. model-00002-of-00048.safetensors +3 -0
  35. model-00003-of-00048.safetensors +3 -0
  36. model-00004-of-00048.safetensors +3 -0
  37. model-00005-of-00048.safetensors +3 -0
  38. model-00006-of-00048.safetensors +3 -0
  39. model-00007-of-00048.safetensors +3 -0
  40. model-00008-of-00048.safetensors +3 -0
  41. model-00009-of-00048.safetensors +3 -0
  42. model-00010-of-00048.safetensors +3 -0
  43. model-00011-of-00048.safetensors +3 -0
  44. model-00012-of-00048.safetensors +3 -0
  45. model-00013-of-00048.safetensors +3 -0
  46. model-00014-of-00048.safetensors +3 -0
  47. model-00015-of-00048.safetensors +3 -0
  48. model-00016-of-00048.safetensors +3 -0
  49. model-00017-of-00048.safetensors +3 -0
  50. model-00018-of-00048.safetensors +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ inference/examples/images/carrots.jpeg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .cache/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ inference/*_output.txt
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - unsloth
4
+ base_model:
5
+ - deepseek-ai/DeepSeek-V4-Flash-Vision-Exp
6
+ license: mit
7
+ library_name: transformers
8
+ ---
9
+ <div>
10
+ <p style="margin-top: 0;margin-bottom: 0;">
11
+ <em><a href="https://docs.unsloth.ai/basics/unsloth-dynamic-v2.0-gguf">Unsloth Dynamic 2.0</a> achieves superior accuracy & outperforms other leading quants.</em>
12
+ </p>
13
+ <div style="display: flex; gap: 5px; align-items: center; ">
14
+ <a href="https://github.com/unslothai/unsloth/">
15
+ <img src="https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png" width="133">
16
+ </a>
17
+ <a href="https://discord.gg/unsloth">
18
+ <img src="https://github.com/unslothai/unsloth/raw/main/images/Discord%20button.png" width="173">
19
+ </a>
20
+ <a href="https://docs.unsloth.ai/">
21
+ <img src="https://raw.githubusercontent.com/unslothai/unsloth/refs/heads/main/images/documentation%20green%20button.png" width="143">
22
+ </a>
23
+ </div>
24
+ </div>
25
+
26
+
27
+ # DeepSeek-V4-Flash-Vision-Exp
28
+
29
+ <!-- markdownlint-disable first-line-h1 -->
30
+ <!-- markdownlint-disable html -->
31
+ <!-- markdownlint-disable no-duplicate-header -->
32
+
33
+ <div align="center">
34
+ <img src="https://github.com/deepseek-ai/DeepSeek-V2/blob/main/figures/logo.svg?raw=true" width="60%" alt="DeepSeek-V4" />
35
+ </div>
36
+ <hr>
37
+ <div align="center" style="line-height: 1;">
38
+ <a href="https://www.deepseek.com/" target="_blank" style="margin: 2px;">
39
+ <img alt="Homepage" src="https://github.com/deepseek-ai/DeepSeek-V2/blob/main/figures/badge.svg?raw=true" style="display: inline-block; vertical-align: middle;"/>
40
+ </a>
41
+ <a href="https://chat.deepseek.com/" target="_blank" style="margin: 2px;">
42
+ <img alt="Chat" src="https://img.shields.io/badge/🤖%20Chat-DeepSeek%20V4-536af5?color=536af5&logoColor=white" style="display: inline-block; vertical-align: middle;"/>
43
+ </a>
44
+ </div>
45
+ <div align="center" style="line-height: 1;">
46
+ <a href="https://huggingface.co/deepseek-ai" target="_blank" style="margin: 2px;">
47
+ <img alt="Hugging Face" src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-DeepSeek%20AI-ffc107?color=ffc107&logoColor=white" style="display: inline-block; vertical-align: middle;"/>
48
+ </a>
49
+ <a href="https://twitter.com/deepseek_ai" target="_blank" style="margin: 2px;">
50
+ <img alt="Twitter Follow" src="https://img.shields.io/badge/Twitter-deepseek_ai-white?logo=x&logoColor=white" style="display: inline-block; vertical-align: middle;"/>
51
+ </a>
52
+ </div>
53
+ <div align="center" style="line-height: 1;">
54
+ <a href="LICENSE" style="margin: 2px;">
55
+ <img alt="License" src="https://img.shields.io/badge/License-MIT-f5de53?&color=f5de53" style="display: inline-block; vertical-align: middle;"/>
56
+ </a>
57
+ </div>
58
+
59
+ ## Introduction
60
+
61
+ We are excited to introduce **DeepSeek-V4-Flash-Vision-Exp**, our first experimental multimodal model in the DeepSeek-V4 family. It builds on the DeepSeek-V4-Flash architecture by incorporating visual modules and undergoing continued training to unlock visual understanding capabilities.
62
+
63
+ Compared to DeepSeek-V4-Flash-0731, DeepSeek-V4-Flash-Vision-Exp achieves substantial improvements on its multimodal agent capabilities, while maintaining comparable performance on text-only agent tasks.
64
+
65
+ <div align="center">
66
+
67
+ | Benchmark | DeepSeek-V4-Flash-Vision-Exp | DeepSeek-V4-Flash-0731 | Opus-4.8 |
68
+ | :--- | :---: | :---: | :---: |
69
+ | **Text Agent Capabilities** | | | |
70
+ | Terminal Bench 2.1 | 83.9 | 82.7 | 85.0 |
71
+ | NL2Repo | 57.7 | 54.2 | 69.7 |
72
+ | Cybergym | 75.3 | 76.7 | 78.3 |
73
+ | DeepSWE | 59.3 | 54.4 | 58.0 |
74
+ | Toolathlon-Verified | 75.9 | 70.3 | 76.2 |
75
+ | DSBench-Hard | 63.6 | 59.6 | 71.7 |
76
+ | AutomationBench (Public) | 25.7 | 25.1 | 27.2 |
77
+ | **Multimodal Agent Capabilities** | | | |
78
+ | ApexBench (Pass@1) | 36.5 | 26.2† | 39.4 |
79
+ | Agents' Last Exam | 27.3 | 25.2† | 25.7 |
80
+ | Chartography | 64.3 | - | 65.0 |
81
+ | ZeroBench (Pass@5) | 35.0 | - | 34.0 |
82
+
83
+ </div>
84
+
85
+ Notes:
86
+
87
+ 1. For the text agent benchmarks above, DeepSeek models are evaluated with the minimal mode of DeepSeek Harness as the agent framework, using the `max` reasoning effort level with `temperature = 1.0, top_p = 0.95`.
88
+ 2. † For ApexBench and Agents' Last Exam, DeepSeek-V4-Flash-0731 ignores the multimodal elements in the input.
89
+
90
+
91
+ ## Repository layout
92
+
93
+ This repository contains the tokenizer, prompt encoding reference, and a
94
+ minimal PyTorch inference implementation for DeepSeek-V4 Flash Vision. The
95
+ reference inference covers the vision encoder and aligner, DFlash attention,
96
+ MoE, Hyper-Connections, and the DSpark forward path.
97
+
98
+ ```text
99
+ .
100
+ ├── encoding/ # OpenAI-style messages -> model prompt
101
+ ├── inference/ # weight conversion and minimal inference
102
+ │ └── examples/ # equivalent TXT and JSON vision prompts
103
+ ├── config.json # Hugging Face model metadata
104
+ ├── generation_config.json
105
+ ├── model.safetensors.index.json
106
+ ├── tokenizer.json
107
+ └── tokenizer_config.json
108
+ ```
109
+
110
+ `encoding/` and `inference/` deliberately remain separate: prompt formatting
111
+ does not depend on PyTorch, while inference imports the sibling encoding module
112
+ with an explicit Python path. No symlinks are required.
113
+
114
+ The tokenizer files are regular files so that the repository can be uploaded
115
+ to Hugging Face without relying on local filesystem symlinks. The large model
116
+ shards are described by `model.safetensors.index.json` and are not duplicated
117
+ inside the source checkout used to assemble this repository.
118
+
119
+ ## Prompt encoding
120
+
121
+ See [`encoding/README.md`](encoding/README.md). Both OpenAI-style JSON content
122
+ blocks and the compact `<image>path</image>` TXT notation are supported. The two
123
+ examples under `inference/examples/` encode to identical prompts and token IDs.
124
+
125
+ ## Minimal inference
126
+
127
+ See [`inference/README.md`](inference/README.md) for dependency installation,
128
+ checkpoint conversion, and TXT/JSON inference commands.
129
+
130
+ ## License
131
+
132
+ This repository is licensed under the [MIT License](LICENSE).
chat_template.jinja ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {#- Unsloth template fixes #}
2
+ {%- if not add_generation_prompt is defined -%}
3
+ {%- set add_generation_prompt = false -%}
4
+ {%- endif -%}
5
+ {%- if not thinking is defined -%}
6
+ {%- if enable_thinking is defined -%}
7
+ {%- set thinking = enable_thinking -%}
8
+ {%- else -%}
9
+ {%- set thinking = false -%}
10
+ {%- endif -%}
11
+ {%- endif -%}
12
+ {%- if not reasoning_effort is defined -%}
13
+ {%- set reasoning_effort = none -%}
14
+ {%- endif -%}
15
+ {%- set dsml_token = '|DSML|' -%}
16
+ {%- set thinking_start_token = '<think>' -%}
17
+ {%- set thinking_end_token = '</think>' -%}
18
+ {%- set reasoning_effort_max = 'Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n' -%}
19
+ {%- set tools_header = '## Tools\n\nYou have access to a set of tools to help answer the user\'s question. You can invoke tools by writing a "<' + dsml_token + 'tool_calls>" block like the following:\n\n<' + dsml_token + 'tool_calls>\n<' + dsml_token + 'invoke name="$TOOL_NAME">\n<' + dsml_token + 'parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</' + dsml_token + 'parameter>\n...\n</' + dsml_token + 'invoke>\n<' + dsml_token + 'invoke name="$TOOL_NAME2">\n...\n</' + dsml_token + 'invoke>\n</' + dsml_token + 'tool_calls>\n\nString parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.\n\nIf thinking_mode is enabled (triggered by ' + thinking_start_token + '), you MUST output your complete reasoning inside ' + thinking_start_token + '...' + thinking_end_token + ' BEFORE any tool calls or final response.\n\nOtherwise, output directly after ' + thinking_end_token + ' with tool calls or final response.\n\n### Available Tool Schemas\n\n' -%}
20
+ {%- set tools_footer = '\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n' -%}
21
+ {%- set response_format_header = '## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n' -%}
22
+ {#- Detect tools anywhere (top-level param or attached to any message): DeepSeek-V4's
23
+ encoder disables reasoning-dropping entirely when tools are present. -#}
24
+ {%- set tp = namespace(has=false) -%}
25
+ {%- if tools is defined and tools -%}
26
+ {%- set tp.has = true -%}
27
+ {%- endif -%}
28
+ {%- for message in messages -%}
29
+ {%- if message['tools'] is defined and message['tools'] -%}
30
+ {%- set tp.has = true -%}
31
+ {%- endif -%}
32
+ {%- endfor -%}
33
+ {#- Build system prompt from all system messages (+ optional per-message response_format). -#}
34
+ {%- set ns = namespace(system_prompt='', is_first_sp=true) -%}
35
+ {%- for message in messages -%}
36
+ {%- if message['role'] == 'system' -%}
37
+ {%- if ns.is_first_sp -%}
38
+ {%- set ns.system_prompt = ns.system_prompt + (message['content'] or '') -%}
39
+ {%- set ns.is_first_sp = false -%}
40
+ {%- else -%}
41
+ {%- set ns.system_prompt = ns.system_prompt + '\n\n' + (message['content'] or '') -%}
42
+ {%- endif -%}
43
+ {%- if message['response_format'] is defined and message['response_format'] -%}
44
+ {%- set ns.system_prompt = ns.system_prompt + '\n\n' + response_format_header + (message['response_format'] | tojson) -%}
45
+ {%- endif -%}
46
+ {%- endif -%}
47
+ {%- endfor -%}
48
+ {%- if tools is defined and tools -%}
49
+ {%- set ts = namespace(schemas='') -%}
50
+ {%- for tool in tools -%}
51
+ {%- if tool['type'] == 'function' -%}
52
+ {%- set ts.schemas = ts.schemas + (tool['function'] | tojson) + '\n' -%}
53
+ {%- endif -%}
54
+ {%- endfor -%}
55
+ {#- Match the reference: tools attach to the system message, so a system message
56
+ that exists but is empty still contributes its "" + "\n\n" separator. -#}
57
+ {%- if not ns.is_first_sp -%}
58
+ {%- set ns.system_prompt = ns.system_prompt + '\n\n' + tools_header + ts.schemas + tools_footer -%}
59
+ {%- else -%}
60
+ {%- set ns.system_prompt = tools_header + ts.schemas + tools_footer -%}
61
+ {%- endif -%}
62
+ {%- endif -%}
63
+ {{- bos_token -}}
64
+ {%- if thinking and reasoning_effort == 'max' -%}
65
+ {{- reasoning_effort_max -}}
66
+ {%- endif -%}
67
+ {{- ns.system_prompt -}}
68
+ {#- Last user-like index: controls which turns keep reasoning. The encoder merges tool
69
+ results into user messages before computing this, so tool turns count as user here. -#}
70
+ {%- set last_user_idx = namespace(value=-1) -%}
71
+ {%- for message in messages -%}
72
+ {%- if message['role'] == 'user' or message['role'] == 'developer' or message['role'] == 'tool' -%}
73
+ {%- set last_user_idx.value = loop.index0 -%}
74
+ {%- endif -%}
75
+ {%- endfor -%}
76
+ {%- set state = namespace(in_user=false) -%}
77
+ {%- for message in messages -%}
78
+ {%- if message['role'] == 'user' -%}
79
+ {%- if state.in_user -%}
80
+ {{- '\n\n' -}}
81
+ {%- else -%}
82
+ {{- '<|User|>' -}}
83
+ {%- set state.in_user = true -%}
84
+ {%- endif -%}
85
+ {{- message['content'] or '' -}}
86
+ {%- if message['task'] is defined and message['task'] and (loop.last or messages[loop.index0 + 1]['role'] in ['assistant', 'latest_reminder']) -%}
87
+ {%- set state.in_user = false -%}
88
+ {%- if message['task'] == 'action' -%}
89
+ {{- '<|Assistant|>' -}}
90
+ {%- if thinking -%}{{- thinking_start_token -}}{%- else -%}{{- thinking_end_token -}}{%- endif -%}
91
+ {{- '<|action|>' -}}
92
+ {%- else -%}
93
+ {{- '<|' + message['task'] + '|>' -}}
94
+ {%- endif -%}
95
+ {%- endif -%}
96
+ {%- elif message['role'] == 'tool' -%}
97
+ {%- if state.in_user -%}
98
+ {{- '\n\n' -}}
99
+ {%- else -%}
100
+ {{- '<|User|>' -}}
101
+ {%- set state.in_user = true -%}
102
+ {%- endif -%}
103
+ {{- '<tool_result>' + (message['content'] or '') + '</tool_result>' -}}
104
+ {%- elif message['role'] == 'developer' -%}
105
+ {#- The encoder's _drop_thinking_messages drops developer turns that sit before the
106
+ last user turn when in thinking mode with reasoning-dropping active (no tools).
107
+ (No-op under llama.cpp, which remaps developer -> system before templating.) -#}
108
+ {%- if thinking and not tp.has and loop.index0 < last_user_idx.value -%}
109
+ {%- else -%}
110
+ {%- set state.in_user = false -%}
111
+ {{- '<|User|>' + (message['content'] or '') -}}
112
+ {%- if message['tools'] is defined and message['tools'] -%}
113
+ {%- set ds = namespace(schemas='') -%}
114
+ {%- for tool in message['tools'] -%}
115
+ {%- if tool['type'] == 'function' -%}
116
+ {%- set ds.schemas = ds.schemas + (tool['function'] | tojson) + '\n' -%}
117
+ {%- endif -%}
118
+ {%- endfor -%}
119
+ {{- '\n\n' + tools_header + ds.schemas + tools_footer -}}
120
+ {%- endif -%}
121
+ {%- if message['response_format'] is defined and message['response_format'] -%}
122
+ {{- '\n\n' + response_format_header + (message['response_format'] | tojson) -}}
123
+ {%- endif -%}
124
+ {%- if message['task'] is defined and message['task'] and (loop.last or messages[loop.index0 + 1]['role'] in ['assistant', 'latest_reminder']) -%}
125
+ {%- if message['task'] == 'action' -%}
126
+ {{- '<|Assistant|>' -}}
127
+ {%- if thinking -%}{{- thinking_start_token -}}{%- else -%}{{- thinking_end_token -}}{%- endif -%}
128
+ {{- '<|action|>' -}}
129
+ {%- else -%}
130
+ {{- '<|' + message['task'] + '|>' -}}
131
+ {%- endif -%}
132
+ {%- endif -%}
133
+ {%- endif -%}
134
+ {%- elif message['role'] == 'latest_reminder' -%}
135
+ {%- set state.in_user = false -%}
136
+ {#- The encoder emits the user/developer -> assistant transition before a
137
+ latest_reminder as well (the opening think stays open across the reminder and is
138
+ closed by the following assistant). Same effective-predecessor rule as assistant. -#}
139
+ {%- set ep = namespace(idx=(loop.index0 - 1), done=false, is_ud=false, has_task=false) -%}
140
+ {%- for _i in range(loop.index0) -%}
141
+ {%- if not ep.done and ep.idx >= 0 -%}
142
+ {%- set _pm = messages[ep.idx] -%}
143
+ {%- if (_pm['role'] == 'developer') and thinking and (not tp.has) and (ep.idx < last_user_idx.value) -%}
144
+ {%- set ep.idx = ep.idx - 1 -%}
145
+ {%- else -%}
146
+ {%- set ep.done = true -%}
147
+ {%- set ep.is_ud = _pm['role'] in ['user', 'developer', 'tool'] -%}
148
+ {%- set ep.has_task = _pm['task'] is defined and _pm['task'] -%}
149
+ {%- endif -%}
150
+ {%- endif -%}
151
+ {%- endfor -%}
152
+ {%- if ep.has_task -%}
153
+ {%- elif ep.is_ud -%}
154
+ {{- '<|Assistant|>' -}}
155
+ {%- if thinking and (tp.has or (loop.index0 > last_user_idx.value)) -%}
156
+ {{- thinking_start_token -}}
157
+ {%- else -%}
158
+ {{- thinking_end_token -}}
159
+ {%- endif -%}
160
+ {%- endif -%}
161
+ {{- '<|latest_reminder|>' + (message['content'] or '') -}}
162
+ {%- elif message['role'] == 'assistant' -%}
163
+ {%- set state.in_user = false -%}
164
+ {#- The encoder emits the "<|Assistant|>" + opening think/end token as a trailing
165
+ transition on a user/developer predecessor (tool results merge into user), never
166
+ on the assistant message itself. Find the effective predecessor, skipping any
167
+ developer the encoder drops (developer before the last user in thinking+drop). -#}
168
+ {%- set ep = namespace(idx=(loop.index0 - 1), done=false, is_ud=false, has_task=false) -%}
169
+ {%- for _i in range(loop.index0) -%}
170
+ {%- if not ep.done and ep.idx >= 0 -%}
171
+ {%- set _pm = messages[ep.idx] -%}
172
+ {%- if (_pm['role'] == 'developer') and thinking and (not tp.has) and (ep.idx < last_user_idx.value) -%}
173
+ {%- set ep.idx = ep.idx - 1 -%}
174
+ {%- else -%}
175
+ {%- set ep.done = true -%}
176
+ {%- set ep.is_ud = _pm['role'] in ['user', 'developer', 'tool'] -%}
177
+ {%- set ep.has_task = _pm['task'] is defined and _pm['task'] -%}
178
+ {%- endif -%}
179
+ {%- endif -%}
180
+ {%- endfor -%}
181
+ {%- set keep_reasoning = tp.has or (loop.index0 > last_user_idx.value) -%}
182
+ {%- if ep.has_task -%}
183
+ {#- The predecessor's task token already emitted the assistant prompt. -#}
184
+ {%- elif ep.is_ud -%}
185
+ {{- '<|Assistant|>' -}}
186
+ {%- if keep_reasoning and thinking -%}
187
+ {{- thinking_start_token -}}
188
+ {%- if message['reasoning_content'] is defined and message['reasoning_content'] -%}
189
+ {{- message['reasoning_content'] -}}
190
+ {%- endif -%}
191
+ {{- thinking_end_token -}}
192
+ {%- else -%}
193
+ {{- thinking_end_token -}}
194
+ {%- endif -%}
195
+ {%- else -%}
196
+ {#- Predecessor is not user/developer/tool (first turn, latest_reminder, another
197
+ assistant, or an all-dropped-developer run): the encoder emits no prefix and
198
+ no opening think; a kept reasoning turn still closes with its content + </think>. -#}
199
+ {%- if keep_reasoning and thinking -%}
200
+ {%- if message['reasoning_content'] is defined and message['reasoning_content'] -%}
201
+ {{- message['reasoning_content'] -}}
202
+ {%- endif -%}
203
+ {{- thinking_end_token -}}
204
+ {%- endif -%}
205
+ {%- endif -%}
206
+ {%- if message['content'] is defined and message['content'] -%}
207
+ {{- message['content'] -}}
208
+ {%- endif -%}
209
+ {%- if message['tool_calls'] -%}
210
+ {{- '\n\n<' + dsml_token + 'tool_calls>\n' -}}
211
+ {%- for tool in message['tool_calls'] -%}
212
+ {%- set func = tool['function'] -%}
213
+ {{- '<' + dsml_token + 'invoke name="' + func['name'] + '">\n' -}}
214
+ {%- set args = func['arguments'] -%}
215
+ {%- if args is string -%}
216
+ {%- set args = args | from_json -%}
217
+ {%- endif -%}
218
+ {%- for key, val in args.items() -%}
219
+ {%- if val is string -%}
220
+ {{- '<' + dsml_token + 'parameter name="' + key + '" string="true">' + val + '</' + dsml_token + 'parameter>\n' -}}
221
+ {%- else -%}
222
+ {{- '<' + dsml_token + 'parameter name="' + key + '" string="false">' + (val | tojson) + '</' + dsml_token + 'parameter>\n' -}}
223
+ {%- endif -%}
224
+ {%- endfor -%}
225
+ {{- '</' + dsml_token + 'invoke>\n' -}}
226
+ {%- endfor -%}
227
+ {{- '</' + dsml_token + 'tool_calls>' -}}
228
+ {%- endif -%}
229
+ {{- '<|end▁of▁sentence|>' -}}
230
+ {%- if message['task'] is defined and message['task'] == 'title' and (loop.last or messages[loop.index0 + 1]['role'] in ['assistant', 'latest_reminder']) -%}
231
+ {{- '<|title|>' -}}
232
+ {%- endif -%}
233
+ {%- endif -%}
234
+ {%- endfor -%}
235
+ {%- if add_generation_prompt -%}
236
+ {#- Suppress the standard generation prefix when the final message already
237
+ emitted a task token (which serves as the generation prompt). -#}
238
+ {%- set _last = messages[messages | length - 1] -%}
239
+ {%- if not (_last['task'] is defined and _last['task']) -%}
240
+ {{- '<|Assistant|>' -}}
241
+ {%- if thinking -%}
242
+ {{- thinking_start_token -}}
243
+ {%- else -%}
244
+ {{- thinking_end_token -}}
245
+ {%- endif -%}
246
+ {%- endif -%}
247
+ {%- endif -%}
248
+ {#- Copyright 2026-present Unsloth. Apache 2.0 License. #}
config.json ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DeepseekV4ForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 0,
8
+ "compress_rates": {
9
+ "compressed_sparse_attention": 4,
10
+ "heavily_compressed_attention": 128
11
+ },
12
+ "compress_rope_theta": 160000,
13
+ "dspark_block_size": 5,
14
+ "dspark_markov_rank": 256,
15
+ "dspark_noise_token_id": 128799,
16
+ "dspark_target_layer_ids": [
17
+ 40,
18
+ 41,
19
+ 42
20
+ ],
21
+ "torch_dtype": "bfloat16",
22
+ "eos_token_id": 1,
23
+ "expert_dtype": "fp4",
24
+ "hc_eps": 1e-06,
25
+ "hc_mult": 4,
26
+ "hc_sinkhorn_iters": 20,
27
+ "head_dim": 512,
28
+ "hidden_act": "silu",
29
+ "hidden_size": 4096,
30
+ "index_head_dim": 128,
31
+ "index_n_heads": 64,
32
+ "index_topk": 512,
33
+ "initializer_range": 0.02,
34
+ "layer_types": [
35
+ "sliding_attention",
36
+ "sliding_attention",
37
+ "compressed_sparse_attention",
38
+ "heavily_compressed_attention",
39
+ "compressed_sparse_attention",
40
+ "heavily_compressed_attention",
41
+ "compressed_sparse_attention",
42
+ "heavily_compressed_attention",
43
+ "compressed_sparse_attention",
44
+ "heavily_compressed_attention",
45
+ "compressed_sparse_attention",
46
+ "heavily_compressed_attention",
47
+ "compressed_sparse_attention",
48
+ "heavily_compressed_attention",
49
+ "compressed_sparse_attention",
50
+ "heavily_compressed_attention",
51
+ "compressed_sparse_attention",
52
+ "heavily_compressed_attention",
53
+ "compressed_sparse_attention",
54
+ "heavily_compressed_attention",
55
+ "compressed_sparse_attention",
56
+ "heavily_compressed_attention",
57
+ "compressed_sparse_attention",
58
+ "heavily_compressed_attention",
59
+ "compressed_sparse_attention",
60
+ "heavily_compressed_attention",
61
+ "compressed_sparse_attention",
62
+ "heavily_compressed_attention",
63
+ "compressed_sparse_attention",
64
+ "heavily_compressed_attention",
65
+ "compressed_sparse_attention",
66
+ "heavily_compressed_attention",
67
+ "compressed_sparse_attention",
68
+ "heavily_compressed_attention",
69
+ "compressed_sparse_attention",
70
+ "heavily_compressed_attention",
71
+ "compressed_sparse_attention",
72
+ "heavily_compressed_attention",
73
+ "compressed_sparse_attention",
74
+ "heavily_compressed_attention",
75
+ "compressed_sparse_attention",
76
+ "heavily_compressed_attention",
77
+ "compressed_sparse_attention"
78
+ ],
79
+ "max_position_embeddings": 1048576,
80
+ "mlp_bias": false,
81
+ "mlp_layer_types": [
82
+ "hash_moe",
83
+ "hash_moe",
84
+ "hash_moe",
85
+ "moe",
86
+ "moe",
87
+ "moe",
88
+ "moe",
89
+ "moe",
90
+ "moe",
91
+ "moe",
92
+ "moe",
93
+ "moe",
94
+ "moe",
95
+ "moe",
96
+ "moe",
97
+ "moe",
98
+ "moe",
99
+ "moe",
100
+ "moe",
101
+ "moe",
102
+ "moe",
103
+ "moe",
104
+ "moe",
105
+ "moe",
106
+ "moe",
107
+ "moe",
108
+ "moe",
109
+ "moe",
110
+ "moe",
111
+ "moe",
112
+ "moe",
113
+ "moe",
114
+ "moe",
115
+ "moe",
116
+ "moe",
117
+ "moe",
118
+ "moe",
119
+ "moe",
120
+ "moe",
121
+ "moe",
122
+ "moe",
123
+ "moe",
124
+ "moe"
125
+ ],
126
+ "model_type": "deepseek_v4",
127
+ "moe_intermediate_size": 2048,
128
+ "mtp_num_hidden_layers": 1,
129
+ "n_routed_experts": 256,
130
+ "n_shared_experts": 1,
131
+ "norm_topk_prob": true,
132
+ "num_attention_heads": 64,
133
+ "num_experts_per_tok": 6,
134
+ "num_hidden_layers": 43,
135
+ "num_key_value_heads": 1,
136
+ "num_nextn_predict_layers": 3,
137
+ "o_groups": 8,
138
+ "o_lora_rank": 1024,
139
+ "output_router_logits": false,
140
+ "pad_token_id": 2,
141
+ "partial_rotary_factor": 0.125,
142
+ "q_lora_rank": 1024,
143
+ "qk_rope_head_dim": 64,
144
+ "quantization_config": {
145
+ "activation_scheme": "dynamic",
146
+ "fmt": "e4m3",
147
+ "quant_method": "fp8",
148
+ "scale_fmt": "ue8m0",
149
+ "weight_block_size": [
150
+ 128,
151
+ 128
152
+ ]
153
+ },
154
+ "rms_norm_eps": 1e-20,
155
+ "rope_parameters": {
156
+ "compress": {
157
+ "attention_factor": 1.0,
158
+ "beta_fast": 32,
159
+ "beta_slow": 1,
160
+ "factor": 16,
161
+ "original_max_position_embeddings": 65536,
162
+ "partial_rotary_factor": 0.125,
163
+ "rope_theta": 160000,
164
+ "rope_type": "yarn",
165
+ "type": "yarn"
166
+ },
167
+ "main": {
168
+ "partial_rotary_factor": 0.125,
169
+ "rope_theta": 10000,
170
+ "rope_type": "default"
171
+ }
172
+ },
173
+ "rope_theta": 10000,
174
+ "routed_scaling_factor": 1.5,
175
+ "router_aux_loss_coef": 0.001,
176
+ "router_jitter_noise": 0.0,
177
+ "scoring_func": "sqrtsoftplus",
178
+ "sliding_window": 128,
179
+ "swiglu_limit": 10.0,
180
+ "tie_word_embeddings": false,
181
+ "topk_method": "noaux_tc",
182
+ "transformers_version": "5.16.0.dev0",
183
+ "unsloth_fixed": true,
184
+ "unsloth_fixed_mtp": true,
185
+ "use_cache": true,
186
+ "vision_dim": 1024,
187
+ "vision_downsample_ratio": 3,
188
+ "vision_inter_dim": 2816,
189
+ "vision_max_n_token": 384,
190
+ "vision_max_wh_ratio": 8,
191
+ "vision_min_pixels": 147456,
192
+ "vision_n_heads": 16,
193
+ "vision_n_layers": 32,
194
+ "vision_patch_size": 14,
195
+ "vision_rope_theta": 10000.0,
196
+ "vocab_size": 129280
197
+ }
encoding/README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DeepSeek-V4 text and vision encoding
2
+
3
+ `encoding_dsv4.py` is the standalone prompt-format reference. It supports
4
+ multi-turn conversations, tool calls, thinking modes, and interleaved image
5
+ content blocks without importing the inference implementation.
6
+
7
+ ## OpenAI-style messages
8
+
9
+ ```python
10
+ from encoding_dsv4 import encode_messages
11
+
12
+ messages = [{
13
+ "role": "user",
14
+ "content": [
15
+ {"type": "text", "text": "第一张图"},
16
+ {
17
+ "type": "image_url",
18
+ "image_url": {"url": "examples/images/carrots.jpeg"},
19
+ },
20
+ {"type": "text", "text": "有什么内容?"},
21
+ ],
22
+ }]
23
+
24
+ # non-thinking
25
+ prompt, media = encode_messages(
26
+ messages,
27
+ thinking_mode="chat",
28
+ return_multi_modal_data=True,
29
+ )
30
+ # prompt:
31
+ # '<|begin▁of▁sentence|><|User|>第一张图\n\n<|deepseek_image|>\n\n有什么内容?<|Assistant|></think>'
32
+
33
+
34
+ # # thinking with `max` reasoning_effort
35
+ # prompt, media = encode_messages(
36
+ # messages,
37
+ # thinking_mode="thinking",
38
+ # reasoning_effort="max",
39
+ # return_multi_modal_data=True,
40
+ # )
41
+ # prompt:
42
+ # <|begin▁of▁sentence|>Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\nYou MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\nDo not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n<|User|>第一张图\n\n<|deepseek_image|>\n\n有什么内容?<|Assistant|><think>
43
+
44
+ ```
45
+
46
+ Images are represented in the prompt by `<|deepseek_image|>`. `media["images"]`
47
+ contains the corresponding image records in exactly the same order. Pixel
48
+ loading and expansion into model image tokens are handled by
49
+ `inference/image_processor.py`.
50
+
51
+ ## Compact TXT notation
52
+
53
+ `parse_tagged_text()` converts a compact prompt such as
54
+
55
+ ```text
56
+ 第一张图<image>examples/images/carrots.jpeg</image>有什么内容?
57
+ ```
58
+
59
+ into the same standard content blocks. It is an input convenience layer, not a
60
+ second encoding implementation.
61
+
62
+ ## Tests
63
+
64
+ From the repository root:
65
+
66
+ ```bash
67
+ python -m pytest -q encoding/test_encoding_dsv4.py
68
+ ```
69
+
70
+ The tests include a check that the TXT and JSON examples encode to the same
71
+ prompt and preserve the same two-image ordering.
encoding/encoding_dsv4.py ADDED
@@ -0,0 +1,957 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepSeek-V4 Text and Vision Encoding
3
+
4
+ A self-contained implementation for encoding/decoding DeepSeek-V4 chat messages
5
+ with tool calling, thinking mode, quick instruction tasks, and image content blocks.
6
+ """
7
+
8
+ from typing import Any, Dict, List, Union, Optional, Tuple
9
+ import copy
10
+ import json
11
+ import re
12
+
13
+ # ============================================================
14
+ # Special Tokens
15
+ # ============================================================
16
+
17
+ bos_token: str = "<|begin▁of▁sentence|>"
18
+ eos_token: str = "<|end▁of▁sentence|>"
19
+ thinking_start_token: str = "<think>"
20
+ thinking_end_token: str = "</think>"
21
+ dsml_token: str = "|DSML|"
22
+
23
+ USER_SP_TOKEN = "<|User|>"
24
+ ASSISTANT_SP_TOKEN = "<|Assistant|>"
25
+ LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
26
+ IMAGE_PLACEHOLDER = "<|deepseek_image|>"
27
+ IMAGE_TAG_PATTERN = re.compile(r"<image>(.*?)</image>", re.DOTALL)
28
+
29
+ # Task special tokens for internal classification tasks
30
+ DS_TASK_SP_TOKENS = {
31
+ "action": "<|action|>",
32
+ "query": "<|query|>",
33
+ "authority": "<|authority|>",
34
+ "domain": "<|domain|>",
35
+ "title": "<|title|>",
36
+ "read_url": "<|read_url|>",
37
+ }
38
+ VALID_TASKS = set(DS_TASK_SP_TOKENS.keys())
39
+
40
+ # ============================================================
41
+ # Templates
42
+ # ============================================================
43
+
44
+ system_msg_template: str = "{content}"
45
+ user_msg_template: str = "{content}"
46
+ latest_reminder_msg_template: str = "{content}"
47
+ assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
48
+ assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
49
+ thinking_template: str = "{reasoning_content}"
50
+
51
+ response_format_template: str = (
52
+ "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
53
+ )
54
+ tool_call_template: str = (
55
+ "<{dsml_token}invoke name=\"{name}\">\n{arguments}\n</{dsml_token}invoke>"
56
+ )
57
+ tool_calls_template = (
58
+ "<{dsml_token}{tc_block_name}>\n{tool_calls}\n</{dsml_token}{tc_block_name}>"
59
+ )
60
+ tool_calls_block_name: str = "tool_calls"
61
+
62
+ tool_output_template: str = (
63
+ "<tool_result>{content}</tool_result>"
64
+ )
65
+
66
+ # Reasoning effort levels. In thinking mode, the prompt for the selected level is
67
+ # prepended at the very beginning of the conversation. `low` is the default and
68
+ # adds nothing.
69
+ REASONING_EFFORT_PROMPTS: Dict[str, str] = {
70
+ "low": "",
71
+ "high": (
72
+ "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
73
+ "You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
74
+ "Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n"
75
+ ),
76
+ "max": (
77
+ "Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n"
78
+ "You MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\n"
79
+ "Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n"
80
+ ),
81
+ }
82
+ DEFAULT_REASONING_EFFORT = "low"
83
+
84
+ TOOLS_TEMPLATE = """## Tools
85
+
86
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
87
+
88
+ <{dsml_token}tool_calls>
89
+ <{dsml_token}invoke name="$TOOL_NAME">
90
+ <{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
91
+ ...
92
+ </{dsml_token}invoke>
93
+ <{dsml_token}invoke name="$TOOL_NAME2">
94
+ ...
95
+ </{dsml_token}invoke>
96
+ </{dsml_token}tool_calls>
97
+
98
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
99
+
100
+ If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
101
+
102
+ Otherwise, output directly after {thinking_end_token} with tool calls or final response.
103
+
104
+ ### Available Tool Schemas
105
+
106
+ {tool_schemas}
107
+
108
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
109
+ """
110
+
111
+ # ============================================================
112
+ # Utility Functions
113
+ # ============================================================
114
+
115
+ def to_json(value: Any) -> str:
116
+ """Serialize a value to JSON string."""
117
+ try:
118
+ return json.dumps(value, ensure_ascii=False)
119
+ except:
120
+ return json.dumps(value, ensure_ascii=True)
121
+
122
+
123
+ def tools_from_openai_format(tools):
124
+ """Extract function definitions from OpenAI-format tool list."""
125
+ return [tool["function"] for tool in tools]
126
+
127
+
128
+ def tool_calls_from_openai_format(tool_calls):
129
+ """Convert OpenAI-format tool calls to internal format."""
130
+ return [
131
+ {
132
+ "name": tool_call["function"]["name"],
133
+ "arguments": tool_call["function"]["arguments"],
134
+ }
135
+ for tool_call in tool_calls
136
+ ]
137
+
138
+
139
+ def tool_calls_to_openai_format(tool_calls):
140
+ """Convert internal tool calls to OpenAI format."""
141
+ return [
142
+ {
143
+ "type": "function",
144
+ "function": {
145
+ "name": tool_call["name"],
146
+ "arguments": tool_call["arguments"],
147
+ }
148
+ }
149
+ for tool_call in tool_calls
150
+ ]
151
+
152
+
153
+ def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
154
+ """
155
+ Encode tool call arguments into DSML parameter format.
156
+
157
+ Args:
158
+ tool_call: Dict with "name" and "arguments" (JSON string) keys.
159
+
160
+ Returns:
161
+ DSML-formatted parameter string.
162
+ """
163
+ p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
164
+ P_dsml_strs = []
165
+
166
+ try:
167
+ arguments = json.loads(tool_call["arguments"])
168
+ except Exception as err:
169
+ arguments = {"arguments": tool_call["arguments"]}
170
+
171
+ for k, v in arguments.items():
172
+ p_dsml_str = p_dsml_template.format(
173
+ dsml_token=dsml_token,
174
+ key=k,
175
+ is_str="true" if isinstance(v, str) else "false",
176
+ value=v if isinstance(v, str) else to_json(v),
177
+ )
178
+ P_dsml_strs.append(p_dsml_str)
179
+
180
+ return "\n".join(P_dsml_strs)
181
+
182
+
183
+ def decode_dsml_to_arguments(tool_name: str, tool_args: Dict[str, Tuple[str, str]]) -> Dict[str, str]:
184
+ """
185
+ Decode DSML parameters back to a tool call dict.
186
+
187
+ Args:
188
+ tool_name: Name of the tool.
189
+ tool_args: Dict mapping param_name -> (value, is_string_flag).
190
+
191
+ Returns:
192
+ Dict with "name" and "arguments" (JSON string) keys.
193
+ """
194
+ def _decode_value(key: str, value: str, string: str):
195
+ if string == "true":
196
+ value = to_json(value)
197
+ return f"{to_json(key)}: {value}"
198
+
199
+ tool_args_json = "{" + ", ".join([_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]) + "}"
200
+ return dict(name=tool_name, arguments=tool_args_json)
201
+
202
+
203
+ def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
204
+ """
205
+ Render tool schemas into the system prompt format.
206
+
207
+ Args:
208
+ tools: List of tool schema dicts (each with name, description, parameters).
209
+
210
+ Returns:
211
+ Formatted tools section string.
212
+ """
213
+ tools_json = [to_json(t) for t in tools]
214
+
215
+ return TOOLS_TEMPLATE.format(
216
+ tool_schemas="\n".join(tools_json),
217
+ dsml_token=dsml_token,
218
+ thinking_start_token=thinking_start_token,
219
+ thinking_end_token=thinking_end_token,
220
+ )
221
+
222
+
223
+ def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
224
+ """Find the index of the last user/developer message."""
225
+ last_user_index = -1
226
+ for idx in range(len(messages) - 1, -1, -1):
227
+ if messages[idx].get("role") in ["user", "developer"]:
228
+ last_user_index = idx
229
+ break
230
+ return last_user_index
231
+
232
+
233
+ # ============================================================
234
+ # Message Rendering
235
+ # ============================================================
236
+
237
+ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: str, drop_thinking: bool = True, reasoning_effort: Optional[str] = None) -> str:
238
+ """
239
+ Render a single message at the given index into its encoded string form.
240
+
241
+ This is the core function that converts each message in the conversation
242
+ into the DeepSeek-V4 format.
243
+
244
+ Args:
245
+ index: Index of the message to render.
246
+ messages: Full list of messages in the conversation.
247
+ thinking_mode: Either "chat" or "thinking".
248
+ drop_thinking: Whether to drop reasoning content from earlier turns.
249
+ reasoning_effort: Reasoning effort level, one of "low", "high", "max".
250
+ None is treated as "low".
251
+
252
+ Returns:
253
+ Encoded string for this message.
254
+ """
255
+ assert 0 <= index < len(messages)
256
+ assert thinking_mode in ["chat", "thinking"], f"Invalid thinking_mode `{thinking_mode}`"
257
+
258
+ prompt = ""
259
+ msg = messages[index]
260
+ last_user_idx = find_last_user_index(messages)
261
+
262
+ role = msg.get("role")
263
+ content = msg.get("content")
264
+ tools = msg.get("tools")
265
+ response_format = msg.get("response_format")
266
+ tool_calls = msg.get("tool_calls")
267
+ reasoning_content = msg.get("reasoning_content")
268
+ wo_eos = msg.get("wo_eos", False)
269
+
270
+ if tools:
271
+ tools = tools_from_openai_format(tools)
272
+ if tool_calls:
273
+ tool_calls = tool_calls_from_openai_format(tool_calls)
274
+
275
+ # Reasoning effort prefix (only at index 0 in thinking mode; "low" adds nothing)
276
+ reasoning_effort = reasoning_effort or DEFAULT_REASONING_EFFORT
277
+ assert reasoning_effort in REASONING_EFFORT_PROMPTS, \
278
+ f"Invalid reasoning effort: {reasoning_effort}, expected one of {list(REASONING_EFFORT_PROMPTS)}"
279
+ if index == 0 and thinking_mode == "thinking":
280
+ prompt += REASONING_EFFORT_PROMPTS[reasoning_effort]
281
+
282
+ if role == "system":
283
+ prompt += system_msg_template.format(content=content or "")
284
+ if tools:
285
+ prompt += "\n\n" + render_tools(tools)
286
+ if response_format:
287
+ prompt += "\n\n" + response_format_template.format(schema=to_json(response_format))
288
+
289
+ elif role == "developer":
290
+ assert content, f"Invalid message for role `{role}`: {msg}"
291
+
292
+ content_developer = USER_SP_TOKEN
293
+ content_developer += content
294
+
295
+ if tools:
296
+ content_developer += "\n\n" + render_tools(tools)
297
+ if response_format:
298
+ content_developer += "\n\n" + response_format_template.format(schema=to_json(response_format))
299
+
300
+ prompt += user_msg_template.format(content=content_developer)
301
+
302
+ elif role == "user":
303
+ prompt += USER_SP_TOKEN
304
+
305
+ # Handle content blocks (tool results mixed with text)
306
+ content_blocks = msg.get("content_blocks")
307
+ if content_blocks:
308
+ parts = []
309
+ for block in content_blocks:
310
+ block_type = block.get("type")
311
+ if block_type == "text":
312
+ parts.append(block.get("text", ""))
313
+ elif block_type == "tool_result":
314
+ tool_content = block.get("content", "")
315
+ if isinstance(tool_content, list):
316
+ text_parts = []
317
+ for b in tool_content:
318
+ if b.get("type") == "text":
319
+ text_parts.append(b.get("text", ""))
320
+ else:
321
+ text_parts.append(f"[Unsupported {b.get('type')}]")
322
+ tool_content = "\n\n".join(text_parts)
323
+ parts.append(tool_output_template.format(content=tool_content))
324
+ else:
325
+ parts.append(f"[Unsupported {block_type}]")
326
+ prompt += "\n\n".join(parts)
327
+ else:
328
+ prompt += content or ""
329
+
330
+ elif role == "latest_reminder":
331
+ prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(content=content)
332
+
333
+ elif role == "tool":
334
+ raise NotImplementedError("deepseek_v4 merges tool messages into user; please preprocess with merge_tool_messages()")
335
+
336
+ elif role == "assistant":
337
+ thinking_part = ""
338
+ tc_content = ""
339
+
340
+ if tool_calls:
341
+ tc_list = [
342
+ tool_call_template.format(
343
+ dsml_token=dsml_token,
344
+ name=tc.get("name"),
345
+ arguments=encode_arguments_to_dsml(tc)
346
+ )
347
+ for tc in tool_calls
348
+ ]
349
+ tc_content += '\n\n' + tool_calls_template.format(
350
+ dsml_token=dsml_token,
351
+ tool_calls="\n".join(tc_list),
352
+ tc_block_name=tool_calls_block_name,
353
+ )
354
+
355
+ summary_content = content or ""
356
+ rc = reasoning_content or ""
357
+
358
+ # Check if previous message has a task - if so, this is a task output (no thinking)
359
+ prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
360
+
361
+ if thinking_mode == "thinking" and not prev_has_task:
362
+ if not drop_thinking or index > last_user_idx:
363
+ thinking_part = thinking_template.format(reasoning_content=rc) + thinking_end_token
364
+ else:
365
+ thinking_part = ""
366
+
367
+ if wo_eos:
368
+ prompt += assistant_msg_wo_eos_template.format(
369
+ reasoning=thinking_part,
370
+ content=summary_content,
371
+ tool_calls=tc_content,
372
+ )
373
+ else:
374
+ prompt += assistant_msg_template.format(
375
+ reasoning=thinking_part,
376
+ content=summary_content,
377
+ tool_calls=tc_content,
378
+ )
379
+ else:
380
+ raise NotImplementedError(f"Unknown role: {role}")
381
+
382
+ # Append transition tokens based on what follows
383
+ if index + 1 < len(messages) and messages[index + 1].get("role") not in ["assistant", "latest_reminder"]:
384
+ return prompt
385
+
386
+ task = messages[index].get("task")
387
+ if task is not None:
388
+ # Task special token for internal classification tasks
389
+ assert task in VALID_TASKS, f"Invalid task: '{task}'. Valid tasks are: {list(VALID_TASKS)}"
390
+ task_sp_token = DS_TASK_SP_TOKENS[task]
391
+
392
+ if task != "action":
393
+ # Non-action tasks: append task sp token directly after the message
394
+ prompt += task_sp_token
395
+ else:
396
+ # Action task: append Assistant + thinking token + action sp token
397
+ prompt += ASSISTANT_SP_TOKEN
398
+ prompt += thinking_end_token if thinking_mode != "thinking" else thinking_start_token
399
+ prompt += task_sp_token
400
+
401
+ elif messages[index].get("role") in ["user", "developer"]:
402
+ # Normal generation: append Assistant + thinking token
403
+ prompt += ASSISTANT_SP_TOKEN
404
+ if not drop_thinking and thinking_mode == "thinking":
405
+ prompt += thinking_start_token
406
+ elif drop_thinking and thinking_mode == "thinking" and index >= last_user_idx:
407
+ prompt += thinking_start_token
408
+ else:
409
+ prompt += thinking_end_token
410
+
411
+ return prompt
412
+
413
+
414
+ # ============================================================
415
+ # Preprocessing
416
+ # ============================================================
417
+
418
+ def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
419
+ """
420
+ Merge tool messages into the preceding user message using content_blocks format.
421
+
422
+ DeepSeek-V4 does not have a standalone "tool" role; instead, tool results
423
+ are encoded as <tool_result> blocks within user messages.
424
+
425
+ This function converts a standard OpenAI-format conversation (with separate
426
+ "tool" role messages) into V4 format where tool results are merged into
427
+ user messages.
428
+
429
+ Args:
430
+ messages: List of message dicts in OpenAI format.
431
+
432
+ Returns:
433
+ Processed message list with tool messages merged into user messages.
434
+ """
435
+ merged: List[Dict[str, Any]] = []
436
+
437
+ for msg in messages:
438
+ msg = copy.deepcopy(msg)
439
+ role = msg.get("role")
440
+
441
+ if role == "tool":
442
+ # Convert tool message to a user message with tool_result block
443
+ tool_block = {
444
+ "type": "tool_result",
445
+ "tool_use_id": msg.get("tool_call_id", ""),
446
+ "content": msg.get("content", ""),
447
+ }
448
+ # Merge into previous message if it's already a user (merged tool)
449
+ if merged and merged[-1].get("role") == "user" and "content_blocks" in merged[-1]:
450
+ merged[-1]["content_blocks"].append(tool_block)
451
+ else:
452
+ merged.append({
453
+ "role": "user",
454
+ "content_blocks": [tool_block],
455
+ })
456
+ elif role == "user":
457
+ content_blocks = msg.get("content_blocks")
458
+ if content_blocks is None:
459
+ content_blocks = [{"type": "text", "text": msg.get("content", "")}]
460
+ if merged and merged[-1].get("role") == "user" and "content_blocks" in merged[-1] and merged[-1].get("task") is None:
461
+ merged[-1]["content_blocks"].extend(content_blocks)
462
+ else:
463
+ # Preserve structured content and all message-level metadata.
464
+ new_msg = msg
465
+ new_msg["content_blocks"] = content_blocks
466
+ merged.append(new_msg)
467
+ else:
468
+ merged.append(msg)
469
+
470
+ return merged
471
+
472
+
473
+ def sort_tool_results_by_call_order(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
474
+ """
475
+ Sort tool_result blocks within user messages by the order of tool_calls
476
+ in the preceding assistant message.
477
+
478
+ Args:
479
+ messages: Preprocessed message list (after merge_tool_messages).
480
+
481
+ Returns:
482
+ Message list with sorted tool result blocks.
483
+ """
484
+ last_tool_call_order: Dict[str, int] = {}
485
+
486
+ for msg in messages:
487
+ role = msg.get("role")
488
+ if role == "assistant" and msg.get("tool_calls"):
489
+ last_tool_call_order = {}
490
+ for idx, tc in enumerate(msg["tool_calls"]):
491
+ tc_id = tc.get("id") or tc.get("function", {}).get("id", "")
492
+ if tc_id:
493
+ last_tool_call_order[tc_id] = idx
494
+
495
+ elif role == "user" and msg.get("content_blocks"):
496
+ tool_blocks = [b for b in msg["content_blocks"] if b.get("type") == "tool_result"]
497
+ if len(tool_blocks) > 1 and last_tool_call_order:
498
+ sorted_blocks = sorted(
499
+ tool_blocks,
500
+ key=lambda b: last_tool_call_order.get(b.get("tool_use_id", ""), 0)
501
+ )
502
+ sorted_idx = 0
503
+ new_blocks = []
504
+ for block in msg["content_blocks"]:
505
+ if block.get("type") == "tool_result":
506
+ new_blocks.append(sorted_blocks[sorted_idx])
507
+ sorted_idx += 1
508
+ else:
509
+ new_blocks.append(block)
510
+ msg["content_blocks"] = new_blocks
511
+
512
+ return messages
513
+
514
+
515
+ # ============================================================
516
+ # Main Encoding Function
517
+ # ============================================================
518
+
519
+ def _encode_messages_text(
520
+ messages: List[Dict[str, Any]],
521
+ thinking_mode: str,
522
+ context: Optional[List[Dict[str, Any]]] = None,
523
+ drop_thinking: bool = True,
524
+ add_default_bos_token: bool = True,
525
+ reasoning_effort: Optional[str] = None,
526
+ ) -> str:
527
+ """
528
+ Encode a list of messages into the DeepSeek-V4 prompt format.
529
+
530
+ This is the main entry point for encoding conversations. It handles:
531
+ - BOS token insertion
532
+ - Thinking mode with optional reasoning content dropping
533
+ - Tool message merging into user messages
534
+ - Multi-turn conversation context
535
+
536
+ Args:
537
+ messages: List of message dicts to encode.
538
+ thinking_mode: Either "chat" or "thinking".
539
+ context: Optional preceding context messages (already encoded prefix).
540
+ drop_thinking: If True, drop reasoning_content from earlier assistant turns
541
+ (only keep reasoning for messages after the last user message).
542
+ add_default_bos_token: Whether to prepend BOS token at conversation start.
543
+ reasoning_effort: Reasoning effort level, one of "low", "high", "max".
544
+ Only takes effect in thinking mode. None is treated as "low".
545
+
546
+ Returns:
547
+ The encoded prompt string.
548
+ """
549
+ context = context if context else []
550
+
551
+ # Preprocess: merge tool messages and sort tool results
552
+ messages = merge_tool_messages(messages)
553
+ messages = sort_tool_results_by_call_order(context + messages)[len(context):]
554
+ if context:
555
+ context = merge_tool_messages(context)
556
+ context = sort_tool_results_by_call_order(context)
557
+
558
+ full_messages = context + messages
559
+
560
+ prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
561
+
562
+ # Resolve drop_thinking: if any message has tools defined, don't drop thinking
563
+ effective_drop_thinking = drop_thinking
564
+ if any(m.get("tools") for m in full_messages):
565
+ effective_drop_thinking = False
566
+
567
+ if thinking_mode == "thinking" and effective_drop_thinking:
568
+ full_messages = _drop_thinking_messages(full_messages)
569
+ # After dropping, recalculate how many messages to render
570
+ # (context may have shrunk too)
571
+ num_to_render = len(full_messages) - len(_drop_thinking_messages(context))
572
+ context_len = len(full_messages) - num_to_render
573
+ else:
574
+ num_to_render = len(messages)
575
+ context_len = len(context)
576
+
577
+ for idx in range(num_to_render):
578
+ prompt += render_message(
579
+ idx + context_len,
580
+ full_messages,
581
+ thinking_mode=thinking_mode,
582
+ drop_thinking=effective_drop_thinking,
583
+ reasoning_effort=reasoning_effort,
584
+ )
585
+
586
+ return prompt
587
+
588
+
589
+ def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
590
+ """
591
+ Drop reasoning_content and non-essential messages before the last user message.
592
+
593
+ Behavior:
594
+ - Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
595
+ - Messages at or after the last user index are always kept.
596
+ - Assistant messages before the last user get reasoning_content removed.
597
+ - Developer messages before the last user are dropped entirely.
598
+ """
599
+ last_user_idx = find_last_user_index(messages)
600
+ result = []
601
+ keep_roles = {"user", "system", "tool", "latest_reminder", "direct_search_results"}
602
+
603
+ for idx, msg in enumerate(messages):
604
+ role = msg.get("role")
605
+ if role in keep_roles or idx >= last_user_idx:
606
+ result.append(msg)
607
+ elif role == "assistant":
608
+ msg = copy.copy(msg)
609
+ msg.pop("reasoning_content", None)
610
+ result.append(msg)
611
+ # developer and other roles before last_user_idx are dropped
612
+
613
+ return result
614
+
615
+
616
+ # ============================================================
617
+ # Vision Message Preprocessing
618
+ # ============================================================
619
+
620
+ def parse_tagged_text(text: str) -> Union[str, List[Dict[str, Any]]]:
621
+ """Convert ``<image>path</image>`` text into standard content blocks."""
622
+ matches = list(IMAGE_TAG_PATTERN.finditer(text))
623
+ remaining = IMAGE_TAG_PATTERN.sub("", text)
624
+ if "<image>" in remaining or "</image>" in remaining:
625
+ raise ValueError("Malformed <image>path</image> tag")
626
+ if not matches:
627
+ return text
628
+
629
+ blocks: List[Dict[str, Any]] = []
630
+ cursor = 0
631
+ for match in matches:
632
+ if match.start() > cursor:
633
+ blocks.append({"type": "text", "text": text[cursor:match.start()]})
634
+ path = match.group(1)
635
+ if not path:
636
+ raise ValueError("Image path must not be empty")
637
+ blocks.append({
638
+ "type": "image_url",
639
+ "image_url": {"url": path},
640
+ })
641
+ cursor = match.end()
642
+ if cursor < len(text):
643
+ blocks.append({"type": "text", "text": text[cursor:]})
644
+ return blocks
645
+
646
+
647
+ def _is_image_block(block: Dict[str, Any]) -> bool:
648
+ """Return whether a content block is an OpenAI/Anthropic/internal image."""
649
+ return isinstance(block, dict) and block.get("type") in ("image", "image_url")
650
+
651
+
652
+ def _extract_image(block: Dict[str, Any]) -> Dict[str, Any]:
653
+ """Normalize a supported image block into an internal image record."""
654
+ record: Dict[str, Any] = {"type": "image"}
655
+ if block.get("type") == "image_url":
656
+ image_url = block.get("image_url")
657
+ if isinstance(image_url, str):
658
+ record["url"] = image_url
659
+ else:
660
+ record["url"] = (image_url or {}).get("url", "")
661
+ else:
662
+ for key in ("source", "url", "data"):
663
+ if key in block:
664
+ record[key] = block[key]
665
+ if not any(record.get(key) for key in ("source", "url", "data")):
666
+ raise ValueError("Image block does not contain a valid source")
667
+ return record
668
+
669
+
670
+ def _process_image_blocks(
671
+ blocks: List[Any], image_placeholder: str = IMAGE_PLACEHOLDER
672
+ ) -> Tuple[List[Any], List[Dict[str, Any]]]:
673
+ """Replace image blocks and collect their records in one ordered traversal."""
674
+ new_blocks: List[Any] = []
675
+ images: List[Dict[str, Any]] = []
676
+ for block in blocks:
677
+ if not isinstance(block, dict):
678
+ new_blocks.append(block)
679
+ continue
680
+ if _is_image_block(block):
681
+ new_blocks.append({"type": "text", "text": image_placeholder})
682
+ images.append(_extract_image(block))
683
+ elif block.get("type") == "tool_result" and isinstance(block.get("content"), list):
684
+ block = copy.copy(block)
685
+ block["content"], nested_images = _process_image_blocks(
686
+ block["content"], image_placeholder)
687
+ new_blocks.append(block)
688
+ images.extend(nested_images)
689
+ elif block.get("type") == "text":
690
+ text = block.get("text") or ""
691
+ if IMAGE_PLACEHOLDER in text:
692
+ raise ValueError(
693
+ f"Text block contains image placeholder '{IMAGE_PLACEHOLDER}': "
694
+ f"'{text[:100]}'. Images should be separate content blocks."
695
+ )
696
+ new_blocks.append(block)
697
+ else:
698
+ new_blocks.append(block)
699
+ return new_blocks, images
700
+
701
+
702
+ def _validate_no_image_sp_tokens(msg: Dict[str, Any]) -> None:
703
+ """Reject user-supplied image placeholder tokens in textual fields."""
704
+ content = msg.get("content")
705
+ if isinstance(content, str) and IMAGE_PLACEHOLDER in content:
706
+ raise ValueError(
707
+ f"Message content contains image special token '{IMAGE_PLACEHOLDER}'. "
708
+ "Images should be provided as image content blocks."
709
+ )
710
+ reasoning_content = msg.get("reasoning_content")
711
+ if isinstance(reasoning_content, str) and IMAGE_PLACEHOLDER in reasoning_content:
712
+ raise ValueError(
713
+ f"reasoning_content contains image special token '{IMAGE_PLACEHOLDER}'"
714
+ )
715
+
716
+
717
+ def process_image_messages(
718
+ messages: List[Dict[str, Any]],
719
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
720
+ """Normalize image blocks and return their records in prompt order."""
721
+ processed: List[Dict[str, Any]] = []
722
+ images: List[Dict[str, Any]] = []
723
+ for msg in messages:
724
+ msg = copy.deepcopy(msg)
725
+ _validate_no_image_sp_tokens(msg)
726
+
727
+ if isinstance(msg.get("content"), list) and "content_blocks" not in msg:
728
+ msg["content_blocks"] = msg.pop("content")
729
+
730
+ if msg.get("content_blocks"):
731
+ msg["content_blocks"], message_images = _process_image_blocks(
732
+ msg["content_blocks"])
733
+ images.extend(message_images)
734
+ if not isinstance(msg.get("content"), str):
735
+ texts = [
736
+ block.get("text", "")
737
+ for block in msg["content_blocks"]
738
+ if isinstance(block, dict) and block.get("type") == "text"
739
+ ]
740
+ msg["content"] = "\n\n".join(texts)
741
+
742
+ processed.append(msg)
743
+ return processed, images
744
+
745
+
746
+ def encode_messages(
747
+ messages: List[Dict[str, Any]],
748
+ thinking_mode: str,
749
+ context: Optional[List[Dict[str, Any]]] = None,
750
+ drop_thinking: bool = True,
751
+ add_default_bos_token: bool = True,
752
+ reasoning_effort: Optional[str] = None,
753
+ return_multi_modal_data: bool = False,
754
+ ) -> Any:
755
+ """Encode text or multimodal messages through one canonical public entrypoint.
756
+
757
+ Text-only calls preserve the original string-returning API. When
758
+ return_multi_modal_data is true, the result is ``(prompt, media_data)``.
759
+ """
760
+ context = context or []
761
+ processed_context, _ = process_image_messages(context) if context else ([], [])
762
+ processed_messages, images = process_image_messages(messages)
763
+ prompt = _encode_messages_text(
764
+ processed_messages,
765
+ thinking_mode=thinking_mode,
766
+ context=processed_context if processed_context else None,
767
+ drop_thinking=drop_thinking,
768
+ add_default_bos_token=add_default_bos_token,
769
+ reasoning_effort=reasoning_effort,
770
+ )
771
+ if return_multi_modal_data:
772
+ return prompt, {"images": images}
773
+ return prompt
774
+
775
+
776
+ def load_cases(input_file: str) -> List[Dict[str, Any]]:
777
+ """Load one or more OpenAI-format conversation cases from JSON."""
778
+ with open(input_file) as file:
779
+ data = json.load(file)
780
+ if isinstance(data, dict):
781
+ data = [data]
782
+ elif data and isinstance(data[0], dict) and "role" in data[0]:
783
+ data = [{"messages": data}]
784
+
785
+ cases = []
786
+ for case in data:
787
+ messages = copy.deepcopy(case["messages"])
788
+ if "tools" in case:
789
+ if not messages:
790
+ raise ValueError("A case with tools must contain at least one message")
791
+ messages[0]["tools"] = case["tools"]
792
+ cases.append({
793
+ "messages": messages,
794
+ "context": case.get("context"),
795
+ "thinking_mode": case.get("thinking_mode"),
796
+ "reasoning_effort": case.get("reasoning_effort"),
797
+ })
798
+ return cases
799
+
800
+
801
+ def encode_case(
802
+ case: Dict[str, Any], thinking_mode: str
803
+ ) -> Tuple[str, List[Dict[str, Any]]]:
804
+ """Encode one JSON case and return its current-turn image records."""
805
+ prompt, media_data = encode_messages(
806
+ case["messages"],
807
+ thinking_mode=case.get("thinking_mode") or thinking_mode,
808
+ context=case.get("context"),
809
+ reasoning_effort=case.get("reasoning_effort"),
810
+ return_multi_modal_data=True,
811
+ )
812
+ return prompt, media_data["images"]
813
+
814
+
815
+ # ============================================================
816
+ # Parsing (Decoding model output)
817
+ # ============================================================
818
+
819
+ def _read_until_stop(index: int, text: str, stop: List[str]) -> Tuple[int, str, Optional[str]]:
820
+ """
821
+ Read text from index until one of the stop strings is found.
822
+
823
+ Returns:
824
+ Tuple of (new_index, content_before_stop, matched_stop_string_or_None).
825
+ """
826
+ min_pos = len(text)
827
+ matched_stop = None
828
+
829
+ for s in stop:
830
+ pos = text.find(s, index)
831
+ if pos != -1 and pos < min_pos:
832
+ min_pos = pos
833
+ matched_stop = s
834
+
835
+ if matched_stop:
836
+ content = text[index:min_pos]
837
+ return min_pos + len(matched_stop), content, matched_stop
838
+ else:
839
+ content = text[index:]
840
+ return len(text), content, None
841
+
842
+
843
+ def parse_tool_calls(index: int, text: str) -> Tuple[int, Optional[str], List[Dict[str, str]]]:
844
+ """
845
+ Parse DSML tool calls from text starting at the given index.
846
+
847
+ Args:
848
+ index: Starting position in text.
849
+ text: The full text to parse.
850
+
851
+ Returns:
852
+ Tuple of (new_index, last_stop_token, list_of_tool_call_dicts).
853
+ Each tool call dict has "name" and "arguments" keys.
854
+ """
855
+ tool_calls: List[Dict[str, Any]] = []
856
+ stop_token = None
857
+ tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
858
+
859
+ while index < len(text):
860
+ index, _, stop_token = _read_until_stop(index, text, [f"<{dsml_token}invoke", tool_calls_end_token])
861
+ if _ != ">\n":
862
+ raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
863
+
864
+ if stop_token == tool_calls_end_token:
865
+ break
866
+
867
+ if stop_token is None:
868
+ raise ValueError("Missing special token in tool calls")
869
+
870
+ index, tool_name_content, stop_token = _read_until_stop(index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"])
871
+
872
+ p_tool_name = re.findall(r'^\s*name="(.*?)">\n$', tool_name_content, flags=re.DOTALL)
873
+ if len(p_tool_name) != 1:
874
+ raise ValueError(f"Tool name format error: '{tool_name_content}'")
875
+ tool_name = p_tool_name[0]
876
+
877
+ tool_args: Dict[str, Tuple[str, str]] = {}
878
+ while stop_token == f"<{dsml_token}parameter":
879
+ index, param_content, stop_token = _read_until_stop(index, text, [f"/{dsml_token}parameter"])
880
+
881
+ param_kv = re.findall(r'^ name="(.*?)" string="(true|false)">(.*?)<$', param_content, flags=re.DOTALL)
882
+ if len(param_kv) != 1:
883
+ raise ValueError(f"Parameter format error: '{param_content}'")
884
+ param_name, string, param_value = param_kv[0]
885
+
886
+ if param_name in tool_args:
887
+ raise ValueError(f"Duplicate parameter name: '{param_name}'")
888
+ tool_args[param_name] = (param_value, string)
889
+
890
+ index, content, stop_token = _read_until_stop(index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"])
891
+ if content != ">\n":
892
+ raise ValueError(f"Parameter format error: expected '>\\n' but got '{content}'")
893
+
894
+ tool_call = decode_dsml_to_arguments(tool_name=tool_name, tool_args=tool_args)
895
+ tool_calls.append(tool_call)
896
+
897
+ return index, stop_token, tool_calls
898
+
899
+
900
+ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[str, Any]:
901
+ """
902
+ Parse a model completion text into a structured assistant message.
903
+
904
+ This function takes the raw text output from the model (a single assistant turn)
905
+ and extracts:
906
+ - reasoning_content (thinking block)
907
+ - content (summary/response)
908
+ - tool_calls (if any)
909
+
910
+ NOTE: This function is designed to parse only correctly formatted strings and
911
+ will raise ValueError for malformed output.
912
+
913
+ Args:
914
+ text: The raw completion text (including EOS token).
915
+ thinking_mode: Either "chat" or "thinking".
916
+
917
+ Returns:
918
+ Dict with keys: "role", "content", "reasoning_content", "tool_calls".
919
+ tool_calls are in OpenAI format.
920
+ """
921
+ summary_content, reasoning_content, tool_calls = "", "", []
922
+ index, stop_token = 0, None
923
+ tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
924
+
925
+ is_thinking = thinking_mode == "thinking"
926
+ is_tool_calling = False
927
+
928
+ if is_thinking:
929
+ index, content_delta, stop_token = _read_until_stop(index, text, [thinking_end_token, tool_calls_start_token])
930
+ reasoning_content = content_delta
931
+ assert stop_token == thinking_end_token, "Invalid thinking format: missing </think>"
932
+
933
+ index, content_delta, stop_token = _read_until_stop(index, text, [eos_token, tool_calls_start_token])
934
+ summary_content = content_delta
935
+ if stop_token == tool_calls_start_token:
936
+ is_tool_calling = True
937
+ else:
938
+ assert stop_token == eos_token, "Invalid format: missing EOS token"
939
+
940
+ if is_tool_calling:
941
+ index, stop_token, tool_calls = parse_tool_calls(index, text)
942
+
943
+ index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
944
+ assert not tool_ends_text, "Unexpected content after tool calls"
945
+
946
+ assert len(text) == index and stop_token in [eos_token, None], "Unexpected content at end"
947
+
948
+ for sp_token in [bos_token, eos_token, thinking_start_token, thinking_end_token, dsml_token]:
949
+ assert sp_token not in summary_content and sp_token not in reasoning_content, \
950
+ f"Unexpected special token '{sp_token}' in content"
951
+
952
+ return {
953
+ "role": "assistant",
954
+ "content": summary_content,
955
+ "reasoning_content": reasoning_content,
956
+ "tool_calls": tool_calls_to_openai_format(tool_calls)
957
+ }
encoding/test_encoding_dsv4.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from encoding_dsv4 import (
6
+ IMAGE_PLACEHOLDER,
7
+ encode_messages,
8
+ load_cases,
9
+ parse_tagged_text,
10
+ )
11
+
12
+
13
+ def test_plain_text_prompt_is_unchanged():
14
+ prompt = encode_messages(
15
+ [{"role": "user", "content": "hello"}],
16
+ thinking_mode="chat",
17
+ )
18
+ assert prompt == (
19
+ "<|begin▁of▁sentence|><|User|>hello"
20
+ "<|Assistant|></think>"
21
+ )
22
+
23
+
24
+ def test_multiturn_text_prompt_is_unchanged():
25
+ prompt = encode_messages(
26
+ [
27
+ {"role": "system", "content": "sys"},
28
+ {"role": "user", "content": "q1"},
29
+ {"role": "assistant", "content": "a1"},
30
+ {"role": "user", "content": "q2"},
31
+ ],
32
+ thinking_mode="chat",
33
+ )
34
+ assert prompt == (
35
+ "<|begin▁of▁sentence|>sys<|User|>q1<|Assistant|></think>"
36
+ "a1<|end▁of▁sentence|><|User|>q2<|Assistant|></think>"
37
+ )
38
+
39
+
40
+ def test_top_level_image_block_returns_matching_placeholder_and_record():
41
+ messages = [{
42
+ "role": "user",
43
+ "content": [
44
+ {"type": "image_url", "image_url": {"url": "images/image_1.jpeg"}},
45
+ {"type": "text", "text": "describe"},
46
+ ],
47
+ }]
48
+ prompt, media = encode_messages(
49
+ messages,
50
+ thinking_mode="chat",
51
+ return_multi_modal_data=True,
52
+ )
53
+ assert prompt == (
54
+ "<|begin▁of▁sentence|><|User|><|deepseek_image|>\n\n"
55
+ "describe<|Assistant|></think>"
56
+ )
57
+ assert prompt.count(IMAGE_PLACEHOLDER) == len(media["images"]) == 1
58
+ assert media["images"][0]["url"] == "images/image_1.jpeg"
59
+
60
+
61
+ def test_tagged_text_matches_standard_image_content_blocks():
62
+ tagged_content = parse_tagged_text(
63
+ "before<image>images/image_1.jpeg</image>after"
64
+ )
65
+ standard_content = [
66
+ {"type": "text", "text": "before"},
67
+ {"type": "image_url", "image_url": {"url": "images/image_1.jpeg"}},
68
+ {"type": "text", "text": "after"},
69
+ ]
70
+ tagged = encode_messages(
71
+ [{"role": "user", "content": tagged_content}],
72
+ thinking_mode="chat",
73
+ return_multi_modal_data=True,
74
+ )
75
+ standard = encode_messages(
76
+ [{"role": "user", "content": standard_content}],
77
+ thinking_mode="chat",
78
+ return_multi_modal_data=True,
79
+ )
80
+ assert tagged == standard
81
+
82
+
83
+ def test_tagged_text_preserves_multiple_image_order():
84
+ content = parse_tagged_text(
85
+ "<image>first.png</image>middle<image>second.png</image>"
86
+ )
87
+ prompt, media = encode_messages(
88
+ [{"role": "user", "content": content}],
89
+ thinking_mode="chat",
90
+ return_multi_modal_data=True,
91
+ )
92
+ assert prompt.count(IMAGE_PLACEHOLDER) == 2
93
+ assert [image["url"] for image in media["images"]] == [
94
+ "first.png",
95
+ "second.png",
96
+ ]
97
+
98
+
99
+ def test_txt_and_json_examples_encode_identically():
100
+ root = Path(__file__).parent.parent
101
+ examples = root / "inference" / "examples"
102
+ text = (examples / "example_vl.txt").read_text().rstrip("\n")
103
+ json_case = load_cases(str(examples / "example_vl_harmony.json"))[0]
104
+
105
+ txt_encoded = encode_messages(
106
+ [{"role": "user", "content": parse_tagged_text(text)}],
107
+ thinking_mode="chat",
108
+ return_multi_modal_data=True,
109
+ )
110
+ json_encoded = encode_messages(
111
+ json_case["messages"],
112
+ thinking_mode="chat",
113
+ return_multi_modal_data=True,
114
+ )
115
+
116
+ assert txt_encoded == json_encoded
117
+ prompt, media = txt_encoded
118
+ assert prompt.count(IMAGE_PLACEHOLDER) == 2
119
+ assert [image["url"] for image in media["images"]] == [
120
+ "examples/images/carrots.jpeg",
121
+ "examples/images/corn.jpeg",
122
+ ]
123
+
124
+
125
+ def test_malformed_tagged_text_is_rejected():
126
+ with pytest.raises(ValueError, match="Malformed"):
127
+ parse_tagged_text("<image>missing end tag")
128
+
129
+
130
+ def test_nested_tool_result_preserves_image_placeholder():
131
+ messages = [{
132
+ "role": "user",
133
+ "content": [{
134
+ "type": "tool_result",
135
+ "tool_use_id": "call-1",
136
+ "content": [
137
+ {"type": "image_url", "image_url": {"url": "images/image_1.jpeg"}},
138
+ {"type": "text", "text": "nested"},
139
+ ],
140
+ }],
141
+ }]
142
+ prompt, media = encode_messages(
143
+ messages,
144
+ thinking_mode="chat",
145
+ return_multi_modal_data=True,
146
+ )
147
+ assert "<tool_result><|deepseek_image|>\n\nnested</tool_result>" in prompt
148
+ assert prompt.count(IMAGE_PLACEHOLDER) == len(media["images"]) == 1
149
+
150
+
151
+ def test_tool_role_with_image_blocks_preserves_placeholder():
152
+ messages = [
153
+ {
154
+ "role": "assistant",
155
+ "content": "",
156
+ "tool_calls": [{
157
+ "id": "call-1",
158
+ "type": "function",
159
+ "function": {"name": "inspect", "arguments": "{}"},
160
+ }],
161
+ },
162
+ {
163
+ "role": "tool",
164
+ "tool_call_id": "call-1",
165
+ "content": [
166
+ {"type": "image_url", "image_url": {"url": "images/image_1.jpeg"}},
167
+ {"type": "text", "text": "tool image"},
168
+ ],
169
+ },
170
+ ]
171
+ prompt, media = encode_messages(
172
+ messages,
173
+ thinking_mode="chat",
174
+ return_multi_modal_data=True,
175
+ )
176
+ assert "<tool_result><|deepseek_image|>\n\ntool image</tool_result>" in prompt
177
+ assert prompt.count(IMAGE_PLACEHOLDER) == len(media["images"]) == 1
178
+
179
+
180
+ def test_context_images_are_not_returned_as_current_media():
181
+ context = [{
182
+ "role": "user",
183
+ "content": [
184
+ {"type": "image_url", "image_url": {"url": "images/image_1.jpeg"}},
185
+ {"type": "text", "text": "previous"},
186
+ ],
187
+ }]
188
+ prompt, media = encode_messages(
189
+ [{"role": "user", "content": "now"}],
190
+ thinking_mode="chat",
191
+ context=context,
192
+ return_multi_modal_data=True,
193
+ )
194
+ assert IMAGE_PLACEHOLDER not in prompt
195
+ assert media == {"images": []}
196
+
197
+
198
+ def test_user_supplied_placeholder_is_rejected():
199
+ with pytest.raises(ValueError, match="image special token"):
200
+ encode_messages(
201
+ [{"role": "user", "content": IMAGE_PLACEHOLDER}],
202
+ thinking_mode="chat",
203
+ )
204
+
205
+
206
+ def test_image_block_without_source_is_rejected():
207
+ with pytest.raises(ValueError, match="valid source"):
208
+ encode_messages(
209
+ [{
210
+ "role": "user",
211
+ "content": [{"type": "image_url", "image_url": {}}],
212
+ }],
213
+ thinking_mode="chat",
214
+ return_multi_modal_data=True,
215
+ )
encoding/tests/test_input_1.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tools": [
3
+ {
4
+ "type": "function",
5
+ "function": {
6
+ "name": "get_weather",
7
+ "description": "Get the weather for a specific location",
8
+ "parameters": {
9
+ "type": "object",
10
+ "properties": {
11
+ "location": {
12
+ "type": "string",
13
+ "description": "The city name"
14
+ },
15
+ "unit": {
16
+ "type": "string",
17
+ "enum": ["celsius", "fahrenheit"],
18
+ "description": "Temperature unit"
19
+ }
20
+ },
21
+ "required": ["location"]
22
+ }
23
+ }
24
+ },
25
+ {
26
+ "type": "function",
27
+ "function": {
28
+ "name": "search",
29
+ "description": "Search the web for information",
30
+ "parameters": {
31
+ "type": "object",
32
+ "properties": {
33
+ "query": {
34
+ "type": "string",
35
+ "description": "Search query"
36
+ },
37
+ "num_results": {
38
+ "type": "integer",
39
+ "description": "Number of results to return"
40
+ }
41
+ },
42
+ "required": ["query"]
43
+ }
44
+ }
45
+ }
46
+ ],
47
+ "messages": [
48
+ {
49
+ "role": "system",
50
+ "content": "You are a helpful assistant."
51
+ },
52
+ {
53
+ "role": "user",
54
+ "content": "What's the weather in Beijing?"
55
+ },
56
+ {
57
+ "role": "assistant",
58
+ "reasoning_content": "The user wants to know the weather in Beijing. I should use the get_weather tool.",
59
+ "tool_calls": [
60
+ {
61
+ "id": "call_001",
62
+ "type": "function",
63
+ "function": {
64
+ "name": "get_weather",
65
+ "arguments": "{\"location\": \"Beijing\", \"unit\": \"celsius\"}"
66
+ }
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "role": "tool",
72
+ "tool_call_id": "call_001",
73
+ "content": "{\"temperature\": 22, \"condition\": \"sunny\", \"humidity\": 45}"
74
+ },
75
+ {
76
+ "role": "assistant",
77
+ "reasoning_content": "Got the weather data. Let me format a nice response.",
78
+ "content": "The weather in Beijing is currently sunny with a temperature of 22°C and 45% humidity."
79
+ }
80
+ ]
81
+ }
encoding/tests/test_input_2.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "You are a helpful assistant."
5
+ },
6
+ {
7
+ "role": "user",
8
+ "content": "Hello"
9
+ },
10
+ {
11
+ "role": "assistant",
12
+ "reasoning_content": "The user said hello, I should greet back.",
13
+ "content": "Hi there! How can I help you?"
14
+ },
15
+ {
16
+ "role": "user",
17
+ "content": "What is the capital of France?"
18
+ },
19
+ {
20
+ "role": "assistant",
21
+ "reasoning_content": "The user asks about the capital of France. It is Paris.",
22
+ "content": "The capital of France is Paris."
23
+ }
24
+ ]
encoding/tests/test_input_3.json ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "该助手为DeepSeek,由深度求索公司创造。"
5
+ },
6
+ {
7
+ "role": "latest_reminder",
8
+ "content": "2026-02-21,星期六,广州,App,中文"
9
+ },
10
+ {
11
+ "role": "developer",
12
+ "content": "小柴胡冲剂和布洛芬能一起吃吗?\n\nCITATION FORMAT: 【{cursor_id}†L{start_line_id}(-L{end_line_id})?】",
13
+ "tools": [
14
+ {
15
+ "type": "function",
16
+ "function": {
17
+ "name": "search",
18
+ "description": "Web search. Split multiple queries with '||'.",
19
+ "parameters": {
20
+ "type": "object",
21
+ "properties": {
22
+ "queries": {
23
+ "type": "string",
24
+ "description": "query1||query2"
25
+ }
26
+ },
27
+ "required": [
28
+ "queries"
29
+ ],
30
+ "additionalProperties": false,
31
+ "$schema": "http://json-schema.org/draft-07/schema#"
32
+ }
33
+ }
34
+ },
35
+ {
36
+ "type": "function",
37
+ "function": {
38
+ "name": "open",
39
+ "description": "Batch open IDs (format 【{id}†...】) or URLs.",
40
+ "parameters": {
41
+ "type": "object",
42
+ "properties": {
43
+ "open_list": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "object",
47
+ "properties": {
48
+ "id": {
49
+ "description": "ID or URL",
50
+ "anyOf": [
51
+ {
52
+ "type": "integer"
53
+ },
54
+ {
55
+ "type": "string"
56
+ }
57
+ ],
58
+ "default": -1
59
+ },
60
+ "cursor": {
61
+ "type": "integer",
62
+ "description": "",
63
+ "default": -1
64
+ },
65
+ "loc": {
66
+ "type": "integer",
67
+ "description": "Start line",
68
+ "default": -1
69
+ },
70
+ "num_lines": {
71
+ "type": "integer",
72
+ "description": "",
73
+ "default": -1
74
+ },
75
+ "view_source": {
76
+ "type": "boolean",
77
+ "description": "",
78
+ "default": false
79
+ }
80
+ },
81
+ "additionalProperties": false
82
+ },
83
+ "description": ""
84
+ }
85
+ },
86
+ "required": [
87
+ "open_list"
88
+ ],
89
+ "additionalProperties": false,
90
+ "$schema": "http://json-schema.org/draft-07/schema#"
91
+ }
92
+ }
93
+ },
94
+ {
95
+ "type": "function",
96
+ "function": {
97
+ "name": "find",
98
+ "description": "Find exact text pattern in pages.",
99
+ "parameters": {
100
+ "type": "object",
101
+ "properties": {
102
+ "find_list": {
103
+ "type": "array",
104
+ "items": {
105
+ "type": "object",
106
+ "properties": {
107
+ "pattern": {
108
+ "type": "string",
109
+ "description": ""
110
+ },
111
+ "cursor": {
112
+ "type": "integer",
113
+ "description": "",
114
+ "default": -1
115
+ }
116
+ },
117
+ "required": [
118
+ "pattern"
119
+ ],
120
+ "additionalProperties": false
121
+ },
122
+ "description": ""
123
+ }
124
+ },
125
+ "required": [
126
+ "find_list"
127
+ ],
128
+ "additionalProperties": false,
129
+ "$schema": "http://json-schema.org/draft-07/schema#"
130
+ }
131
+ }
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ "role": "assistant",
137
+ "content": "",
138
+ "reasoning_content": "用户想知道小柴胡冲剂和布洛芬能否一起服用。",
139
+ "tool_calls": [
140
+ {
141
+ "type": "function",
142
+ "function": {
143
+ "name": "search",
144
+ "arguments": "{\"queries\": \"小柴胡冲剂 布洛芬 相互作用 一起吃\"}"
145
+ }
146
+ }
147
+ ]
148
+ },
149
+ {
150
+ "role": "tool",
151
+ "content": "[0]"
152
+ },
153
+ {
154
+ "role": "assistant",
155
+ "content": "请及时就医。",
156
+ "reasoning_content": "现在开始组织回答。",
157
+ "tool_calls": []
158
+ }
159
+ ]
encoding/tests/test_input_4.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "该助手为DeepSeek-V3,由深度求索公司创造。\n今天是2025年10月17日,星期五。"
5
+ },
6
+ {
7
+ "role": "latest_reminder",
8
+ "content": "2024-11-15,上海市,App,中文"
9
+ },
10
+ {
11
+ "role": "user",
12
+ "content": "热海大滚锅是世界著名温泉吗"
13
+ },
14
+ {
15
+ "role": "assistant",
16
+ "content": "关于热海大滚锅是否是世界著名温泉,可以这样总结:它在**中国乃至全球的地热奇观中占有重要地位**,但“世界著名”的称号更侧重于它作为**独特的地质现象和旅游景点**,而非普遍的温泉疗养体验。\n\n为了让你快速了解,我整理了一个简要的表格:\n\n| 维度 | 热海大滚锅的地位与特点 |\n| :--- | :--- |\n| **地质奇观** | **世界罕见**的高温地热系统,泉眼多、水温高、形态多样。 |\n| **国内声誉** | **中国三大地热区之一**,**国家5A级旅游景区**,自古闻名(徐霞客曾游历并记载)。 |\n| **国际知名度** | 在一些旅游平台被提及为“世界六大温泉”之一,但此说法流传不广,其国际声誉更多建立在地质独特性上。 |\n| **核心体验** | **观赏地热奇观**(如97℃沸腾的“大滚锅”)、**体验温泉煮鸡蛋**。 |\n\n### 💡 游玩攻略与温馨提示\n\n如果你计划前往热海大滚锅,这里有一些实用信息供你参考:\n\n- **门票与开放时间**:\n - **门票**:景区门票约为**50元/人**。如果选择包含温泉沐浴的套餐,价格会更高,例如约**288元**。\n - **开放时间**:景区一般**08:00-18:00**开放,但具体时间可能变动,建议提前核实。\n\n- **特色体验**:\n - **温泉煮鸡蛋**:这几乎是必试项目。可以在景区门口购买用草绳串起的生鸡蛋(约5-8元/串),然后到“大滚锅”旁的指定区域蒸煮,几分钟便可熟食,趣味十足。\n - **金汤足浴**:可以直接用从“大滚锅”流出的温泉水泡脚,缓解旅途疲劳。\n\n- **注意事项**:\n - **安全第一**:“大滚锅”水温极高,务必遵守游览规则,在指定区域内观赏,切勿随意触碰泉水。\n - **规划行程**:建议为热海景区预留**3-4小时**的游览时间。景区内步道不走回头路,出入口有观光车接送。\n\n希望这些信息能帮助你更好地了解热海大滚锅。如果你对腾冲的其他景点或者行程规划有更多疑问,我很乐意提供进一步的信息。",
17
+ "mask": 1
18
+ },
19
+ {
20
+ "role": "user",
21
+ "content": "世界著名温泉有哪些",
22
+ "task": "action"
23
+ },
24
+ {
25
+ "role": "assistant",
26
+ "content": "Search"
27
+ }
28
+ ]
encoding/tests/test_output_1.txt ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|>You are a helpful assistant.
2
+
3
+ ## Tools
4
+
5
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML|tool_calls>" block like the following:
6
+
7
+ <|DSML|tool_calls>
8
+ <|DSML|invoke name="$TOOL_NAME">
9
+ <|DSML|parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML|parameter>
10
+ ...
11
+ </|DSML|invoke>
12
+ <|DSML|invoke name="$TOOL_NAME2">
13
+ ...
14
+ </|DSML|invoke>
15
+ </|DSML|tool_calls>
16
+
17
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
18
+
19
+ If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
20
+
21
+ Otherwise, output directly after </think> with tool calls or final response.
22
+
23
+ ### Available Tool Schemas
24
+
25
+ {"name": "get_weather", "description": "Get the weather for a specific location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit"}}, "required": ["location"]}}
26
+ {"name": "search", "description": "Search the web for information", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "num_results": {"type": "integer", "description": "Number of results to return"}}, "required": ["query"]}}
27
+
28
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
29
+ <|User|>What's the weather in Beijing?<|Assistant|><think>The user wants to know the weather in Beijing. I should use the get_weather tool.</think>
30
+
31
+ <|DSML|tool_calls>
32
+ <|DSML|invoke name="get_weather">
33
+ <|DSML|parameter name="location" string="true">Beijing</|DSML|parameter>
34
+ <|DSML|parameter name="unit" string="true">celsius</|DSML|parameter>
35
+ </|DSML|invoke>
36
+ </|DSML|tool_calls><|end▁of▁sentence|><|User|><tool_result>{"temperature": 22, "condition": "sunny", "humidity": 45}</tool_result><|Assistant|><think>Got the weather data. Let me format a nice response.</think>The weather in Beijing is currently sunny with a temperature of 22°C and 45% humidity.<|end▁of▁sentence|>
encoding/tests/test_output_2.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ <|begin▁of▁sentence|>You are a helpful assistant.<|User|>Hello<|Assistant|></think>Hi there! How can I help you?<|end▁of▁sentence|><|User|>What is the capital of France?<|Assistant|><think>The user asks about the capital of France. It is Paris.</think>The capital of France is Paris.<|end▁of▁sentence|>
encoding/tests/test_output_3.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|>该助手为DeepSeek,由深度求索公司创造。<|latest_reminder|>2026-02-21,星期六,广州,App,中文<|User|>小柴胡冲剂和布洛芬能一起吃吗?
2
+
3
+ CITATION FORMAT: 【{cursor_id}†L{start_line_id}(-L{end_line_id})?】
4
+
5
+ ## Tools
6
+
7
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML|tool_calls>" block like the following:
8
+
9
+ <|DSML|tool_calls>
10
+ <|DSML|invoke name="$TOOL_NAME">
11
+ <|DSML|parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML|parameter>
12
+ ...
13
+ </|DSML|invoke>
14
+ <|DSML|invoke name="$TOOL_NAME2">
15
+ ...
16
+ </|DSML|invoke>
17
+ </|DSML|tool_calls>
18
+
19
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
20
+
21
+ If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
22
+
23
+ Otherwise, output directly after </think> with tool calls or final response.
24
+
25
+ ### Available Tool Schemas
26
+
27
+ {"name": "search", "description": "Web search. Split multiple queries with '||'.", "parameters": {"type": "object", "properties": {"queries": {"type": "string", "description": "query1||query2"}}, "required": ["queries"], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#"}}
28
+ {"name": "open", "description": "Batch open IDs (format 【{id}†...】) or URLs.", "parameters": {"type": "object", "properties": {"open_list": {"type": "array", "items": {"type": "object", "properties": {"id": {"description": "ID or URL", "anyOf": [{"type": "integer"}, {"type": "string"}], "default": -1}, "cursor": {"type": "integer", "description": "", "default": -1}, "loc": {"type": "integer", "description": "Start line", "default": -1}, "num_lines": {"type": "integer", "description": "", "default": -1}, "view_source": {"type": "boolean", "description": "", "default": false}}, "additionalProperties": false}, "description": ""}}, "required": ["open_list"], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#"}}
29
+ {"name": "find", "description": "Find exact text pattern in pages.", "parameters": {"type": "object", "properties": {"find_list": {"type": "array", "items": {"type": "object", "properties": {"pattern": {"type": "string", "description": ""}, "cursor": {"type": "integer", "description": "", "default": -1}}, "required": ["pattern"], "additionalProperties": false}, "description": ""}}, "required": ["find_list"], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#"}}
30
+
31
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
32
+ <|Assistant|><think>用户想知道小柴胡冲剂和布洛芬能否一起服用。</think>
33
+
34
+ <|DSML|tool_calls>
35
+ <|DSML|invoke name="search">
36
+ <|DSML|parameter name="queries" string="true">小柴胡冲剂 布洛芬 相互作用 一起吃</|DSML|parameter>
37
+ </|DSML|invoke>
38
+ </|DSML|tool_calls><|end▁of▁sentence|><|User|><tool_result>[0]</tool_result><|Assistant|><think>现在开始组织回答。</think>请及时就医。<|end▁of▁sentence|>
encoding/tests/test_output_4.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|>该助手为DeepSeek-V3,由深度求索公司创造。
2
+ 今天是2025年10月17日,星期五。<|latest_reminder|>2024-11-15,上海市,App,中文<|User|>热海大滚锅是世界著名温泉吗<|Assistant|></think>关于热海大滚锅是否是世界著名温泉,可以这样总结:它在**中国乃至全球的地热奇观中占有重要地位**,但“世界著名”的称号更侧重于它作为**独特的地质现象和旅游景点**,而非普遍的温泉疗养体验。
3
+
4
+ 为了让你快速了解,我整理了一个简要的表格:
5
+
6
+ | 维度 | 热海大滚锅的地位与特点 |
7
+ | :--- | :--- |
8
+ | **地质奇观** | **世界罕见**的高温地热系统,泉眼多、水温高、形态多样。 |
9
+ | **国内声誉** | **中国三大地热区之一**,**国家5A级旅游景区**,自古闻名(徐霞客曾游历并记载)。 |
10
+ | **国际知名度** | 在一些旅游平台被提及为“世界六大温泉”之一,但此说法流传不广,其国际声誉更多建立在地质独特性上。 |
11
+ | **核心体验** | **观赏地热奇观**(如97℃沸腾的“大滚锅”)、**体验温泉煮鸡蛋**。 |
12
+
13
+ ### 💡 游玩攻略与温馨提示
14
+
15
+ 如果你计划前往热海大滚锅,这里有一些实用信息供你参考:
16
+
17
+ - **门票与开放时间**:
18
+ - **门票**:景区门票约为**50元/人**。如果选择包含温泉沐浴的套餐,价格会更高,例如约**288元**。
19
+ - **开放时间**:景区一般**08:00-18:00**开放,但具体时间可能变动,建议提前核实。
20
+
21
+ - **特色体验**:
22
+ - **温泉煮鸡蛋**:这几乎是必试项目。可以在景区门口购买用草绳串起的生鸡蛋(约5-8元/串),然后到“大滚锅”旁的指定区域蒸煮,几分钟便可熟食,趣味十足。
23
+ - **金汤足浴**:可以直接用从“大滚锅”流出的温泉水泡脚,缓解旅途疲劳。
24
+
25
+ - **注意事项**:
26
+ - **安全第一**:“大滚锅”水温极高,务必遵守游览规则,在指定区域内观赏,切勿随意触碰泉水。
27
+ - **规划行程**:建议为热海景区预留**3-4小时**的游览时间。景区内步道不走回头路,出入口有观光车接送。
28
+
29
+ 希望这些信息能帮助你更好地了解热海大滚锅。如果你对腾冲的其他景点或者行程规划有更多疑问,我很乐意提供进一步的信息。<|end▁of▁sentence|><|User|>世界著名温泉有哪些<|Assistant|></think><|action|>Search<|end▁of▁sentence|>
generation_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 0,
4
+ "eos_token_id": 1,
5
+ "do_sample": true,
6
+ "temperature": 1.0,
7
+ "top_p": 1.0,
8
+ "transformers_version": "4.46.3"
9
+ }
inference/README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal inference
2
+
3
+ This directory contains a readable reference implementation rather than a
4
+ production serving engine. The model code includes Vision + Aligner, DFlash,
5
+ MoE, Hyper-Connections, and `Transformer.forward_spec()` for the DSpark forward
6
+ path. The generation loop remains straightforward autoregressive sampling.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ python -m pip install -r requirements.txt
12
+ ```
13
+
14
+ ## Convert Hugging Face weights
15
+
16
+ The reference runtime uses one converted checkpoint file per tensor-parallel
17
+ rank. From this directory:
18
+
19
+ ```bash
20
+ export HF_CKPT_PATH=/path/to/DeepSeek-V4-Flash-Vision-Exp-HF
21
+ export SAVE_PATH=/path/to/DeepSeek-V4-Flash-Vision-Exp-TP4
22
+ export MP=4
23
+
24
+ python convert.py \
25
+ --hf-ckpt-path "${HF_CKPT_PATH}" \
26
+ --save-path "${SAVE_PATH}" \
27
+ --n-experts 256 \
28
+ --model-parallel "${MP}" \
29
+ --expert-dtype fp4
30
+ ```
31
+
32
+ `convert.py` also copies `tokenizer.json` and `tokenizer_config.json` into the
33
+ converted checkpoint directory. `--tokenizer-path` can be used when tokenizer
34
+ files live outside the weight directory.
35
+
36
+ ## Run the equivalent TXT and JSON examples
37
+
38
+ ```bash
39
+ export CKPT_PATH=/path/to/DeepSeek-V4-Flash-Vision-Exp-TP4
40
+ export MP=4
41
+
42
+ INPUT_FILE=examples/example_vl.txt ./run.sh
43
+ INPUT_FILE=examples/example_vl_harmony.json ./run.sh
44
+ ```
45
+
46
+ The two files express the same interleaved two-image prompt and therefore
47
+ produce identical encoded prompts and input token IDs.
48
+
49
+ For interactive chat:
50
+
51
+ ```bash
52
+ torchrun --nproc-per-node "${MP}" generate.py \
53
+ --ckpt-path "${CKPT_PATH}" \
54
+ --config config.json \
55
+ --interactive \
56
+ --temperature 1.0
57
+ ```
58
+
59
+ For multi-node execution, pass the usual `torchrun --nnodes`, `--node-rank`,
60
+ `--master-addr`, and `--master-port` arguments before `generate.py`.
61
+
62
+ ## Preprocessing tests
63
+
64
+ From the repository root:
65
+
66
+ ```bash
67
+ python -m pytest -q \
68
+ encoding/test_encoding_dsv4.py \
69
+ inference/test_image_processor.py
70
+ ```
inference/config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 129280,
3
+ "dim": 4096,
4
+ "moe_inter_dim": 2048,
5
+ "n_layers": 43,
6
+ "n_hash_layers": 3,
7
+ "n_mtp_layers": 3,
8
+ "dspark_block_size": 5,
9
+ "dspark_noise_token_id": 128799,
10
+ "dspark_target_layer_ids": [40, 41, 42],
11
+ "dspark_markov_rank": 256,
12
+ "n_heads": 64,
13
+ "n_routed_experts": 256,
14
+ "n_shared_experts": 1,
15
+ "n_activated_experts": 6,
16
+ "score_func": "sqrtsoftplus",
17
+ "route_scale": 1.5,
18
+ "norm_eps": 1e-20,
19
+ "swiglu_limit": 10.0,
20
+ "q_lora_rank": 1024,
21
+ "head_dim": 512,
22
+ "rope_head_dim": 64,
23
+ "o_groups": 8,
24
+ "o_lora_rank": 1024,
25
+ "window_size": 128,
26
+ "original_seq_len": 65536,
27
+ "rope_theta": 10000,
28
+ "rope_factor": 16,
29
+ "beta_fast": 32,
30
+ "beta_slow": 1,
31
+ "index_n_heads": 64,
32
+ "index_head_dim": 128,
33
+ "index_topk": 512,
34
+ "hc_mult": 4,
35
+ "hc_sinkhorn_iters": 20,
36
+ "dtype": "fp8",
37
+ "scale_fmt": "ue8m0",
38
+ "expert_dtype": "fp4",
39
+ "compress_rope_theta": 160000,
40
+ "compress_ratios": [0, 0, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 0, 0, 0],
41
+ "vision_n_layers": 32,
42
+ "vision_dim": 1024,
43
+ "vision_n_heads": 16,
44
+ "vision_inter_dim": 2816,
45
+ "vision_patch_size": 14,
46
+ "vision_downsample_ratio": 3,
47
+ "vision_max_n_token": 384,
48
+ "vision_min_pixels": 147456,
49
+ "vision_max_wh_ratio": 8
50
+ }
inference/convert.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import shutil
4
+ from argparse import ArgumentParser
5
+ from glob import glob
6
+ from tqdm import tqdm, trange
7
+
8
+ import torch
9
+ from safetensors.torch import safe_open, save_file
10
+
11
+
12
+ FP4_TABLE = torch.tensor([
13
+ 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
14
+ 0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0
15
+ ], dtype=torch.float32)
16
+
17
+
18
+ def cast_e2m1fn_to_e4m3fn(x: torch.Tensor, scale: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
19
+ """
20
+ Casts a tensor from e2m1fn to e4m3fn losslessly.
21
+ """
22
+ assert x.dtype == torch.int8
23
+ assert x.ndim == 2
24
+ out_dim, in_dim = x.size()
25
+ in_dim *= 2
26
+ fp8_block_size = 128
27
+ fp4_block_size = 32
28
+ assert in_dim % fp8_block_size == 0 and out_dim % fp8_block_size == 0
29
+ assert scale.size(0) == out_dim and scale.size(1) == in_dim // fp4_block_size
30
+
31
+ x = x.view(torch.uint8)
32
+ low = x & 0x0F
33
+ high = (x >> 4) & 0x0F
34
+ x = torch.stack([FP4_TABLE[low.long()], FP4_TABLE[high.long()]], dim=-1).flatten(2)
35
+
36
+ # max_fp4 (6.0) * MAX_OFFSET must fit in e4m3fn (max 448)
37
+ # 6.0 * 2^6 = 384 < 448; 6.0 * 2^7 = 768 > 448; so MAX_OFFSET_BITS = 6
38
+ MAX_OFFSET_BITS = 6
39
+
40
+ bOut = out_dim // fp8_block_size
41
+ bIn = in_dim // fp8_block_size
42
+ # bOut, bIn, 128, 128
43
+ x = x.view(bOut, fp8_block_size, bIn, fp8_block_size).transpose(1, 2)
44
+ # bOut, bIn, 128*4
45
+ scale = scale.float().view(bOut, fp8_block_size, bIn, -1).transpose(1, 2).flatten(2)
46
+ ## bOut, bIn, 1
47
+ scale_max_offset_bits = scale.amax(dim=-1, keepdim=True) / (2**MAX_OFFSET_BITS)
48
+ # bOut, bIn, 128*4
49
+ offset = scale / scale_max_offset_bits
50
+ # bOut, bIn, 128, 128
51
+ offset = offset.unflatten(-1, (fp8_block_size, -1)).repeat_interleave(fp4_block_size, dim=-1)
52
+ x = (x * offset).transpose(1, 2).reshape(out_dim, in_dim)
53
+ return x.to(torch.float8_e4m3fn), scale_max_offset_bits.squeeze(-1).to(torch.float8_e8m0fnu)
54
+
55
+
56
+ mapping = {
57
+ "embed": ("embed", 0),
58
+ "wq_b": ("wq_b", 0),
59
+ "wo_a": ("wo_a", 0),
60
+ "wo_b": ("wo_b", 1),
61
+ "head": ("head", 0),
62
+ "attn_sink": ("attn_sink", 0),
63
+ "weights_proj": ("weights_proj", 0),
64
+ "markov_w1": ("markov_w1", 0),
65
+ "markov_w2": ("markov_w2", 0),
66
+ }
67
+
68
+
69
+ def main(hf_ckpt_path, save_path, n_experts, mp, expert_dtype, tokenizer_path=None):
70
+ """
71
+ Converts and saves model checkpoint files into a specified format.
72
+
73
+ Args:
74
+ hf_ckpt_path (str): Path to the directory containing the input checkpoint files.
75
+ save_path (str): Path to the directory where the converted checkpoint files will be saved.
76
+ n_experts (int): Total number of experts in the model.
77
+ mp (int): Model parallelism factor.
78
+
79
+ Returns:
80
+ None
81
+ """
82
+ torch.set_num_threads(8)
83
+ n_local_experts = n_experts // mp
84
+ state_dicts = [{} for _ in range(mp)]
85
+
86
+ index_path = os.path.join(hf_ckpt_path, "model.safetensors.index.json")
87
+ expected_names = set(json.load(open(index_path))["weight_map"]) if os.path.exists(index_path) else None
88
+ seen_names = set()
89
+
90
+ for file_path in tqdm(glob(os.path.join(hf_ckpt_path, "*.safetensors"))):
91
+ with safe_open(file_path, framework="pt", device="cpu") as f:
92
+ for name in f.keys():
93
+ seen_names.add(name)
94
+ param: torch.Tensor = f.get_tensor(name)
95
+ if name.startswith("model."):
96
+ name = name[len("model."):]
97
+ if name.startswith("mtp.") and ("emb" in name or name.endswith("head.weight")):
98
+ continue
99
+ name = name.replace("self_attn", "attn")
100
+ if not name.startswith("vision."):
101
+ name = name.replace("mlp", "ffn")
102
+ name = name.replace("weight_scale_inv", "scale")
103
+ name = name.replace("e_score_correction_bias", "bias")
104
+ if any(x in name for x in ["hc", "attn_sink", "tid2eid", "ape", "image_"]): # without .weight
105
+ key = name.split(".")[-1]
106
+ else:
107
+ key = name.split(".")[-2]
108
+ if key in mapping:
109
+ new_key, dim = mapping[key]
110
+ else:
111
+ new_key, dim = key, None
112
+ name = name.replace(key, new_key)
113
+ for i in range(mp):
114
+ new_param = param
115
+ if "experts" in name and "shared_experts" not in name:
116
+ idx = int(name.split(".")[-3])
117
+ if idx < i * n_local_experts or idx >= (i + 1) * n_local_experts:
118
+ continue
119
+ elif dim is not None:
120
+ assert param.size(dim) % mp == 0, f"Dimension {dim} must be divisible by {mp}"
121
+ shard_size = param.size(dim) // mp
122
+ new_param = param.narrow(dim, i * shard_size, shard_size).contiguous()
123
+ state_dicts[i][name] = new_param
124
+
125
+ if expected_names is not None:
126
+ assert seen_names == expected_names, (
127
+ f"checkpoint shards incomplete: {len(expected_names - seen_names)} tensors missing, "
128
+ f"{len(seen_names - expected_names)} unexpected (source may be mid-upload)")
129
+
130
+ os.makedirs(save_path, exist_ok=True)
131
+
132
+ for i in trange(mp):
133
+ names = list(state_dicts[i].keys())
134
+ for name in names:
135
+ if name.endswith("wo_a.weight"):
136
+ weight = state_dicts[i][name]
137
+ scale = state_dicts[i].pop(name.replace("weight", "scale"))
138
+ weight = weight.unflatten(0, (-1, 128)).unflatten(-1, (-1, 128)).float() * scale[:, None, :, None].float()
139
+ state_dicts[i][name] = weight.flatten(2, 3).flatten(0, 1).bfloat16()
140
+ elif "experts" in name and state_dicts[i][name].dtype == torch.int8:
141
+ if expert_dtype == "fp8":
142
+ scale_name = name.replace("weight", "scale")
143
+ weight = state_dicts[i].pop(name)
144
+ scale = state_dicts[i].pop(scale_name)
145
+ state_dicts[i][name], state_dicts[i][scale_name] = cast_e2m1fn_to_e4m3fn(weight, scale)
146
+ else:
147
+ state_dicts[i][name] = state_dicts[i][name].view(torch.float4_e2m1fn_x2)
148
+ save_file(state_dicts[i], os.path.join(save_path, f"model{i}-mp{mp}.safetensors"))
149
+
150
+ for file in ["tokenizer.json", "tokenizer_config.json"]:
151
+ old_file_path = os.path.join(tokenizer_path or hf_ckpt_path, file)
152
+ new_file_path = os.path.join(save_path, file)
153
+ if os.path.exists(old_file_path):
154
+ shutil.copyfile(old_file_path, new_file_path)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ parser = ArgumentParser()
159
+ parser.add_argument("--hf-ckpt-path", type=str, required=True)
160
+ parser.add_argument("--save-path", type=str, required=True)
161
+ parser.add_argument("--n-experts", type=int, required=True)
162
+ parser.add_argument("--model-parallel", type=int, required=True)
163
+ parser.add_argument("--expert-dtype", type=str, choices=["fp8", "fp4"], required=False, default=None)
164
+ parser.add_argument("--tokenizer-path", type=str, default=None)
165
+ args = parser.parse_args()
166
+ assert args.n_experts % args.model_parallel == 0, "Number of experts must be divisible by model parallelism"
167
+ main(args.hf_ckpt_path, args.save_path, args.n_experts, args.model_parallel, args.expert_dtype, args.tokenizer_path)
inference/examples/example_vl.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 请按“第一张、第二张”的顺序回答:第一张图<image>examples/images/carrots.jpeg</image>和第二张图<image>examples/images/corn.jpeg</image>中分别是什么食材?它们通常食用的部位分别是什么?
inference/examples/example_vl_harmony.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "messages": [
4
+ {
5
+ "role": "user",
6
+ "content": [
7
+ {
8
+ "type": "text",
9
+ "text": "请按“第一张、第二张”的顺序回答:第一张图"
10
+ },
11
+ {
12
+ "type": "image_url",
13
+ "image_url": {
14
+ "url": "examples/images/carrots.jpeg"
15
+ }
16
+ },
17
+ {
18
+ "type": "text",
19
+ "text": "和第二张图"
20
+ },
21
+ {
22
+ "type": "image_url",
23
+ "image_url": {
24
+ "url": "examples/images/corn.jpeg"
25
+ }
26
+ },
27
+ {
28
+ "type": "text",
29
+ "text": "中分别是什么食材?它们通常食用的部位分别是什么?"
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }
35
+ ]
inference/examples/images/carrots.jpeg ADDED

Git LFS Details

  • SHA256: 5df896a4a07e127281c60fc957f8b3d73f4735b3258a0bf762b4383557f8fa9a
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
inference/examples/images/corn.jpeg ADDED
inference/generate.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from typing import List
6
+
7
+ import torch
8
+ import torch.distributed as dist
9
+ from transformers import AutoTokenizer
10
+ from safetensors.torch import load_model
11
+
12
+ from model import Transformer, ModelArgs
13
+
14
+ current_dir = os.path.dirname(os.path.abspath(__file__))
15
+ encoding_dir = os.path.join(current_dir, "../encoding")
16
+ sys.path.insert(0, os.path.abspath(encoding_dir))
17
+
18
+ from encoding_dsv4 import (
19
+ encode_case,
20
+ load_cases,
21
+ parse_message_from_completion_text,
22
+ parse_tagged_text,
23
+ )
24
+ from image_processor import prepare_vl_inputs
25
+
26
+
27
+ @torch.inference_mode()
28
+ def generate(
29
+ model: Transformer,
30
+ prompt_tokens: List[List[int]],
31
+ max_new_tokens: int,
32
+ eos_id: int,
33
+ images=None,
34
+ ) -> List[List[int]]:
35
+ """Batch generation with right-padded prompts.
36
+
37
+ The first forward pass processes [min_prompt_len:] tokens (prefill phase).
38
+ Subsequent passes generate one token at a time (decode phase). For positions
39
+ still within a prompt, the ground-truth token overrides the model's prediction.
40
+ """
41
+ prompt_lens = [len(t) for t in prompt_tokens]
42
+ assert max(prompt_lens) <= model.max_seq_len, f"Prompt length exceeds model maximum sequence length (max_seq_len={model.max_seq_len})"
43
+ total_len = min(model.max_seq_len, max_new_tokens + max(prompt_lens))
44
+ tokens = torch.full((len(prompt_tokens), total_len), -1, dtype=torch.long)
45
+ for i, t in enumerate(prompt_tokens):
46
+ tokens[i, :len(t)] = torch.tensor(t, dtype=torch.long)
47
+ prev_pos = 0
48
+ finished = torch.tensor([False] * len(prompt_tokens))
49
+ prompt_mask = tokens != -1
50
+ for cur_pos in range(min(prompt_lens), total_len):
51
+ next_token = model.forward(tokens[:, prev_pos:cur_pos], prev_pos, images)[0]
52
+ next_token = torch.where(prompt_mask[:, cur_pos], tokens[:, cur_pos], next_token)
53
+ tokens[:, cur_pos] = next_token
54
+ finished |= torch.logical_and(~prompt_mask[:, cur_pos], next_token == eos_id)
55
+ prev_pos = cur_pos
56
+ if finished.all():
57
+ break
58
+ completion_tokens = []
59
+ for i, toks in enumerate(tokens.tolist()):
60
+ toks = toks[prompt_lens[i]:prompt_lens[i]+max_new_tokens]
61
+ if eos_id in toks:
62
+ toks = toks[:toks.index(eos_id)]
63
+ toks.append(eos_id)
64
+ completion_tokens.append(toks)
65
+ return completion_tokens
66
+
67
+
68
+ def generate_batched(model, prompt_tokens, images, max_new_tokens, eos_id, max_batch_size):
69
+ """Prompts with images run alone (image spans need single-chunk prefill for
70
+ in-image bidirectional attention); text-only prompts run in microbatches."""
71
+ if images is None:
72
+ images = [None] * len(prompt_tokens)
73
+ completion_tokens = [None] * len(prompt_tokens)
74
+ for i, m in enumerate(images):
75
+ if m is not None:
76
+ completion_tokens[i] = generate(model, [prompt_tokens[i]], max_new_tokens, eos_id, [m])[0]
77
+ text_idxs = [i for i, m in enumerate(images) if m is None]
78
+ for j in range(0, len(text_idxs), max_batch_size):
79
+ batch = text_idxs[j:j + max_batch_size]
80
+ completions = generate(model, [prompt_tokens[i] for i in batch], max_new_tokens, eos_id)
81
+ for i, toks in zip(batch, completions):
82
+ completion_tokens[i] = toks
83
+ return completion_tokens
84
+
85
+
86
+ def prepare_case(case, thinking_mode, tokenizer, args):
87
+ """Encode one message case and expand its image placeholders."""
88
+ if case.get("context"):
89
+ raise ValueError(
90
+ "Standalone inference does not support context without a prefilled KV cache")
91
+ prompt, image_records = encode_case(case, thinking_mode)
92
+ tokens, images = prepare_vl_inputs(prompt, image_records, tokenizer, args)
93
+ return prompt, tokens, images
94
+
95
+
96
+ def main(
97
+ ckpt_path: str,
98
+ config: str,
99
+ input_file: str = "",
100
+ interactive: bool = True,
101
+ max_new_tokens: int = 100,
102
+ temperature: float = 1.0,
103
+ thinking_mode: str = "chat",
104
+ ) -> None:
105
+ world_size = int(os.getenv("WORLD_SIZE", "1"))
106
+ rank = int(os.getenv("RANK", "0"))
107
+ local_rank = int(os.getenv("LOCAL_RANK", "0"))
108
+ if world_size > 1:
109
+ dist.init_process_group("nccl")
110
+ global print
111
+ if rank != 0:
112
+ print = lambda *_, **__: None
113
+ torch.cuda.set_device(local_rank)
114
+ torch.cuda.memory._set_allocator_settings("expandable_segments:True")
115
+ torch.set_default_dtype(torch.bfloat16)
116
+ torch.set_num_threads(8)
117
+ torch.manual_seed(33377335)
118
+ with open(config) as f:
119
+ args = ModelArgs(**json.load(f))
120
+ args.temperature = temperature
121
+ if interactive:
122
+ args.max_batch_size = 1
123
+ args.max_seq_len = 64 * 1024
124
+ print(args)
125
+ with torch.device("cuda"):
126
+ model = Transformer(args)
127
+ tokenizer = AutoTokenizer.from_pretrained(ckpt_path)
128
+ print("load model")
129
+ load_model(model, os.path.join(ckpt_path, f"model{rank}-mp{world_size}.safetensors"))
130
+ torch.set_default_device("cuda")
131
+ print("I'm DeepSeek 👋")
132
+
133
+ if interactive:
134
+ messages = []
135
+ while True:
136
+ if world_size == 1:
137
+ prompt = input(">>> ")
138
+ elif rank == 0:
139
+ prompt = input(">>> ")
140
+ objects = [prompt]
141
+ dist.broadcast_object_list(objects, 0)
142
+ else:
143
+ objects = [None]
144
+ dist.broadcast_object_list(objects, 0)
145
+ prompt = objects[0]
146
+ if prompt == "/exit":
147
+ break
148
+ elif prompt == "/clear":
149
+ messages.clear()
150
+ continue
151
+ messages.append({"role": "user", "content": parse_tagged_text(prompt)})
152
+ _, prompt_tokens, images = prepare_case(
153
+ {"messages": messages}, thinking_mode, tokenizer, args)
154
+ completion_tokens = generate(model, [prompt_tokens], max_new_tokens, tokenizer.eos_token_id, [images] if images else None)
155
+ completion = tokenizer.decode(completion_tokens[0])
156
+ print(completion)
157
+ messages.append(parse_message_from_completion_text(completion, thinking_mode=thinking_mode))
158
+ else:
159
+ json_input = input_file.endswith(".json")
160
+ if json_input:
161
+ cases = load_cases(input_file)
162
+ raw_prompts = None
163
+ else:
164
+ with open(input_file) as f:
165
+ raw_prompts = f.read().rstrip("\n").split("\n\n")
166
+ cases = [
167
+ {"messages": [{"role": "user", "content": parse_tagged_text(prompt)}]}
168
+ for prompt in raw_prompts
169
+ ]
170
+
171
+ prepared = [prepare_case(case, thinking_mode, tokenizer, args) for case in cases]
172
+ prompts = [prompt for prompt, _, _ in prepared]
173
+ prompt_tokens = [tokens for _, tokens, _ in prepared]
174
+ images = [image_inputs for _, _, image_inputs in prepared]
175
+ if not any(images):
176
+ images = None
177
+ completion_tokens = generate_batched(model, prompt_tokens, images, max_new_tokens,
178
+ tokenizer.eos_token_id, args.max_batch_size)
179
+ for i, (case, prompt, toks) in enumerate(zip(cases, prompts, completion_tokens)):
180
+ completion = tokenizer.decode(toks)
181
+ print("Prompt:", prompt if json_input else raw_prompts[i])
182
+ print("Completion:", completion)
183
+ if json_input:
184
+ print("Parsed:", parse_message_from_completion_text(
185
+ completion, thinking_mode=case.get("thinking_mode") or thinking_mode))
186
+ print()
187
+
188
+ if world_size > 1:
189
+ dist.destroy_process_group()
190
+
191
+
192
+ if __name__ == "__main__":
193
+ parser = ArgumentParser()
194
+ parser.add_argument("--ckpt-path", type=str, required=True)
195
+ parser.add_argument("--config", type=str, required=True)
196
+ parser.add_argument("--input-file", type=str, default="",
197
+ help="prompts separated by blank lines, or OpenAI-format cases (.json)")
198
+ parser.add_argument("--interactive", action="store_true")
199
+ parser.add_argument("--thinking-mode", type=str, default="chat", choices=["chat", "thinking"])
200
+ parser.add_argument("--max-new-tokens", type=int, default=None, help="default: 16384 for .json input, 200 otherwise")
201
+ parser.add_argument("--temperature", type=float, default=None, help="default: 0.99 for .json input, 1.0 otherwise")
202
+ args = parser.parse_args()
203
+ assert args.input_file or args.interactive, "Either input-file or interactive mode must be specified"
204
+ json_input = args.input_file.endswith(".json")
205
+ if args.max_new_tokens is None:
206
+ args.max_new_tokens = 16384 if json_input else 200
207
+ if args.temperature is None:
208
+ args.temperature = 0.99 if json_input else 1.0
209
+ main(args.ckpt_path, args.config, args.input_file, args.interactive, args.max_new_tokens, args.temperature, args.thinking_mode)
inference/image_processor.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import math
4
+ from dataclasses import dataclass
5
+ from urllib.request import urlopen
6
+
7
+ import numpy as np
8
+ import torch
9
+ from PIL import Image, ImageOps
10
+
11
+ IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5)
12
+ COMPRESS_PAD_TO = 4
13
+
14
+
15
+ @dataclass
16
+ class ImageInput:
17
+ start: int
18
+ patches: torch.Tensor
19
+ n_vit_h: int
20
+ n_vit_w: int
21
+ types: torch.Tensor
22
+ perm: torch.Tensor
23
+
24
+
25
+ def grid_tokens(best_height, best_width, patch_size, downsample_ratio):
26
+ """Number of LLM tokens the aligner grid occupies (N-layout, incl. row/align padding)."""
27
+ n_llm_h = math.ceil((best_height // patch_size) / downsample_ratio)
28
+ n_llm_w = math.ceil((best_width // patch_size) / downsample_ratio)
29
+ num_tokens = n_llm_h * (n_llm_w + 1) + 2
30
+ if n_llm_h % 2 == 1:
31
+ num_tokens += n_llm_w + 1
32
+ num_tokens += (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2
33
+ return n_llm_h, n_llm_w, num_tokens
34
+
35
+
36
+ def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token):
37
+ r = height / width
38
+ max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5
39
+ max_h_float = max_w_float * r
40
+ if max_w_float < 1.0:
41
+ max_w = 1
42
+ max_h = (max_n_token - 2) // (max_w + 1)
43
+ if max_h % 2 == 1:
44
+ max_h -= 1
45
+ best_width = max_w * patch_size * downsample_ratio
46
+ best_height = max_h * patch_size * downsample_ratio
47
+ elif max_h_float < 2.0:
48
+ max_h = 2
49
+ max_w = ((max_n_token - 2) // max_h) - 1
50
+ assert max_w > 1
51
+ best_width = max_w * patch_size * downsample_ratio
52
+ best_height = max_h * patch_size * downsample_ratio
53
+ else:
54
+ max_w = math.floor(max_w_float)
55
+ max_h = math.floor(max_h_float)
56
+ if max_h % 2 == 1:
57
+ max_h -= 1
58
+ beta = min(max_w * patch_size * downsample_ratio / width, max_h * patch_size * downsample_ratio / height)
59
+ best_width = math.floor(width * beta / patch_size) * patch_size
60
+ best_height = math.floor(height * beta / patch_size) * patch_size
61
+ n_llm_h, n_llm_w, num_tokens = grid_tokens(best_height, best_width, patch_size, downsample_ratio)
62
+ return n_llm_h, n_llm_w, best_height, best_width, num_tokens
63
+
64
+
65
+ def safe_resize(height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token):
66
+ max_n_token -= COMPRESS_PAD_TO - 1
67
+ n_llm_h, n_llm_w, num_tokens = grid_tokens(best_height, best_width, patch_size, downsample_ratio)
68
+ budget = max_n_token
69
+ while num_tokens > max_n_token:
70
+ n_llm_h, n_llm_w, best_height, best_width, num_tokens = solve_resize_ratio(
71
+ height, width, patch_size, downsample_ratio, budget)
72
+ budget -= 1
73
+ return n_llm_h, n_llm_w, best_height, best_width
74
+
75
+
76
+ def load_image_bytes(record) -> bytes:
77
+ """Load image bytes from raw/base64 data, an Anthropic source, URL, or path."""
78
+ data = record.get("data")
79
+ if isinstance(data, bytes):
80
+ return data
81
+ if isinstance(data, str):
82
+ return base64.b64decode(data)
83
+
84
+ source = record.get("source")
85
+ if isinstance(source, dict):
86
+ if source.get("data") is not None:
87
+ return base64.b64decode(source["data"])
88
+ if source.get("url"):
89
+ return load_image_bytes({"url": source["url"]})
90
+
91
+ url = record.get("url")
92
+ if isinstance(url, str) and url:
93
+ if url.startswith("data:"):
94
+ header, _, payload = url.partition(",")
95
+ if ";base64" not in header:
96
+ raise ValueError(f"Unsupported data URL encoding: {header}")
97
+ return base64.b64decode(payload)
98
+ if url.startswith(("http://", "https://")):
99
+ with urlopen(url, timeout=30) as response:
100
+ return response.read()
101
+ with open(url, "rb") as file:
102
+ return file.read()
103
+
104
+ raise ValueError(f"Cannot load image from record: {list(record.keys())}")
105
+
106
+
107
+ def load_image(record, args):
108
+ """Load and transform one image record into ViT patches."""
109
+
110
+ p = args.vision_patch_size
111
+ with Image.open(io.BytesIO(load_image_bytes(record))) as source:
112
+ image = source.convert("RGB")
113
+ width, height = image.size
114
+ if args.vision_max_wh_ratio is not None and width > height * args.vision_max_wh_ratio:
115
+ width = height * args.vision_max_wh_ratio
116
+ if 0 < width * height < args.vision_min_pixels:
117
+ ratio = (args.vision_min_pixels / (width * height)) ** 0.5
118
+ width = int(width * ratio)
119
+ height = int(height * ratio)
120
+ best_width = math.ceil(width / p) * p
121
+ best_height = math.ceil(height / p) * p
122
+ n_llm_h, n_llm_w, best_height, best_width = safe_resize(
123
+ height, width, best_height, best_width, p, args.vision_downsample_ratio, args.vision_max_n_token)
124
+ n_vit_h, n_vit_w = best_height // p, best_width // p
125
+ if args.vision_max_wh_ratio is not None and image.width >= args.vision_max_wh_ratio * image.height:
126
+ image = image.resize((best_width, best_height))
127
+ else:
128
+ image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127))
129
+ x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255
130
+ x = ((x - 0.5) / 0.5).to(torch.bfloat16)
131
+ patches = x.reshape(3, n_vit_h, p, n_vit_w, p).permute(1, 3, 0, 2, 4).reshape(n_vit_h * n_vit_w, 3, p, p)
132
+ return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w
133
+
134
+
135
+ def build_image_block(n_llm_h: int, n_llm_w: int, start_pos: int):
136
+ """Builds the N-layout token types (final order) and the aligner-row order for IMAGE slots."""
137
+ compress_pad = COMPRESS_PAD_TO - 1 - start_pos % COMPRESS_PAD_TO
138
+ pad_h = n_llm_h % 2
139
+ rows = n_llm_h + pad_h
140
+ row_len = n_llm_w + 1
141
+ pad_last = rows // 2 * row_len % 2 * 2
142
+ types = torch.tensor(([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h + [IMAGE_PAD] * (row_len * pad_h), dtype=torch.int64)
143
+ order = torch.arange(rows * row_len).view(rows // 2, 2, row_len).transpose(1, 2).reshape(-1)
144
+ image_idx = torch.full((rows * row_len,), -1, dtype=torch.int64)
145
+ image_idx.view(rows, row_len)[:n_llm_h, :n_llm_w] = torch.arange(n_llm_h * n_llm_w).view(n_llm_h, n_llm_w)
146
+ perm = image_idx[order]
147
+ perm = perm[perm >= 0]
148
+ types = torch.cat([
149
+ torch.full((compress_pad,), IMAGE_PAD, dtype=torch.int64),
150
+ torch.tensor([IMAGE_START]),
151
+ types[order],
152
+ torch.full((pad_last,), IMAGE_PAD, dtype=torch.int64),
153
+ torch.tensor([IMAGE_END]),
154
+ ])
155
+ return types, perm
156
+
157
+
158
+ def prepare_vl_inputs(prompt, images, tokenizer, args):
159
+ """Expand image placeholder tokens into sentinel blocks and ImageInput values."""
160
+ from encoding_dsv4 import IMAGE_PLACEHOLDER
161
+
162
+ image_token_id = tokenizer.convert_tokens_to_ids(IMAGE_PLACEHOLDER)
163
+ if image_token_id is None or image_token_id == tokenizer.unk_token_id:
164
+ raise ValueError(f"Token not found in tokenizer: {IMAGE_PLACEHOLDER}")
165
+ prompt_tokens = tokenizer.encode(prompt)
166
+ num_placeholders = sum(token == image_token_id for token in prompt_tokens)
167
+ if num_placeholders != len(images):
168
+ raise ValueError(
169
+ f"Found {num_placeholders} image tokens but got {len(images)} images")
170
+
171
+ tokens, image_inputs = [], []
172
+ image_iter = iter(images)
173
+ for tok in prompt_tokens:
174
+ if tok != image_token_id:
175
+ tokens.append(tok)
176
+ continue
177
+ patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = load_image(
178
+ next(image_iter), args)
179
+ types, perm = build_image_block(n_llm_h, n_llm_w, len(tokens))
180
+ image_inputs.append(ImageInput(len(tokens), patches, n_vit_h, n_vit_w, types, perm))
181
+ tokens += (args.vocab_size + types).tolist()
182
+ if not image_inputs:
183
+ return tokens, None
184
+ return tokens, image_inputs
inference/kernel.py ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import tilelang
3
+ import tilelang.language as T
4
+ from typing import Optional
5
+
6
+
7
+ tilelang.set_log_level("WARNING")
8
+
9
+ pass_configs = {
10
+ tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
11
+ tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
12
+ }
13
+
14
+ FP8 = "float8_e4m3"
15
+ FP4 = "float4_e2m1fn"
16
+ FE8M0 = "float8_e8m0fnu"
17
+ BF16 = "bfloat16"
18
+ FP32 = "float32"
19
+ INT32 = "int32"
20
+
21
+
22
+ def fast_log2_ceil(x):
23
+ """Compute ceil(log2(x)) via IEEE 754 bit manipulation. Avoids slow log/ceil intrinsics."""
24
+ bits_x = T.reinterpret("uint32", x)
25
+ exp_x = (bits_x >> 23) & 0xFF
26
+ man_bits = bits_x & ((1 << 23) - 1)
27
+ return T.Cast("int32", exp_x - 127 + T.if_then_else(man_bits != 0, 1, 0))
28
+
29
+
30
+ def fast_pow2(x):
31
+ """Compute 2^x for integer x via IEEE 754 bit manipulation."""
32
+ bits_x = (x + 127) << 23
33
+ return T.reinterpret("float32", bits_x)
34
+
35
+
36
+ def fast_round_scale(amax, fp8_max_inv):
37
+ return fast_pow2(fast_log2_ceil(amax * fp8_max_inv))
38
+
39
+
40
+ @tilelang.jit(pass_configs=pass_configs)
41
+ def act_quant_kernel(
42
+ N, block_size=128, in_dtype=BF16, out_dtype=FP8, scale_dtype=FP32,
43
+ round_scale=False, inplace=False
44
+ ):
45
+ """Block-wise FP8 quantization. inplace=True does fused quant+dequant back to BF16."""
46
+ M = T.symbolic("M")
47
+ fp8_min = -448.0
48
+ fp8_max = 448.0
49
+ fp8_max_inv = 1 / fp8_max
50
+ num_stages = 0 if round_scale or inplace else 2
51
+ blk_m = 32
52
+ group_size = block_size
53
+ # Internal computation in FP32; scale_dtype controls output storage format.
54
+ compute_dtype = FP32
55
+ out_dtype = in_dtype if inplace else out_dtype
56
+
57
+ @T.prim_func
58
+ def act_quant_kernel_(
59
+ X: T.Tensor[(M, N), in_dtype],
60
+ Y: T.Tensor[(M, N), out_dtype],
61
+ S: T.Tensor[(M, T.ceildiv(N, group_size)), scale_dtype],
62
+ ):
63
+ with T.Kernel(T.ceildiv(M, blk_m), T.ceildiv(N, group_size), threads=128) as (
64
+ pid_m,
65
+ pid_n,
66
+ ):
67
+ x_shared = T.alloc_shared((blk_m, group_size), in_dtype)
68
+ x_local = T.alloc_fragment((blk_m, group_size), in_dtype)
69
+ amax_local = T.alloc_fragment((blk_m,), compute_dtype)
70
+ s_local = T.alloc_fragment((blk_m,), compute_dtype)
71
+ y_local = T.alloc_fragment((blk_m, group_size), out_dtype)
72
+ y_shared = T.alloc_shared((blk_m, group_size), out_dtype)
73
+
74
+ for _ in T.Pipelined(1, num_stages=num_stages):
75
+ T.copy(X[pid_m * blk_m, pid_n * group_size], x_shared)
76
+ T.copy(x_shared, x_local)
77
+ T.reduce_absmax(x_local, amax_local, dim=1)
78
+ for i in T.Parallel(blk_m):
79
+ amax_local[i] = T.max(amax_local[i], 1e-4)
80
+ if round_scale:
81
+ s_local[i] = fast_round_scale(amax_local[i], fp8_max_inv)
82
+ else:
83
+ s_local[i] = amax_local[i] * fp8_max_inv
84
+ if inplace:
85
+ for i, j in T.Parallel(blk_m, group_size):
86
+ y_local[i, j] = T.Cast(
87
+ out_dtype,
88
+ T.Cast(compute_dtype, T.Cast(FP8, T.clamp(
89
+ x_local[i, j] / s_local[i], fp8_min, fp8_max
90
+ ))) * s_local[i],
91
+ )
92
+ else:
93
+ for i, j in T.Parallel(blk_m, group_size):
94
+ y_local[i, j] = T.clamp(
95
+ x_local[i, j] / s_local[i], fp8_min, fp8_max
96
+ )
97
+ for i in T.Parallel(blk_m):
98
+ S[pid_m * blk_m + i, pid_n] = T.Cast(scale_dtype, s_local[i])
99
+ T.copy(y_local, y_shared)
100
+ T.copy(y_shared, Y[pid_m * blk_m, pid_n * group_size])
101
+
102
+ return act_quant_kernel_
103
+
104
+
105
+ def act_quant(
106
+ x: torch.Tensor, block_size: int = 128, scale_fmt: Optional[str] = None,
107
+ scale_dtype: torch.dtype = torch.float32, inplace: bool = False,
108
+ ) -> torch.Tensor:
109
+ """Block-wise FP8 quantization. inplace=True does fused quant+dequant back to BF16.
110
+ When scale_fmt is set, scales are rounded to power-of-2 (MXFP)."""
111
+ N = x.size(-1)
112
+ assert N % block_size == 0
113
+ tl_dtype = FE8M0 if scale_dtype == torch.float8_e8m0fnu else FP32
114
+ z = x.contiguous()
115
+ y = torch.empty_like(z) if inplace else torch.empty_like(z, dtype=torch.float8_e4m3fn)
116
+ s = z.new_empty(*z.size()[:-1], N // block_size, dtype=scale_dtype)
117
+ kernel = act_quant_kernel(
118
+ N, block_size, scale_dtype=tl_dtype,
119
+ round_scale=scale_fmt is not None, inplace=inplace,
120
+ )
121
+ kernel(z.view(-1, N), y.view(-1, N), s.view(-1, N // block_size))
122
+ if inplace:
123
+ x.copy_(y)
124
+ return x
125
+ return y, s
126
+
127
+
128
+ @tilelang.jit(pass_configs=pass_configs)
129
+ def fp4_quant_kernel(
130
+ N, block_size=32, in_dtype=BF16, scale_dtype=FE8M0, inplace=False
131
+ ):
132
+ """Block-wise FP4 quantization. Power-of-2 scale via bit ops. inplace=True does fused quant+dequant."""
133
+ M = T.symbolic("M")
134
+ fp4_max = 6.0
135
+ fp4_max_inv = 1.0 / fp4_max
136
+ blk_m = 32
137
+ group_size = block_size
138
+ compute_dtype = FP32
139
+ out_dtype = in_dtype if inplace else FP4
140
+
141
+ @T.prim_func
142
+ def fp4_quant_kernel_(
143
+ X: T.Tensor[(M, N), in_dtype],
144
+ Y: T.Tensor[(M, N), out_dtype],
145
+ S: T.Tensor[(M, T.ceildiv(N, group_size)), scale_dtype],
146
+ ):
147
+ with T.Kernel(T.ceildiv(M, blk_m), T.ceildiv(N, group_size), threads=128) as (
148
+ pid_m,
149
+ pid_n,
150
+ ):
151
+ x_shared = T.alloc_shared((blk_m, group_size), in_dtype)
152
+ x_local = T.alloc_fragment((blk_m, group_size), in_dtype)
153
+ amax_local = T.alloc_fragment((blk_m,), compute_dtype)
154
+ s_local = T.alloc_fragment((blk_m,), compute_dtype)
155
+ y_local = T.alloc_fragment((blk_m, group_size), out_dtype)
156
+ y_shared = T.alloc_shared((blk_m, group_size), out_dtype)
157
+
158
+ for _ in T.Pipelined(1, num_stages=2):
159
+ T.copy(X[pid_m * blk_m, pid_n * group_size], x_shared)
160
+ T.copy(x_shared, x_local)
161
+ T.reduce_absmax(x_local, amax_local, dim=1)
162
+ for i in T.Parallel(blk_m):
163
+ amax_local[i] = T.max(amax_local[i], 6 * (2**-126))
164
+ s_local[i] = fast_round_scale(amax_local[i], fp4_max_inv)
165
+ if inplace:
166
+ for i, j in T.Parallel(blk_m, group_size):
167
+ y_local[i, j] = T.Cast(
168
+ out_dtype,
169
+ T.Cast(compute_dtype, T.Cast(FP4, T.clamp(
170
+ x_local[i, j] / s_local[i], -fp4_max, fp4_max
171
+ ))) * s_local[i],
172
+ )
173
+ else:
174
+ for i, j in T.Parallel(blk_m, group_size):
175
+ y_local[i, j] = T.clamp(
176
+ x_local[i, j] / s_local[i], -fp4_max, fp4_max
177
+ )
178
+ for i in T.Parallel(blk_m):
179
+ S[pid_m * blk_m + i, pid_n] = T.Cast(scale_dtype, s_local[i])
180
+ T.copy(y_local, y_shared)
181
+ T.copy(y_shared, Y[pid_m * blk_m, pid_n * group_size])
182
+
183
+ return fp4_quant_kernel_
184
+
185
+
186
+ def fp4_act_quant(
187
+ x: torch.Tensor, block_size: int = 32, inplace: bool = False,
188
+ ) -> torch.Tensor:
189
+ """Block-wise FP4 quantization. inplace=True does fused quant+dequant back to BF16."""
190
+ N = x.size(-1)
191
+ assert N % block_size == 0
192
+ z = x.contiguous()
193
+ y = torch.empty_like(z) if inplace else z.new_empty(*z.shape[:-1], N // 2, dtype=torch.float4_e2m1fn_x2)
194
+ s = z.new_empty(*z.size()[:-1], N // block_size, dtype=torch.float8_e8m0fnu)
195
+ kernel = fp4_quant_kernel(N, block_size, inplace=inplace)
196
+ kernel(z.view(-1, N), y.view(-1, y.size(-1)), s.view(-1, N // block_size))
197
+ if inplace:
198
+ x.copy_(y)
199
+ return x
200
+ return y, s
201
+
202
+
203
+ @tilelang.jit(pass_configs=pass_configs)
204
+ def fp8_gemm_kernel(N, K, out_dtype=BF16, accum_dtype=FP32, scale_dtype=FP32):
205
+ assert out_dtype in [BF16, FP32]
206
+
207
+ M = T.symbolic("M")
208
+ group_size = 128
209
+ block_M = 32
210
+ block_N = 128
211
+ block_K = 128
212
+
213
+ @T.prim_func
214
+ def fp8_gemm_kernel_(
215
+ A: T.Tensor[(M, K), FP8],
216
+ B: T.Tensor[(N, K), FP8],
217
+ C: T.Tensor[(M, N), out_dtype],
218
+ scales_a: T.Tensor[(M, T.ceildiv(K, group_size)), scale_dtype],
219
+ scales_b: T.Tensor[(T.ceildiv(N, group_size), T.ceildiv(K, group_size)), scale_dtype],
220
+ ):
221
+ with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (
222
+ bx,
223
+ by,
224
+ ):
225
+ A_shared = T.alloc_shared((block_M, block_K), FP8)
226
+ B_shared = T.alloc_shared((block_N, block_K), FP8)
227
+ C_shared = T.alloc_shared((block_M, block_N), out_dtype)
228
+ Scale_C_shared = T.alloc_shared((block_M), FP32)
229
+ C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
230
+ C_local_accum = T.alloc_fragment((block_M, block_N), accum_dtype)
231
+
232
+ # Improve L2 Cache
233
+ T.use_swizzle(panel_size=10)
234
+ T.clear(C_local)
235
+ T.clear(C_local_accum)
236
+
237
+ K_iters = T.ceildiv(K, block_K)
238
+ for k in T.Pipelined(K_iters, num_stages=4):
239
+ T.copy(A[by * block_M, k * block_K], A_shared)
240
+ T.copy(B[bx * block_N, k * block_K], B_shared)
241
+ # Cast scales to FP32 for computation; scales_b has one value per block_N group
242
+ Scale_B = T.Cast(FP32, scales_b[bx * block_N // group_size, k])
243
+ for i in T.Parallel(block_M):
244
+ Scale_C_shared[i] = T.Cast(FP32, scales_a[by * block_M + i, k]) * Scale_B
245
+
246
+ T.gemm(A_shared, B_shared, C_local, transpose_B=True)
247
+ # Separate accumulator for scale-corrected results (2x accumulation precision)
248
+ for i, j in T.Parallel(block_M, block_N):
249
+ C_local_accum[i, j] += C_local[i, j] * Scale_C_shared[i]
250
+ T.clear(C_local)
251
+ T.copy(C_local_accum, C_shared)
252
+ T.copy(C_shared, C[by * block_M, bx * block_N])
253
+
254
+ return fp8_gemm_kernel_
255
+
256
+
257
+ def fp8_gemm(
258
+ a: torch.Tensor, a_s: torch.Tensor, b: torch.Tensor, b_s: torch.Tensor,
259
+ scale_dtype: torch.dtype = torch.float32,
260
+ ) -> torch.Tensor:
261
+ """C[M,N] = A[M,K] @ B[N,K]^T with per-128 block FP8 scaling on both A and B."""
262
+ assert a.is_contiguous() and b.is_contiguous(), "Input tensors must be contiguous"
263
+ assert a_s.is_contiguous() and b_s.is_contiguous(), (
264
+ "Scaling factor tensors must be contiguous"
265
+ )
266
+ tl_dtype = FE8M0 if scale_dtype == torch.float8_e8m0fnu else FP32
267
+ K = a.size(-1)
268
+ M = a.numel() // K
269
+ N = b.size(0)
270
+ c = a.new_empty(*a.size()[:-1], N, dtype=torch.get_default_dtype())
271
+ kernel = fp8_gemm_kernel(N, K, scale_dtype=tl_dtype)
272
+ kernel(a.view(M, K), b, c.view(M, N), a_s.view(M, -1), b_s)
273
+ return c
274
+
275
+
276
+ @tilelang.jit(pass_configs=pass_configs)
277
+ def sparse_attn_kernel(h: int, d: int, scale=None):
278
+ """Sparse multi-head attention via index gathering + online softmax (FlashAttention-style).
279
+ For each (batch, seq_pos), gathers top-k KV positions by index, computes attention
280
+ with numerically stable running max/sum, and includes a learnable attn_sink bias."""
281
+ b = T.symbolic("b")
282
+ m = T.symbolic("m")
283
+ n = T.symbolic("n")
284
+ topk = T.symbolic("topk")
285
+ if scale is None:
286
+ scale = (1.0 / d) ** 0.5
287
+
288
+ num_stages = 2
289
+ threads = 256
290
+ block = 64
291
+ num_blocks = tilelang.cdiv(topk, block)
292
+
293
+ @T.prim_func
294
+ def sparse_attn_kernel_(
295
+ q: T.Tensor[(b, m, h, d), BF16],
296
+ kv: T.Tensor[(b, n, d), BF16],
297
+ o: T.Tensor[(b, m, h, d), BF16],
298
+ attn_sink: T.Tensor[(h,), FP32],
299
+ topk_idxs: T.Tensor[(b, m, topk), INT32],
300
+ ):
301
+ with T.Kernel(m, b, threads=threads) as (bx, by):
302
+ q_shared = T.alloc_shared((h, d), BF16)
303
+ kv_shared = T.alloc_shared((block, d), BF16)
304
+ o_shared = T.alloc_shared((h, d), BF16)
305
+ acc_s_cast = T.alloc_shared((h, block), BF16)
306
+
307
+ idxs = T.alloc_fragment(block, INT32)
308
+ acc_s = T.alloc_fragment((h, block), FP32)
309
+ acc_o = T.alloc_fragment((h, d), FP32)
310
+ scores_max = T.alloc_fragment(h, FP32)
311
+ scores_max_prev = T.alloc_fragment(h, FP32)
312
+ scores_scale = T.alloc_fragment(h, FP32)
313
+ scores_sum = T.alloc_fragment(h, FP32)
314
+ sum_exp = T.alloc_fragment(h, FP32)
315
+
316
+ T.clear(acc_o)
317
+ T.clear(sum_exp)
318
+ T.fill(scores_max, -T.infinity(FP32))
319
+ T.copy(q[by, bx, :, :], q_shared)
320
+
321
+ for t in T.Pipelined(num_blocks, num_stages=num_stages):
322
+ for i in T.Parallel(block):
323
+ idxs[i] = T.if_then_else(t * block + i < topk, topk_idxs[by, bx, t * block + i], -1)
324
+ for i, j in T.Parallel(block, d):
325
+ kv_shared[i, j] = T.if_then_else(idxs[i] != -1, kv[by, idxs[i], j], 0)
326
+ for i, j in T.Parallel(h, block):
327
+ acc_s[i, j] = T.if_then_else(idxs[j] != -1, 0, -T.infinity(FP32))
328
+ T.gemm(q_shared, kv_shared, acc_s, transpose_B=True, policy=T.GemmWarpPolicy.FullRow)
329
+ for i, j in T.Parallel(h, block):
330
+ acc_s[i, j] *= scale
331
+ T.copy(scores_max, scores_max_prev)
332
+ T.reduce_max(acc_s, scores_max, dim=1, clear=False)
333
+ for i in T.Parallel(h):
334
+ scores_scale[i] = T.exp(scores_max_prev[i] - scores_max[i])
335
+ for i, j in T.Parallel(h, block):
336
+ acc_s[i, j] = T.exp(acc_s[i, j] - scores_max[i])
337
+ T.reduce_sum(acc_s, scores_sum, dim=1)
338
+ for i in T.Parallel(h):
339
+ sum_exp[i] = sum_exp[i] * scores_scale[i] + scores_sum[i]
340
+ T.copy(acc_s, acc_s_cast)
341
+ for i, j in T.Parallel(h, d):
342
+ acc_o[i, j] *= scores_scale[i]
343
+ T.gemm(acc_s_cast, kv_shared, acc_o, policy=T.GemmWarpPolicy.FullRow)
344
+
345
+ for i in T.Parallel(h):
346
+ sum_exp[i] += T.exp(attn_sink[i] - scores_max[i])
347
+ for i, j in T.Parallel(h, d):
348
+ acc_o[i, j] /= sum_exp[i]
349
+ T.copy(acc_o, o_shared)
350
+ T.copy(o_shared, o[by, bx, :, :])
351
+
352
+ return sparse_attn_kernel_
353
+
354
+
355
+ def sparse_attn(
356
+ q: torch.Tensor, kv: torch.Tensor, attn_sink: torch.Tensor, topk_idxs: torch.Tensor, softmax_scale: float
357
+ ) -> torch.Tensor:
358
+ b, s, h, d = q.size()
359
+ # Pad heads to 16 for kernel efficiency (stripped after)
360
+ if h < 16:
361
+ q = torch.cat([q, q.new_zeros(b, s, 16 - h, d)], dim=2)
362
+ attn_sink = torch.cat([attn_sink, attn_sink.new_zeros(16 - h)])
363
+ o = torch.empty_like(q)
364
+ kernel = sparse_attn_kernel(q.size(2), d, softmax_scale)
365
+ kernel(q, kv, o, attn_sink, topk_idxs)
366
+ if h < 16:
367
+ o = o.narrow(2, 0, h).contiguous()
368
+ return o
369
+
370
+
371
+ @tilelang.jit(pass_configs=pass_configs)
372
+ def hc_split_sinkhorn_kernel(hc: int, sinkhorn_iters: int, eps: float):
373
+ n = T.symbolic("n")
374
+ mix_hc = (2 + hc) * hc
375
+ threads = 64
376
+
377
+ @T.prim_func
378
+ def hc_split_sinkhorn_kernel_(
379
+ mixes: T.Tensor[(n, mix_hc), FP32],
380
+ hc_scale: T.Tensor[(3,), FP32],
381
+ hc_base: T.Tensor[(mix_hc,), FP32],
382
+ pre: T.Tensor[(n, hc), FP32],
383
+ post: T.Tensor[(n, hc), FP32],
384
+ comb: T.Tensor[(n, hc, hc), FP32],
385
+ ):
386
+ with T.Kernel(n, threads=threads) as i:
387
+ mixes_shared = T.alloc_shared(mix_hc, FP32)
388
+ comb_frag = T.alloc_fragment((hc, hc), FP32)
389
+ T.copy(mixes[i, :], mixes_shared)
390
+
391
+ for j in T.Parallel(hc):
392
+ pre[i, j] = T.sigmoid(mixes_shared[j] * hc_scale[0] + hc_base[j]) + eps
393
+ for j in T.Parallel(hc):
394
+ post[i, j] = 2 * T.sigmoid(mixes_shared[j + hc] * hc_scale[1] + hc_base[j + hc])
395
+ for j, k in T.Parallel(hc, hc):
396
+ comb_frag[j, k] = mixes_shared[j * hc + k + hc * 2] * hc_scale[2] + hc_base[j * hc + k + hc * 2]
397
+
398
+ row_sum = T.alloc_fragment(hc, FP32)
399
+ col_sum = T.alloc_fragment(hc, FP32)
400
+
401
+ # comb = comb.softmax(-1) + eps
402
+ row_max = T.alloc_fragment(hc, FP32)
403
+ T.reduce_max(comb_frag, row_max, dim=1)
404
+ for j, k in T.Parallel(hc, hc):
405
+ comb_frag[j, k] = T.exp(comb_frag[j, k] - row_max[j])
406
+ T.reduce_sum(comb_frag, row_sum, dim=1)
407
+ for j, k in T.Parallel(hc, hc):
408
+ comb_frag[j, k] = comb_frag[j, k] / row_sum[j] + eps
409
+
410
+ # comb = comb / (comb.sum(-2) + eps)
411
+ T.reduce_sum(comb_frag, col_sum, dim=0)
412
+ for j, k in T.Parallel(hc, hc):
413
+ comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
414
+
415
+ for _ in T.serial(sinkhorn_iters - 1):
416
+ # comb = comb / (comb.sum(-1) + eps)
417
+ T.reduce_sum(comb_frag, row_sum, dim=1)
418
+ for j, k in T.Parallel(hc, hc):
419
+ comb_frag[j, k] = comb_frag[j, k] / (row_sum[j] + eps)
420
+ # comb = comb / (comb.sum(-2) + eps)
421
+ T.reduce_sum(comb_frag, col_sum, dim=0)
422
+ for j, k in T.Parallel(hc, hc):
423
+ comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
424
+
425
+ T.copy(comb_frag, comb[i, :, :])
426
+
427
+ return hc_split_sinkhorn_kernel_
428
+
429
+
430
+ def hc_split_sinkhorn(mixes: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, hc_mult: int = 4, sinkhorn_iters: int = 20, eps: float = 1e-6):
431
+ b, s, _ = mixes.size()
432
+ pre = mixes.new_empty(b, s, hc_mult)
433
+ post = mixes.new_empty(b, s, hc_mult)
434
+ comb = mixes.new_empty(b, s, hc_mult, hc_mult)
435
+ kernel = hc_split_sinkhorn_kernel(hc_mult, sinkhorn_iters, eps)
436
+ kernel(mixes.view(-1, (2 + hc_mult) * hc_mult), hc_scale, hc_base,
437
+ pre.view(-1, hc_mult), post.view(-1, hc_mult), comb.view(-1, hc_mult, hc_mult))
438
+ return pre, post, comb
439
+
440
+
441
+ @tilelang.jit(pass_configs=pass_configs)
442
+ def fp4_gemm_kernel(N, K, out_dtype=BF16, accum_dtype=FP32, scale_dtype=FP32):
443
+ """FP8 act x FP4 weight GEMM kernel.
444
+
445
+ C[M, N] = A_fp8[M, K] @ B_fp4[N, K]^T
446
+
447
+ Act: 1x128 quant on K (reduce dim), FP8 with configurable scale dtype
448
+ Weight: 1x32 quant on K (reduce dim), FP4 with E8M0 scale
449
+
450
+ B is stored as [N, K//2] in float4_e2m1fn_x2, logical [N, K] in fp4.
451
+ The FP4 values are packed along the K (last) dimension.
452
+
453
+ Strategy: load FP4 sub-blocks of size [block_N, sub_K] (sub_K=32),
454
+ cast FP4 to FP8 via float, then do FP8xFP8 GEMM.
455
+ Apply act scale (per 128 on K) and weight scale (per 32 on K) to the accumulator.
456
+ """
457
+ M = T.symbolic("M")
458
+ act_group_size = 128
459
+ weight_group_size = 32
460
+ block_M = 32
461
+ block_N = 128
462
+ block_K = 32 # matches weight_group_size for simple scale handling
463
+ n_sub = act_group_size // block_K # 4 sub-blocks per act scale group
464
+
465
+ @T.prim_func
466
+ def fp4_gemm_kernel_(
467
+ A: T.Tensor[(M, K), FP8],
468
+ B: T.Tensor[(N, K), FP4],
469
+ C: T.Tensor[(M, N), out_dtype],
470
+ scales_a: T.Tensor[(M, T.ceildiv(K, act_group_size)), scale_dtype],
471
+ scales_b: T.Tensor[(N, T.ceildiv(K, weight_group_size)), scale_dtype],
472
+ ):
473
+ with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (
474
+ bx,
475
+ by,
476
+ ):
477
+ A_shared = T.alloc_shared((block_M, block_K), FP8)
478
+ B_fp4_shared = T.alloc_shared((block_N, block_K), FP4)
479
+ B_shared = T.alloc_shared((block_N, block_K), FP8)
480
+ C_shared = T.alloc_shared((block_M, block_N), out_dtype)
481
+ C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
482
+ C_local_accum = T.alloc_fragment((block_M, block_N), accum_dtype)
483
+ scale_a_frag = T.alloc_fragment((block_M,), FP32)
484
+ scale_b_frag = T.alloc_fragment((block_N,), FP32)
485
+
486
+ T.use_swizzle(panel_size=10)
487
+ T.clear(C_local)
488
+ T.clear(C_local_accum)
489
+
490
+ K_iters = T.ceildiv(K, block_K)
491
+ for k in T.Pipelined(K_iters, num_stages=2):
492
+ T.copy(A[by * block_M, k * block_K], A_shared)
493
+ T.copy(B[bx * block_N, k * block_K], B_fp4_shared)
494
+ # FP4->FP8 cast must go through FP32 to avoid ambiguous C++ overload
495
+ for i, j in T.Parallel(block_N, block_K):
496
+ B_shared[i, j] = T.Cast(FP8, T.Cast(FP32, B_fp4_shared[i, j]))
497
+
498
+ # Weight scale: per 32 on K, indexed by k (each k is one block_K=32)
499
+ for i in T.Parallel(block_N):
500
+ scale_b_frag[i] = T.Cast(FP32, scales_b[bx * block_N + i, k])
501
+
502
+ # Act scale: per 128 on K, indexed by k // 4
503
+ for i in T.Parallel(block_M):
504
+ scale_a_frag[i] = T.Cast(FP32, scales_a[by * block_M + i, k // n_sub])
505
+
506
+ T.gemm(A_shared, B_shared, C_local, transpose_B=True)
507
+
508
+ for i, j in T.Parallel(block_M, block_N):
509
+ C_local_accum[i, j] += C_local[i, j] * scale_a_frag[i] * scale_b_frag[j]
510
+ T.clear(C_local)
511
+
512
+ T.copy(C_local_accum, C_shared)
513
+ T.copy(C_shared, C[by * block_M, bx * block_N])
514
+
515
+ return fp4_gemm_kernel_
516
+
517
+
518
+ def fp4_gemm(
519
+ a: torch.Tensor, a_s: torch.Tensor, b: torch.Tensor, b_s: torch.Tensor,
520
+ scale_dtype: torch.dtype = torch.float32,
521
+ ) -> torch.Tensor:
522
+ """C[M,N] = A_fp8[M,K] @ B_fp4[N,K]^T.
523
+ A has per-128 act scale; B has per-32 E8M0 weight scale.
524
+ B is stored as [N, K//2] in float4_e2m1fn_x2 (2 FP4 values per byte, packed along K)."""
525
+ assert a.is_contiguous() and b.is_contiguous(), "Input tensors must be contiguous"
526
+ assert a_s.is_contiguous() and b_s.is_contiguous(), (
527
+ "Scaling factor tensors must be contiguous"
528
+ )
529
+ tl_dtype = FE8M0 if scale_dtype == torch.float8_e8m0fnu else FP32
530
+ K = a.size(-1)
531
+ M = a.numel() // K
532
+ N = b.size(0)
533
+ c = a.new_empty(*a.size()[:-1], N, dtype=torch.get_default_dtype())
534
+ kernel = fp4_gemm_kernel(N, K, scale_dtype=tl_dtype)
535
+ kernel(a.view(M, K), b, c.view(M, N), a_s.view(M, -1), b_s)
536
+ return c
inference/model.py ADDED
@@ -0,0 +1,1046 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from dataclasses import dataclass
3
+ from typing import Tuple, Optional, Literal
4
+ from functools import lru_cache
5
+ from contextlib import contextmanager
6
+
7
+ import torch
8
+ from torch import nn
9
+ import torch.nn.functional as F
10
+ import torch.distributed as dist
11
+
12
+ from kernel import act_quant, fp4_act_quant, fp8_gemm, fp4_gemm, sparse_attn, hc_split_sinkhorn
13
+ from image_processor import IMAGE, IMAGE_START, IMAGE_END
14
+ from vision import ViT, Aligner
15
+
16
+
17
+ world_size = 1
18
+ rank = 0
19
+ block_size = 128
20
+ fp4_block_size = 32
21
+ default_dtype = torch.bfloat16
22
+ scale_fmt = None
23
+ scale_dtype = torch.float32
24
+
25
+
26
+ @contextmanager
27
+ def set_dtype(dtype):
28
+ """Temporarily override torch default dtype, restoring it on exit (even if an exception occurs)."""
29
+ prev = torch.get_default_dtype()
30
+ torch.set_default_dtype(dtype)
31
+ try:
32
+ yield
33
+ finally:
34
+ torch.set_default_dtype(prev)
35
+
36
+ @dataclass
37
+ class ModelArgs:
38
+ """Model hyperparameters. Field names match the config JSON keys."""
39
+ max_batch_size: int = 4
40
+ max_seq_len: int = 4096
41
+ temperature: float = 1
42
+ dtype: Literal["bf16", "fp8"] = "fp8"
43
+ scale_fmt: Literal[None, "ue8m0"] = "ue8m0"
44
+ expert_dtype: Literal[None, "fp4"] = None
45
+ scale_dtype: Literal["fp32", "fp8"] = "fp8"
46
+ vocab_size: int = 129280
47
+ dim: int = 4096
48
+ moe_inter_dim: int = 4096
49
+ n_layers: int = 7
50
+ n_hash_layers: int = 0
51
+ n_mtp_layers: int = 1
52
+ n_heads: int = 64
53
+ # moe
54
+ n_routed_experts: int = 8
55
+ n_shared_experts: int = 1
56
+ n_activated_experts: int = 2
57
+ score_func: Literal["softmax", "sigmoid", "sqrtsoftplus"] = "sqrtsoftplus"
58
+ route_scale: float = 1.
59
+ swiglu_limit: float = 0.
60
+ # mqa
61
+ q_lora_rank: int = 1024
62
+ head_dim: int = 512
63
+ rope_head_dim: int = 64
64
+ norm_eps: float = 1e-6
65
+ o_groups: int = 8
66
+ o_lora_rank: int = 1024
67
+ window_size: int = 128
68
+ compress_ratios: Tuple[int] = (0, 0, 4, 128, 4, 128, 4, 0)
69
+ # yarn
70
+ compress_rope_theta: float = 40000.0
71
+ original_seq_len: int = 0
72
+ rope_theta: float = 10000.0
73
+ rope_factor: float = 40
74
+ beta_fast: int = 32
75
+ beta_slow: int = 1
76
+ # index
77
+ index_n_heads: int = 64
78
+ index_head_dim: int = 128
79
+ index_topk: int = 512
80
+ # hc
81
+ hc_mult: int = 4
82
+ hc_sinkhorn_iters: int = 20
83
+ hc_eps: float = 1e-6
84
+ # dspark
85
+ dspark_block_size: int = 0
86
+ dspark_noise_token_id: int = 0
87
+ dspark_target_layer_ids: Tuple[int] = tuple()
88
+ dspark_markov_rank: int = 256
89
+ # vision
90
+ vision_n_layers: int = 0
91
+ vision_dim: int = 1024
92
+ vision_n_heads: int = 16
93
+ vision_inter_dim: int = 2816
94
+ vision_patch_size: int = 14
95
+ vision_rope_theta: float = 10000.0
96
+ vision_downsample_ratio: int = 3
97
+ vision_max_n_token: int = 384
98
+ vision_min_pixels: int = 147456
99
+ vision_max_wh_ratio: int = 8
100
+
101
+
102
+ class ParallelEmbedding(nn.Module):
103
+ """Embedding sharded along the vocab dimension. Each rank holds vocab_size // world_size rows.
104
+ Out-of-range indices are zero-masked before all_reduce to combine partial embeddings."""
105
+ def __init__(self, vocab_size: int, dim: int):
106
+ super().__init__()
107
+ self.vocab_size = vocab_size
108
+ self.dim = dim
109
+ assert vocab_size % world_size == 0, f"Vocabulary size must be divisible by world size (world_size={world_size})"
110
+ self.part_vocab_size = (vocab_size // world_size)
111
+ self.vocab_start_idx = rank * self.part_vocab_size
112
+ self.vocab_end_idx = self.vocab_start_idx + self.part_vocab_size
113
+ self.weight = nn.Parameter(torch.empty(self.part_vocab_size, self.dim))
114
+
115
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
116
+ mask = (x < self.vocab_start_idx) | (x >= self.vocab_end_idx)
117
+ x = torch.where(mask, 0, x - self.vocab_start_idx)
118
+ y = F.embedding(x, self.weight)
119
+ y[mask] = 0
120
+ if world_size > 1:
121
+ dist.all_reduce(y)
122
+ return y
123
+
124
+
125
+ def linear(x: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor] = None) -> torch.Tensor:
126
+ """Dispatches to fp4_gemm / fp8_gemm / F.linear based on weight dtype.
127
+ For quantized weights, x is first quantized to FP8 via act_quant."""
128
+ if weight.dtype == torch.float4_e2m1fn_x2:
129
+ x, s = act_quant(x, block_size, scale_fmt, scale_dtype)
130
+ return fp4_gemm(x, s, weight, weight.scale, scale_dtype)
131
+ elif weight.dtype == torch.float8_e4m3fn:
132
+ x, s = act_quant(x, block_size, scale_fmt, scale_dtype)
133
+ return fp8_gemm(x, s, weight, weight.scale, scale_dtype)
134
+ else:
135
+ return F.linear(x, weight)
136
+
137
+
138
+ class Linear(nn.Module):
139
+ """Linear layer supporting BF16, FP8, and FP4 weight formats with per-block scaling."""
140
+
141
+ def __init__(self, in_features: int, out_features: int, bias: bool = False, dtype = None):
142
+ super().__init__()
143
+ self.in_features = in_features
144
+ self.out_features = out_features
145
+ dtype = dtype or default_dtype
146
+ if dtype == torch.float4_e2m1fn_x2:
147
+ # FP4: weight is [out, in//2] in float4_e2m1fn_x2, logically [out, in] in fp4
148
+ # Scale is [out, in//32] in float8_e8m0fnu (1 scale per 32 fp4 elements along K)
149
+ self.weight = nn.Parameter(torch.empty(out_features, in_features // 2, dtype=torch.float4_e2m1fn_x2))
150
+ scale_out_features = out_features
151
+ scale_in_features = in_features // fp4_block_size
152
+ self.weight.scale = self.scale = nn.Parameter(torch.empty(scale_out_features, scale_in_features, dtype=torch.float8_e8m0fnu))
153
+ elif dtype == torch.float8_e4m3fn:
154
+ self.weight = nn.Parameter(torch.empty(out_features, in_features, dtype=dtype))
155
+ scale_out_features = (out_features + block_size - 1) // block_size
156
+ scale_in_features = (in_features + block_size - 1) // block_size
157
+ self.weight.scale = self.scale = nn.Parameter(torch.empty(scale_out_features, scale_in_features, dtype=torch.float8_e8m0fnu))
158
+ else:
159
+ self.weight = nn.Parameter(torch.empty(out_features, in_features, dtype=dtype))
160
+ self.register_parameter("scale", None)
161
+ if bias:
162
+ self.bias = nn.Parameter(torch.empty(out_features))
163
+ else:
164
+ self.register_parameter("bias", None)
165
+
166
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
167
+ return linear(x, self.weight, self.bias)
168
+
169
+
170
+ class ColumnParallelLinear(Linear):
171
+ """Shards output dim across TP ranks. No all-reduce needed on output."""
172
+ def __init__(self, in_features: int, out_features: int, bias: bool = False, dtype = None):
173
+ assert out_features % world_size == 0, f"Output features must be divisible by world size (world_size={world_size})"
174
+ self.part_out_features = out_features // world_size
175
+ super().__init__(in_features, self.part_out_features, bias, dtype)
176
+
177
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
178
+ return linear(x, self.weight, self.bias)
179
+
180
+
181
+ class RowParallelLinear(Linear):
182
+ """Shards input dim across TP ranks. All-reduce on output to sum partial results."""
183
+ def __init__(self, in_features: int, out_features: int, bias: bool = False, dtype = None):
184
+ assert in_features % world_size == 0, f"Input features must be divisible by world size (world_size={world_size})"
185
+ self.part_in_features = in_features // world_size
186
+ super().__init__(self.part_in_features, out_features, bias, dtype)
187
+
188
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
189
+ y = linear(x, self.weight, None)
190
+ if world_size > 1:
191
+ y = y.float()
192
+ dist.all_reduce(y)
193
+ if self.bias is not None:
194
+ y += self.bias
195
+ return y.type_as(x)
196
+
197
+
198
+ class RMSNorm(nn.Module):
199
+ def __init__(self, dim: int, eps: float = 1e-6):
200
+ super().__init__()
201
+ self.dim = dim
202
+ self.eps = eps
203
+ # rmsnorm in the checkpoint is stored in bf16, while the parameter here is stored in fp32 for convenient.
204
+ self.weight = nn.Parameter(torch.ones(dim, dtype=torch.float32))
205
+
206
+ def forward(self, x: torch.Tensor):
207
+ dtype = x.dtype
208
+ x = x.float()
209
+ var = x.square().mean(-1, keepdim=True)
210
+ x = x * torch.rsqrt(var + self.eps)
211
+ return (self.weight * x).to(dtype)
212
+
213
+
214
+ @lru_cache(2)
215
+ def precompute_freqs_cis(dim, seqlen, original_seq_len, base, factor, beta_fast, beta_slow) -> torch.Tensor:
216
+ """Precomputes complex exponentials for rotary embeddings with YaRN scaling.
217
+ When original_seq_len > 0, applies frequency interpolation with a smooth
218
+ linear ramp between beta_fast and beta_slow correction ranges."""
219
+
220
+ def find_correction_dim(num_rotations, dim, base, max_seq_len):
221
+ return dim * math.log(max_seq_len / (num_rotations * 2 * math.pi)) / (2 * math.log(base))
222
+
223
+ def find_correction_range(low_rot, high_rot, dim, base, max_seq_len):
224
+ low = math.floor(find_correction_dim(low_rot, dim, base, max_seq_len))
225
+ high = math.ceil(find_correction_dim(high_rot, dim, base, max_seq_len))
226
+ return max(low, 0), min(high, dim-1)
227
+
228
+ def linear_ramp_factor(min, max, dim):
229
+ if min == max:
230
+ max += 0.001
231
+ linear_func = (torch.arange(dim, dtype=torch.float32) - min) / (max - min)
232
+ ramp_func = torch.clamp(linear_func, 0, 1)
233
+ return ramp_func
234
+
235
+ freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
236
+ if original_seq_len > 0:
237
+ low, high = find_correction_range(beta_fast, beta_slow, dim, base, original_seq_len)
238
+ smooth = 1 - linear_ramp_factor(low, high, dim // 2)
239
+ freqs = freqs / factor * (1 - smooth) + freqs * smooth
240
+
241
+ t = torch.arange(seqlen)
242
+ freqs = torch.outer(t, freqs)
243
+ freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
244
+ return freqs_cis
245
+
246
+
247
+ def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor, inverse: bool = False) -> torch.Tensor:
248
+ """Applies rotary positional embeddings in-place. Uses conjugate for inverse (de-rotation)."""
249
+ y = x
250
+ x = torch.view_as_complex(x.float().unflatten(-1, (-1, 2)))
251
+ if inverse:
252
+ freqs_cis = freqs_cis.conj()
253
+ if x.ndim == 3:
254
+ freqs_cis = freqs_cis.view(1, x.size(1), x.size(-1))
255
+ else:
256
+ freqs_cis = freqs_cis.view(1, x.size(1), 1, x.size(-1))
257
+ x = torch.view_as_real(x * freqs_cis).flatten(-2)
258
+ y.copy_(x)
259
+ return y
260
+
261
+
262
+ def rotate_activation(x: torch.Tensor) -> torch.Tensor:
263
+ """Applies randomized Hadamard rotation to spread information across dims before FP8 quant."""
264
+ assert x.dtype == torch.bfloat16
265
+ from fast_hadamard_transform import hadamard_transform
266
+ return hadamard_transform(x, scale=x.size(-1) ** -0.5)
267
+
268
+
269
+ @lru_cache(1)
270
+ def get_window_topk_idxs(window_size: int, bsz: int, seqlen: int, start_pos: int):
271
+ if start_pos >= window_size - 1:
272
+ start_pos %= window_size
273
+ matrix = torch.cat([torch.arange(start_pos + 1, window_size), torch.arange(0, start_pos + 1)], dim=0)
274
+ elif start_pos > 0:
275
+ matrix = F.pad(torch.arange(start_pos + 1), (0, window_size - start_pos - 1), value=-1)
276
+ else:
277
+ base = torch.arange(seqlen).unsqueeze(1)
278
+ matrix = (base - window_size + 1).clamp(0) + torch.arange(min(seqlen, window_size))
279
+ matrix = torch.where(matrix > base, -1, matrix)
280
+ return matrix.int().unsqueeze(0).expand(bsz, -1, -1).contiguous()
281
+
282
+
283
+ def get_image_visible(input_ids: torch.Tensor, vocab_size: int, max_image_tokens: int):
284
+ """Per-token visible counts to the left/right within each [IMAGE_START, IMAGE_END] span."""
285
+ seqlen = input_ids.size(1)
286
+ idx = torch.arange(seqlen, dtype=torch.int32).unsqueeze(0)
287
+ is_start = input_ids == vocab_size + IMAGE_START
288
+ is_end = input_ids == vocab_size + IMAGE_END
289
+ valid = (is_start.cumsum(1) > is_end.cumsum(1)) | is_end
290
+ starts = torch.where(is_start, idx, 0).cummax(1)[0]
291
+ left = (idx - starts) * valid
292
+ ends = torch.where(is_end, idx, seqlen).flip(1).cummin(1)[0].flip(1)
293
+ right = (ends - idx) * valid
294
+ return left.clamp(max=max_image_tokens - 1), right.clamp(max=max_image_tokens)
295
+
296
+
297
+ def get_window_topk_idxs_visible(window_size: int, seqlen: int, left: torch.Tensor, right: torch.Tensor,
298
+ max_image_tokens: int):
299
+ width = min(seqlen, window_size + max_image_tokens)
300
+ idx = torch.arange(seqlen).unsqueeze(0)
301
+ left_add = (left - (window_size - 1)).clamp(min=0)
302
+ starts = (idx - (window_size - 1) - left_add).clamp(min=0)
303
+ matrix = starts.unsqueeze(-1) + torch.arange(width)
304
+ matrix = torch.where(matrix > (idx + right).unsqueeze(-1), -1, matrix)
305
+ return matrix.int().contiguous()
306
+
307
+
308
+ @lru_cache(2)
309
+ def get_compress_topk_idxs(ratio: int, bsz: int, seqlen: int, start_pos: int, offset: int):
310
+ if start_pos > 0:
311
+ matrix = torch.arange(0, (start_pos + 1) // ratio) + offset
312
+ else:
313
+ matrix = torch.arange(seqlen // ratio).repeat(seqlen, 1)
314
+ mask = matrix >= torch.arange(1, seqlen + 1).unsqueeze(1) // ratio
315
+ matrix = torch.where(mask, -1, matrix + offset)
316
+ return matrix.int().unsqueeze(0).expand(bsz, -1, -1).contiguous()
317
+
318
+
319
+ class Compressor(nn.Module):
320
+ """Compresses KV cache via learned gated pooling over `compress_ratio` consecutive tokens.
321
+ When overlap=True (ratio==4), uses overlapping windows for smoother compression boundaries."""
322
+
323
+ def __init__(self, args: ModelArgs, compress_ratio: int = 4, head_dim: int = 512, rotate: bool = False):
324
+ super().__init__()
325
+ self.dim = args.dim
326
+ self.head_dim = head_dim
327
+ self.rope_head_dim = args.rope_head_dim
328
+ self.nope_head_dim = head_dim - args.rope_head_dim
329
+ self.compress_ratio = compress_ratio
330
+ self.overlap = compress_ratio == 4
331
+ self.rotate = rotate
332
+ coff = 1 + self.overlap
333
+
334
+ self.ape = nn.Parameter(torch.empty(compress_ratio, coff * self.head_dim, dtype=torch.float32))
335
+ # wkv and wgate in the checkpoint is stored in bf16, while the parameter here is stored in fp32 for convenient.
336
+ # When overlap, the first half of dims is for overlapping compression, second half for normal.
337
+ self.wkv = Linear(self.dim, coff * self.head_dim, dtype=torch.float32)
338
+ self.wgate = Linear(self.dim, coff * self.head_dim, dtype=torch.float32)
339
+ self.norm = RMSNorm(self.head_dim, args.norm_eps)
340
+ self.kv_cache: torch.Tensor = None # assigned lazily from Attention.kv_cache
341
+ # State buffers for decode-phase incremental compression.
342
+ # With overlap: state[:, :ratio] = overlapping window, state[:, ratio:] = current window.
343
+ self.register_buffer("kv_state", torch.zeros(args.max_batch_size, coff * compress_ratio, coff * self.head_dim, dtype=torch.float32), persistent=False)
344
+ self.register_buffer("score_state", torch.full((args.max_batch_size, coff * compress_ratio, coff * self.head_dim), float("-inf"), dtype=torch.float32), persistent=False)
345
+ self.freqs_cis: torch.Tensor = None
346
+
347
+ def overlap_transform(self, tensor: torch.Tensor, value=0):
348
+ # tensor: [b,s,r,2d]
349
+ b, s, _, _ = tensor.size()
350
+ ratio, d = self.compress_ratio, self.head_dim
351
+ new_tensor = tensor.new_full((b, s, 2 * ratio, d), value)
352
+ new_tensor[:, :, ratio:] = tensor[:, :, :, d:]
353
+ new_tensor[:, 1:, :ratio] = tensor[:, :-1, :, :d]
354
+ return new_tensor
355
+
356
+ def forward(self, x: torch.Tensor, start_pos: int):
357
+ assert self.kv_cache is not None
358
+ bsz, seqlen, _ = x.size()
359
+ ratio, overlap, d, rd = self.compress_ratio, self.overlap, self.head_dim, self.rope_head_dim
360
+ dtype = x.dtype
361
+ # compression need fp32
362
+ x = x.float()
363
+ kv = self.wkv(x)
364
+ score = self.wgate(x)
365
+ if start_pos == 0:
366
+ should_compress = seqlen >= ratio
367
+ remainder = seqlen % ratio
368
+ cutoff = seqlen - remainder
369
+ offset = ratio if overlap else 0
370
+ if overlap and cutoff >= ratio:
371
+ self.kv_state[:bsz, :ratio] = kv[:, cutoff-ratio : cutoff]
372
+ self.score_state[:bsz, :ratio] = score[:, cutoff-ratio : cutoff] + self.ape
373
+ if remainder > 0:
374
+ kv, self.kv_state[:bsz, offset : offset+remainder] = kv.split([cutoff, remainder], dim=1)
375
+ self.score_state[:bsz, offset : offset+remainder] = score[:, cutoff:] + self.ape[:remainder]
376
+ score = score[:, :cutoff]
377
+ kv = kv.unflatten(1, (-1, ratio))
378
+ score = score.unflatten(1, (-1, ratio)) + self.ape
379
+ if overlap:
380
+ kv = self.overlap_transform(kv, 0)
381
+ score = self.overlap_transform(score, float("-inf"))
382
+ kv = (kv * score.softmax(dim=2)).sum(dim=2)
383
+ else:
384
+ should_compress = (start_pos + 1) % self.compress_ratio == 0
385
+ score += self.ape[start_pos % ratio]
386
+ if overlap:
387
+ self.kv_state[:bsz, ratio + start_pos % ratio] = kv.squeeze(1)
388
+ self.score_state[:bsz, ratio + start_pos % ratio] = score.squeeze(1)
389
+ if should_compress:
390
+ kv_state = torch.cat([self.kv_state[:bsz, :ratio, :d], self.kv_state[:bsz, ratio:, d:]], dim=1)
391
+ score_state = torch.cat([self.score_state[:bsz, :ratio, :d], self.score_state[:bsz, ratio:, d:]], dim=1)
392
+ kv = (kv_state * score_state.softmax(dim=1)).sum(dim=1, keepdim=True)
393
+ self.kv_state[:bsz, :ratio] = self.kv_state[:bsz, ratio:]
394
+ self.score_state[:bsz, :ratio] = self.score_state[:bsz, ratio:]
395
+ else:
396
+ self.kv_state[:bsz, start_pos % ratio] = kv.squeeze(1)
397
+ self.score_state[:bsz, start_pos % ratio] = score.squeeze(1)
398
+ if should_compress:
399
+ kv = (self.kv_state[:bsz] * self.score_state[:bsz].softmax(dim=1)).sum(dim=1, keepdim=True)
400
+ if not should_compress:
401
+ return
402
+ kv = self.norm(kv.to(dtype))
403
+ if start_pos == 0:
404
+ freqs_cis = self.freqs_cis[:cutoff:ratio]
405
+ else:
406
+ freqs_cis = self.freqs_cis[start_pos + 1 - self.compress_ratio].unsqueeze(0)
407
+ apply_rotary_emb(kv[..., -rd:], freqs_cis)
408
+ if self.rotate:
409
+ kv = rotate_activation(kv)
410
+ fp4_act_quant(kv, fp4_block_size, True)
411
+ else:
412
+ act_quant(kv[..., :-rd], 64, scale_fmt, scale_dtype, True)
413
+ if start_pos == 0:
414
+ self.kv_cache[:bsz, :seqlen // ratio] = kv
415
+ else:
416
+ self.kv_cache[:bsz, start_pos // ratio] = kv.squeeze(1)
417
+ return kv
418
+
419
+
420
+ class Indexer(torch.nn.Module):
421
+ """Selects top-k compressed KV positions for sparse attention via learned scoring.
422
+ Has its own Compressor (with Hadamard rotation) to build compressed KV for scoring."""
423
+
424
+ def __init__(self, args: ModelArgs, compress_ratio: int = 4):
425
+ super().__init__()
426
+ self.dim = args.dim
427
+ self.n_heads = args.index_n_heads
428
+ self.n_local_heads = args.index_n_heads // world_size
429
+ self.head_dim = args.index_head_dim
430
+ self.rope_head_dim = args.rope_head_dim
431
+ self.index_topk = args.index_topk
432
+ self.q_lora_rank = args.q_lora_rank
433
+ self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.head_dim)
434
+ self.weights_proj = ColumnParallelLinear(self.dim, self.n_heads, dtype=torch.bfloat16)
435
+ self.softmax_scale = self.head_dim ** -0.5
436
+ self.compress_ratio = compress_ratio
437
+
438
+ self.compressor = Compressor(args, compress_ratio, self.head_dim, True)
439
+ self.register_buffer("kv_cache", torch.zeros(args.max_batch_size, args.max_seq_len // compress_ratio, self.head_dim), persistent=False)
440
+ self.freqs_cis = None
441
+
442
+ def forward(self, x: torch.Tensor, qr: torch.Tensor, start_pos: int, offset: int):
443
+ bsz, seqlen, _ = x.size()
444
+ freqs_cis = self.freqs_cis[start_pos:start_pos+seqlen]
445
+ ratio = self.compress_ratio
446
+ rd = self.rope_head_dim
447
+ end_pos = start_pos + seqlen
448
+ if self.compressor.kv_cache is None:
449
+ self.compressor.kv_cache = self.kv_cache
450
+ self.compressor.freqs_cis = self.freqs_cis
451
+ q = self.wq_b(qr)
452
+ q = q.unflatten(-1, (self.n_local_heads, self.head_dim))
453
+ apply_rotary_emb(q[..., -rd:], freqs_cis)
454
+ q = rotate_activation(q)
455
+ # use fp4 simulation for q and kv in indexer
456
+ fp4_act_quant(q, fp4_block_size, True)
457
+ self.compressor(x, start_pos)
458
+ weights = self.weights_proj(x) * (self.softmax_scale * self.n_heads ** -0.5)
459
+ # We performed QAT here, kv could also use fp8 format, though current implementation uses bf16
460
+ index_score = torch.einsum("bshd,btd->bsht", q, self.kv_cache[:bsz, :end_pos // ratio])
461
+ index_score = (index_score.relu_() * weights.unsqueeze(-1)).sum(dim=2)
462
+ if world_size > 1:
463
+ dist.all_reduce(index_score)
464
+ if start_pos == 0:
465
+ mask = torch.arange(seqlen // ratio).repeat(seqlen, 1) >= torch.arange(1, seqlen + 1).unsqueeze(1) // ratio
466
+ index_score += torch.where(mask, float("-inf"), 0)
467
+ topk_idxs = index_score.topk(min(self.index_topk, end_pos // ratio), dim=-1)[1]
468
+ if start_pos == 0:
469
+ mask = topk_idxs >= torch.arange(1, seqlen + 1).unsqueeze(1) // ratio
470
+ topk_idxs = torch.where(mask, -1, topk_idxs + offset)
471
+ else:
472
+ topk_idxs += offset
473
+ return topk_idxs
474
+
475
+
476
+ class Attention(nn.Module):
477
+ """Multi-head Latent Attention (MLA) with sliding window + optional KV compression.
478
+ Uses low-rank Q projection (wq_a -> q_norm -> wq_b) and grouped low-rank O projection."""
479
+ def __init__(self, layer_id: int, args: ModelArgs):
480
+ super().__init__()
481
+ self.layer_id = layer_id
482
+ self.dim = args.dim
483
+ self.n_heads = args.n_heads
484
+ self.n_local_heads = args.n_heads // world_size
485
+ self.q_lora_rank = args.q_lora_rank
486
+ self.o_lora_rank = args.o_lora_rank
487
+ self.head_dim = args.head_dim
488
+ self.rope_head_dim = args.rope_head_dim
489
+ self.nope_head_dim = args.head_dim - args.rope_head_dim
490
+ self.n_groups = args.o_groups
491
+ self.n_local_groups = self.n_groups // world_size
492
+ self.window_size = args.window_size
493
+ self.compress_ratio = args.compress_ratios[layer_id]
494
+ self.eps = args.norm_eps
495
+ self.max_image_tokens = args.vision_max_n_token
496
+
497
+ self.attn_sink = nn.Parameter(torch.empty(self.n_local_heads, dtype=torch.float32))
498
+ self.wq_a = Linear(self.dim, self.q_lora_rank)
499
+ self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
500
+ self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.head_dim)
501
+ self.wkv = Linear(self.dim, self.head_dim)
502
+ self.kv_norm = RMSNorm(self.head_dim, self.eps)
503
+ self.wo_a = ColumnParallelLinear(self.n_heads * self.head_dim // self.n_groups, self.n_groups * args.o_lora_rank, dtype=torch.bfloat16)
504
+ self.wo_b = RowParallelLinear(self.n_groups * args.o_lora_rank, self.dim)
505
+ self.softmax_scale = self.head_dim ** -0.5
506
+
507
+ if self.compress_ratio:
508
+ self.compressor = Compressor(args, self.compress_ratio, self.head_dim)
509
+ if self.compress_ratio == 4:
510
+ self.indexer = Indexer(args, self.compress_ratio)
511
+ else:
512
+ self.indexer = None
513
+
514
+ kv_cache_size = args.window_size + (args.max_seq_len // self.compress_ratio if self.compress_ratio else 0)
515
+ self.register_buffer("kv_cache", torch.zeros(args.max_batch_size, kv_cache_size, self.head_dim), persistent=False)
516
+ if self.compress_ratio:
517
+ original_seq_len, rope_theta = args.original_seq_len, args.compress_rope_theta
518
+ else:
519
+ # disable YaRN and use base rope_theta in pure sliding-window attention
520
+ original_seq_len, rope_theta = 0, args.rope_theta
521
+ freqs_cis = precompute_freqs_cis(self.rope_head_dim, args.max_seq_len, original_seq_len,
522
+ rope_theta, args.rope_factor, args.beta_fast, args.beta_slow)
523
+ self.register_buffer("freqs_cis", freqs_cis, persistent=False)
524
+
525
+ def forward(self, x: torch.Tensor, start_pos: int, visible=None):
526
+ bsz, seqlen, _ = x.size()
527
+ freqs_cis = self.freqs_cis[start_pos:start_pos+seqlen]
528
+ win = self.window_size
529
+ ratio = self.compress_ratio
530
+ rd = self.rope_head_dim
531
+ if self.compress_ratio and self.compressor.kv_cache is None:
532
+ self.compressor.kv_cache = self.kv_cache[:, win:]
533
+ self.compressor.freqs_cis = self.freqs_cis
534
+ if self.indexer is not None:
535
+ self.indexer.freqs_cis = self.freqs_cis
536
+ # q
537
+ qr = q = self.q_norm(self.wq_a(x))
538
+ q = self.wq_b(q).unflatten(-1, (self.n_local_heads, self.head_dim))
539
+ q *= torch.rsqrt(q.square().mean(-1, keepdim=True) + self.eps)
540
+ apply_rotary_emb(q[..., -rd:], freqs_cis)
541
+
542
+ # win kv & topk_idxs
543
+ kv = self.wkv(x)
544
+ kv = self.kv_norm(kv)
545
+ apply_rotary_emb(kv[..., -rd:], freqs_cis)
546
+ # FP8-simulate non-rope dims to match QAT; rope dims stay bf16 for positional precision
547
+ act_quant(kv[..., :-rd], 64, scale_fmt, scale_dtype, True)
548
+ if visible is None:
549
+ topk_idxs = get_window_topk_idxs(win, bsz, seqlen, start_pos)
550
+ else:
551
+ topk_idxs = get_window_topk_idxs_visible(win, seqlen, *visible, self.max_image_tokens)
552
+ if self.compress_ratio:
553
+ offset = kv.size(1) if start_pos == 0 else win
554
+ if self.indexer is not None:
555
+ compress_topk_idxs = self.indexer(x, qr, start_pos, offset).int()
556
+ else:
557
+ compress_topk_idxs = get_compress_topk_idxs(ratio, bsz, seqlen, start_pos, offset)
558
+ topk_idxs = torch.cat([topk_idxs, compress_topk_idxs], dim=-1)
559
+
560
+ # compress kv & attn
561
+ if start_pos == 0:
562
+ if seqlen <= win:
563
+ self.kv_cache[:bsz, :seqlen] = kv
564
+ else:
565
+ cutoff = seqlen % win
566
+ self.kv_cache[:bsz, cutoff: win], self.kv_cache[:bsz, :cutoff] = kv[:, -win:].split([win - cutoff, cutoff], dim=1)
567
+ if self.compress_ratio:
568
+ if (kv_compress := self.compressor(x, start_pos)) is not None:
569
+ kv = torch.cat([kv, kv_compress], dim=1)
570
+ # We performed QAT here, kv could also use fp8 format, though current implementation uses bf16
571
+ o = sparse_attn(q, kv, self.attn_sink, topk_idxs, self.softmax_scale)
572
+ else:
573
+ self.kv_cache[:bsz, start_pos % win] = kv.squeeze(1)
574
+ if self.compress_ratio:
575
+ self.compressor(x, start_pos)
576
+ o = sparse_attn(q, self.kv_cache[:bsz], self.attn_sink, topk_idxs, self.softmax_scale)
577
+ apply_rotary_emb(o[..., -rd:], freqs_cis, True)
578
+
579
+ # o
580
+ o = o.view(bsz, seqlen, self.n_local_groups, -1)
581
+ wo_a = self.wo_a.weight.view(self.n_local_groups, self.o_lora_rank, -1)
582
+ # NOTE: wo_a is FP8 in checkpoint; could do FP8 einsum here for better perf,
583
+ # but using BF16 for simplicity.
584
+ o = torch.einsum("bsgd,grd->bsgr", o, wo_a)
585
+ x = self.wo_b(o.flatten(2))
586
+ return x
587
+
588
+
589
+ class Gate(nn.Module):
590
+ """MoE gating: computes expert routing scores and selects top-k experts.
591
+ Supports hash-based routing (first n_hash_layers) where expert indices are
592
+ predetermined per token ID, and score-based routing (remaining layers)."""
593
+ def __init__(self, layer_id: int, args: ModelArgs):
594
+ super().__init__()
595
+ self.dim = args.dim
596
+ self.topk = args.n_activated_experts
597
+ self.score_func = args.score_func
598
+ self.route_scale = args.route_scale
599
+ self.hash = layer_id < args.n_hash_layers
600
+ self.vocab_size = args.vocab_size
601
+ vl = args.vision_n_layers > 0
602
+ self.weight = nn.Parameter(torch.empty(args.n_routed_experts, args.dim))
603
+ if self.hash:
604
+ self.tid2eid = nn.Parameter(torch.empty(args.vocab_size, args.n_activated_experts, dtype=torch.int32), requires_grad=False)
605
+ if self.hash and not vl:
606
+ self.bias = None
607
+ else:
608
+ self.bias = nn.Parameter(torch.empty(args.n_routed_experts, dtype=torch.float32))
609
+ self.bias_vl = nn.Parameter(torch.empty(args.n_routed_experts, dtype=torch.float32)) if vl else None
610
+
611
+ def forward(self, x: torch.Tensor, input_ids: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, torch.Tensor]:
612
+ scores = linear(x.float(), self.weight.float())
613
+ if self.score_func == "softmax":
614
+ scores = scores.softmax(dim=-1)
615
+ elif self.score_func == "sigmoid":
616
+ scores = scores.sigmoid()
617
+ else:
618
+ scores = F.softplus(scores).sqrt()
619
+ original_scores = scores
620
+ image_mask = (input_ids >= self.vocab_size) if self.bias_vl is not None else None
621
+ # Bias shifts scores for expert selection (topk) but does not affect routing weights.
622
+ if self.hash:
623
+ if image_mask is None:
624
+ indices = self.tid2eid[input_ids]
625
+ else:
626
+ indices = self.tid2eid[torch.where(image_mask, 0, input_ids)]
627
+ vl_indices = (scores + self.bias_vl).topk(self.topk, dim=-1)[1]
628
+ indices = torch.where(image_mask.unsqueeze(-1), vl_indices.to(indices.dtype), indices)
629
+ else:
630
+ if image_mask is None:
631
+ scores = scores + self.bias
632
+ else:
633
+ scores = scores + torch.where(image_mask.unsqueeze(-1), self.bias_vl, self.bias)
634
+ indices = scores.topk(self.topk, dim=-1)[1]
635
+ weights = original_scores.gather(1, indices)
636
+ if self.score_func != "softmax":
637
+ weights /= weights.sum(dim=-1, keepdim=True)
638
+ weights *= self.route_scale
639
+ return weights, indices
640
+
641
+
642
+ class Expert(nn.Module):
643
+ """Single MoE expert: SwiGLU FFN (w1, w2, w3). Computation in float32 for stability."""
644
+ def __init__(self, dim: int, inter_dim: int, dtype=None, swiglu_limit=0):
645
+ super().__init__()
646
+ self.w1 = Linear(dim, inter_dim, dtype=dtype)
647
+ self.w2 = Linear(inter_dim, dim, dtype=dtype)
648
+ self.w3 = Linear(dim, inter_dim, dtype=dtype)
649
+ self.swiglu_limit = swiglu_limit
650
+
651
+ def forward(self, x: torch.Tensor, weights: Optional[torch.Tensor] = None) -> torch.Tensor:
652
+ dtype = x.dtype
653
+ gate = self.w1(x).float()
654
+ up = self.w3(x).float()
655
+ if self.swiglu_limit > 0:
656
+ up = torch.clamp(up, min=-self.swiglu_limit, max=self.swiglu_limit)
657
+ gate = torch.clamp(gate, max=self.swiglu_limit)
658
+ x = F.silu(gate) * up
659
+ if weights is not None:
660
+ x = weights * x
661
+ return self.w2(x.to(dtype))
662
+
663
+
664
+ class MoE(nn.Module):
665
+ """Mixture-of-Experts: gate routes each token to top-k routed experts + 1 shared expert.
666
+ Experts are sharded across TP ranks; each rank handles n_routed_experts // world_size experts."""
667
+ def __init__(self, layer_id: int, args: ModelArgs):
668
+ super().__init__()
669
+ self.layer_id = layer_id
670
+ self.dim = args.dim
671
+ assert args.n_routed_experts % world_size == 0, f"Number of experts must be divisible by world size (world_size={world_size})"
672
+ self.n_routed_experts = args.n_routed_experts
673
+ self.n_local_experts = args.n_routed_experts // world_size
674
+ self.n_activated_experts = args.n_activated_experts
675
+ self.experts_start_idx = rank * self.n_local_experts
676
+ self.experts_end_idx = self.experts_start_idx + self.n_local_experts
677
+ self.gate = Gate(layer_id, args)
678
+ expert_dtype = torch.float4_e2m1fn_x2 if args.expert_dtype == "fp4" else None
679
+ self.experts = nn.ModuleList([Expert(args.dim, args.moe_inter_dim, dtype=expert_dtype, swiglu_limit=args.swiglu_limit) if self.experts_start_idx <= i < self.experts_end_idx else None
680
+ for i in range(self.n_routed_experts)])
681
+ assert args.n_shared_experts == 1
682
+ self.shared_experts = Expert(args.dim, args.moe_inter_dim, swiglu_limit=args.swiglu_limit)
683
+
684
+ def forward(self, x: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor:
685
+ shape = x.size()
686
+ x = x.view(-1, self.dim)
687
+ weights, indices = self.gate(x, input_ids.flatten())
688
+ y = torch.zeros_like(x, dtype=torch.float32)
689
+ counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist()
690
+ for i in range(self.experts_start_idx, self.experts_end_idx):
691
+ if counts[i] == 0:
692
+ continue
693
+ expert = self.experts[i]
694
+ idx, top = torch.where(indices == i)
695
+ y[idx] += expert(x[idx], weights[idx, top, None])
696
+ if world_size > 1:
697
+ dist.all_reduce(y)
698
+ y += self.shared_experts(x)
699
+ return y.type_as(x).view(shape)
700
+
701
+
702
+ class Block(nn.Module):
703
+ """Transformer block with Hyper-Connections (HC) mixing.
704
+ Instead of a simple residual, HC maintains `hc_mult` copies of the hidden state.
705
+ hc_pre: reduces hc copies -> 1 via learned weighted sum (pre-weights from Sinkhorn).
706
+ hc_post: expands 1 -> hc copies via learned post-weights + combination matrix."""
707
+ attention_cls = Attention
708
+
709
+ def __init__(self, layer_id: int, args: ModelArgs):
710
+ super().__init__()
711
+ self.layer_id = layer_id
712
+ self.norm_eps = args.norm_eps
713
+ self.attn = self.attention_cls(layer_id, args)
714
+ self.ffn = MoE(layer_id, args)
715
+ self.attn_norm = RMSNorm(args.dim, self.norm_eps)
716
+ self.ffn_norm = RMSNorm(args.dim, self.norm_eps)
717
+ self.hc_mult = hc_mult = args.hc_mult
718
+ self.hc_sinkhorn_iters = args.hc_sinkhorn_iters
719
+ self.hc_eps = args.hc_eps
720
+ mix_hc = (2 + hc_mult) * hc_mult
721
+ hc_dim = hc_mult * args.dim
722
+ with set_dtype(torch.float32):
723
+ self.hc_attn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
724
+ self.hc_ffn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
725
+ self.hc_attn_base = nn.Parameter(torch.empty(mix_hc))
726
+ self.hc_ffn_base = nn.Parameter(torch.empty(mix_hc))
727
+ self.hc_attn_scale = nn.Parameter(torch.empty(3))
728
+ self.hc_ffn_scale = nn.Parameter(torch.empty(3))
729
+
730
+ def hc_pre(self, x: torch.Tensor, hc_fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor):
731
+ # x: [b,s,hc,d], hc_fn: [mix_hc,hc*d], hc_scale: [3], hc_base: [mix_hc], y: [b,s,hc,d]
732
+ shape, dtype = x.size(), x.dtype
733
+ x = x.flatten(2).float()
734
+ rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
735
+ mixes = F.linear(x, hc_fn) * rsqrt
736
+ pre, post, comb = hc_split_sinkhorn(mixes, hc_scale, hc_base, self.hc_mult, self.hc_sinkhorn_iters, self.hc_eps)
737
+ y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
738
+ return y.to(dtype), post, comb
739
+
740
+ def hc_post(self, x: torch.Tensor, residual: torch.Tensor, post: torch.Tensor, comb: torch.Tensor):
741
+ # x: [b,s,d], residual: [b,s,hc,d], post: [b,s,hc], comb: [b,s,hc,hc], y: [b,s,hc,d]
742
+ y = post.unsqueeze(-1) * x.unsqueeze(-2) + torch.sum(comb.unsqueeze(-1) * residual.unsqueeze(-2), dim=2)
743
+ return y.type_as(x)
744
+
745
+ def forward(self, x: torch.Tensor, start_pos: int, input_ids: Optional[torch.Tensor], *attn_args) -> torch.Tensor:
746
+ residual = x
747
+ x, post, comb = self.hc_pre(x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base)
748
+ x = self.attn_norm(x)
749
+ x = self.attn(x, start_pos, *attn_args)
750
+ x = self.hc_post(x, residual, post, comb)
751
+
752
+ residual = x
753
+ x, post, comb = self.hc_pre(x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base)
754
+ x = self.ffn_norm(x)
755
+ x = self.ffn(x, input_ids)
756
+ x = self.hc_post(x, residual, post, comb)
757
+ return x
758
+
759
+ def hc_head(self, x: torch.Tensor, hc_fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor):
760
+ shape, dtype = x.size(), x.dtype
761
+ x = x.flatten(2).float()
762
+ rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
763
+ mixes = F.linear(x, hc_fn) * rsqrt
764
+ pre = torch.sigmoid(mixes * hc_scale + hc_base) + self.hc_eps
765
+ y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
766
+ return y.to(dtype)
767
+
768
+
769
+ class ParallelHead(nn.Module):
770
+
771
+ def __init__(self, vocab_size: int, dim: int, norm_eps: float = 1e-6, hc_eps: float = 1e-6):
772
+ super().__init__()
773
+ self.vocab_size = vocab_size
774
+ self.dim = dim
775
+ self.norm_eps = norm_eps
776
+ self.hc_eps = hc_eps
777
+ self.part_vocab_size = (vocab_size // world_size)
778
+ # lm_head in the checkpoint is stored in bf16, while the parameter here is stored in fp32 for easier computation of logits later.
779
+ self.weight = nn.Parameter(torch.empty(self.part_vocab_size, self.dim, dtype=torch.float32))
780
+
781
+ def forward(self, x: torch.Tensor, full_logits=False):
782
+ # x: [b,s,hc,d]
783
+ if not full_logits:
784
+ x = x[:, -1]
785
+ logits = F.linear(x.float(), self.weight)
786
+ if world_size > 1:
787
+ all_logits = [torch.empty_like(logits) for _ in range(world_size)]
788
+ dist.all_gather(all_logits, logits)
789
+ logits = torch.cat(all_logits, dim=-1)
790
+ return logits
791
+
792
+
793
+ @lru_cache(1)
794
+ def get_dspark_topk_idxs(window_size: int, bsz: int, block_size: int, start_pos: int):
795
+ assert start_pos > 0
796
+ matrix = torch.cat([torch.arange(min(window_size, start_pos + 1)), window_size + torch.arange(block_size)])
797
+ return matrix.int().view(1, 1, -1).expand(bsz, block_size, -1).contiguous()
798
+
799
+
800
+ class DSparkAttention(Attention):
801
+
802
+ def forward(self, x: torch.Tensor, start_pos: int, main_x: torch.Tensor):
803
+ assert self.compress_ratio == 0
804
+ bsz, seqlen, _ = main_x.size()
805
+ win = self.window_size
806
+ rd = self.rope_head_dim
807
+
808
+ main_freqs_cis = self.freqs_cis[start_pos:start_pos+seqlen]
809
+ main_kv = self.kv_norm(self.wkv(main_x))
810
+ apply_rotary_emb(main_kv[..., -rd:], main_freqs_cis)
811
+ act_quant(main_kv[..., :-rd], 64, scale_fmt, scale_dtype, True)
812
+
813
+ if start_pos == 0:
814
+ if seqlen <= win:
815
+ self.kv_cache[:bsz, :seqlen] = main_kv
816
+ else:
817
+ cutoff = seqlen % win
818
+ self.kv_cache[:bsz, cutoff: win], self.kv_cache[:bsz, :cutoff] = main_kv[:, -win:].split([win - cutoff, cutoff], dim=1)
819
+ return x
820
+
821
+ bsz, block_size, _ = x.size()
822
+ freqs_cis = self.freqs_cis[start_pos+seqlen:start_pos+seqlen+block_size]
823
+
824
+ q = self.q_norm(self.wq_a(x))
825
+ q = self.wq_b(q).unflatten(-1, (self.n_local_heads, self.head_dim))
826
+ q *= torch.rsqrt(q.square().mean(-1, keepdim=True) + self.eps)
827
+ apply_rotary_emb(q[..., -rd:], freqs_cis)
828
+ kv = self.kv_norm(self.wkv(x))
829
+ apply_rotary_emb(kv[..., -rd:], freqs_cis)
830
+ act_quant(kv[..., :-rd], 64, scale_fmt, scale_dtype, True)
831
+
832
+ topk_idxs = get_dspark_topk_idxs(win, bsz, block_size, start_pos)
833
+ self.kv_cache[:bsz, start_pos % win] = main_kv.squeeze(1)
834
+ kv = torch.cat([self.kv_cache[:bsz], kv], dim=1)
835
+ o = sparse_attn(q, kv, self.attn_sink, topk_idxs, self.softmax_scale)
836
+ apply_rotary_emb(o[..., -rd:], freqs_cis, True)
837
+
838
+ o = o.view(bsz, block_size, self.n_local_groups, -1)
839
+ wo_a = self.wo_a.weight.view(self.n_local_groups, self.o_lora_rank, -1)
840
+ o = torch.einsum("bsgd,grd->bsgr", o, wo_a)
841
+ x = self.wo_b(o.flatten(2))
842
+ return x
843
+
844
+
845
+ class DSparkMarkovHead(nn.Module):
846
+ def __init__(self, vocab_size: int, dspark_markov_rank: int):
847
+ super().__init__()
848
+ self.markov_w1 = ParallelEmbedding(vocab_size, dspark_markov_rank)
849
+ self.markov_w2 = ParallelHead(vocab_size, dspark_markov_rank)
850
+
851
+ def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
852
+ embed = self.markov_w1(token_ids)
853
+ logits = self.markov_w2(embed, full_logits=True)
854
+ return logits, embed
855
+
856
+
857
+ class DSparkConfidenceHead(nn.Module):
858
+ def __init__(self, input_dim: int):
859
+ super().__init__()
860
+ # proj in the checkpoint is stored in bf16, while the parameter here is stored in fp32 for fp32 confidence score.
861
+ self.proj = Linear(input_dim, 1, dtype=torch.float32)
862
+
863
+ def forward(self, hidden: torch.Tensor, markov_embed: torch.Tensor):
864
+ hidden = torch.cat([hidden, markov_embed], dim=-1)
865
+ return self.proj(hidden.float()).squeeze(-1)
866
+
867
+
868
+ class DSparkBlock(Block):
869
+ """DSpark stage stored under the mtp.* checkpoint namespace."""
870
+ attention_cls = DSparkAttention
871
+
872
+ def __init__(self, layer_id: int, args: ModelArgs):
873
+ super().__init__(layer_id, args)
874
+ self.dim = args.dim
875
+ stage_id = layer_id - args.n_layers
876
+ self.block_size = args.dspark_block_size
877
+ self.noise_token_id = args.dspark_noise_token_id
878
+ self.temperature = args.temperature
879
+ hc_dim = self.hc_mult * args.dim
880
+ if stage_id == 0:
881
+ assert len(args.dspark_target_layer_ids) > 0, "DSpark needs target layers"
882
+ self.main_proj = Linear(args.dim * len(args.dspark_target_layer_ids), args.dim)
883
+ self.main_norm = RMSNorm(args.dim, args.norm_eps)
884
+ if stage_id == args.n_mtp_layers - 1:
885
+ self.norm = RMSNorm(args.dim, args.norm_eps)
886
+ self.markov_head = DSparkMarkovHead(args.vocab_size, args.dspark_markov_rank)
887
+ self.confidence_head = DSparkConfidenceHead(args.dim + args.dspark_markov_rank)
888
+ with set_dtype(torch.float32):
889
+ self.hc_head_fn = nn.Parameter(torch.empty(self.hc_mult, hc_dim))
890
+ self.hc_head_base = nn.Parameter(torch.empty(self.hc_mult))
891
+ self.hc_head_scale = nn.Parameter(torch.empty(1))
892
+ self.embed: ParallelEmbedding = None
893
+ self.head: ParallelHead = None
894
+
895
+ def forward(self, x: torch.Tensor, start_pos: int, input_ids: torch.Tensor, main_x: torch.Tensor) -> torch.Tensor:
896
+ if start_pos > 0:
897
+ return super().forward(x, start_pos, input_ids, main_x)
898
+ # only compute KV cache in prefill stage
899
+ return self.attn(x, start_pos, main_x)
900
+
901
+ def forward_embed(self, main_hidden: torch.Tensor, input_ids: torch.Tensor):
902
+ assert self.embed is not None
903
+ main_x = self.main_norm(self.main_proj(main_hidden))
904
+ draft_input_ids = input_ids.new_full([input_ids.size(0), self.block_size], self.noise_token_id)
905
+ draft_input_ids[:, 0] = input_ids
906
+ x = self.embed(draft_input_ids)
907
+ x = x.unsqueeze(2).repeat(1, 1, self.hc_mult, 1)
908
+ return x, main_x
909
+
910
+ def forward_head(self, x: torch.Tensor, input_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
911
+ assert self.head is not None
912
+ x = self.hc_head(x, self.hc_head_fn, self.hc_head_scale, self.hc_head_base)
913
+ logits = self.head(self.norm(x), full_logits=True)
914
+ output_ids = input_ids.new_empty(input_ids.size(0), self.block_size + 1)
915
+ output_ids[:, 0] = input_ids
916
+ markov_embeds = []
917
+ for i in range(self.block_size):
918
+ logits_bias, markov_embed = self.markov_head(output_ids[:, i])
919
+ logits[:, i].add_(logits_bias)
920
+ markov_embeds.append(markov_embed)
921
+ output_ids[:, i + 1] = sample(logits[:, i], self.temperature)
922
+ markov_embed = torch.stack(markov_embeds, dim=1)
923
+ confidence = self.confidence_head(x, markov_embed)
924
+ return output_ids, logits, confidence
925
+
926
+
927
+ class Transformer(nn.Module):
928
+ """Full DeepSeek-V4 model: embed -> HC-expand -> N blocks -> HC-head -> logits.
929
+ Sets global state (world_size, rank, default_dtype, scale_fmt, scale_dtype) in __init__."""
930
+ def __init__(self, args: ModelArgs):
931
+ global world_size, rank, default_dtype, scale_fmt, scale_dtype
932
+ world_size = dist.get_world_size() if dist.is_initialized() else 1
933
+ rank = dist.get_rank() if dist.is_initialized() else 0
934
+ default_dtype = torch.float8_e4m3fn if args.dtype == "fp8" else torch.bfloat16
935
+ scale_fmt = "ue8m0" if args.scale_dtype == "fp8" else args.scale_fmt
936
+ scale_dtype = torch.float8_e8m0fnu if args.scale_dtype == "fp8" else torch.float32
937
+ super().__init__()
938
+ self.max_seq_len = args.max_seq_len
939
+ self.temperature = args.temperature
940
+ self.norm_eps = args.norm_eps
941
+ self.hc_eps = args.hc_eps
942
+ self.embed = ParallelEmbedding(args.vocab_size, args.dim)
943
+ self.layers = torch.nn.ModuleList()
944
+ for layer_id in range(args.n_layers):
945
+ self.layers.append(Block(layer_id, args))
946
+ self.norm = RMSNorm(args.dim, self.norm_eps)
947
+ self.head = ParallelHead(args.vocab_size, args.dim, self.norm_eps, self.hc_eps)
948
+ self.mtp = torch.nn.ModuleList()
949
+ self.target_layer_ids = args.dspark_target_layer_ids
950
+ if args.dspark_block_size:
951
+ for layer_id in range(args.n_mtp_layers):
952
+ self.mtp.append(DSparkBlock(args.n_layers + layer_id, args))
953
+ self.mtp[-1].embed = self.embed
954
+ self.mtp[-1].head = self.head
955
+ self.hc_mult = hc_mult = args.hc_mult
956
+ hc_dim = hc_mult * args.dim
957
+ with set_dtype(torch.float32):
958
+ self.hc_head_fn = nn.Parameter(torch.empty(hc_mult, hc_dim))
959
+ self.hc_head_base = nn.Parameter(torch.empty(hc_mult))
960
+ self.hc_head_scale = nn.Parameter(torch.empty(1))
961
+ if args.vision_n_layers > 0:
962
+ self.vision = ViT(args)
963
+ self.aligner = Aligner(args)
964
+ self.image_start = nn.Parameter(torch.empty(args.dim))
965
+ self.image_end = nn.Parameter(torch.empty(args.dim))
966
+ self.image_newline = nn.Parameter(torch.empty(args.dim))
967
+ self.image_pad = nn.Parameter(torch.empty(args.dim))
968
+ self.vocab_size = args.vocab_size
969
+ self.max_image_tokens = args.vision_max_n_token
970
+
971
+ @torch.inference_mode()
972
+ def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
973
+ return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
974
+
975
+ def merge_image_embeddings(self, images, h):
976
+ """Encode each image and write its token block into h in place."""
977
+ device = h.device
978
+ params = torch.stack([self.image_start, self.image_pad, self.image_pad,
979
+ self.image_newline, self.image_end])
980
+ for i, sample in enumerate(images):
981
+ if sample is None:
982
+ continue
983
+ for img in sample:
984
+ embeds = self.encode_image(img.patches.to(device), img.n_vit_h, img.n_vit_w)[img.perm.to(device)]
985
+ types = img.types.to(device)
986
+ block = params[types]
987
+ block[types == IMAGE] = embeds
988
+ h[i, img.start:img.start + block.size(0)] = block
989
+
990
+ @torch.inference_mode()
991
+ def forward(self, input_ids: torch.Tensor, start_pos: int = 0, images=None):
992
+ h = self.embed(input_ids)
993
+ visible = None
994
+ if images is not None:
995
+ if start_pos == 0:
996
+ self.merge_image_embeddings(images, h)
997
+ visible = get_image_visible(input_ids, self.vocab_size, self.max_image_tokens)
998
+ else:
999
+ assert (input_ids < self.vocab_size).all(), "image spans must be prefilled in a single chunk"
1000
+ # Expand to hc_mult copies for Hyper-Connections
1001
+ h = h.unsqueeze(2).repeat(1, 1, self.hc_mult, 1)
1002
+ main_hiddens = []
1003
+ for i, layer in enumerate(self.layers):
1004
+ h = layer(h, start_pos, input_ids, visible)
1005
+ if i in self.target_layer_ids:
1006
+ main_hiddens.append(h.mean(dim=2))
1007
+ h = layer.hc_head(h, self.hc_head_fn, self.hc_head_scale, self.hc_head_base)
1008
+ logits = self.head(self.norm(h))
1009
+ output_ids = sample(logits, self.temperature)
1010
+ main_hidden = torch.cat(main_hiddens, dim=-1) if main_hiddens else None
1011
+ return output_ids, logits, main_hidden
1012
+
1013
+ @torch.inference_mode()
1014
+ def forward_spec(self, input_ids: torch.Tensor, main_hidden: torch.Tensor, start_pos: int = 0):
1015
+ h, main_x = self.mtp[0].forward_embed(main_hidden, input_ids)
1016
+ for layer in self.mtp:
1017
+ h = layer(h, start_pos, input_ids, main_x)
1018
+ if start_pos == 0:
1019
+ return
1020
+ output_ids, logits, confidence = self.mtp[-1].forward_head(h, input_ids)
1021
+ return output_ids, logits, confidence
1022
+
1023
+
1024
+ def sample(logits, temperature: float = 1.0):
1025
+ """Gumbel-max trick: equivalent to multinomial sampling but faster on GPU,
1026
+ since it avoids the GPU-to-CPU sync in torch.multinomial."""
1027
+ if temperature == 0:
1028
+ return logits.argmax(dim=-1)
1029
+ logits = logits / max(temperature, 1e-5)
1030
+ probs = torch.softmax(logits, dim=-1, dtype=torch.float32)
1031
+ return probs.div_(torch.empty_like(probs).exponential_(1)).argmax(dim=-1)
1032
+
1033
+
1034
+ if __name__ == "__main__":
1035
+ torch.set_default_dtype(torch.bfloat16)
1036
+ torch.set_default_device("cuda")
1037
+ torch.manual_seed(0)
1038
+ args = ModelArgs(n_hash_layers=0, dspark_block_size=6, dspark_target_layer_ids=(5, 6))
1039
+ x = torch.randint(0, args.vocab_size, (2, 150))
1040
+ model = Transformer(args)
1041
+
1042
+ output_ids, logits, main_hidden = model(x[:, :128])
1043
+ model.forward_spec(output_ids, main_hidden)
1044
+ for i in range(128, 150):
1045
+ output_ids, logits, main_hidden = model(x[:, i:i+1], i)
1046
+ output_ids, logits, confidence = model.forward_spec(output_ids, main_hidden, i)
inference/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch>=2.10.0
2
+ transformers>5
3
+ safetensors==0.7.0
4
+ fast_hadamard_transform
5
+ tilelang==0.1.8
6
+ numpy
7
+ Pillow
8
+ tqdm
inference/run.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")"
5
+
6
+ : "${CKPT_PATH:?Set CKPT_PATH to the converted tensor-parallel checkpoint directory}"
7
+
8
+ MP="${MP:-4}"
9
+ CONFIG="${CONFIG:-config.json}"
10
+ INPUT_FILE="${INPUT_FILE:-examples/example_vl.txt}"
11
+
12
+ torchrun --nproc-per-node "${MP}" generate.py \
13
+ --ckpt-path "${CKPT_PATH}" \
14
+ --config "${CONFIG}" \
15
+ --input-file "${INPUT_FILE}"
inference/vision.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+
3
+ import torch
4
+ import torch.nn.functional as F
5
+ from torch import nn
6
+
7
+
8
+ @lru_cache(8)
9
+ def get_vision_cos_sin(n_h: int, n_w: int, dim: int, theta: float):
10
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
11
+ hpos = torch.arange(n_h).unsqueeze(1).expand(n_h, n_w)
12
+ wpos = torch.arange(n_w).unsqueeze(0).expand(n_h, n_w)
13
+ freqs = torch.stack([hpos, wpos], dim=-1).reshape(-1, 2, 1).float() * inv_freq
14
+ freqs = freqs.flatten(1)
15
+ return freqs.cos().unsqueeze(1), freqs.sin().unsqueeze(1)
16
+
17
+
18
+ def apply_rotary(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
19
+ dtype = x.dtype
20
+ x1, x2 = x.float().chunk(2, dim=-1)
21
+ return torch.cat([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1).to(dtype)
22
+
23
+
24
+ class RMSNorm(nn.Module):
25
+ def __init__(self, dim: int, eps: float = 1e-6):
26
+ super().__init__()
27
+ self.eps = eps
28
+ self.weight = nn.Parameter(torch.ones(dim, dtype=torch.float32))
29
+
30
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
31
+ dtype = x.dtype
32
+ x = x.float()
33
+ x = x * torch.rsqrt(x.square().mean(-1, keepdim=True) + self.eps)
34
+ return (self.weight * x).to(dtype)
35
+
36
+
37
+ class PatchEmbed(nn.Module):
38
+ def __init__(self, args):
39
+ super().__init__()
40
+ self.proj = nn.Linear(3 * args.vision_patch_size ** 2, args.vision_dim)
41
+
42
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
43
+ return self.proj(x.flatten(1))
44
+
45
+
46
+ class Attention(nn.Module):
47
+ def __init__(self, args):
48
+ super().__init__()
49
+ self.n_heads = args.vision_n_heads
50
+ self.head_dim = args.vision_dim // args.vision_n_heads
51
+ self.wqkv = nn.Linear(args.vision_dim, 3 * args.vision_dim)
52
+ self.wo = nn.Linear(args.vision_dim, args.vision_dim)
53
+
54
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
55
+ n = x.size(0)
56
+ q, k, v = (t.view(n, self.n_heads, self.head_dim) for t in self.wqkv(x).chunk(3, dim=-1))
57
+ q = apply_rotary(q, cos, sin)
58
+ k = apply_rotary(k, cos, sin)
59
+ o = F.scaled_dot_product_attention(q.transpose(0, 1), k.transpose(0, 1), v.transpose(0, 1))
60
+ return self.wo(o.transpose(0, 1).reshape(n, -1))
61
+
62
+
63
+ class MLP(nn.Module):
64
+ def __init__(self, args):
65
+ super().__init__()
66
+ self.w1 = nn.Linear(args.vision_dim, 2 * args.vision_inter_dim, bias=False)
67
+ self.w2 = nn.Linear(args.vision_inter_dim, args.vision_dim, bias=False)
68
+
69
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
70
+ gate, up = self.w1(x).chunk(2, dim=-1)
71
+ return self.w2(F.silu(gate) * up)
72
+
73
+
74
+ class Block(nn.Module):
75
+ def __init__(self, args):
76
+ super().__init__()
77
+ self.norm1 = RMSNorm(args.vision_dim)
78
+ self.attn = Attention(args)
79
+ self.norm2 = RMSNorm(args.vision_dim)
80
+ self.mlp = MLP(args)
81
+
82
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
83
+ x = x + self.attn(self.norm1(x), cos, sin)
84
+ return x + self.mlp(self.norm2(x))
85
+
86
+
87
+ class ViT(nn.Module):
88
+ """DeepSeek ViT: full bidirectional attention over one image with 2D RoPE."""
89
+ def __init__(self, args):
90
+ super().__init__()
91
+ self.rope_dim = args.vision_dim // args.vision_n_heads // 2
92
+ self.rope_theta = args.vision_rope_theta
93
+ self.patch_embed = PatchEmbed(args)
94
+ self.blocks = nn.ModuleList([Block(args) for _ in range(args.vision_n_layers)])
95
+ self.norm = RMSNorm(args.vision_dim)
96
+
97
+ def forward(self, patches: torch.Tensor, n_h: int, n_w: int) -> torch.Tensor:
98
+ x = self.patch_embed(patches)
99
+ cos, sin = get_vision_cos_sin(n_h, n_w, self.rope_dim, self.rope_theta)
100
+ for block in self.blocks:
101
+ x = block(x, cos, sin)
102
+ return self.norm(x)
103
+
104
+
105
+ class Aligner(nn.Module):
106
+ def __init__(self, args):
107
+ super().__init__()
108
+ self.downsample_ratio = args.vision_downsample_ratio
109
+ in_dim = args.vision_dim * self.downsample_ratio ** 2
110
+ self.w1 = nn.Linear(in_dim, args.dim)
111
+ self.w2 = nn.Linear(args.dim, args.dim)
112
+
113
+ def forward(self, x: torch.Tensor, n_h: int, n_w: int) -> torch.Tensor:
114
+ r = self.downsample_ratio
115
+ x = x.view(n_h, n_w, -1).permute(2, 0, 1)
116
+ x = F.pad(x, (0, -n_w % r, 0, -n_h % r))
117
+ x = F.unfold(x.unsqueeze(0), r, stride=r).squeeze(0).transpose(0, 1)
118
+ return self.w2(F.gelu(self.w1(x)))
model-00001-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:367c971dc3cd6a042a9bec1caff508e77eabfaef1df2de3a827397ef8bbc6af3
3
+ size 1991876272
model-00002-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b7bca4e9f4ad1cbd7ae204ba8f452450473efe780ba5e539f9b478a7746ee913
3
+ size 3566323424
model-00003-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:533065ef725b1b85ea6bea54a4b791caa7ea37ca8bf40edb892f1df53d922852
3
+ size 3566323424
model-00004-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f621de245e17c1205fa971a1717c6908caba74eec0f65a8a328731581b2b9870
3
+ size 3596231504
model-00005-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5340e0f26d33480b6b05f030d67a36e812e3e02b60e4da50fc88b0de919775d7
3
+ size 3568770088
model-00006-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc738c546c382768ff054f8dc303579b47b69de4d38477df62f994881f34d672
3
+ size 3590025888
model-00007-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4174ad0eeca4800800415ebc5dc1b023ae0a741b18348070f9e1d92b9bb9ceef
3
+ size 3568770088
model-00008-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3bf01d8580f43cbfe0fbcb033bfb974fa10c214157578394499d3f79a19126c8
3
+ size 3590025888
model-00009-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b77733ec64677bbbb85be438d25bb7792f9c424df273293864b124d089c873a9
3
+ size 3568770088
model-00010-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7467107509c559f1f65e1aa2fb1e3e97b37bf225dcea1734f4b4bbfa9bf2bdc
3
+ size 3590025888
model-00011-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffd9e8592bfbe139541809a5364f2814dabb6f5a41f7a4c5bb24a08f279b3c32
3
+ size 3568770088
model-00012-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90addcbb6508a1cf44d448ea4a7f15cc5382d8e7f7424bb9e8f1f386d7872049
3
+ size 3590027464
model-00013-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6d119524ed0eaab993bf41b6291503c2fd8f2ad9a5523ea2c439cf4a920c4f1
3
+ size 3568771664
model-00014-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b618871b69795bb7f3aede26e1b33689eb7d09793051021a608cfb336b23f689
3
+ size 3590027464
model-00015-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7d7aaa0e8a3badeeb2f3fea217fd8344901f553b4c234db77318fd9705b7dd8
3
+ size 3568771664
model-00016-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2db0bdaee8f745830a96c1d496e89b5d840490b2831e74e14fb0fd643ef3cef9
3
+ size 3590027464
model-00017-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58b4e24235faa2c13a01e0ae548618cec12e3ec04aed28137213362e5219094e
3
+ size 3568771664
model-00018-of-00048.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ee28f2e7e481b13abe84a9926f9a99d0d6b35df6e8b8e72c2de43207d4decc05
3
+ size 3590027464