mini-sglang 是一个用于学习推理服务内部结构的简化项目。一次流式请求从 API 进来,经过多个进程和消息队列,最后把生成结果返回给客户端;真正结束时,还要把这次请求留下的状态清理干净。
这篇只记录其中一个很小但容易漏掉的边界:客户端已经收到最后一个片段时,服务端是否真的完成了收尾。实验不讨论模型质量或 GPU 速度,只把变量收缩到 API 层的请求生命周期。
NOTE实验结论 Day 4 先让测试失败,再修复 API 请求结束后的清理逻辑,最后 4 个测试全部通过。 这次没有测 GPU 性能,重点是确认请求能够“有始有终”。 项目入口:mini-sglang 项目索引 项目计划:mini-sglang 推理可观测性项目计划
日期:2026-09-05
仓库:/home/runqi_yang/src/mini-sglang
分支:main
测试方式:假的收发队列 + 假的 UserReply,不启动 GPU、模型或真实 ZMQ
这次要解决什么
一个请求大致会经历:
- API 接收请求;
- 后端不断返回生成片段;
- 收到最后一个片段;
- 删除这个请求的临时记录。
可以把临时记录理解成餐厅的“正在处理订单”清单。菜已经端出去以后,订单也必须从清单里删掉。否则请求量一多,服务会误以为还有很多请求没有完成,后续的 inflight 指标也会失真。
这正是 Day 4 要先验证的边界。Prometheus 指标放在 Day 5 再接入,先保证被统计的生命周期本身是正确的。
测试范围
测试文件:tests/observability/test_frontend_metrics.py
| 测试场景 | 需要确认的事情 |
|---|---|
流式 /generate | 返回所有片段和 [DONE],结束后清理请求记录 |
| 流式 ChatCompletion | 返回角色块、结束块和 [DONE],结束后清理请求记录 |
| 非流式 ChatCompletion | 收集到最终 reply 后返回完整内容,并清理请求记录 |
| 客户端断开 | 只发送一次 AbortMsg,并清理请求记录 |
测试使用假的收发队列,把 UserReply 直接送给 FrontendManager。这样测试关注的是 API 层的请求状态,不会被模型加载、显存、GPU kernel 或 ZMQ 环境干扰。
第一轮:先测试,发现问题
修复前的结果:
3 failed, 1 passed通过的是客户端断开场景。三个正常完成场景都发现同一个问题:回答内容已经返回,但 ack_map 和 event_map 里的请求记录仍然存在。
问题原因在于:
wait_for_ack()原本把清理代码放在生成器循环之后;- 三个调用方收到最终 reply 后立即
break; - 内层生成器没有继续执行到清理位置。
所以这不是“模型没有回答”,而是“回答完以后忘了收尾”。
修复内容
修改文件:python/minisgl/server/api_server.py
做了三处最小修改:
- 给
wait_for_ack()加上try/finally,无论正常结束还是取消,都尝试清理ack_map和event_map; - 使用
pop(uid, None),让重复清理不会再次抛错; - 流式和非流式调用方不再在最终 reply 处提前
break,让wait_for_ack()自然结束并完成清理。
API 返回内容没有改变,改动只影响请求结束后的内部状态。
第二轮:修复后重新测试
标准 pytest 结果:
.... [100%]4 passed in 3.44s另外完成了:
- Python
compileall检查通过; git diff --check通过;- 客户端断开场景仍然只发送一个
AbortMsg。
项目虚拟环境原本没有安装 pytest。本次为了测试临时安装了测试运行器,测试完成后已清理临时依赖,没有修改 pyproject.toml。
这次实验说明了什么
现在可以确认:
- 流式请求能够正常结束;
- 非流式请求能够正常结束;
- 客户端取消能够正常清理;
- 正常完成和取消都不会留下请求状态。
这次还没有测 TTFT、ITL、吞吐或显存,也还没有真正输出 Prometheus 指标。因此它是一次 API 生命周期正确性测试,不是性能实验。
下一步:Day 5
在这套已经通过测试的生命周期上接入 API 指标:
- 请求接收数;
- 当前进行中的请求数;
- 完成数和取消数;
- TTFT、ITL 和总时延。
指标标签不使用 uid、完整 prompt 或模型完整路径,避免产生高基数和敏感信息。相关概念见 Day 03 指标设计笔记,请求路径见 Day 02 请求生命周期笔记。