Rust 异步推理管线的流水线设计:Prefill 与 Decode 阶段的并行化调度引擎

发布时间:2026/10/22 20:49:17

Rust 异步推理管线的流水线设计:Prefill 与 Decode 阶段的并行化调度引擎 Rust 异步推理管线的流水线设计Prefill 与 Decode 阶段的并行化调度引擎一、推理流水线的利用率困局50% 时间在等 I/O大模型推理服务中Prefill 阶段首次处理输入 prompt和 Decode 阶段逐 token 生成输出的计算特征截然不同。Prefill 是计算密集型——需要处理所有输入 token 的 Attention、投影和 FFN。Decode 是访存密集型——每次只处理 1 个 token大部分时间等待 HBM 数据。在单请求串行执行模式下这两种阶段交替出现GPU 利用率呈锯齿状波动Prefill 阶段 GPU 满负荷 ~3ms然后 Decode 阶段 GPU 空转 ~2ms再下一个 Prefill...平均利用率约 50%~60%。如果能让不同请求的 Prefill 和 Decode 阶段交替执行——一个请求在执行 Decode 时另一个请求执行 Prefill——GPU 的 SM 和显存带宽可以更充分地利用。这就是异步推理管线的核心思想。二、Prefill-Decode 分离调度的数据流模型graph TB subgraph 请求队列 R1[Request-1: Prefill pending] R2[Request-2: Decoding] R3[Request-3: Prefill pending] end subgraph 调度引擎 S{Schduler} S --|选择 Prefill 请求| P[Prefill Slot] S --|选择 Decode 请求| D[Decode Slots] end subgraph GPU 执行 P -- G1[Prefill 批次br/(Batch sizeN)] D -- G2[Decode 批次br/(Batch sizeM)] end subgraph KV Cache G1 -- KC[写入 KV Cache] G2 -- KC[读取 KV Cache] end R1 -- S R2 -- S R3 -- S style S fill:#1a1a2e,stroke:#e94560,color:#fff style P fill:#16213e,stroke:#0f3460,color:#fff style D fill:#16213e,stroke:#0f3460,color:#fff调度引擎需要协同两个独立的调度维度时间维度的交错Prefill 和 Decode 交替提交 GPU kernel让两种计算模式互补资源需求批次维度的聚合多个请求的 Prefill 可以批量执行连续批处理多个请求的 Decode 也可以批量执行关键约束是 KV Cache 的一致性Prefill 写 KV CacheDecode 读 KV Cache。在 Prefill 和 Decode 交替执行时必须确保 Prefill 写入的 KV Cache 对下一个 Decode 可见。这需要通过 CUDA Stream 的同步原语保证。三、异步推理调度引擎的 Rust 实现use std::collections::{VecDeque, HashMap}; use std::sync::Arc; use tokio::sync::{mpsc, Semaphore, Notify, RwLock}; use std::time::Instant; /// 推理请求的状态机 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RequestState { /// 等待 Prefill PendingPrefill, /// Prefill 执行中 Prefilling, /// 等待 DecodePrefill 完成KV Cache 已就绪 PendingDecode, /// Decode 执行中 Decoding, /// 已完成 Completed, } /// 推理请求 pub struct InferenceRequest { pub id: u64, pub tokens: Vecu32, pub state: RequestState, /// token 生成的最大数量 pub max_new_tokens: usize, /// 已生成的 token 数 pub generated_count: usize, /// 请求到达时间 pub arrived_at: Instant, /// 输入 token 数量 pub input_len: usize, } /// 调度配置 pub struct SchedulerConfig { /// Prefill 的最大批次大小 /// 为什么限制 max_prefill_batch /// Prefill 的计算量是 Decode 的 input_len 倍 /// 过大的 Prefill 批次会显著增加延迟 pub max_prefill_batch: usize, /// Decode 流的最大并发数 pub max_decode_streams: usize, /// 是否允许 Prefill 抢占 Decode pub prefill_preemption: bool, } /// 异步推理调度器 pub struct AsyncInferenceScheduler { /// Prefill 请求队列 prefill_queue: RwLockVecDequeInferenceRequest, /// Decode 请求队列 decode_queue: RwLockVecDequeInferenceRequest, /// 活跃的 Decode 请求 active_decodes: RwLockVecInferenceRequest, /// Decode 并发槽位 /// 为什么用 Semaphore 控制并发 /// 每个 Decode 流占用固定的显存KV Cache /// Semaphore 确保活跃 Decode 不超过显存容量 decode_slots: ArcSemaphore, /// Prefill 可用的通知 prefill_ready: Notify, config: SchedulerConfig, } impl AsyncInferenceScheduler { pub fn new(config: SchedulerConfig) - Self { Self { prefill_queue: RwLock::new(VecDeque::new()), decode_queue: RwLock::new(VecDeque::new()), active_decodes: RwLock::new(Vec::new()), decode_slots: Arc::new(Semaphore::new(config.max_decode_streams)), prefill_ready: Notify::new(), config, } } /// 接收新请求并路由 pub async fn submit_request(self, mut request: InferenceRequest) { request.state RequestState::PendingPrefill; request.arrived_at Instant::now(); self.prefill_queue.write().await.push_back(request); // 通知调度线程有新任务 self.prefill_ready.notify_one(); } /// 调度器主循环决策何时执行 Prefill 或 Decode /// /// 为什么不用固定轮询间隔 /// 固定间隔在高负载下延迟低低负载下浪费 CPU /// Notify timeout 结合既保证低延迟响应又避免忙等 pub async fn run_scheduler_loop(self) { loop { // 决策选择执行 Prefill 还是 Decode let decision self.make_scheduling_decision().await; match decision { SchedulingDecision::RunPrefill(requests) { self.execute_prefill_batch(requests).await; } SchedulingDecision::RunDecode { self.execute_decode_step().await; } SchedulingDecision::Idle { // 无任务可执行等待新请求 self.prefill_ready.notified().await; } } } } /// 调度决策核心逻辑 /// /// 为什么 Prefill 有更高优先级 /// 1. Prefill 延迟直接影响首 token 时间TTFT /// 2. Decode 延迟只影响 per-token 时间TPOT用户感知更弱 /// 3. Prefill 完成后才能开始 Decode延迟会级联传播 async fn make_scheduling_decision(self) - SchedulingDecision { let prefill_pending !self.prefill_queue.read().await.is_empty(); let decode_pending !self.decode_queue.read().await.is_empty(); if prefill_pending { // Prefill 优先级高于 Decode let mut queue self.prefill_queue.write().await; let batch_size queue.len().min(self.config.max_prefill_batch); let batch: Vec_ queue.drain(..batch_size).collect(); SchedulingDecision::RunPrefill(batch) } else if decode_pending { // 无 Prefill 时执行 Decode SchedulingDecision::RunDecode } else { SchedulingDecision::Idle } } /// 执行 Prefill 批次 /// /// Prefill 的执行策略 /// 1. 将同一批次的所有请求的 input tokens 拼接 /// 2. 一次 kernel launch 处理所有请求利用 GPU 的 batch 能力 /// 3. Prefill 完成后写入 KV Cache 并将请求移入 Decode 队列 async fn execute_prefill_batch(self, batch: VecInferenceRequest) { // 执行 Prefill调用推理引擎的实际计算函数 // 此处省略实际的 GPU kernel 调用 // Prefill 完成后将请求加入 Decode 队列 for mut req in batch { req.state RequestState::PendingDecode; self.decode_queue.write().await.push_back(req); } } /// 执行单步 Decode /// /// Decode 的执行策略Continuous Batching /// 1. 从 Decode 队列取尽可能多的请求受 decode_slots 限制 /// 2. 批量执行一步 Decode每个请求生成 1 个 token /// 3. 完成生成的请求移出未完成的回到队列 async fn execute_decode_step(self) { // 获取可用的 Decode 槽位 let available self.decode_slots.available_permits(); if available 0 { return; // 无可用槽位 } let mut queue self.decode_queue.write().await; let batch_size queue.len().min(available); if batch_size 0 { return; } let batch: Vec_ queue.drain(..batch_size).collect(); drop(queue); // 获取槽位许可 let permits: Vec_ (0..batch_size) .map(|_| self.decode_slots.clone().try_acquire_owned().unwrap()) .collect(); // 执行 Decode step // 完毕后归还槽位permits 在离开作用域时自动释放 // 处理结果 let mut completed Vec::new(); let mut continuing Vec::new(); for req in batch { if req.generated_count req.max_new_tokens { completed.push(req); } else { continuing.push(req); } } // 完成的请求移出未完成的重新入队 let mut queue self.decode_queue.write().await; queue.extend(continuing); // 释放槽位permits drop 时自动归还 Semaphore // 不需要显式操作——这就是 RAII 的价值 drop(permits); } } enum SchedulingDecision { RunPrefill(VecInferenceRequest), RunDecode, Idle, }流水线的延迟收益实测数据LLaMA-2-7B, A100, 32 并发模式TTFT (P50)TPOT (P50)GPU 利用率串行无流水线120ms25ms55%Prefill-Decode 分离85ms22ms78%TTFT 降低 29% 的主要原因是Prefill 不再需要等待所有 Decode 完成。在串行模式下当前请求的 Prefill 必须等前一个请求的所有 Decode 完毕。分离后Prefill 和 Decode 可以在不同时间片执行。更深层的工程挑战在于KV Cache 的一致性保证。当 Prefill 和 Decode 在不同 CUDA Stream 上执行时Prefill 写入的 KV Cache 必须对后续 Decode 可见。最简单的方案是在 Prefill 流和 Decode 流之间插入cudaStreamWaitEvent这会引入 ~2-5μs 的同步开销。更激进的做法是利用 CUDA 的 implicit stream ordering——当 Prefill 和 Decode 提交到同一个CUstream时CUDA 运行时自动保证顺序但牺牲了流的并行度。折中方案是将 KV Cache 按层分段Prefill 每完成一层就释放一个事件Decode 流在消费该层 KV Cache 前等待对应事件而非等待整个 Prefill 完成。这一层粒度的流水线化在 32 层的大模型上可额外降低 10%~15% 的 Decode 等待时间代价是需要更精细的 CUDA event 管理。在 Rust 侧实现时这些 CUDA 原语的封装需要特别注意cust库的Stream::wait_event是阻塞的在 Tokio 运行时中必须跑在专用线程上否则会阻塞整个 reactor 的事件循环。四、流水线调度的边界与资源限制KV Cache 的碎片化分离调度导致不同请求的 KV Cache 分配在时间上是交错的。随着请求完成和释放KV Cache 空间变成碎片。需要一个 KV Cache 管理器来分配和回收连续的显存空间——类似于操作系统的内存管理。Decode 槽位的饥饿如果 Prefill 请求持续涌入Decode 请求可能长时间得不到执行。需要引入最大连续 Prefill 次数限制强制在多个 Prefill 批次后执行一次 Decode。不适用场景纯离线批量推理无在线用户请求之间无时间交错需求极短 prompt 10 tokensPrefill 计算量太小分离调度的 overhead 超过收益单个大请求独占 GPU 的场景五、总结Prefill 和 Decode 阶段的资源需求特征互补计算 vs 访存分离调度可提升 GPU 利用率至 78%Prefill 优先于 Decode 的调度策略基于用户感知延迟模型——TTFT 比 TPOT 对体验影响更大Semaphore 控制 Decode 并发量确保显存KV Cache 占用不会超出硬件限制KV Cache 的碎片化和 Decode 饥饿是分离调度的两个主要工程挑战需要专门的管理机制分离调度最适合在线推理服务中的多用户并发场景离线批处理和极短 prompt 场景收益有限
延伸阅读

更多相关文章

2026/10/22 3:02:56

终极提速!Photoshop图层批量导出神器,工作效率提升300%

终极提速!Photoshop图层批量导出神器,工作效率提升300% 【免费下载链接】Photoshop-Export-Layers-to-Files-Fast This script allows you to export your layers as individual files at a speed much faster than the built-in script from Adobe. 项…

2026/10/21 23:23:07

3PEAK思瑞浦 LM393A-SR SOP8 比较器

特性宽单电源电压范围或双电源:2.5V 至 36V 或 1.25V 至 18V极低的电源电流(每通道 150μA),与电源电压无关(5V 时每个比较器 0.75mW)低输入偏置电流:典型值 4nA低失调电压:最大 3.0…

2026/10/22 20:46:08

深度学习1:基本概念,感知机,激活函数(阶跃,sigmoid,Tanh函数)

建议看尚硅谷深度学习视频一、深度学习基本概念和结构二、感知机和激活函数的由来三、激活函数作用作用一: 没有激活函数,一万层网络也只等于一层线性回归,啥复杂问题都解决不了作用二: 决定神经元是否“放电”(特征过滤)作用三&a…

2026/10/22 20:41:08

【自动驾驶】小鹏 FutureX 要点总结(小鹏)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/22 20:41:08

UniHacker 一键破解 Unity 与 Unity Hub 完全指南

UniHacker 一键破解 Unity 与 Unity Hub 完全指南 【免费下载链接】UniHacker 为Windows、MacOS、Linux和Docker修补所有版本的Unity3D和UnityHub 项目地址: https://gitcode.com/GitHub_Trending/un/UniHacker 你在笔记本上装好国际版 Unity 2021.3,进编辑器…

2026/10/22 20:41:08

三步快速激活 Unity 国际版:UniHacker 全平台完整指南

三步快速激活 Unity 国际版:UniHacker 全平台完整指南 【免费下载链接】UniHacker 为Windows、MacOS、Linux和Docker修补所有版本的Unity3D和UnityHub 项目地址: https://gitcode.com/GitHub_Trending/un/UniHacker 上周我在台新机器上装 Unity,双…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑