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):
- 覆盖面不足:用户实际会用到大量不属于标准网络(如 ResNet)的算子,例如
mean、max、topk、argmax、sort等张量操作算子,端到端基准不会覆盖它们。 - 问题会被掩盖:ResNet-50 由 Convolution2D、Softmax、Dense 等多个算子组成。假设我们优化了 Convolution2D,但一个 bug 让 Softmax 变慢,端到端整体时间可能依然正常,单算子性能退化被平均效应掩盖,且会累积成难以追踪的问题。
- 难以定位优化目标:在给定网络中,需要知道哪个算子耗时最多才能规划优化工作,而端到端基准无法提供算子级别的细粒度数据。
- 需要跨硬件对比:同一算子在 CPU(oneDNN)、GPU(NVIDIA CUDA 与 cuDNN)等不同硬件基础设施上的表现不同,算子级数据能指导逐算子优化,从而成倍提升端到端性能。
- 支持回归测试与 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/-f | json | json、md | 结果输出格式,md会生成 Markdown 表格 |
--output-file/-o | ./mxnet_operator_benchmarks.json | 任意文件路径 | 输出文件路径;若文件已存在会直接断言报错,避免误覆盖 |
--ctx | cpu | cpu、gpu、gpu(0)、gpu(1)… | 全部算子的全局运行上下文;在 CPU 机器上默认cpu,GPU 机器上默认gpu(0),可通过--ctx gpu(2)覆盖 |
--dtype | float32 | float32、float64、int32、int64 | 全部算子的全局数据类型,可用--dtype float64覆盖 |
--profiler/-p | native | native、python | 计时引擎:native 使用 MXNet 内置 C++ profiler,python 使用 Pythontime包 |
--int64-tensor | off | on、off | 是否使用超大张量输入(维度 >= 2^32)测试算子对 int64 索引/大张量的支持 |
--warmup/-w | 25 | 正整数 | 正式计时前的预热运行次数 |
--runs/-r | 100 | 正整数 | 正式采集数据的运行次数 |
其中--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_add、broadcast_mod、broadcast_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 | 必填 | 单个算子或算子列表(nd、np、npx算子均可) |
inputs | 必填 | 算子输入,键为算子参数名,值为 shape 元组,如{"lhs": (1024, 1024), "rhs": (1024, 1024)} |
run_backward | True | 是否同时跑反向传播基准 |
dtype | 'float32' | 输入张量精度 |
ctx | mx.cpu() | 运行上下文 |
profiler | 'native' | 计时引擎,'python'或'native' |
warmup | 10 | 预热次数(结果会被丢弃) |
runs | 50 | 正式采集次数 |
用例 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.nd的hasattr检查(修改 benchmark_utils.py),使任意算子只要提供合法的inputs、run_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.nd、mx.np、mx.npx三个命名空间中查找同名算子(必要时通过adjust_op_name做命名映射,例如nd的BatchNorm对应np的batch_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.add是x1/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,它对reshape、swapaxes、activation、batch_norm、convolution、add、masked_softmax、slice、fully_connected、batch_dot等算子预置了多组 shape 与参数组合的测试用例,可用于快速验证。
结果输出格式:JSON 与 Markdown
save_to_file()(common_utils.py)支持两种输出格式:
- JSON:
json.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),其表格片段例如:
| Operator | Avg Forward Time (ms) | Avg Backward Time (ms) | Max Mem Usage (Storage) (Bytes) | Inputs |
|---|---|---|---|---|
| broadcast_hypot | 0.0108 | 0.0135 | 0.024 | {'lhs': [(1024, 1024), (10000, 10), (10000, 1)], 'rhs': [...]} |
| sum | 32.4206 | 25.5443 | 0.002 | {'data': (1024, 1024), 'axis': ()} |
| random_normal | 151.0089 | --- | 4194.3042 | {'shape': (1024, 1024)} |
---表示该算子没有对应的反向传播结果(例如shuffle、random_normal等本身不定义反向)。
底层工作原理
opperf 的完整执行链路如下:
- 自动发现算子:op_registry_utils.py 调用
mx.operator.get_all_registered_operators()拉取 MXNet 算子注册表中的全部算子,再通过_select_ops()过滤掉以_contrib、_开头的内部/contrib 算子以及一批已废弃算子(如Pooling_v1、Convolution_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。 - 自动生成输入:根据算子参数名,在 default_params.py 的
DEFAULTS_INPUTS(普通张量)或DEFAULTS_INPUTS_LARGE_TENSOR(--int64-tensor on的超大张量)映射表中查找默认 shape。对于有特殊形状约束的算子会走定制分支,例如depth_to_space、space_to_depth、pad需要 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)。 - 执行并计时:
_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()是保证计时准确的关键。 - 结果解析: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 Storage、MXNET_C_API、operator等段落解析出内存分配统计与前向/反向平均耗时。解析时通过alias_map将别名算子名映射回注册名(如broadcast_plus->broadcast_add、broadcast_minus->broadcast_sub、max_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 社区规划中的功能(部分已完成):
- ✅ 覆盖全部 MXNet 算子(已实现);
- 增强 MXNet profiler,提供程序化获取与处理 profiler 数据的 API;
- 与 CI/CD 系统集成,为 PR 构建、nightly 构建运行算子基准;
- 面向结果分析的可视化 Dashboard 等多种展示形式,用于制定算子性能优化计划;
- 随机化张量形状生成,用于识别算子中的性能瓶颈。
快速上手建议
- 想快速验证工具是否可用:先跑
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),仅供参考