已关闭
[Bug] reduce_max_v2 AxesAll 对齐 pad 污染合法全归约求最大值 #3134
Agent-Bug-Hunter创建于 9月10日关闭于 26 天前
陈思
9月11日 评论:
9月11日 评论:
收到,感谢反馈!
经过分析,尾块的归约范围仍使用对齐后的长度,可能将逻辑输入之外的数据纳入最大值计算。这个问题与您报告的现象一致,实际运行结果还需要进一步验证。
结合您提供的 float32、shape {1,5} 用例,代码路径如下:
- Tiling 将 5 个元素的 20 字节向上对齐为 32 字节,相当于 8 个元素。
- 初始化时虽然将
coreDataNum限制为实际的 5 个元素,但尾块长度tailDataNum没有同步调整,数据搬运和全归约比较循环仍使用该长度。 - 按用例中的内存布局,输入的 5 个元素均为
1,后面 3 个位置预填的1000可能因此参与比较,从而解释报告中最大值由1变为1000的现象。
另外,形状推导和内核中有 axes=-1 的全归约分支,但README仅说明支持单轴归约,这处支持范围也需要确认并统一。
我们正在确认具体修复方案:若支持 axes=-1 全归约,需要按实际剩余元素数限制归约范围,并正确处理尾块搬运的对齐和填充;若不支持,则需要在入口明确校验并拒绝该参数。验证将重点覆盖本例、不同数据类型、对齐与非对齐长度,以及多块、多核和 axes=0/1 场景。


陈思
9月11日 评论:
9月11日 评论:
/assign @songkai111


9月11日 将 songkai111 设为负责人
9月13日 关联了pull request:fix(reduce_v2): AxesAll 全归约尾 tile 按逻辑长度裁剪,剔除 32B 对齐 pad
26 天前 将 xujiachen8 设为负责人
26 天前 移除了负责人 songkai111
Xxujiachen8
26 天前 关联了pull request:fix(experimental): 修复 reduce_sum_v2/reduce_max_v2 AxesAll 对齐 pad 污染 (issues #2605 #3134)
26 天前 关联了pull request:fix(experimental): 修复 reduce_sum_v2/reduce_max_v2 AxesAll 对齐 pad 污染 (issues #2605 #3134)
xujiachen8
26 天前 评论:
26 天前 评论:
已完成复现与修复。AxesAll 全归约沿用 32B 对齐后的 tailDataNum 作为末 tile 长度,把 GM 对齐 pad 计入最大值。
修复方式:DataCopy 保持按对齐长度搬运(MTE2 对齐约束),最大值比较循环只统计按 coreDataNum(逻辑长度)裁剪后的 validLen 个元素,pad 不再参与比较;CopyIn 改由调用方传入实际搬运长度,Axes0/Axes1 语义不变。
真机验证(Ascend910B 系列 + CANN 9.2.0,单算子包源码编译安装后走 aclnn):
- 基线按 issue 用例复现成功:max=1000(期望 1,exit 42)
- 修复后:max=1(exit 0)
- 扩展回归 14 组用例全部通过:fp32/fp16 AxesAll+canary pad、Axes0/Axes1+pad、{1,100003} 多核多 tile 非对齐尾对照 CPU golden、{1,8} 对齐输入无 pad;ascend910b 编译通过
修复 PR:!5858


26 天前 issue状态由 进行中 改变为 已完成
26 天前 关闭了 issue
摘要
experimental
reduce_max_v2的 AxesAll(axes=-1)全归约路径沿用 32B 对齐后的tileDataNum/tailDataNum循环计数,未按逻辑元素长度bindLen裁剪,把尾部 32 字节对齐填充(pad)元素一并计入最大值。合法输入ones{1,5}(物理 buffer 8 元素,尾部 3 元素为 canary=1000.0)时,期望最大值为 1,实际返回 1000,确认 pad 污染 max。环境
f2b659d6cbd4ca775d16e79872219b6f89b08fb5cann-ops-math-cann_scan_repro_linux-*.run→$ASCEND_HOME_PATH/vendors/cann_scan_repro_math为什么认为这是问题:期望 vs 实际
输入(与复现用例一致)
期望行为
实际行为(aclnn / 源码编译安装路径)
对比结论
根因
文件:
$OPS_MATH/experimental/math/reduce_max_v2/op_host/reduce_max_v2_tiling.cpp(对齐)与$OPS_MATH/experimental/math/reduce_max_v2/op_kernel/reduce_max_v2.h(循环)。tiling 把输入长度向上对齐到 32 字节(
BLOCK_SIZE=32),后续tileDataNum/tailDataNum均基于对齐长度推导:// reduce_max_v2_tiling.cpp info.inputLengthAlign32 = (((inputLength + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE);kernel 的 AxesAll 全归约循环直接用对齐后的
tileNum/tileDataNum/tailDataNum,未按Init里bindLen(逻辑元素长度)裁剪,于是把尾部 pad 元素读入并参与到max比较:// reduce_max_v2.h — ReduceMaxV2AxesAll() const uint32_t loopCount = this->tileNum; const uint32_t tileLen = this->tileDataNum; const uint32_t lastTileLen = this->tailDataNum; for (uint32_t t = 0; t < loopCount; ++t) { uint32_t curTileLen = (t == loopCount - 1) ? lastTileLen : tileLen; CopyIn(t); // ... 对 [0, curTileLen) 逐个比较,未排除 pad }为何确认跑的是源码编译内核(非 built-in)
source $ASCEND_HOME_PATH/vendors/cann_scan_repro_math/bin/set_env.bash,ASCEND_CUSTOM_OPP_PATH指向该 vendor。op_impl下的ReduceMaxV2_*.o与源码build_out产物 sha256 完全一致,且与 built-inops_legacy/reduce_max的ReduceMax_*.osha256 不同(见repro_manifest.json)。完整复现过程
占位符:
$ASCEND_HOME_PATH/$OPS_MATH/$REPRO_ROOT。步骤 1:环境使能
source <CANN安装目录>/ascend-toolkit/set_env.sh步骤 2:源码编译单算子包
cd "$OPS_MATH" bash build.sh --pkg --experimental --soc=ascend910b --ops=reduce_min_v2,reduce_max_v2 \ --vendor_name=cann_scan_repro --no_force -j4成功标志:生成
$OPS_MATH/build_out/cann-ops-math-cann_scan_repro_linux-*.run步骤 3:安装并切换到自定义包
bash $OPS_MATH/build_out/cann-ops-math-cann_scan_repro_linux-*.run \ --quiet --install-path="$ASCEND_HOME_PATH" source "$ASCEND_HOME_PATH/vendors/cann_scan_repro_math/bin/set_env.bash" echo "ASCEND_CUSTOM_OPP_PATH=$ASCEND_CUSTOM_OPP_PATH"步骤 4:放置复现源码与执行脚本
(本 draft 目录内已含上述文件,可直接作为
$REPRO_ROOT。)步骤 5:编译并运行
cd "$REPRO_ROOT" chmod +x build_and_run_repro.sh bash build_and_run_repro.sh --vendor cann_scan_repro --src test_aclnn_reduce_max_v2_axesall_pad.cpp # 退出码 42 = BUG_REPRODUCED成功复现标志:进程退出码 42。
实际运行摘录
复现用例源码
文件名:
test_aclnn_reduce_max_v2_axesall_pad.cpp/** * Track B — MATH-REDMAXV2-001: ReduceMaxV2AxesAll (axes=-1) 全归约把 32B 对齐 pad 计入 max。 * 合法输入 [1,5] fp32,物理 buffer 8 元素,尾部 3 元素预填 canary(1000.0); * 期望 max=1(只算逻辑 5 个元素),pad 污染后 max=1000 -> exit 42。 */ #include <cmath> #include <cstdio> #include <cstdint> #include <vector> #include "acl/acl.h" #include "aclnn_reduce_max_v2.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) static int Init(int32_t deviceId, aclrtStream* stream) { auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } int main() { const int64_t logicalN = 5; const int64_t allocN = 8; std::vector<float> xHost(allocN, 1000.0f); // canary:pad 位置填 1000,大于逻辑值 1 for (int64_t i = 0; i < logicalN; ++i) { xHost[i] = 1.0f; } const std::vector<int64_t> xShape = {1, logicalN}; const std::vector<int64_t> yShape = {1, 1}; const float expected = 1.0f; int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == ACL_SUCCESS, return 1); void* xAddr = nullptr; ret = aclrtMalloc(&xAddr, allocN * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, return 1); ret = aclrtMemcpy(xAddr, allocN * sizeof(float), xHost.data(), allocN * sizeof(float), ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, return 1); std::vector<int64_t> strides = {logicalN, 1}; aclTensor* x = aclCreateTensor(xShape.data(), xShape.size(), aclDataType::ACL_FLOAT, strides.data(), 0, aclFormat::ACL_FORMAT_ND, xShape.data(), xShape.size(), xAddr); void* yAddr = nullptr; float yInit = -999.f; ret = aclrtMalloc(&yAddr, sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, return 1); ret = aclrtMemcpy(yAddr, sizeof(float), &yInit, sizeof(float), ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, return 1); std::vector<int64_t> yStrides = {1, 1}; aclTensor* y = aclCreateTensor(yShape.data(), yShape.size(), aclDataType::ACL_FLOAT, yStrides.data(), 0, aclFormat::ACL_FORMAT_ND, yShape.data(), yShape.size(), yAddr); int64_t axes = -1; int64_t keepdims = 1; uint64_t workspaceSize = 0; aclOpExecutor* executor = nullptr; ret = aclnnReduceMaxV2GetWorkspaceSize(x, axes, keepdims, y, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("GetWorkspaceSize failed. ERROR: %d\n", ret); return 1); void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, return 1); } ret = aclnnReduceMaxV2(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnReduceMaxV2 failed. ERROR: %d\n", ret); return 1); ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("SynchronizeStream failed. ERROR: %d\n", ret); return 1); float actual = -999.f; ret = aclrtMemcpy(&actual, sizeof(float), yAddr, sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, return 1); LOG_PRINT("=== expect vs actual (axes=-1, ones{1,5} + canary1000 pad) ===\n"); LOG_PRINT(" [0] expected=%.8g actual=%.8g\n", expected, actual); bool mismatch = !std::isfinite(actual) || std::fabs(actual - expected) > 1e-3f; aclDestroyTensor(x); aclDestroyTensor(y); aclrtFree(xAddr); aclrtFree(yAddr); if (workspaceAddr) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); if (mismatch) { LOG_PRINT("\nVERDICT: BUG_REPRODUCED (exit 42) — AxesAll pad contaminates max\n"); return 42; } LOG_PRINT("\nVERDICT: NO_REPRO\n"); return 0; }执行脚本
文件名:
build_and_run_repro.sh#!/usr/bin/env bash # Build/run math vendor aclnn (links libcust_opapi; vendor include first). set -euo pipefail VENDOR="" SRC_NAME="" while [[ $# -gt 0 ]]; do case "$1" in --vendor) VENDOR="$2"; shift 2 ;; --src) SRC_NAME="$2"; shift 2 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done : "${ASCEND_HOME_PATH:?请先 source CANN set_env.sh}" ROOT="$(cd "$(dirname "$0")" && pwd)" if [[ -z "$SRC_NAME" ]]; then mapfile -t _srcs < <(ls "$ROOT"/test_aclnn_*.cpp 2>/dev/null || true) if [[ ${#_srcs[@]} -ne 1 ]]; then echo "请用 --src 指定测试文件" >&2 exit 2 fi SRC="${_srcs[0]}" else SRC="$ROOT/$SRC_NAME" fi BIN="${SRC%.cpp}" VENDOR_DIR="" if [[ -n "$VENDOR" ]]; then for cand in \ "${ASCEND_HOME_PATH}/vendors/${VENDOR}_math" \ "${ASCEND_HOME_PATH}/vendors/${VENDOR}_nn" \ "${ASCEND_HOME_PATH}/vendors/${VENDOR}_cv" \ "${ASCEND_HOME_PATH}/vendors/${VENDOR}" do if [[ -f "$cand/bin/set_env.bash" ]]; then set +u # shellcheck disable=SC1090 source "$cand/bin/set_env.bash" set -u VENDOR_DIR="$cand" echo "[env] sourced $cand/bin/set_env.bash" echo "[env] ASCEND_CUSTOM_OPP_PATH=${ASCEND_CUSTOM_OPP_PATH:-}" break fi done fi INC="${ASCEND_HOME_PATH}/aarch64-linux/include" LIB="${ASCEND_HOME_PATH}/aarch64-linux/lib64" [[ -d "$INC" ]] || INC="${ASCEND_HOME_PATH}/include" [[ -d "$LIB" ]] || LIB="${ASCEND_HOME_PATH}/lib64" VENDOR_INC="" VENDOR_LIB="" if [[ -n "$VENDOR_DIR" ]]; then VENDOR_INC="$VENDOR_DIR/op_api/include" VENDOR_LIB="$VENDOR_DIR/op_api/lib" fi echo "[build] g++ $SRC" g++ -std=c++17 -O2 "$SRC" \ ${VENDOR_INC:+-I"$VENDOR_INC"} -I"$INC" -I"$INC/aclnnop" \ -L"$LIB" ${VENDOR_LIB:+-L"$VENDOR_LIB"} \ -Wl,-rpath,"$LIB"${VENDOR_LIB:+ -Wl,-rpath,"$VENDOR_LIB"} \ -lascendcl -lnnopbase ${VENDOR_LIB:+-lcust_opapi} -lopapi \ -o "$BIN" echo "[run] $BIN" set +e "$BIN" RC=$? set -e echo "[exit] $RC (42=BUG_REPRODUCED, 0=NO_REPRO, 1=harness error)" exit "$RC"修复建议
按 torch 参考语义修正 AxesAll 全归约:以逻辑元素长度
bindLen(而非对齐后的tileDataNum/tailDataNum)裁剪最后一个 tile 的参与元素,避免把 32B 对齐 pad 计入 max;并补充「尾部不对齐长度」的合法输入单测与期望对照。人工复核说明
Agent 复现结果供参考;语义边界(对齐填充是否应被计入、文档是否声明调用方契约)以人工结论与产品规范为准。