Perplexity工程团队日前发布技术博客《Fast Embeddings on GPUs》,详细披露其嵌入模型服务架构及支撑pplx-embed的推理系统设计。据该团队介绍,嵌入模型在GPU侧的推理效率已在成熟的Hopper和Blackwell硬件上趋于一致,真正的性能提升来自模型周围的运行时与框架层,包括CUDA图管理、异步结果跟踪抽象和Rust请求路径。
Perplexity将嵌入服务分为两类工作负载:批量嵌入用于构建或重建向量数据库,以吞吐量为优先;在线嵌入在查询时处理短查询,要求低延迟。排序任务则介于两者之间,在向量检索后对大批文档进行评分,需要兼顾吞吐与延迟。关键设计决策是,团队没有为嵌入单独构建引擎。由于嵌入模型是小型Transformer,批量嵌入类似于计算密集的预填充阶段,在线嵌入则往往只有几个token,类似于访存受限的解码阶段,因此直接复用了大规模语言模型中的预填充和解码内核。
服务链路中涉及三个组件:Ivy是Rust编写的HTTP网关,负责JSON解析、分词、输入模板化和批切分等CPU侧工作,并将请求转为自定义gRPC协议,同时将大批请求切块并在多副本间负载均衡,以纠正生产环境中载荷大小不一导致的负载不均。Tulip是推理服务器接口,使用Rust、tokio和tonic构建gRPC服务,在调度和批处理后将请求分发给引擎。ROSE(Runtime-Optimized Serving Engine)实现模型推理,主要使用Python,提供内核、层和模型定义,管理CUDA图,并向Tulip暴露step()函数。
Perplexity解释称,其调度器刻意保持简单,采用先到先服务的方式在请求累积时选取序列。理由是,对于所服务序列长度下的嵌入小模型,稠密层的线性成本远高于注意力机制的二次方成本,延迟大致与token数量成正比,而非序列数。当一个批处理在子十亿参数模型上达到约512个token的GPU饱和点时,继续增加序列不会提高效率。
在CUDA图方面,Perplexity为所有嵌入模型构建了全模型CUDA图,将每次内核启动合并为单次驱动调用,以避免CPU端启动开销超过GPU执行时间。由于模型小,当批次达到数千token和数十序列时,GPU计算量才开始超过启动成本。部分注意力实现依赖动态主机端输入,从而阻碍了全模型图的捕获,团队为此向上游FlashInfer提交了更改以支持捕获。CUDA图需按配置捕获,token数量被填充到64或256的倍数桶中,但每个模型仍会生成数千个图,捕获耗时数分钟。团队采用惰性捕获策略:每个配置先进行一次热启动,第二次命中时才触发捕获和重放。这会增加启动阶段的p99延迟,但将数分钟的即时工作分散到数小时内。
另一项优化是LazyTensor,它跟踪页锁定主机缓冲区、cudaMemcpyAsync和CUDA事件。step()不再阻塞等待设备,而是返回LazyTensor,使Rust异步任务可以等待第N批完成的同时,CPU侧继续排队第N+1批。