核心结论: 大模型的工程价值不在于“参数量有多大”,而在于能否在给定硬件预算下把推理延迟和吞吐压到可用水平。理解模型内部的计算结构,是所有优化手段的前提——不知道 KV Cache 存在,就无法理解为什么显存会爆;不知道注意力的二次复杂度,就无法判断长上下文为什么昂贵。
大模型做的事情可以用一句话概括:给定前文 token 序列,预测下一个 token 的概率分布。所有对话、推理、代码生成,都是这个过程的反复迭代。
用代码表示最简推理循环:
import torch
import torch.nn.functional as F
def generate(model, tokenizer, prompt: str,
max_new_tokens: int = 100, temperature: float = 0.7):
ids = tokenizer.encode(prompt, return_tensors="pt")
for _ in range(max_new_tokens):
logits = model(ids).logits[:, -1, :] # 取最后一个位置的分布
logits = logits / temperature # 温度调节随机性
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
ids = torch.cat([ids, next_id], dim=-1)
if next_id.item() == tokenizer.eos_token_id:
break
return tokenizer.decode(ids[0])这段代码揭示了三个工程事实:每次只生成一个 token(所以 token 消耗是逐次的);每次都要重新前向计算(所以缓存至关重要);temperature 控制分布形状(0 趋近确定性,越高越随机)。
Transformer 的核心是自注意力。每个 token 的表示由它与其他所有 token 的注意力加权得到:
def attention(Q, K, V, mask=None):
"""Q,K,V: (batch, heads, seq_len, dim)"""
d_k = Q.size(-1)
scores = Q @ K.transpose(-2, -1) / (d_k ** 0.5)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)
return weights @ Vscores 的形状是 (batch, heads, seq_len, seq_len)。这意味着注意力计算对序列长度是 O(n²) 复杂度。序列长度翻倍,计算量和显存占用翻四倍。
这就是长上下文昂贵的根本原因。一个 128K 上下文的模型,注意力矩阵比 8K 上下文大 256 倍。
推理时,每生成一个新 token,都要重新计算所有历史 token 的 K 和 V。KV Cache 的思路是:把已计算的 K、V 缓存起来,新 token 只计算自己的 Q,与缓存拼接。
class KVCache:
def __init__(self, max_len: int, n_heads: int, head_dim: int):
self.k = torch.zeros(1, n_heads, max_len, head_dim)
self.v = torch.zeros(1, n_heads, max_len, head_dim)
self.pos = 0
def update(self, k_new, v_new):
"""追加新的 K、V,返回完整序列"""
n = k_new.size(2)
self.k[:, :, self.pos:self.pos + n] = k_new
self.v[:, :, self.pos:self.pos + n] = v_new
self.pos += n
return self.k[:, :, :self.pos], self.v[:, :, :self.pos]KV Cache 把每步的计算量从 O(n²) 降到 O(n),但代价是显存。缓存大小 = 2 × 层数 × 头数 × 头维度 × 序列长度 × 精度字节数。一个 70B 模型在 32K 上下文下,KV Cache 可能占用几十 GB 显存。
工程上的应对手段有三类:MQA/GQA(减少 KV 头数)、量化缓存(用 INT8 存储)、分页管理(vLLM 的 PagedAttention)。三者都在“缓存大小”和“计算速度”之间做权衡。
路径一:批处理。 把多个请求合并成一个 batch,提高 GPU 利用率。但不同请求长度不同,需要 padding,padding 会浪费计算。连续批处理(continuous batching)解决了这个问题:
def continuous_batch(requests: list, model, max_batch: int = 8):
"""请求完成即退出,新请求立刻补位"""
active = []
while requests or active:
while len(active) < max_batch and requests:
active.append(requests.pop(0))
# 一步只生成一个 token
for req in active:
req.step(model)
# 移除已完成的请求
active = [r for r in active if not r.finished]
yield [r.output for r in active]路径二:量化。 把权重从 FP16 降到 INT8 或 INT4,显存占用减半到四分之一。代价是精度损失,需要用校准集验证掉点幅度。
路径三:推测解码。 用一个小模型生成候选 token,大模型并行验证,一次接受多个 token。加速比通常在 2 到 3 倍。
def speculative_decode(draft_model, target_model, prompt, k: int = 4):
"""小模型草拟 k 个 token,大模型一次性验证"""
ids = prompt
while True:
draft_ids = draft_model.generate(ids, max_new_tokens=k)
target_logits = target_model(draft_ids).logits
# 逐个位置比较,接受匹配的 token
accepted = compare_and_accept(target_logits, draft_ids)
ids = accepted
if is_done(ids):
break
return ids第一,先测延迟再选方案。 推理延迟分为 prefill(处理输入)和 decode(逐 token 生成)两段。prefill 是计算密集,decode 是显存带宽密集,优化手段完全不同。
第二,显存预算是硬约束。 模型权重、KV Cache、激活值三者共享显存。选模型前先算清楚:权重占多少、缓存留多少、还剩多少给批处理。
第三,量化不是免费的。 INT4 能省显存,但推理时的反量化有开销,低比特下的精度损失需要业务侧评估是否可接受。
第四,批处理提升吞吐但增加延迟。 批越大,单个请求等待时间越长。线上服务要在吞吐和延迟之间找平衡点。
第五,KV Cache 是长上下文的主要成本。 如果业务不需要长上下文,把 max_len 调小是最直接的显存优化。
大模型的工程化不是“调模型”,而是理解计算结构后在硬件约束下做权衡。注意力复杂度、KV Cache 大小、批处理策略,这三件事决定了推理系统能不能用、贵不贵、快不快。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。