Apache Arrow C++ CUDA 支持:设备、上下文、GPU 缓冲与 IPC 的完整 API 解析
【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow
本篇基于 Apache Arrow 官方 API 参考文档 CUDA support 展开,结合cpp/src/arrow/gpu/下的实际源码实现,系统讲解arrow::cuda命名空间中五类核心 API:上下文(Contexts)、设备(Devices)、缓冲区(Buffers)、内存输入/输出(Memory Input/Output)与 IPC 函数。读完后你将能够:在 C++ 程序中通过CudaDeviceManager获取 GPU 设备与上下文、分配和搬运显存、以文件流接口零拷贝读写 GPU 缓冲,并借助 CUDA IPC 跨进程共享 GPU 内存与 RecordBatch 消息。
一、API 总览与构建前提
官方文档 docs/source/cpp/api/cuda.rst 将该 API 划分为五个主题,对应的实现集中位于 cpp/src/arrow/gpu/cuda_context.h、cpp/src/arrow/gpu/cuda_memory.h 和 cpp/src/arrow/gpu/cuda_arrow_ipc.h,全部类型定义在arrow::cuda命名空间中:
| 文档主题 | 对应类型 / 函数 | 实现头文件 |
|---|---|---|
| Contexts | CudaDeviceManager、CudaContext | cuda_context.h |
| Devices | CudaDevice、CudaMemoryManager | cuda_context.h |
| Buffers | CudaBuffer、CudaHostBuffer | cuda_memory.h |
| Memory Input/Output | CudaBufferReader、CudaBufferWriter | cuda_memory.h |
| IPC | CudaIpcMemHandle、SerializeRecordBatch、ReadRecordBatch | cuda_memory.h、cuda_arrow_ipc.h |
该模块由独立的libarrow_cuda目标产出(见 cpp/src/arrow/gpu/CMakeLists.txt,并生成arrow-cuda.pc与ArrowCUDAConfig.cmake供下游链接)。从源码结构看,CMake 构建通过ARROW_CUDA选项启用该子目录(见 cpp/src/arrow/CMakeLists.txt 中的if(ARROW_CUDA)守卫,以及 cpp/CMakeLists.txt 中为arrow-cuda.pc准备的ARROW_CUDA_PC_CFLAGS变量,静态链接时追加-DARROW_CUDA_STATIC)。因此构建 C++ 库时需要 CUDA Toolkit 环境并打开相应构建选项;使用方应链接arrow_cuda组件并包含arrow/gpu/下的头文件。
二、Contexts:CudaDeviceManager 与 CudaContext
2.1 CudaDeviceManager —— 进程级单例入口
CudaDeviceManager(cuda_context.h#L44-L85)是所有 CUDA API 的入口点,通过静态方法Instance()获取进程级单例。其公开成员包括:
Result<std::shared_ptr<CudaDevice>> GetDevice(int device_number):按 CUDA 逻辑设备号获取CudaDevice实例;Result<std::shared_ptr<CudaContext>> GetContext(int device_number):获取指定设备的上下文(源码注释标明返回的是cached context,即会缓存复用);Result<std::shared_ptr<CudaContext>> GetSharedContext(int device_number, void* handle):使用其他库创建的 CUDA 上下文句柄构造一个“共享”上下文,用于与外部库互操作;Result<std::shared_ptr<CudaHostBuffer>> AllocateHost(int device_number, int64_t nbytes):分配对指定 GPU 可快速访问的主机内存;Status FreeHost(void* data, int64_t nbytes):释放AllocateHost分配的内存(源码明确约束:传入指针必须来自AllocateHost);int num_devices() const:返回可见 CUDA 设备数量。
其构造函数为 private,配合静态成员instance_,从源码结构看这是一个线程安全的惰性初始化单例。
2.2 CudaContext —— CUDA 驱动 API 的对象化封装
CudaContext(cuda_context.h#L319-L415)的注释将其定位为“low-level CUDA driver API 的面向对象接口”。它持有私有实现(pimpl,std::shared_ptr<Impl> impl_),关键公开能力有:
- 显存生命周期:
Allocate(int64_t nbytes)在上下文的 GPU 设备上分配显存,返回CudaBuffer;Free(void* device_ptr, int64_t nbytes)释放显存;View(uint8_t* data, int64_t nbytes)把外部显存包装成CudaBuffer视图——注释特别强调调用者负责该内存的分配/释放,且必须属于本上下文的 CUDA context;bytes_allocated()可查询已分配字节数; - IPC 支持:
OpenIpcBuffer(const CudaIpcMemHandle&)打开其他进程导出的 IPC 句柄并映射到本地显存,CloseIpcBuffer(CudaBuffer*)关闭映射(见 2 节之后的 IPC 章节); - 同步与句柄:
Synchronize()阻塞直到该设备所有任务完成;handle()把 CUDA 上下文句柄暴露给其他库;Close()关闭上下文; - 地址转换:
GetDeviceAddress(uint8_t*/uintptr_t addr)返回“可从本上下文 kernel 访问的设备地址”。注释解释了设备地址不一定是显存地址——用cudaMallocHost/cudaHostAlloc分配的主机内存、cudaMallocManaged分配的托管内存、或经cudaHostRegister页锁定的主机内存,都可以给出设备可达地址; - 关联查询:
memory_manager()返回该上下文设备对应的默认CudaMemoryManager,device()/device_number()返回关联设备及其逻辑号。
从源码结构看,CudaContext还封装了私有的ExportIpcBuffer、CopyHostToDevice/CopyDeviceToHost/CopyDeviceToDevice/CopyDeviceToAnotherDevice等拷贝路径(cuda_context.h#L387-L401),这些是CudaBuffer拷贝方法与跨设备CopyBufferFrom/To的底层执行者。
三、Devices:CudaDevice 与 CudaMemoryManager
3.1 CudaDevice —— 与具体 GPU 绑定的 Device 实现
CudaDevice(cuda_context.h#L91-L240)继承自 Arrow 的Device抽象,每个实例绑定一个以逻辑号标识的 CUDA 设备,且device_type()固定返回DeviceAllocationType::kCUDA。核心接口:
static Result<std::shared_ptr<CudaDevice>> Make(int device_number):按设备号构造;- 信息查询:
device_number()(逻辑号)、device_name()(GPU 型号名)、total_memory()(设备总显存)、handle()(原始CUdevice句柄,注释说明可用于向其他库暴露该设备); - 上下文获取:
GetContext()返回与该设备主 CUDA 上下文(primary context)关联的上下文——注释明确这是推荐方式,因为主上下文 API 可与任何使用该 API 的库透明互操作;GetSharedContext(void* handle)则用于互操作非主上下文的库,且句柄不被拥有(销毁CudaContext时不会释放); AllocateHostBuffer(int64_t size):用该设备的主上下文分配主机驻留、GPU 可访问的缓冲;default_memory_manager():返回该设备的默认CudaMemoryManager。
此外CudaDevice内嵌两个面向并发执行的类:
CudaDevice::Stream(标注为 EXPERIMENTAL):包装CUstream。它不拥有CUstream对象,需由外部通过cuStreamCreate/cuStreamDestroy管理;默认构造使用 CUDA 默认流,并刻意禁用从字面量0或nullptr构造(cuda_context.h#L151-L183)。提供WaitEvent与Synchronize,且可显式转换为CUstream;CudaDevice::SyncEvent:包装CUevent,提供Wait()(阻塞直到事件完成)与Record(const Device::Stream&)(在流上记录事件,流完成后触发)。
设备级创建流与事件的统一入口包括MakeStream()/MakeStream(unsigned int flags)/WrapStream(void*, release_fn)和CudaMemoryManager::MakeDeviceSyncEvent()/WrapDeviceSyncEvent()。
源码还提供了类型判别与转换工具:IsCudaDevice(const Device&)与AsCudaDevice(const std::shared_ptr<Device>&)(cuda_context.h#L242-L250),后者在非CudaDevice时返回错误。
3.2 CudaMemoryManager —— GPU 侧的 MemoryManager
CudaMemoryManager(cuda_context.h#L253-L304)继承MemoryManager,是与某个CudaDevice绑定的内存分配器:
AllocateBuffer(int64_t size):在 GPU 上分配 ArrowBuffer(即CudaBuffer);GetBufferReader/GetBufferWriter:为缓冲构造 4 节所述的CudaBufferReader/CudaBufferWriter文件流接口;cuda_device():返回绑定的CudaDevice具体类型指针,省去对device()结果的向下转型;- 跨管理器拷贝/视图:
CopyBufferFrom/CopyBufferTo/CopyNonOwnedFrom/CopyNonOwnedTo/ViewBufferFrom/ViewBufferTo。其中跨设备拷贝最终落到CudaContext::CopyDeviceToAnotherDevice(可推断自 friend 声明与私有拷贝方法的设计); - 同步事件:
MakeDeviceSyncEvent()(内部cuEventCreate,析构时cuEventDestroy)与WrapDeviceSyncEvent()(包装外部CUevent*,可传 no-op 表示所有权在外部)。
对应地,IsCudaMemoryManager/AsCudaMemoryManager用于判别与转型(cuda_context.h#L306-L315)。
四、Buffers:CudaBuffer 与 CudaHostBuffer
4.1 CudaBuffer —— 位于 GPU 设备上的 Arrow Buffer
CudaBuffer(cuda_memory.h#L39-L109)继承自Buffer,代表一块 GPU 显存。头文件顶部的注释给出了重要提醒:“Be careful using this in any Arrow code which may not be GPU-aware”——把它误传给假定 CPU 内存的 Arrow 代码会造成问题。
构造方式有三种:直接持有/不持有显存(own_data)的两种构造(含is_ipc标记表示内存由 IPC 映射而来)、以及从父CudaBuffer切出子视图(parent + offset + size)。主要成员:
static Result<std::shared_ptr<CudaBuffer>> FromBuffer(std::shared_ptr<Buffer>):把通用Buffer转回CudaBuffer;若底层并非 GPU 内存则返回错误——这是从普通 Arrow 数据中识别 GPU 缓冲的标准入口;- 主机↔设备拷贝:
CopyToHost(position, nbytes, out)从 GPU 拷到主机;CopyFromHost(position, data, nbytes)从主机拷入;CopyFromDevice(position, data, nbytes)做设备到设备拷贝(注释假设源与目标显存同属一个上下文);CopyFromAnotherDevice(src_ctx, position, data, nbytes)则显式指定源上下文,用于跨设备拷贝; - IPC 导出:
ExportForIpc()把显存暴露为可被其他进程使用的 IPC 内存并返回CudaIpcMemHandle。注释指出一个关键语义变化:调用后该显存不会随CudaBuffer析构而释放(生命周期交由 IPC 协议管理)。
4.2 CudaHostBuffer 与页锁定主机内存
CudaHostBuffer(cuda_memory.h#L113-L121)继承MutableBuffer,注释说明其内存由cudaHostAlloc创建(即页锁定主机内存),GPU 可快速访问。其GetDeviceAddress(const std::shared_ptr<CudaContext>& ctx)返回 GPU kernel 可直接读取该内存的设备地址。
命名空间级辅助函数补全了该能力面(cuda_memory.h#L245-L264):
AllocateCudaHostBuffer(int device_number, int64_t size):为指定设备分配 CPU 侧、GPU 快速可访问的内存,注释强调 GPU 对该内存享有“fast memory copy”优势;GetDeviceAddress(const uint8_t* cpu_data, const std::shared_ptr<CudaContext>& ctx):低层函数,由 CPU 地址求设备地址;GetHostAddress(uintptr_t device_ptr):低层函数,由设备地址求 CPU 侧可访问地址。
五、Memory Input / Output:CudaBufferReader 与 CudaBufferWriter
5.1 CudaBufferReader —— 面向零拷贝读取
CudaBufferReader(cuda_memory.h#L162-L202)继承 Arrow 的随机读取文件抽象(经RandomAccessFileConcurrencyWrapper包装以支持并发)。类注释中包含必须理解的语义边界:
- 读入
Buffer的 Read 返回指向设备内存的Buffer(零拷贝),通常与期望 CPU 缓冲的 Arrow 代码不兼容; - 读入裸指针的 Read 则把设备内存拷贝到主机目标区域。
公开接口包括supports_zero_copy()、buffer()(返回底层CudaBuffer)、DoRead/DoReadAt(两个重载分别对应“读到裸指针”与“读到 Buffer”两种语义)以及DoTell/DoSeek/DoGetSize。因此它适合作为 GPU 侧数据的io::RandomAccessFile使用,例如直接解码已完全落在显存中的 IPC 消息(见 IPC 章节的ReadRecordBatch)。
5.2 CudaBufferWriter —— 可带 CPU 缓冲区的写入
CudaBufferWriter(cuda_memory.h#L206-L243)继承io::WritableFile,支持Write/WriteAt/Seek/Tell/Flush/Close。其缓冲控制是本类的特色:
- 默认无缓冲写入,每次
Write直接触发一次cudaMemcpy; SetBufferSize(int64_t buffer_size)分配指定大小的 CPU 缓冲来减少cudaMemcpy调用次数(注释原话:“Set CPU buffer size to limit calls to cudaMemcpy”);buffer_size()返回 CPU 缓冲大小(0 表示无缓冲),num_bytes_buffered()返回当前缓存在主机侧的字节数。
这一设计与高频小写入场景直接相关:先聚批在 CPU 再整体拷贝上 GPU,是减少 PCIe 传输开销的典型手段。
六、IPC:CudaIpcMemHandle 与 RecordBatch 的 GPU 序列化
文档将 IPC 分为CudaIpcMemHandle类型与一组自由函数(源码中用 Doxygen 组cuda-ipc-functions标注,见 cuda_arrow_ipc.h#L44)。
6.1 CudaIpcMemHandle —— CUDA IPC 句柄容器
CudaIpcMemHandle(cuda_memory.h#L125-L152)封装 CUDA 驱动的CUipcMemHandle:
static FromBuffer(const void* opaque_handle):从另一进程传来的不透明句柄缓冲(序列化后的CUipcMemHandle字节)构造;Serialize(MemoryPool* pool = default_memory_pool()):把句柄写进一个可传输的Buffer——这是跨进程传递句柄的标准通道;- 内部保留
memory_size()与handle(),由CudaBuffer与CudaContext以 friend 关系访问。
6.2 完整 IPC 工作流
结合 4.1 与 2.2 的方法,跨进程共享 GPU 内存的调用链是:
- 导出方:
CudaBuffer::ExportForIpc()得到CudaIpcMemHandle,经Serialize()变成可序列化Buffer,通过任何进程间通道发送; - 接收方:
CudaIpcMemHandle::FromBuffer(收到字节)还原句柄,再由目标设备的CudaContext::OpenIpcBuffer(handle)得到映射到本地显存的CudaBuffer; - 清理:不再使用时调用
CudaContext::CloseIpcBuffer(buffer)解除映射。
该流程在测试中有直接印证:cpp/src/arrow/gpu/cuda_test.cc 中device_buffer->ExportForIpc()与后续OpenIpcBuffer的配对用法,覆盖了导出—序列化—打开—关闭的完整闭环。
6.3 RecordBatch 级 GPU 序列化函数
cuda-ipc-functions组提供两个面向 Arrow 数据的自由函数(cuda_arrow_ipc.h#L52-L67):
SerializeRecordBatch(const RecordBatch& batch, CudaContext* ctx):把一个 RecordBatch 序列化为 IPC 消息并直接写入 GPU 显存,返回CudaBuffer。这是把数据从主机 Arrow 结构搬到 GPU 的入口;ReadRecordBatch(const std::shared_ptr<Schema>& schema, const ipc::DictionaryMemo* dictionary_memo, const std::shared_ptr<CudaBuffer>& buffer, MemoryPool* pool = default_memory_pool()):ReadRecordBatch的 GPU 特化版本,输入是完全位于显存的CudaBufferIPC 消息。注意其pool参数的用途——注释说明它用于为元数据在主机侧分配空间,数据本体保持在设备上;dictionary_memo可传nullptr(当确定没有字典编码字段时)。
这两个函数与 5.1 的CudaBufferReader配合,可以构建完全驻留 GPU 的“序列化—零拷贝解码”路径,避免中间结果反复过 PCIe。
七、使用注意事项小结
- GPU 感知前提:
CudaBuffer的类注释明确提醒它不应流入“可能不感知 GPU”的 Arrow 代码;凡从CudaBufferReader读到Buffer的零拷贝路径同样适用该约束(cuda_memory.h#L39、cuda_memory.h#L157-L161)。 - 所有权语义:
CudaContext::View创建的缓冲不拥有内存,调用者负责分配/释放;GetSharedContext的句柄不被拥有;CudaDevice::Stream不拥有CUstream。这些“非拥有”包装都要求外部正确管理底层 CUDA 资源。 - 主上下文优先:
CudaDevice::GetContext()使用主上下文是官方注释推荐的互操作方式;仅在与使用非主上下文的库协作时才用GetSharedContext。 - IPC 生命周期:
ExportForIpc()之后显存不再随CudaBuffer析构释放,接收方须调用CloseIpcBuffer完成映射释放,否则会造成资源滞留。 - 写入缓冲:高频小写入场景下用
CudaBufferWriter::SetBufferSize启用 CPU 侧聚批,减少cudaMemcpy次数。
以上所有接口均出自arrow::cuda命名空间,构建时需启用 CUDA 组件(CMake 选项ARROW_CUDA,见 cpp/src/arrow/CMakeLists.txt),并链接arrow_cuda目标;完整的测试覆盖位于 cpp/src/arrow/gpu/cuda_test.cc,性能基准则在同目录的cuda_benchmark.cc中维护,可作为行为验证与性能对照的起点。
【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考