执行摘要
本 PR 针对 Rust 前端多模态提示扩展中的占位符替换操作,将多次 Vec::splice 替换为单次预分配的单向追加,消除了平摊的二次移动开销。在 128k prompt + 64 图像场景下实现 7.33x 加速,且不改变 API 和正确性。
功能与动机
Rust 前端的多模态请求需要将图像占位符替换为模型特定的替换令牌。原实现通过 Vec::splice 逐个替换,每次 splice 都会导致替换位置之后的所有元素移动。随着提示长度和图像数量增加,平摊操作的复杂度接近 O(n*m)。PR body 提供基准测试显示,在 32k prompt + 16 图像时耗时 20.9μs,优化后降至 6.8μs;在极端场景(128k + 64 图像)从 202.6μs 降至 27.6μs。
实现拆解
- 新增独立函数:提取
expand_prompt_token_ids 函数,接收占位符参数,避免与 MultimodalModelInfo 结构耦合。
- 预计算容量:通过
(replacement.tokens.len() - 1) 的累加(使用 saturating_sub 避免溢出)计算出新向量所需精确容量,一次性分配。
- 单次遍历构建:从前向后遍历占位符,将占位符之前的部分段和替换令牌依次追加到预分配向量,最后将原向量替换为新向量。
- 简化委托调用:原有
expand_prompt_tokens 方法简化为直接调用 expand_prompt_token_ids,并传递所需参数。
- 清理 benchmark 文件:移除仅用于演示的 benchmark 文件,保持仓库整洁。
- 后续优化:在第二个提交中,将
replacement.tokens 的迭代器直接用于 mask 构建和向量扩展,避免了一次临时 Vec<u32> 的堆分配。
以下为 expand_prompt_token_ids 函数的核心实现(基于 patch 片段整理,省略了 mask 构造细节):
fn expand_prompt_token_ids(
prompt_token_ids: &mut Vec<u32>,
replacements: Vec<PromptReplacement>,
placeholder_marker_token_id: u32,
placeholder_embed_token_id: u32,
placeholder_token: &str,
) -> Result<Vec<PlaceholderRange>> {
if replacements.is_empty() {
return Ok(Vec::new());
}
// 计算所有替换导致的净增长量
let replacement_growth = replacements.iter().fold(0usize, |total, replacement| {
total.saturating_add(replacement.tokens.len().saturating_sub(1))
});
// 预分配精确容量
let mut expanded =
Vec::with_capacity(prompt_token_ids.len().saturating_add(replacement_growth));
let mut ranges = Vec::with_capacity(replacements.len());
let mut cursor = 0usize;
for replacement in replacements {
if replacement.modality != Modality::Image {
bail_multimodal!(
"unsupported prompt replacement modality `{}`",
replacement.modality
);
}
let offset = find_next_token(prompt_token_ids, placeholder_marker_token_id, cursor)
.ok_or_else(|| {
multimodal!(
"placeholder token `{placeholder_token}` was not found in tokenized prompt"
)
})?;
if replacement.tokens.is_empty() {
bail_multimodal!(
"placeholder token `{placeholder_token}` expanded to no tokens"
);
}
let replacement_len = replacement.tokens.len();
// 追加占位符之前的原始片段
expanded.extend_from_slice(&prompt_token_ids[cursor..offset]);
// 追加替换令牌
expanded.extend(replacement.tokens.iter().map(|&token| token as u32));
// ... (mask 构造和 PlaceholderRange 构建 )
cursor = offset + 1;
}
// 追加剩余尾部
expanded.extend_from_slice(&prompt_token_ids[cursor..]);
*prompt_token_ids = expanded;
Ok(ranges)
}
注意:实际实现还包括 is_embed 布尔 mask 的构造,但为突出重点已省略。
评论区精华
- gemini-code-assist[bot]: “当前实现为每个 replacement 分配了临时
Vec<u32>,建议使用迭代器避免此分配。” 作者在后续提交中实现了此建议,消除了每次替换的堆分配。
- BugenZhao: “Benchmark 仅用于演示且未使用最终函数,要么更新要么删除。” 作者选择删除 benchmark 文件。
风险与影响
- 风险:极低。逻辑完全等价,仅改变数据移动方式。预计算使用 saturating 算术确保安全性。错误消息从格式化字符串改为插值,可能显示略有不同,但不影响功能。
- 影响:限于 Rust 前端的多模态请求路径。对于多图像长提示请求,首 token 延迟(TTFT)显著降低。单图像场景也有 1.76x 提升。不涉及 Python 端或其他模块。
关联脉络
本 PR 是 Rust 前端持续性能优化的一部分。近期历史 PR 中有多个针对多模态和前端性能的改进,如 PR#42796 优化 Qwen2.5-VL 的 CUDA graph 窗口,以及 PR#43445 的 speculative decoding 优化。本 PR 聚焦占位符替换,属于请求处理管线中的基础操作优化,可与上游处理形成累积效果。
参与讨论