0%

GPU 性能瓶颈定位方法

一句话定位:优化的第一步不是改代码,而是判定瓶颈属于哪一类。判错类别,所有后续努力都白费——这也是面试最爱追问的地方。

1. 瓶颈分类与判定信号

现象 瓶颈类别 首要动作
GPU 利用率低但 CPU 跑满 数据供给 / 前处理瓶颈 调 DataLoader(2.9)、优化解析与数据格式(1.7)
Kernel 之间有明显空隙 调度 / 同步问题 减少同步点与 H2D 拷贝、算子融合、CUDA Graph、增大 batch
多卡吞吐不随卡数线性增长 通信瓶颈 看 NCCL 拓扑与并行策略(3.10)
GPU 利用率高但吞吐不达标 计算 / 访存瓶颈 看是否 IO-bound(3.7)、换更优 kernel、量化(4)

2. 必答题:「你怎么知道是 GPU 而不是 CPU 瓶颈?」

标准答法是给出可观测证据链,而不是凭感觉:

  1. 看 GPU 利用率曲线形态nvidia-smi dmon 或 Nsight 采样。
    • 利用率持续高位 → GPU 是瓶颈;
    • 利用率呈锯齿/周期性掉底 → GPU 在等数据 → CPU 侧(加载、前处理)是瓶颈。
  2. 对照 CPU 侧负载:若 CPU 各核接近满载而 GPU 空闲,结论明确。
  3. 做单变量实验:用合成数据(预先常驻显存的假 batch)替换真实 DataLoader 再跑——
    • 吞吐显著提升 → 瓶颈确实在数据管线;
    • 吞吐几乎不变 → 瓶颈在 GPU 计算本身。
  4. 看时间线归因:Nsight Systems 上统计 GPU Kernel 累计时间占墙钟时间的比例(GPU Kernel 占比)。占比低说明大量时间耗在非 Kernel(等待、拷贝、Python 开销)。

这套”先量化观测 → 提出假设 → 单变量验证”的动作,与 Spark 里通过 UI 判断是 Shuffle、倾斜还是资源不足完全同构(见 5.5)。

参考:Nsight Systems 用户指南;nvidia-smi 手册

多卡/多机的通信与调度瓶颈

一句话定位:单卡装不下就要并行,而并行的代价是通信。选错并行策略或忽视拓扑,加卡不加速——判断标准只有一个:扩展效率是否线性

1. 张量并行(TP)vs 流水并行(PP)

1 张量并行(Tensor Parallelism)

  • 做法:把同一层内的权重矩阵按行/列切分到多卡,各卡算一部分再合并。
  • 特点:通信密集——每层前向都需要 AllReduce/AllGather 同步激活,通信发生在每一层、且在关键路径上。
  • 结论:宜放同机内、走 NVLink(高带宽低延迟)。跨机做 TP 会被网络带宽拖死。

2 流水并行(Pipeline Parallelism)

  • 做法:把模型按层切段,不同卡持有不同层,数据像流水线一样逐段流过。
  • 特点:通信量小(只在段边界传激活),适合跨机;但存在气泡(bubble)问题——流水线填充与排空阶段部分卡空闲,micro-batch 数越少气泡占比越高。
  • 缓解:增加 micro-batch 数量、使用 1F1B / 交错式调度减少气泡。

对比记忆:TP 换通信量、PP 换空闲气泡;实践中常混合使用(机内 TP + 跨机 PP,再叠加数据并行)。

2. NCCL 通信原语与拓扑感知

  • AllReduce:所有卡的数据求和(或其他归约)后,结果分发给所有卡——TP 与数据并行的梯度/激活同步主力。
  • AllGather:把各卡各自持有的分片汇集成完整数据,每卡都得到全量(常用于收集切分的激活或权重)。
  • 其余:ReduceScatter、Broadcast、AllToAll(MoE 场景关键)。
  • 拓扑感知:NCCL 会探测硬件拓扑(NVLink / NVSwitch / PCIe / 网卡),自动选择 Ring、Tree 等算法与最优路径。因此物理拓扑决定上限:NVLink 全连接 > PCIe > 跨机 RDMA/以太网,差距可达数量级。部署时要确认卡间实际连接方式(nvidia-smi topo -m)。

3. 如何判断通信瓶颈:看扩展效率是否线性

标准方法:

  1. 测单卡吞吐 T1,测 N 卡吞吐 TN
  2. 计算扩展效率 = TN / (N × T1)
  3. 效率接近 1 → 通信不是瓶颈;效率显著衰减(如 8 卡只有 4~5 倍)→ 通信瓶颈
  4. 进一步用 Nsight Systems 看时间线上通信 Kernel(NCCL kernel)占比与是否与计算重叠;用 NCCL 的带宽基准测试对比理论带宽。

对应动作:调整并行策略(把 TP 收进单机)、增大 micro-batch 减少气泡、开启通信与计算重叠、检查拓扑与网卡配置。

参考:NCCL 官方文档;论文《DeepSpeed-Inference: Enabling Efficient Inference of Transformer Models at Unprecedented Scale》

显存管理与 OOM 排查

一句话定位:显存 OOM 是 GPU 上最高频的故障,排查的关键是分清**”真的用满了”还是“分配器占着没还”**——这与 Spark Executor OOM 的排查思路完全同构。

1. 常见成因

  • batch / 序列过长:显存占用随 batch × 序列长度增长;注意力与 KV Cache(3.4)尤其敏感,是最常见的直接原因。
  • 显存碎片化:反复分配释放不同大小的张量后,空闲显存总量够但没有足够大的连续块,于是分配失败。表现为”明明还剩几个 G 却 OOM”。
  • 中间张量未释放:Python 变量仍持有引用(如把 loss/激活存进 list 做日志、未 detach() 导致计算图与激活被一直保留)。
  • 缓存分配器未归还:PyTorch 使用缓存分配器(caching allocator),释放的张量显存进入其内部缓存池而不立即归还驱动,以加速后续分配。这会让 nvidia-smi 看到的占用远高于实际张量用量,也可能挤压其他进程。

2. 排查手段

  • nvidia-smi:看进程级总占用与显存上限,判断是否被其他进程/多卡分配占用。注意它反映的是分配器保留的显存,不等于张量实际使用量。
  • PyTorch 显存 API
    • torch.cuda.memory_allocated()实际张量占用
    • torch.cuda.memory_reserved()分配器保留总量
    • torch.cuda.memory_summary():分类汇总,快速看出碎片与峰值;
    • 显存快照torch.cuda.memory._record_memory_history() + snapshot 可视化):定位是哪段代码分配了未释放的张量。
  • 核心诊断动作:分析 reservedallocated 的差值
    • 差值大 → 说明分配器缓存了大量碎片显存,属碎片化问题:可尝试 torch.cuda.empty_cache()(归还空闲缓存)、调整 PYTORCH_CUDA_ALLOC_CONF(如 expandable_segments)、统一 batch/序列长度以减少大小多样性。
    • 差值小但 allocated 已接近上限 → 是真实容量不足:只能降 batch、缩序列、梯度累积/checkpointing、量化(4)或换更大显存的卡。

参考:PyTorch 官方文档 CUDA semantics

SGLang 架构与适用场景

一句话定位:面向结构化 / 多轮 / 带复杂控制流的 LLM 程序的执行引擎。它的洞察是——这类程序的请求之间前缀高度重复,于是用前缀树把 KV Cache 复用做到极致。

1. 面向的问题:LLM 程序而非单次调用

现实中的 LLM 应用往往不是”一问一答”,而是一段程序:多轮对话、思维链、工具调用循环、按 schema 抽取字段、并行分支再汇总。

这类工作负载的特征:

  • 大量请求共享相同的前缀(同一 system prompt、同一 few-shot 示例、同一段上下文、同一对话历史);
  • 存在分支与循环,调用之间有依赖关系。

2. 核心机制

2.1 RadixAttention:前缀树复用 KV Cache

  • Radix Tree(基数树/压缩前缀树) 组织所有请求的 KV Cache:树上的每条路径代表一段 token 前缀,其 KV 只存一份。
  • 新请求到来时,在树上匹配最长公共前缀,直接复用已有 KV,只需为新增部分计算 K/V;
  • 配合 LRU 策略淘汰冷前缀。
  • 与 vLLM 的前缀共享(3.5)相比,RadixAttention 是自动、跨请求、可多级共享的通用机制,在前缀重复率高的场景收益极大(省下大量 Prefill 计算与显存)。

2.2 前端 DSL 编排

  • 提供一套嵌入 Python 的 DSL(如 genselect、fork/join 等原语)来描述多轮生成、分支与并行;
  • 前端把程序结构信息传给运行时,使调度器知道后续会用到哪些前缀,从而更好地做缓存复用与并行调度;
  • 同时支持受约束解码(按 JSON schema / 正则约束输出),保障结构化结果可解析。

3. 适用场景

  • Agent:多轮工具调用,system prompt 与历史反复复用;
  • 批量结构化抽取:同一套 prompt 模板 + 大量不同输入,前缀完全一致;
  • 思维链 / 自一致性采样(同一 prompt 多次采样)、多分支评估。

选型直觉:前缀重复率高、控制流复杂 → SGLang;通用高并发单轮服务 → vLLM;稳定配置追求极致性能 → TensorRT-LLM(3.8)。

参考:论文《SGLang: Efficient Execution of Structured Language Model Programs》;SGLang 官方文档

vLLM 与 PagedAttention

一句话定位:把操作系统的虚拟内存分页思想搬到 KV Cache 上——用非连续的固定大小 block 管理显存,把因碎片与预留造成的浪费降到极低,从而大幅提升并发与吞吐。

1. 问题:连续分配造成的双重浪费

传统实现给每个请求预留一段连续显存来放 KV Cache,且必须按可能的最大序列长度预留:

  • 内部浪费(预留过度):请求实际只生成 100 token,却按 2048 预留,剩余全部闲置;
  • 外部浪费(碎片化):请求长短不一、频繁进出,空闲显存被切碎,总量够却放不进新请求。

结果是有效显存利用率很低,能并发的请求数远低于理论值。

2. 机制:借鉴虚拟内存分页

  • 把 KV Cache 切成固定大小的 block(如每 block 存 16 个 token 的 K/V);
  • 一个请求的 KV 序列由若干 block 组成,这些 block 在物理显存上无需连续
  • 用**block table(页表)**维护”逻辑 token 位置 → 物理 block”的映射,注意力 kernel 按页表间接寻址(这就是 PagedAttention)。

直接收益:

  • 消除外部碎片:任何空闲 block 都能被复用;
  • 消除预留浪费:按需增长,用多少分多少(仅最后一个 block 有不足一页的内零头);
  • 显存利用率大幅提升 → 同样显存可容纳更多并发请求 → 吞吐显著提高(论文报告相较此前系统有数倍吞吐提升)。

3. 前缀共享与 Copy-on-Write

多个请求共享同一前缀(同一 system prompt、同一 few-shot 示例、并行采样同一 prompt 的多个输出)时:

  • 前缀对应的 block 只存一份,被多个请求的页表同时引用(引用计数);
  • 当某个请求需要在共享 block 上写入分歧内容时,触发 Copy-on-Write:复制该 block 后再写,其他请求不受影响。

这与操作系统 fork 的 COW 完全同源,能进一步省下大量重复显存。

4. 工程视角

vLLM 是围绕 PagedAttention 构建的推理引擎,同时实现了 Continuous Batching(3.6)——两者配合才是吞吐提升的完整来源:分页解决”装得下多少”,连续批处理解决”跑得满不满”

参考:论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》(Kwon et al., 2023);vLLM 官方文档

KV Cache 机制与优化

一句话定位:KV Cache 用显存换算力——它让自回归生成从 O(n²) 重复计算变成增量计算,但自身的显存开销随 batch × 序列长度线性增长,成为LLM 推理吞吐的头号上限

1. 机制:为什么需要缓存 K/V

自回归生成逐个 token 产出。生成第 t 个 token 时,注意力需要用到前面所有 token 的 Key 和 Value。

  • 若不缓存:每步都要对全部历史 token 重算 K/V,总计算量随序列长度平方增长,且大量重复。
  • KV Cache:把历史 token 的 K/V 张量缓存下来,每步只计算新 token 的 Q/K/V,并把新 K/V 追加进缓存。于是每步计算量与序列长度成线性关系而非重算全部。

由此推理天然分两阶段:

  • Prefill(处理 prompt):一次性并行计算全部输入 token 的 K/V,计算密集
  • Decode(逐 token 生成):每步只算一个 token,读取整个 KV Cache,访存/带宽密集、算力利用率低。

2. 代价:显存随 batch × 序列长度线性增长

KV Cache 大小 ≈ 2(K和V)× batch × 序列长度 × 层数 × KV头数 × head_dim × 精度字节数

  • 关键点:它随 batch 与序列长度线性膨胀,很快就超过模型权重本身的显存占用;
  • 于是能同时容纳多少并发请求由剩余显存决定 → KV Cache 直接决定了最大并发数与吞吐上限,这是”吞吐上不去”最常见的根因。

3. 优化方向

  • 分页管理(Paged):不连续分配、按固定 block 管理,消除碎片与预留浪费 → 见 3.5 PagedAttention/vLLM。
  • 量化 KV:把 KV Cache 存为 INT8/FP8,显存直接减半以上,代价是可能引入精度损失(长上下文更敏感)。
  • 共享前缀复用:多个请求共享相同 system prompt / few-shot 前缀时,前缀的 KV 只存一份并被多请求引用(配合 Copy-on-Write),在 Agent、批量结构化抽取等场景收益极大 → 对应 3.9 SGLang 的 RadixAttention。
  • 附:GQA/MQA 通过减少 KV 头数从模型结构层面缩小 KV Cache。

参考:论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》;Hugging Face 博客《LLM 推理中的 KV Cache 详解》

TensorRT-LLM 架构与部署流程

一句话定位:走编译式优化路线的推理加速方案——把模型提前编译成针对特定 GPU 与配置高度特化的 Engine,换取极致性能,代价是编译耗时与灵活性下降。

1. 核心优化手段

  • 图优化与算子融合:把多个小算子(如 LayerNorm + 残差、GEMM + 激活、Attention 相关算子)融合成单个 Kernel,减少 Kernel 启动开销与中间结果的 HBM 往返(同 3.7 的思路)。
  • Kernel 自动选优(auto-tuning):针对目标 GPU 架构与具体张量形状,在候选 Kernel 实现中实测挑选最快的一个,并固化进 Engine。
  • 量化集成:内置 INT8 / FP8 / INT4(含 SmoothQuant、AWQ 等)支持,与图优化协同(见 4)。
  • In-flight Batching:即 Continuous Batching 的实现(3.6),迭代级调度、完成即出队。
  • 多 GPU 并行策略封装:内建张量并行(TP)与流水并行(PP)的切分与通信逻辑,用户无需手写 NCCL 通信(见 3.10)。

2. 部署流程

  1. 模型转换:把 HuggingFace 等格式的权重转换为 TensorRT-LLM 的模型定义/检查点格式;
  2. 构建 Engine:指定精度、最大 batch、最大输入/输出长度、并行策略等,编译生成 .engine 文件。此步会做图优化与 Kernel 选优,耗时较长(数分钟到数十分钟);
  3. 部署运行:加载 Engine 提供服务,生产上常配合 Triton Inference Server(用 Triton 做请求管理、动态批处理、多模型编排与监控)。

3. 代价与取舍

  • 编译耗时:每次改模型、改精度、改最大长度或换 GPU 型号,通常都要重新编译
  • 灵活性下降:Engine 与”特定 GPU 架构 + 特定形状约束”绑定,不能像 PyTorch 那样随意改结构、动态调参;超出构建时设定的 max_batch/max_len 需重建。
  • 因此选型逻辑很清晰:追求极致吞吐/延迟且模型与配置稳定的生产场景 → TensorRT-LLM;需要快速迭代、频繁换模型 → vLLM 等运行时方案更合适。

参考:NVIDIA TensorRT-LLM 官方文档与 GitHub

数据质量过滤

一句话定位:在规则粗筛之后,用”模型判质量”做精筛——把网页语料里”通顺但没价值”的部分筛掉。这里的核心矛盾不是技术实现,而是质量与数据量的取舍

1. 分类器打分

标准做法(源自 GPT-3 的数据处理方法):

  1. 高质量语料作正样本(如维基百科、书籍、被高质量站点引用的网页);
  2. 取待过滤的网页语料作负样本;
  3. 训练一个轻量分类器(如 fastText / 逻辑回归,特征为 n-gram 或哈希特征)——必须轻量,因为要在十亿级文档上推理;
  4. 对每篇文档打”像高质量语料的程度”分,按分数(常配合按概率的随机保留策略,而非硬阈值)决定保留与否。

要点:分类器判的是”是否像高质量文本的分布“,不是判事实正确性。

2. 规则组合

分类器与规则组合使用、互为补充:

  • 规则负责确定性强的硬伤(乱码、模板、超短文本,见 2.2);
  • 分类器负责”语法通顺但低信息量”的软伤(SEO 文、内容农场、机器翻译痕迹)。

RefinedWeb(Falcon)的实践进一步说明:只靠严格的规则过滤 + 大规模去重,也能从纯网页数据造出媲美精选语料的高质量数据集,这提示规则与去重的性价比常被低估。

3. 核心 Tradeoff:召回质量 vs 数据量损失

这是本环节唯一真正的决策点:

  • 过滤过严 → 语料质量高,但数据量锐减;可能触及模型的数据量下限(Scaling Law 下 token 数不足会直接限制模型上限),且会系统性偏向正样本的风格/领域,损失语料多样性(如误杀口语、少数领域、非主流方言)。
  • 过滤过松 → 数据量大,但低质内容拉低训练效率,模型学到噪声模式。

实践取向:按目标 token 预算反推过滤强度——先确定训练需要多少 token,再调阈值使产出量恰好满足,且对被过滤样本做抽样人工核查,确认没有系统性误杀某一类有价值数据。

参考:GPT-3 论文附录数据过滤方法;论文《The RefinedWeb Dataset for Falcon LLM: Outperforming Curated Corpora with Web Data Only》

语言识别与规则过滤

一句话定位:流水线里最”便宜”也最高性价比的一道粗筛——用轻量分类器定语言、用硬规则砍掉明显低质文本,把数据量先降一个量级,减轻后续昂贵环节(质量过滤、去重)的负担。

1. 语言识别:fastText 分类器 + 阈值

  • fastText 语言识别模型对每条文本打分,输出语言标签与置信度。fastText 基于 n-gram 特征的浅层线性模型,速度极快(单机每秒可处理万级以上文本),适合超大规模语料。
  • 阈值策略:只保留目标语言且置信度高于阈值(如 0.65/0.8)的文本。阈值是典型权衡:
    • 阈值高 → 语言纯度高,但会误杀短文本、多语言混排文本(短文本置信度天然偏低);
    • 阈值低 → 保留量大,但混入他语言噪声。
  • CCNet 的做法即是”语言识别 + 按语言分桶 + 语言模型困惑度排序”,是业界标准范式。

2. 规则过滤的四个维度

规则过滤不依赖模型,纯启发式、可解释、可并行,主要维度:

  • 长度:过短(无信息量,如单句导航文本)或异常过长(拼接/爬取异常)都剔除;
  • 符号占比:标点、特殊符号、数字占比过高 → 多为代码残片、表格、乱码、垃圾内容;
  • 重复行:文档内重复行/重复段落比例过高 → 模板残留、刷屏内容(这也是”文档内去重”,与 2.4 的跨文档去重互补);
  • 停用词密度:正常自然语言有稳定的停用词比例,过低说明不是连贯的自然文本(如关键词堆砌、SEO 垃圾页、列表页)。

3. 瓶颈与优化

该环节是纯 CPU 密集:无 GPU 参与,成本与吞吐取决于 CPU 核数与调度效率。

优化只有两条主线:

  1. 提高并行度:任务天然可并行(逐条独立、无状态),横向扩 CPU 资源即可近线性加速;
  2. 批量化:批量读取与批量推理(fastText 批量打分)、减少 per-record 的函数调用与 I/O 开销,避免小文件与小批次带来的调度开销(对应 1.6 小文件问题)。

参考:fastText 官方文档;论文《CCNet: Extracting High Quality Monolingual Datasets from Web Crawl Data》

DataLoader 参数调优

一句话定位:训练/推理慢,很多时候不是 GPU 不够快,而是数据喂不上。DataLoader 调优的目标只有一个——让数据供给速度追上 GPU 消费速度,把 GPU 利用率从锯齿状拉成一条直线。

1. 三个核心参数

1 num_workers:并行加载子进程数

  • 用多个子进程并行做数据读取与前处理(解析、解码、增强),与主进程的 GPU 计算重叠。
  • 0 表示在主进程加载,GPU 会被数据准备完全阻塞。
  • 过高的代价:每个 worker 是独立进程,会抢占 CPU 并各自持有一份内存(数据集对象、缓冲区),导致 CPU 争抢、内存暴涨甚至 OOM;进程间通信与序列化开销也随之上升。
  • 经验取值:与可用 CPU 核数匹配(常见 4~8 或 核数/GPU数),需实测。

2 pin_memory:锁页内存

  • 设为 True 时,DataLoader 把 batch 放入锁页(page-locked / pinned)内存
  • 作用:锁页内存不会被操作系统换出,因此 Host→Device 拷贝可以走异步 DMA,与计算重叠(配合 non_blocking=True.to(device))。非锁页内存的 H2D 拷贝需要先暂存到临时锁页缓冲,多一次拷贝且无法异步。
  • 代价:占用不可换出的物理内存,过量使用会挤压系统内存。

3 prefetch_factor:每 worker 预取批数

  • 每个 worker 提前准备的 batch 数(默认 2),即预取队列深度。
  • 作用:掩盖 I/O 与前处理延迟——GPU 消费当前 batch 时,后续 batch 已在准备好的队列中,避免 GPU 等待(与 1.6 的异步预取是同一思想)。
  • 代价:队列越深,内存占用越高。

2. 判断依据:GPU 利用率呈锯齿状

这是最实用的诊断信号:

  • nvidia-smi(或 dmon / Nsight)观察 GPU 利用率曲线,若呈锯齿状(忙一下→掉到低位→再忙),说明 GPU 周期性空等 → 数据供给不足,瓶颈在 CPU 侧的加载/前处理,而不是 GPU 算力。
  • 此时正确动作是调 num_workers / prefetch_factor / pin_memory、优化前处理与数据格式(如换 Parquet/Arrow 减少解析开销,见 1.7),而不是换更强的卡。
  • 反之若利用率持续接近满载,才应从 kernel/batch/并行策略入手(对应 3.3 瓶颈定位)。

参考:PyTorch 官方文档 torch.utils.data.DataLoader