Prhub

#37967 [Rust] Bound multimodal media ingress

原始 PR 作者 merrymercy 合并时间 2026-09-04 20:22 文件变更 7 提交数 1 评论 3 代码增减 +274 / -74

执行摘要

为 Rust 多模态入站添加统一请求预算和本地文件特殊处理。

Rust 多模态入站没有应用一致的请求预算跨图片、视频和音频输入。它还将远程下载上限应用于受信任的本地文件,并在执行请求预算前分配了 base64 解码。需要修复这些问题以确保安全性和资源一致性。

这是一个精心设计且重要的改进,值得精读。其核心亮点在于:

1) 在入口层建立了与 Python 一致的、跨模态的聚合资源预算;
2) 通过区分本地文件和远程 I/O 实施差异化的安全策略(本地文件共享整体预算但绕过单源上限,远程 I/O 双重限制);
3) base64 解码的“先预留、后协调”模式确保了内存安全。建议关注 MAX_REQUEST_BYTES 提升后的实际内存占用。

讨论亮点

本次 PR 没有公开的 review 讨论。变更由作者 merrymercy 自行合并。

实现拆解

  1. 入口层配置传递:在 Rust 服务器入口 native_api.rs 中,将 Python 侧配置的 limit_mm_data_per_request(通过 config.rs 中新增的 ServerArgs 字段接收)传递给 prefetch_all 函数。同时,在 Python 配置构建器 config.py 中,从运行时上下文获取该配置并传入。
  2. 核心预取逻辑重构:在 prefetch.rs 的 prefetch_all 函数中,新增对每种模态(image, video, audio)的计数限制检查。收集源时,从仅 image_data 扩展到 image_data, video_data, audio_data。在 fetch_ordered 函数中,根据源路径(/ 开头或 file://)选择调用新的 fetch_local_file_budgeted(用于本地文件)或原有的 fetch_bytes_budgeted(用于远程流)。
  3. 预算与解码机制升级:在 sglang-mm 库的 fetch.rs 中,新增 fetch_local_file_budgeted 函数,用于安全读取本地文件:它会拒绝非正规文件(如 /dev/zero),并在读取前基于文件元数据大小预留字节预算。重写 base64 解码逻辑为 decode_base64_budgeted,在分配解码缓冲区前先预留最大估算大小,并在解码后精确调整。为 ByteBudget 添加 remaining 和 charge_existing 方法,支持跨阶段的预算核算。将驱动 driver.rs 中的 MAX_REQUEST_BYTES 从 256 MiB 提升至 1280 MiB(1.25 GiB)。
  4. 测试与文档配套:在 prefetch.rs 的测试模块中新增 mixed_modalities_preserve_image_video_audio_order 和 per_modality_budget_rejects_before_fetching 测试。在 fetch.rs 中新增 trusted_local_reader_rejects_non_regular_files、inline_base64_uses_the_supplied_request_budget 等测试。更新 sglang-mm/README.md 以反映新的预算语义。
文件 模块 状态 重要度
rust/sglang-server/src/api_server/prefetch.rs 多模态预取 modified 8.88
rust/sglang-mm/src/common/fetch.rs 媒体获取 modified 8.65
rust/sglang-mm/src/driver.rs 媒体驱动 modified 7.25
rust/sglang-server/src/api_server/native_api.rs API 入口 modified 5.91
rust/sglang-server/src/message/config.rs 配置定义 modified 5.4
python/sglang/srt/rust_server/config.py 配置传递 modified 4.49
rust/sglang-mm/README.md 文档 modified 2.1

关键符号

prefetch_all fetch_local_file_budgeted decode_base64_budgeted ByteBudget::charge_existing ByteBudget::remaining

关键源码片段

rust/sglang-server/src/api_server/prefetch.rs entrypoint

核心变更文件,实现了跨模态项目预算检查、每模态计数限制,以及区分本地 / 远程文件的预算策略。

// prefetch.rs
// 重构后的 prefetch_all 函数,现在接受模态限制参数,并汇总所有模态的媒体项。
pub async fn prefetch_all(
    requests: &mut [GenerateRequest],
    modality_limits: &BTreeMap<String, usize>,
) -> Result<(), String> {
    // 闭包 plan 用于规划和验证单个请求的所有模态
    let plan = |mm: &Option<Box<MmData>>| -> Result<Vec<String>, String> {
        let Some(mm) = mm.as_deref() else {
            return Ok(Vec::new());
        };
        let values = [
            ("image", mm.image_data.as_ref()),
            ("video", mm.video_data.as_ref()),
            ("audio", mm.audio_data.as_ref()),
        ];
        // 聚合所有模态的项目数,检查总预算
        let items = values.iter().filter_map(|(_, v)| *v).map(item_count).sum::<usize>();
        if items > MAX_ITEMS_PER_REQUEST {
            return Err(format!("multimodal request exceeds {MAX_ITEMS_PER_REQUEST} media items"));
        }
        // 逐模态检查配置的限制
        for (modality, value) in values {
            let count = value.map(item_count).unwrap_or_default();
            if let Some(limit) = modality_limits.get(modality) && count > *limit {
                let display = modality[..1].to_uppercase() + &modality[1..];
                return Err(format!("{display} count {count} exceeds limit {limit} per request."));
            }
        }
        // 收集所有模态的 I/O 源
        Ok(values.iter().filter_map(|(_, v)| *v).flat_map(io_sources).collect())
    };
    // ... 后续计划执行和并发获取逻辑 ...
}
​
// fetch_ordered 现在根据源类型选择不同的预算函数
async fn fetch_ordered(sources: Vec<String>, total_bytes: u64) -> Result<Vec<Bytes>, String> {
    let budget = Arc::new(ByteBudget::new(total_bytes));
    futures::future::try_join_all(sources.into_iter().map(|src| {
        let budget = Arc::clone(&budget);
        async move {
            // ... 获取信号量许可 ...
            tokio::task::spawn_blocking(move || {
                if src.starts_with('/') || src.starts_with("file://") {
                    // 本地文件:跳过单源上限,但共享整体预算
                    fetch_local_file_budgeted(&src, &budget)
                } else {
                    // 远程流 / 内联:应用单源上限
                    fetch_bytes_budgeted(&src, &budget)
                }
            }).await.map_err(|e| format!("media prefetch: {e}"))?.map(Bytes::from)
        }
    })).await
}
rust/sglang-mm/src/common/fetch.rs core-logic

核心库文件,新增了安全的本地文件预算读取、重写了安全的 base64 预分配解码,并增强了预算协调能力。

// fetch.rs
// ByteBudget 新增方法:查询剩余配额,并为已存在的载荷计费
impl ByteBudget {
    fn remaining(&self) -> u64 {
        self.0.load(std::sync::atomic::Ordering::Acquire)
    }
    /// 为已存在于内存中的载荷(如预取的数据)计费,确保总预算约束。
    pub fn charge_existing(&self, n: usize, what: &str) -> Result<(), String> {
        self.claim(n as u64).map_err(|()| over_budget(what))
    }
}
​
/// 安全读取受信任的本地文件:拒绝非正规文件,并基于元数据大小预留预算。
pub fn fetch_local_file_budgeted(src: &str, budget: &ByteBudget) -> Result<Vec<u8>, String> {
    let path = src.strip_prefix("file://").unwrap_or(src);
    let file = std::fs::File::open(path).map_err(|e| format!("media fetch: {path}: {e}"))?;
    let metadata = file.metadata().map_err(|e| format!("media fetch: stat {path}: {e}"))?;
    if !metadata.is_file() {
        return Err(format!("media fetch: {path}: not a regular file"));
    }
    let expected = metadata.len();
    // 在读取前预留预期大小,防止无限增长
    budget.claim(expected).map_err(|()| over_budget(path))?;
    let mut buf = Vec::with_capacity(usize::try_from(expected).unwrap_or(usize::MAX));
    let read = file.take(expected.saturating_add(1)).read_to_end(&mut buf)
        .map_err(|e| format!("media fetch: read {path}: {e}"))? as u64;
    if read > expected {
        return Err(format!("media fetch: {path}: changed size while reading"));
    }
    // 释放未使用的预留配额
    budget.release(expected - read);
    Ok(buf)
}
​
/// 安全的 base64 解码:先预留最大估算大小,解码后精确调整。
fn decode_base64_budgeted(encoded: &str, budget: &ByteBudget) -> Result<Vec<u8>, String> {
    let encoded = encoded.trim();
    // 计算尾部填充(最多 2 个‘=’)
    let padding = encoded.as_bytes().iter().rev().take_while(|&&byte| byte == b'=').take(2).count() as u64;
    // 估算解码后的最大长度
    let estimate = (encoded.len() as u64)
        .checked_add(3).and_then(|n| n.checked_div(4)).and_then(|n| n.checked_mul(3))
        .and_then(|n| n.checked_sub(padding)).ok_or_else(|| over_budget("base64 payload"))?;
    // 预留估算大小
    let remaining = budget.remaining();
    budget.claim(estimate).map_err(|()| {
        format!("{} (decoded size {estimate} bytes, {remaining} bytes remaining)", over_budget("base64 payload"))
    })?;
    match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
        Ok(decoded) => {
            // 释放差额
            budget.release(estimate - decoded.len() as u64);
            Ok(decoded)
        }
        Err(error) => {
            // 解码失败,释放全部预留
            budget.release(estimate);
            Err(format!("media fetch: base64 decode: {error}"))
        }
    }
}

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

内存与性能风险:MAX_REQUEST_BYTES 从 256 MiB 提升至 1280 MiB,允许单个请求缓冲更多媒体数据,可能增加峰值内存消耗,需关注高并发下的内存压力。本地文件读取路径:fetch_local_file_budgeted 基于文件元数据预分配内存,对于非正规文件或符号链接(已被拒绝)是安全的,但若大量请求并发读取大本地文件,其 I/O 模式可能影响性能。配置同步风险:新增的 limit_mm_data_per_request 配置字段需确保 Python 和 Rust 两侧正确传递,否则可能导致限制不一致。

用户影响:对终端用户无直接感知变化,但提供了更健壮的多模态请求处理,防止因不受限的本地文件或跨模态资源竞争导致的潜在 OOM 或请求拒绝不一致问题。
系统影响:增强了 Rust 多模态路径的安全性和资源管理,使其更接近 Python 路径的语义。提升了处理包含大型本地视频或音频文件的请求的能力。
团队影响:为 Rust 多模态子系统建立了更清晰的预算管理模型,为未来支持更复杂模态组合奠定了基础。

内存预算上限提升 本地文件 I/O 路径变更 Python-Rust 配置同步

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论