从Prompt到Response:大模型推理端到端核心链路深度拆解

📖 主线脉络:一次请求的生命周期

一次 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),内核阻塞发送端,反压信号穿透至 Python yield,GPU 无需空等网络。

二、 预处理层:从字符串到 GPU 就绪张量

请求抵达推理引擎后,需将人类语言转换为 GPU 可执行的张量格式。此阶段发生在 CPU,但必须严格对齐 GPU 内存布局。

2.1 分词与张量化

现代 LLM 使用 BPE (Byte-Pair Encoding) 变体(如 tiktoken)。核心流程:

  1. 文本 UTF-8 编码 → 字节序列
  2. 查表合并高频字节对 → Subword Token IDs
  3. 构造控制张量:
    • 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 实现异步并发:

graph LR CPU[Python/CPU] -->|1. cudaMemcpyAsync| H2D[H2D Stream] CPU -->|2. cudaLaunchKernel| K1[Kernel 1: GEMM] CPU -->|3. cudaLaunchKernel| K2[Kernel 2: FA2] K1 -->|同步点| E1[cudaEvent] K2 --> E1 E1 -->|4. cudaEventSynchronize| CPU
  • 计算流与拷贝流分离 :权重常驻 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 条实践准则

  1. 监控指标定瓶颈GPU Util > 80% 说明计算饱和,优化算子融合/增大 Batch;Mem Bandwidth Util > 80% 说明内存墙,优先量化/压缩 KV Cache/升级 HBM。
  2. 生产必开编译与动态批 :关闭 Python 级循环,启用 torch.compile(fullgraph=True);使用 vLLM/TGI 等支持 Continuous Batching 的引擎,静态 Batch 在长尾场景下浪费 > 40% 算力。
  3. 流式协议控粒度: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)*

相关推荐
Felven2 小时前
D. Matryoshkas
算法
亦暖筑序2 小时前
Message 四分天下:Spring AI 如何统一消息格式
java·人工智能
tinygone2 小时前
OpenClaw通过ACPX调用Claude Code实现飞书操作CC
人工智能·飞书·ai编程
2501_933329552 小时前
AI驱动媒介宣发:Infoseek舆情系统的技术架构与公关实战
数据仓库·人工智能·重构·数据库开发
17(无规则自律)2 小时前
DFS连通域统计:岛屿数量问题及其变形
c++·算法·深度优先
云栖梦泽2 小时前
【AI】AI安全工具:常用AI安全检测工具的使用教程
大数据·人工智能·安全
海兰2 小时前
【AI网关】阿里开源的Higress(OpenAPI-to-MCP工具)
人工智能·架构·开源·银行系统
凤山老林2 小时前
Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
java·人工智能·知识库·rag·spring ai
独断万古他化2 小时前
AI 赋能自动化测试实战:从用例生成到 CI/CD 全流程落地
人工智能·ci/cd·测试