3CUDA 并行程序设计入门(第2版)

线程与执行模型

SIMT 架构详解

WarpSIMT线程同步分支分歧

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章 线程与执行模型 - SIMT 架构详解(Threads and Execution Model)

一、导读

1.1 本章学习目标

通过本章的学习,你将能够:

深入理解 SIMT(单指令多线程)执行模型的工作原理

掌握 warp 的概念及其在 GPU 执行中的作用

理解线程发散(Thread Divergence)的成因和性能影响

掌握多维线程层次结构的设计方法

学会选择合适的线程块大小以优化性能

理解 GPU 调度器如何管理大量并发线程

掌握 occupancy(占用率)的概念及其对性能的影响

能够分析和优化内核函数的并行效率

1.2 为什么本章如此重要

线程是 CUDA 编程的核心抽象。理解线程如何被组织、调度和执行,是编写高效 CUDA 程序的关键。许多性能问题——如线程发散、低占用率、负载不均衡——都源于对线程模型理解不足。

SIMT 架构是 GPU 高性能的秘密所在。它允许数千个线程同时执行,但又保持了编程模型的简洁性。然而,这种架构也带来了一些独特的挑战,特别是当线程的执行路径出现分歧时。

本章将帮助你建立对 GPU 执行模型的直觉,使你能够预测代码的性能特征,并做出明智的设计决策。无论你是要优化现有代码还是设计新的算法,这些知识都是不可或缺的。

1.3 核心问题引导

在深入学习之前,请思考以下问题:

什么是 warp?为什么 GPU 以 warp 为单位调度线程?

线程发散是什么?它为什么会降低性能?

如何选择最优的线程块大小?

什么是 occupancy?高 occupancy 一定意味着高性能吗?

GPU 如何在数千个线程之间快速切换?

线程块之间的执行顺序有保证吗?

为什么线程块的大小通常是 2 的幂次方?

如何避免或减轻线程发散的影响?


二、核心概念详解

3.1 SIMT 执行模型

SIMT(Single Instruction, Multiple Threads)是 NVIDIA GPU 的核心执行模型。它结合了 SIMD 的高吞吐量和多线程的灵活性。

SIMT 的基本原理

在 SIMT 模型中:

  • 一组线程(称为 warp)同时执行相同的指令
  • 每个线程有自己的程序计数器和寄存器状态
  • 每个线程可以独立地处理不同的数据

这与传统的 SIMD 不同:在 SIMD 中,所有处理单元必须严格同步执行;而在 SIMT 中,线程可以有独立的控制流,尽管这会带来性能开销。

Warp:执行的基本单位

Warp 是 GPU 调度和执行的基本单位,包含 32 个线程。关键特性包括:

  • 固定大小:每个 warp 始终包含 32 个线程,不可配置
  • 同步执行:warp 内的所有线程同时执行同一条指令
  • 共享程序计数器:warp 内所有线程共享一个程序计数器
  • 硬件调度:warp 是 GPU 调度器分配给执行单元的最小单位
Warp 结构示意:
┌─────────────────────────────────────────┐
│              Warp (32 threads)           │
├──────┬──────┬──────┬──────┬─────┬──────┤
│Thread│Thread│Thread│Thread│ ... │Thread│
│  0   │  1   │  2   │  3   │     │  31  │
└──────┴──────┴──────┴──────┴─────┴──────┘
         All execute the same instruction

从线程到 Warp 的映射

线程块中的线程按顺序分组为 warps:

  • 线程 0-31 组成 warp 0
  • 线程 32-63 组成 warp 1
  • 依此类推

如果线程块的线程数不是 32 的倍数,最后一个 warp 会有未使用的线程槽位,造成资源浪费。

3.2 Warp 调度与执行

Warp 调度器

现代 GPU 包含多个 warp 调度器,每个流多处理器(SM)通常有 2-4 个调度器。调度器的职责是:

  • 选择就绪的 warp 进行执行
  • 将指令分派给执行单元
  • 处理内存访问请求
  • 管理 warp 的状态转换

指令流水线

GPU 采用深度流水线来隐藏指令延迟:

指令执行阶段:
Fetch → Decode → Issue → Execute → Memory → Writeback

当一个 warp 等待内存访问时,调度器可以切换到另一个就绪的 warp,从而保持执行单元的利用率。

延迟隐藏

GPU 通过上下文切换来隐藏延迟:

  • 内存访问可能需要数百个周期
  • 在此期间,调度器执行其他 warp 的指令
  • 当内存访问完成时,原 warp 恢复执行

这种机制要求有足够的活跃 warp 来填充空闲时间。这就是为什么高 occupancy 有助于性能的原因。

3.3 线程发散(Thread Divergence)

什么是线程发散?

当 warp 内的线程由于条件分支而执行不同的代码路径时,就发生了线程发散。例如:

cpp
__global__ void divergentKernel(float *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n / 2) {
        data[idx] = data[idx] * 2.0f;  // 路径 A
    } else {
        data[idx] = data[idx] + 1.0f;  // 路径 B
    }
}

发散的性能代价

由于 warp 内的线程必须同步执行,当发生发散时:

GPU 串行执行每个分支路径

执行路径 A 时,路径 B 的线程处于空闲状态

执行路径 B 时,路径 A 的线程处于空闲状态

总执行时间是各分支时间之和

在最坏情况下(一半线程走路径 A,一半走路径 B),性能可能下降近 50%。

硬件如何处理发散

GPU 使用掩码(mask)来管理发散:

  • 每个 warp 有一个活动线程掩码
  • 执行某个分支时,只有该分支的线程被标记为活动
  • 非活动线程不产生结果,但消耗时钟周期

避免发散的策略

重新组织数据:使条件相关的元素聚集在一起

使用算术代替分支:用三元运算符或数学技巧消除 if-else

平衡分支:确保各分支的计算量相近

接受必要发散:某些算法不可避免地需要分支,关键是最小化其影响

3.4 线程层次结构设计

一维 vs 多维线程配置

CUDA 支持最多三维的线程块和网格:

cpp
// 一维
dim3 block(256);
dim3 grid(n / 256);

// 二维
dim3 block(16, 16);
dim3 grid(width/16, height/16);

// 三维
dim3 block(8, 8, 8);
dim3 grid(nx/8, ny/8, nz/8);

多维配置的主要优势是:

  • 更自然地映射到多维数据(图像、体积数据)
  • 简化索引计算
  • 提高代码可读性

线程索引计算

对于多维配置,全局索引的计算方式:

cpp
// 二维
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int idx = row * width + col;

// 三维
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int z = blockIdx.z * blockDim.z + threadIdx.z;
int idx = z * width * height + y * width + x;

选择线程块大小的考虑因素

硬件限制:

- 最大线程数每块(通常 1024)

- 最大维度大小(通常 1024×1024×64)

资源约束:

- 每个线程的寄存器用量

- 共享内存需求

- SM 的资源上限

性能优化:

- 线程块大小应为 warp 大小(32)的倍数

- 考虑内存访问模式(合并访问)

- 平衡 occupancy 和资源使用

3.5 Occupancy(占用率)

定义

Occupancy 是指 SM 上活跃的 warp 数量与硬件支持的最大 warp 数量的比值:

Occupancy = 活跃 Warp 数 / 最大 Warp 数

影响 Occupancy 的因素

线程块大小:更大的线程块可以填满更多 warp 槽位

寄存器用量:每个线程使用的寄存器越多,能同时存在的线程越少

共享内存用量:每个块使用的共享内存越多,能同时驻留的块越少

Occupancy 与性能的关系

高 occupancy 有助于:

  • 更好地隐藏内存延迟
  • 提高指令吞吐量
  • 容忍分支发散

但高 occupancy 不一定意味着最佳性能:

  • 有时减少 occupancy 可以增加每个线程可用的资源
  • 更好的内存访问模式可能比高 occupancy 更重要
  • 某些算法不需要高 occupancy 就能达到良好性能

查询 Occupancy

cpp
int minGridSize;
int blockSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, 
                                    myKernel, 0, 0);

这个 API 可以帮助确定最优的线程块大小。

3.6 GPU 硬件架构概览

流多处理器(SM)

SM 是 GPU 的核心计算单元,包含:

  • CUDA 核心(流处理器)
  • Warp 调度器
  • 寄存器文件
  • 共享内存/L1 缓存
  • 特殊功能单元(SFU)

内存层次结构

GPU 内存层次:
┌─────────────────────────────────────┐
│         Global Memory (GDDR/HBM)    │  ← 大容量,高带宽,高延迟
├─────────────────────────────────────┤
│      L2 Cache (shared across SMs)   │
├──────────┬──────────┬───────────────┤
│  SM 0    │  SM 1    │   ... SM N    │
│ ┌──────┐ │ ┌──────┐ │               │
│ │L1/   │ │ │L1/   │ │               │
│ │Shared│ │ │Shared│ │               │
│ └──────┘ │ └──────┘ │               │
│ ┌──────┐ │ ┌──────┐ │               │
│ │Regs  │ │ │Regs  │ │               │
│ └──────┘ │ └──────┘ │               │
└──────────┴──────────┴───────────────┘

执行资源分配

每个 SM 可以同时管理多个线程块,但受限于:

  • 最大线程数(通常 2048)
  • 最大线程块数(通常 32)
  • 寄存器总数
  • 共享内存总量

三、程序员的视角

3.1 在实际项目中的应用

图像处理

二维线程配置天然适合图像处理:

cpp
dim3 block(16, 16);
dim3 grid((width+15)/16, (height+15)/16);

每个线程处理一个像素,线程索引直接对应像素坐标。

矩阵运算

矩阵乘法可以使用二维或三维线程配置:

cpp
dim3 block(16, 16);
dim3 grid((N+15)/16, (N+15)/16);

粒子系统

一维线程配置适合粒子更新:

cpp
dim3 block(256);
dim3 grid((numParticles+255)/256);

3.2 常见陷阱与误区

误区一:线程块越大越好

事实:过大的线程块可能导致资源耗尽,反而降低 occupancy。需要根据寄存器和共享内存用量平衡。

误区二:必须填满所有 warp 槽位

事实:如果数据规模不是 32 的倍数,最后一个 warp 会有空闲线程。这是正常的,只需确保边界检查正确。

陷阱:忽视线程发散

复杂的条件分支可能导致严重的性能下降。尽量重构代码以减少发散。

陷阱:假设线程块执行顺序

线程块可以以任何顺序执行,甚至可能并行执行。不要依赖线程块之间的执行顺序。

3.3 与其他层级的联系

本章的线程模型概念与后续章节密切相关:

  • 第4章:内存访问模式与线程索引的关系
  • 第5章:线程同步的范围和机制
  • 第6章:流与并发执行
  • 第8章:基于线程模型的性能优化

四、代码实战

4.1 演示线程发散

cpp
#include <stdio.h>
#include <cuda_runtime.h>

// 存在线程发散的内核
__global__ void divergentKernel(float *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        if (data[idx] > 0.5f) {
            data[idx] = data[idx] * 2.0f;
        } else {
            data[idx] = data[idx] + 1.0f;
        }
    }
}

// 重构后减少发散的内核
__global__ void optimizedKernel(float *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        float val = data[idx];
        float result = (val > 0.5f) ? (val * 2.0f) : (val + 1.0f);
        data[idx] = result;
    }
}

#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            printf("CUDA error: %s\n", cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

int main() {
    int n = 1 << 20;
    size_t size = n * sizeof(float);
    
    float *h_data = (float *)malloc(size);
    for (int i = 0; i < n; i++) {
        h_data[i] = (float)rand() / RAND_MAX;
    }
    
    float *d_data;
    CUDA_CHECK(cudaMalloc((void **)&d_data, size));
    CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice));
    
    int threadsPerBlock = 256;
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    
    // 测试发散版本
    cudaEvent_t start, stop;
    CUDA_CHECK(cudaEventCreate(&start));
    CUDA_CHECK(cudaEventCreate(&stop));
    
    CUDA_CHECK(cudaEventRecord(start));
    divergentKernel<<<blocksPerGrid, threadsPerBlock>>>(d_data, n);
    CUDA_CHECK(cudaEventRecord(stop));
    CUDA_CHECK(cudaEventSynchronize(stop));
    
    float divergentTime = 0;
    CUDA_CHECK(cudaEventElapsedTime(&divergentTime, start, stop));
    printf("Divergent kernel time: %.3f ms\n", divergentTime);
    
    // 重置数据
    CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice));
    
    // 测试优化版本
    CUDA_CHECK(cudaEventRecord(start));
    optimizedKernel<<<blocksPerGrid, threadsPerBlock>>>(d_data, n);
    CUDA_CHECK(cudaEventRecord(stop));
    CUDA_CHECK(cudaEventSynchronize(stop));
    
    float optimizedTime = 0;
    CUDA_CHECK(cudaEventElapsedTime(&optimizedTime, start, stop));
    printf("Optimized kernel time: %.3f ms\n", optimizedTime);
    
    printf("Speedup: %.2fx\n", divergentTime / optimizedTime);
    
    CUDA_CHECK(cudaFree(d_data));
    CUDA_CHECK(cudaEventDestroy(start));
    CUDA_CHECK(cudaEventDestroy(stop));
    free(h_data);
    
    return 0;
}

4.2 探索不同线程块大小的性能

cpp
#include <stdio.h>
#include <cuda_runtime.h>

__global__ void matrixMulKernel(float *C, const float *A, const float *B, int N) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    
    if (row < N && col < N) {
        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;
    }
}

#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            printf("CUDA error: %s\n", cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

void testBlockSize(int blockSize, int N) {
    size_t size = N * N * sizeof(float);
    
    float *h_A = (float *)malloc(size);
    float *h_B = (float *)malloc(size);
    float *h_C = (float *)malloc(size);
    
    for (int i = 0; i < N * N; i++) {
        h_A[i] = 1.0f;
        h_B[i] = 1.0f;
    }
    
    float *d_A, *d_B, *d_C;
    CUDA_CHECK(cudaMalloc((void **)&d_A, size));
    CUDA_CHECK(cudaMalloc((void **)&d_B, size));
    CUDA_CHECK(cudaMalloc((void **)&d_C, size));
    
    CUDA_CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice));
    
    dim3 block(blockSize, blockSize);
    dim3 grid((N + blockSize - 1) / blockSize, 
              (N + blockSize - 1) / blockSize);
    
    cudaEvent_t start, stop;
    CUDA_CHECK(cudaEventCreate(&start));
    CUDA_CHECK(cudaEventCreate(&stop));
    
    CUDA_CHECK(cudaEventRecord(start));
    matrixMulKernel<<<grid, block>>>(d_C, d_A, d_B, N);
    CUDA_CHECK(cudaEventRecord(stop));
    CUDA_CHECK(cudaEventSynchronize(stop));
    
    float elapsed = 0;
    CUDA_CHECK(cudaEventElapsedTime(&elapsed, start, stop));
    
    printf("Block size %dx%d (%d threads): %.3f ms\n", 
           blockSize, blockSize, blockSize * blockSize, elapsed);
    
    CUDA_CHECK(cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost));
    
    CUDA_CHECK(cudaFree(d_A));
    CUDA_CHECK(cudaFree(d_B));
    CUDA_CHECK(cudaFree(d_C));
    CUDA_CHECK(cudaEventDestroy(start));
    CUDA_CHECK(cudaEventDestroy(stop));
    free(h_A);
    free(h_B);
    free(h_C);
}

int main() {
    int N = 512; // 512x512 矩阵
    
    printf("Testing different block sizes for %dx%d matrix multiplication:\n", N, N);
    
    int blockSizes[] = {4, 8, 16, 32};
    for (int i = 0; i < 4; i++) {
        testBlockSize(blockSizes[i], N);
    }
    
    return 0;
}

4.3 查询设备 Occupancy

cpp
#include <stdio.h>
#include <cuda_runtime.h>

__global__ void sampleKernel(float *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        data[idx] = data[idx] * 2.0f;
    }
}

int main() {
    int device;
    cudaGetDevice(&device);
    
    cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop, device);
    
    printf("Device: %s\n", prop.name);
    printf("Compute Capability: %d.%d\n", prop.major, prop.minor);
    printf("Max threads per block: %d\n", prop.maxThreadsPerBlock);
    printf("Max threads per SM: %d\n", prop.maxThreadsPerMultiProcessor);
    printf("Max blocks per SM: %d\n", prop.maxBlocksPerMultiProcessor);
    printf("Number of SMs: %d\n\n", prop.multiProcessorCount);
    
    // 查询不同线程块大小的 occupancy
    printf("Occupancy analysis:\n");
    printf("%-12s %-15s %-15s\n", "Block Size", "Blocks/SM", "Occupancy");
    printf("--------------------------------------------\n");
    
    int blockSizes[] = {32, 64, 128, 256, 512, 1024};
    
    for (int i = 0; i < 6; i++) {
        int blockSize = blockSizes[i];
        
        int minGridSize, actualBlockSize;
        cudaOccupancyMaxPotentialBlockSize(&minGridSize, &actualBlockSize, 
                                            sampleKernel, 0, 0);
        
        int maxBlocksPerSM;
        cudaOccupancyMaxActiveBlocksPerMultiprocessor(&maxBlocksPerSM, 
                                                       sampleKernel, 
                                                       blockSize, 0);
        
        int maxWarpsPerSM = prop.maxThreadsPerMultiProcessor / 32;
        int activeWarps = maxBlocksPerSM * blockSize / 32;
        float occupancy = (float)activeWarps / maxWarpsPerSM * 100.0f;
        
        printf("%-12d %-15d %.1f%%\n", blockSize, maxBlocksPerSM, occupancy);
    }
    
    return 0;
}

4.4 三维线程配置示例

cpp
#include <stdio.h>
#include <cuda_runtime.h>

// 处理三维体积数据的内核
__global__ void volumeProcessKernel(float *volume, int nx, int ny, int nz) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    int z = blockIdx.z * blockDim.z + threadIdx.z;
    
    if (x < nx && y < ny && z < nz) {
        int idx = z * ny * nx + y * nx + x;
        // 简单的平滑操作
        volume[idx] = volume[idx] * 0.5f + 0.25f;
    }
}

#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            printf("CUDA error: %s\n", cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

int main() {
    int nx = 64, ny = 64, nz = 64;
    size_t size = nx * ny * nz * sizeof(float);
    
    float *h_volume = (float *)malloc(size);
    for (int i = 0; i < nx * ny * nz; i++) {
        h_volume[i] = (float)i / (nx * ny * nz);
    }
    
    float *d_volume;
    CUDA_CHECK(cudaMalloc((void **)&d_volume, size));
    CUDA_CHECK(cudaMemcpy(d_volume, h_volume, size, cudaMemcpyHostToDevice));
    
    // 三维线程配置
    dim3 blockSize(8, 8, 4);
    dim3 gridSize((nx + 7) / 8, (ny + 7) / 8, (nz + 3) / 4);
    
    printf("Grid size: %d x %d x %d\n", gridSize.x, gridSize.y, gridSize.z);
    printf("Block size: %d x %d x %d\n", blockSize.x, blockSize.y, blockSize.z);
    printf("Total threads: %d\n", 
           gridSize.x * gridSize.y * gridSize.z * 
           blockSize.x * blockSize.y * blockSize.z);
    
    volumeProcessKernel<<<gridSize, blockSize>>>(d_volume, nx, ny, nz);
    CUDA_CHECK(cudaDeviceSynchronize());
    
    CUDA_CHECK(cudaMemcpy(h_volume, d_volume, size, cudaMemcpyDeviceToHost));
    
    printf("Volume processing completed.\n");
    
    CUDA_CHECK(cudaFree(d_volume));
    free(h_volume);
    
    return 0;
}

五、深入思考

5.1 历史背景

SIMT 架构的设计受到了多种早期并行计算模型的影响。向量超级计算机(如 Cray)展示了 SIMD 的威力,但也暴露了其缺乏灵活性的问题。图形着色器的发展证明了可编程并行处理的可行性。

NVIDIA 的创新在于将线程级的灵活性与 SIMD 的效率结合起来。warp 作为执行单位的设计是一个巧妙的折中:足够大以实现高效率,足够小以保持灵活性。

5.2 前沿方向

当前的研究正在探索更细粒度的并行模型:

  • Independent Thread Scheduling:Volta 架构引入的新特性,允许 warp 内的线程更独立地执行
  • Cooperative Groups:提供比线程块更灵活的线程分组机制
  • Dynamic Parallelism:允许内核函数启动子内核

5.3 延伸阅读

  • NVIDIA PTX ISA Documentation - 了解底层执行模型
  • "Programming Massively Parallel Processors" Chapter 4
  • NVIDIA Developer Blog 上的 warp 相关文章

六、实验笔记

实验 1:测量线程发散的影响

目标:量化线程发散对性能的影响。

步骤:

创建一个内核,其中包含根据数据值的条件分支

准备两组数据:一组导致严重发散,一组最小化发散

比较两种情况下的执行时间

使用 nsight compute 分析 warp 执行效率

预期结果:发散严重的情况执行时间明显更长。

实验 2:探索 Occupancy 与性能的关系

目标:验证高 occupancy 是否总是带来高性能。

步骤:

使用不同的线程块大小运行相同的内核

记录每种配置的 occupancy 和执行时间

绘制 occupancy 与执行时间的关系图

分析是否存在最优 occupancy

思考题:为什么有时较低的 occupancy 也能获得良好性能?


七、本章小结

本章深入探讨了 CUDA 的线程与执行模型。我们学习了 SIMT 架构的核心概念,包括 warp 的组织方式、调度机制和线程发散的影响。

关键要点包括:

  • Warp 是 GPU 执行的基本单位,包含 32 个线程
  • 线程发散会导致性能下降,因为 warp 必须串行执行不同分支
  • Occupancy 衡量 SM 上活跃 warp 的比例,高 occupancy 有助于隐藏延迟
  • 线程块大小的选择需要平衡资源使用和并行度
  • 多维线程配置可以更自然地映射到多维数据

理解这些概念对于编写高效的 CUDA 程序至关重要。在下一章中,我们将探讨 GPU 内存层次结构,这是另一个影响性能的关键因素。


八、习题

解释 SIMT 与 SIMD 的区别。

为什么 warp 的大小固定为 32 个线程?

描述线程发散的发生过程及其性能影响。

如果一个线程块有 256 个线程,它包含多少个完整的 warp?

什么是 occupancy?列举影响 occupancy 的三个因素。

编写一个内核,展示如何通过重新组织数据来减少线程发散。

解释为什么线程块的执行顺序是不确定的。

对于一个 1024×1024 的图像处理任务,设计合适的线程配置。

查询你当前使用的 GPU 的最大 warp 数和 SM 数量。

分析以下代码是否存在线程发散,如果有,如何优化?

cpp
if (threadIdx.x % 2 == 0) {
    data[idx] = a[idx] + b[idx];
} else {
    data[idx] = a[idx] - b[idx];
}
3/8线程与执
0%