必须在每个工作线程启动cuda任务前显式调用cudasetdevice(device_id),否则所有操作默认落在device 0导致多卡负载不均;需为每张gpu维护独立线程、stream和任务队列,并用cudastreamnonblocking优化小任务调度。

如何用 cudaSetDevice() 显式绑定线程到特定 GPU
多卡负载不均的根源,往往是所有 CUDA 调用默认落在 device 0 上。必须在每个线程启动 CUDA 工作前,显式调用 cudaSetDevice(device_id) —— 这不是可选配置,而是强制前提。否则即使你创建了多个流、多个上下文,底层仍可能争抢同一张卡的资源。
常见错误现象:cudaMemcpy 延迟飙升、cudaEventQuery 长时间返回 cudaErrorNotReady、nvidia-smi 显示仅一张卡满载而其余空闲。
- 每个工作线程(或协程)初始化时必须调用一次
cudaSetDevice(),且不能跨线程复用设备句柄 - 避免在主线程设置 device 后,让子线程继承该状态 —— CUDA context 不跨线程共享,子线程需自己设
- 调用
cudaSetDevice()后建议立即检查返回值:cudaGetLastError(),防止因设备不可用(如被占用、权限不足)导致静默失败
用 cudaStream_t + std::thread 实现 per-GPU 任务队列
负载均衡不是靠“平均分发任务数”,而是靠“让每张卡持续有活干”。最简可行方案是为每张 GPU 维护一个独立线程 + 独立 stream + 独立任务队列 —— 这样能绕过 CUDA context 切换开销,也避免 stream 间隐式同步。
使用场景:批量推理、并行数据预处理、多模型并发训练等 CPU-bound + GPU-bound 混合任务。
- 每个线程循环执行:
pop_task() → cudaSetDevice() → launch_kernel() → cudaStreamSynchronize()(或用 event 异步通知) - 不要把所有 kernel 都塞进同一个 stream;每个 GPU 的 stream 应独占,否则
cudaStreamSynchronize()会意外阻塞其他卡的任务 - 若任务粒度小(如每次只处理几百个样本),务必启用
cudaStreamCreateWithFlags(..., cudaStreamNonBlocking),否则默认 stream 的同步行为会串行化执行
cudaGetDeviceCount() 和 cudaDeviceGetAttribute() 怎么避开“假多卡”陷阱
服务器可能报告 8 张 GPU,但其中部分是 NVLink 桥接的逻辑设备,或被 MIG 切分为多个实例。直接轮询 0..7 极易触发 cudaErrorInvalidDevice。
正确做法是先枚举物理设备,再按真实计算能力筛选:
int count; cudaGetDeviceCount(&count); for (int i = 0; i
-
cudaDeviceGetAttribute(..., cudaDevAttrComputeCapabilityMajor)返回 0 表示设备不可用,不是计算能力为 0 - MIG 模式下,
cudaGetDeviceCount()返回的是 instance 数量,不是物理卡数;需结合nvidia-smi -L与cudaDeviceGetAttribute(..., cudaDevAttrMemoryClockRate)辅助判断 - 某些云环境(如 AWS p3/p4)存在虚拟化层,
cudaSetDevice()成功不代表 kernel 一定能跑 —— 必须用 dummy kernel +cudaStreamSynchronize()做 runtime 探活
为什么不用 NCCL 或 MPI 做负载均衡
NCCL 是为 collective communication(all-reduce / broadcast)设计的,不是任务分发器;MPI 更侧重进程间通信而非设备级调度。强行用它们做“GPU 任务路由”,等于用 TCP/IP 协议栈去控制单个网卡队列深度 —— 抽象层错位,徒增延迟和复杂度。
真正需要的是轻量、确定性、无中心调度器的分发逻辑:
- 任务生成方用 round-robin 或最小负载(查
cudaEventElapsedTime()近期耗时)选卡 - 每张卡的 worker 线程保持“拉取模式”,避免 push 模式下的锁竞争和队列堆积
- 如果涉及跨卡数据交换(如中间结果聚合),才在 kernel 层面用
cudaMemcpyPeer()或cudaMemcpyAsync()+cudaEventRecord()显式协调,而不是依赖高层框架自动优化
最容易被忽略的一点:GPU 间 PCIe 带宽远低于卡内带宽,任何跨卡 memcpy 都应视为高成本操作 —— 负载均衡的目标不是让每张卡“算得一样多”,而是让每张卡“等数据的时间尽量少”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











