📖 主线脉络:一次请求的生命周期
一次 Prompt 到 Response 的过程,并非单一函数调用,而是一条 CPU 预处理 → GPU 并行计算 → 概率采样 → 网络流式回传 的流水线。其核心矛盾始终围绕两个指标:
TTFT (Time To First Token):首字延迟,由Prefill阶段决定TPOT (Time Per Output Token):逐字延迟,由Decode阶段决定
整条链路的物理约束可归结为:
scss
Prefill 阶段 → 计算密集 (Compute-Bound) → 瓶颈在 FLOPS / Tensor Core 利用率
Decode 阶段 → 访存密集 (Memory-Bound) → 瓶颈在 HBM 带宽 / Kernel Launch 开销
下文将按数据流向,逐层拆解 HTTP 协议栈、CPU 张量化、GPU 算子执行、动态调度与流式回传的底层实现。
一、 网络交互层:HTTP 协议如何承载流式推理
大模型推理是典型的 长耗时、渐进式输出 场景。传统 HTTP 请求-响应模型(等待完整响应后返回)会导致客户端连接超时、体验断裂。工业界标准解法是 SSE (Server-Sent Events) + HTTP Chunked。
1.1 协议选型与头部设计
http
POST /v1/chat/completions HTTP/1.1
Host: llm-inference.internal
Authorization: Bearer <token>
Content-Type: application/json
Accept: text/event-stream ← 声明接受流式事件
X-Request-Timeout: 300 ← 业务层超时控制
Connection: keep-alive
HTTP/1.1 Chunked:底层传输编码,服务端通过Transfer-Encoding: chunked将响应拆分为多个 TCP 片段发送,无需提前知道Content-Length。SSE:在 Chunked 之上封装的文本协议。格式固定为event: message\ndata: {json}\n\n,浏览器原生支持EventSource,具备自动重连与心跳保活能力。
1.2 后端异步生成器与流式驱动
Python 推理服务(FastAPI/Uvicorn/TGI)通过异步生成器将 GPU 计算与网络发送解耦:
python
async def stream_response(prompt: str, params: Dict):
async for token_text, is_final in engine.generate(prompt, params):
payload = f"data: {json.dumps({'token': token_text, 'done': is_final})}\n\n"
yield payload.encode('utf-8') # Uvicorn 自动转为 HTTP Chunk 刷新
if is_final: break
Uvicorn/Starlette 底层维护连接池,每当 yield 返回,即触发 response.write() 并 flush TCP 发送缓冲区,实现 计算-网络流水线重叠。
1.3 前端背压控制 (Backpressure)
当客户端网络缓慢或 DOM 渲染阻塞时,若后端无限制推送,将导致内存堆积甚至 OOM。现代前端通过 ReadableStream 实现反压:
javascript
const response = await fetch('/v1/chat/completions', { method: 'POST', body: JSON.stringify(req) });
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read(); // 阻塞读取,自动反压
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件边界
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留未完整行
for (const line of lines) {
if (line.startsWith('data: ')) {
const chunk = JSON.parse(line.slice(6));
renderToken(chunk.token); // 渲染到 UI
}
}
}
reader.read()是 Promise 化操作。若下游处理慢,TCP 接收缓冲区满后,操作系统自动触发 TCP 窗口收缩(Zero Window),内核阻塞发送端,反压信号穿透至 Pythonyield,GPU 无需空等网络。
二、 预处理层:从字符串到 GPU 就绪张量
请求抵达推理引擎后,需将人类语言转换为 GPU 可执行的张量格式。此阶段发生在 CPU,但必须严格对齐 GPU 内存布局。
2.1 分词与张量化
现代 LLM 使用 BPE (Byte-Pair Encoding) 变体(如 tiktoken)。核心流程:
- 文本 UTF-8 编码 → 字节序列
- 查表合并高频字节对 → Subword Token IDs
- 构造控制张量:
input_ids:[1024, 3012, 512, ..., 2](长度 N)attention_mask:[1, 1, 1, ..., 1](因果掩码在 Attention 内核中硬编码,无需显式传递)position_ids:[0, 1, 2, ..., N-1](RoPE 依赖绝对索引)
2.2 锁页内存与异步 H2D 拷贝
Python 默认内存分配在普通页,GPU DMA 无法直接访问,需驱动层二次拷贝。推理引擎在初始化时预分配 Pinned Memory (Page-Locked):
cpp
// C++ / CUDA 层伪代码
void prepare_input(const std::vector<int32_t>& input_ids) {
cudaMallocHost(&pinned_input, batch_size * max_len * sizeof(int32_t));
cudaMemcpyAsync(pinned_input, input_ids.data(), ..., cudaMemcpyHostToDevice, stream_0);
// 触发异步拷贝,CPU 立即返回,不阻塞后续调度
}
异步拷贝与 cudaStream_t 绑定,后续 Kernel 在相同 Stream 上排队,实现 H2D 拷贝与 GPU 计算重叠。
三、 GPU 推理内核:PyTorch 与 CUDA 的底层协同(重点)
这是整个流水线的算力心脏。理解其运行机制,需从 PyTorch 执行图、CUDA 调度原语到显存布局逐层下钻。
3.1 PyTorch 执行模型演进
- Eager Mode :逐行解释执行,每次算子调用触发
cudaLaunchKernel,Python GIL 与 Kernel Launch 开销占比可达 15%~30%。 torch.compile(PyTorch 2.x) :TorchDynamo捕获 Python 字节码 → 构建 FX Graph →TorchInductor生成 Triton/CUDA 融合 Kernel。将MatMul + Bias + Residual + RMSNorm融合为单一 Kernel,减少 HBM 中间张量读写。
3.2 CUDA 执行环境管理
GPU 并非单线程执行,而是通过 Stream 与 Event 实现异步并发:
- 计算流与拷贝流分离 :权重常驻 HBM,激活临时分配。使用独立
cudaStream_t执行D2H拉取 Logits,避免阻塞下一轮Decode。 - 异步屏障 :CPU 提交 Kernel 后不等待,通过
cudaEventRecord标记完成点,仅在采样前同步,最大化 GPU 占用率。
3.3 核心算子到 CUDA Kernel 的映射
| 算子 | 数学形式 | CUDA 实现策略 | 性能关键点 |
|---|---|---|---|
| GEMM | <math xmlns="http://www.w3.org/1998/Math/MathML"> Y = X W T + B Y = XW^T + B </math>Y=XWT+B | cuBLASLt / CUTLASS |
按 128x128 Tile 分块,Shared Memory 缓存权重块,Warp 级矩阵乘 (WMMA) |
| FlashAttention-2/3 | <math xmlns="http://www.w3.org/1998/Math/MathML"> softmax ( Q K T / d ) V \text{softmax}(QK^T/\sqrt{d})V </math>softmax(QKT/d )V | SRAM 感知分块计算 | 避免 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2) <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 矩阵落盘 HBM;分块加载 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q , K , V Q,K,V </math>Q,K,V 至 Shared Mem,在线 Softmax |
| RMSNorm | <math xmlns="http://www.w3.org/1998/Math/MathML"> x / mean ( x 2 ) + ϵ ⋅ γ x / \sqrt{\text{mean}(x^2)+\epsilon} \cdot \gamma </math>x/mean(x2)+ϵ ⋅γ | 逐元素 Kernel 融合 | 与残差连接、激活函数合并为单 Kernel,减少全局同步 |
3.4 PagedAttention:KV Cache 的虚拟化革命
传统推理为每个请求预分配连续显存,导致碎片化与 OOM。vLLM 引入 PagedAttention,将 KV Cache 映射为固定大小的物理块(Block),逻辑序列通过块表(Block Table)索引:
cpp
// PagedAttention 内核启动伪代码 (简化版)
struct BlockTable {
int32_t* logical_to_physical; // [num_blocks]
int32_t* sequence_lengths;
};
__global__ void paged_attention_kernel(
const float* Q, const int32_t* block_table,
float* O, const int seq_len, const int num_heads) {
int block_idx = blockIdx.x;
int head_idx = blockIdx.y;
int token_in_block = threadIdx.x;
// 1. 根据逻辑块索引查找物理块地址
int phys_block_id = block_table[block_idx];
float* K_block = K_cache[phys_block_id];
float* V_block = V_cache[phys_block_id];
// 2. 加载至 Shared Memory (避免重复 HBM 访问)
__shared__ float K_shared[BLOCK_SIZE];
__shared__ float V_shared[BLOCK_SIZE];
K_shared[token_in_block] = K_block[token_in_block];
__syncthreads();
// 3. 计算局部注意力并累积 (Online Softmax)
// ... (FlashAttention 分块逻辑) ...
}
- 优势:逻辑连续 → 物理离散;支持跨请求共享相同 Prefix 的 KV 块;显存利用率提升 2~4 倍。
- CUDA 调度 :调度器维护
Waiting / Running / Swapped队列,按 SM 空闲度动态组装 Batch,每个请求携带独立Block Table指针传入 Kernel。
四、 推理两阶段:Prefill 与 Decode 的算力博弈
模型前向传播并非均质过程。根据输入/输出长度,硬件行为呈现截然不同的特征。
4.1 Prefill 阶段:计算墙 (Compute-Bound)
- 输入 :完整 Prompt(长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N)
- 计算 :并行计算 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 个 Token 的 Attention 与 FFN,生成首个输出 Token 的 Logits
- 瓶颈 :矩阵乘 FLOPS。 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 越大,TTFT 越高,但 GPU 利用率可达 80%+
- 优化:Chunked Prefill
将超长 Prompt 切分为固定大小 Chunk(如 512),每计算完一个 Chunk 即释放 SM 资源给 Decode 请求,避免长请求饿死短请求:
scss
[GPU Timeline]
| Prefill Chunk 1 (Req A) | Decode (Req B) | Prefill Chunk 2 (Req A) | Decode (Req C) |
调度器按优先级抢占 SM,确保 TTFT 分布平滑。
4.2 Decode 阶段:内存墙 (Memory-Bound)
- 输入 :历史序列(长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> N + T N+T </math>N+T) + 当前新 Token(长度 1)
- 计算:仅计算 1 个 Token 的 Attention,读取全部历史 KV Cache
- 瓶颈 :HBM 带宽。每次生成需读取 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( ( N + T ) × d m o d e l ) O((N+T) \times d_{model}) </math>O((N+T)×dmodel) 的 KV 数据,FLOPS 利用率常 < 30%
- 优化:Continuous Batching (动态批处理)
打破传统静态 Batch 的 Padding 浪费。请求可随时进出:- 新请求进入
Waiting队列 - 运行中请求生成 EOS 或达最大长度 → 标记完成,立即从 Batch 中移除
- 调度器从
Waiting队列填补空缺,保持 Batch 填满 SM - 底层通过 Ragged Tensors 处理变长序列,避免无效计算
- 新请求进入
4.3 硬件调度时序对比
ini
Prefill (Req A, len=2048) Decode (Req A+B+C, dynamic batch)
CPU: 构造张量 -> H2D -> Launch Kernel -> 采样 -> Detokenize -> Yield
GPU SMs: [████████████████████████] GEMM/FA [█░░░] [█░░░] [█░░░] (Memory Bound)
HBM: 加载权重 + 写入KV Cache [████████] 读取历史KV -> 计算 -> 写入新KV
Decode 阶段 GPU 大部分时间在等待数据搬运,而非计算。因此 量化 (FP8/INT4) 与 KV Cache 压缩 对吞吐提升远大于增加 SM 数量。
五、 概率采样与响应回传:从 Logits 到文本流
模型输出词表大小的 Logits 向量后,需经采样策略决定下一个 Token,再逆映射为文本流式返回。
5.1 Logits 处理与温度缩放
python
# PyTorch 端等价逻辑 (实际在 CUDA 执行)
logits = logits[:, -1, :] / temperature
logits = logits - logits.max(dim=-1, keepdim=True).values # 防溢出
probs = torch.softmax(logits, dim=-1)
数值稳定性至关重要:减去最大值避免 exp() 溢出,Softmax 在 CUDA 中通常与 Top-p 过滤融合执行。
5.2 Top-k / Top-p 采样 (CUDA 并行实现)
- Top-k :保留概率最高的 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 个,其余置 0
- Top-p (Nucleus) :累积概率达 <math xmlns="http://www.w3.org/1998/Math/MathML"> p p </math>p 的最小集合
CUDA 实现依赖 并行前缀和 (Parallel Prefix Sum / Scan):
cpp
// Thrust/CUB 伪代码
thrust::inclusive_scan(d_probs, d_probs + vocab_size, d_cumsum);
// 查找首个 >= top_p 的索引
auto it = thrust::lower_bound(d_cumsum, d_cumsum + vocab_size, top_p);
int cutoff_idx = it - d_cumsum;
// 将 cutoff_idx 之后概率置 0,重新归一化
前缀和时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( log N ) O(\log N) </math>O(logN),在 1024 个 Thread 的 Warp 级 Scan 中仅需几十纳秒。
5.3 Detokenization 与边界处理
BPE 分词器可能将单词切断:"Hello" → ["Hel", "lo"]。采样出的 Token ID 需通过词表逆映射,并维护字节缓冲区:
python
class StreamDetokenizer:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
self.buffer = b""
def add_token(self, token_id: int) -> str:
self.buffer += self.tokenizer.convert_ids_to_bytes([token_id])
# 尝试解码完整 Unicode 字符
text, remaining = decode_utf8_safely(self.buffer)
self.buffer = remaining
return text
decode_utf8_safely 检查字节序列是否为完整 UTF-8,若截断则缓存等待下一 Token,避免前端出现乱码或替换符 ``。
5.4 流式推送闭环
采样结果 → Detokenizer → 封装 SSE → HTTP Chunk Flush → 前端 ReadableStream 消费。
若前端背压生效(如用户暂停滚动),TCP 窗口收缩 → Uvicorn 写阻塞 → yield 挂起 → GPU 调度器自动降低该请求优先级或暂停 Decode,算力无缝转移至其他活跃请求。
六、 结语:推理工程的"第一性原理"
从 Prompt 到 Response 的全链路,本质是 数据移动成本 vs 计算成本 的持续博弈。脱离硬件谈算法、脱离调度谈吞吐,皆为纸上谈兵。
🔑 给一线工程师的 3 条实践准则
- 监控指标定瓶颈 :
GPU Util > 80%说明计算饱和,优化算子融合/增大 Batch;Mem Bandwidth Util > 80%说明内存墙,优先量化/压缩 KV Cache/升级 HBM。 - 生产必开编译与动态批 :关闭 Python 级循环,启用
torch.compile(fullgraph=True);使用 vLLM/TGI 等支持 Continuous Batching 的引擎,静态 Batch 在长尾场景下浪费 > 40% 算力。 - 流式协议控粒度:SSE Chunk 建议 1~3 Token/次。过小导致 HTTP 头部开销占比高;过大削弱首字体验与前端渲染流畅度。
🔮 架构演进趋势
- 混合架构 :Attention (长程依赖) + SSM/Mamba (线性扫描) 混合,打破 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2) 瓶颈
- 编译栈统一:TorchDynamo → TVM → 硬件专属后端,实现跨芯片 Kernel 自动调优
- 端云协同:云端 Prefill 生成 KV Cache → 边缘端 Decode 逐字生成,降低延迟与带宽
推理系统已从"能跑通"迈入"榨干每一赫兹与每一 GB/s"的深水区。理解这条链路,不仅是调优模型的前提,更是设计下一代 AI 原生架构的基石。
📚 延伸参考
-
1\] Dao et al., *FlashAttention-2: Faster Attention with Better Parallelism*, 2023
-
3\] NVIDIA, *CUDA C++ Best Practices Guide \& cuBLASLt Documentation*
-
5\] WHATWG, *Streams Standard (ReadableStream Backpressure)*