You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Numpy einsum在特定问题规模后性能骤降原因排查

关于NumPy einsum方法A性能骤降的问题

运行该脚本仅需依赖numpy和matplotlib。

import numpy as np
import timeit
import matplotlib.pyplot as plt

n1, n2, n3, n4, n5, m = (101, 33, 1, 32, 2, 32)


def make_arrays(aOrder, bOrder, cOrder):
    a = np.random.randn(n1, m) + 1j * np.random.randn(n1, m)
    b = np.random.randn(n2, m) + 1j * np.random.randn(n2, m)
    c = np.random.randn(n1, n2, n3, n4, n5) + 1j * np.random.randn(n1, n2, n3, n4, n5)
    return (
        np.array(a, order=aOrder),
        np.array(b, order=bOrder),
        np.array(c, order=cOrder),
    )


# used in B()
blockSize = 84

resA = []
resB = []
resC = []

sizes = np.unique(np.exp(np.linspace(2, 6, 8)).astype(np.int64))
numTrials = 10

# overrides m form above
for m in sizes:
    a, b, c = make_arrays("F", "F", "F")

    path = np.einsum_path(
        a,
        [0, 5],
        b,
        [1, 5],
        c,
        [0, 1, Ellipsis],
        [Ellipsis, 5],
        optimize="greedy",
    )

    def A():
        np.einsum(
            a,
            [0, 5],
            b,
            [1, 5],
            c,
            [0, 1, 2, 3, 4],
            [5, 2, 3, 4],
            optimize="greedy",
            order="F",
        )
        # print("einsum\n", res.flags)

    def B():
        numBlocks = int(a.shape[1] // blockSize)
        np.concatenate(
            tuple(
                np.einsum(
                    c,
                    [1, 2, Ellipsis],
                    a[:, kk * blockSize : (kk + 1) * blockSize],
                    [1, 0],
                    b[:, kk * blockSize : (kk + 1) * blockSize],
                    [2, 0],
                    [0, Ellipsis],
                    optimize="greedy",
                    order="F",
                )
                for kk in range(numBlocks)
            )
            + (
                np.einsum(
                    c,
                    [1, 2, Ellipsis],
                    a[:, numBlocks * blockSize :],
                    [1, 0],
                    b[:, numBlocks * blockSize :],
                    [2, 0],
                    [0, Ellipsis],
                    optimize="greedy",
                    order="F",
                ),
            ),
            axis=0,
        )

    def C():
        tmp = np.einsum(a, [0, 5], c, [0, 1, 2, 3, 4], [1, 2, 3, 4, 5], order="F")
        np.einsum(b, [1, 5], tmp, [1, 2, 3, 4, 5], [2, 3, 4, 5], order="F")

    A()
    B()
    C()

    measured = np.mean(timeit.repeat(A, number=numTrials, repeat=numTrials)) / (
        numTrials * m
    )
    resA.append(measured)

    measured = np.mean(timeit.repeat(B, number=numTrials, repeat=numTrials)) / (
        numTrials * m
    )
    resB.append(measured)

    measured = np.median(timeit.repeat(C, number=numTrials, repeat=numTrials)) / (
        numTrials * m
    )
    resC.append(measured)

plt.figure()
plt.semilogy(sizes, resA, label="A")
plt.semilogy(sizes, resB, label="B")
plt.semilogy(sizes, resC, label="C")

plt.legend()
plt.show()

方法说明

  • 方法A:使用einsum的常规方式,原本预期性能表现良好,但当问题规模m超过某个阈值后,计算速度会骤降。
  • 方法B:分块实现,全规模下性能表现稳定,但在m较小时性能弱于A。无论如何调整输入和输出数组的内存布局,该定性表现均无明显变化。
  • 方法C:更为基础的分步实现,性能极差,符合预期。

测试环境与结果

AMD EPYC环境

配置:搭载AMD EPYC 7343 16核处理器、使用IntelPython(基于MKL)且强制MKL仅使用单核心
说明:将运行时间除以问题规模m,以此估算单个问题“切片”的计算成本
AMD EPYC环境测试结果

M2 MacBook Air环境

原生numpy测试

配置:搭载M2芯片的MacBook Air,使用原生numpy
M2原生numpy测试结果

链接Accelerate BLAS库的numpy测试

配置:搭载M2芯片的MacBook Air,使用链接Accelerate BLAS库的numpy
M2 Accelerate BLAS测试结果

核心问题

方法A到底出了什么问题?

题外话

  • 在CuPy上运行相同代码,未出现该现象,对应A方法的实现可良好扩展,至少在上述相同问题规模下表现正常。
  • 若使用opt_einsum进行收缩运算(基本沿用A的代码逻辑),可得到类似B的性能,但稍慢一些。

内容的提问来源于stack exchange,提问作者Labello

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 00:27:51