大模型推理从入门到实测

跟着六个动手阶段,弄懂模型怎样生成一个 Token,再亲手把它跑成服务、测出瓶颈。

开始学习
芯片与周围细密的电路走线
推理芯片概念图输入 / 执行 / 测量
Tensor → Module

阶段一 · Python 与 PyTorch 基础

形状 · 设备 · 精度

学习内容

掌握 Python 函数、类、文件与 JSON 读写、虚拟环境和依赖安装。学习 PyTorch Tensor 的形状、索引、广播、矩阵乘法、reshape、transpose,以及 nn.Module、forward()、device 和 dtype。

动手任务

在 Windows 的 VS Code 里写一个小型 PyTorch 模型,用 eval() 和 inference_mode() 完成前向推理。打印每一层的张量形状、数据类型和设备,再把模型与输入一起移到 GPU 上运行。

验收标准

能在运行前推导每层的输出形状。遇到矩阵维度或 CPU/GPU 设备不匹配时,能定位出错的张量并修正。

展开详细讲解与实验

机制怎么工作

张量的形状描述各轴长度,dtype 描述元素的表示方式,device 描述数据所在设备。对 [批大小, 序列长度, 特征数] 使用 Linear(4, 6) 时,每个位置独立做 4 → 6 的线性变换,前两轴保持不变。eval() 切换模块行为,inference_mode() 关闭这次推理所需之外的自动求导跟踪,实际推理通常需要两者。

一步一步理解

先把一层模型算出来

设输入向量 x=[1,2],权重矩阵的两行为 [3,4] 和 [5,6],偏置 b=[1,-1]。第一项输出为 1×3+2×4+1=12,第二项为 1×5+2×6−1=16。因此 Linear(2,2) 将这个位置变成 [12,16]。把多个位置放进同一张量时,每个位置都使用同一组权重分别计算。

为什么要保留批和序列两根轴

形状 [2,3,4] 中,第一根轴标记两条独立样本,第二根轴标记各自的三个位置,最后一根轴存四个特征。变成 [6,4] 虽然保留全部元素,却把样本边界藏起来了。逐位置线性层可以这样算,再还原形状;涉及位置间关系的注意力必须知道哪些位置属于同一条序列。

一次正确的计时

先运行若干次相同形状的前向,完成初始化与预热。GPU 测试在开始读时钟前同步,在前向结束后再次同步,再读结束时钟。重复多次,保留每次耗时。如果只研究模型前向,把输入搬移放在计时区间之外;如果研究完整请求,就把搬移计入并注明。

自测与答案

问题:输入 [8,16,32] 经过 Linear(32,64),输出是什么?答案是 [8,16,64],因为最后一轴从 32 变为 64。问题:eval() 后为什么还能产生梯度?因为它控制模块的评估行为;是否记录求导信息由求导模式另行控制。

动手步骤

  1. 新建 Python 虚拟环境,按 PyTorch 官方指引安装与你的设备匹配的版本。第一次可以直接在 CPU 上跑。
  2. 先在纸上写下输入、权重和输出的形状。运行代码并核对输出,再将输入最后一维改为 5,观察并解释报错。
  3. 有可用 GPU 时记录同一段代码的设备与精度。若要比较延迟,先预热,并在计时区间前后同步 GPU,区分前向计算和数据传输。
forward_demo.py
import torch
from torch import nn

torch.manual_seed(0)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = nn.Linear(4, 6).to(device).eval()
x = torch.randn(2, 3, 4, device=device)

with torch.inference_mode():
    y = model(x)

for name, tensor in [("input", x), ("weight", model.weight), ("output", y)]:
    print(name, tuple(tensor.shape), tensor.dtype, tensor.device)
assert y.shape == (2, 3, 6)

预期结果

输入是 (2, 3, 4),权重是 (6, 4),输出是 (2, 3, 6)。记录实际设备,并解释为什么权重的两个维度以 out_features、in_features 排列。

手算与排查

按维度复核

输入有 2 × 3 = 6 个位置,每个位置各做一次 4 → 6 的变换,输出共 36 个数。权重有 6 × 4 = 24 个数,另有 6 个偏置。若是 FP32,这个线性层的 30 个参数本身占 120 字节,未计张量元数据与中间结果。

从报错倒推

维度错误时先打印输入最后一维和权重第二维。设备错误时同时打印 model.weight.device 与 x.device。类型错误时查看 dtype。先修一项,再运行同一输入复核。

常见误区

只把模型移到 GPU 会造成设备不匹配。仅用主机时钟包住 CUDA 调用,还可能只测到提交工作所花的时间。

本课术语

Text → Logits

阶段二 · 文本表示与 Transformer 结构

Tokenizer · Q/K/V · 因果掩码

学习内容

学习 Token、词表、Tokenizer、Embedding、位置编码、Q/K/V、多头注意力、因果掩码、FFN、残差连接和层归一化。理解 Decoder-only Transformer 如何把 Token ID 变成 logits。

动手任务

用真实 Tokenizer 编码一句中文和一句英文,观察切分结果。用 PyTorch 写一个简化的因果自注意力模块,打印 Q、K、V、注意力分数和输出的形状。

验收标准

能画出「文本 → Token ID → Embedding → Transformer → logits」的数据流,说清 [批大小, 序列长度, 隐藏维度] 各表示什么,并解释因果掩码为什么要挡住未来位置。

展开详细讲解与实验

机制怎么工作

Tokenizer 把文本变成离散 ID,Embedding 把 ID 映射到向量。每个注意力头用当前位置的 Q 和各键位置的 K 计算分数,缩放并加上掩码,再用 Softmax 得到对 V 的加权系数。FFN 分别处理各位置,残差和归一化帮助信息在层间传播。Decoder-only 模型最终输出每个位置对词表的 logits。

一步一步理解

从一句话走到词表分数

设一句话经过分词后得到三个 ID,示意为 [17,203,8]。这些 ID 本身只是词表索引。Embedding 为每个 ID 查出一个长度为 H 的向量,得到 [1,3,H]。模型逐层更新这三个位置的表示,最后把每个位置映射到大小为 V 的词表,得到 [1,3,V] 的 logits。

注意力的四次运算

先用输入表示分别经过线性映射得到 Q、K、V。再用每个查询向量与各键向量做点积,得到匹配分数,并除以头维度的平方根。随后加入因果掩码,对有效键位置做 Softmax。最后按归一化权重对各个值向量求和。分数决定读取比例,值向量提供被读取的信息。

多头怎样拆分与合并

若隐藏维度是 16,采用四个头,每头维度为 4,可将投影结果 [B,T,16] 重排成 [B,4,T,4]。各头独立计算注意力,再把结果重排回 [B,T,16],交给输出映射。重排时需分清 reshape 的轴划分与 transpose 的轴交换。

自测与答案

问题:第三个位置能否读取第二个位置?可以,第二个位置属于它的历史。问题:既然 Prefill 能同时算所有位置,为什么还要掩码?并行计算描述执行方式,掩码规定信息依赖,保证某位置的预测只使用它有权看到的上下文。

动手步骤

  1. 安装 Transformers,首次运行会下载示例模型的 tokenizer 文件。比较同一意思的中英文如何切分,记录 ID 数量和对应的 token 字符串。
  2. 构造形状 [批大小, 头数, 序列长度, 头维度] 的 Q/K/V。将最后一个位置的 V 改大,只检查前三个位置的输出。
  3. 在原理图里选第三行,用同样的例子画出第四个键位置被遮住的原因。再关掉 is_causal,对比前三个位置的输出。
causal_attention.py
import torch
from torch.nn.functional import scaled_dot_product_attention
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
for text in ("大模型如何推理?", "How do language models infer?"):
    ids = tokenizer(text, add_special_tokens=False).input_ids
    print(text, ids, tokenizer.convert_ids_to_tokens(ids))

torch.manual_seed(0)
q, k, v = [torch.randn(1, 2, 4, 8) for _ in range(3)]
changed = v.clone()
changed[:, :, -1, :] += 10
base = scaled_dot_product_attention(q, k, v, is_causal=True)
new = scaled_dot_product_attention(q, k, changed, is_causal=True)
torch.testing.assert_close(base[:, :, :3, :], new[:, :, :3, :])
print("Q/K/V:", q.shape, "scores:", (1, 2, 4, 4), "output:", base.shape)

预期结果

输出形状为 [1, 2, 4, 8]。只改第四个 V 时,前三个输出通过一致性断言。能解释分数矩阵最后两维分别是哪类位置。

手算与排查

把分数算一遍

在图中选查询 C。假设它与 A、B、C、D 的缩放后分数为 [1, 2, 3, 100]。D 是未来位置,掩码让它在 Softmax 后的权重为 0。前三个权重约为 0.09、0.24、0.67,因此当前输出约是 0.09 V_A + 0.24 V_B + 0.67 V_C。

沿形状找错误

示例的 Q/K/V 是 [1, 2, 4, 8]。每个头有四个查询和四个键,分数张量是 [1, 2, 4, 4]。对最后一轴做 Softmax,再乘 V,输出回到 [1, 2, 4, 8]。如果把查询与键的轴弄反,非对称掩码会暴露问题。

常见误区

矩阵中允许访问的位置仍需经过分数和 Softmax,允许访问不代表它会分到较大的权重。单个 token 也可能是字、词片段或字节相关表示。

本课术语

Prefill → Decode

阶段三 · 自回归生成与 KV Cache

自回归 · 采样 · KV Cache

学习内容

掌握 Prefill 如何处理输入并建立 KV Cache,首个新 Token 如何由最后一个输入位置的 logits 选出,以及 Decode 如何复用历史 K/V 逐步生成。学习贪心解码、Temperature、Top-k、Top-p、EOS 和生成长度限制。

动手任务

用 Transformers 跑通一个小型模型的 generate(),再用 forward() 写一个最小贪心生成循环。固定输入,分别开启和关闭 KV Cache,记录每步送入模型的 Token 数和生成结果。

验收标准

能从第一步讲清一段回答是怎么生成的,解释一次 forward() 和整段 generate() 的关系,并指出开启缓存后省掉了哪些历史计算。

展开详细讲解与实验

机制怎么工作

Prefill 处理整个提示词并写入 K/V,首个输出从最后一个输入位置的 logits 采样。进入 Decode 后,上一轮选出的 Token 才作为新输入写入缓存。贪心取最大 logit,采样可以用 Temperature、Top-k 或 Top-p 调整候选分布。EOS 与输出上限都可能结束生成。

一步一步理解

为什么用最后一个输入位置

自回归模型把位置 i 的输出用于预测位置 i 后面的 Token。输入 A、B、C 时,A 的输出对应预测 B,B 的输出对应预测 C,C 的输出才对应尚未给出的新位置。生成首个新 Token 时读取 C 的 logits;训练时则可以同时利用多个位置的预测构造损失。

缓存为什么能复用

在标准因果注意力中,历史位置的表示只依赖它自己和更早的位置。追加一个新 Token 不会让历史位置突然看见未来,因此在模型、位置与上下文条件一致时,历史 K/V 可以保留。当前新位置仍需要计算自己的 Q/K/V,并用 Q 读取历史缓存。

缓存到底减少了多少输入

提示词长度为 100,继续生成四个 Token。无缓存的四次前向分别送入 100、101、102、103 个位置,合计处理 406 个输入位置。有缓存时送入 100、1、1、1,合计 103 个。这个计数帮助理解重复计算,但实际耗时还受每步注意力范围、并行度和访存影响,不能直接当成加速倍数。

自测与答案

问题:刚刚输出 D 时,缓存中一定有 D 吗?首个新 Token 刚选出时还没有,因为 D 尚未作为输入执行前向。问题:采样结果不同就能断定缓存有错吗?不能,先固定输入 ID、贪心规则、精度和设备,再定位首个分歧。

动手步骤

  1. 参考模型卡安装兼容版本的 PyTorch 和 Transformers,下载小型 Dense 模型。先用 CPU 验证流程,模型加载需要足够内存。
  2. 固定同一提示词和贪心规则,用 generate() 得到参考结果,再比较手写循环开启和关闭缓存时的每次输入长度与生成 ID。
  3. 将注意力掩码与生成序列一起增长。若首个分歧出现,检查特殊 Token、EOS 设置、输入模板、精度,以及模型是否有特殊 logits 处理规则。
greedy_cache.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(name)
model = AutoModelForCausalLM.from_pretrained(name).eval()
prompt = tokenizer("The largest planet is", return_tensors="pt")
stop = model.generation_config.eos_token_id
stop_ids = set(stop if isinstance(stop, list) else [stop])

@torch.inference_mode()
def greedy(use_cache):
    ids = prompt.input_ids
    mask = prompt.attention_mask
    cache = None
    lengths = []
    for _ in range(8):
        current = ids if cache is None else ids[:, -1:]
        lengths.append(current.shape[-1])
        result = model(input_ids=current, attention_mask=mask,
                       past_key_values=cache, use_cache=use_cache)
        chosen = result.logits[:, -1, :].argmax(dim=-1, keepdim=True)
        ids = torch.cat([ids, chosen], dim=-1)
        mask = torch.cat([mask, torch.ones_like(chosen)], dim=-1)
        cache = result.past_key_values
        if chosen.item() in stop_ids:
            break
    return ids, lengths

with torch.inference_mode():
    reference = model.generate(**prompt, max_new_tokens=8, do_sample=False)
for use_cache in (False, True):
    ids, lengths = greedy(use_cache)
    print(use_cache, lengths, "matches generate:", torch.equal(ids, reference))
    print(tokenizer.decode(ids[0], skip_special_tokens=True))

预期结果

提示词有 N 个 Token 时,未缓存的输入长度为 N、N+1、N+2……,开启缓存为 N、1、1……。输出 ID 通常一致,记录可能的首次差异和运行条件。

手算与排查

逐步对账

输入 A、B、C 时,首次前向写入三个位置并产出 D。第二次前向的输入是 D,缓存增长到四个位置并产出 E。把每一步的输入长度、缓存长度、输出 ID 逐行记下。刚输出的 E 要等下一次前向才会写入 KV。

排查输出不一致

先确认两条路径的输入 ID 完全相同,然后比较首个不同 Token 的 logits 和候选分数。缓存路径每次只交新输入,但掩码覆盖完整有效上下文。若分数接近,也要记录数据类型和设备,区分舍入差异与逻辑错误。

常见误区

K/V 缓存复用省去历史位置的重复计算。当前 Q 对历史 K/V 的读取仍有开销,上下文增长时尤其需要考虑显存带宽与缓存容量。

本课术语

Cache → Schedule

阶段四 · 推理效率与常见优化

批处理 · 前缀复用 · 量化

学习内容

理解 KV Cache 的显存开销、连续批处理对不同进度请求的调度、前缀缓存对相同前缀 K/V 的复用,以及低比特量化如何降低存储开销。用 SGLang 的 RadixAttention 理解前缀复用。PagedAttention、投机解码、Prefill/Decode 分离先了解用途。

动手任务

根据模型层数、KV 头数、头维度和数据精度,估算每增加一个 Token 所需的 KV Cache 字节数。用三个长度不同的请求画出静态批处理和连续批处理的调度过程。

验收标准

给定长上下文、请求陆续到达、多请求共享长前缀三种场景,能分别说清瓶颈在哪里、哪种优化可能有效,以及应该测什么来验证。

展开详细讲解与实验

机制怎么工作

对标准全上下文注意力的未分片 KV Cache,元素数由 K/V 两份、层数、KV 头数、头维度与缓存长度相乘得到。连续批处理可以在生成迭代之间换入新请求,前缀缓存复用相同前缀的 K/V,量化改变数值表示和存储大小。这几种办法分别作用于调度、重复计算与存储。

一步一步理解

把 KV 公式逐项拆开

每个位置在每层保留一份 K 和一份 V,所以有系数 2。每份包含 KV 头数乘头维度个元素,再乘每元素字节数得到一层一个位置的字节数。乘层数得到每 Token 总量,乘缓存长度得到单请求数据量。多个互不共享的请求还要分别求和。

共享前缀什么时候划算

两条请求若都以同一段长文开头,框架可能复用该段的 K/V,第二条请求只计算新增后缀。需要匹配的是模型实际看到的 Token 序列与兼容的计算条件。对话模板中的角色符号、位置或文本差异都可能影响命中。实验应记录冷缓存与热缓存,避免把预热收益混入前缀复用收益。

量化和调度怎样分别验证

评价量化时同时记录权重或缓存的精度、显存、延迟和输出质量。低位宽可以减少数据大小,但转换开销与硬件支持会影响速度。评价调度时保持模型与输入不变,比较请求进入服务后的等待、完成时刻与吞吐,观察长短请求混合后谁受益、谁等待更久。

自测与答案

问题:查询头数 32、KV 头数 8,缓存公式用哪个?用 8。问题:模型权重变成 4 位后,KV 一定也是 4 位吗?不一定,缓存有自己的数据类型。问题:教学图从五步缩短到三步能直接代表真实服务快 5/3 倍吗?需要实际计时,因为图省略了每步耗时变化与换入成本。

动手步骤

  1. 用下面脚本算出每增加一个 Token 的 KV 字节数,再分别代入 4,096 和 32,768 个 Token。和网页计算器交叉核对。
  2. 在批处理图中设置两个槽位。A、B 在第一步前到达并分别生成 1 和 3 个 Token,C 在第二步前到达并生成 2 个 Token。逐步比较 C 从哪一步开始执行。
  3. 为长上下文、请求持续到达和共享长前缀分别写一条假设。对应记录显存与 ITL、排队与吞吐、缓存命中与 TTFT。
kv_budget.py
layers, kv_heads, head_dim, bytes_per_value = 32, 8, 128, 2
bytes_per_token = 2 * layers * kv_heads * head_dim * bytes_per_value
print(f"{bytes_per_token:,} bytes / token")
for length in (4096, 32768):
    mib = bytes_per_token * length / 1024**2
    print(f"{length:,} tokens: {mib:,.0f} MiB")

预期结果

结果为每 Token 131,072 字节,4,096 Token 约 512 MiB,32,768 Token 约 4 GiB。教学时间线中 C 在静态模式第 4 步开始,连续模式第 2 步开始。

手算与排查

把单请求算到并发

假设四条请求各缓存 4,096 Token,互不共享前缀,模型和缓存精度与计算器相同,则 KV 数据合计约 2 GiB。模型权重和运行时工作区还要另算。前缀缓存能改变实际驻留量,须按框架的缓存状态测量。

区分三种瓶颈

长输入先看 Prefill 和首 Token 延迟。长输出先看逐步解码的 ITL 与显存读取。持续到达的请求先看排队、槽位利用率及尾延迟。调度时间线隔离了换入时机,实际系统还需测 Prefill 对其他请求的影响。

常见误区

用 KV 头数计算 GQA 的缓存。模型权重精度、KV 精度和实际分配是三个不同口径,分页填充、滑动窗口、分片等策略会改变实际占用。

本课术语

Request → Stream

阶段五 · 推理框架与服务系统

SGLang · 流式响应 · 服务链路

学习内容

以 SGLang 为主,掌握模型加载、离线批量推理、在线服务、对话模板和流式返回。把请求接入、调度、模型执行、缓存管理和前面学过的机制对应起来。

动手任务

用阶段三的模型启动 SGLang 服务,或者调用已有服务。写一个 Python 客户端,分别发送单轮、多轮和流式请求,记录请求发出、首个内容片段收到和生成结束的时刻。

验收标准

能独立调用服务、组织多轮消息并修改生成参数。能沿着一条实际请求讲清楚服务如何接收输入、调度计算并返回结果。

展开详细讲解与实验

机制怎么工作

在线服务接收消息后按模型的对话模板形成提示词。调度器管理排队、批次和缓存,执行器运行前向,生成内容再通过流式接口返回。多轮请求通常要带上相关历史消息,服务是否复用历史前缀取决于其缓存与调度策略。

一步一步理解

沿请求完整走一遍

客户端将消息与生成参数编码成请求,服务解析后按对话模板形成输入 Token。请求等待调度,获得缓存与执行资源后进行 Prefill,选出首 Token。后续迭代执行 Decode,生成内容逐段发送。遇到停止条件后结束请求,并按框架策略释放或保留可复用缓存。

多轮对话该发送什么

第一轮发送 user 消息,得到 assistant 回复。第二轮通常发送第一轮 user、第一轮 assistant 和新的 user 消息,顺序要完整。服务实际收到的内容决定上下文;客户端只显示过一条历史消息,并不自动意味着下一次请求包含了它。

流式输出怎样记录才清楚

给每个到达事件记录相对请求开始的时间,同时保存事件类型与非空内容。首个事件可能只有角色信息。一次内容片段可能包含多个 Token,也可能是网络缓冲后一起到达,因此仅凭片段间隔无法还原每个 Token 的真实生成间隔。能拿到服务端 Token 时间戳时,另存一份并注明时钟位置。

自测与答案

问题:客户端连不上 127.0.0.1 的远程服务时先查什么?这个地址指客户端所在机器,需确认服务到底在哪台主机。问题:请求很久才收到第一个内容片段,一定是模型前向慢吗?还可能包含连接、排队、输入预处理与网络等待,需要分段记录验证。

动手步骤

  1. 按 SGLang 官方安装指引选择支持的环境和设备。服务端示例命令只监听本机。已有服务时,将 base_url、模型名和鉴权配置替换为实际值。
  2. 先用同一客户端发送一条单轮消息,验证返回内容。再加入真实的 assistant 历史回复和新的 user 消息,检查对话模板是否生效。
  3. 请求发出前读时钟,首个非空内容片段到达时再读一次,流关闭后记录总延迟。若要采集严格的 Token 级 ITL,需要服务端或能准确给出 Token 边界的工具。
服务端与客户端准备
# 终端一:启动服务,保持运行
sglang serve Qwen/Qwen2.5-0.5B-Instruct --host 127.0.0.1 --port 30000

# 终端二:安装客户端依赖,再运行 stream_client.py
python -m pip install openai
stream_client.py
from time import perf_counter
from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:30000/v1", api_key="EMPTY",
                max_retries=0, timeout=60.0)
model = client.models.list().data[0].id
messages = [{"role": "user", "content": "用一句话解释 KV Cache。"}]
started = perf_counter()
first_content = None

with client.chat.completions.create(
    model=model, messages=messages, stream=True, max_tokens=64, temperature=0,
) as stream:
    for chunk in stream:
        piece = chunk.choices[0].delta.content if chunk.choices else None
        if piece:
            if first_content is None:
                first_content = perf_counter()
            print(piece, end="", flush=True)

ended = perf_counter()
print(f"\nE2E: {(ended - started) * 1000:.1f} ms")
if first_content is not None:
    print(f"First content: {(first_content - started) * 1000:.1f} ms")

预期结果

记录模型名、请求形态、首个内容片段延迟和总延迟。将单轮与多轮的消息结构保存下来,解释为什么二者的输入 Token 数可能不同。

手算与排查

先验证服务,再写客户端

服务启动后先访问 http://127.0.0.1:30000/v1/models,确认返回模型清单。连接失败时核对进程、端口和网络位置。收到鉴权错误时检查服务实际配置。模型能加载但请求仍失败时,先用很短的输入和输出上限排查设备内存不足。

把时间戳留在原始记录里

至少保留 request_id、start_ms、first_content_ms、end_ms、status、模型名和生成上限。记清 start_ms 和 end_ms 用的是同一进程内的共同起点。模型服务和客户端在不同机器时,别直接拿两个系统时钟相减。

常见误区

空内容的角色事件不是首个输出。客户端测量还包含连接、序列化、网络与排队开销,需和服务端计时区分。

本课术语

Measure → Explain

阶段六 · 性能基准与系统剖析

TTFT · ITL · P95

学习内容

掌握首 Token 延迟(TTFT)、Token 间延迟(ITL)、端到端延迟、吞吐量和峰值显存。区分单个请求的响应体验与整个服务的处理能力,理解 P50 和 P95 各反映什么。

动手任务

用一个 Dense 小模型建立基线。固定模型、精度和采样参数,分别改变输入长度、并发数和前缀复用条件。每组只改变一个因素,记录实际输入和输出 Token 数、软硬件环境、压测命令与结果。

验收标准

交付一套可复跑的压测脚本、一张结果表和一份简短分析。能用数据判断输入变长时首 Token 是否更慢、并发上升时吞吐与尾延迟如何变化、前缀复用是否改善了首 Token 延迟。

展开详细讲解与实验

机制怎么工作

TTFT 看请求发出到首 Token 的等待。ITL 看相邻输出 Token 之间的间隔。端到端延迟看请求完成耗时,P50 和 P95 是分位数。吞吐量按同一观测窗口内完成的请求数或生成的输出 Token 数除以窗口秒数。服务吞吐和单请求体验要一起看。

一步一步理解

用一组可解释的实验开始

先固定一个模型与精度,选三档输入长度,保持输出上限和并发不变。每档重复运行,记录实际 Token 数与失败状态。再固定输入长度扫描并发。最后用一致的负载分别测试共享前缀与不同前缀。这样每组差异都有明确的待验证原因。

分位数怎样从样本算出来

把成功请求的延迟从小到大排序。最近秩方法用 ceil(p×n) 作为从 1 开始的序号。例如十个延迟为 10、20、30……100 ms,P50 取第五个值 50 ms,P95 取第十个值 100 ms。其他插值方法可能给出不同结果,报告中应保留所用算法与样本数。

吞吐与单请求速度一起看

若一个 10 秒的窗口里完成 20 个请求,成功请求共输出 2,000 Token,则请求吞吐为 2 请求/秒,成功输出吞吐为 200 Token/秒。这并不说明每条请求都是 100 Token,也不能由此推导每个用户的等待时间。要同时查看逐请求延迟和实际输出长度。

自测与答案

问题:并发翻倍后吞吐提升但 P95 也升高,怎样判断?说明服务处理能力和较慢请求的等待发生了不同方向的变化,需要按目标延迟要求评估。问题:只保留成功请求会遗漏什么?失败与超时占用的资源、低成功率和长等待,都可能让汇总结果显得过于乐观。

动手步骤

  1. 固定模型、精度、框架版本、采样参数、输出上限与软硬件环境。用相同的预热策略,计时阶段从第一条正式请求发出开始,到最后一条正式请求完成结束。
  2. 每组只改一个因素。先扫输入长度,再扫并发,最后分别设置相同前缀和不同前缀。缓存冷、热条件分开记,每组保留成功、超时和失败记录。
  3. 以逐请求 CSV 保留配置和原始测量。记录实际输入与输出 Token 数,按口径计算 P50/P95、输出 Token 吞吐和成功率。峰值显存用同一采样策略,并说明监测窗口。
requests.csv 示例数据
request_id,status,start_ms,end_ms,input_tokens,output_tokens,ttft_ms
r01,ok,0,400,128,20,92
r02,ok,15,465,128,22,107
r03,timeout,20,520,128,,
summarize_benchmark.py
import csv
from math import ceil

with open("requests.csv", newline="", encoding="utf-8") as file:
    rows = list(csv.DictReader(file))
if not rows:
    raise SystemExit("No requests recorded.")
window = (max(float(r["end_ms"]) for r in rows)
          - min(float(r["start_ms"]) for r in rows)) / 1000
if window <= 0:
    raise SystemExit("The measurement window must be positive.")
success = [r for r in rows if r["status"] == "ok"]
ttft = sorted(float(r["ttft_ms"]) for r in success if r["ttft_ms"])
tokens = sum(int(r["output_tokens"]) for r in success)

print(f"Success: {len(success)}/{len(rows)}")
print(f"Window: {window:.3f} s")
print(f"Successful output throughput: {tokens / window:.2f} token/s")
for p in (0.50, 0.95):
    if ttft:
        print(f"P{p * 100:.0f} TTFT: {ttft[ceil(p * len(ttft)) - 1]:.2f} ms"
              f" (nearest rank, n={len(ttft)})")

预期结果

示例输出成功率 2/3,窗口 0.520 秒,成功请求输出吞吐约 80.77 Token/s,P50/P95 TTFT 分别是 92 和 107 ms。真实报告再补充 E2E 分位数、独立的 Token 时间戳与显存采样。

手算与排查

手算一条时间线

假设 0 ms 发请求,四个输出 Token 分别在 100、128、170、195 ms 到达,210 ms 收到完成事件。TTFT 为 100 ms,三段 ITL 为 28、42、25 ms,E2E 为 210 ms。如果只有内容片段到达时间,只能按片段报告。

让报告支持结论

给每个实验组列出实际输入和输出 Token 长度、并发、缓存冷热、成功率、P50/P95 与吞吐。若增加并发后吞吐趋平、尾延迟升高,再用排队时间和 GPU 轨迹查证原因。若只看到 TTFT 下降,先确认前缀是否命中以及比较组输出长度是否可比。

常见误区

示例 CSV 是教学数据,两个成功样本无法可靠描述尾延迟。失败请求也会占用服务资源,须保留其时长和状态。跨进程采集时先统一时钟口径。

本课术语

把推理拆开看

从注意力的可见范围、一次 Token 生成,到 KV Cache 显存与批处理调度,动手查看每一步。

跟踪一个请求

假设输入已切成 A、B、C 三个 Token,跟踪两个新 Token 的生成。

本步输入
ABC
KV 中的位置
空
累计输出
空

Tokenizer 将文本编码为 A、B、C 三个位置。此时尚未运行模型前向计算。

估算 KV Cache

假设 32 层、8 个 KV 头、头维度 128,缓存使用 16 位精度。按单请求、未分片计算。

4,096 Token
512 MiBKV 缓存估算

2 × 层数 × KV 头数 × 头维度 × 每元素字节数 × 缓存长度

每个 Token 约 128 KiB。计算仅包含 K/V 数据,实际分配还受分页、对齐和运行时策略影响。

因果注意力

点左侧的查询位置,观察这一行能访问哪些键位置。格子显示掩码规则。

Q / KABCD
可读遮挡遮挡遮挡
可读可读遮挡遮挡
可读可读可读遮挡
可读可读可读可读

查询 C 可以读 A、B、C。D 位于未来,进入 Softmax 前被掩码遮挡。

连续批处理

两个执行槽位。A 和 B 首轮到达,分别生成 1 和 3 个 Token。C 次轮到达,需要 2 个 Token。

生成步12345
槽位 1A空空CC
槽位 2BBB空空
第 1 / 5 步

A、B 各生成一个 Token。A 完成,静态批次仍需等 B 完成。

教学模型假设每个活跃请求每步生成一个 Token,忽略 Prefill、换入和显存分配成本。

推理术语表

从张量形状到服务指标,按主题查阅 100 项术语。支持中文、英文和常用缩写。

开发环境与张量26 项
Python
实验使用的编程语言,用来加载模型、组织输入、发送请求和分析结果。
VS Code
用于写代码和运行终端命令的编辑器。编辑器、Python 解释器和实际运行环境要对应。
虚拟环境 · venv
一套独立的 Python 解释器入口与包目录。每个项目可固定自己的依赖版本,减少冲突。
依赖 · pip · uv
依赖是代码运行所需的外部包。pip 和 uv 可管理安装,记录包版本有助于复现。
JSON
适合交换结构化数据的文本格式。接口消息常用对象、列表和字符串表示。
CSV
按行列组织的文本表格,字段通常用逗号分隔。可用于记录每条请求的状态和测量结果。
PyTorch
用于张量运算、神经网络和自动求导的框架。本手册用它实现模型前向与注意力演示。
张量 · Tensor
由多个轴组织的数值数组。标量、向量和矩阵可以看作零维、一维和二维张量。
张量形状 · shape
各轴的长度。形状 [2, 3, 4] 可以表示两个样本、每样本三个位置、每位置四个特征。
索引 · 切片
索引选中轴上的位置,切片选中一段。代码 ids[:, -1:] 对所有样本保留最后一个位置及其轴。
广播 · broadcasting
按规则把可兼容的轴对齐。长度为 1 的轴在计算时可当作沿该轴重复使用。
矩阵乘法 · matmul
用左矩阵的行和右矩阵的列做点积。形状 [m, k] 乘 [k, n] 得到 [m, n]。
改变形状 · reshape
在元素总数相同的前提下更换轴的划分。是否复制取决于原张量的内存布局。
交换维度 · transpose
交换两个轴的顺序,例如将 [B, T, H, D] 变为 [B, H, T, D]。
模型模块 · nn.Module
PyTorch 用来组织参数和子模块的基类。调用模块会通过框架进入它的前向计算。
线性层 · Linear
按 y = xWᵀ + b 变换输入的最后一维。权重形状通常是 [输出特征数, 输入特征数]。
偏置 · bias
线性层在矩阵乘积之后加入的可学习向量。输出有多少特征,通常就有多少偏置元素。
前向计算 · forward()
从输入算到输出。语言模型的一次前向可给出 logits,并按设置返回更新后的 KV Cache。
评估模式 · eval()
将模块切换到评估行为,例如关闭标准 Dropout。它不会单独禁用自动求导。
推理模式 · inference_mode()
关闭自动求导并减少一些张量跟踪开销的 PyTorch 上下文。它不会替代 eval()。
自动求导 · autograd
记录计算依赖并按链式法则求梯度的机制。训练用梯度更新参数,普通推理只执行前向。
随机失活 · Dropout
训练中随机将部分元素置零的正则化操作。标准 Dropout 在模块评估模式下关闭。
设备 · device · CPU · GPU
张量存放与运算的位置。CPU 是通用处理器,GPU 适合大量并行计算。参与同次运算的数据和参数通常须位于同一设备。
CUDA · GPU 同步
CUDA 是 NVIDIA 的 GPU 计算平台。许多调用异步提交,测实际完成时间需同步或用 CUDA Event 在设备侧计时。
数据类型 · dtype · FP32 · FP16 · BF16
元素的表示方式。FP32 占 4 字节,FP16 和 BF16 各占 2 字节。位宽会影响内存,数值范围和舍入特性也可能不同。
随机种子 · manual_seed
用于初始化伪随机数发生器。设置种子便于复跑示例,跨设备或软件版本仍可能存在数值差异。
文本表示与模型结构19 项
Token · 词元
模型输入与输出的基本单位。一个 Token 可能对应一个词、词片段或其他编码单元,字符数与 Token 数通常不同。
分词器 · Tokenizer
在文本和 Token ID 之间转换的组件。不同模型可能使用不同词表、切分规则和特殊 Token。
词表 · Token ID · Vocabulary
词表定义模型能识别的 Token 及其 ID。ID 是索引,大小不表示词义或概率。
嵌入 · Embedding
用 Token ID 查到向量表示。长度为 T 的输入序列在嵌入后有 T 个向量。
位置编码 · RoPE
让模型知道各 Token 位置的机制。旋转位置嵌入 RoPE 通过旋转 Q 和 K 的分量注入位置信息。
Transformer
用注意力在序列位置间交换信息,再结合逐位置前馈层等部件完成计算的模型结构。
Decoder-only · 仅解码器模型
用因果约束的层堆叠处理序列,并按上下文预测下一个 Token。GPT 类和本示例中的 Qwen 属于这一路线。
注意力 · Attention · Self-Attention
按查询与各键的匹配程度组合对应的值。自注意力的 Q、K、V 来自同一序列的表示。
Q / K / V · 查询、键、值
Q 表示当前位置在找什么,K 用来与 Q 算匹配分数,V 是按分数归一化后被加权汇总的信息。
多头注意力 · 头维度 · Attention Head
并行计算多个注意力头,每个头使用一组特征。头维度是每个头处理的特征数。
因果掩码 · causal mask · is_causal
禁止查询读取未来位置,通常在 Softmax 前使被遮挡位置的分数等效为负无穷。图中行 i 只允许列 j ≤ i。
缩放点积注意力 · SDPA
用 QKᵀ / √d 得到分数,加掩码并在键轴做 Softmax,最后乘 V。示例中的 scaled_dot_product_attention 封装了这个操作。
Softmax
把一组分数变成和为 1 的非负权重。对注意力沿键位置计算,对采样则可沿词表计算。
前馈网络 · FFN
在每个位置单独变换其表示的一组层,通常包含线性映射与非线性激活。
激活函数 · activation
给模型变换加入非线性。例如 FFN 若只串联线性映射,整体仍可合并为线性映射。
残差连接 · residual connection
把子层的输入加到输出上,让原来的表示有直接通过子层的路径。
层归一化 · LayerNorm · RMSNorm
按一定规则调节隐藏表示的尺度。LayerNorm 以均值和方差归一化,RMSNorm 用均方根缩放而不减均值。
Logits
模型输出的原始分数。语言模型在每个位置给出一组词表分数,生成时通常使用最后一个有效位置的分数。
序列长度 · 隐藏维度 · 批大小
序列长度数 Token 位置,隐藏维度数每个位置的特征,批大小数同时处理的独立样本。
自回归生成与缓存18 项
推理 · Inference
使用已训练的模型处理输入、得到输出的过程。实验可单测前向,也可测完整在线服务链路。
Dense · 稠密模型 · MoE
Dense 模型通常使用该层的同一组主要参数处理每个 Token。MoE 由路由器选择部分专家子网络处理每个 Token。
Qwen2.5-0.5B-Instruct
实验示例使用的小型指令模型。0.5B 表示约五亿级参数,Instruct 表示模型经过面向指令交互的训练。
Hugging Face · Transformers · from_pretrained
Hugging Face 提供模型托管。Transformers 是模型与分词器工具库,from_pretrained 按模型标识加载预训练配置、权重或分词器文件。
提示词 · Prompt · 上下文
送进模型的当前输入是提示词。推理中的上下文还包括已经加入生成过程的历史输出。
自回归生成 · autoregressive
每一步根据当前上下文预测一个新 Token,再把它加入上下文供下一步使用。
Prefill · 预填充
对输入提示词做前向,建立各层的 K/V;最后一个有效输入位置的 logits 用于产生首个新 Token。
Decode · 逐步解码
这里指逐步生成。已有缓存时,新输入通常是上一轮选出的 Token,用它追加 K/V 并预测下一 Token。Tokenizer 的 decode 方法则指把 ID 转回文本。
KV Cache · 键值缓存 · past_key_values
保留注意力层历史位置的 K 和 V,避免每步重算历史表示。历史 K/V 仍会被当前注意力读取,因此容量与带宽依旧重要。
Attention Mask · 注意力掩码
模型接口中通常用来标记哪些位置属于有效上下文,包含过去与当前输入。它和自动实施的因果约束可以同时存在。
缓存位置 · cache position
当前输入 Token 对应的序列位置。手写生成循环要保持位置、有效注意力范围与缓存长度一致。
生成接口 · generate()
Transformers 的高层生成方法,反复调用前向并处理缓存、候选选择和停止条件。
贪心解码 · argmax
每步选择当前分数最大的 Token。它只比较当前这一步的候选。
采样 · sampling
根据处理后的概率分布抽取下一个 Token。同一输入在采样模式下可能得到不同结果。
温度 · Temperature
采样前用温度缩放分数。较高温度通常使分布更平坦;贪心生成直接选最高分。
Top-k
采样前只保留分数最高的 k 个候选,再对保留项重新归一化。
Top-p · 核采样
按概率由高到低,保留累计概率达到阈值 p 的最小候选集合,再采样。
EOS · 特殊 Token · max_new_tokens
EOS 标记可结束生成的 Token。特殊 Token 还可承担边界或对话角色等用途。max_new_tokens 限制新增 Token 数。
显存与调度16 项
显存 · VRAM · 峰值显存
GPU 的设备内存。权重、缓存、临时激活、工作区等都可能占用它。峰值显存是在指定观测区间内测到的最大占用。
字节 · KiB · MiB · GiB
一字节有 8 位。1 KiB = 1,024 字节,1 MiB = 1,024 KiB,1 GiB = 1,024 MiB。GB 采用十进制单位。
权重 · 参数 · 激活
权重是训练得到的可学习参数。激活是一次前向产生的中间结果,其大小随形状和实现变化。
显存带宽 · memory bandwidth
单位时间能在计算单元与显存之间传输多少数据。缓存变大后,读取 K/V 可能耗费更多时间。
GQA · 分组查询注意力 · KV 头
多个查询头共享一个 K/V 头。估算 KV Cache 应使用实际 KV 头数。
静态批处理 · static batching
一批请求按批次运行。本图采用一批全部完成后再换入下一批的简化模型。
连续批处理 · continuous batching
按迭代更新活动请求集合。请求完成释放资源后,调度器可在后续迭代换入等待请求。
执行槽位 · 活跃请求 · 排队
图中的槽位是允许同时执行的抽象位置。活跃请求参与当前迭代,排队请求已到达但等待资源与调度。
前缀缓存 · prefix caching · 命中率
当请求开头的 Token 序列与缓存匹配,框架可以复用对应 K/V。命中率的统计分母要注明,可以按请求或 Token 计。
RadixAttention · 基数树
SGLang 利用树形前缀索引查找和管理可共享的 KV。公共开头越长,符合条件时可少做更多重复 Prefill。
量化 · quantization
以较低精度表示权重、激活或缓存。能节省存储或传输,速度与质量收益取决于硬件、实现和误差。
PagedAttention · 分页 · 对齐
按块管理 KV 存储,允许逻辑相邻的 Token 使用非连续物理块。块尾填充和对齐都会影响实际占用。
投机解码 · speculative decoding
先提出一段候选 Token,由目标模型批量验证,期望少做目标模型的串行生成步。收益取决于接受率和候选生成代价。
Prefill / Decode 分离 · PD 分离
把输入预填充和逐步解码分配给不同执行资源,以便分别调配;跨资源传送 KV 有额外开销。
分片 · sharding · 滑动窗口
分片把状态分摊到多个设备。滑动窗口注意力限制可见的历史范围,部分实现因此只需保留窗口内的 KV。
调度器 · Scheduler · 执行器
调度器选择哪些请求进入哪次迭代,并管理排队与资源预算;执行器运行模型计算。
在线服务与测量21 项
SGLang · 推理框架
用于运行模型推理服务的框架,包含接口、调度、缓存管理和执行机制。
对话模板 · chat template · 多轮消息
用角色标签、边界符等把 system、user、assistant 消息拼成模型约定的输入。多轮调用通常提交需要保留的历史。
离线批量推理 · 在线服务
离线通常对预先准备的一组输入运行。在线服务随请求到达而处理,需要考虑连接、排队、超时和并发。
HTTP · API · 端点 · localhost
HTTP 是请求与响应协议,API 定义如何访问服务。端点由请求方法和地址确定。localhost 与 127.0.0.1 指本机。
端口 · Port · base_url
端口区分同一主机上的网络服务。base_url 给出客户端使用的协议、主机、端口和公共路径前缀。
OpenAI SDK · 鉴权 · API Key
SDK 是封装接口调用的程序库。API Key 用来识别与授权调用方。本地未启用鉴权的教学服务可使用占位值。
流式返回 · SSE · chunk
输出逐段发送给客户端。SSE 是可用于这种输出的事件传输格式,一个内容片段可能含多个 Token,也可能没有内容。
单调时钟 · perf_counter
用于计算同一进程内的时间差,不受修改系统时间的直接影响。不同进程或主机的绝对读数需采用约定的对齐办法。
请求标识 · request_id · start_ms · end_ms
request_id 标识一条请求。start_ms 和 end_ms 是相对共同起点的毫秒时刻,两者相减得到该请求持续时间。
重试 · timeout
重试是在失败后再尝试,timeout 是超时阈值。基准记录应说明是否自动重试以及超时怎样计入结果。
性能基准 · baseline · 基线
可复跑的参考配置和测量结果。比较时尽量固定模型、版本、精度、负载和记录方式。
压测 · 负载 · 并发
压测用受控请求测系统行为。负载描述请求的长度、到达方式等,并发描述同时在途的请求数量。
预热 · 冷启动
预热是在正式计时前跑若干请求,以减少初始化等影响。若目标是冷启动,需要单独记录首轮启动和首次请求成本。
TTFT · 首 Token 延迟
从规定的请求起点到第一个输出 Token 的时间。客户端只能看到内容块时,应明确报告首内容片段延迟作为近似。
ITL · Token 间延迟 · TPOT
ITL 是相邻输出 Token 的时间间隔。TPOT 常表示首 Token 之后的每 Token 平均耗时,具体计算需注明工具口径。
E2E · 端到端延迟
从指定请求发出时间到规定完成时间的总耗时。客户端口径可能包含序列化、网络、排队与生成。
吞吐量 · Token/s · 观测窗口
单位时间处理的请求数或输出 Token 数。要明确分子是请求还是 Token、成功还是全部,以及分母采用哪个测量窗口。
P50 · P95 · 分位数 · 尾延迟
分位数描述样本分布。P50 对应中位附近,P95 描述较慢的一端。样本少时尾部估计不稳定,需标注样本数和计算方法。
逐请求数据 · 样本数 · 成功率
每条请求保留状态、起止时刻和能测到的指标。成功率是成功请求占发起请求的比例;失败和超时也计入总数。
控制变量 · 对照实验
对比组只改变要研究的因素,其他设置保持一致,以便将观测差异归因给可检验的变化。
性能剖析 · profiling · 瓶颈
用时间轨迹与计数指标观察系统时间花在何处。瓶颈是限制当前负载下性能的环节,会随请求长度和并发变化。

资料与交付

最后留下一套可复跑的压测脚本、一张结果表,以及一份能解释瓶颈的简短分析。

从哪里开始运行

浏览器:直接操作四项原理演示,勾选验收并查看术语。

本机 Python:先用 CPU 做张量、注意力与生成逻辑练习。模型相关实验需要下载权重和足够的内存。

推理服务:准备符合 SGLang 安装要求的环境,或连接已有服务。记录设备、模型、精度和版本后再跑基准。

回到学习路线