亲手实现一个 token 流式服务器
目标
亲手构建一个通过 SSE 流式发送 token 的 server,分别测量 TTFT 与 token 间延迟,并证明 client 中断时生成过程确实会停止。
为什么重要
即使总生成时间同为 6 秒,等 6 秒后一次性显示与 200ms 后开始持续输出,是完全不同的产品。因此,streaming 是 LLM 服务的基本能力;从启用 streaming 的那一刻起,测量方式也会改变——不能只记录响应完成时间,而应分别测量首个 chunk 到达时间和 chunk 之间的间隔。本实验中尤其容易忽略的是第 7 步。用户在长回答生成途中切换到其他问题是非常常见的模式;如果没有 cancel 处理,这些请求会继续占用 batch slot,生成无人查看的 token。并发吞吐量会莫名下降,GPU 账单却不断增加。这是一种安静而昂贵的事故,因此专门分配完整一步来处理。
步骤
- 在
/root/ls/gen.py中创建generate(prompt, n)。相同 prompt 和 n 必须始终返回相同 token sequence。使用相同参数生成/root/ls/gen1.txt和/root/ls/gen2.txt,两个文件内容必须相同。 - 在 127.0.0.1:8170 启动
/root/ls/server.py。POST /generate接收{"prompt":"hello","max_tokens":20},返回{"text":"...","usage":{"prompt_tokens":<n>,"completion_tokens":20}}。 GET /stream?prompt=hello&max_tokens=20使用Content-Type: text/event-stream响应,每个 token 以data: {"token":"..."}加一个空行发送。响应正文中以data:开头的行必须至少有 20 行。- 在 stream 末尾发送
data: [DONE]。 - 使用
/root/ls/ttft.py测量首个 chunk 到达前的时间,并在/root/ls/ttft.txt中写入ttft_ms=<정수>。该值必须大于 0 且小于 3000。 - 在
/root/ls/itl.txt中写入count=<n> p50_ms=<수> p99_ms=<수>。count 必须至少为 19。 - 使用
max_tokens=100打开 stream,只接收 5 个 chunk 后断开。让GET /_debug/generated返回server 在上一个 stream 请求中实际生成的 token 数量(同时包含累计总数会更方便),并在/root/ls/cancel.txt中将该值写为requested=100 generated=<n>。generated 必须不大于 30。 - 在
/root/ls/report.txt中写四行:ttft_ms=<n>、itl_p50_ms=<수>、tokens=<n>、throughput_tps=<수>。
参考
- SSE event 在一行
data: <내용>之后必须紧跟一个空行。 - 必须关闭 framework 或 proxy 的 buffering,streaming 才会真正逐步传输。
- stream 中途发生错误时,无法再用 status code 通知,因为 200 已经发送;错误也必须通过 event 发送。
- 常见错误 1:遗漏 chunk 之间的空行,导致 client 无法读取任何内容。
- 常见错误 2:没有检测 cancel,继续为已断开的请求生成 token。
创建确定性 token generator
在 /root/ls/gen.py 中创建 generate(prompt, n)。相同 prompt 和 n 必须始终返回相同 token sequence。使用相同参数生成 /root/ls/gen1.txt 和 /root/ls/gen2.txt,两个文件内容必须相同。
相同 prompt 必须始终产生相同 token sequence,才能进行评分和评估。可以使用 hash 作为 seed。
创建非 streaming endpoint
在 127.0.0.1:8170 启动 /root/ls/server.py。POST /generate 接收 {"prompt":"hello","max_tokens":20},返回 {"text":"...","usage":{"prompt_tokens":<n>,"completion_tokens":20}}。
首先实现一次性返回的形式,以建立 baseline。请同时返回 token usage。
使用 SSE 发送 chunk
GET /stream?prompt=hello&max_tokens=20 使用 Content-Type: text/event-stream 响应,每个 token 以 data: {"token":"..."} 加一个空行发送。响应正文中以 data: 开头的行必须至少有 20 行。
content type 与 event 分隔规则是固定的。若遗漏 event 之间的空行,client 将无法识别。
发送结束信号
在 stream 末尾发送 data: [DONE]。
有一个惯用 marker 用于告知 stream 何时结束。
测量首个 token 到达时间
使用 /root/ls/ttft.py 测量首个 chunk 到达前的时间,并在 /root/ls/ttft.txt 中写入 ttft_ms=<정수>。该值必须大于 0 且小于 3000。
这是从请求开始到首个 chunk 到达之间的时间。不要与整体完成时间混淆。
统计 token 间延迟分布
在 /root/ls/itl.txt 中写入 count=<n> p50_ms=<수> p99_ms=<수>。count 必须至少为 19。
记录每个 chunk 的到达时间并计算差值。仅计算平均值并不足够。
client 中断时停止生成
使用 max_tokens=100 打开 stream,只接收 5 个 chunk 后断开。让 GET /_debug/generated 返回server 在上一个 stream 请求中实际生成的 token 数量(同时包含累计总数会更方便),并在 /root/ls/cancel.txt 中将该值写为 requested=100 generated=<n>。generated 必须不大于 30。
只有检测到连接断开,才能停止生成。请统计 server 生成的 token 数量来证明。
创建指标报告
在 /root/ls/report.txt 中写四行:ttft_ms=<n>、itl_p50_ms=<수>、tokens=<n>、throughput_tps=<수>。
把前面测量的值集中到一个文件中。吞吐量是生成 token 数除以总时间。