ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型API请求全链路:从Harness编排到KV Cache优化

大模型API请求全链路:从Harness编排到KV Cache优化 说句实在话我入这行这么多年真正让我把“大模型 API 请求”这件事彻底想明白的不是读论文而是某次线上故障。那天服务端监控面板上一个推理节点的 GPU 利用率只有 30%但请求排队时间却在持续上涨用户那边已经开始骂街了。我盯着面板想了很久——模型没崩、机器没挂、网络也没问题那请求到底卡在哪了后来一路追下去才发现问题出在一个大多数人根本不会注意到的环节请求从进来到真正落到 GPU 上计算中间隔着一层叫 Harness 的编排逻辑这层逻辑处理得不好GPU 再强也白搭。也是从那次之后我开始认真梳理“一次大模型 API 请求从发起到返回完整跑起来到底经历了什么”。这个问题看起来基础但牵扯到的知识点横跨网关、调度、显存管理、并发控制每一个环节都有可以深挖的原理和坑。这篇文章我想用一条完整的链路串起这些事请求怎么进 Harness怎么被组装和路由到了 GPU 之后预填充和解码两个阶段分别干了什么一个 GPU 凭什么能同时服务上百个并发请求以及那个既提升吞吐又吃显存大户的 KV Cache到底该怎么算、怎么管。适合正在做大模型 API 服务、推理部署、或者刚开始接触大模型工程化的朋友你们看完之后再遇到延迟高、吞吐上不去、显存不足这类问题就会有比较清晰的排查思路了。1. 从一行 curl 到 GPU 内核请求到达前的那段路很多人都以为调用大模型 API 就是把 prompt 丢过去然后等结果回来。真的是这样吗如果你自己用 Python 写个脚本不加任何框架直接往模型的 HTTP 接口灌请求那确实就这么简单。但一旦到了生产环境尤其是面向多个业务线、多个用户角色的服务事情就完全不一样了。1.1 入口网关请求进门的第一道关卡先看客户端。不管你是用 curl、OpenAI SDK还是自己封装的 HTTP 客户端请求进来的第一个地方不是一个“模型”而是一个 API 网关或者负载均衡层。这一层负责的事情非常务实鉴权认证、限流、参数校验、灰度路由。我见过很多刚接触大模型服务的同学一上来就盯着 GPU 利用率看觉得 GPU 是瓶颈。实际上在流量高峰时最先扛不住的往往是网关。比如某个客户端因为代码 bug循环发起重试请求瞬间把入口打满网关层直接 429。这种问题跟模型推理没有任何关系但表现出来就是“API 请求特别慢、大量超时”。所以说分析大模型 API 请求链路第一步要分清哪些工作是在 GPU 之外完成的。鉴权、计费、限流这些逻辑通常跑在普通的 CPU 服务器上和模型推理完全是两条路径。很多所谓“大模型 API 响应慢”的问题最后定位到的是网关层的限流策略过于激进或者鉴权服务本身响应超时压根没走到推理那一步。1.2 Harness 到底在编排什么会话、工具与模型路由过了网关请求就来到了一个容易被忽略的环节——Harness。这个词在不同语境下含义略有不同但在我这里我把它理解成一个“把客户端请求转换成模型实际输入”的编排层。它做的事情包括但不限于拉取会话历史、组装 system prompt、决定走哪个模型、要不要挂工具调用、响应回来后怎么解析和回传。我举一个实际例子。假设你的业务是一个客服机器人用户在网页上问了一句“我的订单什么时候到”。你从数据库里查到用户 30 天内的订单列表需要让模型根据这些数据生成回答。这一整套“查库、组装上下文、决定模型输出格式”的逻辑就应该写在 Harness 里而不是让模型自己去处理。Harness 提前把用户侧的数据准备好拼进 prompt 里模型只需要负责生成自然语言回复任务边界就非常清晰了。Harness 的另一个重要职责是模型路由。现在的 API 服务基本不可能只部署一个模型。一个平台里可能同时有轻量级模型负责简单的意图识别有中等级别模型负责日常对话还有最重的大模型负责复杂推理任务。Harness 会根据请求的特征——比如 prompt 长度、任务类型、用户等级——把请求路由到不同的模型上。这背后是一套规则或基于上下文的路由策略。做得好能显著降低整体推理成本做得不好所有请求都涌向最大的模型又慢又贵。1.3 上下文组装与 token 预算检查在 Harness 把请求真正送给模型之前还有一个关键步骤计算 token 数量并检查是否超出模型的上下文窗口。热度词里有一条错误信息很典型“error: 400 this models maximum context length is 1048576 tokens.” 这说明模型支持 100 万 token 的上下文窗口但你这次请求的内容加起来的 token 数超过了上限所以被直接拒绝了。这类检查必须在 Harness 层完成而不是等请求打到 GPU 再报错。原因很简单GPU 推理是昂贵资源把一个注定超出上下文的 prompt 送进 GPU既浪费算力又浪费时间。Harness 会使用分词器对 prompt 做一次快速 tokenize统计出总 token 数超过阈值就直接返回错误码。很多人会问为什么不能把上下文窗口设得更大一点这就涉及到了后面要讲的 KV Cache 显存开销问题——上下文越长KV Cache 占用显存越大GPU 能同时处理的并发数就越少。这是一个需要精心平衡的取舍后面会详细展开。2. 进入 GPU预填充、解码与张量在显存里的流动请求通过 Harness 校验和组装之后带着一个完整的 prompt正式进入推理引擎。这个时候决定性能的核心就从 CPU 上的业务逻辑切换到了 GPU 上的计算。在大模型推理的世界里一次请求的 GPU 计算分为两个截然不同的阶段预填充和解码。2.1 预填充阶段一次前向传播吃下整段 Prompt预填充英文叫 Prefill简单理解就是模型“第一次”看到你的完整 prompt并计算每个 token 对应的隐藏状态的过程。注意这个阶段是并行计算——GPU 会把 prompt 里的所有 token 同时喂给 Transformer 网络做一次完整的前向传播得到每个 token 的注意力表示和 Key/Value 向量。这一阶段计算量巨大但因为可以高度并行所以 GPU 的利用率通常很高时间上也相对可控。我打个比方。预填充阶段就像老师拿到一张试卷后先把整张卷子的题目通读一遍把每道题的题干、条件全部理解清楚。这个过程在极短的时间内完成且是并行的——老师可以同时看到所有题目而不是一道一道慢慢看。准确地说预填充阶段要做的事情包括Token Embedding 查找、多层 Transformer Block 计算包括自注意力、前馈网络、最后生成第一个输出 token 的 logits。这个阶段耗时主要受两个因素影响prompt 长度和模型大小。Prompt 越长需要计算的自注意力矩阵就越大耗时自然越长。2.2 解码阶段一个 Token 一个 Token 地挤出来预填充结束后模型进入解码Decode阶段。生成第一个 token 之后模型会把新生成的内容拼到原来的序列后面然后预测下一个 token如此循环。这个阶段最大的特点是串行。因为你只能在完整看到前一个 token 的基础上才能计算下一个 token 的概率分布不可能并行生成。这就非常像老师批改试卷了必须先批改第一题知道学生的答案才能对第二题的作答进行评判。每一步只能走一步走完一步才能看下一步。解码阶段的计算特征和预填充完全不同。它每次只输入一个新 token但需要和序列中所有历史 token 做注意力计算。这意味着每一步的计算量相对较小但内存带宽消耗巨大——因为要把整个模型的权重从显存中搬到计算单元里。GPU 的性能瓶颈从“算力不足”变成了“显存带宽不够用”这是理解大模型推理性能非常关键的一点。2.3 GPU 显存里到底装了什么权重、激活值、临时张量在深入后面的话题之前必须先把 GPU 显存里的布局说清楚。一次推理请求在 GPU 上运行显存主要被三部分占用第一部分是模型权重。70B 参数的模型用 FP16 精度存储权重本身就需要大约 140GB 显存。这部分是固定开销只要模型加载在那儿不管有没有请求都被占着。即便用 INT8 量化70B 也要 70GB 左右仍然是非常大的数字。第二部分是 KV Cache。每处理一个 token模型都要在每一层计算它对应的 Key 向量和 Value 向量。这些信息在后面生成后续 token 时需要反复用到所以必须存下来不能算完就扔。KV Cache 占用的空间跟上下文长度成正比这也是长上下文模型显存压力大的核心原因。第三部分是激活值Activation和中间变量。这部分相对动态但也占着不小的空间。在做前向传播时每一层的输出的张量都需要临时存一下用于后续层的计算。有些推理框架会做激活值重计算或者内存优化目的就是压缩这部分占用。理解了显存的这三个大头后面聊并发调度和 KV Cache 优化就有基础了。任何推理框架的显存优化手段归根结底都是在围绕这三块做文章减少权重占用、高效管理 KV Cache、降低激活值峰值。3. 并发一个 GPU 同时接待上百个请求的秘密很多刚做推理服务的人都会有一个疑问大模型生成一个 token 都要毫秒级一次完整回复要生成几百上千个 token那一个并发请求过来GPU 岂不是被占住几秒钟那怎么做到同时服务几十上百个请求的答案是并发并不是等一个请求完全结束再处理下一个而是多个请求在一个 GPU 上交错执行。这种机制的学名叫连续批处理Continuous Batching是大模型推理服务走向实用化的关键技术。3.1 从静态批处理到连续批处理调度粒度变成了一个 Token最早的推理服务实现或者说很多新手自己写的 Demo都是静态批处理Static Batching。思路很简单攒够一批请求比如 4 个然后一起喂给模型。等待这批全部生成完成后再处理下一批。问题显而易见如果这批请求里有一个生成了 1000 个 token其他三个只生成了 20 个 token那另外三个也必须干等着GPU 的有效利用率极其低下。连续批处理彻底改变了调度的粒度。它把调度单位从“整条请求”细化为“一次解码迭代”。在每个解码步骤调度器会让一批序列同时前向传播各算各的 logits然后各自采样出下一个 token。当某个序列生成了结束符达到最大长度它马上从这个批中退出腾出的位置立刻分配给一个新请求。想象一个火锅店的操作模式静态批处理就像包场——每桌客人进门后必须等人齐了才能开吃而且整店一次性只接待一批客人连续批处理则像正常营业——客人随时来有空桌就安排上吃完就走翻台率自然高得多。3.2 调度器的手里拿着什么牌序列状态与显存块实现连续批处理核心是调度器Scheduler需要对每个序列的运行状态了如指掌。这个请求当前生成到第几个 token 了、它占用了多少 KV Cache 块、它还剩多少显存配额、它是否已经生成了结束符。这些状态信息都保存在调度器的内存数据结构中每次迭代开始前调度器快速扫描一遍决定在这个 iteration 里让哪些序列进入计算。调度决策还涉及优先级策略。比如处于预填充阶段的新请求和正在解码的存量请求谁先谁后如果无条件优先预填充新请求可以很快吐出第一个 tokenTTFT 低但会挤占存量请求的计算资源导致它们的生成速度变慢。如果无条件优先解码存量请求体验好但新请求需要排队首 token 延迟暴涨。实际框架里通常会做“预填充抢占”之类的复杂调度策略在两者间动态平衡。3.3 并发上限卡在哪里不是 GPU 算力是显存理论上只要 GPU 显存装得下就能不断往计算批里塞新请求。但 KV Cache 会随着每个请求生成 token 而不断增大。所以真正限制并发数的最硬约束是显存中 KV Cache 块的剩余量。推理框架的调度器通常在初始化时把一部分显存固定划给 KV Cache 管理池划分成固定大小的块。每个请求根据自己当前的序列长度从池中领取需要数量的 KV 块。当池中空闲块数量不足以分配给一个新请求时调度器就得让请求排队等待直到池中出现空闲块。这也是为什么把上下文窗口设置得越大并发上限反而越低——大窗口意味着每个请求在极端情况下会占用巨大的 KV Cache 空间框架为了避免 OOM只能保守地限制并发数量。4. KV Cache提升吞吐的杠杆也是显存的大胃王现在可以好好讲讲 KV Cache 了。这个是整个大模型推理工程里最让人又爱又恨的角色。理解它既是你优化吞吐量的入口也是你控制显存成本的关键。4.1 为什么非缓存不可注意力计算背后的重复劳动为了说清楚这个问题我们回到 Transformer 的自注意力机制。在生成第 N 个 token 时模型需要计算这个 token 的 Query 向量然后跟序列中前 N-1 个 token 的 Key 向量逐一做点积得到注意力分数再用这些分数对前 N-1 个 token 的 Value 向量做加权求和。这个过程依赖先前所有 token 的 Key 和 Value 向量。如果不做缓存解码第 N 个 token 时就要把前面 N-1 个 token 的 Key 和 Value 重新计算一遍。生成第 N1 个 token 时又要重新算一遍前面 N 个 token 的。这种重复劳动的开销是 O(N^2) 级别的序列越长浪费越可怕。比如一个生成了 1000 个 token 的回答如果不做缓存每生成一个新 token 都要重新计算前面所有 token 的 KV 向量那计算量会膨胀得完全不可接受。KV Cache 的思路很简单粗暴第一次算出一个 token 的 Key 和 Value 向量时把它存进显存后面再用就直接读不重新算。这相当于把重复的计算换成了存储和读取是典型的以空间换时间策略。注意这里的“Cache”跟 CPU 或磁盘缓存的概念不完全一样。KV Cache 不是可选的优化而是几乎必须存在的机制。没有 KV Cache 的 Transformer 生成复杂度高到无法实用。4.2 KV Cache 显存开销的计算一个公式讲清楚到底一个请求会吃掉多少显存我直接给出一个可用的计算公式KV Cache 显存 2 × 层数 × 每个 token 的 KV 维度 × 序列长度 × 精度字节数更精确地说对一个 Transformer 模型假设有 L 层、每层有 H 个注意力头、每个头的维度是 D那么每个 token 每层产生的 KV 数据大小是 2 × H × DK 一份、V 一份。乘以层数 L再乘以序列长度 S再乘以每个元素占用的字节数FP16 是 2 字节就是总占用。我举个例子。一个 70B 参数的模型假设 80 层、80 个注意力头、每个头维度 128那么隐藏维度是 80 × 128 10240。每个 token 的 KV 占用是 2 × 80 × 10240 × 2 字节 3,276,800 字节约 3.125MB。如果一个请求的上下文长度是 4096 tokenKV Cache 占用是 3.125MB × 4096 ≈ 12.8GB。如果你同时处理 10 个这样的请求光 KV Cache 就是 128GB。这下你明白为什么大模型推理服务都拼命塞显存了吧。怪不得标题热词里有人在搜“kv cache计算”——这确实是做推理工程必须手算清楚的东西。4.3 缓存管理的工程实战分页、前缀复用和量化KV Cache 这么吃显存工程上自然有一套化解手段。我梳理几个最实用的方向这些在主流推理框架里都已经有成熟实现。第一是显存分块管理。与其给每个请求预分配一个连续的长序列缓存区不如把 KV Cache 划分成固定大小的块按需分配给请求。这就是 PagedAttention 的核心思想——像操作系统做虚拟内存分页一样管理 KV 块。好处是显存利用率大幅提升不会因为内部碎片浪费空间调度器也能更灵活地对序列做抢占和调度。第二是前缀缓存Prefix Caching。如果你的请求都共享相同的 system prompt或者大量请求带着同一个长文档上下文这些公共前缀的 KV 计算结果其实是完全相同的。框架可以把计算过的前缀 KV 缓存起来新请求只要校验前缀一致就能直接复用省掉一整段预填充计算。在 RAG 场景下这个优化收益非常明显。第三是 KV Cache 量化。把 KV 的数值从 FP16 压到 FP8 甚至 INT4显存占用直接降一半甚至更多。代价是精度损失但对于很多业务场景来说生成质量几乎感知不到差异。我实测下来FP8 的 KV Cache 在绝大多数任务上与 FP16 差别很小但显存压力小太多了。第四是 GQA分组查询注意力架构。这个是在模型层面做优化让多个 Query 头共享同一组 Key 和 Value 头从而大幅减少每层需要缓存的 KV 数量。这也是为什么很多较新的模型能支持超长上下文——它们从架构上就减小了 KV Cache 的膨胀速度。5. 实测与调优一次请求全链路延迟的拆解理论知识讲完了最后落到实操。很多人会遇到一个困惑——明明框架文档都看了性能指标也知道但请求慢的时候就是不知道瓶颈在哪。这里我分享一套我自己的排查和调优方法都是实际项目里反复验证过的。5.1 用量化指标拆解一次请求的生命周期判断大模型 API 性能核心有三个指标TTFTTime To First Token首 token 延迟、TPOTTime Per Output Token每输出一个 token 的耗时、端到端总耗时。这三个指标对应的优化方向完全不同。TTFT 主要受预处理量和排队情况影响。如果你发现 TTFT 很高优先检查的是请求在 Harness 层组装 prompt 花多久、tokenize 花了多久、进入推理引擎后排了多久、预填充计算本身花多久。我见过有人在 Harness 层做了大量数据库查询把一个本该 200ms 出首 token 的请求拖到 2 秒问题完全不在 GPU 上。TPOT 才是真正反映 GPU 推理性能的指标。一个 70B 模型在 A100 上单请求的 TPOT 通常在 30~60ms 左右。如果这个值过高要么是 GPU 算力不足要么是并发太高导致每个请求分到的算力太少要么是注意力计算本身有优化空间。这里需要特别提醒并发提升会提高吞吐但一定会牺牲 TPOT这是客观规律。做压测时不能只看一个指标。端到端总耗时则要再加上网络往返和流式传输的时间。客户端如果用流式方式接收 token那用户感知到的延迟应该是 TTFT 后续每个 token 的间隔而不是总耗时。这也是为什么生产环境强烈建议开启流式输出——用非流式接口等几百上千个 token 全部生成完再一次性返回用户体感会非常糟糕。5.2 一个典型的瓶颈定位案例我拿之前遇到的一个场景举例。部署了一个中型模型单卡 A100 80G看起来并发能力应该不错但压测到 20 并发时吞吐不再上升TTFT 却暴涨到 8 秒。我的排查思路是这样的先看调度器日志发现大量时间花在排队等待 KV Cache 块上——每个请求一进来问调度器要 KV 块但空闲块早被前面 10 个长对话占光了。所以瓶颈不是 GPU 算力而是显存中的 KV 池不够用。解决办法是什么呢一是把请求的最大序列长度上限调低有些业务根本不需要那么长的上下文却被默认值撑大了 KV 预留二是开启 KV Cache 量化把 FP16 压到 FP8三是优化 Harness 层的历史消息裁剪策略只保留最近几轮对话而不是把所有历史都拼进去。三个动作做完同样的硬件并发能力从 20 翻到了 50 左右。这就是我说的“从 Harness 到 GPU”全链路排查的意义。很多性能问题的答案不在你最初以为的那个环节。5.3 监控指标建议与调优节奏最后给出监控和调优层面的几个建议。在推理服务上线时至少要把这些指标接进监控系统GPU 利用率、显存占用按权重/KV/激活值拆分、TTFT 的 P50/P99、TPOT 的 P50/P99、排队请求数、KV Cache 块池空闲量。我要特别强调一点不要只盯着 GPU 利用率。GPU 利用率高不一定代表性能好只能说明计算单元在干活。有一种常见情况是 GPU 利用率 100%但吞吐上不去那是因为小矩阵乘法太多GPU 的 Tensor Core 根本没有被充分利用算力都耗在了低效的小算子调度上。这时候调并发没有意义应该做的是优化模型的算子融合比如 FlashAttention或者调整批处理策略。在调优节奏上我的建议是一次只动一个变量。想调 KV Cache 量化就把所有其他条件保持一致对比量化前后的显存和生成质量。想调并发上限就固定输入输出长度跑标准压测观察 TPOT 的变化曲线。我见过不少人几件事同时改最后出了问题根本没法定位是哪一步导致的。这套方法看起来不起眼但在实际运维中的价值非常大。大模型 API 请求的链路长、环节多没有清晰的指标体系和一根一根排查的耐心出了问题很容易陷入“头痛医头”的被动局面。我自己从那次线上故障之后有一个很深的体会做大模型工程光懂模型结构是不够的必须把请求从入口到 GPU 再到返回的每一段路都走一遍搞清楚每段路上的瓶颈是什么、开销是什么、优化杠杆在哪里。这篇文章写的 Harness 编排、GPU 预填充与解码、并发调度、KV Cache 管理本质上就是这条链路上最关键的几个节点。你把这些节点的原理和相互制约关系弄通了以后再遇到大模型服务的问题心里就有一张完整的地图不会在 GPU 利用率这种表面数字上打转。
返回列表