Skip to content

CUDA函数类型注解:全面指南

概述

CUDA提供了几种函数类型注解,指定函数可以从哪里调用以及它们在哪里执行。理解这些注解对于有效的CUDA编程至关重要,因为它们决定了函数的执行空间和调用约束。

函数类型注解

1. __global__ - 内核函数

目的:定义在GPU上运行并从主机(CPU)调用的函数。

特点: - 在设备(GPU)上执行 - 从主机(CPU)调用 - 必须返回void - 不能从其他设备函数调用 - 异步执行(除非同步)

语法

__global__ void 内核函数(参数) {
    // GPU代码
}

2. __device__ - 设备函数

目的:定义在GPU上运行并从其他GPU函数调用的函数。

特点: - 在设备(GPU)上执行 - 仅从设备代码调用(__global__或其他__device__函数) - 可以返回任何类型 - 不能从主机代码调用 - 默认内联以提高性能

语法

__device__ 返回类型 设备函数(参数) {
    // GPU代码
    return ;
}

3. __host__ - 主机函数

目的:定义在CPU上运行的函数(默认行为)。

特点: - 在主机(CPU)上执行 - 仅从主机代码调用 - 默认注解(可以省略) - 不能从设备代码调用

语法

__host__ 返回类型 主机函数(参数) {
    // CPU代码
    return ;
}

// 等同于:
返回类型 主机函数(参数) {
    // CPU代码
    return ;
}

4. 组合注解

__host__ __device__:可以为主机和设备执行编译的函数。

特点: - 为CPU和GPU编译 - 可以从主机和设备代码调用 - 对工具函数很有用 - 代码必须与两种架构兼容

语法

__host__ __device__ 返回类型 双重函数(参数) {
    // 在CPU和GPU上都能工作的代码
    return ;
}

内存空间注解

__shared__ - 共享内存

目的:在线程块内的共享内存中声明变量。

特点: - 在块中的所有线程之间共享 - 比全局内存快得多 - 大小有限(通常每块48KB-96KB) - 生命周期与块执行匹配

__constant__ - 常量内存

目的:在常量内存中声明只读变量。

特点: - 设备代码中只读 - 缓存以加快访问速度 - 总计限制为64KB - 从主机代码初始化

__managed__ - 统一内存

目的:声明可从CPU和GPU访问且自动迁移的变量。

特点: - 在CPU和GPU之间自动迁移 - 简化内存管理 - 可能对性能有影响 - 需要计算能力3.0+

实际示例和演示

让我们通过相互构建的实际示例来探索这些注解。

示例1:基本内核和设备函数交互

该示例演示了__global__内核如何调用__device__函数:

// 用于数学计算的设备函数
__device__ float computeDistance(float x1, float y1, float x2, float y2) {
    float dx = x2 - x1;
    float dy = y2 - y1;
    return sqrtf(dx * dx + dy * dy);
}

// 使用设备函数的全局内核
__global__ void calculateDistances(float* x1, float* y1, float* x2, float* y2, 
                                 float* distances, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        distances[idx] = computeDistance(x1[idx], y1[idx], x2[idx], y2[idx]);
    }
}

示例2:主机-设备双重函数

该示例展示了可以在CPU和GPU上工作的函数:

// 可以在主机和设备上工作的函数
__host__ __device__ float lerp(float a, float b, float t) {
    return a + t * (b - a);
}

// 使用双重函数的设备函数
__device__ float interpolateValue(float* array, float index, int size) {
    int lower = (int)index;
    int upper = min(lower + 1, size - 1);
    float t = index - lower;
    return lerp(array[lower], array[upper], t);
}

// 同样使用双重函数的主机函数
__host__ void preprocessData(float* data, int size) {
    for (int i = 0; i < size - 1; i++) {
        data[i] = lerp(data[i], data[i + 1], 0.5f);
    }
}

示例3:内存空间注解

该示例演示了不同的内存空间:

// 常量内存声明
__constant__ float convolution_kernel[9];

// 使用共享内存的全局内核
__global__ void convolutionWithShared(float* input, float* output, 
                                    int width, int height) {
    // 用于基于瓦片处理的共享内存
    __shared__ float tile[18][18]; // 假设16x16线程 + 2像素边框

    int tx = threadIdx.x;
    int ty = threadIdx.y;
    int gx = blockIdx.x * blockDim.x + tx;
    int gy = blockIdx.y * blockDim.y + ty;

    // 将数据加载到共享内存中并处理边界
    // ...(实现细节)

    __syncthreads();

    // 使用常量内存卷积核执行卷积
    if (gx < width && gy < height) {
        float result = 0.0f;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                result += tile[ty + i][tx + j] * convolution_kernel[i * 3 + j];
            }
        }
        output[gy * width + gx] = result;
    }
}

最佳实践和指南

1. 函数设计原则

  • 保持设备函数简单:避免__device__函数中的复杂控制流
  • 最小化参数传递:尽可能使用引用或指针
  • 考虑内联:小型__device__函数会自动内联

2. 内存管理

  • 使用适当的内存空间
  • 共享内存用于块内共享的数据
  • 常量内存用于所有线程访问的只读数据
  • 全局内存用于大型数据集

3. 性能考虑

  • 避免分支发散:最小化内核中的if-else语句
  • 优化内存访问模式:确保合并内存访问
  • 明智地使用双重函数__host__ __device__函数可以帮助代码重用

4. 调试和开发

  • 从简单开始:先从基本的__global__内核开始,然后增加复杂性
  • 增量测试:验证每种函数类型是否正常工作
  • 使用适当的错误检查:始终检查CUDA错误代码

常见陷阱和解决方案

1. 调用限制

问题:尝试从主机代码调用__device__函数

// 错误:这会导致编译错误
__device__ int deviceFunc() { return 42; }

int main() {
    int result = deviceFunc(); // 错误!
    return 0;
}

解决方案:使用适当的调用模式

__device__ int deviceFunc() { return 42; }

__global__ void kernel(int* result) {
    *result = deviceFunc(); // 正确
}

2. 返回类型限制

问题:从__global__函数返回非void

// 错误:全局函数必须返回void
__global__ int badKernel() {
    return 42; // 错误!
}

解决方案:使用输出参数

// 正确:使用输出参数
__global__ void goodKernel(int* output) {
    *output = 42;
}

3. 内存空间混淆

问题:错误地访问不同内存空间

__shared__ float sharedData[256];

__global__ void kernel() {
    // 错误:尝试将共享内存地址传递给主机
    cudaMemcpy(hostPtr, sharedData, sizeof(float) * 256, cudaMemcpyDeviceToHost);
}

解决方案:通过全局内存复制

__global__ void kernel(float* globalOutput) {
    __shared__ float sharedData[256];

    // 在共享内存中处理数据
    // ...

    // 复制到全局内存
    if (threadIdx.x < 256) {
        globalOutput[threadIdx.x] = sharedData[threadIdx.x];
    }
}

高级主题

1. 动态并行

CUDA支持从设备代码调用内核(计算能力3.5+):

__global__ void parentKernel() {
    // 从设备启动子内核
    childKernel<<<1, 1>>>();
    cudaDeviceSynchronize(); // 同步子内核
}

__global__ void childKernel() {
    printf("来自子内核的问候!\n");
}

2. 协作组

使用协作组的现代CUDA编程:

#include <cooperative_groups.h>
using namespace cooperative_groups;

__global__ void cooperativeKernel() {
    thread_block block = this_thread_block();

    // 同步块中的所有线程
    block.sync();

    // 使用协作组操作
    int sum = reduce(block, threadIdx.x, plus<int>());
}

结论

理解CUDA函数类型注解是有效GPU编程的基础。这些注解控制:

  1. 执行位置:函数在哪里运行(CPU vs GPU)
  2. 调用上下文:从哪里可以调用函数
  3. 内存访问:函数可以访问哪些内存空间
  4. 性能特性:如何优化函数

通过掌握这些概念并遵循最佳实践,您可以编写高效、可维护的CUDA代码,充分利用GPU计算的强大功能。

相关主题

  • CUDA内存模型
  • 线程同步
  • 性能优化
  • CUDA中的错误处理
  • 高级CUDA功能

本文档提供了CUDA函数注解的全面概述。有关更高级的主题和最新功能,请参阅官方CUDA编程指南。

Share on Share on