cuda编程笔记(7)--多GPU上的CUDA

零拷贝内存

在流中,我们介绍了cudaHostAlloc这个函数,它有一些标志,其中cudaHostAllocMapped允许内存映射到设备,也即GPU可以直接访问主机上的内存,不用额外再给设备指针分配内存

通过下面的操作,即可让设备指针也可访问主机内存

cudaHostAlloc((void**)&a, N * sizeof(float), cudaHostAllocWriteCombined | cudaHostAllocMapped);
cudaHostGetDevicePointer(&dev_a, a, 0); // 将主机指针映射为设备可用指针

由于GPU虚拟内存空间和CPU不同,不能直接使用指针a,必须调用cudaHostGetDevicePointer函数;这样 dev_a 就是设备端可以直接访问的 host 内存。

原理简介

  • 在调用 cudaHostAllocMapped 时,CUDA 会在主机申请一块 页锁定内存(pinned memory);

  • 再通过 cudaHostGetDevicePointer 把这块主机内存映射为设备端地址空间中的指针

  • 当 GPU 访问 dev_a[i] 时,会通过 PCIe 总线从主机 RAM 中取数据,实现 零拷贝访问

所以它虽然“看起来像显存指针”,但其实访问的是主机内存。

下面用该机制重写cuda编程笔记(2.5)--简易的应用代码-CSDN博客里的矢量点乘


#ifndef __CUDACC__
#define __CUDACC__
#endif
#include <cuda_runtime.h>
#include <device_launch_parameters.h>#include <iostream>
#include<cstdio>#define threadsPerBlock 256
const int Blocks = 32;
const int N = Blocks * threadsPerBlock;void error_handling(cudaError_t res) {if (res !=cudaSuccess) {std::cout << "error!" << std::endl;}
}
__global__ void dot(float* a, float* b, float* c) {__shared__ float cache[threadsPerBlock];int tid = threadIdx.x + blockIdx.x * blockDim.x;int cacheIndex = threadIdx.x;float temp = 0;if (tid < N) temp = a[tid] * b[tid];cache[cacheIndex] = temp;__syncthreads();for (int stride = blockDim.x / 2; stride > 0; stride>>= 1) {if (cacheIndex < stride)cache[cacheIndex] += cache[cacheIndex + stride];__syncthreads();}// 将每个 block 的结果写入全局内存if (cacheIndex == 0) {c[blockIdx.x] = cache[0];}}
int main() {cudaEvent_t start, stop;float* a, * b, c, * partial_c;float* dev_a, * dev_b, * dev_partial_c;float elapsedTime;error_handling(cudaEventCreate(&start));error_handling(cudaEventCreate(&stop));//在cpu上分配内存error_handling(cudaHostAlloc((void**)&a, N * sizeof(float),cudaHostAllocWriteCombined|cudaHostAllocMapped));error_handling(cudaHostAlloc((void**)&b, N * sizeof(float), cudaHostAllocWriteCombined | cudaHostAllocMapped));error_handling(cudaHostAlloc((void**)&partial_c, Blocks * sizeof(float), cudaHostAllocWriteCombined | cudaHostAllocMapped));for (int i = 0; i < N; i++) {a[i] = i;b[i] = i * 2;}error_handling(cudaHostGetDevicePointer(&dev_a, a, 0));error_handling(cudaHostGetDevicePointer(&dev_b, b, 0));error_handling(cudaHostGetDevicePointer(&dev_partial_c, partial_c, 0));error_handling(cudaEventRecord(start, 0));dot << < Blocks, threadsPerBlock >> > (dev_a, dev_b, dev_partial_c);error_handling(cudaDeviceSynchronize());error_handling(cudaEventRecord(stop, 0));error_handling(cudaEventSynchronize(stop));error_handling(cudaEventElapsedTime(&elapsedTime, start, stop));c = 0;for (int i = 0; i < Blocks; i++)c += partial_c[i];error_handling(cudaFreeHost(a));error_handling(cudaFreeHost(b));error_handling(cudaFreeHost(partial_c));error_handling(cudaEventDestroy(start));error_handling(cudaEventDestroy(stop));printf("Value calculated: %f\n", c);printf("Time consumed:%f\n", elapsedTime);
}
优点说明
减少显式 cudaMemcpy 调用主机 → 设备零拷贝
避免重复申请/释放显存数据只分配一次
简化代码结构多个内核之间共享同一 host 指针
适合小规模、实时更新场景如 GUI 控件、摄像头图像
缺点说明
访问速度远慢于 global memory因为要通过 PCIe
仅适用于某些 GPU(如支持 UVA)非所有设备支持
最佳性能只在小数据量/零延迟访问场景比如小型图像处理、调试等
受限于 CPU 内存页页大小影响效率,不能高并发

 使用条件

要点说明
GPU 必须支持 UVA(统一虚拟地址空间)可用 cudaGetDeviceProperties() 查询 unifiedAddressing 是否为 1
最好配合 WriteCombined适合只写不读场景(如从主机写入,GPU 读取)
不适合大规模数据训练/推理会严重拖慢 GPU 性能,PCIe 带宽远小于显存带宽

启动多GPU

使用多个线程,就可以同时启动多个 GPU 来并行计算,这是现代 CUDA 编程中非常推荐且常用的做法。 

CUDA 的执行模型是:

  • 每个 CPU 线程 通过 cudaSetDevice(id) 绑定到某个 GPU

  • 每个线程可以在绑定的 GPU 上:

    • 分配显存

    • 启动 kernel

    • 执行 memcpy

    • 做同步

CUDA runtime 为每个 CPU 线程维护独立的 GPU 上下文(context),所以 不同线程绑定不同 GPU,就可以各自独立调度、执行自己的 kernel

#include <thread>
#include <iostream>__global__ void kernel(int id) {printf("Hello from GPU %d, thread %d\n", id, threadIdx.x);
}void gpu_task(int device_id) {cudaSetDevice(device_id);kernel<<<1, 4>>>(device_id);cudaDeviceSynchronize();  // 等待 GPU 完成
}int main() {int num_devices = 0;cudaGetDeviceCount(&num_devices);std::vector<std::thread> threads;for (int i = 0; i < num_devices; ++i) {threads.emplace_back(gpu_task, i);  // 每个线程负责一个 GPU}for (auto& t : threads) t.join(); // 等待所有线程完成return 0;
}

多 GPU 场景下共享主机内存

cudaHostAlloc中当flags传入cudaHostAllocPortable时

就意味着:

✅ 分配出的主机内存是跨 GPU 可见(portable)的,不属于某个特定的 GPU 上下文。

为什么多 GPU 编程中需要 cudaHostAllocPortable

在默认情况下(无 cudaHostAllocPortable):

  • 使用 cudaHostAlloc() 分配的内存只绑定到当前 GPU 上下文

  • 如果你在另一个 GPU 上使用该内存(比如调用 cudaMemcpyAsync),就会报错或性能下降。

加上 cudaHostAllocPortable 后:

  • 这块页锁定内存在所有 GPU 上都能直接访问(只要硬件支持 UVA)。

典型用法:多 GPU + Portable 内存

float *host_ptr;
cudaHostAlloc((void**)&host_ptr, N * sizeof(float), cudaHostAllocPortable);

 然后每个线程可以这样操作:

void run_on_device(int device_id, float* shared_host) {cudaSetDevice(device_id);float *dev_ptr;cudaMalloc(&dev_ptr, N * sizeof(float));// 每个 GPU 从共享主机内存拷贝数据cudaMemcpy(dev_ptr, shared_host, N * sizeof(float), cudaMemcpyHostToDevice);kernel<<<blocks, threads>>>(dev_ptr);cudaDeviceSynchronize();cudaFree(dev_ptr);
}

这样,每个 GPU 都能用同一块主机内存 shared_host 来做数据初始化、写回、交换数据等操作。

常见组合:

cudaHostAllocPortable | cudaHostAllocWriteCombined

GPU A 写结果,GPU B 读取验证

GPU A 写入 shared host memory,GPU B 读取验证是完全可能出现同步问题的

线程之间需要加同步

#include <cuda_runtime.h>
#include <iostream>
#include <thread>
#include <vector>
#include <cassert>#define N 16__global__ void write_kernel(int *data, int val) {int idx = threadIdx.x;if (idx < N) {data[idx] = val * 100 + idx;}
}__global__ void read_kernel(int *data) {int idx = threadIdx.x;if (idx < N) {printf("GPU 1 reads: data[%d] = %d\n", idx, data[idx]);}
}// GPU 0 线程函数:写入共享主机内存
void gpu0_writer(int *host_data, cudaEvent_t write_done_event) {cudaSetDevice(0);cudaStream_t stream;cudaStreamCreate(&stream);int *dev_data;cudaMalloc(&dev_data, N * sizeof(int));write_kernel<<<1, N, 0, stream>>>(dev_data, 1);// 将数据从设备拷贝到共享主机内存cudaMemcpyAsync(host_data, dev_data, N * sizeof(int), cudaMemcpyDeviceToHost, stream);// 记录写入完成事件cudaEventRecord(write_done_event, stream);cudaStreamSynchronize(stream);cudaFree(dev_data);cudaStreamDestroy(stream);std::cout << "[GPU 0] 写入完成\n";
}// GPU 1 线程函数:等待事件后读取共享主机内存
void gpu1_reader(int *host_data, cudaEvent_t write_done_event) {cudaSetDevice(1);cudaStream_t stream;cudaStreamCreate(&stream);// 等待 GPU 0 写入完成cudaStreamWaitEvent(stream, write_done_event, 0);int *dev_data;cudaMalloc(&dev_data, N * sizeof(int));// 从共享主机内存拷贝到 GPU 1 上的显存cudaMemcpyAsync(dev_data, host_data, N * sizeof(int), cudaMemcpyHostToDevice, stream);read_kernel<<<1, N, 0, stream>>>(dev_data);cudaStreamSynchronize(stream);cudaFree(dev_data);cudaStreamDestroy(stream);std::cout << "[GPU 1] 读取完成\n";
}int main() {int gpu_count = 0;cudaGetDeviceCount(&gpu_count);if (gpu_count < 2) {std::cerr << "需要至少两个 GPU!\n";return -1;}// 分配共享主机内存(portable)int *shared_host_data;cudaHostAlloc((void**)&shared_host_data, N * sizeof(int), cudaHostAllocPortable);// 创建用于跨 GPU 通信的事件cudaEvent_t write_done_event;cudaEventCreateWithFlags(&write_done_event, cudaEventDisableTiming); // faster event// 启动两个线程std::thread t0(gpu0_writer, shared_host_data, write_done_event);std::thread t1(gpu1_reader, shared_host_data, write_done_event);t0.join();t1.join();cudaEventDestroy(write_done_event);cudaFreeHost(shared_host_data);return 0;
}

cudaEventCreateWithFlags

事件创建:cudaEventCreateWithFlags

cudaEvent_t evt;
cudaEventCreateWithFlags(&evt, cudaEventDisableTiming); // 推荐带标志创建更轻量
标志含义说明
cudaEventDefault默认行为会记录耗时,可用于性能计时
cudaEventDisableTiming禁用计时功能更轻量,推荐用于同步控制
cudaEventInterprocess可用于多进程共享事件不常用于多 GPU 同步(属于高级功能)

cudaEventRecord

表示 之前所有 stream中的操作都完成时,该事件被标记完成。 

cudaStreamWaitEvent

cudaError_t cudaStreamWaitEvent(cudaStream_t stream, cudaEvent_t event, unsigned int flags);
参数类型含义
streamcudaStream_t要等待事件的 CUDA 流。这个 stream 将在 event 被触发后才开始执行其后续任务。
eventcudaEvent_t要等待的事件。这个事件应该在其他设备或流上通过 cudaEventRecord 创建。
flagsunsigned int当前必须设为 0。(CUDA 12.4 以前不支持其他选项)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/90901.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/90901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IP地址混乱?监控易IPAM实现全网地址自动化管理与非法接入告警

IP地址出现混乱状况&#xff1f;监控易IPAM能够达成对全网地址予以自动化管理的目标&#xff0c;同时还可针对非法接入的情况发出告警信息。办公室毫无预兆地突然断网了&#xff0c;经过一番仔细排查之后&#xff0c;发现原来是IP地址出现了冲突的情况。有人私自接了路由器&…

安全监测预警平台的应用场景

随着城市化进程加快和基础设施规模扩大&#xff0c;各类安全风险日益突出。安全监测预警平台作为现代安全管理的重要工具&#xff0c;通过整合物联网、大数据、人工智能等先进技术&#xff0c;实现对各类安全隐患的实时监测、智能分析和精准预警。本文将详细探讨安全监测预警平…

007_用例与应用场景

用例与应用场景 目录 内容创作编程开发数据分析客户服务教育培训商业智能研究辅助 内容创作 文案撰写 应用场景&#xff1a; 营销文案和广告语产品描述和说明书社交媒体内容邮件营销内容 实际案例&#xff1a; 任务&#xff1a;为新款智能手表撰写产品描述 输入&#x…

Unity物理系统由浅入深第一节:Unity 物理系统基础与应用

Unity物理系统由浅入深第一节&#xff1a;Unity 物理系统基础与应用 Unity物理系统由浅入深第二节&#xff1a;物理系统高级特性与优化 Unity物理系统由浅入深第三节&#xff1a;物理引擎底层原理剖析 Unity物理系统由浅入深第四节&#xff1a;物理约束求解与稳定性 Unity 引擎…

《[系统底层攻坚] 张冬〈大话存储终极版〉精读计划启动——存储架构原理深度拆解之旅》-系统性学习笔记(适合小白与IT工作人员)

&#x1f525; 致所有存储技术探索者笔者近期将系统攻克存储领域经典巨作——张冬老师编著的《大话存储终极版》。这部近千页的存储系统圣经&#xff0c;以庖丁解牛的方式剖析了&#xff1a;存储硬件底层架构、分布式存储核心算法、超融合系统设计哲学等等。喜欢研究数据存储或…

flutter鸿蒙版 环境配置

flutter支持开发鸿蒙,但是需要专门的flutter鸿蒙项目, Flutter鸿蒙化环境配置&#xff08;windows&#xff09;_flutter config --ohos-sdk-CSDN博客

Java 高级特性实战:反射与动态代理在 spring 中的核心应用

在 Java 开发中&#xff0c;反射和动态代理常被视为 “高级特性”&#xff0c;它们看似抽象&#xff0c;却支撑着 Spring、MyBatis 等主流框架的核心功能。本文结合手写 spring 框架的实践&#xff0c;从 “原理” 到 “落地”&#xff0c;详解这两个特性如何解决实际问题&…

Codeforces Round 855 (Div. 3)

A. Is It a Cat? 去重&#xff0c; 把所有字符看成大写字符&#xff0c; 然后去重&#xff0c; 观察最后结果是不是“MEOW” #include <bits/stdc.h> #define int long longvoid solve() {int n;std::cin >> n;std::string ans, t;std::cin >> ans;for (int…

Scrapy选择器深度指南:CSS与XPath实战技巧

引言&#xff1a;选择器在爬虫中的核心地位在现代爬虫开发中&#xff0c;​​选择器​​是数据提取的灵魂工具。根据2023年网络爬虫开发者调查数据显示&#xff1a;​​92%​​ 的数据提取错误源于选择器编写不当熟练使用选择器的开发效率相比新手提升 ​​300%​​同时掌握CSS…

Windos服务器升级MySQL版本

Windos服务器升级MySQL版本 1.备份数据库 windows下必须以管理员身份运行命令行工具进行备份&#xff0c;如果没有配置MySQL的环境变量&#xff0c;需要进入MySQL Server 的bin目录输入指令&#xff0c; mysqldump -u root -p --all-databases > backup.sql再输入数据库密码…

告别频繁登录!Nuxt3 + TypeScript + Vue3实战:双Token无感刷新方案全解析

前言 在现代 Web 应用中&#xff0c;身份认证是保障系统安全的重要环节。传统的单 Token 认证方式存在诸多不足&#xff0c;如 Token 过期后需要用户重新登录&#xff0c;影响用户体验。本文将详细介绍如何在 Nuxt3 TypeScript Vue3 项目中实现无感刷新 Token 机制&#xff…

Linux——Redis

目录 一、Redis概念 1.1 Redis定义 1.2 Redis的特点 1.3 Redis的用途 1.4 Redis与其他数据库的对比 二、Redis数据库 三、Redis五个基本类型 3.1 字符串 3.2 列表(list) ——可以有相同的值 3.3 集合(set) ——值不能重复 3.4 哈希(hash) ——类似于Map集合 3.5 有序…

【AI大模型】部署优化量化:INT8压缩模型

INT8&#xff08;8位整数&#xff09;量化是AI大模型部署中最激进的压缩技术&#xff0c;通过将模型权重和激活值从FP32降至INT8&#xff08;-128&#xff5e;127整数&#xff09;&#xff0c;实现4倍内存压缩2-4倍推理加速&#xff0c;是边缘计算和高并发服务的核心优化手段。…

LFU 缓存

题目链接 LFU 缓存 题目描述 注意点 1 < capacity < 10^40 < key < 10^50 < value < 10^9对缓存中的键执行 get 或 put 操作&#xff0c;使用计数器的值将会递增当缓存达到其容量 capacity 时&#xff0c;则应该在插入新项之前&#xff0c;移除最不经常使…

检查输入有效性(指针是否为NULL)和检查字符串长度是否为0

检查输入有效性&#xff08;指针是否为NULL&#xff09;和检查字符串长度是否为0 这两个检查针对的是完全不同的边界情况&#xff0c;都是必要的防御性编程措施&#xff1a; 1. 空指针检查 if(!src) 目的&#xff1a;防止解引用空指针场景&#xff1a;当调用者传入 NULL 时风险…

Apache POI 的 HSSFWorkbook、SXSSFWorkbook和XSSFWorkbook三者的区别

HSSFWorkbook 专用于处理Excel 97-2003&#xff08;.xls&#xff09;格式的二进制文件。基于纯Java实现&#xff0c;所有数据存储在内存中&#xff0c;适合小规模数据&#xff08;通常不超过万行&#xff09;。内存占用较高&#xff0c;但功能完整&#xff0c;支持所有旧版Exce…

冷冻电镜重构的GPU加速破局:从Relion到CryoSPARC的并行重构算法

点击 “AladdinEdu&#xff0c;同学们用得起的【H卡】算力平台”&#xff0c;H卡级别算力&#xff0c;按量计费&#xff0c;灵活弹性&#xff0c;顶级配置&#xff0c;学生专属优惠。 一、冷冻电镜重构的算力困局 随着单粒子冷冻电镜&#xff08;cryo-EM&#xff09;分辨率突破…

算法学习笔记:16.哈希算法 ——从原理到实战,涵盖 LeetCode 与考研 408 例题

在计算机科学中&#xff0c;哈希算法&#xff08;Hash Algorithm&#xff09;是一种将任意长度的输入数据映射到固定长度输出的技术&#xff0c;其输出称为哈希值&#xff08;Hash Value&#xff09;或散列值。哈希算法凭借高效的查找、插入和删除性能&#xff0c;在数据存储、…

16018.UE4+Airsim仿真环境搭建超级详细

文章目录 1 源码下载2 下载安装软件2.1 安装 UE4 软件2.2 安装visual studio 20223 编译airsim源码4 进入AirSim工程,打开工程5 UE4 工程创建5.1 下载免费场景 CityPark,并创建工程5.2 工程编译5.2.1 将airsim 插件拷贝到 UE4工程路径中5.2.2 修改工程配置文件5.2.3 创建c++类…

Python 实战:构建 Git 自动化助手

在多项目协作、企业级工程管理或开源社区维护中&#xff0c;经常面临需要同时管理数十甚至上百个 Git 仓库的场景&#xff1a;多仓库需要统一 pull 拉取更新定期向多个项目批量 commit 和 push自动备份 Git 项目批量拉取私有仓库并管理密钥为解决这类高频、重复、机械性工作&am…