Prhub

#39000 [Router] Fleet-wide sampling contract 1/3: the config surface

原始 PR 作者 Kangyan-Zhou 合并时间 2026-09-16 12:39 文件变更 24 提交数 1 评论 0 代码增减 +939 / -0

执行摘要

新增采样契约配置面:类型、解析与启动校验

PR body 明确指出:托管端点的运营方需要采样契约是「他们的」而不是「引擎的」——一个固定配置适用于路由器的所有客户端,独立于部署镜像里默认值的变化。具体场景是像 Moonshot 自己的 API 那样服务 Kimi 模型,由 Kimi-Vendor-Verifier 的 tests/params 套件校验(受管参数允许客户端省略、发送恰好配置的值或收到 400)。PR 设计为 Stack 1/3:本 PR 只加配置面,后续 #39001 做请求路径上的强制与注入,#39002 做零拷贝注入与 e2e 代理测试。

值得精读。本 PR 展示了一个高质量的「配置面先行、请求路径后置」的分层实现策略,手写解析器、有序 entries 解码、编译期 ALL/index 一致性断言、启动即失败(fail fast)的运维理念都很有参考价值。重点关注 sampling.rs 的解析与校验架构,以及它如何为 Stack 2/3 的注入与拒绝逻辑预留清晰的接口边界。

讨论亮点

本 PR 没有 review 评论线程;唯一的 review 是 sherlockwu 的 APPROVED(无正文评论)。设计文档本身通过大量内联注释承载了关键设计决策:手写解析器而非 serde、有序 ENTRIES 而非 Map、ParamSpec::Exact 保留 serde_json::Number 而非 f64、band 在 allow 下作为启动错误而非 no-op、SamplingField::ALL 与 index 的编译期一致性断言等。这些都属于作者在 PR body 中主动说明、且通过注释与测试固化的设计权衡,而非评审交锋。

实现拆解

实现分为以下几步:

  1. 新增 SamplingOverrides 数据模型与手写解析器(experimental/sgl-router/src/config/sampling.rs,+812 行):定义 SamplingOverrides(BTreeMap<SamplingField, ParamSpec> + ConflictPolicy)、SamplingField(8 个字段枚举)、ParamSpec(Exact(serde_json::Number) 或 Range{lo,hi})、ConflictPolicy(Reject 默认 / Allow)。刻意手写解析而非 serde(deny_unknown_fields),因为该 flag 只在启动时读一次且 router 出错会 crash-loop,错误信息必须点名违规 key、值及其违反的域;同时外层对象和 band 都按有序 ENTRIES 解码而非 serde_json::Map,避免重复 key 时静默只保留最后一个值。
  2. CLI 入口接入(experimental/sgl-router/src/config/cli.rs):新增 --override-sampling-params(JSON 字符串)和 --sampling-param-conflict(reject/allow,clap 用 requires = "override_sampling_params" 保证单独设置该模式会被拒绝)。Cli::into_config 中解析 flag 并写入 ModelConfig.sampling_overrides。
  3. 配置类型与校验挂接(experimental/sgl-router/src/config/types.rs、mod.rs):ModelConfig 新增 sampling_overrides 字段(默认空),Config::validate 调用 SamplingOverrides::validate(),确保任何路径手工构造的 SamplingOverrides(测试 fixture、未来 config 文件、admin API)也必须满足与 flag 相同的域约束。
  4. 全仓 20 处 ModelConfig 字面量补齐:源码(policies/factory.rs、server/app_context.rs、server/routes/models.rs、server/routes/tokenize.rs、tokenizer/mod.rs、workers/manager.rs)与测试(tests/component/discovery/static_urls.rs、tests/proxy/ 下多个测试)的 ModelConfig 构造点统一加 sampling_overrides: Default::default()。
  5. 测试配套:sampling.rs 内 12 个单元测试覆盖全部参数、band、整数归一化、top_k 哨兵值、i64 边界、ALL/index 一致性、validate 拒绝手工构造 spec,以及 31 例畸形输入表;cli.rs 3 个测试覆盖 flag 到达 ModelConfig、reject 默认模式、allow 模式选择、--sampling-param-conflict 单独使用被 clap 拒绝、畸形 JSON 启动失败。
文件 模块 状态 重要度
experimental/sgl-router/src/config/sampling.rs 配置层 added 9.36
experimental/sgl-router/src/config/cli.rs 配置层 modified 8.3
experimental/sgl-router/src/config/types.rs 配置层 modified 5.44
experimental/sgl-router/src/config/mod.rs 配置层 modified 5.28
experimental/sgl-router/src/policies/factory.rs 策略模块 modified 4.16

关键符号

parse_sampling_overrides SamplingOverrides::validate validate_spec parse_band checked_value check_domain canonical_number SamplingField::from_wire_name SamplingField::wire_name SamplingField::index Cli::into_config

关键源码片段

experimental/sgl-router/src/config/cli.rs entrypoint

CLI 入口:新增两个 flag 的定义、`requires` 约束以及在 `into_config` 中把解析结果写入 `ModelConfig` 的接线逻辑,同时承载 3 个验证 flag 到达配置层的测试。

// ---- fleet-wide sampling contract (opt-in) ----
/// 采样参数固定 fleet-wide,以一个 JSON 对象给出,key 为请求体字段名。
///
/// 配置值在请求省略该字段时注入;`--sampling-param-conflict` 决定请求显式
/// 发送不同值时的行为。未知/重复 key、越界值、`allow` 下的 band 都会在启动
/// 时报错并点名违规 key。
#[arg(long, value_name = "JSON")]
pub override_sampling_params: Option<String>,
/// 冲突策略:`reject`(默认)准入前 400;`allow` 原样转发客户端值。
///
/// clap 的 `requires` 保证该模式单独设置(没有 `--override-sampling-params`
/// 时)会被拒绝——模式本身不治理任何东西。
#[arg(
    long,
    value_enum,
    value_name = "MODE",
    requires = "override_sampling_params"
)]
pub sampling_param_conflict: Option<ConflictPolicy>,
​
// 在 `Cli::into_config` 中解析 flag 并写入 `ModelConfig`。
let sampling_overrides = match &self.override_sampling_params {
    None => SamplingOverrides::default(), // 未设置 = 旧行为,零开销
    Some(raw) => {
        // 解析失败直接返回 Err,启动即失败而不是带病运行。
        parse_sampling_overrides(raw, self.sampling_param_conflict.unwrap_or_default())?
    }
};
​
let config = Config {
    // ... 其它字段省略 ...
    model: ModelConfig {
        // ... 其它字段省略 ...
        sampling_overrides,
    },
    // ... 其它字段省略 ...
};
config.validate()?;
experimental/sgl-router/src/config/types.rs entrypoint

`ModelConfig` 新增 `sampling_overrides` 字段,是采样契约从 CLI 到运行时的数据载体,所有手工构造 `ModelConfig` 的点都必须补齐该字段。

// `ModelConfig` 结构体(节选):新增字段放在所有既有可选字段之后,
// 默认空值保证未启用时行为完全不变。
#[derive(Debug, Clone)]
pub struct ModelConfig {
    pub id: String,
    pub tokenizer_path: String,
    pub policy: PolicyKind,
    pub decode_policy: DecodePolicyKind,
    pub bucket_config: Option<BucketConfig>,
    pub circuit_breaker: Option<CircuitBreakerConfig>,
    pub cache_aware: Option<CacheAwareConfig>,
    pub sticky: Option<StickyConfig>,
    pub affinity: Option<AffinityConfig>,
    pub fused: Option<Vec<FusedTerm>>,
    pub eligibility: Option<EligibilityConfig>,
    /// 采样参数 fleet-wide 固定,以及请求发送不同值时的行为:准入前 400,
    /// 或客户端值原样转发。任一模式下,配置值都会在请求省略字段时注入——
    /// 见 `SamplingOverrides`。空值(默认)保持现状。
    pub sampling_overrides: SamplingOverrides,
}

评论区精华

review 无评论,设计决策内嵌于注释 other

sherlockwu 直接 APPROVED,未留下任何评论。PR 的关键权衡(手写解析器 vs serde、有序 entries vs Map、Exact 保留 Number vs f64、band 在 allow 下报错)均通过模块级注释和 PR body 说明呈现。

结论:无争议点;设计决策以代码注释和测试形式固化为契约的一部分。 · 已解决

风险与影响

技术上风险较低,因为本 PR 明确不触碰请求路径,默认配置下行为零变化。主要风险点包括:

  1. 契约扩展的兼容性:ConflictPolicy 若未来需要扩展(如 rewrite 模式),allow/reject 之外的语义需要重新审视 band 约束;当前代码将 band-under-allow 固化为启动错误,未来若引入新策略需同步更新 validate_spec。
  2. 域约束与引擎实际的偏差:接受的域刻意采用 OpenAI 客户端库普遍校验的范围,比 SamplingParams.verify 更窄;若未来引擎支持更宽范围(如 temperature 负值或 top_k: -1 哨兵),配置域的收紧可能成为误拒绝源,需要回头调整 check_domain。
  3. f64 band 边界精度:band 的 lo/hi 存为 f64,对 n 这类大整数(接近 i64 边界)字段,band 模式下的精度损失可能漏掉边界值;当前文档已注明 band 主要用于连续域字段。
  4. 行为变更面扩散:新增字段穿透 ModelConfig 所有构造点,后续若有人手工构造 Config 未补该字段会编译失败(好事),但若改用 Default::default() 模式构造 Config 则可能漏配。

影响范围集中在 experimental/sgl-router 模块,对引擎(SGLang SRT)零影响。对用户:默认无行为变化;开启两个 flag 后,运营方可以声明一个固定采样契约,客户端请求将受 reject(400 前置拒绝)或 allow(客户端值穿透)约束,为托管 Kimi 等模型提供与上游 API 一致的参数治理。对团队:本 PR 为后续 #39001(请求路径强制+注入)和 #39002(零拷贝注入与 e2e 测试)铺好了类型地基,SamplingOverrides.validate() 被 Config::validate 调用意味着任何未来的配置来源都会执行同样的校验,降低了后续栈的回归风险。影响程度中等偏低——配置面改动大但无运行路径变更。

默认无行为变更 启动期校验失败即 crash 配置域可能窄于引擎支持范围 后续栈依赖此类型契约

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论