Appearance
端到端推理流程
从一个 HTTP 请求到达服务器,到 SSE 流式输出最后一个 token,完整追踪一次推理请求的全链路。每个阶段标注对应的 学习笔记 以便深入阅读。
总览
| 阶段 | 核心函数 | 主要文件 | Day |
|---|---|---|---|
| HTTP 接收 | main() → client_main() | ds4_server.c | Day 12 |
| Prompt 渲染 | render_chat_prompt_text() | ds4_server.c | Day 10 |
| 分词 | ds4_tokenize_rendered_chat() | ds4.c | Day 5 |
| Prefill | ds4_session_sync() → prefill_layer_major_cpu() | ds4.c | Day 9 |
| Decode 循环 | layer_forward_raw_swa_one() × 43 | ds4.c | Day 7 / Day 8 |
| SSE 流式输出 | sse_chunk() | ds4_server.c | Day 12 |
| Tool Calling | parse_function_call() | ds4_server.c | Day 10 |
阶段一:HTTP 请求接收
入口:main()(ds4_server.c 约 8089 行)创建监听 socket,循环 accept() 接受连接。每连接启动一个线程运行 client_main()(ds4_server.c 约 7678 行)。
请求解析:parse_chat_request()(ds4_server.c 约 1984 行)从 JSON body 中提取:
messages数组 → 对话历史temperature、top_p等采样参数stream: true→ 启用 SSE 流式tools数组 → Tool Calling 定义
Context Length 检查:request_exceeds_context()(ds4_server.c 约 4680 行)在入队前检查 prompt tokens 是否 >= context size。超限时 http_error_context_length_exceeded() 返回 400 错误,包含 n_prompt_tokens 和 n_ctx,让客户端精确了解差距。此检查在入队之前执行,避免浪费 worker 线程时间。错误格式根据 API 类型自动适配(OpenAI 用 code: "context_length_exceeded",Anthropic 用 type: "error")。
c
// `parse_chat_request()` (ds4_server.c 约 1984 行)
static int parse_chat_request(json_t *root, struct ds4_chat_request *req) {
req->messages = json_object_get(root, "messages");
req->stream = json_boolean_value(json_object_get(root, "stream"));
req->temperature = json_real_value_or(json_object_get(root, "temperature"), 0.0);
// ...
}解析完成后构造 job 推入队列,Worker 线程(线程池)取出执行。
阶段二:Prompt 渲染与分词
Prompt 渲染
render_chat_prompt_text()(ds4_server.c 约 1901 行)将 messages 数组拼成模型期望的格式:
<|begin▁of▁sentence|><|User|>你好<|Assistant|>关键步骤:
- 插入 BOS token(
<|begin▁of▁sentence|>) - 为每条消息包裹角色标记(
<|User|>/<|Assistant|>) - 系统 prompt 插入最前
- 最后追加
<|Assistant|>引导模型开始生成
分词
ds4_tokenize_rendered_chat()(ds4.c 约 14716 行)将渲染后的文本切成 token ID 序列:
c
// `ds4_tokenize_rendered_chat()` (ds4.c 约 14716 行)
int ds4_tokenize_rendered_chat(struct ds4_context *ctx,
const char *text,
int32_t *tokens,
int32_t n_max_tokens) {
// 1. BPE 分词:按学到的合并规则从长到短匹配
// 2. Special token 优先匹配(<|User|> 等是单独 token)
// 3. 输出 token ID 数组
}BPE 分词器按词表中 129280 个 token 进行最长匹配。高频组合(如"喜欢")是单个 token,低频组合被拆成多个。详见 Day 5 — 采样策略 中的分词部分。
阶段三:Prefill 预填充
用户发来的是完整对话历史,模型需要一次性"消化"所有 context token。这就是 Prefill 阶段。
KV Cache 前缀匹配
ds4_session_sync()(ds4.c 约 17201 行)先检查 KV Cache 是否有可复用的前缀:
多轮对话中,前几轮的 KV 已缓存,只需 prefill 用户新输入。这大幅减少重复计算。
逐层批处理
prefill_layer_major_cpu()(ds4.c 约 7674 行)以层为主序处理所有 prefill token:
token 批次 [t0, t1, ..., tn]
│
├─ Layer 0: Embedding → RoPE → Attention → MoE → 残差
├─ Layer 1: ...
├─ ...
└─ Layer 42: ...层为主序意味着同一层的权重在 cache 中只加载一次,对所有 token 做完计算再换下一层。这比"token 为主序"(每个 token 穿过所有层)的缓存友好度高得多。
Prefill 阶段结束后,所有 context token 的 K/V 都已写入 KV Cache,后续 decode 只需处理新生成的一个 token。
阶段四:Decode 循环
这是推理的核心——逐 token 自回归生成。每一步的完整流程:
4.1 Embedding + HC 展开
c
// `embed_token_f16()` (ds4.c 约 2684 行) — Embedding 查表
const uint16_t *row = base + (uint64_t)token * stride;
for (uint64_t i = 0; i < stride; i++)
out[i] = f16_to_f32(row[i]);Token ID 通过查表变成 4096 维向量。HC(Head Chunk)将这个向量复制到 43 层各自的计算缓冲区,避免层间数据依赖时的内存冲突。
4.2 单层前向
每层执行两次"归一化 → 计算 → 残差":
# 第一次:注意力
x_norm = RMSNorm(x)
Q, K, V = x_norm @ W_qkv # LoRA 低秩分解
Q, K = RoPE(Q, K, position) # 编码位置
KVCache.append(K, V) # 缓存
attn = Attention(Q, KVCache) # 64 头 MQA, 窗口 128
x = x + attn # 残差
# 第二次:MoE
x_norm = RMSNorm(x)
scores = x_norm @ W_router # 256 个专家分数
top6 = TopK(scores, 6) # 选 6 个
expert_out = Σ(top6_i(x_norm)) + shared(x_norm) # 加权 + 共享
x = x + expert_out # 残差43 层层层堆叠,每层在输入上"批注"新信息(残差连接保证原始信息不丢失)。
4.3 采样
最后一层输出经 lm_head 投影为 129280 个 logits,然后:
c
// `sample_full_vocab()` (ds4.c 约 14934 行) — 轮盘赌采样
// 1. Temperature 缩放
for (int i = 0; i < n; i++) logits[i] /= temperature;
// 2. Top-P 过滤(`sample_top_p_min_p()` ds4.c 约 15023 行)
// 按概率降序排列,累加到 p 后丢弃剩余
// 3. 采样
float rnd = sample_rng_f32(rng) * sum;
for (int i = 0; i < DS4_N_VOCAB; i++) {
rnd -= probs[i];
if (rnd <= 0.0f) return i;
}采样策略详见 Day 5。Temperature 越低输出越确定(趋近 argmax),越高越随机。
阶段五:SSE 流式输出
每生成一个 token,sse_chunk()(ds4_server.c 约 3119 行)立即推送给客户端:
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"你"},"index":0}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"好"},"index":0}]}
data: [DONE]UTF-8 安全处理
中文字符占 3 个 UTF-8 字节,可能跨越多次 recv。sse_chunk() 内部维护 UTF-8 解码状态:
c
// `sse_chunk()` (ds4_server.c 约 3119 行)
// 如果 token 的 UTF-8 编码不完整(字节被截断)
// 缓存已接收字节,等下一个 token 到来后拼完整再输出这保证客户端收到的每个 SSE chunk 都是合法的 UTF-8 字符串,不会出现乱码。
SSE 格式兼容 OpenAI API 兼容,任何支持 OpenAI SDK 的客户端都能直接对接。
阶段六:Tool Calling
当模型决定调用工具时,生成特殊格式的 token 序列:
xml
<tool_calls>
{"name": "get_weather", "arguments": {"city": "北京"}}
</tool_calls>DSML 解码状态机
模型输出的 token 序列需要经过 DSML(DeepSeek Markup Language)解码才能还原为结构化数据:
parse_function_call()(ds4_server.c 约 966 行)解析<tool_calls>中的 JSONparse_tool_calls_value()(ds4_server.c 约 1014 行)处理参数数组append_dsml_text_escaped()(ds4_server.c 约 1768 行)处理 DSML 转义
温度覆盖
Tool Calling 模式下,temperature 会被覆盖为 0(argmax),确保工具调用的格式稳定不走样。模型需要在精确的结构化输出和自由文本生成之间切换。
阶段七:Live Tool Continuation(工具调用快速续接)
上游新增(2025-05-15):工具调用后不再重建整个上下文,而是直接在 live KV 状态上追加后缀。详见
misc/RESPONSE_API.md和misc/ANTHROPIC_LIVE_CONTINUATION.md。
问题
工具调用前的实现:模型采样完工具调用后,服务器尝试"规范化"(canonicalize)live KV 检查点,让它与下一请求的完整 prompt 精确匹配。这导致:
text
tool checkpoint canonicalization needs rebuild ... common=16846 live=16994 canonical=16937
tool checkpoint canonicalized ... via=rebuild百万 token 上下文下,这个重建就是一次近乎完整的 prefill,耗时 10 秒以上。
核心思想
采样的 KV 状态是最高保真度的状态。 客户端可见的协议对象只应"选择"这个状态,而不是强迫服务器重建它。
两种协议的绑定方式
| 协议 | 绑定 ID | 端点 | 用途 |
|---|---|---|---|
| OpenAI Responses | call_id(function_call_output) | /v1/responses | Codex CLI 等 Agent 工具 |
| Anthropic Messages | tool_use_id(tool_result) | /v1/messages | Claude Code 等 |
两种协议共享同一套 live KV 续接逻辑:ID 匹配 → 跳过 prefill → 只 tokenize 后缀。
后缀 Token 序列
快速路径只 tokenize 并追加极少 token:
text
<|end▁of▁sentence|><|User|><tool_result>...<|Assistant|><think-or-/think>避免了整个上下文的重建。
函数索引表
| 阶段 | 函数 | 文件 | 位置 |
|---|---|---|---|
| HTTP 入口 | main() | ds4_server.c | 约 8089 行 |
| 连接处理 | client_main() | ds4_server.c | 约 7678 行 |
| 请求解析 | parse_chat_request() | ds4_server.c | 约 1984 行 |
| Context 检查 | request_exceeds_context() | ds4_server.c | 约 4680 行 |
| Context 错误 | http_error_context_length_exceeded() | ds4_server.c | 约 4686 行 |
| Prompt 渲染 | render_chat_prompt_text() | ds4_server.c | 约 1901 行 |
| 分词 | ds4_tokenize_rendered_chat() | ds4.c | 约 14716 行 |
| Session 同步 | ds4_session_sync() | ds4.c | 约 17201 行 |
| Prefill | prefill_layer_major_cpu() | ds4.c | 约 7674 行 |
| Embedding | embed_token_f16() | ds4.c | 约 2684 行 |
| RoPE | rope_tail_ext_inplace() | ds4.c | 约 4694 行 |
| KV Cache | kv_cache_push_raw() | ds4.c | 约 6307 行 |
| 注意力 | layer_attention_rows_one() | ds4.c | 约 4897 行 |
| MoE Router | layer_router_probs_one() | ds4.c | 约 5169 行 |
| Top-K | topk_desc() | ds4.c | 约 5211 行 |
| 单层前向 | layer_forward_raw_swa_one() | ds4.c | 约 7438 行 |
| 采样 | sample_full_vocab() | ds4.c | 约 14934 行 |
| Top-P/Min-P | sample_top_p_min_p() | ds4.c | 约 15023 行 |
| Argmax | sample_argmax() | ds4.c | 约 14874 行 |
| SSE 输出 | sse_chunk() | ds4_server.c | 约 3119 行 |
| Tool 解析 | parse_function_call() | ds4_server.c | 约 966 行 |
| Tool 参数 | parse_tool_calls_value() | ds4_server.c | 约 1014 行 |
| DSML 转义 | append_dsml_text_escaped() | ds4_server.c | 约 1768 行 |
| DSML 词表 | dsml_id 初始化 | ds4.c | 约 14621 行 |