或许每个人都会有些疑惑
大模型的token是怎么样输出出来的?
阶段一:模型加载(Disk → CPU RAM → GPU VRAM)
当你启动 SGLang 并指定加载 Qwen3-14B 时,系统经历以下过程:
1. 磁盘读取(NVMe SSD → 系统内存)
模型文件通常以 safetensors 格式存储在磁盘上,Qwen3-14B 的 FP16 权重大约 28GB(14B 参数 × 2 字节)。操作系统通过 DMA(Direct Memory Access)将文件从 NVMe SSD 读入系统内存,绕过 CPU 直接走 DMA 控制器,速度取决于 SSD 的顺序读取带宽(通常 3-7 GB/s)。CPU 此时的工作是:解析模型配置文件(config.json)、确定网络结构(层数、注意力头数、FFN 维度等)、加载 tokenizer 词表。
2. 通过 PCIe 传输到 GPU 显存
权重张量从系统内存通过 PCIe 总线拷贝到 GPU 的 HBM(高带宽显存)。这一步使用 cudaMemcpyHostToDevice。PCIe 4.0 x16 的单向带宽约 32 GB/s,PCIe 5.0 约 64 GB/s,所以 28GB 的权重需要大约 0.5-1 秒传输完成。如果是多 GPU(如张量并行),SGLang 会根据并行策略将不同层或同一层的不同 shard 分发到不同 GPU 上,此时还涉及 NVLink 通信(带宽 600-900 GB/s,远快于 PCIe)。
3. GPU 显存布局
到达 GPU 后,显存中需要存放三大块内容:模型权重(固定占用,FP16 约 28GB)、KV Cache(动态分配,随 batch size 和序列长度增长)、以及激活值的临时空间。SGLang 在启动时会预分配一块连续的 KV Cache pool,使用 PagedAttention 技术以 “page” 为单位管理,避免碎片化。
阶段二:请求到达与预处理(CPU 侧 SGLang 调度)当用户发来一个请求时:
1. Tokenization(CPU 上执行) SGLang 收到 HTTP 请求后,先在 CPU 侧用 BPE tokenizer 将文本转成 token ID 序列。例如”你好,请解释量子计算”可能被切成 8-15 个 token。这一步很快(微秒级),因为是纯 CPU 字符串操作。
2. Continuous batching 调度器 SGLang 的核心创新之一是连续批处理(continuous batching)。与传统的”等凑满一个 batch 再跑”不同,调度器每一步都会:把新到的请求插入 prefill 队列、把已经在生成中的请求继续放在 decode 队列、当某个请求生成完毕(遇到 EOS 或达到 max_tokens),立刻释放它的 slot 并插入新请求。这使得 GPU 利用率极高,不会因为某个请求先完成而空等。
3. RadixAttention 前缀缓存 SGLang 独有的 RadixAttention 机制会检查新请求的 prompt 是否与之前请求有共同前缀(比如相同的 system prompt)。如果有,直接复用已经算好的 KV Cache,跳过重复计算,大幅降低 prefill 时间。
阶段三:GPU 推理——Prefill 与 Decode 的核心计算
这是整个系统最关键的部分。每生成一个 token,GPU 内部都要完成一次完整的前向传播。### Prefill 阶段(处理全部输入 token)
当新请求进入时,GPU 需要一次性处理整个 prompt 的所有 token。假设 prompt 有 1000 个 token:
Embedding 查表:从 HBM 中的 embedding 矩阵查出 1000 个向量,每个向量 5120 维(Qwen3-14B 的 hidden size),共读取约 10MB。这是纯显存读取操作。
每一层 Transformer 的计算(共 40 层):
- RMSNorm:逐元素操作,计算量小但仍需读写整个张量,属于 memory-bound 操作。
- 注意力计算(GQA):Qwen3 使用 Grouped Query Attention,比如 40 个 Q head 但只有 8 个 KV head,大幅减少 KV Cache 的显存消耗。核心运算是
Q·K^T(矩阵乘法,由 Tensor Core 加速)、除以 √d、softmax、再乘 V。在 prefill 阶段,这是一个大矩阵乘法,是 compute-bound(计算受限),FlashAttention 内核会将整个计算融合在一个 kernel 中完成,避免中间结果写回 HBM。 - KV Cache 写入:将本层计算出的 K 和 V 向量写入 HBM 的 KV Cache 区域。PagedAttention 以 block 为单位分配,block 不需要物理连续。
- FFN(SwiGLU):两次大型 GEMM(up-projection 和 down-projection),中间经过 SiLU 门控。对 Qwen3-14B,FFN 中间维度约 13824,所以一次 FFN 涉及两个
[seq_len, 5120] × [5120, 13824]的矩阵乘法。这也是 compute-bound,几乎完全由 Tensor Core 执行。 - 残差连接:简单的逐元素加法。
Prefill 的总特点:大 batch 的矩阵运算,GPU 的 Tensor Core 利用率很高,FLOPS 是瓶颈,而非显存带宽。
Decode 阶段(逐 token 生成)
Prefill 完成后,每一步只需要处理一个新 token(或 continuous batching 中的多个请求各一个 token):
- 注意力变成了
[1, d] × [seq_len, d]^T,本质上是矩阵-向量乘法(GEMV),计算量很小,但仍需读取整个 KV Cache。这使得 decode 阶段变成 memory-bound:每个 token 生成的计算量只需几 GFLOPS,但要读取数十 GB 的权重和 KV Cache。 - HBM 带宽成为瓶颈。A100 的 HBM 带宽约 2 TB/s,要读取 28GB 权重生成一个 token,理论上每秒最多 ~70 tokens(单请求)。这就是为什么 decode 速度远慢于 prefill。
- Tensor Core 大部分时间是空闲的——数据搬运比计算更花时间。
阶段四:Token 采样与输出
1. Logits → Sampling(GPU 上完成) LM head 输出一个 152064 维的 logits 向量(Qwen3 的词表大小),经过 temperature 缩放(除以 T 值)、top-p 截断(累积概率达到阈值后截断剩余 token),然后通过 multinomial sampling 从概率分布中随机采样一个 token ID。这些操作全在 GPU 上完成,计算量很小。
2. Token ID → 文本(CPU 上完成)
采样出的 token ID 通过 PCIe 返回 CPU 侧(只传几个字节,延迟可忽略)。SGLang 的 detokenizer 在 CPU 上将 ID 转成 UTF-8 文本。这里有个细节:某些 Unicode 字符(如中文)可能跨越多个 token,detokenizer 需要增量解码,只有当凑齐一个完整字符时才输出。
3. SSE 流式返回
文本通过 HTTP Server-Sent Events(SSE)协议实时流式推送给客户端。用户每收到一个 token 对应的文字片段,前端就追加渲染——这就是你看到文字”一个字一个字蹦出来”的原因。
4. 自回归循环
同时,新生成的 token ID 被送回 GPU,作为下一步 decode 的输入,重复整个前向传播过程。这就是自回归(autoregressive)生成:每个 token 都依赖前面所有 token。
各组件瓶颈总结
| 阶段 | 关键硬件 | 瓶颈类型 | 典型耗时 |
|---|---|---|---|
| 磁盘 → 内存 | NVMe SSD | IO bandwidth | 4-8 秒(加载 28GB) |
| 内存 → 显存 | PCIe bus | 传输带宽 | 0.5-1 秒 |
| Prefill | GPU Tensor Core | Compute-bound | 50-500ms(取决于 prompt 长度) |
| Decode 每 token | GPU HBM | Memory-bound | 10-30ms/token |
| 采样 + 输出 | CPU + 网络 | 可忽略 | <1ms |
核心要点:模型加载是一次性成本(启动时几秒钟),之后每次请求的延迟主要由 prefill(一次性处理 prompt)和 decode(逐 token 生成)决定。Decode 阶段受限于 HBM 带宽而非算力——这就是为什么 H100 比 A100 快,主要因为 HBM3 带宽从 2 TB/s 提升到 3.35 TB/s。SGLang 通过 continuous batching、RadixAttention 前缀缓存和 PagedAttention 等优化,尽可能减少浪费,让 GPU 在瓶颈带宽上做更多有用功。