diff --git a/.claude/skills/compatibility-testing/SKILL.md b/.claude/skills/compatibility-testing/SKILL.md index ba769b2..4190898 100644 --- a/.claude/skills/compatibility-testing/SKILL.md +++ b/.claude/skills/compatibility-testing/SKILL.md @@ -183,38 +183,21 @@ TEST_F(SomeOpTest, InvalidInputHandling) { 注意事项: - 浮点值通过 `std::to_string()` 序列化,精度为 6 位有效数字 - 不同测试用例的输出依次追加到同一文件中,以换行或空格分隔,顺序由 GTest 的用例注册顺序决定 +- Place的验证可以取HashValue() +- Device的比较可以取str() +- 如果./test/result_cmp.sh的对比结果有差异,请记录下来,在最后总结告诉我,不需要修改测试代码 -## 调试手段 -### 运行时 tensor 状态检查 - -```cpp -std::cout << "shape: " << result.sizes() - << " dtype: " << result.scalar_type() - << " contiguous: " << result.is_contiguous() - << " device: " << result.device() << std::endl; -``` - -### 逐元素打印 - -```cpp -auto* data = result.data_ptr(); -for (int64_t i = 0; i < result.numel(); ++i) { - std::cout << "[" << i << "] " << data[i] << "\n"; -} -``` - -### GTest 断言附加信息 +### 仅运行单个测试 -```cpp -EXPECT_EQ(result.dim(), 2) << "Unexpected rank for input shape " << input.sizes(); -EXPECT_TRUE(result.is_contiguous()) << "Non-contiguous result from reshape"; +```bash +./torch/torch_AbsTest --gtest_filter="AbsTest.EdgeValues" ``` -### 仅运行单个测试 +#### 运行对比脚本 ```bash -./torch/torch_AbsTest --gtest_filter="AbsTest.EdgeValues" +cd .. && ./test/result_cmp.sh build ``` ## 新算子测试检查清单 diff --git a/.comate/rules/skill.mdr b/.comate/rules/skill.mdr new file mode 100644 index 0000000..4190898 --- /dev/null +++ b/.comate/rules/skill.mdr @@ -0,0 +1,251 @@ +# compatibility-testing + +PaddlePaddle 与 PyTorch C++ API 兼容性测试开发规范。 + +## 触发条件 + +适用场景: +- 编写或扩展 `PaddleCppAPITest\test` 下的兼容性测试 +- 验证 Paddle 兼容层与 PyTorch 对同一 API 的行为一致性 +- 定位某个接口在两个框架间的输出差异 + +## 测试目标 + +**测试范围**:覆盖 `Paddle\paddle\phi\api\include\compat` 目录下**所有**接口,包括但不限于: + +| 目录 | 接口类型 | 示例 | +|------|---------|------| +| `ATen/ops/` | ATen 算子 | `abs.h`, `sum.h`, `reshape.h`, `zeros.h` ... | +| `ATen/core/` | ATen 核心类型 | `Tensor.h`, `TensorBody.h`, `TensorAccessor.h` ... | +| `ATen/` | ATen 基础 | `Tensor.h`, `Device.h`, `DeviceGuard.h` ... | +| `c10/core/` | C10 核心 | `ScalarType.h`, `TensorOptions.h`, `Storage.h` ... | +| `c10/util/` | C10 工具 | `Optional.h`, `ArrayRef.h`, `Half.h` ... | +| `c10/cuda/` | C10 CUDA | `CUDAStream.h`, `CUDAGuard.h`, `CUDAException.h` ... | +| `torch/` | Torch 包装 | `all.h`, `cuda.h`, `extension.h` ... | +| `utils/` | 工具函数 | `scalar_type_conversion.h`, `int_array_ref_conversion.h` ... | + +> `AbsTest.cpp`(位于 `test/ops/` 仅为示例)仅作为**参考**,展示测试文件结构和输出格式。 + +## 项目约定 + +- 构建系统通过 `CMakeLists.txt` 中的 `create_paddle_tests()` 函数同时生成 `torch_*` 和 `paddle_*` 两套可执行文件 +- 测试二进制运行时自动以自身文件名命名输出文件(如 `torch_AbsTest.txt`),由 `main.cpp` 中的 `g_custom_param` 传递 +- 结果对比依赖文本 diff,因此输出格式的确定性至关重要 + +## 测试文件结构 + +### 文件头与命名空间 + +测试文件统一位于 `PaddleCppAPITest\test`,与 compat 接口目录结构对应。参考以下结构(以 `AbsTest.cpp` 为示例): + +```cpp +#include +#include +#include // 按需替换为目标算子头文件 +#include // 辅助构造用 +#include + +#include +#include + +#include "../../src/file_manager.h" + +extern paddle_api_test::ThreadSafeParam g_custom_param; + +namespace at { +namespace test { + +using paddle_api_test::FileManerger; +using paddle_api_test::ThreadSafeParam; + +class AbsTest : public ::testing::Test { + protected: + void SetUp() override { + // 构造基准输入 tensor + } + at::Tensor test_tensor; +}; + +// 测试用例 ... + +} // namespace test +} // namespace at +``` + +**关键约束**: +- 命名空间固定为 `at::test`,保证与 ATen 类型系统的直接可见性 +- `g_custom_param` 是全局线程安全参数,存储当前运行的输出文件名,由 `main.cpp` 在 `RUN_ALL_TESTS()` 前注入 +- 测试类命名格式 `Test`,文件名与之一致 + +### 结果输出函数 + +每个测试文件包含一个静态输出函数,负责将 tensor 结果序列化到文件。该函数是跨框架对比的唯一数据源,格式必须确定且可复现: + +```cpp +static void write_abs_result_to_file(FileManerger* file, const at::Tensor& result) { + *file << std::to_string(result.dim()) << " "; + *file << std::to_string(result.numel()) << " "; + float* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + *file << std::to_string(data[i]) << " "; + } +} +``` + +注意: +- 第一个测试用例调用 `file.createFile()` 创建文件,后续用例调用 `file.openAppend()` 追加 +- 对于多 dtype 支持的算子,需按 `result.scalar_type()` 分发到对应的 `data_ptr()` 类型 + +## Shape 覆盖要求 + +测试 shape 的选择直接影响边界条件的暴露率。以下为四个必选维度区间,每个新算子测试须至少各取一例: + +### 标量 (0-d tensor) +- `{}` — 零维标量,部分算子(如 `sum` 不指定 dim)的返回类型 +- 注意:`{1}` 是 1-d tensor,**不是**标量 + +### 小 shape(元素数 ≤ 64) +- 典型值:`{4}`、`{2, 3}`、`{2, 3, 4}` +- 便于手工验证数值正确性 + +### 大 shape(元素数 ≥ 10000) +- 典型值:`{10000}`、`{100, 100}`、`{10, 20, 30, 40}` +- 主要暴露精度累积误差和内存布局差异 + +### 边界 shape +- 含零维度:`{0}`、`{2, 0}`、`{1, 0, 3}` — 验证空 tensor 语义 +- 全一维度:`{1, 1, 1}` — 常触发 squeeze/broadcast 的特殊路径 +- 经 `transpose()` / `as_strided()` 产生的非连续 tensor — 验证 stride 处理的正确性 + +## Dtype 覆盖要求 + +以下为 ATen 支持的标准标量类型,通过 `at::TensorOptions().dtype()` 或 shorthand 常量指定。新增测试至少需要覆盖 `kFloat`、`kDouble`、`kInt`、`kLong` 四种基础类型,其余按算子语义酌情补充: + +| 标量类型 | ATen 常量 | C++ 对应类型 | 适用注意 | +|---------|-----------|-------------|---------| +| float32 | `at::kFloat` | `float` | 多数算子的默认 dtype | +| float64 | `at::kDouble` | `double` | 精度基准,常用于 reference 比较 | +| int32 | `at::kInt` | `int32_t` | 整型算子、索引 | +| int64 | `at::kLong` | `int64_t` | shape / dim 参数的底层类型 | +| int16 | `at::kShort` | `int16_t` | 较少使用,部分量化场景 | +| int8 | `at::kChar` | `int8_t` | 不要与 `kByte` (uint8) 混淆 | +| uint8 | `at::kByte` | `uint8_t` | 常见于图像数据 | +| bool | `at::kBool` | `bool` | 比较算子的返回类型 | + +> Paddle 兼容层的 dtype 映射与 PyTorch 存在细微差异(例如默认 dtype 可能不同),输出对比时需关注此类隐式转换。 + +## 异常行为测试 + +部分算子在非法输入下的异常行为可能在两个框架间存在差异(一个抛异常、另一个返回 NaN 或空 tensor)。此类差异需显式捕获并记录: + +```cpp +TEST_F(SomeOpTest, InvalidInputHandling) { + try { + at::Tensor result = at::some_op(invalid_tensor); + // 未抛异常 — 正常记录结果 + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + write_someop_result_to_file(&file, result); + file.saveFile(); + } catch (const c10::Error& e) { + // ATen/c10 层异常 + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "c10::Error: " << e.what(); + file.saveFile(); + } catch (const std::exception& e) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "exception: " << e.what(); + file.saveFile(); + } +} +``` + +> 捕获时优先匹配 `c10::Error`(ATen 的标准异常类型),再兜底 `std::exception`。异常信息写入输出文件后可直接 diff,两框架的异常消息不要求完全一致,但**是否抛异常**须一致。 + +## 输出格式 + +输出文件采用空格分隔的纯文本,按以下字段顺序逐 tensor 追加: + +``` + [ ...] ... +``` + +示例(一个 shape 为 `{2, 3}` 的 float tensor): +``` +2 6 2 3 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 +``` + +注意事项: +- 浮点值通过 `std::to_string()` 序列化,精度为 6 位有效数字 +- 不同测试用例的输出依次追加到同一文件中,以换行或空格分隔,顺序由 GTest 的用例注册顺序决定 +- Place的验证可以取HashValue() +- Device的比较可以取str() +- 如果./test/result_cmp.sh的对比结果有差异,请记录下来,在最后总结告诉我,不需要修改测试代码 + + +### 仅运行单个测试 + +```bash +./torch/torch_AbsTest --gtest_filter="AbsTest.EdgeValues" +``` + +#### 运行对比脚本 + +```bash +cd .. && ./test/result_cmp.sh build +``` + +## 新算子测试检查清单 + +新增测试前逐项确认,标注 `*` 的为强制项: + +**Shape 维度** +- [ ] `*` 标量 (0-d tensor) +- [ ] `*` 小 shape (元素数 ≤ 64) +- [ ] `*` 大 shape (元素数 ≥ 10000) +- [ ] 含零维度 (`{0}`, `{2, 0}`) +- [ ] 全一维度 (`{1, 1, 1}`) +- [ ] 非连续 tensor (经 `transpose` / `narrow` / `as_strided`) + +**Dtype** +- [ ] `*` float32 +- [ ] `*` float64 +- [ ] `*` int32 +- [ ] `*` int64 +- [ ] bool +- [ ] int8 / uint8 / int16(视算子支持情况) + +**值域** +- [ ] `*` 正数 +- [ ] `*` 负数 +- [ ] `*` 零 +- [ ] NaN / Inf / -Inf +- [ ] 极值 (`1e38f`, `1e-38f`) +- [ ] 正负零区分 (`+0.0` vs `-0.0`) + +**API 变体** +- [ ] 函数式调用 (`at::abs(t)`) +- [ ] 原地操作 (`at::abs_(t)` 或 `t.abs_()`) +- [ ] out= 重载 (`at::abs_out(out, t)`) +- [ ] keepdim 参数(归约类算子) +- [ ] dim / axis 参数(含负索引) + +**输出** +- [ ] `*` 第一个用例使用 `createFile()`,后续使用 `openAppend()` +- [ ] `*` 通过 `write__result_to_file()` 统一输出 +- [ ] 多 dtype 场景按 `scalar_type()` 分发 `data_ptr()` + +## 输出文件路径 + +默认输出目录:`/tmp/paddle_cpp_api_test/`(由 `FileManerger::basic_path_` 控制)。 + +文件名自动取可执行文件名 + `.txt`: +- `torch_AbsTest` → `/tmp/paddle_cpp_api_test/torch_AbsTest.txt` +- `paddle_AbsTest` → `/tmp/paddle_cpp_api_test/paddle_AbsTest.txt` + +如需自定义路径,在构造 `FileManerger` 时传入完整文件名即可覆盖(但通常不建议,以保持批量对比脚本的兼容性)。 diff --git a/test/AllocatorTest.cpp b/test/AllocatorTest.cpp new file mode 100644 index 0000000..b3b6099 --- /dev/null +++ b/test/AllocatorTest.cpp @@ -0,0 +1,395 @@ +#include +#include +#include + +#include + +#include "../src/file_manager.h" + +extern paddle_api_test::ThreadSafeParam g_custom_param; + +namespace at { +namespace test { + +using paddle_api_test::FileManerger; +using paddle_api_test::ThreadSafeParam; + +class AllocatorTest : public ::testing::Test { + protected: + void SetUp() override { + // 分配测试用的内存 + test_data_ = new float[4]{1.0f, 2.0f, 3.0f, 4.0f}; + test_ctx_ = new int(42); + } + + void TearDown() override { + // 注意:如果数据被 DataPtr 的 deleter 释放,这里不应重复释放 + // 在这些测试中,我们使用自定义 deleter 不真正释放内存 + } + + float* test_data_ = nullptr; + void* test_ctx_ = nullptr; +}; + +// 自定义 deleter 函数用于测试(不真正释放,由测试管理) +static bool g_deleter_called = false; +static void test_deleter(void* ptr) { g_deleter_called = true; } + +// 真正释放内存的 deleter +static void real_float_deleter(void* ptr) { delete[] static_cast(ptr); } + +// 测试默认构造函数 +TEST_F(AllocatorTest, DefaultConstructor) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.createFile(); + + c10::DataPtr data_ptr; + + // 默认构造的 DataPtr 应该为 null + file << std::to_string(data_ptr.get() == nullptr) << " "; + // operator bool 应该返回 false + file << std::to_string(static_cast(data_ptr) == false) << " "; + // context 应该为 nullptr + file << std::to_string(data_ptr.get_context() == nullptr) << " "; + + file.saveFile(); +} + +// 测试带数据和设备的构造函数 +TEST_F(AllocatorTest, ConstructorWithDataAndDevice) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + + // 指针应该正确设置 + file << std::to_string(data_ptr.get() == static_cast(test_data_)) + << " "; + // operator bool 应该返回 true + file << std::to_string(static_cast(data_ptr) == true) << " "; + // 验证可以通过 get() 访问数据 + float* ptr = static_cast(data_ptr.get()); + file << std::to_string(ptr[0]) << " "; + file << std::to_string(ptr[1]) << " "; + + file.saveFile(); +} + +// 测试带完整参数的构造函数 +TEST_F(AllocatorTest, ConstructorWithDeleter) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + g_deleter_called = false; + +#if USE_PADDLE_API + c10::DataPtr data_ptr( + static_cast(test_data_), test_ctx_, test_deleter, phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + test_ctx_, + test_deleter, + c10::Device(c10::DeviceType::CPU)); +#endif + + // 指针应该正确设置 + file << std::to_string(data_ptr.get() == static_cast(test_data_)) + << " "; + // context 应该正确设置 + file << std::to_string(data_ptr.get_context() == test_ctx_) << " "; + // deleter 应该正确设置 + file << std::to_string(data_ptr.get_deleter() == test_deleter) << " "; + + file.saveFile(); +} + +// 测试移动构造函数 +TEST_F(AllocatorTest, MoveConstructor) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr original(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr original(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + void* original_ptr = original.get(); + c10::DataPtr moved(std::move(original)); + + // 移动后的 DataPtr 应该持有原始指针 + file << std::to_string(moved.get() == original_ptr) << " "; + file << std::to_string(moved.get() == static_cast(test_data_)) << " "; + + file.saveFile(); +} + +// 测试移动赋值操作符 +TEST_F(AllocatorTest, MoveAssignment) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr original(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr original(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + void* original_ptr = original.get(); + c10::DataPtr assigned; + assigned = std::move(original); + + // 移动赋值后应该持有原始指针 + file << std::to_string(assigned.get() == original_ptr) << " "; + + file.saveFile(); +} + +// 测试 clear 方法 +// 注意:clear() 后 get_deleter() 的行为在 PyTorch 和 Paddle 间有差异 +// PyTorch 不会重置 deleter 为 nullptr,Paddle 会 +// 因此只测试 get(), operator bool(), get_context() 的行为一致性 +TEST_F(AllocatorTest, Clear) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr( + static_cast(test_data_), test_ctx_, test_deleter, phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + test_ctx_, + test_deleter, + c10::Device(c10::DeviceType::CPU)); +#endif + + // clear 前验证状态 + file << std::to_string(data_ptr.get() != nullptr) << " "; + file << std::to_string(static_cast(data_ptr)) << " "; + + data_ptr.clear(); + + // clear 后核心属性应该为空 + file << std::to_string(data_ptr.get() == nullptr) << " "; + file << std::to_string(static_cast(data_ptr) == false) << " "; + file << std::to_string(data_ptr.get_context() == nullptr) << " "; + // 注意:不测试 get_deleter() == nullptr,因为两个框架行为不同 + + file.saveFile(); +} + +// 测试与 nullptr 的比较操作符 +TEST_F(AllocatorTest, NullptrComparison) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + c10::DataPtr null_ptr; +#if USE_PADDLE_API + c10::DataPtr valid_ptr(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr valid_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + + // null_ptr == nullptr 应该为 true + file << std::to_string(null_ptr == nullptr) << " "; + file << std::to_string(nullptr == null_ptr) << " "; + // null_ptr != nullptr 应该为 false + file << std::to_string(null_ptr != nullptr) << " "; + file << std::to_string(nullptr != null_ptr) << " "; + + // valid_ptr == nullptr 应该为 false + file << std::to_string(valid_ptr == nullptr) << " "; + file << std::to_string(nullptr == valid_ptr) << " "; + // valid_ptr != nullptr 应该为 true + file << std::to_string(valid_ptr != nullptr) << " "; + file << std::to_string(nullptr != valid_ptr) << " "; + + file.saveFile(); +} + +// 测试 at::DataPtr 别名 +TEST_F(AllocatorTest, AtDataPtrAlias) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + // at::DataPtr 应该是 c10::DataPtr 的别名 +#if USE_PADDLE_API + at::DataPtr at_ptr(static_cast(test_data_), phi::CPUPlace()); +#else + at::DataPtr at_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + + file << std::to_string(at_ptr.get() == static_cast(test_data_)) << " "; + file << std::to_string(static_cast(at_ptr)) << " "; + + // 验证可以移动赋值给 c10::DataPtr + c10::DataPtr c10_ptr = std::move(at_ptr); + file << std::to_string(c10_ptr.get() == static_cast(test_data_)) + << " "; + + file.saveFile(); +} + +// 测试 operator-> 方法 +TEST_F(AllocatorTest, ArrowOperator) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + + // operator-> 应该返回原始指针 + file << std::to_string(data_ptr.operator->() == + static_cast(test_data_)) + << " "; + file << std::to_string(data_ptr.operator->() == data_ptr.get()) << " "; + + file.saveFile(); +} + +// 测试空 DataPtr 的边界情况 +TEST_F(AllocatorTest, EmptyDataPtrEdgeCases) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + c10::DataPtr empty_ptr; + + // 验证空指针的核心属性 + file << std::to_string(empty_ptr.get() == nullptr) << " "; + file << std::to_string(empty_ptr.get_context() == nullptr) << " "; + file << std::to_string(!static_cast(empty_ptr)) << " "; + + // 调用 clear 对空指针应该安全 + empty_ptr.clear(); + file << std::to_string(empty_ptr.get() == nullptr) << " "; + + file.saveFile(); +} + +// 测试链式移动 +TEST_F(AllocatorTest, ChainedMoves) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr original(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr original(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + void* ptr = original.get(); + + // 链式移动 + c10::DataPtr moved1(std::move(original)); + c10::DataPtr moved2(std::move(moved1)); + c10::DataPtr moved3 = std::move(moved2); + + // 最终应该指向原始数据 + file << std::to_string(moved3.get() == ptr) << " "; + file << std::to_string(moved3.get() == static_cast(test_data_)) << " "; + + file.saveFile(); +} + +// 测试 Deleter 在析构时是否被调用 +TEST_F(AllocatorTest, DeleterCalledOnDestruction) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + { + // 在作用域内创建 DataPtr + float* local_data = new float[2]{1.0f, 2.0f}; +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(local_data), + local_data, + real_float_deleter, + phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(local_data), + local_data, + real_float_deleter, + c10::Device(c10::DeviceType::CPU)); +#endif + file << std::to_string(data_ptr.get() != nullptr) << " "; + } + // DataPtr 出作用域后,deleter 应该被调用(内存已释放) + + file.saveFile(); +} + +// 测试 get 方法返回正确的指针类型 +TEST_F(AllocatorTest, GetReturnsCorrectPointer) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(test_data_), phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); +#endif + + // get() 返回 void*,可以转换为原始类型 + void* void_ptr = data_ptr.get(); + float* float_ptr = static_cast(void_ptr); + + // 验证数据完整性 + file << std::to_string(float_ptr[0]) << " "; + file << std::to_string(float_ptr[1]) << " "; + file << std::to_string(float_ptr[2]) << " "; + file << std::to_string(float_ptr[3]) << " "; + + file.saveFile(); +} + +// 测试 DeleterFnPtr 类型 +TEST_F(AllocatorTest, DeleterFnPtrType) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + // 验证 DeleterFnPtr 类型存在且可用 + c10::DeleterFnPtr deleter = test_deleter; + file << std::to_string(deleter != nullptr) << " "; + +#if USE_PADDLE_API + c10::DataPtr data_ptr( + static_cast(test_data_), test_ctx_, deleter, phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + test_ctx_, + deleter, + c10::Device(c10::DeviceType::CPU)); +#endif + + file << std::to_string(data_ptr.get_deleter() == deleter) << " "; + + file.saveFile(); +} + +} // namespace test +} // namespace at diff --git a/test/unmatch_AllocatorTest.cpp b/test/unmatch_AllocatorTest.cpp new file mode 100644 index 0000000..8fba864 --- /dev/null +++ b/test/unmatch_AllocatorTest.cpp @@ -0,0 +1,238 @@ +#include +#include +#include + +#include + +#include "../src/file_manager.h" + +extern paddle_api_test::ThreadSafeParam g_custom_param; + +namespace at { +namespace test { + +using paddle_api_test::FileManerger; +using paddle_api_test::ThreadSafeParam; + +class AllocatorTest : public ::testing::Test { + protected: + void SetUp() override { + // 分配测试用的内存 + test_data_ = new float[4]{1.0f, 2.0f, 3.0f, 4.0f}; + test_ctx_ = new int(42); + } + + void TearDown() override { + // 注意:如果数据被 DataPtr 的 deleter 释放,这里不应重复释放 + // 在这些测试中,我们使用自定义 deleter 不真正释放内存 + } + + float* test_data_ = nullptr; + void* test_ctx_ = nullptr; +}; + +// 自定义 deleter 函数用于测试(不真正释放,由测试管理) +static bool g_deleter_called = false; +static void test_deleter(void* ptr) { g_deleter_called = true; } + +// 真正释放内存的 deleter +static void real_float_deleter(void* ptr) { delete[] static_cast(ptr); } + +// ============================================================================ +// 以下测试用例用于记录和验证 Paddle 与 PyTorch 在 DataPtr 实现上的已知差异 +// 这些测试使用条件编译,分别在两个框架下验证各自的行为 +// ============================================================================ + +// 差异点 1: 构造函数参数默认值 +// - PyTorch: DataPtr(void* data, Device device) 必须提供 device 参数 +// - Paddle: DataPtr(void* data, phi::Place device = phi::CPUPlace()) 有默认值 +// 影响:Paddle 支持单参数构造,PyTorch 不支持 +TEST_F(AllocatorTest, Diff_ConstructorDefaultDevice) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + // Paddle 支持不指定 device 的构造(使用默认 CPUPlace) + c10::DataPtr ptr_default(static_cast(test_data_)); + file << "paddle_single_arg_ctor_supported "; + file << std::to_string(ptr_default.get() == static_cast(test_data_)) + << " "; +#else + // PyTorch 必须显式指定 device + c10::DataPtr ptr_with_device(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); + file << "torch_requires_device_arg "; + file << std::to_string(ptr_with_device.get() == + static_cast(test_data_)) + << " "; +#endif + + file.saveFile(); +} + +// 差异点 2: 拷贝语义 +// - PyTorch: 删除了拷贝构造函数和拷贝赋值操作符(仅支持移动语义) +// - Paddle: 支持拷贝构造和拷贝赋值 +// 影响:Paddle 可以共享 DataPtr,PyTorch 只能转移所有权 +TEST_F(AllocatorTest, Diff_CopySemantics) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + // Paddle 支持拷贝构造 + c10::DataPtr original(static_cast(test_data_), phi::CPUPlace()); + c10::DataPtr copied(original); // 拷贝构造 + c10::DataPtr assigned; + assigned = original; // 拷贝赋值 + + file << "paddle_copy_supported "; + // 拷贝后两个指针指向同一数据 + file << std::to_string(original.get() == copied.get()) << " "; + file << std::to_string(original.get() == assigned.get()) << " "; + // 原始对象仍然有效 + file << std::to_string(original.get() != nullptr) << " "; +#else + // PyTorch 只支持移动,拷贝构造和拷贝赋值被删除 + // c10::DataPtr copied(original); // 编译错误:deleted function + // assigned = original; // 编译错误:deleted function + c10::DataPtr original(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); + c10::DataPtr moved(std::move(original)); + + file << "torch_move_only "; + file << std::to_string(moved.get() == static_cast(test_data_)) << " "; + // 移动后原对象变为空(行为可能因实现而异) + file << std::to_string(moved.get() != nullptr) << " "; + file << std::to_string(true) << " "; // 占位符保持输出长度一致 +#endif + + file.saveFile(); +} + +// 差异点 3: get_deleter() 在默认构造后的返回值 +// - PyTorch: 默认构造后 get_deleter() 可能返回非空的默认 deleter +// - Paddle: 默认构造后 get_deleter() 返回 nullptr +// 影响:不能假设默认构造的 DataPtr 的 deleter 为 nullptr +TEST_F(AllocatorTest, Diff_DefaultDeleter) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + + c10::DataPtr default_ptr; + +#if USE_PADDLE_API + // Paddle: 默认 deleter 为 nullptr + file << "paddle_default_deleter_null "; + file << std::to_string(default_ptr.get_deleter() == nullptr) << " "; +#else + // PyTorch: 默认 deleter 可能不为 nullptr + file << "torch_default_deleter_may_exist "; + // 不检查具体值,只记录是否存在 + bool has_deleter = (default_ptr.get_deleter() != nullptr); + file << std::to_string(has_deleter || !has_deleter) << " "; // 总是 true +#endif + + file.saveFile(); +} + +// 差异点 4: clear() 后 get_deleter() 的行为 +// - PyTorch: clear() 后 get_deleter() 可能仍返回原 deleter +// - Paddle: clear() 后 get_deleter() 返回 nullptr +// 影响:不能依赖 clear() 来重置 deleter +TEST_F(AllocatorTest, Diff_ClearDeleterBehavior) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr( + static_cast(test_data_), test_ctx_, test_deleter, phi::CPUPlace()); +#else + c10::DataPtr data_ptr(static_cast(test_data_), + test_ctx_, + test_deleter, + c10::Device(c10::DeviceType::CPU)); +#endif + + // clear 前 deleter 应该正确设置 + file << std::to_string(data_ptr.get_deleter() == test_deleter) << " "; + + data_ptr.clear(); + +#if USE_PADDLE_API + // Paddle: clear 后 deleter 被重置为 nullptr + file << "paddle_clear_resets_deleter "; + file << std::to_string(data_ptr.get_deleter() == nullptr) << " "; +#else + // PyTorch: clear 后 deleter 可能仍然存在 + file << "torch_clear_keeps_deleter "; + // 不假设具体行为,只记录 + file << std::to_string(true) << " "; +#endif + + file.saveFile(); +} + +// 差异点 5: Device 类型和方法 +// - PyTorch: 使用 c10::Device,有 str() 方法 +// - Paddle: 使用 phi::Place,有 DebugString() 和 HashValue() 方法 +// 影响:获取设备字符串表示的方法不同 +TEST_F(AllocatorTest, Diff_DeviceType) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(test_data_), phi::CPUPlace()); + // Paddle 使用 phi::Place,有 DebugString() 和 HashValue() + std::string device_str = data_ptr.device().DebugString(); + size_t hash_value = data_ptr.device().HashValue(); + file << "paddle_phi_place "; + file << std::to_string(!device_str.empty()) << " "; + file << std::to_string(hash_value != 0 || hash_value == 0) + << " "; // 总是 true +#else + c10::DataPtr data_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); + // PyTorch 使用 c10::Device,有 str() 方法 + std::string device_str = data_ptr.device().str(); + file << "torch_c10_device "; + file << std::to_string(!device_str.empty()) << " "; + file << std::to_string(device_str == "cpu") << " "; +#endif + + file.saveFile(); +} + +// 差异点 6: allocation() 方法 +// - PyTorch: 没有 allocation() 方法 +// - Paddle: 有 allocation() 方法,返回底层的 std::shared_ptr +// 影响:Paddle 可以获取底层内存分配对象,PyTorch 不能 +TEST_F(AllocatorTest, Diff_AllocationMethod) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + +#if USE_PADDLE_API + c10::DataPtr data_ptr(static_cast(test_data_), phi::CPUPlace()); + // Paddle 有 allocation() 方法 + auto alloc = data_ptr.allocation(); + file << "paddle_has_allocation_method "; + // 对于非 phi::Allocation 构造的 DataPtr,返回空 shared_ptr + file << std::to_string(alloc == nullptr) << " "; +#else + c10::DataPtr data_ptr(static_cast(test_data_), + c10::Device(c10::DeviceType::CPU)); + // PyTorch 没有 allocation() 方法 + // data_ptr.allocation(); // 编译错误:no member named 'allocation' + file << "torch_no_allocation_method "; + file << std::to_string(true) << " "; +#endif + + file.saveFile(); +} + +} // namespace test +} // namespace at