Merged
修改aclnnstack golden #6240
zhanglei842Add on 29 days ago
修改aclnnstack golden #6240
Merged
zhanglei842Add on 29 days ago
Total: 1 files changed+350-13
@@ -9,31 +9,368 @@
9# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.9# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10# See LICENSE in the root of the software repository for the full text of the License.10# See LICENSE in the root of the software repository for the full text of the License.
11# ----------------------------------------------------------------------------11# ----------------------------------------------------------------------------
12+"""Pack 多通路测试规范(TestSpec 格式)。
13+ 
14+pack 是纯数据搬运算子(kernel Pack 等价 torch.stack),输出即输入按 axis 拼接的
15+逐 bit 拷贝,任意 dtype(含浮点)均二进制一致,因此:
16+ 
17+- golden 统一对标 torch 接口(kernel 流程 numpy 输入内部转 torch.stack 计算);
18+- 精度标准统一为 binary_equal;
19+- third_party 仅供远程 XPU 通路消费(--xpu-perf 三方性能采集 / cross_check 交叉
20+ 校验),精度跑批不读它(binary_equal 路由不经过 third_party)。纯拷贝场景无需
21+ cross_check;声明 third_party 是为 --xpu-perf 对标 GPU 性能时,让远程 XPU 知道
22+ 执行哪个标杆 API(torch.stack)。
23+ 
24+通路支持表:
25+ 
26+| 通路 | 支持 | 依据 |
27+|--------|------|------|
28+| kernel | 是 | op_host/pack_def.cpp 定义 Pack |
29+| geir | 是 | op_graph/pack_proto.h |
30+| aclnn | 是 | op_api/aclnn_stack.h 暴露 aclnnStack |
31+"""
32+ 
12import numpy as np33import numpy as np
13import torch34import torch
14 35 
36+__spec__ = {
37+ "pack": "PackTestSpec",
38+ "aclnnStack": "AclnnStackTestSpec",
39+ "torch.stack": "StackTorchTestSpec",
40+}
41+ 
15__golden__ = {42__golden__ = {
16- "aclnn": {43+ "aclnn": {"aclnnStack": "aclnn_stack_golden"},
17- "aclnnStack": "aclnn_stack_golden",
18- },
19 "kernel": {"pack": "pack_golden"},44 "kernel": {"pack": "pack_golden"},
20}45}
21 46 
47+# torch.from_numpy 只认 numpy 原生 dtype;这些类型在 kernel 流程以 ml_dtypes/en_dtypes
48+# 扩展 numpy 数组到达 golden,需走 numpy bit 路径(np.stack 与 torch.stack 逐 bit 等价)
49+_NUMPY_ONLY_DTYPES = ("float8_e5m2", "float8_e4m3fn", "hifloat8", "bfloat16")
50+ 
51+_BINARY_TOLERANCE = {
52+ "float16": {"standard": "binary_equal"},
53+ "float32": {"standard": "binary_equal"},
54+ "bfloat16": {"standard": "binary_equal"},
55+ "float64": {"standard": "binary_equal"},
56+ "int8": {"standard": "binary_equal"},
57+ "int16": {"standard": "binary_equal"},
58+ "int32": {"standard": "binary_equal"},
59+ "int64": {"standard": "binary_equal"},
60+ "uint8": {"standard": "binary_equal"},
61+ "uint16": {"standard": "binary_equal"},
62+ "uint32": {"standard": "binary_equal"},
63+ "uint64": {"standard": "binary_equal"},
64+ "bool": {"standard": "binary_equal"},
65+ "complex32": {"standard": "binary_equal"},
66+ "complex64": {"standard": "binary_equal"},
67+ "complex128": {"standard": "binary_equal"},
68+ "hifloat8": {"standard": "binary_equal"},
69+ "float8_e5m2": {"standard": "binary_equal"},
70+ "float8_e4m3fn": {"standard": "binary_equal"},
71+}
72+ 
73+ 
74+def _flatten_strings(container):
75+ out = []
76+ 
77+ def _walk(node):
78+ if isinstance(node, (tuple, list)):
79+ for item in node:
80+ _walk(item)
81+ elif isinstance(node, str):
82+ out.append(node)
83+ 
84+ _walk(container)
85+ return out
86+ 
87+ 
88+def _is_complex32_storage(arrays, kwargs):
89+ """complex32 以 float16 + 尾维 [2] 存储(real/imag 交错),stack 轴须插在尾维之前。"""
90+ declared = _flatten_strings(kwargs.get("input_dtypes", ()))
91+ if "complex32" not in declared:
92+ return False
93+ first = arrays[0]
94+ return (
95+ hasattr(first, "dtype")
96+ and first.dtype.name == "float16"
97+ and first.ndim >= 1
98+ and first.shape[-1] == 2
99+ )
100+ 
22 101 
23def pack_golden(x, axis: int = 0, N: int = 1, **kwargs):102def pack_golden(x, axis: int = 0, N: int = 1, **kwargs):
24- """103+ """Kernel golden for pack,对标 torch.stack。
25- Kernel golden for pack.
26- All the parameters follow @pack_def.cpp without outputs.
27- All the input Tensors are numpy.ndarray.
28- kwargs may contain: short_soc_version, input_ori_shapes, output_ori_shapes,
29- input_formats, output_formats, input_ori_formats, output_ori_formats,
30- input_dtypes, output_dtypes.
31- """
32 104 
33- return np.stack(x, axis=axis)105+ 参数名与顺序跟随 pack_def.cpp(不含输出)。输入 x 为 numpy.ndarray 列表;
106+ kwargs 可能包含 input/output 的 dtypes、shapes、formats 等元信息。
107+ 
108+ fp8/bf16 的 numpy 数组为 ml_dtypes/en_dtypes 扩展 dtype,torch.from_numpy
109+ 不识别,走 np.stack(纯搬运,与 torch.stack 逐 bit 等价);
110+ complex32 为 float16+尾维[2] 存储,负 axis 需归一化到尾维之前。
111+ """
112+ del N
113+ arrays = list(x)
114+ if not arrays:
115+ return [np.empty(0)]
116+ if _is_complex32_storage(arrays, kwargs):
117+ view_ndim = arrays[0].ndim - 1
118+ if axis < 0:
119+ axis = axis + view_ndim + 1
120+ return [np.stack(arrays, axis=axis)]
121+ if any(a.dtype.name in _NUMPY_ONLY_DTYPES for a in arrays):
122+ return [np.stack(arrays, axis=axis)]
123+ return [torch.stack([torch.from_numpy(t) for t in arrays], dim=axis).numpy()]
124+ 
125+ 
126+_ACLNN_TORCH_DTYPES = {
127+ "float16": torch.float16,
128+ "float32": torch.float32,
129+ "float64": torch.float64,
130+ "bfloat16": torch.bfloat16,
131+ "float8_e5m2": torch.float8_e5m2,
132+ "float8_e4m3fn": torch.float8_e4m3fn,
133+ "int8": torch.int8,
134+ "int16": torch.int16,
135+ "int32": torch.int32,
136+ "int64": torch.int64,
137+ "uint8": torch.uint8,
138+ "uint16": torch.uint16,
139+ "uint32": torch.uint32,
140+ "uint64": torch.uint64,
141+ "bool": torch.bool,
142+ "complex64": torch.complex64,
143+ "complex128": torch.complex128,
144+}
145+ 
146+ 
147+def _stack_output_dtype(tensor_dtypes):
148+ """从嵌套 tensor_dtypes 提取输出 dtype 名(输出恒位于结构末尾)。"""
149+ if not tensor_dtypes:
150+ return None
151+ out = tensor_dtypes[-1]
152+ if isinstance(out, (tuple, list)):
153+ out = out[0] if out else None
154+ return out
155+ 
156+ 
157+def _resolve_np_dtype(dtype_name):
158+ """dtype 名 -> numpy dtype 对象(fp8/bf16 解析为 ml_dtypes/en_dtypes 扩展 dtype)。"""
159+ name = str(dtype_name).split(".")[-1]
160+ if name in ("double", "fp64", "f64"):
161+ name = "float64"
162+ if name == "bfloat16":
163+ from ml_dtypes import bfloat16
164+ 
165+ return np.dtype(bfloat16)
166+ if name == "float8_e5m2":
167+ from ml_dtypes import float8_e5m2
168+ 
169+ return np.dtype(float8_e5m2)
170+ if name == "float8_e4m3fn":
171+ from ml_dtypes import float8_e4m3fn
172+ 
173+ return np.dtype(float8_e4m3fn)
174+ if name == "hifloat8":
175+ from en_dtypes import hifloat8
176+ 
177+ return np.dtype(hifloat8)
178+ return np.dtype(name)
179+ 
180+ 
181+_INT_WIDTH = {"int8": 8, "int16": 16, "int32": 32, "int64": 64}
182+_FP8_NAMES = ("float8_e5m2", "float8_e4m3fn", "hifloat8")
183+ 
184+ 
185+def _cann_promote(dtype_a, dtype_b):
186+ """复刻 CANN op::PromoteType(kPromoteTypesLookup)的关键提升规则。"""
187+ a = str(dtype_a).split(".")[-1]
188+ b = str(dtype_b).split(".")[-1]
189+ if a == b:
190+ return a
191+ pair = (a, b)
192+ if "complex128" in pair:
193+ return "complex128"
194+ if "complex64" in pair:
195+ return "complex128" if "float64" in pair else "complex64"
196+ if a in _FP8_NAMES or b in _FP8_NAMES:
197+ return "float32" # CANN 查找表 fp8 行零值填充,与任意异类提升为 float
198+ if "float64" in pair:
199+ return "float64"
200+ if "bfloat16" in pair:
201+ other = b if a == "bfloat16" else a
202+ if other in ("float32", "float16"):
203+ return "float32"
204+ if other == "bool" or other in _INT_WIDTH or other == "uint8":
205+ return "bfloat16"
206+ raise RuntimeError(f"PromoteType({a}, {b}) is undefined in CANN.")
207+ if "float32" in pair:
208+ other = b if a == "float32" else a
209+ if other == "bool" or other in _INT_WIDTH or other == "uint8":
210+ return "float32"
211+ raise RuntimeError(f"PromoteType({a}, {b}) is undefined in CANN.")
212+ if "float16" in pair:
213+ other = b if a == "float16" else a
214+ if other == "bool" or other in _INT_WIDTH or other == "uint8":
215+ return "float16"
216+ raise RuntimeError(f"PromoteType({a}, {b}) is undefined in CANN.")
217+ if "uint8" in pair:
218+ other = b if a == "uint8" else a
219+ if other == "bool":
220+ return "uint8"
221+ if other in _INT_WIDTH:
222+ return "int16" if _INT_WIDTH[other] < 16 else other
223+ raise RuntimeError(f"PromoteType({a}, {b}) is undefined in CANN.")
224+ if a in _INT_WIDTH and b in _INT_WIDTH:
225+ return a if _INT_WIDTH[a] >= _INT_WIDTH[b] else b
226+ if "bool" in pair:
227+ other = b if a == "bool" else a
228+ if other in _INT_WIDTH:
229+ return other
230+ raise RuntimeError(f"PromoteType({a}, {b}) is undefined in CANN.")
231+ 
232+ 
233+def _np_value_cast(arr, np_dtype):
234+ """numpy 值转换;ml_dtypes/en_dtypes 跨库不能直转时经 float32 桥接(无损等价)。"""
235+ if arr.dtype == np_dtype:
236+ return arr
237+ try:
238+ return arr.astype(np_dtype)
239+ except (TypeError, ValueError):
240+ return arr.astype(np.float32).astype(np_dtype)
34 241 
35 242 
36def aclnn_stack_golden(tensors, dim, out, **kwargs):243def aclnn_stack_golden(tensors, dim, out, **kwargs):
244+ """aclnn golden for aclnnStack,对标 torch.stack。
245+ 
246+ aclnnStack 实现见 op_api/aclnn_stack.cpp:输入 cast 到 promoteType 后打包,
247+ 输出侧再 `l0op::Cast(packResult, out dtype)`(与 stack 后对结果 .to(out dtype)
248+ 等价)。因此 golden 在 stack 之后把输出转换为 CSV 声明的 out dtype。
249+ 
250+ 输入按类型分发(不再依赖 ttk 的 use_numpy 私有 kwarg):ttk 侧同一开关
251+ (is_torch_dtype_support)同时决定 use_numpy 取值与输入以 numpy 还是 torch
252+ 张量下发,两者严格一一对应,isinstance 判断与之等价。hifloat8 等非 torch
253+ 原生 dtype 以 numpy 数组(ml_dtypes/en_dtypes 扩展 dtype)到达,分两种情况:
254+ - 输入与输出同 dtype:无值转换、纯逐 bit 搬移,把输入数组原始字节按 uint8
255+ 载体张量做 stack,字节序列与设备输出一致(比对侧 requant 将双方 view 成
256+ int8 按字节比对)。
257+ - 输入与输出 dtype 不同(如混合 int16/bf16 输入转 hifloat8 输出):为值
258+ 转换,按算子 cast 链走 numpy:各输入 astype(promoteType) -> np.stack ->
259+ astype(out dtype),与 NPU Cast 的 RNE 舍入按位一致。
260+ """
261+ del out
37 if hasattr(dim, "item"):262 if hasattr(dim, "item"):
38 dim = dim.item()263 dim = dim.item()
39- return [torch.stack(tensors, dim=dim)]264+ if tensors and isinstance(tensors[0], np.ndarray):
265+ out_name = _stack_output_dtype(kwargs.get("tensor_dtypes"))
266+ in_names = {t.dtype.name for t in tensors}
267+ if in_names == {out_name}:
268+ itemsize = tensors[0].dtype.itemsize
269+ if dim < 0:
270+ dim += tensors[0].ndim + 1
271+ carriers = [
272+ torch.frombuffer(t.tobytes(), dtype=torch.uint8).reshape(
273+ t.shape + (itemsize,)
274+ )
275+ for t in tensors
276+ ]
277+ return [torch.stack(carriers, dim=dim)]
278+ promote = tensors[0].dtype.name
279+ for t in tensors[1:]:
280+ promote = _cann_promote(t.dtype.name, promote)
281+ stacked = np.stack(
282+ [_np_value_cast(t, _resolve_np_dtype(promote)) for t in tensors], axis=dim
283+ )
284+ return [_np_value_cast(stacked, _resolve_np_dtype(out_name))]
285+ result = torch.stack(tensors, dim=dim)
286+ target = _ACLNN_TORCH_DTYPES.get(_stack_output_dtype(kwargs.get("tensor_dtypes")))
287+ if target is not None and result.dtype != target:
288+ result = result.to(target)
289+ return [result]
zhanw_coding
zhanw_codingzhanw_coding20 days ago

确认下 aclnn 和 torch 的行为是否一致,一致的话,建议直接用 torch.stack,只对不支持的类型做处理

likedislike
zhanglei842
15 days ago Comments:
290+ 
291+ 
292+class _PackTorchStack:
293+ """kernel pack 的三方标杆(远程 XPU 上执行)。
294+ 
295+ pack 参数名(x/axis/N,随 pack_def.cpp)与 torch.stack(tensors/dim)不一致,
296+ 字符串形式无法按名绑定,用类形式适配:参数名对齐算子定义,服务端把输入转成
297+ 目标设备上的 torch 张量后实例化调用(契约:参数名 ⊆ inputs∪attrs)。
298+ """
299+ 
300+ def __call__(self, x, axis=0, N=1, **kwargs):
301+ del N
302+ return [torch.stack(list(x), dim=axis)]
303+ 
304+ 
305+class PackTestSpec:
306+ """kernel/geir 共用 spec。golden 入口接收 numpy 数组。"""
307+ 
308+ @staticmethod
309+ def golden(x, axis=0, N=1, **kwargs):
310+ return pack_golden(x, axis=axis, N=N, **kwargs)
311+ 
312+ third_party = {"torch": _PackTorchStack}
313+ 
314+ tolerance = _BINARY_TOLERANCE
315+ 
316+ 
317+class AclnnStackTestSpec:
318+ """aclnnStack spec。golden 入口接收 torch 张量。"""
319+ 
320+ @staticmethod
321+ def golden(tensors, dim, out, **kwargs):
322+ return aclnn_stack_golden(tensors, dim, out, **kwargs)
323+ 
324+ # aclnnStack 输入名(tensors/dim)与 torch.stack 参数名一致,字符串形式可按名绑定
325+ third_party = {"torch": "torch.stack"}
326+ 
327+ tolerance = _BINARY_TOLERANCE
328+ 
329+ 
330+def stack_torch_golden(tensors, dim=0, out=None, **kwargs):
331+ """e2e (framework-api) golden for torch.stack。
332+ 
333+ e2e golden 三级回退(framework_api/golden_generation.py):CSV golden_api 列 →
334+ 自定义插件(本函数,经 __spec__["torch.stack"] 注册)→ 同 API CPU 复算。
335+ torch 原生 dtype 原本第三级即可覆盖,注册本 spec 后统一由其接管:
336+ 
337+ - torch 张量输入:直通 torch.stack(含 out= 用例),与第三级 CPU 复算同语义;
338+ - numpy 输入(hifloat8 等非 torch 原生 dtype;第三级 is_dtype_support=False
339+ 会返回 UNSUPPORTED,必须由本插件兜底):同 dtype 纯位搬运,np.stack 与
340+ torch.stack 逐 bit 等价;混合 dtype 不支持——torch 的 promote 语义无法在
341+ numpy 侧对非原生 dtype 复刻,设备侧 torch_npu 对 hifloat8 的 promote 行为
342+ 也无标杆,直接报 NotImplementedError 拒绝而非静默给错值。
343+ 
344+ 与 aclnnStack golden 的本质差异:e2e 对标框架 API 语义,无 aclnn 的
345+ promote→stack→cast 链,输出 dtype 恒等于输入 dtype;torch.stack 的 out 为
346+ keyword-only 且返回值型用例不传,签名须带默认值(e2e 还会额外下发 backend
347+ 等 kwarg,由 **kwargs 吸收)。
348+ """
349+ if hasattr(dim, "item"):
350+ dim = dim.item()
351+ if tensors and isinstance(tensors[0], np.ndarray):
352+ in_names = {t.dtype.name for t in tensors}
353+ if len(in_names) != 1:
354+ raise NotImplementedError(
355+ f"torch.stack e2e golden 不支持混合 numpy dtype {sorted(in_names)}:"
356+ "非 torch 原生 dtype 的 promote 语义无法在 numpy 侧复刻"
357+ )
358+ # torch_npu 对 hifloat8 无真实 torch.dtype:设备侧 npu_dtype_cast 返回
359+ # uint8 字节载体(torch_npu.hifloat8 为整型枚举 290)。e2e 比对按实际
360+ # 输出 dtype(uint8)走 binary_equal,golden 须返回同形态字节视图。
361+ return [np.ascontiguousarray(np.stack(list(tensors), axis=dim)).view(np.uint8)]
362+ if out is None:
363+ return [torch.stack(tensors, dim=dim)]
364+ return [torch.stack(tensors, dim=dim, out=out)]
365+ 
366+ 
367+class StackTorchTestSpec:
368+ """e2e spec for torch.stack。golden 入口接收 torch 张量或 numpy 数组。"""
369+ 
370+ @staticmethod
371+ def golden(tensors, dim=0, out=None, **kwargs):
372+ return stack_torch_golden(tensors, dim=dim, out=out, **kwargs)
373+ 
374+ third_party = {"torch": "torch.stack"}
375+ 
376+ tolerance = _BINARY_TOLERANCE