AI/ML 详解:推理优化

Day 9 · 2026-05-26
面向:有编程经验的非 AI 方向工程师

KV 缓存KV Cache

显存基础优化
一句话类比

KV cache 就是自回归生成的 memoization——和动态规划里"算过的子问题别再算一遍"完全同构。后端世界对应:流式聚合的中间状态(Flink 的 keyed state)、数据库的物化视图HTTP 长连接的会话上下文。没有它,每生成一个 token 都要把之前所有 token 的 attention 重新跑一遍——O(n²) 变 O(n³)。

它解决什么问题 + 工作机制

LLM 推理是自回归的:生成第 100 个 token 时,attention 要算它对前 99 个 token 的相关性。Attention 的公式是 softmax(Q·KT/√d)·V——其中 Q(Query) 是当前 token 的"提问向量",K(Key) 是历史 token 的"标签向量",V(Value) 是历史 token 的"内容向量"。直觉:Q 像查询,K 像索引,V 像被查的数据。

关键观察:第 1-99 个 token 的 K 和 V 在生成第 100 个 token 时完全不变——它们只依赖各自位置之前的内容。所以 K/V 算一次就能永久复用,只有新 token 的 Q/K/V 需要新算。把所有历史 K/V 存起来,就是 KV cache。

两阶段推理(Prefill vs Decode)

Prefill 阶段 一次性处理整个 prompt(比如 1000 token),并行算 K/V 写入 cache
→ 计算密集(compute-bound):GPU 矩阵乘法跑满

Decode 阶段 每次只生成 1 个新 token,复用 cache 里 1000 条历史 K/V
→ 显存带宽密集(memory-bound):搬运 KV 比计算慢得多

显存代价:每个 token ≈ 2 × n_layers × n_heads × head_dim × 2 bytes
Llama-3 70B:每 token 约 320 KB,10K context = 3.2 GB,batch=32 时占 100 GB+

这就是为什么 LLM 推理的瓶颈不在算力,而在显存带宽——一个反直觉但极其重要的事实。后续三个优化(speculative decoding、continuous batching、quantization)本质都在"想办法多榨点带宽出来"。

代码示例
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B")
m = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B", torch_dtype=torch.float16).cuda()

input_ids = tok("The capital of France is", return_tensors="pt").input_ids.cuda()
past_key_values = None  # ← KV cache,初始为空

for _ in range(20):
    with torch.no_grad():
        out = m(input_ids=input_ids, past_key_values=past_key_values, use_cache=True)
    next_id = out.logits[:, -1, :].argmax(-1, keepdim=True)
    past_key_values = out.past_key_values  # ← 累积 cache
    input_ids = next_id  # ← 下一轮只喂"新 token",前面的从 cache 取
    if next_id.item() == tok.eos_token_id: break
# 关掉 use_cache 让你直观感受差距——同样输出 20 token,慢 10-50 倍
常见误区 + 实践场景
"开了 KV cache 就够快了"——错。真实生产里 cache 本身成了瓶颈:长 context 时显存装不下、batch 之间共享 prefix 又被重复存。所以 2023 年才有 PagedAttention(vLLM)借用 OS 虚拟内存的"分页"思想管理 KV cache、Prefix Caching 跨请求共享公共前缀的 KV——本质上把"内存分配器"的全套学问搬进了 LLM 推理引擎。
📌 BigCat 场景:自己跑本地 LLM(LM Studio / Ollama)时,上下文越长生成越慢不是错觉,是 KV cache 在显存带宽里被反复搬运。这就是为什么"先压缩历史再问下一句"比"无脑加长 context"快得多——你在直接减少 KV 搬运量。
Takeaway + 思考题
💡 LLM 推理是显存带宽问题不是算力问题。理解这一点,所有后续优化都自然了。
🤔 你熟悉的哪些后端系统也是"看起来 CPU 紧张,实际上是 I/O / 内存带宽紧张"?两类问题的优化策略有什么共通点?

推测解码Speculative Decoding

延迟并行
一句话类比

和 CPU 的分支预测 + 推测执行(speculative execution)是同一个套路:用一个便宜的"小模型"先抢跑几步,再让"大模型"一次性并行验证。验证通过就赚到,验证失败就回滚。后端类比:乐观锁(先写后查,冲突再重试)、predicate pushdown(先用便宜的过滤器砍掉大部分数据)。

它解决什么问题 + 工作机制

Decode 阶段每次只算 1 个新 token,但每次都得把整个 70B 模型的参数从显存搬到 GPU 算一遍——参数搬运 = 140 GB(FP16),即使 H100 也要花约 50ms。瓶颈是带宽不是算力,意味着 GPU 算力其实大量闲置。Speculative decoding 的洞察:既然 forward pass 一次的带宽成本是固定的,那一次验证 5 个候选 token几乎不增加成本——免费的并行。

机制三步走:

  • ① Draft(草稿):用一个 1B 量级的小模型连续生成 K 个 token(K 通常 4-8)。小模型快 10-20 倍,串行跑 K 步成本很低;
  • ② Verify(验证):把这 K 个 token 一次性喂给大模型,一次 forward pass 同时算出每个位置大模型自己会预测什么;
  • ③ Accept / Reject:从头比对,连续相同的部分接受;第一个不同的位置用大模型的预测替换、丢弃后面的草稿。
一轮 speculative decoding

Draft 小模型生成 5 个候选:Thecatsatonmat
大模型一次 forward 并行验证:Thecatsatamat丢弃
结果:本轮 1 次大模型调用产出 4 个 token(vs 普通解码 4 次调用)

关键不变量:输出分布和原大模型完全一致(数学可证)——不是近似,是无损加速

性能数学:设接受率 α,每轮 K 个草稿,平均产出 (1-αK+1)/(1-α) 个 token。α=0.7、K=5 时约 2.9 token / 大模型调用——理论 2.9x 加速。实测 1.5-3x,剩余开销在小模型本身。Medusa / EAGLE 等变体把"小模型"换成在大模型上加几个轻量 head,省掉独立小模型的开销,加速比可达 3-4x。

代码示例
# HuggingFace transformers 内置 assisted_generation 即 speculative decoding
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

big   = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-70B", torch_dtype=torch.float16, device_map="auto")
draft = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B",  torch_dtype=torch.float16, device_map="auto")
tok   = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-70B")

prompt = tok("Write a haiku about caching:", return_tensors="pt").to("cuda")

# assistant_model 就是"草稿模型"——大小模型必须用同一家族(共享 tokenizer + 词表)
out = big.generate(
    **prompt,
    assistant_model=draft,       # ← 关键参数
    max_new_tokens=100,
    do_sample=False,
)
print(tok.decode(out[0]))
# 输出和不开 assistant_model 时完全一致,速度 1.5-3x。
# vLLM / TensorRT-LLM / SGLang 在生产端有更优实现(PagedAttn + speculative 融合)
常见误区 + 实践场景
"加大 K 一定更快"——错。K 太大时小模型偏离大模型越多,接受率 α 急剧下降,反而做了一堆白工。还有一个隐藏陷阱:batch size > 1 时 speculative 收益骤降,因为大模型本来就在用 batch 把带宽用满,再叠并行验证收益边际。生产经验:低 batch + 长输出场景(如交互式 chat)受益最大;高 batch + 短输出(如批量分类)几乎没用,关掉更省。
📌 BigCat 场景:本地跑 Ollama 时同时下载一个同家族的 1B 模型当 draft,对 70B 主模型加速 2x——交互式对话的"首字延迟"和"打字速度"都会肉眼可见地改善。这是个人 AI 超级个体工作流里最容易吃到的免费午餐。
Takeaway + 思考题
💡 Speculative decoding 是无损加速——输出和原模型 bit-for-bit 一致,只是把闲置的算力换成了延迟。
🤔 "用便宜的近似先抢跑,再用昂贵的精确算法校验"——你能在自己的工作里找到几个对应的工程或决策模式?

连续批处理Continuous Batching

吞吐调度
一句话类比

阻塞式线程池升级到异步事件循环的范式跃迁——和 Node.js / Netty / asyncio 是同一个内核思想。传统 static batching 像"木桶效应":整批等最慢那个请求生成完才能换下一批;continuous batching 像 OS 调度器,谁先完成就立刻让出位置,新请求填进来。vLLM 2023 论文把这件事推向主流,让吞吐量直接涨 5-23 倍。

它解决什么问题 + 工作机制

LLM 请求的输出长度方差极大:有人要 10 token,有人要 2000 token。传统做法是"request-level batching"——攒 8 个请求一起跑,但因为 batch 同步前进,所有人都得等最慢那个 2000 token 跑完。中间 GPU 大量空转,吞吐拉胯。

Continuous batching(也叫 iteration-level scheduling)把调度粒度从"整个请求"降到"每一步 decode":

  • 每生成一个 token 后调度器都重新审视一次 batch;
  • 完成的请求立刻退出,释放它在 KV cache 里的内存;
  • 等待队列里的新请求立刻填进空位,开始 prefill;
  • 同一个 batch 里 prefill 和 decode 阶段可以混跑(chunked prefill 进一步把长 prefill 拆成小块插到 decode 间隙)。
Static batching(旧)
T1: R1R2R3R4 并行 decode
T2: R1R2R3R4 R2 完成但还在占位
T3: R1R3R4 GPU 空转 ↑
T4: R1R4 继续浪费

Continuous batching(vLLM)
T1: R1R2R3R4 正常并行
T2: R1R5R3R4 R2 完成→ R5 立刻补位
T3: R1R5R6R4 R3 完成→ R6 补位
每一步 GPU 都满载,吞吐 5-23x

实现挑战在 KV cache 内存管理:传统做法给每个请求预分配最大长度的 KV 显存,浪费 60-80%。vLLM 的 PagedAttention 把 KV 切成固定 4KB 的"页"按需分配(直接借用 OS 虚拟内存的分页思想),让 continuous batching 真正可用。这是 2023 年系统侧最重要的 LLM 服务突破,今天 vLLM / SGLang / TensorRT-LLM / TGI 全都用类似机制。

代码示例
# vLLM 把 continuous batching 默认开启——你只管批量 submit 请求即可
from vllm import LLM, SamplingParams

llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct",
          gpu_memory_utilization=0.9,
          max_num_seqs=256)  # ← 最大并发请求数,调度器自动 in-flight 调度

prompts = [
    "Explain caching in one paragraph.",   # 输出短
    "Write a 1000-word essay on consciousness.",  # 输出长
    "What is 2+2?",                          # 极短
    # ... 几十几百个混合长度请求一起喂
]
params = SamplingParams(max_tokens=1024, temperature=0.7)

outputs = llm.generate(prompts, params)
# 短请求 1-2 秒就返回不必等长的,长请求继续跑——同一 batch 持续被新请求填满
# 同样硬件下,吞吐对比 HuggingFace transformers 通常 10x+,
# 这是为什么所有生产 LLM serving 都用 vLLM/SGLang/TensorRT-LLM 而不是裸 transformers
常见误区 + 实践场景
"continuous batching 让单个请求变快"——错。它不降低单请求延迟,反而可能因为和其他请求共享 GPU 稍微变慢。它优化的是吞吐量(系统每秒服务多少 token)——你为多人服务时的总成本,不是你一个人发请求时的体感速度。OpenAI / Anthropic API 之所以能定到 $0.5-3 / 1M token 的价格,靠的就是这套底层调度把 GPU 利用率从 30% 推到 80%+。
📌 BigCat 场景:自己开发个人 AI 工具时,一个人用就别上 vLLM——HuggingFace transformers + KV cache 完全够。但如果你做一个给家人朋友共用的 AI 助手(哪怕只有 5-10 人),换 vLLM 立刻能在同一台机器上服务 10x 用户,单位成本断崖式下降。判断分水岭:是否有并发请求
Takeaway + 思考题
💡 Continuous batching 是调度问题不是模型问题——同样的模型权重,换个服务框架吞吐能差 10x。
🤔 你做过的系统里,"调度粒度从大变小"(线程→协程、批处理→流处理)带来过什么数量级的性能跃迁?背后的不变模式是什么?

量化Quantization

压缩精度
一句话类比

量化 = 神经网络的 JPEG 压缩——主动接受可控的有损精度损失换取显存和速度的几何级数收益。后端类比:定点数代替浮点数(嵌入式系统传统手法)、列存压缩(Parquet 用 dictionary encoding 把 string 压成 int)、protobuf 的 varint(小数字用更少字节)。所有这些都在押注:"数据的分布特性允许我们用更少 bit 还原原貌。"

它解决什么问题 + 工作机制

Llama-3 70B 原版 FP16 占 140 GB 显存——单张 H100(80 GB)装不下。但 H100 卖 ¥30 万。量化到 INT4 后只占 35 GB,一张消费级 RTX 4090(24 GB)配上 CPU offload 就能跑,成本差 100 倍。这就是量化为什么是本地 / 边缘部署 LLM 的入场券

核心公式x_int = round((x_float - zero) / scale),反量化 x_float ≈ x_int * scale + zero直觉:把浮点数线性映射到一个小整数区间——scale 是"每步多大",zero 是"哪里是零点"。FP16 的 [-65504, 65504] 压到 INT8 的 [-128, 127] 就丢精度,但模型权重的分布是钟形曲线,大部分集中在零附近,损失远比想象中小。

主流方案对比(精度 vs 显存)

FP16 / BF16 16 bit · 100% 显存 · 0 损失 · 基线
FP8 8 bit · 50% · ~0 损失 · H100/B200 硬件支持
INT8 (W8A8) 8 bit · 50% · <1% 精度损失 · SmoothQuant/LLM.int8()
INT4 (GPTQ/AWQ) 4 bit · 25% · 1-3% 损失 · 主流本地部署
INT2 / GGUF Q2 2-3 bit · 12% · 显著损失 · 极限压缩

三种主流算法的工程取舍:
GPTQ:post-training,按层逐个最小化量化误差,准但慢(小时级)
AWQ:保留 1% 的"重要权重"不量化(按激活幅度排序),速度精度平衡
GGUF (llama.cpp):多种 Q2-Q8 等级,CPU/Mac 友好,本地玩家首选
QAT:训练时模拟量化误差,最准但要重新训练(成本高)

权重量化(weight-only) vs 激活也量化(W·A)是两条不同路线:仅权重量化省显存 + 加快内存搬运(因为 LLM 推理瓶颈正是带宽),实现简单;激活也量化能用上 GPU 的 INT8 tensor core 真正提速但精度更难控。社区共识:本地推理用 weight-only INT4(AWQ/GPTQ)云端高吞吐用 W8A8 / FP8

代码示例
# 用 bitsandbytes 直接加载 4-bit 量化模型——一行配置搞定
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

qcfg = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",        # normal-float 4,QLoRA 论文提出,对正态分布最优
    bnb_4bit_use_double_quant=True,    # 量化"量化常数",再省一点
)
m = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    quantization_config=qcfg,
    device_map="auto",
)
tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")

ids = tok("Define entropy in one sentence.", return_tensors="pt").to("cuda")
print(tok.decode(m.generate(**ids, max_new_tokens=100)[0]))
# 8B 模型从 16GB 降到 5GB——M2 Mac/RTX 3060 都能跑,质量损失通常<2%
常见误区 + 实践场景
"INT4 损失只有 1-3%,几乎免费"——分情况。在多选题、摘要这类任务上确实几乎无感;但在数学推理、代码生成、长链 CoT、罕见语言上量化损失会被链式放大——8B 量化版有时输出会偏离原版很远,看起来"变笨了"。评估纪律:在你自己的真实任务上对比量化版 vs 原版的输出,别只信公开 benchmark。Karpathy 一再提醒:benchmark 上 1% 损失,到 agent 长链任务里可能是 30% 任务失败率。
📌 BigCat 场景:把 70B 模型 4-bit 量化跑在 MacBook M3 Max(48GB 统一内存)做离线私密对话——读论文、写日记、复盘家庭决策——隐私级别远高于云 API。这是"AI 超级个体"很重要的一块拼图:云端用最强模型做开放任务,本地量化模型做隐私敏感任务,按数据敏感度分流。
Takeaway + 思考题
💡 量化是有损压缩——必须在你的真实任务上量化前后对比评估,benchmark 数字不够。
🤔 "用更少 bit 表达同样的意思"在你的工作中还有哪些场景?信息论的"有效熵"和工程上的"表示精度"是同一回事吗?

深入资源Further Reading

深入思考Deep Questions

1. 为什么说"LLM 推理是显存带宽问题不是算力问题"?这个认知如何重塑你对 GPU 选型 / 成本估算 / 优化优先级的判断?
核心数据:H100 算力 ~989 TFLOPS(FP16),显存带宽 ~3 TB/s。Llama-70B 推理 decode 一个 token,参数量 70B × 2 bytes = 140 GB 必须从显存搬到计算单元——理论极限 140/3000 ≈ 47ms,无论算力多强,下界就是这个。这就是"memory-bound"的真实含义。重塑判断:(a) 选卡看 HBM 带宽不是 TFLOPS——H100 vs H200 算力一样,但 H200 显存 141GB / 4.8 TB/s 带宽,推理 LLM 直接快 40-60%;(b) batch 大小是免费午餐——同一次参数搬运服务多个请求,吞吐线性涨直到算力打满(这就是 continuous batching 的物理基础);(c) 量化的真正价值是减少搬运,不是减少计算——INT4 让搬运量从 140GB 降到 35GB,立刻 4x 加速;(d) speculative decoding 利用"算力闲置"——既然带宽是瓶颈,多算几个验证 token 几乎免费。BigCat 你的分布式背景里"I/O bound vs CPU bound"诊断习惯直接迁移过来:用 nvidia-smi 看 GPU 利用率,发现持续 30-50% 而不是 90%+ 时,几乎必然是带宽问题,加 batch / 加 speculative / 加量化都比换卡有效。
2. KV cache (Day 9) 和 prompt caching (Day 8) 听起来都在"缓存",但它们解决的问题完全不同。说清楚区别后,你会怎么组合用?
这是非常容易混淆的两层。KV cache单次请求内部的优化——生成第 N 个 token 时复用前 N-1 个 token 已经算好的 K/V,每次请求结束就丢弃。这是推理引擎层的事,对应用透明,不开等于 O(n³) 不可用。Prompt caching跨请求之间的优化——把多次请求都用的 system prompt / few-shot / 工具 schema 的 KV 持久化到显存或 SSD,下次请求命中就跳过 prefill。这是应用层 + 服务端共同优化,要应用主动标记缓存断点。组合使用:(a) 让你的 prompt 结构稳定不变的部分前置 → prompt caching 命中;(b) 单次推理内部,引擎自动用 KV cache;(c) 在 vLLM / SGLang 上还有第三层 radix tree based prefix cache——多个并发请求共享相同前缀的 KV,比 prompt caching 更细粒度且自动。三层叠加:跨请求 prefix 缓存 + 单请求 KV 缓存 + 显式 prompt caching = LLM 服务最完整的"缓存栈"。这个分层结构和数据库的 buffer pool / query plan cache / materialized view 几乎是一对一映射。
3. Speculative decoding 用"小模型猜 + 大模型验"的模式 —— 这个"廉价近似 + 精确验证"的思想能不能扩展到 Agent 工作流?
能且已经在发生。映射模式:把大模型当 verifier小模型 / 工具 / 启发式当 proposer。具体例子:(a) 多路径推理——让 8B 模型并行生成 5 个解题思路,70B 模型只对最有希望的 1-2 个继续展开,成本比纯 70B 思考省 5-10x;(b) 工具调用预筛——小模型先决定调哪个工具、参数大概是什么,大模型只在小模型不确定时介入;(c) Code agent 中的 lint-then-think——先跑 type checker / linter 拿"廉价信号",把明显错误剔除,再让大模型集中处理真正难的;(d) RAG 重排——便宜的 BM25/embedding 召回 1000 条 → 中等模型 rerank 到 20 → 大模型只读 top-5。共同模式:用便宜的过程切除大量明显错误的可能性,让昂贵的资源只处理高价值不确定性。这其实是所有工程系统应对"判断力昂贵 + 候选爆炸"的通用解——查询优化器、CDN 命中分层、人类组织的"流水线审核 → 专家终审"都是这个模式。BigCat 你做"AI 超级个体"时这个思路特别重要:不是所有问题都需要 Opus,先用 Haiku 过一遍,再用 Opus 处理 Haiku 标记为"不确定"或"高风险"的——同样输出质量下成本能降 5-10x。
4. 量化是有损压缩,但 LLM 量化损失比预期小很多——这背后有什么深层结构性原因?和神经科学/信息论的什么发现可以对照?
这是 ML 里一个深刻而美丽的现象,连结到几个跨学科观察。表面原因:(a) 神经网络权重是钟形分布,绝大部分集中在零附近,浪费高精度的 head/tail 几乎无信息;(b) 神经网络有大量冗余,多个权重的组合能补偿单个的量化误差(类似纠错码的冗余编码);(c) 推理时输入也是分布偏中心化的,量化噪声被后续 layer 的非线性"平均掉"。更深层的对应:(i) 神经科学——生物神经元的 spike 本质是 1-bit 离散信号,整个大脑用极低精度做高级认知,证明智能不依赖高精度连续表示;(ii) 信息论——Shannon 的 rate-distortion 理论早就说过,对于结构化数据,有效信息量远小于原始 bit 数,量化只是逼近真实的"有效熵";(iii) 稀疏编码理论(Olshausen-Field 1996)——感觉皮层用稀疏低精度编码自然图像,效率远高于稠密表示;(iv) 佛学/认知科学的"够用就好"——感知和决策从不追求精确,而是追求足够好的辨别力,这正是量化在做的事。这个观察反过来给个体启发:追求"高精度"未必是智能的本质,"在正确的粒度上做对事"才是。BigCat 你做跨学科类比时会注意到这个 pattern 反复出现——大脑、压缩算法、组织管理、个人决策,都在解同一个问题:受限资源下保持有效辨别力
5. 推理优化让"小模型 + 优化"和"大模型 + 默认设置"的成本曲线发生交叉——这对个体使用 AI 的策略意味着什么?
关键事实:今天一个量化到 4-bit 的 Llama-3.1-70B 跑在 M3 Max 上,约等于一年前的 GPT-4 能力,零边际成本 + 完全本地 + 离线可用。一个 14B-32B 的小模型用 speculative + vLLM 服务,单 token 成本比直接调 API 低 5-20x。这意味着能力曲线和成本曲线正在解耦:以前"想用更强能力 = 多付钱",现在"想用更强能力 = 选对模型 + 用对优化"。对个体策略的含义:(a) 按敏感度分流——隐私强相关(家庭、健康、财务、私人想法)走本地量化模型,开放任务走云端最强模型;(b) 按延迟需求分流——交互式打字走 speculative decoding 优化的小模型,批量任务走云端高吞吐;(c) 按预算长尾分流——大量低价值高频查询(搜索、改写、翻译)走本地 / 小模型,少数高价值决策(重大判断、长链推理)才动用 Opus;(d) "AI 工具箱"而非"AI 单工具"心态——像选编程语言一样选模型,永远是 fit-for-purpose。BigCat 你追求"AI 超级个体"的本质,就是设计自己的 LLM 推理栈——一个本地 + 多云端、按数据敏感度和价值分级的路由系统。这套架构思维你在分布式系统里已经无比熟悉,迁移到个人 AI 工作流是顺势而为。一年内,"个人 AI 编排架构师"会成为有竞争力的隐形技能。