安全扩展边界#

这一节会回答"哪些层适合扩展,哪些层一动就容易破坏不变量",把扩展动作从"能改"提升到"知道该改哪里"。

为什么要先讲边界再讲扩展#

对大型 runtime 来说,最大的风险通常不是"不会改",而是"改错层"。如果边界判断错了,你很可能写出一个能工作、但很快就会污染主链的 patch。

下面用具体文件路径把安全层和危险层说清楚。

安全扩展层:协议表面#

代表路径python/sglang/srt/entrypoints/openai/

这里是 OpenAI-compatible API 的协议转换层:protocol.py 定义请求/响应结构,serving_chat.pyserving_completions.py 等文件把 HTTP 请求转换成内部对象。

为什么安全:这层的改动不会影响 Req 的生命周期、不会改变调度判断,只影响"协议层怎样呈现能力"。

典型安全扩展:在 ChatCompletionRequest 里加一个新的可选字段(比如 cache_hint),然后在 to_sampling_params() 里把它映射到内部参数。整个改动局限在 protocol.py 一个文件,不需要改 scheduler.pymodel_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.pymodels/ 目录

添加一个新模型架构,只需要在 models/ 里实现标准接口(forwardload_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。核心执行链(ForwardBatchModelRunner → 采样)不受影响。

为什么安全: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_queuerunning_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.pypython/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.pyForwardBatch

ForwardBatch 里的 out_cache_locreq_pool_indicesseq_lens 等张量字段是调度层和执行层的契约。如果你改变了这些字段的语义(比如 out_cache_loc 从"新分配的 slot 索引"改成"所有已分配 slot"),就必须同时修改所有消费这个字段的地方——包括 model_runner.py 里的 KV 写入逻辑、chunked prefill 的 slot 累积逻辑,以及 DetokenizerManager 的输出收口。

漏改任意一处都会导致 KV 写到错误位置,输出结果不可预测。

一个判断题#

如果你准备做一处改动,先问三个问题:

  1. 能不能只在 entrypoints/ 或 parser 层解决?如果能,就不要往更深处改。
  2. 如果需要改 scheduler.py,改动是否只在"把已有信息传给更外层"而不是"改变 admission 判断"?如果是前者,风险可控;后者需要完整状态机分析。
  3. 如果需要改 ForwardBatch 的字段语义,能否列出所有消费这个字段的位置?如果列不出来,先 grep -r "out_cache_loc" 再动手。

小结#

扩展层代表路径风险为什么
协议表面entrypoints/openai/不影响 Req 生命周期
模型适配model_executor/models/通过接口隔离
Parser / structured outputtool_call.py 等执行链之后运行
观测路径observability/、metrics 上报只写,不反馈决策
Scheduler admissionscheduler.py 核心函数KV 分配不变量
KV cache 生命周期radix_cache.py引用计数不变量
ForwardBatch 字段语义forward_batch_info.py跨层契约