Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7976d36
deepseek41: gguf constants, arch enums, tensor/key contract
danielhanchen Oct 17, 2026
baa94f5
ggml : add MXFP8 (E4M3 + E8M0) block type
danielhanchen Oct 17, 2026
ae08e81
convert : add DeepSeek-V4.1 (deepseek41) converter
danielhanchen Oct 17, 2026
3766bbd
convert : deepseek41 stream MXFP4 experts through a thread pool
danielhanchen Oct 17, 2026
bae963f
deepseek41: model graph (Engram, single-pass mHC, ratio 1/2 compresso…
danielhanchen Oct 17, 2026
f79bf19
convert : deepseek41 read weight and scale from their own shards
danielhanchen Oct 17, 2026
d29889d
deepseek41: test-llama-archs config, saver keys, meta split, engram t…
danielhanchen Oct 17, 2026
0c1b44d
deepseek41: builder A status and full-model handoff notes
danielhanchen Oct 17, 2026
7e34c44
deepseek41: tag l_out for parity tooling
danielhanchen Oct 17, 2026
445d59c
convert : deepseek41 dense type follows --outtype, router kept F32
danielhanchen Oct 17, 2026
6438476
deepseek41: TENSORS.md F32 pins
danielhanchen Oct 17, 2026
5813b51
Merge branch 'dsv41' into dsv41-mxfp8
danielhanchen Oct 17, 2026
70e2170
ggml : MXFP8 is gather-only; convert : deepseek41 writes engram.offse…
danielhanchen Oct 17, 2026
a773f63
convert : deepseek41 --outtype f32 keeps dense F32, Engram keys optional
danielhanchen Oct 17, 2026
91b8b86
deepseek41: STATUS_A porting-workflow A-step record (A1, A2, A4)
danielhanchen Oct 17, 2026
2f4055f
deepseek41: serialize Engram token history in KV state; router weight…
danielhanchen Oct 17, 2026
54ec0d2
deepseek41: tag per-layer selected top-k (sel_top_k) for selection pa…
danielhanchen Oct 17, 2026
1d51c09
chat: DeepSeek-V4.1 template detection and DSML tool parser (leading-…
danielhanchen Oct 17, 2026
eeba836
deepseek41: STATUS_A critique fold-in (boundary fixture, precision at…
danielhanchen Oct 17, 2026
8e3b617
deepseek41: STATUS_A gate on the shared reference fixtures (F32 exper…
danielhanchen Oct 17, 2026
d7e3835
deepseek41: STATUS_A CUDA gate and full-model dump handoff
danielhanchen Oct 17, 2026
c9786d7
deepseek41: STATUS_A flash-attention attribution, multi-sequence and …
danielhanchen Oct 17, 2026
a60112c
deepseek41: STATUS_A selection-set gate vs shared traces
danielhanchen Oct 17, 2026
9f13d35
deepseek41: STATUS_A first full-model results
danielhanchen Oct 17, 2026
3fd0f27
deepseek41: keep Engram tables in host memory under tensor overrides …
danielhanchen Oct 17, 2026
25c0ac3
deepseek41: STATUS_A correct the stale MXFP4 fixture number
danielhanchen Oct 17, 2026
a574da3
deepseek41: STATUS_A full-model dump phases and batch-split attribution
danielhanchen Oct 17, 2026
0e6a85d
Merge branch 'dsv41-mxfp8' into dsv41
danielhanchen Oct 17, 2026
47dd980
models/templates: drop duplicate DeepSeek-V4.1 template (identical to…
danielhanchen Oct 17, 2026
8f6d7b6
deepseek41: Engram gate off for embedding (image) tokens, NULL token …
danielhanchen Oct 17, 2026
2f73c68
deepseek41: drop working notes and the F32-exact debug knob from the …
danielhanchen Oct 17, 2026
cf7d735
Fix merge conflicts
danielhanchen Oct 17, 2026
cd1714a
models/templates: DeepSeek-V4.1 template namespace attribute list -> …
danielhanchen Oct 17, 2026
0d87f1d
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 17, 2026
1c9a39a
deepseek41: DSpark speculative draft (DFLASH single-pass mHC variant,…
danielhanchen Oct 17, 2026
70b2dae
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 17, 2026
7a58283
deepseek41: STATUS_A A2 results (CPU vs MATCHED, A5 pin sim, DSpark)
danielhanchen Oct 17, 2026
da47a97
ggml: MXFP8 -> fork-local type id 67 (42..63 reserved for upstream), …
danielhanchen Oct 17, 2026
f8cfdae
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 17, 2026
71752e6
Fix merge conflicts
danielhanchen Oct 17, 2026
3ac687a
deepseek41: STATUS_A MXFP8 67, header patcher, pin after #61, real DS…
danielhanchen Oct 17, 2026
70c3c18
deepseek41: STATUS_A full-model selection sets vs MATCHED
danielhanchen Oct 17, 2026
f947deb
deepseek41: STATUS_A full-model gate against the official MATCHED oracle
danielhanchen Oct 17, 2026
9d110cc
deepseek41: keep F32-weight matmuls off TF32 on CUDA (ubatch-dependen…
danielhanchen Oct 17, 2026
0d25f72
deepseek41: STATUS_A selection-trace layout, DSpark identity check
danielhanchen Oct 17, 2026
44d2f26
ggml-cuda: deterministic tie selection in the radix top-k
danielhanchen Oct 17, 2026
6b00e25
chat: DeepSeek V4.1 keeps non-OpenAI message keys and media block sep…
danielhanchen Oct 17, 2026
a9df79a
chat: DeepSeek V4.1 maps OpenAI reasoning_effort values, parses syste…
danielhanchen Oct 17, 2026
07d0fa0
deepseek41: STATUS_A CUDA matmul kernel check
danielhanchen Oct 18, 2026
d0ff0cc
deepseek41: tokens shared by several sequences (llama-batched) instea…
danielhanchen Oct 18, 2026
db044c8
deepseek41: embedding contexts return the flat HC state (n_embd_out w…
danielhanchen Oct 18, 2026
094024b
llama-context: re-reserve the scheduler on every embeddings toggle
danielhanchen Oct 18, 2026
7704fce
chat: DeepSeek V4/V4.1 thinking-mode content continuation consumes th…
danielhanchen Oct 18, 2026
4ebca5e
deepseek41: skip the F32-precision matmul flag for quantized hc_fn, r…
danielhanchen Oct 18, 2026
ee0bedb
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 18, 2026
d6338d1
Merge branch 'iq1-narrow' (with b11707) into dsv41-pin
danielhanchen Oct 18, 2026
58ab94b
Fix merge conflicts
danielhanchen Oct 18, 2026
328c85a
ggml-cuda: add the GGML_PREC_F32 matmul path without moving the cc / …
danielhanchen Oct 18, 2026
e266b19
Merge branch 'iq1-narrow' (with b11715) into dsv41-pin
danielhanchen Oct 18, 2026
6172bb1
ggml-cuda: radix top-k writes its indices in a reproducible order
danielhanchen Oct 18, 2026
e3657ee
Merge branch 'dsv41-rob' into dsv41 (runtime robustness fixes)
danielhanchen Oct 18, 2026
61c6e87
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 18, 2026
4a314d3
Merge branch 'dsv41' into dsv41-pin
danielhanchen Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 87 additions & 4 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,41 @@ std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const json & messa
if (message.contains("tool_call_id")) {
msg.tool_call_id = message.at("tool_call_id");
}
{
json extra = json::object();
for (const auto & [k, v] : message.items()) {
if (k != "role" && k != "content" && k != "tool_calls" && k != "reasoning_content" &&
k != "name" && k != "tool_call_id") {
extra[k] = v;
}
}
if (has_tool_calls) {
// per tool call: keys besides type/function/id, and function keys besides name/arguments
json tc_extra = json::array();
bool any = false;
for (const auto & tool_call : message.at("tool_calls")) {
json e = json::object();
for (const auto & [k, v] : tool_call.items()) {
if (k != "type" && k != "function" && k != "id") {
e[k] = v;
}
}
for (const auto & [k, v] : tool_call.at("function").items()) {
if (k != "name" && k != "arguments") {
e["function"][k] = v;
}
}
any = any || !e.empty();
tc_extra.push_back(e);
}
if (any) {
extra["__tool_call_extras"] = tc_extra;
}
}
if (!extra.empty()) {
msg.extra_json = extra.dump();
}
}

msgs.push_back(msg);
}
Expand Down Expand Up @@ -516,6 +551,51 @@ static json render_message_to_json(const std::vector<common_chat_msg> & msgs, co
return messages_inp_normalizer(c).normalize(messages);
}

// DeepSeek V4.1: its template reads non-OpenAI message keys ("task", per-message "tools", tool call
// "namespace"), and joins content blocks with "\n\n" around images, so keep media markers as typed text
// parts instead of the default glued string and restore the extra keys.
static json render_message_to_json_deepseek_v4_1(const std::vector<common_chat_msg> & msgs, const jinja::caps & c) {
json messages = json::array();
for (const auto & msg : msgs) {
json jmsg;
if (msg.contains_media() && msg.content.empty()) {
common_chat_msg typed = msg;
for (auto & part : typed.content_parts) {
part.type = "text";
}
jmsg = typed.to_json_oaicompat(/* concat_typed_text= */ false);
} else {
jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false);
}
if (!msg.extra_json.empty()) {
const json extra = json::parse(msg.extra_json);
for (const auto & [k, v] : extra.items()) {
if (k == "__tool_call_extras") {
if (!jmsg.contains("tool_calls")) {
continue;
}
auto & tcs = jmsg.at("tool_calls");
for (size_t i = 0; i < tcs.size() && i < v.size(); i++) {
for (const auto & [tk, tv] : v[i].items()) {
if (tk == "function") {
for (const auto & [fk, fv] : tv.items()) {
tcs[i]["function"][fk] = fv;
}
} else {
tcs[i][tk] = tv;
}
}
}
} else if (!jmsg.contains(k)) {
jmsg[k] = v;
}
}
}
messages.push_back(std::move(jmsg));
}
return messages_inp_normalizer(c).normalize(messages);
}

// DEPRECATED: only used in tests
json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msgs, bool concat_typed_text) {
jinja::caps c;
Expand Down Expand Up @@ -1208,11 +1288,12 @@ static common_chat_params_init_fn common_chat_template_detect_params_init(const
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
// The template source contains the token as a variable assignment, not as a literal in markup.
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
if (src.find("dsml_token") != std::string::npos &&
src.find("DSML") != std::string::npos &&
// V4.1 names it " calls" (leading-space tag names) and its template may not name the dsml_token variable.
if (((src.find("dsml_token") != std::string::npos && src.find("DSML") != std::string::npos) ||
src.find("|DSML| calls>") != std::string::npos) &&
(src.find("function_calls") != std::string::npos ||
src.find("tool_calls") != std::string::npos)) {
LOG_DBG("Using specialized template: DeepSeek V3.2/V4\n");
LOG_DBG("Using specialized template: DeepSeek V3.2/V4/V4.1\n");
return common_chat_params_init_deepseek_v3_2;
}

Expand Down Expand Up @@ -1313,7 +1394,9 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_
workaround::trim_all_content(trimmed_messages);
messages_to_render = &trimmed_messages;
}
params.messages = render_message_to_json(*messages_to_render, tmpl.original_caps());
params.messages = src.find("|DSML| calls>") != std::string::npos ?
render_message_to_json_deepseek_v4_1(*messages_to_render, tmpl.original_caps()) :
render_message_to_json(*messages_to_render, tmpl.original_caps());
params.tool_choice = inputs.tool_choice;
params.reasoning_format = inputs.reasoning_format;
params.enable_thinking = inputs.enable_thinking;
Expand Down
3 changes: 3 additions & 0 deletions common/chat.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ struct common_chat_msg {
std::string reasoning_content;
std::string tool_name;
std::string tool_call_id;
// Message keys the OpenAI fields above do not cover (e.g. "task", per-message "tools", tool call
// "namespace"), kept as a JSON object string. Only templates that read them get them (DeepSeek V4.1).
std::string extra_json;

common_json to_json_oaicompat(bool concat_typed_text = false) const;

Expand Down
66 changes: 56 additions & 10 deletions common/parsers/deepseek.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "parsers.h"
#include "log.h"

// The DeepSeek V4 reference implementation renders consecutive tool results into a single
// user block, ordered by the tool call order of the preceding assistant message (matched
Expand Down Expand Up @@ -75,13 +76,31 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
// non-thinking generation prompt ends with a bare </think> instead of an empty
// <think></think> pair.
const bool is_v4 = tmpl.source().find("function_calls") == std::string::npos;
// V4.1 keeps the V4 behaviour but its DSML tag names carry a leading space:
// <|DSML| calls>, <|DSML| invoke name="...">, <|DSML| parameter name="..." string="...">.
const bool is_v41 = is_v4 && tmpl.source().find("|DSML| calls>") != std::string::npos;

std::optional<json> adjusted_messages;
if (is_v4) {
adjusted_messages = deepseek_v4_sort_tool_results(inputs.messages);
}

auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
// V4.1 can declare tools on a system message instead of top-level "tools": parse calls to those too.
json tools = inputs.tools;
if (is_v41 && !(tools.is_array() && !tools.empty()) && inputs.messages.is_array()) {
for (const auto & msg : inputs.messages) {
if (msg.value("role", "") == "system" && msg.contains("tools") && msg.at("tools").is_array()) {
if (!tools.is_array()) {
tools = json::array();
}
for (const auto & tool : msg.at("tools")) {
tools.push_back(tool);
}
}
}
}

auto has_tools = tools.is_array() && !tools.empty();
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
Expand All @@ -90,17 +109,40 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
if (is_v4 && has_response_format) {
additional_context = json{ { "response_format", inputs.json_schema } };
}
// V4.1 accepts low/high/max or 1-100 and raises on anything else: map the remaining OpenAI values
// (minimal -> low, xhigh -> max, medium -> unset = template default) instead of failing the request.
if (is_v41 && inputs.extra_context.is_object() && inputs.extra_context.contains("reasoning_effort") &&
inputs.extra_context.at("reasoning_effort").is_string()) {
const std::string effort = inputs.extra_context.at("reasoning_effort").get<std::string>();
json mapped;
if (effort == "minimal") {
mapped = "low";
} else if (effort == "xhigh") {
mapped = "max";
} else if (effort == "medium") {
mapped = nullptr;
}
if (effort == "minimal" || effort == "xhigh" || effort == "medium") {
LOG_DBG("%s: DeepSeek V4.1 reasoning_effort '%s' -> %s\n", __func__, effort.c_str(),
mapped.is_null() ? "unset (template default)" : mapped.get<std::string>().c_str());
if (!additional_context.has_value()) {
additional_context = json::object();
}
(*additional_context)["reasoning_effort"] = mapped;
}
}

const std::string DSML = "|DSML|";
const std::string THINK_START = "<think>";
const std::string THINK_END = "</think>";
const std::string TC_BLOCK = is_v4 ? "tool_calls" : "function_calls";
const std::string FC_START = "<" + DSML + TC_BLOCK + ">";
const std::string FC_END = "</" + DSML + TC_BLOCK + ">";
const std::string INVOKE_START = "<" + DSML + "invoke";
const std::string INVOKE_END = "</" + DSML + "invoke>";
const std::string PARAM_START = "<" + DSML + "parameter";
const std::string PARAM_END = "</" + DSML + "parameter>";
const std::string TAG_SP = is_v41 ? " " : "";
const std::string TC_BLOCK = is_v41 ? "calls" : is_v4 ? "tool_calls" : "function_calls";
const std::string FC_START = "<" + DSML + TAG_SP + TC_BLOCK + ">";
const std::string FC_END = "</" + DSML + TAG_SP + TC_BLOCK + ">";
const std::string INVOKE_START = "<" + DSML + TAG_SP + "invoke";
const std::string INVOKE_END = "</" + DSML + TAG_SP + "invoke>";
const std::string PARAM_START = "<" + DSML + TAG_SP + "parameter";
const std::string PARAM_END = "</" + DSML + TAG_SP + "parameter>";
const std::string GEN_PROMPT = "<|Assistant|>";
const std::string TC_SEPARATOR = "\n\n";

Expand Down Expand Up @@ -152,7 +194,7 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
// build tool call section first since we might need it in reasoning
auto tool_choice = p.choice();
if (has_tool_calls) {
foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) {
foreach_function(tools, [&](size_t tool_index, const json & tool) {
const auto & function = tool.at("function");
std::string name = function.at("name");

Expand Down Expand Up @@ -223,7 +265,11 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
}

if (extract_reasoning && inputs.enable_thinking) {
reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
// V4 renders a content continuation without reasoning as "<|Assistant|></think>" + content even in
// thinking mode (generation_prompt above), so the bare THINK_END must be consumed here as well, or it
// leaks into the final message content (the streamed deltas, which start after the prefill, omit it)
reasoning = is_v4 ? p.optional((THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END) | p.literal(THINK_END))
: p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
reasoning_with_tc = THINK_START +
p.reasoning(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START, THINK_END })) +
p.space() + obligatory_tool_calls;
Expand Down
1 change: 1 addition & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"LingDSparkModel": "qwen",
"DeepseekV4ForCausalLM": "deepseek",
"DeepseekV4DSparkModel": "deepseek",
"DeepseekV41ForCausalLM": "deepseek41",
"DistilBertForMaskedLM": "bert",
"DistilBertForSequenceClassification": "bert",
"DistilBertModel": "bert",
Expand Down
Loading