Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

AI 编译器 / AI Compilers

1. AI 编译器全景——为什么模型需要编译器 / The AI Compiler Landscape and Why Models Need Compilers

2. 编译原理速通——面向 ML 工程师的核心概念 / Compiler Fundamentals for Machine Learning Engineers

3. 中间表示基础——理解 IR 层级与 lowering 链路 / Intermediate Representation Levels and Lowering Pipelines

4. 计算图的构建与表示 / Building and Representing Computational Graphs

5. MLIR 架构、方言与渐进式降级 / MLIR Architecture, Dialects, and Progressive Lowering

6. 算子语义、广播、归约与形状推导 / Operator Semantics, Broadcasting, Reduction, and Shape Inference

7. 模型前端格式:ONNX、TFLite、HLO 与 SavedModel / Model Frontend Formats: ONNX, TFLite, HLO, and SavedModel

8. 图优化 Pass——经典优化在 ML 中的应用 / Graph Optimization Passes for Machine Learning

9. 算子融合——编译器最重要的性能优化 / Operator Fusion as a Core Compiler Optimization

10. 内存规划——Buffer 分配与生命周期管理 / Memory Planning, Buffer Allocation, and Lifetime Management

11. Layout 优化——数据排布转换与内存效率 / Layout Optimization for Data Movement and Memory Efficiency

12. 动态 Shape——符号分析与形状处理 / Dynamic Shapes, Symbolic Analysis, and Shape Processing

13. 硬件约束下的操作调度 / Operation Scheduling Under Hardware Constraints

14. 从模板、DSL 到 IR 降级的代码生成架构 / Code Generation Architectures from Templates and DSLs to IR Lowering

15. CPU 后端:SIMD、分块与多线程 / CPU Backends with SIMD, Tiling, and Multithreading

16. CUDA 后端:合并访存与 Tensor Core / CUDA Backends, Memory Coalescing, and Tensor Cores

17. NPU 后端:脉动阵列与端侧 AI 生态 / NPU Backends, Systolic Arrays, and Edge AI Ecosystems

18. Kernel 性能基础:Roofline 与 Occupancy / Kernel Performance Fundamentals with Roofline and Occupancy

19. CUTLASS 与分层 GEMM 模板 / CUTLASS and Hierarchical GEMM Templates

20. TVM Tensor Expression 与计算调度分离 / TVM Tensor Expressions and Compute-Schedule Separation

21. 使用 Triton 编写高性能 GPU Kernel / Triton for High-Performance GPU Kernels in Python

22. 基于成本模型与实测搜索的自动调度 / Automatic Scheduling with Cost Models and Measurement-Based Search

23. XLA 内部机制:HLO、融合与 SPMD / XLA Internals, HLO, Fusion, and SPMD

24. Torch-MLIR:从 PyTorch 算子到 MLIR 方言 / Torch-MLIR from PyTorch Operators to MLIR Dialects

25. torch.compile:Dynamo、AOTAutograd、Inductor 与 Triton / Torch Compile with Dynamo, AOTAutograd, Inductor, and Triton

26. 从 MLIR 经 LLVM 降级到机器码 / Lowering from MLIR Through LLVM to Machine Code

27. 量化——低精度推理的工程实践 / Engineering Low-Precision Inference with Quantization

28. 分布式编译与训练——多设备编排的编译器支持 / Compiler Support for Distributed Training and Multi-Device Orchestration

29. 生产调试——真实问题的编译器视角排查 / Production Debugging from the Compiler Perspective

30. 未来方向——AI 编译器的新挑战与机遇 / Future Challenges and Opportunities for AI Compilers

本页目录

📅 创建时间:2026-06-03 🏷️ 标签:#Kernel #Roofline #Occupancy #ArithmeticIntensity #内存带宽 #计算密度 #性能优化 📚 前置知识:[[/04-ai/01-llm-engineering/07-llm-evolution]](LLM 发展脉络) 📚 相关知识:[[15-backend-cuda]](CUDA 后端)[[14-backend-cpu]](CPU 后端)[[18-cutlass]](CUTLASS)[[19-tvm-te]](TVM TE)[[20-triton]](Triton)


Kernel 性能基础:Roofline 与 Occupancy / Kernel Performance Fundamentals with Roofline and Occupancy ​

┌──────────────────────────────────────────────────────────────────────────────┐ │ 📖 场景:你写了一个矩阵乘法 kernel │ ├──────────────────────────────────────────────────────────────────────────────┤ │ 你写了一个矩阵乘法 kernel,跑 benchmark 比 cuBLAS 慢 10 倍。 │ │ 你知道逻辑是对的,问题在硬件层面。但你不知道具体哪里出了问题。 │ └──────────────────────────────────────────────────────────────────────────────┘

第1节 Roofline Model——理解计算上限 ​

1.1 什么是 Roofline Model ​

Roofline Model 是一个性能分析可视化工具,它告诉你一个 kernel 的理论上限取决于什么。在 GPU 上,内存带宽和计算峰值是两个硬上限——你的 kernel 性能不会超过其中任何一个。

         A100 FP32 Peak: 19.5 TFLOPS
        /
       /  ← 计算峰值上限 (Compute-Bound)
      /
     /
    /
   /
  / ← 斜率 = 内存带宽 / 计算峰值
 /
/
+--------------------------------------------------------→ 算术强度 (Flops/Byte)
   ↑
   内存带宽上限 (Memory-Bound)
   
   A100 HBM Bandwidth = 2039 GB/s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键公式:

  • 算术强度 (Arithmetic Intensity) = Flops / Bytes — 每次内存访问做多少计算
  • 计算峰值上限 = min(Peak FLOPS, Bandwidth × Arithmetic Intensity)
  • ** Roofline 曲线** = min(AithmeticIntensity × Bandwidth, PeakFLOPS)

1.2 两种瓶颈类型 ​

瓶颈类型英文特征优化方向
内存瓶颈Memory-Bound算术强度低,等待内存减少内存访问、增加复用
计算瓶颈Compute-Bound算术强度高,CPU/ALU 饱和提升计算利用率、增加并行度

1.3 A100 具体数据 ​

硬件参数 (A100 SXM):
  - FP32 Peak FLOPS:    19.5 TFLOPS    (单精度计算上限)
  - FP64 Peak FLOPS:    9.7 TFLOPS    (双精度)
  - Tensor FP16 Peak:   312 TFLOPS    (Tensor Core)
  - HBM Bandwidth:      2039 GB/s      (显存带宽)
  - L2 Cache Bandwidth: ~2.5 TB/s     (约 1.2x HBM)
  - L1 Cache Bandwidth: ~13 TB/s       (约 6x HBM)

算术强度临界点 (FP32):
  - 临界算术强度 = PeakFLOPS / Bandwidth
                   = 19.5e12 / 2039e9
                   ≈ 9.56 Flops/Byte
                   
  - 如果 Flops/Byte < 9.56  → Memory-Bound
  - 如果 Flops/Byte > 9.56  → Compute-Bound
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

1.4 用 Roofline 诊断你的 Matmul Kernel ​

回头看你的 naive matmul kernel:

cuda
__global__ void matmul(float* C, float* A, float* B, int M, int N, int K) {
    // 每次外层循环迭代:
    //   读取: A[row*K+k]  → 1 float = 4 bytes
    //   读取: B[k*N+col]  → 1 float = 4 bytes
    //   计算: 1次乘 + 1次加 = 2 Flops
    // 
    // 总共读取: 2 * K * 4 bytes per element
    // 总共计算: 2 * K Flops per element
    //
    // 算术强度 = 2K / (2K * 4) = 2/4 = 0.5 Flops/Byte
    // 远低于 A100 临界点 9.56 → 严重 Memory-Bound!
    
    int row = blockIdx.x * blockDim.x + threadIdx.x;
    int col = blockIdx.y * blockDim.y + threadIdx.y;
    if (row < M && col < N) {
        float sum = 0.0f;
        for (int k = 0; k < K; k++)
            sum += A[row * K + k] * B[k * N + col];  // 每步读 8 bytes,算 2 Flops
        C[row * N + col] = sum;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

问题诊断:

  1. 每个线程每次迭代只处理 1 个输出元素,但每个元素需要读取 K 行 + K 列的数据
  2. K=1024 时,每个线程要读 2048 个 float = 8KB 数据,但只做了 2048 次乘加
  3. 算术强度 = 2K / (2K × 4) = 0.5 Flops/Byte,比临界点低 19 倍
  4. GPU 大部分时间在等待内存,而不是计算

1.5 Roofline 分析 Python 脚本 ​

python
#!/usr/bin/env python3
"""
Roofline Model 分析工具
分析给定 kernel 的算术强度,判断瓶颈类型
"""

# ======================== 硬件参数 ========================
# A100 SXM4 80GB
HARDWARE = {
    "A100": {
        "fp32_peak_tflops": 19.5,        # TFLOPS
        "fp16_tensor_tflops": 312.0,      # TFLOPS (Tensor Core)
        "hbm_bandwidth_gbps": 2039.0,    # GB/s
        "l2_bandwidth_gbps": 2500.0,     # GB/s (估算)
        "l1_bandwidth_gbps": 13000.0,    # GB/s (估算)
    },
    "H100": {
        "fp32_peak_tflops": 67.0,        # TFLOPS
        "fp8_tensor_tflops": 3958.0,      # TFLOPS (Hopper FP8)
        "hbm_bandwidth_gbps": 3350.0,    # GB/s (H100 SXM5)
        "hbm3_bandwidth_gbps": 5100.0,   # GB/s (H200)
    },
}

def compute_arithmetic_intensity(flops_per_element: float, 
                                  bytes_per_element: float) -> float:
    """
    计算算术强度:Flops/Bytes
    
    Args:
        flops_per_element: 每个输出元素的浮点运算次数
        bytes_per_element:  每个输出元素需要读取的总字节数
    Returns:
        算术强度 (Flops/Byte)
    """
    return flops_per_element / bytes_per_element

def roofline_attainment(arithmetic_intensity: float,
                        hardware: dict,
                        dtype: str = "fp32") -> dict:
    """
    计算 Roofline 上限和实际可达性能
    
    Args:
        arithmetic_intensity: 算术强度 (Flops/Byte)
        hardware: 硬件参数字典
        dtype: 数据类型 ("fp32", "fp16", "fp8")
    Returns:
        包含分析结果的字典
    """
    # 根据 dtype 选择峰值 FLOPS
    if dtype == "fp32":
        peak_tflops = hardware["fp32_peak_tflops"]
    elif dtype == "fp16":
        peak_tflops = hardware.get("fp16_tensor_tflops", hardware["fp32_peak_tflops"] * 2)
    elif dtype == "fp8":
        peak_tflops = hardware.get("fp8_tensor_tflops", 0)
    else:
        peak_tflops = hardware["fp32_peak_tflops"]
    
    bandwidth = hardware["hbm_bandwidth_gbps"]  # GB/s
    peak_gflops = peak_tflops * 1000            # GFLOPS
    
    # Roofline 临界点: PeakFLOPS / Bandwidth
    # 在这个点上,内存带宽和计算峰值相等
    # 低于这个点 → memory-bound;高于这个点 → compute-bound
    critical_ai = peak_gflops / bandwidth       # Flops/Byte
    memory_bound_roof = bandwidth * arithmetic_intensity  # GFLOPS
    compute_bound_roof = peak_gflops            # GFLOPS
    
    # 实际可达性能上限
    attainable = min(memory_bound_roof, compute_bound_roof)
    
    # 判断瓶颈类型
    if arithmetic_intensity < critical_ai:
        bottleneck = "Memory-Bound"
        theoretical_tflops = memory_bound_roof / 1000  # TFLOPS
    else:
        bottleneck = "Compute-Bound"
        theoretical_tflops = peak_tflops
    
    # 计算利用率(相对于峰值)
    # 实际性能 / Roofline 上限 = 利用率百分比
    # 这个比例告诉你 kernel 有多少优化空间
    roofline_limit = min(bandwidth * arithmetic_intensity, peak_gflops)
    roofline_utilization = min(arithmetic_intensity / critical_ai, 1.0) if critical_ai > 0 else 0
    
    return {
        "arithmetic_intensity": arithmetic_intensity,
        "critical_ai": critical_ai,
        "bottleneck": bottleneck,
        "peak_tflops": peak_tflops,
        "memory_bandwidth_gbps": bandwidth,
        "roofline_limit_gflops": roofline_limit,
        "attainable_tflops": theoretical_tflops,
        "roofline_utilization": roofline_utilization,
        "efficiency": (theoretical_tflops / peak_tflops * 100) if peak_tflops > 0 else 0,
    }

def analyze_naive_matmul(M, N, K, dtype="fp32"):
    """分析 naive matmul kernel 的算术强度"""
    # 每个输出元素 C[i,j] = sum_k A[i,k] * B[k,j]
    # 读取: A 需要 M*K 个 float,B 需要 K*N 个 float
    #       分摊到每个输出: (M*K + K*N) / (M*N) = K*(M+N)/(M*N) ≈ 2K/MN * M*N = 2K bytes
    # 等等,这个算法每个线程读一行 A 和一列 B
    # 实际每个输出元素读取: 2*K float = 8K bytes
    bytes_per_output = 2 * K * 4  # K 行 A + K 列 B,每 float 4 bytes
    flops_per_output = 2 * K      # K 次乘 + K 次加 = 2K Flops
    
    ai = compute_arithmetic_intensity(flops_per_output, bytes_per_output)
    print(f"\n{'='*60}")
    print(f"Naive Matmul Analysis: M={M}, N={N}, K={K}, dtype={dtype}")
    print(f"{'='*60}")
    print(f"  每元素读取字节数: {bytes_per_output} bytes ({2*K} floats)")
    print(f"  每元素浮点运算数: {flops_per_output} Flops ({K}乘 + {K}加)")
    print(f"  算术强度: {ai:.2f} Flops/Byte")
    
    result = roofline_attainment(ai, HARDWARE["A100"], dtype)
    
    print(f"\n  【瓶颈诊断】")
    print(f"  瓶颈类型: {result['bottleneck']}")
    print(f"  Roofline 临界点: {result['critical_ai']:.2f} Flops/Byte")
    print(f"  理论上限: {result['attainable_tflops']:.2f} TFLOPS")
    print(f"  计算峰值: {result['peak_tflops']:.2f} TFLOPS")
    print(f"  理论效率: {result['efficiency']:.1f}%")
    
    if result['bottleneck'] == "Memory-Bound":
        print(f"\n  ⚠️  Memory-Bound: 大部分时间在等内存")
        print(f"     优化方向: 增加数据复用 (tiling)、使用 shared memory")
        print(f"     理论加速: {result['peak_tflops'] / result['attainable_tflops']:.1f}x (如果变成 Compute-Bound)")
    
    return result

# 分析不同 K 值
if __name__ == "__main__":
    print("=" * 60)
    print("Roofline Model 分析 - A100 GPU")
    print("=" * 60)
    
    # K 值越小,算术强度越低(每个元素读 2K floats,但 K 小)
    # 注意:算术强度 = 2K / (8K) = 0.25,和 K 无关!
    # 真正的问题是:每个线程独立读一行 A 和一列 B,没有数据复用
    
    # 但如果我们用 tiling,每次把一个 tile 加载到 shared memory:
    # tile 大小 TILE_M × TILE_K
    # 每个 tile 加载一次 A[TILE_M, TILE_K],供 TILE_M 个线程复用
    # 每个元素读取: TILE_K * 4 bytes (A) + TILE_K * 4 bytes (B) / TILE_M 线程分摊
    # 实际上 tiling 后算术强度 = (2 * TILE_K * TILE_M) / (TILE_K * 4 + TILE_K * 4) = TILE_M / 4
    
    TILE_M = 64  # 假设每个 tile 64 行
    print(f"\n使用 TILE_M={TILE_M} 的 tiling 后:")
    print(f"  每个输出元素的算术强度提升到: {TILE_M / 4:.1f} Flops/Byte")
    
    result_tiled = roofline_attainment(TILE_M / 4, HARDWARE["A100"], "fp32")
    print(f"  瓶颈类型: {result_tiled['bottleneck']}")
    print(f"  理论上限: {result_tiled['attainable_tflops']:.2f} TFLOPS")
    print(f"  理论效率: {result_tiled['efficiency']:.1f}%")
    
    # 当 TILE_M >= 38 时 (临界算术强度 ~9.56),算术强度 > 9.56
    # kernel 从 memory-bound 变为 compute-bound
    print(f"\n{'='*60}")
    print("关键发现:")
    print("  - Naive kernel: 算术强度 0.25 Flops/Byte,效率 <3%")
    print("  - Tiling 后: 算术强度 = TILE_M/4,提升到 16+ 时接近峰值")
    print("  - 优化空间: ~7x 加速 (从 2.7 TFLOPS 到 19.5 TFLOPS)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

第2节 GPU Occupancy——每 SM 活跃线程数 ​

2.1 什么是 Occupancy ​

GPU Occupancy(占用率) = 实际使用的线程数 / 最大可支持线程数。它衡量 GPU SM(流多处理器)的利用程度。

A100 SM 资源限制:
┌─────────────────────────────────────────────────────────┐
│  每个 SM 最多 2048 线程 (128 threads/block × 16 blocks)  │
│  每个 SM 最多 32 个 block                               │
│  每个 SM 最多 64 warps (32 threads/warp × 64 warps)     │
│  每个 SM 最多 16 个 CTA (cooperative thread array)      │
│                                                         │
│  Register 限制: 每 SM 最多 65536 registers              │
│  Shared Memory 限制: 每 SM 最多 164 KB                  │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10

2.2 寄存器压力和 Shared Memory 压力 ​

Occupancy 下降的两个主要原因:

压力类型原因影响
Register 压力每个线程使用过多寄存器SM 能放的线程数减少
Shared Memory 压力每个 block 使用过多 shared memorySM 能放的 block 数减少

Occupancy 计算公式:

Occupancy = min(
    ThreadsPerSM / MaxThreadsPerSM,          # 线程数限制
    RegistersPerSM / (RegistersPerThread × ThreadsPerSM),  # 寄存器限制
    SharedMemPerSM / (SharedMemPerBlock × BlocksPerSM)     # Shared Memory 限制
)
1
2
3
4
5

2.3 计算 Naive Matmul 的 Occupancy ​

cuda
__global__ void matmul(float* C, float* A, float* B, int M, int N, int K) {
    // 分析每个线程的寄存器使用:
    // - row, col, k: int 类型 → 2 registers (或 1)
    // - sum: float → 1 register
    // - A[row*K+k], B[k*N+col]: 寄存器间接寻址,不占寄存器
    // 总计约 3-4 registers per thread
    
    int row = blockIdx.x * blockDim.x + threadIdx.x;
    int col = blockIdx.y * blockDim.y + threadIdx.y;
    if (row < M && col < N) {
        float sum = 0.0f;
        for (int k = 0; k < K; k++)
            sum += A[row * K + k] * B[k * N + col];
        C[row * N + col] = sum;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

用 nvcc --ptxas-info 分析:

ptxas info: 0 bytes gmem, 0 bytes cmem[3]
ptxas info: Compiling entry function 'matmul' for sm_80
ptxas info: Function properties for matmul
    0 bytes stack frame, 0 bytes spilled stores, 0 bytes spilled loads
ptxas info: Used 4 registers, 0 bytes smem, 0 bytes cmem

每个线程 4 registers:
  - A100 每 SM 65536 registers / 4 = 16384 threads per SM
  - 实际限制: 16384 < 2048 (最大值) → Occupancy 由寄存器限制
  - 但 16384/2048 = 100% → 寄存器够用
  
Block 大小 256 (8×32):
  - 2048 threads / SM ÷ 256 threads/block = 8 blocks per SM
  - 8 blocks × 0 smem = 0 smem < 164KB → shared memory 足够
  - 实际 Occupancy ≈ 100%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

但注意:高 Occupancy 不等于高性能。Naive matmul 虽然 Occupancy 接近 100%,但因为算术强度只有 0.25 Flops/Byte,它仍然是 Memory-Bound,大量时间在等待内存读取。

2.4 为什么高 Occupancy 可能还是慢 ​

指标Naive Matmul优化后 Matmul
Occupancy~100%~75%
算术强度0.25 Flops/Byte16 Flops/Byte
实际性能~2.7 TFLOPS~16 TFLOPS
瓶颈Memory-BoundCompute-Bound

结论: Occupancy 告诉你"GPU 有多忙",但 Roofline 告诉你忙在哪里。一个 100% Occupancy 的 Memory-Bound kernel,可能比 75% Occupancy 的 Compute-Bound kernel 慢 5 倍。

第3节 Arithmetic Intensity 深入分析 ​

3.1 GEMM 的计算密度随矩阵大小变化 ​

GEMM 的算术强度公式:

C = A × B, 其中 A ∈ R^(M×K), B ∈ R^(K×N), C ∈ R^(M×N)

对于标准 GEMM:
  - 总 Flops = 2 × M × N × K (乘加)
  - 总 Bytes = M×K×4 + K×N×4 + M×N×4 (A+B+C 各一次读)
  - 每输出元素 Bytes = (M×K + K×N) / (M×N) × 4 = 4K × (1/M + 1/N)
  - 每输出元素 Flops = 2K
  - 算术强度 = 2K / (4K × (1/M + 1/N)) = 1 / (2 × (1/M + 1/N))
                                        = MN / (2(M+N))
1
2
3
4
5
6
7
8
9

简化近似:对于 M, N >> K 的情况(常见于深度学习 batch matmul):

算术强度 ≈ MN / (2(M+N)) ≈ min(M, N) / 2

当 M = N = 2048, K = 64:
  算术强度 = 2048 × 2048 / (2 × (2048 + 2048))
           = 4,194,304 / 8192
           ≈ 512 Flops/Byte  → Compute-Bound!
1
2
3
4
5
6

3.2 为什么大矩阵快,小矩阵慢 ​

算术强度对比 (FP32, A100):

矩阵大小          K值   算术强度    瓶颈类型    理论性能
──────────────────────────────────────────────────────
64×64             64    ~16        Memory     ~32 GFLOPS
256×256           256   ~64        Memory     ~130 GFLOPS  
512×512           512   ~128       临界附近    ~250 GFLOPS
1024×1024         1024  ~256       Compute    ~2 TFLOPS
2048×2048         2048  ~512       Compute    ~10 TFLOPS
4096×4096         4096  ~1024      Compute    ~19 TFLOPS (接近峰值)
1
2
3
4
5
6
7
8
9
10

关键规律: 当矩阵很大时,每个元素需要读取的数据量相对减少(数据复用率提高),算术强度上升,变成 Compute-Bound,性能接近峰值。

3.3 深度学习中 Batch Matmul 的特殊之处 ​

Transformer 中的 Attention 计算:

Q ∈ R^(B, H, S, D), K ∈ R^(B, H, S, D), V ∈ R^(B, H, S, D)
Attention Score = Q × K^T ∈ R^(B, H, S, S)

对于 S=512, D=64:
  - M = B×H = 12×16 = 192
  - N = S = 512
  - K = D = 64
  
  算术强度 = MN / (2(M+N)) = 192×512 / (2×704) ≈ 70 Flops/Byte
  
  → Memory-Bound 但接近临界点
  → Flash Attention 通过 IO-Aware 算法优化这个问题
1
2
3
4
5
6
7
8
9
10
11
12

第4节 Memory Access Pattern 分析 ​

4.1 Global Memory 访问模式 ​

Coalesced vs Uncoalesced 访问:

cuda
// ❌ Uncoalesced 访问 (每个线程访问不连续地址)
__global__ void bad_access(float* A, float* B, int N) {
    int tid = threadIdx.x;
    int lane_id = tid % 32;
    // 线程束中每个线程访问 A[lane_id * stride + offset]
    // stride = N/32,导致 32 个线程访问 32 个不相邻的内存行
    float val = A[lane_id * (N/32)];  // 内存访问不合并
    B[tid] = val;
}

// ✅ Coalesced 访问 (连续线程访问连续地址)
__global__ void good_access(float* A, float* B, int N) {
    int tid = threadIdx.x;
    int block_start = blockIdx.x * blockDim.x;
    // 线程 0 访问 A[block_start + 0]
    // 线程 1 访问 A[block_start + 1]
    // ... 线程 31 访问 A[block_start + 31]
    // 合并成一次内存事务!
    float val = A[block_start + tid];
    B[tid] = val * 2.0f;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

4.2 Shared Memory Bandwidth vs Global Memory ​

内存层级带宽对比 (A100):
┌─────────────────────────────────────────────┐
│  Register     → ~100,000 GB/s (理论无限)     │
│  L1 Cache     →    13,000 GB/s              │
│  L2 Cache     →     2,500 GB/s              │
│  Shared Mem   →     13,000 GB/s              │
│  HBM (Global) →     2,039 GB/s              │
└─────────────────────────────────────────────┘

Shared Memory 带宽是 Global Memory 的 ~6.4 倍
这就是 tiling 优化的核心:通过 shared memory 复用数据,减少 global memory 访问
1
2
3
4
5
6
7
8
9
10
11

4.3 矩阵乘法的分块策略 ​

Naive Matmul 的内存访问 (K=1024):
  
  每个线程计算 C[i,j] = sum_k A[i,k] * B[k,j]
  
  对于线程 (i,j):
    - 读取 A[i,*]: K 个 float (行 i 的全部元素)
    - 读取 B[*,j]: K 个 float (列 j 的全部元素)
    - 写 C[i,j]: 1 个 float
  
  问题:同一行 A[i,*] 被 N 个线程重复读取 N 次!
        同一列 B[*,j] 被 M 个线程重复读取 M 次!

使用 Tiling 后的内存访问 (Tile=32):
  
  Block (bi, bj) 处理子矩阵 C[bi:bi+32, bj:bj+32]
  
  Step 1: 把 A[bi:bi+32, :] 和 B[: , bj:bj+32] 加载到 shared memory
    - 一次读取,供 32×32 = 1024 个线程复用
    - 相比 naive,减少了 32 倍的 global memory 访问
  
  Step 2: 每个线程从 shared memory 读取 32 个 A 元素 + 32 个 B 元素
    - Shared memory 带宽 13 TB/s vs Global memory 2 TB/s
    - 加速 6.4 倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第5节 Kernel 性能瓶颈诊断流程 ​

5.1 诊断步骤 ​

┌──────────────────────────────────────────────────────────────┐
│  步骤 1: 测量实际性能                                         │
│    → 运行 benchmark,记录 GFLOPS / 延迟                       │
├──────────────────────────────────────────────────────────────┤
│  步骤 2: 计算算术强度                                         │
│    → Flops / Bytes = ? Flops/Byte                           │
├──────────────────────────────────────────────────────────────┤
│  步骤 3: 对比 Roofline                                        │
│    → Memory-Bound 还是 Compute-Bound?                       │
│    → 距离理论上限有多远?                                     │
├──────────────────────────────────────────────────────────────┤
│  步骤 4: 内存访问分析                                         │
│    → 访问模式是否 coalesced?                                │
│    → shared memory 复用率如何?                               │
│    → 使用 Nsight Compute 分析内存效率                         │
├──────────────────────────────────────────────────────────────┤
│  步骤 5: Occupancy 分析                                       │
│    → 使用 Nsight Compute 或 nvprof                          │
│    → 是寄存器压力还是 shared memory 压力?                   │
├──────────────────────────────────────────────────────────────┤
│  步骤 6: 针对性优化                                          │
│    → Memory-Bound: tiling, shared memory, 数据预取          │
│    → Compute-Bound: 减少分支,提升 ILP, 使用 Tensor Core     │
└──────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

5.2 Nsight Compute 分析命令 ​

bash
# 编译 kernel
nvcc -O3 -use_fast_norm -arch=sm_80 -lineinfo matmul.cu -o matmul

# 运行 Nsight Compute 分析
ncu --set full ./matmul

# 关键指标解读:
#  achieved_occupancy: 实际 SM 占用率
#  sm_efficiency: SM 利用率
#  warp_execution_efficiency: Warp 执行效率
#  gld_throughput: 全局加载吞吐
#  gst_throughput: 全局存储吞吐
#  l1_texture_cache_hit_rate: L1 缓存命中率
#  shared_efficiency: Shared memory 效率

# 详细内存分析
ncu --set full --metrics l1tex__t_sectors_pipe_lsu_mem_global_op_ld.sum,
                        l1tex__t_sectors_pipe_lsu_mem_global_op_st.sum,
                        sm__throughput.avg.pct_of_peak_sustained_elapsed ./matmul
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第6节 CPU vs GPU Kernel 设计差异 ​

6.1 核心设计哲学差异 ​

维度CPU KernelGPU Kernel
并行粒度线程级并行(几个到几十个线程)数千个线程并行执行
内存层级L1/L2/L3 缓存层次清晰Shared Memory + Global Memory
瓶颈通常是计算(ALU 饱和)通常是内存带宽
优化重点缓存局部性、分支预测内存合并、Occupancy、算术强度
编程模型多线程 + SIMD 向量化SIMT(单指令多线程)

6.2 CPU 上的 GEMM 优化策略 ​

c
// CPU GEMM 优化:分块 + 缓存友好
void gemm_blocked(float* C, float* A, float* B, int M, int N, int K) {
    // 分块大小:32×32 (能在 L1 cache 中放下)
    const int BLOCK = 32;
    
    // 外层循环:遍历 C 的块
    for (int i = 0; i < M; i += BLOCK) {
        for (int j = 0; j < N; j += BLOCK) {
            // 中层循环:累加 K 方向
            for (int k = 0; k < K; k += BLOCK) {
                // 内层核心:2 层嵌套的小矩阵乘
                // BLOCK×BLOCK 块能完全放入 L1 cache (32×32×4×3 ≈ 12KB < 48KB L1)
                for (int ii = i; ii < min(i+BLOCK, M); ii++) {
                    for (int jj = j; jj < min(j+BLOCK, N); jj++) {
                        float sum = 0.0f;
                        for (int kk = k; kk < min(k+BLOCK, K); kk++) {
                            sum += A[ii * K + kk] * B[kk * N + jj];
                        }
                        C[ii * N + jj] += sum;
                    }
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

CPU vs GPU 优化对比:

CPU GEMM 优化:
  - 分块大小由 L1/L2/L3 cache 大小决定(通常 32-64)
  - 关注 cache line 复用(一次加载后用多次)
  - 使用 SIMD 指令(AVX-512: 512bits = 16×float)
  - 分支预测优化(循环展开)

GPU GEMM 优化:
  - 分块大小由 shared memory 和 register 决定(通常 16-256)
  - 关注 global memory 合并访问
  - 使用 Tensor Core(16×16 矩阵乘)
  - Warp 协同调度(__syncwarp())
1
2
3
4
5
6
7
8
9
10
11

升华 ​

┌────────────────────────────────────────────────────────────────────────┐
│                     🚀 Kernel 性能优化核心原则                          │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  ① 先测后优化:永远用 Roofline Model 确定瓶颈类型再动手                  │
│     盲目优化 Memory-Bound kernel 的计算部分 = 浪费时间                  │
│                                                                        │
│  ② 算术强度是根本:提高数据复用率才是 Memory-Bound 的正解                │
│     Tiling、Shared Memory、数据布局转换都是手段                          │
│                                                                        │
│  ③ Occupancy 高 ≠ 性能好:高 Occupancy + Memory-Bound = 假忙            │
│     要同时关注 SM Efficiency 和 Memory Efficiency                      │
│                                                                        │
│  ④ 测量驱动:nsys/ncu 分析结果是事实,猜测不是                         │
│     优化前后对比,目标是什么指标提升了多少?                               │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

"AI 可查 vs 必须理解"清单 ​

必须理解(不理解就等于不会):

  • 🔴 Roofline Model 的物理意义:为什么算术强度决定瓶颈类型,临界点怎么算
  • 🔴 Memory-Bound vs Compute-Bound 的区别:分别意味着什么,如何针对性优化
  • 🔴 算术强度公式:Flops/Bytes 怎么推导,GEMM 的算术强度怎么算
  • 🔴 Tiling 的本质:为什么能提升算术强度,shared memory 复用数据的原理
  • 🔴 Occupancy 局限性:Occupancy 高但还是慢的原因分析

AI 可查(知道去哪查就行):

  • ✅ A100/H100 具体带宽和算力数据(硬件参数随时变化)
  • ✅ Nsight Compute 具体 metrics 含义(官方文档)
  • ✅ nvcc 编译选项和 PTXAS 寄存器分配策略(官方文档)
  • ✅ 特定 GPU architecture 的 shared memory 大小限制(硬件规格)
  • ✅ 具体的 cuBLAS/cuDNN kernel 性能数据(NVIDIA 官方 benchmark)

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇17. NPU 后端:脉动阵列与端侧 AI 生态 / NPU Backends, Systolic Arrays, and Edge AI Ecosystems
下一篇19. CUTLASS 与分层 GEMM 模板 / CUTLASS and Hierarchical GEMM Templates

持续记录,持续成长

Copyright © Tidenflow