已关闭
[Bug] reduce_max_v2 AxesAll 对齐 pad 污染合法全归约求最大值 #3134
Agent-Bug-Hunter创建于  9月10日关闭于  26 天前
Agent-Bug-Hunter
9月10日 创建

摘要

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。


环境

项 值
芯片 Ascend910B3
CANN 9.1.0-beta.3
ops-math HEAD f2b659d6cbd4ca775d16e79872219b6f89b08fb5
编译命令 见下文「完整复现过程」
安装方式 cann-ops-math-cann_scan_repro_linux-*.run → $ASCEND_HOME_PATH/vendors/cann_scan_repro_math
Track / Pattern B / novel-align-pad-reduce
Finding MATH-REDMAXV2-001
运行时证明 见下文「为何确认跑的是源码编译内核」

为什么认为这是问题:期望 vs 实际

输入(与复现用例一致)

x: shape {1,5}, dtype float32, 值全为 1.0(物理 buffer 分配 8 元素,尾部 3 元素预填 canary=1000.0)
axes=-1, keepdims=1

期望行为

用例 期望
axes=-1 ones{1,5}+canary1000 pad 成功:最大值 = 1.0(只统计逻辑 5 个元素,忽略对齐 pad)

实际行为(aclnn / 源码编译安装路径)

用例 API 返回 关键输出 说明
axes=-1 ones{1,5}+canary1000 pad SUCCESS actual=1000 exit 42,pad 污染 max

对比结论

用例 期望 实际
axes=-1 ones{1,5}+canary1000 pad 1 1000

根因

文件:$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)

  1. 环境:安装自定义包后执行 source $ASCEND_HOME_PATH/vendors/cann_scan_repro_math/bin/set_env.bash,ASCEND_CUSTOM_OPP_PATH 指向该 vendor。
  2. 二进制指纹:vendor op_impl 下的 ReduceMaxV2_*.o 与源码 build_out 产物 sha256 完全一致,且与 built-in ops_legacy/reduce_max 的 ReduceMax_*.o sha256 不同(见 repro_manifest.json)。
  3. (推荐)排除 built-in:临时移走 built-in 对应算子内核目录后仍能复现。

完整复现过程

占位符:$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:放置复现源码与执行脚本

$REPRO_ROOT/
  test_aclnn_reduce_max_v2_axesall_pad.cpp
  build_and_run_repro.sh

(本 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。

实际运行摘录

[env] sourced $ASCEND_HOME_PATH/cann-9.1.0-beta.3/vendors/cann_scan_repro_math/bin/set_env.bash
[env] ASCEND_CUSTOM_OPP_PATH=$ASCEND_HOME_PATH/cann-9.1.0-beta.3/vendors/cann_scan_repro_math:
[build] g++ $CANN_SCAN/workspace/runs/20260907_ops_4repos/repro/reduce_max_v2/test_aclnn_reduce_max_v2_axesall_pad.cpp
[run] $CANN_SCAN/workspace/runs/20260907_ops_4repos/repro/reduce_max_v2/test_aclnn_reduce_max_v2_axesall_pad
=== expect vs actual (axes=-1, ones{1,5} + canary1000 pad) ===
  [0] expected=1 actual=1000

VERDICT: BUG_REPRODUCED (exit 42) — AxesAll pad contaminates max
[exit] 42  (42=BUG_REPRODUCED, 0=NO_REPRO, 1=harness error)

复现用例源码

文件名: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 复现结果供参考;语义边界(对齐填充是否应被计入、文档是否声明调用方契约)以人工结论与产品规范为准。

likedislike
陈思
陈思成员
9月11日 评论:

收到,感谢反馈!

经过分析,尾块的归约范围仍使用对齐后的长度,可能将逻辑输入之外的数据纳入最大值计算。这个问题与您报告的现象一致,实际运行结果还需要进一步验证。

结合您提供的 float32、shape {1,5} 用例,代码路径如下:

  1. Tiling 将 5 个元素的 20 字节向上对齐为 32 字节,相当于 8 个元素。
  2. 初始化时虽然将 coreDataNum 限制为实际的 5 个元素,但尾块长度 tailDataNum 没有同步调整,数据搬运和全归约比较循环仍使用该长度。
  3. 按用例中的内存布局,输入的 5 个元素均为 1,后面 3 个位置预填的 1000 可能因此参与比较,从而解释报告中最大值由 1 变为 1000 的现象。

另外,形状推导和内核中有 axes=-1 的全归约分支,但README仅说明支持单轴归约,这处支持范围也需要确认并统一。

我们正在确认具体修复方案:若支持 axes=-1 全归约,需要按实际剩余元素数限制归约范围,并正确处理尾块搬运的对齐和填充;若不支持,则需要在入口明确校验并拒绝该参数。验证将重点覆盖本例、不同数据类型、对齐与非对齐长度,以及多块、多核和 axes=0/1 场景。

likedislike
陈思
陈思成员
9月11日 评论:

/assign @songkai111

likedislike
CANN-robotCANN-robot成员
9月11日 将 songkai111 设为负责人
不喜欢雨天ༀ不喜欢雨天ༀ
9月13日 关联了pull request:fix(reduce_v2): AxesAll 全归约尾 tile 按逻辑长度裁剪,剔除 32B 对齐 pad
Xxujiachen8成员
26 天前 将 xujiachen8 设为负责人
Xxujiachen8成员
26 天前 移除了负责人 songkai111
Xxujiachen8成员
26 天前 关联了pull request:fix(experimental): 修复 reduce_sum_v2/reduce_max_v2 AxesAll 对齐 pad 污染 (issues #2605 #3134)
xujiachen8成员
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

likedislike
Ssongkai111成员
26 天前 issue状态由 进行中 改变为 已完成
Ssongkai111成员
26 天前 关闭了 issue