《CUDA图优化与TensorRT动态形状》

2. CUDA基础回顾:GPU架构、CUDA编程模型、内存层次结构、线程组织与调度

在深入CUDA图优化与TensorRT动态形状之前,必须夯实CUDA编程的核心基础。GPU并非“更快的CPU”,而是一种大规模并行吞吐量处理器。理解其硬件架构、编程模型、内存层次与线程调度机制,是后续分析图捕获、内核融合与动态形状优化的前提。

2.1 GPU硬件架构:从SM到CUDA Core

现代NVIDIA GPU(如Ampere、Hopper、Ada Lovelace架构)的核心计算单元是流式多处理器(Streaming Multiprocessor, SM)。每个SM包含若干CUDA Core(FP32/INT32单元)、Tensor Core(用于矩阵乘加)、特殊函数单元(SFU)、共享内存(Shared Memory)、寄存器文件(Register File)以及调度器(Warp Scheduler)。

以Ampere架构的GA102为例,一个SM包含128个CUDA Core、4个Tensor Core、4个Warp Scheduler,以及最大228KB可配置的L1/共享内存。GPU由多个GPC(图形处理簇)和TPC(纹理处理簇)组成,每个TPC包含两个SM。硬件层级关系如下:

GPU (例如 A100)
└── GPC (图形处理簇) ×8
    └── TPC (纹理处理簇) ×2
        └── SM (流式多处理器) ×2
            ├── CUDA Core ×128 (FP32)
            ├── Tensor Core ×4
            ├── Warp Scheduler ×4
            ├── Register File (64K × 32-bit)
            └── Shared Memory / L1 Cache (最大228KB)

关键认知:GPU的并行度来自SM的数量和每个SM的吞吐能力。一个A100 GPU拥有108个SM,每个SM同时可驻留2048个线程,因此硬件理论上可同时承载约22万个线程。但实际并行执行的线程数受限于寄存器、共享内存等资源。

GPU硬件架构层级与SM内部结构 GPU 设备 (例如 A100, 108 SM) GPC ×8 TPC ×2 SM (流式多处理器) Warp Scheduler ×4 CUDA Core ×128 FP32 / INT32 4组 × 32 Tensor Core ×4 矩阵乘加 (FP16/BF16) Register File 64K × 32-bit Shared Memory / L1 Cache 最大 228 KB (可配置) ← 硬件线程调度单位 硬件层级要点 • 线程 (Thread): 最小执行单元,运行在CUDA Core上。 • Warp (线程束): 32个线程组成一个Warp,是硬件调度 和执行的基本单位。同一Warp内线程 执行相同的指令(SIMT模型)。 • SM 驻留: 每个SM最多驻留2048个线程(64个 Warp),由Warp Scheduler分派执行。 • 资源限制: 寄存器(64K×32bit)和共享内存(228KB) 是限制SM驻留线程数的关键资源。 ⚠ 动态形状影响: 形状变化导致线程块大小和共享内存 分配变化,影响SM资源利用率和调度。

2.2 CUDA编程模型:网格、线程块与线程

CUDA采用层次化编程模型,将并行计算抽象为三层:网格(Grid)→ 线程块(Block)→ 线程(Thread)。内核函数(Kernel)以网格形式启动,每个网格包含若干线程块,每个线程块包含若干线程。这种层次结构直接映射到硬件:线程块被调度到SM上执行,线程块内的线程通过共享内存协作。

线程和线程块可以使用dim3类型定义多维索引(最多三维),便于处理图像、体素等数据。典型的内核启动语法如下:

// 定义线程块和网格维度
dim3 blockDim(16, 16);      // 线程块:16×16 = 256线程
dim3 gridDim((width + 15) / 16, (height + 15) / 16);

// 启动内核
myKernel<<<gridDim, blockDim>>>(d_input, d_output, width, height);

// 内核内部获取索引
__global__ void myKernel(const float* in, float* out, int w, int h) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    if (x < w && y < h) {
        int idx = y * w + x;
        out[idx] = in[idx] * 2.0f;
    }
}

编程模型与硬件的映射关系:

  • 线程(Thread) → 运行在CUDA Core上,每个线程有独立的程序计数器和寄存器。
  • 线程块(Block) → 被调度到单个SM上,块内线程可通过共享内存通信,并支持同步(__syncthreads())。
  • 网格(Grid) → 由多个线程块组成,块之间无通信,可独立调度到不同SM。

2.3 内存层次结构:从全局内存到寄存器

CUDA内存模型是性能优化的核心。不同内存的容量、延迟、带宽差异巨大,合理使用内存层次是CUDA优化的关键。下表总结了主要内存类型及其特征:

内存类型 作用域 生命周期 延迟 缓存 典型用途
寄存器 单个线程 内核执行期间 ~1周期 无 局部变量、中间计算
共享内存 线程块内 内核执行期间 ~20-30周期 无(手动管理) 块内数据共享、数据复用
L1缓存 SM内 自动 ~30周期 硬件管理 全局内存访问加速
L2缓存 GPU全局 自动 ~200周期 硬件管理 跨SM数据共享
全局内存 所有线程 主机分配至释放 ~400-800周期 L1/L2 主数据存储、主机-设备通信
常量内存 所有线程 主机分配至释放 ~400周期(缓存后快) 常量缓存 只读参数、系数
纹理内存 所有线程 主机分配至释放 ~400周期(缓存后快) 纹理缓存 图像处理、空间局部性访问

动态形状对内存的影响:TensorRT动态形状意味着输入张量的尺寸在运行时才确定。这会导致:

  • 全局内存分配需要动态调整,可能引入额外拷贝开销。
  • 共享内存大小需要根据形状动态配置,影响SM驻留线程数。
  • 访问模式随形状变化,可能导致缓存命中率下降。

2.4 线程组织与调度机制

GPU的线程调度遵循SIMT(单指令多线程)模型。硬件以Warp(32线程)为单位进行调度。当一个Warp中的线程执行条件分支时,会产生分支发散(Branch Divergence),导致串行执行不同路径,降低效率。

调度流程如下:

  1. 网格启动:主机端通过<<<gridDim, blockDim>>>启动内核,GPU工作调度器将线程块分配到空闲SM。
  2. Warp划分:每个线程块被划分为多个Warp(线程ID连续32个线程为一组)。例如,256线程的块被分为8个Warp。
  3. Warp调度:SM内的Warp Scheduler每个周期选择一个就绪的Warp发射指令。当Warp等待内存访问时,调度器切换到其他Warp,隐藏延迟。
  4. 执行:Warp中的32个线程在同一周期执行相同指令,但操作不同的数据。
// 线程索引计算示例
__global__ void exampleKernel(float* data, int pitch) {
    // 线性索引
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 2D索引
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    int idx_2d = y * pitch + x;
    
    // Warp内线程ID
    int lane = threadIdx.x % 32;
    // Warp ID(块内)
    int warp_id = threadIdx.x / 32;
}

Warp调度与延迟隐藏:GPU通过大量线程 + 快速Warp切换来隐藏内存延迟。当某个Warp因访问全局内存而等待时(约400-800周期),调度器立即切换到其他就绪Warp执行计算。因此,足够的并行度(Occupancy)是性能的关键。Occupancy = 活跃Warp数 / 最大Warp数。动态形状可能导致线程块大小变化,影响Occupancy。

2.5 案例:矩阵乘法的CUDA实现与优化思路

矩阵乘法(GEMM)是深度学习中最核心的运算。以C = A × B为例,朴素实现和优化实现差异巨大。

朴素实现(直接映射):

__global__ void matmul_naive(const float* A, const float* B, float* C, int N) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    
    float sum = 0.0f;
    for (int k = 0; k < N; ++k) {
        sum += A[row * N + k] * B[k * N + col];
    }
    C[row * N + col] = sum;
}

该实现每个线程计算一个输出元素,但每次循环都访问全局内存,内存带宽成为瓶颈。优化思路包括:

  • 共享内存分块(Tiling):将A和B的块加载到共享内存,减少全局内存访问次数。
  • 寄存器缓存:每个线程计算多个输出元素,复用寄存器中的数据。
  • 避免Bank Conflict:调整共享内存访问模式,使同一Warp的线程访问不同Bank。
  • 向量化加载:使用float4一次加载4个浮点数,提高内存吞吐。

TensorRT的启示:TensorRT的自动调优(如tactic选择)本质上是在搜索最优的GEMM实现策略(包括tile大小、unroll因子、向量化宽度等)。动态形状使得每次推理时形状可能不同,TensorRT需要为每个形状选择或调整最优tactic,这正是动态形状优化的核心挑战之一。

2.6 常见误区与复盘

误区1:认为线程越多性能越好。

线程过多会导致寄存器溢出(Spill)到局部内存,反而降低性能。每个SM的寄存器文件是有限的(64K×32bit),如果每个线程使用过多寄存器,SM能驻留的线程数就会减少,Occupancy下降。

误区2:忽略Warp发散的影响。

在条件分支中,如果Warp内线程走向不同路径,所有路径都会串行执行。例如:

if (threadIdx.x % 2 == 0) {
    // 偶数线程执行
} else {
    // 奇数线程执行
}

这个Warp需要执行两次,效率减半。应尽量让分支以Warp为单位(即32的倍数)对齐。

误区3:共享内存使用不当。

共享内存容量有限(最大228KB/SM),且需要手动管理Bank Conflict。动态形状下,共享内存大小需要动态调整,若分配过大,会降低Occupancy;若过小,则无法有效复用数据。

复盘总结:CUDA性能优化的本质是最大化有效计算/内存访问比。核心手段包括:

  1. 提高数据复用(共享内存、寄存器缓存)。
  2. 保证内存访问合并(Coalesced Access),即Warp内线程访问连续内存地址。
  3. 维持足够高的Occupancy以隐藏延迟。
  4. 避免Warp发散和Bank Conflict。

这些原则在TensorRT的图优化中同样适用——图优化通过融合内核、减少内存往返、调整数据布局来达到类似目的。