|
| 1 | +--- |
| 2 | +name: compatibility-testing |
| 3 | +description: 'PaddlePaddle 与 PyTorch C++ API 兼容性测试开发规范。适用于:编写或扩展 test/ 目录下的兼容性测试、验证 Paddle 兼容层与 PyTorch 同一 API 的行为一致性、定位接口输出差异、新增算子测试、覆盖 Shape/Dtype/值域/API 变体。Use when: writing compatibility tests, adding operator tests, checking ATen/c10 API behavior differences between Paddle and PyTorch.' |
| 4 | +argument-hint: '要测试的算子名称或 API,例如 abs、sum、reshape' |
| 5 | +--- |
| 6 | + |
| 7 | +# compatibility-testing |
| 8 | + |
| 9 | +PaddlePaddle 与 PyTorch C++ API 兼容性测试开发规范。 |
| 10 | + |
| 11 | +## 触发条件 |
| 12 | + |
| 13 | +适用场景: |
| 14 | +- 编写或扩展 `PaddleCppAPITest\test` 下的兼容性测试 |
| 15 | +- 验证 Paddle 兼容层与 PyTorch 对同一 API 的行为一致性 |
| 16 | +- 定位某个接口在两个框架间的输出差异 |
| 17 | + |
| 18 | +## 测试目标 |
| 19 | + |
| 20 | +**测试范围**:覆盖 `Paddle\paddle\phi\api\include\compat` 目录下**所有**接口,包括但不限于: |
| 21 | + |
| 22 | +| 目录 | 接口类型 | 示例 | |
| 23 | +|------|---------|------| |
| 24 | +| `ATen/ops/` | ATen 算子 | `abs.h`, `sum.h`, `reshape.h`, `zeros.h` ... | |
| 25 | +| `ATen/core/` | ATen 核心类型 | `Tensor.h`, `TensorBody.h`, `TensorAccessor.h` ... | |
| 26 | +| `ATen/` | ATen 基础 | `Tensor.h`, `Device.h`, `DeviceGuard.h` ... | |
| 27 | +| `c10/core/` | C10 核心 | `ScalarType.h`, `TensorOptions.h`, `Storage.h` ... | |
| 28 | +| `c10/util/` | C10 工具 | `Optional.h`, `ArrayRef.h`, `Half.h` ... | |
| 29 | +| `c10/cuda/` | C10 CUDA | `CUDAStream.h`, `CUDAGuard.h`, `CUDAException.h` ... | |
| 30 | +| `torch/` | Torch 包装 | `all.h`, `cuda.h`, `extension.h` ... | |
| 31 | +| `utils/` | 工具函数 | `scalar_type_conversion.h`, `int_array_ref_conversion.h` ... | |
| 32 | + |
| 33 | +> `AbsTest.cpp`(位于 `test/ops/` 仅为示例)仅作为**参考**,展示测试文件结构和输出格式。 |
| 34 | +
|
| 35 | +## 项目约定 |
| 36 | + |
| 37 | +- 构建系统通过 `CMakeLists.txt` 中的 `create_paddle_tests()` 函数同时生成 `torch_*` 和 `paddle_*` 两套可执行文件 |
| 38 | +- 测试二进制运行时自动以自身文件名命名输出文件(如 `torch_AbsTest.txt`),由 `main.cpp` 中的 `g_custom_param` 传递 |
| 39 | +- 结果对比依赖文本 diff,因此输出格式的确定性至关重要 |
| 40 | + |
| 41 | +## 测试文件结构 |
| 42 | + |
| 43 | +### 文件头与命名空间 |
| 44 | + |
| 45 | +测试文件统一位于 `PaddleCppAPITest\test`,与 compat 接口目录结构对应。参考以下结构(以 `AbsTest.cpp` 为示例): |
| 46 | + |
| 47 | +```cpp |
| 48 | +#include <ATen/ATen.h> |
| 49 | +#include <ATen/core/Tensor.h> |
| 50 | +#include <ATen/ops/abs.h> // 按需替换为目标算子头文件 |
| 51 | +#include <ATen/ops/zeros.h> // 辅助构造用 |
| 52 | +#include <gtest/gtest.h> |
| 53 | + |
| 54 | +#include <string> |
| 55 | +#include <vector> |
| 56 | + |
| 57 | +#include "../../src/file_manager.h" |
| 58 | + |
| 59 | +extern paddle_api_test::ThreadSafeParam g_custom_param; |
| 60 | + |
| 61 | +namespace at { |
| 62 | +namespace test { |
| 63 | + |
| 64 | +using paddle_api_test::FileManerger; |
| 65 | +using paddle_api_test::ThreadSafeParam; |
| 66 | + |
| 67 | +class AbsTest : public ::testing::Test { |
| 68 | + protected: |
| 69 | + void SetUp() override { |
| 70 | + // 构造基准输入 tensor |
| 71 | + } |
| 72 | + at::Tensor test_tensor; |
| 73 | +}; |
| 74 | + |
| 75 | +// 测试用例 ... |
| 76 | + |
| 77 | +} // namespace test |
| 78 | +} // namespace at |
| 79 | +``` |
| 80 | +
|
| 81 | +**关键约束**: |
| 82 | +- 命名空间固定为 `at::test`,保证与 ATen 类型系统的直接可见性 |
| 83 | +- `g_custom_param` 是全局线程安全参数,存储当前运行的输出文件名,由 `main.cpp` 在 `RUN_ALL_TESTS()` 前注入 |
| 84 | +- 测试类命名格式 `<OpName>Test`,文件名与之一致 |
| 85 | +
|
| 86 | +### 结果输出函数 |
| 87 | +
|
| 88 | +每个测试文件包含一个静态输出函数,负责将 tensor 结果序列化到文件。该函数是跨框架对比的唯一数据源,格式必须确定且可复现: |
| 89 | +
|
| 90 | +```cpp |
| 91 | +static void write_abs_result_to_file(FileManerger* file, const at::Tensor& result) { |
| 92 | + *file << std::to_string(result.dim()) << " "; |
| 93 | + *file << std::to_string(result.numel()) << " "; |
| 94 | + float* data = result.data_ptr<float>(); |
| 95 | + for (int64_t i = 0; i < result.numel(); ++i) { |
| 96 | + *file << std::to_string(data[i]) << " "; |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +注意: |
| 102 | +- 第一个测试用例调用 `file.createFile()` 创建文件,后续用例调用 `file.openAppend()` 追加 |
| 103 | +- 每个用例输出前须写入**用例名标签**,输出末尾须追加 `"\n"` 换行,使每个用例占独立一行(详见"输出格式"章节) |
| 104 | +- 输出函数参数使用 `FileManerger*`(指针),调用处传 `&file`。不能使用非 const 引用,否则违反 Google C++ 规范(cpplint `runtime/references`) |
| 105 | +- 对于多 dtype 支持的算子,需按 `result.scalar_type()` 分发到对应的 `data_ptr<T>()` 类型 |
| 106 | + |
| 107 | +## Shape 覆盖要求 |
| 108 | + |
| 109 | +测试 shape 的选择直接影响边界条件的暴露率。以下为四个必选维度区间,每个新算子测试须至少各取一例: |
| 110 | + |
| 111 | +### 标量 (0-d tensor) |
| 112 | +- `{}` — 零维标量,部分算子(如 `sum` 不指定 dim)的返回类型 |
| 113 | +- 注意:`{1}` 是 1-d tensor,**不是**标量 |
| 114 | + |
| 115 | +### 小 shape(元素数 ≤ 64) |
| 116 | +- 典型值:`{4}`、`{2, 3}`、`{2, 3, 4}` |
| 117 | +- 便于手工验证数值正确性 |
| 118 | + |
| 119 | +### 大 shape(元素数 ≥ 10000) |
| 120 | +- 典型值:`{10000}`、`{100, 100}`、`{10, 20, 30, 40}` |
| 121 | +- 主要暴露精度累积误差和内存布局差异 |
| 122 | + |
| 123 | +### 边界 shape |
| 124 | +- 含零维度:`{0}`、`{2, 0}`、`{1, 0, 3}` — 验证空 tensor 语义 |
| 125 | +- 全一维度:`{1, 1, 1}` — 常触发 squeeze/broadcast 的特殊路径 |
| 126 | +- 经 `transpose()` / `as_strided()` 产生的非连续 tensor — 验证 stride 处理的正确性 |
| 127 | + |
| 128 | +## Dtype 覆盖要求 |
| 129 | + |
| 130 | +以下为 ATen 支持的标准标量类型,通过 `at::TensorOptions().dtype()` 或 shorthand 常量指定。新增测试至少需要覆盖 `kFloat`、`kDouble`、`kInt`、`kLong` 四种基础类型,其余按算子语义酌情补充: |
| 131 | + |
| 132 | +| 标量类型 | ATen 常量 | C++ 对应类型 | 适用注意 | |
| 133 | +|---------|-----------|-------------|---------| |
| 134 | +| float32 | `at::kFloat` | `float` | 多数算子的默认 dtype | |
| 135 | +| float64 | `at::kDouble` | `double` | 精度基准,常用于 reference 比较 | |
| 136 | +| int32 | `at::kInt` | `int32_t` | 整型算子、索引 | |
| 137 | +| int64 | `at::kLong` | `int64_t` | shape / dim 参数的底层类型 | |
| 138 | +| int16 | `at::kShort` | `int16_t` | 较少使用,部分量化场景 | |
| 139 | +| int8 | `at::kChar` | `int8_t` | 不要与 `kByte` (uint8) 混淆 | |
| 140 | +| uint8 | `at::kByte` | `uint8_t` | 常见于图像数据 | |
| 141 | +| bool | `at::kBool` | `bool` | 比较算子的返回类型 | |
| 142 | + |
| 143 | +> Paddle 兼容层的 dtype 映射与 PyTorch 存在细微差异(例如默认 dtype 可能不同),输出对比时需关注此类隐式转换。 |
| 144 | + |
| 145 | +## 异常行为测试 |
| 146 | + |
| 147 | +部分算子在非法输入下的异常行为可能在两个框架间存在差异(一个抛异常、另一个返回 NaN 或空 tensor)。此类差异需显式捕获并记录: |
| 148 | + |
| 149 | +```cpp |
| 150 | +TEST_F(SomeOpTest, InvalidInputHandling) { |
| 151 | + auto file_name = g_custom_param.get(); |
| 152 | + FileManerger file(file_name); |
| 153 | + file.openAppend(); |
| 154 | + file << "InvalidInputHandling "; |
| 155 | + try { |
| 156 | + at::Tensor result = at::some_op(invalid_tensor); |
| 157 | + // 未抛异常 — 正常记录结果 |
| 158 | + write_someop_result_to_file(&file, result); |
| 159 | + } catch (const std::exception& e) { |
| 160 | + file << "exception "; |
| 161 | + } |
| 162 | + file << "\n"; |
| 163 | + file.saveFile(); |
| 164 | +} |
| 165 | +``` |
| 166 | +
|
| 167 | +**注意事项**: |
| 168 | +- **不要使用 `c10::Error` 作为 catch 类型** — 在 Paddle 兼容层中 `c10::Error` 是 `C10ErrorType` 枚举常量(定义在 `c10/util/Exception.h`),不是异常类。统一使用 `std::exception` 捕获即可。 |
| 169 | +- **不要输出 `e.what()`** — 两个框架的异常消息文本不同,会产生大量无意义的 diff。仅记录 `"exception "` 标记是否抛出异常。 |
| 170 | +- 两框架的对比重点是**是否抛异常**须一致,异常消息内容不要求匹配。 |
| 171 | +
|
| 172 | +## 输出格式 |
| 173 | +
|
| 174 | +输出文件采用空格分隔的纯文本,**每个用例独占一行**,格式如下: |
| 175 | +
|
| 176 | +``` |
| 177 | +<TestCaseName> <ndim> <numel> [<size_0> <size_1> ...] <val_0> <val_1> ...\n |
| 178 | +``` |
| 179 | +
|
| 180 | +- **用例名标签**:每行以用例名(如 `SumAllElements`)开头,与后续数据以空格分隔 |
| 181 | +- **换行分隔**:每个用例输出末尾追加 `"\n"`,使 diff 能逐行定位到具体出错的用例 |
| 182 | +
|
| 183 | +示例(SumTest 的部分输出): |
| 184 | +``` |
| 185 | +SumAllElements 0 1 21.000000 |
| 186 | +SumWithDtype 7 0 1 21.000000 |
| 187 | +SumAlongDim0 1 3 3 5.000000 7.000000 9.000000 |
| 188 | +SumInt32 0 1 100 |
| 189 | +``` |
| 190 | +
|
| 191 | +代码示例: |
| 192 | +```cpp |
| 193 | +TEST_F(SumTest, SumAllElements) { |
| 194 | + auto file_name = g_custom_param.get(); |
| 195 | + FileManerger file(file_name); |
| 196 | + file.createFile(); // 第一个用例 |
| 197 | + file << "SumAllElements "; // 用例名标签 |
| 198 | + at::Tensor result = at::sum(test_tensor); |
| 199 | + write_sum_result_to_file(&file, result); |
| 200 | + file << "\n"; // 换行分隔 |
| 201 | + file.saveFile(); |
| 202 | +} |
| 203 | +
|
| 204 | +TEST_F(SumTest, SumWithDtype) { |
| 205 | + auto file_name = g_custom_param.get(); |
| 206 | + FileManerger file(file_name); |
| 207 | + file.openAppend(); // 后续用例 |
| 208 | + file << "SumWithDtype "; // 用例名标签 |
| 209 | + // ... |
| 210 | + file << "\n"; |
| 211 | + file.saveFile(); |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +注意事项: |
| 216 | +- 浮点值通过 `std::to_string()` 序列化,精度为 6 位有效数字 |
| 217 | +- 用例名标签使得 diff 输出直接可读,无需逐字节计数来定位差异 |
| 218 | +- 不同测试用例的输出依次追加到同一文件中,顺序由 GTest 的用例注册顺序决定 |
| 219 | +- Place的验证可以取HashValue() |
| 220 | +- Device的比较可以取str() |
| 221 | +- 如果./test/result_cmp.sh的对比结果有差异,请记录下来,在最后总结告诉我,不需要修改测试代码 |
| 222 | + |
| 223 | + |
| 224 | +### 仅运行单个测试 |
| 225 | + |
| 226 | +```bash |
| 227 | +./torch/torch_AbsTest --gtest_filter="AbsTest.EdgeValues" |
| 228 | +``` |
| 229 | + |
| 230 | +#### 运行对比脚本 |
| 231 | + |
| 232 | +```bash |
| 233 | +cd .. && ./test/result_cmp.sh build |
| 234 | +``` |
| 235 | + |
| 236 | +## 新算子测试检查清单 |
| 237 | + |
| 238 | +新增测试前逐项确认,标注 `*` 的为强制项: |
| 239 | + |
| 240 | +**Shape 维度** |
| 241 | +- [ ] `*` 标量 (0-d tensor) |
| 242 | +- [ ] `*` 小 shape (元素数 ≤ 64) |
| 243 | +- [ ] `*` 大 shape (元素数 ≥ 10000) |
| 244 | +- [ ] 含零维度 (`{0}`, `{2, 0}`) |
| 245 | +- [ ] 全一维度 (`{1, 1, 1}`) |
| 246 | +- [ ] 非连续 tensor (经 `transpose` / `narrow` / `as_strided`) |
| 247 | + |
| 248 | +**Dtype** |
| 249 | +- [ ] `*` float32 |
| 250 | +- [ ] `*` float64 |
| 251 | +- [ ] `*` int32 |
| 252 | +- [ ] `*` int64 |
| 253 | +- [ ] bool |
| 254 | +- [ ] int8 / uint8 / int16(视算子支持情况) |
| 255 | + |
| 256 | +**值域** |
| 257 | +- [ ] `*` 正数 |
| 258 | +- [ ] `*` 负数 |
| 259 | +- [ ] `*` 零 |
| 260 | +- [ ] NaN / Inf / -Inf |
| 261 | +- [ ] 极值 (`1e38f`, `1e-38f`) |
| 262 | +- [ ] 正负零区分 (`+0.0` vs `-0.0`) |
| 263 | + |
| 264 | +**API 变体** |
| 265 | +- [ ] 函数式调用 (`at::abs(t)`) |
| 266 | +- [ ] 原地操作 (`at::abs_(t)` 或 `t.abs_()`) |
| 267 | +- [ ] out= 重载 (`at::abs_out(out, t)`) |
| 268 | +- [ ] keepdim 参数(归约类算子) |
| 269 | +- [ ] dim / axis 参数(含负索引) |
| 270 | + |
| 271 | +**输出** |
| 272 | +- [ ] `*` 第一个用例使用 `createFile()`,后续使用 `openAppend()` |
| 273 | +- [ ] `*` 每个用例输出前写入用例名标签,末尾追加 `"\n"` 换行 |
| 274 | +- [ ] `*` 通过 `write_<op>_result_to_file()` 统一输出 |
| 275 | +- [ ] 多 dtype 场景按 `scalar_type()` 分发 `data_ptr<T>()` |
| 276 | +- [ ] 异常捕获统一使用 `std::exception`(不要用 `c10::Error`),不输出 `e.what()` |
| 277 | + |
| 278 | +## 输出文件路径 |
| 279 | + |
| 280 | +默认输出目录:`/tmp/paddle_cpp_api_test/`(由 `FileManerger::basic_path_` 控制)。 |
| 281 | + |
| 282 | +文件名自动取可执行文件名 + `.txt`: |
| 283 | +- `torch_AbsTest` → `/tmp/paddle_cpp_api_test/torch_AbsTest.txt` |
| 284 | +- `paddle_AbsTest` → `/tmp/paddle_cpp_api_test/paddle_AbsTest.txt` |
| 285 | + |
| 286 | +如需自定义路径,在构造 `FileManerger` 时传入完整文件名即可覆盖(但通常不建议,以保持批量对比脚本的兼容性)。 |
0 commit comments