2. NVLink带宽基准测试
任务目标
编写一个CUDA程序,通过NVLink点对点通信在两个GPU之间传输数据,测量实际带宽,并与PCIe带宽进行对比。程序需支持选择使用NVLink或PCIe路径,并输出可复现的带宽结果。
环境依赖
- NVIDIA GPU(双卡,支持NVLink,如A100/A800/H800等)
- CUDA Toolkit ≥ 11.0(建议12.x)
- Linux x86_64(或WSL2)
- GPU驱动 ≥ 450.80.02
实现原理与代码
使用CUDA的cudaMemcpyPeerAsync实现设备间直接拷贝(NVLink路径),使用cudaMemcpy经主机中转实现PCIe路径对比。通过cudaDeviceEnablePeerAccess启用UVA(统一虚拟地址)以支持点对点访问。带宽计算基于传输字节数除以实测耗时。
// nvlink_bw_test.cu
#include <cstdio>
#include <cstdlib>
#include <chrono>
#include <cuda_runtime.h>
#define CHECK_CUDA(call) do { \
cudaError_t err = (call); \
if (err != cudaSuccess) { \
printf("CUDA error at %s:%d: %s\n", __FILE__, __LINE__, \
cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while(0)
// 传输大小(字节):从1MB到1GB,按2倍递增
const size_t SIZES[] = {
1ULL<<20, 2ULL<<20, 4ULL<<20, 8ULL<<20,
16ULL<<20, 32ULL<<20, 64ULL<<20,
128ULL<<20, 256ULL<<20, 512ULL<<20,
1ULL<<30
};
const int NUM_SIZES = sizeof(SIZES) / sizeof(SIZES[0]);
// 测量一次拷贝耗时(毫秒)
float measure_transfer(void* dst, int dst_dev, void* src, int src_dev,
size_t bytes, cudaStream_t stream) {
cudaEvent_t start, stop;
CHECK_CUDA(cudaEventCreate(&start));
CHECK_CUDA(cudaEventCreate(&stop));
CHECK_CUDA(cudaEventRecord(start, stream));
CHECK_CUDA(cudaMemcpyPeerAsync(dst, dst_dev, src, src_dev, bytes, stream));
CHECK_CUDA(cudaEventRecord(stop, stream));
CHECK_CUDA(cudaStreamSynchronize(stream));
float ms = 0.0f;
CHECK_CUDA(cudaEventElapsedTime(&ms, start, stop));
CHECK_CUDA(cudaEventDestroy(start));
CHECK_CUDA(cudaEventDestroy(stop));
return ms;
}
// 通过主机中转(PCIe路径)
float measure_pcie_transfer(void* dst, void* src, size_t bytes,
cudaStream_t stream) {
void* host_buf = nullptr;
CHECK_CUDA(cudaMallocHost(&host_buf, bytes));
cudaEvent_t start, stop;
CHECK_CUDA(cudaEventCreate(&start));
CHECK_CUDA(cudaEventCreate(&stop));
CHECK_CUDA(cudaEventRecord(start, stream));
CHECK_CUDA(cudaMemcpyAsync(host_buf, src, bytes,
cudaMemcpyDeviceToHost, stream));
CHECK_CUDA(cudaMemcpyAsync(dst, host_buf, bytes,
cudaMemcpyHostToDevice, stream));
CHECK_CUDA(cudaEventRecord(stop, stream));
CHECK_CUDA(cudaStreamSynchronize(stream));
float ms = 0.0f;
CHECK_CUDA(cudaEventElapsedTime(&ms, start, stop));
CHECK_CUDA(cudaEventDestroy(start));
CHECK_CUDA(cudaEventDestroy(stop));
CHECK_CUDA(cudaFreeHost(host_buf));
return ms;
}
int main() {
int dev_count = 0;
CHECK_CUDA(cudaGetDeviceCount(&dev_count));
if (dev_count < 2) {
printf("需要至少2块GPU,当前检测到%d块\n", dev_count);
return EXIT_FAILURE;
}
int dev0 = 0, dev1 = 1;
CHECK_CUDA(cudaSetDevice(dev0));
CHECK_CUDA(cudaDeviceEnablePeerAccess(dev1, 0));
CHECK_CUDA(cudaSetDevice(dev1));
CHECK_CUDA(cudaDeviceEnablePeerAccess(dev0, 0));
// 检查是否支持点对点(NVLink)
int can_access = 0;
CHECK_CUDA(cudaDeviceCanAccessPeer(&can_access, dev0, dev1));
if (!can_access) {
printf("警告:GPU %d 和 %d 之间不支持点对点访问,"
"NVLink路径将不可用\n", dev0, dev1);
}
cudaStream_t stream;
CHECK_CUDA(cudaStreamCreate(&stream));
printf("%-12s %-15s %-15s %-15s\n",
"大小(MB)", "NVLink(GB/s)", "PCIe(GB/s)", "加速比");
printf("------------------------------------------------------------\n");
for (int i = 0; i < NUM_SIZES; i++) {
size_t bytes = SIZES[i];
double mb = (double)bytes / (1024.0 * 1024.0);
// 分配设备内存
void *d_src = nullptr, *d_dst = nullptr;
CHECK_CUDA(cudaSetDevice(dev0));
CHECK_CUDA(cudaMalloc(&d_src, bytes));
CHECK_CUDA(cudaMemset(d_src, 0xAB, bytes));
CHECK_CUDA(cudaSetDevice(dev1));
CHECK_CUDA(cudaMalloc(&d_dst, bytes));
// NVLink路径(点对点)
float nvlink_ms = 0.0f;
if (can_access) {
nvlink_ms = measure_transfer(d_dst, dev1, d_src, dev0,
bytes, stream);
}
// PCIe路径(经主机中转)
float pcie_ms = measure_pcie_transfer(d_dst, d_src, bytes, stream);
double nvlink_gbps = 0.0, pcie_gbps = 0.0;
if (nvlink_ms > 0.0f) {
nvlink_gbps = (double)bytes / (nvlink_ms / 1000.0) / 1e9;
}
if (pcie_ms > 0.0f) {
pcie_gbps = (double)bytes / (pcie_ms / 1000.0) / 1e9;
}
double speedup = (pcie_ms > 0.0f && nvlink_ms > 0.0f) ?
pcie_ms / nvlink_ms : 0.0;
printf("%-12.1f %-15.2f %-15.2f %-15.2f\n",
mb, nvlink_gbps, pcie_gbps, speedup);
CHECK_CUDA(cudaFree(d_src));
CHECK_CUDA(cudaFree(d_dst));
}
CHECK_CUDA(cudaStreamDestroy(stream));
CHECK_CUDA(cudaSetDevice(dev0));
CHECK_CUDA(cudaDeviceDisablePeerAccess(dev1));
CHECK_CUDA(cudaSetDevice(dev1));
CHECK_CUDA(cudaDeviceDisablePeerAccess(dev0));
return EXIT_SUCCESS;
}
编译与运行
nvcc -arch=sm_80 -O3 nvlink_bw_test.cu -o nvlink_bw_test
./nvlink_bw_test
注意:-arch需根据实际GPU架构调整(A100为sm_80,H100为sm_90)。
验证方法与判定标准
- 功能验证:程序正常输出表格,无CUDA错误;NVLink列在支持点对点的GPU上应显示非零带宽值。
- 带宽合理性:NVLink 3.0(A100)理论带宽为600GB/s(双向),实测单向通常为250~300GB/s;NVLink 4.0(H100)理论900GB/s,实测约400~450GB/s。PCIe 4.0 x16单向理论约32GB/s,实测约25GB/s。
- 加速比:NVLink带宽应显著高于PCIe(通常5~10倍),加速比列应明显大于1。
- 可重复性:多次运行结果波动应在±5%以内(排除系统负载干扰)。
常见问题
- 点对点访问失败:确认GPU拓扑为NVLink直连(运行
nvidia-smi topo -m查看),且驱动支持UVA。 - 带宽偏低:检查是否使用了
cudaMemcpyPeerAsync(同步版本会阻塞流水线);确认GPU处于持久模式且未被其他进程占用。 - 编译错误:若使用CUDA 11.x,需将
cudaMemcpyPeerAsync替换为cudaMemcpyAsync并显式指定cudaMemcpyDeviceToDevice(在启用P2P后两者等价)。