MXNet 算子级性能基准测试:使用 opperf 逐算子剖析前向/反向耗时与内存分配
2026/9/21 2:01:41 网站建设 项目流程

MXNet 算子级性能基准测试:使用 opperf 逐算子剖析前向/反向耗时与内存分配

【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mxne/mxnet

本文基于 Apache MXNet 仓库自带的benchmark/opperf工具(Operator Performance Benchmarks),讲解如何对单个 MXNet 算子或算子分组进行精细化的性能基准测试。与端到端网络基准(如 ResNet-50 跑 ImageNet)不同,opperf 直接针对mx.nd.*命名空间下的每个算子测量前向执行时间、反向执行时间与内存分配情况,适合算子内核开发者定位性能回退、框架维护者做 nightly 回归测试,以及硬件团队对比 CPU(oneDNN)与 GPU(CUDA/cuDNN)不同后端下的算子表现。读完本文,你将掌握 opperf 的六种典型用法、全部命令行参数、两种 profiler 引擎的区别,以及它从 MXNet 算子注册表中自动发现算子并生成输入数据的底层工作方式。

为什么需要算子级基准测试

端到端基准(例如在 ImageNet 上评测 ResNet-50)能反映深度学习框架的整体性能与健康状况,但它存在几个天然的盲区,这正是 opperf 诞生的动机(见 README):

  1. 覆盖面不足:用户实际会用到大量不属于标准网络(如 ResNet)的算子,例如meanmaxtopkargmaxsort等张量操作算子,端到端基准不会覆盖它们。
  2. 问题会被掩盖:ResNet-50 由 Convolution2D、Softmax、Dense 等多个算子组成。假设我们优化了 Convolution2D,但一个 bug 让 Softmax 变慢,端到端整体时间可能依然正常,单算子性能退化被平均效应掩盖,且会累积成难以追踪的问题。
  3. 难以定位优化目标:在给定网络中,需要知道哪个算子耗时最多才能规划优化工作,而端到端基准无法提供算子级别的细粒度数据。
  4. 需要跨硬件对比:同一算子在 CPU(oneDNN)、GPU(NVIDIA CUDA 与 cuDNN)等不同硬件基础设施上的表现不同,算子级数据能指导逐算子优化,从而成倍提升端到端性能。
  5. 支持回归测试与 CI/CD:可以将框架接入 CI/CD 系统,为 PR 运行逐算子性能测试。例如当某个 PR 修改了TransposeConv2D的内核时,只需针对该算子运行基准即可验证性能。

因此,opperf 提供了让用户和框架开发者轻松为单个算子运行基准测试的能力。

环境准备

opperf 的使用前提非常简单(见 README):

  • 已安装 MXNet(版本 >= 1.5.1);
  • 由于 opperf 依赖仓库内的 Python 包结构,需要克隆 MXNet 仓库,并将仓库根目录加入PYTHONPATH(当前 opperf 暂不支持通过 PyPi 二进制直接使用):
export PYTHONPATH=$PYTHONPATH:/path/to/mxnet/

设置完成后即可通过benchmark/opperf/opperf.py命令行入口,或直接import benchmark.opperf.*在 Python 脚本中调用。

用例 1:运行全部算子的基准测试

一条命令即可运行 MXNet 全部 NDArray 算子的基准测试(使用默认输入),并将结果保存为 JSON 文件:

python mxnet/benchmark/opperf/opperf.py --output-format json --output-file mxnet_operator_benchmark_results.json

从 opperf.py 入口实现 可以看到,main()会解析命令行参数、运行run_all_mxnet_operator_benchmarks(),将结果按算子名排序后经save_to_file()写出,最后还会调用get_operators_with_no_benchmark()打印出「已注册但未被基准覆盖」的算子清单,方便社区持续补全覆盖度。

命令行参数详解

opperf 支持的完整参数列表(以 opperf.py 的 argparse 定义 为准):

参数默认值取值范围说明
--output-format/-fjsonjsonmd结果输出格式,md会生成 Markdown 表格
--output-file/-o./mxnet_operator_benchmarks.json任意文件路径输出文件路径;若文件已存在会直接断言报错,避免误覆盖
--ctxcpucpugpugpu(0)gpu(1)全部算子的全局运行上下文;在 CPU 机器上默认cpu,GPU 机器上默认gpu(0),可通过--ctx gpu(2)覆盖
--dtypefloat32float32float64int32int64全部算子的全局数据类型,可用--dtype float64覆盖
--profiler/-pnativenativepython计时引擎:native 使用 MXNet 内置 C++ profiler,python 使用 Pythontime
--int64-tensoroffonoff是否使用超大张量输入(维度 >= 2^32)测试算子对 int64 索引/大张量的支持
--warmup/-w25正整数正式计时前的预热运行次数
--runs/-r100正整数正式采集数据的运行次数

其中--int64-tensor on会切换到 default_params.py 中定义的DEFAULTS_INPUTS_LARGE_TENSOR输入集,例如二元广播算子会用(2**16, 2**16)(2**28, 2**4)(2**32, 1)这样的大 shape。同时需要注意:线性代数算子(linalg_*)在大张量模式下会被跳过,原因是 linalg 算子不支持 int64 张量数据(opperf.py 中有对应处理)。

用例 2:运行某一类算子的基准测试

benchmark/opperf/nd_operations/目录按功能类别组织算子基准,例如 binary_operators.py 中的run_mx_binary_broadcast_operators_benchmarks()覆盖broadcast_addbroadcast_modbroadcast_power等 20 个二元广播算子。在 Python 脚本中调用即可:

#!/usr/bin/python from benchmark.opperf.nd_operations.binary_operators import run_mx_binary_broadcast_operators_benchmarks # Run all Binary Broadcast operations benchmarks with default input values print(run_mx_binary_broadcast_operators_benchmarks())

在 CPU 机器上的典型输出如下(每个算子会针对多组默认输入分别给出结果):

{'broadcast_mod': [{'avg_time_forward_broadcast_mod': 28.7063, 'avg_time_mem_alloc_cpu/0': 4194.3042, 'avg_time_backward_broadcast_mod': 12.0954, 'inputs': {'lhs': (1024, 1024), 'rhs': (1024, 1024)}}, {'avg_time_forward_broadcast_mod': 2.7332, 'avg_time_mem_alloc_cpu/0': 400.0, 'avg_time_backward_broadcast_mod': 1.1288, 'inputs': {'lhs': (10000, 10), 'rhs': (10000, 10)}}, {'avg_time_forward_broadcast_mod': 30.5322, 'avg_time_mem_alloc_cpu/0': 4000.0, 'avg_time_backward_broadcast_mod': 225.0255, 'inputs': {'lhs': (10000, 1), 'rhs': (10000, 100)}}], 'broadcast_power': [{'avg_time_backward_broadcast_power': 49.5871, 'avg_time_forward_broadcast_power': 18.0954, 'avg_time_mem_alloc_cpu/0': 4194.3042, 'inputs': {'lhs': (1024, 1024), 'rhs': (1024, 1024)}}, {'avg_time_backward_broadcast_power': 4.6623, 'avg_time_forward_broadcast_power': 1.8283, 'avg_time_mem_alloc_cpu/0': 400.0, 'inputs': {'lhs': (10000, 10), 'rhs': (10000, 10)}}, {'avg_time_backward_broadcast_power': 279.922, 'avg_time_forward_broadcast_power': 24.4621, 'avg_time_mem_alloc_cpu/0': 4000.0, 'inputs': {'lhs': (10000, 1), 'rhs': (10000, 100)}}], ..... }

nd_operations/目录下按类别提供的基准入口包括:unary、binary(broadcast / element_wise / misc)、gemm、random sampling、reduction、sorting & searching、indexing routines、array rearrange、shape、expanding、rounding、join & split、NN basic、activation、pooling、convolution、transpose convolution、optimizer、loss、linalg、misc 等,全部类别会在run_all_mxnet_operator_benchmarks()中依次执行。

用例 3:运行单个特定算子的基准测试

如果想针对nd.add这样一个具体算子做基准,可以调用 benchmark_utils.py 中的run_performance_test()

#!/usr/bin/python import mxnet as mx from mxnet import nd from benchmark.opperf.utils.benchmark_utils import run_performance_test add_res = run_performance_test(nd.add, run_backward=True, dtype='float32', ctx=mx.cpu(), inputs=[{"lhs": (1024, 1024), "rhs": (1024, 1024)}], warmup=10, runs=25) print(add_res)

CPU 机器上的输出示例:

{'add': [{'avg_time_mem_alloc_cpu/0': 102760.4453, 'avg_time_forward_broadcast_add': 4.0372, 'avg_time_backward_broadcast_add': 5.3841, 'inputs': {'lhs': (1024, 1024), 'rhs': (1024, 1024)}}]}

run_performance_test()的完整签名(benchmark_utils.py)为:

参数默认值说明
ops必填单个算子或算子列表(ndnpnpx算子均可)
inputs必填算子输入,键为算子参数名,值为 shape 元组,如{"lhs": (1024, 1024), "rhs": (1024, 1024)}
run_backwardTrue是否同时跑反向传播基准
dtype'float32'输入张量精度
ctxmx.cpu()运行上下文
profiler'native'计时引擎,'python''native'
warmup10预热次数(结果会被丢弃)
runs50正式采集次数

用例 4:用同一组输入运行一组算子

将多个算子以列表传入即可共享同一组输入,便于横向对比同族算子的性能:

#!/usr/bin/python import mxnet as mx from mxnet import nd from benchmark.opperf.utils.benchmark_utils import run_performance_test add_res = run_performance_test([nd.add, nd.subtract], run_backward=True, dtype='float32', ctx=mx.cpu(), inputs=[{"lhs": (1024, 1024), "rhs": (1024, 1024)}], warmup=10, runs=25) print(add_res)

输出中每个算子各占一个键:

{'add': [{'avg_time_mem_alloc_cpu/0': 102760.4453, 'avg_time_forward_broadcast_add': 4.0372, 'avg_time_backward_broadcast_add': 5.3841, 'inputs': {'lhs': (1024, 1024), 'rhs': (1024, 1024)}}], 'subtract': [{'avg_time_forward_broadcast_sub': 5.5137, 'avg_time_mem_alloc_cpu/0': 207618.0469, 'avg_time_backward_broadcast_sub': 7.2976, 'inputs': {'lhs': (1024, 1024), 'rhs': (1024, 1024)}} ]}

用例 5:本地 profile 内部算子

opperf 默认支持mx.nd.*命名空间下的算子,但本地调试时可以 profilemx.nd.internal.*命名空间下的内部算子。README 中给出的思路是:移除run_performance_test中对op.__name__是否属于mx.ndhasattr检查(修改 benchmark_utils.py),使任意算子只要提供合法的inputsrun_backward等参数即可被 profile。修改后的效果示例:

>>> import mxnet as mx >>> from mxnet import nd >>> from benchmark.opperf.utils.benchmark_utils import run_performance_test >>> run_performance_test(mx.nd._internal._copyto, ... inputs=[{"data": mx.nd.array([1, 2]), ... "out": mx.nd.empty(shape=mx.nd.array([1, 2]).shape, ctx=mx.cpu())}]) INFO:root:Begin Benchmark - _copyto INFO:root:Complete Benchmark - _copyto [{'_copyto': [{'inputs': {'data': '<NDArray 2 @cpu(0)>', 'out': '<NDArray 2 @cpu(0)>'}, 'max_storage_mem_alloc_cpu/0': 0.004}]}]

用例 6:对比 NDArray 与 Numpy 算子的性能

run_benchmark_operator()(benchmark_utils.py)会自动在mx.ndmx.npmx.npx三个命名空间中查找同名算子(必要时通过adjust_op_name做命名映射,例如ndBatchNorm对应npbatch_norm),用同一 size 的张量分别跑一遍并打印对比:

#!/usr/bin/python from benchmark.opperf.utils.benchmark_utils import run_benchmark_operator run_benchmark_operator(name="add", run_backward=True)

CPU 机器上的输出示例:

<module 'mxnet.ndarray'> [{'add': [{'inputs': {'lhs': (128, 128), 'rhs': (128, 128)}, 'max_storage_mem_alloc_cpu/0': 32.768, 'avg_time_forward_add': 0.0496, 'avg_time_backward_add': 0.0793}]}] <module 'mxnet.numpy'> [{'add': [{'inputs': {'x1': (128, 128), 'x2': (128, 128)}, 'max_storage_mem_alloc_cpu/0': 32.768, 'avg_time_forward_add': 0.0484, 'avg_time_backward_add': 0.0898}]}]

注意nd.add的参数名是lhs/rhs,而np.addx1/x2,工具会自动适配。所有类型为 NDArray 的参数都会自动使用size指定的 shape;若算子需要更多参数或不同 shape 的 NDArray,通过additional_inputs传入:

run_benchmark_operator(name="pick", size=(128, 128), additional_inputs={"index": (128, 1)})

仓库中还提供了 pytest 化的算子对比测试入口 benchmark_operators_pytest.py,它对reshapeswapaxesactivationbatch_normconvolutionaddmasked_softmaxslicefully_connectedbatch_dot等算子预置了多组 shape 与参数组合的测试用例,可用于快速验证。

结果输出格式:JSON 与 Markdown

save_to_file()(common_utils.py)支持两种输出格式:

  • JSONjson.dump按缩进写出,便于程序化处理与后续分析;
  • Markdown_prepare_markdown()会先输出运行时特性(runtime_features,例如编译选项 OPENCV/CUDA 等,取自 op_registry_utils.py 的 get_current_runtime_features()),再以表格形式输出结果。native profiler 模式下的表头为「Operator / Inputs / Max Mem Usage (Storage) (Bytes) / Avg Forward Time (ms) / Avg Backward Time (ms)」,python profiler 模式下的表头为「Operator / Avg Time (ms) / P50 Time (ms) / P90 Time (ms) / P99 Time (ms) / Inputs」。

仓库自带的样例结果见 mxnet_operator_benchmark_results_cpu.md(CPU,MXNet v1.4.1,C5.8x 实例)与mxnet_operator_benchmark_results_gpu.md(GPU),其表格片段例如:

OperatorAvg Forward Time (ms)Avg Backward Time (ms)Max Mem Usage (Storage) (Bytes)Inputs
broadcast_hypot0.01080.01350.024{'lhs': [(1024, 1024), (10000, 10), (10000, 1)], 'rhs': [...]}
sum32.420625.54430.002{'data': (1024, 1024), 'axis': ()}
random_normal151.0089---4194.3042{'shape': (1024, 1024)}

---表示该算子没有对应的反向传播结果(例如shufflerandom_normal等本身不定义反向)。

底层工作原理

opperf 的完整执行链路如下:

  1. 自动发现算子:op_registry_utils.py 调用mx.operator.get_all_registered_operators()拉取 MXNet 算子注册表中的全部算子,再通过_select_ops()过滤掉以_contrib_开头的内部/contrib 算子以及一批已废弃算子(如Pooling_v1Convolution_v1等),同时把_backward_*算子与对应前向算子合并,标记has_backward属性。随后_set_op_arguments()通过mx.operator.get_operator_arguments()获取每个算子的参数名、参数类型与参数个数(narg),再按类别(unary、broadcast、elemwise、random、reduction、linalg、NN 等)分别筛选,例如二元广播算子的判定条件是op_name.startswith("broadcast_") and narg == 2 and "lhs" in arg_names and "rhs" in arg_names
  2. 自动生成输入:根据算子参数名,在 default_params.py 的DEFAULTS_INPUTS(普通张量)或DEFAULTS_INPUTS_LARGE_TENSOR--int64-tensor on的超大张量)映射表中查找默认 shape。对于有特殊形状约束的算子会走定制分支,例如depth_to_spacespace_to_depthpad需要 4D 张量,broadcast_axis/broadcast_to需要至少一个维度为 1,random_randint只能用 int dtype,random_*/sample_*算子只能用 float dtype。之后由_prepare_op_inputs()(benchmark_utils.py)用mx.random.seed(41)固定随机种子,通过nd.normal初始化每个 NDArray 参数并attach_grad()(仅当run_backward=True)。
  3. 执行并计时_run_operator_performance_test()(benchmark_utils.py)先按warmup次数预热(丢弃结果),再按runs次数正式执行。执行函数位于 ndarray_utils.py:nd_forward_backward_and_profile()mx.autograd.record()中执行算子并调用res.backward(),最后nd.waitall()同步等待全部异步操作完成;nd_forward_and_profile()只执行前向。由于 MXNet 是异步执行模型,waitall()是保证计时准确的关键。
  4. 结果解析:native profiler 由 profiler_utils.py 的cpp_profile装饰器实现——调用profiler.set_config(profile_all=True, aggregate_stats=True)profiler.set_state('run')开始采集,结束后profiler.dumps(reset=True)拿到 profiler dump 字符串,再由parse_profiler_dump()Device StorageMXNET_C_APIoperator等段落解析出内存分配统计与前向/反向平均耗时。解析时通过alias_map将别名算子名映射回注册名(如broadcast_plus->broadcast_addbroadcast_minus->broadcast_submax_axis->max)。README 也注明:目前解析 profiler dump 字符串属于临时方案,后续计划为 MXNet profiler 增加get_summary()reset()等程序化读取 API 后移除解析逻辑。

内存指标的注意事项

README 特别强调:测量到的内存是池化内存(pool memory),来自 profiler 的Memory: cpu/0(或gpu/0)统计项,即 MXNet 存储分配器为该算子分配的内存,并不等于算子实际请求的精确内存量。在解读max_storage_mem_alloc_cpu/0/avg_time_mem_alloc_cpu/0这类字段时要留意这一点。

使用 Python 计时器(python profiler)

native profiler 依赖 MXNet 内置 C++ profiler,适合框架开发者;而普通用户可以选择 Pythontime包作为计时引擎,无需依赖 profiler 的解析逻辑。两种方式均可用:

为全部算子启用 python profiler:

python mxnet/benchmark/opperf/opperf.py --profiler='python'

为特定算子启用 python profiler(在run_performance_test中传profiler='python'):

add_res = run_performance_test([nd.add, nd.subtract], run_backward=True, dtype='float32', ctx=mx.cpu(), inputs=[{"lhs": (1024, 1024), "rhs": (1024, 1024)}], warmup=10, runs=25, profiler='python')

python_profile装饰器(profiler_utils.py)的实现是:对每次运行用time.perf_counter()记录前后时间差(换算为毫秒),收集runs次后计算平均值(avg)与 p50/p90/p99 百分位。因此 python profiler 的输出比 native 更丰富:

{'avg_time_add': 0.4053, 'p50_time_add': 16.7610, 'p90_time_add': 18.0817, 'p99_time_add': 19.0601, 'inputs': {...}}

扩展方向与 TODO

README 列出了 opperf 社区规划中的功能(部分已完成):

  1. ✅ 覆盖全部 MXNet 算子(已实现);
  2. 增强 MXNet profiler,提供程序化获取与处理 profiler 数据的 API;
  3. 与 CI/CD 系统集成,为 PR 构建、nightly 构建运行算子基准;
  4. 面向结果分析的可视化 Dashboard 等多种展示形式,用于制定算子性能优化计划;
  5. 随机化张量形状生成,用于识别算子中的性能瓶颈。

快速上手建议

  • 想快速验证工具是否可用:先跑python mxnet/benchmark/opperf/opperf.py --output-format md --output-file bench.md -w 5 -r 10,用较小的 warmup/runs 缩短执行时间。
  • 想排查特定算子的回归:用 用例 3 的方式直接对目标算子做基准,与仓库自带的 CPU 结果样例 或mxnet_operator_benchmark_results_gpu.md对比。
  • 想在多硬件上对比:分别用--ctx cpu--ctx gpu(0)跑同一算子集合,再对比 native profiler 输出的前向/反向平均耗时。
  • 相关源码入口:命令行入口 opperf.py、基准核心 benchmark_utils.py、算子注册表操作 op_registry_utils.py、profiler 引擎 profiler_utils.py、默认输入规则 default_params.py。

【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mxne/mxnet

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询