安全扩展边界#
这一节会回答"哪些层适合扩展,哪些层一动就容易破坏不变量",把扩展动作从"能改"提升到"知道该改哪里"。
为什么要先讲边界再讲扩展#
对大型 runtime 来说,最大的风险通常不是"不会改",而是"改错层"。如果边界判断错了,你很可能写出一个能工作、但很快就会污染主链的 patch。
下面用具体文件路径把安全层和危险层说清楚。
安全扩展层:协议表面#
代表路径:python/sglang/srt/entrypoints/openai/
这里是 OpenAI-compatible API 的协议转换层:protocol.py 定义请求/响应结构,serving_chat.py、serving_completions.py 等文件把 HTTP 请求转换成内部对象。
为什么安全:这层的改动不会影响 Req 的生命周期、不会改变调度判断,只影响"协议层怎样呈现能力"。
典型安全扩展:在 ChatCompletionRequest 里加一个新的可选字段(比如 cache_hint),然后在 to_sampling_params() 里把它映射到内部参数。整个改动局限在 protocol.py 一个文件,不需要改 scheduler.py 或 model_runner.py。
# protocol.py 里的安全扩展示例
class ChatCompletionRequest(BaseModel):
# ... 已有字段 ...
cache_hint: Optional[str] = None # 新增可选字段
def to_sampling_params(self) -> SamplingParams:
# ... 已有逻辑 ...
if self.cache_hint:
sampling_params["cache_key"] = self.cache_hint
return SamplingParams(**sampling_params)这类改动的回归路径也很短:只需要测试 ChatCompletionRequest 的序列化/反序列化和 to_sampling_params() 的输出,不需要跑完整的调度回归。
安全扩展层:模型适配面#
代表路径:python/sglang/srt/model_executor/model_runner.py 和 models/ 目录
添加一个新模型架构,只需要在 models/ 里实现标准接口(forward、load_weights 等),然后在模型注册表里登记。这不会改变 ModelRunner 怎样调度 batch,也不会改变 KV cache 的分配逻辑。
为什么安全:ModelRunner 通过接口调用模型,不关心具体架构。新模型的 forward 逻辑对调度层是透明的。
危险信号:如果你需要为这个模型修改 ForwardBatch 的字段,或者需要在 scheduler.py 里加特殊判断,说明这不是一个"只在模型层改"的问题,已经跨层了。
安全扩展层:parser 和结构化输出#
代表路径:python/sglang/srt/entrypoints/openai/ 下的 tool_call.py 或 structured generation 相关文件
添加新的 output parser(比如支持新的 tool call 格式),只需要在 parser 层实现新的解析逻辑,然后在 DetokenizerManager 的输出处理里挂上新 parser。核心执行链(ForwardBatch → ModelRunner → 采样)不受影响。
为什么安全:parser 在执行链之后运行,只处理已生成的 token 序列,不影响 token selection。
安全扩展层:观测和验证#
代表路径:python/sglang/srt/managers/scheduler.py 里的 metrics 上报,或 observability/ 目录
在 scheduler 里加一个新的 Prometheus counter,或者在 request_logger 里加一个新的时间戳记录点,不会改变任何调度不变量。
为什么安全:metrics 上报是只读侧写路径,不反馈到决策逻辑。唯一的风险是在热路径上加入过重的上报(比如每个 decode step 都做 dict 构造),但这是性能问题,不是正确性问题。
危险层:scheduler admission#
代表路径:python/sglang/srt/managers/scheduler.py 里的 _get_next_batch_to_run()、_add_request_to_running_batch() 等函数
这里管理的不变量非常多:
waiting_queue和running_batch的状态必须互斥;- 每次 admit 都会预先分配 KV slots,admission 失败时必须回滚这些分配;
- chunked prefill 的状态机需要跨轮次追踪同一个
Req的进度; - priority 和 grammar 约束的交互会改变 admit 判断。
哪里容易出问题:如果你在 _get_next_batch_to_run() 里加了新的 admission 判断,但忘记处理"这个请求被拒绝后 KV 分配要不要回滚"这个问题,就会出现显存泄漏(KV slots 被预分配但不被释放)。这类问题在 OOM 率较低的场景下可能几个小时都不暴露。
改这层之前:先把完整的 admission 状态机画出来,确认自己的改动在所有分支上都保持了 req.status、KV 分配和 waiting_queue/running_batch 之间的一致性。
危险层:KV cache 生命周期#
代表路径:python/sglang/srt/mem_cache/radix_cache.py、python/sglang/srt/mem_cache/base_prefix_cache.py
RadixCache 维护的不变量包括:
- 节点引用计数:正在被 running batch 使用的 KV 不能被 evict;
- 只有叶节点可以被 evict(父节点有子节点时不能被驱逐);
match_prefix返回的长度必须是 page_size 的整数倍;insert之后必须更新token_to_kv_pool的映射。
哪里容易出问题:如果你修改了 eviction 策略,绕过了"引用计数 > 0 的节点不可驱逐"的检查,就会出现"KV slot 被驱逐但 running batch 仍然持有引用"的情况——表现为乱码输出或段错误,难以定位。
危险层:执行前向对象#
代表路径:python/sglang/srt/model_executor/forward_batch_info.py(ForwardBatch)
ForwardBatch 里的 out_cache_loc、req_pool_indices、seq_lens 等张量字段是调度层和执行层的契约。如果你改变了这些字段的语义(比如 out_cache_loc 从"新分配的 slot 索引"改成"所有已分配 slot"),就必须同时修改所有消费这个字段的地方——包括 model_runner.py 里的 KV 写入逻辑、chunked prefill 的 slot 累积逻辑,以及 DetokenizerManager 的输出收口。
漏改任意一处都会导致 KV 写到错误位置,输出结果不可预测。
一个判断题#
如果你准备做一处改动,先问三个问题:
- 能不能只在
entrypoints/或 parser 层解决?如果能,就不要往更深处改。 - 如果需要改
scheduler.py,改动是否只在"把已有信息传给更外层"而不是"改变 admission 判断"?如果是前者,风险可控;后者需要完整状态机分析。 - 如果需要改
ForwardBatch的字段语义,能否列出所有消费这个字段的位置?如果列不出来,先grep -r "out_cache_loc"再动手。
小结#
| 扩展层 | 代表路径 | 风险 | 为什么 |
|---|---|---|---|
| 协议表面 | entrypoints/openai/ | 低 | 不影响 Req 生命周期 |
| 模型适配 | model_executor/models/ | 低 | 通过接口隔离 |
| Parser / structured output | tool_call.py 等 | 低 | 执行链之后运行 |
| 观测路径 | observability/、metrics 上报 | 低 | 只写,不反馈决策 |
| Scheduler admission | scheduler.py 核心函数 | 高 | KV 分配不变量 |
| KV cache 生命周期 | radix_cache.py | 高 | 引用计数不变量 |
| ForwardBatch 字段语义 | forward_batch_info.py | 高 | 跨层契约 |
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。