第 12 章 结果处理与 Detokenizer
本章回答:采样出的 token id 如何变成流式文本?增量反分词解决什么问题?stop 字符串、finish_reason 在哪里处理?overlap 造成的重复输出如何防住?
12.1 三段输出链
输出侧经过三个处理站,与第 5 章的输入链对称:
- Scheduler 内:
process_batch_result(srt/managers/scheduler.py:4765)分派到batch_result_processor.process_batch_result_decode/prefill(srt/managers/scheduler_components/batch_result_processor.py:921),把 token id 写进req.output_ids、判定完成状态、触发 KV 写回; - 流式化:
output_streamer.stream_output(srt/managers/scheduler_components/output_streamer.py:119)按stream_interval攒批,组装BatchTokenIDOutput发往 Detokenizer; - Detokenizer 进程:增量反分词成文本,
BatchStrOutput回流 TokenizerManager(第 5 章handle_loop)。
flowchart LR
S1["process_batch_result<br/>(scheduler.py:4765)"] --> S2["output_streamer<br/>(output_streamer.py:119)"]
S2 -->|"ZMQ BatchTokenIDOutput"| D["DetokenizerManager<br/>(detokenizer_manager.py:179)"]
D -->|"ZMQ BatchStrOutput"| T["TokenizerManager<br/>_handle_batch_output"]
T --> HTTP["HTTP 层 yield"]12.2 process_batch_result:账本结算
decode 路径的结算逻辑(batch_result_processor.py:921 起)按序处理:
result.copy_done.synchronize():等 GPU→CPU 拷贝事件(overlap 下该拷贝与前向流并行);- 逐请求写
next_token_id进req.output_ids,检查 EOS/stop token/长度上限,落finished_reason; - overlap 分支特别防御(
:994附近):if (self.enable_overlap ...) and (req.finished() or req.is_retracted): continue——注释原文解释:overlap 下一个请求可能多输出一次并在这里命中两次,这个 continue 防止 dummy 输出; - 完成/中途均调用
maybe_cache_unfinished_req(:383)把 KV 写回 radix 树(第 9 章)。
prefill 路径(process_batch_result_prefill)多做一件事:从 logits 中提取 return_logprob 需要的 prompt 部分 logprobs,然后同样走流式化。
12.3 stream_output:攒批与切片
srt/managers/scheduler_components/output_streamer.py:119 的 stream_output 用 _GenerationStreamAccumulator 收集本批要外发的片段。频率由两层间隔控制(output_streamer.py 构造参数):stream_interval(默认 1,srt/arg_groups/fields/serving.py:255——按 token 长度的流式间隔)与 DEFAULT_FORCE_STREAM_INTERVAL(无论快慢,攒到阈值必须外发一次,防止长输出请求一直不发流)。聚合后 send_to_detokenizer.send_output(payload) 发出,或直推 Rust server(self.rust_server.push_generation(payload),:214 分支——Rust server 模式下 detokenizer 由 Rust 侧接管)。
12.4 增量反分词
DetokenizerManager(srt/managers/detokenizer_manager.py:102)的事件循环极简(:179):
# srt/managers/detokenizer_manager.py:179(节选)
def event_loop(self):
"""The event loop that handles requests"""
while True:
with self.soft_watchdog.disable():
recv_obj = sock_recv(self.recv_from_scheduler)
output = self._request_dispatcher(recv_obj)
if output is not None:
sock_send(self.send_to_tokenizer, output)
self.soft_watchdog.feed()反分词的核心难点:token 边界 ≠ 字符边界。一个 UTF-8 字符可能被切成多个 token,新 token 到达前无法确定上一段文本是否已经「封口」。detokenizer_manager.py:79 起的 DecodeStatus 结构用 decode_ids / read_offset / surr_offset 维护每个请求的解码游标;_decode_batch_token_id_output(:303)按游标解码,把「确定段」发出、「疑似尾部」留待下批。trim_matched_stop(:189)在文本/整流上做 stop 字符串裁剪:匹配到 stop 串时按 no_stop_trim 决定保留还是剔除,stop token(int 型)则直接去掉最后一个 token。
vocab 越界防御:_clamp_decode_ids(:226)把超过 vocab_size 的 id 钳到合法范围,防御性处理越界 id 的崩溃。
12.5 完整回程时序
sequenceDiagram
participant MR as ModelRunner
participant S as Scheduler
participant DS as Detokenizer
participant TM as TokenizerManager
MR-->>S: next_token_ids(GPU→CPU)
S->>S: process_batch_result:output_ids/finish/KV 写回
S->>DS: BatchTokenIDOutput(按 stream_interval)
DS->>DS: 增量反分词 + trim stop
DS->>TM: BatchStrOutput
TM->>TM: meta_info 组装(cached_tokens、finish_reason…)
TM-->>TM: 唤醒 generate_request 的 yield12.6 输出侧的失败边界
- 重复输出:overlap 双发由
process_batch_result_decode的 finished 检查拦截(12.2); - 请求被 retract 后又完成:
req.is_retracted与 finished 一并检查,避免收回请求吃进脏 token; - TM 侧状态已删:
_handle_batch_output中rid查不到状态时警告并丢弃(srt/managers/tokenizer_manager.py:2369),health check 请求有专门的静默分支——源码注释承认这是已知竞态; - Detokenizer 卡死:事件循环的
soft_watchdog检测消息流停滞(与第 5 章 TM 的狗对称)。
12.7 本章收束
- 输出链三站:结算(token → output_ids + finish + KV 写回)、攒批(stream_interval + 强制间隔)、反分词(游标式增量解码)。
- 增量反分词的状态(decode_ids/read_offset/surr_offset)是「token 与字符边界不对齐」这一问题的最小解。
- overlap 的输出侧代价是重复输出风险,用 finished 检查显式防住;这与第 6 章的张量保活、第 11 章的延迟采样构成 overlap 代价的完整清单。
- 至此黄金路径全部走完;第四部分转向「一个进程装不下」之后的并行、分离与路由。