本节摘要:本节交付全书第一个完整可跑的程序:一张批尺寸 8、通道 64 的半精度特征图,经过 3 乘 3 卷积拿到输出,并用 CPU 参考实现做数值核对。代码按 4.1 的五步流程组织,每段附讲解与预期输出。把它在新环境里跑通,等于宣布柜台验收合格——此后遇到环境问题,先回来跑它。
前节立了流程总纲,本节兑现成代码。这段示范值得你逐行敲进编辑器——它同时是环境冒烟测试、API 语法模板和后续性能实验的种子代码。
业务内容:批 8、通道 64、高宽 224 的 NHWC 半精度张量,与 128 个 3 乘 3 卷积核做同尺寸卷积,输出批 8、通道 128、高宽 224。这个尺寸故意选得"顺风顺水":通道数对齐、布局是官配 NHWC、精度是半精度——按第三章的检查单,它应当直通张量核快车道。程序分四段:初始化、登记、询问与执行、核对。
#include <cudnn.h> #include <cuda_runtime.h> #include <cstdio> #include <cstdlib> #include <cmath> #define CHECK(x) do { \ cudnnStatus_t s = (x); \ if (s != CUDNN_STATUS_SUCCESS) { \ printf("cuDNN error at %d: %s\n", __LINE__, cudnnGetErrorString(s)); \ exit(1); \ } \ } while (0) #define CUDACHECK(x) do { \ cudaError_t e = (x); \ if (e != cudaSuccess) { printf("CUDA error at %d: %s\n", __LINE__, cudaGetErrorString(e)); exit(1); } \ } while (0) int main() { const int N = 8, C = 64, H = 224, W = 224, K = 128; void *d_x, *d_w, *d_y; // 设备侧三块显存:输入、卷积核、输出 cudnnHandle_t handle; CHECK(cudnnCreate(&handle)); // 第一步:开窗口 return 0; }
段末提醒一件重要的事:输入、卷积核、输出三块设备显存要用 CUDA 的分配接口准备,cuDNN 不替你管显存(第一章的职责边界)。完整写法如下,随后立即进入登记段:
// 按半精度两字节分配三块设备显存 CUDACHECK(cudaMalloc(&d_x, (size_t)N * C * H * W * 2)); CUDACHECK(cudaMalloc(&d_w, (size_t)K * C * 3 * 3 * 2)); CUDACHECK(cudaMalloc(&d_y, (size_t)N * K * H * W * 2)); // 主机侧随机初始化输入与卷积核后拷入设备(数据准备,略去填充细节)
cudnnTensorDescriptor_t xDesc, yDesc; cudnnFilterDescriptor_t wDesc; cudnnConvolutionDescriptor_t convDesc; CHECK(cudnnCreateTensorDescriptor(&xDesc)); CHECK(cudnnSetTensor4dDescriptor(xDesc, CUDNN_TENSOR_NHWC, CUDNN_DATA_HALF, N, C, H, W)); // 输入:NHWC 半精度 CHECK(cudnnCreateTensorDescriptor(&yDesc)); CHECK(cudnnSetTensor4dDescriptor(yDesc, CUDNN_TENSOR_NHWC, CUDNN_DATA_HALF, N, K, H, W)); // 输出:通道换成 K CHECK(cudnnCreateFilterDescriptor(&wDesc)); CHECK(cudnnSetFilter4dDescriptor(wDesc, CUDNN_DATA_HALF, CUDNN_TENSOR_NHWC, K, C, 3, 3)); // 卷积核:K 个 3x3 CHECK(cudnnCreateConvolutionDescriptor(&convDesc)); CHECK(cudnnSetConvolution2dDescriptor(convDesc, 1, 1, 1, 1, 1, 1, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT)); // 填充1步长1,累加单精度 CHECK(cudnnSetConvolutionMathType(convDesc, CUDNN_TENSOR_OP_MATH)); // 走张量核
四个易错点在真实编码里的高发位置:输入输出描述符的通道数别忘了换(C 换 K);卷积核描述符的维度顺序是"输出通道在前";算子描述符的累加精度选单精度(张量核的高精度累加惯例);数学模式显式声明,别指望默认值帮你开闸。
cudnnConvolutionFwdAlgoPerf_t perf; int returned = 0; CHECK(cudnnGetConvolutionForwardAlgorithm_v7(handle, xDesc, wDesc, convDesc, yDesc, 1, &returned, &perf)); printf("heuristic algo=%d, ws=%zu\n", perf.algo, perf.memory); // 问方案 void* ws = nullptr; if (perf.memory > 0) CUDACHECK(cudaMalloc(&ws, perf.memory)); // 按需求租地 float alpha = 1.0f, beta = 0.0f; CHECK(cudnnConvolutionForward(handle, &alpha, xDesc, d_x, wDesc, d_w, convDesc, perf.algo, ws, perf.memory, &beta, yDesc, d_y)); // 办业务 cudaDeviceSynchronize(); // 同步:等活干完
alpha 与 beta 是两个容易被略过的缩放系数:beta 为零意味着输出按覆盖写入,非零则按累加写入——某些多分支结构(残差连接)靠它省一次加法。执行调用返回后必须同步,理由前节讲过。
// 把结果拷回主机,与 CPU 参考实现比对(参考实现此处省略为函数调用) // 参考值:ref[n][k][h][w] = sum_{c,i,j} x[n][c][h+i-1][w+j-1] * w[k][c][i][j] std::vector<__half> y_host((size_t)N * K * H * W); CUDACHECK(cudaMemcpy(y_host.data(), d_y, y_host.size() * 2, cudaMemcpyDeviceToHost)); double maxErr = 0.0; // ……逐点与参考值比较,半精度场景容差建议在百分位量级…… printf("max relative error: %g\n", maxErr); // 预期输出:1e-3 量级以内
半精度的核对容差要放宽到百分位量级——低精度的舍入误差天然存在,比对的是"量级一致、趋势一致",不是逐位相等。第一次跑通时,屏幕上应当看到三样东西:启发式算法编号、工作空间字节数(本例通常为非零或零都有可能,取决于库的选择)、以及一个很小的最大相对误差。三样齐了,柜台验收通过。
💡 把这个程序留作资产:新机器上跑一遍是环境冒烟测试;把尺寸与循环改成参数化,它就变成第六章基准测试的雏形;往里加一层 ReLU 描述,你就提前摸到了第五章融合的门口。一份代码三用,值得好好收着。