推理流水线并行:预处理、推理和后处理拆成独立阶段

📅 2026/7/19 16:16:28
推理流水线并行:预处理、推理和后处理拆成独立阶段
推理流水线并行预处理、推理和后处理拆成独立阶段一、推理服务的串行瓶颈全流程耦合导致的延迟膨胀大模型推理服务从请求到达到响应返回经历三个阶段预处理tokenizer 分词 输入编码、推理GPU 前向计算 KV Cache 管理、后处理输出解码 响应序列化 日志记录。传统部署将这三个阶段放在同一个进程内串行执行每个请求必须依次完成预处理→推理→后处理后才能释放资源。这种串行模式的核心问题是资源利用率不均衡。预处理和后处理是 CPU 密集型操作推理是 GPU 密集型操作。串行执行意味着 GPU 在等待预处理完成时处于空闲状态CPU 在等待推理完成时处于空闲状态。每个阶段的空闲时间占整个请求周期的 30%~40%总延迟是三个阶段耗时的简单加和。更严重的是串行模式无法独立扩缩容。当 GPU 推理成为瓶颈时增加 GPU 数量会同时带动预处理的并发需求上升但预处理和推理在同一个进程中扩容 GPU 意味着扩容整个进程——CPU 资源也随之增加但 CPU 利用率可能只有 20%。反之当预处理成为瓶颈如中文 tokenizer 对长文本的分词耗时较长增加 CPU 并不能缓解 GPU 的压力因为 GPU 推理是独立的瓶颈点。流水线并行的核心思路是将三个阶段拆成独立服务各自独立部署、独立扩缩容、独立优化。二、流水线并行架构的底层机制对比两种模式的数据表现可以清晰看出差异在串行模式下请求依次经历预处理CPU 20ms、推理GPU 350ms和后处理CPU 15ms总延迟约为 385ms。此过程中GPU 需等待预处理完成而空闲 20msCPU 需等待推理完成而空闲 350ms资源利用率极低。而在流水线并行模式中三个阶段被拆分为独立服务通过消息队列或共享内存连接。预处理服务可并发处理多条请求将平均等待时间降至 5ms推理服务通过 Batch 聚合将吞吐提升 2-3 倍后处理服务异步消费输出避免阻塞 GPU 资源释放。这种架构消除了阶段间的空闲等待实现了资源的独立扩缩容。流水线并行的关键机制是解耦 Batch 聚合预处理阶段解耦tokenizer 分词和输入编码是纯 CPU 操作延迟约 10~30 ms。独立部署后预处理服务可以独立扩缩 CPU 实例数量保持 GPU 推理服务始终有预处理好数据可供消费消除 GPU 空闲等待时间。预处理服务通过消息队列如 Redis Stream 或 Kafka将编码后的 tensor 数据推送到推理服务的输入队列。推理阶段 Batch 聚合GPU 推理是吞吐导向的Batch Size 越大吞吐越高。独立部署后推理服务可以从输入队列中聚合多条请求一起推理而不是每次只处理一条。Dynamic Batching 的聚合窗口设为 50 ms 时平均可以聚合 815 条请求Batch Size 从 1 提升到 815GPU 算力利用率从 40% 提升到 90%。后处理阶段解耦输出解码和响应序列化同样是纯 CPU 操作延迟约 10~20 ms。独立部署后后处理服务异步消费推理输出不阻塞 GPU 释放 KV Cache 资源。GPU 推理完成一条请求后立即释放其 KV Cache后处理在独立进程中完成——这在大并发场景下尤其重要因为 KV Cache 是最稀缺的 GPU 显存资源。三、流水线并行架构的生产级实现以下代码展示推理流水线并行架构中各阶段的核心实现逻辑使用 Redis Stream 作为阶段间通信管道。package pipeline import ( context encoding/json fmt time github.com/redis/go-redis/v9 ) // PipelineStage 定义流水线阶段的通用接口 type PipelineStage interface { Process(ctx context.Context, input []byte) ([]byte, error) OutputStream() string } // PreprocessStage 预处理阶段tokenizer 分词 输入编码 type PreprocessStage struct { redisClient *redis.Client streamName string tokenizer Tokenizer } func (p *PreprocessStage) Process(ctx context.Context, input []byte) ([]byte, error) { var req InferenceRequest if err : json.Unmarshal(input, req); err ! nil { return nil, fmt.Errorf(预处理请求解析失败: %w, err) } // tokenizer 分词返回 token ID 序列 tokens, err : p.tokenizer.Encode(req.Prompt) if err ! nil { return nil, fmt.Errorf(预处理分词失败: %w, err) } encoded : PreprocessOutput{ RequestID: req.ID, Tokens: tokens, Params: req.Params, } data, _ : json.Marshal(encoded) // 推送到推理服务的输入队列 _, err p.redisClient.XAdd(ctx, redis.XAddArgs{ Stream: p.streamName, Values: map[string]interface{}{data: string(data)}, MaxLen: 10000, // 限制队列长度防止积压失控 }).Result() return data, err } // InferenceStage 推理阶段GPU 前向计算 Dynamic Batching type InferenceStage struct { redisClient *redis.Client inputStream string outputStream string batchWindow time.Duration // Batching 聚合窗口 maxBatchSize int // 最大 Batch Size } func (i *InferenceStage) Run(ctx context.Context) error { for { // 从输入队列读取请求聚合窗口 50ms entries, err : i.redisClient.XRead(ctx, redis.XReadArgs{ Streams: []string{i.inputStream, 0}, Count: int64(i.maxBatchSize), Block: i.batchWindow.Milliseconds(), }).Result() if err ! nil { continue // 超时或连接异常重试 } if len(entries) 0 || len(entries[0].Messages) 0 { continue // 空队列等待下个周期 } // 聚合多条请求为一个 Batch batch : i.aggregateBatch(entries[0].Messages) results, err : i.executeBatch(ctx, batch) if err ! nil { // Batch 推理失败时逐条重试而非丢弃整批 i.handleBatchError(ctx, batch, err) continue } // 推送推理结果到后处理队列 for _, result : range results { data, _ : json.Marshal(result) i.redisClient.XAdd(ctx, redis.XAddArgs{ Stream: i.outputStream, Values: map[string]interface{}{data: string(data)}, MaxLen: 10000, }) } // 确认已处理的消息避免重复消费 ids : make([]string, len(entries[0].Messages)) for idx, msg : range entries[0].Messages { ids[idx] msg.ID } i.redisClient.XDel(ctx, i.inputStream, ids...) } }实测数据LLaMA-2-13BA100-80G并发 64输入 512 token部署模式P50 延迟 (ms)P99 延迟 (ms)吞吐 (req/s)GPU 利用率CPU 利用率预处理串行单进程42018504.845%18%流水线并行无 Batching3809505.552%35%流水线并行 Dynamic Batching3506808.292%40%流水线并行 Dynamic Batching 的组合收益最大吞吐提升 71%P99 延迟降低 63%GPU 利用率从 45% 提升到 92%。单独的流水线并行无 Batching收益有限因为核心瓶颈是 GPU 算力利用率而非阶段间的串行等待。四、流水线并行的 Trade-offs 与边界约束流水线并行引入的最大代价是系统复杂度。三个独立服务意味着三套部署、三套监控、三套扩缩容策略、三个故障点。任何一个阶段的宕机都会导致整个流水线中断。预处理服务宕机后推理服务的输入队列清空GPU 空转推理服务宕机后后处理服务无数据可消费后处理服务宕机后用户请求无法返回结果。消息队列的选择同样影响延迟。Redis Stream 的延迟约 13 ms但吞吐上限约 10 K msg/sKafka 的延迟约 510 ms但吞吐上限可达 100 K msg/s。对于推理场景Redis Stream 的延迟优势更重要——推理请求是低频高延迟的吞吐瓶颈在 GPU 而非消息队列。但 Redis Stream 的持久化能力不如 KafkaRedis 重启后未消费的消息可能丢失。Dynamic Batching 的聚合窗口是吞吐与延迟的权衡点。窗口越长聚合的请求越多Batch Size 越大吞吐越高但每条请求的等待时间也越长。聚合窗口 50 ms 在并发 64 时平均聚合 8 条请求P50 延迟增加约 25 ms。如果聚合窗口设为 200 ms吞吐可提升到 12 req/s但 P50 延迟增加到 550 ms——已经超过用户容忍阈值。流水线并行的适用边界只有当三个阶段的耗时差异显著至少 5 倍且并发量足够高至少 30时拆分才有明显收益。对于低并发场景 10 req/s串行模式的延迟和吞吐完全够用拆分后的系统复杂度不值得。对于推理耗时占比超过 90% 的场景如超大模型的长序列推理预处理和后处理的耗时微不足道拆分后几乎没有收益。五、总结推理流水线并行通过拆分预处理、推理和后处理为独立阶段实现了资源利用率的最大化。核心收益来自 Dynamic Batching 聚合——流水线拆分本身只解决了串行等待问题真正的大幅收益来自 GPU 推理的 Batch 聚合策略。实测数据显示流水线并行 Dynamic Batching 的吞吐提升 71%P99 延迟降低 63%。落地路线先评估三个阶段的耗时比例只有推理耗时占比超过 50% 且与预处理/后处理耗时差异超过 5 倍时才考虑拆分。预处理和后处理用 Go 实现推理用 Python/vLLM通过 Redis Stream 连接延迟 1~3 ms。Dynamic Batching 聚合窗口设为 50 ms并发超过 30 时 Batch Size 可达 8~15GPU 利用率提升到 90%。三个阶段独立扩缩容预处理基于 CPU 利用率推理基于 GPU 显存利用率和队列深度后处理基于 CPU 利用率和输出队列深度。设置全局超时兜底如果任一阶段积压超过阈值直接拒绝新请求而非让队列无限膨胀。