尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

长上下文推理降本新思路:Prefill-as-a-Service与PD分离架构解析

长上下文推理降本新思路:Prefill-as-a-Service与PD分离架构解析 长上下文推理正在从“能用”走向“好用”但成本问题始终是一道坎。上下文窗口从 8K 扩展到 128K、1M显存消耗和预填充计算量都在快速上升。摩尔线程近期发布《MTT S5000 Prefill-as-a-Service 技术白皮书》把 Prefill 阶段单独拆出来作为一种服务形态思路值得认真拆解。本文将从长上下文推理的痛点切入讲清楚 Prefill-as-a-Service 的核心原理、MTT S5000 在其中的定位以及如果要自己落地这类架构应该怎么设计、怎么评估、怎么避坑。1. 长上下文推理的成本瓶颈在哪里1.1 一次推理被拆成两个阶段在聊 Prefill-as-a-Service 之前需要先把大模型推理的基本过程讲清楚。以典型的自回归生成模型为例一次完整的请求处理可以分为两个阶段。Prefill 阶段也叫预填充阶段。用户输入的一段 Prompt 会被一次性送入模型Transformer 的每一层都要对整段输入做并行计算得到每个 token 对应的 Key 和 Value写入 KV Cache。这个阶段的特点是“一次算完”计算量大致正比于输入序列长度的平方。Decode 阶段也叫解码阶段。模型逐 token 生成输出每生成一个 token都要读取已有的 KV Cache再写入新的 KV然后做一次前向计算。这个阶段的特点是“逐 token 串行”访存开销远大于计算开销。在很多推理框架中这两个阶段共享同一批 GPU 资源排队、抢占、算力错配的问题也就随之而来。判断一次推理请求“快不快”通常会看两个指标TTFTTime To First Token从请求发出到收到第一个输出 token 的时间主要取决于 Prefill 阶段。TPOTTime Per Output Token每生成一个 token 的平均耗时主要取决于 Decode 阶段。长上下文场景下Prefill 阶段的计算量和显存占用会急剧膨胀TTFT 被拉长整个系统的吞吐也会被拖累。1.2 Prefill 阶段为什么“烧钱”可以从三个维度看 Prefill 的成本压力。第一是计算量。输入越长Prefill 阶段需要做的矩阵乘法规模越大。比如输入长度从 1K 涨到 32KPrefill 计算量并不是线性增长而是近似二次增长。这是因为注意力机制中每个 token 都要和其他所有 token 计算相关性。第二是显存。Prefill 阶段要临时保存大量的中间状态同时生成完整的 KV Cache。假设模型是 7B 参数、使用 FP16 精度当上下文达到 128K 时单条请求的 KV Cache 就可能占用数 GB 显存。如果服务需要支撑高并发显存会迅速成为瓶颈。第三是资源利用率。Prefill 阶段计算密集GPU 的算力可以打得很满Decode 阶段访存密集算力利用率反而不高。当两者混在同一批 GPU 上时GPU 一会儿要应付大矩阵乘一会儿要处理小批量逐 token 推理调度开销大整体利用率很难拉高。换句话说长上下文推理贵不是贵在“模型参数大”而是贵在“Prefill 计算 KV Cache 显存”这两项开销被放大了。1.3 传统单阶段部署的算力错配“错配”是理解 Prefill 服务化的关键。在传统部署方式中一个推理实例同时承担 Prefill 和 Decode。这种模式在短上下文、低并发场景下问题不大但在长上下文场景下会暴露两个典型问题。第一长请求会阻塞短请求。一个 32K 输入的 Prefill 请求可能占住 GPU 很长时间后续的短请求只能在队列里等TTFT 飙升。第二算力浪费。为了满足 Prefill 阶段的大计算量你会采购高算力 GPU但同样一块 GPU 在处理 Decode 阶段的逐 token 生成时算力利用率往往不到 Prefill 阶段的一半。也就是说硬件买来是按峰值算力付费的但实际跑的时候平均利用率并不高。这个问题不能靠单纯“堆卡”解决。堆卡能提升总吞吐但无法解决算力类型不匹配的问题。要真正降本需要让不同类型的计算跑在更匹配的硬件和调度策略上。1.4 需要新的软件架构而不是只换硬件如果只是把 GPU 换成更大显存、更高算力的型号问题能缓解但成本仍然高。因为瓶颈不只是硬件能力还包括软件调度方式。把 Prefill 和 Decode 拆开、分别调度、分别部署是业界逐步形成的一种共识。这种思路下负责 Prefill 的节点可以专注处理长输入、大计算量任务负责 Decode 的节点可以专注做低延迟逐 token 生成。两者之间通过 KV Cache 传递中间结果。摩尔线程提出的 Prefill-as-a-Service正是沿着这个方向做的一种工程化产品形态。下面我们把这种架构拆开来看。2. 什么是 Prefill-as-a-Service2.1 从“一个模型做完”到“两个服务协作”Prefill-as-a-Service 直译过来是“预填充即服务”。它不是一个新的模型也不是一个新的推理算法而是把原来一个推理实例内部的 Prefill 阶段拆成一个独立的、可被远程调用的服务。在传统架构中一次请求的处理流程是用户请求 - 推理实例Prefill Decode - 返回结果在 Prefill-as-a-Service 架构中流程变成用户请求 - Prefill 服务生成 KV Cache - Decode 服务基于 KV Cache 逐 token 生成 - 返回结果Prefill 服务收到完整输入后只负责把输入编码成 KV Cache然后把 KV Cache 传给 Decode 服务。Decode 服务不需要重新处理整段输入直接从已有 KV Cache 开始生成。这种拆分的本质是把“高计算量、高并发度”的任务和“低计算量、高访存量”的任务放到不同的资源池中处理。2.2 Prefill 服务化的三大收益收益一减少算力浪费。PreFill 服务可以用一批偏计算型的 GPU 承载Decode 服务用另一批偏吞吐型的 GPU 承载。各自选型更精准综合成本更低。收益二缩短长请求的 TTFT。专们服务于 Prefill 的节点可以集中算力处理长输入不再被 Decode 阶段拖慢长上下文请求的首 token 延迟会明显改善。收益三提升系统的稳定性和扩展性。Prefill 服务的负载和 Decode 服务的负载可以分别扩缩容。比如白天短对话多就给 Decode 服务扩容遇到批量文档分析任务就优先扩容 Prefill 服务。2.3 与常见推理框架里的 Chunked Prefill 的区别很多同学可能会把 Prefill-as-a-Service 和推理框架中的 Chunked Prefill 混在一起。这里要做一个区分。Chunked Prefill 是把一个长输入的 Prefill 计算拆成多个 chunk插入到 Decode 过程中交错执行目的是避免 Prefill 长时间独占 GPU。它仍然发生在同一个推理实例内部。Prefill-as-a-Service 则是把 Prefill 阶段从主推理链路中拆出去独立部署、独立扩展。它属于架构层面的拆分比 Chunked Prefill 更彻底。两者可以结合使用。Prefill 服务内部也可以把长请求拆成多个 chunk 来调度但对外来看它已经是一个独立的服务单元。2.4 一张图看懂 PD 分离这里用 ASCII 简图表达 Prefill/Decode 分离的整体链路用户请求 | v --------------------- | Prefill 服务节点 | | - 长输入编码 | | - 生成 KV Cache | | - 语义/前缀缓存 | --------------------- | | KV Cache通过共享存储或网络传输 v --------------------- | Decode 服务节点 | | - 逐 token 生成 | | - 实时交互输出 | --------------------- | v 用户收到流式结果实际工程实现中KV Cache 的传递方式有几种选择通过共享内存传递、通过分布式缓存服务传递、或者通过序列化后走网络传输。不同方案在延迟、带宽、复杂度上各有取舍后面会展开讲。3. MTT S5000 在 PaaS 架构中的定位3.1 MTT S5000 的产品定位根据摩尔线程公开信息MTT S5000 是面向大模型训练与推理场景的服务器级 GPU基于摩尔线程 MUSA 统一系统架构。它的典型特征是大显存、高带宽适合承载大模型这类显存和带宽敏感型负载。需要提醒的是本文不做具体产品参数的罗列因为公开渠道可确认的细节有限且产品参数可能随版本迭代变化。我们更值得关注的是它在架构层面的定位MTT S5000 在 Prefill-as-a-Service 场景中扮演的角色是“Prefill 计算节点的算力底座”。也就是说它承担的任务不是决定整个推理链路怎么跑而是为 Prefill 阶段的密集矩阵计算和大规模 KV Cache 读写提供更合适的硬件支撑。3.2 为什么专用的 Prefill 加速卡能压低成本一个容易被忽视的常识是Prefill 阶段和 Decode 阶段对硬件资源的偏好是不同的。Prefill 阶段是典型的 compute-bound计算密集型。它需要高算力、大显存能一次性把很长的输入序列完成前向计算同时缓存住大量 KV。Decode 阶段是 memory-bound访存密集型。单个 token 的计算量不大但需要频繁读写 KV Cache因此更依赖显存带宽和低延迟。如果一块 GPU 同时跑两种负载选型时只能两头兼顾实际使用时两头都吃不满。而把 Prefill 拆分出来后Prefill 节点可以按“算力优先”来部署Decode 节点按“吞吐优先”来部署。每一块卡都跑自己最擅长的任务单位 token 的生产成本自然下降。3.3 白皮书披露的技术方向摩尔线程这份白皮书的核心价值不在于“发布了一款新 GPU”而在于给出了一个面向长上下文推理的端到端架构思路。公开信息显示白皮书围绕 Prefill-as-a-Service 展开重点讨论长上下文推理场景下的成本优化、资源调度和性能评估方法。对开发者而言真正值得关注的并不是某一张卡的具体跑分而是这种“服务化拆分”的方法能不能在自己的业务中复用。白皮书把 Prefill 服务化为一种可运营、可调度、可计费的独立服务这相当于把大模型推理从“单机单卡跑模型”升级成了“分布式流水线生产”。这种思路上的转变比单纯看某一项硬件指标更有参考意义。理解这一点再看后面的架构设计和代码示例思路会顺很多。4. 环境准备与部署整体思路4.1 参考部署拓扑由于 Prefill-as-a-Service 依赖具体的硬件环境、推理框架和网络拓扑本文不写死某个版本而是给出一个通用的部署思路。你可以把下面的拓扑当作设计蓝图再按自己的实际环境调整。Node APrefill 服务节点 - 1 台服务器配置 MTT S5000 或同类大显存 GPU - 部署 Prefill Worker - 对外暴露 gRPC/HTTP 接口接收长输入 Node BDecode 服务节点 - 1 台或多台服务器配置推理 GPU - 部署 Decode Worker - 从 KV Cache 系统读取中间状态逐 token 生成 Node C调度与缓存节点 - KV Cache 存储共享文件系统 / 分布式缓存 / 对象存储 - 请求调度器决定哪些请求走 PaaS哪些走本机推理在实际生产环境中Node C 可以是一个分布式 KV Cache 集群也可以复用 Redis、etcd 等基础设施关键是具备足够的带宽和低延迟访问能力。4.2 服务组件划分从一个可落地的最小系统来看Prefill-as-a-Service 需要包含四个核心组件请求入口服务接收用户请求判断请求是长上下文还是短上下文决定是否走 Prefill 服务。Prefill Worker加载模型权重对输入做 Prefill 计算生成 KV Cache并写入 KV Cache 存储。KV Cache Manager管理 KV Cache 的写入、读取、过期和复用。Decode Worker从 KV Cache Manager 拉取对应请求的 KV执行自回归生成。这四个组件可以是四个独立进程也可以部署在不同机器上。关键设计原则是Prefill Worker 不参与逐 token 生成Decode Worker 不参与长输入编码。4.3 版本提醒大模型推理框架迭代非常快vLLM、SGLang、TensorRT-LLM 等框架对 PD 分离的支持程度各不相同。有的框架已经支持 KV Cache 远端传输有的还停留在单机内部拆分。在实际部署前务必确认以下几点推理框架是否支持导出 Prefill 阶段的 KV CacheKV Cache 的传输格式是否开放是否跨语言可读框架版本升级会不会破坏 KV Cache 的兼容性硬件平台对远程 KV Cache 访问的网络延迟是否可接受。下面给出的示例代码主要用于理解 Prefill-as-a-Service 的核心流程不代表某一个具体框架的官方 API。5. 核心流程与代码示例5.1 Prefill Worker 服务端伪代码这里用一个简化到只保留核心逻辑的 Python 示例演示 Prefill Worker 的职责。实际项目中你会用推理框架的原生 API 替代下面手工实现的矩阵乘法但流程是一致的。# 文件路径prefill_worker.py import numpy as np class PrefillWorker: def __init__(self, model_dim: int, max_seq_len: int): # 实际项目中这里会加载真实模型权重 # 示例中仅创建随机权重用于演示流程 self.model_dim model_dim self.max_seq_len max_seq_len # 模拟模型权重W_Q, W_K, W_V 等 self.w_k np.random.randn(model_dim, model_dim).astype(np.float32) * 0.01 self.w_v np.random.randn(model_dim, model_dim).astype(np.float32) * 0.01 def prefill(self, input_ids: list[int]) - dict: 对输入的完整 token 序列执行 Prefill生成 KV Cache。 返回一个字典包含 K 和 V 矩阵。 seq_len len(input_ids) if seq_len self.max_seq_len: raise ValueError(finput seq_len {seq_len} exceeds max_seq_len {self.max_seq_len}) # 模拟 embedding将 token id 映射为向量 # 这里不做真实 embedding直接用随机矩阵代替 emb np.random.randn(seq_len, self.model_dim).astype(np.float32) # 模拟 QKV 投影 k emb self.w_k.T # shape: (seq_len, model_dim) v emb self.w_v.T # shape: (seq_len, model_dim) return {k: k, v: v}这段代码的核心意图是Prefill Worker 接收完整的输入 token 序列一次性计算并返回对应的 K 矩阵和 V 矩阵。这两份矩阵就是 Decode 阶段要反复读取的 KV Cache。5.2 KV Cache 传递格式说明Prefill Worker 算出的 KV Cache 需要传给 Decode Worker。最简单的格式是两个二维数组字段含义request_id请求唯一标识k_cache形状为 (seq_len, num_heads, head_dim) 的 Key 缓存v_cache形状为 (seq_len, num_heads, head_dim) 的 Value 缓存在真实框架中KV Cache 的维度通常比这个复杂有层数、头数、分块等维度。示例中为了可读性把维度简化为一层、单头。完整的 Prefill 响应协议可以设计为{ request_id: req-20240601-001, model_name: qwen2-7b, k_cache_shape: [128, 16, 128], v_cache_shape: [128, 16, 128], k_cache_path: /kv_cache/req-20240601-001/k.bin, v_cache_path: /kv_cache/req-20240601-001/v.bin }这里的 k_cache_path 和 v_cache_path 指向 KV Cache 的存储位置。Decode Worker 根据路径读取缓存而不是通过 JSON 传输大数组这样可以避免巨大的网络开销。5.3 Decode 侧如何消费 Prefill 结果Decode Worker 拿到 KV Cache 后只需做下一步工作根据缓存继续生成 token。# 文件路径decode_worker.py import numpy as np class DecodeWorker: def __init__(self, model_dim: int): self.model_dim model_dim # 模拟权重输出投影 self.w_out np.random.randn(model_dim, model_dim).astype(np.float32) * 0.01 def decode_one_token(self, kv_cache: dict, last_token_emb: np.ndarray) - int: 输入一个 token 的 embedding根据已有的 KV Cache 生成下一个 token。 这里省略了注意力计算只保留核心调用结构。 # 实际中会依据 kv_cache[k] 和 kv_cache[v] 计算注意力 logits last_token_emb self.w_out.T next_token_id int(np.argmax(logits)) return next_token_id需要注意Decode Worker 每生成一个 token都会把新的 K、V 追加到 KV Cache 中。在真实框架中这意味着 KV Cache 是动态增长的Decode Worker 需要支持 KV Cache 的追加写入。5.4 调度器 demo长短请求拆分调度器的作用是在请求进入时决定走哪条链路。短请求直接在本机完成 Prefill Decode长请求则走 Prefill-as-a-Service。# 文件路径scheduler.py class RequestScheduler: def __init__(self, prefill_client, decode_client, local_engine, max_local_prefill_len2048): self.prefill_client prefill_client self.decode_client decode_client self.local_engine local_engine self.max_local_prefill_len max_local_prefill_len def dispatch(self, input_ids: list[int]) - str: seq_len len(input_ids) if seq_len self.max_local_prefill_len: # 短请求本机直接推理 result self.local_engine.generate(input_ids) return result # 长请求走 Prefill-as-a-Service kv_cache self.prefill_client.prefill(input_ids) result self.decode_client.generate(kv_cache) return result这个调度器的阈值设计非常重要。阈值设得太低短请求也会频繁走远端服务网络开销会吃掉性能阈值设得太高长请求仍会堵在本机。最优阈值需要通过压测确定一般结合“本机 Prefill 延迟”和“远端 Prefill 延迟 网络传输延迟”两条曲线来选择交叉点。6. 性能评估与成本分析6.1 关键指标定义评估 Prefill-as-a-Service 是否有效不能只看单卡跑分需要从服务维度定义指标。指标定义关注原因Prefill 吞吐单位时间内处理的输入 token 数衡量 Prefill 服务的处理能力平均 TTFT请求发出到首 token 返回的延迟衡量用户体验TPOT每生成一个 token 的耗时衡量 Decode 服务质量KV Cache 命中率请求可以复用已有 KV Cache 的比例命中率越高成本越低GPU 综合利用率Prefill 节点和 Decode 节点的利用率加权值衡量硬件是否被吃满6.2 对比方法建议做三组对比实验基线不拆分单实例同时处理 Prefill 和 Decode。方案 A使用 Chunked Prefill在实例内部交错调度。方案 B使用 Prefill-as-a-ServicePrefill 和 Decode 独立部署。控制变量要做到同样的模型、同样的输入长度、同样的并发数、同样的 GPU 总预算。这样对比出来的 TTFT、吞吐和成本数据才有参考价值。6.3 如何评估“成本下降”是否真实成本下降不能只看硬件采购价格还要看单位有效 token 的综合成本。一个可用的简化公式是单位 token 成本 硬件成本 电费 网络带宽成本/ 有效完成的 token 数拆分后Prefill 节点和 Decode 节点使用不同类型的 GPU单卡平均利用率上升波峰波谷的资源缺口更小单位 token 成本通常会下降。但如果你的业务中 80% 以上请求都是短请求拆分带来的收益就不明显因为大部分请求根本不会走到 Prefill 服务。6.4 长上下文场景下的显存估算做一个粗略估算帮助理解为什么长上下文必须考虑 KV Cache 优化。假设模型隐藏层维度为 d层数为 LKV Cache 显存大致为KV Cache 显存 ≈ 2K 和 V * L * seq_len * d * 每个元素字节数以 7B 模型为例常见配置是 d4096L32。如果每个元素用 FP16 存储2 字节一条 128K 上下文的请求需要的 KV Cache 显存大约是2 * 32 * 131072 * 4096 * 2 字节 ≈ 68.7 GB这个数量级已经超过很多单卡显存。因此长上下文推理在工程上必须配合 KV Cache 量化、缓存复用、PD 分离等手段否则成本会失控。这也是 Prefill-as-a-Service 存在的重要背景。7. 常见问题与排查思路在实际落地 Prefill-as-a-Service 的过程中团队最容易踩到下面几个坑。问题现象常见原因解决思路长请求 TTFT 反而更高网络传输 KV Cache 消耗时间过长使用共享存储或 RDMA减少序列化开销KV Cache 读出来后结果不对传输过程中精度丢失或维度错位增加校验和对比 K/V 矩阵维度Prefill 节点利用率很低调度策略没有把长请求集中到 Prefill 节点调整调度阈值增加长请求路由规则显存不足 OOMKV Cache 没有及时清理或复用策略缺失引入 LRU 淘汰策略定期清理过期缓存Decode 节点成为瓶颈所有请求都集中在 Decode 侧串行生成扩大 Decode 节点数量做负载均衡短请求也被远程调度调度阈值设置不合理通过压测确定最优阈值7.1 排查 KV Cache 不一致问题KV Cache 不一致是 PD 分离架构里最隐蔽的问题。任何一个维度的错位都会导致生成结果错误或乱码。建议按以下顺序排查确认 Prefill 输出的 K/V 张量 shape 与 Decode 侧模型期望的 shape 一致。确认数据类型一致比如都是 FP16 还是 BF16。确认是否需要归一化或分离头维度的 reshape。确认 KV Cache 存储路径上的二进制文件没有因为写入中断而残缺。7.2 排查网络传输瓶颈如果 Prefill 节点算得很快但整体 TTFT 没降下来多半是卡在网络传输。可以从两点入手压缩传输对 KV Cache 做量化或压缩减少网络负载。就近调度让 Prefill 节点和 Decode 节点部署在同一机房通过内网高速链路传输。8. 工程落地最佳实践8.1 按场景选择拆分策略Prefill-as-a-Service 不是万能方案。以下场景最适合拆分需要频繁处理长文档、长代码、多轮长对话输入长度分布极不均匀长请求和短请求混合对 TTFT 敏感希望长请求延迟可控希望用更便宜的方案承接长上下文流量。如果业务基本是短输入或者并发压力不大引入 PD 分离反而会增加系统复杂度和网络开销不建议盲目跟风。8.2 KV Cache 缓存复用长上下文场景下很多请求可能共享同样的前缀比如同一个系统提示词、同一份文档背景。可以设计一个“前缀缓存”层把高频前缀对应的 KV Cache 缓存起来。新请求到达时只需计算增量部分能大幅降低 Prefill 计算量。实现时需要注意缓存一致性当模型版本升级或 Prompt 模板变化时旧缓存必须立即失效否则会输出错误结果。8.3 网络与调度设计KV Cache 的传递是 PD 分离架构中最容易出问题的一环。建议优先考虑共享内存或本地文件系统如果必须走网络优先选择 RDMA 或 InfiniBand 等低延迟方案。调度器不要做成单点。请求量上来后调度器本身可能成为瓶颈需要在调度层做水平扩展同时引入请求队列和背压机制避免请求堆积。8.4 可观测性建设PD 分离之后一个请求会经过多个服务排查问题的难度会显著上升。建议在早期就建立全链路追踪每个请求都带上 request_id记录各个阶段耗时请求接入耗时Prefill 计算耗时KV Cache 写入耗时KV Cache 传输耗时Decode 平均生成耗时8.5 安全与权限边界Prefill 服务作为一个独立服务需要考虑接口鉴权、访问控制和数据隔离。特别是 KV Cache 中可能包含用户输入的内容缓存存储必须做好权限控制避免越权读取。生产环境变更前务必先在测试环境验证 KV Cache 格式兼容性和模型输出一致性再逐步灰度到生产流量。9. 总结从硬件到架构的协同优化摩尔线程《MTT S5000 Prefill-as-a-Service 技术白皮书》带来的核心启发不是“某张卡有多强”而是“长上下文推理的优化应该从架构层面入手”。Prefill 阶段和 Decode 阶段对算力资源的需求差异如此之大把它们继续混跑在同一个实例里本质上是对硬件算力的浪费。通过把 Prefill 服务化将 KV Cache 作为中间产物在服务间传递可以让不同硬件承担不同任务。MTT S5000 这类大显存 GPU 在 Prefill 场景中的定位是作为高计算密度的算力底座配合调度器、缓存管理、网络传输共同降低长上下文推理的单位 token 成本。如果你正在做长上下文推理相关的工程开发可以先从这三个方向入手验证第一步梳理当前请求的输入长度分布确认长请求占比第二步在推理框架中开启 KV Cache 导出功能单独跑一个 Prefill Worker 做压测第三步对比拆分前后的 TTFT、吞吐和单位 token 成本再决定是否继续投入。本文涉及的架构思路和代码示例可以作为你手头方案的参考框架。
返回列表