ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

NumPy 广义通用函数(gufunc)C-API 完整指南:从签名语法到核心维度处理

NumPy 广义通用函数(gufunc)C-API 完整指南:从签名语法到核心维度处理 科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载导读广义通用函数Generalized universal functions简称 gufunc是 NumPy 对通用函数ufunc的重要扩展普通 ufunc 的初等函数只能做标量对标量的逐元素运算而 gufunc 允许初等函数作用于子数组对子数组例如向量内积、矩阵乘法、卷积等。本指南以 NumPy 官方 C-API 文档为核心系统讲解 gufunc 的签名signature语法、核心维度core dimension与循环维度loop dimension的匹配规则、C 语言实现路径PyUFunc_FromFuncAndDataAndSignature以及通过process_core_dims_func钩子定制输出形状计算的完整方法。读完本文你将掌握为 NumPy 编写自定义 gufunc 的全部技术要点并能结合仓库源码理解其底层实现。本文对应的官方文档位于 doc/source/reference/c-api/generalized-ufuncs.rst建议结合 ufuncs 参考指南 一并阅读。从 ufunc 到 gufunc突破逐元素的限制在实际计算中我们需要的循环往往不止于标量上的运算还包括向量或子数组上的运算。NumPy 通过将通用函数ufunc泛化来实现这一需求普通 ufunc初等函数被限制为逐元素element-by-element操作即每个输入输出都是标量核心维度为零广义版本gufunc支持子数组对子数组sub-array by sub-array的操作。Perl 的向量库 PDL 提供了类似功能NumPy 在 gufunc 中沿用了其相关术语。每个 gufunc 都携带一组关联信息指明其输入、输出各自的核心维度数逐元素的 ufunc 核心维度为零。所有参数的核心维度列表称为该 ufunc 的签名signature。例如numpy.add的签名是(),()-()定义了两个标量输入和一个标量输出。再如inner1d(a, b)签名为(i),(i)-()它沿每个输入的最后一个轴做内积同时保持其余索引不变。若a的形状为(3, 5, N)、b的形状为(5, N)则输出形状为(3, 5)底层初等函数会被调用3 * 5次。签名中为每个输入指定了一个核心维度(i)为输出指定了零个核心维度()因为该初等函数接收两个一维数组、返回一个标量。通过复用相同的名字i我们规定两个对应维度必须具有相同大小。超出核心维度之外的那些维度被称为循环维度loop dimensions上述例子中即(3, 5)。核心术语定义术语含义初等函数Elementary Function每个 ufunc 由执行最基础操作的初等函数构成例如两数相加是两数组相加的最基础操作。ufunc 在数组的不同部分多次调用该初等函数。初等函数的输入/输出可以是向量例如inner1d的初等函数接收两个向量作为输入。签名Signature描述 ufunc 初等函数输入/输出维度的字符串详见下一节。核心维度Core Dimension初等函数每个输入/输出的维度数由其核心维度定义零个核心维度对应标量输入/输出。核心维度被映射到输入/输出数组的最后几个维度上。维度名Dimension Name签名中代表一个核心维度的名字。不同维度可以共享同一个名字表示它们大小相同。维度索引Dimension Index代表维度名的整数按照各名字在签名中首次出现的顺序枚举。循环维度Loop Dimensions核心维度之外的维度在各输入之间按广播规则对齐。签名Signature详解签名定义了输入/输出变量的核心维度数从而也定义了维度的收缩contraction方式。签名由如下格式的字符串表示每个输入或输出数组的核心维度用括号中的维度名列表表示即(i_1,...,i_N)标量输入/输出记为()。除了i_1、i_2等之外可以使用任何合法的 Python 变量名不同参数的维度列表之间用,分隔输入与输出参数之间用-分隔如果在多处使用同一个维度名则强制要求这些对应维度的大小相同。签名的形式化语法BNF如下Signature :: Input arguments - Output arguments Input arguments :: Argument list Output arguments :: Argument list Argument list :: nil | Argument | Argument , Argument list Argument :: ( Core dimension list ) Core dimension list :: nil | Core dimension | Core dimension , Core dimension list Core dimension :: Dimension name Dimension modifier Dimension name :: valid Python variable name | valid integer Dimension modifier :: nil | ?签名解析的底层实现在仓库源码 numpy/_core/src/umath/ufunc_object.c 中函数_parse_signatureL309 起负责将签名字符串解析为PyUFuncObject的内部结构它逐字符跳过空白、校验-分隔符与(/)括号配对并为每个维度名分配一个全局的维度索引对应结构中的core_num_dim_ix。解析时对同一名字是否重复出现会做一致性检查签名有语法错误时例如缺少括号、缺少输入参数会设置parse_error并返回 -1。这正是文档中白空格被忽略同一维度名强制同大小等规则在实现层面的落点。签名规则注意事项引号仅为表述清晰所用无修饰符的、共享同一名字的核心维度必须具有相同大小。每个维度名通常对应初等函数实现中的一层循环白空格会被忽略整数作为维度名会把该维度冻结为该数值若名字带有?修饰符则该维度只有在所有共享它的输入和输出上均存在时才作为核心维度否则它会被忽略并为初等函数替换为大小为 1 的维度。常见签名示例名称签名常见用途add(),()-()二元 ufuncsum1d(i)-()归约reductioninner1d(i),(i)-()向量-向量乘法matmat(m,n),(n,p)-(m,p)矩阵乘法vecmat(n),(n,p)-(p)向量-矩阵乘法matvec(m,n),(n)-(m)矩阵-向量乘法matmul(m?,n),(n,p?)-(m?,p?)上述四种的组合outer_inner(i,t),(j,t)-(i,j)在最后一个维度上做内积、在倒数第二个维度上做外积其余维度循环/广播cross1d(3),(3)-(3)叉积其中最后一个维度被冻结且必须为 3这些签名并非纸上谈兵——在仓库的测试扩展模块 numpy/_core/src/umath/_umath_tests.c.src 中可以看到真实定义inner1d_signature (i),(i)-()L84、matrix_multiply_signature (m,n),(n,p)-(m,p)L154、matmul_signature (m?,n),(n,p?)-(m?,p?)L156、cross1d_signature (3),(3)-(3)L224、euclidean_pdist_signature (n,d)-(p)L263以及cumsum_signature (i)-(i)L321。冻结维度Frozen Dimensions与性能最后一个例子cross1d的(3),(3)-(3)是冻结核心维度的实例维度大小被写死为 3。冻结核心维度可以提升 ufunc 的性能因为它让实现者可以为该固定大小编写专门优化的代码路径并在编译期/运行期省去一部分维度检查与通用循环逻辑。核心维度与循环维度的匹配规则签名决定了每个输入/输出数组的维度如何被切分为核心维度与循环维度签名中的每个维度按形状元组的末尾向前与传入数组的维度一一匹配。这些是核心维度必须存在于数组中否则会报错签名的同一标签对应的核心维度如inner1d签名(i),(i)-()中的i必须大小完全一致不进行广播从所有输入中移除核心维度后其余维度一起按广播规则对齐定义出循环维度每个输出的形状由循环维度 该输出的核心维度共同决定。输出核心维度未确定的情况通常输出中所有核心维度的大小由输入数组中具有相同标签的核心维度决定。但这并非硬性要求——可以定义某个标签首次出现在输出中的签名不过调用这类函数时需要格外小心。典型例子是函数euclidean_pdist(a)签名为(n,d)-(p)给定n个d维向量计算所有唯一成对欧氏距离。输出维度p必须等于n * (n - 1) / 2但默认情况下由调用者负责传入大小正确的输出数组。若某个输出核心维度的大小无法从传入的输入或输出数组推导出来会抛出异常。从源码看这一检查位于 numpy/_core/src/umath/ufunc_object.c 的 L1656-L1680在调用process_core_dims_func之后代码会遍历所有输出操作数的核心维度若发现core_dim_sizes[core_dim_index] 0从未被指定则抛出ValueError: Output operand ... has core dimension ... unspecified。同文件 L1630-L1644 则实现了同一标签核心维度大小必须精确匹配否则报错的校验逻辑。想要让这类未确定维度自动计算可以通过定义PyUFunc_ProcessCoreDimsFunc函数并赋值给PyUFuncObject结构体的process_core_dims_func字段来改变默认行为详见后文。历史行为差异NumPy 1.10 之前需要特别注意的是在 NumPy 1.10.0 之前检查规则宽松得多缺失的核心维度会通过在形状前前置 1的方式补出来具有相同标签的核心维度会被广播到一起未确定的维度会被创建为大小为 1。从 1.10.0 开始这些宽松行为被收紧为上面介绍的严格规则因此面向新版本编写的 gufunc 不应依赖旧的自动补维行为。用 C-API 实现 gufunc创建入口PyUFunc_FromFuncAndDataAndSignature现有的接口保持不变PyUFunc_FromFuncAndData仍可用于实现专门的仅含标量初等函数的 ufunc。若要声明更通用的 ufunc可使用PyUFunc_FromFuncAndDataAndSignature其参数列表与PyUFunc_FromFuncAndData相同只是额外多一个以 C 字符串形式指定签名的参数。此外仓库中还提供了带恒等值identity的变体PyUFunc_FromFuncAndDataAndSignatureAndIdentity。在 numpy/_core/code_generators/numpy_api.py 的 API 清单中可以看到PyUFunc_FromFuncAndDataAndSignature的 API 编号为 31PyUFunc_FromFuncAndDataAndSignatureAndIdentity的 API 编号为 42且标记为MinVersion(1.16)即 NumPy 1.16 起可用。这两个符号的实际实现位于 numpy/_core/src/umath/ufunc_object.c 的 L5340-L5420 附近后者在内部会调用前者并负责初始化process_core_dims_func NULL等字段。回调函数初等函数的 C 接口回调函数的类型与标量 ufunc 时代相同void (*foo)(char **args, intp *dimensions, intp *steps, void *func)调用时args长度为nargs的列表包含所有输入/输出参数的数据指针steps对于标量初等函数长度同样为nargs表示各参数使用的步长stridedimensions指向单个整数的指针定义要循环的轴的大小。对于非平凡签名dimensions从第二个条目开始还会包含各核心维度的大小。每个唯一的维度名只提供一个大小且大小按该维度名在签名中首次出现的顺序给出。steps的前nargs个元素与标量 ufunc 一致其后的元素按顺序包含所有参数的所有核心维度的步长。以签名为(i,j),(i)-()的 ufunc 为例args将包含三个指针分别指向输入/输出数组a、b、c的数据dimensions为[N, I, J]N是循环大小I和J分别是核心维度i和j的大小steps为[a_N, b_N, c_N, a_i, a_j, b_i]包含所需的全部步长。这与PyUFuncObject结构体中的内部字段一一对应在 numpy/_core/include/numpy/ufuncobject.h 中可以找到core_enabled0 为标量 ufunc1 为 gufunc、core_num_dim_ix签名中不同维度名的数量、core_num_dims每个参数的核心维度个数、core_dim_ixs扁平化的维度索引、core_offsets每个参数第一个核心维度在core_dim_ixs中的位置等价于cumsum(core_num_dims)以及core_signature用于打印的签名字符串等字段L145-L170。定制核心维度处理process_core_dims_func可选函数PyUFunc_ProcessCoreDimsFunc存储在 ufunc 的process_core_dims_func属性上为 gufunc 作者提供了处理传入数组核心维度的钩子。其函数签名定义为numpy/_core/include/numpy/ufuncobject.htypedef int (PyUFunc_ProcessCoreDimsFunc)( struct _tagPyUFuncObject *ufunc, npy_intp *core_dim_sizes);该钩子有两个主要用途校验约束检查 gufunc 对核心维度施加的约束是否满足不满足则设置异常计算输出形状为未被输入数组确定的输出核心维度计算形状。在钩子收到的core_dim_sizes数组中未被输入确定的维度值为 -1函数可以基于输入中出现的核心维度将其替换为合适的值。该钩子在内核中的调用点位于 numpy/_core/src/umath/ufunc_object.c 的 L1649-L1654先完成所有操作数核心维度大小的采集与同标签一致性检查然后调用ufunc-process_core_dims_func(ufunc, core_dim_sizes)若返回值非 0 则整体失败返回 -1。示例一minmax—— 用钩子校验约束考虑签名(n)-(2)的 gufuncminmax它同时计算一个序列的最小值和最大值。由于长度为 0 的序列的最小/最大值没有意义它应要求n 0。可以这样实现int minmax_process_core_dims(PyUFuncObject *ufunc, npy_intp *core_dim_sizes) { npy_intp n core_dim_sizes[0]; if (n 0) { PyErr_SetString(PyExc_ValueError, minmax requires the core dimension to be at least 1.); return -1; } return 0; }此例中core_dim_sizes数组长度为 2。第二个值恒为 2签名里写死的输出维度因此无需检查核心维度n存储在第一个元素中。当发现n为 0 时函数设置异常并返回 -1。示例二conv1d—— 用钩子计算输出形状考虑 gufuncconv1d其初等函数计算两个一维数组x、y长度分别为m、n的full卷积输出长度为m n - 1。实现时签名设为(m),(n)-(p)在钩子中若发现核心维度p为 -1则将其替换为m n - 1若p不是 -1则必须校验给定值等于m n - 1否则设置异常并返回 -1为保证结果有意义还要求m n至少为 1即两个输入不能同时长度为 0。参考实现如下int conv1d_process_core_dims(PyUFuncObject *ufunc, npy_intp *core_dim_sizes) { // core_dim_sizes will hold the core dimensions [m, n, p]. // p will be -1 if the caller did not provide the out argument. npy_intp m core_dim_sizes[0]; npy_intp n core_dim_sizes[1]; npy_intp p core_dim_sizes[2]; npy_intp required_p m n - 1; if (m 0 n 0) { // Disallow both inputs having length 0. PyErr_SetString(PyExc_ValueError, conv1d: both inputs have core dimension 0; the function requires that at least one input has size greater than 0.); return -1; } if (p -1) { // Output array was not given in the call of the ufunc. // Set the correct output size here. core_dim_sizes[2] required_p; return 0; } // An output array *was* given. Validate its core dimension. if (p ! required_p) { PyErr_Format(PyExc_ValueError, conv1d: the core dimension p of the out parameter does not equal m n - 1, where m and n are the core dimensions of the inputs x and y; got m%zd and n%zd so p must be %zd, but got p%zd., m, n, required_p, p); return -1; } return 0; }钩子函数**绝不能修改 core_dim_sizes 中输入值不是 -1 的条目**。修改非 -1 的值通常会导致 ufunc 输出错误甚至可能导致 Python 解释器崩溃。仓库中的真实 gufunc 案例除上述文档示例外NumPy 仓库本身大量使用 gufunc线性代数模块numpy/linalg/umath_linalg.cpp 使用带形状签名的 gufunc 实现矩阵运算numpy.matmul的签名即文档示例表中的(m?,n),(n,p?)-(m?,p?)通过?修饰符让批处理维度可选从而统一了向量/矩阵乘法的四种组合测试扩展模块numpy/_core/src/umath/_umath_tests.c.src 提供了inner1d、innerwt、matrix_multiply、cross1d、euclidean_pdist、cumsum等一批用于验证 gufunc 机制的示例实现vecdot的签名可以从测试中直接验证在 numpy/_core/tests/test_ufunc.py 的test_get_signatureL457-L458中np.vecdot.signature被断言等于(n),(n)-()签名解析与失败路径同文件中的test_signature0至test_signature10系列L335-L439覆盖了空核心签名、matrix_multiply签名、matmul签名等多种输入下的解析结果test_signature_failure_extra_parenthesis、test_signature_failure_mismatching_parenthesis、test_signature_failure_signature_missing_input_arg、test_signature_failure_signature_missing_output_argL441-L455则验证了非法签名如((i)),(i)-()、(i),)i(-()、缺少输入或输出参数会被正确拒绝。这些测试同时印证了文档中签名语法规则在实现层的严格落地。总结与兼容性要点广义通用函数是 NumPy 把循环从标量提升到子数组层面的核心机制其关键要素可归纳为签名是 gufunc 的灵魂用(i,j),(k)-(l)形式描述核心维度结构支持共享标签、整数冻结维度与?可选维度维度匹配遵循末尾对齐、同标签严格同大小、其余维度广播的规则C-API入口为PyUFunc_FromFuncAndDataAndSignature以及 1.16 起可用的...AndIdentity变体回调函数通过dimensions/steps数组获知循环大小、核心维度大小与全部步长process_core_dims_func钩子用于校验核心维度约束、计算未确定的输出核心维度但严禁改写非 -1 的值。最后兼容性提醒NumPy 1.10.0 之前对缺失核心维度的补 1、同标签广播、未确定维度置 1 等宽松行为已被移除新编写的 gufunc 必须显式满足全部核心维度约束需要输出维度推导时请使用process_core_dims_func钩子。更完整的参考信息可继续阅读 ufuncs 参考指南 与官方 C-API 文档 generalized-ufuncs.rst。赞分享科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载相关推荐NumPy NEP 20 深度解析广义 ufuncgufunc签名扩展——固定维度、可缺失维度与可广播维度NumPy NEP 20 深度解析广义 ufuncgufunc签名扩展——固定维度、可缺失维度与可广播维度 导读 本篇文章以 NumPy 官方设计提案 N科学计算数据分析NumPy 广义 ufuncGeneralized Universal Functions完全指南从 NEP 5 签名语法到 C 级实现原理NumPy 广义 ufuncGeneralized Universal Functions完全指南从 NEP 5 签名语法到 C 级实现原理 本篇指南以科学计算数据分析NumPy 1.16.0 版本深度解析__array_function__ 协议落地、gufunc 签名扩展与核心架构重构NumPy 1.16.0 版本深度解析 __array_function__ 协议落地、gufunc 签名扩展与核心架构重构 本文基于本仓库 doc/chan科学计算数据分析上一篇深入理解acts_as_commentable_with_threading源码从生成器到模型关联的实现原理下一篇解决跨浏览器Flex布局间隙兼容性问题的PostCSS方案实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表