商汤 AI Infra 面试
Q: GEMM优化怎么做?为什么一个线程要计算C矩阵8x8个元素?寄存器会溢出吗?
GEMM优化核心思路:
GEMM(C = A * B)优化的本质是通过多级分块(Tiling)最大化数据复用,减少对HBM的访问次数。
1 | 全局内存 → Block Tile (加载到共享内存, ~128x128) |
为什么一个线程计算8x8个元素?——提高计算访存比(Arithmetic Intensity):
以外积(Outer Product)方式计算为例:
- 每个线程从共享内存读取A的一列片段(8个float)和B的一行片段(8个float) → 共读取16个float
- 用这16个float计算8×8=64次FMA → 计算访存比 = 64/16 = 4:1
- 如果每个线程只算1×1:读取2个float做1次FMA → 计算访存比 = 1:2
- 8x8的计算访存比是1x1的16倍,大幅减少共享内存带宽压力
寄存器使用分析——会溢出吗?
| 用途 | 寄存器数量 |
|---|---|
| C矩阵结果 (8x8 float) | 64个 |
| A片段缓存 (8 float) | 8个 |
| B片段缓存 (8 float) | 8个 |
| 循环变量、指针、临时值 | ~10-20个 |
| 合计 | ~90-100个 |
A100每个线程最多可用255个寄存器(每SM共65536个),100个寄存器远未溢出。但需注意:
- 寄存器使用量直接影响occupancy:用100个寄存器时,每SM最多驻留65536/100≈655个线程(约20个warp),occupancy约30%
- 这是性能权衡:较低occupancy但高计算效率(data reuse)通常比高occupancy但低效率更优
- Cutlass/cuBLAS中的高性能kernel通常在occupancy 25-50%时达到峰值性能
Q: #pragma unroll的作用?
#pragma unroll指示编译器将循环体展开为顺序执行的代码,消除循环控制开销。
展开前后对比:
1 | // 展开前 |
为什么能提升性能?
- 消除循环控制开销:减少了比较、分支跳转、计数器递增指令
- 提高ILP(指令级并行):展开后的独立指令可以被warp调度器同时发射到不同功能单元
- 使编译器更好优化:展开后编译器能看到更多数据依赖关系,做更好的寄存器分配和指令重排
- 减少分支预测失败:GPU上分支比CPU更昂贵(可能导致warp divergence)
使用方式:
1 | #pragma unroll // 完全展开(编译器自动判断循环次数) |
注意事项:
- 循环次数必须是编译时常量才能完全展开
- 过度展开会增加寄存器压力和指令cache miss
- CUDA中内层循环(如K维度的累加循环)展开效果最佳
Q: 与Cutlass/cuBLAS相比,手写GEMM kernel性能如何?
性能层次对比:
| 实现 | 典型性能 | 特点 |
|---|---|---|
| cuBLAS | 硬件峰值90-95% | NVIDIA黑盒库,针对各GPU架构和shape高度调优 |
| Cutlass | 硬件峰值85-93% | 开源C++模板库,提供定制灵活性 |
| 手写优化Kernel | 硬件峰值75-90% | 需要深入理解硬件,开发周期长 |
| 朴素实现 | 硬件峰值<10% | 对照基准 |
cuBLAS的优势:
- 针对每个GPU架构(sm_80/sm_89/sm_90)有独立优化的kernel库
- 内置heuristic自动选择最优kernel(根据M/N/K shape和数据类型)
- 利用未公开的硬件特性和指令
- 包含thousands of pre-tuned kernel变体
Cutlass的优势:
- 模板化设计,支持自定义epilogue(如GEMM+Bias+ReLU融合)
- 支持自定义数据布局和Tensor Core MMA指令
- 提供不同粒度的组件(从GEMM到warp-level MMA到thread-level copy)
- 性能接近cuBLAS,但完全可控
手写kernel有优势的场景:
- 非标准shape(如极端的M=1或K很小的Skinny GEMM)
- 自定义融合(GEMM + 复杂后处理逻辑)
- 非标准数据类型或布局
- 与框架特定的内存管理集成(如PagedAttention中的非连续KV读取)
Q: 了解Tensor Core吗?
Tensor Core是NVIDIA GPU上的专用**矩阵乘加(MMA)**硬件单元,通过在单个时钟周期内完成小矩阵乘法来大幅提升算力。
工作原理:
1 | D = A * B + C |
各代GPU的Tensor Core对比:
| 架构 | GPU | 支持精度 | 峰值算力(FP16) | MMA指令形状 |
|---|---|---|---|---|
| Volta (sm_70) | V100 | FP16 | 125 TFLOPS | 8x8x4 |
| Ampere (sm_80) | A100 | FP16/BF16/TF32/INT8/FP64 | 312 TFLOPS | 16x8x16 |
| Hopper (sm_90) | H100 | +FP8(E4M3/E5M2) | 989 TFLOPS(FP16) | 16x8x16/64x… |
| Blackwell (sm_100) | B200 | +FP4 | 2.5 PFLOPS(FP4) | 更大 |
使用方式(从高到低):
- cuBLAS/cuDNN:自动使用Tensor Core(最简单)
- Cutlass:模板级别配置MMA指令
- **WMMA API (C++)**:
nvcuda::wmma::mma_sync(),warp级别操作 - PTX/SASS MMA指令:最细粒度,如
mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16
使用约束:
- 矩阵维度必须是特定值的倍数(如16的倍数)
- 数据需要满足对齐要求(通常128-bit对齐)
- 只支持特定数据类型组合(如FP16输入+FP32累加)
- 需要整个warp协同执行MMA操作
Q: 做过低比特位的GEMM吗?
低比特GEMM(INT8/INT4/FP8)是模型推理加速的关键技术,核心是用低精度计算换取更高吞吐和更低带宽需求。
为什么低比特GEMM更快?
- 带宽节省:INT8数据量是FP16的一半,memory-bound场景下直接快2倍
- 计算吞吐翻倍:A100 INT8 Tensor Core算力624 TOPS vs FP16 312 TFLOPS
- 显存节省:模型权重减小一半(INT8)或四分之一(INT4)
量化GEMM的计算流程:
1 | Y_fp32 = dequant(X_int8) * dequant(W_int8) |
关键技术挑战:
| 挑战 | 解决方案 |
|---|---|
| 量化精度损失 | Per-channel/per-group scale;校准数据集选择 |
| 激活值outlier | SmoothQuant转移难度到权重;per-token动态量化 |
| 混合精度累加 | INT8乘+INT32/FP32累加(Tensor Core原生支持) |
| 数据布局对齐 | Interleaved layout/CUTLASS最优布局 |
| 不同量化粒度 | Per-tensor(简单) vs per-channel(精度好) vs per-group(折中) |
FP8 vs INT8对比(H100):
| 特性 | INT8 | FP8 (E4M3) |
|---|---|---|
| 动态范围 | [-128, 127] | [-448, 448] |
| 精度 | 均匀分布 | 近似对数分布(小值精度高) |
| 需要zero-point | 常需要 | 不需要 |
| H100峰值 | 1979 TOPS | 3958 TOPS |
| 量化误差 | 对outlier敏感 | 更好处理不同scale |