找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4025

积分

0

好友

531

主题
发表于 5 小时前 | 查看: 4| 回复: 0

每次调用 LLM 的 generate() 函数,都会在同一块 GPU 上经历两个截然不同的计算阶段:

  • prefill:处理你的输入 prompt,主要受计算能力限制;
  • decode:逐个生成 token,主要受内存带宽限制。

大多数推理优化手段都只针对其中一个阶段发力。想让部署真正快起来,第一步就得诊断清楚——瓶颈到底卡在哪一步。

这篇文章会从分词后的输入一路讲到流式输出,把整条流水线走通,同时看清每个阶段的时间都消耗在哪里。

1. 分词与嵌入

Byte Pair Encoding(BPE)这类 tokenizer 会把原始文本转换成词表里的整数 ID。词表规模通常在 50,000 个 token 左右。

prompt = "How does inference work?"
ids = tokenizer.encode(prompt)
# ids -> [2437, 1374, 32278, 670, 30]

每个 ID 会映射到词嵌入表中对应的一行。Embedding Table 是一个形状为 [vocab_size, hidden_dim] 的可学习矩阵。对于 hidden dimension 为 4,096 的模型,每个 token 会变成一个 4,096 维的向量。

# embedding_table has shape [vocab_size, hidden_dim]
vectors = embedding_table[ids]   # shape: [num_tokens, 4096]

词嵌入表结构示意:token ID 被 lookup 成一个 4096 维的高维向量,相同语义的 token 在向量空间中更接近

位置信息也会在这一步注入。多数现代架构使用 Rotary Position Embeddings(RoPE),它不会额外增加位置向量,而是通过旋转 embedding 向量来编码位置信息。

2. Transformer 层

嵌入后的序列会穿过一组 Transformer 层,层数通常在 32 到 80 以上,取决于模型规模。每一层依次执行两个操作。

1. Self-attention

通过可学习的权重矩阵,为每个 token 计算三组投影:Query Q、Key K 和 Value V。

注意力头计算流程:输入嵌入 X 经由 W_Q、W_K、W_V 生成 Q、K、V,再计算出包含上下文的注意力输出

每个 token 的 Query 会和其它所有 token 的 Key 计算相似度分数。分数经过 scaling 和 softmax 之后,决定了各 token 的 Value 以多大权重混合进当前 token 的表示中。

# scores: how much each token attends to every other token
Q, K, V  = x @ Wq, x @ Wk, x @ Wv

scores = (Q @ K.T) / sqrt(d_k)

weights = softmax(scaled)          # one row per token, sums to 1

attn_output  = weights @ V

2. Feed-forward network(FFN)

用两层 MLP 独立处理每个 token 的向量。你可以这么理解:Attention 负责在不同位置之间传递信息,而 FFN 负责变换每个位置上的表示。

最后一层结束后,模型把最后一个 token 的 hidden state 投影回词表大小,即 [hidden_dim, vocab_size],再做 softmax,从得到的分布里采样出第一个输出 token。

3. Prefill:计算受限阶段

处理输入 prompt 是第一阶段。所有 token 会并行处理:每个 token 的 Q、K、V 同时算好,attention 以大型矩阵乘矩阵(matmul)的形式运行。

这是一个 compute-bound 的工作。瓶颈在于 GPU 的算术吞吐量,GPU 利用率通常很高。衡量这个阶段的指标叫 TTFT(Time to First Token),也就是从请求发出到第一个输出 token 出现的延迟。

prefill 阶段还会顺便填满 KV cache:每一层的 K 和 V 张量都会存进 GPU 显存,供后续反复读取。

# Prefill: process the whole prompt in one shot
hidden = embed(prompt_tokens) + positions
for layer in model.layers:
    Q, K, V = project(hidden)           # for ALL tokens at once
    hidden  = attention(Q, K, V) + hidden
    hidden  = feedforward(hidden) + hidden
    cache_kv(layer, K, V)               # save for later
first_token = sample(project_to_vocab(hidden[-1]))

4. Decode:内存受限阶段

第一个 token 生成后,模型便切换到逐个 token 生成的模式。对每个新 token,模型只计算这个 token 的 Q、K、V——之前的 K 和 V 已经在缓存里了。

# Decode: one token per iteration
token = first_token
steps = 0
while token != STOP and steps < MAX_STEPS:
    x = embed(token) + position(steps)
    for layer in model.layers:
        q, k, v = project(x)
        K_all, V_all = caches[layer].append(k, v)   # cached history + new
        x = layer.forward(q, K_all, V_all, x)       # attention + FFN, residuals
    token = sample(project_to_vocab(x))
    steps += 1
    yield token

每一步的算术量都很小。它只用一个 query 向量跟缓存里的 key 矩阵做运算,不再是完整的矩阵乘法。但 GPU 仍然要为这次“小计算”从显存中加载所有权重矩阵,以及完整的 K/V 缓存。瓶颈就从计算吞吐切换到了内存带宽。

KV 缓存机制:每次只计算新 token 的 q、k、v,随后从缓存中读取历史 K、V,完成新 token 的上下文感知

衡量这个阶段的指标是 ITL(Inter-Token Latency),也就是连续两个输出 token 之间的时间。ITL 越低,模型的响应就越流畅,用户体感越“丝滑”。

5. KV 缓存

如果没有缓存,生成一个 1,000-token 的回答,每一步都得在持续增长的完整序列上重新计算 attention,计算复杂度会变成平方级。

KV cache 把每一层的 K 和 V 张量存下来,并在生成时增量追加。对于长输出,这通常能带来 5 倍甚至更高的提速。

代价也很直接——缓存会随序列长度线性增长,而且每一层都有自己的专属缓存。拿 13B 参数的模型来说,每个 token 的缓存大约消耗 1 MB。一个 4K-token 的上下文,光 KV cache 就能吃掉 4 GB 显存。这也就是长上下文为啥昂贵。

缓存会直接跟 batch size 争夺 GPU 内存。单个请求占的缓存越多,同一块 GPU 能承载的并发请求就越少。

常见的缓解手段包括:把缓存量化到 INT8 或 INT4;使用 sliding window attention 丢弃窗口之外的过时 token;用 grouped-query attention(GQA)让多个 attention head 共享 K/V 以压缩缓存量;以及 vLLM 背后的 PagedAttention,像操作系统管理虚拟内存那样分页管理缓存,从而消灭碎片。

6. 围绕缓存重新设计注意力

量化和分页策略都把 KV cache 看作一项需要管理的固定成本。而 2026 年 4 月 24 日发布的 DeepSeek V4 Preview 走了另一条路:直接重新设计注意力机制,让缓存从骨子里变得更小。

DeepSeek-V4 论文预览:通过混合注意力架构大幅降低长上下文下的单 token 计算量和 KV 缓存占用

V4 混合了两种压缩注意力机制:

  • Compressed Sparse Attention (CSA):先用 softmax-gated pooling 将 KV 条目压缩 4 倍,再做 sparse attention。
  • Heavily Compressed Attention (HCA):更激进,把 128 个 token 的 KV 合并成一条压缩条目,在压缩表示上执行 dense attention。

在 1M-token 上下文的场景中,相比 DeepSeek-V3.2,V4-Pro 的单 token 推理 FLOPs 只要 27%,KV cache 只需 10%。换算成直观数据:bf16 下 1M 上下文每条序列需 9.62 GiB 的 KV cache;同条件下 V3.2 风格的架构估算需要 83.9 GiB。若再叠上 fp4/fp8 量化,缓存还能再缩一半。

KV cache 已然成为硬约束,模型架构正在围绕它快速演进。

7. 量化

训练时为保障梯度稳定,通常使用 FP32 或 BF16。推理则不需要这种精度。降低 bit width 带来的内存节省是线性的:

  • 7B 参数,FP32:28 GB
  • 7B 参数,FP16/BF16:14 GB
  • 7B 参数,INT8:7 GB
  • 7B 参数,INT4:3.5 GB

量化前后内存占用对比:7B 模型从 FP32 的 28GB 降至 INT4 的 3.5GB,使其能在笔记本 GPU 上运行

INT4 正是 7B 模型能够跑在 4~6 GB 显存的笔记本 GPU 上的原因。GPTQ 和 AWQ 等方法通过引入 per-channel scaling factors,尽可能弥补有损压缩带来的质量损失。做得好的 INT4 模型在标准 benchmark 上往往只比全精度模型低 1~2 个百分点。

从 FP16 降到 INT8,推理延迟通常能砍半,质量损失几乎可以忽略。对绝大多数部署场景来说,量化是投产比最高的优化手段。

8. 推理服务基础设施

现代推理服务器会在 prefill-decode 循环外围再加一组优化:

推理优化层:continuous batching 混流多用户 token、speculative decoding 用小模型提案大模型验证、PagedAttention 分页管理缓存

  • Continuous batching 在同一个 GPU step 中交错处理多个请求的 token,即便处在 memory-bound 的 decode 阶段也能维持较高的利用率。
  • Speculative decoding 先用小型的 draft model 提出一串候选 token,再由大模型在一次 forward pass 中统一验证。只要 draft model 的接受率够高,就能把多次串行的 decode step 压缩成一次并行验证。

标准解码与推测解码对比:推测解码用小模型生成草稿 token,大模型单次前向验证,显著提升单步吞吐量

  • PagedAttention(vLLM) 将 KV cache 内存切成固定大小的 block,从根本上消除了碎片,让单块 GPU 能同时服务更多请求。

vLLM、TensorRT-LLM、Text Generation Inference(TGI)这类框架把这些技术组合在一起。因为 decode 阶段留出了大量闲置算力,continuous batching 就能拿其他请求的 token 来填满,一块 GPU 是可以实实在在地撑起几十个并发用户的。

9. 完整推理路径

LLM 推理全流程:从分词、嵌入、Prefill、Decode 循环到流式输出,并标注了计算密集和内存密集两个阶段

  1. Tokenize:文本经 BPE 变成整数 ID。
  2. Embed:ID 变成向量,RoPE 编码位置。
  3. Prefill:所有输入 token 并行经过每一层。compute-bound,填充 KV cache,并输出第一个 token。
  4. Decode loop:逐 token 生成。为新 token 投影 Q,对缓存里的 K/V 做 attention,跑 FFN,采样,并把新 K/V 追加进缓存。memory-bound。
  5. Detokenize:token ID 映射回文本字符并流式输出。

10. 一些实践结论

  • 长 prompt 的成本主要表现为 TTFT,集中在 prefill 阶段。
  • 长输出的成本主要表现为 ITL,集中在 decode 阶段。
  • 这两个阶段消耗的是不同类型的硬件资源。
  • 上下文长度并不免费。它会膨胀 KV cache,直接拉低并发处理能力(batch capacity)。
  • decode 阶段即便服务器看起来已经满载,GPU 利用率也可能跌到 30%,因为瓶颈在内存带宽,不在计算单元。
  • 解决方法往往不是堆更多算力,而是用更快的内存、更小的缓存,或者更高明的 batching 策略。

当有人抱怨“模型太慢了”,第一步诊断可以这样切入:它是启动慢,还是流式输出时一个字一个字往外蹦?启动慢多半是 prefill-bound,得从优化 TTFT 下手;流式输出卡顿多半是 decode-bound,那就该重点优化 ITL。

LLM 推理本质上是两份工作:Prefill 是计算密集型,Decode 是内存密集型,二者的优化策略完全不同




上一篇:Codex AI 编程智能体教程:从入门到进阶的 10 个实用技巧
下一篇:Claude Code创始人谈AI时代角色融合:未来团队需要这五种人
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-1 05:47 , Processed in 1.885758 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表