Skip to content

推理基础:延迟从哪来 ​

所在组:组 2 · 推理与接口 | 上一组出口:能说出模型生命周期如何影响工程决策(模型是带行为、预算、能力边界三个接口属性的可替换能力) | 本组出口:能把一次用户感知的延迟拆进排队 / prefill / decode / 网络四段,说出每段谁负责、你能拉哪根杠杆 前置:模型生命周期(桥接) | 下一步:高效服务、模型 API 契约

1. 概述 ​

本页补上"模型"与"API"之间此前缺失的一层:生成式模型不是"输入进、结果出"的同步函数,而是一条两阶段流水线。不理解这条流水线,你解释不了首字为什么慢、后面的字为什么匀速出来、账单为什么和输出长度成正比,也判断不了什么时候该离开托管 API。

生成分两个阶段,延迟结构完全不同:

  • prefill(预填充):把整段 prompt 一次性并行处理,为每个 prompt token 计算注意力所需的 Key/Value。这一步决定首字时间(TTFT,Time To First Token)。
  • decode(解码):一个 token 一个 token 串行生成,每一步都要"看见"之前所有 token。这一步决定字间延迟(ITL,Inter-Token Latency)和总时长。

延迟分解表:用户感知的一次延迟由四段构成 ​

段发生什么谁负责你的杠杆
排队(queue)请求等待进入一个批次或实例槽位serving 平台(厂商或自建集群)并发整形、优先级档位、削峰
prefill整段 prompt 并行算完,KV 写入缓存模型服务进程缩短 prompt、提高前缀缓存命中(→ 高效服务)
decode逐 token 生成,受显存带宽约束模型服务进程限制 max_tokens、投机解码(厂商侧)
网络TLS、网关、代理缓冲(流式被缓冲会退化成整段到达)你的基础设施 + 厂商边缘节点就近接入、确认流式不被代理缓冲(→ 流式响应)

关键推论:TTFT ≈ 排队 + prefill + 网络;总时长 ≈ TTFT + (输出 token 数 − 1) × 单步 decode。prompt 长度主要打 TTFT,输出长度主要打总时长——这是所有延迟优化的归因起点。

何时使用 / 何时不用 ​

  • 用:做延迟预算与归因(TTFT 突高查哪)、读懂成本结构(为什么按 token 计费、输入输出不同价)、评估"该不该自建 serving"。
  • 不用:训练与注意力的数学推导——去 Learn LLM 第 9 章;厂商具体字段与错误码——去 模型 API 契约;把 token 渲染成界面——去 生成式 UI。

决策表:模型消费的三种形态 ​

形态方向控制权状态信任域最低复杂度
厂商 API出站请求/响应厂商管排队、batching、缓存;你管调用无状态(KV cache 在厂商侧)数据出你的域一个 fetch 就能跑
自建 serving(vLLM 等)你自己集群内执行全部自管:批调度、KV 显存、量化KV cache 在你的 GPU 上数据不出域GPU 运维 + 模型供应链
端侧推理用户设备内执行完全自管,受设备算力硬约束设备本地数据不出设备模型分发 + 运行时(→ 浏览器与端侧推理)

从最低复杂度起步:绝大多数应用从厂商 API 开始;只有当成本、延迟、数据边界三者之一被 API 卡死时,才评估自建;端侧只在离线/隐私/尾延迟有硬需求时进入。

历史版本里程碑 ​

  • FlashAttention 论文(arXiv 2205.14135,2022-05):IO 感知的精确注意力,减少 HBM 读写——现代 serving 内核的事实基线。retrievedAt 2026-09-01。
  • vLLM 论文发表于 SOSP 2023(vLLM 官方文档首页列出)。retrievedAt 2026-09-01。
  • 其余引擎与厂商时间线未验证,不编造。

2. 使用 ​

最小实战:零 key 两阶段服务模拟器(≤15 分钟) ​

无 API key、无依赖、输出确定性。用一个 TypeScript 文件模拟"两阶段服务":prefill 一次性处理 prompt,decode 逐 token 吐出,KV cache 用数组缓存已算过的 token。时间用抽象成本单位(cost units)计量,保证每次运行输出一致。

环境:Node ≥ 23.6(直接运行 .mts);Node 22.6–23.5 加 --experimental-strip-types。保存为 inference-sim.mts:

ts
// fixture: 零 key、零依赖的两阶段推理模拟。输出确定性,可重复。
const FORWARD_PER_TOKEN = 12; // 一个 token 完整 forward 的成本(无 cache 时整个前缀每步重算)
const KV_READ_PER_TOKEN = 1;  // 有 cache 的 decode:读一条 K/V 代替一次 forward

interface Timing {
  ttftCost: number;      // 排队 + prefill(本例排队 = 0,只有 prefill)
  decodeCost: number;    // 所有 decode 步之和
  totalCost: number;
  tokens: number;
  avgPerToken: number;
  stepsGrowth: number[]; // 第 1 步 / 中间步 / 最后一步的单步成本
}

function serve(promptTokens: number, newTokens: number, useCache: boolean): Timing {
  // 阶段 1:prefill —— 整段 prompt 只处理一次,每个 prompt token 的 K/V 写入缓存
  const prefillCost = promptTokens * FORWARD_PER_TOKEN;
  const kvCache: number[] = useCache ? Array.from({ length: promptTokens }, (_, i) => i) : [];

  // 阶段 2:decode —— 每步 1 个 token;新 token 必须"看见"之前所有 token
  let decodeCost = 0;
  const stepsGrowth: number[] = [];
  for (let step = 0; step < newTokens; step++) {
    const context = promptTokens + step; // 新 token 要注意到的 token 数
    let stepCost: number;
    if (useCache) {
      // 新 token 做 1 次 forward + 每个历史 token 读 1 次缓存
      stepCost = FORWARD_PER_TOKEN + context * KV_READ_PER_TOKEN;
      kvCache.push(promptTokens + step); // 新 token 的 K/V 追加进缓存,不再重算
    } else {
      // 负例:无缓存 —— 每步对整个前缀重跑 forward
      stepCost = (context + 1) * FORWARD_PER_TOKEN;
    }
    decodeCost += stepCost;
    if (step === 0 || step === Math.floor(newTokens / 2) || step === newTokens - 1) stepsGrowth.push(stepCost);
  }
  const totalCost = prefillCost + decodeCost;
  return {
    ttftCost: prefillCost,
    decodeCost,
    totalCost,
    tokens: newTokens,
    avgPerToken: Math.round((totalCost / newTokens) * 10) / 10,
    stepsGrowth,
  };
}

const PROMPT = 200; // prompt token 数
const OUT = 40;     // 生成 token 数

const withCache = serve(PROMPT, OUT, true);
const noCache = serve(PROMPT, OUT, false);

const fmt = (n: number) => n.toLocaleString('en-US');
const report = (label: string, t: Timing) => {
  console.log(`--- ${label} ---`);
  console.log(`TTFT (prefill):        ${fmt(t.ttftCost)} units`);
  console.log(`decode (${t.tokens} tokens): ${fmt(t.decodeCost)} units`);
  console.log(`total:                 ${fmt(t.totalCost)} units; avg/token: ${t.avgPerToken}`);
  console.log(`step cost (first/mid/last): ${t.stepsGrowth.map(fmt).join(' / ')}`);
};
report('with KV cache', withCache);
report('without KV cache (negative case)', noCache);
const saved = ((1 - withCache.totalCost / noCache.totalCost) * 100).toFixed(1);
console.log(`KV cache saved: ${saved}% of total cost`);
console.log(`throughput proxy (tokens per 1k units): cached ${(1000 / (withCache.totalCost / OUT)).toFixed(2)} vs no-cache ${(1000 / (noCache.totalCost / OUT)).toFixed(2)}`);

运行与正常输出:

text
$ node inference-sim.mts
--- with KV cache ---
TTFT (prefill):        2,400 units
decode (40 tokens): 9,260 units
total:                 11,660 units; avg/token: 291.5
step cost (first/mid/last): 212 / 232 / 251
--- without KV cache (negative case) ---
TTFT (prefill):        2,400 units
decode (40 tokens): 105,840 units
total:                 108,240 units; avg/token: 2706
step cost (first/mid/last): 2,412 / 2,652 / 2,880
KV cache saved: 89.2% of total cost
throughput proxy (tokens per 1k units): cached 3.43 vs no-cache 0.37

负例解读(读三件事):

  1. TTFT 完全相同(2,400):KV cache 不改变 prefill——prefill 本来就只算一次。
  2. 两种情况的单步成本都在增长(212→251 与 2,412→2,880):decode 天然要读全部上下文,缓存去掉的是"重算 forward",不是"读历史"这件事。
  3. 常数差 12 倍(FORWARD_PER_TOKEN / KV_READ_PER_TOKEN):这正是 KV cache 存在的理由——读一条缓存远比重跑一层网络便宜。

验收:三段输出与上面一致;KV cache saved 为 89.2%。清理:删除文件即可,无网络无端口。

场景矩阵 ​

场景输入动作输出适用不适用
基础:TTFT 归因用户反馈"等半天才出字"改 PROMPT 重跑,看 TTFT 变化TTFT 随 prompt 线性涨长 prompt 场景先查 prefill输出阶段慢(改 OUT 看总时长)
常见:输出长度预算总时长随回答变长恶化改 OUT,观察 decode 占比decode 占比随输出增长设 max_tokens 上限的依据TTFT 问题
组合:长 prompt + 长输出知识库上下文全塞 prompt大 PROMPT + 大 OUTdecode 单步成本被两头推高理解为何要裁剪上下文(→ 会话与状态)—

3. 原理 ​

prefill 与 decode 是两种不同的计算 ​

prefill 是并行的一次性计算:prompt 的所有 token 同时过网络,GPU 的算力被喂饱,成本大致随 prompt 长度线性增长。它是 TTFT 的主要构成(叠加排队与网络)。

decode 是串行的访存受限计算:第 N 个 token 依赖第 N−1 个的输出,无法并行;每一步要把之前所有 token 的 Key/Value 从显存读一遍。现代 GPU 算力过剩、显存带宽是瓶颈,所以 decode 的快慢主要看"读多少显存"而不是"算多少浮点"。

KV cache:为什么存在、代价是什么 ​

为什么存在:decode 每一步都需要全部历史 token 的 K/V 参与注意力。不缓存,就等于每生成一个 token 都把前缀重新 forward 一遍(上面 fixture 的负例)。缓存后,历史部分从"重算网络"退化为"读一次缓存"。

内存代价:每个 token 的 KV 占用大致为

text
2 (K 和 V) × 层数 × KV 头数 × 每头维度 × 每参数字节数

对给定模型这是一个常数 × token 数:KV 显存随上下文长度线性增长。这就是"context 长度是内存预算"的物理来源——厂商标注的最大上下文,本质是"KV cache + 权重 + 激活能塞进显存"的约束。工程含义:上下文不是免费的,每多留一轮历史,每个后续 token 的访存都更贵(fixture 中 212→251 的增长就是它的缩影)。

batching:吞吐与延迟的权衡 ​

单个请求的 decode 读不完显存带宽,GPU 大量算力闲置。把多个请求的 decode 步拼成一个批次,权重只读一遍、多个请求分摊,吞吐上升而单请求延迟几乎不变——直到算力饱和。连续批处理(continuous batching,vLLM 文档术语)进一步让新请求随时插入正在生成的批次,不必等整批完成(vLLM 首页特性列表:continuous batching、chunked prefill、prefix caching,retrievedAt 2026-09-01)。

对你的含义:你共享厂商的批次。排队延迟来自"等槽位",prefill/decode 单步延迟相对稳定;高峰期 TTFT 恶化通常是排队在涨,而不是模型在变慢。

serving 的基本形态 ​

  • 厂商 API:排队、batching、KV cache、量化全部由厂商承担,你按 token 付费(计费结构 → 高效服务)。
  • 自建 serving:vLLM 一类引擎把上述机制产品化(PagedAttention 管理块状 KV、automatic prefix caching、INT8/INT4 量化、投机解码,术语见其文档首页,retrievedAt 2026-09-01)。你换取控制权,付出 GPU 运维与模型供应链。
  • 端侧:算力与显存被设备硬约束,模型必须裁剪(量化、小模型),详见 浏览器与端侧推理。

规范要求 vs 本地实测 ​

断言来源本地 fixture 实测(上方)
TTFT 由 prefill 决定,与输出长度无关两阶段结构推论实测:OUT 变化只影响 decodeCost,ttftCost 不动
decode 单步成本随上下文线性增长注意力每步读全部历史实测:212 → 232 → 251(prompt 200 + 步数推进)
KV cache 消除的是前缀重算,不是历史读取KV cache 语义实测:有/无 cache 单步成本都在增长,仅常数差 12 倍
batching 提升吞吐而非单请求延迟vLLM 文档术语(L1)未覆盖:fixture 是单请求模型(见未决问题)
排队在高峰会成为 TTFT 主项通用 serving 经验未覆盖:fixture 排队恒为 0(见未决问题)

与 Learn LLM 的边界 ​

prefill/decode 的手写实现、KV cache 的数值等价验证、量化(INT8/INT4)的原理推导,全部归 Learn LLM 第 9 章(推理与量化)。本页只保留"影响工程决策"的深度:延迟归因、内存预算、选型。

4. 开发 ​

延迟预算的集成检查清单 ​

  • 把 TTFT 与 ITL 分开埋点(流式首 chunk 时间 ≠ 总时长)——没有分开的埋点,后面所有 runbook 都没有证据。
  • 给 max_tokens 设上限并纳入评审:输出长度直接乘 decode 成本。
  • prompt 长度分布纳入监控:TTFT 恶化先看它,再看缓存命中率,最后才怀疑厂商。

调试 runbook ​

症状 → 证据 → 处理 → 完成标准 ​

症状:用户反馈"要等很久才出第一个字",出来之后倒是流畅。 证据:流式埋点拆开看:TTFT 高、ITL 正常;按请求记录 prompt token 数分布;厂商返回的缓存命中字段(如 cached_tokens)。 处理:按顺序排查——① 排队(高峰时段对比、并发是否超配额);② prefill(prompt 是否变长:历史全量重发、模板膨胀);③ 前缀缓存是否失效(prompt 结构改版把动态内容挪到了前面)。 完成标准:TTFT p50/p95 回到基线;能指着数据说出"慢在哪一段"。

症状 → 证据 → 处理 → 完成标准 ​

症状:账单突增,但调用量没涨。 证据:对账 usage:每轮请求的 input tokens 是否随轮次线性膨胀(context 整段重算);缓存命中占比是否下跌。 处理:加会话历史裁剪与 token 预算(→ 会话与状态);把 prompt 重排为"稳定前缀在前、动态内容在后"(→ 高效服务);核对 max_tokens 是否被调大。 完成标准:每轮 input tokens 不随轮次无限增长;单会话成本曲线斜率回落。

症状 → 证据 → 处理 → 完成标准 ​

症状:生成中途卡顿、字与字之间偶尔停顿(ITL 抖动)。 证据:逐 chunk 时间戳看长尾;确认是 decode 抖动而非网络(本地直连对比);检查输出中是否触发长推理段。 处理:网络侧确认代理不缓冲 SSE(→ 流式响应);decode 侧限制输出长度、拆分长任务;持续抖动属于厂商容量问题,走状态页与降级链(→ 浏览器与端侧推理)。 完成标准:ITL p95 回稳;卡顿样本能归因到网络或 decode 之一。

反模式清单 ​

  • 用总时长当唯一延迟指标——TTFT 和 ITL 的优化手段完全不同,混在一起无法归因。
  • 不限 max_tokens——输出长度是乘法项,失控输出直接放大成本与时长。
  • 假设"上下文窗口大 = 随便塞"——每 token KV 显存与每步访存都在为长度付费。
  • TTFT 一慢就换模型——先拆段归因,多数时候是排队或 prompt 变长。
  • "看似成功但证据不足":没有分段埋点就宣称"优化生效"。

5. 资料库 ​

四级阅读路线 ​

  • Beginner(2 条):跑通本页 fixture;读 Learn LLM 第 9 章的 prefill/decode 与 KV cache 小节。
  • Builder(2 条):OpenAI prompt caching 指南(静态前缀在前的工程意义);模型 API 契约(把本页认知落成调用代码)。
  • Operator(2 条):vLLM 文档首页(serving 术语总览:continuous batching / chunked prefill / prefix caching);厂商状态页与延迟埋点看板。
  • Researcher(2 条):FlashAttention 论文(IO 感知注意力);vLLM 论文(SOSP 2023,经 vLLM 文档首页索引)。

资源表 ​

名称层级canonical URL用途支持的断言下一步
Learn LLM 第 9 章 · 推理与量化Ehttps://llm.zenheart.site/chapters/09-inference-cache原理与从零实现prefill/decode、KV cache、量化的深层归 Learn LLM手写一遍 cache/full 等价
vLLM 文档首页L1https://docs.vllm.ai/en/latest/serving 术语与特性总览continuous batching、chunked prefill、prefix caching、量化、投机解码为 vLLM 特性按 feature 页深挖
OpenAI Prompt Caching 指南L0https://platform.openai.com/docs/guides/prompt-caching前缀缓存工程≥1024 token 自动缓存;cached_tokens 字段;KV 张量在 prefill 产生改造 prompt 结构
Anthropic Prompt Caching 文档L0https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching显式缓存断点cache_control、5 分钟 TTL、最短可缓存长度对比两家缓存模型
FlashAttention 论文L4https://arxiv.org/abs/2205.14135内核层原理IO 感知注意力减少 HBM 读写(2022-05)阅读 tiling 设计
本页 fixtureEinference-sim.mts(正文内联)零 key 验证两阶段成本结构;KV cache 的收益与边界改参数做归因练习

retrievedAt:全部网页资源 2026-09-01。

主动证伪与未决问题 ​

  • fixture 是单请求模型:batching 与排队只引用了 vLLM 术语与通用经验,未在本地实测。
  • 成本单位是抽象的:12:1 的重算/读取比是教学常数,不是任何真实硬件的测量值;真实比值取决于模型层数、算子实现与硬件带宽。
  • "TTFT ≈ 排队 + prefill + 网络"是结构分解,各段真实占比需在自己的埋点数据里验证。
  • 推理 token 计费:OpenAI 定价页说明 reasoning token 不可见但按输出 token 计费并占用上下文(retrievedAt 2026-09-01)——本页未展开,归 模型 API 契约。

learn-ai 到此为止 / 继续去哪 ​

本页管"延迟与成本的结构"。这些机制如何变成账单上更便宜的字段 → 高效服务;把认知落成调用代码 → 模型 API 契约;数学与从零实现 → Learn LLM 第 9 章;上线后的成本治理 → 成本与性能。

为前端工程师打造 · 基于 VitePress 构建