diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0a61e27..365086b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,13 @@ + ### PR types + ### PR changes + ### Description + diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index f9851ab..2b8df5d 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -13,48 +13,28 @@ on: - '**.rst' jobs: - link-check: - name: Check Links + docs-check: + name: Validate Documentation runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - - name: Check Markdown Links - uses: gaurav0/markdown-link-check@v1 + - name: Check Markdown Links (Rust) + uses: lycheeverse/lychee-action@v2.6.1 with: - use-quiet-mode: 'yes' - use-verbose-mode: 'no' + args: --verbose docs/**/*.md *.md - spell-check: - name: Spell Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Spell Check - uses: streetsidesoftware/action-spellcheck@v0 + - name: Run Spell Check (cspell) + uses: streetsidesoftware/cspell-action@v2 with: - check-co-authored-commits: true + files: docs/**/*.md *.md - format-check: - name: Format Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install Prettier - run: npm install --global prettier + - name: Cleanup temporary files + run: | + rm -rf lychee/ - - name: Check Markdown Format + - name: Format Check with Prettier run: | - # 使用 prettier 检查格式,但不自动修改 + npm install -g prettier npx prettier --check "docs/**/*.md" "**/*.md" - # 如果格式不正确,此命令会失败,CI 将标记为失败 diff --git a/README.md b/README.md index 435a0b9..bd2b399 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,18 @@ # PaDiff ![](https://img.shields.io/badge/version-v0.1-brightgreen) ![](https://img.shields.io/badge/docs-latest-brightgreen) ![](https://img.shields.io/badge/PRs-welcome-orange) ![](https://img.shields.io/badge/pre--commit-Yes-brightgreen) +**P**addle **A**utomatically **Diff** precision toolkits. -**P**addle **A**utomatically **Diff** precision toolkits. - - -## 最近更新(latest 9.8) +## 最近更新(latest 9.11) ### 使用单行命令对齐(支持前反向对齐) -运行命令前,请运行 `python -m padiff.cli -h` 获取更详细的参数说明。 - -直接通过命令行运行 - -```sh -python -m padiff.cli \ - --pt_cmd "python torch_project/run.py" \ - --pd_cmd "python paddle_project/run.py" \ - --pt_model_name "pt_model" \ - --pd_model_name "pd_model" \ - --pt_optim_name "pt_optimizer" \ - --pd_optim_name "pd_optimizer" \ - --log_dir "./padiff_log" \ - --align_depth 1 \ - --single_step_mode "forward" \ - --atol 1e-4 \ - --rtol 1e-5 \ - --compare_mode mean \ - --action_name equal -``` - -或将命令写入 .yaml 文件后,运行 +将命令写入配置文件后,通过如下命令运行: ```sh python -m padiff.cli --config padiff_config.yaml ``` -yaml 文件样例 - -```python -# padiff_config.yaml -pt_cmd: "python transformer4sr/train_transformer.py" -pd_cmd: "python paddle_project/train_transformer.py" -pt_model_name: "transformer_pt" -pd_model_name: "transformer_pd" -pt_optim_name: "optimizer_pt" -pd_optim_name: "optimizer_pd" -log_dir: "./padiff_log" -align_depth: 2 -single_step_mode: "forward" -atol: 1.0e-04 -rtol: 1.0e-05 -compare_mode: "mean" -action_name: "equal" -``` +完整文件示例请参考 [配置文件说明文档](docs/CLIConfig.md),同时,运行命令前,请运行 `python -m padiff.cli -h` 获取更详细的参数说明。 ### log 设置 @@ -62,37 +22,31 @@ action_name: "equal" #### 开启静默模式 -或者为了保持控制台信息简洁,可以设置环境变量 `PADIFF_SILENT=1`,以便仅保存 log 文件,不在控制台输出 log 信息 - +为了保持控制台信息简洁,可以设置环境变量 `PADIFF_SILENT=1`,此模式下仅保存 log 文件,不在控制台输出 log 信息 ## 简介 PaDiff 是基于 PaddlePaddle 与 PyTorch 的模型精度对齐工具。传入 Paddle 或 Torch 模型,对齐训练中间结果以及训练后的模型权重,并提示精度 diff 第一次出现的位置。 -- 文档目录 [Guides](docs/README.md) -- 使用教程 [Tutorial](docs/Tutorial.md) -- 对齐ViTPose流程 [ViTPose](docs/CheckViTPose.md) -- 接口参数说明 [Interface](docs/Interfaces.md) -- 常见问题解答 [FAQs](docs/FAQs.md) - - - +- 文档目录 [Guides](docs/README.md) +- 使用教程 [Tutorial](docs/Tutorial.md) +- 对齐ViTPose流程 [ViTPose](docs/CheckViTPose.md) +- 接口参数说明 [Interface](docs/Interfaces.md) +- 常见问题解答 [FAQs](docs/FAQs.md) ## 安装 - PaDiff v0.2 版本已发布,可通过如下命令安装: +PaDiff v0.2 版本已发布,可通过如下命令安装: - ``` +``` pip install padiff - ``` +``` - 尝鲜版或开发者推荐clone源码并使用如下命令安装: +尝鲜版或开发者推荐clone源码并使用如下命令安装: - ``` +``` python setup.py install - ``` - - +``` ## 快速开始 @@ -129,8 +83,6 @@ inp = ({'x': torch.as_tensor(inp) }, auto_diff(module, layer, inp, atol=1e-4, auto_init=True) ``` - - ### 离线对齐 ```py @@ -202,7 +154,8 @@ for i in range(6): ``` ### 框架与编译器对齐 -使用文档 [CINN](padiff/cinn_diff/README.md) + +使用文档 [CINN](padiff/experimental/cinn_diff/README.md) ```python import os @@ -223,6 +176,6 @@ if __name__ == '__main__': ## 已支持 `Special Init` 的组件 -- MultiHeadAttention -- LSTM -- BatchNorm2D +- MultiHeadAttention +- LSTM +- BatchNorm2D diff --git a/docs/CLIConfig.md b/docs/CLIConfig.md new file mode 100644 index 0000000..7b326be --- /dev/null +++ b/docs/CLIConfig.md @@ -0,0 +1,162 @@ +# PaDiff 配置文件参考(命令行运行) + +本工具支持代码自动注入,通过命令行运行,运行时通过 YAML 配置文件接收所有参数。 +配置文件分为三个主要部分:`CLI`, `PaDiffGuard`, 和 `COMPARE`。 + +完整文件示例请参考 [config_example](./config_example.yaml) + +## CLI 部分 + +定义与脚本执行相关的参数。 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +| --------------- | ------ | ---- | -------------- | -------------------------------- | +| `pt_cmd` | string | 是 | - | 运行 PyTorch 脚本的完整命令 | +| `pd_cmd` | string | 是 | - | 运行 PaddlePaddle 脚本的完整命令 | +| `pt_model_name` | string | 否 | "model" | PyTorch 模型实例的变量名 | +| `pd_model_name` | string | 否 | "model" | PaddlePaddle 模型实例的变量名 | +| `pt_optim_name` | string | 否 | null | PyTorch 优化器实例的变量名 | +| `pd_optim_name` | string | 否 | null | PaddlePaddle 优化器实例的变量名 | +| `log_dir` | string | 否 | "./padiff_log" | 日志和报告的输出目录 | + +## PaDiffGuard 部分 + +定义模型对齐的核心行为。 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +| ------------------- | ------------ | ---- | ------ | ---------------------------------------------------- | +| `align_depth` | int or "inf" | 否 | "inf" | 对齐的深度。"inf" 表示最细粒度 | +| `single_step_mode` | string | 否 | null | 单步对齐模式 ("forward", "backward", "both") | +| `load_init_weights` | bool | 否 | false | 是否自动对齐初始化权重,若已手动对齐,请设置为 false | +| `load_first_inputs` | bool | 否 | false | 是否自动第一次的输入,若已手动对齐,请设置为 false | +| `max_calls` | int | 否 | 1 | 最大前反向调用次数 | +| `black_list` | list | 否 | [] | 不参与对齐的层名列表,列表内元素为 str 类型 | +| `keys_mapping` | dict | 否 | null | 模型参数名映射字典 | + +## COMPARE 部分 + +定义结果对比的精度和逻辑。 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +| -------------- | ------ | ---- | ------- | --------------------------------------- | +| `atol` | float | 否 | 1e-6 | 绝对误差容忍度 | +| `rtol` | float | 否 | 1e-6 | 相对误差容忍度 | +| `compare_mode` | string | 否 | "mean" | 对比模式 ("mean", "strict", "abs_mean") | +| `action_name` | string | 否 | "equal" | 对比动作 ("equal", "loose_equal") | + +## 示例和详细说明 + +#### 命令参数 (pt_cmd, --pd_cmd) + +- 这些参数是您运行原始模型的完整命令 +- 通常以 'python' 开头 +- 必须指向包含您模型代码的 Python 脚本 +- 必需被包含在 config 文件中,或通过命令行传入 + +``` +pt_cmd: "python torch_project/run.py" +pd_cmd: "python paddle_project/run.py" +``` + +#### 模型变量名参数 (pt_model_name, pd_model_name) + +- 这些参数指定您在脚本中创建模型实例的**变量名** +- 它们不是类名,也不是文件名 +- 它们是模型实例化时 `=` 左边的标识符 + +``` +# 如果您的 PyTorch 脚本中有: +my_torch_model = MyNet() +output = my_torch_model(input_tensor) +# 那么应该使用: +pt_model_name: "my_torch_model" + +# 如果您的 Paddle 脚本中有: +net = SimplePaddle() +out = net.generate(input_tensor) +# 那么应该使用: +pd_model_name: "net" + +# 如果您的 Paddle 脚本中有: +trainer = SFTTrainer( + args=training_args, + model="Qwen/Qwen2.5-0.5B-Instruct", + train_dataset=dataset, +) +trainer.train() +那么应该使用: +pd_model_name: "trainer.model" +``` + +#### 优化器名参数 (pt_optim_name, pd_optim_name) + +- 这些参数指定您在脚本中创建优化器实例的**变量名**。 +- 它们不是类名,也不是文件名。 +- 它们是优化器实例化时 `=` 左边的标识符。 +- 该参数为非必须参数,默认值: None (不传递优化器) + +``` +# 如果您的 PyTorch 脚本中有: +optim = torch.optim.Adam( + transformer.parameters(), + lr=1.0, + betas=(0.9, 0.98), + eps=1e-9, +) +# 那么应该使用: +pt_optim_name: "optim" + +# 如果您的 Paddle 脚本中有: +trainer = SFTTrainer( + args=training_args, + model="Qwen/Qwen2.5-0.5B-Instruct", + train_dataset=dataset, +) +trainer.train() +# 由于 trainer.train() 中通常已经包含了完整的前反向过程,因此不需要传递此参数 +``` + +#### 日志目录参数 (log_dir) + +- 指定生成报告和日志的目录 +- 默认值: ./padiff_log + +``` +log_dir: "./padiff_log" +``` + +#### 对齐深度参数 (align_depth) + +- 控制对齐的粒度。通过指定一个深度值,可以忽略该深度以下的所有子模块 +- 值为整数: 指定一个具体的深度。例如,--align_depth 1 会忽略深度为1及以下的所有子模块 +- 默认值: 'inf' ,即无限深度,会对齐到最细粒度的层(如 Linear, ReLU) +- 值为整数,当数值超过模型最大迭代深度时,相当于 'inf' + +``` +align_depth: 0 # 只对齐顶层模块 +align_depth: 1 # 对齐到第一层子模块 +align_depth: "inf" # 对齐到最细粒度 +``` + +#### 单步对齐模式参数 (single_step_mode) + +- 启用逐层对齐模式 +- 可选值: forward, backward, both +- 默认值: None (不启用) +- 当启用时,工具会从自动加载基准模型的输出,并用其替换对齐模型的相应层输出 + +#### 结果对比参数(COMPARE) + +- 控制模型输出结果的对比精度和模式 +- atol: 绝对误差容忍度 (default: 1e-6) +- rtol: 相对误差容忍度 (default: 1e-6) +- compare_mode: 对比模式,具体内容请看对应文档。可选值: mean, strict, abs_mean, 默认值: "mean" +- action_name: 对比行为,具体内容请看对应文档。可选值: equal, loose_equal, 默认值: "equal" + +``` +COMPARE: + atol: 1e-4 + rtol: 1e-5 + compare_mode: "mean" + action_name: "loose_equal" +``` diff --git a/docs/CheckViTPose.md b/docs/CheckViTPose.md index 173ae7e..a02a296 100644 --- a/docs/CheckViTPose.md +++ b/docs/CheckViTPose.md @@ -2,7 +2,7 @@ - [0. 准备工作](#0-准备工作) - [1. 单 step 的前向对齐](#1-单-step-的前向对齐) - [关于输入数据](#关于输入数据) - - [关于黑白名单和 layer\_map](#关于黑白名单和-layer_map) + - [关于黑白名单和 layer_map](#关于黑白名单和-layer_map) - [关于参数设置](#关于参数设置) - [示例代码](#示例代码) - [2. 损失函数精度验证](#2-损失函数精度验证) @@ -13,12 +13,10 @@ - [关于参数设置](#关于参数设置-2) - [示例代码](#示例代码-2) - [4. 出现 diff 时进行精确定位](#4-出现-diff-时进行精确定位) - - [工具逻辑说明:关于 single\_step 模式](#工具逻辑说明关于-single_step-模式) + - [工具逻辑说明:关于 single_step 模式](#工具逻辑说明关于-single_step-模式) - [关于参数设置](#关于参数设置-3) - [示例代码](#示例代码-3) - - # 使用PaDiff工具对齐ViTPose流程示例 **阅读本文档前,建议先阅读[Tutorial](Tutorial.md),对工具的使用方法有一个基本的认识** @@ -27,18 +25,16 @@ 在使用PaDiff工具前,需要自行编写部分代码,包括: -1. 加载(或定义) paddle 模型以及 torch 模型 -2. 准备 dataloader 逻辑(若必要) +1. 加载(或定义) paddle 模型以及 torch 模型 +2. 准备 dataloader 逻辑(若必要) 完成准备后,使用工具进行对齐的步骤是基本固定的: -1. 得到 paddle 以及 torch 模型 -2. 取得模型的输入数据 -3. 生成 layer_map 结构 -4. 调用 assign_weight 初始化模型权重 -5. 调用 auto_diff 接口进行对齐 - - +1. 得到 paddle 以及 torch 模型 +2. 取得模型的输入数据 +3. 生成 layer_map 结构 +4. 调用 assign_weight 初始化模型权重 +5. 调用 auto_diff 接口进行对齐 以下是加载ViTPose模型的代码示例 @@ -206,8 +202,6 @@ def build_paddle_data_pipeline(): return dataset,loader ``` - - ## 1. 单 step 的前向对齐 单 step 的模型前向对齐检查中,不更新权重,对每一组输入数据进行一次独立的对齐检查。对应代码示例见下方的代码块。 @@ -227,14 +221,13 @@ def build_paddle_data_pipeline(): 除了必需的输入之外,可以注意以下几个参数的设置: -1. auto_init - - 在下方示例代码中,需要对不同的数据进行单 step 对齐检查,不需要重复进行权重初始化行为。因此设置为 False +1. auto_init -2. options + 在下方示例代码中,需要对不同的数据进行单 step 对齐检查,不需要重复进行权重初始化行为。因此设置为 False - - single_step 选项:在模型对齐的开始,建议关闭 single_step ,确认模型存在 diff 时再打开它来帮助定位具体的 diff 位置 - - diff_phase 选项:由于目前的任务是单 step 的前向对齐检查,设置 diff_phase 选项为 "forward",可以只定位模型的前向逻辑,跳过backward 部分(不会更新模型权重) +2. options + - single_step 选项:在模型对齐的开始,建议关闭 single_step ,确认模型存在 diff 时再打开它来帮助定位具体的 diff 位置 + - diff_phase 选项:由于目前的任务是单 step 的前向对齐检查,设置 diff_phase 选项为 "forward",可以只定位模型的前向逻辑,跳过backward 部分(不会更新模型权重) ### 示例代码 @@ -292,8 +285,6 @@ def test_forward(): break ``` - - ## 2. 损失函数精度验证 在确认模型功能初步对齐后,可以添加指定的损失函数参与对齐,以保证损失函数的精度。工具将检查损失函数的输出是否对齐,但不会检查损失函数的内部逻辑,当损失函数的输出精度不对齐时,将打印相应的 log 信息进行提示。 @@ -373,8 +364,6 @@ def test_forward(): break ``` - - ## 3. 带 optimizer 的精度对齐 多 step 的对齐检查意味着需要在每一个 step 间更新模型权重,然后进行下一个step 的对齐,相对于单 step 的模型对齐检查更复杂 。auto_diff 接口支持传入指定的 optimizer 参与对齐,传入的 optimizer 可以是一个 optimizer 实例,也可以是一个 lambda 函数。 @@ -385,26 +374,26 @@ optimizer 参数的具体的使用方法详见 [Tutorial](Tutorial.md),以下 在检查到输入参数包含了 optimizer 后,auto_diff 接口将按照以下逻辑进行相关检查。 -1. 运行模型前反向计算,对比计算过程 -2. 在运行完毕模型的 backward 部分后,对比检查模型记录的梯度大小 -3. 调用传入的 optimizer ,更新模型权重 -4. 更新权重后,检查模型权重间的数值精度误差 +1. 运行模型前反向计算,对比计算过程 +2. 在运行完毕模型的 backward 部分后,对比检查模型记录的梯度大小 +3. 调用传入的 optimizer ,更新模型权重 +4. 更新权重后,检查模型权重间的数值精度误差 因此,在调用 optimizer 后出现的模型权重差异可以确定为 optimizer 精度问题,在 log 信息中将给出相应的提示 ### 关于参数设置 -1. auto_init +1. auto_init - 由于在多 step 对齐检查中,需要更新权重,因此 auto_init 必须设置为 False,否则在每一个 step 前都会触发权重的拷贝。 + 由于在多 step 对齐检查中,需要更新权重,因此 auto_init 必须设置为 False,否则在每一个 step 前都会触发权重的拷贝。 -2. optimizer +2. optimizer - 进行多 step 的对齐检查时必须显式地提供 optimizer ,否则工具将不知道如何更新模型权重。关于 optimizer 的设置和使用。 + 进行多 step 的对齐检查时必须显式地提供 optimizer ,否则工具将不知道如何更新模型权重。关于 optimizer 的设置和使用。 ### 示例代码 -1. 使用 dataloader 进行多 step 的对齐检查(每一个step使用不同的input data) +1. 使用 dataloader 进行多 step 的对齐检查(每一个step使用不同的input data) ```py from padiff import auto_diff, assign_weight, LayerMap() @@ -461,7 +450,7 @@ def test_forward(): break ``` -2. 使用同一组输入进行多 step 对齐检查 +2. 使用同一组输入进行多 step 对齐检查 ```py from padiff import auto_diff, assign_weight, LayerMap() @@ -506,16 +495,14 @@ def test_forward(): ) ``` - - ## 4. 出现 diff 时进行精确定位 在对齐检查的过程中可能出现这种情况:auto_diff 接口发现了精度 diff,但 log 信息中定位到的位置却是 Linear 等常见的 API,检查后未发现 Linear 存在 diff。 这可能是由于精度误差累积引起的,可以通过使用 single_step 对齐模式进行精度对齐定位,具体方法是: -1. 在接口参数中打开 single_step 模式的开关 -2. 进行对齐检查的同时不断调整 atol, rtol 等参数,以找出 diff 出现的具体位置(开启 single_step 模式后,diff 数值的数量级可能下降) +1. 在接口参数中打开 single_step 模式的开关 +2. 进行对齐检查的同时不断调整 atol, rtol 等参数,以找出 diff 出现的具体位置(开启 single_step 模式后,diff 数值的数量级可能下降) ### 工具逻辑说明:关于 single_step 模式 @@ -523,7 +510,7 @@ def test_forward(): ### 关于参数设置 -使用 single_step 模式,只需要将 options 参数中的 "single_step" 选项设置为 True,此时,"diff_phase" 选项将控制single_step 的行为,它能够被指定为 "forward", "backward", "both" 3种可能 +使用 single_step 模式,只需要将 options 参数中的 "single_step" 选项设置为 True,此时,"diff_phase" 选项将控制single_step 的行为,它能够被指定为 "forward", "backward", "both" 3种可能 注意:当需要进行 "backward" 的 single_step 对齐时,auto_diff 会额外运行一次前向网络。 diff --git a/docs/FAQs.md b/docs/FAQs.md index 8d20c82..79b57cc 100644 --- a/docs/FAQs.md +++ b/docs/FAQs.md @@ -8,10 +8,10 @@ - [如何进行非fp32模型的对齐](#如何进行非fp32模型的对齐) - [由padiff引发的import问题](#由padiff引发的import问题) - # FAQs ## 自动跳过 wrap layer + 在老版本的 PaDiff 工具中,会自动过滤 wrap layer (即没有parameter的layer),但这种行为可能会导致用户的困惑,而且可能会改变记录的模型结构,因此在 develop 版本中,我们取消了这种过滤行为,这可能导致老版本能直接对齐的模型在 develop 版本下无法直接对齐。 但在 torch 和 paddle 模型对齐时,不过滤可能需要用户自己手动添加很多 ignore 语句,为了方便与 torch 模型的对齐,可以通过使用环境变量 "PADIFF_SIKP_WRAP_LAYER" 来开启过滤行为(默认是关闭的)。 @@ -21,6 +21,7 @@ ## assign weight 的错误 **assign weight 做了什么** + 1. assign weight 首先尝试寻找对应的 layer/module 2. assign weight 尝试在对应的 layer/module 中,同步遍历它们的 parameters 3. assign weight 会略过不包含 parameter 的 layer/module (也会略过 LayerMap 指定的部分) @@ -36,11 +37,9 @@ padiff允许模型使用 buffer,embedding,但它们的使用必须对应。 ![Pasted Graphic 5](https://user-images.githubusercontent.com/79986504/227197672-1ecc6b74-d796-447f-8508-2bcf6cbb6bc6.png) - > 解决方案: > > 1. 修改模型代码,使用对应的权重实现方案 -> > 2. 使用 layer_map 指定顶层模型的一一对应关系,并自定义初始化函数,详见链接 [自定义初始化函数的方法](SpecialInit.md) **因模型权重定义顺序导致的错误** @@ -65,37 +64,28 @@ padiff允许模型使用 buffer,embedding,但它们的使用必须对应。 2. 当前版本下,special init 对 base_model 和 raw_model 有顺序要求,其语义为将 base_model 的权重拷贝到 raw_model,若传入模型顺序相反,可能导致无法找到对应的 special init 初始化函数 - - ## 出现 CUBLAS ERROR 请确认当前使用的 cuda 版本在当前 paddle 支持的 cuda 版本之内,(cuda 11.4 不在支持范围内) 详见 https://www.paddlepaddle.org.cn/ - ## 使用包含随机性的op padiff 无法对齐包含随机性 op 的模型,例如 dropout。 测试时需要自行注释相关代码,使用 padiff 的 api 级别对齐检查可以帮助定位相关 api 的位置。 - - ## 显存溢出 使用 auto_diff 接口出现显存溢出时,请尝试减小 batchsize,或尝试使用离线对齐方案。 在使用离线对齐方案时出现显存溢出,请检查模型原本所需的显存资源量。 - - ## 如何设置模型device auto_diff 工具的工作与 device 无关,如果需要进行 cpu/gpu 的对齐,只需要传入device 为 cpu/gpu 的模型以及输入即可 -- 在调用 paddle 模型构造函数以及 input data 初始化前,使用 `paddle.set_device(xxx)` -- 在构造 torch 模型后,使用 `torch_module = torch_module.to(xxx)`, `torch_input = torch_input.to(xxx)` - - +- 在调用 paddle 模型构造函数以及 input data 初始化前,使用 `paddle.set_device(xxx)` +- 在构造 torch 模型后,使用 `torch_module = torch_module.to(xxx)`, `torch_input = torch_input.to(xxx)` ## 如何进行非fp32模型的对齐 @@ -103,11 +93,9 @@ auto_diff 工具的工作与 device 无关,如果需要进行 cpu/gpu 的对 在使用离线对齐解决方案时,可以正常使用 amp guard。 - - ## 由padiff引发的import问题 此类问题可能是 padiff 开启 api 级别的对齐检查引起的 -1. 尝试将 padiff 的 import 后置 -2. 若仍不能避免错误,可以先关闭 api 级别对齐检查再尝试(默认是关闭的) +1. 尝试将 padiff 的 import 后置 +2. 若仍不能避免错误,可以先关闭 api 级别对齐检查再尝试(默认是关闭的) diff --git a/docs/Interfaces.md b/docs/Interfaces.md index 7f7df96..c1eec81 100644 --- a/docs/Interfaces.md +++ b/docs/Interfaces.md @@ -2,45 +2,42 @@ - [一、单模型运行及文件dump](#一单模型运行及文件dump) - [关于可dump的信息](#关于可dump的信息) - [设置dump路径](#设置dump路径) - - [创建proxy\_model](#创建proxy_model) + - [创建proxy_model](#创建proxy_model) - [运行前反向逻辑](#运行前反向逻辑) - - [try\_dump接口](#try_dump接口) + - [try_dump接口](#try_dump接口) - [自由度更高的dump接口](#自由度更高的dump接口) - [设置黑白名单](#设置黑白名单) - - [设置 layer\_map](#设置-layer_map) + - [设置 layer_map](#设置-layer_map) - [调用原模型的接口](#调用原模型的接口) - [二、离线对齐工具](#二离线对齐工具) - - [check\_report的报错信息](#check_report的报错信息) + - [check_report的报错信息](#check_report的报错信息) - [其余接口的报错信息](#其余接口的报错信息) - [三、`auto_diff` 接口参数](#三auto_diff-接口参数) - [接口函数签名](#接口函数签名) - [必要参数](#必要参数) - - [黑白名单和layer\_map](#黑白名单和layer_map) + - [黑白名单和layer_map](#黑白名单和layer_map) - [可选参数](#可选参数) - [kwargs 可选项](#kwargs-可选项) - [四、`assign_weight` 接口参数](#四assign_weight-接口参数) - [函数接口签名](#函数接口签名) - [参数说明](#参数说明) - - - # Interfaces ## 一、单模型运行及文件dump ### 关于可dump的信息 -1. Report +1. Report - 调用create_model接口后,生成的ProxyModel绑定着一份Report,当模型执行forward或backward逻辑时,会将相关的信息记录到ProxyModel绑定的Report中,在运行完毕后可以选择进行dump操作。Report会随着运行不断累积,直到调用 clear_report 或 try_dump 接口来实现清空。 + 调用create_model接口后,生成的ProxyModel绑定着一份Report,当模型执行forward或backward逻辑时,会将相关的信息记录到ProxyModel绑定的Report中,在运行完毕后可以选择进行dump操作。Report会随着运行不断累积,直到调用 clear_report 或 try_dump 接口来实现清空。 -2. Parameters +2. Parameters - 调用create_model接口后,能够dump ProxyModel 的 parameters,包括权重值和梯度值。在dump parameter 相关数据时,会根据ProxyModel所记录的黑白名单信息进行筛选,并按照模型结构进行保存。 + 调用create_model接口后,能够dump ProxyModel 的 parameters,包括权重值和梯度值。在dump parameter 相关数据时,会根据ProxyModel所记录的黑白名单信息进行筛选,并按照模型结构进行保存。 -3. 文件结构 - 每一次dump都会生成一个json文件,json文件中记录各类meta信息以及树状结构,tensor信息被保存为npy文件,其文件路径记录在json文件中。 +3. 文件结构 + 每一次dump都会生成一个json文件,json文件中记录各类meta信息以及树状结构,tensor信息被保存为npy文件,其文件路径记录在json文件中。 ``` |-- root_path @@ -62,6 +59,7 @@ ``` ### 设置dump路径 + 通过调用`set_dump_root_path(path)`接口来设置PaDiff进行dump的root_path信息。 ### 创建proxy_model @@ -83,7 +81,6 @@ model = create_model(SimpleLayer(), name="Simple", dump_freq=2) # name, dump_ `dump_freq`用来指定保存中间变量的间隔(默认值为1,即每步都保存),需要保存的那一步会运行的非常缓慢(存在大量的cpu<--->gpu之间的copy)。 - ### 运行前反向逻辑 损失函数以及优化器的使用与ProxyModel无关 @@ -97,8 +94,6 @@ model.backward(loss) # 通过 model.backward 来执行反向 optimizer.step() ``` - - ### try_dump接口 - try_dump有一个内置的计数,当且仅当内部计数器可以整除create_model时传入的dump_freq时,才会真正地触发dump @@ -116,8 +111,6 @@ for data in dataloader(): model.try_dump(dir_path) # dir_path 可选项,try_dump 提供默认值 ``` - - ### 自由度更高的dump接口 try_dump 旨在提供一个方便快速测试的接口。事实上也可以使用下面的其他接口来实现灵活度更高的dump方案。 @@ -140,8 +133,6 @@ for idx, data in enumerate(dataloader()): model.clear_report() # 需要手动删除 report,否则将不断累积 ``` - - ### 设置黑白名单 黑白名单将影响模型dump哪些部分的数据 @@ -150,9 +141,9 @@ for idx, data in enumerate(dataloader()): - `update_black_list`与`update_white_list`需要传递一个实例列表来指定特定的实例,只有在列表中的实例会被加入到名单,与列表中实例属于同一类的其他实例不会被自动加入到名单中 - `update_black_list_with_class`与`update_white_list_with_class`需要传递一个类名,所有属于该类的实例会被自动加入名单中 - 设置黑白名单的接口需要提供 "mode" 参数: - - mode = "self",仅将目标加入黑名单/白名单 - - mode = "sublayers",仅将目标的 sublayer 加入黑名单/白名单 - - mode = "all",将目标及其 sublayer 加入黑名单/白名单 + - mode = "self",仅将目标加入黑名单/白名单 + - mode = "sublayers",仅将目标的 sublayer 加入黑名单/白名单 + - mode = "all",将目标及其 sublayer 加入黑名单/白名单 - 对于大模型,如果不设置白名单,则会dump模型所有的中间变量与所有的参数以及相应的梯度,对于内存与磁盘空间的消耗非常的大。建议在大模型精度对齐的过程中进行白名单的配置。 ```py @@ -164,17 +155,15 @@ model.update_black_list_with_class(paddle.nn.Linear, "sublayers") model.update_white_list_with_class(MultiHeadAttention, "sublayers") ``` - - ### 设置 layer_map -- 该功能用于指定两个模型中的某些组件的对应关系,它的主要作用是: - 1. 对齐顶层接口对齐,但内部实现不同的组件(使用黑名单可以达到同样效果) - 2. 调整模型对齐的顺序(例如有两个结构上平行的sublayer,但它们实际的调用顺序不一致,这不影响逻辑但影响对齐,可以使用 layer_map 功能进行对应) - 3. 在需要使用 padiff 工具初始化模型权重时,配合自定义特殊初始化逻辑使用(见[特殊初始化功能](SpecialInit.md)) -- 设置layer_map的同时会自动调用 `model.update_black_list(layer_map, "sublayers")` -- 指定layer_map后,在离线对齐时,会根据layer_map的顺序调整对齐顺序 -- 可以使用 auto_layer_map 接口进行自动搜索 (需传入 "base" 或 "raw" 指定对齐时的定位) +- 该功能用于指定两个模型中的某些组件的对应关系,它的主要作用是: + 1. 对齐顶层接口对齐,但内部实现不同的组件(使用黑名单可以达到同样效果) + 2. 调整模型对齐的顺序(例如有两个结构上平行的sublayer,但它们实际的调用顺序不一致,这不影响逻辑但影响对齐,可以使用 layer_map 功能进行对应) + 3. 在需要使用 padiff 工具初始化模型权重时,配合自定义特殊初始化逻辑使用(见[特殊初始化功能](SpecialInit.md)) +- 设置layer_map的同时会自动调用 `model.update_black_list(layer_map, "sublayers")` +- 指定layer_map后,在离线对齐时,会根据layer_map的顺序调整对齐顺序 +- 可以使用 auto_layer_map 接口进行自动搜索 (需传入 "base" 或 "raw" 指定对齐时的定位) ```py model0 = create_model(SimpleLayer0(), name="Simple0") @@ -187,8 +176,6 @@ model0.auto_layer_map("base") model1.auto_layer_map("raw") ``` - - ### 调用原模型的接口 ProxyModel 的 model 成员即为原模型。若需要调用源模型的`[named_]parameters()`,`[named_]children()`,`[named_]sublayers()`等方法时,请通过`model.model.xxx()`方式进行,不要对create_model返回的model进行方法调用。 @@ -198,26 +185,24 @@ model = create_model(SimpleLayer(), name="Simple") model.model.XXX ``` - - ## 二、离线对齐工具 为不同的dump接口提供了不同的离线对齐工具。若提供的路径下所有的文件均以`step_`开头,则对齐工具会自动遍历检测所有的step文件夹,否则只检测当前文件夹。若当前检测文件夹(可能是用户提供的路径,也可能是扩展了`step_`信息的路径)下的所有文件均以`rank_`开头,则对齐工具会自动遍历检测所有的rank文件夹,否则只检测当前文件夹: -- check_report -- check_params -- check_weights -- check_grads +- check_report +- check_params +- check_weights +- check_grads 离线对齐工具的接口都是一致的,以check_report 为例,函数签名为: `check_report(report_path_0, report_path_1, cfg=None)` -- report_path_0、report_path_1:待对齐的文件路径,这个路径与调用dump时的路径保持一致即可(即json文件所在文件夹的路径) -- cfg:一个字典,记录了用于对齐的参数 - - "atol":绝对精度误差上限,默认值为 `0` (与numpy.testing.assert_allclose的atol默认值相同) - - "rtol":相对精度误差上限,默认值为 `1e-7`(与numpy.testing.assert_allclose的rtol默认值相同) - - "compare_mode":比较模式设定,可选 `"mean"|"strict"` 默认为 `"mean"`。 `"mean"` 表示使用Tensor间误差的均值作为对齐标准; `"strict"` 表示对Tensor进行逐数据(Elementwise)的对齐检查。 +- report_path_0、report_path_1:待对齐的文件路径,这个路径与调用dump时的路径保持一致即可(即json文件所在文件夹的路径) +- cfg:一个字典,记录了用于对齐的参数 + - "atol":绝对精度误差上限,默认值为 `0` (与numpy.testing.assert_allclose的atol默认值相同) + - "rtol":相对精度误差上限,默认值为 `1e-7`(与numpy.testing.assert_allclose的rtol默认值相同) + - "compare_mode":比较模式设定,可选 `"mean"|"strict"` 默认为 `"mean"`。 `"mean"` 表示使用Tensor间误差的均值作为对齐标准; `"strict"` 表示对Tensor进行逐数据(Elementwise)的对齐检查。 ### check_report的报错信息 @@ -274,6 +259,7 @@ PipelineParallel ``` log中的信息为: + ```text ========================= weights value is different. @@ -298,19 +284,20 @@ Max relative difference: 1.541 ## 三、`auto_diff` 接口参数 ### 接口函数签名 + `auto_diff(base_model, raw_model, inputs, loss_fns=None, optimizers=None, **kwargs)` 用于对齐模型 ### 必要参数 - - `base_model` :作为对齐基准的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 - - 在模型初始化时,将 base_model 的权重拷贝至 raw_model。 - - 在 single_step 模式下,将 base_model 的输入同步作为 raw_model 的输入。 +- `base_model` :作为对齐基准的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 + - 在模型初始化时,将 base_model 的权重拷贝至 raw_model。 + - 在 single_step 模式下,将 base_model 的输入同步作为 raw_model 的输入。 - - `raw_model` :待对齐的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 +- `raw_model` :待对齐的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 - - `inputs` :样例数据。传入结构为 (base_model_inputs, raw_model_inputs) 的 list/tuple,其中 base_model_inputs 和 raw_model_inputs 是 dict 类型,最终以 `model(**model_inputs)` 的形式进行传参。 +- `inputs` :样例数据。传入结构为 (base_model_inputs, raw_model_inputs) 的 list/tuple,其中 base_model_inputs 和 raw_model_inputs 是 dict 类型,最终以 `model(**model_inputs)` 的形式进行传参。 ### 黑白名单和layer_map @@ -320,32 +307,30 @@ Max relative difference: 1.541 ### 可选参数 - - `loss_fns` :损失函数。传入结构为 (base_model_loss, raw_model_loss) 的 list/tuple。 要求传入的 loss function 只接受一个参数。 - - - `optimizers` :优化器。传入结构为 (base_model_opt, raw_model_opt) 的 list/tuple。由 paddle/torch 的优化器或 lambda 函数组成,当传入 lambda 函数,它需要同时完成 step 和clear grad的 操作。 +- `loss_fns` :损失函数。传入结构为 (base_model_loss, raw_model_loss) 的 list/tuple。 要求传入的 loss function 只接受一个参数。 +- `optimizers` :优化器。传入结构为 (base_model_opt, raw_model_opt) 的 list/tuple。由 paddle/torch 的优化器或 lambda 函数组成,当传入 lambda 函数,它需要同时完成 step 和clear grad的 操作。 ### kwargs 可选项 - - `atol` : 绝对精度误差上限,默认值为 `0` - - - `rtol` : 相对精度误差上限,默认值为 `1e-7` +- `atol` : 绝对精度误差上限,默认值为 `0` - - `auto_init` : 是否使用 base_model 的权重初始化 raw_model,默认为 `True` +- `rtol` : 相对精度误差上限,默认值为 `1e-7` - - `compare_mode` : `"mean"|"strict"` 默认为 `"mean"`。 `"mean"` 表示使用Tensor间误差的均值作为对齐标准; `"strict"` 表示对Tensor进行逐数据(Elementwise)的对齐检查。 +- `auto_init` : 是否使用 base_model 的权重初始化 raw_model,默认为 `True` - - `diff_phase` : `"both"|"forward"|"backward"` 默认为 `"both"`。设置为 `"both"` 时,工具将比较前反向的 diff;当设置为 `"forward"` 时,仅比较前向 diff,且会跳过模型的 backward 计算过程。"backward" 仅在使用 single_step 时有效。 +- `compare_mode` : `"mean"|"strict"` 默认为 `"mean"`。 `"mean"` 表示使用Tensor间误差的均值作为对齐标准; `"strict"` 表示对Tensor进行逐数据(Elementwise)的对齐检查。 - - `single_step` : `True|False` 默认为 `False`。设置为 `True` 时开启单步对齐模式,forward 过程中每一个 step 都会同步模型的输入,可以避免层间误差累积。 - - > 注: - > - > single_step 模式下,对齐检查的逻辑会随着 diff_phase 属性的变化而不同。如果需要同时用 single_step 对齐前反向,则 padiff 将会运行模型两次,并分别进行前向和反向的 single_step 对齐检查 (single step 模式下运行模型的 forward 无法正常训练)。 +- `diff_phase` : `"both"|"forward"|"backward"` 默认为 `"both"`。设置为 `"both"` 时,工具将比较前反向的 diff;当设置为 `"forward"` 时,仅比较前向 diff,且会跳过模型的 backward 计算过程。"backward" 仅在使用 single_step 时有效。 +- `single_step` : `True|False` 默认为 `False`。设置为 `True` 时开启单步对齐模式,forward 过程中每一个 step 都会同步模型的输入,可以避免层间误差累积。 +> 注: +> +> single_step 模式下,对齐检查的逻辑会随着 diff_phase 属性的变化而不同。如果需要同时用 single_step 对齐前反向,则 padiff 将会运行模型两次,并分别进行前向和反向的 single_step 对齐检查 (single step 模式下运行模型的 forward 无法正常训练)。 使用代码示例: + ```py from padiff import auto_diff import torch @@ -377,24 +362,21 @@ inp = ({'x': torch.as_tensor(inp) }, auto_diff(module, layer, inp, atol=1e-4, rtol=0, auto_init=True, compare_mode='strict', single_step=False) ``` - - ## 四、`assign_weight` 接口参数 ### 函数接口签名 + `assign_weight(base_model, raw_model)` 将 base_model 模型权重复制到 raw_model 模型中 ### 参数说明 -- `base_model` :基准权重值,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 -- `raw_model` :待初始化的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 - - - +- `base_model` :基准权重值,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 +- `raw_model` :待初始化的模型,期望为ProxyModel。若传入原生 paddle/torch 模型,则进行自动转换,并且默认不使用黑白名单以及 layer_map 机制 使用代码示例: + ```py from padiff import assign_weight import torch diff --git a/docs/README.md b/docs/README.md index 023880a..4c7cc99 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ ## Guides -- [仓库README](../README.md) -- [使用教程](Tutorial.md) -- [对齐ViTPose流程](CheckViTPose.md) -- [接口信息](Interfaces.md) -- [Special init 机制](SpecialInit.md) +- [仓库README](../README.md) +- [使用教程](Tutorial.md) +- [对齐ViTPose流程](CheckViTPose.md) +- [接口信息](Interfaces.md) +- [Special init 机制](SpecialInit.md) diff --git a/docs/SingleStep.md b/docs/SingleStep.md index 48ee1cb..15d7e5d 100644 --- a/docs/SingleStep.md +++ b/docs/SingleStep.md @@ -1,6 +1,5 @@ - [SingleStep 的工作原理](#singlestep-的工作原理) - # SingleStep 的工作原理 在 auto_diff 接口中传入 `single_step=True` 即可开启单步对齐功能,其原理如下图所示。 diff --git a/docs/SpecialInit.md b/docs/SpecialInit.md index bb1a3f4..e043fec 100644 --- a/docs/SpecialInit.md +++ b/docs/SpecialInit.md @@ -3,20 +3,18 @@ - [为什么需要 SpecialInit 机制](#为什么需要-specialinit-机制) - [什么时候触发 SpecialInit 机制](#什么时候触发-specialinit-机制) - [设置自定义初始化逻辑的完整流程示例](#设置自定义初始化逻辑的完整流程示例) -- [设置 layer\_map 的方法](#设置-layer_map-的方法) - - [使用 set\_layer\_map 手动指定](#使用-set_layer_map-手动指定) - - [使用 auto\_layer\_map 自动指定](#使用-auto_layer_map-自动指定) +- [设置 layer_map 的方法](#设置-layer_map-的方法) + - [使用 set_layer_map 手动指定](#使用-set_layer_map-手动指定) + - [使用 auto_layer_map 自动指定](#使用-auto_layer_map-自动指定) - [自定义模型初始化函数](#自定义模型初始化函数) -- [使用 add\_special\_init 接口注册自定义初始化函数](#使用-add_special_init-接口注册自定义初始化函数) +- [使用 add_special_init 接口注册自定义初始化函数](#使用-add_special_init-接口注册自定义初始化函数) - [如何向本 repo 贡献模型初始化函数](#如何向本-repo-贡献模型初始化函数) - ## 已支持Special Init的组件 -- MultiHeadAttention -- LSTM -- BatchNorm2D - +- MultiHeadAttention +- LSTM +- BatchNorm2D ## 概述 @@ -31,13 +29,12 @@ ### 设置自定义初始化逻辑的完整流程示例 自定义模型初始化逻辑的过程主要为: + 1. 编写模型初始化函数 2. 使用 add_special_init 接口注册函数 3. 通过设置 layer_map 触发并使用模型初始化函数 4. 向本 repo 贡献你的初始化函数 (与前面的步骤无关) - - ## 设置 layer_map 的方法 ### 使用 set_layer_map 手动指定 @@ -53,7 +50,6 @@ model0.set_layer_map([model0.model.linear1, model0.model.linear2]) model1.set_layer_map([model1.model.linear1, model1.model.linear2]) ``` - ### 使用 auto_layer_map 自动指定 使用 auto_layer_map 能搜索当前已经支持 SpecialInit 的组件 @@ -104,6 +100,7 @@ add_special_init 共有5个输入,前两个标明 base_model 的框架名和 > 模型名在不同框架下可能有所差异,例如下面代码中,BatchNorm2D 在 paddle 和 torch 中就有不同的名字 例子: + ```py from padiff import add_special_init @@ -123,13 +120,13 @@ add_special_init("torch", "BatchNorm2d", "paddle", "BatchNorm2D", init_BatchNorm **对 Paddle 框架提供的Layer组件,如果存在与 Torch 提供的组件不对齐,可以将相应的初始化函数提供给本 Repo** -1. 找到 special_init 文件夹,并新建你的文件 +1. 找到 special_init 文件夹,并新建你的文件 该文件夹位于 `PaDiff/padiff/weight_init/special_init`,请在该文件夹下新建文件 `init_XXXXX.py` (必须以 `init_` 开头) -2. 在新建的文件中编写初始化函数 +2. 在新建的文件中编写初始化函数 -3. 使用 register 装饰器装饰初始化函数 +3. 使用 register 装饰器装饰初始化函数 register 装饰器的参数与 add_special_init 接口的前四个参数一致 @@ -152,6 +149,6 @@ def init_BatchNorm2D(module, layer): layer.set_state_dict(param_dict) ``` -4. 提交PR +4. 提交PR 完成上面的文件编写后,可以联系 repo 管理员 review 并合入,提交后 padiff 工具就能够支持对应模型的初始化逻辑,无需重复编写。 diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 3c9f85d..4ab7d90 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -1,4 +1,4 @@ -- [auto\_diff 接口 Tutorial](#auto_diff-接口-tutorial) +- [auto_diff 接口 Tutorial](#auto_diff-接口-tutorial) - [一、 使用方法](#一-使用方法) - [二、阅读输出信息](#二阅读输出信息) - [2.1 正确对齐时的输出信息](#21-正确对齐时的输出信息) @@ -8,11 +8,11 @@ - [三、使用loss \& optimizer](#三使用loss--optimizer) - [3.1 使用loss](#31-使用loss) - [3.2 使用optimizer](#32-使用optimizer) - - [四、使用assign\_weight](#四使用assign_weight) + - [四、使用assign_weight](#四使用assign_weight) - [全局变量开关](#全局变量开关) - - [API 级别的对齐检查](#api-级别的对齐检查) - - [略过 wrap\_layer](#略过-wrap_layer) - - [在前反向对齐时,打印子模型python路径](#在前反向对齐时打印子模型python路径) + - [API 级别的对齐检查](#api-级别的对齐检查) + - [略过 wrap_layer](#略过-wrap_layer) + - [在前反向对齐时,打印子模型python路径](#在前反向对齐时打印子模型python路径) # auto_diff 接口 Tutorial @@ -22,13 +22,13 @@ 使用 `padiff` 进行模型对齐检查有几个基本的步骤: -1. 分别构造两个待对齐的 paddle 或 torch 模型 -2. 分别构造两个模型的输入数据 -3. 调用 `auto_diff` API 接口 +1. 分别构造两个待对齐的 paddle 或 torch 模型 +2. 分别构造两个模型的输入数据 +3. 调用 `auto_diff` API 接口 以下是一段使用 padiff 工具进行对齐的完整代码 (以对齐 paddle 模型和 torch 模型为例) -> 注意:在模型定义时,需要将forward中所使用的子模型在 `__init__` 函数中定义,并保证其中的子模型定义顺序一致**,具体可见下方示例代码 +> 注意:在模型定义时,需要将forward中所使用的子模型在 `__init__` 函数中定义,并保证其中的子模型定义顺序一致\*\*,具体可见下方示例代码 ```py from padiff import auto_diff @@ -84,22 +84,18 @@ inp = ({"x": torch.as_tensor(inp)}, auto_diff(module, layer, inp, atol=1e-4, compare_mode="strict", single_step=False) ``` - - ## 二、阅读输出信息 padiff 的工作可以分为几个阶段,在发生错误时,需要首先判断在哪个阶段发生了错误 -1. 权重拷贝阶段(当设置参数 `auto_weights` 为 `True` 时) -2. 模型前反向对齐阶段 -3. 模型权重&梯度对齐阶段 +1. 权重拷贝阶段(当设置参数 `auto_weights` 为 `True` 时) +2. 模型前反向对齐阶段 +3. 模型权重&梯度对齐阶段 当 padiff 进行多个 step 的对齐检查时,以上2、3阶段循环执行 下面介绍正确对齐,以及在不同阶段产生错误时的输出信息。 - - ### 2.1 正确对齐时的输出信息 ```bash @@ -122,13 +118,12 @@ padiff 的工作可以分为几个阶段,在发生错误时,需要首先判 [AutoDiff] SUCCESS !!! ``` - - ### 2.2 模型权重拷贝失败时的报错信息 当看到 `Assign weight Failed` ,说明权重拷贝出现了问题,并在下文中附上具体的错误信息 -- 在拷贝权重过程中,没有 parameter,或被 LayerMap 指定的 layer/module, 会被标注上 (skip) -- 可以通过设置环境变量 `export PADIFF_PATH_LOG=ON` 在 log 信息中添加 layer/module 的具体路径 + +- 在拷贝权重过程中,没有 parameter,或被 LayerMap 指定的 layer/module, 会被标注上 (skip) +- 可以通过设置环境变量 `export PADIFF_PATH_LOG=ON` 在 log 信息中添加 layer/module 的具体路径 ```bash [AutoDiff] Your options: @@ -167,6 +162,7 @@ Hint: ``` 其中打印的 log 信息为: + ``` # /workspace/PaDiff/padiff_log/weight_init_SimpleModule(base_model).log @@ -175,6 +171,7 @@ SimpleModule(base_model) SimpleModule +--- Linear <--- *** HERE *** ``` + ``` # /workspace/PaDiff/padiff_log/weight_init_SimpleLayer(raw_model).log @@ -186,19 +183,18 @@ SimpleLayer(raw_model) 可能的问题有: -1. 子模型/权重定义顺序不对齐 => 修改代码对齐,或使用 `LayerMap` 指定, -2. 子模型的 paddle 与 torch 实现方式不一致(权重等对不齐)=> 使用 `LayerMap` 指定 +1. 子模型/权重定义顺序不对齐 => 修改代码对齐,或使用 `LayerMap` 指定, +2. 子模型的 paddle 与 torch 实现方式不一致(权重等对不齐)=> 使用 `LayerMap` 指定 > 注:LayerMap 的使用方式详见:[LayerMap使用说明](SpecialInit.md) 若不使用 padiff 的权重初始化功能,可以避免此类错误,但在权重与梯度检查时会遇见同样的问题 - ### 2.3 模型前反向对齐失败时的输出信息 -1. 指明 diff 出现的阶段:`Forward Stage` or `Backward Stage`,该信息出现在日志的开头 -2. 打印出现精度 diff 时的比较信息,包括绝对误差和相对误差数值 -3. 打印模型结构,并用括号标注结点类型,用`<--- *** HERE ***`指示出现diff的位置(log将输出到文件中) +1. 指明 diff 出现的阶段:`Forward Stage` or `Backward Stage`,该信息出现在日志的开头 +2. 打印出现精度 diff 时的比较信息,包括绝对误差和相对误差数值 +3. 打印模型结构,并用括号标注结点类型,用`<--- *** HERE ***`指示出现diff的位置(log将输出到文件中) 定位精度误差位置后,可进行验证排查: @@ -283,7 +279,7 @@ SimpleLayer(raw_model) 在日志文件中,将记录出现diff的权重路径以及比较信息(对每一处diff都会记录一组信息),例如: -- 当检查到weight或grad存在diff,可能是反向计算出现问题,也可能是Loss function 或 optimizer出现问题(若传入了loss以及optimizer) +- 当检查到weight或grad存在diff,可能是反向计算出现问题,也可能是Loss function 或 optimizer出现问题(若传入了loss以及optimizer) ``` ========================= @@ -306,8 +302,6 @@ Max relative difference: 0.72396755 y: array(-0.001896, dtype=float32) ``` - - ## 三、使用loss & optimizer ### 3.1 使用loss @@ -316,9 +310,9 @@ Max relative difference: 0.72396755 须知: -1. 传入的 `loss_fn` 是一个可选项,不指定 `loss_fn` 时,将使用 `auto_diff` 内置的一个 `fake loss function` 进行计算,该函数将 output 整体求平均值并返回。 -2. **`loss_fn` 只接受一个输入(即model的output),并输出一个scale tensor**。无法显式传入label,但可以通过 lambda 或者闭包等方法间接实现。 -3. `loss_fn` 也可以是一个 model ,但是 `loss_fn` 内部的逻辑将不会参与对齐检查, padiff 只会检查 `loss_fn` 的输出是否对齐 +1. 传入的 `loss_fn` 是一个可选项,不指定 `loss_fn` 时,将使用 `auto_diff` 内置的一个 `fake loss function` 进行计算,该函数将 output 整体求平均值并返回。 +2. **`loss_fn` 只接受一个输入(即model的output),并输出一个scale tensor**。无法显式传入label,但可以通过 lambda 或者闭包等方法间接实现。 +3. `loss_fn` 也可以是一个 model ,但是 `loss_fn` 内部的逻辑将不会参与对齐检查, padiff 只会检查 `loss_fn` 的输出是否对齐 > **注:** 利用 `partial` 绑定 label 是一种简单的构造 `loss_fn` 的方法,使用时需注意,必须将参数名与参数值进行绑定,否则可能在传参时错位 @@ -360,19 +354,17 @@ auto_diff(module, layer, inp, auto_init=True, atol=1e-4, loss_fn=[ ]) ``` - - ### 3.2 使用optimizer 能够向 padiff 工具传入 `optimizers`,在多 step 对齐下,将使用 `optimizers` 更新模型 须知: -1. `optimizers` 是可选的,若不传入,padiff 并不提供默认的 `optimzers` ,将跳过权重更新的步骤 -2. padiff 不会检查 `optimizers` 内部是否对齐,但是会检查 step 后的 grad 是否对齐 -3. `optimizer` 有两种使用方式: - - 依次传入一组 `paddle.optimizer.Optimizer` 或 `torch.optim.Optimizer` 类型的 optimizers - - 依次传入两个**无输入的 lambda**,分别负责 paddle 模型与 torch 模型的权重更新,可在其中实现自定义操作 +1. `optimizers` 是可选的,若不传入,padiff 并不提供默认的 `optimzers` ,将跳过权重更新的步骤 +2. padiff 不会检查 `optimizers` 内部是否对齐,但是会检查 step 后的 grad 是否对齐 +3. `optimizer` 有两种使用方式: + - 依次传入一组 `paddle.optimizer.Optimizer` 或 `torch.optim.Optimizer` 类型的 optimizers + - 依次传入两个**无输入的 lambda**,分别负责 paddle 模型与 torch 模型的权重更新,可在其中实现自定义操作 ```py class SimpleLayer(paddle.nn.Layer): @@ -399,8 +391,6 @@ auto_diff( ) ``` - - ## 四、使用assign_weight `assign_weight` 用于复制 torch 模型的权重到 paddle 模型,具体接口参数信息见:[接口信息](Interfaces.md),关于权重初始化的高级设置见 [特殊初始化](SpecialInit.md) @@ -409,7 +399,7 @@ auto_diff( 须知: -- 如果 `assign_weight` 失败,则函数的返回值为 `False`(不会抛出异常) +- 如果 `assign_weight` 失败,则函数的返回值为 `False`(不会抛出异常) ```py import os @@ -425,9 +415,6 @@ module = SimpleModule() assign_weight(layer, module) ``` - - - # 全局变量开关 ### API 级别的对齐检查 diff --git a/docs/config_example.yaml b/docs/config_example.yaml new file mode 100644 index 0000000..5d35b7d --- /dev/null +++ b/docs/config_example.yaml @@ -0,0 +1,32 @@ +# --- CLI 部分 --- +# 定义脚本执行相关参数 + +CLI: + pt_cmd: "python torch_project/run.py" # PyTorch 脚本运行命令 + pd_cmd: "python paddle_project/run.py" # PaddlePaddle 脚本运行命令 + pt_model_name: "pt_model" # PyTorch 模型变量名 + pd_model_name: "pd_model" # PaddlePaddle 模型变量名 + pt_optim_name: "pt_optimizer" # (可选) PyTorch 优化器变量名 + pd_optim_name: "pd_optimizer" # (可选) PaddlePaddle 优化器变量名 + log_dir: "./padiff_log" # (可选) 日志目录 + +# --- COMPARE 部分 --- +# 定义结果对比逻辑 +COMPARE: + atol: 1.0e-06 # (可选) 绝对误差 + rtol: 1.0e-06 # (可选) 相对误差 + compare_mode: "mean" # (可选) 对比模式 + action_name: "equal" # (可选) 对比动作 + +# --- PaDiffGuard 部分 --- +# 定义模型对齐行为 +PaDiffGuard: + align_depth: 1 # (可选) 对齐深度 + single_step_mode: "forward" # (可选) 单步模式 + load_init_weights: false # (可选) 加载初始权重 + load_first_inputs: false # (可选) 加载首次输入 + max_calls: 1 # (可选) 最大调用次数 + black_list: ["TorcheFakeLayer", "PaddleFakeLayer"] # (可选) 不参与对齐的层的黑名单 + keys_mapping: # (可选) 参数名映射 key(paddle模型参数名): value(torch模型参数名) + "parm_name_of_paddle_model": "parm_name_of_torch_model" + "model_pd.layers.0.input_layernorm.weight": "model_pt.layers.0.input_layernorm.weight" diff --git a/padiff/abstracts/hooks/guard.py b/padiff/abstracts/hooks/guard.py index 2711494..db34807 100644 --- a/padiff/abstracts/hooks/guard.py +++ b/padiff/abstracts/hooks/guard.py @@ -133,6 +133,11 @@ def tracked_forward(*args, **kwargs): logger.warning("Skipped capturing or loading input: both args and kwargs are empty.") return original_forward(*args, **kwargs) + corrected_args = args + if args and isinstance(args[0], (paddle.nn.Layer, torch.nn.Module)): + logger.debug("Ignoring model instance in args[0] during input capture.") + corrected_args = args[1:] + if load_first_inputs and base_dump_path and framework and not hasattr(report, "_inputs_loaded"): assert framework is not None, "'framework' must be setted if 'load_first_inputs' is True" logger.info("Loading first input from dump") @@ -168,17 +173,17 @@ def serialize(x): return (type(x).__name__, str(x)) serialized = { - "args": [serialize(x) for x in args] if args else [], + "args": [serialize(x) for x in corrected_args] if corrected_args else [], "kwargs": {k: serialize(v) for k, v in kwargs.items()}, } if serialized["args"] or serialized["kwargs"]: report.first_input = serialized report.first_input_captured = True - logger.info(f"Captured full input: args={len(args)}, kwargs={list(kwargs.keys())}") + logger.info(f"Captured full input: args={len(corrected_args)}, kwargs={list(kwargs.keys())}") else: logger.warning("Skipped capturing input: serialized input is empty.") - return original_forward(*args, **kwargs) + return original_forward(*corrected_args, **kwargs) model.forward = tracked_forward model._padiff_input_captured = True @@ -217,7 +222,7 @@ def PaDiffGuard( optimizer=None, name="model", align_depth="inf", - single_step_mode=None, # None, "forward", "backward" + single_step_mode=None, # None, "forward", "backward", "both" load_init_weights=False, load_first_inputs=False, base_dump_path=None, @@ -280,8 +285,11 @@ def PaDiffGuard( yield model - except SystemExit as e: - logger.info("PaDiffGuard: SystemExit received, skipping dump_report.") + except _CallsComplete as e: + pass + + except Exception as e: + logger.error(f"PaDiffGuard: failed! {e}") raise finally: @@ -291,5 +299,5 @@ def PaDiffGuard( if optimizer is None: proxy_model.dump_grads(proxy_model.dump_path) except Exception as e: - logger.error(f"Failed to dump: {e}") + logger.error(f"PaDiffGuard: failed to dump! {e}") sys.exit(0) diff --git a/padiff/ast_injector.py b/padiff/ast_injector.py index b8e44eb..11e9575 100644 --- a/padiff/ast_injector.py +++ b/padiff/ast_injector.py @@ -24,21 +24,21 @@ def __init__( framework: str, model_name="model", mode="base", - alignment_dir=None, **kwargs, ): - self.framework = framework self.base_name = model_name.split(".")[0] # get trainer if trainer.model self.model_name = model_name # model(inputs) self.padiff_model_name = f"model_{framework.lower()}" # "model_paddle" self.proxy_model_name = "proxy_model" # proxy_model = create_model(model) self.mode = mode - if self.mode == "align": - assert alignment_dir is not None, "'alignment_dir' should not be None in align mode." - self.alignment_dir = alignment_dir self.kwargs = kwargs + self.kwargs["framework"] = framework + + if self.mode == "align": + base_dump_path = kwargs.get("base_dump_path", None) + assert base_dump_path is not None, "'base_dump_path' should not be None in align mode." - # black list: calls to these methods will not be injected into PaDiffGuard + # exclude_methods: calls to these methods will not be injected into PaDiffGuard self.exclude_methods = { "to", "train", @@ -60,16 +60,9 @@ def __init__( def visit_Module(self, node): node.body = self.add_imports(node) self.generic_visit(node) - # dump_stmt = self.add_dump_report(node) - # node.body.append(dump_stmt) return node def visit_Assign(self, node): - # # model = SimplePaddle() - # for target in node.targets: - # if isinstance(target, ast.Name) and target.id == self.model_name: - # return self.add_create_model(node) - # with PaDiffGuard(proxy_model): if self.is_model_call(node.value): return self.wrap_with_guard(node) @@ -117,7 +110,6 @@ def add_imports(self, node): for stmt in node.body: if isinstance(stmt, ast.ImportFrom) and stmt.module == "padiff": imported_names = {alias.name for alias in stmt.names} - # required = {"create_model", "PaDiffGuard", "dump_report"} required = {"PaDiffGuard"} if required <= imported_names: has_padiff_import = True @@ -127,64 +119,11 @@ def add_imports(self, node): import_from = ast.ImportFrom( module="padiff", - names=[ - # ast.alias(name="create_model", asname=None), - ast.alias(name="PaDiffGuard", asname=None), - # ast.alias(name="dump_report", asname=None), - ], + names=[ast.alias(name="PaDiffGuard", asname=None)], level=0, ) return [import_from] + node.body - def add_create_model(self, node): - # Temporarily abandoned - # proxy_model = create_model() - assign_proxy = ast.Assign( - targets=[ast.Name(id=self.proxy_model_name, ctx=ast.Store())], - value=ast.Call( - func=ast.Name(id="create_model", ctx=ast.Load()), - args=[ast.Name(id=self.model_name, ctx=ast.Load())], - keywords=[ast.keyword(arg="name", value=ast.Constant(value=self.padiff_model_name))], - ), - ) - - # model._padiff_wrapped = True - mark_wrapped = ast.Assign( - targets=[ - ast.Attribute( - value=ast.Name(id=self.model_name, ctx=ast.Load()), attr="_padiff_wrapped", ctx=ast.Store() - ) - ], - value=ast.Constant(value=True), - ) - - # global _padiff_proxy_model - global_decl = ast.Global(names=["_padiff_proxy_model"]) - - # _padiff_proxy_model = proxy_model - assign_global = ast.Assign( - targets=[ast.Name(id="_padiff_proxy_model", ctx=ast.Store())], - value=ast.Name(id=self.proxy_model_name, ctx=ast.Load()), - ) - - # combine to if not hasattr() - wrapper = ast.If( - test=ast.UnaryOp( - op=ast.Not(), - operand=ast.Call( - func=ast.Name(id="hasattr", ctx=ast.Load()), - args=[ast.Name(id=self.model_name, ctx=ast.Load()), ast.Constant(value="_padiff_wrapped")], - keywords=[], - ), - ), - body=[assign_proxy, mark_wrapped, global_decl, assign_global], - orelse=[], - ) - - ast.copy_location(wrapper, node) - ast.fix_missing_locations(wrapper) - return [node, wrapper] - def wrap_with_guard(self, node): path = self.model_name.split(".") model_node = ast.Name(id=path[0], ctx=ast.Load()) @@ -194,59 +133,23 @@ def wrap_with_guard(self, node): guard_keywords = [] + # name + name_kw = ast.keyword(arg="name", value=ast.Constant(value=self.padiff_model_name)) + guard_keywords.append(name_kw) + # optimizer if "optimizer" in self.kwargs: optim_kw = ast.keyword(arg="optimizer", value=ast.Name(id=self.kwargs["optimizer"], ctx=ast.Load())) guard_keywords.append(optim_kw) - if self.mode == "align": - # load_init_weights - load_weights_kw = ast.keyword(arg="load_init_weights", value=ast.Constant(value=True)) - guard_keywords.append(load_weights_kw) - logger.warning( - "The current injection does not include the 'keys_mapping' parameter of loading init weights. " - "If the model parameter names are inconsistent, please manually modify the injected script " - f"'debug_inject_{self.framework}.py' and pass 'keys_mapping' to 'PaDiffGuard(...)'" - ) - - # load_first_inputs - load_inputs_kw = ast.keyword(arg="load_first_inputs", value=ast.Constant(value=True)) - guard_keywords.append(load_inputs_kw) - - # framework - framework_kw = ast.keyword(arg="framework", value=ast.Constant(value=self.framework)) - guard_keywords.append(framework_kw) - - # align_depth - if "align_depth" in self.kwargs and self.kwargs["align_depth"] != "inf": - align_depth_kw = ast.keyword(arg="align_depth", value=ast.Constant(value=self.kwargs["align_depth"])) - guard_keywords.append(align_depth_kw) - - # single_step_mode - single_step_mode = self.kwargs.get("single_step_mode") - if single_step_mode is not None: - single_step_kw = ast.keyword( - arg="single_step_mode", value=ast.Constant(value=self.kwargs["single_step_mode"]) - ) - guard_keywords.append(single_step_kw) + for key, value in self.kwargs.items(): + if key in ["optimizer"]: + continue - # base_dump_path - if self.mode == "align" or single_step_mode is not None: - base_dump_path_kw = ast.keyword(arg="base_dump_path", value=ast.Constant(value=self.alignment_dir)) - guard_keywords.append(base_dump_path_kw) - - # black_list - if "black_list" in self.kwargs: - black_list_kw = ast.keyword(arg="black_list", value=ast.Constant(value=self.kwargs["black_list"])) - guard_keywords.append(black_list_kw) - - # name - name_kw = ast.keyword(arg="name", value=ast.Constant(value=self.padiff_model_name)) - guard_keywords.append(name_kw) - - # max_calls - max_calls_kw = ast.keyword(arg="max_calls", value=ast.Constant(value=1)) - guard_keywords.append(max_calls_kw) + ast_value = self.safe_ast_value(value) + if ast_value is not None: + keyword = ast.keyword(arg=key, value=ast_value) + guard_keywords.append(keyword) with_stmt = ast.With( items=[ @@ -265,23 +168,23 @@ def wrap_with_guard(self, node): ast.fix_missing_locations(with_stmt) return with_stmt - def add_dump_report(self, node): - # Temporarily abandoned - dump_stmt = ast.Expr( - value=ast.Call( - func=ast.Name(id="dump_report", ctx=ast.Load()), - args=[ - ast.Name(id="_padiff_proxy_model", ctx=ast.Load()), - ast.Attribute( - value=ast.Name(id="_padiff_proxy_model", ctx=ast.Load()), attr="dump_path", ctx=ast.Load() - ), - ], - keywords=[], - ) - ) - ast.copy_location(dump_stmt, node) - ast.fix_missing_locations(dump_stmt) - return dump_stmt + def safe_ast_value(self, py_value): + if isinstance(py_value, bool): + return ast.Constant(value=py_value) + elif isinstance(py_value, (int, float, str)): + return ast.Constant(value=py_value) + elif py_value is None: + return ast.Constant(value=None) + elif isinstance(py_value, list): + elts = [self.safe_ast_value(item) for item in py_value] + return ast.List(elts=elts, ctx=ast.Load()) + elif isinstance(py_value, dict): + keys = [ast.Constant(k) for k in py_value.keys()] + values = [self.safe_ast_value(v) for v in py_value.values()] + return ast.Dict(keys=keys, values=values) + else: + logger.warning(f"Cannot inject parameter of type {type(py_value)}. Skipping.") + return None def create_injected_script( @@ -289,7 +192,6 @@ def create_injected_script( framework: str, model_name: str = "model", mode: str = "base", - alignment_dir: str = None, **kwargs, ) -> str: # read source script @@ -307,7 +209,6 @@ def create_injected_script( framework, model_name=model_name, mode=mode, - alignment_dir=alignment_dir, **kwargs, ) new_tree = injector.visit(tree) diff --git a/padiff/cli.py b/padiff/cli.py index e0f7551..e71b74b 100644 --- a/padiff/cli.py +++ b/padiff/cli.py @@ -31,10 +31,27 @@ def load_yaml_config(config_path): with open(config_path, "r") as f: if ext in [".yaml", ".yml"]: - return yaml.safe_load(f) + config = yaml.safe_load(f) else: raise ValueError(f"Unsupported config file format: {ext}") + if "CLI" not in config: + raise ValueError("Config file must contain a 'CLI' section.") + + cli_cfg = {} + for k, v in config["CLI"].items(): + cli_cfg[k] = v + + guard_cfg = {} + for k, v in config.get("PaDiffGuard", {}).items(): + guard_cfg[k] = v + + compare_cfg = {} + for k, v in config.get("COMPARE", {}).items(): + compare_cfg[k] = v + + return cli_cfg, guard_cfg, compare_cfg + def run_with_padiff( cmd: str, @@ -43,7 +60,7 @@ def run_with_padiff( optim_name=None, mode="base", alignment_dir=None, - **kwargs, + guard_cfg=None, ): # parse command parts = cmd.split() @@ -57,13 +74,35 @@ def run_with_padiff( logger.error(f"Script not found: {script_path}") sys.exit(1) + script_kwargs = guard_cfg.copy() + if mode == "base": + script_kwargs.pop("single_step_mode", None) + script_kwargs.pop("load_init_weights", None) + script_kwargs.pop("load_first_inputs", None) + script_kwargs.pop("base_dump_path", None) + elif mode == "align": + script_kwargs["base_dump_path"] = alignment_dir + logger.warning( + "The current injection only support 'keys_mapping' parameter of type dict 'dict' for loading " + "init weights. If you want to pass in Callable type which also supported by function " + "'load_init_weights_from_dump(...)', please manually modify the injected script " + f"'debug_inject_{framework}.py' and pass it to 'PaDiffGuard(...)'." + ) + else: + logger.error(f"Invalid mode: {mode}. Must be 'base' or 'align'.") + sys.exit(1) + if optim_name is not None: - kwargs["optimizer"] = optim_name + script_kwargs["optimizer"] = optim_name # run injected script - injected_script = create_injected_script(script_path, framework, model_name, mode, alignment_dir, **kwargs) - injected_filename = os.path.basename(injected_script) + try: + injected_script = create_injected_script(script_path, framework, model_name, mode, **script_kwargs) + except Exception as e: + logger.error(f"Failed to inject script: {e}") + sys.exit(1) + injected_filename = os.path.basename(injected_script) new_cmd = ["python", injected_filename] + parts[2:] logger.info(f"Running: {' '.join(new_cmd)}") script_dir = os.path.dirname(os.path.abspath(script_path)) @@ -83,238 +122,139 @@ def main(): epilog=""" === PaDiff 参数使用详解 === - 本工具通过静态代码注入(AST)自动分析您的模型。为确保注入成功,请正确设置以下参数。 - 注意,除了使用命令行外,您可以将所有参数写入一个文件,然后通过 --config 选项加载。 - * 支持格式: .yaml, .yml - * .yaml 格式示例: - pt_cmd: python torch_model.py - pd_cmd: python paddle_model.py - align_depth: inf - * 使用方式: python -m padiff.cli --config config.yaml - * 命令行参数会覆盖配置文件中的同名参数。 - - 1. 命令参数 (--pt_cmd, --pd_cmd): - 这些参数是您运行原始模型的完整命令。 - * 通常以 'python' 开头。 - * 必须指向包含您模型代码的 Python 脚本。 + 本工具通过静态代码注入(AST)自动分析您的模型。 + * 请将参数写入一个文件,然后通过 --config 选项加载 + * 同时提供少量命令行参数,这些参数会覆盖配置文件中的同名参数 - 示例: - --pt_cmd "python /path/to/your/torch_script.py" - --pd_cmd "python /path/to/your/paddle_script.py" + 1. 配置文件路径 (--config): + * 必需 使用配置文件 + * 支持格式: .yaml, .yml - 2. 模型变量名参数 (--pt_model_name, --pd_model_name): - 这些参数指定您在脚本中创建模型实例的**变量名**。 - * 它们不是类名,也不是文件名。 - * 它们是模型实例化时 `=` 左边的标识符。 + 示例: + --config "/path/to/your/config.yaml" - 示例: - 如果您的 PyTorch 脚本中有: - my_torch_model = MyNet() - output = my_torch_model(input_tensor) - 那么您应该使用: - --pt_model_name my_torch_model - - 如果您的 Paddle 脚本中有: - net = SimplePaddle() - out = net.generate(input_tensor) - 那么您应该使用: - --pd_model_name net - - 如果您的 Paddle 脚本中有: - trainer = SFTTrainer( - args=training_args, - model="Qwen/Qwen2.5-0.5B-Instruct", - train_dataset=dataset, - ) - trainer.train() - 那么您应该使用: - --pd_model_name trainer.model - - 3. 优化器名参数 (--pt_optim_name, --pd_optim_name): - 这些参数指定您在脚本中创建优化器实例的**变量名**。 - * 它们不是类名,也不是文件名。 - * 它们是优化器实例化时 `=` 左边的标识符。 - * 该参数为非必须参数,默认值: None (不传递优化器) + 2. 命令参数 (--pt_cmd, --pd_cmd): + * 必需 被包含在 config 文件中,或 通过命令行传入 + * 这些参数是您运行原始模型的完整命令 + * 通常以 'python' 开头 + * 必须指向包含您模型代码的 Python 脚本 示例: - 如果您的 PyTorch 脚本中有: - optim = torch.optim.Adam( - transformer.parameters(), - lr=1.0, - betas=(0.9, 0.98), - eps=1e-9, - ) - 那么您应该使用: - --pt_optim_name optim - - 如果您的 Paddle 脚本中有: - trainer = SFTTrainer( - args=training_args, - model="Qwen/Qwen2.5-0.5B-Instruct", - train_dataset=dataset, - ) - trainer.train() - 由于 trainer.train() 中通常已经包含了完整的前反向过程,因此不需要传递此参数 - - 4. 日志目录参数 (--log_dir): - 指定生成报告和日志的目录。 - * 默认值: ./padiff_log + --pt_cmd "python /path/to/your/torch_script.py" + --pd_cmd "python /path/to/your/paddle_script.py" - 5. 对齐深度参数 (--align_depth): - 控制对齐的粒度。通过指定一个深度值,可以忽略该深度以下的所有子模块。 - * 值为整数: 指定一个具体的深度。例如,--align_depth 1 会忽略深度为1及以下的所有子模块。 - * 默认值: 'inf' ,即无限深度,会对齐到最细粒度的层(如 Linear, ReLU)。 - * 值为整数,当数值超过模型最大迭代深度时,相当于 'inf'。 - * 示例: - --align_depth 0 # 只对齐顶层模块 - --align_depth 1 # 对齐到第一层子模块 - --align_depth inf # 对齐到最细粒度 - - 6. 单步对齐模式参数 (--single_step_mode): - 启用逐层对齐模式。 - * 可选值: forward, backward, both - * 默认值: None (不启用) - * 当启用时,工具会从自动加载基准模型的输出,并用其替换对齐模型的相应层输出。 - - 7. 结果对比参数: - 控制模型输出结果的对比精度和模式。 - * --atol: 绝对误差容忍度 (default: 1e-6) - * --rtol: 相对误差容忍度 (default: 1e-6) - * --compare_mode: 对比模式,具体内容请看对应文档。可选值: mean, strict, abs_mean, 默认值: "mean" - * --action_name: 对比逻辑,具体内容请看对应文档。可选值: equal, loose_equal, 默认值: "equal" - * 示例: - --atol 1e-4 --rtol 1e-5 --compare_mode mean --action_name equal + 3. 日志目录参数 (--log_dir): + * 可选参数 + * 指定生成报告和日志的目录 + * 默认值: ./padiff_log 使用示例: - padiff \\ - --pt_cmd "python torch_model.py" \\ - --pd_cmd "python paddle_model.py" \\ - --pt_model_name "model" \\ - --pd_model_name "model" \\ - --pt_optim_name "optimizer" \\ - --pd_optim_name "optimizer" \\ - --log_dir "./my_alignment_results" \\ - --align_depth 1 \\ - --single_step_mode "forward" \\ - --atol 1e-4 \\ - --rtol 1e-5 \\ - --compare_mode mean \\ - --action_name equal + python -m padiff.cli --config config.yaml --pt_cmd xxx --pd_cmd xxx --log_dir xxx + + 配置文件示例: + CLI: + pt_cmd: "python torch_project/run.py" + pd_cmd: "python paddle_project/run.py" + pt_model_name: "pt_model" + pd_model_name: "pd_model" + pt_optim_name: "pt_optimizer" # not required + pd_optim_name: "pd_optimizer" # not required + log_dir: "./padiff_log" # not required + + PaDiffGuard: + align_depth: 1 # not required + single_step_mode: "forward" # not required + max_calls: 1 # not required + load_init_weights: false # not required + load_first_inputs: false # not required + black_list: [] # not required + keys_mapping: # not required, only support 'dict' now when using cli command + "parm_name_of_paddle_model": "parm_name_of_torch_model" + "model_pd.layers.0.input_layernorm.weight": "model_pt.layers.0.input_layernorm.weight" + + COMPARE: # not required + atol: 1.0e-06 + rtol: 1.0e-06 + compare_mode: "mean" + action_name: "equal" """, formatter_class=argparse.RawDescriptionHelpFormatter, ) - parser.add_argument("--config", type=str, help="Path to the config file") - parser.add_argument("--pt_cmd", type=str, help='PyTorch command, e.g., "python /torch_dir/torch_model.py"') - parser.add_argument("--pd_cmd", type=str, help='Paddle command, e.g., "python /paddle_dir/paddle_model.py"') - parser.add_argument( - "--pt_model_name", - type=str, - default="model", - help="The model name that appears in the pytorch script's code (default: 'model')", - ) - parser.add_argument( - "--pd_model_name", - type=str, - default="model", - help="The model name that appears in the paddle script's code (default: 'model')", - ) + parser.add_argument("--config", type=str, required=True, help="Path to the YAML configuration file.") parser.add_argument( - "--pt_optim_name", + "--pt_cmd", type=str, - default=None, - help="The model name that appears in the pytorch script's code (default: None)", + help="Override 'pt_cmd' (pyTorch command) in config, e.g., 'python /torch_dir/torch_model.py'", ) parser.add_argument( - "--pd_optim_name", + "--pd_cmd", type=str, - default=None, - help="The model name that appears in the paddle script's code (default: None)", + help="Override 'pd_cmd' (paddle command) in config, e.g., 'python /paddle_dir/paddle_model.py'", ) parser.add_argument( "--log_dir", type=str, default="./padiff_log", - help="Directory to save logs and reports (default: './padiff_log')", - ) - parser.add_argument("--align_depth", type=str, default="inf", help="Depth of alignment (default: 'inf')") - parser.add_argument( - "--single_step_mode", - choices=["forward", "backward", "both"], - default=None, - help="Enable single-step alignment mode. Choices: forward, backward, both. (default: None, disabled)", - ) - parser.add_argument( - "--black_list", - type=str, - nargs="*", - help="List of layer names to add to the black list.", - ) - parser.add_argument( - "--atol", type=float, default=1e-6, help="Absolute tolerance for result comparison (default: 1e-6)" - ) - parser.add_argument( - "--rtol", type=float, default=1e-6, help="Relative tolerance for result comparison (default: 1e-6)" - ) - parser.add_argument( - "--compare_mode", - choices=["mean", "strict", "abs_mean"], - default="mean", - help="Comparison mode for result checking. Choices: mean, strict, abs_mean (default: mean)", - ) - parser.add_argument( - "--action_name", - choices=["equal", "loose_equal"], - default="equal", - help="Activation function name for specific comparison logic. Choices: equal, loose_equal (default: equal)", + help="Override 'log_dir' (directory to save logs and reports) in config. (default: './padiff_log')", ) - known_args, _ = parser.parse_known_args() - if known_args.config: - try: - config_args = load_yaml_config(known_args.config) - parser.set_defaults(**config_args) - args = parser.parse_args() - except Exception as e: - print(f"Error loading config file {known_args.config}: {e}") - sys.exit(1) - args = parser.parse_args() - args_dict = vars(args) - config_path = args_dict.pop("config", None) - if config_path: - logger.info(f"Configuration loaded from: {config_path}") - - pt_cmd = args_dict.pop("pt_cmd", None) - pd_cmd = args_dict.pop("pd_cmd", None) - if pt_cmd is None or pd_cmd is None: - logger.error("--pt_cmd and --pd_cmd are required. You must provide it via command line or in the config file.") - parser.print_help() + + try: + cli_cfg, guard_cfg, compare_cfg = load_yaml_config(args.config) + except Exception as e: + print(f"Error loading config: {e}") sys.exit(1) - log_dir = args_dict.pop("log_dir", "./padiff_log") - logger.reset_dir(log_dir) + if args.pt_cmd: + cli_cfg["pt_cmd"] = args.pt_cmd + if args.pd_cmd: + cli_cfg["pd_cmd"] = args.pd_cmd + if args.log_dir: + cli_cfg["log_dir"] = args.log_dir - pt_model_name = args_dict.pop("pt_model_name", "model") - pd_model_name = args_dict.pop("pd_model_name", "model") + log_dir = cli_cfg.pop("log_dir", "./padiff_log") + logger.reset_dir(log_dir) - single_step_mode_value = args_dict.pop("single_step_mode", None) - pd_kwargs = dict(args_dict) - if single_step_mode_value is not None: - pd_kwargs["single_step_mode"] = single_step_mode_value + pt_cmd = cli_cfg.get("pt_cmd") + pd_cmd = cli_cfg.get("pd_cmd") + if not pt_cmd or not pd_cmd: + logger.error("Both 'pt_cmd' and 'pd_cmd' must be provided (via config or command line).") + parser.print_help() + sys.exit(1) - compare_cfg = { - "atol": args_dict.pop("atol", 1.0e-4), - "rtol": args_dict.pop("rtol", 1.0e-6), - "compare_mode": args_dict.pop("compare_mode", "mean"), - "action_name": args_dict.pop("action_name", "equal"), - } + pt_model_name = cli_cfg.get("pt_model_name", "model") + pd_model_name = cli_cfg.get("pd_model_name", "model") + pt_optim_name = cli_cfg.get("pt_optim_name") + pd_optim_name = cli_cfg.get("pd_optim_name") - pt_optim_name = args_dict.pop("pt_optim_name", None) - pd_optim_name = args_dict.pop("pd_optim_name", None) + logger.info("Code injection and script execution...") + try: + pt_dump_path = run_with_padiff( + cmd=pt_cmd, + framework="torch", + model_name=pt_model_name, + optim_name=pt_optim_name, + mode="base", + alignment_dir=None, + guard_cfg=guard_cfg, + ) + pd_dump_path = run_with_padiff( + cmd=pd_cmd, + framework="paddle", + model_name=pd_model_name, + optim_name=pd_optim_name, + mode="align", + alignment_dir=pt_dump_path, + guard_cfg=guard_cfg, + ) + except Exception as e: + logger.error(f"An error occurred during execution: {type(e).__name__}: {str(e)}") + import traceback - pt_dump_path = run_with_padiff(pt_cmd, "torch", pt_model_name, pt_optim_name, **args_dict) - pd_dump_path = run_with_padiff(pd_cmd, "paddle", pd_model_name, pd_optim_name, "align", pt_dump_path, **pd_kwargs) + traceback.print_exc() + sys.exit(1) logger.info("Running comparison...") try: diff --git a/padiff/comparison/actions.py b/padiff/comparison/actions.py index 7afb037..0840035 100644 --- a/padiff/comparison/actions.py +++ b/padiff/comparison/actions.py @@ -127,13 +127,24 @@ def __call__(self, file_list_0, file_list_1, cfg): continue if tensor_0.shape != tensor_1.shape: - logger.debug(f"Shape of tensors are not equal: {tensor_0.shape}!={tensor_1.shape}") - if tensor_0.size == tensor_1.size: - logger.debug(f"Try to reshape them to {tensor_0.shape}") + debug_msg = f"Shape of tensors are not equal: {tensor_0.shape}!={tensor_1.shape}. " + + if tensor_0.shape == tensor_1.shape[::-1]: + debug_msg += "Try to transpose one of them." + tensor_1 = np.transpose(tensor_1) + elif tensor_0.size == tensor_1.size: + debug_msg += ( + f"Try to reshape them to {tensor_0.shape}. Attention: this may " + "put elements in wrong positions even though they actually have the same value." + ) tensor_1 = np.reshape(tensor_1, tensor_0.shape) else: + debug_msg += "however tensors cannot be converted to each other, skip!" + logger.debug(debug_msg) continue + logger.debug(debug_msg) + assert_tensor_equal(tensor_0, tensor_1, cfg) num_success += 1 diff --git a/padiff/comparison/checker/params.py b/padiff/comparison/checker/params.py index d975d7b..064d225 100644 --- a/padiff/comparison/checker/params.py +++ b/padiff/comparison/checker/params.py @@ -82,6 +82,8 @@ def _check_params_impl(node_lists, reports, compare_target, cfg): param_path_0 is not None and param_path_1 is not None ), f"{compare_target.capitalize()} for at least one of base or raw model is not found." + logger.debug(f"Checking {compare_target} of {node_0['route']}.{param_name_0}(base)") + settings = global_yaml_loader.get_weight_settings( (node_0["name"], node_1["name"]), (reports[0]["framework"], reports[1]["framework"]), diff --git a/padiff/comparison/checker/reports.py b/padiff/comparison/checker/reports.py index 1164cdd..38cccbe 100644 --- a/padiff/comparison/checker/reports.py +++ b/padiff/comparison/checker/reports.py @@ -92,7 +92,7 @@ def check_forward(nodes, reports, cfg): if not nodes[1]["reordered"]: reorder_and_match_sublayers(nodes, reports) except Exception as e: - msg = f"While checking forward, diff found at base_model {nodes[0]['name']} vs raw_model {nodes[1]['name']}\n" + msg = f"While checking forward, diff found at {nodes[0]['name']}(base) vs {nodes[1]['name']}(raw)\n" msg += "Call `reorder_and_match_sublayers` for more detailed infos, but error occurs again:\n" msg += f"{type(e).__name__}: {str(e)}" logger.error(msg) @@ -114,6 +114,7 @@ def check_forward(nodes, reports, cfg): def check_backward(nodes, reports, cfg): + logger.debug(f"Checking backward of {nodes[0]['name']}") action_name = cfg.get("action_name", None) act = get_action(reports[0], nodes[0], reports[1], nodes[1], name=action_name) try: @@ -130,14 +131,15 @@ def check_backward(nodes, reports, cfg): if not nodes[1]["reordered"]: reorder_and_match_sublayers(nodes, reports) except Exception as e: - msg = f"While checking backward, diff found at base_model {nodes[0]['name']} vs raw_model {nodes[1]['name']}\n" + msg = f"While checking backward, diff found at {nodes[0]['name']}(base) vs {nodes[1]['name']}(raw)\n" msg += "Call `reorder_and_match_sublayers` for more detailed infos, but error occurs again:\n" msg += f"{type(e).__name__}: {str(e)}" - print_report_info(nodes, reports, compare_info, "Backward", msg) - return False + logger.error(msg) + # print_report_info(nodes, reports, compare_info, "Backward", msg) + # return False for child_0, child_1 in zip(reversed(nodes[0]["children"]), reversed(nodes[1]["children"])): - res = check_forward((child_0, child_1), reports, cfg) + res = check_backward((child_0, child_1), reports, cfg) if res == False: return False diff --git a/padiff/comparison/manual.py b/padiff/comparison/manual.py index 96c6de7..f1eb7f0 100644 --- a/padiff/comparison/manual.py +++ b/padiff/comparison/manual.py @@ -18,6 +18,9 @@ def compare_dumps(dump_path1, dump_path2, cfg=None, diff_phase="both"): + if dump_path1 == dump_path2: + logger.error("❌ compare_dumps: FAILED !!! Two dump paths should not be same!\n") + # check report logger.info("🔍 Start comparison report (check_report)...") try: diff --git a/padiff/experimental/cinn_diff/README.md b/padiff/experimental/cinn_diff/README.md index 576b3a7..b382677 100644 --- a/padiff/experimental/cinn_diff/README.md +++ b/padiff/experimental/cinn_diff/README.md @@ -7,6 +7,7 @@ ### 一键运行模式 **example** + ```python import os from padiff import cinn_diff @@ -23,12 +24,14 @@ if __name__ == '__main__': run_script = "/root/workspace/PaddleNLP/model_zoo/bert/run_bert.sh" run(run_script, None, None) ``` + **run_script** 模型运行脚本,使用时提供脚本路径,需在模型内部实现好动转静 **base_env** 模型基线运行的环境变量 初始配置为 + ```python { "CUDA_VISIBLE_DEVICES" : "0", @@ -45,6 +48,7 @@ if __name__ == '__main__': **cinn_env** 模型接入编译器运行的环境变量 初始配置为 + ```python { "FLAGS_use_cinn" : "1", @@ -64,14 +68,17 @@ step1: 准备模型运行脚本,跑通动转静+组合算子+编译器 step2: 手动运行动转静+组合算子的基线模型 基线模型运行是需要配置如下环境变量 + ``` "FLAGS_save_static_runtime_data" : "1", "FLAGS_static_runtime_data_save_path" : "./base", ``` + step3: 手动运行动转静+组合算子+编译器的模型 接入编译器的模型运行需要配置如下环境变量 + ``` "FLAGS_save_static_runtime_data" : "1", @@ -79,6 +86,7 @@ step3: 手动运行动转静+组合算子+编译器的模型 "FLAGS_cinn_pass_visualize_dir": "./cinn/cinn_pass", ``` + step4: 运行模型精度对齐脚本 ```python @@ -90,7 +98,8 @@ auto_diff(base_path, compare_path, atol=0, rtol=0) ``` 模型运行脚本环境变量配置例子 -``` shell + +```shell #!/bin/bash export CUDA_VISIBLE_DEVICES=5 export NVIDIA_TF32_OVERRIDE=1 @@ -124,7 +133,7 @@ python run_pretrain.py \ ``` ## 运行结果 -![运行结果图](./img/run_ret.png) +![运行结果图](./img/run_ret.png) 更多功能正在研发中... diff --git a/padiff/utils/utils.py b/padiff/utils/utils.py index 40ef696..fad452f 100644 --- a/padiff/utils/utils.py +++ b/padiff/utils/utils.py @@ -21,6 +21,7 @@ import os.path as osp import traceback +from .log import logger def set_seed(seed=42): @@ -140,24 +141,32 @@ def traverse(structure, on_leaf, on_container=None): new_dict[k] = traverse(structure[k], on_leaf, on_container) return on_container(new_dict) if on_container else new_dict - # list - if isinstance(structure, list): + # list or tuple + if isinstance(structure, (list, tuple)): result = [traverse(item, on_leaf, on_container) for item in structure] return on_container(result) if on_container else result - # tuple - if isinstance(structure, tuple): - # namedtuple - if hasattr(structure, "_fields"): - result = type(structure)( - *[traverse(getattr(structure, field), on_leaf, on_container) for field in structure._fields] - ) - return on_container(result) if on_container else result - else: # tuple - result = tuple(traverse(item, on_leaf, on_container) for item in structure) - return on_container(result) if on_container else result - - # others like, ModelOutput, CausalLMOutputWithPast, DynamicCache + # Sequence-like objects (e.g., DynamicCache, ModelOutput) + if hasattr(structure, "__getitem__") and hasattr(structure, "__len__"): + try: + items = [] + for i in range(len(structure)): + traversed_item = traverse(structure[i], on_leaf, on_container) + items.append(traversed_item) + for i, item in enumerate(items): + structure[i] = item + return structure + except Exception: + pass + + # namedtuple + if hasattr(structure, "_fields"): + result = type(structure)( + *[traverse(getattr(structure, field), on_leaf, on_container) for field in structure._fields] + ) + return on_container(result) if on_container else result + + # object with __dict__ if hasattr(structure, "__dict__"): try: new_obj = type(structure)() @@ -166,10 +175,10 @@ def traverse(structure, on_leaf, on_container=None): v = getattr(structure, k) setattr(new_obj, k, traverse(v, on_leaf, on_container)) return on_container(new_obj) if on_container else new_obj - except: + except Exception: pass - # others + # generic object if hasattr(structure, "__class__"): try: new_obj = type(structure)() @@ -181,10 +190,11 @@ def traverse(structure, on_leaf, on_container=None): except: pass return on_container(new_obj) if on_container else new_obj - except: + except Exception: pass # default: treat as leaves + logger.debug(f"Failed to traverse structure with type {type(structure)}, treat it as a leaf.") return on_leaf(structure) diff --git a/tests/test_cli_end_to_end.py b/tests/test_cli_end_to_end.py index 5536191..33e9e1f 100644 --- a/tests/test_cli_end_to_end.py +++ b/tests/test_cli_end_to_end.py @@ -15,12 +15,11 @@ import unittest import os -import shutil -import tempfile import sys from unittest.mock import patch from io import StringIO import numpy as np +import yaml from padiff.cli import main as padiff_cli_main @@ -57,8 +56,6 @@ def main(): optimizer.clear_grad() if __name__ == "__main__": - import os - os.makedirs("{dump_path}", exist_ok=True) main() """ @@ -95,17 +92,17 @@ def main(): optimizer.zero_grad() if __name__ == "__main__": - import os - os.makedirs("{dump_path}", exist_ok=True) main() """ class TestCliEndToEnd(unittest.TestCase): def setUp(self): - self.test_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.test_dir) + # self.test_dir = tempfile.mkdtemp() + # self.addCleanup(shutil.rmtree, self.test_dir) + self.test_dir = "./temp/" + self.config_path = os.path.join(self.test_dir, "config.yaml") self.paddle_script_path = os.path.join(self.test_dir, "paddle_script.py") self.torch_script_path = os.path.join(self.test_dir, "torch_script.py") self.input_file = os.path.join(self.test_dir, "input.npy") @@ -121,18 +118,16 @@ def setUp(self): input_dim=100, hidden_dim=100, output_dim=10, - input_file=self.input_file, - model_name="model_torch", - dump_path=os.path.join(self.log_dir, "torch"), + input_file="input.npy", + dump_path="torch", ) paddle_script = PADDLE_SCRIPT_TEMPLATE.format( input_dim=100, hidden_dim=100, output_dim=10, - input_file=self.input_file, - model_name="model_paddle", - dump_path=os.path.join(self.log_dir, "paddle"), + input_file="input.npy", + dump_path="paddle", ) with open(self.torch_script_path, "w") as f: @@ -140,26 +135,44 @@ def setUp(self): with open(self.paddle_script_path, "w") as f: f.write(paddle_script) - assert os.path.exists(self.torch_script_path), f"torch_script.py not created: {self.torch_script_path}" - assert os.path.exists(self.paddle_script_path), f"paddle_script.py not created: {self.paddle_script_path}" - assert os.path.getsize(self.torch_script_path) > 0, f"torch_script.py is empty: {self.torch_script_path}" - assert os.path.getsize(self.paddle_script_path) > 0, f"paddle_script.py is empty: {self.paddle_script_path}" - - def _run_cli_test(self, extra_args): - test_args = [ - "padiff", - "--pt_cmd", - f"python {self.torch_script_path}", - "--pd_cmd", - f"python {self.paddle_script_path}", - "--pt_model_name", - "model", - "--pd_model_name", - "model", - "--log_dir", - self.log_dir, - ] - test_args.extend(extra_args) + for path in [self.torch_script_path, self.paddle_script_path]: + assert os.path.exists(path), f"Script not created: {path}" + assert os.path.getsize(path) > 0, f"Script is empty: {path}" + + def _create_config_file(self, overrides=None): + config = { + "CLI": { + "pt_cmd": f"python {self.torch_script_path}", + "pd_cmd": f"python {self.paddle_script_path}", + "pt_model_name": "model", + "pd_model_name": "model", + "log_dir": self.log_dir, + }, + "PaDiffGuard": { + "align_depth": "inf", + "single_step_mode": None, + "max_calls": 1, + "load_init_weights": False, + "load_first_inputs": False, + "black_list": [], + "keys_mapping": None, + }, + "COMPARE": {"atol": 1e-6, "rtol": 1e-6, "compare_mode": "mean", "action_name": "equal"}, + } + + if overrides: + for section, updates in overrides.items(): + if section in config: + config[section].update(updates) + + with open(self.config_path, "w") as f: + yaml.dump(config, f, default_flow_style=False, sort_keys=False) + + def _run_cli_test(self, extra_args=None): + if extra_args is None: + extra_args = [] + + test_args = ["padiff", "--config", self.config_path] + extra_args with patch.object(sys, "argv", test_args): with patch("sys.stdout", new=StringIO()) as fake_out: @@ -173,31 +186,57 @@ def _run_cli_test(self, extra_args): self.fail(f"CLI raised an unexpected exception: {type(e).__name__}: {str(e)}") def test_end_to_end_basic(self): + self._create_config_file() self._run_cli_test([]) def test_end_to_end_with_optimizer(self): - self._run_cli_test(["--pt_optim_name", "optimizer", "--pd_optim_name", "optimizer"]) + overrides = {"CLI": {"pt_optim_name": "optimizer", "pd_optim_name": "optimizer"}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_align_depth(self): - self._run_cli_test(["--align_depth", "0"]) + overrides = {"PaDiffGuard": {"align_depth": 0}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_single_step(self): - self._run_cli_test(["--single_step_mode", "forward"]) - self._run_cli_test(["--single_step_mode", "backward"]) - self._run_cli_test(["--single_step_mode", "both"]) + for mode in ["forward", "backward", "both"]: + with self.subTest(mode=mode): + overrides = {"PaDiffGuard": {"single_step_mode": mode}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_black_list(self): - self._run_cli_test(["--black_list", "Linear"]) + overrides = {"PaDiffGuard": {"black_list": ["Linear"]}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_different_atol_rtol(self): - self._run_cli_test(["--atol", "1e-3", "--rtol", "1e-4"]) + overrides = {"COMPARE": {"atol": 1e-3, "rtol": 1e-4}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_different_compare_mode(self): - self._run_cli_test(["--compare_mode", "strict"]) - self._run_cli_test(["--compare_mode", "abs_mean"]) + for compare_mode in ["strict", "abs_mean"]: + with self.subTest(compare_mode=compare_mode): + overrides = {"COMPARE": {"compare_mode": compare_mode}} + self._create_config_file(overrides) + self._run_cli_test() def test_end_to_end_with_different_action(self): - self._run_cli_test(["--action_name", "loose_equal"]) + overrides = {"COMPARE": {"action_name": "loose_equal"}} + self._create_config_file(overrides) + self._run_cli_test() + + def test_end_to_end_with_command_line_override(self): + self._create_config_file() + extra_args = [ + "--pt_cmd", + f"python {self.torch_script_path}".replace("run.py", "modified_run.py"), + "--log_dir", + os.path.join(self.test_dir, "overridden_log_dir"), + ] + self._run_cli_test(extra_args) if __name__ == "__main__":