已关闭
[Question|问题咨询]: fp64→fp32 cast 对 fp32 次正规(subnormal)数被置零/截小,与 CPU/GPU 不一致,文档未描述,是否正常 #1567
StoneChan_创建于  8月21日关闭于  8月26日
StoneChan_
StoneChan_成员
8月21日 创建

Welcome to ask questions and discuss with other members.

问题描述

一、问题概述

在 Ascend 上执行 fp64→fp32 的 dtype 转换(tensor.float() / aclnnCast,内部走向量 cast/CVT 指令)时,
凡是落在 fp32 次正规(subnormal/denormal)区间 (0, 1.18e-38) 的输出值,与 CPU、GPU 的结果不一致:

  • 大部分被直接置 0(近似 FTZ flush-to-zero);
  • 靠近 fp32 正规边界(约 5.9e-39 ~ 1.18e-38)的一小部分被保留但值被截小。

正规数区间则 bit 级完全一致。已确认不是舍入模式(round mode)选错,疑似硬件向量 cast 对次正规支持不足/默认 FTZ。

二、环境信息

项 值
设备 Ascend950PR_9579(arch35 / REGBASE)
CANN /data/stonechan/Ascend/cann-9.2.0
torch / torch_npu 2.12.0 / 2.12.0
算子 Cast(double→float,即 fp64→fp32)

三、复现步骤

export PYTHONPATH=...
python reproduce.py   # 见下方第六节脚本

依赖:

import numpy as np, torch
import torch_npu

四、现象(实测数据)

在 [1.4e-45, 1.18e-38)(fp32 可表示次正规区间)内对数均匀采样 600 个点,逐点对比 CPU 与 NPU 的 fp64→fp32:

设备: Ascend950PR_9579
采样点(正负各半): 600
分类(仅统计 CPU 保留为正次正规的位置 |cpu|>=1.4013e-45):
   CPU与NPU一致(保留)   : 0
   NPU清零(FTZ)        : 580   (96.7%)
   NPU保留但截小        : 20    (3.3%)

分界信息(按输入绝对值):
  清零样本 |x| 范围    : [1.48725e-45, 5.47214e-39]
  截小样本 |x| 范围    : [6.21962e-39, 1.11652e-38]

关键点:CPU 保留的正次正规位置,NPU 100% 与其不一致(清 0 或截小)。

逐个样例:

|x|=2.29e-39  cpu=2.283288e-39  npu=0.0          # 清零
|x|=6.96e-39  cpu=6.962499e-39  npu=2.170054e-39  # 截小(约 1/3)
|x|=9.86e-39  cpu=9.857891e-39  npu=7.96084e-39   # 截小
|x|=1.09e-38  cpu=1.0941224e-38 npu=1.0127505e-38 # 截小(接近边界,更接近)

在真实用例上的体现

all_Tensor.float_deterministic.json 这套 fp64 用例(range=±inf,约 1.3%~1.5% 元素落在次正规区间),
全部 17 条精度失败,其不一致元素 100% 集中在 fp32 次正规区间,正规区间 bit 级一致:

case  56: 不一致=2    全部次正规
case 237: 不一致=284  全部次正规
case 336: 不一致=1589 全部次正规
case 562: 不一致=169563 全部次正规
(其余 13 条一致,非次正规部分无任何差异)

五、已做的定位(代码层面)

1. 触发的算子链

tensor.float()(fp64)在 NPU 上:

aten::to → aten::_to_copy → aten::copy_ → aclnnInplaceCopy

aclnnInplaceCopy 内部由于 dtype 不同调用 l0op::Cast(op_api/aclnn_copy.cpp):

if (dstDtype != srcDtype) {
    srcCast = l0op::Cast(srcContiguous, dstDtype, ...);  // fp64->fp32 在这里
}
l0op::ViewCopy(srcBroadcast, selfRef, ...);              // 之后是同 dtype 纯搬移

独立调用 aclnnCast(self.to(torch.float32))同样复现。

2. round mode:fp64→fp32 配的是 RINT(与 CPU 一致)

op_kernel/arch35/cast_struct.h:

CAST_POLICY_DEFINE(double, float, DT_DOUBLE, DT_FLOAT, CAST_TEMPLATE_DIRECT_CAST, ...,
    CAST_ROUND_MODE_RINT, CAST_ROUND_MODE_RINT, ...)   // 就近舍入

因此不是舍入策略导致(RINT 与 CPU/IEEE 默认一致,正规区间也因此 bit 级一致)。

六、最小复现脚本 reproduce.py

import numpy as np
import torch
import torch_npu

FN32_MIN = 1.17549435e-38      # fp32 最小正规数
FN32_MIN_SUB = 1.40129846e-45   # fp32 最小次正规 = 2^-149

def make_probe(n):
    rng = np.random.default_rng(7)
    lo, hi = np.log10(FN32_MIN_SUB), np.log10(FN32_MIN)
    vals = 10.0 ** rng.uniform(lo, hi, n)
    return np.concatenate([vals, -vals]).astype(np.float64)

x_np = make_probe(300)
x = torch.from_numpy(x_np).double().to("npu:0")
torch.npu.synchronize()
cpu = x.cpu().to(torch.float32).numpy()
npu = x.to(torch.float32).cpu().numpy()

flushed   = (cpu != 0) & (npu == 0)
truncated = (cpu != 0) & (npu != 0) & (npu != cpu)
same      = (cpu == npu)

print("CPU与NPU一致(保留)  :", int(same.sum()))
print("NPU清零(FTZ)        :", int(flushed.sum()))
print("NPU保留但截小        :", int(truncated.sum()))
print("清零样本|x|范围      :", (np.abs(x_np)[flushed].min(), np.abs(x_np)[flushed].max()) if flushed.any() else None)
print("截小样本|x|范围      :", (np.abs(x_np)[truncated].min(), np.abs(x_np)[truncated].max()) if truncated.any() else None)

注:脚本需在有 NPU 的设备上执行;将 npu:0 换成实际设备即可。

七、对比参考(CPU / GPU 行为)

同一份 fp64 输入、同样的 .to(torch.float32):

  • CPU(x86, torch):保留全部次正规,与 IEEE 一致。
  • GPU(CUDA, 默认 ftz=false):保留全部次正规,与 CPU 一致(实测 #cast差异=0)。
  • Ascend(本问题):次正规被清 0 / 截小,与 CPU、GPU 均不一致。

八、想请教的问题

  1. Ascend 向量 cast(double→float)对 fp32 次正规数是否是硬件不支持/默认 FTZ(flush-to-zero)?
  2. 若是,是否属于预期行为?有没有办法让 cast 在次正规区间与 CPU/GPU 对齐(保留次正规)?
  3. 靠近边界(~5.9e-39 ~ 1.18e-38)的“保留但截小”是什么机制?它和全量清零的分界点 ~5.5e-39~6.2e-39 是如何决定的?
  4. 若要保证与 CPU/GPU 的次正规语义一致,官方建议的做法是什么(如:用例侧规避、配置项、还是算子/指令层修复)?

九、其它

  • 结论:差异 100% 限定在 fp32 次正规区间,正规数区间 bit 级一致 → 属于 cast 对次正规处理(FTZ/截断)的语义差异,而非计算错误。
  • 涉及的转换:double → float(以及可能其它含次正规的窄化转换)。
likedislike
StoneChan_StoneChan_成员
8月21日 添加了label:question
StoneChan_StoneChan_成员
8月21日 修改标题为 “[Question|问题咨询]: fp64→fp32 cast 对 fp32 次正规(subnormal)数被置零/截小,与 CPU/GPU 不一致,文档未描述,是否正常”,原标题为“[Question|问题咨询]: fp64→fp32 cast 对 fp32 次正规(subnormal)数被置零/截小,与 CPU/GPU 不一致”
StoneChan_
StoneChan_成员
8月21日 评论:
Llihuaichao成员
8月21日 将 li-xingyue-lxy 设为负责人
StoneChan_StoneChan_成员
8月26日 issue状态由 进行中 改变为 已解决
StoneChan_StoneChan_成员
8月26日 关闭了 issue