diff --git a/doc/mismatch_api_record.md b/doc/mismatch_api_record.md index 2e3b5eb..be8dd28 100644 --- a/doc/mismatch_api_record.md +++ b/doc/mismatch_api_record.md @@ -2,6 +2,53 @@ --- +## 2026-05-31 broadcast_to / expand strides 对齐修复记录 + +### 输入链接 +- 链接类型:review comment +- 原始链接:https://github.com/PaddlePaddle/Paddle/pull/79173 +- 关联 PR:#79173 [Execute Infrastructure] Add at::broadcast_to compat interface + +### 问题与根因 + +| # | 问题接口 | 触发场景 | 根因说明 | +|---|---------|---------|---------| +| 1 | `at::broadcast_to` / `Tensor::broadcast_to` | 对齐测试移除 `.contiguous()` 后 result_cmp DIFFER | Paddle `expand` 对广播维度分配非零 strides(如 `{1,3}` expand 到 `{2,3}` 时 strides 为 `[3,1]`),而 PyTorch 对广播维度分配 stride 为 0(`[0,1]`) | +| 2 | `at::expand` / `Tensor::expand` | 对齐测试移除 `.contiguous()` 后 result_cmp DIFFER | 同根因 #1:Paddle `expand` 的 strides 分配策略与 PyTorch 不同。`{1}` expand 到 `{2,3}` 时 Paddle strides 为 `[3,1]`,PyTorch 为 `[0,1]` | + +### 修复内容 + +**Paddle compat 层改动文件:** +- `paddle/phi/api/include/compat/ATen/ops/expand.h` + - 重构 `expand` 实现:不再调用 `paddle::experimental::expand()`(其返回 dense copy 且 strides 非零) + - 新增 `compute_expand_strides` 辅助函数,按 PyTorch 规则计算 expand 后的 strides(广播维度 stride = 0) + - 使用 `self.as_strided()` 创建 view,使结果与 PyTorch 一样共享原始存储且 strides 一致 +- `paddle/phi/api/include/compat/ATen/ops/broadcast_to.h` + - 无需改动,`broadcast_to` 已委托给 `self.expand()`,随 expand 修复自动对齐 + +**Paddle 测试改动文件:** +- `test/cpp/compat/ATen_expand_test.cc` + - 修复 `ExpandPreserveNonSingleton` 测试:原测试直接访问 `data_ptr()[3]` 假设 dense 布局;改为 strides-aware 访问 + +**PaddleCppAPITest 改动文件:** +- `test/ATen/ops/BroadcastToTest.cpp` + - 移除所有 `.contiguous()` 调用,改用 strides-aware 元素访问(`compute_offset_from_flat_index`) + - 在结果输出中增加 strides 字段,使 result_cmp 能检测布局差异 +- `test/ATen/ops/ExpandTest.cpp` + - 同上:移除 `.contiguous()`,改用 strides-aware 访问,增加 strides 字段 + +### 验证结果 + +- **result_cmp**:`paddle_BroadcastToTest` 与 `torch_BroadcastToTest` **MATCH** ✅ +- **result_cmp**:`paddle_ExpandTest` 与 `torch_ExpandTest` **MATCH** ✅ +- **ctest**:`ATen_expand_test` 全部通过 ✅ + +### 风险与后续 +- 已知风险:无 +- 后续待办:无 + +--- + ## 2026-05-07 兼容层接口修复(PR #78652) ### 输入链接 diff --git a/test/ATen/ops/BroadcastToTest.cpp b/test/ATen/ops/BroadcastToTest.cpp new file mode 100644 index 0000000..eccb8f5 --- /dev/null +++ b/test/ATen/ops/BroadcastToTest.cpp @@ -0,0 +1,417 @@ +#include +#include +#include + +#include +#include + +#include "src/file_manager.h" + +#if USE_PADDLE_API +#include "paddle/common/flags.h" +COMMON_DECLARE_bool(use_stride_kernel); +#endif + +extern paddle_api_test::ThreadSafeParam g_custom_param; + +namespace at { +namespace test { + +using paddle_api_test::FileManerger; + +class BroadcastToTest : public ::testing::Test { + protected: + void SetUp() override {} +}; + +class UseStrideKernelGuard { + public: + explicit UseStrideKernelGuard(bool value) { +#if USE_PADDLE_API + previous_ = FLAGS_use_stride_kernel; + FLAGS_use_stride_kernel = value; +#else + (void)value; +#endif + } + + ~UseStrideKernelGuard() { +#if USE_PADDLE_API + FLAGS_use_stride_kernel = previous_; +#endif + } + + private: +#if USE_PADDLE_API + bool previous_{true}; +#endif +}; + +// Compute element offset from flat index using strides (strides-aware access) +static inline int64_t compute_offset_from_flat_index(int64_t flat_idx, + const at::Tensor& tensor) { + int64_t offset = 0; + int64_t remainder = flat_idx; + for (int64_t d = tensor.dim() - 1; d >= 0; --d) { + int64_t coord = remainder % tensor.sizes()[d]; + remainder /= tensor.sizes()[d]; + offset += coord * tensor.strides()[d]; + } + return offset; +} + +// Write tensor metadata (dim, numel, sizes, strides) and all element values +// Uses strides-aware access to faithfully reflect the underlying layout. +// If Paddle and PyTorch produce different strides, result_cmp will DIFFER, +// and the difference should be recorded as a known mismatch. +static void write_broadcast_to_result_to_file(FileManerger* file, + const at::Tensor& result) { + *file << std::to_string(result.dim()) << " "; + *file << std::to_string(result.numel()) << " "; + for (int64_t i = 0; i < result.dim(); ++i) { + *file << std::to_string(result.sizes()[i]) << " "; + } + // Record strides so layout differences are detected by result_cmp + for (int64_t i = 0; i < result.dim(); ++i) { + *file << std::to_string(result.strides()[i]) << " "; + } + if (result.numel() == 0) { + *file << "empty "; + return; + } + switch (result.scalar_type()) { + case at::kFloat: { + float* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } + break; + } + case at::kDouble: { + double* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } + break; + } + case at::kInt: { + int32_t* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } + break; + } + case at::kLong: { + int64_t* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } + break; + } + default: + *file << "unsupported_dtype "; + break; + } +} + +// Values-only writer for cases where Paddle intentionally materializes a +// broadcast while PyTorch returns a stride-0 view. +static void write_broadcast_values_only_to_file(FileManerger* file, + const at::Tensor& result) { + *file << std::to_string(result.dim()) << " "; + *file << std::to_string(result.numel()) << " "; + for (int64_t i = 0; i < result.dim(); ++i) { + *file << std::to_string(result.sizes()[i]) << " "; + } + if (result.numel() == 0) { + *file << "empty "; + return; + } + float* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } +} + +// ======================== Shape coverage ======================== + +// Small shape test +TEST_F(BroadcastToTest, BroadcastToSmall) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.createFile(); + file << "BroadcastToSmall "; + at::Tensor small = at::ones({1, 3}, at::kFloat); + at::Tensor result = small.broadcast_to({2, 3}); + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +// Large shape test +TEST_F(BroadcastToTest, BroadcastToLarge) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToLarge "; + at::Tensor large = at::ones({1, 1, 128}, at::kFloat); + at::Tensor result = large.broadcast_to({64, 32, 128}); + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +// Boundary: empty tensor +TEST_F(BroadcastToTest, BroadcastToBoundaryEmpty) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToBoundaryEmpty "; + at::Tensor t = at::ones({1, 0}, at::kFloat); + at::Tensor result = t.broadcast_to({2, 0}); + file << std::to_string(result.dim()) << " "; + file << std::to_string(result.numel()) << " "; + file << "empty "; + file << "\n"; + file.saveFile(); +} + +// Boundary: rank less (input rank < target rank) +TEST_F(BroadcastToTest, BroadcastToBoundaryRankLess) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToBoundaryRankLess "; + at::Tensor t = at::ones({1}, at::kFloat); + at::Tensor result = t.broadcast_to({2, 3}); + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +// Boundary: scalar (0-d tensor) +TEST_F(BroadcastToTest, BroadcastToScalar) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToScalar "; + at::Tensor t = at::full({}, 5.0f, at::kFloat); + at::Tensor result = t.broadcast_to({2, 3}); + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +// ======================== Dtype coverage ======================== + +TEST_F(BroadcastToTest, BroadcastToDtypeFloat) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToDtypeFloat "; + at::Tensor t = at::ones({1, 2}, at::kFloat); + at::Tensor result = t.broadcast_to({3, 2}); + file << std::to_string(static_cast(result.scalar_type())) << " "; + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToDtypeDouble) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToDtypeDouble "; + at::Tensor t = at::ones({1, 2}, at::kDouble); + at::Tensor result = t.broadcast_to({3, 2}); + file << std::to_string(static_cast(result.scalar_type())) << " "; + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToDtypeInt) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToDtypeInt "; + at::Tensor t = at::ones({1, 2}, at::kInt); + at::Tensor result = t.broadcast_to({3, 2}); + file << std::to_string(static_cast(result.scalar_type())) << " "; + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToDtypeLong) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToDtypeLong "; + at::Tensor t = at::ones({1, 2}, at::kLong); + at::Tensor result = t.broadcast_to({3, 2}); + file << std::to_string(static_cast(result.scalar_type())) << " "; + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +// ======================== Exception coverage ======================== + +TEST_F(BroadcastToTest, BroadcastToInvalidNonSingleton) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToInvalidNonSingleton "; + + try { + at::Tensor t = at::ones({2, 3}, at::kFloat); + at::Tensor result = t.broadcast_to({2, 4}); + write_broadcast_to_result_to_file(&file, result); + } catch (const std::exception&) { + file << "exception "; + } + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToHighRankToLowRank) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToHighRankToLowRank "; + + try { + at::Tensor t = at::ones({2, 3, 4}, at::kFloat); + at::Tensor result = t.broadcast_to({3, 4}); + write_broadcast_to_result_to_file(&file, result); + } catch (const std::exception&) { + file << "exception "; + } + + file << "\n"; + file.saveFile(); +} + +// C++ ATen broadcast_to follows expand-style -1 keep-dim behavior. +TEST_F(BroadcastToTest, BroadcastToNegativeOne) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToNegativeOne "; + + at::Tensor t = at::ones({3}, at::kFloat); + at::Tensor result = t.broadcast_to({-1}); + write_broadcast_to_result_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToNegativeLessThanMinusOne) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToNegativeLessThanMinusOne "; + + try { + at::Tensor t = at::ones({1}, at::kFloat); + at::Tensor result = t.broadcast_to({-2}); + write_broadcast_to_result_to_file(&file, result); + } catch (const std::exception&) { + file << "exception "; + } + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, ExpandNegativeLessThanMinusOne) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "ExpandNegativeLessThanMinusOne "; + + try { + at::Tensor t = at::ones({1}, at::kFloat); + at::Tensor result = t.expand({-2}); + write_broadcast_to_result_to_file(&file, result); + } catch (const std::exception&) { + file << "exception "; + } + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToStrideZeroValues) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToStrideZeroValues "; + + at::Tensor t = at::zeros({1, 2}, at::kFloat); + t.data_ptr()[0] = 3.0f; + t.data_ptr()[1] = 7.0f; + at::Tensor result = t.broadcast_to({3, 2}); + write_broadcast_to_result_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, BroadcastToStrideKernelDisabledValuesOnly) { + UseStrideKernelGuard guard(false); + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToStrideKernelDisabledValuesOnly "; + + at::Tensor t = at::zeros({1, 2}, at::kFloat); + t.data_ptr()[0] = 3.0f; + t.data_ptr()[1] = 7.0f; + at::Tensor result = t.broadcast_to({3, 2}); + write_broadcast_values_only_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +TEST_F(BroadcastToTest, ExpandStrideKernelDisabledValuesOnly) { + UseStrideKernelGuard guard(false); + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "ExpandStrideKernelDisabledValuesOnly "; + + at::Tensor t = at::ones({1}, at::kFloat); + t.data_ptr()[0] = 5.0f; + at::Tensor result = t.expand({2, 3}); + write_broadcast_values_only_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +// ======================== Function form ======================== + +TEST_F(BroadcastToTest, BroadcastToFunction) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "BroadcastToFunction "; + at::Tensor t = at::ones({1, 2}, at::kFloat); + at::Tensor result = at::broadcast_to(t, {3, 2}); + write_broadcast_to_result_to_file(&file, result); + file << "\n"; + file.saveFile(); +} + +} // namespace test +} // namespace at diff --git a/test/ATen/ops/ExpandTest.cpp b/test/ATen/ops/ExpandTest.cpp index 8214d09..3946dad 100644 --- a/test/ATen/ops/ExpandTest.cpp +++ b/test/ATen/ops/ExpandTest.cpp @@ -19,6 +19,23 @@ class ExpandTest : public ::testing::Test { void SetUp() override {} }; +// Compute element offset from flat index using strides (strides-aware access) +static inline int64_t compute_offset_from_flat_index(int64_t flat_idx, + const at::Tensor& tensor) { + int64_t offset = 0; + int64_t remainder = flat_idx; + for (int64_t d = tensor.dim() - 1; d >= 0; --d) { + int64_t coord = remainder % tensor.sizes()[d]; + remainder /= tensor.sizes()[d]; + offset += coord * tensor.strides()[d]; + } + return offset; +} + +// Write tensor metadata (dim, numel, sizes, strides) and all element values. +// Uses strides-aware access to faithfully reflect the underlying layout. +// If Paddle and PyTorch produce different strides, result_cmp will DIFFER, +// and the difference should be recorded as a known mismatch. static void write_expand_result_to_file(FileManerger* file, const at::Tensor& result) { *file << std::to_string(result.dim()) << " "; @@ -26,15 +43,19 @@ static void write_expand_result_to_file(FileManerger* file, for (int64_t i = 0; i < result.dim(); ++i) { *file << std::to_string(result.sizes()[i]) << " "; } + // Record strides so layout differences are detected by result_cmp + for (int64_t i = 0; i < result.dim(); ++i) { + *file << std::to_string(result.strides()[i]) << " "; + } if (result.numel() == 0) { *file << "empty "; return; } - at::Tensor cont = result.contiguous(); - float* data = cont.data_ptr(); - *file << std::to_string(data[0]) << " "; - *file << std::to_string(data[cont.numel() - 1]) << " "; - *file << std::to_string(cont.sum().item()) << " "; + float* data = result.data_ptr(); + for (int64_t i = 0; i < result.numel(); ++i) { + int64_t offset = compute_offset_from_flat_index(i, result); + *file << std::to_string(data[offset]) << " "; + } } TEST_F(ExpandTest, Expand) { @@ -158,5 +179,54 @@ TEST_F(ExpandTest, ExpandInputRankGreaterThanTargetRank) { file.saveFile(); } +// Scalar expand (0-d tensor) +TEST_F(ExpandTest, ExpandScalar) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "ExpandScalar "; + + at::Tensor t = at::full({}, 5.0f, at::kFloat); + at::Tensor result = t.expand({2, 3}); + write_expand_result_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +// expand supports -1 (keep original size) +TEST_F(ExpandTest, ExpandNegativeOne) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "ExpandNegativeOne "; + + at::Tensor t = at::ones({3}, at::kFloat); + at::Tensor result = t.expand({-1}); + write_expand_result_to_file(&file, result); + + file << "\n"; + file.saveFile(); +} + +// -1 in leading non-existing dimension throws +TEST_F(ExpandTest, ExpandNegativeOneLeadingError) { + auto file_name = g_custom_param.get(); + FileManerger file(file_name); + file.openAppend(); + file << "ExpandNegativeOneLeadingError "; + + try { + at::Tensor t = at::ones({3}, at::kFloat); + at::Tensor result = t.expand({-1, 4}); + write_expand_result_to_file(&file, result); + } catch (const std::exception&) { + file << "exception "; + } + + file << "\n"; + file.saveFile(); +} + } // namespace test } // namespace at