如何优化Python NumPy中大型批量张量交叉并矢积的性能?
我需要在Python(NumPy)中计算两个二阶张量的次对称和主对称交叉并矢积,公式如下:
C[i,j,k,l] = ( A[i, k] * B[j, l] + A[i, l] * B[k, j] + B[i, k] * A[j, l] + B[i, l] * A[k, j] ) / 4
我有两个形状为(3, 3, n)的二阶张量,最后一个轴为批量维度,且以Voigt符号缩减向量存储(只存上三角元素,顺序为主对角线开始:[C00, C11, C22, C01, C12, C02]),示例转换代码如下:
import numpy as np # 生成随机3x3张量 F = np.eye(3).reshape(3, 3, 1) + np.random.rand(3, 3, 100000) / 10 # 得到对称二阶张量 C = F.T @ F # 转换为Voigt格式向量 i = [0, 1, 2, 0, 1, 0] j = [0, 1, 2, 1, 2, 2] A = C[i, j]
我在自己的Python包hyperelastic中实现了该功能,当前代码如下,耗时约100ms(测试环境下):
import numpy as np def cdya(A, B): i, j = [a.ravel() for a in np.indices((6, 6))] a = np.array([(0, 0), (1, 1), (2, 2), (0, 1), (1, 2), (0, 2)]) b = np.array([0, 3, 5, 3, 1, 4, 5, 4, 2]).reshape(3, 3) i, j, k, l = np.hstack([a[i], a[j]]).T ik = b[i, k].reshape(6, 6) jl = b[j, l].reshape(6, 6) il = b[i, l].reshape(6, 6) kj = b[k, j].reshape(6, 6) C = (A[ik] * B[jl] + A[il] * B[kj]) / 2 if A is not B: C += (B[ik] * A[jl] + B[il] * A[kj]) / 2 C /= 2 return C A = np.random.rand(6, 100000) B = np.random.rand(6, 100000) C = cdya(A, B)
作为对比,使用全张量存储+np.einsum的等效实现耗时约160ms:
A = np.random.rand(3, 3, 100000) B = np.random.rand(3, 3, 100000) C = ( np.einsum("ij...,kl...->ikjl...", A, B) + np.einsum("ij...,kl...->ilkj...", A, B) + np.einsum("ij...,kl...->ikjl...", B, A) + np.einsum("ij...,kl...->ilkj...", B, A) ) / 4
想请教:当前Voigt格式的实现是否还有优化空间?还是已经达到了无JIT情况下的较好性能水平?
当前实现的性能定位
你的Voigt格式实现已经比全张量einsum版本快60%,在无JIT(如Numba)的纯NumPy场景下,这个表现已经属于较好的性能水平——你充分利用了Voigt格式的维度缩减(从3x3降到6维),避免了全张量的冗余计算,而且通过预计算索引映射减少了运行时的动态计算开销。
可优化的细节方向
如果想进一步压榨性能,可以从以下几个点入手:
预计算所有固定索引,避免函数内重复计算
当前函数每次调用都会重新生成i,j、a、b、ik/jl/il/kj这些固定不变的索引矩阵。可以把这些常量提前定义在函数外部,消除重复计算的开销:# 预计算所有固定索引,放在函数外部 _i, _j = [a.ravel() for a in np.indices((6, 6))] _a = np.array([(0, 0), (1, 1), (2, 2), (0, 1), (1, 2), (0, 2)]) _b = np.array([0, 3, 5, 3, 1, 4, 5, 4, 2]).reshape(3, 3) _i_j_k_l = np.hstack([_a[_i], _a[_j]]).T _ik = _b[_i_j_k_l[0], _i_j_k_l[2]].reshape(6, 6) _jl = _b[_i_j_k_l[1], _i_j_k_l[3]].reshape(6, 6) _il = _b[_i_j_k_l[0], _i_j_k_l[3]].reshape(6, 6) _kj = _b[_i_j_k_l[2], _i_j_k_l[1]].reshape(6, 6) def cdya(A, B): C = (A[_ik] * B[_jl] + A[_il] * B[_kj]) / 2 if A is not B: C = (A[_ik] * B[_jl] + A[_il] * B[_kj] + B[_ik] * A[_jl] + B[_il] * A[_kj]) / 4 return C这个改动对高频调用的场景收益明显。
合并运算步骤,减少浮点操作次数
当A != B时,当前代码是先加两组项再除以2,可直接合并为一次除法操作,减少额外浮点运算:if A is not B: C = (A[_ik] * B[_jl] + A[_il] * B[_kj] + B[_ik] * A[_jl] + B[_il] * A[_kj]) / 4确保输入数组连续
如果输入的A或B是非连续内存的数组(比如切片生成的),可以在函数开头添加A = np.ascontiguousarray(A)、B = np.ascontiguousarray(B),确保NumPy的索引操作是高效的连续内存访问。
总结
你的当前实现已经非常接近纯NumPy环境下的性能天花板,上述优化属于"边际收益"级别的调整,能把耗时再降低10%-20%左右,但不会有数量级的提升。如果追求更高性能,需要引入JIT工具,但你明确要求避免的话,现有方案已经足够优秀。
内容的提问来源于stack exchange,提问作者adtzlr

