L2.6 算子 Shape 泛化与性能分析
三维坐标
layer: L2(数据与算子)|level: Senior|pillar: 编程与编译 + 训推框架上一篇我们把算子编译到了硬件上,本文要回答一个更工程化的问题:同一个算子在千变万化的输入 shape(变长序列、不同 batch)下,如何既不浪费算力、又能被精确度量与归因? 你将建立两套核心武器——「Shape 泛化的工程权衡」与「Roofline + Profiler 的性能定位方法论」。
学习目标
- 前置知识:读过 L0 全层(尤其 L0.2 三大物理墙:算力墙 / 显存墙、Roofline 拐点的直觉);读过 L1.1(知道 GPU Tensor Core「精度换吞吐」、warp 调度与 occupancy);读过本 L2 层前序(L2.1~L2.5:算子如何被编译成 kernel、什么 是 GEMM / Attention 算子);会写基础 PyTorch、能跑通一段推理脚本即可,无需 CUDA 编程经验。
- 学完产出:① 能写出静态 Padding 的有效算力利用率公式(区分 FFN 线性项与 Attention 平方项),并据此量化「一条长序列拖垮整个 batch」浪费了多少 FLOPs;② 能讲清 Padding / Bucket / Padding-Free 三档泛化策略各自的收益与代价,并给出一棵随长度方差、QPS、团队工程能力变化的决策树;③ 能用 Roofline 模型把任意算子钉在斜坡区(Memory-Bound)或平顶区(Compute-Bound),手算 ridge point 并解释为什么 LLM decode 几乎全是 Memory-Bound;④ 能照「系统级 timeline → top-k kernel → Roofline 归因 → 单 kernel 深挖」这条定位漏斗,排除「GPU 利用率低就是 kernel 烂」之类的误判;⑤ 亲手用
torch.profiler抓一段推理 trace,导出 chrome trace 并对 top-3 kernel 做 Memory/Compute-Bound 归因。 - 阅读姿势:盯住一条主线——「先量化,再决策;先定位,再优化」。无论是选 Shape 泛化策略,还是判 kernel 瓶颈,本文的全部方法论都在反对「凭直觉拍脑袋调优」:用一个利用率公式量化浪费、用 Roofline 一眼判定天花板、用 Profiler 漏斗逐层收敛——把性能工程从玄学变成可复算的工程。
背景与现状
在大模型推理与训练里,输入 shape 几乎从不固定:每个请求的序列长度(seq_len)不同、每个 batch 的样本数不同。而 GPU/NPU 的 kernel 往往针对「固定 shape」编译最优,于是诞生了一对永恒的矛盾——算 力浪费(为了对齐 shape 而做无用计算)与 工程复杂度(为了不浪费而引入变长处理逻辑)。
围绕这对矛盾,业界的演进可概括为三个阶段:
- 静态 Padding 时代:把一个 batch 内所有序列补齐到最大长度(甚至补到模型支持的
max_seq_len),实现简单、kernel 规整,但浪费的算力随长度方差线性增长——一个[8, 2048]的 batch 里若真实有效 token 只占 30%,就有 70% 的 FLOPs 在算 padding。 - 动态 Bucket 时代:把相近长度的序列**分桶(bucketing)**后分别 padding,把浪费控制在桶内方差。代价是调度复杂、桶边界要调参、小桶利用率低。
- Padding-Free / 变长时代:以 FlashAttention 的 varlen 接口、vLLM 的 continuous batching、FlashDecoding 为代表,用
cu_seqlens(累积序列长度)把多条变长序列拼成一条连续内存,彻底消灭 padding 算力浪费。代价是算子实现门槛极高、对 kernel 与内存布局有强约束。
业界信号:HuggingFace Transformers 在 4.x 引入
attn_implementation="flash_attention_2"并支持 padding-free 训练;vLLM 把 continuous batching 做成默认调度策略。这说明「消灭 padding 浪费」已从论文走向生产标配——Senior 工程师的价值,正体现在能量化这笔浪费并选择正确的泛化策略。
而要「量化浪费」「定位瓶颈」,光靠直觉不行,必须有可观测工具与理论模型:
| 工具 / 模型 | 解决什么问题 | 粒度 |
|---|---|---|
| Nsight Systems | 系统级 timeline:CPU/GPU 时间线、kernel 启动间隙、H2D/D2H 拷贝 | 系统级 |
| Nsight Compute | 单 kernel 深挖:SM 占用率、内存吞吐、warp stall 原因 | kernel 级 |
| Ascend Insight | 昇腾 NPU 的算子级 profiling(对标 Nsight) | 算子 / kernel 级 |
| PyTorch Profiler | 框架内一站式:aten 算子 → 底层 kernel 的映射 + chrome trace | 算子 → kernel |
| Roofline 模型 | 理论判定 kernel 是 Memory-Bound 还是 Compute-Bound | 理论 |
原理与架构
2.1 静态 Padding 的算力浪费——可量化
设一个 batch 有 B 条序列,真实长度为 L_i,padding 后统一到 L_max = max(L_i)。对 Transformer,Attention 的计算量约正比于序列长度的平方,FFN 正比于序列长度。有效算力占比可近似为:
利用率 ≈ Σ L_i / (B · L_max) # FFN 主导(线性)
利用率 ≈ Σ L_i² / (B · L_max²) # Attention 主导(平方,浪费更狠)
当长度方差大时,L_max 被极端长序列拉高,分母膨胀,利用率骤降——这正是「一条长序列拖垮整个 batch」的根因。Bucket 的本质就是把方差大的整体拆成方差小的子集,Padding-Free 则直接令分母 B·L_max → Σ L_i,利用率回归 100%。
工程权衡铁律:Padding-Free 不是「免费午餐」。它要求算子原生支持
cu_seqlens、要求 KV Cache 按 token 而非按样本组织(PagedAttention)、要求 loss/mask 逻辑同步改写。在「序列长度方差小」的场景,静态 padding 的简单性反而更划算——先 profile 量化浪费,再决定是否上变长。
2.2 Roofline 模型——判定 Memory-Bound vs Compute-Bound
光知道某个 kernel「慢」还不够,要知道它「为什么慢、优化天花板在哪」。Roofline 模型 用两个硬件常数和一个算子属性,把任意 kernel 钉在一张图上:
- 峰值算力
π(FLOPS):硬件每秒最多做多少次浮点运算。 - 峰值带宽
β(Bytes/s):硬件每秒最多从显存搬多少字节。 - 算术强度
I(Arithmetic Intensity)= FLOPs / Bytes:算子每搬运 1 字节数据,能做多少次浮点运算。这是算子自身的属性,与硬件无关。
可达性能上界为:
可达 FLOPS = min( π , β × I )
二者相等处的算术强度叫 ridge point(脊点 / 拐点):I_ridge = π / β。
一个具体算例:以 A100(FP16 Tensor Core 峰值 ≈ 312 TFLOPS,HBM2e 带宽 ≈ 2.0 TB/s)为例,I_ridge = 312e12 / 2.0e12 ≈ 156 FLOPs/Byte。
- 大矩阵乘
[4096,4096]×[4096,4096]:FLOPs ≈ 2·4096³ ≈ 1.37e11,读写字节 ≈ 3·4096²·2 ≈ 1.0e8,I ≈ 1370 FLOPs/Byte ≫ 156→ Compute-Bound,应吃满 Tensor Core。 - LayerNorm / Softmax / 残差加:每个元素只做几次运算就要读写一遍,
I通常 < 5 → Memory-Bound,瓶颈在带宽,优化靠融合(kernel fusion)减少显存往返,而非堆算力。
这里的关键在于:LLM 推理的 decode 阶段(逐 token 生成)几乎全是 Memory-Bound——batch=1 时 GEMM 退化成 GEMV,算术强度极低,瓶颈是把权重从 HBM 搬出来。这就是为什么推理优化的核心是显存带宽与权重量化(INT8/INT4 减少搬运字节),而非提升峰值算力。Roofline 一眼看穿这件事。
2.3 工具分工:从系统到 kernel 的定位漏斗
这条漏斗的纪律:永远从系统级 timeline 入手(先看有没有 GPU 空跑、CPU launch bound、拷贝瓶颈),再钻到具体 kernel,最后用 Roofline 判定方向、用 Nsight Compute 验证细节。跳过系统级直接调单 kernel,常常是「优化了一个本来就不在关键路径上的算子」。
动手实践:极简代码实操
实验目标:用 torch.profiler 抓一段推理的 timeline,导出 chrome trace 可视化,按设备耗时排序定位 top-3 kernel,并用 Roofline 直觉对每个 kernel 做 Memory/Compute-Bound 归因。产出物:一张 trace.json(可拖进 chrome://tracing / edge://tracing 或 Perfetto 查看)+ top-3 kernel 的归因表。
3.1 环境准备
# 推荐 Python 3.11;用 uv 或 venv 隔离
python3 -m venv .venv && source .venv/bin/activate
# 有 NVIDIA GPU:装 CUDA 版(自动选)
pip install torch torchvision
# 无 GPU(Mac/CPU):装 CPU 版,实验同样跑得通
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu