Prhub

#39677 dsv4.1: Rust extension modules for image preprocessing, KV pool names, and PD bootstrap

原始 PR 作者 hnyls2002 合并时间 2026-09-16 09:11 文件变更 5 提交数 3 评论 1 代码增减 +197 / -0

执行摘要

为 DeepSeek-V4.1 补齐 Rust 侧图像预处理、KV 池名与 PD bootstrap 布局字段

PR body 明确说明这是 DeepSeek-V4.1 支持的 Rust 侧先行落地("Lands the Rust side of DeepSeek-V4.1 support ahead of the Python changes"),并给出拆分的工程理由:预编译 Rust 扩展的缓存 key 会哈希 rust/**,如果和 Python 侧改动放在同一个 PR,每次 push 都会因为命中不了缓存而重新编译扩展;单独先合入可以让缓存 key 稳定。三块改动分别对应 V4.1 必需的能力:sglang-mm 的 dsv41 模块提供图像预处理(resize_patchify),sglang-radix-tree 补齐 deepseek_v4_c1/c2 系列 KV 池名,sglang-server 在 PD bootstrap 路由上新增 dsv41_spec_layout 以便跨 prefill rank 校验 DSpark 布局一致性。验证方式是 cargo test -p sglang-server --lib disaggregation 5/5 通过(含既有 route_contract_matches_python_client)。

值得精读,尤其适合关注多模态预处理实现和 PD 拆分启动期校验的工程师。

推荐关注的设计决策:

  • dsv41/mod.rs 中"先做全量几何校验再计算"的防御式写法,以及用 256 项 bf16 LUT 替代逐像素浮点运算的性能取舍;par::for_chunks_mut 按 patch 分块并行是天然无数据竞争的分工方式,值得作为同类 preprocessing 的模板。
  • bootstrap.rs 把一致性校验放进 rcu 闭包并让闭包在 mismatch 时"原样返回",从而既不增加 registered_count 也不污染 prefill_ranks——这是一个值得学习的"校验失败不留副作用"写法,但要注意并发重跑闭包的语义细节。
  • PoolName 的双向字符串映射分布在 unified_tree_core.rs 与 python_bindings.rs 两个文件,属于容易漏改的隐式契约,建议后续补充一个穷举 round trip 的 Rust 单测来锁死。

如果需要跟进,重点看后续 Python 侧 PR 是否与 dsv41_spec_layout、deepseek_v4_c1/c2 池名以及 resize_patchify 的签名严格对齐。

讨论亮点

本 PR 的 review 区为空(review_comments_count = 0),讨论主要发生在 PR body 和 CI 状态上:

"Landing this separately keeps the prebuilt-Rust-extension cache key stable for the Python-side changes that follow: the key hashes rust/**, so any PR carrying these files misses the cache and recompiles the extensions on every push."

作者在 body 中把这批 Rust 改动单拆成 PR 的动机写得很明确,属于典型的"先固化编译缓存、再推 Python 侧"的分阶段落地策略。

CI:Latest PR Test (Base) 失败,Latest PR Test (Extra) 失败,AMD 无运行记录;随后作者在 issue 评论中执行 /tag-and-rerun-ci。

这里没有技术性交锋或设计争议,唯一可观察到的信号是 CI 需要重跑,说明首轮流水线状态并不干净(不过失败原因未在材料中给出,无法判断是否与本 PR 改动相关)。

实现拆解

  1. sglang-mm 新增 dsv41 图像预处理模块:新增 rust/sglang-mm/src/dsv41/mod.rs(+139 行),定义 ImagePlan(out_h/out_w/resize_h/resize_w/top/left/patch_size)与核心函数 resize_patchify(rgb, h, w, plan) -> Result<Vec<u16>, String>。实现分三步:先做完整几何合法性校验(patch_size 非零、输出尺寸能被 patch_size 整除、resize 尺寸不超过输出尺寸、padding 起点不越界、h*w*3 == rgb.len() 且用 checked_mul 防溢出),再用 crate::common::resize::resize_rgb 按 PIL bicubic 重采样,最后按 patch 分块(crate::common::par::for_chunks_mut,每块 3*ps*ps)把像素重排为 CHW patch 顺序,越界区域填 127(对应归一化后的 0),并通过一张 256 项 bf16 LUT 把 (v/255-0.5)/0.5 量化为 bf16 bits。Python 侧通过 #[cfg(feature = "python")] mod python 暴露 resize_patchify(arr, output_size, resize_size, padding_start, patch_size),要求 HWC 且连续,用 py.detach 释放 GIL 执行重活,返回 PyArray1<u16>(bf16 bits)。
  2. sglang-mm 模块注册与依赖接线:rust/sglang-mm/src/lib.rs 加 pub mod dsv41; 并在 _multimodal pymodule 中调用 dsv41::register(m)?,与既有 common/inkling/qwen_vl 保持一致;注意该 crate 同时以纯 Rust rlib 形式被 sglang-server 链接(default-features = false 时无 pyo3),因此 register 只在 python feature 下存在,新增代码必须满足这两种构建形态。
  3. sglang-radix-tree 补齐 c1/c2 KV 池名:rust/sglang-radix-tree/src/unified_tree_core.rs 的 enum PoolName 新增 DeepseekV4C1/C1Indexer/C1IndexerScale 与 DeepseekV4C2/C2Indexer/C2IndexerScale 六个变体(插在 C128 之后、State 变体之前);rust/sglang-radix-tree/src/python_bindings.rs 同步更新 pool_name_str() 和 parse_pool_name(),新增 "deepseek_v4_c1"/"deepseek_v4_c1_indexer"/"deepseek_v4_c1_indexer_scale"/"deepseek_v4_c2"/"deepseek_v4_c2_indexer"/"deepseek_v4_c2_indexer_scale" 的双向映射。这两处必须成对修改,否则 Python 侧传字符串会命中 unknown pool name 的 PyValueError。
  4. sglang-server PD bootstrap 新增 dsv41_spec_layout 并做一致性校验:rust/sglang-server/src/api_server/disaggregation/bootstrap.rs 中 PrefillServerInfo 加 #[serde(skip_serializing_if = "Option::is_none")] dsv41_spec_layout: Option<serde_json::Value>,Topology 加同名字段,Route(PUT /route 请求体)加 #[serde(default)] dsv41_spec_layout。route_put 在 ArcSwap::rcu 闭包内新增 layout_mismatch 判定:若已经注册过 rank(registered_count > 0)且已有布局与本次请求不一致,则直接返回未修改的 topo(不递增计数、不写 rank),闭包外据此返回 400 与错误信息 "DeepSeek-V4.1 DSpark PD layout differs across prefill ranks";route_get 的 sentinel 聚合分支回传该字段。
  5. 测试随源码内联落地:本 PR 没有新增独立测试文件,但在 bootstrap.rs 的 mod tests 中新增 dspark_layout_round_trip_and_rank_mismatch,覆盖三条路径—— PUT 携带 layout 返回 200、GET sentinel 能取回相同 layout(round trip)、后续 PUT 携带不兼容 layout(null)返回 400 且已存布局保持不变。这属于距源码极近的契约测试,与文件内既有 route_contract_matches_python_client 互补。
文件 模块 状态 重要度
rust/sglang-mm/src/dsv41/mod.rs 图像预处理 added 8.39
rust/sglang-server/src/api_server/disaggregation/bootstrap.rs PD 引导 modified 7.41
rust/sglang-radix-tree/src/python_bindings.rs 基数树绑定 modified 5.68
rust/sglang-radix-tree/src/unified_tree_core.rs 统一树核心 modified 5.39
rust/sglang-mm/src/lib.rs 多模态入口 modified 4.81

关键符号

resize_patchify ImagePlan register route_put route_get pool_name_str parse_pool_name dspark_layout_round_trip_and_rank_mismatch

关键源码片段

rust/sglang-radix-tree/src/python_bindings.rs data-contract

PoolName 与 Python 字符串的双向映射表,新增 6 个 c1/c2 池名条目;与 unified_tree_core.rs 的枚举变体必须成对修改。

/// 把 Rust 的 PoolName 枚举转成 Python 侧使用的字符串标识。
fn pool_name_str(name: PoolName) -> &'static str {
    match name {
        PoolName::Kv => "kv",
        PoolName::Mamba => "mamba",
        PoolName::Swa => "swa",
        PoolName::Indexer => "indexer",
        PoolName::DeepseekV4C4 => "deepseek_v4_c4",
        PoolName::DeepseekV4C4Indexer => "deepseek_v4_c4_indexer",
        PoolName::DeepseekV4C4IndexerScale => "deepseek_v4_c4_indexer_scale",
        PoolName::DeepseekV4C128 => "deepseek_v4_c128",
        // 新增:V4.1 的 c1 / c2 KV 池及其中间态池。
        PoolName::DeepseekV4C1 => "deepseek_v4_c1",
        PoolName::DeepseekV4C1Indexer => "deepseek_v4_c1_indexer",
        PoolName::DeepseekV4C1IndexerScale => "deepseek_v4_c1_indexer_scale",
        PoolName::DeepseekV4C2 => "deepseek_v4_c2",
        PoolName::DeepseekV4C2Indexer => "deepseek_v4_c2_indexer",
        PoolName::DeepseekV4C2IndexerScale => "deepseek_v4_c2_indexer_scale",
        PoolName::DeepseekV4C4State => "deepseek_v4_c4_state",
        PoolName::DeepseekV4C4IndexerState => "deepseek_v4_c4_indexer_state",
        PoolName::DeepseekV4C128State => "deepseek_v4_c128_state",
        PoolName::Draft => "draft",
        PoolName::DraftIndexer => "draft_indexer",
        PoolName::DraftSwa => "draft_swa",
    }
}
​
/// 把 Python 传来的字符串反解为 Rust 枚举;未知名字报 PyValueError。
/// 反向映射与 pool_name_str 必须保持严格对称,否则会出现“能输出但读不回”的契约裂缝。
fn parse_pool_name(name: &str) -> PyResult<PoolName> {
    match name {
        "kv" => Ok(PoolName::Kv),
        "mamba" => Ok(PoolName::Mamba),
        "swa" => Ok(PoolName::Swa),
        "indexer" => Ok(PoolName::Indexer),
        "deepseek_v4_c4" => Ok(PoolName::DeepseekV4C4),
        "deepseek_v4_c4_indexer" => Ok(PoolName::DeepseekV4C4Indexer),
        "deepseek_v4_c4_indexer_scale" => Ok(PoolName::DeepseekV4C4IndexerScale),
        "deepseek_v4_c128" => Ok(PoolName::DeepseekV4C128),
        // 新增:与 pool_name_str 一一对应的 c1 / c2 系列。
        "deepseek_v4_c1" => Ok(PoolName::DeepseekV4C1),
        "deepseek_v4_c1_indexer" => Ok(PoolName::DeepseekV4C1Indexer),
        "deepseek_v4_c1_indexer_scale" => Ok(PoolName::DeepseekV4C1IndexerScale),
        "deepseek_v4_c2" => Ok(PoolName::DeepseekV4C2),
        "deepseek_v4_c2_indexer" => Ok(PoolName::DeepseekV4C2Indexer),
        "deepseek_v4_c2_indexer_scale" => Ok(PoolName::DeepseekV4C2IndexerScale),
        "deepseek_v4_c4_state" => Ok(PoolName::DeepseekV4C4State),
        "deepseek_v4_c4_indexer_state" => Ok(PoolName::DeepseekV4C4IndexerState),
        "deepseek_v4_c128_state" => Ok(PoolName::DeepseekV4C128State),
        "draft" => Ok(PoolName::Draft),
        "draft_indexer" => Ok(PoolName::DraftIndexer),
        "draft_swa" => Ok(PoolName::DraftSwa),
        other => Err(PyValueError::new_err(format!("unknown pool name: {other}"))),
    }
}
rust/sglang-radix-tree/src/unified_tree_core.rs data-contract

PoolName 枚举本身新增 6 个 c1/c2 变体,是 KV 池标识的数据契约源头,下游匹配与映射都依赖它。

/// 作为 PoolTransfer 标识的知名池名。
/// 注意:这是跨语言契约的源头——Rust 侧新增变体后,
/// python_bindings.rs 的字符串映射以及 Python 侧的池注册逻辑需同步跟进。
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum PoolName {
    #[default]
    Kv,
    Mamba,
    Swa,
    Indexer,
    DeepseekV4C4,
    DeepseekV4C4Indexer,
    DeepseekV4C4IndexerScale,
    DeepseekV4C128,
    // 新增:V4.1 的 c1 / c2 系列池(含 indexer 与 indexer_scale 三种粒度)。
    DeepseekV4C1,
    DeepseekV4C1Indexer,
    DeepseekV4C1IndexerScale,
    DeepseekV4C2,
    DeepseekV4C2Indexer,
    DeepseekV4C2IndexerScale,
    DeepseekV4C4State,
    DeepseekV4C4IndexerState,
    DeepseekV4C128State,
    Draft,
    DraftIndexer,
    DraftSwa,
}
rust/sglang-mm/src/lib.rs entrypoint

dsv41 模块的接线入口:声明模块并在 PyO3 pymodule 中注册,使其在 python feature 下可被 Python 侧调用。

//! sglang-mm: 面向 SGLang 的 Rust 加速多模态预处理。
//!
//! 以两种方式构建:
//! * PyO3 扩展 `sglang.srt.rust_extensions._multimodal`(feature `python`),
//!   供 Python 处理器(如 Inkling)与 parity 测试使用。
//! * 纯 Rust `rlib`(`default-features = false`),由 `sglang-server` 的
//!   MM worker 路径链接——该依赖图内不含 pyo3。
​
pub mod common;
pub mod driver;
// 新增:V4.1 图像预处理模块(resize + pad + patchify)。
pub mod dsv41;
pub mod inkling;
pub mod pipeline;
pub mod qwen_vl;
pub mod registry;
​
#[cfg(feature = "python")]
use pyo3::prelude::*;
​
// 仅在 python feature 下导出;纯 Rust rlib 构建时不引入 pyo3。
#[cfg(feature = "python")]
#[pymodule]
fn _multimodal(m: &Bound<'_, PyModule>) -> PyResult<()> {
    common::register(m)?;
    inkling::register(m)?;
    // 注册为新子模块 "dsv41",与既有处理器并列。
    dsv41::register(m)?;
    qwen_vl::register(m)?;
    Ok(())
}

评论区精华

为何把 Rust 侧改动单独拆成一个 PR 设计

作者在 PR body 中说明:预编译 Rust 扩展的缓存 key 会哈希 `rust/**`,如果把 V4.1 的 Python 侧改动与这些 Rust 文件放在同一个 PR,每次 push 都会因为缓存 miss 而重新编译扩展,拖慢迭代。因此先把 Rust 侧固化合入,让缓存 key 稳定下来。

结论:作者自行做出决策并按此拆分落地,review 区无反对意见;这是 sglang 仓库中 `rust/**` 与 Python 改动分阶段落地的工程实践。 · 已解决

PD bootstrap 布局不一致的拒绝策略 正确性

`route_put` 在 `ArcSwap::rcu` 闭包内新增 `layout_mismatch` 判定:当已有 rank 注册(`registered_count > 0`)且 `dsv41_spec_layout` 与本次请求不同时,闭包原样返回未修改的拓扑,闭包外返回 400 并给出 `DeepSeek-V4.1 DSpark PD layout differs across prefill ranks`。讨论区没有对此提出异议,但 " 第一个注册者无条件定义契约 " 意味着首个 rank 若不带该字段,后续带布局的 rank 会被判为不一致。

结论:按当前实现落地,并由内联单测 `dspark_layout_round_trip_and_rank_mismatch` 覆盖 round trip 与 400 两条路径;首个 rank 决定布局这一隐含前提未在代码注释中显式声明。 · 已解决

新增 PoolName 变体的跨语言同步 设计

`unified_tree_core.rs` 的 `PoolName` 新增 6 个 c1/c2 变体后,必须同步修改 `python_bindings.rs` 的 `pool_name_str` 与 `parse_pool_name`,否则 Python 侧字符串会命中 `unknown pool name` 的 `PyValueError`。两处分散在不同文件,属容易漏改的隐式契约。

结论:本 PR 已同步两处,映射对称;但材料中未见针对 PoolName 全集的 round trip 穷举单测,后续新增变体仍有漏改风险。 · 已解决

向后兼容性与线上格式 设计

`dsv41_spec_layout` 在响应结构体上使用 `#[serde(skip_serializing_if = "Option::is_none")]`,在请求结构体上使用 `#[serde(default)]`,作者在 body 中明确 "clients that do not send it see the current wire format unchanged"。

结论:未发送该字段的客户端线上格式不变,属向后兼容的增量字段。 · 已解决

CI 首轮失败与重跑 测试

PR body 的 CI 状态显示 Latest PR Test (Base) 为 no_entry_sign、Latest PR Test (Extra) 为 x,AMD 无运行记录;随后作者在 issue 评论中执行 `/tag-and-rerun-ci`。材料未给出失败日志,无法判断是否与本 PR 相关。

结论:以重跑结果为准,本 PR 自身声明的验证是 `cargo test -p sglang-server --lib disaggregation` 5/5 通过(含既有 `route_contract_matches_python_client`)。 · unresolved

风险与影响

  1. 布局校验的时机语义可能被误用:bootstrap.rs 的 layout_mismatch 判定条件是 registered_count > 0,即第 1 个注册的 rank 无条件设定布局。如果第一个到达的请求恰好不带 dsv41_spec_layout(None),后续真正带布局的 rank 会被判为 mismatch 并返回 400,从而让整个 prefill 集群注册失败。这个"先到者定契约"的行为在 Route 字段可选时是一个潜在陷阱,且 None 与具体布局之间的比较没有区分"未声明"与"声明为空"。
  2. rcu 闭包内的副作用变量:layout_mismatch 是在 ArcSwap::rcu 闭包内被赋值的可变捕获变量。既有注释已说明 rcu 在写竞争下可能重跑闭包,本次新增的赋值依赖"最后一次执行的结果",在并发注册场景下该标志与最终提交的拓扑状态理论上可能来自不同轮次。当前行为在单测下正确,但值得留意其并发语义。
  3. 新增 PoolName 变体是跨语言枚举的扩展:unified_tree_core.rs 的 PoolName 是 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)] 枚举,虽然 Rust 侧无显式数值序列化,但 Python 侧通过字符串映射,属于隐式数据契约。任何按穷举 match 处理 PoolName 的下游位置(包括 Python 侧可能存在的枚举镜像)都需要同步,否则编译期或运行期会暴露 unknown pool name / 不匹配的 non-exhaustive 错误。
  4. resize_patchify 是全新未接入的热路径:该函数本身对输入做了较完整的边界与溢出校验(checked_mul、几何约束、连续性检查),风险较低;但它是新代码,且 PR 未附带 Rust 侧数值等价测试(与 Python/PIL 参考实现的 parity 测试),首次接入 Python 处理器时才是真正的验证点。ImagePlan 的约束依赖于调用方(Python 侧)正确计算 resize 尺寸和 padding 起点。
  5. 兼容性风险较低但需注意:dsv41_spec_layout 在响应侧使用 skip_serializing_if = "Option::is_none",PR body 明确"client 不发该字段则线上格式不变"。主要不确定性在于 Python 客户端(后续 PR)能否正确消费该字段,目前 Rust 侧是先行接口。
  6. CI 状态:PR body 中 Base/Extra 两个 PR Test 均为失败,TR 中仅有作者 /tag-and-rerun-ci;材料未给出失败日志,无法确认是否由本 PR 引起,需以最终 CI 结果为准。

对系统:这三个改动分别落在多模态预处理(sglang-mm)、KV 池命名契约(sglang-radix-tree)和 PD 拆分引导(sglang-server)三条链路上,都是 DeepSeek-V4.1 支持的前置依赖,本身不改变现有模型的运行行为——新增的模块、枚举变体和可选字段在旧路径上均不被触发。

对用户:直接用户是运行 DeepSeek-V4.1 的部署方。Python 侧改动落地后,V4.1 的图像预处理将走 Rust 加速路径(resize_patchify,含 PIL bicubic 语义对齐与 bf16 量化,py.detach 释放 GIL),HiCache 可以正确识别 deepseek_v4_c1/c2 系列池,PD 拆分启动阶段会主动拒绝 prefill rank 之间 DSpark 布局不一致的配置,把原本可能表现为运行期错乱的问题前移到启动期 400。

对团队/工程流程:本 PR 的核心价值之一是工程节奏控制——把 rust/** 改动与 Python 改动拆开,保住预编译扩展的缓存命中。这种"Rust 先行、Python 后续"的拆分方式会被后续 V4.1 相关 PR 复用,形成一条可预期的落地序列。

影响程度:文件数少(5 个)、纯新增(+197/-0),对现有功能几乎零回归面;主要影响面在 V4.1 这一条尚未完全打通的特性线上。

首个注册 rank 决定布局契约 rcu 闭包内副作用变量 跨语言枚举隐式契约 新代码无 parity 测试 CI 首轮失败待确认

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论