news 2026/9/10 18:54:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型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 model's 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 里让哪些序列进入计算。

调度决策还涉及优先级策略。比如,处于预填充阶段的新请求和正在解码的存量请求,谁先谁后?如果无条件优先预填充,新请求可以很快吐出第一个 token(TTFT 低),但会挤占存量请求的计算资源,导致它们的生成速度变慢。如果无条件优先解码,存量请求体验好,但新请求需要排队,首 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 重新计算一遍。生成第 N+1 个 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 × D(K 一份、V 一份)。乘以层数 L,再乘以序列长度 S,再乘以每个元素占用的字节数(FP16 是 2 字节),就是总占用。

我举个例子。一个 70B 参数的模型,假设 80 层、80 个注意力头、每个头维度 128,那么隐藏维度是 80 × 128 = 10240。每个 token 的 KV 占用是 2 × 80 × 10240 × 2 字节 = 3,276,800 字节,约 3.125MB。

如果一个请求的上下文长度是 4096 token,KV 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 性能,核心有三个指标:TTFT(Time To First Token,首 token 延迟)、TPOT(Time 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 利用率这种表面数字上打转。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:54:03

Hadoop与3D打印结合的制造业大数据分析实践

1. 项目背景与核心价值 当制造业遇上大数据和增材制造技术,一场生产效率革命正在悄然发生。这个项目将Hadoop分布式计算框架与3D打印技术相结合,构建了一套面向制造领域的数据分析解决方案。在实际生产环境中,我们每天需要处理来自数百台3D打…

作者头像 李华
网站建设 2026/9/10 18:52:40

电网源储荷协调调度:MATLAB建模与多时间尺度优化

1. 项目背景与核心挑战 现代电网正面临前所未有的转型压力。随着新能源渗透率不断提高,传统"源随荷动"的调度模式已难以应对风光出力的随机性和波动性。去年我在参与某省级电网调度系统升级时,曾遇到这样一个典型案例:某日午间光伏…

作者头像 李华
网站建设 2026/9/10 18:52:35

C语言指针与计算机英语术语的实战练习指南

1. C语言基础练习与计算机英语的黄金组合今天想和大家分享一个我坚持了三个月的学习组合:每天完成一组C语言基础练习学习5个计算机英语术语。这个看似简单的习惯,让我的编程能力在短时间内有了质的飞跃。特别是最近在做嵌入式开发项目时,突然…

作者头像 李华
网站建设 2026/9/10 18:50:38

Tigshop v5.8.14更新解析:搜店铺优化与跨境装修实践

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

作者头像 李华
网站建设 2026/9/10 18:50:33

Hive与HBase核心技术对比与大数据选型指南

1. 项目概述:Hive与HBase的技术定位差异第一次接触大数据生态的技术选型时,很多工程师都会困惑于Hive和HBase的选择。这就像装修时纠结该用实木地板还是瓷砖——两者都能解决地面铺设问题,但材质特性和适用场景截然不同。我在金融和电商行业的…

作者头像 李华