ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Apache Arrow Python CUDA 集成实战:GPU 设备内存中的 Buffer、Context 与 Numba 互操作

Apache Arrow Python CUDA 集成实战:GPU 设备内存中的 Buffer、Context 与 Numba 互操作 Apache Arrow Python CUDA 集成实战GPU 设备内存中的 Buffer、Context 与 Numba 互操作【免费下载链接】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 官方文档 CUDA Integration 与仓库内pyarrow.cuda的 Cython 实现系统讲解如何在 PyArrow 中创建 CUDA 上下文Context、在 CPU 与 GPU 之间零拷贝/拷贝式地搬运数据CudaBuffer、处理跨设备内存的安全边界以及通过to_numba/from_numba与 Numba JIT 编译器互操作、在 GPU 上直接运行 Arrow 数据。读完后你将掌握pyarrow.cuda的完整 API 面、其底层 C CUDA 库的对应关系以及在生产代码中规避把 GPU 指针交给 CPU 函数这类致命错误的实践方法。1. Arrow 不局限于 CPU 内存Arrow 的数据容器Buffer传统上位于计算机主存host memory中。但 Arrow 的抽象并不止步于此它也提供了访问位于 CUDA 兼容 GPU 设备内存device memory中的 buffer 的机制这就是pyarrow.cuda模块中的CudaBuffer。注意CUDA 支持是可选功能必须在构建时启用。如果你的包管理器如发行版或预编译 wheel没有开启该选项则需要自行构建 Arrow。这也是文档中所有示例都标注# doctest: SKIP的原因——CI 环境不一定具备 GPU。从源码结构看这一功能由三层构成C 核心层cpp/src/arrow/gpu/cuda_context.h 定义了CudaDeviceManager、CudaDevice、CudaMemoryManager和CudaContext四个核心类构成对 CUDA driver API 的对象化封装cpp/src/arrow/gpu/cuda_memory.h 提供CudaBuffer/CudaHostBuffercpp/src/arrow/gpu/cuda_arrow_ipc.h 提供 GPU 内存上的 IPC 消息序列化/反序列化。Python 绑定层python/pyarrow/_cuda.pyx 用 Cython 将上述 C 类包装为Context、CudaBuffer、HostBuffer、IpcMemHandle等 Python 可见类型。测试层python/pyarrow/tests/test_cuda.py 通过pytest.importorskip(pyarrow.cuda)在无 GPU 环境下自动跳过验证了全部上下文与 buffer 行为。2. CUDA 上下文Context一个 CUDA 上下文代表对某个特定 CUDA 设备的访问入口。创建访问 0 号设备的上下文 from pyarrow import cuda ctx cuda.Context(0)cuda.Context的构造参数比文档示例更丰富。从 python/pyarrow/_cuda.pyx 的实现可以看到其签名Context(device_number0, handle0)device_number : int默认 0指定请求 CUDA driver 上下文的 GPU 设备编号。实现中会先通过CCudaDeviceManager.Instance()查询设备总数若device_number超出范围会抛出ValueError: device_number argument must be non-negative less than N该行为在 test_CUDA.py 的test_Context中有对应断言。handle : int默认 0可选用于包装由其他库例如 Numba已经创建的共享 CUDA 上下文句柄。当handle 0时调用 C 侧的GetContext(device_number)否则调用GetSharedContext(device_number, handle)后者不持有该句柄的所有权。这一设计在 C 头文件中也有明确注释cuda_context.h 的CudaDevice类获取设备上下文推荐走 primary context 路径以便与任何使用 primary context API 的库透明互操作而GetSharedContext只应在需要与非 primary context 的第三方库互操作时使用。围绕Context还有几个实用的成员同样出自 _cuda.pyx成员说明Context.get_num_devices()静态方法返回可见 GPU 设备数量常用于设备编号合法性检查ctx.device_number属性返回该上下文绑定的设备编号ctx.handle属性返回底层 context handle 的指针值可用于与其他库共享ctx.synchronize()阻塞直到该设备完成所有已提交任务回拷数据前必须先同步ctx.bytes_allocated属性返回当前上下文已分配的字节数便于内存监控ctx.memory_manager/ctx.device返回绑定的MemoryManager与Device实例Device类型为pa.DeviceAllocationType.CUDAContext.from_numba(contextNone)从 Numba CUDA 上下文反向创建共享Contextctx.to_numba()将 Arrow 上下文转换为 Numbadriver.Context对象其中from_numba/to_numba是两个库之间上下文互认的关键。from_numba直接取 Numba 上下文的device.id与handle.value走共享句柄路径to_numba则用ctypes.c_void_p(self.handle)重建 Numba 的Context对象并用一个DummyPendingDeallocs替换其析构队列——因为上下文的生命周期由 pyarrow 管理避免 Numba 侧重复释放实现见 _cuda.pyx。3. CUDA 缓冲区的创建与回拷3.1 从 host 数据创建设备缓冲区通过Context.buffer_from_data方法可以把任意 Python buffer-like 对象包括 Arrow buffer、NumPy 数组等从主存拷贝到 GPU 内存 import numpy as np arr np.arange(4, dtypenp.int32) arr.nbytes 16 cuda_buf ctx.buffer_from_data(arr) type(cuda_buf) pyarrow._cuda.CudaBuffer cuda_buf.size 16 cuda_buf.address 30088364544 cuda_buf.context.device_number 0buffer_from_data的完整签名是(data, offset0, size-1)实现逻辑_cuda.pyx可以概括为四步用pyarrow_is_cudabuffer(data)判断来源host 数据先经as_buffer转成 ArrowBufferdevice 数据则原样使用校验offset/sizeoffset越界抛ValueError(offset argument is out-of-range)size越界抛ValueError(requested larger slice than available in device buffer)若发生切片offset ! 0 or size ! bsize先用buf.slice(offset, size)做零拷贝切片调用self.new_buffer(size)内部走 C 的CudaContext::Allocate即cudaMalloc语义分配新设备内存再按来源分别调用copy_from_hostH2D或copy_from_deviceD2D完成拷贝。注意语义要点buffer_from_data总是分配一块新的设备内存并拷贝即使源已经是CudaBuffer也是如此若源与目标同设备拷贝路径是设备到设备拷贝CopyFromDevice而不是引用共享。3.2 回拷到 host对称地copy_to_host把 GPU 数据拷回主存返回一个普通的 CPUBuffer buf cuda_buf.copy_to_host() type(buf) pyarrow.lib.Buffer np.frombuffer(buf, dtypenp.int32) array([0, 1, 2, 3], dtypeint32)其参数行为值得注意_cuda.pyxcopy_to_host(position0, nbytes-1, bufNone, memory_poolNone, resizableFalse)。nbytes默认为 -1表示从position拷到缓冲区末尾传入预分配的buf时若nbytes 0则拷到 host buffer 填满为止。拷贝在with nogil:中执行 C 层CudaBuffer::CopyToHost避免 GIL 成为瓶颈。文档还特别提醒调用者有责任确保所有影响该内存的 GPU 任务已经完成必要时先调用ctx.synchronize()。CudaBuffer还附带两个便利方法slice(offset, length)返回零拷贝的设备缓冲区切片越界时自动收缩到可用长度以及to_pybytes()内部就是copy_to_host().to_pybytes()。3.3 安全边界不要把 CUDA buffer 传给 CPU 函数原文档中有一条非常重要的警告许多 Arrow 函数假定接收 CPU buffer但不会检查 buffer 的实际类型。把 CUDA buffer 传给这类函数会导致段错误 pa.py_buffer(bx * 16).equals(cuda_buf) Segmentation fault从源码结构看这一风险是真实的CudaBuffer继承自Buffercdef class CudaBuffer(Buffer)见 _cuda.pyx因此能通过大量基于isinstance(..., Buffer)的隐式类型检查而它的address是设备地址CPU 代码一旦按 host 指针解引用就会崩溃。为防御误用CudaBuffer显式禁用了 PEP 3118 buffer 协议def __getbuffer__(self, cp.Py_buffer* buffer, int flags): # Device buffer contains data pointers on the device. Hence, # cannot support buffer protocol PEP-3118 for CudaBuffer. raise BufferError(buffer protocol for device buffer not supported)实践建议在把 buffer 喂给普通 Arrow 计算函数之前先显式检查其类型或统一copy_to_host()后再操作反过来cuda.CudaBuffer.from_buffer(buf)静态方法可以在已知某Buffer实为 CudaBuffer 时做类型还原内部调用 C 的CCudaBuffer::FromBuffer。4. 与 Numba 的互操作文档指出从纯 Python 直接操作 Arrow CUDA buffer 能做的事不多但它们支持与 Numba——一个能把 Python 代码 JIT 成优化 CUDA kernel 的编译器——互操作。这是当前pyarrow.cuda的主要实战场景。4.1 Arrow → Numba让 kernel 消费 GPU 上的 Arrow 数据第一步定义一个对int32数组逐元素 1 的 Numba CUDA kernel假定数组可写 import numba.cuda numba.cuda.jit ... def increment_by_one(an_array): ... pos numba.cuda.grid(1) ... if pos an_array.size: ... an_array[pos] 1第二步用 Numba 的DeviceNDArray把 CUDA buffer 包装为带完整元数据shape、strides、dtype的 device array——这样 Numba 才能识别数组特征、用正确的类型声明编译 kernel。元数据可以直接取自原始 NumPy 数组。关键点GPU 数据没有被拷贝只是被指针指向 from numba.cuda.cudadrv.devicearray import DeviceNDArray device_arr DeviceNDArray(arr.shape, arr.strides, arr.dtype, ... gpu_datacuda_buf.to_numba())CudaBuffer.to_numba()返回一个 NumbaMemoryPointer封装self.address与self.size。从实现_cuda.pyx可以看出它做了兼容性处理优先按新版 Numba 的MemoryPointer(context, pointer, size)构造若签名不匹配新版 Numba 不再接受 context 参数则降级为MemoryPointer(pointer, size)从而横跨新旧 Numba 版本。文档同时指出当前的局限理想情况下我们希望定义一个 CPU 内存中的 Arrow array在不丢失类型信息的前提下把它拷到 CUDA 内存然后直接在其上调用 Numba kernel 而无需手工构造DeviceNDArray——这在当前版本尚不可行元数据桥接仍需手工完成。第三步以 16×16 的网格尺寸运行 kernel再拷回主存验证 increment_by_one16, 16 np.frombuffer(cuda_buf.copy_to_host(), dtypenp.int32) array([1, 2, 3, 4], dtypeint32)4.2 Numba → Arrow把 Numba 设备数组视为 CudaBuffer反向路径Numba 创建的设备数组可以经CudaBuffer.from_numba工厂方法直接视图化为 Arrow CUDA buffer。先造一个 Numba 设备数组 arr np.arange(10, 14, dtypenp.int32) arr array([10, 11, 12, 13], dtypeint32) device_arr numba.cuda.to_device(arr)然后用它创建指向同一块设备内存的 CudaBuffer。这一次无需显式传入 CUDA context合适的 context 会自动从 Numba 对象中取出并适配 cuda_buf cuda.CudaBuffer.from_numba(device_arr.gpu_data) cuda_buf.size 16 cuda_buf.address 30088364032 cuda_buf.context.device_number 0实现上_cuda.pyxfrom_numba(mem)接收 Numba 的MemoryPointer先Context.from_numba(mem.context)从 Numba 上下文重建共享的 Arrow 上下文再走ctx.foreign_buffer(mem.device_pointer_value, mem.size, basemem)创建非拥有的设备视图——base参数持有 Numba 对象引用保证其内存比 CudaBuffer 视图存活得更久。对空指针device_pointer_value is None and size 0则退化为new_buffer(0)。之后照常可以拷回主存 np.frombuffer(cuda_buf.copy_to_host(), dtypenp.int32) array([10, 11, 12, 13], dtypeint32)5. 文档之外的pyarrow.cuda能力速览关联文档聚焦Context Buffer Numba这条主线但仓库中的 _cuda.pyx 还实现了若干进阶 API适合在性能敏感或多进程场景下参考cuda.new_host_buffer(size, device0)分配 CUDA 可快速访问的 pinned host 内存对应 C 侧cudaHostAlloc语义的CudaHostBufferH2D/D2H 传输带宽优于普通 malloc 内存。Context.new_buffer(nbytes)直接分配空的设备缓冲区CudaContext::Allocate配合copy_from_host(position, nbytes)/copy_from_device(buf, position, nbytes)手动编排拷贝。跨设备拷贝时两个 buffer 的 context handle 不同自动切换 C 层的CopyFromAnotherDevice路径。Context.foreign_buffer(address, size, baseNone)把外部设备地址包装为 CudaBuffer 视图地址会先经get_device_address映射为 kernel 可达的设备地址managed memory、pinned host 内存等场景。IPC 共享CudaBuffer.export_for_ipc()导出IpcMemHandle可serialize(pool)成Buffer跨进程传输对端用IpcMemHandle.from_buffer(opaque_handle)反序列化后经ctx.open_ipc_buffer(handle)打开设备缓冲区。注意导出后该段设备内存不再随 CudaBuffer 析构释放生命周期管理需自行保证。GPU 上的 IPC 消息读写cuda.serialize_record_batch(batch, ctx)把 RecordBatch 的 IPC 消息直接写入 GPU 设备内存cuda.read_message(source, pool)从CudaBuffer或cuda.BufferReader反序列化元数据拷到 host数据体保留在 devicecuda.read_record_batch(buffer, schema, ...)进一步重建出携带设备指针的RecordBatch。这条链路对应 C 头文件 cuda_arrow_ipc.h是Arrow IPC 格式在 GPU 上零拷贝落地的关键能力。cuda.BufferReader/cuda.BufferWriter把 CudaBuffer 暴露为 Arrow 文件接口RandomAccessFile/OutputStream。BufferReader.read_buffer(nbytes)返回设备 buffer 的零拷贝切片视图BufferWriter默认非缓冲可经buffer_size设置 CPU 侧缓冲以减少cudaMemcpy调用次数。文档警告这类接口返回的是设备内存指针只能做指针算术不能喂给期望 host 内存的 Arrow 代码。6. 小结与使用前提适用前提Python 环境中的 pyarrow 必须在构建时启用 CUDA 支持否则import pyarrow.cuda直接失败且运行机器具备 CUDA 设备测试套件以pytest.importorskip(pyarrow.cuda)的方式优雅降级test_cuda.py。核心心智模型Context是设备入口可用handle与其他库共享上下文CudaBuffer是继承自Buffer但地址属于设备的类型——它不能参加任何假定 host 内存的 Arrow 操作跨边界必须显式copy_to_host/copy_from_host/copy_from_device与 Numba 之间通过to_numba/from_numba以及Context.from_numba/to_numba以共享句柄方式互认避免重复分配与拷贝。代码入口Python 绑定集中在 python/pyarrow/_cuda.pyxC 侧核心为 cpp/src/arrow/gpu/cuda_context.h、cuda_memory.h 与 cuda_arrow_ipc.h行为验证见 python/pyarrow/tests/test_cuda.py。延伸学习可参考 Numba 官方文档中的 CUDA 支持章节见原文档 docs/source/python/integration/cuda.rst 的 seealso 部分。【免费下载链接】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),仅供参考
返回列表