ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Numba CUDA 原子操作(cuda.atomic)完全指南:从 API 用法到 LLVM 底层实现

Numba CUDA 原子操作(cuda.atomic)完全指南:从 API 用法到 LLVM 底层实现 编译器高性能计算【免费下载链接】numbaNumPy aware dynamic Python compiler using LLVM项目地址https://gitcode.com/gh_mirrors/nu/numba点击查看免费下载本指南以 Numba 官方文档 docs/source/cuda/intrinsics.rst 为骨架系统讲解 Numba CUDA 目标中受支持的原子操作cuda.atomic命名空间下每一个操作的功能、签名、支持的数据类型与返回值语义并结合仓库源码类型注册、LLVM lowering、NVVM IR 模板剖析其底层实现原理。读完本文你将能够熟练使用原子操作在 GPU kernel 中安全地完成求和、极值、位运算、自增自减与 CAS比较交换等并发更新并理解这些操作在编译链路中如何被翻译为底层指令。一、什么是 Numba CUDA 原子操作在 CUDA 编程中当大量线程并发地读写同一块内存位置时普通指令会产生数据竞争race condition结果不确定。原子操作Atomic Operation保证读-改-写三步以不可分割的方式完成是 GPU 上实现直方图统计、归约reduction、计数器、锁等并发算法的基石。Numba 的 CUDA 目标在cuda.atomic命名空间下提供了对 CUDA 原子操作子集的访问所有操作均作用于设备端数组可以是全局内存数组也可以结合共享内存数组使用。原文档明确指出Numba provides access to some of the atomic operations supported in CUDA即只实现了 CUDA 原子操作的一部分——具体支持哪些、支持哪些类型正是本文接下来要展开的内容。需要说明的是该文档页首包含.. cuda-deprecated::指令定义于 docs/source/conf.py渲染后会显示 CUDA Built-in Target deprecation notice 警告Numba 内置的 CUDA 目标的后续开发已迁移至 NVIDIA 维护的 numba-cuda 包。本文内容基于当前仓库的既有实现迁移后 API 语义大体保持一致。二、cuda.atomic 完整 API 一览与支持矩阵原文档通过 Sphinxautomodule:: numba.cuda:members: atomic指令自动从 numba/cuda/stubs.py 提取atomic命名空间下所有操作的 docstring 作为文档正文。这些操作的语义、类型限制与返回值约定如下操作调用签名原子语义支持的数据类型cuda.atomic.addadd(ary, idx, val)ary[idx] valfloat64, float32, int32, uint32, int64, uint64cuda.atomic.subsub(ary, idx, val)ary[idx] - valfloat64, float32, int32, uint32, int64, uint64cuda.atomic.and_and_(ary, idx, val)ary[idx] valint32, uint32, int64, uint64cuda.atomic.or_or_(ary, idx, val)ary[idx] \| valint32, uint32, int64, uint64cuda.atomic.xorxor(ary, idx, val)ary[idx] ^ valint32, uint32, int64, uint64cuda.atomic.incinc(ary, idx, val)ary[idx] 1达到val后回绕为 0uint32, uint64cuda.atomic.decdec(ary, idx, val)ary[idx] (value if ary[idx]0 or ary[idx]value else ary[idx]-1)uint32, uint64cuda.atomic.exchexch(ary, idx, val)ary[idx] val交换int32, uint32, int64, uint64cuda.atomic.maxmax(ary, idx, val)ary[idx] max(ary[idx], val)float64, float32, int32, uint32, int64, uint64cuda.atomic.minmin(ary, idx, val)ary[idx] min(ary[idx], val)float64, float32, int32, uint32, int64, uint64cuda.atomic.nanmaxnanmax(ary, idx, val)ary[idx] max(ary[idx], val)NaN 视为缺失值float64, float32, int32, uint32, int64, uint64cuda.atomic.nanminnanmin(ary, idx, val)ary[idx] min(ary[idx], val)NaN 视为缺失值float64, float32, int32, uint32, int64, uint64cuda.atomic.compare_and_swapcompare_and_swap(ary, old, val)当ary[0] old时赋值为val仅一维数组int32, uint32, int64, uint64cuda.atomic.cascas(ary, idx, old, val)当ary[idx] old时赋值为val任意维度int32, uint32, int64, uint64所有操作均返回旧值old value即如同原子加载一样读取到的、本次更新之前该位置的值——stub docstring 中对每个操作都重复强调了这一点见 numba/cuda/stubs.py。这一约定对实现无锁算法非常关键你可以通过返回值判断本次操作是否成功竞争。2.1 类型矩阵来自类型注册表上表的类型支持范围并非随意编写而是由 numba/cuda/cudadecl.py 中三组类型元组精确控制all_numba_types (types.float64, types.float32, types.int32, types.uint32, types.int64, types.uint64) # 浮点 有/无符号整数 integer_numba_types (types.int32, types.uint32, types.int64, types.uint64) # 仅整数 unsigned_int_numba_types (types.uint32, types.uint64) # 仅无符号整数随后通过工厂函数_gen(l_key, supported_types)批量生成各操作的 typing 模板Cuda_atomic_add _gen(cuda.atomic.add, all_numba_types) # 加减、极值 Cuda_atomic_and _gen(cuda.atomic.and_, integer_numba_types) # 位运算、交换 Cuda_atomic_inc _gen(cuda.atomic.inc, unsigned_int_numba_types) # 自增/自减Cuda_atomic_compare_and_swap与Cuda_atomic_cas则单独注册cudadecl.pycompare_and_swap只接受一维数组cas支持任意维度。这些模板类最终由CudaAtomicTemplate属性模板挂载到cuda.atomic模块上cudadecl.py从而在类型推断阶段被解析为对应的types.Function。2.2 类型/维度不匹配时的行为_gen生成的generic()方法会做两层检查cudadecl.py若ary.dtype不在该操作的支持类型元组内直接返回None类型推断失败最终报出 Untyped global name / 类型错误若数组为一维签名固定为(ary.dtype, ary, types.intp, ary.dtype)若维度大于一维第三个参数索引类型为元组。在 lowering 阶段若遇到不支持的类型实现会显式抛出TypeError例如ptx_atomic_inc中Unimplemented atomic inc with %s arraynumba/cuda/cudaimpl.py。三、实战示例一一维数组原子求最大值原文档给出的第一个示例是用cuda.atomic.max并行求一个数组的最大值。这里完整保留原示例并加以注释from numba import cuda import numpy as np cuda.jit def max_example(result, values): Find the maximum value in values and store in result[0] tid cuda.threadIdx.x bid cuda.blockIdx.x bdim cuda.blockDim.x i (bid * bdim) tid cuda.atomic.max(result, 0, values[i]) arr np.random.rand(16384) result np.zeros(1, dtypenp.float64) max_example256, 64 print(result[0]) # Found using cuda.atomic.max print(max(arr)) # Print max(arr) for comparison (should be equal!)要点拆解索引计算i (bid * bdim) tid是 CUDA 一维网格的经典全局线程号公式。此处启动配置max_example[256, 64]表示 256 个 block、每 block 64 个线程共 16384 个线程恰好与arr的元素数一一对应每个线程把自己的values[i]原子地竞选写入result[0]。目标位置是标量式索引cuda.atomic.max(result, 0, values[i])中idx0表示所有线程都竞争同一个元素result[0]。验证方式由于cuda.atomic.max的语义是ary[idx] max(ary[idx], val)无论线程以何种顺序到达result[0]最终一定等于max(arr)程序末尾与 NumPy 的max(arr)对比验证一致性。原文档特别提醒这不是求最大值的最高效方式但它直观地展示了原子操作的工作方式。实际高性能场景通常先用 per-block 共享内存归约、再做 block 间原子归约参考 numba/cuda/tests/cudapy/test_atomics.py 中共享内存版本的测试写法。四、实战示例二多维数组与元组索引原文档指出多维数组通过传一个整数元组作为索引来支持。第二个示例展示了三维情形cuda.jit def max_example_3d(result, values): Find the maximum value in values and store in result[0]. Both result and values are 3d arrays. i, j, k cuda.grid(3) # Atomically store to result[0,1,2] from values[i, j, k] cuda.atomic.max(result, (0, 1, 2), values[i, j, k]) arr np.random.rand(1000).reshape(10, 10, 10) result np.zeros((3, 3, 3), dtypenp.float64) max_example_3d(2, 2, 2), (5, 5, 5) print(result[0, 1, 2], , np.max(arr))要点拆解cuda.grid(3)返回三维全局线程坐标(i, j, k)values[i, j, k]是每个线程负责的元素cuda.atomic.max(result, (0, 1, 2), values[i, j, k])中索引参数是(0, 1, 2)这个元组所有线程都把最大值写入三维数组result的固定位置[0, 1, 2]启动配置(2, 2, 2)个 block、每个(5, 5, 5)个线程共 1000 个线程与 1000 个元素的arr一一对应。4.1 元组索引在源码中的处理无论是一维的标量int索引还是多维的元组索引lowering 层都由_atomic_dispatcher统一处理numba/cuda/cudaimpl.pylower(stubs.atomic.add, types.Array, types.intp, types.Any) lower(stubs.atomic.add, types.Array, types.UniTuple, types.Any) lower(stubs.atomic.add, types.Array, types.Tuple, types.Any) _atomic_dispatcher def ptx_atomic_add_tuple(...): ..._atomic_dispatcher调用_normalize_indicescudaimpl.py将标量索引包装成单元素元组、把元组各分量 cast 为intp、并校验索引维度与数组维度一致随后通过cgutils.get_item_pointer(..., wraparoundTrue)计算目标内存地址再按 dtype 分派到具体的原子指令实现。这就是一维传 int、多维传 tuple的统一入口。五、底层实现原理从 atomic_rmw 到 NVVM IR 模板Numba 的 CUDA 原子操作不是凭空魔法它们最终落在两类底层机制上LLVM 的 read-modify-writeRMW内建指令以及Numba 自带的 NVVM IR 内联模板。5.1 整数原子操作直接映射 LLVM atomic_rmw整数类型的add、sub、位运算、exch、max/min等直接调用 LLVM builder 的atomic_rmw# add 的整数分支numba/cuda/cudaimpl.py#L749-L750 return builder.atomic_rmw(add, ptr, val, monotonic) # max有符号/无符号使用不同 RMW 操作码numba/cuda/cudaimpl.py#L838-L841 elif dtype in (types.int32, types.int64): return builder.atomic_rmw(max, ptr, val, orderingmonotonic) elif dtype in (types.uint32, types.uint64): return builder.atomic_rmw(umax, ptr, val, orderingmonotonic)注意到无符号极值使用了umax/umin操作码与有符号max/min区分位运算and_/or_/xor与交换exch则由同一个工厂ptx_atomic_bitwise和ptx_atomic_exch以and/or/xor/xchg生成cudaimpl.py。所有原子 RMW 的内存序统一为monotonic。5.2 浮点原子操作NVVM 声明函数 IR 模板替换CUDA 的浮点atomicAdd/atomicMax/atomicMin等并非所有架构都由硬件原生支持因此 numba/cuda/nvvmutils.py 为 float32/float64 的max/min/nanmax/nanmin以及 int32/int64 的inc/dec声明了___numba_atomic_*系列外部函数。编译时numba/cuda/cudadrv/nvvm.py 的llvm_replace()将这些声明字符串替换为内联的 NVVM IR 实现模板例如ir_numba_atomic_minmax_templatenvvm.py。以浮点极值为例模板的关键逻辑是早期返回对于普通min/max若val或当前值任一为 NaN直接返回fcmp uno检查NANOPnnan olt/nnan ogt对于nanmax/nanminNaN 被当作缺失值处理NANnanOPult/ugt行为恰如 stub docstring 所述nanmax(NaN, n) n、nanmin(n, NaN) n核心循环lt_check比较当前值是否小于/大于val→attempt用 CAS 尝试把新值写入bitcast到整型以便比较交换→ 失败则重试直到 CAS 成功。inc/dec也使用类似的 CAS 重试循环nvvm.pyinc在old val时自增、否则回绕为 0dec在old 0或old val时保持val否则减 1。5.3 CAS 与 compare_and_swapptx_atomic_cascudaimpl.py通过nvvmutils.atomic_cmpxchg生成cmpxchg指令仅支持整型ptx_atomic_compare_and_swap则只是cas的特例把索引固定为常量0后复用同一实现cudaimpl.py。CAS 是所有比较-交换类无锁原语锁、自旋、链表插入的基础也是上面浮点极值模板内部依赖的构件。六、测试验证仓库如何保证原子操作正确性仓库在 numba/cuda/tests/cudapy/test_atomics.py 中提供了完整的TestCudaAtomics测试套件覆盖本文表格中的全部操作。测试设计具有很强的参考价值多维度覆盖通过gen_atomic_extreme_funcstest_atomics.py为max/min/nanmax/nanmin批量生成 1D 索引、2D 规范化索引、单索引、共享内存四类变体 kernel索引边界测试atomic_binary_1dim_shared等 device 函数中对索引取模并用neg_idx参数验证负索引的包装行为共享内存测试atomic_binary_1dim_shared2演示原子操作同样适用于cuda.shared.array这是块内归约的惯用法数值对比每个测试都与 NumPy 的等价计算结果比对例如用np.max验证atomic_max的结果与文档示例的验证思路一致。阅读这些测试是快速掌握原子操作正确用法的捷径也是把示例代码迁移到生产项目前的第一步验证。七、注意事项与最佳实践只对数组操作cuda.atomic系列函数的第一个参数必须是设备端数组全局内存或共享内存不支持标量变量多个线程竞争同一地址时才能体现原子性。返回旧值所有操作返回原子读取的旧值。需要实现谁赢了竞争之类的逻辑时用返回值判断即可无需额外加锁。类型必须匹配val的类型必须与数组 dtype 一致否则 lowering 阶段会抛出TypeError浮点/整数/无符号整数的支持范围不同写代码前对照上文的类型矩阵。性能考量原子操作以串行化竞争为代价换取正确性竞争激烈时是性能瓶颈。文档示例也强调这不是最有效率的方式——大数据量归约建议先做共享内存分块归约再用原子操作合并块级结果。内存序实现中统一使用monotonic内存序cudaimpl.py满足最终一致的计数/归约需求若需更强的跨线程可见性保证需要结合cuda.syncthreads()或内存栅栏自行组织。NaN 语义差异普通max/min遇 NaN 行为与 CUDA 硬件语义一致NaN 可能胜出而nanmax/nanmin把 NaN 当作缺失值忽略——处理含 NaN 数据时务必按需选对函数。弃用提示如开篇所述Numba 内置 CUDA 目标的后续开发已迁移到 NVIDIA numba-cuda 包新项目可关注该迁移状态见 docs/source/cuda/index.rst 中的弃用说明。八、进一步阅读完整 API 定义与 docstringnumba/cuda/stubs.py类型注册与签名推断numba/cuda/cudadecl.pyLLVM lowering 实现numba/cuda/cudaimpl.pyNVVM IR 内联模板numba/cuda/cudadrv/nvvm.py原子操作声明函数numba/cuda/nvvmutils.py完整测试套件numba/cuda/tests/cudapy/test_atomics.py赞分享编译器高性能计算【免费下载链接】numbaNumPy aware dynamic Python compiler using LLVM项目地址https://gitcode.com/gh_mirrors/nu/numba点击查看免费下载相关推荐Julia 多线程与原子操作 API 全指南从 threads、spawn 到 Threads.Atomic 与底层同步原语Julia 多线程与原子操作 API 全指南从 threads 、 spawn 到 Threads.Atomic 与底层同步原语 本文以 Julia 标准编程语言编译器语言运行时标准库JIT编译RxJava 阻塞式 Observable 操作符完全指南blockingFirst 到 blockingIterable 的用法与底层实现RxJava 阻塞式 Observable 操作符完全指南blockingFirst 到 blockingIterable 的用法与底层实现 本文以 RxJa后端异步编程Numba CUDA 设备函数Device Function编写指南cuda.jit(deviceTrue) 的用法、限制与底层原理Numba CUDA 设备函数Device Function编写指南 cuda.jit deviceTrue 的用法、限制与底层原理 本文以 Numb编译器高性能计算创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表