From 27b05395cdbb0f44c8b8cdc5a357e63ff856e19a Mon Sep 17 00:00:00 2001 From: h00613304 Date: Fri, 25 Aug 2023 14:38:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=B0=86ptdbg=E5=B7=A5=E5=85=B7=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=87=B3att=E4=BB=93=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ptdbg_ascend/CMakeLists.txt | 19 + debug/accuracy_tools/ptdbg_ascend/README.md | 244 +++ debug/accuracy_tools/ptdbg_ascend/RELEASE.md | 4 + debug/accuracy_tools/ptdbg_ascend/configure | 14 + .../accuracy_tools/ptdbg_ascend/configure.py | 105 ++ debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md | 33 + ...71\345\272\224\345\205\263\347\263\273.md" | 39 + .../ptdbg_ascend/doc/img/auto_analyze_log.png | Bin 0 -> 65144 bytes ...0\203\275\350\257\264\346\230\216_v1.0.md" | 539 ++++++ ...0\203\275\350\257\264\346\230\216_v2.0.md" | 1001 +++++++++++ ...0\203\275\350\257\264\346\230\216_v3.1.md" | 999 +++++++++++ ...0\203\275\350\257\264\346\230\216_v3.2.md" | 1464 +++++++++++++++++ ...67\345\217\226\346\226\271\346\263\225.md" | 38 + ...50\344\276\213\350\257\264\346\230\216.md" | 42 + .../ptdbg_ascend/figures/advisor_summary.png | Bin 0 -> 13908 bytes .../ptdbg_ascend/figures/auto_analyze_log.png | Bin 0 -> 65144 bytes .../ptdbg_ascend/figures/compare_struct.png | Bin 0 -> 110216 bytes .../ptdbg_ascend/figures/h5_file_struct.png | Bin 0 -> 17174 bytes .../ptdbg_ascend/figures/module_compare.png | Bin 0 -> 56655 bytes .../ptdbg_ascend/figures/op_compare.png | Bin 0 -> 38489 bytes .../ptdbg_ascend/img/module_compare.png | Bin 0 -> 56655 bytes .../ptdbg_ascend/img/op_compare.png | Bin 0 -> 38489 bytes .../ptdbg_ascend/src/python/MANIFEST.in | 3 + .../src/python/ptdbg_ascend/__init__.py | 34 + .../python/ptdbg_ascend/advisor/advisor.py | 114 ++ .../ptdbg_ascend/advisor/advisor_const.py | 51 + .../ptdbg_ascend/advisor/advisor_result.py | 56 + .../common/compare_script.template | 14 + .../src/python/ptdbg_ascend/common/utils.py | 649 ++++++++ .../ptdbg_ascend/compare/acc_compare.py | 600 +++++++ .../compare/distributed_compare.py | 91 + .../python/ptdbg_ascend/debugger/__init__.py | 0 .../ptdbg_ascend/debugger/debugger_config.py | 33 + .../debugger/precision_debugger.py | 86 + .../src/python/ptdbg_ascend/dump/dump.py | 319 ++++ .../src/python/ptdbg_ascend/dump/utils.py | 284 ++++ .../ptdbg_ascend/hook_module/__init__.py | 0 .../ptdbg_ascend/hook_module/hook_module.py | 94 ++ .../ptdbg_ascend/hook_module/register_hook.py | 137 ++ .../hook_module/support_wrap_ops.yaml | 1054 ++++++++++++ .../hook_module/wrap_functional.py | 103 ++ .../hook_module/wrap_npu_custom.py | 61 + .../ptdbg_ascend/hook_module/wrap_tensor.py | 66 + .../ptdbg_ascend/hook_module/wrap_torch.py | 108 ++ .../ptdbg_ascend/hook_module/wrap_vf.py | 64 + .../ptdbg_ascend/overflow_check/__init__.py | 0 .../ptdbg_ascend/overflow_check/info_dump.py | 246 +++ .../overflow_check/overflow_check.py | 180 ++ .../ptdbg_ascend/overflow_check/utils.py | 76 + .../src/python/ptdbg_ascend/parse.py | 4 + .../ptdbg_ascend/parse_tool/__init__.py | 0 .../src/python/ptdbg_ascend/parse_tool/cli.py | 31 + .../ptdbg_ascend/src/python/setup.py | 50 + .../test/resources/compare/advisor.txt | 3 + .../compare/compare_result_20230703104808.csv | 9 + .../compare_result_without_accuracy.csv | 9 + .../test/resources/compare/npu_test.pkl | 8 + .../ptdbg_ascend/test/run_test.sh | 42 + .../ptdbg_ascend/test/run_ut.py | 41 + .../test/ut/overflow/test_overflow_check.py | 53 + .../test/ut/overflow/test_overflow_utils.py | 58 + .../ptdbg_ascend/test/ut/test_acc_compare.py | 135 ++ .../ptdbg_ascend/test/ut/test_advisor.py | 35 + .../test/ut/test_advisor_result.py | 41 + .../ptdbg_ascend/test/ut/test_common_util.py | 87 + .../ptdbg_ascend/test/ut/test_hooks.py | 71 + .../ptdbg_ascend/test/ut/test_utils.py | 43 + debug/accuracy_tools/ptdbg_ascend/tools/.keep | 0 68 files changed, 9784 insertions(+) create mode 100644 debug/accuracy_tools/ptdbg_ascend/CMakeLists.txt create mode 100644 debug/accuracy_tools/ptdbg_ascend/README.md create mode 100644 debug/accuracy_tools/ptdbg_ascend/RELEASE.md create mode 100644 debug/accuracy_tools/ptdbg_ascend/configure create mode 100644 debug/accuracy_tools/ptdbg_ascend/configure.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/Pytorch \350\277\220\347\256\227\351\207\215\350\275\275API\345\222\214Acl\347\256\227\345\255\220\345\257\271\345\272\224\345\205\263\347\263\273.md" create mode 100644 debug/accuracy_tools/ptdbg_ascend/doc/img/auto_analyze_log.png create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v1.0.md" create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v2.0.md" create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.1.md" create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/rank_id\350\216\267\345\217\226\346\226\271\346\263\225.md" create mode 100644 "debug/accuracy_tools/ptdbg_ascend/doc/\345\217\215\345\220\221ACL dump\347\224\250\344\276\213\350\257\264\346\230\216.md" create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/advisor_summary.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/auto_analyze_log.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/compare_struct.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/h5_file_struct.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/module_compare.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/figures/op_compare.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/img/module_compare.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/img/op_compare.png create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/MANIFEST.in create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/__init__.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/advisor/advisor.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/advisor/advisor_const.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/advisor/advisor_result.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/common/compare_script.template create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/common/utils.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/acc_compare.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/distributed_compare.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/__init__.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/debugger_config.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/precision_debugger.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/dump.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/utils.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/__init__.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/hook_module.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/register_hook.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/support_wrap_ops.yaml create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_functional.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_npu_custom.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_tensor.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_torch.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_vf.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/__init__.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/info_dump.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/overflow_check.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/utils.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/__init__.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/cli.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/src/python/setup.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/resources/compare/advisor.txt create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_20230703104808.csv create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_without_accuracy.csv create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/resources/compare/npu_test.pkl create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/run_test.sh create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/run_ut.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_check.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_utils.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_acc_compare.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor_result.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_common_util.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_hooks.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/test/ut/test_utils.py create mode 100644 debug/accuracy_tools/ptdbg_ascend/tools/.keep diff --git a/debug/accuracy_tools/ptdbg_ascend/CMakeLists.txt b/debug/accuracy_tools/ptdbg_ascend/CMakeLists.txt new file mode 100644 index 0000000000..f4b3ccd3a5 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.5) +project(PtdbgAscend) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_SKIP_RPATH TRUE) + +if (NOT EXISTS ${CMAKE_CURRENT_LIST_DIR}/tools/PYTHON_BIN_PATH) + message(FATAL_ERROR "No validate configuration found. Did you forget to configure first?") +endif () + +file(STRINGS "${CMAKE_CURRENT_LIST_DIR}/tools/PYTHON_BIN_PATH" PYTHON_BIN_PATH) + +add_custom_target(ptdbg_ascend ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_LIST_DIR}/src/python ${CMAKE_BINARY_DIR}/ptdbg_ascend + COMMAND cd ${CMAKE_BINARY_DIR}/ptdbg_ascend && ${PYTHON_BIN_PATH} setup.py bdist_wheel + VERBATIM + ) + +install(CODE "execute_process(COMMAND ${PYTHON_BIN_PATH} -m pip install ${CMAKE_BINARY_DIR}/ptdbg_ascend/dist/ptdbg_ascend-3.2-py3-none-any.whl --upgrade)") diff --git a/debug/accuracy_tools/ptdbg_ascend/README.md b/debug/accuracy_tools/ptdbg_ascend/README.md new file mode 100644 index 0000000000..729c54f8ef --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/README.md @@ -0,0 +1,244 @@ +# **PyTorch精度工具** + +## 快速安装 + +进行PyTorch精度比对需要将ptdbg_ascend精度工具分别安装在CPU或GPU环境以及NPU环境下。 + +1. whl包获取。 + + 请通过下表链接下载ptdbg_ascend精度工具whl包,推荐下载最新版本。 + + | ptdbg_ascend版本 | 发布日期 | 支持PyTorch版本 | 下载链接 | 参考指南 | 校验码 | + | ---------------- | --------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | + | 3.2 | 2023-8-17 | 1.8.1/1.11.0/2.0/2.1 | [ptdbg_ascend-3.2-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/3.0/ptdbg_ascend-3.2-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v3.2](doc/ptdbg_ascend精度工具功能说明_v3.2.md) | 0116f66c7c893fc171bfa86e12ecfbf9cd062aedd176a0e67befb880b995f472 | + | 3.1 | 2023-8-02 | 1.8.1/1.11.0/2.0 | [ptdbg_ascend-3.1-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/3.0/ptdbg_ascend-3.1-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v3.1](doc/ptdbg_ascend精度工具功能说明_v3.1.md) | ef0dd5f96faf3576466545f082383eece409f25642a9bc4d0efc944969c1445a | + | 2.0 | 2023-7-07 | 1.8.1/1.11.0/2.0 | [ptdbg_ascend-2.0-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/2.0/ptdbg_ascend-2.0-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v2.0](doc/ptdbg_ascend精度工具功能说明_v2.0.md) | 85e046f133f0f40ed660337ce8207249b1dac47ac668910625bea49809f31d66 | + | 1.0 | 2023-3-30 | 1.8.1/1.11.0 | [ptdbg_ascend-1.0-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/1.0/ptdbg_ascend-1.0-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v1.0](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/ptdbg_ascend%E7%B2%BE%E5%BA%A6%E5%B7%A5%E5%85%B7%E5%8A%9F%E8%83%BD%E8%AF%B4%E6%98%8E_v1.0.md) | 0559e12ba7accf80d182f227698163ee0de88bf86b1e9cd9f33b16fdead14759 | + +2. whl包校验。 + + 1. 根据以上下载链接下载whl包到Linux安装环境。 + + 2. 进入whl包所在目录,执行如下命令。 + + ``` + sha256sum {name}.whl + ``` + + {name}为whl包名称。 + + 若回显呈现对应版本whl包一致的**校验码**,则表示下载了正确的ptdbg_ascend精度工具whl安装包。示例如下: + + ``` + sha256sum ptdbg_ascend-3.1-py3-none-any.whl + ef0dd5f96faf3576466545f082383eece409f25642a9bc4d0efc944969c1445a ptdbg_ascend-3.1-py3-none-any.whl + ``` + +3. whl包安装。 + + 执行如下命令进行安装。 + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl + ``` + + 若为覆盖安装,请在命令行末尾增加“--force-reinstall”参数强制安装,例如: + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl --force-reinstall + ``` + + 提示如下信息则表示安装成功。 + + ```bash + Successfully installed ptdbg_ascend-{version} + ``` + +## **PyTorch精度工具简介** + +### 概述 + +在PyTorch训练网络,对同一模型或API调试过程中,遇到API相关的计算精度问题,定位时费时费力。 + +ptdbg_ascend为PyTorch精度工具,用来进行PyTorch整网API粒度的数据dump、精度比对和溢出检测,从而定位PyTorch训练场景下的精度问题。 + +**使用场景** + +主要的使用场景包括: + +- 同一模型,从CPU或GPU移植到NPU中存在精度下降问题,对比NPU芯片中的API计算数值与CPU或GPU芯片中的API计算数值,进行问题定位。 +- 同一模型,进行迭代(模型、框架版本升级或设备硬件升级)时存在的精度下降问题,对比相同模型在迭代前后版本的API计算数值,进行问题定位。 + +### 原理介绍 + +精度对比工具,通过在PyTorch模型中注册hook,跟踪计算图中API的前向传播与反向传播时的输入与输出,排查存在计算精度误差,进行问题的精准定位。 + +**精度比对流程** + +1. 当模型在CPU或GPU上进行正向和反向传播时,分别dump每一层的数值输入与输出。 + +2. 当模型在NPU中进行计算时,采用相同的方式dump下相应的数据。 + +3. 通过对比dump出的数值,计算余弦相似度和最大绝对误差的方式,定位和排查NPU API存在的计算精度问题。如图1所示。 + + 图1:精度比对逻辑图 + + ![op_compare](figures/module_compare.png) + +**API匹配条件** + +进行精度比对时,需要判断CPU或GPU的API与NPU的API是否相同可比对,须满足以下匹配条件: + +- 两个API的名称相同,API命名规则:`{api_type}_{api_name}_{api调用次数}_{正反向}_{输入输出}.index`,如:Functional_conv2d_1_backward_input.0。 +- 两个API的输入输出Tensor数量和各个Tensor的Shape相同。 + +通常满足以上两个条件,ptdbg_ascend就认为是同一个API,成功进行API的匹配,后续进行相应的计算精度比对。 + +## **PyTorch精度工具安装** + +### 环境准备 + +- 通过pip安装环境依赖wheel、numpy、pandas(1.3.5及以上版本)和pyyaml。 +- ptdbg_ascend与PyTorch有严格的版本配套关系,使用工具前,您需要确保已经正确安装了PyTorch v1.8.1、PyTorch v1.11.0或PyTorch v2.0.0版本: + - CPU或GPU环境:请至[PyTorch官网](https://www.pytorch.org)下载并安装。 + - NPU环境:请参见《[CANN软件安装指南](https://www.hiascend.com/document/detail/zh/canncommercial/63RC1/envdeployment/instg/instg_000002.html)》“安装开发环境 > 在昇腾设备上安装 > 安装深度学习框架 > 安装PyTorch”章节进行安装。 + +### 安装 + +进行PyTorch精度比对需要将ptdbg_ascend精度工具分别安装在CPU或GPU环境以及NPU环境下。 + +ptdbg_ascend精度工具的安装方式包括:**下载whl包安装**和**源代码编译安装**。 + +#### 下载whl包安装 + +1. whl包获取。 + + 请通过下表链接下载ptdbg_ascend精度工具whl包,推荐下载最新版本。 + + | ptdbg_ascend版本 | 发布日期 | 支持PyTorch版本 | 下载链接 | 校验码 | 参考指南 | + | ---------------- | --------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | + | 3.2 | 2023-8-17 | 1.8.1/1.11.0/2.0/2.1 | [ptdbg_ascend-3.2-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/3.0/ptdbg_ascend-3.2-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v3.2](doc/ptdbg_ascend精度工具功能说明_v3.2.md) | 0116f66c7c893fc171bfa86e12ecfbf9cd062aedd176a0e67befb880b995f472 | + | 3.1 | 2023-8-02 | 1.8.1/1.11.0/2.0 | [ptdbg_ascend-3.1-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/3.0/ptdbg_ascend-3.1-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v3.1](doc/ptdbg_ascend精度工具功能说明_v3.1.md) | ef0dd5f96faf3576466545f082383eece409f25642a9bc4d0efc944969c1445a | + | 2.0 | 2023-7-07 | 1.8.1/1.11.0/2.0 | [ptdbg_ascend-2.0-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/2.0/ptdbg_ascend-2.0-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v2.0](doc/ptdbg_ascend精度工具功能说明_v2.0.md) | 85e046f133f0f40ed660337ce8207249b1dac47ac668910625bea49809f31d66 | + | 1.0 | 2023-3-30 | 1.8.1/1.11.0 | [ptdbg_ascend-1.0-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/1.0/ptdbg_ascend-1.0-py3-none-any.whl) | [ptdbg_ascend精度工具功能说明_v1.0](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/ptdbg_ascend精度工具功能说明_v1.0.md) | 0559e12ba7accf80d182f227698163ee0de88bf86b1e9cd9f33b16fdead14759 | + +2. whl包校验。 + + 1. 根据以上下载链接下载whl包到Linux安装环境。 + + 2. 进入whl包所在目录,执行如下命令。 + + ``` + sha256sum {name}.whl + ``` + + {name}为whl包名称。 + + 若回显呈现对应版本whl包一致的**校验码**,则表示下载了正确的ptdbg_ascend精度工具whl安装包。示例如下: + + ``` + sha256sum ptdbg_ascend-3.1-py3-none-any.whl + ef0dd5f96faf3576466545f082383eece409f25642a9bc4d0efc944969c1445a ptdbg_ascend-3.1-py3-none-any.whl + ``` + +3. whl包安装。 + + 执行如下命令进行安装。 + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl + ``` + + 若为覆盖安装,请在命令行末尾增加“--force-reinstall”参数强制安装,例如: + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl --force-reinstall + ``` + + 提示如下信息则表示安装成功。 + + ```bash + Successfully installed ptdbg_ascend-{version} + ``` + +#### 源代码编译安装 + +1. 安装依赖。 + + 编译前需要安装wheel。 + + ```bash + pip3 install wheel + ``` + +2. 下载源码。 + + ```bash + git clone https://gitee.com/ascend/tools.git + ``` + +3. 配置安装环境。 + + ```bash + cd tools/ptdbg_ascend + bash ./configure + ``` + + 默认情况下,执行上述命会弹出如下交互式会话窗口。 + + 您的会话可能有所不同,请以实际情况为准。 + + ```bash + Please specify the location of python with available pytorch v1.8.1/v1.11.0 site-packages installed. [Default is /usr/bin/python3] + (You can make this quiet by set env [ADAPTER_TARGET_PYTHON_PATH]): + ``` + + 此时要求输入安装了PyTorch v1.8.1或者v1.11.0 版本的Python解释器路径,若默认路径正确,回车,否则请输入正确的Python解释器路径。 + + > 也可以通过设置ADAPTER_TARGET_PYTHON_PATH的环境变量,来抑制交互式窗口弹出,但是要确保路径是有效的,否则仍然弹出。 + + 配置完成后提示如下信息则表示Python解释器验证成功。 + + ```bash + Configuration finished + ``` + +4. 配置cmake。 + + ```bash + mkdir build + cd build + cmake .. + ``` + + 可能需要几分钟时间下载ptdbg_ascend的依赖项目以完成配置。 + +5. 执行编译。 + + ```bash + make + ``` + + 编译结束后生成如下whl包。 + + ```bash + ./ptdbg_ascend/dist/ptdbg_ascend-{version}-py3-none-any.whl + ``` + +6. 安装。 + + 执行如下命令进行ptdbg_ascend安装。 + + ```bash + pip3 install ./ptdbg_ascend/dist/ptdbg_ascend-{version}-py3-none-any.whl --upgrade --force-reinstall + ``` + +完成ptdbg_ascend安装后,可以进行PyTorch精度数据的dump和、比对和溢出检测等操作,详细介绍请参见《[PyTorch精度工具使用指南](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc)》。 + +## 贡献 + +push代码前,请务必保证已经完成了基础功能测试和网络测试。 + +## Release Notes + +Release Notes请参见[RELEASE](RELEASE.md). diff --git a/debug/accuracy_tools/ptdbg_ascend/RELEASE.md b/debug/accuracy_tools/ptdbg_ascend/RELEASE.md new file mode 100644 index 0000000000..f37c0731e8 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/RELEASE.md @@ -0,0 +1,4 @@ +# Release 3.2 + +This is the initial release of Pytorch precision compare tools which was designed by the researchers + and engineers in Huawei Technologies Co.,Ltd. \ No newline at end of file diff --git a/debug/accuracy_tools/ptdbg_ascend/configure b/debug/accuracy_tools/ptdbg_ascend/configure new file mode 100644 index 0000000000..a953879ec9 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/configure @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e +set -o pipefail + +if [ -z "$PYTHON_BIN_PATH" ]; then + PYTHON_BIN_PATH=$(which python3 || which python || true) +fi + +# Set all env variables +CONFIGURE_DIR=$(dirname "$0") +"$PYTHON_BIN_PATH" "${CONFIGURE_DIR}/configure.py" "$@" + +echo "Configuration finished" diff --git a/debug/accuracy_tools/ptdbg_ascend/configure.py b/debug/accuracy_tools/ptdbg_ascend/configure.py new file mode 100644 index 0000000000..b914b32e9f --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/configure.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +Function: +This class mainly involves tf common function. +Copyright Information: +HuaWei Technologies Co.,Ltd. All Rights Reserved © 2022 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import subprocess +import sys + +_PYTORCH_VERSION_1_8 = "1.8" +_PYTORCH_VERSION_1_11 = "1.11" +_PYTORCH_VERSION_2_0 = "2.0" +_PYTORCH_VERSION_2_1 = "2.1" +_PYTHON_BIN_PATH_ENV = "ADAPTER_TARGET_PYTHON_PATH" +_ASCEND_INSTALLED_PATH_ENV = "ASCEND_INSTALLED_PATH" + + +def run_command(cmd): + """run_command.""" + output = subprocess.check_output(cmd) + return output.decode('UTF-8').strip() + + +def get_input(question): + """get_input.""" + try: + try: + answer = raw_input(question) + except NameError: + answer = input(question) + except EOFError: + answer = '' + return answer + + +def config_path(file_name): + """config_path.""" + return os.path.join("tools", file_name) + + +def setup_python(env_path): + """Get python install path.""" + default_python_bin_path = sys.executable + ask_python_bin_path = ('Please specify the location of python with valid ' + 'pytorch 1.8/1.11/2.0/2.1 site-packages installed. [Default ' + 'is %s]\n(You can make this quiet by set env ' + '[ADAPTER_TARGET_PYTHON_PATH]): ') % default_python_bin_path + custom_python_bin_path = env_path + while True: + if not custom_python_bin_path: + python_bin_path = get_input(ask_python_bin_path) + else: + python_bin_path = custom_python_bin_path + custom_python_bin_path = None + if not python_bin_path: + python_bin_path = default_python_bin_path + # Check if the path is valid + if os.path.isfile(python_bin_path) and os.access(python_bin_path, os.X_OK): + pass + elif not os.path.exists(python_bin_path): + print('Invalid python path: %s cannot be found.' % python_bin_path) + continue + else: + print('%s is not executable. Is it the python binary?' % python_bin_path) + continue + + try: + compile_args = run_command([ + python_bin_path, '-c', + 'import distutils.sysconfig; import torch; print(torch.__version__ + "|" +' + ' "|".join(torch.__path__) + "|" + distutils.sysconfig.get_python_inc())']).split("|") + if (not compile_args[0].startswith(_PYTORCH_VERSION_1_8)) and \ + (not compile_args[0].startswith(_PYTORCH_VERSION_1_11)) and \ + (not compile_args[0].startswith(_PYTORCH_VERSION_2_0)) and \ + (not compile_args[0].startswith(_PYTORCH_VERSION_2_1)): + print('Currently supported Pytorch version is %s/%s, we got %s.' + % (_PYTORCH_VERSION_1_8, _PYTORCH_VERSION_1_11, _PYTORCH_VERSION_2_0, _PYTORCH_VERSION_2_1, compile_args[0])) + continue + except subprocess.CalledProcessError: + print('Pytorch is not installed or does not work properly.') + continue + # Write tools/python_bin_path.sh + with open(config_path('PYTHON_BIN_PATH'), 'w') as f: + f.write(python_bin_path) + with open(config_path('PYTORCH_INSTALLED_PATH'), 'w') as f: + f.write(compile_args[1]) + break + + +def main(): + """main.""" + env_snapshot = dict(os.environ) + setup_python(env_snapshot.get(_PYTHON_BIN_PATH_ENV)) + + +if __name__ == '__main__': + main() diff --git a/debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md b/debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md new file mode 100644 index 0000000000..4ec3b2d30b --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md @@ -0,0 +1,33 @@ +## FAQ + +### 1. 单机多卡场景dump目录下只生成一个rank目录或pkl文件格式损坏 + +**故障现象** + +dump目录下只生成一个rank目录或dump目录下的pkl文件格式损坏、内容不完整。 + +**故障原因** + +通常是因为register_hook没有正确配置,带着工具没有获取正确的`rank_id`(从rank参数读取或从模型参数的device_id读取)。 + +**故障处理** + +register_hook需要在set_dump_path之后调用,也需要在每个进程上被调用,建议在搬运模型数据到卡之后调用。识别方法如下: + +- 找到训练代码中遍历epoch的for循环或遍历数据集的for循环,把register_hook放到循环开始前即可。 +- 找到训练代码中调用DDP或者DistributedDataParallel的代码行,把register_hook放到该代码行所在的代码块之后。 +- 若代码中均无以上两种情况,那么尽可能把这行代码往后放,并配置register_hook的rank参数。 + +### 2. HCCL 报错: error code: EI0006 + +**故障现象** + +使用ptdbg_ascend工具时,报错: error code: EI0006。 + +**故障原因** + +CANN软件版本较低导致不兼容。 + +**故障处理** + +升级新版CANN软件版本。 diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/Pytorch \350\277\220\347\256\227\351\207\215\350\275\275API\345\222\214Acl\347\256\227\345\255\220\345\257\271\345\272\224\345\205\263\347\263\273.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/Pytorch \350\277\220\347\256\227\351\207\215\350\275\275API\345\222\214Acl\347\256\227\345\255\220\345\257\271\345\272\224\345\205\263\347\263\273.md" new file mode 100644 index 0000000000..860b455588 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/Pytorch \350\277\220\347\256\227\351\207\215\350\275\275API\345\222\214Acl\347\256\227\345\255\220\345\257\271\345\272\224\345\205\263\347\263\273.md" @@ -0,0 +1,39 @@ +工具Dump文件命名规则,`{api_type}_{api_name}_{api调用次数}_{正反向}_{输入输出}.index`, 如 Functional_conv2d_1_backward_input.0 +Tensor___bool___0_forward_input.0 +Torch_conv2d_1_backward_input.0 + +Pytorch运算符重载后API(主要涉及api_type为Tensor),工具当前支持Dump的运算符和ACL对应关系整理如下 + +| API | ACL | 运算符 | +| ------------ | ------------ | ------------ | +| __ add __ | Add | + | +| __ and __ | BitwiseAnd | & | +| __ bool __ | NonZero | if x | +| __ div __ | RealDiv | / | +| __ ge __ | GreaterEqual | >= | +| __ gt__ | Greater | > | +| __ iadd__ | Add | += | +| __ iand__ | BitwiseAnd | &= | +| __ idiv__ | RealDiv | /= | +| __ ifloordiv__ | FloorDiv | //= | +| __ ilshift__ | LeftShift | <<= | +| __ imod__ | FloorMod | %= | +| __ imul__ | Mul | *= | +| __ ior__ | BitwiseOr | \|= | +| __ irshift__ | RightShift | >= | +| __ isub__ | Sub | -= | +| __ ixor__ | BitwiseXor | ^= | +| __ lshift__ |LeftShift | << | +| __ matmul__ | Dot | @ | +| __ mod__ | FloorMod | % | +| __ mul__ | Mul | * | +| __ nonzero__ | NonZero | 暂无 | +| __ or__ | BitwiseOr | \| | +| __ radd__ | Add | + | +| __ rmul__ | Mul | * | +| __ rshift__ | RightShift | >> | +| __ sub__ | Sub | - | +| __ truediv__ | RealDiv | / | +| __ xor__ | BitwiseXor | ^ | +| floor_divide | FloorDiv | // | +| __ getitem __ | Strideslice | [] | diff --git a/debug/accuracy_tools/ptdbg_ascend/doc/img/auto_analyze_log.png b/debug/accuracy_tools/ptdbg_ascend/doc/img/auto_analyze_log.png new file mode 100644 index 0000000000000000000000000000000000000000..999b47f97ef5661316c7e61dbdc93c87996259f3 GIT binary patch literal 65144 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 literal 0 HcmV?d00001 diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v1.0.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v1.0.md" new file mode 100644 index 0000000000..bce52373d8 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v1.0.md" @@ -0,0 +1,539 @@ +# **PyTorch精度工具使用指南** + +## 简介 +本文介绍ptdbg_ascend精度工具,用来进行整网API粒度的数据dump,精度比对和溢出检测,从而定位pytoch训练场景下的精度问题。 + +## 工具安装 + +### 环境和依赖 + +#### 环境 +- 可执行pytorch训练任务的训练环境(安装了Pytorch 1.8 或者 Pytorch 1.11版本) + +#### 工具依赖 +- 通过pip安装环境依赖numpy、pandas、pyyaml + +### 工具安装方式 + +ptdbg_ascend精度工具的安装方式包括:下载whl包安装和源代码编译安装。本文主要介绍whl包安装,源码编译安装详见:[ptdbg_ascend](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend)。 + + +#### 下载whl包安装 + +1. 下载ptdbg_ascend精度工具的whl包。 + + - [ptdbg_ascend-1.0-py3-none-any.whl](https://ptdbg.obs.myhuaweicloud.com/package/ptdbg_ascend/1.0/ptdbg_ascend-1.0-py3-none-any.whl) + +2. 执行如下命令,进行安装。 + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl + ``` + + {version}表示软件版本号。 + + 说明:若为覆盖安装,请增加“--force-reinstall”参数强制安装,例如: + + ```bash + pip3 install ./ptdbg_ascend-{version}-py3-none-any.whl --force-reinstall + ``` + + 分别提示如下信息则表示安装成功: + + ```bash + # ptdbg_ascend精度工具 + Successfully installed ptdbg_ascend-{version} + ``` + +## 功能介绍 + +### 接口说明 + +工具提供如下接口函数用于dump过程的配置,描述如下: + +| 函数 | 描述 | +| ------------------- |---------------------------------------------------------------------------------------------------| +| set_dump_path | 用于设置dump文件的路径(包含文件名),参数示例:“/var/log/dump/npu_dump.pkl” | +| set_dump_switch | 设置dump范围,不设置则默认处于关闭状态。第一个参数为:“ON” 或者 "OFF",若需要控制dump的算子范围,则需要第二、三个参数,默认不配置 | +| seed_all | 固定随机数,参数为随机数种子,默认种子为:1234. | + | set_backward_input | 设置反向ACL级别dump时需要的反向输入的路径,参数示例:"acl_dump_xxx/Functional_conv2d_1_backward_input.0.npy" +| register_hook | 用于注册dump回调函数,例如:注册精度比对hook:register_hook(model, acc_cmp_dump). | +| compare | 比对接口,将GPU/CPU/NPU的dump文件进行比对,第三个参数为存放比对结果的目录;
文件名称基于时间戳自动生成,格式为:compare_result_timestamp.csv. | +| parse | (若pkl文件中有)打印特定api接口的堆栈信息、统计数据信息,第一个参数为pkl文件名,第二个参数为要抽取的api接口前缀,例如"Torch_norm_1_forward". | +| compare_distributed | 单机多卡场景下的比对,自动检索和匹配对应卡和进程所dump的数据文件,再调用compare做比对。也支持单机单卡使用。 | + +### 数据dump +#### 使用说明 +1) seed_all和set_dump_path在训练主函数main一开始就调用,避免随机数固定不全; +2) register_hook须在set_dump_path之后调用,避免dump数据路径设置错误 +3) set_dump_switch提供多种dump模式,可以根据不同场景选择dump方式 +4) 进行CPU数据dump时,请安装torch包而非torch_npu包,避免工具无法识别使用场景,导致失败 +5) TASK_QUEUE_ENABLE环境变量会导致算子下发和执行异步进行,因此在ACL dump前需要将TASK_QUEUE_ENABLE关闭,需要在执行运行命令前先export TASK_QUEUE_ENABLE=0 +``` +# 多种dump模式介绍 + +# 示例1: dump指定api/api列表. +set_dump_switch("ON", mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]) + +# 示例2: dump指定范围. 会dump Tensor_abs_1_forward 到 Tensor_transpose_3_forward之间的所有api +set_dump_switch("ON", mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + +# 示例3: STACK模式,只dump堆栈信息, 示例中dump "Tensor_abs_1_forward" 到 "Tensor_transpose_3_forward" 之间所有api的STACK信息 +set_dump_switch("ON", mode="stack", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + +# 示例4: dump指定api/api列表的ACL级别的输入输出数据 +set_dump_switch("ON", mode="acl", scope=["Tensor_abs_1_forward"]) + +# 示例5: dump指定某一类api的api级别输入输出数据 +set_dump_switch("ON", mode="api_list", api_list=["relu"]) + +# 示例6: dump全部api级别输入输出数据以及相应堆栈信息 +set_dump_switch("ON", mode="api_stack") + +``` +4) dump数据存盘说明:
+ +- 精度比对dump场景
+ 假设配置的dump文件名为npu_dump.pkl,此时dump的结果为两部分: + +* 文件npu_dump.pkl 中包含dump数据的api名称、dtype、 shape、统计信息:max, min, mean.
+* 文件夹npu_dump_timestamp,文件夹下为numpy格式的dump数据.
+ numpy文件保存的前缀和Pytorch对应关系如下 + +| 前缀 | Torch模块 | +| ------------------- |---------------------------------------------------------------------------------------------------| +| Tensor | torch.Tensor | +| Torch | torch | +| Functional | torch.nn.functional | +| NPU | NPU亲和算子 | +| VF | torch._VF | + +当dump模式配置为 "api_stack"时 假设配置的dump文件名为npu_dump.pkl,文件名会被添加api_stack前缀,此时dump的结果为两部分: +* 文件api_stack_npu_dump.pkl 中包含dump数据的api名称、dtype、 shape、统计信息:max, min, mean,以及堆栈信息。
+* 文件夹api_stack_npu_dump_timestamp,文件夹下为numpy格式的dump数据.
+ +**【新改动】** 单机多卡比对功能已上线,dump数据文件夹组织统一改为如下格式 + ``` + ├── dump_path + │   └── ptdbg_dump_v1.0 + │   ├── rank0 + │   │   ├── myDump + | | | ├── Tensor_permute_1_forward.npy + | | | ... + | | | └── Fcuntion_linear_5_backward_output.npy + │   │ └── myDump.pkl + │   ├── rank1 + | | ├── myDump + | | | └── ... + | | └── myDump.pkl + │   ├── rank2 + | | ├── myDump + | | | └── ... + | | └── myDump.pkl + │   ├── ... + │   | + | └── rank7 + ``` +引入这个格式是为了区分各卡所dump数据,有多少张卡就有多少个rank文件夹。同时为了避免单卡和多卡使用方式割裂,单机单卡使用工具也会形成上述文件夹格式,仅在卡数量上有区别。 +具体生成方式和单机多卡的精度工具使用教程见下文场景4。 + + +5) 整网dump和指定范围dump结果的区别: +* 指定范围dump时,npu_dump.pkl 中还包含stack信息
+ +6) 溢出检测dump场景
+测试不需要配置dump文件名,会在当前目录自动生成`ptdbg_dump_v1.0`文件夹,并且按卡数量创建rank文件夹,每张卡dump数据会在对应rank文件夹中: + +* 溢出检测的pkl文件名格式为`Overflow_info_{timestamp}.pkl`,每次溢出时时间戳不同
+ pkl文件中包含dump数据的api名称、dtype、 shape(不包含统计信息max, min, mean)。 +* 对应的dump数据存放目录为`Overflow_info_{timestamp}`,dump数据为完整Tensor数据,存放格式为numpy。 + + +## 场景化示例 +### 场景1:训练场景的精度问题分析 +第一步,整网Dump比对,初步定位异常范围
+数据dump。NPU和GPU/CPU数据,下面以NPU为例(GPU/CPU dump基本相同):
+``` +from ptdbg_ascend import * + +# 在main函数开始前固定随机数 +seed_all() + +# 设置dump路径(含文件名)和dump_tag。dump_tag会体现在数据文件夹的文件名上 +# 多卡使用时最好也在main函数开始前设置 +set_dump_path("./npu_dump.pkl", dump_tag="dump_conv2d") + +... + +# 注册精度比对dump的hook函数 +# 第一个参数是model对象, 第二个参数为精度比对dump的钩子函数,配置为:acc_cmp_dump,该函数从ptdbg_ascend中import + +# 示例 +register_hook(model, acc_cmp_dump) + +... + +# dump默认处于关闭状态,设置dump开关为打开 +# 如果只在特定的step dump,则在期望dump的迭代开始前打开dump开关,step结束后关掉。 +set_dump_switch("ON") + +... + +# 在期望dump的step结束后关闭dump开关 +set_dump_switch("OFF") + +... + +``` + +比对dump数据
+``` +from ptdbg_ascend import * + +... + +# 数据dump完成后,比对dump的NPU vs GPU/CPU数据, compare第二个参数中的目录必须是已存在的目录 +比对示例: +dump_result_param={ +"npu_pkl_path": "./npu_dump.pkl", +"bench_pkl_path": "./gpu_dump.pkl", +"npu_dump_data_dir": "./npu_dump_20230104_13434", +"bench_dump_data_dir": "./gpu_dump_20230104_132544", +"is_print_compare_log": True +} +compare(dump_result_param, "./output", True) +``` +Dump数据时使用"api_stack" 模式时进行比对dump数据
+``` +from ptdbg_ascend import * + +... + +# 数据dump完成后,比对dump的NPU vs GPU/CPU数据, compare第二个参数中的目录必须是已存在的目录, stack_mode参数需要配置为True, 默认为False +# 请注意:stack_mode为True时,需配置使用"api_stack"模式下的dump数据,其他模式均不需要设置stack_mode +# api_stack为"api_stack"模式下自动生成的前缀(参考4.dump数据存盘数据说明) +比对示例: +dump_result_param={ +"npu_pkl_path": "./api_stack_npu_dump.pkl", +"bench_pkl_path": "./api_stack_gpu_dump.pkl", +"npu_dump_data_dir": "./api_stack_npu_dump_20230104_13434", +"bench_dump_data_dir": "./api_stack_gpu_dump_20230104_132544", +"is_print_compare_log": True +} +compare(dump_result_param, "./output", True, stack_mode=True) +# 比对结果中将展示堆栈信息 +``` + + +第二步:缩小范围分析
+ 指定api范围做完整数据的dump,此时也可以做精度比对。
+ 指定范围dump时,还会dump出stack信息,便于找到api调用点。
+ 示例代码中只包含第一步基础之上,需要调整的设置。 +``` +# 设置dump路径(含文件名),dump路径若不重新设置,会导致整网dump的数据被覆盖 +set_dump_path("./npu_dump_scope.pkl") + +... + +# 注册精度比对dump的hook函数 +register_hook(model, acc_cmp_dump) + +... + +# 通过set_dump_switch控制dump的范围 +# 示例1: dump指定api/api列表. +set_dump_switch("ON", mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_forward"]) +# 示例2: dump指定范围. 会dump Tensor_abs_1_forward 到 Tensor_transpose_2_forward之间的所有api +set_dump_switch("ON", mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_2_forward"]) +# 示例3: dump指定前向api的ACL级别数据. +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='dump.json') +set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"]) +# 示例4: dump指定反向api的ACL级别数据. +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='dump.json') +set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) +set_backward_input(["xxx/Functional_conv2d_1_backward_input.0.npy"]) +... +``` +按范围dump后的分析
+可以基于dump的完整数据做比对,可以结合堆栈信息分析代码,也可以做单API模型的问题复现; + +### 场景2:提取指定API的堆栈信息/dump数据的统计信息 +指定范围dump的信息可能包含多个api,且pkl文件显示不直观,这里通过parse接口可以清晰的显示特定api的堆栈信息和dump数据统计信息 +``` +from ptdbg_ascend import * + +# 提取dump信息中第21次调用的API:Torch_batch_normal的堆栈信息及数据统计信息 +parse("./npu_dump.pkl", "Torch_batch_normal_1_forward") +``` + +### 场景3:溢出检测分析(NPU场景识别aicore浮点溢出,GPU和CPU不支持) +#### 1. api溢出检测,溢出api,api级数据dump +``` +from ptdbg_ascend import * + +# 在main函数起始位置固定随机数 +seed_all() + +... + +#注册溢出检测的hook: +# 第一个参数是model对象, 第二个参数为精度比对dump的钩子函数名,必须配置为:overflow_check,该函数从ptdbg_ascend中import +# 第三个参数为溢出检测的次数,例如配置为3,表示检测到第三次溢出时停止训练; + +# 示例,检测到2次溢出后退出 +register_hook(model, overflow_check, overflow_nums=2) + +... +``` +注:单机多卡使用时各卡单独计算溢出次数。 + +#### 2. api溢出检测,溢出api,acl级数据dump + +``` +from ptdbg_ascend import * + +# 在main函数起始位置固定随机数 +seed_all() + +... + +#注册溢出检测的hook: +# 第一个参数是model对象, 第二个参数为精度比对dump的钩子函数名,必须配置为:overflow_check,该函数从ptdbg_ascend中import +# 第三个参数为overflow_nums表示第几次溢出时,停止训练,例如配置为3,表示检测到第三次溢出时停止训练,过程中检测到溢出API对应ACL数据均dump;默认不配置即检测到一次溢出,训练停止 +# 第四个参数为dump_mode,控制针对溢出api的dump模式,默认api,如需进一步定位acl数据,可配置为dump_mode="acl" +# 第五个参数为dump_config,acl dump的配置文件,dump_mode="acl"时,此配置项为必须的。例如:dump_config='/home/xxx/dump.json' + +# 针对正向溢出场景,可以直接通过上述配置,将溢出api进行acl粒度的数据dump +# 示例,检测到1次溢出后退出,并针对溢出api,进行对应acl粒度的数据dump +register_hook(model, overflow_check, dump_mode='acl', dump_config='/home/xxx/dump.json') + +... + +# 默认全量进行溢出检测 +# 如果只在特定的step 溢出检测,则在期望溢出检测的迭代开始前打开溢出检测开关,step结束后关掉。 +set_overflow_check_switch("ON") + +... + +# 在期望溢出检测的step结束后关闭溢出检测开关 +set_overflow_check_switch("OFF") + +... + +# 对于反向溢出场景获取反向acl级别数据 +# 使用acl模式,配置上梯度输入文件,再进行一次dump +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='dump.json') +set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) +set_backward_input(["xxx/Functional_conv2d_1_backward_input.0.npy"]) # 该输入文件为首次运行得到的反向输入 +``` +#### dump.json配置示例 +``` +{ + "dump": + { + "dump_list":[], + "dump_path":"/home/HwHiAiUser/dump/output", + "dump_mode":"all", + "dump_op_switch":"on" + } +} +``` +#### dump.json参数说明 +| 字段名 | 说明 | +|-----------------|---------------------------------------------------------------------------------------------------| +| dump_list | 待dump数据的算子模型。为空,无需配置。 | +| dump_path | dump数据文件存储到运行环境的目录,支持配置绝对路径或相对路径:
* 绝对路径配置以“/”开头,例如:/home/HwHiAiUser/output。
* 相对路径配置直接以目录名开始,例如:output。
例如:dump_path配置为/home/HwHiAiUser/output,则dump数据文件存储到运行环境的/home/HwHiAiUser/output目录下。 | +| dump_mode | dump数据模式,配置如下:
* output:dump算子的输出数据,默认取值output。
* input:dump算子的输入数据。
* all:dump算子的输入、输出数据。| +| dump_op_switch | 单算子模型dump数据开关,配置如下:
* off:关闭单算子模型dump,默认取值off。
* on:开启单算子模型dump。| + +##### dump路径说明 +采集的dump数据会在{dump_path}/{time}/{deviceid}/{model_id}目录下生成,例如“/home/HwHiAiUser/output/20200808163566/0/0” +``` +├── 20230131172437 +│   └── 1 +│   ├── 0 +│   │   ├── Add.Add.45.0.1675157077183551 +│   │   ├── Cast.trans_Cast_0.31.0.1675157077159449 +│   │   ├── Cast.trans_Cast_5.43.0.1675157077180129 +│   │   ├── MatMul.MatMul.39.0.1675157077172961 +│   │   ├── Mul.Mul.29.0.1675157077155731 +│   │   ├── NPUAllocFloatStatus.NPUAllocFloatStatus.24.0.1675157077145262 +│   │   ├── TransData.trans_TransData_1.33.0.1675157077162791 +│   │   └── TransData.trans_TransData_4.41.0.1675157077176648 +│   ├── 1701737061 +│   │   └── Cast.trans_Cast_2.35.0.1675157077166214 +│   ├── 25 +│   │   └── NPUClearFloatStatus.NPUClearFloatStatus.26.0.1675157077150342 +│   └── 68 +│   └── TransData.trans_TransData_3.37.0.1675157077169473 +``` +#### 注意事项 +此功能原理是,针对溢出阶段,开启acl dump模式,重新对溢出阶段执行,落盘数据。 +* dump_mode="acl"场景下,会增加npu的内存消耗,请用户谨慎开启。 + +* 针对前向溢出api,可以通过以上原理,重新精准执行到溢出前向api,因此可以得到前向溢出api的全部acl数据。 + +* 部分api存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm, 该场景会影响acl init初始化多次,导致功能异常。针对此场景,后续会针对性做适配,当前版本可能存在此问题 + +* 针对前向溢出api,可以通过overflow_nums,配置允许的溢出次数,并将每次溢出api的全部acl数据dump下来,到达指定溢出次数后停止,停止后会看到堆栈打印包含如下字段。 + ValueError: [overflow xxx times]: dump file is saved in 'xxxxx.pkl'. + 其中xxx times为用户设置的次数,xxxxx.pkl为文件生成路径 + +* 对于反向溢出场景获取acl级别数据,第一轮获取反向算子的输入数据,准备好后配置dump.json,并配置好输入数据路径,相关配置如下: + + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["xxx/Functional_conv2d_1_backward_input.0.npy"]) + +### 场景四 单机多卡场景使用精度比对工具 +精度工具单机多卡功能继承了单机单卡时工具的所有功能,如果你想了解工具的基本功能,请参阅上面的场景一到场景三。 +如果你已经熟悉单机单卡使用本工具,想了解如何单机多卡使用,那么请参考[迅速上手:单机多卡使用注意事项](./NotesForMultiCardTraining.md) + +**文件夹格式改动** + +为了支持单机多卡场景,我们模仿ACL溢出检测dump的文件夹,区分了不同rank所dump的数据文件。 +假设dump路径设置为`set_dump_path('./dump_path/myDump.pkl', dump_tag='dump_conv2d')`, +则数据(pkl和包含npy文件的文件夹)会dump在:`./dump_path/{dump_tag}_{version}/rank{rankid}/`路径下。比如: + + ``` + ├── dump_path + │   └── dump_conv2d_v1.0 + │   ├── rank0 + │   │   ├── myDump + | | | ├── Tensor_permute_1_forward.npy + | | | ... + | | | └── Fcuntion_linear_5_backward_output.npy + │   │ └── myDump.pkl + │   ├── rank1 + | | ├── myDump + | | | └── ... + | | └── myDump.pkl + │   ├── rank2 + | | ├── myDump + | | | └── ... + | | └── myDump.pkl + │   ├── ... + │   | + | └── rank7 + ``` + +具体地说,dump_path下首先产生一个`{dump_tag}_{version}`文件夹,`dump_tag`是set_dump_path传入参数设置的,可以用来提高文件夹辨识度。 +`version`是工具版本,用于区分不同版本工具所dump的数据这个文件夹中会根据实际使用卡的数量产生若干`rank`文件夹。 +每张卡上dump结果产生pkl和npy数据文件夹会存在对应的rank文件夹下。 +需要注意的是,如果以相同的dump_path和dump_tag运行两次,则**第二次的数据文件会覆盖第一次的**。 + +**单机多卡使用说明** +1. set_dump_path 设置dump目标路径 + +由于上述文件夹结构改动,你可能已经注意到了最终dump的pkl路径和原本set_dump_path传入的路径不同。 +另外,我们给set_dump_path新增了一个参数`dump_tag`,用来标识本次dump的用途,优化文件夹结构。 +比如,你正在用工具调试ResNet50,首先做了一次全量dump,可以 + +``` +set_dump_path('./dump_resnet50/dump.pkl', dump_tag='all') +``` +经过全量dump你发现其中某个conv2d算子计算误差较大,想要定位到代码行,那么可以 +``` +set_dump_path('./dump_resnet50/dump.pkl', dump_tag='conv2d_stack') +``` +并在`set_dump_switch`时启用stack模式。这样在`dump_resnet50`文件夹下就会分别有`all_{version}`和`conv2d_stack_{version}`两个文件夹,方便查看。 + +2. register_hook 注册工具的dump或溢出检测钩子 + +为了方便区分不同卡上的dump数据,调用register_hook时可以通过`rank`参数传入各自进程所对应的`rank_id`,比如 + +``` +register_hook(model, acc_cmp_dump, rank=rank_id) +``` + +`rank`将决定该进程所dump数据被存入哪个`rank`文件夹(如上面文件夹格式所描述)。如果不清楚当前rank id或者不显式传入, +工具将隐式从传入模型的参数读取`device.index`信息作为`rank`。因此隐式读取时用户须保证在模型已经上卡之后再调用`register_hook` +需要注意的是,由于该函数会创建各卡dump数据时的目标`rank`文件夹,因此在调用register_hook前必须先set_dump_path,否则set_dump_path会失效。 + +3. compare_distributed 分布式比对 + +dump数据之后的比对建议使用`compare_distributed`接口。调用该接口需要传入`npu_dump_dir`, `bench_dump_dir`, `output_path`三个参数, +前两者代表需要比对的两次运行数据所在的总文件夹路径,即上文所说的`{dump_path}/{dump_tag}_{version}` 。函数会自动检测文件夹下的`rank`文件夹并按顺序一一对应, +并调用compare逐个做比对,最终对每对`rank`文件夹生成一个csv比对结果。 + +在上面的例子中,我们可以传入 `dump_path/dump_conv2d_v1.0` 作为`npu_dump_dir`参数。 + + 假设我们要比对的标杆数据在`dump_gpu/dump_conv2d_v1.0`文件夹(文件夹下应有对应数量的rank文件夹),要比对以上两次运行所产生的数据差异, + 就可以把这个路径作为`bench_dump_dir`传入。如: +```python +compare_distributed('dump_path/dump_conv2d_v1.0', 'dump_gpu/dump_conv2d_v1.0', './output') +``` +另外,原本`compare`比对函数支持的参数如`shape_flag`、`stack_mode`等,`compare_distributed`函数也支持。 + +**注意:两次运行须用相同数量的卡,传入`compare_distributed`的两个文件夹下须有相同个数的rank文件夹,且不包含其他无关文件,否则将无法比对。** + +### **NPU自定义算子dump** +对于NPU vs NPU场景,本工具还支持对NPU自定义算子的数据dump,目前支持列表如下 + +| NPU自定义算子 | +| ------ | +| torch_npu.one_ | +| torch_npu.npu_sort_v2 | +| torch_npu.npu_transpose | +| torch_npu.npu_broadcast | +| torch_npu.npu_dtype_cast | +| torch_npu.empty_with_format | +| torch_npu.npu_one_hot | +| torch_npu.npu_stride_add | +| torch_npu.npu_ps_roi_pooling | +| torch_npu.npu_roi_align | +| torch_npu.npu_nms_v4 | +| torch_npu.npu_iou | +| torch_npu.npu_nms_with_mask | +| torch_npu.npu_pad | +| torch_npu.npu_bounding_box_encode | +| torch_npu.npu_bounding_box_decode | +| torch_npu.npu_batch_nms | +| torch_npu.npu_slice | +| torch_npu._npu_dropout | +| torch_npu.npu_indexing +| torch_npu.npu_ifmr | +| torch_npu.npu_max | +| torch_npu.npu_scatter | +| torch_npu.npu_layer_norm_eval | +| torch_npu.npu_alloc_float_status | +| torch_npu.npu_get_float_status | +| torch_npu.npu_clear_float_status | +| torch_npu.npu_confusion_transpose | +| torch_npu.npu_bmmV2 | +| torch_npu.fast_gelu | +| torch_npu.npu_sub_sample | +| torch_npu.npu_deformable_conv2d | +| torch_npu.npu_mish | +| torch_npu.npu_anchor_response_flags | +| torch_npu.npu_yolo_boxes_encode | +| torch_npu.npu_grid_assign_positive | +| torch_npu.npu_normalize_batch | +| torch_npu.npu_masked_fill_range | +| torch_npu.npu_linear | +| torch_npu.npu_bert_apply_adam | +| torch_npu.npu_giou | +| torch_npu.npu_ciou | +| torch_npu.npu_ciou_backward | +| torch_npu.npu_diou | +| torch_npu.npu_diou_backward | +| torch_npu.npu_sign_bits_pack | +| torch_npu.npu_sign_bits_unpack | + +### **计算精度评价指标** + +在进行计算精度匹配时,基本共识为默认CPU或GPU的算子计算结果是准确的,最终比对生成的csv文件中主要包括以下的几个属性: + +| NPU Name | Bench Name | Npu Tensor Dtype | Bench Tensor Dtype | Npu Tensor Shape | Bench Tensor Shape | Cosine | MaxAbsError | ... | +|:--------:|:----------:|:----------------:|:-------------------:|:-----------------:|:-------------------:|:-------:|:-------------:|:-----:| + +其中主要使用算子Name、Dtype、Shape用于描述算子的基本特征,Cosine(余弦相似)、MaxAbsError(最大绝对误差)作为评价计算精度的主要评估指标: + +1. 余弦相似度(通过计算两个向量的余弦值来判断其相似度): + + +当余弦夹角数值越接近于1说明计算出的两个张量越相似,在计算中可能会存在nan,主要由于可能会出现其中一个向量为0 + +2. MaxAbsError(最大绝对误差): + +当最大绝对误差越接近0表示其计算的误差越小 diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v2.0.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v2.0.md" new file mode 100644 index 0000000000..eba61ce1a6 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v2.0.md" @@ -0,0 +1,1001 @@ +# **PyTorch精度工具使用指南** + +本文主要介绍PyTorch精度工具精度工具ptdbg_ascend的使用以及精度比对场景示例。 + +ptdbg_ascend工具的原理及安装请参见《[PyTorch精度工具](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/README.md)》。 + +## PyTorch精度比对总体流程 + +1. 准备CPU或GPU训练工程。 + +2. 在环境下安装ptdbg_ascend工具。 + +3. 在训练脚本内插入ptdbg_ascend工具dump接口。 + +4. 执行训练dump数据。 + +5. 将CPU或GPU训练工程迁移为NPU训练工程。 + + 请参见《[PyTorch模型迁移和训练指南](https://www.hiascend.com/document/detail/zh/canncommercial/63RC1/modeldevpt/ptmigr/ptmigr_0001.html)》。 + +6. 在NPU环境下安装ptdbg_ascend工具。 + +7. 在NPU训练脚本内插入ptdbg_ascend工具dump接口。 + +8. NPU环境下执行训练dump数据。 + +9. 创建并配置精度比对脚本,例如compare.py。 + +10. 执行CPU或GPU dump与NPU dump数据的精度比对。 + +11. 比对结果分析。 + +## 场景化示例 + +本章节主要介绍通过ptdbg_ascend工具进行精度比对和分析,主要使用“**CPU或GPU及NPU精度数据dump**”和“**CPU或GPU与NPU精度数据比对**”章节中介绍的ptdbg_ascend工具接口。 + +### 单卡场景精度比对 + +**精度分析建议** + +PyTorch训练场景的精度问题分析建议参考以下思路进行精度比对和比对结果分析: + +1. 整网比对:dump整网数据并进行精度比对,初步定位异常范围。 +2. 缩小范围:根据Accuracy Reached or Not找出不符合精度标准的API。 +3. 范围比对:对不符合精度标准的API重新dump。 +4. 分析原因并优化:分析API精度不符合标准的原因并进行优化调整。 +5. 整网比对:重新进行整网比对,判断优化后的API是否已符合精度标准以及是否出现新的精度问题。 +6. 重复1~5步,直到不存在精度问题为止。 + +**精度分析示例** + +1. dump整网数据。 + + 分别dump CPU或GPU以及NPU数据,在PyTorch训练脚本插入dump接口,示例代码如下(下面以NPU为例,CPU或GPU dump基本相同): + + ```python + from ptdbg_ascend import * + + # 在main函数开始前固定随机数 + seed_all() + + # 配置dump数据目录路径和名称 + set_dump_path("./npu_dump", dump_tag='all') + + # 注册dump回调函数 + register_hook(model, acc_cmp_dump) + + ... + + # 在第一个迭代开始的位置开启dump和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="api_stack", filter_switch="OFF") + + ... + + # 在第一个迭代结束的位置关闭dump + set_dump_switch("OFF") + ``` + +2. 比对整网数据。 + + 第1步中的NPU dump数据文件为npu_dump.pkl,假设NPU dump npy数据目录为npu_dump,GPU dump数据文件为gpu_dump.pkl,GPU dump npy数据目录为gpu_dump。 + + 创建并配置精度比对脚本,以创建compare.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + dump_result_param={ + "npu_pkl_path": "./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "bench_pkl_path": "./gpu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "npu_dump_data_dir": "./npu_dump/all_v2.0/rank0/api_stack_dump", + "bench_dump_data_dir": "./gpu_dump/all_v2.0/rank0/api_stack_dump", + "is_print_compare_log": True + } + compare(dump_result_param, "./output") + ``` + + 执行比对: + + ```bash + python3 compare.py + ``` + + 在output目录下生成结果文件,包括:`compare_result_{timestamp}.csv`和`advisor_{timestamp}.txt` + +3. 找出存在问题的API。 + + 1. 根据`advisor_{timestamp}.txt`或打屏信息的提示,可找到存在精度问题的算子(Suspect Nodes)和专家建议(Expert Advice) + + ![auto_analyze_log](img/auto_analyze_log.png) + + 2. 根据第2步结果文件`compare_result_{timestamp}.csv`中的Accuracy Reached or No字段显示为NO的API,针对该API执行后续比对操作,分析该API存在的精度问题。 + +4. (可选)提取指定API的堆栈信息和dump数据统计信息。 + + 通过parse接口可以清晰的显示特定API的堆栈信息和dump数据统计信息,结合堆栈信息分析代码中可能存在的精度问题。 + + 创建并配置提取脚本,以创建parse.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + + # 提取dump信息中第1次调用的API:Torch_batch_normal的堆栈信息及数据统计信息 + parse("./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", "Torch_batch_normal_1_forward") + ``` + + 执行提取: + + ```bash + python3 parse.py + ``` + + + +5. (可选)指定API dump数据。 + + - dump指定前向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='forward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定前向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"], filter_switch="OFF") + + ... + + set_dump_switch("OFF") + ``` + + - dump指定反向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='backward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定反向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"], filter_switch="OFF") + set_backward_input(["./npu_dump/all_v2.0/rank0/api_stack_dump/Functional_conv2d_1_backward_input.0.npy"]) + + ... + + set_dump_switch("OFF") + ``` + +6. (可选)重新比对。 + + 根据第4或5步的dump数据重新配置compare.py并执行比对,可以对单API模型进行问题复现。 + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +### 多卡场景精度比对 + +精度工具支持多卡场景的精度比对,多卡场景的dump步骤与单卡场景完全一致,请参见“**单卡场景精度比对**”章节,不同的是多卡数据精度比对时需要使用“compare_distributed”函数进行比对。如下示例: + +说明:多机多卡场景需要每个设备单独执行比对操作。 + +假设NPU dump npy数据目录为npu_dump/dump_conv2d_v1.0,GPU dump npy数据目录为gpu_dump/dump_conv2d_v1.0。 + +1. 创建比对脚本,例如compare_distributed.py,拷贝如下代码。 + + ```python + from ptdbg_ascend import * + compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') + ``` + +2. 执行比对: + + ```bash + python3 compare_distributed.py + ``` + +两次运行须用相同数量的卡,传入`compare_distributed`的两个文件夹下须有相同个数的rank文件夹,且不包含其他无关文件,否则将无法比对。 + +**多卡set_dump_path注意事项** + +多卡一般为多进程,须保证每个进程都正确调用set_dump_path,或把set_dump_path插入到import语句后,如: + +```python +from ptdbg_ascend import * +seed_all() +set_dump_path('./dump_resnet') +``` + +如此可保证set_dump_path在每个进程都被调用。 + +**多卡register_hook注意事项** + +register_hook需要在set_dump_path之后调用,也需要在每个进程上被调用,建议在搬运模型数据到卡之后调用。识别方法如下: + +- 找到训练代码中遍历epoch的for循环或遍历数据集的for循环,把register_hook放到循环开始前即可。 +- 找到训练代码中调用DDP或者DistributedDataParallel的代码行,把register_hook放到该代码行所在的代码块之后。 +- 若代码中均无以上两种情况,需要保证register_hook在模型定义之后插入,并配置rank参数。rank参数获取rank_id请参见“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”。 + +### NPU vs NPU精度比对 + +对于NPU vs NPU场景,是针对同一模型,进行迭代(模型、API版本升级或设备硬件升级)时存在的精度下降问题,对比相同模型在迭代前后版本的API计算数值,进行问题定位。 + +一般情况下迭代涉及NPU自定义算子,因此,可以仅dump NPU自定义算子进行比对。比对精度问题分析请参见“**单卡场景精度比对**”章节。 + +工具当前支持dump NPU自定义算子如下: + +| 序号 | NPU自定义算子 | +| :--- | ----------------------------------- | +| 1 | torch_npu.one_ | +| 2 | torch_npu.npu_sort_v2 | +| 3 | torch_npu.npu_transpose | +| 4 | torch_npu.npu_broadcast | +| 5 | torch_npu.npu_dtype_cast | +| 6 | torch_npu.empty_with_format | +| 7 | torch_npu.npu_one_hot | +| 8 | torch_npu.npu_stride_add | +| 9 | torch_npu.npu_ps_roi_pooling | +| 10 | torch_npu.npu_roi_align | +| 11 | torch_npu.npu_nms_v4 | +| 12 | torch_npu.npu_iou | +| 13 | torch_npu.npu_nms_with_mask | +| 14 | torch_npu.npu_pad | +| 15 | torch_npu.npu_bounding_box_encode | +| 16 | torch_npu.npu_bounding_box_decode | +| 17 | torch_npu.npu_batch_nms | +| 18 | torch_npu.npu_slice | +| 19 | torch_npu._npu_dropout | +| 20 | torch_npu.npu_indexing | +| 21 | torch_npu.npu_ifmr | +| 22 | torch_npu.npu_max | +| 23 | torch_npu.npu_scatter | +| 24 | torch_npu.npu_layer_norm_eval | +| 25 | torch_npu.npu_alloc_float_status | +| 26 | torch_npu.npu_get_float_status | +| 27 | torch_npu.npu_clear_float_status | +| 28 | torch_npu.npu_confusion_transpose | +| 29 | torch_npu.npu_bmmV2 | +| 30 | torch_npu.fast_gelu | +| 31 | torch_npu.npu_sub_sample | +| 32 | torch_npu.npu_deformable_conv2d | +| 33 | torch_npu.npu_mish | +| 34 | torch_npu.npu_anchor_response_flags | +| 35 | torch_npu.npu_yolo_boxes_encode | +| 36 | torch_npu.npu_grid_assign_positive | +| 37 | torch_npu.npu_normalize_batch | +| 38 | torch_npu.npu_masked_fill_range | +| 39 | torch_npu.npu_linear | +| 40 | torch_npu.npu_bert_apply_adam | +| 41 | torch_npu.npu_giou | +| 42 | torch_npu.npu_ciou | +| 43 | torch_npu.npu_ciou_backward | +| 44 | torch_npu.npu_diou | +| 45 | torch_npu.npu_diou_backward | +| 46 | torch_npu.npu_sign_bits_pack | +| 47 | torch_npu.npu_sign_bits_unpack | + +### 溢出检测场景 + +溢出检测是针对NPU的PyTorch API,检测是否存在溢出的情况。当前仅支持识别aicore浮点溢出。 + +溢出检测原理:针对溢出阶段,开启acl dump模式,重新对溢出阶段执行,落盘数据。 + +建议按照如下步骤操作: + +1. 在NPU环境下安装ptdbg_ascend工具。 + +2. 在NPU训练脚本内插入ptdbg_ascend工具溢出检测接口。 + + - 示例1:全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check, overflow_nums=3) + + ... + ``` + + 多卡使用时各卡单独计算溢出次数。 + + - 示例2:dump指定API的ACL级别溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定API的ACL级别溢出数据 + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + + # 在期望溢出检测的step位置开始前打开溢出检测开关 + set_overflow_check_switch("ON") + + ... + + # 在step结束的位置关闭溢出检测开关 + set_overflow_check_switch("OFF") + + ... + ``` + + - 示例3:dump指定反向API的ACL级别的溢出数据 + + 1. 进行全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check) + + ... + ``` + + 2. dump指定反向API的ACL级别的溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定反向API的ACL级别溢出数据 + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/ptdbg_dump_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 针对前向溢出API,可以通过overflow_nums,配置允许的溢出次数,并将每次溢出API的全部ACL数据dump下来,到达指定溢出次数后停止,停止后会看到堆栈打印包含如下字段。 + + ```bash + ValueError: [overflow xxx times]: dump file is saved in 'xxxxx.pkl'. + ``` + + 其中xxx times为用户设置的次数,xxxxx.pkl为文件生成路径。 + +3. NPU环境下执行训练dump溢出数据。 + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +## CPU或GPU及NPU精度数据dump + +### 总体说明 + +- 本节主要介绍CPU或GPU及NPU精度数据dump所需要的函数以及示例。 + +- ptdbg_ascend工具默认情况下仅dump PyTorch模型的API输入输出数据进行精度比对,若在比对结果中发现某个API下可能存在ACL的精度问题,那么可以选择dump该API的ACL级别数据进行精度分析。 + +- 某些torch api的输出不是Tensor类型的数据。对于此类API的反向过程进行ACL dump,工具会在运行日志中给出对应的Warning(is not of tensor type and cannot be automatically derived)提示。如若想要进行该类API反向ACL dump,可以通过手动构建单API用例的方式进行ACL dump,具体用例可参见“**[反向ACL dump用例说明](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/%E5%8F%8D%E5%90%91ACL%20dump%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E.md)**”。 + +- 工具性能:dump数据量较小时(小于5G),参考dump速度0.1GB/s;dump数据量较大时,参考dump速度0.2GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。Dump速度的计算方式:Dump数据量/(单个step添加Dump耗时-原始单个step耗时)。 + +### 约束 + +- 进行CPU或GPU数据dump时,请安装torch包而非torch_npu包,避免工具无法识别使用场景,导致失败。 + +- TASK_QUEUE_ENABLE环境变量会导致API下发和执行异步进行,因此在ACL dump前需要将TASK_QUEUE_ENABLE关闭,即export TASK_QUEUE_ENABLE=0。 + +- 不建议在PyTorch训练脚本中同时添加dump接口和性能数据采集(如Ascend PyThon Profiler)接口,二者可能相互影响导致数据不准确。 + +### seed_all + +**功能说明** + +固定随机数。通过固定随机数保证模型的输入或输出一致。在训练主函数开始前调用,避免随机数固定不全。 + +dump操作必选。 + +**函数原型** + +```python +seed_all(seed=1234, mode=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------ | ------------------------------------------------------------ | -------- | +| seed | 随机数种子。参数示例:seed=1000。默认值为:1234。 | 否 | +| mode | 确定性计算模式。可配置True或False。参数示例:mode=True。默认为False。
即使在相同的硬件和输入下,API多次执行的结果也可能不同,开启确定性计算是为了保证在相同的硬件和输入下,API多次执行的结果相同。
确定性计算会导致API执行性能降低,建议在发现模型多次执行结果不同的情况下开启。
rnn类算子、ReduceSum、ReduceMean等算子可能与确定性计算存在冲突,若开启确定性计算后多次执行的结果不相同,则考虑存在这些算子。 | 否 | + +**函数示例** + +seed_all函数的随机数种子,取默认值即可,无须配置;第二个参数默认关闭,不开启确定性计算时也无须配置。 + +- 示例1:仅固定随机数,不开启确定性计算 + + ```python + seed_all() + ``` + +- 示例2:固定随机数,开启确定性计算 + + ```python + seed_all(mode=True) + ``` + +**固定随机数范围** + +seed_all函数可固定随机数的范围如下表。 + +| API | 固定随机数 | +| ---------------------------------------- | --------------------------- | +| os.environ['PYTHONHASHSEED'] = str(seed) | 禁止Python中的hash随机化 | +| random.seed(seed) | 设置random随机生成器的种子 | +| np.random.seed(seed) | 设置numpy中随机生成器的种子 | +| torch.manual_seed(seed) | 设置当前CPU的随机种子 | +| torch.cuda.manual_seed(seed) | 设置当前GPU的随机种子 | +| torch.cuda.manual_seed_all(seed) | 设置所有GPU的随机种子 | +| torch_npu.npu.manual_seed(seed) | 设置当前NPU的随机种子 | +| torch_npu.npu.manual_seed_all(seed) | 设置所有NPU的随机种子 | +| torch.backends.cudnn.enable=False | 关闭cuDNN | +| torch.backends.cudnn.benchmark=False | cuDNN确定性地选择算法 | +| torch.backends.cudnn.deterministic=True | cuDNN仅使用确定性的卷积算法 | + +需要保证CPU或GPU以及NPU的模型输入完全一致,dump数据的比对才有意义,seed_all并不能保证模型输入完全一致,如下表所示场景需要用户自行保证输入的一致性。 + +| 场景 | 固定方法 | +| --------------- | ------------- | +| 数据集的shuffle | 关闭shuffle。 | +| dropout | 关闭dropout。 | + +关闭shuffle示例: + +```python +train_loader = torch.utils.data.DataLoader( + train_dataset, + batch_size = batch_size, + shuffle = False, + num_workers = num_workers +) +``` + +关闭dropout示例: + +```python +torch.nn.functional.dropout(input, p = 0) +``` + +将所有包含dropout的代码设置p = 0,或者可以将所有包含dropout的代码注释。 + +### set_dump_path + +**功能说明** + +设置dump数据目录。建议在seed_all函数之后调用且需要保证训练进程能够调用该函数;多卡时须保证每个进程都能调用该函数。 + +dump操作必选。 + +**函数原型** + +```python +set_dump_path(fpath=None, dump_tag='ptdbg_dump') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------- | ------------------------------------------------------------ | -------- | +| fpath | 设置dump数据目录路径。参数示例:'./dump_path'。dump_path须为已存在目录。
默认在指定的dump_path路径下生成`ptdbg_dump_{version}`目录,并在该目录下生成`dump.pkl`文件以及`dump`数据文件保存目录。
当set_dump_switch函数配置了mode参数时,`dump.pkl`文件以及`dump`数据文件保存目录名称添加mode参数值为前缀,详情请参见“**dump数据存盘说明**”。 | 是 | +| dump_tag | 设置dump数据目录名称。参数示例:dump_tag='dump_conv2d'。默认dump数据目录命名为ptdbg_dump_{version}。
{version}为当前安装ptdbg_ascend工具版本。目录结构参见“**dump数据存盘说明**”。
配置该参数会将生成的`ptdbg_dump_{version}`目录名称变更为dump_tag配置的值,如`dump_conv2d_{version}`。 | 否 | + +**函数示例** + +- 示例1:设置dump数据目录路径 + + ```python + set_dump_path('./dump_path') + ``` + +- 示例2:设置dump数据目录名称 + + ```python + set_dump_path('./dump_path', dump_tag='dump_conv2d') + ``` + + +若以相同的dump数据目录多次dump,则会因同名导致覆盖;多次dump建议配置不同的dump_tag。 + +### register_hook + +**功能说明** + +注册工具钩子函数。在set_dump_path之后调用。 + +dump操作必选。 + +**函数原型** + +```python +register_hook(model, hook, overflow_nums=overflow_nums, dump_mode=dump_mode, dump_config=dump_config_file, rank=0) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| model | model对象。 | 是 | +| hook | 注册工具的dump和溢出检测钩子。可取值overflow_check和acc_cmp_dump,二选一。 | 是 | +| overflow_nums | 控制溢出次数,表示第N次溢出时,停止训练,过程中检测到溢出API对应ACL数据均dump。参数示例:overflow_nums=3。配置overflow_check时可配置,默认不配置,即检测到1次溢出,训练停止。 | 否 | +| dump_mode | 控制针对溢出API的dump模式。可取值"api"或"acl",配置acl时表示dump ACL级别的溢出数据,此时set_dump_path参数不生效,dump数据目录由dump_config的.json文件配置,参数示例:dump_mode="acl"。默认不配置,即dump API级别的溢出数据。 | 否 | +| dump_config | acl dump的配置文件。dump_mode="acl"时,该参数必选;dump_mode="api"时,该参数不选。参数示例:dump_config='./dump.json'。 | 否 | +| rank | 控制dump数据保存的rank目录名称。参数示例:rank=1。默认不配置,即自动读取dump数据所属的卡并保存在该卡对应的rank目录下。目录结构参见“**dump数据存盘说明**”。
多卡情况下,可能出现工具识别rank出错,导致dump数据保存到错误的rank目录下,此时需要根据“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”配置该参数,以获取正确的rank_id;工具可正确识别rank_id时无须配置该参数。 | 否 | + +**函数示例** + +- 示例1:注册工具钩子函数 + + ```python + register_hook(model, acc_cmp_dump) + ``` + +- 示例2:dump指定API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + ``` + + 需要配置set_dump_switch的mode="acl"以及scope指定为前向或反向API,请参见“**set_dump_switch”**的示例。 + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置dump数据目录。 + +- 示例3:溢出检测dump + + ```python + register_hook(model, overflow_check, overflow_nums=3) + ``` + + dump执行时会在set_dump_path的fpath参数指定的目录下生成ptdbg_dump_{version}目录,保存溢出数据。 + + 多卡场景时,需要检测到至少有一张卡溢出次数达到overflow_nums时,训练结束。 + + 仅支持NPU环境。 + +- 示例4:dump指定API的ACL级别溢出数据 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + + 仅支持NPU环境。 + +### set_dump_switch + +**功能说明** + +设置dump范围。建议在register_hook函数之后的脚本内任意位置插入,但进行精度问题排查建议参照“场景化示例 > 单卡场景精度比对”章节的顺序,先从第一个迭代开始的位置调用并dump整网数据。 + +dump操作必选。 + +**函数原型** + +```python +set_dump_switch(switch, mode='all', scope=[], api_list=[], filter_switch='ON', dump_mode='all') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| --------------- | ------------------------------------------------------------ | -------- | +| switch | dump开关。可取值"ON"或"OFF"。须在选定dump开始的位置配置set_dump_switch("ON");dump结束的位置设置set_dump_switch("OFF"),不设置OFF则表示dump从set_dump_switch("ON")开始的所有数据。 | 是 | +| mode | dump模式。可取值"list"、"range"、"stack"、"acl"、"api_list"、"api_stack",各参数含义请参见本节的“**函数示例**”。参数示例:mode="list"。默认为空。该参数配置值将作为dump数据文件名的前缀,详情请参见“**dump数据存盘说明**”。 | 否 | +| scope或api_list | dump范围。根据model配置的模式选择dump的API范围。参数示例:scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward"])、api_list=["relu"]。默认为空。 | 否 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | +| dump_mode | dump数据过滤。可取值“all”、“forward”和“backward”,表示仅保存dump的数据中文件名包含“forward”或“backward”的前向或反向.npy文件。参数示例dump_mode='backward'。默认为all,即保存所有dump的数据。 | 否 | + +**推荐配置** + +```python +set_dump_switch("ON", mode="api_stack", filter_switch="OFF") +``` + +开启dump数据和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量。 + +**函数示例** + +set_dump_switch可配置多中dump模式,示例如下: + +说明:以下均以dump部分API数据为例,API名可以从首次dump整网数据的结果csv文件中的NPU Name或Bench Name列获取。 + +- 示例1:dump指定API列表 + + ```python + set_dump_switch("ON", mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]) + ``` + +- 示例2:dump指定范围 + + ```python + set_dump_switch("ON", mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例3:STACK模式,只dump堆栈信息 + + ```python + set_dump_switch("ON", mode="stack", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例4:dump指定前向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件。 + +- 示例4:dump指定反向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件,并通过set_backward_input设置反向API输入的.npy文件。 + +- 示例5:dump指定某一类API的API级别输入输出数据 + + ```python + set_dump_switch("ON", mode="api_list", api_list=["relu"]) + ``` + + mode="api_list"时不配置scope。 + +- 示例6:dump全部API级别输入输出数据以及相应堆栈信息 + + ```python + set_dump_switch("ON", mode="api_stack") + ``` + + mode="api_stack"时不配置scope。 + +- 示例7: dump全部API级别输入输出数据并包含bool和整型的tensor以及浮点、bool和整型的标量,默认不配置为ON,会过滤bool和整型数据 + + ```python + set_dump_switch("ON", filter_switch="OFF") + ``` + + 配置filter_switch="OFF"同时也可以配置mode、scope和api_list,除dump ACL级别数据。 + +- 示例8:仅保存dump的数据文件名包含“backward”的反向.npy文件 + + ```python + set_dump_switch("ON", dump_mode="backward") + ``` + + +以上示例均不set_dump_switch("OFF"),表示从set_dump_switch("ON")插入的位置开始到整体训练结束均进行示例中配置的范围dump;若在脚本中插入set_dump_switch("OFF"),则dump操作在此结束。 + +### set_overflow_check_switch + +**功能说明** + +置溢出检测范围。默认不配置该函数,全量进行溢出检测。 + +仅支持NPU环境。 + +**函数原型** + +```python +set_overflow_check_switch(switch, filter_switch='ON') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| switch, | 检测开关。可取值"ON"或"OFF"。如果只在特定的step溢出检测,则在期望溢出检测的step位置开始前插入set_overflow_check_switch("ON"),在step结束的位置插入set_overflow_check_switch("OFF")。 | 是 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | + +**函数示例** + +- 示例1:指定范围溢出检测 + + ```python + register_hook(model, overflow_check) + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,dump执行时会在当前目录自动生成ptdbg_dump_{version}目录,保存溢出数据。 + +- 示例2:前向API的ACL级别范围溢出检测 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + +### set_backward_input + +**功能说明** + +设置反向ACL级别dump时需要的反向输入的.npy文件。 + +**函数原型** + +```python +set_backward_input(backward_input) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| backward_input | 该输入文件为首次运行训练dump得到反向API输入的.npy文件。例如若需要dump Functional_conv2d_1 API的反向过程的输入输出,则需要在dump目录下查找命名包含Functional_conv2d_1、backward和input字段的.npy文件。 | 是 | + +**函数示例** + +```python +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') +set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) +set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) +``` + +### dump.json配置文件说明 + +**dump.json配置示例** + +```python +{ + "dump": + { + "dump_list":[], + "dump_path":"./dump/output", + "dump_mode":"all", + "dump_op_switch":"on" + } +} +``` + +**dump.json参数说明** + +| 字段名 | 说明 | +| -------------- | ------------------------------------------------------------ | +| dump_list | 待dump数据的API模型。为空,无需配置。 | +| dump_path | dump数据文件存储到运行环境的目录,主要用于指定ACL dump数据路径。支持配置绝对路径或相对路径。dump_path须为已存在目录。 | +| dump_mode | dump数据模式,配置如下:
- output:dump API的输出数据。默认值。
- input:dump API的输入数据。
- all:dump API的输入、输出数据。 | +| dump_op_switch | 单API模型dump数据开关,配置如下: * off:关闭单API模型dump,默认值。 * on:开启单API模型dump。 | + +**dump目录说明** + +配置register_hook的dump_config后,采集的dump数据会在{dump_path}/{time}/{deviceid}/{model_id}目录下生成,例如“/home/HwHiAiUser/output/20200808163566/0/0” + +```bash +├── 20230131172437 +│   └── 1 +│   ├── 0 +│   │   ├── Add.Add.45.0.1675157077183551 +│   │   ├── Cast.trans_Cast_0.31.0.1675157077159449 +│   │   ├── Cast.trans_Cast_5.43.0.1675157077180129 +│   │   ├── MatMul.MatMul.39.0.1675157077172961 +│   │   ├── Mul.Mul.29.0.1675157077155731 +│   │   ├── NPUAllocFloatStatus.NPUAllocFloatStatus.24.0.1675157077145262 +│   │   ├── TransData.trans_TransData_1.33.0.1675157077162791 +│   │   └── TransData.trans_TransData_4.41.0.1675157077176648 +│   ├── 1701737061 +│   │   └── Cast.trans_Cast_2.35.0.1675157077166214 +│   ├── 25 +│   │   └── NPUClearFloatStatus.NPUClearFloatStatus.26.0.1675157077150342 +│   └── 68 +│   └── TransData.trans_TransData_3.37.0.1675157077169473 +``` + +### dump数据存盘说明 + +dump结果目录结构示例如下: + +```bash +├── dump_path +│ └── ptdbg_dump_{version} +│ ├── rank0 +│ │ ├── dump +| | | ├── Tensor_permute_1_forward.npy +| | | ... +| | | └── Fcuntion_linear_5_backward_output.npy +│ │ └── dump.pkl +│ ├── rank1 +| | ├── dump +| | | └── ... +| | └── dump.pkl +│ ├── ... +│ | +| └── rank7 +``` + +其中ptdbg_dump_{version}为未设置set_dump_path的dump_tag参数时的默认命名;rank为设备上各卡的ID,每张卡上dump的数据会生成对应dump目录,可由register_hook函数的rank参数控制rank目录名称。 + +**精度比对dump场景** + +精度比对dump场景的结果如下: + +* dump.pkl文件:包含dump数据的API名称、dtype、 shape以及各数据的max、min、mean统计信息。 + +* dump目录:目录下为npy格式的dump数据。 + + npy文件保存的前缀和PyTorch对应关系如下 + + | 前缀 | Torch模块 | + | ---------- | ------------------- | + | Tensor | torch.Tensor | + | Torch | torch | + | Functional | torch.nn.functional | + | NPU | NPU亲和算子 | + | VF | torch._VF | + +当set_dump_switch配置mode参数(例如:mode="api_stack" )时,dump结果的文件名会添加api_stack前缀,dump结果如下: + +* api_stack_dump.pkl +* api_stack_dump目录 + +**溢出检测dump场景** + +register_hook设置了overflow_check时,检测API溢出,dump结果的文件名固定为Overflow_info_{timestamp},dump结果如下: + +* Overflow_info_{timestamp}.pkl +* Overflow_info_{timestamp}目录 + +## CPU或GPU与NPU精度数据比对 + +### 总体说明 + +- 本节主要介绍CPU或GPU与NPU精度数据比对的函数以及示例。 + +- 比对函数均通过单独创建精度比对脚本执行,可支持单卡和多卡场景的精度数据比对。 + +- 工具性能:比对数据量较小时(参考值单份文件小于10GB),参考比对速度0.1GB/s;比对数据量较大时,参考比对速度0.3GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。比对速度的计算方式:两份比对文件大小/比对耗时。 + +### 约束 + +- NPU自研API,在CPU或GPU若没有对应的API,该API的dump数据不比对。 + +- NPU与CPU或GPU的计算结果误差可能会随着模型的执行不断累积,最终会出现同一个API因为输入的数据差异较大而无法比对的情况。 + +- CPU或GPU与NPU中两个相同的API会因为调用次数不同导致无法比对或比对到错误的API,不影响整体运行,该API忽略。 + +### compare_distributed + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,支持单卡和多卡,可同时比对多卡的dump数据。多机场景需要每个设备单独执行比对操作。可自动检索和匹配对应卡和进程所dump的数据文件,再调用compare进行比对。单机单卡时与compare函数二选一。 + +**函数原型** + +```python +compare_distributed(npu_dump_dir, bench_dump_dir, output_path, **kwargs) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| npu_dump_dir | 配置NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./npu_dump/dump_conv2d_v2.0'。 | 是 | +| bench_dump_dir | 配置CPU、GPU或NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./gpu_dump/dump_conv2d_v2.0'。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。需要预先创建output_path目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_rank{npu_ID}-rank{cpu/gpu/npu_ID}_{timestamp}.csv`。 | 是 | +| **kwargs | 支持compare的所有可选参数。 | 否 | + +**函数示例** + +创建比对脚本,例如compare_distributed.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') +``` + +### compare + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,仅支持单机单卡。 + +**函数原型** + +```python +compare(input_param, output_path, stack_mode=False, auto_analyze=True, suffix='', fuzzy_match=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------ | ------------------------------------------------------------ | -------- | +| input_param | 配置dump数据文件及目录。配置参数包括:
- "npu_pkl_path":指定NPU dump目录下的.pkl文件。参数示例:"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "bench_pkl_path":指定CPU、GPU或NPU dump目录下的.pkl文件。参数示例:"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "npu_dump_data_dir":"指定NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "bench_dump_data_dir":"指定CPU、GPU或NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "is_print_compare_log":配置是否开启日志打屏。可取值True或False。可选。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_{timestamp}.csv`。 | 是 | +| stack_mode | 配置stack_mode的开关。仅当dump数据时配置set_dump_switch的mode="api_stack"时需要开启。参数示例:stack_mode=True,默认为False。 | 否 | +| auto_analyze | 自动精度分析,开启后工具自动针对比对结果进行分析,识别到第一个精度不达标节点(在比对结果文件中的“Accuracy Reached or Not”列显示为No),并给出问题可能产生的原因(打屏展示并生成advisor_{timestamp}.txt文件)。可取值True或False,参数示例:auto_analyze=False,默认为True。 | 否 | +| suffix | 标识比对结果的文件名。配置的suffix值在比对结果文件名的compare_result和{timestamp}中间插入,例如:`compare_result_{suffix}_{timestamp}`。默认为空。 | 否 | +| fuzzy_match | 模糊匹配。开启后,对于网络中同一层级且命名仅调用次数不同的API,可匹配并进行比对。可取值True或False,参数示例:fuzzy_match=True,默认为False。 | 否 | + +**函数示例** + +单机单卡场景下创建比对脚本,例如compare.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +dump_result_param={ +"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"bench_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"is_print_compare_log": True +} +compare(dump_result_param, "./output", stack_mode=True) +``` + +### parse + +parse 。取值为:
* 第一个参数指定dump数据文件中的pkl文件名。参数示例:"./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl"。必选。
* 第二个参数指定待提取的API接口前缀。参数示例:"Torch_norm_1_forward"。必选。
仅NPU环境支持。 + +**功能说明** + +提取dump信息中的堆栈信息及数据统计信息 + +**函数原型** + +```python +parse(pkl_file, moudule_name_prefix) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------------- | ------------------------------------------------------------ | -------- | +| pkl_file | 指定dump数据文件中的pkl文件名。参数示例:"./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl"。 | 是 | +| moudule_name_prefix | 指定待提取的API接口前缀。参数示例:"Torch_norm_1_forward"。 | 是 | + +**函数示例** + +创建堆栈信息及数据统计信息提取脚本,例如parse.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +parse("./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl", "Torch_batch_normal_1_forward") +``` + +### 计算精度评价指标 + +PyTorch精度比对是以CPU或GPU的计算结果为标杆,计算Cosine(余弦相似度)和MaxAbsErr(最大绝对误差),根据这两个结果判断API在运行时是否存在精度问题。 + +计算精度评价指标: + +1. Cosine:通过计算两个向量的余弦值来判断其相似度,数值越接近于1说明计算出的两个张量越相似,实际可接受阈值为大于0.99。在计算中可能会存在nan,主要由于可能会出现其中一个向量为0。 +2. MaxAbsError:当最大绝对误差越接近0表示其计算的误差越小,实际可接受阈值为小于0.001。 + +精度比对结果csv文件中只需要通过Accuracy Reached or Not来判断计算精度是否达标,判断标准如下: + +1. Cosine < 0.99 且 MaxAbsError > 0.001时,精度不达标,标记为“No”。 +2. Cosine < 0.9,精度不达标,标记为“No”。 +3. MaxAbsError > 1,精度不达标,标记为“No”。 +4. 其余情况下记为精度达标,标记为“Yes”。 + +## FAQ + +[FAQ](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/FAQ.md) diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.1.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.1.md" new file mode 100644 index 0000000000..6cb1103716 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.1.md" @@ -0,0 +1,999 @@ +# **PyTorch精度工具使用指南** + +本文主要介绍PyTorch精度工具精度工具ptdbg_ascend的使用以及精度比对场景示例。 + +ptdbg_ascend工具的原理及安装请参见《[PyTorch精度工具](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/README.md)》。 + +## PyTorch精度比对总体流程 + +1. 准备CPU或GPU训练工程。 + +2. 在环境下安装ptdbg_ascend工具。 + +3. 在训练脚本内插入ptdbg_ascend工具dump接口。 + +4. 执行训练dump数据。 + +5. 将CPU或GPU训练工程迁移为NPU训练工程。 + + 请参见《[PyTorch模型迁移和训练指南](https://www.hiascend.com/document/detail/zh/canncommercial/63RC1/modeldevpt/ptmigr/ptmigr_0001.html)》。 + +6. 在NPU环境下安装ptdbg_ascend工具。 + +7. 在NPU训练脚本内插入ptdbg_ascend工具dump接口。 + +8. NPU环境下执行训练dump数据。 + +9. 创建并配置精度比对脚本,例如compare.py。 + +10. 执行CPU或GPU dump与NPU dump数据的精度比对。 + +11. 比对结果分析。 + +## 场景化示例 + +本章节主要介绍通过ptdbg_ascend工具进行精度比对和分析,主要使用“**CPU或GPU及NPU精度数据dump**”和“**CPU或GPU与NPU精度数据比对**”章节中介绍的ptdbg_ascend工具接口。 + +### 单卡场景精度比对 + +**精度分析建议** + +PyTorch训练场景的精度问题分析建议参考以下思路进行精度比对和比对结果分析: + +1. 整网比对:dump整网数据并进行精度比对,初步定位异常范围。 +2. 缩小范围:根据Accuracy Reached or Not找出不符合精度标准的API。 +3. 范围比对:对不符合精度标准的API重新dump。 +4. 分析原因并优化:分析API精度不符合标准的原因并进行优化调整。 +5. 整网比对:重新进行整网比对,判断优化后的API是否已符合精度标准以及是否出现新的精度问题。 +6. 重复1~5步,直到不存在精度问题为止。 + +**精度分析示例** + +1. dump整网数据。 + + 分别dump CPU或GPU以及NPU数据,在PyTorch训练脚本插入dump接口,示例代码如下(下面以NPU为例,CPU或GPU dump基本相同): + + ```python + from ptdbg_ascend import * + + # 在main函数开始前固定随机数 + seed_all() + + # 配置dump数据目录路径和名称 + set_dump_path("./npu_dump", dump_tag='all') + + # 注册dump回调函数 + register_hook(model, acc_cmp_dump) + + ... + + # 在第一个迭代开始的位置开启dump和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="api_stack", filter_switch="OFF") + + ... + + # 在第一个迭代结束的位置关闭dump + set_dump_switch("OFF") + ``` + +2. 比对整网数据。 + + 第1步中的NPU dump数据文件为npu_dump.pkl,假设NPU dump npy数据目录为npu_dump,GPU dump数据文件为gpu_dump.pkl,GPU dump npy数据目录为gpu_dump。 + + 创建并配置精度比对脚本,以创建compare.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + dump_result_param={ + "npu_pkl_path": "./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "bench_pkl_path": "./gpu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "npu_dump_data_dir": "./npu_dump/all_v2.0/rank0/api_stack_dump", + "bench_dump_data_dir": "./gpu_dump/all_v2.0/rank0/api_stack_dump", + "is_print_compare_log": True + } + compare(dump_result_param, "./output") + ``` + + 执行比对: + + ```bash + python3 compare.py + ``` + + 在output目录下生成结果文件,包括:`compare_result_{timestamp}.csv`和`advisor_{timestamp}.txt` + +3. 找出存在问题的API。 + + 1. 根据`advisor_{timestamp}.txt`或打屏信息的提示,可找到存在精度问题的算子(Suspect Nodes)和专家建议(Expert Advice) + + ![auto_analyze_log](img/auto_analyze_log.png) + + 2. 根据第2步结果文件`compare_result_{timestamp}.csv`中的Accuracy Reached or No字段显示为NO的API,针对该API执行后续比对操作,分析该API存在的精度问题。 + +4. (可选)提取指定API的堆栈信息和dump数据统计信息。 + + 通过parse接口可以清晰的显示特定API的堆栈信息和dump数据统计信息,结合堆栈信息分析代码中可能存在的精度问题。 + + 创建并配置提取脚本,以创建parse.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + + # 提取dump信息中第1次调用的API:Torch_batch_normal的堆栈信息及数据统计信息 + parse("./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", "Torch_batch_normal_1_forward") + ``` + + 执行提取: + + ```bash + python3 parse.py + ``` + + + +5. (可选)指定API dump数据。 + + - dump指定前向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='forward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定前向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"], filter_switch="OFF") + + ... + + set_dump_switch("OFF") + ``` + + - dump指定反向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='backward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定反向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"], filter_switch="OFF") + set_backward_input(["./npu_dump/all_v2.0/rank0/api_stack_dump/Functional_conv2d_1_backward_input.0.npy"]) + + ... + + set_dump_switch("OFF") + ``` + +6. (可选)重新比对。 + + 根据第4或5步的dump数据重新配置compare.py并执行比对,可以对单API模型进行问题复现。 + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +### 多卡场景精度比对 + +精度工具支持多卡场景的精度比对,多卡场景的dump步骤与单卡场景完全一致,请参见“**单卡场景精度比对**”章节,不同的是多卡数据精度比对时需要使用“compare_distributed”函数进行比对。如下示例: + +说明:多机多卡场景需要每个设备单独执行比对操作。 + +假设NPU dump npy数据目录为npu_dump/dump_conv2d_v1.0,GPU dump npy数据目录为gpu_dump/dump_conv2d_v1.0。 + +1. 创建比对脚本,例如compare_distributed.py,拷贝如下代码。 + + ```python + from ptdbg_ascend import * + compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') + ``` + +2. 执行比对: + + ```bash + python3 compare_distributed.py + ``` + +两次运行须用相同数量的卡,传入`compare_distributed`的两个文件夹下须有相同个数的rank文件夹,且不包含其他无关文件,否则将无法比对。 + +**多卡set_dump_path注意事项** + +多卡一般为多进程,须保证每个进程都正确调用set_dump_path,或把set_dump_path插入到import语句后,如: + +```python +from ptdbg_ascend import * +seed_all() +set_dump_path('./dump_resnet') +``` + +如此可保证set_dump_path在每个进程都被调用。 + +**多卡register_hook注意事项** + +register_hook需要在set_dump_path之后调用,也需要在每个进程上被调用,建议在搬运模型数据到卡之后调用。识别方法如下: + +- 找到训练代码中遍历epoch的for循环或遍历数据集的for循环,把register_hook放到循环开始前即可。 +- 找到训练代码中调用DDP或者DistributedDataParallel的代码行,把register_hook放到该代码行所在的代码块之后。 +- 若代码中均无以上两种情况,需要保证register_hook在模型定义之后插入,并配置rank参数。rank参数获取rank_id请参见“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”。 + +### NPU vs NPU精度比对 + +对于NPU vs NPU场景,是针对同一模型,进行迭代(模型、API版本升级或设备硬件升级)时存在的精度下降问题,对比相同模型在迭代前后版本的API计算数值,进行问题定位。 + +一般情况下迭代涉及NPU自定义算子,因此,可以仅dump NPU自定义算子进行比对。比对精度问题分析请参见“**单卡场景精度比对**”章节。 + +工具当前支持dump NPU自定义算子如下: + +| 序号 | NPU自定义算子 | +| :--- | ----------------------------------- | +| 1 | torch_npu.one_ | +| 2 | torch_npu.npu_sort_v2 | +| 3 | torch_npu.npu_transpose | +| 4 | torch_npu.npu_broadcast | +| 5 | torch_npu.npu_dtype_cast | +| 6 | torch_npu.empty_with_format | +| 7 | torch_npu.npu_one_hot | +| 8 | torch_npu.npu_stride_add | +| 9 | torch_npu.npu_ps_roi_pooling | +| 10 | torch_npu.npu_roi_align | +| 11 | torch_npu.npu_nms_v4 | +| 12 | torch_npu.npu_iou | +| 13 | torch_npu.npu_nms_with_mask | +| 14 | torch_npu.npu_pad | +| 15 | torch_npu.npu_bounding_box_encode | +| 16 | torch_npu.npu_bounding_box_decode | +| 17 | torch_npu.npu_batch_nms | +| 18 | torch_npu.npu_slice | +| 19 | torch_npu._npu_dropout | +| 20 | torch_npu.npu_indexing | +| 21 | torch_npu.npu_ifmr | +| 22 | torch_npu.npu_max | +| 23 | torch_npu.npu_scatter | +| 24 | torch_npu.npu_layer_norm_eval | +| 25 | torch_npu.npu_alloc_float_status | +| 26 | torch_npu.npu_get_float_status | +| 27 | torch_npu.npu_clear_float_status | +| 28 | torch_npu.npu_confusion_transpose | +| 29 | torch_npu.npu_bmmV2 | +| 30 | torch_npu.fast_gelu | +| 31 | torch_npu.npu_sub_sample | +| 32 | torch_npu.npu_deformable_conv2d | +| 33 | torch_npu.npu_mish | +| 34 | torch_npu.npu_anchor_response_flags | +| 35 | torch_npu.npu_yolo_boxes_encode | +| 36 | torch_npu.npu_grid_assign_positive | +| 37 | torch_npu.npu_normalize_batch | +| 38 | torch_npu.npu_masked_fill_range | +| 39 | torch_npu.npu_linear | +| 40 | torch_npu.npu_bert_apply_adam | +| 41 | torch_npu.npu_giou | +| 42 | torch_npu.npu_ciou | +| 43 | torch_npu.npu_ciou_backward | +| 44 | torch_npu.npu_diou | +| 45 | torch_npu.npu_diou_backward | +| 46 | torch_npu.npu_sign_bits_pack | +| 47 | torch_npu.npu_sign_bits_unpack | + +### 溢出检测场景 + +溢出检测是针对NPU的PyTorch API,检测是否存在溢出的情况。当前仅支持识别aicore浮点溢出。 + +溢出检测原理:针对溢出阶段,开启acl dump模式,重新对溢出阶段执行,落盘数据。 + +建议按照如下步骤操作: + +1. 在NPU环境下安装ptdbg_ascend工具。 + +2. 在NPU训练脚本内插入ptdbg_ascend工具溢出检测接口。 + + - 示例1:全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check, overflow_nums=3) + + ... + ``` + + 多卡使用时各卡单独计算溢出次数。 + + - 示例2:dump指定API的ACL级别溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定API的ACL级别溢出数据 + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + + # 在期望溢出检测的step位置开始前打开溢出检测开关 + set_overflow_check_switch("ON") + + ... + + # 在step结束的位置关闭溢出检测开关 + set_overflow_check_switch("OFF") + + ... + ``` + + - 示例3:dump指定反向API的ACL级别的溢出数据 + + 1. 进行全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check) + + ... + ``` + + 2. dump指定反向API的ACL级别的溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定反向API的ACL级别溢出数据 + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/ptdbg_dump_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 针对前向溢出API,可以通过overflow_nums,配置允许的溢出次数,并将每次溢出API的全部ACL数据dump下来,到达指定溢出次数后停止,停止后会看到堆栈打印包含如下字段。 + + ```bash + ValueError: [overflow xxx times]: dump file is saved in 'xxxxx.pkl'. + ``` + + 其中xxx times为用户设置的次数,xxxxx.pkl为文件生成路径。 + +3. NPU环境下执行训练dump溢出数据。 + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +## CPU或GPU及NPU精度数据dump + +### 总体说明 + +- 本节主要介绍CPU或GPU及NPU精度数据dump所需要的函数以及示例。 + +- ptdbg_ascend工具默认情况下仅dump PyTorch模型的API输入输出数据进行精度比对,若在比对结果中发现某个API下可能存在ACL的精度问题,那么可以选择dump该API的ACL级别数据进行精度分析。 + +- 某些torch api的输出不是Tensor类型的数据。对于此类API的反向过程进行ACL dump,工具会在运行日志中给出对应的Warning(is not of tensor type and cannot be automatically derived)提示。如若想要进行该类API反向ACL dump,可以通过手动构建单API用例的方式进行ACL dump,具体用例可参见“**[反向ACL dump用例说明](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/%E5%8F%8D%E5%90%91ACL%20dump%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E.md)**”。 + +- 工具性能:dump数据量较小时(小于5G),参考dump速度0.1GB/s;dump数据量较大时,参考dump速度0.2GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。Dump速度的计算方式:Dump数据量/(单个step添加Dump耗时-原始单个step耗时)。 + +### 约束 +- 进行CPU或GPU数据dump时,请安装torch包而非torch_npu包,避免工具无法识别使用场景,导致失败。 + +- TASK_QUEUE_ENABLE环境变量会导致API下发和执行异步进行,因此在ACL dump前需要将TASK_QUEUE_ENABLE关闭,即export TASK_QUEUE_ENABLE=0。 + +- 不建议在PyTorch训练脚本中同时添加dump接口和性能数据采集(如Ascend PyThon Profiler)接口,二者可能相互影响导致数据不准确。 + +### seed_all + +**功能说明** + +固定随机数。通过固定随机数保证模型的输入或输出一致。在训练主函数开始前调用,避免随机数固定不全。 + +dump操作必选。 + +**函数原型** + +```python +seed_all(seed=1234, mode=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------ | ------------------------------------------------------------ | -------- | +| seed | 随机数种子。参数示例:seed=1000。默认值为:1234。 | 否 | +| mode | 确定性计算模式。可配置True或False。参数示例:mode=True。默认为False。
即使在相同的硬件和输入下,API多次执行的结果也可能不同,开启确定性计算是为了保证在相同的硬件和输入下,API多次执行的结果相同。
确定性计算会导致API执行性能降低,建议在发现模型多次执行结果不同的情况下开启。
rnn类算子、ReduceSum、ReduceMean等算子可能与确定性计算存在冲突,若开启确定性计算后多次执行的结果不相同,则考虑存在这些算子。 | 否 | + +**函数示例** + +seed_all函数的随机数种子,取默认值即可,无须配置;第二个参数默认关闭,不开启确定性计算时也无须配置。 + +- 示例1:仅固定随机数,不开启确定性计算 + + ```python + seed_all() + ``` + +- 示例2:固定随机数,开启确定性计算 + + ```python + seed_all(mode=True) + ``` + +**固定随机数范围** + +seed_all函数可固定随机数的范围如下表。 + +| API | 固定随机数 | +| ---------------------------------------- | --------------------------- | +| os.environ['PYTHONHASHSEED'] = str(seed) | 禁止Python中的hash随机化 | +| random.seed(seed) | 设置random随机生成器的种子 | +| np.random.seed(seed) | 设置numpy中随机生成器的种子 | +| torch.manual_seed(seed) | 设置当前CPU的随机种子 | +| torch.cuda.manual_seed(seed) | 设置当前GPU的随机种子 | +| torch.cuda.manual_seed_all(seed) | 设置所有GPU的随机种子 | +| torch_npu.npu.manual_seed(seed) | 设置当前NPU的随机种子 | +| torch_npu.npu.manual_seed_all(seed) | 设置所有NPU的随机种子 | +| torch.backends.cudnn.enable=False | 关闭cuDNN | +| torch.backends.cudnn.benchmark=False | cuDNN确定性地选择算法 | +| torch.backends.cudnn.deterministic=True | cuDNN仅使用确定性的卷积算法 | + +需要保证CPU或GPU以及NPU的模型输入完全一致,dump数据的比对才有意义,seed_all并不能保证模型输入完全一致,如下表所示场景需要保证输入的一致性。 + +| 场景 | 固定方法 | +| --------------- | ------------- | +| 数据集的shuffle | 关闭shuffle。 | +| dropout | 关闭dropout。 | + +关闭shuffle示例: + +```python +train_loader = torch.utils.data.DataLoader( + train_dataset, + batch_size = batch_size, + shuffle = False, + num_workers = num_workers +) +``` + +关闭dropout: + +在使用from ptdbg import *后,工具会自动将torch.nn.functional.dropout、torch.nn.functional.dropout2d、torch.nn.functional.dropout3d、torch.nn.Dropout、torch.nn.Dropout2d、torch.nn.Dropout3d的接口参数p置为0。 + +### set_dump_path + +**功能说明** + +设置dump数据目录。建议在seed_all函数之后调用且需要保证训练进程能够调用该函数;多卡时须保证每个进程都能调用该函数。 + +dump操作必选。 + +**函数原型** + +```python +set_dump_path(fpath=None, dump_tag='ptdbg_dump') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------- | ------------------------------------------------------------ | -------- | +| fpath | 设置dump数据目录路径。参数示例:'./dump_path'。dump_path须为已存在目录。
默认在指定的dump_path路径下生成`ptdbg_dump_{version}`目录,并在该目录下生成`dump.pkl`文件以及`dump`数据文件保存目录。
当set_dump_switch函数配置了mode参数时,`dump.pkl`文件以及`dump`数据文件保存目录名称添加mode参数值为前缀,详情请参见“**dump数据存盘说明**”。 | 是 | +| dump_tag | 设置dump数据目录名称。参数示例:dump_tag='dump_conv2d'。默认dump数据目录命名为ptdbg_dump_{version}。
{version}为当前安装ptdbg_ascend工具版本。目录结构参见“**dump数据存盘说明**”。
配置该参数会将生成的`ptdbg_dump_{version}`目录名称变更为dump_tag配置的值,如`dump_conv2d_{version}`。 | 否 | + +**函数示例** + +- 示例1:设置dump数据目录路径 + + ```python + set_dump_path('./dump_path') + ``` + +- 示例2:设置dump数据目录名称 + + ```python + set_dump_path('./dump_path', dump_tag='dump_conv2d') + ``` + + +若以相同的dump数据目录多次dump,则会因同名导致覆盖;多次dump建议配置不同的dump_tag。 + +### register_hook + +**功能说明** + +注册工具钩子函数。在set_dump_path之后调用。 + +dump操作必选。 + +**函数原型** + +```python +register_hook(model, hook, overflow_nums=overflow_nums, dump_mode=dump_mode, dump_config=dump_config_file, rank=0) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| model | model对象。 | 是 | +| hook | 注册工具的dump和溢出检测钩子。可取值overflow_check和acc_cmp_dump,二选一。 | 是 | +| overflow_nums | 控制溢出次数,表示第N次溢出时,停止训练,过程中检测到溢出API对应ACL数据均dump。参数示例:overflow_nums=3。配置overflow_check时可配置,默认不配置,即检测到1次溢出,训练停止。 | 否 | +| dump_mode | 控制针对溢出API的dump模式。可取值"api"或"acl",配置acl时表示dump ACL级别的溢出数据,此时set_dump_path参数不生效,dump数据目录由dump_config的.json文件配置,参数示例:dump_mode="acl"。默认不配置,即dump API级别的溢出数据。 | 否 | +| dump_config | acl dump的配置文件。dump_mode="acl"时,该参数必选;dump_mode="api"时,该参数不选。参数示例:dump_config='./dump.json'。 | 否 | +| rank | 控制dump数据保存的rank目录名称。参数示例:rank=1。默认不配置,即自动读取dump数据所属的卡并保存在该卡对应的rank目录下。目录结构参见“**dump数据存盘说明**”。
多卡情况下,可能出现工具识别rank出错,导致dump数据保存到错误的rank目录下,此时需要根据“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”配置该参数,以获取正确的rank_id;工具可正确识别rank_id时无须配置该参数。 | 否 | + +**函数示例** + +- 示例1:注册工具钩子函数 + + ```python + register_hook(model, acc_cmp_dump) + ``` + +- 示例2:dump指定API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + ``` + + 需要配置set_dump_switch的mode="acl"以及scope指定为前向或反向API,请参见“**set_dump_switch”**的示例。 + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置dump数据目录。 + +- 示例3:溢出检测dump + + ```python + register_hook(model, overflow_check, overflow_nums=3) + ``` + + dump执行时会在set_dump_path的fpath参数指定的目录下生成ptdbg_dump_{version}目录,保存溢出数据。 + + 多卡场景时,需要检测到至少有一张卡溢出次数达到overflow_nums时,训练结束。 + + 仅支持NPU环境。 + +- 示例4:dump指定API的ACL级别溢出数据 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + + 仅支持NPU环境。 + +### set_dump_switch + +**功能说明** + +设置dump范围。建议在register_hook函数之后的脚本内任意位置插入,但进行精度问题排查建议参照“场景化示例 > 单卡场景精度比对”章节的顺序,先从第一个迭代开始的位置调用并dump整网数据。 + +dump操作必选。 + +**函数原型** + +```python +def set_dump_switch(switch, mode="all", scope=[], api_list=[], filter_switch=Const.ON, dump_mode=["all"]): +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| --------------- | ------------------------------------------------------------ | -------- | +| switch | dump开关。可取值"ON"或"OFF"。须在选定dump开始的位置配置set_dump_switch("ON");dump结束的位置设置set_dump_switch("OFF"),不设置OFF则表示dump从set_dump_switch("ON")开始的所有数据。 | 是 | +| mode | dump模式。可取值"all"、"list"、"range"、"stack"、"acl"、"api_list"、"api_stack",各参数含义请参见本节的“**函数示例**”。参数示例:mode="list"。默认为空。该参数配置值将作为dump数据文件名的前缀,详情请参见“**dump数据存盘说明**”。 | 否 | +| scope或api_list | dump范围。根据model配置的模式选择dump的API范围。参数示例:scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward"]、api_list=["relu"]。默认为空。 | 否 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | +| dump_mode | dump数据过滤。可取值“all”、“forward”、“backward”、input和output,表示仅保存dump的数据中文件名包含“forward”、“backward”、input或output的前向、反向、输入或输出的.npy文件。参数示例dump_mode=["backward"]或dump_mode=["forward", "backward"]。默认为all,即保存所有dump的数据。除了all参数只能单独配置外,其他参数可以自由组合。 | 否 | + +**推荐配置** + +```python +set_dump_switch("ON", mode="api_stack", filter_switch="OFF") +``` + +开启dump数据和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量。 + +**函数示例** + +set_dump_switch可配置多中dump模式,示例如下: + +说明:以下均以dump部分API数据为例,API名可以从首次dump整网数据的结果csv文件中的NPU Name或Bench Name列获取。 + +- 示例1:dump指定API列表 + + ```python + set_dump_switch("ON", mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]) + ``` + +- 示例2:dump指定范围 + + ```python + set_dump_switch("ON", mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例3:STACK模式,只dump堆栈信息 + + ```python + set_dump_switch("ON", mode="stack", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例4:dump指定前向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件。 + +- 示例4:dump指定反向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件,并通过set_backward_input设置反向API输入的.npy文件。 + +- 示例5:dump指定某一类API的API级别输入输出数据 + + ```python + set_dump_switch("ON", mode="api_list", api_list=["relu"]) + ``` + + mode="api_list"时不配置scope。 + +- 示例6:dump全部API级别输入输出数据以及相应堆栈信息 + + ```python + set_dump_switch("ON", mode="api_stack") + ``` + + mode="api_stack"时不配置scope。 + +- 示例7: dump全部API级别输入输出数据并包含bool和整型的tensor以及浮点、bool和整型的标量,默认不配置为ON,会过滤bool和整型数据 + + ```python + set_dump_switch("ON", filter_switch="OFF") + ``` + + 配置filter_switch="OFF"同时也可以配置mode、scope和api_list,除dump ACL级别数据。 + +- 示例8:仅保存dump的数据文件名包含“backward”的反向.npy文件 + + ```python + set_dump_switch("ON", dump_mode=["backward"]) + ``` + + +以上示例均不set_dump_switch("OFF"),表示从set_dump_switch("ON")插入的位置开始到整体训练结束均进行示例中配置的范围dump;若在脚本中插入set_dump_switch("OFF"),则dump操作在此结束。 + +### set_overflow_check_switch + +**功能说明** + +置溢出检测范围。默认不配置该函数,全量进行溢出检测。 + +仅支持NPU环境。 + +**函数原型** + +```python +set_overflow_check_switch(switch, filter_switch='ON') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| switch, | 检测开关。可取值"ON"或"OFF"。如果只在特定的step溢出检测,则在期望溢出检测的step位置开始前插入set_overflow_check_switch("ON"),在step结束的位置插入set_overflow_check_switch("OFF")。 | 是 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | + +**函数示例** + +- 示例1:指定范围溢出检测 + + ```python + register_hook(model, overflow_check) + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,dump执行时会在当前目录自动生成ptdbg_dump_{version}目录,保存溢出数据。 + +- 示例2:前向API的ACL级别范围溢出检测 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + +### set_backward_input + +**功能说明** + +设置反向ACL级别dump时需要的反向输入的.npy文件。 + +**函数原型** + +```python +set_backward_input(backward_input) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| backward_input | 该输入文件为首次运行训练dump得到反向API输入的.npy文件。例如若需要dump Functional_conv2d_1 API的反向过程的输入输出,则需要在dump目录下查找命名包含Functional_conv2d_1、backward和input字段的.npy文件。 | 是 | + +**函数示例** + +```python +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') +set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) +set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) +``` + +### dump.json配置文件说明 + +**dump.json配置示例** + +```python +{ + "dump": + { + "dump_list":[], + "dump_path":"./dump/output", + "dump_mode":"all", + "dump_op_switch":"on" + } +} +``` + +**dump.json参数说明** + +| 字段名 | 说明 | +| -------------- | ------------------------------------------------------------ | +| dump_list | 待dump数据的API模型。为空,无需配置。 | +| dump_path | dump数据文件存储到运行环境的目录,主要用于指定ACL dump数据路径。支持配置绝对路径或相对路径。dump_path须为已存在目录。 | +| dump_mode | dump数据模式,配置如下:
- output:dump API的输出数据。默认值。
- input:dump API的输入数据。
- all:dump API的输入、输出数据。 | +| dump_op_switch | 单API模型dump数据开关,配置如下: * off:关闭单API模型dump,默认值。 * on:开启单API模型dump。 | + +**dump目录说明** + +配置register_hook的dump_config后,采集的dump数据会在{dump_path}/{time}/{deviceid}/{model_id}目录下生成,例如“/home/HwHiAiUser/output/20200808163566/0/0” + +```bash +├── 20230131172437 +│   └── 1 +│   ├── 0 +│   │   ├── Add.Add.45.0.1675157077183551 +│   │   ├── Cast.trans_Cast_0.31.0.1675157077159449 +│   │   ├── Cast.trans_Cast_5.43.0.1675157077180129 +│   │   ├── MatMul.MatMul.39.0.1675157077172961 +│   │   ├── Mul.Mul.29.0.1675157077155731 +│   │   ├── NPUAllocFloatStatus.NPUAllocFloatStatus.24.0.1675157077145262 +│   │   ├── TransData.trans_TransData_1.33.0.1675157077162791 +│   │   └── TransData.trans_TransData_4.41.0.1675157077176648 +│   ├── 1701737061 +│   │   └── Cast.trans_Cast_2.35.0.1675157077166214 +│   ├── 25 +│   │   └── NPUClearFloatStatus.NPUClearFloatStatus.26.0.1675157077150342 +│   └── 68 +│   └── TransData.trans_TransData_3.37.0.1675157077169473 +``` + +### dump数据存盘说明 + +dump结果目录结构示例如下: + +```bash +├── dump_path +│ └── ptdbg_dump_{version} +│ ├── rank0 +│ │ ├── dump +| | | ├── Tensor_permute_1_forward.npy +| | | ... +| | | └── Fcuntion_linear_5_backward_output.npy +│ │ └── dump.pkl +│ ├── rank1 +| | ├── dump +| | | └── ... +| | └── dump.pkl +│ ├── ... +│ | +| └── rank7 +``` + +其中ptdbg_dump_{version}为未设置set_dump_path的dump_tag参数时的默认命名;rank为设备上各卡的ID,每张卡上dump的数据会生成对应dump目录,可由register_hook函数的rank参数控制rank目录名称。 + +**精度比对dump场景** + +精度比对dump场景的结果如下: + +* dump.pkl文件:包含dump数据的API名称、dtype、 shape以及各数据的max、min、mean统计信息。 + +* dump目录:目录下为npy格式的dump数据。 + + npy文件保存的前缀和PyTorch对应关系如下 + + | 前缀 | Torch模块 | + | ---------- | ------------------- | + | Tensor | torch.Tensor | + | Torch | torch | + | Functional | torch.nn.functional | + | NPU | NPU亲和算子 | + | VF | torch._VF | + +当set_dump_switch配置mode参数(例如:mode="api_stack" )时,dump结果的文件名会添加api_stack前缀,dump结果如下: + +* api_stack_dump.pkl +* api_stack_dump目录 + +**溢出检测dump场景** + +register_hook设置了overflow_check时,检测API溢出,dump结果的文件名固定为Overflow_info_{timestamp},dump结果如下: + +* Overflow_info_{timestamp}.pkl +* Overflow_info_{timestamp}目录 + +## CPU或GPU与NPU精度数据比对 + +### 总体说明 + +- 本节主要介绍CPU或GPU与NPU精度数据比对的函数以及示例。 + +- 比对函数均通过单独创建精度比对脚本执行,可支持单卡和多卡场景的精度数据比对。 + +- 工具性能:比对数据量较小时(参考值单份文件小于10GB),参考比对速度0.1GB/s;比对数据量较大时,参考比对速度0.3GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。比对速度的计算方式:两份比对文件大小/比对耗时。 + +### 约束 + +- NPU自研API,在CPU或GPU若没有对应的API,该API的dump数据不比对。 + +- NPU与CPU或GPU的计算结果误差可能会随着模型的执行不断累积,最终会出现同一个API因为输入的数据差异较大而无法比对的情况。 + +- CPU或GPU与NPU中两个相同的API会因为调用次数不同导致无法比对或比对到错误的API,不影响整体运行,该API忽略。 + +### compare_distributed + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,支持单卡和多卡,可同时比对多卡的dump数据。多机场景需要每个设备单独执行比对操作。可自动检索和匹配对应卡和进程所dump的数据文件,再调用compare进行比对。单机单卡时与compare函数二选一。 + +**函数原型** + +```python +compare_distributed(npu_dump_dir, bench_dump_dir, output_path, **kwargs) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| npu_dump_dir | 配置NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./npu_dump/dump_conv2d_v2.0'。 | 是 | +| bench_dump_dir | 配置CPU、GPU或NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./gpu_dump/dump_conv2d_v2.0'。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。需要预先创建output_path目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_rank{npu_ID}-rank{cpu/gpu/npu_ID}_{timestamp}.csv`。 | 是 | +| **kwargs | 支持compare的所有可选参数。 | 否 | + +**函数示例** + +创建比对脚本,例如compare_distributed.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') +``` + +### compare + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,仅支持单机单卡。 + +**函数原型** + +```python +compare(input_param, output_path, stack_mode=False, auto_analyze=True, suffix='', fuzzy_match=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------ | ------------------------------------------------------------ | -------- | +| input_param | 配置dump数据文件及目录。配置参数包括:
- "npu_pkl_path":指定NPU dump目录下的.pkl文件。参数示例:"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "bench_pkl_path":指定CPU、GPU或NPU dump目录下的.pkl文件。参数示例:"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "npu_dump_data_dir":"指定NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "bench_dump_data_dir":"指定CPU、GPU或NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "is_print_compare_log":配置是否开启日志打屏。可取值True或False。可选。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_{timestamp}.csv`。 | 是 | +| stack_mode | 配置stack_mode的开关。仅当dump数据时配置set_dump_switch的mode="api_stack"时需要开启。参数示例:stack_mode=True,默认为False。 | 否 | +| auto_analyze | 自动精度分析,开启后工具自动针对比对结果进行分析,识别到第一个精度不达标节点(在比对结果文件中的“Accuracy Reached or Not”列显示为No),并给出问题可能产生的原因(打屏展示并生成advisor_{timestamp}.txt文件)。可取值True或False,参数示例:auto_analyze=False,默认为True。 | 否 | +| suffix | 标识比对结果的文件名。配置的suffix值在比对结果文件名的compare_result和{timestamp}中间插入,例如:`compare_result_{suffix}_{timestamp}`。默认为空。 | 否 | +| fuzzy_match | 模糊匹配。开启后,对于网络中同一层级且命名仅调用次数不同的API,可匹配并进行比对。可取值True或False,参数示例:fuzzy_match=True,默认为False。 | 否 | + +**函数示例** + +单机单卡场景下创建比对脚本,例如compare.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +dump_result_param={ +"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"bench_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"is_print_compare_log": True +} +compare(dump_result_param, "./output", stack_mode=True) +``` + +### parse + +**功能说明** + +解析并提取dump信息中的堆栈信息及数据统计信息。 + +**函数原型** + +```python +parse(pkl_file, moudule_name_prefix) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------------- | ------------------------------------------------------------ | -------- | +| pkl_file | 指定dump数据文件中的pkl文件名。参数示例:"./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl"。 | 是 | +| moudule_name_prefix | 指定待提取的API接口前缀。参数示例:"Torch_norm_1_forward"。 | 是 | + +**函数示例** + +创建堆栈信息及数据统计信息提取脚本,例如parse.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +parse("./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl", "Torch_batch_normal_1_forward") +``` + +### 计算精度评价指标 + +PyTorch精度比对是以CPU或GPU的计算结果为标杆,计算Cosine(余弦相似度)、MaxAbsErr(最大绝对误差)和MaxRelativeErr(最大相对误差),根据这两个结果判断API在运行时是否存在精度问题。 + +计算精度评价指标: + +1. Cosine:通过计算两个向量的余弦值来判断其相似度,数值越接近于1说明计算出的两个张量越相似,实际可接受阈值为大于0.99。在计算中可能会存在nan,主要由于可能会出现其中一个向量为0。 + +2. MaxAbsErr:当最大绝对误差越接近0表示其计算的误差越小,实际可接受阈值为小于0.001。 + +3. MaxRelativeErr:当最大相对误差越接近0表示其计算的误差越小。 + + 当dump数据中存在0或Nan时,比对结果中最大相对误差则出现inf或Nan的情况,属于正常现象。 + +精度比对结果csv文件中只需要通过Accuracy Reached or Not来判断计算精度是否达标,判断标准如下: + +1. Cosine < 0.99 且 MaxAbsError > 0.001时,精度不达标,标记为“No”。 +2. Cosine < 0.9,精度不达标,标记为“No”。 +3. MaxAbsError > 1,精度不达标,标记为“No”。 +4. 其余情况下记为精度达标,标记为“Yes”。 + +## FAQ + +[FAQ](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/FAQ.md) diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" new file mode 100644 index 0000000000..8fbef92fce --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" @@ -0,0 +1,1464 @@ +# **PyTorch精度工具使用指南** + +本文主要介绍PyTorch精度工具精度工具ptdbg_ascend的使用以及精度比对场景示例。 + +ptdbg_ascend工具的原理及安装请参见《[PyTorch精度工具](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/README.md)》。 + +## PyTorch精度比对总体流程 + +1. 准备CPU或GPU训练工程。 + +2. 在环境下安装ptdbg_ascend工具。 + +3. 在训练脚本内插入ptdbg_ascend工具dump接口。 + +4. 执行训练dump数据。 + +5. 将CPU或GPU训练工程迁移为NPU训练工程。 + + 请参见《[PyTorch模型迁移和训练指南](https://www.hiascend.com/document/detail/zh/canncommercial/63RC1/modeldevpt/ptmigr/ptmigr_0001.html)》。 + +6. 在NPU环境下安装ptdbg_ascend工具。 + +7. 在NPU训练脚本内插入ptdbg_ascend工具dump接口。 + +8. NPU环境下执行训练dump数据。 + +9. 创建并配置精度比对脚本,例如compare.py。 + +10. 执行CPU或GPU dump与NPU dump数据的精度比对。 + +11. 比对结果分析。 + +## 场景化示例 + +本章节主要介绍通过ptdbg_ascend工具进行精度比对和分析,主要使用“**CPU或GPU及NPU精度数据dump**”和“**CPU或GPU与NPU精度数据比对**”章节中介绍的ptdbg_ascend工具接口。 + +### 单卡场景精度比对 + +**精度分析建议** + +PyTorch训练场景的精度问题分析建议参考以下思路进行精度比对和比对结果分析: + +1. 整网比对:dump整网数据并进行精度比对,初步定位异常范围。 +2. 缩小范围:根据Accuracy Reached or Not找出不符合精度标准的API。 +3. 范围比对:对不符合精度标准的API重新dump。 +4. 分析原因并优化:分析API精度不符合标准的原因并进行优化调整。 +5. 整网比对:重新进行整网比对,判断优化后的API是否已符合精度标准以及是否出现新的精度问题。 +6. 重复1~5步,直到不存在精度问题为止。 + +**精度分析示例** + +1. dump整网数据。 + + 分别dump CPU或GPU以及NPU数据,在PyTorch训练脚本插入dump接口,示例代码如下(下面以NPU为例,CPU或GPU dump基本相同): + + ```python + from ptdbg_ascend import * + + # 在main函数开始前固定随机数 + seed_all() + + # 配置dump数据目录路径和名称 + set_dump_path("./npu_dump", dump_tag='all') + + # 注册dump回调函数 + register_hook(model, acc_cmp_dump) + + ... + + # 在第一个迭代开始的位置开启dump和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="api_stack", filter_switch="OFF") + + ... + + # 在第一个迭代结束的位置关闭dump + set_dump_switch("OFF") + ``` + +2. 比对整网数据。 + + 第1步中的NPU dump数据文件为npu_dump.pkl,假设NPU dump npy数据目录为npu_dump,GPU dump数据文件为gpu_dump.pkl,GPU dump npy数据目录为gpu_dump。 + + 创建并配置精度比对脚本,以创建compare.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + dump_result_param={ + "npu_pkl_path": "./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "bench_pkl_path": "./gpu_dump/all_v2.0/rank0/api_stack_dump.pkl", + "npu_dump_data_dir": "./npu_dump/all_v2.0/rank0/api_stack_dump", + "bench_dump_data_dir": "./gpu_dump/all_v2.0/rank0/api_stack_dump", + "is_print_compare_log": True + } + compare(dump_result_param, "./output") + ``` + + 执行比对: + + ```bash + python3 compare.py + ``` + + 在output目录下生成结果文件,包括:`compare_result_{timestamp}.csv`和`advisor_{timestamp}.txt` + +3. 找出存在问题的API。 + + 1. 根据`advisor_{timestamp}.txt`或打屏信息的提示,可找到存在精度问题的算子(Suspect Nodes)和专家建议(Expert Advice) + + ![auto_analyze_log](img/auto_analyze_log.png) + + 2. 根据第2步结果文件`compare_result_{timestamp}.csv`中的Accuracy Reached or No字段显示为NO的API,针对该API执行后续比对操作,分析该API存在的精度问题。 + +4. (可选)提取指定API的堆栈信息和dump数据统计信息。 + + 通过parse接口可以清晰的显示特定API的堆栈信息和dump数据统计信息,结合堆栈信息分析代码中可能存在的精度问题。 + + 创建并配置提取脚本,以创建parse.py为例,示例代码如下: + + ```python + from ptdbg_ascend import * + + # 提取dump信息中第1次调用的API:Torch_batch_normal的堆栈信息及数据统计信息 + parse("./npu_dump/all_v2.0/rank0/api_stack_dump.pkl", "Torch_batch_normal_1_forward") + ``` + + 执行提取: + + ```bash + python3 parse.py + ``` + + + +5. (可选)指定API dump数据。 + + - dump指定前向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='forward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定前向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"], filter_switch="OFF") + + ... + + set_dump_switch("OFF") + ``` + + - dump指定反向API的ACL级别数据 + + ```python + from ptdbg_ascend import * + + # 固定随机数,开启确定性计算 + seed_all(mode=True) + set_dump_path("./dump_path", dump_tag='backward') + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + + # dump指定反向API的ACL级别数据、bool和整型的tensor以及浮点、bool和整型的标量 + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"], filter_switch="OFF") + set_backward_input(["./npu_dump/all_v2.0/rank0/api_stack_dump/Functional_conv2d_1_backward_input.0.npy"]) + + ... + + set_dump_switch("OFF") + ``` + +6. (可选)重新比对。 + + 根据第4或5步的dump数据重新配置compare.py并执行比对,可以对单API模型进行问题复现。 + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +### 多卡场景精度比对 + +精度工具支持多卡场景的精度比对,多卡场景的dump步骤与单卡场景完全一致,请参见“**单卡场景精度比对**”章节,不同的是多卡数据精度比对时需要使用“compare_distributed”函数进行比对。如下示例: + +说明:多机多卡场景需要每个设备单独执行比对操作。 + +假设NPU dump npy数据目录为npu_dump/dump_conv2d_v1.0,GPU dump npy数据目录为gpu_dump/dump_conv2d_v1.0。 + +1. 创建比对脚本,例如compare_distributed.py,拷贝如下代码。 + + ```python + from ptdbg_ascend import * + compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') + ``` + +2. 执行比对: + + ```bash + python3 compare_distributed.py + ``` + +两次运行须用相同数量的卡,传入`compare_distributed`的两个文件夹下须有相同个数的rank文件夹,且不包含其他无关文件,否则将无法比对。 + +**多卡set_dump_path注意事项** + +多卡一般为多进程,须保证每个进程都正确调用set_dump_path,或把set_dump_path插入到import语句后,如: + +```python +from ptdbg_ascend import * +seed_all() +set_dump_path('./dump_resnet') +``` + +如此可保证set_dump_path在每个进程都被调用。 + +**多卡register_hook注意事项** + +register_hook需要在set_dump_path之后调用,也需要在每个进程上被调用,建议在搬运模型数据到卡之后调用。识别方法如下: + +- 找到训练代码中遍历epoch的for循环或遍历数据集的for循环,把register_hook放到循环开始前即可。 +- 找到训练代码中调用DDP或者DistributedDataParallel的代码行,把register_hook放到该代码行所在的代码块之后。 +- 若代码中均无以上两种情况,需要保证register_hook在模型定义之后插入,并配置rank参数。rank参数获取rank_id请参见“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”。 + +### NPU vs NPU精度比对 + +对于NPU vs NPU场景,是针对同一模型,进行迭代(模型、API版本升级或设备硬件升级)时存在的精度下降问题,对比相同模型在迭代前后版本的API计算数值,进行问题定位。 + +一般情况下迭代涉及NPU自定义算子,因此,可以仅dump NPU自定义算子进行比对。比对精度问题分析请参见“**单卡场景精度比对**”章节。 + +工具当前支持dump NPU自定义算子如下: + +| 序号 | NPU自定义算子 | +| :--- | ----------------------------------- | +| 1 | torch_npu.one_ | +| 2 | torch_npu.npu_sort_v2 | +| 3 | torch_npu.npu_transpose | +| 4 | torch_npu.npu_broadcast | +| 5 | torch_npu.npu_dtype_cast | +| 6 | torch_npu.empty_with_format | +| 7 | torch_npu.npu_one_hot | +| 8 | torch_npu.npu_stride_add | +| 9 | torch_npu.npu_ps_roi_pooling | +| 10 | torch_npu.npu_roi_align | +| 11 | torch_npu.npu_nms_v4 | +| 12 | torch_npu.npu_iou | +| 13 | torch_npu.npu_nms_with_mask | +| 14 | torch_npu.npu_pad | +| 15 | torch_npu.npu_bounding_box_encode | +| 16 | torch_npu.npu_bounding_box_decode | +| 17 | torch_npu.npu_batch_nms | +| 18 | torch_npu.npu_slice | +| 19 | torch_npu._npu_dropout | +| 20 | torch_npu.npu_indexing | +| 21 | torch_npu.npu_ifmr | +| 22 | torch_npu.npu_max | +| 23 | torch_npu.npu_scatter | +| 24 | torch_npu.npu_layer_norm_eval | +| 25 | torch_npu.npu_alloc_float_status | +| 26 | torch_npu.npu_get_float_status | +| 27 | torch_npu.npu_clear_float_status | +| 28 | torch_npu.npu_confusion_transpose | +| 29 | torch_npu.npu_bmmV2 | +| 30 | torch_npu.fast_gelu | +| 31 | torch_npu.npu_sub_sample | +| 32 | torch_npu.npu_deformable_conv2d | +| 33 | torch_npu.npu_mish | +| 34 | torch_npu.npu_anchor_response_flags | +| 35 | torch_npu.npu_yolo_boxes_encode | +| 36 | torch_npu.npu_grid_assign_positive | +| 37 | torch_npu.npu_normalize_batch | +| 38 | torch_npu.npu_masked_fill_range | +| 39 | torch_npu.npu_linear | +| 40 | torch_npu.npu_bert_apply_adam | +| 41 | torch_npu.npu_giou | +| 42 | torch_npu.npu_ciou | +| 43 | torch_npu.npu_ciou_backward | +| 44 | torch_npu.npu_diou | +| 45 | torch_npu.npu_diou_backward | +| 46 | torch_npu.npu_sign_bits_pack | +| 47 | torch_npu.npu_sign_bits_unpack | + +### 溢出检测场景 + +溢出检测是针对NPU的PyTorch API,检测是否存在溢出的情况。当前仅支持识别aicore浮点溢出。 + +溢出检测原理:针对溢出阶段,开启acl dump模式,重新对溢出阶段执行,落盘数据。 + +建议按照如下步骤操作: + +1. 在NPU环境下安装ptdbg_ascend工具。 + +2. 在NPU训练脚本内插入ptdbg_ascend工具溢出检测接口。 + + - 示例1:全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check, overflow_nums=3) + + ... + ``` + + 多卡使用时各卡单独计算溢出次数。 + + - 示例2:dump指定API的ACL级别溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定API的ACL级别溢出数据 + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + + # 在期望溢出检测的step位置开始前打开溢出检测开关 + set_overflow_check_switch("ON") + + ... + + # 在step结束的位置关闭溢出检测开关 + set_overflow_check_switch("OFF") + + ... + ``` + + - 示例3:dump指定反向API的ACL级别的溢出数据 + + 1. 进行全量溢出检测 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # 设置检测到3次溢出后退出训练 + register_hook(model, overflow_check) + + ... + ``` + + 2. dump指定反向API的ACL级别的溢出数据 + + ```python + from ptdbg_ascend import * + seed_all() + ... + # dump指定反向API的ACL级别溢出数据 + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/ptdbg_dump_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 针对前向溢出API,可以通过overflow_nums,配置允许的溢出次数,并将每次溢出API的全部ACL数据dump下来,到达指定溢出次数后停止,停止后会看到堆栈打印包含如下字段。 + + ```bash + ValueError: [overflow xxx times]: dump file is saved in 'xxxxx.pkl'. + ``` + + 其中xxx times为用户设置的次数,xxxxx.pkl为文件生成路径。 + +3. NPU环境下执行训练dump溢出数据。 + + 针对输入正常但输出存在溢出的API,会训练执行目录下将溢出的API信息dump并保存为`forward_info_{pid}.json`和`backward_info_{pid}.json`,通过 [Ascend模型精度预检工具](https://gitee.com/ascend/att/tree/master/debug/accuracy_tools/api_accuracy_checker)对json文件进行解析,输出溢出API为正常溢出还是非正常溢出,从而帮助用户快速判断。 + + 精度预检工具执行命令如下: + + ``` + cd $ATT_HOME/debug/accuracy_tools/api_accuracy_checker/run_ut + python run_overflow_check.py -forward ./forward_info_0.json -backward ./backward_info_0.json + ``` + +**注意事项** + +* dump_mode="acl"场景下,会增加npu的内存消耗,请谨慎开启。 +* 部分API存在调用嵌套关系,比如functional.batch_norm实际调用torch.batch_norm,该场景会影响acl init初始化多次,导致功能异常。 + +## debugger方式dump和溢出检测(推荐) + +### PrecisionDebugger模块 + +**功能说明** + +PrecisionDebugger模块包含dump和溢出检测功能的总体配置项。可以指定dump目录,设置dump或溢出检测功能,指定dump的卡和迭代。 + +可以在from ptdbg_ascend import *和模型初始化之间的任意位置添加该模块。 + +**原型** + +```python +PrecisionDebugger(dump_path=None, hook_name=None, rank=None): +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| --------- | ------------------------------------------------------------ | -------- | +| dump_path | 设置dump数据目录路径,参数示例:"./dump_path"。dump_path的父目录须为已存在目录。
默认在指定的dump_path路径下生成`ptdbg_dump_{version}`目录,并在该目录下生成`dump.pkl`文件以及`dump`数据文件保存目录。
当**configure_hook**函数配置了mode参数时,`dump.pkl`文件以及`dump`数据文件保存目录名称添加mode参数值为前缀,详情请参见“**dump数据存盘说明**”。 | 是 | +| hook_name | dump模式,可取值dump和overflow_check,表示dump和溢出检测功能,二选一。 | 是 | +| rank | 指定对某张卡上的数据进行dump或溢出检测,默认未配置(表示dump所有卡的数据),须根据实际卡的Rank ID配置。 | 否 | + +### configure_hook函数(可选) + +**功能说明** + +设置dump范围。 + +建议在**PrecisionDebugger**模块与模型初始化之间的任意位置添加,不添加此函数时默认使用mode="api_stack" dump整网数据。 + +**原型** + +dump: + +```python +debugger.configure_hook(mode="api_stack", scope=[], api_list=[], filter_switch="ON", acl_config=None, backward_input=[], input_output_mode=["all"]) +``` + +溢出检测: + +```python +debugger.configure_hook(mode=None, acl_config=None, overflow_nums=1) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ----------------- | ------------------------------------------------------------ | -------- | +| mode | dump模式。可取值"all"、"list"、"range"、"stack"、"acl"、"api_list"、"api_stack",各参数含义请参见本节的“**函数示例**”。参数示例:mode="list"。默认为api_stack。该参数配置值将作为dump数据文件名的前缀,详情请参见“**dump数据存盘说明**”。 | 否 | +| scope或api_list | dump范围。根据model配置的模式选择dump的API范围,mode="api_list"时,需要配置api_list=[],其他模式有需要时配置scope=[]。参数示例:scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward"]、api_list=["relu"]。默认为空。 | 否 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | +| acl_config | acl dump的配置文件。mode="acl"时,该参数必选;mode为其他值时,该参数不选。参数示例:acl_config='./dump.json'。dump.json配置文件详细介绍请参见“**dump.json配置文件说明**”。 | 否 | +| backward_input | 该输入文件为首次运行训练dump得到反向API输入的.npy文件。例如若需要dump Functional_conv2d_1 API的反向过程的输入输出,则需要在dump目录下查找命名包含Functional_conv2d_1、backward和input字段的.npy文件。 | 否 | +| input_output_mode | dump数据过滤。可取值"all"、"forward"、"backward"、"input"和"output",表示仅保存dump的数据中文件名包含"forward"、"backward"、"input"和"output"的前向、反向、输入或输出的.npy文件。参数示例input_output_mode=["backward"]或input_output_mode=["forward", "backward"]。默认为all,即保存所有dump的数据。除了all参数只能单独配置外,其他参数可以自由组合。 | 否 | +| overflow_nums | 控制溢出次数,表示第N次溢出时,停止训练,过程中检测到溢出API对应ACL数据均dump。参数示例:overflow_nums=3。配置overflow_check时可配置,默认不配置,即检测到1次溢出,训练停止。 | 否 | + +**函数示例** + +configure_hook可配置多种dump模式,示例如下: + +说明:以下均以dump部分API数据为例,API名可以从首次dump整网数据的结果csv文件中的NPU Name或Bench Name列获取。 + +- 示例1:dump指定API列表 + + ```python + debugger.configure_hook(mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]) + ``` + +- 示例2:dump指定范围 + + ```python + debugger.configure_hook(mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例3:STACK模式,只dump堆栈信息 + + ```python + debugger.configure_hook(mode="stack", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例4:dump指定前向API的ACL级别数据 + + ```python + debugger.configure_hook(mode="acl", scope=["Tensor_permute_1_forward"], acl_config="./dump.json") + ``` + +- 示例4:dump指定反向API的ACL级别数据 + + ```python + debugger.configure_hook(mode="acl", scope=["Functional_conv2d_1_backward"], acl_config="./dump.json", backward_input=["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + +- 示例5:dump指定某一类API的API级别输入输出数据 + + ```python + debugger.configure_hook(mode="api_list", api_list=["relu"]) + ``` + + mode="api_list"时不配置scope。 + +- 示例6:dump全部API级别输入输出数据以及相应堆栈信息 + + ```python + debugger.configure_hook(mode="api_stack") + ``` + + mode="api_stack"时不配置scope。 + +- 示例7: dump全部API级别输入输出数据并包含bool和整型的tensor以及浮点、bool和整型的标量,默认不配置为ON,会过滤bool和整型数据 + + ```python + debugger.configure_hook(filter_switch="OFF") + ``` + + 配置filter_switch="OFF"同时也可以配置mode、scope和api_list,除dump ACL级别数据。 + +- 示例8:仅保存dump的数据文件名包含“backward”的反向.npy文件 + + ```python + debugger.configure_hook(input_output_mode=["backward"]) + ``` + +- 示例9:溢出检测dump + + ```python + debugger.configure_hook(overflow_nums=1) + ``` + + dump执行时会在**PrecisionDebugger**模块的dump_path参数指定的目录下生成ptdbg_dump_{version}目录,保存溢出数据。 + + 多卡场景时,需要检测到至少有一张卡溢出次数达到overflow_nums时,训练结束。 + + 仅支持NPU环境。 + +- 示例10:dump指定API的ACL级别溢出数据 + + ```python + debugger.configure_hook(mode="acl", acl_config="./dump.json") + ``` + + 该场景**PrecisionDebugger**模块的dump_path参数不生效,由acl_config中的dump.json文件配置溢出数据目录。 + + 仅支持NPU环境。 + +### start函数 + +**功能说明** + +dump或溢出检测启动函数。 + +在模型初始化之后的任意位置添加。 + +**原型** + +```python +debugger.start() +``` + +该函数为类函数,可以使用debugger.start()也可以使用PrecisionDebugger.start()。 + +### stop函数 + +**功能说明** + +dump或溢出检测停止函数。 + +在**start**函数之后的任意位置添加。 + +**原型** + +```python +debugger.stop() +``` + +该函数为类函数,可以使用debugger.stopt()也可以使用PrecisionDebugger.stop()。 + +### 示例代码 + +- 示例1:开启dump + + ```python + from ptdbg_ascend import * + debugger = PrecisionDebugger(dump_path="./dump_path", hook_name="dump") + + # 模型初始化 + # 下面代码也可以用PrecisionDebugger.start()和PrecisionDebugger.stop() + debugger.start() + + ... + + debugger.stop() + ``` + +- 示例2:开启溢出检测dump + + ```python + from ptdbg_ascend import * + debugger = PrecisionDebugger(dump_path="./dump_path", hook_name="overflow_check") + + # 模型初始化 + # 下面代码也可以用PrecisionDebugger.start()和PrecisionDebugger.stop() + debugger.start() + + ... + + debugger.stop() + ``` + +## CPU或GPU及NPU精度数据dump + +### 总体说明 + +- 本节主要介绍CPU或GPU及NPU精度数据dump所需要的函数以及示例。 + +- ptdbg_ascend工具默认情况下仅dump PyTorch模型的API输入输出数据进行精度比对,若在比对结果中发现某个API下可能存在ACL的精度问题,那么可以选择dump该API的ACL级别数据进行精度分析。 + +- 某些torch api的输出不是Tensor类型的数据。对于此类API的反向过程进行ACL dump,工具会在运行日志中给出对应的Warning(is not of tensor type and cannot be automatically derived)提示。如若想要进行该类API反向ACL dump,可以通过手动构建单API用例的方式进行ACL dump,具体用例可参见“**[反向ACL dump用例说明](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/%E5%8F%8D%E5%90%91ACL%20dump%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E.md)**”。 + +- 工具性能:dump数据量较小时(小于5G),参考dump速度0.1GB/s;dump数据量较大时,参考dump速度0.2GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。Dump速度的计算方式:Dump数据量/(单个step添加Dump耗时-原始单个step耗时)。 + +### 约束 +- 进行CPU或GPU数据dump时,请安装torch包而非torch_npu包,避免工具无法识别使用场景,导致失败。 + +- TASK_QUEUE_ENABLE环境变量会导致API下发和执行异步进行,因此在ACL dump前需要将TASK_QUEUE_ENABLE关闭,即export TASK_QUEUE_ENABLE=0。 + +- 不建议在PyTorch训练脚本中同时添加dump接口和性能数据采集(如Ascend PyThon Profiler)接口,二者可能相互影响导致数据不准确。 + +### seed_all + +**功能说明** + +固定随机数。通过固定随机数保证模型的输入或输出一致。在训练主函数开始前调用,避免随机数固定不全。 + +dump操作必选。 + +**函数原型** + +```python +seed_all(seed=1234, mode=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------ | ------------------------------------------------------------ | -------- | +| seed | 随机数种子。参数示例:seed=1000。默认值为:1234。 | 否 | +| mode | 确定性计算模式。可配置True或False。参数示例:mode=True。默认为False。
即使在相同的硬件和输入下,API多次执行的结果也可能不同,开启确定性计算是为了保证在相同的硬件和输入下,API多次执行的结果相同。
确定性计算会导致API执行性能降低,建议在发现模型多次执行结果不同的情况下开启。
rnn类算子、ReduceSum、ReduceMean等算子可能与确定性计算存在冲突,若开启确定性计算后多次执行的结果不相同,则考虑存在这些算子。 | 否 | + +**函数示例** + +seed_all函数的随机数种子,取默认值即可,无须配置;第二个参数默认关闭,不开启确定性计算时也无须配置。 + +- 示例1:仅固定随机数,不开启确定性计算 + + ```python + seed_all() + ``` + +- 示例2:固定随机数,开启确定性计算 + + ```python + seed_all(mode=True) + ``` + +**固定随机数范围** + +seed_all函数可固定随机数的范围如下表。 + +| API | 固定随机数 | +| ---------------------------------------- | --------------------------- | +| os.environ['PYTHONHASHSEED'] = str(seed) | 禁止Python中的hash随机化 | +| random.seed(seed) | 设置random随机生成器的种子 | +| np.random.seed(seed) | 设置numpy中随机生成器的种子 | +| torch.manual_seed(seed) | 设置当前CPU的随机种子 | +| torch.cuda.manual_seed(seed) | 设置当前GPU的随机种子 | +| torch.cuda.manual_seed_all(seed) | 设置所有GPU的随机种子 | +| torch_npu.npu.manual_seed(seed) | 设置当前NPU的随机种子 | +| torch_npu.npu.manual_seed_all(seed) | 设置所有NPU的随机种子 | +| torch.backends.cudnn.enable=False | 关闭cuDNN | +| torch.backends.cudnn.benchmark=False | cuDNN确定性地选择算法 | +| torch.backends.cudnn.deterministic=True | cuDNN仅使用确定性的卷积算法 | + +需要保证CPU或GPU以及NPU的模型输入完全一致,dump数据的比对才有意义,seed_all并不能保证模型输入完全一致,如下表所示场景需要保证输入的一致性。 + +| 场景 | 固定方法 | +| --------------- | ------------- | +| 数据集的shuffle | 关闭shuffle。 | +| dropout | 关闭dropout。 | + +关闭shuffle示例: + +```python +train_loader = torch.utils.data.DataLoader( + train_dataset, + batch_size = batch_size, + shuffle = False, + num_workers = num_workers +) +``` + +关闭dropout: + +在使用from ptdbg import *后,工具会自动将torch.nn.functional.dropout、torch.nn.functional.dropout2d、torch.nn.functional.dropout3d、torch.nn.Dropout、torch.nn.Dropout2d、torch.nn.Dropout3d的接口参数p置为0。 + +### set_dump_path + +**功能说明** + +设置dump数据目录。建议在seed_all函数之后调用且需要保证训练进程能够调用该函数;多卡时须保证每个进程都能调用该函数。 + +dump操作必选。 + +**函数原型** + +```python +set_dump_path(fpath=None, dump_tag='ptdbg_dump') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------- | ------------------------------------------------------------ | -------- | +| fpath | 设置dump数据目录路径。参数示例:'./dump_path'。dump_path须为已存在目录。
默认在指定的dump_path路径下生成`ptdbg_dump_{version}`目录,并在该目录下生成`dump.pkl`文件以及`dump`数据文件保存目录。
当set_dump_switch函数配置了mode参数时,`dump.pkl`文件以及`dump`数据文件保存目录名称添加mode参数值为前缀,详情请参见“**dump数据存盘说明**”。 | 是 | +| dump_tag | 设置dump数据目录名称。参数示例:dump_tag='dump_conv2d'。默认dump数据目录命名为ptdbg_dump_{version}。
{version}为当前安装ptdbg_ascend工具版本。目录结构参见“**dump数据存盘说明**”。
配置该参数会将生成的`ptdbg_dump_{version}`目录名称变更为dump_tag配置的值,如`dump_conv2d_{version}`。 | 否 | + +**函数示例** + +- 示例1:设置dump数据目录路径 + + ```python + set_dump_path('./dump_path') + ``` + +- 示例2:设置dump数据目录名称 + + ```python + set_dump_path('./dump_path', dump_tag='dump_conv2d') + ``` + + +若以相同的dump数据目录多次dump,则会因同名导致覆盖;多次dump建议配置不同的dump_tag。 + +### register_hook + +**功能说明** + +注册工具钩子函数。在set_dump_path之后调用。 + +dump操作必选。 + +**函数原型** + +```python +register_hook(model, hook, overflow_nums=overflow_nums, dump_mode=dump_mode, dump_config=dump_config_file, rank=0) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| model | model对象。 | 是 | +| hook | 注册工具的dump和溢出检测钩子。可取值overflow_check和acc_cmp_dump,二选一。 | 是 | +| overflow_nums | 控制溢出次数,表示第N次溢出时,停止训练,过程中检测到溢出API对应ACL数据均dump。参数示例:overflow_nums=3。配置overflow_check时可配置,默认不配置,即检测到1次溢出,训练停止。 | 否 | +| dump_mode | 控制针对溢出API的dump模式。可取值"api"或"acl",配置acl时表示dump ACL级别的溢出数据,此时set_dump_path参数不生效,dump数据目录由dump_config的.json文件配置,参数示例:dump_mode="acl"。默认不配置,即dump API级别的溢出数据。 | 否 | +| dump_config | acl dump的配置文件。dump_mode="acl"时,该参数必选;dump_mode="api"时,该参数不选。参数示例:dump_config='./dump.json'。 | 否 | +| rank | 控制dump数据保存的rank目录名称。参数示例:rank=1。默认不配置,即自动读取dump数据所属的卡并保存在该卡对应的rank目录下。目录结构参见“**dump数据存盘说明**”。
多卡情况下,可能出现工具识别rank出错,导致dump数据保存到错误的rank目录下,此时需要根据“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”配置该参数,以获取正确的rank_id;工具可正确识别rank_id时无须配置该参数。 | 否 | + +**函数示例** + +- 示例1:注册工具钩子函数 + + ```python + register_hook(model, acc_cmp_dump) + ``` + +- 示例2:dump指定API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + ``` + + 需要配置set_dump_switch的mode="acl"以及scope指定为前向或反向API,请参见“**set_dump_switch”**的示例。 + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置dump数据目录。 + +- 示例3:溢出检测dump + + ```python + register_hook(model, overflow_check, overflow_nums=3) + ``` + + dump执行时会在set_dump_path的fpath参数指定的目录下生成ptdbg_dump_{version}目录,保存溢出数据。 + + 多卡场景时,需要检测到至少有一张卡溢出次数达到overflow_nums时,训练结束。 + + 仅支持NPU环境。 + +- 示例4:dump指定API的ACL级别溢出数据 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + + 仅支持NPU环境。 + +### set_dump_switch + +**功能说明** + +设置dump范围。建议在register_hook函数之后的脚本内任意位置插入,但进行精度问题排查建议参照“场景化示例 > 单卡场景精度比对”章节的顺序,先从第一个迭代开始的位置调用并dump整网数据。 + +dump操作必选。 + +**函数原型** + +```python +def set_dump_switch(switch, mode="all", scope=[], api_list=[], filter_switch="ON", dump_mode=["all"]): +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| --------------- | ------------------------------------------------------------ | -------- | +| switch | dump开关。可取值"ON"或"OFF"。须在选定dump开始的位置配置set_dump_switch("ON");dump结束的位置设置set_dump_switch("OFF")。 | 是 | +| mode | dump模式。可取值"all"、"list"、"range"、"stack"、"acl"、"api_list"、"api_stack",各参数含义请参见本节的“**函数示例**”。参数示例:mode="list"。默认为all。该参数配置值将作为dump数据文件名的前缀,详情请参见“**dump数据存盘说明**”。 | 否 | +| scope或api_list | dump范围。根据model配置的模式选择dump的API范围。参数示例:scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward"]、api_list=["relu"]。默认为空。 | 否 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | +| dump_mode | dump数据过滤。可取值"all"、"forward"、"backward"、"input"和"output",表示仅保存dump的数据中文件名包含"forward"、"backward"、"input"和"output"的前向、反向、输入或输出的.npy文件。参数示例dump_mode=["backward"]或dump_mode=["forward", "backward"]。默认为all,即保存所有dump的数据。除了all参数只能单独配置外,其他参数可以自由组合。 | 否 | + +**推荐配置** + +```python +set_dump_switch("ON", mode="api_stack", filter_switch="OFF") +``` + +开启dump数据和堆栈模式,同时为保证数据完整性开启dump bool和整型的tensor以及浮点、bool和整型的标量。 + +**函数示例** + +set_dump_switch可配置多种dump模式,示例如下: + +说明:以下均以dump部分API数据为例,API名可以从首次dump整网数据的结果csv文件中的NPU Name或Bench Name列获取。 + +- 示例1:dump指定API列表 + + ```python + set_dump_switch("ON", mode="list", scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]) + ``` + +- 示例2:dump指定范围 + + ```python + set_dump_switch("ON", mode="range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例3:STACK模式,只dump堆栈信息 + + ```python + set_dump_switch("ON", mode="stack", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]) + ``` + +- 示例4:dump指定前向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Tensor_permute_1_forward"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件。 + +- 示例4:dump指定反向API的ACL级别数据 + + ```python + register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') + set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) + set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) + ``` + + 需要配置register_hook的dump_mode='acl'和dump_config配置文件,并通过set_backward_input设置反向API输入的.npy文件。 + +- 示例5:dump指定某一类API的API级别输入输出数据 + + ```python + set_dump_switch("ON", mode="api_list", api_list=["relu"]) + ``` + + mode="api_list"时不配置scope。 + +- 示例6:dump全部API级别输入输出数据以及相应堆栈信息 + + ```python + set_dump_switch("ON", mode="api_stack") + ``` + + mode="api_stack"时不配置scope。 + +- 示例7: dump全部API级别输入输出数据并包含bool和整型的tensor以及浮点、bool和整型的标量,默认不配置为ON,会过滤bool和整型数据 + + ```python + set_dump_switch("ON", filter_switch="OFF") + ``` + + 配置filter_switch="OFF"同时也可以配置mode、scope和api_list,除dump ACL级别数据。 + +- 示例8:仅保存dump的数据文件名包含“backward”的反向.npy文件 + + ```python + set_dump_switch("ON", dump_mode=["backward"]) + ``` + +以上示例均需要在结束dump的位置插入set_dump_switch("OFF")。 + +set_dump_switch配置mode为all或api_stack时,结束dump后,在dump目录下会自动生成compare_data.py比对脚本模板,示例如下: + +```python +from ptdbg_ascend import compare + +pkl_path = "%s" +dump_data_dir = "%s" + +dump_path_param = { + "npu_pkl_path": , + "bench_pkl_path": , + "npu_dump_data_dir": , + "bench_dump_data_dir": , + "is_print_compare_log": True +} + +compare(dump_path_param, output_path="", stack_mode="%s") +``` + +pkl_path和dump_data_dir字段会自动识别pkl和dump目录的路径,用户需要判断当前dump的环境是NPU、CPU或GPU,并将pkl_path和dump_data_dir字段填入下方dump_path_param函数对应的字段中,例如当前设备为NPU,那么填写方式如下: + +```python +from ptdbg_ascend import compare + +pkl_path = "%s" +dump_data_dir = "%s" + +dump_path_param = { + "npu_pkl_path": pkl_path, + "bench_pkl_path": , + "npu_dump_data_dir": dump_data_dir, + "bench_dump_data_dir": , + "is_print_compare_log": True +} + +compare(dump_path_param, output_path="", stack_mode="%s") +``` + +此时,另一侧数据的路径,需要用户另外识别并填入。 + +### set_overflow_check_switch + +**功能说明** + +置溢出检测范围。默认不配置该函数,全量进行溢出检测。 + +仅支持NPU环境。 + +**函数原型** + +```python +set_overflow_check_switch(switch, filter_switch='ON') +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------- | ------------------------------------------------------------ | -------- | +| switch, | 检测开关。可取值"ON"或"OFF"。如果只在特定的step溢出检测,则在期望溢出检测的step位置开始前插入set_overflow_check_switch("ON"),在step结束的位置插入set_overflow_check_switch("OFF")。 | 是 | +| filter_switch | 开启dump bool和整型的tensor以及浮点、bool和整型的标量。可取值"ON"或"OFF"。参数示例:filter_switch="OFF"。默认不配置,即filter_switch="ON",表示不dump上述数据。 | 否 | + +**函数示例** + +- 示例1:指定范围溢出检测 + + ```python + register_hook(model, overflow_check) + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,dump执行时会在当前目录自动生成ptdbg_dump_{version}目录,保存溢出数据。 + +- 示例2:前向API的ACL级别范围溢出检测 + + ```python + register_hook(model, overflow_check, dump_mode='acl', dump_config='./dump.json') + set_overflow_check_switch("ON") + + ... + + set_overflow_check_switch("OFF") + ``` + + 该场景set_dump_path不生效,由dump_config中的dump.json文件配置溢出数据目录。 + +### set_backward_input + +**功能说明** + +设置反向ACL级别dump时需要的反向输入的.npy文件。 + +**函数原型** + +```python +set_backward_input(backward_input) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| backward_input | 该输入文件为首次运行训练dump得到反向API输入的.npy文件。例如若需要dump Functional_conv2d_1 API的反向过程的输入输出,则需要在dump目录下查找命名包含Functional_conv2d_1、backward和input字段的.npy文件。 | 是 | + +**函数示例** + +```python +register_hook(model, acc_cmp_dump, dump_mode='acl', dump_config='./dump.json') +set_dump_switch("ON", mode="acl", scope=["Functional_conv2d_1_backward"]) +set_backward_input(["./npu_dump/dump_conv2d_v2.0/rank0/dump/Functional_conv2d_1_backward_input.0.npy"]) +``` + +### dump.json配置文件说明 + +**dump.json配置示例** + +```python +{ + "dump": + { + "dump_list":[], + "dump_path":"./dump/output", + "dump_mode":"all", + "dump_op_switch":"on" + } +} +``` + +**dump.json参数说明** + +| 字段名 | 说明 | +| -------------- | ------------------------------------------------------------ | +| dump_list | 待dump数据的API模型。为空,无需配置。 | +| dump_path | dump数据文件存储到运行环境的目录,主要用于指定ACL dump数据路径。支持配置绝对路径或相对路径。dump_path须为已存在目录。 | +| dump_mode | dump数据模式,配置如下:
- output:dump API的输出数据。默认值。
- input:dump API的输入数据。
- all:dump API的输入、输出数据。 | +| dump_op_switch | 单API模型dump数据开关,配置如下: * off:关闭单API模型dump,默认值。 * on:开启单API模型dump。 | + +**dump目录说明** + +配置register_hook的dump_config后,采集的dump数据会在{dump_path}/{time}/{deviceid}/{model_id}目录下生成,例如“/home/HwHiAiUser/output/20200808163566/0/0” + +```bash +├── 20230131172437 +│   └── 1 +│   ├── 0 +│   │   ├── Add.Add.45.0.1675157077183551 +│   │   ├── Cast.trans_Cast_0.31.0.1675157077159449 +│   │   ├── Cast.trans_Cast_5.43.0.1675157077180129 +│   │   ├── MatMul.MatMul.39.0.1675157077172961 +│   │   ├── Mul.Mul.29.0.1675157077155731 +│   │   ├── NPUAllocFloatStatus.NPUAllocFloatStatus.24.0.1675157077145262 +│   │   ├── TransData.trans_TransData_1.33.0.1675157077162791 +│   │   └── TransData.trans_TransData_4.41.0.1675157077176648 +│   ├── 1701737061 +│   │   └── Cast.trans_Cast_2.35.0.1675157077166214 +│   ├── 25 +│   │   └── NPUClearFloatStatus.NPUClearFloatStatus.26.0.1675157077150342 +│   └── 68 +│   └── TransData.trans_TransData_3.37.0.1675157077169473 +``` + +### dump数据存盘说明 + +dump结果目录结构示例如下: + +```bash +├── dump_path +│ └── ptdbg_dump_{version} +│ ├── rank0 +│ │ ├── dump +| | | ├── Tensor_permute_1_forward.npy +| | | ... +| | | └── Fcuntion_linear_5_backward_output.npy +│ │ └── dump.pkl +│ ├── rank1 +| | ├── dump +| | | └── ... +| | └── dump.pkl +│ ├── ... +│ | +| └── rank7 +``` + +其中ptdbg_dump_{version}为未设置set_dump_path的dump_tag参数时的默认命名;rank为设备上各卡的ID,每张卡上dump的数据会生成对应dump目录,可由register_hook函数的rank参数控制rank目录名称。 + +**精度比对dump场景** + +精度比对dump场景的结果如下: + +* dump.pkl文件:包含dump数据的API名称、dtype、 shape以及各数据的max、min、mean统计信息。 + +* dump目录:目录下为npy格式的dump数据。 + + npy文件保存的前缀和PyTorch对应关系如下 + + | 前缀 | Torch模块 | + | ---------- | ------------------- | + | Tensor | torch.Tensor | + | Torch | torch | + | Functional | torch.nn.functional | + | NPU | NPU亲和算子 | + | VF | torch._VF | + +当set_dump_switch或configure_hook配置mode参数(例如:mode="api_stack" )时,dump结果的文件名会添加api_stack前缀,dump结果如下: + +* api_stack_dump.pkl +* api_stack_dump目录 + +**溢出检测dump场景** + +register_hook设置了overflow_check时,检测API溢出,dump结果的文件名固定为Overflow_info_{timestamp},dump结果如下: + +* Overflow_info_{timestamp}.pkl +* Overflow_info_{timestamp}目录 + +## CPU或GPU与NPU精度数据比对 + +### 总体说明 + +- 本节主要介绍CPU或GPU与NPU精度数据比对的函数以及示例。 + +- 比对函数均通过单独创建精度比对脚本执行,可支持单卡和多卡场景的精度数据比对。 + +- 工具性能:比对数据量较小时(参考值单份文件小于10GB),参考比对速度0.1GB/s;比对数据量较大时,参考比对速度0.3GB/s。 + 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 + + 用户环境性能弱于标准约束或非独占使用的比对速度酌情向下浮动。比对速度的计算方式:两份比对文件大小/比对耗时。 + +### 约束 + +- NPU自研API,在CPU或GPU若没有对应的API,该API的dump数据不比对。 + +- NPU与CPU或GPU的计算结果误差可能会随着模型的执行不断累积,最终会出现同一个API因为输入的数据差异较大而无法比对的情况。 + +- CPU或GPU与NPU中两个相同的API会因为调用次数不同导致无法比对或比对到错误的API,不影响整体运行,该API忽略。 + +### compare_distributed + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,支持单卡和多卡,可同时比对多卡的dump数据。多机场景需要每个设备单独执行比对操作。可自动检索和匹配对应卡和进程所dump的数据文件,再调用compare进行比对。单机单卡时与compare函数二选一。 + +**函数原型** + +```python +compare_distributed(npu_dump_dir, bench_dump_dir, output_path, **kwargs) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| -------------- | ------------------------------------------------------------ | -------- | +| npu_dump_dir | 配置NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./npu_dump/dump_conv2d_v2.0'。 | 是 | +| bench_dump_dir | 配置CPU、GPU或NPU环境下的dump目录,即set_dump_path函数的dump_tag参数对应的目录名称。参数示例:'./gpu_dump/dump_conv2d_v2.0'。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。需要预先创建output_path目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_rank{npu_ID}-rank{cpu/gpu/npu_ID}_{timestamp}.csv`。 | 是 | +| **kwargs | 支持compare的所有可选参数。 | 否 | + +**函数示例** + +创建比对脚本,例如compare_distributed.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +compare_distributed('./npu_dump/ptdbg_dump_v2.0', './gpu_dump/ptdbg_dump_v2.0', './output') +``` + +### compare + +**功能说明** + +将CPU或GPU与NPU的dump文件进行比对,仅支持单机单卡。 + +**函数原型** + +```python +compare(input_param, output_path, stack_mode=False, auto_analyze=True, suffix='', fuzzy_match=False) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------ | ------------------------------------------------------------ | -------- | +| input_param | 配置dump数据文件及目录。配置参数包括:
- "npu_pkl_path":指定NPU dump目录下的.pkl文件。参数示例:"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "bench_pkl_path":指定CPU、GPU或NPU dump目录下的.pkl文件。参数示例:"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl"。必选。
- "npu_dump_data_dir":"指定NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "bench_dump_data_dir":"指定CPU、GPU或NPU dump目录下的dump数据目录。参数示例:"npu_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump"。必选。
- "is_print_compare_log":配置是否开启日志打屏。可取值True或False。可选。 | 是 | +| output_path | 配置比对结果csv文件存盘目录。参数示例:'./output'。文件名称基于时间戳自动生成,格式为:`compare_result_{timestamp}.csv`。 | 是 | +| stack_mode | 配置stack_mode的开关。仅当dump数据时配置set_dump_switch的mode="api_stack"时需要开启。参数示例:stack_mode=True,默认为False。 | 否 | +| auto_analyze | 自动精度分析,开启后工具自动针对比对结果进行分析,识别到第一个精度不达标节点(在比对结果文件中的“Accuracy Reached or Not”列显示为No),并给出问题可能产生的原因(打屏展示并生成advisor_{timestamp}.txt文件)。可取值True或False,参数示例:auto_analyze=False,默认为True。 | 否 | +| suffix | 标识比对结果的文件名。配置的suffix值在比对结果文件名的compare_result和{timestamp}中间插入,例如:`compare_result_{suffix}_{timestamp}`。默认为空。 | 否 | +| fuzzy_match | 模糊匹配。开启后,对于网络中同一层级且命名仅调用次数不同的API,可匹配并进行比对。可取值True或False,参数示例:fuzzy_match=True,默认为False。 | 否 | + +**函数示例** + +单机单卡场景下创建比对脚本,例如compare.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +dump_result_param={ +"npu_pkl_path": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"bench_pkl_path": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump.pkl", +"npu_dump_data_dir": "./npu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"bench_dump_data_dir": "./gpu_dump/ptdbg_dump_v2.0/rank0/api_stack_dump", +"is_print_compare_log": True +} +compare(dump_result_param, "./output", stack_mode=True) +``` + +### parse + +**功能说明** + +解析并提取dump信息中的堆栈信息及数据统计信息。 + +**函数原型** + +```python +parse(pkl_file, moudule_name_prefix) +``` + +**参数说明** + +| 参数名 | 说明 | 是否必选 | +| ------------------- | ------------------------------------------------------------ | -------- | +| pkl_file | 指定dump数据文件中的pkl文件名。参数示例:"./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl"。 | 是 | +| moudule_name_prefix | 指定待提取的API接口前缀。参数示例:"Torch_norm_1_forward"。 | 是 | + +**函数示例** + +创建堆栈信息及数据统计信息提取脚本,例如parse.py,拷贝如下代码,具体参数请根据实际环境修改。 + +```python +from ptdbg_ascend import * +parse("./npu_dump/ptdbg_dump_v2.0/rank0/dump.pkl", "Torch_batch_normal_1_forward") +``` + +### 计算精度评价指标 + +PyTorch精度比对是以CPU或GPU的计算结果为标杆,计算Cosine(余弦相似度)、MaxAbsErr(最大绝对误差)和MaxRelativeErr(最大相对误差),根据这两个结果判断API在运行时是否存在精度问题。 + +计算精度评价指标: + +1. Cosine:通过计算两个向量的余弦值来判断其相似度,数值越接近于1说明计算出的两个张量越相似,实际可接受阈值为大于0.99。在计算中可能会存在nan,主要由于可能会出现其中一个向量为0。 + +2. MaxAbsErr:当最大绝对误差越接近0表示其计算的误差越小,实际可接受阈值为小于0.001。 + +3. MaxRelativeErr:当最大相对误差越接近0表示其计算的误差越小。 + + 当dump数据中存在0或Nan时,比对结果中最大相对误差则出现inf或Nan的情况,属于正常现象。 + +精度比对结果csv文件中只需要通过Accuracy Reached or Not来判断计算精度是否达标,判断标准如下: + +1. Cosine < 0.99 且 MaxAbsError > 0.001时,精度不达标,标记为“No”。 +2. Cosine < 0.9,精度不达标,标记为“No”。 +3. MaxAbsError > 1,精度不达标,标记为“No”。 +4. 其余情况下记为精度达标,标记为“Yes”。 + +## ptdbg_ascend.parse数据解析功能 + +ptdbg_ascend.parse为命令行交互式界面解析工具,提供更多的数据解析功能并且展示结果。 + +主要的使用场景包括: + +- 支持指定ACL层级算子数据比对。 +- 支持指定ACL层级算子数据转换及展示。 +- 支持交互式指定pkl文件中API对应dump数据查看。 +- 支持API进行可选层级比对和打印(统计级和像素级)。 + +安装ptdbg_ascend工具后,可以通过使用命令 **python -m ptdbg_ascend.parse** 进入交互式界面,可在parse的界面中执行Shell命令,以及上述场景的相关解析命令。Ctrl+C可以退出该界面。 + +### ACL层级算子数据比对 + +- 依赖:CANN包中的msaccucmp工具。 + +- 输入以下比对命令进行数据比对。 + + ```bash + vc -m [*my_dump_path*] -g [*golden_dump_path*] (-out) [*output_path*] + ``` + + | 参数名称 | 说明 | 是否必选 | + | -------- | ------------------------------------------------------------ | -------- | + | -m | 待比对dump数据目录。 | 是 | + | -g | dump数据目录。 | 是 | + | -out | 结果输出目录。 | 否 | + | -asc | 指定msaccucmp路径,默认路径为:/usr/local/Ascend/ascend-toolkit/latest/tools/operator_cmp/compare/msaccucmp.py。 | 否 | + + - 输出结果:result_{timestamp}.csv文件。 + - 若指定-out参数需要用户传入输出路径,并且路径需要已存在。 + - 若未指定输出目录或指定目录不存在, 则比对结束后将结果保存在默认目录 “./parse_data/comapre_result”中,比对结束后会打印log提示输出结果存放路径。 + +**示例** + +```bash +# 传入待比对数据目录以及标杆数据目录 +Parse >>> vc -m ./my_dump_path -g ./golden_data_path +...... +# 比对结果打印 +[INFO] The comparison result have been written to "./parse_data/compare_result/result_20230818104735.csv". +[INFO] The command was completed and took 6 seconds. +[INFO] Compare finished!! +``` + +### ACL算子数据的npy转换 + +- 依赖:CANN包中的msaccucmp工具。 + +- 输入以下转换命令进行数据转换, 将ACL级别dump数据转为npy文件。 + + ```bash + dc -n [*file_name/file_path*] (-out) [*output_path*] + ``` + + | 参数名称 | 说明 | 是否必选 | + | -------- | ------------------------------------------------------------ | -------- | + | -n | 需转换的dump数据文件或dump数据文件目录。 | 是 | + | -out | 结果输出目录。 | 否 | + | -asc | 指定msaccucmp路径,默认路径为:/usr/local/Ascend/ascend-toolkit/latest/tools/operator_cmp/compare/msaccucmp.py | 否 | + + [^]: 若传入单个dump文件,则转换单个文件,若传入dump文件目录则转换目录下所有dump文件。 + + - 输出结果:npy文件。 + - 若指定-out参数需要用户传入输出路径,并且路径需要已存在。 + - 若未指定输出目录或指定目录不存在, 则比对结束后将结果保存在默认目录 “./parse_data/convert_result”中,比对结束后会打印log提示输出结果存放路径及转换结果。 + +- 输入以下命令,展示npy数据统计信息。 + + ```bash + pt -n [*file_path*] + ``` + + | 参数名称 | 说明 | 是否必选 | + | -------- | ------------- | -------- | + | -n | npy文件路径。 | 是 | + + 打印统计信息:shape, dtype, max, min和mean。 + +**示例1** + +```bash +# 传入需转换的dump文件目录 +Parse >>> dc -n ./dump_data/ +...... +# 转换结果 +╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ SrcFile: ./dump_data/ +│ - Add.fp32_vars_add_2fp32_vars_Relu_9.31.5.1636595794731103.input.0.npy │ +│ - Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.output.0.npy │ +│ - Add.fp32_vars_add_2fp32_vars_Relu_9.31.5.1636595794731103.input.1.npy │ +│ - Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.input.1.npy │ +│ - Add.fp32_vars_add_3fp32_vars_Relu_12.40.5.1636595794846124.input.1.npy │ +│ - Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.input.0.npy │ +│ - Add.fp32_vars_add_3fp32_vars_Relu_12.40.5.1636595794846124.input.0.npy │ +│ - Add.fp32_vars_add_2fp32_vars_Relu_9.31.5.1636595794731103.output.0.npy │ +│ - Add.fp32_vars_add_3fp32_vars_Relu_12.40.5.1636595794846124.output.0.npy │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ +``` + +**示例2** + +```bash +# 查看某个dump数据块的数据信息 +# 默认会将数据中的tensor保存成 txt +Parse >>> pt -n ./parse_data/dump_convert/Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.output.0.npy +...... +# 打印统计信息 +[Shape: (1, 16, 56, 56, 16)] [Dtype: float16] [Max: 452.0] [Min: -408.5] [Mean: -3.809] +Path: ./parse_data/dump_convert/Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.input.0.npy +TextFile:./parse_data/dump_convert/Add.fp32_vars_add_1fp32_vars_Relu_6.24.5.1636595794631347.input.0.npy.txt +``` + +### pkl文件中指定API的dump数据信息查看 + +- 输入以下命令,解析并输出pkl文件中指定api的统计信息。 + + ```bash + pk -f [*pkl_path*] -n [*api_name*] + ``` + + | 参数名称 | 说明 | 是否必选 | + | -------- | ----------------- | -------- | + | -f | 指定pkl文件路径。 | 是 | + | -n | 指定API名称。 | 是 | + + - 输出结果:打印统计信息(shape, dtype, max和min mean)。 + - 若pkl文件中存在相应的堆栈信息,则会打印堆栈信息。 + +**示例** + +```bash +# 传入pkl文件及api名称 +Parse >>> pk -f ./torch_dump/ptdbg_v3.2/rank0/api_stack_dump.pkl -n Functional_conv2d_0_forward +...... +# 打印统计信息及堆栈(pkl文件不包含堆栈则不会打印堆栈) + +Statistic Info: + [Functional_conv2d_0_forward_input.0][dtype: torch.float32][shape: [2, 1, 2, 2]][max: 1.576936960220337][min: -0.9757485389709473][mean: 0.4961632490158081] + [Functional_conv2d_0_forward_input.1][dtype: torch.float32][shape: [2, 1, 2, 2]][max: 0.20064473152160645][min: -0.47102075815200806][mean: -0.20796933770179749] + [Functional_conv2d_0_forward_input.2][dtype: torch.float32][shape: [2]][max: 0.17380613088607788][min: -0.16853803396224976][mean: 0.0026340484619140625] + [Functional_conv2d_0_forward_output][dtype: torch.float32][shape: [2, 2, 1, 1]][max: 0.02364911139011383][min: -1.762906551361084][mean: -0.6710853576660156] +``` + +### API可选层级比对 + +- 输入以下命令, 进行统计级和像素级比对。 + + ```bash + cn -m [*my_data *.npy*] -g [*gloden *.npy*] (-p) [*num*] (-al) [*atol*] (-rl) [*rtol*] + ``` + + - 统计级比对:对tensor整体进行余弦值及相对误差的计算。 + - 像素级比对:对输入的两个npy文件进行逐元素比对。若两个tensor对应元素的相对误差或绝对误差大于**误差阈值**(-al和-rl配置)则被标记为错误数据。 + + | 参数名称 | 说明 | 是否必选 | + | -------- | ----------------------------------------------- | -------- | + | -m | 待比对数据。 | 是 | + | -g | 标杆数据。 | 是 | + | -p | 设置比对结束后打印错误元素的个数,默认值20。 | 否 | + | -al | 判定数据存在精度问题的绝对误差阈值,默认0.001。 | 否 | + | -rl | 判定数据存在精度问题的相对误差阈值,默认0.001。 | 否 | + | -s | 将npy文件保存成txt文件,用于查看,默认开启。 | 否 | + + 输出结果: + + - 统计级比对结果。 + - 两个文件的统计信息(shape, dtype, max, min和mean)。 + - 错误数据打印表格。 + +**示例** + +```bash +# 对比两个tensor的数据 +Parse >>> cn -m Add.InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.323.1619494134703053.output.0.npy -g InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.0.1619492699305998.npy -p 10 -s -al 0.002 -rl 0.005 + Error Item Table Top Item Table +┏━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓ ┏━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ +┃ Index ┃ Left ┃ Right ┃ Diff ┃ ┃ Index ┃ Left ┃ Right ┃ Diff ┃ +┡━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩ ┡━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ +│ 155 │ 0.024600908 │ 0.022271132 │ 0.002329776 │ │ 0 │ -0.9206961 │ -0.9222216 │ 0.0015255213 │ +│ 247 │ 0.015752593 │ 0.017937578 │ 0.0021849852 │ │ 1 │ -0.6416973 │ -0.64051837 │ 0.0011789203 │ +│ 282 │ -0.0101207765 │ -0.007852031 │ 0.0022687456 │ │ 2 │ -0.35383835 │ -0.35433492 │ 0.0004965663 │ +│ 292 │ 0.019581757 │ 0.02240482 │ 0.0028230622 │ │ 3 │ -0.18851271 │ -0.18883198 │ 0.00031927228 │ +│ 640 │ -0.06593232 │ -0.06874806 │ 0.0028157383 │ │ 4 │ -0.43508735 │ -0.43534422 │ 0.00025686622 │ +│ 1420 │ 0.09293677 │ 0.09586689 │ 0.0029301196 │ │ 5 │ 1.4447614 │ 1.4466647 │ 0.0019032955 │ +│ 1462 │ -0.085207745 │ -0.088047795 │ 0.0028400496 │ │ 6 │ -0.3455438 │ -0.3444429 │ 0.0011008978 │ +│ 1891 │ -0.03433288 │ -0.036525503 │ 0.002192624 │ │ 7 │ -0.6560242 │ -0.6564579 │ 0.0004336834 │ +│ 2033 │ 0.06828873 │ 0.07139922 │ 0.0031104907 │ │ 8 │ -2.6964858 │ -2.6975214 │ 0.0010356903 │ +│ 2246 │ -0.06376442 │ -0.06121233 │ 0.002552092 │ │ 9 │ -0.73746175 │ -0.73650354 │ 0.00095820427 │ +└───────┴───────────────┴──────────────┴──────────────┘ └───────┴─────────────┴─────────────┴───────────────┘ +╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Left: | +│ |- NpyFile: ./dump/temp/decode/Add.InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.323.1619494134703053.output.0.npy | +│ |- TxtFile: ./dump/temp/decode/Add.InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.323.1619494134703053.output.0.npy.txt | +│ |- NpySpec: [Shape: (32, 8, 8, 320)] [Dtype: float32] [Max: 5.846897] [Min: -8.368301] [Mean: -0.72565556] | +│ DstFile: │ +│ |- NpyFile: ./dump/cpu/InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.0.1619492699305998.npy | +│ |- TxtFile: ./dump/cpu/InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.0.1619492699305998.npy.txt | +│ |- NpySpec: [Shape: (32, 8, 8, 320)] [Dtype: float32] [Max: 5.8425903] [Min: -8.374472] [Mean: -0.7256237] │ +│ NumCnt: 655360 │ +│ AllClose: False │ +│ CosSim: 0.99999493 │ +│ ErrorPer: 0.023504638671875 (rl= 0.005, al= 0.002) │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +``` + +## FAQ + +[FAQ](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/FAQ.md) diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/rank_id\350\216\267\345\217\226\346\226\271\346\263\225.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/rank_id\350\216\267\345\217\226\346\226\271\346\263\225.md" new file mode 100644 index 0000000000..f084d99030 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/rank_id\350\216\267\345\217\226\346\226\271\346\263\225.md" @@ -0,0 +1,38 @@ +# rank_id获取方法 + +## **通过环境变量获取** + +当前进程的rank_id可能保存在环境变量中,比如`LOCAL_RANK`。则可以通过如下示例来检查当前进程的rank_id: + +```python +import os +print("Local rank is: ", os.environ.get('LOCAL_RANK')) +``` + +若打印结果显示该环境变量被配置过,如: + +```python +# 以单机8卡为例 +Local rank is: 0 +Local rank is: 2 +Local rank is: 3 +Local rank is: 1 +Local rank is: 4 +Local rank is: 5 +Local rank is: 6 +Local rank is: 7 +``` + +那么将该环境变量作为rank传参即可自动获取到rank_id,如: + +```python +register_hook(model, acc_cmp_dump, rank=os.environ.get('LOCAL_RANK') +``` + +## **通过命令行参数获取** + +若通过命令行参数传入rank_id,比如`--local_rank`。那么可以在代码中找到`args.local_rank` 来作为rank参数值。如: + +```bash +register_hook(model, acc_cmp_dump, rank=args.local_rank) +``` diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/\345\217\215\345\220\221ACL dump\347\224\250\344\276\213\350\257\264\346\230\216.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/\345\217\215\345\220\221ACL dump\347\224\250\344\276\213\350\257\264\346\230\216.md" new file mode 100644 index 0000000000..bf8989e8a4 --- /dev/null +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/\345\217\215\345\220\221ACL dump\347\224\250\344\276\213\350\257\264\346\230\216.md" @@ -0,0 +1,42 @@ +### 反向ACL dump用例说明 + +当前昇腾AI处理器上的PyTorch框架通过torch_npu.npu中的init_dump(),set_dump()和finalize_dump()接口来进行ACL级别的数据采集。首先init_dump()会进行初始化dump配置,然后通过set_dump()接口传入配置文件来配置dump参数,最后通过finalize_dump来结束dump。 下面将以torch.sort运算的反向过程为例,介绍反向ACL数据dump的方法。 + +```bash +import numpy as np +import torch +import torch_npu +torch_npu.npu.set_device("npu:0") +input = torch.tensor(np.load(input_path)).requires_grad_().npu() +grad = torch.tensor(np.load(grad_path)).requires_grad_().npu() +b, c = torch.sort(input) +torch_npu.npu.init_dump() +torch_npu.npu.set_dump("dump.json") +torch_npu.npu.synchronize() +b.backward(grad) +torch_npu.npu.synchronize() +torch_npu.npu.finalize_dump() +``` + +- input_path是该API前向运算的输入,可以通过ACL dump的API名称获得。如想要对Torch_sort_0_backward进行ACL dump,则该反向API对应的前向过程输入为Torch_sort_0_forward_input.0.npy。 +- grad_path是该API反向运算的输入,同理可以通过期望dump的API名称获得。 + +- b, c是torch.sort的输出,分别表示排序后的tensor和排序后tensor中的各元素在原始tensor中的位置。对torch.sort进行反向时,需要对b进行backward。 + +**dump.json配置** + +```bash +{ + "dump": + { + "dump_list": [], + "dump_path": "/home/HwHiAiUser/dump/output", + "dump_mode": "all", + "dump_op_switch": "on" + } +} +``` + +**查看dump数据** + +采集的dump数据会在{dump_path}/{time}/{device_id}/{model_id}/{data_index}目录下生成。 diff --git a/debug/accuracy_tools/ptdbg_ascend/figures/advisor_summary.png b/debug/accuracy_tools/ptdbg_ascend/figures/advisor_summary.png new file mode 100644 index 0000000000000000000000000000000000000000..317584f559dd8557bb29773428b33c4962bfd8a3 GIT binary patch literal 13908 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 literal 0 HcmV?d00001 diff --git a/debug/accuracy_tools/ptdbg_ascend/figures/auto_analyze_log.png b/debug/accuracy_tools/ptdbg_ascend/figures/auto_analyze_log.png new file mode 100644 index 0000000000000000000000000000000000000000..999b47f97ef5661316c7e61dbdc93c87996259f3 GIT binary patch literal 65144 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 literal 0 HcmV?d00001 diff --git a/debug/accuracy_tools/ptdbg_ascend/figures/compare_struct.png b/debug/accuracy_tools/ptdbg_ascend/figures/compare_struct.png new file mode 100644 index 0000000000000000000000000000000000000000..4faf1dd73f48082e8b2e5876f92bac451ddd9558 GIT binary patch literal 110216 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 literal 0 HcmV?d00001 diff --git a/debug/accuracy_tools/ptdbg_ascend/figures/module_compare.png b/debug/accuracy_tools/ptdbg_ascend/figures/module_compare.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1ea564eb191807034afd8aceac92b29b62a086 GIT binary patch literal 56655 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 z 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 z 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 2 else None, + Const.ACL: lambda: ValueError("set_dump_switch, scope param set invalid, only one api name is supported in acl mode.") if len(scope) != 1 else None, + Const.API_LIST: lambda: ValueError("Current dump mode is 'api_list', but the content of api_list parameter is empty or valid.") if not isinstance(api_list, list) or len(api_list) < 1 else None, + Const.API_STACK: lambda: None, + } + if mode not in Const.DUMP_MODE: + msg = "Current mode '%s' is not supported. Please use the field in %s" % \ + (mode, Const.DUMP_MODE) + raise CompareException(CompareException.INVALID_DUMP_MODE, msg) + + if mode_check[mode]() is not None: + raise mode_check[mode]() + +def check_switch_valid(switch): + if switch not in ["ON", "OFF"]: + raise ValueError("Please set switch with 'ON' or 'OFF'.") + +def check_dump_mode_valid(dump_mode): + if not isinstance(dump_mode, list): + print_warn_log("Please set dump_mode as a list.") + dump_mode = [dump_mode] + if not all(mode in ["all", "forward", "backward", "input", "output"] for mode in dump_mode): + raise ValueError("Please set dump_mode as a list containing one or more of the following: 'all', 'forward', 'backward', 'input', 'output'.") + if 'input' not in dump_mode and 'output' not in dump_mode: + dump_mode.extend(['input', 'output']) + if 'forward' not in dump_mode and 'backward' not in dump_mode: + dump_mode.extend(['forward', 'backward']) + if 'all' in dump_mode or set(["forward", "backward", "input", "output"]).issubset(set(dump_mode)): + return ['all'] + return dump_mode + +def check_summary_only_valid(summary_only): + if not isinstance(summary_only, bool): + print_error_log("Params auto_analyze only support True or False.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + return summary_only + +def check_compare_param(input_parma, output_path, stack_mode=False, auto_analyze=True, + fuzzy_match=False): # 添加默认值来让不传参时能通过参数检查 + if not (isinstance(input_parma, dict) and isinstance(output_path, str) + and isinstance(stack_mode, bool) and isinstance(fuzzy_match, bool)): + print_error_log("Invalid input parameters") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + if not isinstance(auto_analyze, bool): + print_error_log("Params auto_analyze only support True or False.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + check_file_or_directory_path(input_parma.get("npu_pkl_path"), False) + check_file_or_directory_path(input_parma.get("bench_pkl_path"), False) + check_file_or_directory_path(input_parma.get("npu_dump_data_dir"), True) + check_file_or_directory_path(input_parma.get("bench_dump_data_dir"), True) + check_file_or_directory_path(output_path, True) + npu_pkl = open(input_parma.get("npu_pkl_path"), "r") + bench_pkl = open(input_parma.get("bench_pkl_path"), "r") + check_file_mode(npu_pkl.name, bench_pkl.name, stack_mode) + _check_pkl(npu_pkl, input_parma.get("npu_pkl_path")) + _check_pkl(bench_pkl, input_parma.get("bench_pkl_path")) + return npu_pkl, bench_pkl + + +def check_file_or_directory_path(path, isdir=False): + """ + Function Description: + check whether the path is valid + Parameter: + path: the path to check + isdir: the path is dir or file + Exception Description: + when invalid data throw exception + """ + if isdir: + if not os.path.exists(path): + print_error_log('The path {} is not exist.'.format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + if not os.path.isdir(path): + print_error_log('The path {} is not a directory.'.format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + if not os.access(path, os.W_OK): + print_error_log( + 'The path {} does not have permission to write. Please check the path permission'.format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + else: + if not os.path.isfile(path): + print_error_log('{} is an invalid file or non-exist.'.format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + check_file_valid(path) + + if not os.access(path, os.R_OK): + print_error_log( + 'The path {} does not have permission to read. Please check the path permission'.format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + +def _check_pkl(pkl_file_handle, file_name): + tensor_line = pkl_file_handle.readline() + if len(tensor_line) == 0: + print_error_log("dump file {} have empty line!".format(file_name)) + raise CompareException(CompareException.INVALID_DUMP_FILE) + pkl_file_handle.seek(0, 0) + + +def is_starts_with(string, prefixes): + return any(string.startswith(prefix) for prefix in prefixes) + + +def check_file_mode(npu_pkl, bench_pkl, stack_mode): + npu_pkl_name = os.path.split(npu_pkl)[-1] + bench_pkl_name = os.path.split(bench_pkl)[-1] + + if not is_starts_with(npu_pkl_name, prefixes) and not is_starts_with(bench_pkl_name, prefixes): + if stack_mode: + print_error_log("The current file does not contain stack information, please turn off the stack_mode") + raise CompareException(CompareException.INVALID_COMPARE_MODE) + elif is_starts_with(npu_pkl_name, prefixes) and is_starts_with(bench_pkl_name, prefixes): + if not stack_mode: + print_error_log("The current file contains stack information, please turn on the stack_mode") + raise CompareException(CompareException.INVALID_COMPARE_MODE) + else: + print_error_log("The dump mode of the two files is not same, please check the dump files") + raise CompareException(CompareException.INVALID_COMPARE_MODE) + + +def check_file_size(input_file, max_size): + try: + file_size = os.path.getsize(input_file) + except OSError as os_error: + print_error_log('Failed to open "%s". %s' % (input_file, str(os_error))) + raise CompareException(CompareException.INVALID_FILE_ERROR) + if file_size > max_size: + print_error_log('The size (%d) of %s exceeds (%d) bytes, tools not support.' + % (file_size, input_file, max_size)) + raise CompareException(CompareException.INVALID_FILE_ERROR) + + +def check_file_not_exists(file_path): + if os.path.exists(file_path) or os.path.islink(file_path): + remove_path(file_path) + + +def remove_path(path): + if not os.path.exists(path): + return + try: + if os.path.islink(path) or os.path.isfile(path): + os.remove(path) + else: + shutil.rmtree(path) + except PermissionError: + print_error_log("Failed to delete {}. Please check the permission.".format(path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + +def get_dump_data_path(dump_dir): + """ + Function Description: + traverse directories and obtain the absolute path of dump data + Parameter: + dump_dir: dump data directory + Return Value: + dump data path,file is exist or file is not exist + """ + dump_data_path = None + file_is_exist = False + + check_file_or_directory_path(dump_dir, True) + for dir_path, sub_paths, files in os.walk(dump_dir): + if len(files) != 0: + dump_data_path = dir_path + file_is_exist = True + break + dump_data_path = dir_path + return dump_data_path, file_is_exist + + +def get_api_name_from_matcher(name): + api_matcher = re.compile(Const.API_PATTERN) + match = api_matcher.match(name) + return match.group(1) if match else "" + + +def modify_dump_path(dump_path, mode): + if mode == Const.ALL: + return dump_path + file_name = os.path.split(dump_path) + mode_file_name = mode + "_" + file_name[-1] + return os.path.join(file_name[0], mode_file_name) + + +def create_directory(dir_path): + """ + Function Description: + creating a directory with specified permissions + Parameter: + dir_path: directory path + Exception Description: + when invalid data throw exception + """ + if not os.path.exists(dir_path): + try: + os.makedirs(dir_path, mode=0o700) + except OSError as ex: + print_error_log( + 'Failed to create {}.Please check the path permission or disk space .{}'.format(dir_path, str(ex))) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + +def execute_command(cmd): + """ + Function Description: + run the following command + Parameter: + cmd: command + Exception Description: + when invalid command throw exception + """ + print_info_log('Execute command:%s' % cmd) + process = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + while process.poll() is None: + line = process.stdout.readline() + line = line.strip() + if line: + print(line) + if process.returncode != 0: + print_error_log('Failed to execute command:%s' % " ".join(cmd)) + raise CompareException(CompareException.INVALID_DATA_ERROR) + + +def save_numpy_data(file_path, data): + """ + save_numpy_data + """ + if not os.path.exists(os.path.dirname(file_path)): + os.makedirs(os.path.dirname(file_path)) + np.save(file_path, data) + + +def parse_arg_value(values): + """ + parse dynamic arg value of atc cmdline + """ + value_list = [] + for item in values.split(Const.SEMICOLON): + value_list.append(parse_value_by_comma(item)) + return value_list + + +def parse_value_by_comma(value): + """ + parse value by comma, like '1,2,4,8' + """ + value_list = [] + value_str_list = value.split(Const.COMMA) + for value_str in value_str_list: + value_str = value_str.strip() + if value_str.isdigit() or value_str == '-1': + value_list.append(int(value_str)) + else: + print_error_log("please check your input shape.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + return value_list + + +def get_data_len_by_shape(shape): + data_len = 1 + for item in shape: + if item == -1: + print_error_log("please check your input shape, one dim in shape is -1.") + return -1 + data_len = data_len * item + return data_len + + +def add_time_as_suffix(name): + return '{}_{}.csv'.format(name, time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))) + + +def get_time(): + return datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S") + + +def format_value(value): + return '{:.6f}'.format(value) + + +def torch_device_guard(func): + if is_gpu or torch_without_guard_version: + return func + # Parse args/kwargs matched torch.device objects + + @torch_npu_device_guard + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + +def seed_all(seed=1234, mode=False): + random.seed(seed) + os.environ['PYTHONHASHSEED'] = str(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.use_deterministic_algorithms(mode) + if is_gpu: + torch.cuda.manual_seed_all(seed) + torch.cuda.manual_seed(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.enable = False + torch.backends.cudnn.benchmark = False + else: + torch_npu.npu.manual_seed_all(seed) + torch_npu.npu.manual_seed(seed) + + +def get_process_rank(model): + print_info_log("Rank id is not provided. Trying to get the rank id of the model.") + try: + device = next(model.parameters()).device + except StopIteration: + print_warn_log('There is no parameter in the model. Fail to get rank id.') + return 0, False + if device.type == 'cpu': + print_warn_log("Warning: the debugger is unable to get the rank id. " + "This may cause the dumpped data to be corrupted in the " + "case of distributed training. (You may ignore this if you are using only one card.) " + "Transfer the model to npu or gpu before register_hook() to avoid this warning.") + return 0, False + else: + return device.index, True + + +def parameter_adapter(func): + + @wraps(func) + def inner(self, *args, **kwargs): + if self.op_name_ == "__getitem__" and len(args) > 1 and isinstance(args[1], torch.Tensor): + input = args[0] + indices = args[1] + if indices.dtype == torch.uint8: + indices = indices.bool() + if indices.dtype == torch.bool: + if indices.shape == input.shape: + return getattr(torch._C._VariableFunctionsClass, "masked_select")(input, indices) + else: + indices = getattr(torch._C._VariableFunctionsClass, "nonzero")(indices, as_tuple=True) + return getattr(torch._C._TensorBase, "__getitem__")(input, indices) + elif indices.dtype != torch.bool: + if len(indices.shape) == 1: + return func(self, input, indices.tolist()) + elif len(indices.shape) == 2: + result = [func(self, input, index) for index in indices.tolist()] + return getattr(torch._C._VariableFunctionsClass, "stack")(result, 0) + else: + res = [input[tensor_index] for tensor_index in indices] + return getattr(torch._C._VariableFunctionsClass, "stack")(res, 0) + return func(self, *args, **kwargs) + return inner + + +def generate_compare_script(dump_path, pkl_file_path, dump_switch_mode): + template_path = os.path.join(os.path.dirname(__file__), "compare_script.template") + pkl_dir = os.path.dirname(pkl_file_path) + compare_script_path = os.path.join(pkl_dir, "compare_data.py") + is_api_stack = "True" if dump_switch_mode == Const.API_STACK else "False" + + try: + with open(template_path, 'r') as ftemp, \ + os.fdopen(os.open(compare_script_path, Const.WRITE_FLAGS, Const.WRITE_MODES), 'w+') as fout: + code_temp = ftemp.read() + fout.write(code_temp % (pkl_file_path, dump_path, is_api_stack)) + except OSError: + print_error_log(f"Failed to open file. Please check file {template_path} or path {pkl_dir}.") + + print_info_log(f"Generate compare script successfully which is {compare_script_path}.") + + +def check_is_npu(): + return not is_gpu + + +def check_file_valid(file_path): + if os.path.islink(file_path): + print_error_log('The file path {} is a soft link.'.format(file_path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + if len(os.path.realpath(file_path)) > Const.DIRECTORY_LENGTH or len(os.path.basename(file_path)) > \ + Const.FILE_NAME_LENGTH: + print_error_log('The file path length exceeds limit.') + raise CompareException(CompareException.INVALID_PATH_ERROR) + + if not re.match(Const.FILE_PATTERN, os.path.realpath(file_path)): + print_error_log('The file path {} contains special characters.'.format(file_path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + + if os.path.isfile(file_path): + file_size = os.path.getsize(file_path) + if file_path.endswith(Const.PKL_SUFFIX) and file_size > Const.ONE_GB: + print_error_log('The file {} size is greater than 1GB.'.format(file_path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) + if file_path.endswith(Const.NUMPY_SUFFIX) and file_size > Const.TEN_GB: + print_error_log('The file {} size is greater than 10GB.'.format(file_path)) + raise CompareException(CompareException.INVALID_PATH_ERROR) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/acc_compare.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/acc_compare.py new file mode 100644 index 0000000000..6683fd1808 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/acc_compare.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import json +import multiprocessing +import os.path +import stat +import sys + +import numpy as np +import pandas as pd + +from ..advisor.advisor import Advisor +from ..common.utils import check_compare_param, add_time_as_suffix, \ + print_warn_log, print_error_log, CompareException, Const,\ + CompareConst, format_value, check_file_not_exists, check_file_valid + + +def correct_data(result): + if result == CompareConst.NAN: + return result + if float(result) > 0.99999: + return '1.0' + return result + + +def cosine_similarity(n_value, b_value): + np.seterr(divide='ignore', invalid='ignore') + if len(n_value) == 1: + return "unsupported", "This tensor is scalar." + num = n_value.dot(b_value) + a_norm = np.linalg.norm(n_value) + b_norm = np.linalg.norm(b_value) + message = '' + if a_norm <= Const.FLOAT_EPSILON and b_norm <= Const.FLOAT_EPSILON: + result = '1.0' + elif a_norm <= Const.FLOAT_EPSILON: + message = 'Cannot compare by Cosine Similarity, All the data is Zero in npu dump data.' + result = CompareConst.NAN + elif b_norm <= Const.FLOAT_EPSILON: + message = 'Cannot compare by Cosine Similarity, All the data is Zero in Bench dump data.' + result = CompareConst.NAN + else: + cos = num / (a_norm * b_norm) + if np.isnan(cos): + message = 'Cannot compare by Cosine Similarity, the dump data has NaN.' + result = CompareConst.NAN + else: + result = format_value(cos) + result = correct_data(result) + return result, message + + +def get_rmse(n_value, b_value): + rmse = np.linalg.norm(n_value - b_value) / np.sqrt(len(n_value)) + if np.isnan(rmse): + rmse = CompareConst.NAN + return rmse, "" + + +def get_mape(n_value, b_value): + mape_val = np.sum(np.abs((n_value - b_value) / b_value)) / len(b_value) * 100 + mape = CompareConst.NAN if np.isnan(mape_val) else str(round(mape_val, 4)) + '%' + return mape, "" + + +def get_max_abs_err(n_value, b_value): + temp_res = n_value - b_value + max_value = np.max(np.abs(temp_res)) + return format_value(max_value), "" + + +def get_max_relative_err(n_value, b_value): + np.seterr(divide='ignore', invalid='ignore') + if b_value.dtype in CompareConst.FLOAT_TYPE: + zero_mask = (b_value == 0) + b_value[zero_mask] += np.finfo(b_value.dtype).eps + n_value[zero_mask] += np.finfo(b_value.dtype).eps + else: + n_value, b_value = n_value.astype(float), b_value.astype(float) + zero_mask = (b_value == 0) + b_value[zero_mask] += np.finfo(float).eps + n_value[zero_mask] += np.finfo(float).eps + relative_err = np.divide((n_value - b_value), b_value) + max_relative_err = np.max(np.abs(relative_err)) + if np.isnan(max_relative_err): + message = 'Cannot compare by MaxRelativeError, the data contains nan in dump data.' + return CompareConst.NAN, message + return format_value(max_relative_err), "" + + +def check_op(npu_dict, bench_dict, fuzzy_match): + a_op_name = npu_dict["op_name"] + b_op_name = bench_dict["op_name"] + struct_match = check_struct_match(npu_dict, bench_dict) + if not fuzzy_match: + return a_op_name == b_op_name and struct_match + is_match = True + try: + is_match = fuzzy_check_op(a_op_name, b_op_name) + except Exception as err: + print_warn_log("%s and %s can not fuzzy match." % (a_op_name, b_op_name)) + is_match = False + finally: + return is_match and struct_match + + +def check_struct_match(npu_dict, bench_dict): + npu_struct_in = npu_dict.get("input_struct") + bench_struct_in = bench_dict.get("input_struct") + npu_struct_out = npu_dict.get("output_struct") + bench_struct_out = bench_dict.get("output_struct") + is_match = npu_struct_in == bench_struct_in and npu_struct_out == bench_struct_out + if not is_match: + if len(npu_struct_in) == 0 or len(bench_struct_in) == 0 or len(npu_struct_in) != len(bench_struct_in): + return False + struct_in_is_match = check_type_shape_match(npu_struct_in, bench_struct_in) + struct_out_is_match = check_type_shape_match(npu_struct_out, bench_struct_out) + is_match = struct_in_is_match and struct_out_is_match + return is_match + + +def check_type_shape_match(npu_struct, bench_struct): + shape_type_match = False + for npu_type_shape, bench_type_shape in zip(npu_struct, bench_struct): + npu_type = npu_type_shape[0] + npu_shape = npu_type_shape[1] + bench_type = bench_type_shape[0] + bench_shape = bench_type_shape[1] + shape_match = npu_shape == bench_shape + type_match = npu_type == bench_type + if not type_match: + if [npu_type, bench_type] in [["torch.float16", "torch.float32"], ["torch.float32", "torch.float16"]]: + type_match = True + else: + type_match = False + shape_type_match = shape_match and type_match + if not shape_type_match: + return False + return shape_type_match + + +def fuzzy_check_op(npu_name_list, bench_name_list): + if len(npu_name_list) == 0 or len(bench_name_list) == 0 or len(npu_name_list) != len(bench_name_list): + return False + is_match = True + for npu_name, bench_name in zip(npu_name_list, bench_name_list): + is_match = fuzzy_check_name(npu_name, bench_name) + if not is_match: + break + return is_match + + +def fuzzy_check_name(npu_name, bench_name): + if "forward" in npu_name and "forward" in bench_name: + is_match = rename_api(npu_name, "forward") == rename_api(bench_name, "forward") + elif "backward" in npu_name and "backward" in bench_name: + is_match = rename_api(npu_name, "backward") == rename_api(bench_name, "backward") + else: + is_match = npu_name == bench_name + return is_match + + +def rename_api(npu_name, process): + npu_split = npu_name.split(process) + torch_func_index, in_out = npu_split[0], npu_split[1] + torch_func_split = torch_func_index.rsplit("_", 2) + torch_func = str(torch_func_split[0]) + str(in_out) + return torch_func + + +def merge_tensor(tensor_list): + op_dict = {} + op_dict["op_name"] = [] + op_dict["input_struct"] = [] + op_dict["output_struct"] = [] + op_dict["summery"] = [] + op_dict["stack_info"] = [] + + for tensor in tensor_list: + if tensor[0].find("stack_info") != -1: + op_dict["stack_info"].append(tensor[1]) + break + op_dict["op_name"].append(tensor[0]) + if tensor[0].find("input") != -1: + op_dict["input_struct"].append((tensor[3], tensor[4])) + elif tensor[0].find("output") != -1: + op_dict["output_struct"].append((tensor[3], tensor[4])) + + if tensor[1] <= Const.DUMP_RATIO_MAX: + op_dict["summery"].append(tensor[5]) + + return op_dict + + +def read_op(ops_queue, pkl_file_handle, stack_mode): + tensor_list = [] + read_err = False + read_output_flag = {"last_line": False, "curr_line": False} + end_flag = "stack_info" if stack_mode is True else "output" + + while True: + curr_pos = pkl_file_handle.tell() + tensor_line = pkl_file_handle.readline() + if len(tensor_line) == 0 and not read_output_flag.get("curr_line"): + read_err = True + break + if tensor_line == '\n': + continue + if len(tensor_line) != 0: + tensor_data = json.loads(tensor_line) + read_output_flag["last_line"] = read_output_flag.get("curr_line") + read_output_flag["curr_line"] = True if tensor_data[0].find(end_flag) != -1 else False + + if (read_output_flag.get("last_line") and not read_output_flag.get("curr_line")) \ + or (len(tensor_line) == 0 and read_output_flag.get("curr_line")): # end of file scenario + ops_queue.append(merge_tensor(tensor_list)) + # the pos of the handle needs to restore to the start of the next api. + pkl_file_handle.seek(curr_pos, 0) + break + tensor_list.append(tensor_data) + + return not read_err + + +def match_op(npu_queue, bench_queue, fuzzy_match): + for b_index, b_op in enumerate(bench_queue[0: -1]): + if check_op(npu_queue[-1], b_op, fuzzy_match): + return len(npu_queue) - 1, b_index + if check_op(npu_queue[-1], bench_queue[-1], fuzzy_match): + return len(npu_queue) - 1, len(bench_queue) - 1 + for n_index, n_op in enumerate(npu_queue[0: -1]): + if check_op(n_op, bench_queue[-1], fuzzy_match): + return n_index, len(bench_queue) - 1 + return -1, -1 + + +def get_accuracy(result, n_dict, b_dict): + index_out = 0 + npu_stack_info = n_dict.get("stack_info", None) + bench_stack_info = b_dict.get("stack_info", None) + + for index, n_name in enumerate(n_dict["op_name"]): + b_name = b_dict["op_name"][index] + if n_name.find("input") != -1: + n_struct = n_dict["input_struct"][index] + b_struct = b_dict["input_struct"][index] + else: + n_struct = n_dict["output_struct"][index_out] + b_struct = b_dict["output_struct"][index_out] + index_out += 1 + err_msg = "" + accuracy_check_res = CompareConst.ACCURACY_CHECK_YES + + result_item = [n_name, b_name, n_struct[0], b_struct[0], n_struct[1], b_struct[1], " ", " ", " "] + + summery_data = n_dict.get("summery")[index] + result_item.extend(summery_data) + + summery_data = b_dict.get("summery")[index] + result_item.extend(summery_data) + result_item.append(accuracy_check_res) + result_item.append(err_msg) + if npu_stack_info and bench_stack_info and index == 0: + result_item.extend(npu_stack_info) + + result.append(result_item) + + +def _do_multi_process(input_parma, result_path): + try: + _handle_multi_process(compare_ops, input_parma, result_path, multiprocessing.Manager().RLock()) + except FileNotFoundError as error: + print("File not Found. compare failed!") + return + except IOError as error: + print("IOEError. compare failed!") + return + + +def read_dump_path(result_path): + try: + csv_pd = pd.read_csv(result_path) + npu_dump_name_list = csv_pd.iloc[0:, 0].tolist() + bench_dump_name_list = csv_pd.iloc[0:, 1].tolist() + op_name_mapping_dict = {} + for index, _ in enumerate(npu_dump_name_list): + npu_dump_name = npu_dump_name_list[index] + bench_dump_name = bench_dump_name_list[index] + op_name_mapping_dict[npu_dump_name] = [npu_dump_name, bench_dump_name] + return op_name_mapping_dict + except FileNotFoundError as error: + print(error) + raise FileNotFoundError(error) + except IOError as error: + print(error) + raise IOError(error) + + +def _handle_multi_process(func, input_parma, result_path, lock): + process_num = int((multiprocessing.cpu_count() + 1) / 2) + op_name_mapping_dict = read_dump_path(result_path) + op_names = [] + for _ in range(process_num): + op_names.append([]) + all_op_names = list(op_name_mapping_dict.keys()) + for i, op_name in enumerate(all_op_names): + op_names[i % process_num].append(op_name) + all_tasks = [] + pool = multiprocessing.Pool(process_num) + + def err_call(args): + try: + pool.terminate() + if os.path.exists(result_path): + os.remove(result_path) + sys.exit(args) + except SystemExit as error: + print('multiprocess compare failed! season:{}'.format(args)) + + for process_idx, fusion_op_names in enumerate(op_names): + idx = [process_num, process_idx] + task = pool.apply_async(func, + args=(idx, fusion_op_names, op_name_mapping_dict, result_path, lock, input_parma), + error_callback=err_call) + all_tasks.append(task) + pool.close() + pool.join() + + +def compare_ops(idx, fusion_op_names, dump_path_dict, result_path, lock, input_parma): + cos_result = [] + max_err_result = [] + max_relative_err_result = [] + err_mess = [] + is_print_compare_log = input_parma.get("is_print_compare_log") + for i, op_name in enumerate(fusion_op_names): + if is_print_compare_log: + print("start comapre: {}".format(op_name)) + cos_sim, max_abs_err, max_relative_err, err_msg = compare_by_op(op_name, dump_path_dict, input_parma) + if is_print_compare_log: + print("[{}] Compare result: cosine {}, max_abs_err {}, max_relative_err {}, {}".format(op_name, cos_sim, max_abs_err, max_relative_err, err_msg)) + cos_result.append(cos_sim) + max_err_result.append(max_abs_err) + max_relative_err_result.append(max_relative_err) + err_mess.append(err_msg) + _save_cmp_result(idx, cos_result, max_err_result, max_relative_err_result, err_mess, result_path, lock) + + +def _save_cmp_result(idx, cos_result, max_err_result, max_relative_err_result, err_msg, result_path, lock): + lock.acquire() + try: + csv_pd = pd.read_csv(result_path, dtype=str) + process_num = idx[0] + process_idx = idx[1] + for i, _ in enumerate(cos_result): + process_index = i * process_num + process_idx + csv_pd.loc[process_index, CompareConst.COSINE] = cos_result[i] + csv_pd.loc[process_index, CompareConst.MAX_ABS_ERR] = max_err_result[i] + csv_pd.loc[process_index, CompareConst.MAX_RELATIVE_ERR] = max_relative_err_result[i] + csv_pd.loc[process_index, CompareConst.ERROR_MESSAGE] = err_msg[i] + csv_pd.loc[process_index, CompareConst.ACCURACY] = check_accuracy(cos_result[i], max_err_result[i]) + csv_pd.to_csv(result_path, index=False) + except FileNotFoundError as error: + print(error) + raise FileNotFoundError(error) + except IOError as error: + print(error) + raise IOError(error) + finally: + lock.release() + + +def check_accuracy(cos, max_abs_err): + if cos == CompareConst.SHAPE_UNMATCH: + return CompareConst.ACCURACY_CHECK_UNMATCH + if cos == CompareConst.NAN or max_abs_err == CompareConst.NAN: + return CompareConst.NAN + if cos == "N/A" or max_abs_err == "N/A": + return CompareConst.ACCURACY_CHECK_NO + try: + cos, max_abs_err = float(cos), float(max_abs_err) + except ValueError: + print_warn_log("Cosine or MaxAbsErr can not get float value.") + return CompareConst.NAN + if cos < CompareConst.COS_THRESHOLD and max_abs_err > CompareConst.MAX_ABS_ERR_THRESHOLD: + return CompareConst.ACCURACY_CHECK_NO + if cos < CompareConst.COS_MAX_THRESHOLD or max_abs_err > CompareConst.MAX_ABS_ERR_MAX_THRESHOLD: + return CompareConst.ACCURACY_CHECK_NO + return CompareConst.ACCURACY_CHECK_YES + + +def compare_by_op(op_name, op_name_mapping_dict, input_parma): + npu_bench_name_list = op_name_mapping_dict[op_name] + if npu_bench_name_list[1] == CompareConst.NAN: + return CompareConst.NAN, CompareConst.NAN, CompareConst.NAN, CompareConst.NO_BENCH + try: + n_path = os.path.join(input_parma.get("npu_dump_data_dir"), npu_bench_name_list[0] + ".npy") + b_path = os.path.join(input_parma.get("bench_dump_data_dir"), npu_bench_name_list[1] + ".npy") + check_file_valid(n_path) + check_file_valid(b_path) + n_value = np.load(n_path) + b_value = np.load(b_path) + except IOError as error: + return CompareConst.NAN, CompareConst.NAN, CompareConst.NAN, "Dump file:{} not found.".format(error.filename) + if len(n_value.shape) == 0: + if n_value.dtype == bool: + n_value = n_value.astype(float) + b_value = b_value.astype(float) + max_abs_err, _ = get_max_abs_err(n_value, b_value) + max_relative_err, _ = get_max_relative_err(n_value, b_value) + return "unsupported", max_abs_err, max_relative_err, "This is type of scalar data, can not compare." + if n_value.size == 0: + return "unsupported", 0, 0, "This is empty data, can not compare." + if n_value.shape != b_value.shape: + return CompareConst.SHAPE_UNMATCH, CompareConst.SHAPE_UNMATCH, CompareConst.SHAPE_UNMATCH, "Shape of NPU and bench Tensor do not match. Skipped." + if n_value.dtype != b_value.dtype: + print_warn_log("Dtype of NPU and bench Tensor do not match:{}".format(op_name)) + err_msg = " Dtype of NPU and bench Tensor do not match." + else: + err_msg = "" + + n_value, b_value = handle_inf_nan(n_value, b_value) + if n_value is CompareConst.NAN or b_value is CompareConst.NAN: + return "N/A", "N/A", "N/A", "The position of inf or nan in NPU and bench Tensor do not match." + + + n_value = n_value.reshape(-1).astype(float) + b_value = b_value.reshape(-1).astype(float) + err_msg = "" + cos_sim, message = cosine_similarity(n_value, b_value) + + max_abs_err, _ = get_max_abs_err(n_value, b_value) + max_relative_err, message = get_max_relative_err(n_value, b_value) + + if not err_msg: + err_msg += message + else: + err_msg = err_msg + ' ' + message + + if npu_bench_name_list[0] != npu_bench_name_list[1]: + err_msg += " Fuzzy matching data, the comparison accuracy may be affected." + return cos_sim, max_abs_err, max_relative_err, err_msg + + +def handle_inf_nan(n_value, b_value): + n_inf = np.isinf(n_value) + b_inf = np.isinf(b_value) + n_nan = np.isnan(n_value) + b_nan = np.isnan(b_value) + if np.any(n_inf) or np.any(b_inf) or np.any(n_nan) or np.any(b_nan): + if np.array_equal(n_inf, b_inf) and np.array_equal(n_nan, b_nan): + n_value[n_inf] = 0 + b_value[b_inf] = 0 + n_value[n_nan] = 0 + b_value[b_nan] = 0 + else: + return CompareConst.NAN, CompareConst.NAN + return n_value, b_value + + +def compare(input_parma, output_path, **kwargs): + if kwargs.get('suffix'): + print_error_log("Argument 'suffix' is not supported for compare.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + try: + npu_pkl, bench_pkl = check_compare_param(input_parma, output_path, **kwargs) + except CompareException as error: + print_error_log('Compare failed. Please check the arguments and do it again!') + sys.exit(error.code) + compare_core(input_parma, output_path, npu_pkl, bench_pkl, **kwargs) + + +def compare_core(input_parma, output_path, npu_pkl, bench_pkl, stack_mode=False, auto_analyze=True, + suffix='', fuzzy_match=False): + result = compare_process(npu_pkl, bench_pkl, stack_mode, fuzzy_match) + npu_pkl.close() + bench_pkl.close() + + columns = [CompareConst.NPU_NAME, CompareConst.BENCH_NAME, CompareConst.NPU_DTYPE, CompareConst.BENCH_DTYPE, + CompareConst.NPU_SHAPE, CompareConst.BENCH_SHAPE, CompareConst.COSINE, CompareConst.MAX_ABS_ERR, + CompareConst.MAX_RELATIVE_ERR] + columns.extend([CompareConst.NPU_MAX, CompareConst.NPU_MIN, CompareConst.NPU_MEAN]) + columns.extend([CompareConst.BENCH_MAX, CompareConst.BENCH_MIN, CompareConst.BENCH_MEAN]) + columns.extend([CompareConst.ACCURACY, CompareConst.ERROR_MESSAGE]) + if stack_mode: + columns.extend([CompareConst.STACK]) + result_df = pd.DataFrame(result, columns=columns) + + file_name = add_time_as_suffix("compare_result" + suffix) + file_path = os.path.join(os.path.realpath(output_path), file_name) + check_file_not_exists(file_path) + with os.fdopen(os.open(file_path, os.O_RDWR | os.O_CREAT, stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP), 'w+') as fout: + result_df.to_csv(fout, index=False) + + _do_multi_process(input_parma, file_path) + if auto_analyze: + advisor = Advisor(file_path, output_path) + advisor.analysis() + + +def parse(pkl_file, module_name_prefix): + pkl_handle = open(pkl_file, "r") + done = False + title_printed = False + while not done: + pkl_line = pkl_handle.readline() + if pkl_line == '\n': + continue + if len(pkl_line) == 0: + done = True + break + + msg = json.loads(pkl_line) + info_prefix = msg[0] + if not info_prefix.startswith(module_name_prefix): + continue + + if info_prefix.find("stack_info") != -1: + print("\nTrace back({}):".format(msg[0])) + for item in reversed(msg[1]): + print(" File \"{}\", line {}, in {}".format(item[0], item[1], item[2])) + print(" {}".format(item[3])) + continue + if len(msg) > 5: + summery_info = " [{}][dtype: {}][shape: {}][max: {}][min: {}][mean: {}]" \ + .format(msg[0], msg[3], msg[4], msg[5][0], msg[5][1], msg[5][2]) + if not title_printed: + print("\nStatistic Info:") + title_printed = True + print(summery_info) + pkl_handle.close() + + +def compare_process(npu_pkl_handle, bench_pkl_handle, stack_mode, fuzzy_match): + if fuzzy_match: + print_warn_log("This task uses fuzzy matching, which may affect the accuracy of the comparison.") + npu_ops_queue = [] + bench_ops_queue = [] + result = [] + while True: + npu_file_flag = read_op(npu_ops_queue, npu_pkl_handle, stack_mode) + bench_file_flag = read_op(bench_ops_queue, bench_pkl_handle, stack_mode) + if (not npu_file_flag and not bench_file_flag) \ + or (len(npu_ops_queue) == 0 or len(bench_ops_queue) == 0): + break + n_match_point, b_match_point = match_op(npu_ops_queue, bench_ops_queue, fuzzy_match) + if n_match_point == -1 and b_match_point == -1: + continue + n_match_data = npu_ops_queue[n_match_point] + b_match_data = bench_ops_queue[b_match_point] + un_match_data = npu_ops_queue[0: n_match_point] + for npu_data in un_match_data: + get_un_match_accuracy(result, npu_data) + get_accuracy(result, n_match_data, b_match_data) + del npu_ops_queue[0: n_match_point + 1] + del bench_ops_queue[0: b_match_point + 1] + if npu_ops_queue: + for npu_data in npu_ops_queue: + get_un_match_accuracy(result, npu_data) + return result + + +def get_un_match_accuracy(result, n_dict): + index_out = 0 + npu_stack_info = n_dict.get("stack_info", None) + bench_name, bench_type, bench_shape = CompareConst.NAN, CompareConst.NAN, CompareConst.NAN + for index, n_name in enumerate(n_dict["op_name"]): + if n_name.find("input") != -1: + n_struct = n_dict["input_struct"][index] + else: + n_struct = n_dict["output_struct"][index_out] + index_out += 1 + err_msg = CompareConst.NO_BENCH + accuracy_check_res = CompareConst.NAN + + result_item = [n_name, bench_name, n_struct[0], bench_type, n_struct[1], bench_shape, " ", " ", " "] + summery_data = n_dict.get("summery")[index] + result_item.extend(summery_data) + summery_data = [CompareConst.NAN]*3 + result_item.extend(summery_data) + result_item.append(accuracy_check_res) + result_item.append(err_msg) + if npu_stack_info and index == 0: + result_item.extend(npu_stack_info) + result.append(result_item) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/distributed_compare.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/distributed_compare.py new file mode 100644 index 0000000000..d92b0a145b --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/compare/distributed_compare.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +import os, sys +import re +from ..common.utils import print_error_log, CompareException, check_compare_param +from .acc_compare import compare_core + + +def compare_distributed(npu_dump_dir, bench_dump_dir, output_path, **kwargs): + def check_and_return_dir_contents(dump_dir, prefix): + contents = os.listdir(dump_dir) + pattern = re.compile(f'^{prefix}[0-9]+$') + for name in contents: + match = pattern.match(name) + if match is None: + msg = (f"dump_dir contains '{name}'. Expected '{prefix}'. This name is not in the format of dump output. " + f"Please check and delete irrelevant files in {dump_dir} and try again.") + print_error_log(msg) + raise CompareException(CompareException.INVALID_PATH_ERROR) + return contents + + def extract_pkl_and_data_dir(dirname): + pkl_path, dump_data_dir, pkl_name, dump_data_dirname = '', '', '', '' + for fname in os.listdir(dirname): + full_path = os.path.join(dirname, fname) + if os.path.isdir(full_path): + dump_data_dir = full_path + dump_data_dirname = fname + elif full_path.endswith('.pkl'): + pkl_path = full_path + pkl_name = fname + # Provide robustness on invalid directory inputs + if not pkl_path: + print_error_log(f'No file is found in dump dir {dirname}. ') + raise CompareException(CompareException.NO_DUMP_FILE_ERROR) + if dump_data_dir == '': + print_error_log(f'No directory is found in dump dir {dirname}. ') + raise CompareException(CompareException.NO_DUMP_FILE_ERROR) + name_body, ext = os.path.splitext(pkl_name) + pattern = re.compile(f'{name_body}$') + match = pattern.match(dump_data_dirname) + if match is None: + print_error_log('The names of pkl and directory do not match! ' + f'Please check the names and remove irrelevant files in {dirname}. ') + raise CompareException(CompareException.INVALID_FILE_ERROR) + return pkl_path, dump_data_dir + + + if kwargs.get('suffix'): + print_error_log("Argument 'suffix' is not supported for compare_distributed.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + # get the ranks and match by order + npu_ranks = sorted(check_and_return_dir_contents(npu_dump_dir, 'rank')) + bench_ranks = sorted(check_and_return_dir_contents(bench_dump_dir, 'rank')) + if len(npu_ranks) != len(bench_ranks): + print_error_log('The number of ranks in the two runs are different. ' + 'Unable to match the ranks. Please use another folder to compare ' + 'or use compare() api and manually match the ranks.') + raise CompareException(CompareException.INVALID_PATH_ERROR) + for nr, br in zip(npu_ranks, bench_ranks): + n_dir = os.path.join(npu_dump_dir, nr) + b_dir = os.path.join(bench_dump_dir, br) + npu_pkl_path, npu_dump_data_dir = extract_pkl_and_data_dir(n_dir) + bench_pkl_path, bench_dump_data_dir = extract_pkl_and_data_dir(b_dir) + dump_result_param = { + 'npu_pkl_path': npu_pkl_path, + 'bench_pkl_path': bench_pkl_path, + 'npu_dump_data_dir': npu_dump_data_dir, + 'bench_dump_data_dir': bench_dump_data_dir, + 'is_print_compare_log':True + } + try: + npu_pkl, bench_pkl = check_compare_param(dump_result_param, output_path, **kwargs) + except CompareException as error: + print_error_log('Compare failed. Please check the arguments and do it again!') + sys.exit(error.code) + compare_core(dump_result_param, output_path, npu_pkl, bench_pkl, suffix=f'_{nr}-{br}', **kwargs) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/__init__.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/debugger_config.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/debugger_config.py new file mode 100644 index 0000000000..7c92c3c124 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/debugger_config.py @@ -0,0 +1,33 @@ +import os +from ..common.utils import print_warn_log + + +class DebuggerConfig: + def __init__(self, dump_path, hook_name, rank=None, step=[0]): + self.dump_path = dump_path + self.hook_name = hook_name + self.rank = rank + self.step = step + if self.step: + self.step.sort() + self.check() + + def check(self): + dump_root = os.path.split(self.dump_path)[0] + if not os.path.exists(dump_root): + raise ValueError("dump path {} does not exist".format(dump_root)) + if self.hook_name not in ["dump", "overflow_check"]: + raise ValueError("hook_name should be in ['dump', 'overflow_check']".format(self.hook_name)) + if self.rank is not None and not isinstance(self.rank, int): + raise ValueError("rank {} should be int".format(self.rank)) + elif isinstance(self.rank, int): + print_warn_log(f"Rank argument is provided. Only rank {self.rank} data will be dumpped.") + if not isinstance(self.step, list): + raise ValueError("step {} should be list".format(self.step)) + if len(self.step) == 0: + raise ValueError("step {} should not be empty".format(self.step)) + for s in self.step: + if not isinstance(s, int): + raise ValueError("step element {} should be int".format(s)) + return True + diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/precision_debugger.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/precision_debugger.py new file mode 100644 index 0000000000..e9db590c67 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/debugger/precision_debugger.py @@ -0,0 +1,86 @@ +import os +from ..common.utils import Const, make_dump_path_if_not_exists, print_error_log, print_info_log +from ..dump.dump import DumpUtil, acc_cmp_dump, write_to_disk +from ..dump.utils import set_dump_path, set_dump_switch_print_info, generate_dump_path_str, \ + set_dump_switch_config, set_backward_input +from ..overflow_check.utils import OverFlowUtil +from ..overflow_check.overflow_check import overflow_check +from ..hook_module.register_hook import register_hook_core +from ..hook_module.hook_module import HOOKModule +from .debugger_config import DebuggerConfig + + +class PrecisionDebugger: + first_start = True + hook_func = None + + # 提供两种使用方式:逐个传参和构造config后传config,看哪种使用方式更受欢迎,之后只保留一种 + def __init__(self, dump_path=None, hook_name=None, rank=None, step=[0], config=None): + if config is None: + if dump_path is None or hook_name is None: + err_msg = "You must provide dump_path and hook_name argument to PrecisionDebugger\ + when config is not provided." + raise Exception(err_msg) + self.config = DebuggerConfig(dump_path, hook_name, rank, step) + else: + self.config = config + print_info_log("Debugger gets config, it will override preceding arguments.") + + self.configure_hook = self.get_configure_hook(config.hook_name) + self.configure_hook() + DumpUtil.target_iter = config.step + DumpUtil.target_rank = config.rank + make_dump_path_if_not_exists(config.dump_path) + set_dump_path(config.dump_path) + if config.hook_name == "overflow_check": + PrecisionDebugger.hook_func = overflow_check + else: + PrecisionDebugger.hook_func = acc_cmp_dump + + def get_configure_hook(self, hook_name): + if hook_name == "dump": + return self.configure_full_dump + elif hook_name == "overflow_check": + return self.configure_overflow_dump + else: + raise ValueError("hook name {} is not in ['dump', 'overflow_check']".format(hook_name)) + + def configure_full_dump(self, mode='api_stack', scope=[], api_list=[], filter_switch=Const.ON, + input_output_mode=[Const.ALL], acl_config=None, backward_input=[], summary_only=False): + set_dump_switch_config(mode=mode, scope=scope, api_list=api_list, + filter_switch=filter_switch, dump_mode=input_output_mode, summary_only=summary_only) + if mode == 'acl' and acl_config is None: + raise ValueError("acl_config must be configured when mode is 'acl'") + elif mode == 'acl' and acl_config is not None: + DumpUtil.dump_config = acl_config + if mode == 'acl' and 'backward' in scope and not backward_input: + raise ValueError("backward_input must be configured when mode is 'acl' and scope contains 'backward'") + elif mode == 'acl' and 'backward' in scope and backward_input: + set_backward_input(backward_input) + + def configure_overflow_dump(self, mode="api", acl_config=None, overflow_nums=1): + if mode == "acl": + DumpUtil.dump_switch_mode = mode + DumpUtil.dump_config = acl_config + if acl_config is None: + raise ValueError("acl_config must be configured when mode is 'acl'") + if isinstance(overflow_nums, int): + OverFlowUtil.overflow_nums = overflow_nums + else: + raise ValueError("overflow_nums must be int") + + @classmethod + def start(cls): + if cls.first_start: + register_hook_core(cls.hook_func) + cls.first_start = False + DumpUtil.dump_switch = "ON" + dump_path_str = generate_dump_path_str() + set_dump_switch_print_info("ON", DumpUtil.dump_switch_mode, dump_path_str) + + @classmethod + def stop(cls): + DumpUtil.dump_switch = "OFF" + dump_path_str = generate_dump_path_str() + set_dump_switch_print_info("OFF", DumpUtil.dump_switch_mode, dump_path_str) + write_to_disk() diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/dump.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/dump.py new file mode 100644 index 0000000000..2b1b5a9677 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/dump.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import inspect +import json +import os +import stat +import numpy as np +import torch +import threading + +try: + import torch_npu +except ImportError: + is_gpu = True +else: + is_gpu = False + +from .utils import DumpUtil, _set_dump_switch4api_list, make_dump_data_dir, get_tensor_rank, create_dirs_if_not_exist +from ..common.utils import print_warn_log, Const, print_info_log, modify_dump_path +from ..dump.utils import check_writable + +forward_init_status = False +backward_init_status = False + +backward_threading_id = 0 + +api_list = [] +thread_lock = threading.Lock() +pkl_name = "" +multi_output_apis = ["_sort_", "npu_flash_attention"] + +class DataInfo(object): + def __init__(self, data, save_data, summary_data, dtype, shape): + self.data = data + self.save_data = save_data + self.summary_data = summary_data + self.dtype = dtype + self.shape = shape + + +def get_not_float_tensor_info(data): + if data.numel() == 0 or data.dtype == torch.bool: + tensor_max = [] + tensor_min = [] + tensor_mean = [] + elif len(data.shape) == 0: + tensor_max = data.cpu().detach().float().numpy().tolist() + tensor_min = data.cpu().detach().float().numpy().tolist() + tensor_mean = data.cpu().detach().float().numpy().tolist() + else: + tensor_max = torch._C._VariableFunctionsClass.max(data).cpu().detach().float().numpy().tolist() + tensor_min = torch._C._VariableFunctionsClass.min(data).cpu().detach().float().numpy().tolist() + tensor_mean = torch._C._VariableFunctionsClass.mean(data.float()).cpu().detach().float().numpy().tolist() + return get_tensor_data_info(data, tensor_max, tensor_min, tensor_mean) + + +def get_scalar_data_info(data): + summary_data = [data, data, data] + return DataInfo(data, data, summary_data, str(type(data)), str([])) + + +def get_float_tensor_info(data): + tensor_max = torch._C._VariableFunctionsClass.max(data).cpu().detach().float().numpy().tolist() + tensor_min = torch._C._VariableFunctionsClass.min(data).cpu().detach().float().numpy().tolist() + tensor_mean = torch._C._VariableFunctionsClass.mean(data).cpu().detach().float().numpy().tolist() + return get_tensor_data_info(data, tensor_max, tensor_min, tensor_mean) + + +def get_tensor_data_info(data, tensor_max, tensor_min, tensor_mean): + summary_data = [] + saved_tensor = data.contiguous().cpu().detach() + if data.dtype == torch.bfloat16: + saved_numpy = saved_tensor.to(torch.float32).numpy() + else: + saved_numpy = saved_tensor.numpy() + summary_data.extend([tensor_max, tensor_min, tensor_mean]) + return DataInfo(data, saved_numpy, summary_data, str(data.dtype), tuple(data.shape)) + + +def json_dump_condition(prefix): + cur_threading_id = threading.current_thread().ident + global backward_threading_id + if not backward_threading_id and Const.BACKWARD in prefix: + backward_threading_id = cur_threading_id + return (Const.BACKWARD in prefix and backward_threading_id == cur_threading_id) or 'forward' in prefix + + +def dump_tensor(x, prefix, dump_step, dump_file_name): + global data_info + if isinstance(x, (tuple, list)) and x: + for i, item in enumerate(x): + dump_tensor(item, "{}.{}".format(prefix, i), dump_step, dump_file_name) + return + elif isinstance(x, torch.Tensor): + if x.is_meta: + print_info_log(f"Meta tensor {prefix} is skipped.") + return + if x.numel() == 0 or len(x.shape) == 0 or not x.is_floating_point(): + if DumpUtil.dump_filter_switch == Const.OFF: + data_info = get_not_float_tensor_info(x) + dump_data(dump_file_name, dump_step, prefix, data_info) + else: + return + else: + data_info = get_float_tensor_info(x) + dump_data(dump_file_name, dump_step, prefix, data_info) + + elif DumpUtil.dump_filter_switch == Const.OFF: + if isinstance(x, bool) or isinstance(x, int) or isinstance(x, float): + data_info = get_scalar_data_info(x) + dump_data(dump_file_name, dump_step, prefix, data_info) + + +def dump_data(dump_file_name, dump_step, prefix, data_info): + global api_list + thread_lock.acquire() + try: + if json_dump_condition(prefix): + output_path = os.path.join(DumpUtil.dump_data_dir, f'{prefix}.npy') + if not DumpUtil.summary_only: + np.save(output_path, data_info.save_data) + api_list.append([prefix, dump_step, [], data_info.dtype, data_info.shape, data_info.summary_data]) + except Exception as e: + print_warn_log("Dump data failed, error: {}".format(e)) + finally: + thread_lock.release() + + +def dump_stack_info(name_template, dump_file): + stack_str = [] + for (_, path, line, func, code, _) in inspect.stack()[3:]: + if code: + stack_line = [path, str(line), func, code[0].strip() if code else code] + else: + stack_line = [path, str(line), func, code] + stack_str.append(stack_line) + + prefix = name_template.format("stack_info") + if DumpUtil.dump_switch_mode in Const.DUMP_MODE: + if json_dump_condition(prefix): + if Const.ALL in DumpUtil.dump_mode: + api_list.append([prefix, stack_str]) + else: + for mode in DumpUtil.dump_mode: + if mode in prefix: + api_list.append([prefix, stack_str]) + else: + api_list.append([prefix, stack_str]) + + +def dump_api_tensor(dump_step, in_feat, name_template, out_feat, dump_file): + if Const.BACKWARD in name_template and Const.FORWARD not in DumpUtil.dump_mode: + if 'input' in DumpUtil.dump_mode: + dump_tensor(out_feat, name_template.format("input"), dump_step, dump_file) + if 'output' in DumpUtil.dump_mode: + dump_tensor(in_feat, name_template.format("output"), dump_step, dump_file) + if Const.ALL in DumpUtil.dump_mode: + dump_tensor(out_feat, name_template.format("input"), dump_step, dump_file) + dump_tensor(in_feat, name_template.format("output"), dump_step, dump_file) + elif Const.BACKWARD not in name_template and Const.BACKWARD not in DumpUtil.dump_mode: + if 'input' in DumpUtil.dump_mode: + dump_tensor(in_feat, name_template.format("input"), dump_step, dump_file) + if 'output' in DumpUtil.dump_mode: + dump_tensor(out_feat, name_template.format("output"), dump_step, dump_file) + if Const.ALL in DumpUtil.dump_mode: + dump_tensor(in_feat, name_template.format("input"), dump_step, dump_file) + dump_tensor(out_feat, name_template.format("output"), dump_step, dump_file) + + +def dump_acc_cmp(name, in_feat, out_feat, dump_step, module): + dump_file = DumpUtil.get_dump_path() + dump_file = modify_dump_path(dump_file, DumpUtil.dump_switch_mode) + _set_dump_switch4api_list(name) + if DumpUtil.get_dump_switch(): + rank = get_tensor_rank(in_feat, out_feat) + if DumpUtil.target_rank is not None: + if rank != DumpUtil.target_rank: + return + dump_file = create_dirs_if_not_exist(rank, dump_file) + global pkl_name + pkl_name = dump_file + if DumpUtil.dump_init_enable: + DumpUtil.dump_init_enable = False + DumpUtil.dump_data_dir = make_dump_data_dir(dump_file) \ + if DumpUtil.dump_switch_mode not in [Const.STACK, Const.ACL] and not DumpUtil.summary_only else "" + if os.path.exists(dump_file) and not os.path.isdir(dump_file): + check_writable(dump_file) + os.remove(dump_file) + + name_prefix = name + name_template = f"{name_prefix}" + "_{}" + if DumpUtil.dump_switch_mode in [Const.ALL, Const.API_LIST]: + dump_api_tensor(dump_step, in_feat, name_template, out_feat, dump_file) + elif DumpUtil.dump_switch_mode == Const.API_STACK: + dump_api_tensor(dump_step, in_feat, name_template, out_feat, dump_file) + dump_stack_info(name_template, dump_file) + elif DumpUtil.check_switch_scope(name_prefix): + if DumpUtil.dump_switch_mode == Const.ACL: + acl_dump(module, name, name_prefix) + elif DumpUtil.dump_switch_mode != Const.STACK: + dump_api_tensor(dump_step, in_feat, name_template, out_feat, dump_file) + dump_stack_info(name_template, dump_file) + + +def acl_dump(module, module_name, name_prefix): + if name_prefix in DumpUtil.backward_input: + dump_mode_backward_acl_dump(module, module_name, DumpUtil.backward_input.get(name_prefix)) + else: + forward_acl_dump(module, module_name) + + +def Op_Need_Trigger(module_name): + if 'Tensor___getitem___' in module_name: + return True + return False + + +def forward_acl_dump(module, module_name): + global forward_init_status + global backward_init_status + if not forward_init_status and not backward_init_status: + forward_init_status = True + torch_npu.npu.init_dump() + torch_npu.npu.set_dump(DumpUtil.dump_config) + torch_npu.npu.synchronize() + if Op_Need_Trigger(module_name): + module.forward(*module.input_args, **module.input_kwargs).cpu() + else: + module.forward(*module.input_args, **module.input_kwargs) + torch_npu.npu.synchronize() + torch_npu.npu.finalize_dump() + del module.input_args + del module.input_kwargs + forward_init_status = False + print_info_log("Dump %s op file." % module_name) + + +def acl_backward_dump_status(output, grad, module_name): + if isinstance(output, torch.Tensor): + output.backward(grad, retain_graph=True) + return True + + for api_name in multi_output_apis: + if api_name in module_name: + output[0].backward(grad, retain_graph=True) + return True + return False + + +def dump_mode_backward_acl_dump(module, module_name, grad_path): + global forward_init_status + global backward_init_status + module_name = module_name.replace(Const.FORWARD, Const.BACKWARD) + if not forward_init_status and not backward_init_status: + forward_init_status = True + module.input_args = list(module.input_args) + for i, data in enumerate(module.input_args): + if isinstance(data, torch.Tensor) and data.grad_fn: + module.input_args[i] = data.detach().requires_grad_() + output = module.forward(*module.input_args, **module.input_kwargs) + grad = torch.tensor(np.load(grad_path)).to("npu").requires_grad_() + torch_npu.npu.init_dump() + torch_npu.npu.set_dump(DumpUtil.dump_config) + torch_npu.npu.synchronize() + if not acl_backward_dump_status(output, grad, module_name): + print_warn_log("The output of {} is not of tensor type and cannot be automatically derived. " + "you can manually construct a single API backward case for ACL dump.".format(module_name)) + torch_npu.npu.synchronize() + torch_npu.npu.finalize_dump() + del module.input_args + del module.input_kwargs + forward_init_status = False + print_info_log("Dump %s op file." % module_name) + + +def acc_cmp_dump(name, **kwargs): + dump_step = kwargs.get('dump_step', 1) + pid = kwargs.get('pid') + if not pid: + return RuntimeError("Not get the specified process pid.") + + def acc_cmp_hook(module, in_feat, out_feat): + if pid == os.getpid(): + dump_acc_cmp(name, in_feat, out_feat, dump_step, module) + if hasattr(module, "input_args"): + del module.input_args + if hasattr(module, "input_kwargs"): + del module.input_kwargs + + return acc_cmp_hook + + +def write_to_disk(): + if api_list: + with open(pkl_name, 'a') as f: + try: + f.write('\n'.join(json.dumps(item) for item in api_list)) + f.write('\n') + except: + raise Exception("write to disk failed") + + +def get_pkl_file_path(): + return pkl_name diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/utils.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/utils.py new file mode 100644 index 0000000000..bc2313448e --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/dump/utils.py @@ -0,0 +1,284 @@ +import os +import shutil +import sys +import re +from pathlib import Path +import torch + +from ..dump import dump +from ..common.utils import print_error_log, CompareException, DumpException, Const, get_time, print_info_log, \ + check_mode_valid, get_api_name_from_matcher, check_switch_valid, check_dump_mode_valid, check_summary_only_valid, generate_compare_script, \ + check_is_npu, check_file_valid + +from ..common.version import __version__ + +dump_count = 0 +range_begin_flag, range_end_flag = False, False + + +class DumpUtil(object): + dump_data_dir = None + dump_path = None + dump_switch = None + dump_switch_mode = Const.ALL # all, api_stack, list, stack... + dump_switch_scope = [] + dump_init_enable = False + dump_api_list = [] + dump_filter_switch = None + dump_mode = ['all'] + backward_input = {} + dump_dir_tag = 'ptdbg_dump' + dump_config = None + dataloader_iter = 0 + target_iter = None + target_rank = None + summary_only = False + + @staticmethod + def incr_iter_num_maybe_exit(): + if DumpUtil.target_iter is None: + return + if DumpUtil.dataloader_iter == DumpUtil.target_iter: + set_dump_switch("ON") + elif DumpUtil.dataloader_iter > DumpUtil.target_iter: + raise Exception("Ptdbg: exit after iteration {}".format(DumpUtil.target_iter)) + else: + set_dump_switch("OFF") + DumpUtil.dataloader_iter += 1 + + @staticmethod + def set_dump_path(save_path): + DumpUtil.dump_path = save_path + DumpUtil.dump_init_enable = True + + @staticmethod + def set_dump_config(dump_config): + DumpUtil.dump_config = dump_config + + @staticmethod + def set_dump_switch(switch, mode=None, scope=None, api_list=None, filter_switch=None, dump_mode=None, summary_only=False): + DumpUtil.dump_switch = switch + if mode is not None: + DumpUtil.dump_switch_mode = mode + DumpUtil.dump_init_enable = True + if scope is not None: + DumpUtil.dump_switch_scope = scope + if api_list is not None: + DumpUtil.dump_api_list = [api.lower() for api in api_list] + if filter_switch is not None: + DumpUtil.dump_filter_switch = filter_switch + if dump_mode is not None: + DumpUtil.dump_mode = dump_mode if isinstance(dump_mode, list) else [dump_mode] + + if mode == Const.ACL: + DumpUtil.dump_switch_scope = [api_name.replace("backward", "forward") for api_name in scope] + DumpUtil.summary_only = summary_only + + def check_list_or_acl_mode(name_prefix): + global dump_count + for item in DumpUtil.dump_switch_scope: + if name_prefix.startswith(item): + dump_count = dump_count + 1 + return True + + def check_range_mode(name_prefix): + global range_begin_flag + global range_end_flag + if name_prefix.startswith(DumpUtil.dump_switch_scope[0]): + range_begin_flag = True + return True + if name_prefix.startswith(DumpUtil.dump_switch_scope[1]): + range_end_flag = True + return True + if range_begin_flag and not range_end_flag: + return True + return False + + def check_stack_mode(name_prefix): + if len(DumpUtil.dump_switch_scope) == 0: + return True + elif len(DumpUtil.dump_switch_scope) == 1: + return name_prefix.startswith(DumpUtil.dump_switch_scope[0]) + elif len(DumpUtil.dump_switch_scope) == 2: + return DumpUtil.check_range_mode(name_prefix) + else: + print_error_log("dump scope is invalid, Please set the scope mode in" + " set_dump_switch with 'all', 'list', 'range', 'stack', 'acl', 'api_list'!") + return False + + check_mapper = { + Const.LIST: check_list_or_acl_mode, + Const.ACL: check_list_or_acl_mode, + Const.RANGE: check_range_mode, + Const.STACK: check_stack_mode + } + + @staticmethod + def check_switch_scope(name_prefix): + if DumpUtil.dump_switch_mode in DumpUtil.check_mapper: + check_func = DumpUtil.check_mapper[DumpUtil.dump_switch_mode] + return check_func(name_prefix) + return False + + @staticmethod + def get_dump_path(): + if DumpUtil.dump_path: + return DumpUtil.dump_path + + if DumpUtil.dump_switch_mode == Const.ALL: + raise RuntimeError("get_dump_path: the file path is empty," + " you must use set_dump_path to set a valid dump path!!!") + else: + dir_path = os.path.realpath("./") + dump_file_name = "scope_dump_{}_{}_{}.pkl".format( + DumpUtil.dump_switch_mode, DumpUtil.dump_switch_scope[0], get_time()) + DumpUtil.dump_path = os.path.join(dir_path, dump_file_name) + return DumpUtil.dump_path + + @staticmethod + def get_dump_switch(): + return DumpUtil.dump_switch == "ON" + + +def set_dump_path(fpath=None, dump_tag='ptdbg_dump'): + if fpath is None: + raise RuntimeError("set_dump_path '{}' error, please set a valid filename".format(fpath)) + return + check_file_valid(fpath) + real_path = os.path.realpath(fpath) + if not os.path.isdir(real_path): + print_error_log( + "set_dump_path '{}' error, the path is not a directory please set a valid directory.".format(real_path)) + raise DumpException(DumpException.INVALID_PATH_ERROR) + DumpUtil.set_dump_path(real_path) + DumpUtil.dump_dir_tag = dump_tag + + +def get_tensor_rank(in_feat, out_feat): + def get_tensor_rank_single(x): + if isinstance(x, (list, tuple)): + if len(x) > 0: + return get_tensor_rank_single(x[0]) + return None + elif isinstance(x, torch.Tensor): + device = x.device + if device.type == 'cpu': + return None + else: + return device.index + return None + in_rank = get_tensor_rank_single(in_feat) + if in_rank is None: + out_rank = get_tensor_rank_single(out_feat) + if out_rank is None: + return 0 + return out_rank + return in_rank + + +def create_dirs_if_not_exist(rank, dump_file): + dump_path, file_name = os.path.split(dump_file) + rank_dir = os.path.join(dump_path, f"rank{rank}") + dump_file = os.path.join(rank_dir, file_name) + if not os.path.isdir(rank_dir): + Path(rank_dir).mkdir(mode=0o750, exist_ok=True) + return dump_file + + +def generate_dump_path_str(): + if DumpUtil.dump_switch_mode == 'acl': + if DumpUtil.dump_config == '': + print_error_log("Please provide dump config for register hook before turning on dump switch!") + raise DumpException(DumpException.NONE_ERROR) + dump_path = f"according to dump config {DumpUtil.dump_config}" + else: + dump_dir, dump_file = os.path.split(DumpUtil.dump_path) + if not dump_file.endswith(".pkl"): + dump_dir = DumpUtil.dump_path + dump_path = f"to {dump_dir}" + return dump_path + + +def set_dump_switch(switch, mode=Const.ALL, scope=[], api_list=[], filter_switch=Const.ON, dump_mode=[Const.ALL], summary_only=False): + try: + check_switch_valid(switch) + except (CompareException, AssertionError) as err: + print_error_log(str(err)) + sys.exit() + DumpUtil.set_dump_switch(switch, summary_only=summary_only) + dump_path_str = generate_dump_path_str() + if switch == "OFF": + dump.write_to_disk() + if check_is_npu() and DumpUtil.dump_switch_mode in [Const.ALL, Const.API_STACK, Const.LIST, Const.RANGE]: + generate_compare_script(DumpUtil.dump_data_dir, dump.get_pkl_file_path(), DumpUtil.dump_switch_mode) + set_dump_switch_print_info(switch, mode, dump_path_str) + set_dump_switch_config(mode=mode, scope=scope, api_list=api_list, filter_switch=filter_switch, dump_mode=dump_mode,summary_only=summary_only) + + +def set_dump_switch_config(mode=Const.ALL, scope=[], api_list=[], filter_switch=Const.ON, dump_mode=[Const.ALL], summary_only=False): + try: + check_mode_valid(mode, scope, api_list) + check_switch_valid(filter_switch) + dump_mode = check_dump_mode_valid(dump_mode) + summary_only = check_summary_only_valid(summary_only) + except (CompareException, AssertionError) as err: + print_error_log(str(err)) + sys.exit() + switch = DumpUtil.dump_switch + DumpUtil.set_dump_switch("OFF", mode=mode, scope=scope, api_list=api_list, filter_switch=filter_switch, + dump_mode=dump_mode, summary_only=summary_only) + DumpUtil.dump_switch = switch + + +def set_dump_switch_print_info(switch, mode, dump_path_str): + global dump_count + if switch == "ON": + print_info_log(f"Dump switch is turned on. Dump data will be saved {dump_path_str}. ") + if mode == Const.LIST: + dump_count = 0 + else: + print_info_log(f"Dump switch is turned off. ") + if mode == Const.LIST: + print_info_log("The number of matched dump is {}".format(dump_count)) + + +def _set_dump_switch4api_list(name): + if DumpUtil.dump_api_list: + api_name = get_api_name_from_matcher(name) + DumpUtil.dump_switch = "ON" if api_name in DumpUtil.dump_api_list else "OFF" + + +def set_backward_input(backward_input): + for index, api_name in enumerate(DumpUtil.dump_switch_scope): + DumpUtil.backward_input[api_name] = backward_input[index] + + +def make_dump_data_dir(dump_file_name): + dump_path, file_name = os.path.split(os.path.realpath(dump_file_name)) + name_body, name_extension = os.path.splitext(file_name) + output_dir = os.path.join(dump_path, f"{name_body}") + if not os.path.exists(output_dir): + Path(output_dir).mkdir(mode=0o750, exist_ok=True) + else: + shutil.rmtree(output_dir, ignore_errors=True) + Path(output_dir).mkdir(mode=0o750, exist_ok=True) + return output_dir + + +def make_dump_dirs(): + dump_file_name, dump_file_name_body = "dump.pkl", "dump" + dump_root_dir = DumpUtil.dump_path if DumpUtil.dump_path else "./" + tag_dir = os.path.join(dump_root_dir, DumpUtil.dump_dir_tag + f'_v{__version__}') + Path(tag_dir).mkdir(mode=0o750, parents=True, exist_ok=True) + DumpUtil.dump_dir = tag_dir + dump_file_path = os.path.join(tag_dir, dump_file_name) + DumpUtil.set_dump_path(dump_file_path) + + +def check_writable(dump_file): + if not os.access(dump_file, os.W_OK): + print_error_log( + 'The path {} does not have permission to write. Please check the path permission'.format( + dump_file)) + raise DumpException(DumpException.INVALID_PATH_ERROR) + diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/__init__.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/hook_module.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/hook_module.py new file mode 100644 index 0000000000..0de75ffe9a --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/hook_module.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + + +import functools + +import torch +import torch.nn as nn +import torch.utils.hooks as full_hooks + +module_count = {} + + +class HOOKModule(nn.Module): + + def __init__(self, hook) -> None: + super(HOOKModule, self).__init__() + self.has_overflow = False + self.input_args = tuple() + self.input_kwargs = dict() + prefix = "" + if hasattr(self, "prefix_op_name_"): + prefix = self.prefix_op_name_ + + if prefix not in module_count: + module_count[prefix] = 1 + prefix += '0_' + else: + module_count[prefix] += 1 + prefix = prefix + str(module_count[prefix] - 1) + '_' + + self.register_forward_hook(hook(prefix + "forward")) + self.register_backward_hook(hook(prefix + "backward")) + + def __call__(self, *input, **kwargs): + full_backward_hooks, non_full_backward_hooks = [], [] + if len(self._backward_hooks) > 0: + full_backward_hooks, non_full_backward_hooks = self._get_backward_hooks() + for hook in self._forward_pre_hooks.values(): + result = hook(self, input) + if result is not None: + if not isinstance(result, tuple): + result = (result,) + input = result + bw_hook = None + if len(full_backward_hooks) > 0: + bw_hook = full_hooks.BackwardHook(self, full_backward_hooks) + input = bw_hook.setup_input_hook(input) + self.input_args = input + self.input_kwargs = kwargs + if torch._C._get_tracing_state(): + result = self._slow_forward(*input, **kwargs) + else: + result = self.forward(*input, **kwargs) + for hook in self._forward_hooks.values(): + hook_result = hook(self, input, result) + if hook_result is not None: + result = hook_result + if bw_hook: + result = bw_hook.setup_output_hook(result) + if len(non_full_backward_hooks) > 0: + var = result + while not isinstance(var, torch.Tensor): + if isinstance(var, dict): + var = next((v for v in var.values() if isinstance(v, torch.Tensor))) + elif isinstance(var, (list, tuple)): + if var: + var = var[0] + else: + return result + else: + return result + grad_fn = var.grad_fn + if grad_fn is not None: + for hook in non_full_backward_hooks: + wrapper = functools.partial(hook, self) + functools.update_wrapper(wrapper, hook) + grad_fn.register_hook(wrapper) + self._maybe_warn_non_full_backward_hook(input, result, grad_fn) + return result diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/register_hook.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/register_hook.py new file mode 100644 index 0000000000..85a5b3516d --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/register_hook.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import functools +import os + +import torch + +from . import wrap_torch, wrap_functional, wrap_tensor, wrap_vf +from .hook_module import HOOKModule +from .wrap_functional import remove_dropout +from ..common.utils import check_file_or_directory_path, print_error_log, CompareException, Const, \ + print_info_log, print_warn_log, get_process_rank +from ..dump.utils import make_dump_dirs, DumpUtil +from ..overflow_check.utils import OverFlowUtil + +try: + import torch_npu +except ImportError: + is_gpu = True +else: + is_gpu = False + from . import wrap_npu_custom + +make_dir_flag = True + + +def initialize_hook(hook): + wrap_tensor.wrap_tensor_ops_and_bind(hook) + for attr_name in dir(wrap_tensor.HOOKTensor): + if attr_name.startswith("wrap_"): + setattr(torch.Tensor, attr_name[5:], getattr(wrap_tensor.HOOKTensor, attr_name)) + + wrap_torch.wrap_torch_ops_and_bind(hook) + for attr_name in dir(wrap_torch.HOOKTorchOP): + if attr_name.startswith("wrap_"): + setattr(torch, attr_name[5:], getattr(wrap_torch.HOOKTorchOP, attr_name)) + + wrap_functional.wrap_functional_ops_and_bind(hook) + for attr_name in dir(wrap_functional.HOOKFunctionalOP): + if attr_name.startswith("wrap_"): + setattr(torch.nn.functional, attr_name[5:], getattr(wrap_functional.HOOKFunctionalOP, attr_name)) + + wrap_vf.wrap_vf_ops_and_bind(hook) + for attr_name in dir(wrap_vf.HOOKVfOP): + if attr_name.startswith("wrap_"): + setattr(torch._VF, attr_name[5:], getattr(wrap_vf.HOOKVfOP, attr_name)) + + if not is_gpu: + wrap_npu_custom.wrap_npu_ops_and_bind(hook) + for attr_name in dir(wrap_npu_custom.HOOKNpuOP): + if attr_name.startswith("wrap_"): + setattr(torch_npu, attr_name[5:], getattr(wrap_npu_custom.HOOKNpuOP, attr_name)) + +def add_clear_overflow(func): + first_module = True + def clear_overflow_wrapper(*args, **kwargs): + nonlocal first_module + if first_module: + torch_npu._C._clear_overflow_npu() + first_module = False + return func(*args, **kwargs) + return clear_overflow_wrapper + + +def register_hook(model, hook, **kwargs): + print_info_log("Please disable dataloader shuffle before running the program.") + OverFlowUtil.overflow_nums = kwargs.get('overflow_nums', 1) + dump_mode, dump_config_file = init_dump_config(kwargs) + if dump_mode == 'acl': + DumpUtil.dump_switch_mode = dump_mode + DumpUtil.dump_config = dump_config_file + register_hook_core(hook, **kwargs) + + +def register_hook_core(hook, **kwargs): + global make_dir_flag + + pid = os.getpid() + need_clear = True + if make_dir_flag: + make_dump_dirs() + make_dir_flag = False + hook_name = hook.__name__ + + if "overflow_check" in hook_name and not is_gpu: + if hasattr(torch_npu._C, "_enable_overflow_npu"): + torch_npu._C._enable_overflow_npu() + print_info_log("Enable overflow function success.") + else: + print_warn_log("Api '_enable_overflow_npu' is not exist, " + "the overflow detection function on milan platform maybe not work! " + "please check the version of software torch_npu.") + # In NPU scene, clear the overflow flag before overflow detection + if need_clear: + HOOKModule.__init__ = add_clear_overflow(HOOKModule.__init__) + elif "acc_cmp_dump" in hook_name: + remove_dropout() + + print_info_log("Start mounting the {} hook function to the model.".format(hook_name)) + hook = functools.partial(hook, dump_step=0, pid=pid) + print_info_log("The {} hook function is successfully mounted to the model.".format(hook_name)) + + initialize_hook(hook) + + +def init_dump_config(kwargs): + dump_mode = kwargs.get('dump_mode', "api") + dump_config = kwargs.get('dump_config') + dump_config_file = '' + if dump_mode not in Const.SUPPORT_DUMP_MODE: + print_error_log("dump_mode only support %s" % Const.SUPPORT_DUMP_MODE) + raise CompareException(CompareException.INVALID_PARAM_ERROR) + if dump_mode == "acl": + if dump_config is None: + print_error_log("dump_mode is acl mode, dump_config must be configured.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + dump_config_file = os.path.realpath(dump_config) + check_file_or_directory_path(dump_config_file) + if not dump_config.endswith(".json"): + print_error_log("dump_config must be configure json file.") + raise CompareException(CompareException.INVALID_PARAM_ERROR) + return dump_mode, dump_config_file diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/support_wrap_ops.yaml b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/support_wrap_ops.yaml new file mode 100644 index 0000000000..cbc3dd2161 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/support_wrap_ops.yaml @@ -0,0 +1,1054 @@ +# Copyright (c) 2020 Huawei Technologies Co., Ltd +# All rights reserved. +# +# Licensed under the BSD 3-Clause License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://opensource.org/licenses/BSD-3-Clause +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# List of ops that register hooks + +functional: + - conv1d + - conv2d + - conv3d + - conv_transpose1d + - conv_transpose2d + - conv_transpose3d + - conv_tbc + - avg_pool1d + - avg_pool2d + - avg_pool3d + - fractional_max_pool2d_with_indices + - fractional_max_pool2d + - fractional_max_pool3d_with_indices + - fractional_max_pool3d + - max_pool1d_with_indices + - max_pool1d + - max_pool2d_with_indices + - max_pool2d + - max_pool3d_with_indices + - max_pool3d + - max_unpool1d + - max_unpool2d + - max_unpool3d + - lp_pool2d + - lp_pool1d + - adaptive_max_pool1d_with_indices + - adaptive_max_pool1d + - adaptive_max_pool2d_with_indices + - adaptive_max_pool2d + - adaptive_max_pool3d_with_indices + - adaptive_max_pool3d + - adaptive_avg_pool1d + - adaptive_avg_pool2d + - adaptive_avg_pool3d + - dropout + - alpha_dropout + - dropout2d + - dropout3d + - feature_alpha_dropout + - threshold + - threshold_ + - relu + - relu_ + - glu + - hardtanh + - hardtanh_ + - relu6 + - elu + - elu_ + - selu + - selu_ + - celu + - celu_ + - leaky_relu + - leaky_relu_ + - prelu + - rrelu + - rrelu_ + - logsigmoid + - gelu + - hardshrink + - tanhshrink + - softsign + - softplus + - softmin + - softmax + - gumbel_softmax + - log_softmax + - softshrink + - tanh + - sigmoid + - hardsigmoid + - linear + - bilinear + - silu + - hardswish + - embedding + - embedding_bag + - batch_norm + - instance_norm + - layer_norm + - group_norm + - local_response_norm + - ctc_loss + - nll_loss + - poisson_nll_loss + - gaussian_nll_loss + - kl_div + - cross_entropy + - binary_cross_entropy + - binary_cross_entropy_with_logits + - smooth_l1_loss + - l1_loss + - mse_loss + - margin_ranking_loss + - hinge_embedding_loss + - multilabel_margin_loss + - soft_margin_loss + - multilabel_soft_margin_loss + - cosine_embedding_loss + - multi_margin_loss + - pixel_shuffle + - pixel_unshuffle + - channel_shuffle + - upsample + - interpolate + - upsample_nearest + - upsample_bilinear + - grid_sample + - affine_grid + - pad + - pairwise_distance + - pdist + - cosine_similarity + - one_hot + - triplet_margin_loss + - triplet_margin_with_distance_loss + - normalize + - unfold + - fold + - multi_head_attention_forward + +tensor: + - __add__ + - __and__ + - __bool__ + - __div__ + - __eq__ + - __ge__ + - __gt__ + - __getitem__ + - __iadd__ + - __iand__ + - __idiv__ + - __ifloordiv__ + - __ilshift__ + - __imod__ + - __imul__ + - __ior__ + - __irshift__ + - __isub__ + - __ixor__ + - __lshift__ + - __matmul__ + - __mod__ + - __mul__ + - __nonzero__ + - __or__ + - __radd__ + - __rmul__ + - __rshift__ + - __sub__ + - __truediv__ + - __xor__ + - abs + - abs_ + - absolute + - absolute_ + - acos + - acos_ + - acosh + - acosh_ + - add + - add_ + - addbmm + - addbmm_ + - addcdiv + - addcdiv_ + - addcmul + - addcmul_ + - addmm + - addmm_ + - addmv + - addmv_ + - addr + - addr_ + - align_as + - align_to + - all + - allclose + - amax + - amin + - angle + - any + - arccos + - arccos_ + - arccosh + - arccosh_ + - arcsin + - arcsin_ + - arcsinh + - arcsinh_ + - arctan + - arctan_ + - arctanh + - arctanh_ + - argmax + - argmin + - argsort + - asin + - asin_ + - asinh + - asinh_ + - atan + - atan2 + - atan2_ + - atan_ + - atanh + - atanh_ + - baddbmm + - baddbmm_ + - bernoulli + - bernoulli_ + - bincount + - bitwise_and + - bitwise_and_ + - bitwise_not + - bitwise_not_ + - bitwise_or + - bitwise_or_ + - bitwise_xor + - bitwise_xor_ + - bmm + - broadcast_to + - cauchy_ + - ceil + - ceil_ + - cholesky + - chunk + - clamp + - cholesky_solve + - cholesky_inverse + - clamp_ + - clamp_max + - clamp_max_ + - clip + - clamp_min + - clamp_min_ + - clip_ + - copysign + - copysign_ + - cos + - cos_ + - cosh + - cosh_ + - count_nonzero + - cummax + - cummin + - cumprod + - cumprod_ + - cumsum + - cumsum_ + - deg2rad + - deg2rad_ + - det + - diag + - diag_embed + - diagflat + - diagonal + - diff + - dist + - digamma + - digamma_ + - div + - div_ + - divide + - divide_ + - dot + - eig + - eq + - eq_ + - erf + - equal + - erf_ + - erfc + - erfc_ + - erfinv + - erfinv_ + - exp + - exp2 + - exp2_ + - expm1 + - exp_ + - expm1_ + - exponential_ + - fill_ + - fix + - fill_diagonal_ + - fix_ + - flip + - fliplr + - flatten + - flipud + - float_power + - float_power_ + - floor + - floor_ + - floor_divide + - floor_divide_ + - fmax + - fmin + - fmod + - fmod_ + - frac + - frac_ + - gather + - gcd + - gcd_ + - ge + - ge_ + - geometric_ + - geqrf + - ger + - greater + - greater_ + - gt + - gt_ + - greater_equal + - greater_equal_ + - hardshrink + - heaviside + - heaviside_ + - histc + - hypot + - hypot_ + - igamma + - igamma_ + - igammac + - igammac_ + - index_add + - index_add_ + - inverse + - index_copy + - index_copy_ + - index_fill + - index_fill_ + - index_put + - index_put_ + - inner + - index_select + - isclose + - isfinite + - isinf + - isnan + - isneginf + - isposinf + - isreal + - kron + - kthvalue + - lcm + - lcm_ + - ldexp + - ldexp_ + - le + - le_ + - lerp + - lerp_ + - where + - less + - less_ + - less_equal + - less_equal_ + - lgamma + - lgamma_ + - log + - log10 + - log10_ + - log1p + - log1p_ + - log2 + - log2_ + - log_ + - log_normal_ + - log_softmax + - logcumsumexp + - logdet + - logaddexp + - logaddexp2 + - logical_and + - logical_and_ + - logical_not + - logit + - logical_not_ + - logical_or + - logical_or_ + - logical_xor + - logical_xor_ + - logit_ + - logsumexp + - lstsq + - lt + - lt_ + - lu_solve + - map2_ + - map_ + - masked_fill + - matmul + - masked_fill_ + - masked_scatter + - masked_scatter_ + - masked_select + - matrix_exp + - max + - maximum + - mean + - matrix_power + - median + - min + - minimum + - mm + - mode + - msort + - mul + - mul_ + - multinomial + - multiply + - multiply_ + - mv + - mvlgamma + - mvlgamma_ + - nansum + - narrow + - narrow_copy + - ne + - ne_ + - neg + - neg_ + - negative + - negative_ + - nonzero + - normal_ + - not_equal + - not_equal_ + - permute + - pinverse + - polygamma + - pow + - pow_ + - polygamma_ + - prelu + - prod + - put_ + - rad2deg + - rad2deg_ + - ravel + - real + - reciprocal + - reciprocal_ + - relu + - relu_ + - remainder + - repeat_interleave + - reshape + - remainder_ + - renorm + - renorm_ + - repeat + - reshape_as + - resize_ + - resize_as_ + - roll + - rot90 + - round + - round_ + - rsqrt + - rsqrt_ + - scatter + - scatter_ + - scatter_add + - scatter_add_ + - select + - sgn + - sgn_ + - sigmoid + - sigmoid_ + - sign + - sign_ + - signbit + - sin + - sin_ + - sinc + - sinc_ + - sinh + - sinh_ + - slogdet + - smm + - softmax + - solve + - sort + - split_with_sizes + - sqrt + - sqrt_ + - square + - square_ + - squeeze + - squeeze_ + - sspaddmm + - std + - sub + - sub_ + - sum + - sum_to_size + - svd + - symeig + - t + - t_ + - take + - tan + - tan_ + - tanh + - tanh_ + - tensor_split + - tile + - topk + - transpose + - transpose_ + - triangular_solve + - tril + - tril_ + - triu + - true_divide + - triu_ + - true_divide_ + - trunc + - trunc_ + - type_as + - unbind + - unflatten + - unfold + - unsafe_chunk + - unsqueeze + - unsafe_split + - unsafe_split_with_sizes + - var + - vdot + - unsqueeze_ + - view_as + - xlogy + - xlogy_ + +torch: + - _adaptive_avg_pool2d + - _add_relu + - _add_relu_ + - _aminmax + - _batch_norm_impl_index + - _convolution + - abs + - abs_ + - absolute + - acos + - acos_ + - acosh + - acosh_ + - adaptive_avg_pool1d + - adaptive_max_pool1d + - add + - addbmm + - addcdiv + - addcmul + - addmm + - addmv + - addmv_ + - addr + - amax + - affine_grid_generator + - align_tensors + - all + - alpha_dropout + - amin + - alpha_dropout_ + - angle + - any + - arange + - arccos + - arccos_ + - arccosh + - arccosh_ + - arcsin + - arcsin_ + - arcsinh + - arcsinh_ + - arctan + - arctan_ + - arctanh + - arctanh_ + - argmax + - argmin + - argsort + - asin + - asin_ + - asinh + - asinh_ + - atan + - atan2 + - atan_ + - atanh + - atanh_ + - atleast_1d + - atleast_2d + - atleast_3d + - avg_pool1d + - baddbmm + - bartlett_window + - batch_norm_backward_elemt + - batch_norm_backward_reduce + - batch_norm_elemt + - batch_norm_gather_stats + - batch_norm_gather_stats_with_counts + - bernoulli + - batch_norm_stats + - batch_norm_update_stats + - bilinear + - bincount + - binomial + - binary_cross_entropy_with_logits + - bitwise_and + - bitwise_not + - bitwise_or + - bitwise_xor + - blackman_window + - block_diag + - bmm + - broadcast_tensors + - broadcast_to + - cartesian_prod + - cat + - cdist + - ceil + - ceil_ + - celu + - celu_ + - chain_matmul + - channel_shuffle + - cholesky + - cholesky_inverse + - cholesky_solve + - choose_qparams_optimized + - chunk + - clamp + - clamp_ + - clamp_max + - clamp_max_ + - clamp_min + - clamp_min_ + - clip + - clip_ + - clone + - column_stack + - combinations + - constant_pad_nd + - conv1d + - conv2d + - conv3d + - conv_tbc + - conv_transpose1d + - conv_transpose2d + - conv_transpose3d + - cos + - convolution + - copysign + - cos_ + - cosh + - cosh_ + - cosine_embedding_loss + - cosine_similarity + - count_nonzero + - cross + - ctc_loss + - cummax + - cummin + - cumprod + - cumsum + - deg2rad + - deg2rad_ + - det + - diag + - diag_embed + - diff + - diagflat + - diagonal + - digamma + - dist + - div + - divide + - dot + - dropout + - dropout_ + - dsmm + - dstack + - eig + - einsum + - embedding + - embedding_bag + - embedding_renorm_ + - eq + - equal + - erf + - erf_ + - erfc + - erfc_ + - erfinv + - exp + - exp2 + - exp2_ + - exp_ + - expm1 + - expm1_ + - eye + - feature_dropout + - feature_alpha_dropout + - feature_alpha_dropout_ + - feature_dropout_ + - fix + - fill_ + - fix_ + - flatten + - flip + - fliplr + - flipud + - float_power + - floor + - floor_ + - floor_divide + - fmax + - fmin + - fmod + - frac + - frac_ + - full + - frobenius_norm + - full_like + - gather + - gcd + - gcd_ + - ge + - geqrf + - ger + - greater + - greater_equal + - grid_sampler + - grid_sampler_2d + - group_norm + - grid_sampler_3d + - gru + - gru_cell + - gt + - hamming_window + - hann_window + - hardshrink + - heaviside + - hinge_embedding_loss + - histc + - hsmm + - hspmm + - hstack + - hypot + - igamma + - igammac + - index_add + - index_copy + - inner + - index_fill + - index_put + - index_put_ + - index_select + - instance_norm + - isclose + - isfinite + - isinf + - isnan + - isneginf + - isposinf + - istft + - kaiser_window + - kl_div + - kron + - kthvalue + - layer_norm + - lcm + - lcm_ + - ldexp + - ldexp_ + - le + - lerp + - less + - less_equal + - lgamma + - linspace + - log + - log10 + - log10_ + - log1p + - log1p_ + - log2 + - log2_ + - log_softmax + - log_ + - logaddexp + - logaddexp2 + - logcumsumexp + - logdet + - logical_and + - logical_not + - logical_or + - logical_xor + - logit + - logit_ + - logspace + - logsumexp + - lstm + - lstm_cell + - lstsq + - lt + - lu_solve + - masked_fill + - margin_ranking_loss + - masked_scatter + - masked_select + - matrix_exp + - matmul + - matrix_power + - matrix_rank + - max + - max_pool1d + - max_pool2d + - max_pool1d_with_indices + - max_pool3d + - maximum + - mean + - median + - min + - minimum + - mm + - mode + - moveaxis + - movedim + - msort + - mul + - multinomial + - multiply + - mv + - mvlgamma + - nan_to_num + - nan_to_num_ + - nanmedian + - nansum + - narrow + - native_batch_norm + - native_group_norm + - narrow_copy + - native_layer_norm + - native_norm + - ne + - neg + - negative + - neg_ + - negative_ + - nextafter + - nonzero + - norm_except_dim + - normal + - not_equal + - nuclear_norm + - ones_like + - pairwise_distance + - pdist + - pinverse + - pixel_shuffle + - pixel_unshuffle + - poisson + - poisson_nll_loss + - polar + - polygamma + - pow + - prelu + - prod + - rad2deg + - promote_types + - rad2deg_ + - range + - ravel + - real + - reciprocal + - relu + - reciprocal_ + - relu_ + - remainder + - renorm + - repeat_interleave + - reshape + - resize_as_ + - roll + - rot90 + - round + - round_ + - rrelu + - rrelu_ + - rsqrt + - row_stack + - rsqrt_ + - rsub + - saddmm + - scalar_tensor + - scatter + - select + - scatter_add + - searchsorted + - selu + - selu_ + - sgn + - sigmoid + - sigmoid_ + - sign + - signbit + - sin + - sin_ + - sinc + - sinc_ + - sinh + - sinh_ + - slogdet + - smm + - softmax + - solve + - sort + - sparse_coo_tensor + - square + - split_with_sizes + - spmm + - sqrt + - sqrt_ + - square_ + - squeeze + - sspaddmm + - stack + - std + - std_mean + - sub + - subtract + - sum + - svd + - swapaxes + - swapdims + - symeig + - t + - take + - tan + - tan_ + - tanh + - tanh_ + - tensordot + - tensor_split + - threshold + - threshold_ + - tile + - topk + - transpose + - trapz + - triangular_solve + - tril + - tril_indices + - triplet_margin_loss + - triu + - triu_indices + - true_divide + - trunc + - trunc_ + - unique_consecutive + - xlogy + - unbind + - unique_dim + - unsafe_chunk + - unsafe_split + - vander + - var + - vdot + - unsafe_split_with_sizes + - unsqueeze + - var_mean + - vstack + - where + - xlogy_ + +_VF: + - lstm + +torch_npu: + - one_ + - npu_sort_v2 + - npu_transpose + - npu_broadcast + - npu_dtype_cast + - empty_with_format + - npu_one_hot + - npu_stride_add + - npu_ps_roi_pooling + - npu_roi_align + - npu_nms_v4 + - npu_iou + - npu_nms_with_mask + - npu_pad + - npu_bounding_box_encode + - npu_bounding_box_decode + - npu_batch_nms + - npu_slice + - _npu_dropout + - npu_indexing + - npu_ifmr + - npu_max + - npu_scatter + - npu_layer_norm_eval + - npu_alloc_float_status + - npu_get_float_status + - npu_clear_float_status + - npu_confusion_transpose + - npu_bmmV2 + - fast_gelu + - npu_sub_sample + - npu_deformable_conv2d + - npu_mish + - npu_anchor_response_flags + - npu_yolo_boxes_encode + - npu_grid_assign_positive + - npu_normalize_batch + - npu_masked_fill_range + - npu_linear + - npu_bert_apply_adam + - npu_giou + - npu_ciou + - npu_ciou_backward + - npu_diou + - npu_diou_backward + - npu_sign_bits_pack + - npu_sign_bits_unpack + - npu_flash_attention \ No newline at end of file diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_functional.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_functional.py new file mode 100644 index 0000000000..c6e119a8bf --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_functional.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os + +import torch +import yaml + +from .hook_module import HOOKModule +from ..common.utils import torch_device_guard, print_info_log + +def remove_dropout(): + if torch.__version__ > "1.8": + print_info_log("For precision comparison, the probability p in the dropout method is set to 0.") + import torch.nn.functional as F + from torch import _VF + from torch.overrides import has_torch_function_unary, handle_torch_function + + def function_dropout(input: torch.Tensor, p: float = 0.5, training: bool = True, + inplace: bool = False) -> torch.Tensor: + if has_torch_function_unary(input): + return handle_torch_function(function_dropout, (input,), input, p=0., training=training, inplace=inplace) + if p < 0.0 or p > 1.0: + raise ValueError("dropout probability has to be between 0 and 1, " "but got {}".format(p)) + return _VF.dropout_(input, 0., training) if inplace else _VF.dropout(input, 0., training) + + + def function_dropout2d(input: torch.Tensor, p: float = 0.5, training: bool = True, + inplace: bool = False) -> torch.Tensor: + if has_torch_function_unary(input): + return handle_torch_function(function_dropout2d, (input,), input, p=0., training=training, inplace=inplace) + if p < 0.0 or p > 1.0: + raise ValueError("dropout probability has to be between 0 and 1, " "but got {}".format(p)) + return _VF.feature_dropout_(input, 0., training) if inplace else _VF.feature_dropout(input, 0., training) + + + def function_dropout3d(input: torch.Tensor, p: float = 0.5, training: bool = True, + inplace: bool = False) -> torch.Tensor: + if has_torch_function_unary(input): + return handle_torch_function(function_dropout3d, (input,), input, p=0., training=training, inplace=inplace) + if p < 0.0 or p > 1.0: + raise ValueError("dropout probability has to be between 0 and 1, " "but got {}".format(p)) + return _VF.feature_dropout_(input, 0., training) if inplace else _VF.feature_dropout(input, 0., training) + + F.dropout = function_dropout + F.dropout2d = function_dropout2d + F.dropout3d = function_dropout3d + +cur_path = os.path.dirname(os.path.realpath(__file__)) +yaml_path = os.path.join(cur_path, "support_wrap_ops.yaml") +with open(yaml_path, 'r') as f: + WrapFunctionalOps = yaml.safe_load(f).get('functional') + +for f in dir(torch.nn.functional): + locals().update({f: getattr(torch.nn.functional, f)}) + + +def get_functional_ops(): + global WrapFunctionalOps + _all_functional_ops = dir(torch.nn.functional) + return set(WrapFunctionalOps) & set(_all_functional_ops) + + +class HOOKFunctionalOP(object): + pass + + +class FunctionalOPTemplate(HOOKModule): + def __init__(self, op_name, hook): + self.op_name_ = op_name + self.prefix_op_name_ = "Functional_" + str(op_name) + "_" + super().__init__(hook) + + @torch_device_guard + def forward(self, *args, **kwargs): + return eval(self.op_name_)(*args, **kwargs) + + +def wrap_functional_op(op_name, hook): + def functional_op_template(*args, **kwargs): + return FunctionalOPTemplate(op_name, hook)(*args, **kwargs) + + return functional_op_template + + +def wrap_functional_ops_and_bind(hook): + _functional_ops = get_functional_ops() + for op_name in _functional_ops: + setattr(HOOKFunctionalOP, "wrap_" + op_name, wrap_functional_op(op_name, hook)) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_npu_custom.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_npu_custom.py new file mode 100644 index 0000000000..4e127c87b7 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_npu_custom.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os +import torch +import torch_npu +import yaml + +from .hook_module import HOOKModule +from ..common.utils import torch_device_guard, torch_without_guard_version + +cur_path = os.path.dirname(os.path.realpath(__file__)) +yaml_path = os.path.join(cur_path, "support_wrap_ops.yaml") +with open(yaml_path, 'r') as f: + WrapNpuOps = yaml.safe_load(f).get('torch_npu') + + +class HOOKNpuOP(object): + pass + + +class NpuOPTemplate(HOOKModule): + + def __init__(self, op_name, hook): + self.op_name_ = op_name + self.prefix_op_name_ = "NPU_" + str(op_name) + "_" + super().__init__(hook) + + @torch_device_guard + def forward(self, *args, **kwargs): + if torch_without_guard_version: + return getattr(torch.ops.npu, str(self.op_name_))(*args, **kwargs) + else: + return getattr(torch_npu._C._VariableFunctionsClass, str(self.op_name_))(*args, **kwargs) + +def wrap_npu_op(op_name, hook): + + def npu_op_template(*args, **kwargs): + return NpuOPTemplate(op_name, hook)(*args, **kwargs) + + return npu_op_template + + +def wrap_npu_ops_and_bind(hook): + _npu_ops = WrapNpuOps + for op_name in _npu_ops: + setattr(HOOKNpuOP, "wrap_" + str(op_name), wrap_npu_op(op_name, hook)) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_tensor.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_tensor.py new file mode 100644 index 0000000000..f87b400dc9 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_tensor.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os + +import torch +import yaml + +from .hook_module import HOOKModule +from ..common.utils import torch_device_guard, parameter_adapter + +cur_path = os.path.dirname(os.path.realpath(__file__)) +yaml_path = os.path.join(cur_path, "support_wrap_ops.yaml") +with open(yaml_path, 'r') as f: + WrapTensorOps = yaml.safe_load(f).get('tensor') + + +def get_tensor_ops(): + global WrapTensorOps + _tensor_ops = dir(torch._C._TensorBase) + return set(WrapTensorOps) & set(_tensor_ops) + + +class HOOKTensor(object): + pass + + +class TensorOPTemplate(HOOKModule): + + def __init__(self, op_name, hook): + self.op_name_ = op_name + self.prefix_op_name_ = "Tensor_" + str(op_name) + "_" + super().__init__(hook) + + @torch_device_guard + @parameter_adapter + def forward(self, *args, **kwargs): + return getattr(torch._C._TensorBase, str(self.op_name_))(*args, **kwargs) + + +def wrap_tensor_op(op_name, hook): + + def tensor_op_template(*args, **kwargs): + return TensorOPTemplate(op_name, hook)(*args, **kwargs) + + return tensor_op_template + + +def wrap_tensor_ops_and_bind(hook): + _tensor_ops = get_tensor_ops() + for op_name in _tensor_ops: + setattr(HOOKTensor, "wrap_" + str(op_name), wrap_tensor_op(op_name, hook)) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_torch.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_torch.py new file mode 100644 index 0000000000..e69a89d9ef --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_torch.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os + +import torch +import yaml + +from .hook_module import HOOKModule +from ..common.utils import torch_device_guard + +cur_path = os.path.dirname(os.path.realpath(__file__)) +yaml_path = os.path.join(cur_path, "support_wrap_ops.yaml") +with open(yaml_path, 'r') as f: + WrapTorchOps = yaml.safe_load(f).get('torch') + + +def get_torch_ops(): + global WrapTorchOps + _torch_ops = dir(torch._C._VariableFunctionsClass) + return set(WrapTorchOps) & set(_torch_ops) + + +class HOOKTorchOP(object): + pass + + +class TorchOPTemplate(HOOKModule): + + def __init__(self, op_name, hook): + self.op_name_ = op_name + self.prefix_op_name_ = "Torch_" + str(op_name) + "_" + super().__init__(hook) + + def input_param_need_adapt(self): + special_op_list = ["broadcast_tensors"] + for item in special_op_list: + if item in self.op_name_: + return True + return False + + def einsum_adapt(self, *args): + if len(args) < 2: + raise ValueError('einsum(): must specify the equation string and at least one operand, ' + 'or at least one operand and its subscripts list') + equation = None + operands = None + if isinstance(args[0], torch.Tensor): + def parse_subscript(n: int) -> str: + if n == Ellipsis: + return '...' + if n >= 0 and n < 26: + return chr(ord('A') + n) + if n >= 26 and n < 52: + return chr(ord('a') + n - 26) + raise ValueError('einsum(): subscript in subscript list is not within the valid range [0, 52]') + equation = ','.join(''.join(parse_subscript(s) for s in l) for l in args[1::2]) + + if len(args) % 2 == 1: + equation += '->' + ''.join(parse_subscript(s) for s in args[-1]) + operands = args[:-1:2] + else: + operands = args[::2] + else: + equation = args[0] + operands = args[1:] + + if len(operands) == 1 and isinstance(operands[0], (list, tuple)): + _operands = operands[0] + return self.einsum_adapt(equation, *_operands) + return equation, operands + + @torch_device_guard + def forward(self, *args, **kwargs): + if self.input_param_need_adapt(): + return getattr(torch._C._VariableFunctionsClass, str(self.op_name_))(args, **kwargs) + else: + if self.op_name_ == 'einsum': + args = self.einsum_adapt(*args) + return getattr(torch._C._VariableFunctionsClass, str(self.op_name_))(*args, **kwargs) + + +def wrap_torch_op(op_name, hook): + + def torch_op_template(*args, **kwargs): + return TorchOPTemplate(op_name, hook)(*args, **kwargs) + + return torch_op_template + + +def wrap_torch_ops_and_bind(hook): + _torch_ops = get_torch_ops() + for op_name in _torch_ops: + setattr(HOOKTorchOP, "wrap_" + op_name, wrap_torch_op(op_name, hook)) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_vf.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_vf.py new file mode 100644 index 0000000000..b01f28fea7 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/hook_module/wrap_vf.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os + +import torch +import yaml + +from .hook_module import HOOKModule +from ..common.utils import torch_device_guard + +cur_path = os.path.dirname(os.path.realpath(__file__)) +yaml_path = os.path.join(cur_path, "support_wrap_ops.yaml") +with open(yaml_path, 'r') as f: + WrapVfOps = yaml.safe_load(f).get('_VF') + + +def get_vf_ops(): + global WrapVfOps + # _all_functional_ops = dir(torch.nn.functional) + # assert set(WrapFunctionalOps) <= set(_all_functional_ops) + return WrapVfOps + + +class HOOKVfOP(object): + pass + + +class VfOPTemplate(HOOKModule): + def __init__(self, op_name, hook): + self.op_name_ = op_name + self.prefix_op_name_ = "VF_" + str(op_name) + "_" + super().__init__(hook) + + @torch_device_guard + def forward(self, *args, **kwargs): + return getattr(torch._C._VariableFunctionsClass, str(self.op_name_))(*args, **kwargs) + + +def wrap_vf_op(op_name, hook): + def vf_op_template(*args, **kwargs): + return VfOPTemplate(op_name, hook)(*args, **kwargs) + + return vf_op_template + + +def wrap_vf_ops_and_bind(hook): + _vf_ops = get_vf_ops() + for op_name in _vf_ops: + setattr(HOOKVfOP, "wrap_" + op_name, wrap_vf_op(op_name, hook)) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/__init__.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/info_dump.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/info_dump.py new file mode 100644 index 0000000000..204f3de460 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/info_dump.py @@ -0,0 +1,246 @@ +import inspect +import fcntl +import json +import os +import torch +import threading + +import numpy as np + +from ..common.utils import print_error_log + + +special_torch_object = ["memory_format"] +lock = threading.Lock() + + +def write_npy(file_path, tensor): + saved_tensor = tensor.contiguous().cpu().detach() + if tensor.dtype == torch.bfloat16: + saved_numpy = saved_tensor.to(torch.float32).numpy() + else: + saved_numpy = saved_tensor.numpy() + if os.path.exists(file_path): + raise ValueError(f"File {file_path} already exists") + np.save(file_path, saved_numpy) + full_path = os.path.abspath(file_path) + return full_path + + +class APIInfo: + def __init__(self, api_name, is_forward, save_real_data=False): + self.rank = os.getpid() + self.api_name = api_name + self.save_real_data = save_real_data + self.torch_object_key = {'device': self.analyze_device_in_kwargs, 'dtype': self.analyze_dtype_in_kwargs} + self.is_forward = is_forward + self.args_num = 0 + + def analyze_element(self, element): + if isinstance(element, (list, tuple)): + out = [] + for item in element: + out.append(self.analyze_element(item)) + elif isinstance(element, dict): + out = {} + for key, value in element.items(): + if key in self.torch_object_key.keys(): + fun = self.torch_object_key[key] + out[key] = fun(value) + elif key in special_torch_object: + continue + else: + out[key] = self.analyze_element(value) + + elif isinstance(element, torch.Tensor): + out = self.analyze_tensor(element, self.save_real_data) + + elif self.is_builtin_class(element): + out = self.analyze_builtin(element) + else: + msg = f"Type {type(element)} is unsupported at analyze_element" + print_error_log(msg) + + raise NotImplementedError(msg) + return out + + def analyze_tensor(self, arg, save_real_data): + single_arg = {} + if not save_real_data: + single_arg.update({'type': 'torch.Tensor'}) + single_arg.update({'dtype': str(arg.dtype)}) + single_arg.update({'shape': arg.shape}) + single_arg.update({'Max': self.transfer_types(self.get_tensor_extremum(arg, 'max'), str(arg.dtype))}) + single_arg.update({'Min': self.transfer_types(self.get_tensor_extremum(arg, 'min'), str(arg.dtype))}) + single_arg.update({'requires_grad': arg.requires_grad}) + + else: + dump_path = "./" + api_args = self.api_name + '*' + str(self.args_num) + if self.is_forward: + forward_real_data_path = os.path.join(dump_path, 'forward_real_data') + if not os.path.exists(forward_real_data_path): + os.makedirs(forward_real_data_path, 0o755) + + file_path = os.path.join(forward_real_data_path, f'{api_args}.npy') + else: + backward_real_data_path = os.path.join(dump_path, 'backward_real_data') + if not os.path.exists(backward_real_data_path): + os.makedirs(backward_real_data_path, 0o755) + file_path = os.path.join(backward_real_data_path, f'{api_args}.npy') + self.args_num += 1 + npy_path = write_npy(file_path, arg) + single_arg.update({'type': 'torch.Tensor'}) + single_arg.update({'datapath': npy_path}) + single_arg.update({'requires_grad': arg.requires_grad}) + return single_arg + + def analyze_builtin(self, arg): + single_arg = {} + if isinstance(arg, slice): + single_arg.update({'type': "slice"}) + single_arg.update({'value': [arg.start, arg.stop, arg.step]}) + else: + single_arg.update({'type': self.get_type_name(str(type(arg)))}) + single_arg.update({'value': arg}) + return single_arg + + def transfer_types(self, data, dtype): + if 'int' in dtype or 'bool' in dtype: + return int(data) + else: + return float(data) + + def is_builtin_class(self, element): + if element is None or isinstance(element, (bool, int, float, str, slice)): + return True + return False + + def analyze_device_in_kwargs(self, element): + single_arg = {} + single_arg.update({'type': 'torch.device'}) + if not isinstance(element, str): + + if hasattr(element, "index"): + device_value = element.type + ":" + str(element.index) + single_arg.update({'value': device_value}) + else: + device_value = element.type + else: + single_arg.update({'value': element}) + return single_arg + + def analyze_dtype_in_kwargs(self, element): + single_arg = {} + single_arg.update({'type': 'torch.dtype'}) + single_arg.update({'value': str(element)}) + return single_arg + + def get_tensor_extremum(self, data, operator): + if data.dtype is torch.bool: + if operator == 'max': + return True in data + elif operator == 'min': + return False not in data + if operator == 'max': + return torch._C._VariableFunctionsClass.max(data).item() + else: + return torch._C._VariableFunctionsClass.min(data).item() + + def get_type_name(self, name): + + left = name.index("'") + right = name.rindex("'") + return name[left + 1: right] + + +class ForwardAPIInfo(APIInfo): + def __init__(self, name, save_real_data, args, kwargs): + super().__init__(name, is_forward=True, save_real_data=save_real_data) + self.analyze_api_input(args, kwargs) + self.analyze_api_call_stack() + + def analyze_api_input(self, args, kwargs): + args_info_list = self.analyze_element(args) + kwargs_info_dict = self.analyze_element(kwargs) + self.api_info_struct = {self.api_name: {"args": args_info_list, "kwargs": kwargs_info_dict}} + + def analyze_api_call_stack(self): + stack_str = [] + for (_, path, line, func, code, _) in inspect.stack()[3:]: + if not code: continue + stack_line = " ".join([ + "File", ", ".join([path, " ".join(["line", str(line)]), " ".join(["in", func]), + " ".join(["\n", code[0].strip()])])]) + stack_str.append(stack_line) + self.stack_info_struct = {self.api_name: stack_str} + + +class BackwardAPIInfo(APIInfo): + def __init__(self, name, grads): + super().__init__(name, is_forward=False) + self.analyze_api_input(grads) + + def analyze_api_input(self, grads): + grads_info_list = self.analyze_element(grads) + self.grad_info_struct = {self.api_name: grads_info_list} + + +def write_api_info_json(api_info): + dump_path = "./" + rank = api_info.rank + if isinstance(api_info, ForwardAPIInfo): + file_path = os.path.join(dump_path, f'forward_info_{rank}.json') + stack_file_path = os.path.join(dump_path, f'stack_info_{rank}.json') + write_json(file_path, api_info.api_info_struct) + write_json(stack_file_path, api_info.stack_info_struct, indent=4) + + elif isinstance(api_info, BackwardAPIInfo): + file_path = os.path.join(dump_path, f'backward_info_{rank}.json') + write_json(file_path, api_info.grad_info_struct) + else: + raise ValueError(f"Invalid api_info type {type(api_info)}") + + +def write_json(file_path, data, indent=None): + if not os.path.exists(file_path): + with open(file_path, 'w') as f: + f.write("{\n}") + lock.acquire() + with open(file_path, 'a+') as f: + fcntl.flock(f, fcntl.LOCK_EX) + try: + f.seek(0, os.SEEK_END) + f.seek(f.tell() - 1, os.SEEK_SET) + f.truncate() + if f.tell() > 3: + f.seek(f.tell() - 1, os.SEEK_SET) + f.truncate() + f.write(',\n') + f.write(json.dumps(data, indent=indent)[1:-1] + '\n}') + except Exception as e: + raise ValueError(f"Json save failed:{e}") + finally: + fcntl.flock(f, fcntl.LOCK_UN) + lock.release() + + +def initialize_output_json(): + dump_path = os.path.realpath("./") + files = ['forward_info.json', 'backward_info.json', 'stack_info.json'] + + forward_real_data_path = os.path.join(dump_path, 'forward_real_data') + if os.path.exists(forward_real_data_path): + raise ValueError(f"file {forward_real_data_path} already exists, please remove it first") + else: + os.mkdir(forward_real_data_path, mode=0o750) + + backward_real_data_path = os.path.join(dump_path, 'backward_real_data') + if os.path.exists(backward_real_data_path): + raise ValueError(f"file {backward_real_data_path} already exists, please remove it first") + else: + os.mkdir(backward_real_data_path, mode=0o750) + for file in files: + file_path = os.path.join(dump_path, file) + if os.path.exists(file_path): + raise ValueError(f"file {file_path} already exists, please remove it first or use a new dump path") \ No newline at end of file diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/overflow_check.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/overflow_check.py new file mode 100644 index 0000000000..60693298e2 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/overflow_check.py @@ -0,0 +1,180 @@ +import os +import glob +import torch + +from ..common.utils import print_warn_log, get_time, print_info_log +from ..dump.dump import forward_init_status, forward_acl_dump +from .utils import OverFlowUtil, dump_overflow +from ..dump.utils import DumpUtil, Const, get_tensor_rank, create_dirs_if_not_exist +from .info_dump import write_api_info_json, ForwardAPIInfo, BackwardAPIInfo +from ..dump import dump + +try: + import torch_npu +except ImportError: + is_gpu = True +else: + is_gpu = False + +backward_init_status = False +api_overflow = [] +forward_api_info = {} +backward_api_info = {} +FORWARD_REAL_DATA_PATH = os.path.join('./', 'forward_real_data') +BACKWARD_REAL_DATA_PATH = os.path.join('./', 'backward_real_data') + + +def check_overflow_environment(pid): + if not OverFlowUtil.get_overflow_check_switch(): + return False + if pid != os.getpid(): + return False + if is_gpu: + print_warn_log("Overflow detection is not supported in the GPU environment.") + return False + global backward_init_status + if backward_init_status or forward_init_status: + return False + return True + + +def check_data_overflow(x): + if isinstance(x, (tuple, list)) and x: + for i, item in enumerate(x): + if True == check_data_overflow(item): + return True + return False + else: + if isinstance(x, torch.Tensor) and x.numel() != 0 and x.dtype != torch.bool: + if len(x.shape) == 0: + tensor_max = x.cpu().detach().float().numpy().tolist() + tensor_min = tensor_max + else: + tensor_max = torch._C._VariableFunctionsClass.max(x).cpu().detach().float().numpy().tolist() + tensor_min = torch._C._VariableFunctionsClass.min(x).cpu().detach().float().numpy().tolist() + # inf + if tensor_max == float('inf') or tensor_min == float('-inf'): + return True + if x.dtype in [torch.float16, torch.float32, torch.bfloat16] and \ + (tensor_max == torch.finfo(x.dtype).max or tensor_min == torch.finfo(x.dtype).min): + return True + # nan + elif tensor_max != tensor_max or tensor_min != tensor_min: + return True + else: + return False + elif isinstance(x, bool) or isinstance(x, int) or isinstance(x, float): + if x == float('inf') or x == float('-inf') or x != x: + return True + else: + return False + else: + return False + + +def check_path(apis, path): + return any(api in path for api in apis) + + +def overflow_check(name, **kwargs): + overflow_nums = OverFlowUtil.overflow_nums + pid = kwargs.get('pid') + dump_mode = DumpUtil.dump_switch_mode + if not pid: + return RuntimeError("Not get the specified process pid.") + + def overflowcheck_hook(module, in_feat, out_feat): + if not check_overflow_environment(pid): + return + rank = get_tensor_rank(in_feat, out_feat) + if DumpUtil.target_rank is not None: + if rank != DumpUtil.target_rank: + return + dump_path = create_dirs_if_not_exist(rank, DumpUtil.dump_path) + dump_dir = os.path.split(dump_path)[0] + global api_overflow + global forward_api_info + global backward_api_info + + module_name = name + if hasattr(torch_npu._C, '_npu_is_support_inf_nan') and torch_npu._C._npu_is_support_inf_nan(): + # backward API endwith backward + if module_name.endswith(Const.BACKWARD): + check_feat = in_feat + else: + check_feat = out_feat + module.has_overflow = check_data_overflow(check_feat) + else: + module.has_overflow = torch_npu._C._check_overflow_npu() + if not module.has_overflow: + if hasattr(module, 'input_args'): + del module.input_args + if hasattr(module, 'input_kwargs'): + del module.input_kwargs + if module.has_overflow and OverFlowUtil.check_overflow_dump_times(overflow_nums): + need_replicate = overflow_type_judge(in_feat, out_feat, module_name) + if need_replicate: + if module_name.endswith(Const.FORWARD): + forward_api_info.update({name: ForwardAPIInfo(name, True, module.input_args, module.input_kwargs)}) + api_overflow.append(module_name) + else: + api_overflow.append(module_name.replace("backward", "forward")) + backward_api_info.update({name: BackwardAPIInfo(name, out_feat)}) + OverFlowUtil.inc_overflow_dump_times() + dump_file_name = os.path.join(dump_dir, + "Overflow_info_{}_{}.pkl".format(get_time(), OverFlowUtil.real_overflow_dump_times)) + dump_overflow(module_name, in_feat, out_feat, dump_file_name) + dump.pkl_name = dump_file_name + + print_warn_log("[overflow {} times]: module name :'{}' is overflow and dump file is saved in '{}'." + .format(OverFlowUtil.real_overflow_dump_times, module_name, + os.path.realpath(dump_file_name))) + if dump_mode == "acl": + acl_dump(module, module_name) + dump.write_to_disk() + dump.api_list.clear() + # clear overflow flag for the next check + torch_npu._C._clear_overflow_npu() + if not OverFlowUtil.check_overflow_dump_times(overflow_nums): + for key in forward_api_info: + write_api_info_json(forward_api_info[key]) + for key in backward_api_info: + write_api_info_json(backward_api_info[key]) + raise ValueError("[overflow {} times]: dump file is saved in '{}'." + .format(OverFlowUtil.real_overflow_dump_times, os.path.realpath(dump_file_name))) + return + + def delete_forward_npy(api_overflow_list, api_info): + for path in glob.glob(FORWARD_REAL_DATA_PATH + "/*.npy"): + if not check_path(api_overflow_list, path): + os.remove(os.path.abspath(path)) + for key in list(api_info.keys()): + if key not in api_overflow: + del forward_api_info[key] + + def overflow_type_judge(in_feat, out_feat, module_name): + if module_name.endswith(Const.BACKWARD): + check_feat = out_feat + else: + check_feat = in_feat + if check_data_overflow(check_feat): + print_warn_log("module name :'{}' is overflow and its inputs already has an overflow, so you need " + "to go back to find where the overflow started.".format(module_name)) + return False + elif not check_data_overflow(in_feat) and not check_data_overflow(out_feat): + print_warn_log("module name :'{}' is overflow and its inputs and outputs do not overflow, " + "so this is a process overflow".format(module_name)) + return False + else: + print_warn_log("module name :'{}' is overflow. Its input is normal and its output " + "is overflow.".format(module_name)) + return True + + def acl_dump(module, module_name): + if "forward" in module_name: + forward_acl_dump(module, module_name) + if "backward" in module_name: + print_info_log("The overflow is caused by backward operator {}. " + "You can use reverse acl dump(mode='acl') to get operator dump data.".format(module_name)) + + return overflowcheck_hook diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/utils.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/utils.py new file mode 100644 index 0000000000..cff9c07efa --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/overflow_check/utils.py @@ -0,0 +1,76 @@ +import json +import os +import stat +import torch + +import numpy as np + +from ..common.utils import Const, check_switch_valid +from ..dump.dump import dump_stack_info, get_scalar_data_info, dump_data, \ + get_not_float_tensor_info, get_float_tensor_info +from ..dump.utils import DumpUtil, make_dump_data_dir + + +class OverFlowUtil(object): + overflow_check_switch = None + overflow_filter_switch = None + real_overflow_dump_times = 0 + overflow_nums = 1 + + @staticmethod + def set_overflow_check_switch(switch, filter_switch): + OverFlowUtil.overflow_check_switch = switch + OverFlowUtil.overflow_filter_switch = filter_switch + + @staticmethod + def get_overflow_check_switch(): + if OverFlowUtil.overflow_check_switch is None: + return True + return OverFlowUtil.overflow_check_switch == "ON" + + @staticmethod + def inc_overflow_dump_times(): + OverFlowUtil.real_overflow_dump_times += 1 + + @staticmethod + def check_overflow_dump_times(need_dump_times): + return OverFlowUtil.real_overflow_dump_times < need_dump_times + + +def set_overflow_check_switch(switch, filter_switch=Const.ON): + check_switch_valid(switch) + check_switch_valid(filter_switch) + + OverFlowUtil.set_overflow_check_switch(switch, filter_switch) + + +def dump_overflow(module_name, in_feat, out_feat, dump_file): + name_template = f"{module_name}" + "_{}" + DumpUtil.dump_data_dir = make_dump_data_dir(dump_file) + dump_stack_info(name_template, dump_file) + if "forward" in name_template: + _dump_tensor_completely(in_feat, name_template.format("input"), dump_file) + _dump_tensor_completely(out_feat, name_template.format("output"), dump_file) + else: + _dump_tensor_completely(in_feat, name_template.format("output"), dump_file) + _dump_tensor_completely(out_feat, name_template.format("input"), dump_file) + + +def _dump_tensor_completely(x, prefix, dump_file_name): + dump_flag = Const.DUMP_RATIO_MAX + 1 + if isinstance(x, (tuple, list)) and x: + for i, item in enumerate(x): + _dump_tensor_completely(item, "{}.{}".format(prefix, i), dump_file_name) + elif isinstance(x, torch.Tensor): + if x.numel() == 0 or len(x.shape) == 0 or not x.is_floating_point(): + if OverFlowUtil.overflow_filter_switch == Const.OFF: + data_info = get_not_float_tensor_info(x) + dump_data(dump_file_name, dump_flag, prefix, data_info) + else: + data_info = get_float_tensor_info(x) + dump_data(dump_file_name, dump_flag, prefix, data_info) + + elif OverFlowUtil.overflow_filter_switch == Const.OFF: + if isinstance(x, bool) or isinstance(x, int) or isinstance(x, float): + data_info = get_scalar_data_info(x) + dump_data(dump_file_name, dump_flag, prefix, data_info) diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse.py new file mode 100644 index 0000000000..9f7a395c2e --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse.py @@ -0,0 +1,4 @@ +from .parse_tool import cli + +if __name__ == '__main__': + cli.parse() diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/__init__.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/cli.py b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/cli.py new file mode 100644 index 0000000000..a751c159eb --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/ptdbg_ascend/parse_tool/cli.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2022-2023. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +from .lib.interactive_cli import InteractiveCli + + +def _run_interactive_cli(cli=None): + print("Interactive command mode") + if not cli: + cli = InteractiveCli() + try: + cli.cmdloop(intro="Start Parsing........") + except KeyboardInterrupt: + print("Exit parsing.......") + + +def parse(): + _run_interactive_cli() diff --git a/debug/accuracy_tools/ptdbg_ascend/src/python/setup.py b/debug/accuracy_tools/ptdbg_ascend/src/python/setup.py new file mode 100644 index 0000000000..90311ae94e --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/src/python/setup.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Copyright (C) 2019-2020. Huawei Technologies Co., Ltd. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import setuptools +from pathlib import Path +import stat +import os + +VERSION = '3.2' + +def generate_ptdbg_ascend_version(): + ptdbg_ascend_root = Path(__file__).parent + version_path = ptdbg_ascend_root / "ptdbg_ascend" / "common" / "version.py" + if version_path.exists(): + version_path.unlink() + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + modes = stat.S_IWUSR | stat.S_IRUSR + with os.fdopen(os.open(version_path, flags, modes), 'w') as f: + f.write("__version__ = '{version}'\n".format(version = VERSION)) + +generate_ptdbg_ascend_version() + +setuptools.setup(name='ptdbg_ascend', + version=VERSION, + description='This is a pytorch precision comparison tools', + long_description='This is a pytorch precision comparison tools, include overflow detect tool', + packages=setuptools.find_packages(), + install_requires = [ + "wheel", + "numpy", + "pandas >= 1.3.5", + "pyyaml" + ], + include_package_data=True, + ext_modules=[], + zip_safe=False) diff --git a/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/advisor.txt b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/advisor.txt new file mode 100644 index 0000000000..5c4825e28e --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/advisor.txt @@ -0,0 +1,3 @@ +Line: NA +Suspect Nodes: NA +Expert Advice: All data in comparison result meets the accuracy requirements. diff --git a/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_20230703104808.csv b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_20230703104808.csv new file mode 100644 index 0000000000..a7742ff3fd --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_20230703104808.csv @@ -0,0 +1,9 @@ +NPU Name,Bench Name,NPU Tensor Dtype,Bench Tensor Dtype,NPU Tensor Shape,Bench Tensor Shape,Cosine,MaxAbsErr,NPU max,NPU min,NPU mean,Bench max,Bench min,Bench mean,Accuracy Reached or Not,Err_message +Functional_linear_0_forward_input.0,Functional_linear_0_forward_input.0,torch.float32,torch.float32,"[3, 2]","[3, 2]",1.0,0.000000,1.948258399963379,-1.0052297115325928,-0.2003595232963562,1.948258399963379,-1.0052297115325928,-0.2003595232963562,Yes, +Functional_linear_0_forward_input.1,Functional_linear_0_forward_input.1,torch.float32,torch.float32,"[3, 2]","[3, 2]",1.0,0.000000,0.28375449776649475,-0.6661239266395569,-0.2789986729621887,0.28375449776649475,-0.6661239266395569,-0.2789986729621887,Yes, +Functional_linear_0_forward_input.2,Functional_linear_0_forward_input.2,torch.float32,torch.float32,[3],[3],1.0,0.000000,0.2457989901304245,-0.6338542103767395,-0.14437106251716614,0.2457989901304245,-0.6338542103767395,-0.14437106251716614,Yes, +Functional_linear_0_forward_output,Functional_linear_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1.0,0.000000,0.8278868794441223,-0.8729169964790344,0.16790540516376495,0.8278868794441223,-0.8729169964790344,0.16790540516376495,Yes, +Torch_relu_0_forward_input.0,Torch_relu_0_forward_input.0,torch.float32,torch.float32,"[3, 3]","[3, 3]",1.0,0.000000,0.8278868794441223,-0.8729169964790344,0.16790540516376495,0.8278868794441223,-0.8729169964790344,0.16790540516376495,Yes, +Torch_relu_0_forward_output,Torch_relu_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1.0,0.000000,0.8278868794441223,0.0,0.31367552280426025,0.8278868794441223,0.0,0.31367552280426025,Yes, +Functional_relu_0_forward_input.0,Functional_relu_0_forward_input.0,torch.float32,torch.float32,"[3, 3]","[3, 3]",1.0,0.000000,0.8278868794441223,-0.8729169964790344,0.16790540516376495,0.8278868794441223,-0.8729169964790344,0.16790540516376495,Yes, +Functional_relu_0_forward_output,Functional_relu_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1.0,0.000000,0.8278868794441223,0.0,0.31367552280426025,0.8278868794441223,0.0,0.31367552280426025,Yes, diff --git a/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_without_accuracy.csv b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_without_accuracy.csv new file mode 100644 index 0000000000..404af78ec0 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/compare_result_without_accuracy.csv @@ -0,0 +1,9 @@ +NPU Name,Bench Name,NPU Tensor Dtype,Bench Tensor Dtype,NPU Tensor Shape,Bench Tensor Shape,Cosine,MaxAbsErr,NPU max,NPU min,NPU mean,Bench max,Bench min,Bench mean,Accuracy Reached or Not,Err_message +,Functional_linear_0_forward_input.0,torch.float32,torch.float32,"[3, 2]","[3, 2]",1,0,1.9482584,-1.005229712,-0.200359523,1.9482584,-1.005229712,-0.200359523,, +,Functional_linear_0_forward_input.1,torch.float32,torch.float32,"[3, 2]","[3, 2]",1,0,0.283754498,-0.666123927,-0.278998673,0.283754498,-0.666123927,-0.278998673,, +,Functional_linear_0_forward_input.2,torch.float32,torch.float32,[3],[3],1,0,0.24579899,-0.63385421,-0.144371063,0.24579899,-0.63385421,-0.144371063,, +,Functional_linear_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1,0,0.827886879,-0.872916996,0.167905405,0.827886879,-0.872916996,0.167905405,, +,Torch_relu_0_forward_input.0,torch.float32,torch.float32,"[3, 3]","[3, 3]",1,0,0.827886879,-0.872916996,0.167905405,0.827886879,-0.872916996,0.167905405,, +,Torch_relu_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1,0,0.827886879,0,0.313675523,0.827886879,0,0.313675523,, +,Functional_relu_0_forward_input.0,torch.float32,torch.float32,"[3, 3]","[3, 3]",1,0,0.827886879,-0.872916996,0.167905405,0.827886879,-0.872916996,0.167905405,, +,Functional_relu_0_forward_output,torch.float32,torch.float32,"[3, 3]","[3, 3]",1,0,0.827886879,0,0.313675523,0.827886879,0,0.313675523,, diff --git a/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/npu_test.pkl b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/npu_test.pkl new file mode 100644 index 0000000000..2e00b07b7c --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/resources/compare/npu_test.pkl @@ -0,0 +1,8 @@ +["Functional_linear_0_forward_input.0", 1, [], "torch.float32", [3, 2], [1.948258399963379, -1.0052297115325928, -0.2003595232963562]] +["Functional_linear_0_forward_input.1", 1, [], "torch.float32", [3, 2], [0.28375449776649475, -0.6661239266395569, -0.2789986729621887]] +["Functional_linear_0_forward_input.2", 1, [], "torch.float32", [3], [0.2457989901304245, -0.6338542103767395, -0.14437106251716614]] +["Functional_linear_0_forward_output", 1, [], "torch.float32", [3, 3], [0.8278868794441223, -0.8729169964790344, 0.16790540516376495]] +["Torch_relu_0_forward_input.0", 1, [], "torch.float32", [3, 3], [0.8278868794441223, -0.8729169964790344, 0.16790540516376495]] +["Torch_relu_0_forward_output", 1, [], "torch.float32", [3, 3], [0.8278868794441223, 0.0, 0.31367552280426025]] +["Functional_relu_0_forward_input.0", 1, [], "torch.float32", [3, 3], [0.8278868794441223, -0.8729169964790344, 0.16790540516376495]] +["Functional_relu_0_forward_output", 1, [], "torch.float32", [3, 3], [0.8278868794441223, 0.0, 0.31367552280426025]] diff --git a/debug/accuracy_tools/ptdbg_ascend/test/run_test.sh b/debug/accuracy_tools/ptdbg_ascend/test/run_test.sh new file mode 100644 index 0000000000..6da8c4da3d --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/run_test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +CUR_DIR=$(dirname $(readlink -f $0)) +TOP_DIR=${CUR_DIR}/.. +TEST_DIR=${TOP_DIR}/"test" +SRC_DIR=${TOP_DIR}/"src"/"python" + +clean() { + cd ${TEST_DIR} + + if [ -e ${TEST_DIR}/"report" ]; then + rm -r ${TEST_DIR}/"report" + echo "remove last ut_report successfully." + fi + + if [ -e ${SRC_DIR}/"build" ]; then + cd ${SRC_DIR} + rm -r build + rm -r ptdbg_ascend.egg-info + echo "remove last build cache." + fi + + if [ -e ${SRC_DIR}/"ptdbg_ascend"/"common"/"version.py" ]; then + rm ${SRC_DIR}/"ptdbg_ascend"/"common"/"version.py" + echo "remove last generated 'version.py'." + fi +} + +run_ut() { + export PYTHONPATH=${SRC_DIR}:${PYTHONPATH} && python3 run_ut.py +} + +main() { + clean + if [ "$1"x == "clean"x ]; then + return 0 + fi + + cd ${SRC_DIR} && python3 setup.py build + cd ${TEST_DIR} && run_ut +} + +main $@ diff --git a/debug/accuracy_tools/ptdbg_ascend/test/run_ut.py b/debug/accuracy_tools/ptdbg_ascend/test/run_ut.py new file mode 100644 index 0000000000..5972e73d27 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/run_ut.py @@ -0,0 +1,41 @@ +import os +import shutil +import subprocess +import sys + +def run_ut(): + cur_dir = os.path.realpath(os.path.dirname(__file__)) + top_dir = os.path.realpath(os.path.dirname(cur_dir)) + ut_path = os.path.join(cur_dir, "ut/") + src_dir = os.path.join(top_dir, "src/python") + report_dir = os.path.join(cur_dir, "report") + + if os.path.exists(report_dir): + shutil.rmtree(report_dir) + + os.makedirs(report_dir) + + cmd = ["python3", "-m", "pytest", ut_path, "--junitxml=" + report_dir + "/final.xml", + "--cov=" + src_dir, "--cov-branch", "--cov-report=xml:" + report_dir + "/coverage.xml"] + + result_ut = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + while result_ut.poll() is None: + line = result_ut.stdout.readline().strip() + if line: + print(line) + + ut_flag = False + if result_ut.returncode == 0: + ut_flag = True + print("run ut successfully.") + else: + print("run ut failed.") + + return ut_flag + +if __name__=="__main__": + if run_ut(): + sys.exit(0) + else: + sys.exit(1) diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_check.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_check.py new file mode 100644 index 0000000000..ced2146ce1 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_check.py @@ -0,0 +1,53 @@ +# coding=utf-8 +import os +import pytest +import unittest +from ptdbg_ascend.overflow_check import overflow_check +from ptdbg_ascend.overflow_check import utils +from ptdbg_ascend.overflow_check.utils import OverFlowUtil, dump_overflow + +ON = "ON" +OFF = "OFF" +ERROR_PID = 1 + + +class TestUtilsMethods(unittest.TestCase): + + def test_check_overflow_environment_1(self): + utils.set_overflow_check_switch(OFF, OFF) + OverFlowUtil.get_overflow_check_switch() + res = overflow_check.check_overflow_environment(ERROR_PID) + self.assertEqual(res, False) + + def test_check_overflow_environment_2(self): + utils.set_overflow_check_switch(ON, ON) + OverFlowUtil.get_overflow_check_switch() + res = overflow_check.check_overflow_environment(ERROR_PID) + self.assertEqual(res, False) + + def test_check_overflow_environment_3(self): + utils.set_overflow_check_switch(ON, ON) + OverFlowUtil.get_overflow_check_switch() + pid = os.getpid() + overflow_check.is_gpu = True + res = overflow_check.check_overflow_environment(pid) + self.assertEqual(res, False) + + def test_check_overflow_environment_4(self): + utils.set_overflow_check_switch(ON, ON) + OverFlowUtil.get_overflow_check_switch() + pid = os.getpid() + overflow_check.is_gpu = False + overflow_check.backward_init_status = True + res = overflow_check.check_overflow_environment(pid) + self.assertEqual(res, False) + + def test_check_overflow_environment_5(self): + utils.set_overflow_check_switch(ON, ON) + OverFlowUtil.get_overflow_check_switch() + pid = os.getpid() + overflow_check.is_gpu = False + overflow_check.backward_init_status = False + res = overflow_check.check_overflow_environment(pid) + self.assertEqual(res, True) + diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_utils.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_utils.py new file mode 100644 index 0000000000..f084ca2113 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/overflow/test_overflow_utils.py @@ -0,0 +1,58 @@ +# coding=utf-8 +import pytest +import unittest +from ptdbg_ascend.overflow_check import utils +from ptdbg_ascend.overflow_check.utils import OverFlowUtil, dump_overflow + +ON = "ON" +OFF = "OFF" + + +class TestUtilsMethods(unittest.TestCase): + + def test_set_overflow_check_switch_error1(self): + with pytest.raises(Exception) as error: + res = OverFlowUtil.set_overflow_check_switch("abc") + self.assertEqual(error.type, TypeError) + + def test_set_overflow_check_switch_error2(self): + with pytest.raises(Exception) as error: + res = utils.set_overflow_check_switch("abc") + self.assertEqual(error.type, AssertionError) + + def test_set_overflow_check_switch_error3(self): + with pytest.raises(Exception) as error: + res = utils.set_overflow_check_switch(ON, "abc") + self.assertEqual(error.type, AssertionError) + + def test_OverFlowUtil_set_overflow_check_switch(self): + OverFlowUtil.set_overflow_check_switch(ON, OFF) + self.assertEqual(OverFlowUtil.overflow_check_switch, ON) + self.assertEqual(OverFlowUtil.overflow_filter_switch, OFF) + + def test_get_overflow_check_switch(self): + res = OverFlowUtil.get_overflow_check_switch() + self.assertEqual(res, True) + + def test_inc_overflow_dump_times(self): + OverFlowUtil.inc_overflow_dump_times() + self.assertEqual(OverFlowUtil.real_overflow_dump_times, 1) + + def test_check_overflow_dump_times(self): + res = OverFlowUtil.check_overflow_dump_times(100) + self.assertEqual(res, True) + + def test_set_overflow_check_switch_success1(self): + utils.set_overflow_check_switch(OFF, OFF) + self.assertEqual(OverFlowUtil.overflow_check_switch, OFF) + self.assertEqual(OverFlowUtil.overflow_filter_switch, OFF) + + def test_set_overflow_check_switch_success2(self): + utils.set_overflow_check_switch(ON) + self.assertEqual(OverFlowUtil.overflow_check_switch, ON) + self.assertEqual(OverFlowUtil.overflow_filter_switch, ON) + + def test_set_overflow_check_switch_success3(self): + utils.set_overflow_check_switch(ON, ON) + self.assertEqual(OverFlowUtil.overflow_check_switch, ON) + self.assertEqual(OverFlowUtil.overflow_filter_switch, ON) diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_acc_compare.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_acc_compare.py new file mode 100644 index 0000000000..7c28922a74 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_acc_compare.py @@ -0,0 +1,135 @@ +# coding=utf-8 +import unittest +import numpy as np +import os +from ptdbg_ascend.compare import acc_compare as compare +from ptdbg_ascend.common.utils import CompareConst + + +npu_dict = {'op_name': ['Functional_conv2d_0_forward_input.0', 'Functional_conv2d_0_forward_input.1', 'Functional_conv2d_0_forward_input.2', 'Functional_conv2d_0_forward_output'],\ + 'input_struct': [('torch.float32', [1, 1, 28, 28]), ('torch.float32', [16, 1, 5, 5]), ('torch.float32', [16])],\ + 'output_struct': [('torch.float32', [1, 16, 28, 28])], 'summery': [[3.029174327850342, -2.926689624786377, -0.06619918346405029], \ + [0.19919930398464203, -0.19974489510059357, 0.006269412115216255], [0.19734230637550354, -0.18177609145641327, 0.007903944700956345], [2.1166646480560303, -2.190781354904175, -0.003579073818400502]], 'stack_info': []} +bench_dict = {'op_name': ['Functional_conv2d_0_forward_input.0', 'Functional_conv2d_0_forward_input.1', 'Functional_conv2d_0_forward_input.2', 'Functional_conv2d_0_forward_output'],\ + 'input_struct': [('torch.float32', [1, 1, 28, 28]), ('torch.float32', [16, 1, 5, 5]), ('torch.float32', [16])],\ + 'output_struct': [('torch.float32', [1, 16, 28, 28])], 'summery': [[3.029174327850342, -2.926689624786377, -0.06619918346405029], \ + [0.19919930398464203, -0.19974489510059357, 0.006269412115216255], [0.19734230637550354, -0.18177609145641327, 0.007903944700956345], [2.1166646480560303, -2.190781354904175, -0.003579073818400502]], 'stack_info': []} +tensor_list = [['Functional_conv2d_0_forward_input.0', 1, [], 'torch.float32', [1, 1, 28, 28], [3.029174327850342, -2.926689624786377, -0.06619918346405029]],\ + ['Functional_conv2d_0_forward_input.1', 1, [], 'torch.float32', [16, 1, 5, 5], [0.19919930398464203, -0.19974489510059357, 0.006269412115216255]], \ + ['Functional_conv2d_0_forward_input.2', 1, [], 'torch.float32', [16], [0.19734230637550354, -0.18177609145641327, 0.007903944700956345]],\ + ['Functional_conv2d_0_forward_output', 1, [], 'torch.float32', [1, 16, 28, 28], [2.1166646480560303, -2.190781354904175, -0.003579073818400502]]] +result_op_dict = {'op_name': ['Functional_conv2d_0_forward_input.0', 'Functional_conv2d_0_forward_input.1', 'Functional_conv2d_0_forward_input.2', 'Functional_conv2d_0_forward_output'], \ +'input_struct': [('torch.float32', [1, 1, 28, 28]), ('torch.float32', [16, 1, 5, 5]), ('torch.float32', [16])], \ +'output_struct': [('torch.float32', [1, 16, 28, 28])], 'summery': [[3.029174327850342, -2.926689624786377, -0.06619918346405029], [0.19919930398464203, -0.19974489510059357, 0.006269412115216255], \ +[0.19734230637550354, -0.18177609145641327, 0.007903944700956345], [2.1166646480560303, -2.190781354904175, -0.003579073818400502]], 'stack_info': []} + +o_result = [['Functional_conv2d_0_forward_input.0', 'Functional_conv2d_0_forward_input.0', 'torch.float32', 'torch.float32', [1, 1, 28, 28], [1, 1, 28, 28], ' ', ' ', ' ', 3.029174327850342, -2.926689624786377, -0.06619918346405029, 3.029174327850342, -2.926689624786377, -0.06619918346405029, 'Yes', ''], ['Functional_conv2d_0_forward_input.1', 'Functional_conv2d_0_forward_input.1', 'torch.float32', 'torch.float32', [16, 1, 5, 5], [16, 1, 5, 5], ' ', ' ', ' ', 0.19919930398464203, -0.19974489510059357, 0.006269412115216255, 0.19919930398464203, -0.19974489510059357, 0.006269412115216255, 'Yes', ''], ['Functional_conv2d_0_forward_input.2', 'Functional_conv2d_0_forward_input.2', 'torch.float32', 'torch.float32', [16], [16], ' ', ' ', ' ', 0.19734230637550354, -0.18177609145641327, 0.007903944700956345, 0.19734230637550354, -0.18177609145641327, 0.007903944700956345, 'Yes', ''], ['Functional_conv2d_0_forward_output', 'Functional_conv2d_0_forward_output', 'torch.float32', 'torch.float32', [1, 16, 28, 28], [1, 16, 28, 28], ' ', ' ', ' ', 2.1166646480560303, -2.190781354904175, -0.003579073818400502, 2.1166646480560303, -2.190781354904175, -0.003579073818400502, 'Yes', '']] + +class TestUtilsMethods(unittest.TestCase): + def test_correct_data(self): + input_1 = 'NAN' + result_1 = compare.correct_data(input_1) + self.assertEqual(result_1, 'NAN') + input_2 = '0.99999' + result_2 = compare.correct_data(input_2) + self.assertEqual(result_2, '0.99999') + input_3 = '0.999991' + result_3 = compare.correct_data(input_3) + self.assertEqual(result_3, '1.0') + + def test_cosine_similarity_when_all_result_less_than_epsilon(self): + n_value = np.array([0, 0, 0]) + b_value = np.array([0, 0, 0]) + result, message = compare.cosine_similarity(n_value, b_value) + self.assertEqual(result, '1.0') + self.assertEqual(message, '') + + def test_cosine_similarity_when_only_npu_result_less_than_epsilon(self): + n_value = np.array([0, 0, 0]) + b_value = np.array([1, 2, 3]) + result, message = compare.cosine_similarity(n_value, b_value) + self.assertEqual(result, CompareConst.NAN) + self.assertEqual(message, 'Cannot compare by Cosine Similarity, All the data is Zero in npu dump data.') + + def test_cosine_similarity_when_only_bench_result_less_than_epsilon(self): + n_value = np.array([1, 2, 3]) + b_value = np.array([0, 0, 0]) + result, message = compare.cosine_similarity(n_value, b_value) + self.assertEqual(result, CompareConst.NAN) + self.assertEqual(message, 'Cannot compare by Cosine Similarity, All the data is Zero in Bench dump data.') + + def test_cosine_similarity_when_all_result_greater_than_epsilon_with_no_nan(self): + n_value = np.array([1, 2, 3]) + b_value = np.array([1, 2, 3]) + result, message = compare.cosine_similarity(n_value, b_value) + + self.assertEqual(result, '1.0') + self.assertEqual(message, '') + + def test_cosine_similarity_when_all_result_greater_than_epsilon_with_nan(self): + n_value = np.array([1, 2, np.nan]) + b_value = np.array([1, 2, 3]) + result, message = compare.cosine_similarity(n_value, b_value) + self.assertEqual(result, CompareConst.NAN) + self.assertEqual(message, 'Cannot compare by Cosine Similarity, the dump data has NaN.') + + def test_get_rmse_when_rmse_is_nan(self): + n_value = np.array([1, 2, np.nan]) + b_value = np.array([1, 2, 3]) + rmse, message = compare.get_rmse(n_value, b_value) + self.assertEqual(rmse, CompareConst.NAN) + self.assertEqual(message, "") + + def test_get_mape_when_mape_is_nan(self): + n_value = np.array([1, 2, np.nan]) + b_value = np.array([1, 2, 3]) + mape, message = compare.get_mape(n_value, b_value) + self.assertEqual(mape, CompareConst.NAN) + self.assertEqual(message, "") + + def test_get_max_relative_err_when_max_relative_is_nan(self): + n_value = np.array([1, 2, np.nan]) + b_value = np.array([1, 2, 3]) + max_relative_err, message = compare.get_max_relative_err(n_value, b_value) + self.assertEqual(max_relative_err, CompareConst.NAN) + self.assertEqual(message, 'Cannot compare by MaxRelativeError, the data contains nan in dump data.') + + def test_get_max_relative_err_when_max_relative_is_not_nan(self): + n_value = np.array([1, 2, 3]) + b_value = np.array([1, 2, 3]) + max_relative_err, message = compare.get_max_relative_err(n_value, b_value) + self.assertEqual(max_relative_err, "0.000000") + self.assertEqual(message, "") + + def test_check_op(self): + fuzzy_match = False + result = compare.check_op(npu_dict, bench_dict, fuzzy_match) + self.assertEqual(result, True) + + def test_merge_tensor(self): + op_dict = compare.merge_tensor(tensor_list) + self.assertEqual(op_dict, result_op_dict) + + def test_read_op(self): + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + pkl_dir = os.path.join(base_dir, "resources/compare/npu_test.pkl") + + npu_ops_queue = [] + npu_pkl_handle = open(pkl_dir, "r") + stack_mode = False + result = compare.read_op(npu_ops_queue, npu_pkl_handle, stack_mode) + self.assertEqual(result, True) + + + def test_match_op(self): + fuzzy_match = False + a, b = compare.match_op([npu_dict], [bench_dict], fuzzy_match) + self.assertEqual(a, 0) + self.assertEqual(b, 0) + + def test_get_accuracy(self): + result = [] + compare.get_accuracy(result, npu_dict, bench_dict) + + self.assertEqual(result, o_result) diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor.py new file mode 100644 index 0000000000..0bee5d4afb --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright Huawei Technologies Co., Ltd. 2023-2023. All rights reserved. +import os +import shutil +import unittest +from ptdbg_ascend.advisor.advisor import Advisor +from ptdbg_ascend.common.utils import CompareException + + +class TestAdvisor(unittest.TestCase): + def setUp(self) -> None: + os.makedirs("test_result/output", exist_ok=True) + self.output_path = os.path.abspath("test_result/output") + + def tearDown(self) -> None: + shutil.rmtree("test_result/", ignore_errors=True) + + def test_analysis_when_csv_path_is_not_exist(self): + advisor = Advisor("resources/compare/test.pkl", self.output_path) + self.assertRaises(CompareException, advisor.analysis) + + def test_analysis_when_csv_path_is_invalid(self): + advisor = Advisor("resources/compare/npu_test_1.pkl", self.output_path) + self.assertRaises(CompareException, advisor.analysis) + + def test_analysis_when_csv_is_valid(self): + advisor = Advisor("resources/compare/compare_result_20230703104808.csv", self.output_path) + advisor.analysis() + filenames = os.listdir(self.output_path) + self.assertEqual(len(filenames), 1) + + def test_analysis_when_accuracy_and_npu_name_not_in_csv(self): + advisor = Advisor("resources/compare/compare_result_without_accuracy.csv", self.output_path) + self.assertRaises(AttributeError, advisor.analysis) diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor_result.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor_result.py new file mode 100644 index 0000000000..1c84039159 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_advisor_result.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright Huawei Technologies Co., Ltd. 2023-2023. All rights reserved. +import difflib +import os +import shutil +import unittest +from ptdbg_ascend.advisor.advisor import Advisor + + +class TestAdvisor(unittest.TestCase): + def setUp(self) -> None: + os.makedirs("test_result/output", exist_ok=True) + self.output_path = os.path.abspath("test_result/output") + self.has_error = False + + def tearDown(self) -> None: + shutil.rmtree("test_result/", ignore_errors=True) + + def test_advisor_summary_file(self): + advisor = Advisor("resources/compare/compare_result_20230703104808.csv", self.output_path) + advisor.analysis() + filenames = os.listdir(self.output_path) + for filename in filenames: + filename = os.path.join(self.output_path, filename) + self.result_check("resources/compare/advisor.txt", filename) + self.assertFalse(self.has_error) + + def result_check(self, standard_file, output_file): + with open(standard_file, 'r', encoding='utf-8') as st_file: + standard_content = st_file.read().splitlines() + with open(output_file, 'r', encoding='utf-8') as out_file: + output_content = out_file.read().splitlines() + result = list(difflib.unified_diff(standard_content, output_content, n=0)) + if result: + print('\n\n-------------------------------------------------------------------------', flush=True) + print(f'[ERROR] {output_file.replace(self.output_path, "")} advisor summary are inconsistent.', + flush=True) + print('\n'.join(result), flush=True) + print('-------------------------------------------------------------------------', flush=True) + self.has_error = True diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_common_util.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_common_util.py new file mode 100644 index 0000000000..5fc5ae51ea --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_common_util.py @@ -0,0 +1,87 @@ +# coding=utf-8 +import unittest +import time +from datetime import datetime, timezone +from ptdbg_ascend.common import utils as common +#from ptdbg_ascend.common import CompareException + +class TestCommonUtilsMethods(unittest.TestCase): + + def test_VersionCheck(self): + V0_1 = "0.1" + V1_8 = "1.8" + V1_11 = "1.11" + V2_0 = "2.0" + V2_1 = "2.1" + version_check = common.VersionCheck + self.assertFalse(version_check.check_torch_version(V0_1)) + self.assertTrue(version_check.check_torch_version(V1_8) or version_check.check_torch_version(V1_11) or version_check.check_torch_version(V2_0) or version_check.check_torch_version(V2_1)) + + def test_check_mode_valid(self): + mode_check = common.check_mode_valid + self.assertEqual(mode_check("all"), None) + self.assertEqual(mode_check("list",scope=["Tensor_permute_1_forward", "Tensor_transpose_2_forward", "Torch_relu_3_backward"]), None) + self.assertEqual(mode_check("range", scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]), None) + self.assertEqual(mode_check("stack",scope=["Tensor_abs_1_forward", "Tensor_transpose_3_forward"]), None) + self.assertEqual(mode_check("acl",scope=["Tensor_permute_1_forward"]), None) + self.assertEqual(mode_check("api_list",api_list=["relu"]), None) + self.assertEqual(mode_check("api_stack"), None) + self.assertRaises(common.CompareException, mode_check, "api_stack_123") + + def test_parse_arg_value(self): + data = [[1, 2, 4, 8]] + self.assertEqual(common.parse_arg_value("1,2,4,8"), data) + + def test_parse_value_by_comma(self): + data = [1, 2, 4, 8] + self.assertEqual(common.parse_value_by_comma("1,2,4,8"), data) + + def test_get_data_len_by_shape(self): + getshape = common.get_data_len_by_shape + data = [1, 2, 4, 8] + self.assertEqual(getshape(data), 64) + data = [-1, 2, 4, 8] + self.assertEqual(getshape(data), -1) + + def test_add_time_as_suffix(self): + name = "op_cmp" + csv_name = '{}_{}.csv'.format(name, time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))) + self.assertEqual(common.add_time_as_suffix(name), csv_name) + + def test_get_time(self): + time = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S") + self.assertEqual(common.get_time(), time) + + def test_format_value(self): + value = 12345.6789 + format_value = '{:.6f}'.format(value) + self.assertEqual(common.format_value(value), format_value) + + def test_modify_dump_path(self): + dump_path = "/usr/dump" + mode = "api_stack" + self.assertEqual(common.modify_dump_path(dump_path, mode), "/usr/api_stack_dump") + + def test_create_directory(self): + pass + + def test_execute_command(self): + pass + + def test_save_numpy_data(self): + pass + + def test_torch_device_guard(self): + pass + + def test_seed_all(self): + pass + + def test_get_process_rank(self): + pass + + def test_check_file_size(self): + pass + + def test_get_dump_data_path(self): + pass \ No newline at end of file diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_hooks.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_hooks.py new file mode 100644 index 0000000000..7874d3c2fa --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_hooks.py @@ -0,0 +1,71 @@ +# coding=utf-8 +import os +import unittest +from ptdbg_ascend.dump import utils as hooks + + +class TestUtilsMethods(unittest.TestCase): + + def test_set_dump_switch_only_set_switch_as_on(self): + dump_count = hooks.dump_count + dump_util = hooks.DumpUtil + switch_on = "ON" + mode_all = "all" + hooks.set_dump_switch(switch_on) + self.assertEqual(dump_util.dump_switch, switch_on) + self.assertEqual(dump_util.dump_switch_mode, mode_all) + self.assertTrue(dump_util.dump_init_enable) + self.assertEqual(dump_util.dump_switch_scope, []) + self.assertEqual(dump_util.dump_api_list, []) + self.assertEqual(dump_util.dump_filter_switch, switch_on) + self.assertEqual(dump_count, 0) + + def test_set_dump_switch_mode_is_list(self): + scope_list = ["Tensor_permute_1_forward", "Tensor_transpose_2_forward"] + dump_util = hooks.DumpUtil + hooks.set_dump_switch("ON", mode="list", scope=scope_list) + self.assertEqual(dump_util.dump_switch_mode, "list") + self.assertEqual(dump_util.dump_switch_scope, scope_list) + + def test_set_dump_switch_mode_is_range(self): + scope_list = ["Tensor_permute_1_forward", "Tensor_transpose_3_forward"] + dump_util = hooks.DumpUtil + hooks.set_dump_switch("ON", mode="range", scope=scope_list) + self.assertEqual(dump_util.dump_switch_mode, "range") + self.assertEqual(dump_util.dump_switch_scope, scope_list) + + def test_set_dump_switch_mode_is_stack(self): + scope_list = ["Tensor_abs_1_forward", "Tensor_transpose_3_forward"] + dump_util = hooks.DumpUtil + hooks.set_dump_switch("ON", mode="stack", scope=scope_list) + self.assertEqual(dump_util.dump_switch_mode, "stack") + self.assertEqual(dump_util.dump_switch_scope, scope_list) + + def test_set_dump_switch_mode_is_api_list(self): + api_list = ["Transpose", "Relu", "triu"] + lower_api_list = ["transpose", "relu", "triu"] + dump_util = hooks.DumpUtil + hooks.set_dump_switch("ON", mode="api_list", api_list=api_list) + self.assertEqual(dump_util.dump_switch_mode, "api_list") + self.assertEqual(dump_util.dump_api_list, lower_api_list) + + def test_set_dump_switch_mode_is_acl(self): + scope_list = ["Tensor_transpose_3_backward"] + replace_scope = ["Tensor_transpose_3_forward"] + dump_util = hooks.DumpUtil + hooks.set_dump_switch("ON", mode="acl", scope=scope_list) + self.assertEqual(dump_util.dump_switch_mode, "acl") + self.assertEqual(dump_util.dump_switch_scope, replace_scope) + + def test_set_dump_filter_switch_off(self): + dump_util = hooks.DumpUtil + hooks.DumpUtil.dump_path='/home/dump_path/ptdbg_dump_v3.2/rank0' + hooks.set_dump_switch("ON", filter_switch="OFF") + self.assertEqual(dump_util.dump_filter_switch, "OFF") + + def test_set_dump_path(self): + dump_util = hooks.DumpUtil + hooks.set_dump_path("resources", dump_tag="dump_data") + output_path = os.path.abspath("resources") + self.assertEqual(dump_util.dump_path, output_path) + self.assertEqual(dump_util.dump_dir_tag, "dump_data") diff --git a/debug/accuracy_tools/ptdbg_ascend/test/ut/test_utils.py b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_utils.py new file mode 100644 index 0000000000..ae1f332118 --- /dev/null +++ b/debug/accuracy_tools/ptdbg_ascend/test/ut/test_utils.py @@ -0,0 +1,43 @@ +import unittest +import pytest +import ptdbg_ascend.common.utils as utils + +from ptdbg_ascend.common.utils import CompareException + + +class TestUtilsMethods(unittest.TestCase): + def test_get_api_name_from_matcher(self): + normal_name = "Functional_relu__1_output" + unusual_name = "Functional_norm_layer_1_output" + error_name = "Tensor_onnx::connect_1_input" + api_name_1 = utils.get_api_name_from_matcher(normal_name) + api_name_2 = utils.get_api_name_from_matcher(unusual_name) + api_name_3 = utils.get_api_name_from_matcher(error_name) + self.assertEqual(api_name_1, "relu") + self.assertEqual(api_name_2, "norm_layer") + self.assertEqual(api_name_3, "") + + def test_check_file_or_directory_path_1(self): + file = "list" + with pytest.raises(CompareException) as error: + utils.check_file_or_directory_path(file) + self.assertEqual(error.value.code, CompareException.INVALID_PATH_ERROR) + + def test_check_file_or_directory_path_2(self): + file = "/list/dir" + with pytest.raises(CompareException) as error: + utils.check_file_or_directory_path(file) + self.assertEqual(error.value.code, CompareException.INVALID_PATH_ERROR) + + def test_check_file_size_1(self): + file = "/list/dir" + with pytest.raises(CompareException) as error: + utils.check_file_size(file, 100) + self.assertEqual(error.value.code, CompareException.INVALID_FILE_ERROR) + + def test_check_file_size_2(self): + file = "../run_ut.py" + with pytest.raises(CompareException) as error: + utils.check_file_size(file, 0) + self.assertEqual(error.value.code, CompareException.INVALID_FILE_ERROR) + diff --git a/debug/accuracy_tools/ptdbg_ascend/tools/.keep b/debug/accuracy_tools/ptdbg_ascend/tools/.keep new file mode 100644 index 0000000000..e69de29bb2 -- Gitee From 74ec9a9a1c5f8b8196124b198622cde3ebe59c34 Mon Sep 17 00:00:00 2001 From: hekunkun Date: Fri, 25 Aug 2023 07:07:25 +0000 Subject: [PATCH 2/2] =?UTF-8?q?update=20debug/accuracy=5Ftools/ptdbg=5Fasc?= =?UTF-8?q?end/doc/ptdbg=5Fascend=E7=B2=BE=E5=BA=A6=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=AF=B4=E6=98=8E=5Fv3.2.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: hekunkun --- ...12\237\350\203\275\350\257\264\346\230\216_v3.2.md" | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" index 8fbef92fce..8603e80ed0 100644 --- "a/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" +++ "b/debug/accuracy_tools/ptdbg_ascend/doc/ptdbg_ascend\347\262\276\345\272\246\345\267\245\345\205\267\345\212\237\350\203\275\350\257\264\346\230\216_v3.2.md" @@ -2,7 +2,7 @@ 本文主要介绍PyTorch精度工具精度工具ptdbg_ascend的使用以及精度比对场景示例。 -ptdbg_ascend工具的原理及安装请参见《[PyTorch精度工具](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/README.md)》。 +ptdbg_ascend工具的原理及安装请参见《[PyTorch精度工具](https://gitee.com/kun_8/att_1/blob/master/debug/accuracy_tools/ptdbg_ascend/README.md)》。 ## PyTorch精度比对总体流程 @@ -220,7 +220,7 @@ register_hook需要在set_dump_path之后调用,也需要在每个进程上被 - 找到训练代码中遍历epoch的for循环或遍历数据集的for循环,把register_hook放到循环开始前即可。 - 找到训练代码中调用DDP或者DistributedDataParallel的代码行,把register_hook放到该代码行所在的代码块之后。 -- 若代码中均无以上两种情况,需要保证register_hook在模型定义之后插入,并配置rank参数。rank参数获取rank_id请参见“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”。 +- 若代码中均无以上两种情况,需要保证register_hook在模型定义之后插入,并配置rank参数。rank参数获取rank_id请参见“**[rank_id获取方法](https://gitee.com/kun_8/att_1/blob/master/debug/accuracy_tools/ptdbg_ascend/doc/rank_id获取方法.md)**”。 ### NPU vs NPU精度比对 @@ -594,7 +594,7 @@ debugger.stop() - ptdbg_ascend工具默认情况下仅dump PyTorch模型的API输入输出数据进行精度比对,若在比对结果中发现某个API下可能存在ACL的精度问题,那么可以选择dump该API的ACL级别数据进行精度分析。 -- 某些torch api的输出不是Tensor类型的数据。对于此类API的反向过程进行ACL dump,工具会在运行日志中给出对应的Warning(is not of tensor type and cannot be automatically derived)提示。如若想要进行该类API反向ACL dump,可以通过手动构建单API用例的方式进行ACL dump,具体用例可参见“**[反向ACL dump用例说明](https://gitee.com/ascend/tools/blob/master/ptdbg_ascend/doc/%E5%8F%8D%E5%90%91ACL%20dump%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E.md)**”。 +- 某些torch api的输出不是Tensor类型的数据。对于此类API的反向过程进行ACL dump,工具会在运行日志中给出对应的Warning(is not of tensor type and cannot be automatically derived)提示。如若想要进行该类API反向ACL dump,可以通过手动构建单API用例的方式进行ACL dump,具体用例可参见“**[反向ACL dump用例说明](https://gitee.com/kun_8/att_1/blob/master/debug/accuracy_tools/ptdbg_ascend/doc/%E5%8F%8D%E5%90%91ACL%20dump%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E.md)**”。 - 工具性能:dump数据量较小时(小于5G),参考dump速度0.1GB/s;dump数据量较大时,参考dump速度0.2GB/s。 推荐环境配置:独占环境,CPU核心数192,固态硬盘(IO速度参考:固态硬盘 > 500MB/s,机械硬盘60 ~ 170MB/s)。 @@ -746,7 +746,7 @@ register_hook(model, hook, overflow_nums=overflow_nums, dump_mode=dump_mode, dum | overflow_nums | 控制溢出次数,表示第N次溢出时,停止训练,过程中检测到溢出API对应ACL数据均dump。参数示例:overflow_nums=3。配置overflow_check时可配置,默认不配置,即检测到1次溢出,训练停止。 | 否 | | dump_mode | 控制针对溢出API的dump模式。可取值"api"或"acl",配置acl时表示dump ACL级别的溢出数据,此时set_dump_path参数不生效,dump数据目录由dump_config的.json文件配置,参数示例:dump_mode="acl"。默认不配置,即dump API级别的溢出数据。 | 否 | | dump_config | acl dump的配置文件。dump_mode="acl"时,该参数必选;dump_mode="api"时,该参数不选。参数示例:dump_config='./dump.json'。 | 否 | -| rank | 控制dump数据保存的rank目录名称。参数示例:rank=1。默认不配置,即自动读取dump数据所属的卡并保存在该卡对应的rank目录下。目录结构参见“**dump数据存盘说明**”。
多卡情况下,可能出现工具识别rank出错,导致dump数据保存到错误的rank目录下,此时需要根据“**[rank_id获取方法](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/rank_id获取方法.md)**”配置该参数,以获取正确的rank_id;工具可正确识别rank_id时无须配置该参数。 | 否 | +| rank | 控制dump数据保存的rank目录名称。参数示例:rank=1。默认不配置,即自动读取dump数据所属的卡并保存在该卡对应的rank目录下。目录结构参见“**dump数据存盘说明**”。
多卡情况下,可能出现工具识别rank出错,导致dump数据保存到错误的rank目录下,此时需要根据“**[rank_id获取方法](https://gitee.com/kun_8/att_1/blob/master/debug/accuracy_tools/ptdbg_ascend/doc/rank_id获取方法.md)**”配置该参数,以获取正确的rank_id;工具可正确识别rank_id时无须配置该参数。 | 否 | **函数示例** @@ -1461,4 +1461,4 @@ Parse >>> cn -m Add.InceptionV3_InceptionV3_Mixed_7a_Branch_0_add_3.323.16194941 ## FAQ -[FAQ](https://gitee.com/ascend/tools/tree/master/ptdbg_ascend/doc/FAQ.md) +[FAQ](https://gitee.com/kun_8/att_1/blob/master/debug/accuracy_tools/ptdbg_ascend/doc/FAQ.md) -- Gitee