已合并
feat: add tensorflow e2e #90
RuiWang_创建于 21 天前
feat: add tensorflow e2e #90
已合并
RuiWang_创建于 21 天前
44 个文件变更+2488-1161
@@ -0,0 +1,9 @@
1+testcase_name,api_name,tensor_view_shapes,tensor_dtypes,attributes
2+tf_add_f32,tf.raw_ops.Add,"((2,3),(2,3))","('float32','float32')",
3+tf_add_f16,tf.raw_ops.Add,"((128,256),(128,256))","('float16','float16')",
4+tf_add_broadcast,tf.raw_ops.Add,"((4,1,3),(1,5,3))","('float32','float32')",
5+tf_relu_f32,tf.nn.relu,"((64,128),)","('float32',)",
6+tf_relu_f16,tf.nn.relu,"((32,64),)","('float16',)",
7+tf_math_add,tf.math.add,"((2,3),(2,3))","('float32','float32')",
8+tf_abs,tf.math.abs,"((2,3,4),)","('float32',)",
9+tf_matmul,tf.linalg.matmul,"((2,3),(3,4))","('float32','float32')",
@@ -7,13 +7,13 @@ alias() carries the segment name; soc_version removed (merged into device_name).
7"""7"""
8from ttk.core_modules.framework_api.backends.base import Backend8from ttk.core_modules.framework_api.backends.base import Backend
9from ttk.core_modules.framework_api.backends.torch_backend import TorchBackend9from ttk.core_modules.framework_api.backends.torch_backend import TorchBackend
10-from ttk.core_modules.framework_api.backends.npu_backend import NpuTorchBackend10+from ttk.core_modules.framework_api.backends.npu_torch_backend import NpuTorchBackend
11-from ttk.core_modules.framework_api.backends.xpu_backend import XpuTorchBackend11+from ttk.core_modules.framework_api.backends.xpu_torch_backend import XpuTorchBackend
12-from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend12+from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend
13 13 
14 14 
15def test_backend_abc_has_new_methods():15def test_backend_abc_has_new_methods():
16- assert all(hasattr(Backend, m) for m in ["device_name", "alias", "use_device", "is_npu"])16+ assert all(hasattr(Backend, m) for m in ["device_name", "device_type", "has_device", "is_npu"])
17 17 
18 18 
19def test_torchbackend_is_available_via_getattr(monkeypatch):19def test_torchbackend_is_available_via_getattr(monkeypatch):
@@ -34,12 +34,12 @@ def test_torchbackend_is_available_via_getattr(monkeypatch):
34 assert tb.device_count() == 134 assert tb.device_count() == 1
35 35 
36 36 
37-def test_torchbackend_alias_default_is_segment_name():37+def test_torchbackend_device_type_default_is_segment_name():
38- """alias() returns _segment_name (config-driven); empty until injected.38+ """device_type() returns _segment_name (config-driven); empty until injected.
39 TorchBackend itself is never built by _build, so its _segment_name stays ''39 TorchBackend itself is never built by _build, so its _segment_name stays ''
40 until a test (or _build) sets it."""40 until a test (or _build) sets it."""
41 tb = TorchBackend()41 tb = TorchBackend()
42- assert tb.alias() == ""42+ assert tb.device_type() == ""
43 43 
44 44 
45def test_torchbackend_device_name_is_model_via_get_device_name(monkeypatch):45def test_torchbackend_device_name_is_model_via_get_device_name(monkeypatch):
@@ -64,44 +64,58 @@ def test_torchbackend_is_npu_default_false():
64 assert tb.is_npu() is False64 assert tb.is_npu() is False
65 65 
66 66 
67-def test_torchbackend_use_device_default_true():67+def test_torchbackend_has_device_default_true():
68 tb = TorchBackend()68 tb = TorchBackend()
69- assert tb.use_device() is True69+ assert tb.has_device() is True
70 70 
71 71 
72# --- Task 3: three hardware backends onto TorchBackend ---72# --- Task 3: three hardware backends onto TorchBackend ---
73-# alias() is now config-driven: _build injects _segment_name = the yaml segment73+# device_type() is now config-driven: _build injects _segment_name = the yaml segment
74# key. Tests that build backends directly must set _segment_name to mimic _build.74# key. Tests that build backends directly must set _segment_name to mimic _build.
75 75 
76-def test_npu_is_npu_alias_soc_series(monkeypatch):76+ 
77- nb = NpuTorchBackend(); nb.torch_lib = "npu"; nb.profile = {}77+def test_npu_is_npu_device_type_soc_series(monkeypatch):
78+ nb = NpuTorchBackend()
79+ nb.torch_lib = "npu"
80+ nb.profile = {}
78 nb._segment_name = "npu" # mimic _build injection81 nb._segment_name = "npu" # mimic _build injection
79 assert nb.is_npu() is True82 assert nb.is_npu() is True
80- assert nb.alias() == "npu"83+ assert nb.device_type() == "npu"
81 84 
82 85 
83-def test_xpu_alias_uses_device():86+def test_xpu_device_type_uses_device():
84- xb = XpuTorchBackend(); xb.torch_lib = "cuda"; xb.profile = {}87+ xb = XpuTorchBackend()
88+ xb.torch_lib = "cuda"
89+ xb.profile = {}
85 xb._segment_name = "xpu" # mimic _build injection90 xb._segment_name = "xpu" # mimic _build injection
86- assert xb.alias() == "xpu"91+ assert xb.device_type() == "xpu"
87- assert xb.use_device() is True92+ assert xb.has_device() is True
88 93 
89 94 
90def test_cpu_no_device():95def test_cpu_no_device():
91- cb = CpuTorchBackend(); cb.torch_lib = "cpu"; cb.profile = {}96+ cb = CpuTorchBackend()
97+ cb.torch_lib = "cpu"
98+ cb.profile = {}
92 # CPU never goes through _build; _segment_name = 'cpu' is a class attribute.99 # CPU never goes through _build; _segment_name = 'cpu' is a class attribute.
93- assert cb.alias() == "cpu"100+ assert cb.device_type() == "cpu"
94- assert cb.use_device() is False101+ assert cb.has_device() is False
95 102 
96 103 
97def test_npu_is_available_uses_torch_npu(monkeypatch):104def test_npu_is_available_uses_torch_npu(monkeypatch):
98 """NPU asys 去除:is_available 走 torch.npu.is_available(非 asys)。"""105 """NPU asys 去除:is_available 走 torch.npu.is_available(非 asys)。"""
99 import torch106 import torch
107+ 
100 class _FakeNpu:108 class _FakeNpu:
101- def is_available(self): return True109+ def is_available(self):
102- def device_count(self): return 2110+ return True
111+ 
112+ def device_count(self):
113+ return 2
114+ 
103 monkeypatch.setattr(torch, "npu", _FakeNpu(), raising=False)115 monkeypatch.setattr(torch, "npu", _FakeNpu(), raising=False)
104- nb = NpuTorchBackend(); nb.torch_lib = "npu"; nb.profile = {}116+ nb = NpuTorchBackend()
117+ nb.torch_lib = "npu"
118+ nb.profile = {}
105 assert nb.is_available() is True119 assert nb.is_available() is True
106 assert nb.device_count() == 2120 assert nb.device_count() == 2
107 121 
@@ -111,7 +125,7 @@ def test_npu_soc_series_uses_model_not_segment(monkeypatch):
111 get_npu_hw_info, NOT the segment name 'npu'. Asserts the model is passed125 get_npu_hw_info, NOT the segment name 'npu'. Asserts the model is passed
112 to get_npu_hw_info and short_soc_version is returned verbatim."""126 to get_npu_hw_info and short_soc_version is returned verbatim."""
113 import torch127 import torch
114- from ttk.core_modules.framework_api.backends import npu_backend128+ from ttk.core_modules.framework_api.backends import npu_torch_backend
115 129 
116 class _FakeNpu:130 class _FakeNpu:
117 @staticmethod131 @staticmethod
@@ -126,56 +140,59 @@ def test_npu_soc_series_uses_model_not_segment(monkeypatch):
126 captured["arg"] = full_soc_version140 captured["arg"] = full_soc_version
127 return {"short_soc_version": "Ascend910B"}141 return {"short_soc_version": "Ascend910B"}
128 142 
129- monkeypatch.setattr(npu_backend, "get_npu_hw_info", _fake_hw_info)143+ monkeypatch.setattr(npu_torch_backend, "get_npu_hw_info", _fake_hw_info)
130 144 
131- nb = NpuTorchBackend(); nb.torch_lib = "npu"; nb.profile = {}145+ nb = NpuTorchBackend()
146+ nb.torch_lib = "npu"
147+ nb.profile = {}
132 assert nb.soc_series() == "Ascend910B"148 assert nb.soc_series() == "Ascend910B"
133 # get_npu_hw_info received the MODEL ('Ascend910B3'), not the segment 'npu'.149 # get_npu_hw_info received the MODEL ('Ascend910B3'), not the segment 'npu'.
134 assert captured["arg"] == "Ascend910B3"150 assert captured["arg"] == "Ascend910B3"
135 151 
136 152 
137-# --- alias = config-driven segment name (not hardcoded per subclass) ---153+# --- device_type = config-driven segment name (not hardcoded per subclass) ---
138 154 
139-def test_build_alias_is_segment_name_not_hardcoded():155+ 
140- """_build injects _segment_name = the yaml segment key; alias() returns it156+def test_build_device_type_is_segment_name_not_hardcoded():
141- verbatim. A 'gpu' segment with torch_lib='cuda' yields alias() == 'gpu'157+ """_build injects _segment_name = the yaml segment key; device_type() returns it
158+ verbatim. A 'gpu' segment with torch_lib='cuda' yields device_type() == 'gpu'
142 (NOT the hardcoded 'xpu' the old override returned)."""159 (NOT the hardcoded 'xpu' the old override returned)."""
143- from ttk.core_modules.framework_api.backends import _build, XpuTorchBackend160+ from ttk.core_modules.framework_api.backends import _build
161+ from ttk.core_modules.framework_api.backends.xpu_torch_backend import XpuTorchBackend
144 162 
145 profile = {"torch_lib": "cuda", "profiler": {"activities": ["CPU", "CUDA"]}}163 profile = {"torch_lib": "cuda", "profiler": {"activities": ["CPU", "CUDA"]}}
146 b = _build("torch", "gpu", profile)164 b = _build("torch", "gpu", profile)
147 assert isinstance(b, XpuTorchBackend) # cuda -> generic accelerator class165 assert isinstance(b, XpuTorchBackend) # cuda -> generic accelerator class
148 assert b.torch_lib == "cuda"166 assert b.torch_lib == "cuda"
149- assert b.alias() == "gpu" # segment-name driven, not hardcoded "xpu"167+ assert b.device_type() == "gpu" # segment-name driven, not hardcoded "xpu"
150 168 
151 169 
152def test_build_arbitrary_segment_name_carried_through():170def test_build_arbitrary_segment_name_carried_through():
153- """Segment names are arbitrary: 'custom' segment -> alias() == 'custom'."""171+ """Segment names are arbitrary: 'custom' segment -> device_type() == 'custom'."""
154- from ttk.core_modules.framework_api.backends import _build, XpuTorchBackend172+ from ttk.core_modules.framework_api.backends import _build
173+ from ttk.core_modules.framework_api.backends.xpu_torch_backend import XpuTorchBackend
155 174 
156 profile = {"torch_lib": "mlu", "profiler": "builtin"}175 profile = {"torch_lib": "mlu", "profiler": "builtin"}
157 b = _build("torch", "custom", profile)176 b = _build("torch", "custom", profile)
158 assert isinstance(b, XpuTorchBackend)177 assert isinstance(b, XpuTorchBackend)
159- assert b.alias() == "custom"178+ assert b.device_type() == "custom"
160 179 
161 180 
162def test_build_npu_torch_lib_routes_to_npu_backend():181def test_build_npu_torch_lib_routes_to_npu_backend():
163 """torch_lib='npu' -> NpuTorchBackend regardless of segment name."""182 """torch_lib='npu' -> NpuTorchBackend regardless of segment name."""
164- from ttk.core_modules.framework_api.backends import _build, NpuTorchBackend183+ from ttk.core_modules.framework_api.backends import _build
165 184 
166 profile = {"torch_lib": "npu", "profiler": "builtin"}185 profile = {"torch_lib": "npu", "profiler": "builtin"}
167 b = _build("torch", "ascend", profile)186 b = _build("torch", "ascend", profile)
168 assert isinstance(b, NpuTorchBackend)187 assert isinstance(b, NpuTorchBackend)
169- assert b.alias() == "ascend" # segment name, not "npu"188+ assert b.device_type() == "ascend" # segment name, not "npu"
170 189 
171 190 
172def test_build_cpu_torch_lib_routes_to_cpu_backend():191def test_build_cpu_torch_lib_routes_to_cpu_backend():
173 """torch_lib='cpu' -> CpuTorchBackend; _segment_name still injected."""192 """torch_lib='cpu' -> CpuTorchBackend; _segment_name still injected."""
174- from ttk.core_modules.framework_api.backends import _build, CpuTorchBackend193+ from ttk.core_modules.framework_api.backends import _build
175 194 
176 profile = {"torch_lib": "cpu", "profiler": {"activities": ["CPU"]}}195 profile = {"torch_lib": "cpu", "profiler": {"activities": ["CPU"]}}
177 b = _build("torch", "cpu", profile)196 b = _build("torch", "cpu", profile)
178 assert isinstance(b, CpuTorchBackend)197 assert isinstance(b, CpuTorchBackend)
179- assert b.alias() == "cpu"198+ assert b.device_type() == "cpu"
180- 
181- 
@@ -14,30 +14,35 @@ After Task 7:
14"""14"""
15import subprocess15import subprocess
16 16 
17-from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend17+from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend
18-from ttk.core_modules.framework_api.backends.npu_backend import NpuTorchBackend18+from ttk.core_modules.framework_api.backends.npu_torch_backend import NpuTorchBackend
19 19 
20 20 
21-def test_cpu_device_name_is_cpu_alias():21+def test_cpu_device_name_is_cpu_device_type():
22- """Task 7 后 cpu device_name 走 alias(cpu 无 get_device_name)。"""22+ """Task 7 后 cpu device_name 走 device_type(cpu 无 get_device_name)。"""
23- cb = CpuTorchBackend(); cb.torch_lib = "cpu"; cb.profile = {}23+ cb = CpuTorchBackend()
24- assert cb.use_device() is False24+ cb.torch_lib = "cpu"
25+ cb.profile = {}
26+ assert cb.has_device() is False
25 assert cb.is_npu() is False27 assert cb.is_npu() is False
26- assert cb.alias() == "cpu"28+ assert cb.device_type() == "cpu"
27- # cpu has no torch.cpu.get_device_name -> override keeps alias()29+ # cpu has no torch.cpu.get_device_name -> override keeps device_type()
28 assert cb.device_name() == "cpu"30 assert cb.device_name() == "cpu"
29 31 
30 32 
31def test_soc_version_method_removed():33def test_soc_version_method_removed():
32 """soc_version 合并进 device_name,base/backend 不再暴露 soc_version。"""34 """soc_version 合并进 device_name,base/backend 不再暴露 soc_version。"""
33- cb = CpuTorchBackend(); cb.torch_lib = "cpu"; cb.profile = {}35+ cb = CpuTorchBackend()
34- assert not hasattr(cb, "soc_version"), \36+ cb.torch_lib = "cpu"
35- "soc_version must be removed in Task 7 (merged into device_name)"37+ cb.profile = {}
38+ assert not hasattr(cb, "soc_version"), "soc_version must be removed in Task 7 (merged into device_name)"
36 39 
37 40 
38def test_soc_series_default_is_device_name_model():41def test_soc_series_default_is_device_name_model():
39 """默认 soc_series() == device_name()(型号);NpuTorchBackend override short。"""42 """默认 soc_series() == device_name()(型号);NpuTorchBackend override short。"""
40- cb = CpuTorchBackend(); cb.torch_lib = "cpu"; cb.profile = {}43+ cb = CpuTorchBackend()
44+ cb.torch_lib = "cpu"
45+ cb.profile = {}
41 # base default degrades soc_series to device_name (model)46 # base default degrades soc_series to device_name (model)
42 assert cb.soc_series() == cb.device_name()47 assert cb.soc_series() == cb.device_name()
43 48 
@@ -45,48 +50,49 @@ def test_soc_series_default_is_device_name_model():
45def test_no_string_comparison_on_role():50def test_no_string_comparison_on_role():
46 """grep 确认无 =='npu'/'gpu'/'cpu' 角色字符串逻辑残留 in framework_api。51 """grep 确认无 =='npu'/'gpu'/'cpu' 角色字符串逻辑残留 in framework_api。
47 52 
48- Role comparisons (alias()/device_name()/soc_series() == 'npu'/'gpu'/'cpu')53+ Role comparisons (device_type()/device_name()/soc_series() == 'npu'/'gpu'/'cpu')
49- are forbidden — routing goes through is_npu()/alias()/use_device().54+ are forbidden — routing goes through is_npu()/device_type()/has_device().
50 torch_lib value matches (e.g. ``torch_lib == "npu"`` for class derivation in55 torch_lib value matches (e.g. ``torch_lib == "npu"`` for class derivation in
51 _build, ``torch_lib == "cpu"`` for the cpu-skip) are ALLOWED: torch_lib is56 _build, ``torch_lib == "cpu"`` for the cpu-skip) are ALLOWED: torch_lib is
52 the torch module attribute, not a role.57 the torch module attribute, not a role.
53 58 
54 Implementation note: _build derives the backend class from torch_lib (cuda/59 Implementation note: _build derives the backend class from torch_lib (cuda/
55 mlu/musa -> XpuTorchBackend, npu -> NpuTorchBackend, cpu -> CpuTorchBackend);60 mlu/musa -> XpuTorchBackend, npu -> NpuTorchBackend, cpu -> CpuTorchBackend);
56- the alias is config-driven (_segment_name = yaml segment key), so no61+ the device_type is config-driven (_segment_name = yaml segment key), so no
57 'xpu'/'gpu' role string is ever compared.62 'xpu'/'gpu' role string is ever compared.
58 """63 """
59 r = subprocess.run(64 r = subprocess.run(
60- ["grep", "-rnE", "--include=*.py",65+ ["grep", "-rnE", "--include=*.py", r"""== ?["'](npu|gpu|cpu)["']""", "ttk/core_modules/framework_api/"],
61- r'''== ?["'](npu|gpu|cpu)["']''',66+ capture_output=True,
62- "ttk/core_modules/framework_api/"],67+ text=True,
63- capture_output=True, text=True,
64 )68 )
65 # filter out allowed torch_lib value matches + docstring example text.69 # filter out allowed torch_lib value matches + docstring example text.
66- residue = [70+ residue = [ln for ln in r.stdout.splitlines() if ln and "torch_lib" not in ln and "yields alias" not in ln]
67- ln for ln in r.stdout.splitlines()
68- if ln and "torch_lib" not in ln
69- and "yields alias" not in ln
70- ]
71 assert not residue, f"string comparison residue:\n" + "\n".join(residue)71 assert not residue, f"string comparison residue:\n" + "\n".join(residue)
72 72 
73 73 
74def test_no_inequality_comparison_on_role():74def test_no_inequality_comparison_on_role():
75- """grep 确认无 !='npu'/'gpu'/'cpu' 角色字符串逻辑残留 in framework_api。"""75+ """grep 确认无 !='npu'/'gpu'/'cpu' 角色字符串逻辑残留 in framework_api。
76+ 
77+ tf_device_type value matches (e.g. ``tf_device_type != "cpu"`` in
78+ TfBackend.has_device) are ALLOWED: tf_device_type is a config attribute,
79+ not a role.
80+ """
76 r = subprocess.run(81 r = subprocess.run(
77- ["grep", "-rnE", "--include=*.py",82+ ["grep", "-rnE", "--include=*.py", r"""!= ?["'](npu|gpu|cpu)["']""", "ttk/core_modules/framework_api/"],
78- r'''!= ?["'](npu|gpu|cpu)["']''',83+ capture_output=True,
79- "ttk/core_modules/framework_api/"],84+ text=True,
80- capture_output=True, text=True,
81 )85 )
82- assert r.returncode != 0, f"string inequality residue:\n{r.stdout}"86+ residue = [ln for ln in r.stdout.splitlines() if ln and "torch_lib" not in ln and "tf_device_type" not in ln]
87+ assert not residue, f"string inequality residue:\n" + "\n".join(residue)
83 88 
84 89 
85def test_no_soc_version_residue_in_ttk():90def test_no_soc_version_residue_in_ttk():
86 """全仓 grep 确认 .soc_version( 调用零残留(已合并进 device_name)。"""91 """全仓 grep 确认 .soc_version( 调用零残留(已合并进 device_name)。"""
87 r = subprocess.run(92 r = subprocess.run(
88- ["grep", "-rnE", r'\.soc_version\(', "ttk/"],93+ ["grep", "-rnE", r"\.soc_version\(", "ttk/"],
89- capture_output=True, text=True,94+ capture_output=True,
95+ text=True,
90 )96 )
91 assert r.returncode != 0, f".soc_version() residue:\n{r.stdout}"97 assert r.returncode != 0, f".soc_version() residue:\n{r.stdout}"
92 98 
@@ -99,7 +105,9 @@ def test_get_profiler_uses_is_npu_and_profile_not_device_name():
99 must still resolve correctly -> proves no string compare on device_name.105 must still resolve correctly -> proves no string compare on device_name.
100 """106 """
101 from ttk.core_modules.framework_api.profiler import (107 from ttk.core_modules.framework_api.profiler import (
102- get_profiler, TorchProfiler, WallClockProfiler,108+ get_profiler,
109+ TorchProfiler,
110+ WallClockProfiler,
103 )111 )
104 112 
105 class _ModelNameBackend:113 class _ModelNameBackend:
@@ -108,13 +116,12 @@ def test_get_profiler_uses_is_npu_and_profile_not_device_name():
108 torch_lib = "cuda"116 torch_lib = "cuda"
109 # _build injects torch_lib into profile; mirror that invariant here so117 # _build injects torch_lib into profile; mirror that invariant here so
110 # TorchProfiler.__init__ (which reads profile["torch_lib"] per §5.3) works.118 # TorchProfiler.__init__ (which reads profile["torch_lib"] per §5.3) works.
111- profile = {"torch_lib": "cuda",119+ profile = {"torch_lib": "cuda", "profiler": {"activities": ["CPU", "CUDA"]}}
112- "profiler": {"activities": ["CPU", "CUDA"]}}
113 120 
114 def device_name(self, dev_id=0):121 def device_name(self, dev_id=0):
115 return "AscendWhatever-Model-Name" # deliberately non-segment122 return "AscendWhatever-Model-Name" # deliberately non-segment
116 123 
117- def alias(self):124+ def device_type(self):
118 return "xpu"125 return "xpu"
119 126 
120 def is_npu(self):127 def is_npu(self):
@@ -11,7 +11,7 @@ in order, _probe each non-cpu profile, build first hit; cpu fallback.
11import importlib11import importlib
12 12 
13from ttk.core_modules.framework_api.backends import get_backend, _probe13from ttk.core_modules.framework_api.backends import get_backend, _probe
14-from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend14+from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend
15 15 
16 16 
17def test_probe_cuda_skips_import(monkeypatch):17def test_probe_cuda_skips_import(monkeypatch):
@@ -53,7 +53,5 @@ def test_probe_catches_runtime_error(monkeypatch):
53 53 
54def test_auto_detect_falls_back_to_cpu(monkeypatch):54def test_auto_detect_falls_back_to_cpu(monkeypatch):
55 """All miss -> cpu fallback."""55 """All miss -> cpu fallback."""
56- monkeypatch.setattr(56+ monkeypatch.setattr("ttk.core_modules.framework_api.backends._hw_profiles", lambda fw: {})
57- "ttk.core_modules.framework_api.backends._hw_profiles", lambda fw: {}
58- )
59 assert isinstance(get_backend(force_cpu=False), CpuTorchBackend)57 assert isinstance(get_backend(force_cpu=False), CpuTorchBackend)
@@ -28,15 +28,14 @@ class _FakeBackend:
28 def __init__(self, profile):28 def __init__(self, profile):
29 self.profile = profile29 self.profile = profile
30 30 
31- def alias(self):31+ def device_type(self):
32 return "fake"32 return "fake"
33 33 
34 def is_npu(self):34 def is_npu(self):
35 return False35 return False
36 36 
37 37 
38-def _make_profiler_via_real_init(monkeypatch, activities, torch_lib="cuda",38+def _make_profiler_via_real_init(monkeypatch, activities, torch_lib="cuda", device_time_attr=None):
39- device_time_attr=None):
40 """Build a TorchProfiler through the real __init__ with profile() stubbed.39 """Build a TorchProfiler through the real __init__ with profile() stubbed.
41 40 
42 monkeypatches torch.profiler.profile so no real profiler is constructed;41 monkeypatches torch.profiler.profile so no real profiler is constructed;
@@ -96,7 +95,7 @@ def test_device_time_fallback_legacy_self_device_total():
96 """95 """
97 prof = TorchProfiler.__new__(TorchProfiler)96 prof = TorchProfiler.__new__(TorchProfiler)
98 prof._device_time_attr = None97 prof._device_time_attr = None
99- evt = _Evt(self_cuda_time_total=11.0) # 无 self_device_time_total98+ evt = _Evt(self_cuda_time_total=11.0) # 无 self_device_time_total
100 assert prof._device_time(evt, "cuda") == 11.099 assert prof._device_time(evt, "cuda") == 11.0
101 100 
102 101 
@@ -110,10 +109,13 @@ def test_device_time_final_no_attrs_returns_zero():
110 109 
111# --- I1: __init__ data-driven activities + _device contract ---110# --- I1: __init__ data-driven activities + _device contract ---
112 111 
112+ 
113def test_init_device_acts_from_activities_with_cuda(monkeypatch):113def test_init_device_acts_from_activities_with_cuda(monkeypatch):
114 """activities=[CPU, CUDA] -> _device_acts=["CUDA"], _device=torch_lib."""114 """activities=[CPU, CUDA] -> _device_acts=["CUDA"], _device=torch_lib."""
115 prof, backend, captured = _make_profiler_via_real_init(115 prof, backend, captured = _make_profiler_via_real_init(
116- monkeypatch, activities=["CPU", "CUDA"], torch_lib="cuda",116+ monkeypatch,
117+ activities=["CPU", "CUDA"],
118+ torch_lib="cuda",
117 )119 )
118 assert prof._device_acts == ["CUDA"]120 assert prof._device_acts == ["CUDA"]
119 assert prof._device == "cuda"121 assert prof._device == "cuda"
@@ -124,7 +126,9 @@ def test_init_device_acts_from_activities_with_cuda(monkeypatch):
124def test_init_device_acts_empty_for_cpu_only(monkeypatch):126def test_init_device_acts_empty_for_cpu_only(monkeypatch):
125 """activities=[CPU] -> _device_acts=[] (CPU-only profile)."""127 """activities=[CPU] -> _device_acts=[] (CPU-only profile)."""
126 prof, backend, captured = _make_profiler_via_real_init(128 prof, backend, captured = _make_profiler_via_real_init(
127- monkeypatch, activities=["CPU"], torch_lib="cpu",129+ monkeypatch,
130+ activities=["CPU"],
131+ torch_lib="cpu",
128 )132 )
129 assert prof._device_acts == []133 assert prof._device_acts == []
130 assert prof._device == "cpu"134 assert prof._device == "cpu"
@@ -137,7 +141,9 @@ def test_init_device_is_torch_lib_not_activity_name(monkeypatch):
137 (Here simulated with torch_lib 'musa' + activity 'MUSA'.)141 (Here simulated with torch_lib 'musa' + activity 'MUSA'.)
138 """142 """
139 prof, backend, captured = _make_profiler_via_real_init(143 prof, backend, captured = _make_profiler_via_real_init(
140- monkeypatch, activities=["CPU", "CUDA"], torch_lib="musa",144+ monkeypatch,
145+ activities=["CPU", "CUDA"],
146+ torch_lib="musa",
141 )147 )
142 assert prof._device == "musa" # torch_lib, not "cuda" (activity name)148 assert prof._device == "musa" # torch_lib, not "cuda" (activity name)
143 149 
@@ -151,7 +157,9 @@ def test_init_unknown_activity_raises_valueerror(monkeypatch):
151def test_init_device_time_attr_passed_through(monkeypatch):157def test_init_device_time_attr_passed_through(monkeypatch):
152 """device_time_attr is read from profile['profiler'] when present."""158 """device_time_attr is read from profile['profiler'] when present."""
153 prof, backend, captured = _make_profiler_via_real_init(159 prof, backend, captured = _make_profiler_via_real_init(
154- monkeypatch, activities=["CPU", "CUDA"], torch_lib="cuda",160+ monkeypatch,
161+ activities=["CPU", "CUDA"],
162+ torch_lib="cuda",
155 device_time_attr="cuda_time_total",163 device_time_attr="cuda_time_total",
156 )164 )
157 assert prof._device_time_attr == "cuda_time_total"165 assert prof._device_time_attr == "cuda_time_total"
@@ -159,6 +167,7 @@ def test_init_device_time_attr_passed_through(monkeypatch):
159 167 
160# --- I1: result() cpu + device branches ---168# --- I1: result() cpu + device branches ---
161 169 
170+ 
162class _FakeEvent:171class _FakeEvent:
163 """Stand-in for a torch.profiler Event for result() tests."""172 """Stand-in for a torch.profiler Event for result() tests."""
164 173 
@@ -225,16 +234,15 @@ def test_result_device_branch_collects_kernels():
225 234 
226def test_result_device_branch_uses_explicit_attr():235def test_result_device_branch_uses_explicit_attr():
227 """device_time_attr override takes precedence over self_device_time_total."""236 """device_time_attr override takes precedence over self_device_time_total."""
228- events = [_FakeEvent("k1", 1, cuda_time_total=999.0,237+ events = [_FakeEvent("k1", 1, cuda_time_total=999.0, self_cuda_time_total=200.0)]
229- self_cuda_time_total=200.0)]238+ prof = _prof_for_result(events, device_acts=["CUDA"], device="cuda", device_time_attr="cuda_time_total")
230- prof = _prof_for_result(events, device_acts=["CUDA"], device="cuda",
231- device_time_attr="cuda_time_total")
232 res = prof.result(_FakeBackend({}), repeat_count=1)239 res = prof.result(_FakeBackend({}), repeat_count=1)
233 assert res.kernel_details.kernels[0].device_us == 999.0240 assert res.kernel_details.kernels[0].device_us == 999.0
234 241 
235 242 
236# --- I5: get_profiler RuntimeError includes backend alias ---243# --- I5: get_profiler RuntimeError includes backend alias ---
237 244 
245+ 
238def test_get_profiler_npu_api_on_non_npu_includes_backend_alias():246def test_get_profiler_npu_api_on_non_npu_includes_backend_alias():
239 """I5: torch_npu.* on non-NPU backend -> RuntimeError names current alias."""247 """I5: torch_npu.* on non-NPU backend -> RuntimeError names current alias."""
240 backend = _FakeBackend({"torch_lib": "cuda"})248 backend = _FakeBackend({"torch_lib": "cuda"})
@@ -251,10 +259,11 @@ def test_get_profiler_torch_with_npu_builtin_returns_npu_profiler():
251 259 
252 class _NpuBackend:260 class _NpuBackend:
253 """Mock NPU backend with builtin profiler config."""261 """Mock NPU backend with builtin profiler config."""
262+ 
254 def __init__(self):263 def __init__(self):
255 self.profile = {"profiler": "builtin"}264 self.profile = {"profiler": "builtin"}
256 265 
257- def alias(self):266+ def device_type(self):
258 return "npu"267 return "npu"
259 268 
260 def is_npu(self):269 def is_npu(self):
@@ -262,5 +271,4 @@ def test_get_profiler_torch_with_npu_builtin_returns_npu_profiler():
262 271 
263 backend = _NpuBackend()272 backend = _NpuBackend()
264 profiler = get_profiler("torch.add", backend)273 profiler = get_profiler("torch.add", backend)
265- assert isinstance(profiler, NpuProfiler), \274+ assert isinstance(profiler, NpuProfiler), f"Expected NpuProfiler, got {type(profiler).__name__}"
266- f"Expected NpuProfiler, got {type(profiler).__name__}"
@@ -21,7 +21,7 @@ class _FakeBackend:
21 def device_name(self, dev_id=0):21 def device_name(self, dev_id=0):
22 return "AscendGPU-Model-Name"22 return "AscendGPU-Model-Name"
23 23 
24- def alias(self):24+ def device_type(self):
25 return "xpu"25 return "xpu"
26 26 
27 def is_npu(self):27 def is_npu(self):
@@ -1,47 +1,49 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2# -*- coding: UTF-8 -*-2# -*- coding: UTF-8 -*-
3# Copyright (c) 2026 Huawei Technologies Co., Ltd.3# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4-# This program is free software; you can redistribute it and/or modify it under the terms of conditions of4+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
5# CANN Open Software License Agreement Version 2.0 (the "License").5# CANN Open Software License Agreement Version 2.0 (the "License").
6# See LICENSE in the root of the software repository for the full text of the License.6# See LICENSE in the root of the software repository for the full text of the License.
7 7 
8-"""Tests for non-contiguous stride preservation in profiling_utils.8+"""Tests for non-contiguous stride preservation via Backend.clone / to_device.
9 9 
10-clone_preserving_stride and _to_device_preserving_stride must keep the10+clone() and to_device(preserve_stride=True) must keep the original (possibly
11-original (possibly non-contiguous) stride of a tensor, unlike .clone() /11+non-contiguous) stride of a tensor, unlike .clone() / .to(device) / .npu()
12-.to(device) / .npu() which materialize non-contiguous views into contiguous12+which materialize non-contiguous views into contiguous tensors.
13-tensors.
14"""13"""
14+ 
15import os15import os
16 16 
17import numpy as np17import numpy as np
18import pytest18import pytest
19import torch19import torch
20 20 
21-from ttk.core_modules.framework_api.profiling_utils import (21+from ttk.core_modules.framework_api.backends.torch_backend import TorchBackend
22- clone_preserving_stride,
23- _to_device_preserving_stride,
24-)
25 22 
26 23 
27-def _make_non_contiguous(shape=(1, 128, 8, 4), stride=(16384, 64, 4, 1),24+def _make_non_contiguous(shape=(1, 128, 8, 4), stride=(16384, 64, 4, 1), dtype=torch.float32, device="cpu"):
28- dtype=torch.float32, device="cpu"):
29 """Build a non-contiguous view via as_strided over a larger storage."""25 """Build a non-contiguous view via as_strided over a larger storage."""
30 storage = torch.arange(26 storage = torch.arange(
31 max(stride[0] * shape[0], 1) if len(stride) == len(shape) else np.prod(shape),27 max(stride[0] * shape[0], 1) if len(stride) == len(shape) else np.prod(shape),
32- dtype=dtype, device=device,28+ dtype=dtype,
29+ device=device,
33 ).reshape(-1)30 ).reshape(-1)
34 return torch.as_strided(storage, shape, stride, 0)31 return torch.as_strided(storage, shape, stride, 0)
35 32 
36 33 
37class TestClonePreservingStride:34class TestClonePreservingStride:
35+ def _backend(self):
36+ tb = TorchBackend()
37+ tb.torch_lib = "cpu"
38+ tb.profile = {}
39+ return tb
38 40 
39 def test_none_returns_none(self):41 def test_none_returns_none(self):
40- assert clone_preserving_stride(None) is None42+ assert self._backend().clone(None) is None
41 43 
42 def test_contiguous_falls_back_to_clone(self):44 def test_contiguous_falls_back_to_clone(self):
43 t = torch.arange(12, dtype=torch.float32).reshape(3, 4)45 t = torch.arange(12, dtype=torch.float32).reshape(3, 4)
44- out = clone_preserving_stride(t)46+ out = self._backend().clone(t)
45 assert out is not t47 assert out is not t
46 assert torch.equal(out, t)48 assert torch.equal(out, t)
47 assert out.is_contiguous()49 assert out.is_contiguous()
@@ -49,20 +51,20 @@ class TestClonePreservingStride:
49 def test_non_contiguous_stride_preserved(self):51 def test_non_contiguous_stride_preserved(self):
50 t = _make_non_contiguous()52 t = _make_non_contiguous()
51 assert not t.is_contiguous()53 assert not t.is_contiguous()
52- out = clone_preserving_stride(t)54+ out = self._backend().clone(t)
53 assert out.stride() == t.stride()55 assert out.stride() == t.stride()
54 assert not out.is_contiguous()56 assert not out.is_contiguous()
55 57 
56 def test_non_contiguous_data_equal(self):58 def test_non_contiguous_data_equal(self):
57 t = _make_non_contiguous()59 t = _make_non_contiguous()
58- out = clone_preserving_stride(t)60+ out = self._backend().clone(t)
59 assert torch.equal(out, t)61 assert torch.equal(out, t)
60 62 
61 def test_non_contiguous_bfloat16_stride_preserved(self):63 def test_non_contiguous_bfloat16_stride_preserved(self):
62 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.bfloat16)64 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.bfloat16)
63 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)65 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
64 assert not t.is_contiguous()66 assert not t.is_contiguous()
65- out = clone_preserving_stride(t)67+ out = self._backend().clone(t)
66 assert out.stride() == (16384, 64, 4, 1)68 assert out.stride() == (16384, 64, 4, 1)
67 assert not out.is_contiguous()69 assert not out.is_contiguous()
68 assert torch.equal(out, t)70 assert torch.equal(out, t)
@@ -70,31 +72,53 @@ class TestClonePreservingStride:
70 def test_clone_does_not_flatten(self):72 def test_clone_does_not_flatten(self):
71 t = _make_non_contiguous()73 t = _make_non_contiguous()
72 assert not t.is_contiguous()74 assert not t.is_contiguous()
73- out = clone_preserving_stride(t)75+ out = self._backend().clone(t)
74 assert out.stride() == t.stride()76 assert out.stride() == t.stride()
75 assert out.shape == t.shape77 assert out.shape == t.shape
76 assert out.dtype == t.dtype78 assert out.dtype == t.dtype
77 79 
78 def test_independent_storage(self):80 def test_independent_storage(self):
79 t = _make_non_contiguous()81 t = _make_non_contiguous()
80- out = clone_preserving_stride(t)82+ out = self._backend().clone(t)
81 out[0, 0, 0, 0] = 999.083 out[0, 0, 0, 0] = 999.0
82 assert t[0, 0, 0, 0].item() != 999.084 assert t[0, 0, 0, 0].item() != 999.0
83 85 
86+ def test_clone_preserves_stride_gap_data(self):
87+ """Clone must copy the *entire* underlying storage, not just visible elements.
84 88 
85-class _FakeBackend:89+ ``empty_strided`` + ``copy_`` leaves stride-gap memory uninitialized, so
86- """Minimal backend for _to_device_preserving_stride."""90+ an operator reading into gaps sees garbage that differs from the original.
91+ The clone's storage must be the same size as the original's and contain
92+ identical gap data.
93+ """
94+ storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)
95+ t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
96+ out = self._backend().clone(t)
97+ assert out.untyped_storage().size() == t.untyped_storage().size()
98+ gap_idx = 8200
99+ out_flat = torch.as_strided(
100+ torch.empty(0, dtype=out.dtype, device=out.device).set_(
101+ out.untyped_storage(), 0, (out.untyped_storage().size() // out.element_size(),), (1,)
102+ ),
103+ (out.untyped_storage().size() // out.element_size(),),
104+ (1,),
105+ 0,
106+ )
107+ assert out_flat[gap_idx].item() == storage[gap_idx].item()
87 108 
88- def __init__(self, torch_lib="npu"):109+ def test_clone_preserves_nonzero_offset(self):
89- self.torch_lib = torch_lib110+ storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)
90- 111+ t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 128)
91- def to_device(self, tensor, dev_id=0):112+ out = self._backend().clone(t)
92- return getattr(tensor, self.torch_lib)(dev_id)113+ assert out.stride() == t.stride()
114+ assert out.storage_offset() == t.storage_offset()
115+ assert torch.equal(out, t)
93 116 
94 117 
95_npu_ok = False118_npu_ok = False
96try:119try:
97 import torch_npu120 import torch_npu
121+ 
98 torch_npu.npu.set_device(0)122 torch_npu.npu.set_device(0)
99 _npu_ok = torch_npu.npu.is_available()123 _npu_ok = torch_npu.npu.is_available()
100except Exception:124except Exception:
@@ -106,7 +130,18 @@ _ASCEND_ENV_SNAPSHOT = {k: os.environ.get(k) for k in _ASCEND_ENV_KEYS}
106 130 
107@pytest.mark.skipif(not _npu_ok, reason="NPU not available")131@pytest.mark.skipif(not _npu_ok, reason="NPU not available")
108class TestToDevicePreservingStrideNPU:132class TestToDevicePreservingStrideNPU:
109- """NPU: .npu() flattens non-contiguous; _to_device_preserving_stride must keep stride."""133+ """NPU: .npu() flattens non-contiguous; to_device(preserve_stride=True) must keep stride.
134+ 
135+ Uses TorchBackend._to_device_preserving_stride directly (the method that
136+ NpuTorchBackend inherits) to avoid NpuTorchBackend.to_device's dtype-specific
137+ fast path which is covered by other integration tests.
138+ """
139+ 
140+ def _move(self, tensor, dev_id=0):
141+ tb = TorchBackend()
142+ tb.torch_lib = "npu"
143+ tb.profile = {}
144+ return tb._to_device_preserving_stride(tensor, dev_id)
110 145 
111 @pytest.fixture(autouse=True)146 @pytest.fixture(autouse=True)
112 def _restore_ascend_env(self, monkeypatch):147 def _restore_ascend_env(self, monkeypatch):
@@ -117,13 +152,11 @@ class TestToDevicePreservingStrideNPU:
117 yield152 yield
118 153 
119 def test_none_returns_none(self):154 def test_none_returns_none(self):
120- backend = _FakeBackend("npu")155+ assert self._move(None) is None
121- assert _to_device_preserving_stride(None, backend, 0) is None
122 156 
123 def test_contiguous_uses_npu(self):157 def test_contiguous_uses_npu(self):
124 t = torch.arange(12, dtype=torch.float32)158 t = torch.arange(12, dtype=torch.float32)
125- backend = _FakeBackend("npu")159+ out = self._move(t)
126- out = _to_device_preserving_stride(t, backend, 0)
127 assert out.is_contiguous()160 assert out.is_contiguous()
128 assert torch.equal(out.cpu(), t)161 assert torch.equal(out.cpu(), t)
129 assert out.device.type == "npu"162 assert out.device.type == "npu"
@@ -132,8 +165,7 @@ class TestToDevicePreservingStrideNPU:
132 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)165 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)
133 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)166 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
134 assert not t.is_contiguous()167 assert not t.is_contiguous()
135- backend = _FakeBackend("npu")168+ out = self._move(t)
136- out = _to_device_preserving_stride(t, backend, 0)
137 assert out.stride() == (16384, 64, 4, 1)169 assert out.stride() == (16384, 64, 4, 1)
138 assert not out.is_contiguous()170 assert not out.is_contiguous()
139 assert out.device.type == "npu"171 assert out.device.type == "npu"
@@ -141,16 +173,30 @@ class TestToDevicePreservingStrideNPU:
141 def test_non_contiguous_data_equal(self):173 def test_non_contiguous_data_equal(self):
142 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)174 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)
143 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)175 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
144- backend = _FakeBackend("npu")176+ out = self._move(t)
145- out = _to_device_preserving_stride(t, backend, 0)
146 assert torch.equal(out.cpu(), t)177 assert torch.equal(out.cpu(), t)
147 178 
179+ def test_non_contiguous_gap_data_equal(self):
180+ """to_device must copy the full storage so stride-gap data matches."""
181+ storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.float32)
182+ t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
183+ out = self._move(t)
184+ assert out.untyped_storage().size() == t.untyped_storage().size()
185+ out_flat = torch.as_strided(
186+ torch.empty(0, dtype=out.dtype, device=out.device).set_(
187+ out.untyped_storage(), 0, (out.untyped_storage().size() // out.element_size(),), (1,)
188+ ),
189+ (out.untyped_storage().size() // out.element_size(),),
190+ (1,),
191+ 0,
192+ )
193+ assert out_flat[4096].cpu().item() == storage[4096].item()
194+ 
148 def test_bfloat16_non_contiguous_stride_preserved(self):195 def test_bfloat16_non_contiguous_stride_preserved(self):
149 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.bfloat16)196 storage = torch.arange(2 * 256 * 8 * 4, dtype=torch.bfloat16)
150 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)197 t = torch.as_strided(storage, (1, 128, 8, 4), (16384, 64, 4, 1), 0)
151 assert not t.is_contiguous()198 assert not t.is_contiguous()
152- backend = _FakeBackend("npu")199+ out = self._move(t)
153- out = _to_device_preserving_stride(t, backend, 0)
154 assert out.stride() == (16384, 64, 4, 1)200 assert out.stride() == (16384, 64, 4, 1)
155 assert not out.is_contiguous()201 assert not out.is_contiguous()
156 202 
@@ -10,6 +10,7 @@ These tests never run cannsim / camodel — they mock ``ASCEND_TOOLKIT_HOME`` an
10the camodel directory resolution, mirroring the existing simulator backend10the camodel directory resolution, mirroring the existing simulator backend
11tests (``test_simulator_backend.py``).11tests (``test_simulator_backend.py``).
12"""12"""
13+ 
13import os14import os
14from types import SimpleNamespace15from types import SimpleNamespace
15 16 
@@ -64,10 +65,10 @@ class _FakeBackend:
64 def __init__(self):65 def __init__(self):
65 self._dev = True66 self._dev = True
66 67 
67- def use_device(self):68+ def has_device(self):
68 return self._dev69 return self._dev
69 70 
70- def alias(self):71+ def device_type(self):
71 return "npu"72 return "npu"
72 73 
73 74 
@@ -91,7 +92,7 @@ class TestFrameworkApiInstanceNpusim:
91 )92 )
92 monkeypatch.setattr(93 monkeypatch.setattr(
93 "ttk.core_modules.framework_api.instance.get_backend",94 "ttk.core_modules.framework_api.instance.get_backend",
94- lambda force_cpu: _FakeBackend(),95+ lambda force_cpu, framework="torch": _FakeBackend(),
95 )96 )
96 monkeypatch.delenv("LD_LIBRARY_PATH", raising=False)97 monkeypatch.delenv("LD_LIBRARY_PATH", raising=False)
97 sw = self._switches(tmp_path)98 sw = self._switches(tmp_path)
@@ -171,9 +172,9 @@ class TestCollectSimReport:
171 e2e_profiling._collect_sim_report(self._testcase(), sw)172 e2e_profiling._collect_sim_report(self._testcase(), sw)
172 173 
173 case_path = case_dir(sw, "add_f32_01")174 case_path = case_dir(sw, "add_f32_01")
174- assert (case_path / "instr.bin").is_file() # moved into case dir175+ assert (case_path / "instr.bin").is_file() # moved into case dir
175- assert not (tmp_path / "instr.bin").exists() # removed from worker cwd176+ assert not (tmp_path / "instr.bin").exists() # removed from worker cwd
176- assert reported == [case_path] # report triggered177+ assert reported == [case_path] # report triggered
177 178 
178 def test_collect_without_report(self, tmp_path, monkeypatch):179 def test_collect_without_report(self, tmp_path, monkeypatch):
179 from ttk.core_modules.framework_api import profiling as e2e_profiling180 from ttk.core_modules.framework_api import profiling as e2e_profiling
@@ -191,8 +192,8 @@ class TestCollectSimReport:
191 e2e_profiling._collect_sim_report(self._testcase(), sw)192 e2e_profiling._collect_sim_report(self._testcase(), sw)
192 193 
193 case_path = case_dir(sw, "add_f32_01")194 case_path = case_dir(sw, "add_f32_01")
194- assert (case_path / "instr.bin").is_file() # still collected195+ assert (case_path / "instr.bin").is_file() # still collected
195- assert not reported # but no report196+ assert not reported # but no report
196 197 
197 def test_skip_non_npusim(self, tmp_path, monkeypatch):198 def test_skip_non_npusim(self, tmp_path, monkeypatch):
198 from ttk.core_modules.framework_api import profiling as e2e_profiling199 from ttk.core_modules.framework_api import profiling as e2e_profiling
@@ -208,7 +209,7 @@ class TestCollectSimReport:
208 209 
209 e2e_profiling._collect_sim_report(self._testcase(), sw)210 e2e_profiling._collect_sim_report(self._testcase(), sw)
210 211 
211- assert (tmp_path / "instr.bin").exists() # untouched212+ assert (tmp_path / "instr.bin").exists() # untouched
212 assert not reported213 assert not reported
213 214 
214 def test_skip_missing_or_empty_instr(self, tmp_path, monkeypatch):215 def test_skip_missing_or_empty_instr(self, tmp_path, monkeypatch):
@@ -227,4 +228,4 @@ class TestCollectSimReport:
227 e2e_profiling._collect_sim_report(self._testcase(), sw)228 e2e_profiling._collect_sim_report(self._testcase(), sw)
228 229 
229 assert not reported230 assert not reported
230- assert (tmp_path / "instr.bin").exists() # empty file left in place231+ assert (tmp_path / "instr.bin").exists() # empty file left in place
@@ -84,7 +84,11 @@ def test_round_trip_uses_only_typed_data_files(tmp_path, file_format):
84 store = ManualDataStore(tmp_path)84 store = ManualDataStore(tmp_path)
85 85 
86 case_dir = store.write_case(86 case_dir = store.write_case(
87- case, "aclnn", inputs, goldens, scalars=scalars,87+ case,
88+ "aclnn",
89+ inputs,
90+ goldens,
91+ scalars=scalars,
88 file_format=file_format,92 file_format=file_format,
89 )93 )
90 loaded = store.load_case(case, "aclnn")94 loaded = store.load_case(case, "aclnn")
@@ -98,11 +102,7 @@ def test_round_trip_uses_only_typed_data_files(tmp_path, file_format):
98 f"input_0_float32.{file_format}",102 f"input_0_float32.{file_format}",
99 f"input_1_float32.{file_format}",103 f"input_1_float32.{file_format}",
100 f"scalar_0_float32.{file_format}",104 f"scalar_0_float32.{file_format}",
101- (105+ ("golden_0_float32__shape_2.bin" if file_format == "bin" else f"golden_0_float32.{file_format}"),
102- "golden_0_float32__shape_2.bin"
103- if file_format == "bin"
104- else f"golden_0_float32.{file_format}"
105- ),
106 }106 }
107 107 
108 108 
@@ -115,9 +115,7 @@ def test_complete_dataset_remains_loadable_after_directory_move(tmp_path, file_f
115 source = tmp_path / "source"115 source = tmp_path / "source"
116 destination = tmp_path / "destination"116 destination = tmp_path / "destination"
117 117 
118- ManualDataStore(source).write_case(118+ ManualDataStore(source).write_case(case, "aclnn", inputs, goldens, scalars=scalars, file_format=file_format)
119- case, "aclnn", inputs, goldens, scalars=scalars, file_format=file_format
120- )
121 shutil.copytree(source, destination)119 shutil.copytree(source, destination)
122 loaded = ManualDataStore(destination).load_case(case, "aclnn")120 loaded = ManualDataStore(destination).load_case(case, "aclnn")
123 loaded_goldens = loaded.load_goldens(references=goldens)121 loaded_goldens = loaded.load_goldens(references=goldens)
@@ -132,8 +130,7 @@ def test_npy_round_trip_restores_custom_bfloat16_dtype(tmp_path):
132 case = _e2e_case("bfloat16_npy")130 case = _e2e_case("bfloat16_npy")
133 case.tensor_dtypes = ("bfloat16", "bfloat16")131 case.tensor_dtypes = ("bfloat16", "bfloat16")
134 dtype = resolve_custom_numpy_dtypes(("bfloat16",))[0]132 dtype = resolve_custom_numpy_dtypes(("bfloat16",))[0]
135- inputs = [np.arange(9, dtype=np.float32).astype(dtype).reshape(3, 3),133+ inputs = [np.arange(9, dtype=np.float32).astype(dtype).reshape(3, 3), np.zeros((2, 2), dtype=dtype)]
136- np.zeros((2, 2), dtype=dtype)]
137 golden = [np.ones((2, 2), dtype=dtype)]134 golden = [np.ones((2, 2), dtype=dtype)]
138 store = ManualDataStore(tmp_path)135 store = ManualDataStore(tmp_path)
139 136 
@@ -187,9 +184,7 @@ def test_kernel_round_trip_uses_kernel_csv_shapes(tmp_path, file_format):
187 goldens = [np.array([3.0, 4.0], np.float32)]184 goldens = [np.array([3.0, 4.0], np.float32)]
188 store = ManualDataStore(tmp_path)185 store = ManualDataStore(tmp_path)
189 186 
190- case_dir = store.write_case(187+ case_dir = store.write_case(case, "kernel", inputs, goldens, file_format=file_format)
191- case, "kernel", inputs, goldens, file_format=file_format
192- )
193 loaded = store.load_case(case, "kernel")188 loaded = store.load_case(case, "kernel")
194 loaded_goldens = loaded.load_goldens(189 loaded_goldens = loaded.load_goldens(
195 shapes=case.flat_output_shapes,190 shapes=case.flat_output_shapes,
@@ -230,10 +225,12 @@ def test_e2e_none_golden_suppresses_optional_device_output(tmp_path):
230 )225 )
231 226 
232 loaded = store.load_case(case, "e2e")227 loaded = store.load_case(case, "e2e")
233- goldens = loaded.load_goldens(references=[228+ goldens = loaded.load_goldens(
234- np.zeros((2, 2), np.float32),229+ references=[
235- np.zeros((1, 2), np.float32),230+ np.zeros((2, 2), np.float32),
236- ])231+ np.zeros((1, 2), np.float32),
232+ ]
233+ )
237 234 
238 np.testing.assert_array_equal(goldens[0], np.ones((2, 2), np.float32))235 np.testing.assert_array_equal(goldens[0], np.ones((2, 2), np.float32))
239 assert goldens[1] is None236 assert goldens[1] is None
@@ -260,7 +257,7 @@ def test_tensor_list_grouping_is_rebuilt_from_csv_structure(tmp_path):
260 257 
261 loaded = store.load_case(case, "e2e")258 loaded = store.load_case(case, "e2e")
262 switches = SimpleNamespace(plugin_path=("must-not-be-scanned",))259 switches = SimpleNamespace(plugin_path=("must-not-be-scanned",))
263- backend = SimpleNamespace(alias=lambda: "npu")260+ backend = SimpleNamespace(device_type=lambda: "npu", inputs_from_numpy=lambda tc, ri: ri)
264 generate_inputs(case, switches, backend, object(), stored_inputs=loaded.inputs)261 generate_inputs(case, switches, backend, object(), stored_inputs=loaded.inputs)
265 262 
266 assert isinstance(case.tensors[0], list)263 assert isinstance(case.tensors[0], list)
@@ -274,7 +271,8 @@ def test_changed_csv_dtype_is_rejected_by_filename(tmp_path):
274 case = _e2e_case("contract_case")271 case = _e2e_case("contract_case")
275 store = ManualDataStore(tmp_path)272 store = ManualDataStore(tmp_path)
276 store.write_case(273 store.write_case(
277- case, "e2e",274+ case,
275+ "e2e",
278 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],276 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
279 [np.zeros((2, 2), np.float32)],277 [np.zeros((2, 2), np.float32)],
280 )278 )
@@ -291,7 +289,8 @@ def test_precision_fields_can_change_without_invalidating_prepared_data(tmp_path
291 case.absolute_precision = (1e-5,)289 case.absolute_precision = (1e-5,)
292 store = ManualDataStore(tmp_path)290 store = ManualDataStore(tmp_path)
293 store.write_case(291 store.write_case(
294- case, "e2e",292+ case,
293+ "e2e",
295 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],294 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
296 [np.zeros((2, 2), np.float32)],295 [np.zeros((2, 2), np.float32)],
297 )296 )
@@ -306,7 +305,8 @@ def test_corrupt_file_is_rejected_before_loading(tmp_path):
306 case = _e2e_case("hash_case")305 case = _e2e_case("hash_case")
307 store = ManualDataStore(tmp_path)306 store = ManualDataStore(tmp_path)
308 case_dir = store.write_case(307 case_dir = store.write_case(
309- case, "e2e",308+ case,
309+ "e2e",
310 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],310 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
311 [np.zeros((2, 2), np.float32)],311 [np.zeros((2, 2), np.float32)],
312 )312 )
@@ -334,6 +334,7 @@ def test_self_describing_format_rejects_same_size_wrong_shape(tmp_path, file_for
334 np.save(path, wrong_shape)334 np.save(path, wrong_shape)
335 else:335 else:
336 import torch336 import torch
337+ 
337 torch.save(torch.from_numpy(wrong_shape), path)338 torch.save(torch.from_numpy(wrong_shape), path)
338 339 
339 with pytest.raises(ManualDataError, match="stored shape"):340 with pytest.raises(ManualDataError, match="stored shape"):
@@ -363,6 +364,7 @@ def test_self_describing_format_rejects_same_size_wrong_dtype(tmp_path, file_for
363 np.save(path, wrong_dtype)364 np.save(path, wrong_dtype)
364 else:365 else:
365 import torch366 import torch
367+ 
366 torch.save(torch.from_numpy(wrong_dtype), path)368 torch.save(torch.from_numpy(wrong_dtype), path)
367 369 
368 with pytest.raises(ManualDataError, match="dtype .* != filename dtype"):370 with pytest.raises(ManualDataError, match="dtype .* != filename dtype"):
@@ -521,7 +523,8 @@ def test_unknown_sidecar_is_rejected(tmp_path):
521 case = _e2e_case("no_sidecars")523 case = _e2e_case("no_sidecars")
522 store = ManualDataStore(tmp_path)524 store = ManualDataStore(tmp_path)
523 case_dir = store.write_case(525 case_dir = store.write_case(
524- case, "e2e",526+ case,
527+ "e2e",
525 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],528 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
526 [np.zeros((2, 2), np.float32)],529 [np.zeros((2, 2), np.float32)],
527 )530 )
@@ -538,8 +541,7 @@ def test_unknown_sidecar_is_rejected(tmp_path):
538 (("pt", "npy", "bin"), "bin", 3.0),541 (("pt", "npy", "bin"), "bin", 3.0),
539 ],542 ],
540)543)
541-def test_mixed_data_formats_use_whole_dataset_priority(544+def test_mixed_data_formats_use_whole_dataset_priority(tmp_path, formats, expected_format, expected_value):
542- tmp_path, formats, expected_format, expected_value):
543 case = _e2e_case("mixed_formats")545 case = _e2e_case("mixed_formats")
544 values = {"pt": 1.0, "npy": 2.0, "bin": 3.0}546 values = {"pt": 1.0, "npy": 2.0, "bin": 3.0}
545 target_store = ManualDataStore(tmp_path / "mixed")547 target_store = ManualDataStore(tmp_path / "mixed")
@@ -561,17 +563,11 @@ def test_mixed_data_formats_use_whole_dataset_priority(
561 shutil.copy2(path, target_dir / path.name)563 shutil.copy2(path, target_dir / path.name)
562 564 
563 loaded = target_store.load_case(case, "e2e")565 loaded = target_store.load_case(case, "e2e")
564- loaded_golden = loaded.load_goldens(566+ loaded_golden = loaded.load_goldens(references=[np.zeros((2, 2), np.float32)])
565- references=[np.zeros((2, 2), np.float32)]
566- )
567 567 
568 assert loaded.file_format == expected_format568 assert loaded.file_format == expected_format
569- np.testing.assert_array_equal(569+ np.testing.assert_array_equal(loaded.inputs[0], np.full((3, 3), expected_value, np.float32))
570- loaded.inputs[0], np.full((3, 3), expected_value, np.float32)570+ np.testing.assert_array_equal(loaded_golden[0], np.full((2, 2), expected_value, np.float32))
571- )
572- np.testing.assert_array_equal(
573- loaded_golden[0], np.full((2, 2), expected_value, np.float32)
574- )
575 571 
576 572 
577def test_incomplete_high_priority_format_does_not_fall_back(tmp_path):573def test_incomplete_high_priority_format_does_not_fall_back(tmp_path):
@@ -621,14 +617,13 @@ def test_long_case_name_maps_stably_within_directory_limit(tmp_path):
621 assert store.case_dir(name + "changed") != case_dir617 assert store.case_dir(name + "changed") != case_dir
622 618 
623 619 
624-@pytest.mark.parametrize(620+@pytest.mark.parametrize("filename", ["input_1_float32.bin", "golden_0_float32__shape_2x2.bin"])
625- "filename", ["input_1_float32.bin", "golden_0_float32__shape_2x2.bin"]
626-)
627def test_missing_data_slot_is_rejected(tmp_path, filename):621def test_missing_data_slot_is_rejected(tmp_path, filename):
628 case = _e2e_case("missing_slot")622 case = _e2e_case("missing_slot")
629 store = ManualDataStore(tmp_path)623 store = ManualDataStore(tmp_path)
630 case_dir = store.write_case(624 case_dir = store.write_case(
631- case, "e2e",625+ case,
626+ "e2e",
632 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],627 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
633 [np.zeros((2, 2), np.float32)],628 [np.zeros((2, 2), np.float32)],
634 )629 )
@@ -643,7 +638,8 @@ def test_extra_data_slot_is_rejected(tmp_path):
643 case = _e2e_case("extra_slot")638 case = _e2e_case("extra_slot")
644 store = ManualDataStore(tmp_path)639 store = ManualDataStore(tmp_path)
645 case_dir = store.write_case(640 case_dir = store.write_case(
646- case, "e2e",641+ case,
642+ "e2e",
647 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],643 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
648 [np.zeros((2, 2), np.float32)],644 [np.zeros((2, 2), np.float32)],
649 )645 )
@@ -658,7 +654,8 @@ def test_nonempty_none_marker_is_rejected(tmp_path, file_format):
658 case = _e2e_case("none_golden")654 case = _e2e_case("none_golden")
659 store = ManualDataStore(tmp_path)655 store = ManualDataStore(tmp_path)
660 case_dir = store.write_case(656 case_dir = store.write_case(
661- case, "e2e",657+ case,
658+ "e2e",
662 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],659 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
663 [None],660 [None],
664 file_format=file_format,661 file_format=file_format,
@@ -675,7 +672,8 @@ def test_bin_golden_filename_shape_matches_device_output(tmp_path):
675 store = ManualDataStore(tmp_path)672 store = ManualDataStore(tmp_path)
676 golden = np.arange(4, dtype=np.float32).reshape(2, 2)673 golden = np.arange(4, dtype=np.float32).reshape(2, 2)
677 case_dir = store.write_case(674 case_dir = store.write_case(
678- case, "e2e",675+ case,
676+ "e2e",
679 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],677 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
680 [golden],678 [golden],
681 )679 )
@@ -711,8 +709,7 @@ def test_bin_golden_rejects_same_numel_wrong_device_shape(tmp_path):
711 ((2, 0, 3), "2x0x3"),709 ((2, 0, 3), "2x0x3"),
712 ],710 ],
713)711)
714-def test_bin_golden_shape_filename_handles_scalar_and_zero_dimensions(712+def test_bin_golden_shape_filename_handles_scalar_and_zero_dimensions(tmp_path, shape, token):
715- tmp_path, shape, token):
716 case = _e2e_case(f"shape_{token}")713 case = _e2e_case(f"shape_{token}")
717 store = ManualDataStore(tmp_path)714 store = ManualDataStore(tmp_path)
718 golden = np.zeros(shape, dtype=np.float32)715 golden = np.zeros(shape, dtype=np.float32)
@@ -773,7 +770,8 @@ def test_golden_sentinel_never_publishes_case_directory(tmp_path):
773 770 
774 with pytest.raises(ManualDataError, match="sentinel"):771 with pytest.raises(ManualDataError, match="sentinel"):
775 store.write_case(772 store.write_case(
776- case, "e2e",773+ case,
774+ "e2e",
777 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],775 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
778 ["GOLDEN_FAILURE"],776 ["GOLDEN_FAILURE"],
779 )777 )
@@ -813,9 +811,7 @@ def test_invalidate_case_unlinks_directory_symlink_without_touching_target(tmp_p
813 811 
814def test_prepare_and_replay_helpers_share_store_policy(tmp_path):812def test_prepare_and_replay_helpers_share_store_policy(tmp_path):
815 prepare_case = _e2e_case("prepare_helper")813 prepare_case = _e2e_case("prepare_helper")
816- prepare_switches = SimpleNamespace(814+ prepare_switches = SimpleNamespace(manual_data_mode="prepare", manual_data_dirs=(str(tmp_path),))
817- manual_data_mode="prepare", manual_data_dirs=(str(tmp_path),)
818- )
819 815 
820 prepare_store = prepare_manual_data_store(prepare_case, "e2e", prepare_switches)816 prepare_store = prepare_manual_data_store(prepare_case, "e2e", prepare_switches)
821 817 
@@ -824,9 +820,7 @@ def test_prepare_and_replay_helpers_share_store_policy(tmp_path):
824 820 
825 replay_case = _e2e_case("replay_helper")821 replay_case = _e2e_case("replay_helper")
826 inputs = [np.ones((3, 3), np.float32), np.zeros((2, 2), np.float32)]822 inputs = [np.ones((3, 3), np.float32), np.zeros((2, 2), np.float32)]
827- ManualDataStore(tmp_path).write_case(823+ ManualDataStore(tmp_path).write_case(replay_case, "e2e", inputs, [np.zeros((2, 2), np.float32)])
828- replay_case, "e2e", inputs, [np.zeros((2, 2), np.float32)]
829- )
830 replay_switches = SimpleNamespace(824 replay_switches = SimpleNamespace(
831 manual_data_mode="replay",825 manual_data_mode="replay",
832 manual_data_dirs=(str(tmp_path),),826 manual_data_dirs=(str(tmp_path),),
@@ -847,7 +841,8 @@ def test_prepare_and_replay_helpers_share_store_policy(tmp_path):
847def test_registered_provider_is_an_extension_point_for_future_csv_sources(tmp_path):841def test_registered_provider_is_an_extension_point_for_future_csv_sources(tmp_path):
848 case = _e2e_case("provider_case")842 case = _e2e_case("provider_case")
849 ManualDataStore(tmp_path).write_case(843 ManualDataStore(tmp_path).write_case(
850- case, "e2e",844+ case,
845+ "e2e",
851 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],846 [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)],
852 [np.zeros((2, 2), np.float32)],847 [np.zeros((2, 2), np.float32)],
853 )848 )
@@ -873,12 +868,8 @@ def test_case_provider_takes_priority_over_cli_batch_directories(tmp_path):
873 inputs = [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)]868 inputs = [np.zeros((3, 3), np.float32), np.zeros((2, 2), np.float32)]
874 cli_root = tmp_path / "cli"869 cli_root = tmp_path / "cli"
875 provider_root = tmp_path / "provider"870 provider_root = tmp_path / "provider"
876- ManualDataStore(cli_root).write_case(871+ ManualDataStore(cli_root).write_case(case, "e2e", inputs, [np.full((2, 2), 1, np.float32)])
877- case, "e2e", inputs, [np.full((2, 2), 1, np.float32)]872+ ManualDataStore(provider_root).write_case(case, "e2e", inputs, [np.full((2, 2), 2, np.float32)])
878- )
879- ManualDataStore(provider_root).write_case(
880- case, "e2e", inputs, [np.full((2, 2), 2, np.float32)]
881- )
882 switches = SimpleNamespace(manual_data_dirs=(cli_root,))873 switches = SimpleNamespace(manual_data_dirs=(cli_root,))
883 874 
884 def provider(*_):875 def provider(*_):
@@ -899,9 +890,7 @@ def test_case_provider_takes_priority_over_cli_batch_directories(tmp_path):
899 890 
900def test_provider_replay_obeys_device_stage_constraints(tmp_path):891def test_provider_replay_obeys_device_stage_constraints(tmp_path):
901 case = _e2e_case("provider_constraints")892 case = _e2e_case("provider_constraints")
902- switches = SimpleNamespace(893+ switches = SimpleNamespace(manual_data_dirs=(), golden_mode="Enable", validate_only=False, force_cpu=True)
903- manual_data_dirs=(), golden_mode="Enable", validate_only=False, force_cpu=True
904- )
905 894 
906 def provider(*_):895 def provider(*_):
907 return tmp_path896 return tmp_path
@@ -915,15 +904,15 @@ def test_provider_replay_obeys_device_stage_constraints(tmp_path):
915 904 
916 905 
917def test_e2e_restore_rebuilds_view_without_running_input_plugin():906def test_e2e_restore_rebuilds_view_without_running_input_plugin():
907+ from ttk.core_modules.framework_api.input_generation import np_to_torch_inputs
908+ 
918 case = _e2e_case("restore_e2e")909 case = _e2e_case("restore_e2e")
919 storage = np.arange(9, dtype=np.float32).reshape(3, 3)910 storage = np.arange(9, dtype=np.float32).reshape(3, 3)
920 output = np.zeros((2, 2), dtype=np.float32)911 output = np.zeros((2, 2), dtype=np.float32)
921 switches = SimpleNamespace(plugin_path=("must-not-be-scanned",))912 switches = SimpleNamespace(plugin_path=("must-not-be-scanned",))
922- backend = SimpleNamespace(alias=lambda: "npu")913+ backend = SimpleNamespace(device_type=lambda: "npu", inputs_from_numpy=np_to_torch_inputs)
923 914 
924- raw = generate_inputs(915+ raw = generate_inputs(case, switches, backend, object(), stored_inputs=[storage, output])
925- case, switches, backend, object(), stored_inputs=[storage, output]
926- )
927 916 
928 assert tuple(case.tensors[0].stride()) == (3, 1)917 assert tuple(case.tensors[0].stride()) == (3, 1)
929 assert float(case.tensors[0][0, 0]) == float(storage.ravel()[1])918 assert float(case.tensors[0][0, 0]) == float(storage.ravel()[1])
@@ -97,6 +97,7 @@ def test_e2e_prepare_stops_before_api_resolution_and_device_execution(monkeypatc
97 97 
98 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)98 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)
99 monkeypatch.setattr(e2e_profiling, "generate_inputs", generate)99 monkeypatch.setattr(e2e_profiling, "generate_inputs", generate)
100+ 
100 def generate_golden(*_args, **_kwargs):101 def generate_golden(*_args, **_kwargs):
101 inputs[0][:] = -1102 inputs[0][:] = -1
102 return golden103 return golden
@@ -108,7 +109,13 @@ def test_e2e_prepare_stops_before_api_resolution_and_device_execution(monkeypatc
108 result = FrameworkApiReturnStructure()109 result = FrameworkApiReturnStructure()
109 110 
110 e2e_profiling._do_profile(111 e2e_profiling._do_profile(
111- case, SimpleNamespace(alias=lambda: "npu"), {}, {}, 0, switches, result112+ case,
113+ SimpleNamespace(device_type=lambda: "npu", inputs_from_numpy=lambda tc, ri: ri),
114+ {},
115+ {},
116+ 0,
117+ switches,
118+ result,
112 )119 )
113 120 
114 assert result.precision_status == "PASS"121 assert result.precision_status == "PASS"
@@ -131,13 +138,17 @@ def test_e2e_failed_reprepare_invalidates_previous_case(monkeypatch, tmp_path):
131 store.write_case(case, "e2e", inputs, [np.ones(2, np.float32)])138 store.write_case(case, "e2e", inputs, [np.ones(2, np.float32)])
132 switches = _switches(tmp_path, "prepare")139 switches = _switches(tmp_path, "prepare")
133 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)140 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)
134- monkeypatch.setattr(141+ monkeypatch.setattr(e2e_profiling, "generate_inputs", MagicMock(side_effect=RuntimeError("input failure")))
135- e2e_profiling, "generate_inputs", MagicMock(side_effect=RuntimeError("input failure"))
136- )
137 142 
138 result = FrameworkApiReturnStructure()143 result = FrameworkApiReturnStructure()
139 e2e_profiling._do_profile(144 e2e_profiling._do_profile(
140- case, SimpleNamespace(alias=lambda: "npu"), {}, {}, 0, switches, result145+ case,
146+ SimpleNamespace(device_type=lambda: "npu", inputs_from_numpy=lambda tc, ri: ri),
147+ {},
148+ {},
149+ 0,
150+ switches,
151+ result,
141 )152 )
142 153 
143 assert result.precision_status == "FAIL"154 assert result.precision_status == "FAIL"
@@ -161,8 +172,11 @@ def test_e2e_replay_skips_input_and_golden_generation(monkeypatch, tmp_path):
161 generated.side_effect = restore172 generated.side_effect = restore
162 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)173 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)
163 monkeypatch.setattr(e2e_profiling, "generate_inputs", generated)174 monkeypatch.setattr(e2e_profiling, "generate_inputs", generated)
164- monkeypatch.setattr(e2e_profiling, "_generate_golden_data",175+ monkeypatch.setattr(
165- MagicMock(side_effect=AssertionError("golden generation must be skipped")))176+ e2e_profiling,
177+ "_generate_golden_data",
178+ MagicMock(side_effect=AssertionError("golden generation must be skipped")),
179+ )
166 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))180 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))
167 monkeypatch.setattr(e2e_profiling, "_execute_eager", lambda *_: (golden, None))181 monkeypatch.setattr(e2e_profiling, "_execute_eager", lambda *_: (golden, None))
168 monkeypatch.setattr(e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())182 monkeypatch.setattr(e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())
@@ -173,11 +187,9 @@ def test_e2e_replay_skips_input_and_golden_generation(monkeypatch, tmp_path):
173 monkeypatch.setattr(e2e_profiling, "_apply_pre_compare", lambda *_: None)187 monkeypatch.setattr(e2e_profiling, "_apply_pre_compare", lambda *_: None)
174 monkeypatch.setattr(e2e_profiling, "_evaluate_eager_precision", evaluated)188 monkeypatch.setattr(e2e_profiling, "_evaluate_eager_precision", evaluated)
175 monkeypatch.setattr(e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None)189 monkeypatch.setattr(e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None)
176- backend = SimpleNamespace(alias=lambda: "npu", use_device=lambda: False)190+ backend = SimpleNamespace(device_type=lambda: "npu", has_device=lambda: False, inputs_from_numpy=lambda tc, ri: ri)
177 191 
178- e2e_profiling._do_profile(192+ e2e_profiling._do_profile(case, backend, {}, {}, 0, switches, FrameworkApiReturnStructure())
179- case, backend, {}, {}, 0, switches, FrameworkApiReturnStructure()
180- )
181 193 
182 assert generated.call_count == 1194 assert generated.call_count == 1
183 np.testing.assert_array_equal(evaluated.call_args.args[3][0], golden[0])195 np.testing.assert_array_equal(evaluated.call_args.args[3][0], golden[0])
@@ -218,20 +230,14 @@ def test_e2e_replay_custom_compare_receives_restored_inputs(monkeypatch, tmp_pat
218 )230 )
219 monkeypatch.setattr(e2e_profiling, "get_spec_attr", spec_attr)231 monkeypatch.setattr(e2e_profiling, "get_spec_attr", spec_attr)
220 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))232 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))
221- monkeypatch.setattr(233+ monkeypatch.setattr(e2e_profiling, "_execute_eager", lambda *_: ([golden[0].copy()], None))
222- e2e_profiling, "_execute_eager", lambda *_: ([golden[0].copy()], None)234+ monkeypatch.setattr(e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())
223- )
224- monkeypatch.setattr(
225- e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext()
226- )
227 monkeypatch.setattr(e2e_profiling, "_profiling_print", lambda *_: None)235 monkeypatch.setattr(e2e_profiling, "_profiling_print", lambda *_: None)
228 monkeypatch.setattr(e2e_profiling, "_dump_inputs", lambda *_: None)236 monkeypatch.setattr(e2e_profiling, "_dump_inputs", lambda *_: None)
229 monkeypatch.setattr(e2e_profiling, "_dump_goldens", lambda *_: None)237 monkeypatch.setattr(e2e_profiling, "_dump_goldens", lambda *_: None)
230 monkeypatch.setattr(e2e_profiling, "_dump_outputs", lambda *_: None)238 monkeypatch.setattr(e2e_profiling, "_dump_outputs", lambda *_: None)
231- monkeypatch.setattr(239+ monkeypatch.setattr(e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None)
232- e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None240+ backend = SimpleNamespace(device_type=lambda: "npu", has_device=lambda: False, inputs_from_numpy=lambda tc, ri: ri)
233- )
234- backend = SimpleNamespace(alias=lambda: "npu", use_device=lambda: False)
235 result = FrameworkApiReturnStructure()241 result = FrameworkApiReturnStructure()
236 242 
237 e2e_profiling._do_profile(case, backend, {}, {}, 0, switches, result)243 e2e_profiling._do_profile(case, backend, {}, {}, 0, switches, result)
@@ -267,8 +273,11 @@ def test_e2e_provider_automatically_selects_replay(monkeypatch, tmp_path):
267 273 
268 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)274 monkeypatch.setattr(e2e_profiling, "get_process_context", _process_context)
269 monkeypatch.setattr(e2e_profiling, "generate_inputs", restore)275 monkeypatch.setattr(e2e_profiling, "generate_inputs", restore)
270- monkeypatch.setattr(e2e_profiling, "_generate_golden_data",276+ monkeypatch.setattr(
271- MagicMock(side_effect=AssertionError("golden generation must be skipped")))277+ e2e_profiling,
278+ "_generate_golden_data",
279+ MagicMock(side_effect=AssertionError("golden generation must be skipped")),
280+ )
272 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))281 monkeypatch.setattr(e2e_profiling, "resolve_api", lambda *_: (lambda *_: None, False))
273 monkeypatch.setattr(e2e_profiling, "_execute_eager", lambda *_: (golden, None))282 monkeypatch.setattr(e2e_profiling, "_execute_eager", lambda *_: (golden, None))
274 monkeypatch.setattr(e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())283 monkeypatch.setattr(e2e_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())
@@ -279,13 +288,11 @@ def test_e2e_provider_automatically_selects_replay(monkeypatch, tmp_path):
279 monkeypatch.setattr(e2e_profiling, "_apply_pre_compare", lambda *_: None)288 monkeypatch.setattr(e2e_profiling, "_apply_pre_compare", lambda *_: None)
280 monkeypatch.setattr(e2e_profiling, "_evaluate_eager_precision", lambda *_: None)289 monkeypatch.setattr(e2e_profiling, "_evaluate_eager_precision", lambda *_: None)
281 monkeypatch.setattr(e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None)290 monkeypatch.setattr(e2e_profiling, "_profiling_end_print", lambda *_args, **_kwargs: None)
282- backend = SimpleNamespace(alias=lambda: "npu", use_device=lambda: False)291+ backend = SimpleNamespace(device_type=lambda: "npu", has_device=lambda: False, inputs_from_numpy=lambda tc, ri: ri)
283 292 
284 register_manual_data_directory_provider(provider)293 register_manual_data_directory_provider(provider)
285 try:294 try:
286- e2e_profiling._do_profile(295+ e2e_profiling._do_profile(case, backend, {}, {}, 0, switches, FrameworkApiReturnStructure())
287- case, backend, {}, {}, 0, switches, FrameworkApiReturnStructure()
288- )
289 finally:296 finally:
290 unregister_manual_data_directory_provider(provider)297 unregister_manual_data_directory_provider(provider)
291 298 
@@ -321,8 +328,7 @@ def test_aclnn_prepare_stops_before_device_execution(monkeypatch, tmp_path):
321 328 
322 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)329 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)
323 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)330 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)
324- monkeypatch.setattr(aclnn_profiling, "OpApiInfoKeeper",331+ monkeypatch.setattr(aclnn_profiling, "OpApiInfoKeeper", lambda: SimpleNamespace(has_api=lambda *_: True))
325- lambda: SimpleNamespace(has_api=lambda *_: True))
326 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)332 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)
327 monkeypatch.setattr(aclnn_profiling, "GoldenGenerator", Golden)333 monkeypatch.setattr(aclnn_profiling, "GoldenGenerator", Golden)
328 monkeypatch.setattr(aclnn_profiling, "Comparator", compare)334 monkeypatch.setattr(aclnn_profiling, "Comparator", compare)
@@ -348,9 +354,7 @@ def test_aclnn_failed_reprepare_invalidates_previous_case(monkeypatch, tmp_path)
348 inputs = [np.ones(2, np.float32), np.zeros(2, np.float32)]354 inputs = [np.ones(2, np.float32), np.zeros(2, np.float32)]
349 scalar = [np.array(0.5, np.float32)]355 scalar = [np.array(0.5, np.float32)]
350 store = ManualDataStore(tmp_path)356 store = ManualDataStore(tmp_path)
351- store.write_case(357+ store.write_case(case, "aclnn", inputs, [np.ones(2, np.float32)], scalars=scalar)
352- case, "aclnn", inputs, [np.ones(2, np.float32)], scalars=scalar
353- )
354 switches = _switches(tmp_path, "prepare")358 switches = _switches(tmp_path, "prepare")
355 359 
356 class Inputs:360 class Inputs:
@@ -363,7 +367,8 @@ def test_aclnn_failed_reprepare_invalidates_previous_case(monkeypatch, tmp_path)
363 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)367 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)
364 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)368 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)
365 monkeypatch.setattr(369 monkeypatch.setattr(
366- aclnn_profiling, "OpApiInfoKeeper",370+ aclnn_profiling,
371+ "OpApiInfoKeeper",
367 lambda: SimpleNamespace(has_api=lambda *_: True),372 lambda: SimpleNamespace(has_api=lambda *_: True),
368 )373 )
369 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)374 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)
@@ -379,9 +384,7 @@ def test_aclnn_replay_skips_input_and_golden_plugins(monkeypatch, tmp_path):
379 inputs = [np.array([1.0, 2.0], np.float32), np.zeros(2, np.float32)]384 inputs = [np.array([1.0, 2.0], np.float32), np.zeros(2, np.float32)]
380 scalar = [np.array(0.5, np.float32)]385 scalar = [np.array(0.5, np.float32)]
381 golden = [np.array([3.0, 4.0], np.float32)]386 golden = [np.array([3.0, 4.0], np.float32)]
382- ManualDataStore(tmp_path).write_case(387+ ManualDataStore(tmp_path).write_case(case, "aclnn", inputs, golden, scalars=scalar)
383- case, "aclnn", inputs, golden, scalars=scalar
384- )
385 switches = _switches(tmp_path, "replay")388 switches = _switches(tmp_path, "replay")
386 restore = MagicMock()389 restore = MagicMock()
387 390 
@@ -405,11 +408,11 @@ def test_aclnn_replay_skips_input_and_golden_plugins(monkeypatch, tmp_path):
405 408 
406 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)409 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)
407 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)410 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)
408- monkeypatch.setattr(aclnn_profiling, "OpApiInfoKeeper",411+ monkeypatch.setattr(aclnn_profiling, "OpApiInfoKeeper", lambda: SimpleNamespace(has_api=lambda *_: True))
409- lambda: SimpleNamespace(has_api=lambda *_: True))
410 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)412 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)
411- monkeypatch.setattr(aclnn_profiling, "GoldenGenerator",413+ monkeypatch.setattr(
412- MagicMock(side_effect=AssertionError("golden plugin must be skipped")))414+ aclnn_profiling, "GoldenGenerator", MagicMock(side_effect=AssertionError("golden plugin must be skipped"))
415+ )
413 monkeypatch.setattr(aclnn_profiling, "Comparator", Comparator)416 monkeypatch.setattr(aclnn_profiling, "Comparator", Comparator)
414 monkeypatch.setattr(aclnn_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())417 monkeypatch.setattr(aclnn_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())
415 monkeypatch.setattr(aclnn_profiling, "__profiling_print", lambda *_: None)418 monkeypatch.setattr(aclnn_profiling, "__profiling_print", lambda *_: None)
@@ -421,7 +424,8 @@ def test_aclnn_replay_skips_input_and_golden_plugins(monkeypatch, tmp_path):
421 aclnn_profiling,424 aclnn_profiling,
422 "do_profiling",425 "do_profiling",
423 lambda *_: ApiProfilingResult(426 lambda *_: ApiProfilingResult(
424- True, output_bytes=[np.zeros(2, np.float32).tobytes()],427+ True,
428+ output_bytes=[np.zeros(2, np.float32).tobytes()],
425 output_view_shapes=[(2,)],429 output_view_shapes=[(2,)],
426 ),430 ),
427 )431 )
@@ -437,9 +441,7 @@ def test_aclnn_replay_runs_current_custom_compare(monkeypatch, tmp_path):
437 inputs = [np.array([1.0, 2.0], np.float32), np.zeros(2, np.float32)]441 inputs = [np.array([1.0, 2.0], np.float32), np.zeros(2, np.float32)]
438 scalar = [np.array(0.5, np.float32)]442 scalar = [np.array(0.5, np.float32)]
439 golden = [np.array([3.0, 4.0], np.float32)]443 golden = [np.array([3.0, 4.0], np.float32)]
440- ManualDataStore(tmp_path).write_case(444+ ManualDataStore(tmp_path).write_case(case, "aclnn", inputs, golden, scalars=scalar)
441- case, "aclnn", inputs, golden, scalars=scalar
442- )
443 switches = _switches(tmp_path, "replay")445 switches = _switches(tmp_path, "replay")
444 switches.plugin_path = (str(tmp_path),)446 switches.plugin_path = (str(tmp_path),)
445 captured = {}447 captured = {}
@@ -469,17 +471,17 @@ def test_aclnn_replay_runs_current_custom_compare(monkeypatch, tmp_path):
469 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)471 monkeypatch.setattr(aclnn_profiling, "get_global_storage", lambda: switches)
470 monkeypatch.setattr(aclnn_comparison, "get_global_storage", lambda: switches)472 monkeypatch.setattr(aclnn_comparison, "get_global_storage", lambda: switches)
471 monkeypatch.setattr(aclnn_comparison, "get_spec_attr", spec_attr)473 monkeypatch.setattr(aclnn_comparison, "get_spec_attr", spec_attr)
472- monkeypatch.setattr(474+ monkeypatch.setattr(aclnn_comparison.Comparator, "_output_bytes_to_tensors", restore_outputs)
473- aclnn_comparison.Comparator, "_output_bytes_to_tensors", restore_outputs
474- )
475 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)475 monkeypatch.setattr(aclnn_profiling, "get_process_context", _process_context)
476 monkeypatch.setattr(476 monkeypatch.setattr(
477- aclnn_profiling, "OpApiInfoKeeper",477+ aclnn_profiling,
478+ "OpApiInfoKeeper",
478 lambda: SimpleNamespace(has_api=lambda *_: True),479 lambda: SimpleNamespace(has_api=lambda *_: True),
479 )480 )
480 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)481 monkeypatch.setattr(aclnn_profiling, "InputGenerator", Inputs)
481 monkeypatch.setattr(482 monkeypatch.setattr(
482- aclnn_profiling, "GoldenGenerator",483+ aclnn_profiling,
484+ "GoldenGenerator",
483 MagicMock(side_effect=AssertionError("golden plugin must be skipped")),485 MagicMock(side_effect=AssertionError("golden plugin must be skipped")),
484 )486 )
485 monkeypatch.setattr(aclnn_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())487 monkeypatch.setattr(aclnn_profiling, "DeviceLock", lambda *_args, **_kwargs: nullcontext())
@@ -520,9 +522,7 @@ def test_loaded_golden_still_uses_custom_compare(monkeypatch):
520 monkeypatch.setattr(e2e_profiling, "get_spec_attr", spec_attr)522 monkeypatch.setattr(e2e_profiling, "get_spec_attr", spec_attr)
521 result = FrameworkApiReturnStructure()523 result = FrameworkApiReturnStructure()
522 524 
523- e2e_profiling._evaluate_eager_precision(525+ e2e_profiling._evaluate_eager_precision(case, [], [output], [loaded_golden], switches, None, result)
524- case, [], [output], [loaded_golden], switches, None, result
525- )
526 526 
527 assert result.precision_status == "PASS"527 assert result.precision_status == "PASS"
528 assert result.eager_precision == "CUSTOM_PASS"528 assert result.eager_precision == "CUSTOM_PASS"
@@ -283,6 +283,58 @@ def _log_manual_data_configuration(sw):
283 )283 )
284 284 
285 285 
286+def _detect_framework_from_csv(input_files):
287+ """Peek at the first CSV to detect framework from api_name column.
288+ 
289+ Reads the CSV header to find the api_name column, then checks the first
290+ data row's api_name value. Returns 'tf' if it starts with 'tf.' or
291+ 'tensorflow.', otherwise 'torch'.
292+ 
293+ One CSV must contain only one framework's APIs: torch_npu and npu_device
294+ each initialize the NPU runtime exclusively, so mixing frameworks in a
295+ single run causes runtime conflicts. This function detects the framework
296+ from the first data row and validates that all subsequent rows are
297+ consistent.
298+ """
299+ if not input_files:
300+ return "torch"
301+ import csv
302+ from ttk.core_modules.framework_api.framework_detector import detect_framework
303+ 
304+ try:
305+ with open(input_files[0], "r", newline="") as f:
306+ reader = csv.reader(f)
307+ header = next(reader, None)
308+ if not header:
309+ return "torch"
310+ try:
311+ api_idx = header.index("api_name")
312+ except ValueError:
313+ return "torch"
314+ row = next(reader, None)
R
RRuiWang_17 天前

_detect_framework_from_csv 只读取第一个 CSV 文件的第一个数据行来判断框架。如果同一个 CSV 中混合了 torch 和 tf 的 API,所有行都会用第一行检测到的框架来选 backend,导致后续 API 执行时才报错。建议在文档中明确「一个 CSV 只支持一种框架」的约束,或逐行检测框架并校验一致性。

likedislike
315+ if not row or api_idx >= len(row):
316+ return "torch"
317+ first_api = row[api_idx].strip()
318+ first_framework = detect_framework(first_api)
319+ for row in reader:
320+ if api_idx < len(row):
321+ row_api = row[api_idx].strip()
322+ row_framework = detect_framework(row_api)
323+ if row_framework != first_framework:
324+ raise ValueError(
325+ f"Mixed frameworks in one CSV is not supported: "
326+ f"first row is {first_framework} (api_name='{first_api}'), "
327+ f"but found {row_framework} (api_name='{row_api}') in a later row. "
328+ f"Please split into separate CSV files per framework."
329+ )
330+ return first_framework
331+ except ValueError:
332+ raise
333+ except Exception as e:
334+ logging.warning(f"Failed to detect framework from CSV, defaulting to torch: {e}")
335+ return "torch"
336+ 
337+ 
286def run_with_switches(sw):338def run_with_switches(sw):
287 from ttk.core_modules.tbe_logging import default_logging_config339 from ttk.core_modules.tbe_logging import default_logging_config
288 from ttk.utilities import set_global_storage340 from ttk.utilities import set_global_storage
@@ -301,6 +353,7 @@ def run_with_switches(sw):
301 if sw.test_mode == "framework-api":353 if sw.test_mode == "framework-api":
302 from ttk.core_modules.framework_api.instance import FrameworkApiInstance354 from ttk.core_modules.framework_api.instance import FrameworkApiInstance
303 355 
356+ sw.framework = _detect_framework_from_csv(sw.input_files)
304 ins = FrameworkApiInstance()357 ins = FrameworkApiInstance()
305 elif sw.test_mode == "geir":358 elif sw.test_mode == "geir":
306 from ttk.core_modules.geir.instance import GeirInstance359 from ttk.core_modules.geir.instance import GeirInstance
@@ -10,7 +10,7 @@ from ttk.cli.sim_args import add_sim_args, apply_sim_args
10 10 
11 11 
12def register_e2e_command(subparsers):12def register_e2e_command(subparsers):
13- parser = subparsers.add_parser("e2e", help="Framework API mode: torch_npu end-to-end test")13+ parser = subparsers.add_parser("e2e", help="Framework API mode: torch_npu / TensorFlow end-to-end test")
14 add_common_args(parser)14 add_common_args(parser)
15 add_device_args(parser)15 add_device_args(parser)
16 _add_e2e_args(parser)16 _add_e2e_args(parser)
@@ -12,6 +12,7 @@
12"""12"""
13Resolve api_name string to callable object.13Resolve api_name string to callable object.
14Handles both module functions (torch.add) and Tensor methods (torch.Tensor.relu_).14Handles both module functions (torch.add) and Tensor methods (torch.Tensor.relu_).
15+Also supports TF APIs (tf.raw_ops.Add, tf.nn.relu, etc.).
15"""16"""
16 17 
17from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader18from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader
@@ -29,12 +30,20 @@ def resolve_api(api_name: str):
29 'torch_npu.npu_conv2d' -> (torch_npu.npu_conv2d, False)30 'torch_npu.npu_conv2d' -> (torch_npu.npu_conv2d, False)
30 'torch.Tensor.relu_' -> ('relu_', True)31 'torch.Tensor.relu_' -> ('relu_', True)
31 'torch.Tensor.npu_scatter_' -> ('npu_scatter_', True)32 'torch.Tensor.npu_scatter_' -> ('npu_scatter_', True)
33+ 'tf.raw_ops.Add' -> (tf.raw_ops.Add, False)
34+ 'tf.nn.relu' -> (tf.nn.relu, False)
32 """35 """
33 parts = api_name.split(".")36 parts = api_name.split(".")
34 37 
35 if len(parts) < 2:38 if len(parts) < 2:
36 raise ValueError(f"Invalid api_name: {api_name}, expected format: module.func")39 raise ValueError(f"Invalid api_name: {api_name}, expected format: module.func")
37 40 
41+ # TF: use resolve_callable_str (lazy import tensorflow)
42+ if api_name.startswith(("tf.", "tensorflow.")):
43+ from ttk.utilities.func_dispatch import resolve_callable_str
44+ 
45+ return resolve_callable_str(api_name), False
46+ 
38 TorchOpsPackageLoader.ensure_registered(api_name)47 TorchOpsPackageLoader.ensure_registered(api_name)
39 48 
40 # torch.Tensor.xxx -> Tensor method49 # torch.Tensor.xxx -> Tensor method
@@ -13,12 +13,7 @@ import importlib
13import logging13import logging
14from typing import Optional14from typing import Optional
15 15 
16-import torch
17- 
18from .base import Backend16from .base import Backend
19-from .npu_backend import NpuTorchBackend
20-from .xpu_backend import XpuTorchBackend
21-from .cpu_backend import CpuTorchBackend
22from ....config.loader import get_hardware_config17from ....config.loader import get_hardware_config
23 18 
24_log = logging.getLogger(__name__)19_log = logging.getLogger(__name__)
@@ -50,9 +45,7 @@ def _validate_profile(name: str, profile: dict) -> None:
50 raise ValueError(f"profile '{name}' missing torch_lib")45 raise ValueError(f"profile '{name}' missing torch_lib")
51 prof = profile.get("profiler")46 prof = profile.get("profiler")
52 if prof != "builtin" and not (isinstance(prof, dict) and "activities" in prof):47 if prof != "builtin" and not (isinstance(prof, dict) and "activities" in prof):
53- raise ValueError(48+ raise ValueError(f"profile '{name}' profiler must be 'builtin' or dict with activities")
54- f"profile '{name}' profiler must be 'builtin' or dict with activities"
55- )
56 49 
57 50 
58def _build(framework: str, name: str, profile: dict) -> Backend:51def _build(framework: str, name: str, profile: dict) -> Backend:
@@ -69,10 +62,16 @@ def _build(framework: str, name: str, profile: dict) -> Backend:
69 _validate_profile(name, profile)62 _validate_profile(name, profile)
70 torch_lib = profile["torch_lib"]63 torch_lib = profile["torch_lib"]
71 if torch_lib == "npu":64 if torch_lib == "npu":
65+ from .npu_torch_backend import NpuTorchBackend
66+ 
72 cls = NpuTorchBackend67 cls = NpuTorchBackend
73 elif torch_lib == "cpu":68 elif torch_lib == "cpu":
69+ from .cpu_torch_backend import CpuTorchBackend
70+ 
74 cls = CpuTorchBackend71 cls = CpuTorchBackend
75 else: # mlu / musa / other -> generic accelerator72 else: # mlu / musa / other -> generic accelerator
73+ from .xpu_torch_backend import XpuTorchBackend
74+ 
76 cls = XpuTorchBackend75 cls = XpuTorchBackend
77 b = cls()76 b = cls()
78 b.torch_lib = torch_lib77 b.torch_lib = torch_lib
@@ -98,6 +97,8 @@ def _probe(profile: dict) -> bool:
98 _log.warning("hardware probe skipped: profile missing torch_lib (%s)", profile)97 _log.warning("hardware probe skipped: profile missing torch_lib (%s)", profile)
99 return False98 return False
100 try:99 try:
100+ import torch
101+ 
101 if lib != "cuda":102 if lib != "cuda":
102 importlib.import_module(f"torch_{lib}")103 importlib.import_module(f"torch_{lib}")
103 mod = getattr(torch, lib, None)104 mod = getattr(torch, lib, None)
@@ -105,34 +106,56 @@ def _probe(profile: dict) -> bool:
105 except Exception as e:106 except Exception as e:
106 _log.warning(107 _log.warning(
107 "hardware probe failed for torch_lib=%s: %s",108 "hardware probe failed for torch_lib=%s: %s",
108- lib, e,109+ lib,
110+ e,
109 )111 )
110 return False112 return False
111 113 
112 114 
113-def get_backend(force_cpu: bool = False) -> Backend:115+def get_backend(force_cpu: bool = False, framework: str = "torch") -> Backend:
114 """Resolve a hardware Backend.116 """Resolve a hardware Backend.
115 117 
116 Resolution order (first match wins):118 Resolution order (first match wins):
117 119 
118- - ``force_cpu`` -> CpuTorchBackend.120+ - ``force_cpu`` -> CpuTorchBackend (torch) or CpuTfBackend (tf).
119- - else auto-detect: iterate ``_hw_profiles("torch")`` in declared order,121+ - else auto-detect: torch reads config profiles; tf checks npu_device.
120- skip profiles whose ``torch_lib`` is 'cpu' (CPU is the fallback below),122+ - nothing detected -> CPU backend fallback.
121- ``_probe`` each; first hit is ``_build``.
122- - nothing detected -> CpuTorchBackend fallback.
123 123 
124 Instances are not cached: each call builds fresh.124 Instances are not cached: each call builds fresh.
125 """125 """
126 if force_cpu:126 if force_cpu:
127+ if framework == "tf":
128+ from .cpu_tf_backend import CpuTfBackend
129+ 
130+ return CpuTfBackend()
131+ from .cpu_torch_backend import CpuTorchBackend
132+ 
127 return CpuTorchBackend()133 return CpuTorchBackend()
128 134 
135+ if framework == "tf":
136+ try:
137+ import importlib.util
138+ 
139+ has_npu_device = importlib.util.find_spec("npu_device") is not None
140+ except Exception:
141+ has_npu_device = False
142+ if has_npu_device:
143+ _log.info("Active hardware: npu (tf via npu_device)")
144+ from .npu_tf_backend import NpuTfBackend
R
RRuiWang_17 天前

tf 分支用 importlib.util.find_spec('npu_device') 只能确认包装是否安装,不能确认 NPU 硬件可用。紧接着 NpuTfBackend()init 里调 npu_device.open(),如果硬件不可用会直接抛异常,没有回退到 CPU。torch 路径有 _probe 探测,tf 这里建议把 NpuTfBackend() 包在 try 里,open 失败时回退 CpuTfBackend 并告警。

likedislike
145+ 
146+ return NpuTfBackend()
147+ _log.warning("npu_device not installed, falling back to CPU. TF NPU testing requires 'pip install npu_device'.")
148+ from .cpu_tf_backend import CpuTfBackend
149+ 
150+ return CpuTfBackend()
151+ 
129 for name, profile in _hw_profiles("torch").items():152 for name, profile in _hw_profiles("torch").items():
130 # Skip CPU profiles during auto-detect (CPU is the fallback below).153 # Skip CPU profiles during auto-detect (CPU is the fallback below).
131 if profile.get("torch_lib") == "cpu":154 if profile.get("torch_lib") == "cpu":
132 continue155 continue
133 if _probe(profile):156 if _probe(profile):
134- _log.info(157+ _log.info("Active hardware: %s (torch_lib=%s)", name, profile["torch_lib"])
135- "Active hardware: %s (torch_lib=%s)", name, profile["torch_lib"]
136- )
137 return _build("torch", name, profile)158 return _build("torch", name, profile)
159+ from .cpu_torch_backend import CpuTorchBackend
160+ 
138 return CpuTorchBackend()161 return CpuTorchBackend()
@@ -12,7 +12,6 @@
12from abc import ABC, abstractmethod12from abc import ABC, abstractmethod
13 13 
14import numpy as np14import numpy as np
15-import torch
16 15 
17 16 
18class Backend(ABC):17class Backend(ABC):
@@ -31,7 +30,7 @@ class Backend(ABC):
31 CpuTorchBackend (never goes through _build) sets the30 CpuTorchBackend (never goes through _build) sets the
32 class attribute _segment_name = 'cpu'.31 class attribute _segment_name = 'cpu'.
33 is_npu() -- convenience predicate (default False; NpuTorchBackend overrides).32 is_npu() -- convenience predicate (default False; NpuTorchBackend overrides).
34- use_device() -- whether device resources are used.33+ has_device() -- whether device resources are used.
35 soc_series() -- short SoC series; default degrades to device_name() (model);34 soc_series() -- short SoC series; default degrades to device_name() (model);
36 NpuTorchBackend overrides via get_npu_hw_info(model).35 NpuTorchBackend overrides via get_npu_hw_info(model).
37 36 
@@ -49,9 +48,11 @@ class Backend(ABC):
49 48 
50 def device_name(self, dev_id: int = 0) -> str:49 def device_name(self, dev_id: int = 0) -> str:
51 """Return hardware MODEL name via torch.<torch_lib>.get_device_name."""50 """Return hardware MODEL name via torch.<torch_lib>.get_device_name."""
51+ import torch
52+ 
52 return getattr(torch, self.torch_lib).get_device_name(dev_id)53 return getattr(torch, self.torch_lib).get_device_name(dev_id)
53 54 
54- def alias(self) -> str:55+ def device_type(self) -> str:
R
RRuiWang_17 天前

本次重构将 alias() 改名为 device_type()、use_device() 改名为 has_device(),并将 cpu_backend.py/npu_backend.py/xpu_backend.py 重命名为 *_torch_backend.py,同时从 profiling_utils.py 移除了 clone_preserving_stride 和 _to_device_preserving_stride。但对应的测试文件未同步更新:test_backends.py 和 test_contract_migration.py 仍从旧模块路径导入(如 from ...cpu_backend import CpuTorchBackend)并调用 .alias()/.use_device(),test_profiling_utils_stride.py 仍导入已删除的 clone_preserving_stride。已实际运行 pytest 确认这三个测试文件在收集阶段即报 ModuleNotFoundError / ImportError,完全无法运行。请同步更新测试。

likedislike
55 """Return the config-segment name (config-driven via _segment_name).56 """Return the config-segment name (config-driven via _segment_name).
56 57 
57 No longer hardcoded per subclass: _build injects _segment_name = the yaml58 No longer hardcoded per subclass: _build injects _segment_name = the yaml
@@ -68,9 +69,13 @@ class Backend(ABC):
68 def device_count(self) -> int:69 def device_count(self) -> int:
69 """Return number of available devices."""70 """Return number of available devices."""
70 71 
71- @abstractmethod72+ def to_device(self, tensor, dev_id: int = 0, preserve_stride: bool = False):
R
RRuiWang_17 天前

to_deviceto_numpy@abstractmethod 改成了带 raise NotImplementedError 的普通方法。这样子类如果忘了实现,不会再在实例化时报错,而是延迟到运行时调用才抛异常,问题更难发现。如果是为了让某些 backend 不强制实现,至少在文档里说明哪些必须覆盖。

likedislike
72- def to_device(self, tensor, dev_id: int = 0):73+ """Move tensor to target device.
73- """Move tensor to target device."""74+ 
75+ preserve_stride=True: preserve non-contiguous stride (torch only).
76+ Default: framework's standard device move.
77+ """
78+ raise NotImplementedError
74 79 
75 @abstractmethod80 @abstractmethod
76 def synchronize(self, dev_id: int = 0):81 def synchronize(self, dev_id: int = 0):
@@ -80,12 +85,15 @@ class Backend(ABC):
80 def from_numpy(self, arr: np.ndarray):85 def from_numpy(self, arr: np.ndarray):
81 """Convert numpy array to framework tensor (zero-copy preferred)."""86 """Convert numpy array to framework tensor (zero-copy preferred)."""
82 87 
83- @abstractmethod88+ def to_numpy(self, tensor, safe: bool = False) -> np.ndarray:
84- def to_numpy(self, tensor) -> np.ndarray:89+ """Convert framework tensor to numpy array.
85- """Convert framework tensor to numpy array."""90+ 
91+ safe=True: extra detach/clone for inplace-extracted tensors.
92+ """
93+ raise NotImplementedError
86 94 
87 @abstractmethod95 @abstractmethod
88- def use_device(self) -> bool:96+ def has_device(self) -> bool:
89 """Whether use device resource."""97 """Whether use device resource."""
90 98 
91 def is_npu(self) -> bool:99 def is_npu(self) -> bool:
@@ -95,3 +103,81 @@ class Backend(ABC):
95 def soc_series(self) -> str:103 def soc_series(self) -> str:
96 """Default: degrade to device_name() (model). NpuTorchBackend overrides short."""104 """Default: degrade to device_name() (model). NpuTorchBackend overrides short."""
97 return self.device_name()105 return self.device_name()
106+ 
107+ # ========== Framework tensor-lifecycle methods ==========
108+ 
109+ def clone(self, tensor):
110+ """Make a copy of a framework tensor (for inplace backup)."""
111+ import copy
112+ 
113+ return copy.copy(tensor)
114+ 
115+ def restore_inplace(self, target, backup):
116+ """Restore a tensor from backup after an inplace operation. Default: no-op."""
117+ pass
118+ 
119+ def is_npu_only(self, api_name: str) -> bool:
120+ """Whether the API can only run on device (not CPU). Default: False."""
121+ return False
122+ 
123+ def supports_graph_mode(self) -> bool:
124+ """Whether this backend supports graph mode compilation."""
125+ return True
126+ 
127+ def supports_format_cast(self) -> bool:
128+ """Whether this backend supports NPU format cast."""
129+ return False
130+ 
131+ def set_deterministic_level(self, level):
132+ """Set deterministic computation level. Default: no-op."""
133+ pass
134+ 
135+ def device_scope(self, dev_id=0):
136+ """Context manager for device-scoped execution. Default: nullcontext."""
137+ from contextlib import nullcontext
138+ 
139+ return nullcontext()
140+ 
141+ def wrap_eager_callable(self, resolved):
142+ """Wrap an API callable for eager execution. Default: no-op."""
143+ return resolved
144+ 
145+ def needs_numpy_fallback(self, testcase) -> bool:
146+ """Whether plugins should receive numpy arrays (instead of framework tensors).
147+ 
148+ True when the framework cannot natively represent the testcase's dtype
149+ (e.g. torch with float8/int4), so data is passed as numpy arrays instead.
150+ """
151+ return False
152+ 
153+ def inputs_from_numpy(self, testcase, raw_inputs):
154+ """Convert flat numpy arrays to framework tensors.
155+ 
156+ When needs_numpy_fallback() is True (framework cannot natively
157+ represent the dtype, e.g. torch int4/float8), raw numpy arrays
158+ are returned as-is so plugins receive numpy instead of crashing
159+ in the framework tensor conversion.
160+ """
161+ if self.needs_numpy_fallback(testcase):
162+ return list(raw_inputs)
163+ return [self.from_numpy(arr) if arr is not None else None for arr in raw_inputs]
164+ 
165+ def result_to_numpy(self, result, copy=False):
166+ """Convert an API result (Tensor/tuple/scalar) to list of numpy arrays."""
167+ if result is None:
168+ return [None]
169+ if isinstance(result, (tuple, list)):
170+ nps = []
171+ for r in result:
172+ if r is None:
173+ nps.append(None)
174+ elif hasattr(r, "numpy") and callable(getattr(r, "numpy")):
175+ arr = self.to_numpy(r)
176+ nps.append(arr.copy() if copy else arr)
177+ else:
178+ nps.append(np.array(r))
179+ return nps
180+ if hasattr(result, "numpy") and callable(getattr(result, "numpy")):
181+ arr = self.to_numpy(result)
182+ return [arr.copy() if copy else arr]
183+ return [np.array(result)]
@@ -0,0 +1,59 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+from __future__ import annotations
11+ 
12+"""CPU TF backend (for golden baseline or no-device testing).
13+ 
14+When npu_device.open() has been called (NPU TF backend active), NPU becomes
15+the TF default device via _ContextWithDefaultDevice. CPU golden generation
16+must explicitly place tensors and ops on CPU to avoid silently running on NPU.
17+"""
18+ 
19+from .tf_backend import TfBackend
20+ 
21+ 
22+class CpuTfBackend(TfBackend):
23+ """CPU TF backend (for golden baseline or no-device testing)."""
24+ 
25+ tf_device_type = "cpu"
26+ profile = {"profiler": {"activities": ["CPU"]}}
R
RRuiWang_17 天前

这个类属性是给 TorchProfiler 读 profile["torch_lib"] 用的,但 TfBackend 走的是 WallClockProfiler/TfNpuProfiler,根本不读这个属性。照搬了 CpuTorchBackend 的写法但实际没用到,可以删掉避免误导。

likedislike
27+ _segment_name = "cpu"
28+ 
29+ def is_available(self) -> bool:
30+ return True
31+ 
32+ def device_count(self) -> int:
33+ return 1
34+ 
35+ def from_numpy(self, arr):
36+ import tensorflow as tf
37+ from ttk.utilities.dtypes import normalize_to_tf_dtype
38+ import numpy as np
39+ 
40+ if arr is None:
41+ return None
42+ arr = np.ascontiguousarray(arr)
43+ arr = normalize_to_tf_dtype(arr)
44+ with tf.device("/CPU:0"):
45+ return tf.convert_to_tensor(arr)
46+ 
47+ def to_device(self, tensor, dev_id=0, preserve_stride=False):
48+ return self.from_numpy(tensor)
49+ 
50+ def device_scope(self, dev_id=0):
51+ import tensorflow as tf
52+ 
53+ return tf.device("/CPU:0")
54+ 
55+ def synchronize(self, dev_id=0):
56+ pass
57+ 
58+ def has_device(self) -> bool:
59+ return False
Rttk/core_modules/framework_api/backends/cpu_backend.pyttk/core_modules/framework_api/backends/cpu_torch_backend.py+5-6
@@ -34,7 +34,7 @@ class CpuTorchBackend(TorchBackend):
34 _segment_name = "cpu"34 _segment_name = "cpu"
35 35 
36 def device_name(self, dev_id: int = 0) -> str:36 def device_name(self, dev_id: int = 0) -> str:
37- return self.alias() # CPU has no torch.cpu.get_device_name37+ return self.device_type() # CPU has no torch.cpu.get_device_name
38 38 
39 def is_available(self) -> bool:39 def is_available(self) -> bool:
40 return True40 return True
@@ -42,7 +42,9 @@ class CpuTorchBackend(TorchBackend):
42 def device_count(self) -> int:42 def device_count(self) -> int:
43 return 143 return 1
44 44 
45- def to_device(self, tensor, dev_id=0):45+ def to_device(self, tensor, dev_id=0, preserve_stride=False):
46+ if preserve_stride:
47+ return self._to_device_preserving_stride(tensor, dev_id)
46 return self.from_numpy(tensor)48 return self.from_numpy(tensor)
47 49 
48 def synchronize(self, dev_id=0):50 def synchronize(self, dev_id=0):
@@ -51,8 +53,5 @@ class CpuTorchBackend(TorchBackend):
51 def from_numpy(self, arr):53 def from_numpy(self, arr):
52 return numpy_to_torch_tensor(arr)54 return numpy_to_torch_tensor(arr)
53 55 
54- def to_numpy(self, tensor):56+ def has_device(self) -> bool:
55- return torch_to_numpy_tensor(tensor.detach().contiguous())
56- 
57- def use_device(self) -> bool:
58 return False57 return False
@@ -0,0 +1,149 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+from __future__ import annotations
11+ 
12+"""NPU TF backend — uses npu_device plugin for Ascend NPU.
13+ 
14+Corresponds to NpuTorchBackend (torch_npu). npu_device.open() must be called
15+BEFORE any TF eager operations (tf.convert_to_tensor etc.), because it swaps
16+the global TF context to _ContextWithDefaultDevice with NPU as default device.
17+Calling it after the context is already initialized will not take effect.
18+ 
19+Therefore open() is called in __init__ (at backend creation time, before
20+generate_inputs creates the first tf.Tensor), not lazily in to_device.
21+ 
22+as_default() monkey-patches ops.device to _device_consistent_with_context,
23+which ignores the argument device path and always uses ctx.default_device
24+(NPU:0). Therefore to_device / device_scope need not (and cannot) place
25+tensors on a specific NPU via tf.device — all ops auto-dispatch to NPU.
26+"""
27+import logging
28+ 
29+from contextlib import nullcontext
30+ 
31+from .tf_backend import TfBackend
32+ 
33+ 
34+class NpuTfBackend(TfBackend):
35+ """NPU TF backend via npu_device plugin."""
36+ 
37+ tf_device_type = "NPU"
38+ _segment_name = "npu"
39+ 
40+ _opened_device = None
41+ 
42+ def __init__(self):
43+ self._ensure_npu_opened(0)
44+ 
45+ def is_npu(self) -> bool:
46+ return True
47+ 
48+ def is_available(self) -> bool:
49+ try:
50+ import importlib.util
51+ 
52+ return importlib.util.find_spec("npu_device") is not None
53+ except Exception:
54+ return False
55+ 
56+ def device_count(self) -> int:
57+ return 1
58+ 
59+ def to_device(self, tensor, dev_id=0, preserve_stride=False):
60+ return self.from_numpy(tensor)
61+ 
62+ def synchronize(self, dev_id=0):
63+ pass
64+ 
65+ def device_scope(self, dev_id=0):
66+ return nullcontext()
67+ 
68+ def _ensure_npu_opened(self, dev_id):
69+ if NpuTfBackend._opened_device is None:
70+ import npu_device
71+ 
72+ handle = npu_device.open(dev_id)
73+ handle.as_default()
74+ NpuTfBackend._opened_device = dev_id
75+ elif NpuTfBackend._opened_device != dev_id:
76+ raise RuntimeError(
77+ f"npu_device only supports one device; already opened "
78+ f"{NpuTfBackend._opened_device}, cannot open {dev_id}"
79+ )
80+ 
81+ def device_name(self, dev_id=0):
82+ try:
83+ from ...dsmi import DSMIInterface
84+ from ttk.utilities.platform import get_npu_hw_info
85+ 
86+ platform = DSMIInterface().get_chip_info(dev_id).get_complete_platform()
87+ return get_npu_hw_info(platform).get("short_soc_version", platform)
88+ except Exception:
89+ return self._segment_name or "NPU"
90+ 
91+ def soc_series(self):
R
RRuiWang_17 天前

soc_series 直接返回 "npu",而 torch 的 NpuTorchBackend 会通过 get_npu_hw_info 返回真实 SoC 系列(如 Ascend910B)。如果下游有按 soc_series 做硬件分支的逻辑,TF 这边拿到的永远是 "npu",可能走错分支。如果 npu_device 拿不到 SoC 信息,至少在注释里说明这个限制。

likedislike
92+ return self.device_name()
93+ 
94+ def supports_graph_mode(self) -> bool:
95+ return True
96+ 
97+ def wrap_eager_callable(self, resolved):
98+ """Wrap API in tf.function so eager ops dispatch to NPU kernels.
99+ 
100+ npu_device registers NPU as a custom device whose execute callback
101+ only triggers GE graph compilation (and thus real NPU kernel launches)
102+ for ops inside tf.function. Bare eager calls fall back to CPU.
103+ Wrapping with tf.function(autograph=False) preserves single-op
104+ semantics (no input_signature, TF auto-traces by actual shape) while
105+ ensuring the op runs on NPU.
106+ 
107+ tf.raw_ops.* require keyword args; we generate a wrapper that binds
108+ positional inputs to the API's tensor parameter names at call time,
109+ so tf.function tracing passes them as kwargs. Non-tensor params use
110+ the API's own defaults.
111+ """
112+ import inspect
113+ import tensorflow as tf
114+ 
115+ try:
116+ sig = inspect.signature(resolved)
117+ param_names = [
118+ name
119+ for name, p in sig.parameters.items()
120+ if name != "name"
121+ and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
122+ ]
123+ except (ValueError, TypeError):
124+ param_names = []
125+ 
126+ if param_names:
127+ names = param_names
128+ 
129+ def wrapper(*args, **kwargs):
R
RRuiWang_17 天前

wrap_eager_callable 里的 wrapper 用 for i, name in enumerate(names) 只遍历了参数名列表,如果实参 args 比参数名多,多出来的位置参数会被直接丢弃。另外这段位置参数转关键字的逻辑和 TfGraphWrapper.__call___build_kw_function 重复了三份,建议抽个公共函数。

likedislike
130+ call_kwargs = {}
131+ for i, name in enumerate(names):
132+ if i < len(args) and args[i] is not None:
133+ call_kwargs[name] = args[i]
134+ call_kwargs.update(kwargs)
135+ return resolved(**call_kwargs)
136+ else:
137+ wrapper = resolved
138+ 
139+ return tf.function(autograph=False)(wrapper)
140+ 
141+ def set_deterministic_level(self, level):
142+ try:
143+ import npu_device
144+ 
145+ cfg = npu_device.global_options()
146+ cfg.deterministic = level
147+ npu_device.global_options()
R
RRuiWang_17 天前

set_deterministic_level 里第二次调用 npu_device.global_options() 的返回值被直接丢弃了。如果 global_options() 每次返回新的配置对象,那上面设在 cfg 上的 deterministic 根本不会生效;如果是单例,这行就是多余的死代码。无论哪种情况这行都不对,确认下正确的应用方式,要么删掉要么改成真正提交配置的调用。

likedislike
148+ except Exception as e:
149+ logging.warning(f"Failed to set TF deterministic: {e}")
Rttk/core_modules/framework_api/backends/npu_backend.pyttk/core_modules/framework_api/backends/npu_torch_backend.py+17-9
@@ -13,7 +13,8 @@ import numpy as np
13 13 
14from .torch_backend import TorchBackend14from .torch_backend import TorchBackend
15from ....utilities import (15from ....utilities import (
16- is_torch_native_dtype, get_npu_hw_info,16+ is_torch_native_dtype,
17+ get_npu_hw_info,
17)18)
18 19 
19 20 
@@ -34,22 +35,24 @@ class NpuTorchBackend(TorchBackend):
34 def is_npu(self) -> bool:35 def is_npu(self) -> bool:
35 return True36 return True
36 37 
37- def to_device(self, tensor, dev_id=0):38+ def to_device(self, tensor, dev_id=0, preserve_stride=False):
39+ if preserve_stride:
40+ return self._to_device_preserving_stride(tensor, dev_id)
38 import torch_npu # NPU-only: keep import in method body (lazy)41 import torch_npu # NPU-only: keep import in method body (lazy)
39- str_dtype = tensor.dtype.name42+ 
40- if is_torch_native_dtype(tensor.dtype.name):43+ str_dtype = str(tensor.dtype).split(".")[-1]
44+ if is_torch_native_dtype(str_dtype):
41 torch_tensor = self.from_numpy(tensor)45 torch_tensor = self.from_numpy(tensor)
42 return torch_tensor.npu(dev_id)46 return torch_tensor.npu(dev_id)
43 else:47 else:
44- if str_dtype == 'int4':48+ if str_dtype == "int4":
45 raise RuntimeError(f"Dtype [{str_dtype}] is not supported yet.")49 raise RuntimeError(f"Dtype [{str_dtype}] is not supported yet.")
46- elif str_dtype == 'float8_e8m0':50+ elif str_dtype == "float8_e8m0":
47 return self.from_numpy(tensor).npu(dev_id)51 return self.from_numpy(tensor).npu(dev_id)
48 else:52 else:
49 np_fp32 = tensor.astype(np.float32)53 np_fp32 = tensor.astype(np.float32)
50 npu_torch_tensor = torch_npu.npu_dtype_cast(54 npu_torch_tensor = torch_npu.npu_dtype_cast(
51- self.from_numpy(np_fp32).npu(dev_id),55+ self.from_numpy(np_fp32).npu(dev_id), dtype=getattr(torch_npu, str_dtype)
52- dtype=getattr(torch_npu, str_dtype)
53 )56 )
54 return npu_torch_tensor57 return npu_torch_tensor
55 58 
@@ -59,6 +62,11 @@ class NpuTorchBackend(TorchBackend):
59 def soc_series(self):62 def soc_series(self):
60 try:63 try:
61 hw_info = get_npu_hw_info(self.device_name())64 hw_info = get_npu_hw_info(self.device_name())
62- return hw_info['short_soc_version']65+ return hw_info["short_soc_version"]
63 except (FileNotFoundError, RuntimeError, KeyError):66 except (FileNotFoundError, RuntimeError, KeyError):
64 return self.device_name()67 return self.device_name()
68+ 
69+ def set_deterministic_level(self, level):
70+ import torch_npu
71+ 
72+ torch_npu.npu.set_deterministic_level(level)
@@ -0,0 +1,100 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+from __future__ import annotations
11+ 
12+"""TfBackend: TF-generic intermediate layer.
13+ 
14+Holds the shared TF implementation (numpy<->tf conversion, device move via
15+``tf.device``). Hardware-specific subclasses override ``is_npu`` only.
16+"""
17+ 
18+import numpy as np
19+ 
20+from .base import Backend
21+ 
22+ 
23+class TfBackend(Backend):
24+ """Shared TF implementation; subclass per hardware."""
25+ 
26+ tf_device_type: str = ""
27+ 
28+ def is_available(self) -> bool:
29+ import tensorflow as tf
30+ 
31+ return bool(tf.config.list_physical_devices(self.tf_device_type))
32+ 
33+ def device_count(self) -> int:
34+ import tensorflow as tf
35+ 
36+ return len(tf.config.list_physical_devices(self.tf_device_type))
37+ 
38+ def to_device(self, tensor, dev_id: int = 0, preserve_stride: bool = False):
39+ import tensorflow as tf
40+ 
41+ t = self.from_numpy(tensor)
42+ if self.tf_device_type and self.tf_device_type != "cpu":
43+ with tf.device(f"/{self.tf_device_type}:{dev_id}"):
44+ return tf.identity(t)
45+ return t
46+ 
47+ def synchronize(self, dev_id: int = 0):
48+ pass
49+ 
50+ def from_numpy(self, arr):
51+ import tensorflow as tf
52+ from ttk.utilities.dtypes import normalize_to_tf_dtype
53+ 
54+ if arr is None:
55+ return None
56+ arr = np.ascontiguousarray(arr)
57+ arr = normalize_to_tf_dtype(arr)
58+ return tf.convert_to_tensor(arr)
59+ 
60+ def to_numpy(self, tensor, safe: bool = False):
R
RRuiWang_17 天前

to_numpy 直接 tensor.numpy() 返回,对 bfloat16 给的是 tf.bfloat16.as_numpy_dtype,和 op 路径 __call_tf_api 里用的 tf_dtype_revert(转成 ml_dtypes bfloat16)不一致。跨路径比对或自定义插件返回 ml_dtypes bfloat16 时可能出 dtype 不匹配。建议在 TF 的 to_numpy 里也统一走 tf_dtype_revert。

likedislike
61+ if tensor is None:
62+ return None
63+ return tensor.numpy()
64+ 
65+ def has_device(self) -> bool:
66+ return self.tf_device_type != "cpu"
atomgit-bot
atomgit-botatomgit-bot21 天前

🟡 Medium Priority

use_device() 第 60 行使用 self.tf_device_type != "cpu" 判断,当 tf_device_type 为空字符串 ""(类默认值)时返回 True,表示"使用了设备"。但 to_device() 第 37 行使用 if self.tf_device_type and self.tf_device_type != "cpu" 作为守卫条件——空字符串为 falsy,守卫不通过,tensor 不会被放置到任何设备上。两个方法对"是否使用设备"的判断逻辑不一致。

影响:如果某个子类未覆盖这两个方法且 tf_device_type 未显式设置(即为空字符串 ""),上层调用 use_device() 返回 True 后会假设后端已启用设备加速,但实际 to_device() 不会做任何设备放置,导致静默的性能/行为回退(例如 profiling 路径 profiling.py:656 会根据 use_device() 结果做不同处理)。

建议:让 use_device()to_device() 使用相同的守卫条件:bool(self.tf_device_type) and self.tf_device_type != "cpu"

改动建议
66
+ def use_device(self) -> bool:
66
- return self.tf_device_type != "cpu"
67
+ return bool(self.tf_device_type) and self.tf_device_type != "cpu"
应用建议
likedislike
67+ 
68+ def device_name(self, dev_id: int = 0) -> str:
69+ return self.device_type()
70+ 
71+ def is_npu(self) -> bool:
72+ return False
73+ 
74+ def soc_series(self) -> str:
75+ return self.device_name()
76+ 
77+ def clone(self, tensor):
78+ import tensorflow as tf
79+ 
80+ return tf.identity(tensor)
81+ 
82+ def restore_inplace(self, target, backup):
83+ pass
84+ 
85+ def is_npu_only(self, api_name: str) -> bool:
86+ return api_name.startswith(("tf.npu_",))
R
RRuiWang_17 天前

is_npu_only 只判断了 tf.npu_ 前缀,但 detect_framework、api_resolver 等都同时认 tf.tensorflow. 两种前缀。tensorflow.npu_xxx 这类 API 不会被识别成 NPU 专属,golden 会在 CPU 上尝试执行并失败。建议同时匹配 tensorflow.npu_

likedislike
87+ 
88+ def supports_graph_mode(self) -> bool:
89+ return True
90+ 
91+ def supports_format_cast(self) -> bool:
92+ return False
93+ 
94+ def needs_numpy_fallback(self, testcase) -> bool:
95+ return not testcase.is_tf_dtype_support()
96+ 
97+ def inputs_from_numpy(self, testcase, raw_inputs):
98+ from ..input_generation import np_to_tf_inputs
99+ 
100+ return np_to_tf_inputs(testcase, raw_inputs)
@@ -35,20 +35,88 @@ class TorchBackend(Backend):
35 def device_count(self) -> int:35 def device_count(self) -> int:
36 return getattr(torch, self.torch_lib).device_count()36 return getattr(torch, self.torch_lib).device_count()
37 37 
38- def to_device(self, tensor, dev_id: int = 0):38+ def to_device(self, tensor, dev_id: int = 0, preserve_stride: bool = False):
39+ if preserve_stride:
40+ return self._to_device_preserving_stride(tensor, dev_id)
39 t = self.from_numpy(tensor)41 t = self.from_numpy(tensor)
40 return getattr(t, self.torch_lib)(dev_id)42 return getattr(t, self.torch_lib)(dev_id)
41 43 
44+ def _to_device_preserving_stride(self, tensor, dev_id):
45+ """Move a (possibly non-contiguous) tensor to device preserving stride."""
46+ if tensor is None:
47+ return None
48+ if self.torch_lib == "cpu":
49+ return tensor
50+ if tensor.is_contiguous():
51+ return getattr(tensor, self.torch_lib)(dev_id)
52+ return self._clone_full_storage(tensor, f"{self.torch_lib}:{dev_id}")
53+ 
42 def synchronize(self, dev_id: int = 0):54 def synchronize(self, dev_id: int = 0):
43 getattr(torch, self.torch_lib).synchronize(dev_id)55 getattr(torch, self.torch_lib).synchronize(dev_id)
44 56 
45 def from_numpy(self, arr):57 def from_numpy(self, arr):
46 return numpy_to_torch_tensor(arr)58 return numpy_to_torch_tensor(arr)
47 59 
48- def to_numpy(self, tensor):60+ def to_numpy(self, tensor, safe: bool = False):
49- # .contiguous() guards torch_to_numpy_tensor against non-contiguous tensors61+ tensor = tensor.detach()
50- # (matches existing cpu_backend behaviour).62+ if safe:
51- return torch_to_numpy_tensor(tensor.detach().cpu().contiguous())63+ tensor = tensor.clone()
64+ return torch_to_numpy_tensor(tensor.cpu().contiguous())
52 65 
53- def use_device(self) -> bool:66+ def has_device(self) -> bool:
54 return True67 return True
68+ 
69+ def _clone_full_storage(self, tensor, dst_device=None):
70+ """Clone a non-contiguous tensor preserving stride AND stride-gap data.
71+ 
72+ ``empty_strided`` + ``copy_`` only allocates storage for the visible
73+ elements of the view, leaving stride-gap memory uninitialized. An
74+ operator that reads into those gaps sees garbage that differs between
75+ clones and the original tensor — which is why the last profiling round
76+ (using the original) can diverge from earlier rounds (using clones).
77+ 
78+ To fix this we clone the *entire* underlying storage (gaps included)
79+ and rebuild the non-contiguous view via ``as_strided``.
80+ """
81+ src_storage = tensor.untyped_storage()
82+ elem_size = tensor.element_size()
83+ storage_numel = src_storage.size() // elem_size
84+ src_flat = torch.empty(0, dtype=tensor.dtype, device=tensor.device).set_(src_storage, 0, (storage_numel,), (1,))
85+ new_flat = src_flat.to(dst_device) if dst_device else src_flat.clone()
86+ return torch.as_strided(new_flat, tensor.shape, tensor.stride(), tensor.storage_offset())
87+ 
88+ def clone(self, tensor):
89+ """Clone a tensor preserving its non-contiguous stride and gap data.
90+ 
91+ Unlike ``torch.Tensor.clone()``, which materializes a non-contiguous
92+ view into a contiguous tensor, this clones the full underlying storage
93+ (so stride-gap data is identical to the original) and rebuilds the
94+ non-contiguous view via ``as_strided``.
95+ """
96+ if tensor is None:
97+ return None
98+ if tensor.is_contiguous():
99+ return tensor.clone()
100+ return self._clone_full_storage(tensor)
101+ 
102+ def restore_inplace(self, target, backup):
103+ target[:] = backup
104+ 
105+ def is_npu_only(self, api_name: str) -> bool:
106+ return api_name.startswith(("torch_npu.", "torch.npu"))
107+ 
108+ def supports_graph_mode(self) -> bool:
109+ return True
110+ 
111+ def supports_format_cast(self) -> bool:
112+ return self.is_npu()
113+ 
114+ def needs_numpy_fallback(self, testcase) -> bool:
115+ return not testcase.is_torch_dtype_support()
116+ 
117+ def inputs_from_numpy(self, testcase, raw_inputs):
R
RRuiWang_17 天前

inputs_from_numpy 现在无条件调用 np_to_torch_inputs 把 numpy 转成 torch tensor,但 needs_numpy_fallback 的注释说"当框架无法原生表示 dtype 时数据以 numpy 数组传入"。对 int4 这类 dtype,numpy_to_torch_tensor 会直接抛 RuntimeError,而旧逻辑在 not is_torch_dtype_support() 时是传原始 numpy 的。这既是个回归又和 use_numpy 标志自相矛盾(标志说是 numpy,实际给的是 torch tensor)。建议在 needs_numpy_fallback 返回 True 时直接返回 raw numpy。

likedislike
118+ if self.needs_numpy_fallback(testcase):
119+ return list(raw_inputs)
120+ from ..input_generation import np_to_torch_inputs
121+ 
122+ return np_to_torch_inputs(testcase, raw_inputs)
Rttk/core_modules/framework_api/backends/xpu_backend.pyttk/core_modules/framework_api/backends/xpu_torch_backend.py+0-0
文件重命名但无更改。
@@ -3,9 +3,9 @@
3# Copyright (c) 2026 Huawei Technologies Co., Ltd.3# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4 4 
5"""5"""
6-FrameworkApiInfoKeeper — cached API parameter info for torch/torch_npu.6+FrameworkApiInfoKeeper — cached API parameter info for torch/torch_npu/tf.
7 7 
8-Uses simple_param_extractor for auto-parsing with manual override support.8+Uses simple_param_extractor for torch/torch_npu and tf_param_extractor for TF.
9Validates testcase parameters against API signatures.9Validates testcase parameters against API signatures.
10"""10"""
11 11 
@@ -13,14 +13,11 @@ import logging
13from typing import Optional, Dict13from typing import Optional, Dict
14 14 
15from ttk.utilities import Singleton15from ttk.utilities import Singleton
16-from ttk.utilities.simple_param_extractor import (16+from ttk.utilities.simple_param_extractor import APIParamInfo, get_api_params, register_api_params, ParamInfo
17- APIParamInfo, get_api_params, register_api_params, ParamInfo
18-)
19from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader17from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader
20 18 
21 19 
22class FrameworkApiInfoKeeper(metaclass=Singleton):20class FrameworkApiInfoKeeper(metaclass=Singleton):
23- 
24 def __init__(self):21 def __init__(self):
25 self._cache: Dict[str, Optional[APIParamInfo]] = {}22 self._cache: Dict[str, Optional[APIParamInfo]] = {}
26 23 
@@ -28,8 +25,13 @@ class FrameworkApiInfoKeeper(metaclass=Singleton):
28 if api_name in self._cache:25 if api_name in self._cache:
29 return self._cache[api_name]26 return self._cache[api_name]
30 try:27 try:
31- TorchOpsPackageLoader.ensure_registered(api_name)28+ if api_name.startswith(("tf.", "tensorflow.")):
32- info = get_api_params(api_name)29+ from ttk.utilities.tf_param_extractor import extract_tf_params
30+ 
31+ info = extract_tf_params(api_name)
32+ else:
33+ TorchOpsPackageLoader.ensure_registered(api_name)
34+ info = get_api_params(api_name)
33 except Exception as e:35 except Exception as e:
34 logging.warning(f"Parse {api_name} signature failed: {type(e).__name__}: {e}")36 logging.warning(f"Parse {api_name} signature failed: {type(e).__name__}: {e}")
35 info = None37 info = None
@@ -50,17 +52,18 @@ class FrameworkApiInfoKeeper(metaclass=Singleton):
50 register_api_params(api_name, params, source)52 register_api_params(api_name, params, source)
51 self._cache[api_name] = get_api_params(api_name)53 self._cache[api_name] = get_api_params(api_name)
52 54 
53- def validate_testcase_params(self, api_name: str, tensor_count: int,55+ def validate_testcase_params(self, api_name: str, tensor_count: int, scalar_count: int = 0) -> Optional[str]:
54- scalar_count: int = 0) -> Optional[str]:
55 info = self.get(api_name)56 info = self.get(api_name)
56 if info is None:57 if info is None:
57 return None58 return None
58 api_tensor_count = info.tensor_count59 api_tensor_count = info.tensor_count
59 api_scalar_count = info.scalar_count60 api_scalar_count = info.scalar_count
60 if tensor_count != api_tensor_count:61 if tensor_count != api_tensor_count:
61- return (f"API [{api_name}] has {api_tensor_count} tensor parameters, "62+ return (
62- f"but testcase configured {tensor_count}. "63+ f"API [{api_name}] has {api_tensor_count} tensor parameters, "
63- f"(source: {info.source})")64+ f"but testcase configured {tensor_count}. "
65+ f"(source: {info.source})"
66+ )
64 return None67 return None
65 68 
66 def get_tensor_distribution(self, api_name: str) -> tuple:69 def get_tensor_distribution(self, api_name: str) -> tuple:
@@ -0,0 +1,43 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+ 
11+"""Framework detection from api_name prefix."""
12+ 
13+from typing import Optional
14+ 
15+ 
16+def detect_framework(api_name: str) -> str:
17+ """Detect framework ('torch' or 'tf') from api_name prefix.
18+ 
19+ Routing rule:
20+ "tf." / "tensorflow." prefix -> "tf"
21+ everything else (torch., torch_npu., torch.ops., torch.Tensor.) -> "torch"
22+ """
23+ if not api_name:
24+ return "torch"
25+ if api_name.startswith(("tf.", "tensorflow.")):
26+ return "tf"
27+ return "torch"
28+ 
29+ 
30+def is_inplace_tensor_method(api_name: str, framework: Optional[str] = None) -> bool:
31+ """Check if api_name is an inplace tensor method.
32+ 
33+ torch: torch.Tensor.xxx_ (trailing underscore)
34+ tf: never (TF has no inplace tensor methods)
35+ """
36+ if framework is None:
37+ framework = detect_framework(api_name)
38+ if framework == "tf":
39+ return False
40+ if not api_name:
41+ return False
42+ parts = api_name.split(".")
43+ return len(parts) >= 3 and parts[0] == "torch" and parts[1] == "Tensor" and parts[-1].endswith("_")
@@ -18,6 +18,7 @@ Golden data generation with three-level fallback:
18All paths use testcase.get_param_plan() to get consistent arg ordering18All paths use testcase.get_param_plan() to get consistent arg ordering
19with profiling.py — same plan, same (*args, **kwargs) layout.19with profiling.py — same plan, same (*args, **kwargs) layout.
20"""20"""
21+ 
21import logging22import logging
22 23 
23from ttk.core_modules.testcase_manager.param_plan import build_positional_args24from ttk.core_modules.testcase_manager.param_plan import build_positional_args
@@ -25,13 +26,27 @@ from ttk.core_modules.plugin_loader import get_plugin_function
25from ttk.utilities.container_utils import apply_as_list, flatten_nested_sequence26from ttk.utilities.container_utils import apply_as_list, flatten_nested_sequence
26 27 
27from .api_resolver import resolve_api28from .api_resolver import resolve_api
28-from .backends.cpu_backend import CpuTorchBackend as CpuBackend29+from .framework_detector import detect_framework
29from .framework_api_info_keeper import FrameworkApiInfoKeeper30from .framework_api_info_keeper import FrameworkApiInfoKeeper
30 31 
31-_cpu_backend = CpuBackend()32+_cpu_backend_cache = {}
32 33 
33 34 
34-def generate_golden(testcase, raw_inputs, plugin_path=None, switches=None, backend='cpu'):35+def _get_cpu_backend(framework="torch"):
36+ """Get cached CPU backend for the given framework."""
37+ if framework not in _cpu_backend_cache:
38+ if framework == "tf":
39+ from .backends.cpu_tf_backend import CpuTfBackend
40+ 
41+ _cpu_backend_cache[framework] = CpuTfBackend()
42+ else:
43+ from .backends.cpu_torch_backend import CpuTorchBackend
44+ 
45+ _cpu_backend_cache[framework] = CpuTorchBackend()
46+ return _cpu_backend_cache[framework]
47+ 
48+ 
49+def generate_golden(testcase, raw_inputs, plugin_path=None, switches=None, backend="cpu"):
35 """50 """
36 Generate golden data using three-level fallback.51 Generate golden data using three-level fallback.
37 52 
@@ -45,41 +60,42 @@ def generate_golden(testcase, raw_inputs, plugin_path=None, switches=None, backe
45 Returns:60 Returns:
46 list of numpy arrays (golden outputs)61 list of numpy arrays (golden outputs)
47 """62 """
48- golden_api = getattr(testcase, 'golden_api', None)63+ golden_api = getattr(testcase, "golden_api", None)
49 dist = testcase.tensor_list_dist64 dist = testcase.tensor_list_dist
50 api_name = testcase.api_name65 api_name = testcase.api_name
66+ framework = detect_framework(api_name)
67+ cpu_backend = _get_cpu_backend(framework)
51 68 
52 # --- Priority 1: CSV golden_api column (different API) ---69 # --- Priority 1: CSV golden_api column (different API) ---
53 if golden_api:70 if golden_api:
54 if golden_api.lower() == "disable":71 if golden_api.lower() == "disable":
55 return ["SUPPRESSED"]72 return ["SUPPRESSED"]
56- if golden_api.startswith(('torch_npu.', 'torch.npu')):73+ if cpu_backend.is_npu_only(golden_api):
74+ return ["UNSUPPORTED"]
75+ if not testcase.is_dtype_support():
76+ logging.debug(f"[golden] Skip golden_api={golden_api} for {api_name}: non-native dtype detected")
57 return ["UNSUPPORTED"]77 return ["UNSUPPORTED"]
58- if not testcase.is_torch_dtype_support():
59- logging.debug(f"[golden] Skip golden_api={golden_api} for {api_name}: "
60- f"non-torch-native dtype detected")
61- return ['UNSUPPORTED']
62 if golden_api == api_name:78 if golden_api == api_name:
63 golden_api_info = testcase.get_api_info()79 golden_api_info = testcase.get_api_info()
64 else:80 else:
65 golden_api_info = FrameworkApiInfoKeeper().get(golden_api)81 golden_api_info = FrameworkApiInfoKeeper().get(golden_api)
66- return _run_api_on_cpu(golden_api, raw_inputs, testcase, dist,82+ return _run_api_on_cpu(
67- api_info=golden_api_info)83+ golden_api, raw_inputs, testcase, dist, api_info=golden_api_info, cpu_backend=cpu_backend
84+ )
68 85 
69 # --- Priority 2: Custom plugin via plugin_loader ---86 # --- Priority 2: Custom plugin via plugin_loader ---
70 func = get_plugin_function(api_name, "golden", "e2e", plugin_path)87 func = get_plugin_function(api_name, "golden", "e2e", plugin_path)
71 if func is not None:88 if func is not None:
72- return _call_plugin_with_plan(testcase, func, switches, backend)89+ return _call_plugin_with_plan(testcase, func, switches, backend, cpu_backend)
73 90 
74 # --- Priority 3: Same API on CPU ---91 # --- Priority 3: Same API on CPU ---
75- if not testcase.is_torch_dtype_support():92+ if not testcase.is_dtype_support():
76- logging.debug(f"[golden] Skip CPU golden for {api_name}: "93+ logging.debug(f"[golden] Skip CPU golden for {api_name}: non-native dtype detected, no custom plugin found")
77- f"non-torch-native dtype detected, no custom plugin found")94+ return ["UNSUPPORTED"]
78- return ['UNSUPPORTED']95+ if cpu_backend.is_npu_only(api_name):
79- elif api_name.startswith(('torch_npu.', 'torch.npu')):
80 return ["UNSUPPORTED"]96 return ["UNSUPPORTED"]
81 try:97 try:
82- return _run_api_on_cpu(api_name, raw_inputs, testcase, dist)98+ return _run_api_on_cpu(api_name, raw_inputs, testcase, dist, cpu_backend=cpu_backend)
83 except Exception as e:99 except Exception as e:
84 raise RuntimeError(100 raise RuntimeError(
85 f"{api_name} cannot run on CPU and has no e2e custom plugin. "101 f"{api_name} cannot run on CPU and has no e2e custom plugin. "
@@ -87,14 +103,16 @@ def generate_golden(testcase, raw_inputs, plugin_path=None, switches=None, backe
87 ) from e103 ) from e
88 104 
89 105 
90-def _run_api_on_cpu(api_name, raw_inputs, testcase, dist, api_info=None):106+def _run_api_on_cpu(api_name, raw_inputs, testcase, dist, api_info=None, cpu_backend=None):
91 """Execute API on CPU, return numpy result list.107 """Execute API on CPU, return numpy result list.
92 108 
93 api_info=None: reuse testcase's param plan (same API path).109 api_info=None: reuse testcase's param plan (same API path).
94 api_info=...: independently match overload (golden_api path).110 api_info=...: independently match overload (golden_api path).
95 """111 """
96- cpu_inputs = [_cpu_backend.from_numpy(x.copy()) if x is not None else None112+ if cpu_backend is None:
97- for x in raw_inputs]113+ framework = detect_framework(api_name)
114+ cpu_backend = _get_cpu_backend(framework)
115+ cpu_inputs = [cpu_backend.from_numpy(x.copy()) if x is not None else None for x in raw_inputs]
98 if dist:116 if dist:
99 nested = apply_as_list(cpu_inputs, dist)117 nested = apply_as_list(cpu_inputs, dist)
100 else:118 else:
@@ -102,19 +120,22 @@ def _run_api_on_cpu(api_name, raw_inputs, testcase, dist, api_info=None):
102 120 
103 if api_info is not None:121 if api_info is not None:
104 args, kwargs, oidx = build_positional_args(122 args, kwargs, oidx = build_positional_args(
105- api_name, nested, testcase.attributes or {},123+ api_name,
124+ nested,
125+ testcase.attributes or {},
106 testcase.output_tensor_indexes,126 testcase.output_tensor_indexes,
107 tensor_distribution=[d > 0 for d in dist] if dist else None,127 tensor_distribution=[d > 0 for d in dist] if dist else None,
108- api_info=api_info)128+ api_info=api_info,
129+ )
109 else:130 else:
110 plan = testcase.get_param_plan()131 plan = testcase.get_param_plan()
111 args, kwargs, _ = plan.build_args(nested)132 args, kwargs, _ = plan.build_args(nested)
112 oidx = plan.overload_index133 oidx = plan.overload_index
113 134 
114- return _exec_and_convert(api_name, args, kwargs, oidx)135+ return _exec_and_convert(api_name, args, kwargs, oidx, cpu_backend)
115 136 
116 137 
117-def _call_plugin_with_plan(testcase, func, switches=None, backend='cpu'):138+def _call_plugin_with_plan(testcase, func, switches=None, backend="cpu", cpu_backend=None):
118 """Call golden plugin using testcase's param plan — same arg order as profiling.139 """Call golden plugin using testcase's param plan — same arg order as profiling.
119 Interface is identical to input plugin, except golden returns values.140 Interface is identical to input plugin, except golden returns values.
120 Uses testcase.tensors (CPU nested tensors) directly, like aclnn.141 Uses testcase.tensors (CPU nested tensors) directly, like aclnn.
@@ -122,67 +143,53 @@ def _call_plugin_with_plan(testcase, func, switches=None, backend='cpu'):
122 plan = testcase.get_param_plan()143 plan = testcase.get_param_plan()
123 if plan is None:144 if plan is None:
124 raise RuntimeError(f"No param plan for {testcase.api_name}, cannot call golden plugin")145 raise RuntimeError(f"No param plan for {testcase.api_name}, cannot call golden plugin")
125- use_torch = testcase.is_torch_dtype_support()146+ if cpu_backend is None:
147+ framework = detect_framework(testcase.api_name)
148+ cpu_backend = _get_cpu_backend(framework)
149+ use_numpy = cpu_backend.needs_numpy_fallback(testcase)
126 150 
127 args, kwargs, extra_attrs = plan.build_args(testcase.tensors)151 args, kwargs, extra_attrs = plan.build_args(testcase.tensors)
128 extra = {152 extra = {
129- 'backend': backend,153+ "backend": backend,
130- 'tensor_formats': testcase.tensor_formats,154+ "tensor_formats": testcase.tensor_formats,
131- 'tensor_dtypes': testcase.tensor_dtypes,155+ "tensor_dtypes": testcase.tensor_dtypes,
132- 'use_torch': use_torch,156+ "use_numpy": use_numpy,
133- 'short_soc_version': getattr(switches, 'short_soc_version', None),157+ "short_soc_version": getattr(switches, "short_soc_version", None),
134- 'testcase_name': testcase.testcase_name,158+ "testcase_name": testcase.testcase_name,
135 }159 }
136 extra.update(extra_attrs)160 extra.update(extra_attrs)
137 161 
138- if hasattr(testcase, 'batch_axis') and testcase.batch_axis is not None:162+ if hasattr(testcase, "batch_axis") and testcase.batch_axis is not None:
139- extra['batch_axis'] = testcase.batch_axis163+ extra["batch_axis"] = testcase.batch_axis
140- if hasattr(testcase, 'batch_slice_info') and testcase.batch_slice_info is not None:164+ if hasattr(testcase, "batch_slice_info") and testcase.batch_slice_info is not None:
141- extra['batch_slice_info'] = testcase.batch_slice_info165+ extra["batch_slice_info"] = testcase.batch_slice_info
142- if hasattr(testcase, 'batch_seed') and testcase.batch_seed is not None:166+ if hasattr(testcase, "batch_seed") and testcase.batch_seed is not None:
143- extra['batch_seed'] = testcase.batch_seed167+ extra["batch_seed"] = testcase.batch_seed
144 168 
145 import inspect169 import inspect
170+ 
146 sig = inspect.signature(func)171 sig = inspect.signature(func)
147 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):172 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
148 kwargs.update(extra)173 kwargs.update(extra)
149 else:174 else:
150 kwargs.update({k: v for k, v in extra.items() if k in sig.parameters})175 kwargs.update({k: v for k, v in extra.items() if k in sig.parameters})
151 result = func(*args, **kwargs)176 result = func(*args, **kwargs)
152- return _to_numpy_result(result)177+ return cpu_backend.result_to_numpy(result)
153 178 
154 179 
155-def _to_numpy_result(result):180+def _exec_and_convert(api_name, args, kwargs, overload_index=0, cpu_backend=None):
156- """Convert golden result to list of numpy arrays. Handles Tensor and scalar returns."""
157- import numpy as np
158- import torch
159- if isinstance(result, (tuple, list)):
160- nps = []
161- for r in result:
162- if r is None:
163- nps.append(None)
164- elif isinstance(r, torch.Tensor):
165- nps.append(_cpu_backend.to_numpy(r))
166- else:
167- nps.append(np.array(r))
168- return nps
169- if isinstance(result, torch.Tensor):
170- return [_cpu_backend.to_numpy(result)]
171- if result is None:
172- return [None]
173- return [np.array(result)]
174- 
175- 
176-def _exec_and_convert(api_name, args, kwargs, overload_index=0):
177 """Execute API and convert result to list of numpy arrays."""181 """Execute API and convert result to list of numpy arrays."""
178 from .eager_execution import call_api182 from .eager_execution import call_api
179- resolved, is_tensor_method = resolve_api(api_name)
180- if is_tensor_method:
181- if args[0] is None:
182- return [None]
183- result = call_api(api_name, overload_index,
184- getattr(args[0], resolved), args[1:], kwargs)
185- else:
186- result = call_api(api_name, overload_index, resolved, args, kwargs)
187 183 
188- return _to_numpy_result(result)184+ resolved, is_tensor_method = resolve_api(api_name)
185+ if cpu_backend is None:
186+ framework = detect_framework(api_name)
187+ cpu_backend = _get_cpu_backend(framework)
188+ with cpu_backend.device_scope(0):
189+ if is_tensor_method:
190+ if args[0] is None:
191+ return [None]
192+ result = call_api(api_name, overload_index, getattr(args[0], resolved), args[1:], kwargs)
193+ else:
194+ result = call_api(api_name, overload_index, resolved, args, kwargs)
195+ return cpu_backend.result_to_numpy(result)
@@ -13,6 +13,7 @@
13Graph mode execution for framework_api.13Graph mode execution for framework_api.
14Wraps API in GraphNetwork + torch.compile for GE graph mode testing.14Wraps API in GraphNetwork + torch.compile for GE graph mode testing.
15"""15"""
16+ 
16import functools17import functools
17import logging18import logging
18 19 
@@ -22,7 +23,7 @@ from ttk.test_spec import get_spec_attr
22 23 
23from .graph_network import GraphNetwork, split_params24from .graph_network import GraphNetwork, split_params
24from .profiler import get_profiler25from .profiler import get_profiler
25-from .profiling_utils import clone_preserving_stride, prepare_device_args, result_to_numpy26+from .profiling_utils import prepare_device_args
26 27 
27WARMUP_COUNT = 528WARMUP_COUNT = 5
28 29 
@@ -32,15 +33,18 @@ def _get_npu_backend():
32 import torch_npu33 import torch_npu
33 import torchair34 import torchair
34 from torchair.configs.compiler_config import CompilerConfig35 from torchair.configs.compiler_config import CompilerConfig
36+ 
35 config = CompilerConfig()37 config = CompilerConfig()
36 return torchair.get_npu_backend(compiler_config=config)38 return torchair.get_npu_backend(compiler_config=config)
37 39 
40+ 
38@functools.lru_cache(maxsize=1)41@functools.lru_cache(maxsize=1)
39def _get_npu_backend_aclgraph():42def _get_npu_backend_aclgraph():
40 """获取 aclgraph 模式的 NPU backend。"""43 """获取 aclgraph 模式的 NPU backend。"""
41 npu_backend = "npugraph_ex"44 npu_backend = "npugraph_ex"
42 return npu_backend45 return npu_backend
43 46 
47+ 
44def _compile_model(model, backend, dynamic, fullgraph):48def _compile_model(model, backend, dynamic, fullgraph):
45 """Compile model with torch.compile. Returns compiled callable or raises."""49 """Compile model with torch.compile. Returns compiled callable or raises."""
46 compiled = torch.compile(50 compiled = torch.compile(
@@ -51,6 +55,7 @@ def _compile_model(model, backend, dynamic, fullgraph):
51 )55 )
52 return compiled56 return compiled
53 57 
58+ 
54def _compile_model_aclgraph(model, backend):59def _compile_model_aclgraph(model, backend):
55 """以 aclgraph 模式编译模型"""60 """以 aclgraph 模式编译模型"""
56 compiled = torch.compile(61 compiled = torch.compile(
@@ -61,16 +66,27 @@ def _compile_model_aclgraph(model, backend):
61 )66 )
62 return compiled67 return compiled
63 68 
64-def _run_compiled(compiled, args, kwargs, backend, dev_id, switches,69+ 
65- is_inplace, inplace_backup, api_name,70+def _run_compiled(
66- inplace_backups=None, inplace_kwargs_keys=None):71+ compiled,
67- """Run compiled model with warmup + profiling. Returns (result_nps, perf)."""72+ args,
73+ kwargs,
74+ backend,
75+ dev_id,
76+ switches,
77+ is_inplace,
78+ inplace_backup,
79+ api_name,
80+ testcase_name="",
81+ inplace_backups=None,
82+ inplace_kwargs_keys=None,
83+):
68 run_count = switches.run_time84 run_count = switches.run_time
69 is_kwargs_mode = inplace_kwargs_keys is not None85 is_kwargs_mode = inplace_kwargs_keys is not None
70 86 
71 result = compiled(*args, **kwargs)87 result = compiled(*args, **kwargs)
72 backend.synchronize(dev_id)88 backend.synchronize(dev_id)
73- result_nps = result_to_numpy(result, backend, copy=is_inplace)89+ result_nps = backend.result_to_numpy(result, copy=is_inplace)
74 90 
75 if switches.warmup:91 if switches.warmup:
76 for _ in range(WARMUP_COUNT):92 for _ in range(WARMUP_COUNT):
@@ -106,19 +122,19 @@ def _run_compiled(compiled, args, kwargs, backend, dev_id, switches,
106 for idx, key in inplace_kwargs_keys.items():122 for idx, key in inplace_kwargs_keys.items():
107 if key in kwargs and kwargs[key] is not None:123 if key in kwargs and kwargs[key] is not None:
108 original_tensors[idx] = (key, kwargs[key])124 original_tensors[idx] = (key, kwargs[key])
109- inplace_clones[idx] = (key, [clone_preserving_stride(kwargs[key]) for _ in range(run_count - 1)])125+ inplace_clones[idx] = (key, [backend.clone(kwargs[key]) for _ in range(run_count - 1)])
110 else:126 else:
111 if inplace_backups:127 if inplace_backups:
112 for idx in inplace_backups:128 for idx in inplace_backups:
113 if idx < len(args) and args[idx] is not None:129 if idx < len(args) and args[idx] is not None:
114 original_tensors[idx] = args[idx]130 original_tensors[idx] = args[idx]
115- inplace_clones[idx] = [clone_preserving_stride(args[idx]) for _ in range(run_count - 1)]131+ inplace_clones[idx] = [backend.clone(args[idx]) for _ in range(run_count - 1)]
116 if is_inplace and inplace_backup is not None and 0 not in original_tensors:132 if is_inplace and inplace_backup is not None and 0 not in original_tensors:
117 if args and args[0] is not None:133 if args and args[0] is not None:
118 original_tensors[0] = args[0]134 original_tensors[0] = args[0]
119- inplace_clones[0] = [clone_preserving_stride(args[0]) for _ in range(run_count - 1)]135+ inplace_clones[0] = [backend.clone(args[0]) for _ in range(run_count - 1)]
120 136 
121- profiler = get_profiler(api_name, backend)137+ profiler = get_profiler(api_name, backend, testcase_name=testcase_name, root_path=switches.root_path)
122 with profiler:138 with profiler:
123 for i in range(run_count):139 for i in range(run_count):
124 if i < run_count - 1:140 if i < run_count - 1:
@@ -143,12 +159,24 @@ def _run_compiled(compiled, args, kwargs, backend, dev_id, switches,
143 perf = profiler.result(backend, run_count)159 perf = profiler.result(backend, run_count)
144 160 
145 if not is_inplace:161 if not is_inplace:
146- result_nps = result_to_numpy(result, backend, copy=is_inplace)162+ result_nps = backend.result_to_numpy(result, copy=is_inplace)
147 163 
148 return result_nps, perf164 return result_nps, perf
149 165 
150 166 
151-def _execute_graph(testcase, backend, dev_id, switches, plan, resolved, is_tensor_method, is_inplace, raw_inputs, dynamic, is_aclgraph=False):167+def _execute_graph(
168+ testcase,
169+ backend,
170+ dev_id,
171+ switches,
172+ plan,
173+ resolved,
174+ is_tensor_method,
175+ is_inplace,
176+ raw_inputs,
177+ dynamic,
178+ is_aclgraph=False,
179+):
152 """180 """
153 Execute API in GE graph mode via torch.compile with profiling.181 Execute API in GE graph mode via torch.compile with profiling.
154 182 
@@ -172,6 +200,7 @@ def _execute_graph(testcase, backend, dev_id, switches, plan, resolved, is_tenso
172 return [], None200 return [], None
173 201 
174 import torch_npu202 import torch_npu
203+ 
175 torch_npu.npu.set_device(dev_id)204 torch_npu.npu.set_device(dev_id)
176 205 
177 if is_aclgraph:206 if is_aclgraph:
@@ -186,20 +215,19 @@ def _execute_graph(testcase, backend, dev_id, switches, plan, resolved, is_tenso
186 215 
187 inplace_backup = None216 inplace_backup = None
188 217 
189- inplace_input_indexes = getattr(testcase, 'inplace_input_indexes', None) or ()218+ inplace_input_indexes = getattr(testcase, "inplace_input_indexes", None) or ()
190 inplace_backups = {}219 inplace_backups = {}
191 if inplace_input_indexes:220 if inplace_input_indexes:
192 for idx in sorted(inplace_input_indexes):221 for idx in sorted(inplace_input_indexes):
193 if idx < len(args) and args[idx] is not None:222 if idx < len(args) and args[idx] is not None:
194- inplace_backups[idx] = clone_preserving_stride(args[idx])223+ inplace_backups[idx] = backend.clone(args[idx])
195 224 
196 custom_cls = get_spec_attr(testcase.api_name, "torch_graph", switches.plugin_path)225 custom_cls = get_spec_attr(testcase.api_name, "torch_graph", switches.plugin_path)
197 226 
198 inplace_kwargs_keys = None227 inplace_kwargs_keys = None
199 if custom_cls:228 if custom_cls:
200 logging.info(f"Using custom graph module: {custom_cls.__name__}")229 logging.info(f"Using custom graph module: {custom_cls.__name__}")
201- init_kwargs, fwd_kwargs = split_params(230+ init_kwargs, fwd_kwargs = split_params(custom_cls, plan.overload_params, args, kwargs)
202- custom_cls, plan.overload_params, args, kwargs)
203 model = custom_cls(**init_kwargs)231 model = custom_cls(**init_kwargs)
204 run_args, run_kwargs = [], fwd_kwargs232 run_args, run_kwargs = [], fwd_kwargs
205 run_inplace = is_inplace233 run_inplace = is_inplace
@@ -214,9 +242,10 @@ def _execute_graph(testcase, backend, dev_id, switches, plan, resolved, is_tenso
214 else:242 else:
215 logging.info("Using generic GraphNetwork")243 logging.info("Using generic GraphNetwork")
216 if is_inplace:244 if is_inplace:
217- inplace_backup = clone_preserving_stride(args[0]) if args and args[0] is not None else None245+ inplace_backup = backend.clone(args[0]) if args and args[0] is not None else None
218 246 
219 if is_tensor_method:247 if is_tensor_method:
248+ 
220 def api_caller(*args, **kwargs):249 def api_caller(*args, **kwargs):
221 return getattr(args[0], resolved)(*args[1:], **kwargs)250 return getattr(args[0], resolved)(*args[1:], **kwargs)
222 else:251 else:
@@ -241,10 +270,19 @@ def _execute_graph(testcase, backend, dev_id, switches, plan, resolved, is_tenso
241 else:270 else:
242 compiled = _compile_model(model, npu_backend, dynamic, use_fullgraph)271 compiled = _compile_model(model, npu_backend, dynamic, use_fullgraph)
243 result_nps, perf = _run_compiled(272 result_nps, perf = _run_compiled(
244- compiled, run_args, run_kwargs, backend, dev_id, switches,273+ compiled,
245- run_inplace, inplace_backup if run_inplace else None, testcase.api_name,274+ run_args,
275+ run_kwargs,
276+ backend,
277+ dev_id,
278+ switches,
279+ run_inplace,
280+ inplace_backup if run_inplace else None,
281+ testcase.api_name,
282+ testcase_name=testcase.testcase_name,
246 inplace_backups=inplace_backups if inplace_input_indexes else None,283 inplace_backups=inplace_backups if inplace_input_indexes else None,
247- inplace_kwargs_keys=inplace_kwargs_keys)284+ inplace_kwargs_keys=inplace_kwargs_keys,
285+ )
248 286 
249 except Exception as e:287 except Exception as e:
250 logging.error(f"Graph {mode_str} execution failed: {e}", exc_info=True)288 logging.error(f"Graph {mode_str} execution failed: {e}", exc_info=True)
@@ -15,6 +15,7 @@ Input data generation for framework_api tests.
15Generates numpy arrays from testcase metadata, applies custom plugin overrides,15Generates numpy arrays from testcase metadata, applies custom plugin overrides,
16and converts to framework tensors (torch).16and converts to framework tensors (torch).
17"""17"""
18+ 
18import numpy as np19import numpy as np
19 20 
20from ttk.core_modules.plugin_loader import get_plugin_function21from ttk.core_modules.plugin_loader import get_plugin_function
@@ -41,47 +42,43 @@ def generate_inputs(testcase, switches, backend, plan, stored_inputs=None):
41 if stored_inputs is not None:42 if stored_inputs is not None:
42 testcase.np_storages = list(stored_inputs)43 testcase.np_storages = list(stored_inputs)
43 raw_inputs = build_views_from_storages(testcase)44 raw_inputs = build_views_from_storages(testcase)
44- _set_runtime_tensors(testcase, raw_inputs)45+ _set_runtime_tensors(testcase, raw_inputs, backend)
45 return raw_inputs46 return raw_inputs
46 47 
47 raw_inputs = default_generate_inputs(testcase, switches)48 raw_inputs = default_generate_inputs(testcase, switches)
48 override_tensors_from_attributes(testcase, raw_inputs)49 override_tensors_from_attributes(testcase, raw_inputs)
49 50 
50 plugin_path = switches.plugin_path51 plugin_path = switches.plugin_path
51- input_func = get_plugin_function(52+ input_func = get_plugin_function(testcase.api_name, "input", "e2e", plugin_path)
52- testcase.api_name, "input", "e2e", plugin_path
53- )
54 if input_func is not None:53 if input_func is not None:
55- use_torch = testcase.is_torch_dtype_support()54+ plugin_inputs = backend.inputs_from_numpy(testcase, raw_inputs)
55+ use_numpy = backend.needs_numpy_fallback(testcase)
56 dist = testcase.tensor_list_dist56 dist = testcase.tensor_list_dist
57- if use_torch:
58- plugin_inputs = np_to_torch_inputs(testcase, raw_inputs)
59- else:
60- plugin_inputs = raw_inputs
61 if dist:57 if dist:
62 nested_for_plugin = apply_as_list(plugin_inputs, dist)58 nested_for_plugin = apply_as_list(plugin_inputs, dist)
63 else:59 else:
64 nested_for_plugin = plugin_inputs60 nested_for_plugin = plugin_inputs
65 args, kwargs, extra_attrs = plan.build_args(nested_for_plugin)61 args, kwargs, extra_attrs = plan.build_args(nested_for_plugin)
66 extra = {62 extra = {
67- 'backend': backend.alias(),63+ "backend": backend.device_type(),
68- 'tensor_formats': testcase.tensor_formats,64+ "tensor_formats": testcase.tensor_formats,
69- 'tensor_dtypes': testcase.tensor_dtypes,65+ "tensor_dtypes": testcase.tensor_dtypes,
70- 'use_torch': use_torch,66+ "use_numpy": use_numpy,
71- 'short_soc_version': switches.short_soc_version,67+ "short_soc_version": switches.short_soc_version,
72- 'testcase_name': testcase.testcase_name,68+ "testcase_name": testcase.testcase_name,
73- 'input_ranges': testcase.input_data_ranges,69+ "input_ranges": testcase.input_data_ranges,
74 }70 }
75 extra.update(extra_attrs)71 extra.update(extra_attrs)
76 72 
77- if hasattr(testcase, 'batch_axis') and testcase.batch_axis is not None:73+ if hasattr(testcase, "batch_axis") and testcase.batch_axis is not None:
78- extra['batch_axis'] = testcase.batch_axis74+ extra["batch_axis"] = testcase.batch_axis
79- if hasattr(testcase, 'batch_slice_info') and testcase.batch_slice_info is not None:75+ if hasattr(testcase, "batch_slice_info") and testcase.batch_slice_info is not None:
80- extra['batch_slice_info'] = testcase.batch_slice_info76+ extra["batch_slice_info"] = testcase.batch_slice_info
81- if hasattr(testcase, 'batch_seed') and testcase.batch_seed is not None:77+ if hasattr(testcase, "batch_seed") and testcase.batch_seed is not None:
82- extra['batch_seed'] = testcase.batch_seed78+ extra["batch_seed"] = testcase.batch_seed
83 79 
84 import inspect80 import inspect
81+ 
85 sig = inspect.signature(input_func)82 sig = inspect.signature(input_func)
86 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):83 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
87 kwargs.update(extra)84 kwargs.update(extra)
@@ -89,17 +86,13 @@ def generate_inputs(testcase, switches, backend, plan, stored_inputs=None):
89 kwargs.update({k: v for k, v in extra.items() if k in sig.parameters})86 kwargs.update({k: v for k, v in extra.items() if k in sig.parameters})
90 input_func(*args, **kwargs)87 input_func(*args, **kwargs)
91 88 
92- _set_runtime_tensors(testcase, raw_inputs)89+ _set_runtime_tensors(testcase, raw_inputs, backend)
93 return raw_inputs90 return raw_inputs
94 91 
95 92 
96-def _set_runtime_tensors(testcase, raw_inputs):93+def _set_runtime_tensors(testcase, raw_inputs, backend):
97 """Rebuild framework tensors and TensorList nesting from backing storages."""94 """Rebuild framework tensors and TensorList nesting from backing storages."""
98- use_torch = testcase.is_torch_dtype_support()95+ flat_tensors = backend.inputs_from_numpy(testcase, raw_inputs)
99- if use_torch:
100- flat_tensors = np_to_torch_inputs(testcase, raw_inputs)
101- else:
102- flat_tensors = list(raw_inputs)
103 dist = testcase.tensor_list_dist96 dist = testcase.tensor_list_dist
104 if dist:97 if dist:
105 testcase.tensors = apply_as_list(flat_tensors, dist)98 testcase.tensors = apply_as_list(flat_tensors, dist)
@@ -116,10 +109,10 @@ def np_to_torch_inputs(testcase, raw_inputs):
116 """109 """
117 import torch110 import torch
118 from ttk.utilities.dtypes import numpy_to_torch_tensor111 from ttk.utilities.dtypes import numpy_to_torch_tensor
119- np_storages = getattr(testcase, 'np_storages', None)112+ 
113+ np_storages = getattr(testcase, "np_storages", None)
120 if np_storages is None:114 if np_storages is None:
121- return [torch.from_numpy(np.ascontiguousarray(arr)) if arr is not None else None115+ return [torch.from_numpy(np.ascontiguousarray(arr)) if arr is not None else None for arr in raw_inputs]
122- for arr in raw_inputs]
123 flat_shapes = testcase.flat_tensor_view_shapes116 flat_shapes = testcase.flat_tensor_view_shapes
124 flat_dtypes = testcase.flat_tensor_dtypes117 flat_dtypes = testcase.flat_tensor_dtypes
125 result = []118 result = []
@@ -140,6 +133,31 @@ def np_to_torch_inputs(testcase, raw_inputs):
140 return result133 return result
141 134 
142 135 
136+def np_to_tf_inputs(testcase, raw_inputs):
137+ """Convert numpy arrays to tf.Tensor.
138+ 
139+ TF tensors are immutable, so non-contiguous views cannot use as_strided.
140+ Instead, we generate contiguous tensors from the raw numpy views.
141+ Tensors sourced from attributes use tf.constant for graph-mode const folding.
142+ """
143+ import tensorflow as tf
144+ from ttk.utilities.dtypes import normalize_to_tf_dtype
145+ 
146+ const_indexes = getattr(testcase, "const_input_indexes", None) or set()
147+ result = []
148+ for idx, arr in enumerate(raw_inputs):
149+ if arr is None:
150+ result.append(None)
151+ continue
152+ contiguous = np.ascontiguousarray(arr)
153+ contiguous = normalize_to_tf_dtype(contiguous)
154+ if idx in const_indexes:
155+ result.append(tf.constant(contiguous))
156+ else:
157+ result.append(tf.convert_to_tensor(contiguous))
158+ return result
159+ 
160+ 
143def assign_tensor_value(arr, val, label):161def assign_tensor_value(arr, val, label):
144 """Assign a Python value to a numpy array (possibly scalar/non-contiguous/empty)."""162 """Assign a Python value to a numpy array (possibly scalar/non-contiguous/empty)."""
145 if arr.size == 0:163 if arr.size == 0:
@@ -161,6 +179,9 @@ def override_tensors_from_attributes(testcase, raw_inputs):
161 For TensorList, two modes:179 For TensorList, two modes:
162 1. Scalar broadcast: val is a single number or [scalar] -> all sub-tensors get same value180 1. Scalar broadcast: val is a single number or [scalar] -> all sub-tensors get same value
163 2. Per-tensor: val is a list, len must match sub-tensor count -> applied one-by-one181 2. Per-tensor: val is a list, len must match sub-tensor count -> applied one-by-one
182+ 
183+ Records tensor indexes sourced from attributes in testcase.const_input_indexes
184+ so backends can create const tensors (tf.constant / torch.tensor) for them.
164 """185 """
165 info = testcase.get_api_info()186 info = testcase.get_api_info()
166 if not info or not testcase.attributes:187 if not info or not testcase.attributes:
@@ -179,12 +200,14 @@ def override_tensors_from_attributes(testcase, raw_inputs):
179 if param.name not in testcase.attributes:200 if param.name not in testcase.attributes:
180 continue201 continue
181 val = testcase.attributes[param.name]202 val = testcase.attributes[param.name]
182- if param.is_tensor_like and val in (None, 'None', ''):203+ if param.is_tensor_like and val in (None, "None", ""):
183 raise ValueError(204 raise ValueError(
184 f"[{testcase.testcase_name}] Invalid testcase: param '{param.name}' "205 f"[{testcase.testcase_name}] Invalid testcase: param '{param.name}' "
185 f"has a tensor in view_shapes but attributes specifies "206 f"has a tensor in view_shapes but attributes specifies "
186 f"{repr(val)}. Tensor params should be provided via "207 f"{repr(val)}. Tensor params should be provided via "
187- f"view_shapes or with a concrete value in attributes, not None.")208+ f"view_shapes or with a concrete value in attributes, not None."
209+ )
210+ testcase.const_input_indexes.add(idx)
188 if param.is_tensor_list:211 if param.is_tensor_list:
189 sub_tensors = nested_np[idx]212 sub_tensors = nested_np[idx]
190 num_sub = len(sub_tensors)213 num_sub = len(sub_tensors)
@@ -196,11 +219,11 @@ def override_tensors_from_attributes(testcase, raw_inputs):
196 if len(val) != num_sub:219 if len(val) != num_sub:
197 raise ValueError(220 raise ValueError(
198 f"Specify TensorList [{param.name}] for case [{testcase.testcase_name}] "221 f"Specify TensorList [{param.name}] for case [{testcase.testcase_name}] "
199- f"from `attributes` length mismatch: got {len(val)}, expected {num_sub}.")222+ f"from `attributes` length mismatch: got {len(val)}, expected {num_sub}."
223+ )
200 per_tensor_val = list(val)224 per_tensor_val = list(val)
201 for j in range(num_sub):225 for j in range(num_sub):
202- assign_tensor_value(sub_tensors[j], per_tensor_val[j],226+ assign_tensor_value(sub_tensors[j], per_tensor_val[j], f"{param.name}[{j}]")
203- f"{param.name}[{j}]")
204 else:227 else:
205 assign_tensor_value(nested_np[idx], val, param.name)228 assign_tensor_value(nested_np[idx], val, param.name)
206 229 
@@ -228,8 +251,8 @@ def generate_np_storages(testcase, switches):
228 flat_shapes = testcase.flat_tensor_view_shapes251 flat_shapes = testcase.flat_tensor_view_shapes
229 flat_dtypes = resolve_custom_numpy_dtypes(testcase.flat_tensor_dtypes)252 flat_dtypes = resolve_custom_numpy_dtypes(testcase.flat_tensor_dtypes)
230 ranges = testcase.flat_input_data_ranges or ()253 ranges = testcase.flat_input_data_ranges or ()
231- base_seed = getattr(switches, 'random_seed', None)254+ base_seed = getattr(switches, "random_seed", None)
232- batch_seed = getattr(testcase, 'batch_seed', None)255+ batch_seed = getattr(testcase, "batch_seed", None)
233 for idx, view_shape in enumerate(flat_shapes):256 for idx, view_shape in enumerate(flat_shapes):
234 if view_shape is None:257 if view_shape is None:
235 np_storages.append(None)258 np_storages.append(None)
@@ -242,12 +265,13 @@ def generate_np_storages(testcase, switches):
242 if idx not in pure_output_indexes:265 if idx not in pure_output_indexes:
243 if base_seed and batch_seed is not None:266 if base_seed and batch_seed is not None:
244 # batch consistency compare different case support same shape tensor has same value267 # batch consistency compare different case support same shape tensor has same value
245- np.random.seed(base_seed + idx) 268+ np.random.seed(base_seed + idx)
246 rd = RandomData(dtype, s_shape, data_range)269 rd = RandomData(dtype, s_shape, data_range)
247 np_storages.append(rd.generate(distribution))270 np_storages.append(rd.generate(distribution))
248 else:271 else:
249 from ttk.utilities.data import fixed_np_array272 from ttk.utilities.data import fixed_np_array
250- init_val = 0 if testcase.api_name in ("torch.ones",) else 1273+ 
274+ init_val = 0 if testcase.api_name in ("torch.ones", "tf.ones") else 1
251 np_storages.append(fixed_np_array(dtype, s_shape, init_value=init_val))275 np_storages.append(fixed_np_array(dtype, s_shape, init_value=init_val))
252 276 
253 testcase.np_storages = np_storages277 testcase.np_storages = np_storages
@@ -275,6 +299,7 @@ def build_views_from_storages(testcase):
275def to_non_contiguous_view(storage, view_shape, view_stride, view_offset):299def to_non_contiguous_view(storage, view_shape, view_stride, view_offset):
276 """Create non-contiguous view from contiguous storage using numpy as_strided."""300 """Create non-contiguous view from contiguous storage using numpy as_strided."""
277 from ttk.utilities.dtypes import np_as_strided_safe301 from ttk.utilities.dtypes import np_as_strided_safe
302+ 
278 dtype = storage.dtype303 dtype = storage.dtype
279 byte_strides = tuple(s * dtype.itemsize for s in view_stride)304 byte_strides = tuple(s * dtype.itemsize for s in view_stride)
280 if view_offset and view_offset > 0:305 if view_offset and view_offset > 0:
@@ -12,6 +12,7 @@
12"""12"""
13FrameworkApiInstance — InstanceBase implementation for framework_api tests.13FrameworkApiInstance — InstanceBase implementation for framework_api tests.
14"""14"""
15+ 
15import logging16import logging
16import os17import os
17 18 
@@ -30,10 +31,11 @@ class FrameworkApiInstance(InstanceBase):
30 switches = get_global_storage()31 switches = get_global_storage()
31 if switches.backend == "npusim":32 if switches.backend == "npusim":
32 self._inject_camodel_env(switches)33 self._inject_camodel_env(switches)
33- self.backend = get_backend(switches.force_cpu)34+ framework = getattr(switches, "framework", "torch")
34- if not self.backend.use_device():35+ self.backend = get_backend(switches.force_cpu, framework=framework)
36+ if not self.backend.has_device():
35 switches.proc_no_reuse = True37 switches.proc_no_reuse = True
36- logging.info(f"Framework API mode: backend={self.backend.alias()}")38+ logging.info(f"Framework API mode: backend={self.backend.device_type()}, framework={framework}")
37 39 
38 @staticmethod40 @staticmethod
39 def _inject_camodel_env(switches):41 def _inject_camodel_env(switches):
@@ -79,9 +81,7 @@ class FrameworkApiInstance(InstanceBase):
79 logging.info(f"Device platform: {switches.dev_plat}")81 logging.info(f"Device platform: {switches.dev_plat}")
80 82 
81 def setup_profile_object(self):83 def setup_profile_object(self):
82- self.profile_object = FrameworkApiProfileObject(84+ self.profile_object = FrameworkApiProfileObject(self.task_keeper, self.mp_context, self.backend)
83- self.task_keeper, self.mp_context, self.backend
84- )
85 85 
86 def device_info(self, dev_id: int) -> str:86 def device_info(self, dev_id: int) -> str:
87- return f"{self.backend.alias()}:{dev_id}"87+ return f"{self.backend.device_type()}:{dev_id}"
@@ -14,11 +14,10 @@ Performance profiling using framework-specific profilers.
14Context manager pattern: profiler only collects data within `with` block.14Context manager pattern: profiler only collects data within `with` block.
15Warmup and repeat logic is controlled by the caller.15Warmup and repeat logic is controlled by the caller.
16"""16"""
17+ 
17import csv18import csv
18import logging19import logging
19import os20import os
20-import shutil
21-import tempfile
22import time21import time
23from abc import ABC, abstractmethod22from abc import ABC, abstractmethod
24from dataclasses import dataclass, field23from dataclasses import dataclass, field
@@ -71,15 +70,12 @@ class NpuProfiler(FrameworkProfiler):
71 kernel_details.csv / operator_details.csv for device-side timing.70 kernel_details.csv / operator_details.csv for device-side timing.
72 """71 """
73 72 
74- def __init__(self, backend):73+ def __init__(self, backend, testcase_name="", root_path="."):
75- self._tmpdir = tempfile.mkdtemp(prefix="ttk_npu_prof_")74+ self._testcase_name = testcase_name or "unknown"
75+ self._outdir = os.path.join(root_path, "msprof", "e2e", self._testcase_name)
76+ os.makedirs(self._outdir, exist_ok=True)
76 self._prof = None77 self._prof = None
77 78 
78- def _cleanup_tmpdir(self):
79- if self._tmpdir and os.path.isdir(self._tmpdir):
80- shutil.rmtree(self._tmpdir, ignore_errors=True)
81- self._tmpdir = None
82- 
83 def __enter__(self):79 def __enter__(self):
84 from torch_npu.profiler import (80 from torch_npu.profiler import (
85 ProfilerActivity,81 ProfilerActivity,
@@ -102,10 +98,9 @@ class NpuProfiler(FrameworkProfiler):
102 record_shapes=True,98 record_shapes=True,
103 experimental_config=experimental_config,99 experimental_config=experimental_config,
104 schedule=schedule(wait=0, warmup=1, active=1, repeat=1),100 schedule=schedule(wait=0, warmup=1, active=1, repeat=1),
105- on_trace_ready=tensorboard_trace_handler(self._tmpdir),101+ on_trace_ready=tensorboard_trace_handler(self._outdir),
106 )102 )
107 self._prof.start()103 self._prof.start()
108- # Advance past warmup phase so the active phase starts on __exit__'s step()
109 self._prof.step()104 self._prof.step()
110 return self105 return self
111 106 
@@ -114,9 +109,6 @@ class NpuProfiler(FrameworkProfiler):
114 self._prof.step()109 self._prof.step()
115 self._prof.stop()110 self._prof.stop()
116 111 
117- def __del__(self):
118- self._cleanup_tmpdir()
119- 
120 def result(self, backend, repeat_count) -> ProfileResult:112 def result(self, backend, repeat_count) -> ProfileResult:
121 kernel_csv = self._find_csv("kernel_details.csv")113 kernel_csv = self._find_csv("kernel_details.csv")
122 operator_csv = self._find_csv("operator_details.csv")114 operator_csv = self._find_csv("operator_details.csv")
@@ -130,8 +122,6 @@ class NpuProfiler(FrameworkProfiler):
130 if operator_csv:122 if operator_csv:
131 total_cpu_us = self._parse_operator_cpu_time(operator_csv)123 total_cpu_us = self._parse_operator_cpu_time(operator_csv)
132 124 
133- self._cleanup_tmpdir()
134- 
135 return ProfileResult(125 return ProfileResult(
136 elapsed_us=total_device_us / max(repeat_count, 1),126 elapsed_us=total_device_us / max(repeat_count, 1),
137 kernel_details=KernelDetails(127 kernel_details=KernelDetails(
@@ -143,7 +133,7 @@ class NpuProfiler(FrameworkProfiler):
143 133 
144 def _find_csv(self, filename):134 def _find_csv(self, filename):
145 """Find a CSV file in the profiler output directory tree."""135 """Find a CSV file in the profiler output directory tree."""
146- for root, _, files in os.walk(self._tmpdir):136+ for root, _, files in os.walk(self._outdir):
147 if filename in files:137 if filename in files:
148 return os.path.join(root, filename)138 return os.path.join(root, filename)
149 return None139 return None
@@ -171,8 +161,12 @@ class NpuProfiler(FrameworkProfiler):
171 kernels_map[name]["max_us"] = max(kernels_map[name]["max_us"], duration)161 kernels_map[name]["max_us"] = max(kernels_map[name]["max_us"], duration)
172 kernels_map[name]["min_us"] = min(kernels_map[name]["min_us"], duration)162 kernels_map[name]["min_us"] = min(kernels_map[name]["min_us"], duration)
173 else:163 else:
174- kernels_map[name] = {"total_us": duration, "calls": 1,164+ kernels_map[name] = {
175- "max_us": duration, "min_us": duration}165+ "total_us": duration,
166+ "calls": 1,
167+ "max_us": duration,
168+ "min_us": duration,
169+ }
176 except Exception as e:170 except Exception as e:
177 logging.warning(f"Failed to parse {csv_path}: {e}")171 logging.warning(f"Failed to parse {csv_path}: {e}")
178 172 
@@ -228,9 +222,7 @@ class TorchProfiler(FrameworkProfiler):
228 activities.append(getattr(ProfilerActivity, a))222 activities.append(getattr(ProfilerActivity, a))
229 except AttributeError:223 except AttributeError:
230 valid = [n for n in dir(ProfilerActivity) if not n.startswith("_")]224 valid = [n for n in dir(ProfilerActivity) if not n.startswith("_")]
231- raise ValueError(225+ raise ValueError(f"unknown ProfilerActivity '{a}'; valid: {valid}") from None
232- f"unknown ProfilerActivity '{a}'; valid: {valid}"
233- ) from None
234 self._prof = profile(activities=activities, record_shapes=True)226 self._prof = profile(activities=activities, record_shapes=True)
235 # Non-CPU activity names (e.g. ["MLU"]); empty for CPU-only profiles.227 # Non-CPU activity names (e.g. ["MLU"]); empty for CPU-only profiles.
236 self._device_acts = [a for a in cfg["activities"] if a != "CPU"]228 self._device_acts = [a for a in cfg["activities"] if a != "CPU"]
@@ -286,12 +278,14 @@ class TorchProfiler(FrameworkProfiler):
286 for evt in events:278 for evt in events:
287 device_us = self._device_time(evt)279 device_us = self._device_time(evt)
288 if device_us > 0:280 if device_us > 0:
289- device_kernels.append(KernelInfo(281+ device_kernels.append(
290- name=evt.key,282+ KernelInfo(
291- device_us=device_us,283+ name=evt.key,
292- calls=evt.count,284+ device_us=device_us,
293- avg_us=device_us / max(evt.count, 1),285+ calls=evt.count,
294- ))286+ avg_us=device_us / max(evt.count, 1),
287+ )
288+ )
295 total_device_us += device_us289 total_device_us += device_us
296 return ProfileResult(290 return ProfileResult(
297 elapsed_us=total_device_us / max(repeat_count, 1),291 elapsed_us=total_device_us / max(repeat_count, 1),
@@ -334,22 +328,145 @@ class WallClockProfiler(FrameworkProfiler):
334 )328 )
335 329 
336 330 
337-def get_profiler(api_name: str, backend) -> FrameworkProfiler:331+_KERNEL_TASK_TYPES = frozenset({"KERNEL_AIVEC", "KERNEL_AICORE"})
332+ 
333+ 
334+class TfNpuProfiler(FrameworkProfiler):
335+ """Profiler for TF NPU using torch_npu.profiler.
336+ 
337+ torch_npu.profiler wraps the CANN profiling subsystem and works for TF ops
338+ dispatched to NPU via npu_device, but only graph mode (tf.function) produces
339+ per-kernel task_time rows. Eager mode falls back to wall-clock timing.
340+ 
341+ Unlike NpuProfiler (which parses kernel_details.csv from the torch dispatch
342+ layer), TF ops do not go through torch's dispatcher so kernel_details.csv is
343+ not produced. Instead we parse task_time.csv which contains the raw CANN
344+ task-level records (kernel_name, kernel_type, task_time(us)).
345+ """
346+ 
347+ def __init__(self, backend, testcase_name="", root_path="."):
348+ self._testcase_name = testcase_name or "unknown"
349+ self._outdir = os.path.join(root_path, "msprof", "e2e", self._testcase_name)
350+ os.makedirs(self._outdir, exist_ok=True)
351+ self._prof = None
352+ self._wall = WallClockProfiler(backend)
353+ 
354+ def __enter__(self):
355+ from torch_npu.profiler import (
356+ ProfilerActivity,
357+ schedule,
358+ tensorboard_trace_handler,
359+ profile,
360+ )
361+ 
362+ self._prof = profile(
363+ activities=[ProfilerActivity.CPU, ProfilerActivity.NPU],
364+ record_shapes=True,
365+ schedule=schedule(wait=0, warmup=1, active=1, repeat=1),
366+ on_trace_ready=tensorboard_trace_handler(self._outdir),
367+ )
368+ self._prof.start()
369+ self._prof.step()
370+ self._wall.__enter__()
371+ return self
372+ 
373+ def __exit__(self, *exc):
374+ self._wall.__exit__(*exc)
375+ if self._prof:
376+ self._prof.step()
377+ self._prof.stop()
378+ 
379+ def result(self, backend, repeat_count) -> ProfileResult:
380+ task_csv = self._find_csv("task_time.csv")
381+ kernels = []
382+ total_device_us = 0.0
383+ if task_csv:
384+ kernels, total_device_us = self._parse_task_time(task_csv)
385+ 
386+ if total_device_us > 0:
387+ return ProfileResult(
388+ elapsed_us=total_device_us / max(repeat_count, 1),
389+ kernel_details=KernelDetails(
390+ kernels=kernels,
391+ total_device_us=total_device_us,
392+ total_cpu_us=0.0,
393+ ),
394+ )
395+ return self._wall.result(backend, repeat_count)
396+ 
397+ def _find_csv(self, filename):
398+ for root, _, files in os.walk(self._outdir):
399+ if filename in files:
400+ return os.path.join(root, filename)
401+ return None
402+ 
403+ @staticmethod
404+ def _parse_task_time(csv_path):
405+ """Parse task_time.csv for per-kernel device timing.
406+ 
407+ Only rows with kernel_type in _KERNEL_TASK_TYPES (KERNEL_AIVEC /
408+ KERNEL_AICORE) represent actual NPU kernel execution; other rows
409+ (MODEL_EXECUTE, NOTIFY_*, PROFILER_TRACE_EX, PLACE_HOLDER_SQE) are
410+ overhead and are excluded.
411+ """
412+ kernels_map = {}
413+ total_device_us = 0.0
414+ try:
415+ with open(csv_path, newline="") as f:
416+ reader = csv.DictReader(f)
417+ for row in reader:
418+ ktype = (row.get("kernel_type") or "").strip()
419+ if ktype not in _KERNEL_TASK_TYPES:
420+ continue
421+ name = (row.get("kernel_name") or "").strip()
422+ try:
423+ dur = float(row.get("task_time(us)", 0))
424+ except (ValueError, TypeError):
425+ continue
426+ if name and dur > 0:
427+ total_device_us += dur
428+ if name in kernels_map:
429+ kernels_map[name]["total_us"] += dur
430+ kernels_map[name]["calls"] += 1
431+ kernels_map[name]["max_us"] = max(kernels_map[name]["max_us"], dur)
432+ kernels_map[name]["min_us"] = min(kernels_map[name]["min_us"], dur)
433+ else:
434+ kernels_map[name] = {"total_us": dur, "calls": 1, "max_us": dur, "min_us": dur}
435+ except Exception as e:
436+ logging.warning(f"Failed to parse {csv_path}: {e}")
437+ 
438+ kernels = [
439+ KernelInfo(
440+ name=name,
441+ device_us=info["total_us"],
442+ calls=info["calls"],
443+ avg_us=info["total_us"] / info["calls"],
444+ max_us=info["max_us"],
445+ min_us=info["min_us"],
446+ )
447+ for name, info in kernels_map.items()
448+ ]
449+ return kernels, total_device_us
450+ 
451+ 
452+def get_profiler(api_name: str, backend, testcase_name: str = "", root_path: str = ".") -> FrameworkProfiler:
338 """Select profiler based on api_name prefix and backend.453 """Select profiler based on api_name prefix and backend.
339 454 
340 Hardware-neutral: routes on is_npu() + profile['profiler'] rather455 Hardware-neutral: routes on is_npu() + profile['profiler'] rather
341 than device_name() string compares.456 than device_name() string compares.
342 """457 """
458+ if api_name.startswith(("tf.", "tensorflow.")):
459+ if backend.is_npu():
460+ return TfNpuProfiler(backend, testcase_name, root_path)
461+ return WallClockProfiler(backend)
343 if api_name.startswith("torch_npu."):462 if api_name.startswith("torch_npu."):
344 if not backend.is_npu():463 if not backend.is_npu():
345- raise RuntimeError(464+ raise RuntimeError(f"API '{api_name}' requires NPU backend, but current is '{backend.device_type()}'")
346- f"API '{api_name}' requires NPU backend, "465+ return NpuProfiler(backend, testcase_name, root_path)
347- f"but current is '{backend.alias()}'"
348- )
349- return NpuProfiler(backend)
350 if api_name.startswith("torch."):466 if api_name.startswith("torch."):
351 # NPU with builtin profiler -> NpuProfiler; otherwise TorchProfiler.467 # NPU with builtin profiler -> NpuProfiler; otherwise TorchProfiler.
352 if backend.is_npu() and backend.profile.get("profiler") == "builtin":468 if backend.is_npu() and backend.profile.get("profiler") == "builtin":
353- return NpuProfiler(backend)469+ return NpuProfiler(backend, testcase_name, root_path)
354 return TorchProfiler(backend)470 return TorchProfiler(backend)
355- return WallClockProfiler(backend)471+ 
472+ return WallClockProfiler(backend)
@@ -41,11 +41,9 @@ from ttk.utilities.container_utils import apply_as_list, get_global_storage
41from .api_resolver import resolve_api41from .api_resolver import resolve_api
42from .backends import get_backend42from .backends import get_backend
43from .eager_execution import call_api43from .eager_execution import call_api
44-from .golden_generation import generate_golden
45-from .graph_execution import _execute_graph
46from .input_generation import generate_inputs44from .input_generation import generate_inputs
47from .profiler import get_profiler45from .profiler import get_profiler
48-from .profiling_utils import clone_preserving_stride, prepare_device_args, result_to_numpy46+from .profiling_utils import prepare_device_args
49from .result import FrameworkApiReturnStructure47from .result import FrameworkApiReturnStructure
50 48 
51WARMUP_COUNT = 549WARMUP_COUNT = 5
@@ -69,7 +67,7 @@ def _profiling_print(testcase, backend, dev_id, switches):
69 f"\n{separator}\n"67 f"\n{separator}\n"
70 f"API Name: {testcase.api_name}\n"68 f"API Name: {testcase.api_name}\n"
71 f"Golden API: {testcase.golden_api}\n"69 f"Golden API: {testcase.golden_api}\n"
72- f"Backend: {backend.alias()}\n"70+ f"Backend: {backend.device_type()}\n"
73 f"////////////// Tensors //////////////\n"71 f"////////////// Tensors //////////////\n"
74 f"Input View Shapes: {testcase.tensor_view_shapes}\n"72 f"Input View Shapes: {testcase.tensor_view_shapes}\n"
75 f"Input Dtypes: {testcase.tensor_dtypes}\n"73 f"Input Dtypes: {testcase.tensor_dtypes}\n"
@@ -250,7 +248,9 @@ def profile_process(testcase, device_grant_events, device_granted_indices, dev_i
250 248 
251 if switches.single_testcase_log_mode:249 if switches.single_testcase_log_mode:
252 _log_dir = build_single_log_dir(switches.test_mode, testcase.api_name, switches.root_path)250 _log_dir = build_single_log_dir(switches.test_mode, testcase.api_name, switches.root_path)
253- default_logging_config(file_handler=switches.logging_to_file, testcase_name=testcase.testcase_name, log_dir=_log_dir)251+ default_logging_config(
252+ file_handler=switches.logging_to_file, testcase_name=testcase.testcase_name, log_dir=_log_dir
253+ )
254 254 
255 return_struct = FrameworkApiReturnStructure()255 return_struct = FrameworkApiReturnStructure()
256 256 
@@ -282,7 +282,8 @@ def _get_or_create_backend(switches):
282 cached = process_ctx.storage.get("framework_api_backend")282 cached = process_ctx.storage.get("framework_api_backend")
283 if cached is not None:283 if cached is not None:
284 return cached284 return cached
285- backend = get_backend(switches.force_cpu)285+ framework = getattr(switches, "framework", "torch")
286+ backend = get_backend(switches.force_cpu, framework=framework)
286 process_ctx.storage["framework_api_backend"] = backend287 process_ctx.storage["framework_api_backend"] = backend
287 return backend288 return backend
288 289 
@@ -343,12 +344,8 @@ def _ensure_deterministic_level_e2e(process_ctx, backend, testcase):
343 return344 return
344 if backend.is_npu():345 if backend.is_npu():
345 try:346 try:
346- import torch_npu347+ backend.set_deterministic_level(det_level)
347- 348+ logging.info(f"NPU deterministic level set (e2e batch consistency for {testcase.testcase_name})")
348- torch_npu.npu.set_deterministic_level(det_level)
349- logging.info(
350- f"NPU deterministic level set to {det_level} (e2e batch consistency for {testcase.testcase_name})"
351- )
352 except Exception as e:349 except Exception as e:
353 logging.warning(f"Failed to set deterministic level: {e}")350 logging.warning(f"Failed to set deterministic level: {e}")
354 process_ctx.storage["_deterministic_level_set"] = True351 process_ctx.storage["_deterministic_level_set"] = True
@@ -358,61 +355,69 @@ def _execute_eager(testcase, backend, dev_id, switches, plan, resolved, is_tenso
358 """Build device tensors, run API in eager mode with profiling, return (result_nps, perf) or raises."""355 """Build device tensors, run API in eager mode with profiling, return (result_nps, perf) or raises."""
359 if backend.is_npu():356 if backend.is_npu():
360 import torch_npu357 import torch_npu
358+ 
361 torch_npu.npu.set_device(dev_id)359 torch_npu.npu.set_device(dev_id)
360+ resolved = backend.wrap_eager_callable(resolved)
362 args, kwargs = prepare_device_args(testcase, backend, dev_id, plan, raw_inputs)361 args, kwargs = prepare_device_args(testcase, backend, dev_id, plan, raw_inputs)
363 362 
364 run_count = switches.run_time363 run_count = switches.run_time
365- profiler = get_profiler(testcase.api_name, backend)364+ profiler = get_profiler(
365+ testcase.api_name, backend, testcase_name=testcase.testcase_name, root_path=switches.root_path
366+ )
366 367 
367 inplace_input_indexes = getattr(testcase, "inplace_input_indexes", None) or ()368 inplace_input_indexes = getattr(testcase, "inplace_input_indexes", None) or ()
368 inplace_input_backups = {}369 inplace_input_backups = {}
369 if inplace_input_indexes:370 if inplace_input_indexes:
370 for idx in inplace_input_indexes:371 for idx in inplace_input_indexes:
371 if idx < len(args) and args[idx] is not None:372 if idx < len(args) and args[idx] is not None:
372- inplace_input_backups[idx] = clone_preserving_stride(args[idx])373+ inplace_input_backups[idx] = backend.clone(args[idx])
373 374 
374 if is_inplace:375 if is_inplace:
375- inplace_backup = clone_preserving_stride(args[0]) if args and args[0] is not None else None376+ inplace_backup = backend.clone(args[0]) if args and args[0] is not None else None
376- if is_tensor_method:377+ with backend.device_scope(dev_id):
377- if args[0] is not None:378+ if is_tensor_method:
378- result = call_api(testcase.api_name, plan.overload_index, getattr(args[0], resolved), args[1:], kwargs)379+ if args[0] is not None:
380+ result = call_api(
381+ testcase.api_name, plan.overload_index, getattr(args[0], resolved), args[1:], kwargs
382+ )
383+ else:
384+ result = None
379 else:385 else:
380- result = None386+ result = call_api(testcase.api_name, plan.overload_index, resolved, args, kwargs)
381- else:
382- result = call_api(testcase.api_name, plan.overload_index, resolved, args, kwargs)
383 backend.synchronize(dev_id)387 backend.synchronize(dev_id)
384- result_nps = result_to_numpy(result, backend, copy=True)388+ result_nps = backend.result_to_numpy(result, copy=True)
385 if inplace_backup is not None:389 if inplace_backup is not None:
386- args[0][:] = inplace_backup390+ backend.restore_inplace(args[0], inplace_backup)
387 else:391 else:
388 result = None392 result = None
389 393 
390 if switches.warmup:394 if switches.warmup:
391 for _ in range(WARMUP_COUNT):395 for _ in range(WARMUP_COUNT):
392 if is_inplace and inplace_backup is not None:396 if is_inplace and inplace_backup is not None:
393- args[0][:] = inplace_backup397+ backend.restore_inplace(args[0], inplace_backup)
394 for idx, backup in inplace_input_backups.items():398 for idx, backup in inplace_input_backups.items():
395- args[idx][:] = backup399+ backend.restore_inplace(args[idx], backup)
396- if is_tensor_method:400+ with backend.device_scope(dev_id):
397- getattr(args[0], resolved)(*args[1:], **kwargs) if args[0] is not None else None401+ if is_tensor_method:
398- else:402+ getattr(args[0], resolved)(*args[1:], **kwargs) if args[0] is not None else None
399- resolved(*args, **kwargs)403+ else:
404+ resolved(*args, **kwargs)
400 backend.synchronize(dev_id)405 backend.synchronize(dev_id)
401 406 
402 for idx, backup in inplace_input_backups.items():407 for idx, backup in inplace_input_backups.items():
403- args[idx][:] = backup408+ backend.restore_inplace(args[idx], backup)
404 if is_inplace and inplace_backup is not None:409 if is_inplace and inplace_backup is not None:
405- args[0][:] = inplace_backup410+ backend.restore_inplace(args[0], inplace_backup)
406 411 
407 inplace_clones = {}412 inplace_clones = {}
408 original_tensors = {}413 original_tensors = {}
409 for idx in inplace_input_backups:414 for idx in inplace_input_backups:
410 original_tensors[idx] = args[idx]415 original_tensors[idx] = args[idx]
411- inplace_clones[idx] = [clone_preserving_stride(args[idx]) for _ in range(run_count - 1)]416+ inplace_clones[idx] = [backend.clone(args[idx]) for _ in range(run_count - 1)]
412 if is_inplace and inplace_backup is not None and 0 not in original_tensors:417 if is_inplace and inplace_backup is not None and 0 not in original_tensors:
413 if args and args[0] is not None:418 if args and args[0] is not None:
414 original_tensors[0] = args[0]419 original_tensors[0] = args[0]
415- inplace_clones[0] = [clone_preserving_stride(args[0]) for _ in range(run_count - 1)]420+ inplace_clones[0] = [backend.clone(args[0]) for _ in range(run_count - 1)]
416 421 
417 with profiler:422 with profiler:
418 for i in range(run_count):423 for i in range(run_count):
@@ -422,10 +427,11 @@ def _execute_eager(testcase, backend, dev_id, switches, plan, resolved, is_tenso
422 else:427 else:
423 for idx in original_tensors:428 for idx in original_tensors:
424 args[idx] = original_tensors[idx]429 args[idx] = original_tensors[idx]
425- if is_tensor_method:430+ with backend.device_scope(dev_id):
426- r = getattr(args[0], resolved)(*args[1:], **kwargs) if args[0] is not None else None431+ if is_tensor_method:
427- else:432+ r = getattr(args[0], resolved)(*args[1:], **kwargs) if args[0] is not None else None
428- r = resolved(*args, **kwargs)433+ else:
434+ r = resolved(*args, **kwargs)
429 if not is_inplace:435 if not is_inplace:
430 result = r436 result = r
431 backend.synchronize(dev_id)437 backend.synchronize(dev_id)
@@ -433,14 +439,14 @@ def _execute_eager(testcase, backend, dev_id, switches, plan, resolved, is_tenso
433 perf = profiler.result(backend, run_count)439 perf = profiler.result(backend, run_count)
434 440 
435 if not is_inplace:441 if not is_inplace:
436- result_nps = result_to_numpy(result, backend)442+ result_nps = backend.result_to_numpy(result)
437 443 
438 if inplace_input_indexes:444 if inplace_input_indexes:
439 if result_nps is None:445 if result_nps is None:
440 result_nps = []446 result_nps = []
441 for idx in sorted(inplace_input_indexes):447 for idx in sorted(inplace_input_indexes):
442 if idx < len(args) and args[idx] is not None:448 if idx < len(args) and args[idx] is not None:
443- inplace_np = backend.to_numpy(args[idx].detach().clone())449+ inplace_np = backend.to_numpy(args[idx], safe=True)
444 result_nps.append(inplace_np)450 result_nps.append(inplace_np)
445 451 
446 del args, kwargs452 del args, kwargs
@@ -455,7 +461,9 @@ def _generate_golden_data(testcase, raw_inputs, switches, backend, dump=True):
455 golden_nps = ["SUPPRESSED"]461 golden_nps = ["SUPPRESSED"]
456 else:462 else:
457 try:463 try:
458- golden_nps = generate_golden(testcase, raw_inputs, switches.plugin_path, switches, backend.alias())464+ from .golden_generation import generate_golden
465+ 
466+ golden_nps = generate_golden(testcase, raw_inputs, switches.plugin_path, switches, backend.device_type())
459 except Exception:467 except Exception:
460 logging.exception(f"[{testcase.testcase_name}] Golden generation failure")468 logging.exception(f"[{testcase.testcase_name}] Golden generation failure")
461 golden_nps = ["GOLDEN_FAILURE"]469 golden_nps = ["GOLDEN_FAILURE"]
@@ -577,8 +585,7 @@ def _collect_sim_report(testcase, switches):
577 585 
578 src = Path(switches.root_path) / "instr.bin"586 src = Path(switches.root_path) / "instr.bin"
579 if not src.is_file() or src.stat().st_size == 0:587 if not src.is_file() or src.stat().st_size == 0:
580- logging.warning("[%s] no instr.bin in worker cwd; skip sim report",588+ logging.warning("[%s] no instr.bin in worker cwd; skip sim report", testcase.testcase_name)
581- testcase.testcase_name)
582 return589 return
583 case_path = case_dir(switches, testcase.testcase_name)590 case_path = case_dir(switches, testcase.testcase_name)
584 case_path.mkdir(parents=True, exist_ok=True)591 case_path.mkdir(parents=True, exist_ok=True)
@@ -588,8 +595,7 @@ def _collect_sim_report(testcase, switches):
588 # Without this warning the move failure is silent: instr.bin stays in the595 # Without this warning the move failure is silent: instr.bin stays in the
589 # worker cwd, gets overwritten by the next case, and the user is left596 # worker cwd, gets overwritten by the next case, and the user is left
590 # without a sim report and without any hint.597 # without a sim report and without any hint.
591- logging.warning("[%s] failed to move instr.bin into %s: %s",598+ logging.warning("[%s] failed to move instr.bin into %s: %s", testcase.testcase_name, case_path, e)
592- testcase.testcase_name, case_path, e)
593 return599 return
594 if getattr(switches, "sim_report", False):600 if getattr(switches, "sim_report", False):
595 from ttk.core_modules.simulator.report import maybe_generate_sim_report601 from ttk.core_modules.simulator.report import maybe_generate_sim_report
@@ -681,9 +687,12 @@ def _do_profile(testcase, backend, device_grant_events, device_granted_indices,
681 graph_enabled = (687 graph_enabled = (
682 switches.cst_switches.enabled or switches.dyn_switches.enabled or getattr(switches, "aclgraph_enabled", False)688 switches.cst_switches.enabled or switches.dyn_switches.enabled or getattr(switches, "aclgraph_enabled", False)
683 )689 )
690+ if graph_enabled and not backend.supports_graph_mode():
691+ logging.warning(f"Graph mode not supported by backend {backend.device_type()}, skipping graph execution")
692+ graph_enabled = False
684 693 
685 process_ctx.notify_status("OnAcquireLock")694 process_ctx.notify_status("OnAcquireLock")
686- use_device = backend.use_device()695+ use_device = backend.has_device()
687 result_nps = None696 result_nps = None
688 perf = None697 perf = None
689 graph_cst_nps = None698 graph_cst_nps = None
@@ -708,9 +717,19 @@ def _do_profile(testcase, backend, device_grant_events, device_granted_indices,
708 testcase, backend, dev_id, switches, plan, resolved, is_tensor_method, is_inplace, raw_inputs717 testcase, backend, dev_id, switches, plan, resolved, is_tensor_method, is_inplace, raw_inputs
709 )718 )
710 if graph_enabled:719 if graph_enabled:
720+ from .framework_detector import detect_framework
721+ 
722+ if detect_framework(testcase.api_name) == "tf":
723+ from .tf_graph_execution import _execute_tf_graph
724+ 
725+ graph_fn = _execute_tf_graph
726+ else:
727+ from .graph_execution import _execute_graph
728+ 
729+ graph_fn = _execute_graph
711 if getattr(switches, "aclgraph_enabled", False):730 if getattr(switches, "aclgraph_enabled", False):
712 process_ctx.notify_status("OnGraphAclgraph")731 process_ctx.notify_status("OnGraphAclgraph")
713- graph_aclgraph_nps, graph_aclgraph_perf = _execute_graph(732+ graph_aclgraph_nps, graph_aclgraph_perf = graph_fn(
714 testcase,733 testcase,
715 backend,734 backend,
716 dev_id,735 dev_id,
@@ -725,7 +744,7 @@ def _do_profile(testcase, backend, device_grant_events, device_granted_indices,
725 )744 )
726 if switches.cst_switches.enabled:745 if switches.cst_switches.enabled:
727 process_ctx.notify_status("OnGraphCst")746 process_ctx.notify_status("OnGraphCst")
728- graph_cst_nps, graph_cst_perf = _execute_graph(747+ graph_cst_nps, graph_cst_perf = graph_fn(
729 testcase,748 testcase,
730 backend,749 backend,
731 dev_id,750 dev_id,
@@ -739,7 +758,7 @@ def _do_profile(testcase, backend, device_grant_events, device_granted_indices,
739 )758 )
740 if switches.dyn_switches.enabled:759 if switches.dyn_switches.enabled:
741 process_ctx.notify_status("OnGraphDyn")760 process_ctx.notify_status("OnGraphDyn")
742- graph_dyn_nps, graph_dyn_perf = _execute_graph(761+ graph_dyn_nps, graph_dyn_perf = graph_fn(
743 testcase,762 testcase,
744 backend,763 backend,
745 dev_id,764 dev_id,
@@ -1,17 +1,18 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2# -*- coding: UTF-8 -*-2# -*- coding: UTF-8 -*-
3# Copyright (c) 2026 Huawei Technologies Co., Ltd.3# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4-# This program is free software, you can redistribute it and/or modify it under the terms and conditions of4+# This program is free software; you can redistribute it and/or modify it under
5-# CANN Open Software License Agreement Version 2.0 (the "License").5+# the terms of conditions of CANN Open Software License Agreement Version 2.0
6-# Please refer to the License for details. You may not use this file except in compliance with the License.6+# (the "License").
7-# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8-# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9# See LICENSE in the root of the software repository for the full text of the License.7# See LICENSE in the root of the software repository for the full text of the License.
10 8 
11 9 
12"""10"""
13Profiling utility functions shared between profiling.py and graph_execution.py.11Profiling utility functions shared between profiling.py and graph_execution.py.
12+ 
13+Framework-neutral — all framework-specific logic is delegated to backend methods.
14"""14"""
15+ 
15import numpy as np16import numpy as np
16 17 
17 18 
@@ -32,99 +33,36 @@ def apply_format_cast(tensors, formats):
32 return result33 return result
33 34 
34 35 
35-def clone_preserving_stride(t):
36- """Clone a tensor preserving its non-contiguous stride.
37- 
38- Unlike torch.Tensor.clone(), which materializes a non-contiguous view into
39- a contiguous tensor, this allocates storage via empty_strided with the
40- original shape+stride and copies data in, so downstream operators still
41- receive the original (possibly non-contiguous) memory layout.
42- """
43- import torch
44- if t is None:
45- return None
46- if t.is_contiguous():
47- return t.clone()
48- new_t = torch.empty_strided(t.shape, t.stride(), dtype=t.dtype, device=t.device)
49- new_t.copy_(t)
50- return new_t
51- 
52- 
53-def _to_device_preserving_stride(tensor, backend, dev_id):
54- """Move a (possibly non-contiguous) CPU tensor to device preserving stride.
55- 
56- Tensor.to(device) / .npu() flatten non-contiguous views into contiguous
57- tensors. To preserve the original stride, this allocates storage on the
58- device via empty_strided with the original shape+stride and copies data in.
59- """
60- import torch
61- if tensor is None:
62- return None
63- if tensor.is_contiguous():
64- return getattr(tensor, backend.torch_lib)(dev_id)
65- dev_t = torch.empty_strided(
66- tensor.shape, tensor.stride(), dtype=tensor.dtype,
67- device=f"{backend.torch_lib}:{dev_id}")
68- dev_t.copy_(tensor)
69- return dev_t
70- 
71- 
72-def result_to_numpy(result, backend, copy=False):
73- """Convert API result to numpy array.
74- 
75- Handles: Tensor, tuple/list of results, and scalar returns (bool/int/float/dtype).
76- Scalar results are wrapped in a 0-d numpy array.
77- Returns list of numpy arrays or None.
78- """
79- import torch
80- if result is None:
81- return None
82- if isinstance(result, (tuple, list)):
83- nps = []
84- for r in result:
85- if r is None:
86- nps.append(None)
87- elif isinstance(r, torch.Tensor):
88- arr = backend.to_numpy(r)
89- nps.append(arr.copy() if copy else arr)
90- else:
91- nps.append(np.array(r))
92- return nps
93- if isinstance(result, torch.Tensor):
94- arr = backend.to_numpy(result)
95- return [arr.copy() if copy else arr]
96- return [np.array(result)]
97- 
98- 
99def prepare_device_args(testcase, backend, dev_id, plan, raw_inputs):36def prepare_device_args(testcase, backend, dev_id, plan, raw_inputs):
100 """Prepare device tensors and build args/kwargs for API execution.37 """Prepare device tensors and build args/kwargs for API execution.
101- 38+ 
102 Shared logic between eager and graph execution modes:39 Shared logic between eager and graph execution modes:
103- 1. Convert raw_inputs to device tensors40+ 1. Convert raw_inputs to device tensors (preserving stride for torch)
104 2. Apply NPU format cast if needed41 2. Apply NPU format cast if needed
105 3. Apply tensor list distribution if needed42 3. Apply tensor list distribution if needed
106 4. Build args/kwargs using plan43 4. Build args/kwargs using plan
107- 44+ 
108 Args:45 Args:
109 testcase: TestcaseE2e46 testcase: TestcaseE2e
110 backend: Backend instance47 backend: Backend instance
111 dev_id: device ID48 dev_id: device ID
112 plan: ParamPlan49 plan: ParamPlan
113 raw_inputs: numpy input arrays50 raw_inputs: numpy input arrays
114- 51+ 
115 Returns:52 Returns:
116 tuple: (args, kwargs) ready for API call53 tuple: (args, kwargs) ready for API call
117 """54 """
118 from ttk.utilities.container_utils import apply_as_list55 from ttk.utilities.container_utils import apply_as_list
119 56 
120- use_torch_tensors = getattr(testcase, "tensors", None) is not None and testcase.is_torch_dtype_support()57+ use_framework_tensors = getattr(testcase, "tensors", None) is not None
121- if use_torch_tensors:58+ if use_framework_tensors:
122 flat_tensors = testcase.flatten_tensors59 flat_tensors = testcase.flatten_tensors
123- dev_tensors = [_to_device_preserving_stride(t, backend, dev_id) if t is not None else None60+ dev_tensors = [
124- for t in flat_tensors]61+ backend.to_device(t, dev_id, preserve_stride=True) if t is not None else None for t in flat_tensors
62+ ]
125 else:63 else:
126 dev_tensors = [backend.to_device(x, dev_id) if x is not None else None for x in raw_inputs]64 dev_tensors = [backend.to_device(x, dev_id) if x is not None else None for x in raw_inputs]
127- if testcase.tensor_formats and backend.is_npu():65+ if testcase.tensor_formats and backend.supports_format_cast():
128 dev_tensors = apply_format_cast(dev_tensors, testcase.flat_tensor_formats)66 dev_tensors = apply_format_cast(dev_tensors, testcase.flat_tensor_formats)
129 dist = testcase.tensor_list_dist67 dist = testcase.tensor_list_dist
130 if dist:68 if dist:
@@ -0,0 +1,119 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+ 
11+"""TF graph mode execution via tf.function.
12+ 
13+Corresponds to torch's graph_execution.py (torch.compile + torchair).
14+tf.function is TF's native graph compilation — npu_device handles NPU
15+dispatch internally, no separate compiler backend needed.
16+"""
17+ 
18+import logging
19+ 
20+from .profiling_utils import prepare_device_args
21+from .tf_graph_network import TfGraphWrapper
22+ 
23+WARMUP_COUNT = 5
24+ 
25+ 
26+def _build_input_signature(testcase, dynamic):
27+ """Build tf.TensorSpec list from testcase tensor_view_shapes/dtypes.
28+ 
29+ static (dynamic=False): fixed shapes → corresponds to -c/--const
30+ dynamic (dynamic=True): None dimensions → corresponds to -d/--dynamic
31+ """
32+ import tensorflow as tf
33+ from ttk.utilities.dtypes import str_to_tf_dtype
34+ 
35+ sig = []
36+ flat_shapes = testcase.flat_tensor_view_shapes
37+ flat_dtypes = testcase.flat_tensor_dtypes
38+ for shape, dtype_str in zip(flat_shapes, flat_dtypes):
39+ if shape is None:
R
RRuiWang_17 天前

_build_input_signature 遇到 shape is None 就跳过,导致 input_signature 的长度可能小于实际 tensor 数。后面 TfGraphWrappern_sig = len(input_signature) 截断参数,被跳过的 tensor 不会绑进 graph。如果 None shape 对应的是真实输入就会漏掉,确认下 None shape 的语义。

likedislike
40+ continue
41+ dims = list(shape) if not dynamic else [None] * len(shape)
42+ tf_dtype = str_to_tf_dtype(dtype_str)
43+ if tf_dtype is None:
44+ logging.warning(f"Cannot map dtype {dtype_str} to tf.dtype, skipping input_signature")
45+ return None
46+ sig.append(tf.TensorSpec(dims, tf_dtype))
47+ return sig if sig else None
48+ 
49+ 
50+def _execute_tf_graph(
51+ testcase,
52+ backend,
53+ dev_id,
54+ switches,
55+ plan,
56+ resolved,
57+ is_tensor_method,
58+ is_inplace,
59+ raw_inputs,
60+ dynamic,
61+ is_aclgraph=False,
62+):
63+ """Execute API in TF graph mode via tf.function with profiling.
64+ 
65+ Args:
66+ testcase: TestcaseE2e
67+ backend: Backend instance (NpuTfBackend or CpuTfBackend)
68+ dev_id: device ID
69+ switches: SWITCHES
70+ plan: ParamPlan
71+ resolved: resolved API callable
72+ is_tensor_method: unused placeholder (always False for TF; kept for
73+ signature parity with torch's _execute_graph so the caller can
74+ use a single graph_fn variable for both frameworks)
75+ is_inplace: unused placeholder (always False for TF; same reason)
76+ raw_inputs: numpy input arrays
77+ dynamic: True for dynamic shape graph, False for static shape graph
78+ 
79+ Returns:
80+ (list of numpy arrays, ProfileResult) on success, or ([], None) on failure
81+ """
82+ if is_aclgraph:
83+ logging.warning("aclgraph mode not supported for TF, skipping")
84+ return [], None
85+ 
86+ mode_str = "dynamic" if dynamic else "static"
87+ logging.info(f"Executing TF graph mode: {mode_str}")
88+ 
89+ try:
90+ args, kwargs = prepare_device_args(testcase, backend, dev_id, plan, raw_inputs)
91+ 
92+ input_signature = _build_input_signature(testcase, dynamic)
93+ wrapper = TfGraphWrapper(resolved, input_signature=input_signature, dynamic=dynamic, api_name=testcase.api_name)
94+ 
95+ if switches.warmup:
96+ for _ in range(WARMUP_COUNT):
97+ wrapper(*args, **kwargs)
98+ backend.synchronize(dev_id)
99+ 
100+ from .profiler import get_profiler
101+ 
102+ profiler = get_profiler(
103+ testcase.api_name, backend, testcase_name=testcase.testcase_name, root_path=switches.root_path
104+ )
105+ run_count = switches.run_time
106+ result = None
107+ with profiler:
108+ for _ in range(run_count):
109+ result = wrapper(*args, **kwargs)
110+ backend.synchronize(dev_id)
111+ 
112+ perf = profiler.result(backend, run_count)
113+ result_nps = backend.result_to_numpy(result)
114+ except Exception as e:
115+ logging.error(f"TF graph {mode_str} execution failed: {e}", exc_info=True)
116+ return [], None
117+ 
118+ del args, kwargs
119+ return result_nps, perf
@@ -0,0 +1,99 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+ 
11+"""TfGraphWrapper — tf.function wrapper for TF graph mode testing.
12+ 
13+Corresponds to torch's GraphNetwork (torch.nn.Module) + torch.compile.
14+tf.function is TF's native graph compilation mechanism — no separate
15+compiler backend needed (npu_device handles NPU dispatch internally).
16+"""
17+ 
18+ 
19+class TfGraphWrapper:
20+ """Wrap a TF API callable in tf.function for graph-mode execution.
21+ 
22+ For static shape (-c): input_signature with fixed TensorSpec shapes.
23+ For dynamic shape (-d): input_signature with None dimensions.
24+ 
25+ tf.raw_ops.* ops require keyword args; the wrapper binds positional
26+ inputs to the API's tensor parameter names via inspect.signature,
27+ so tf.function tracing passes them as kwargs.
28+ """
29+ 
30+ def __init__(self, api_func, input_signature=None, dynamic=False, api_name=None):
31+ import tensorflow as tf
32+ 
33+ self._api_func = api_func
34+ self._dynamic = dynamic
35+ self._api_name = api_name
36+ self._input_signature = input_signature
37+ self._param_names = self._extract_tensor_param_names(api_func, api_name)
38+ 
39+ if self._param_names and input_signature is not None:
40+ self._tf_func = self._build_kw_function(api_func, self._param_names, input_signature)
41+ elif input_signature is not None:
42+ self._tf_func = tf.function(api_func, input_signature=input_signature, autograph=False)
43+ self._tf_func.get_concrete_function()
44+ else:
45+ self._tf_func = tf.function(api_func, autograph=False)
46+ 
47+ @staticmethod
48+ def _build_kw_function(api_func, param_names, input_signature):
49+ """Build tf.function with explicit named params matching input_signature.
50+ 
51+ tf.raw_ops.* require keyword args; we generate a wrapper with explicit
52+ parameter names (matching param_names) so input_signature binds correctly,
53+ and the wrapper forwards them as kwargs to the API. Non-tensor params
54+ are omitted so the API uses its own defaults.
55+ """
56+ import tensorflow as tf
57+ 
58+ n_sig = len(input_signature)
59+ tensor_names = param_names[:n_sig]
R
RRuiWang_17 天前

_build_kw_functiontensor_names = param_names[:n_sig],前提是 API 的前 n_sig 个参数恰好都是 tensor。但 param_names 包含所有位置参数(含标量),如果有标量参数排在 tensor 前面,切片就会把标量名当 tensor 名、漏掉真正的 tensor。对 tf.raw_ops 多数成立,但 tf.nn/tf.math 里有反例,建议按 _is_tensor_param 过滤而不是按位置切。

likedislike
60+ 
61+ def wrapper(*args):
62+ kwargs = {name: val for name, val in zip(tensor_names, args)}
63+ return api_func(**kwargs)
64+ 
65+ tf_func = tf.function(wrapper, input_signature=input_signature, autograph=False)
66+ tf_func.get_concrete_function()
67+ return tf_func
68+ 
69+ @staticmethod
70+ def _extract_tensor_param_names(api_func, api_name):
71+ """Extract tensor parameter names from the API signature."""
72+ import inspect
73+ 
74+ try:
75+ sig = inspect.signature(api_func)
76+ names = []
77+ for name, p in sig.parameters.items():
78+ if name == "name":
79+ continue
80+ if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
81+ names.append(name)
82+ elif p.kind == inspect.Parameter.VAR_POSITIONAL:
83+ break
84+ return names if names else None
85+ except (ValueError, TypeError):
86+ return None
87+ 
88+ def __call__(self, *args, **kwargs):
89+ if self._param_names:
90+ call_kwargs = {}
91+ for i, name in enumerate(self._param_names):
92+ if i < len(args) and args[i] is not None:
93+ call_kwargs[name] = args[i]
94+ elif name in kwargs and kwargs[name] is not None:
95+ call_kwargs[name] = kwargs[name]
96+ n_sig = len(self._input_signature) if self._input_signature else len(call_kwargs)
97+ call_kwargs = {k: v for i, (k, v) in enumerate(call_kwargs.items()) if i < n_sig}
98+ return self._tf_func(*call_kwargs.values())
99+ return self._tf_func(*args, **kwargs)
@@ -206,8 +206,7 @@ def _kernel_param_order(op_info) -> list:
206 """def.cpp param order (inputs then attrs) for server-side pool merge."""206 """def.cpp param order (inputs then attrs) for server-side pool merge."""
207 if not op_info:207 if not op_info:
208 return []208 return []
209- return ([inp["name"] for inp in op_info["inputs"]] +209+ return [inp["name"] for inp in op_info["inputs"]] + [attr["name"] for attr in op_info["attr"]]
210- [attr["name"] for attr in op_info["attr"]])
211 210 
212 211 
213def profile_process(212def profile_process(
@@ -225,7 +224,9 @@ def profile_process(
225 process_ctx.change_name(context.testcase_name)224 process_ctx.change_name(context.testcase_name)
226 if switches.single_testcase_log_mode:225 if switches.single_testcase_log_mode:
227 _log_dir = build_single_log_dir(switches.test_mode, context.op_name, switches.root_path)226 _log_dir = build_single_log_dir(switches.test_mode, context.op_name, switches.root_path)
228- default_logging_config(file_handler=switches.logging_to_file, testcase_name=context.testcase_name, log_dir=_log_dir)227+ default_logging_config(
228+ file_handler=switches.logging_to_file, testcase_name=context.testcase_name, log_dir=_log_dir
229+ )
229 manual_mode = getattr(switches, "manual_data_mode", None)230 manual_mode = getattr(switches, "manual_data_mode", None)
230 manual_case = None231 manual_case = None
231 try:232 try:
@@ -378,7 +379,7 @@ def profile_process(
378 # Following actions need to acquire global lock379 # Following actions need to acquire global lock
379 process_ctx.notify_status("OnAcquireLock")380 process_ctx.notify_status("OnAcquireLock")
380 device_id = [dev_id]381 device_id = [dev_id]
381- use_device = switches.mode.use_device()382+ use_device = switches.mode.has_device()
382 with DeviceLock(383 with DeviceLock(
383 process_ctx,384 process_ctx,
384 dev_id,385 dev_id,
@@ -393,8 +394,7 @@ def profile_process(
393 if get_global_storage().backend == "npusim":394 if get_global_storage().backend == "npusim":
394 from ttk.core_modules.simulator import run_kernel_sim395 from ttk.core_modules.simulator import run_kernel_sim
395 396 
396- (context.dyn_prof_result, context.cst_prof_result,397+ (context.dyn_prof_result, context.cst_prof_result, context.bin_prof_result) = run_kernel_sim(context)
397- context.bin_prof_result) = run_kernel_sim(context)
398 else:398 else:
399 context.dyn_prof_result = do_profiling(context, "dynamic")399 context.dyn_prof_result = do_profiling(context, "dynamic")
400 process_ctx.notify_status("OnCstProfiling")400 process_ctx.notify_status("OnCstProfiling")
@@ -96,7 +96,7 @@ class GoldenGenerator:
96 "tensor_dtypes": self._ctx.tensor_dtypes,96 "tensor_dtypes": self._ctx.tensor_dtypes,
97 "tensor_formats": self._ctx.tensor_formats,97 "tensor_formats": self._ctx.tensor_formats,
98 "scalar_dtypes": self._ctx.scalar_dtypes,98 "scalar_dtypes": self._ctx.scalar_dtypes,
99- "use_torch": self._ctx.is_torch_dtype_support(),99+ "use_numpy": not self._ctx.is_torch_dtype_support(),
100 }100 }
101 if hasattr(self._ctx, "batch_axis") and self._ctx.batch_axis is not None:101 if hasattr(self._ctx, "batch_axis") and self._ctx.batch_axis is not None:
102 kwargs["batch_axis"] = self._ctx.batch_axis102 kwargs["batch_axis"] = self._ctx.batch_axis
@@ -119,8 +119,7 @@ class GoldenGenerator:
119 tensors = self._package_golden_tensors() # input tensors only (pure outputs skipped)119 tensors = self._package_golden_tensors() # input tensors only (pure outputs skipped)
120 if op_api_info is not None:120 if op_api_info is not None:
121 tensor_names = op_api_info.tensors121 tensor_names = op_api_info.tensors
122- pure_output_names = {tensor_names[i] for i in self._ctx.pure_output_indexes122+ pure_output_names = {tensor_names[i] for i in self._ctx.pure_output_indexes if i < len(tensor_names)}
123- if i < len(tensor_names)}
124 tensor_queue = list(tensors)123 tensor_queue = list(tensors)
125 scalar_queue = list(self._ctx.flatten_scalars or ())124 scalar_queue = list(self._ctx.flatten_scalars or ())
126 for name, info in op_api_info.params.items():125 for name, info in op_api_info.params.items():
@@ -11,7 +11,6 @@
11Input generation method for Universal testcases11Input generation method for Universal testcases
12"""12"""
13 13 
14- 
15__all__ = ["InputGenerator"]14__all__ = ["InputGenerator"]
16 15 
17 16 
@@ -28,6 +27,7 @@ from ....utilities import apply_as_list, resolve_custom_numpy_dtypes, numpy_to_t
28from ....utilities import get, get_global_storage, RandomData27from ....utilities import get, get_global_storage, RandomData
29from ...plugin_loader import get_plugin_function28from ...plugin_loader import get_plugin_function
30 29 
30+ 
31class InputGenerator:31class InputGenerator:
32 def __init__(self, context: TestcaseAclnn):32 def __init__(self, context: TestcaseAclnn):
33 self._ctx = context33 self._ctx = context
@@ -57,53 +57,47 @@ class InputGenerator:
57 self._package_scalars_numpy()57 self._package_scalars_numpy()
58 58 
59 # Check special input operators59 # Check special input operators
60- input_func = get_plugin_function(self._ctx.api_name,60+ input_func = get_plugin_function(self._ctx.api_name, "input", "aclnn", self._switch.plugin_path)
61- "input", "aclnn", self._switch.plugin_path)
62 if input_func:61 if input_func:
63 self._call_custom_input(input_func)62 self._call_custom_input(input_func)
64 63 
65 def _restore(self, stored_inputs, stored_scalars):64 def _restore(self, stored_inputs, stored_scalars):
66 """Restore final generated state without rerunning random/input plugins."""65 """Restore final generated state without rerunning random/input plugins."""
67 self._ctx.np_storages = list(stored_inputs)66 self._ctx.np_storages = list(stored_inputs)
68- use_torch = self._ctx.is_torch_dtype_support()67+ use_numpy = not self._ctx.is_torch_dtype_support()
69- if use_torch:68+ if use_numpy:
70- self._convert_np_to_torch_tensor()
71- else:
72 self._convert_np_to_numpy_view()69 self._convert_np_to_numpy_view()
70+ else:
71+ self._convert_np_to_torch_tensor()
73 72 
74 scalar_values = []73 scalar_values = []
75 flat_scalar_dtypes = self._ctx.flat_scalar_dtypes or ()74 flat_scalar_dtypes = self._ctx.flat_scalar_dtypes or ()
76 for index, value in enumerate(stored_scalars):75 for index, value in enumerate(stored_scalars):
77- if value is None or not use_torch:76+ if value is None or use_numpy:
78 scalar_values.append(value)77 scalar_values.append(value)
79 continue78 continue
80 dtype = get(flat_scalar_dtypes, index)79 dtype = get(flat_scalar_dtypes, index)
81- scalar_values.append(80+ scalar_values.append(numpy_to_torch_tensor(value, is_complex32="complex32" in str(dtype)).squeeze())
82- numpy_to_torch_tensor(value, is_complex32="complex32" in str(dtype)).squeeze()81+ self._ctx.scalars = tuple(apply_as_list(scalar_values, self._ctx.scalar_list_dist))
83- )
84- self._ctx.scalars = tuple(apply_as_list(
85- scalar_values, self._ctx.scalar_list_dist
86- ))
87 82 
88 def _call_custom_input(self, input_func):83 def _call_custom_input(self, input_func):
89 plan = self._ctx.get_param_plan()84 plan = self._ctx.get_param_plan()
90- args, extra_attrs = plan.build_args(self._ctx.tensors, self._ctx.scalars,85+ args, extra_attrs = plan.build_args(self._ctx.tensors, self._ctx.scalars, self._ctx.attributes)
91- self._ctx.attributes)
92 kwargs = {86 kwargs = {
93- 'short_soc_version': self._switch.short_soc_version,87+ "short_soc_version": self._switch.short_soc_version,
94- 'testcase_name': self._ctx.testcase_name,88+ "testcase_name": self._ctx.testcase_name,
95- 'tensor_dtypes': self._ctx.tensor_dtypes,89+ "tensor_dtypes": self._ctx.tensor_dtypes,
96- 'tensor_formats': self._ctx.tensor_formats,90+ "tensor_formats": self._ctx.tensor_formats,
97- 'scalar_dtypes': self._ctx.scalar_dtypes,91+ "scalar_dtypes": self._ctx.scalar_dtypes,
98- 'input_ranges': self._ctx.input_data_ranges,92+ "input_ranges": self._ctx.input_data_ranges,
99- 'use_torch': self._ctx.is_torch_dtype_support(),93+ "use_numpy": not self._ctx.is_torch_dtype_support(),
100 }94 }
101- if hasattr(self._ctx, 'batch_axis') and self._ctx.batch_axis is not None:95+ if hasattr(self._ctx, "batch_axis") and self._ctx.batch_axis is not None:
102- kwargs['batch_axis'] = self._ctx.batch_axis96+ kwargs["batch_axis"] = self._ctx.batch_axis
103- if hasattr(self._ctx, 'batch_slice_info') and self._ctx.batch_slice_info is not None:97+ if hasattr(self._ctx, "batch_slice_info") and self._ctx.batch_slice_info is not None:
104- kwargs['batch_slice_info'] = self._ctx.batch_slice_info98+ kwargs["batch_slice_info"] = self._ctx.batch_slice_info
105- if hasattr(self._ctx, 'batch_seed') and self._ctx.batch_seed is not None:99+ if hasattr(self._ctx, "batch_seed") and self._ctx.batch_seed is not None:
106- kwargs['batch_seed'] = self._ctx.batch_seed100+ kwargs["batch_seed"] = self._ctx.batch_seed
107 kwargs.update(extra_attrs)101 kwargs.update(extra_attrs)
108 input_func(*args, **kwargs)102 input_func(*args, **kwargs)
109 103 
@@ -118,8 +112,8 @@ class InputGenerator:
118 flat_shapes = self._ctx.flat_tensor_view_shapes112 flat_shapes = self._ctx.flat_tensor_view_shapes
119 113 
120 ranges = self._ctx.flat_input_data_ranges or ()114 ranges = self._ctx.flat_input_data_ranges or ()
121- base_seed = getattr(self._switch, 'random_seed', None)115+ base_seed = getattr(self._switch, "random_seed", None)
122- batch_seed = getattr(self._ctx, 'batch_seed', None)116+ batch_seed = getattr(self._ctx, "batch_seed", None)
123 for idx, vs in enumerate(flat_shapes):117 for idx, vs in enumerate(flat_shapes):
124 if vs is None:118 if vs is None:
125 arrays.append(None)119 arrays.append(None)
@@ -132,13 +126,14 @@ class InputGenerator:
132 # pure input & inplace output126 # pure input & inplace output
133 if base_seed and batch_seed is not None:127 if base_seed and batch_seed is not None:
134 # batch consistency compare different case support same shape tensor has same value128 # batch consistency compare different case support same shape tensor has same value
135- numpy.random.seed(base_seed + idx) 129+ numpy.random.seed(base_seed + idx)
136 rd = RandomData(dtype, ss, data_range)130 rd = RandomData(dtype, ss, data_range)
137 arrays.append(rd.generate(self._switch.input_distribution))131 arrays.append(rd.generate(self._switch.input_distribution))
138 actual_data_ranges.append(tuple(rd.data_range))132 actual_data_ranges.append(tuple(rd.data_range))
139 else:133 else:
140 # pure output. initial it as dtype(1)134 # pure output. initial it as dtype(1)
141 from ttk.utilities.data import fixed_np_array135 from ttk.utilities.data import fixed_np_array
136+ 
142 init_val = 0 if self._ctx.api_name in ("aclnnInplaceOne",) else 1137 init_val = 0 if self._ctx.api_name in ("aclnnInplaceOne",) else 1
143 arrays.append(fixed_np_array(dtype, ss, init_value=init_val))138 arrays.append(fixed_np_array(dtype, ss, init_value=init_val))
144 actual_data_ranges.append(data_range)139 actual_data_ranges.append(data_range)
@@ -160,11 +155,11 @@ class InputGenerator:
160 np_arr = rd.generate(self._switch.input_distribution)155 np_arr = rd.generate(self._switch.input_distribution)
161 t_scalar = numpy_to_torch_tensor(np_arr, is_complex32="complex32" in str(dtype))156 t_scalar = numpy_to_torch_tensor(np_arr, is_complex32="complex32" in str(dtype))
162 scalars.append(t_scalar.squeeze())157 scalars.append(t_scalar.squeeze())
163- self._ctx.scalars = apply_as_list(scalars,158+ self._ctx.scalars = apply_as_list(scalars, self._ctx.scalar_list_dist)
164- self._ctx.scalar_list_dist)
165 159 
166 def _convert_np_to_torch_tensor(self):160 def _convert_np_to_torch_tensor(self):
167 import torch161 import torch
162+ 
168 torch_tensors = []163 torch_tensors = []
169 flat_shapes = self._ctx.flat_tensor_view_shapes164 flat_shapes = self._ctx.flat_tensor_view_shapes
170 for idx, np_arr in enumerate(self._ctx.np_storages):165 for idx, np_arr in enumerate(self._ctx.np_storages):
@@ -182,12 +177,13 @@ class InputGenerator:
182 else:177 else:
183 t_view = torch.as_strided(t_storage, v_shape, v_stride, v_offset)178 t_view = torch.as_strided(t_storage, v_shape, v_stride, v_offset)
184 except RuntimeError:179 except RuntimeError:
185- logging.error(f"torch.as_strided failed. storage_shape={t_storage.shape} "180+ logging.error(
186- f"view_shape={v_shape}, view_stride={v_shape}, view_offset={v_offset}")181+ f"torch.as_strided failed. storage_shape={t_storage.shape} "
182+ f"view_shape={v_shape}, view_stride={v_shape}, view_offset={v_offset}"
183+ )
187 raise184 raise
188 torch_tensors.append(t_view)185 torch_tensors.append(t_view)
189- self._ctx.tensors = apply_as_list(torch_tensors,186+ self._ctx.tensors = apply_as_list(torch_tensors, self._ctx.tensor_list_dist)
190- self._ctx.tensor_list_dist)
191 187 
192 def _package_scalars(self):188 def _package_scalars(self):
193 """189 """
@@ -198,7 +194,7 @@ class InputGenerator:
198 194 
199 op_api_info: OpApiInfo = OpApiInfoKeeper().info_of(self._ctx.api_name)195 op_api_info: OpApiInfo = OpApiInfoKeeper().info_of(self._ctx.api_name)
200 scalars = self._ctx.scalars196 scalars = self._ctx.scalars
201- scalars = scalars[:len(op_api_info.scalars)]197+ scalars = scalars[: len(op_api_info.scalars)]
202 for idx, s_name in enumerate(op_api_info.scalars):198 for idx, s_name in enumerate(op_api_info.scalars):
203 if idx >= len(scalars):199 if idx >= len(scalars):
204 raise RuntimeError(f"Some Scalar/ScalarList is not configured: {op_api_info.scalars[idx:]}")200 raise RuntimeError(f"Some Scalar/ScalarList is not configured: {op_api_info.scalars[idx:]}")
@@ -206,12 +202,13 @@ class InputGenerator:
206 val = self._ctx.attributes[s_name]202 val = self._ctx.attributes[s_name]
207 if isinstance(val, (list, tuple)):203 if isinstance(val, (list, tuple)):
208 if not isinstance(scalars[idx], list):204 if not isinstance(scalars[idx], list):
209- raise RuntimeError(f"[{s_name}] is a list/tuple configured in attributes. "205+ raise RuntimeError(
210- f"But got a scalar rather than ScalarList [{scalars[idx]}]. "206+ f"[{s_name}] is a list/tuple configured in attributes. "
211- f"Check scalar_dtypes nesting.")207+ f"But got a scalar rather than ScalarList [{scalars[idx]}]. "
208+ f"Check scalar_dtypes nesting."
209+ )
212 if len(val) != len(scalars[idx]):210 if len(val) != len(scalars[idx]):
213- raise RuntimeError(f"Value count of [{s_name}] mismatch: "211+ raise RuntimeError(f"Value count of [{s_name}] mismatch: expected [{len(scalars[idx])}].")
214- f"expected [{len(scalars[idx])}].")
215 for j in range(len(scalars[idx])):212 for j in range(len(scalars[idx])):
216 scalars[idx][j] = torch.tensor(val[j], dtype=scalars[idx][j].dtype)213 scalars[idx][j] = torch.tensor(val[j], dtype=scalars[idx][j].dtype)
217 else:214 else:
@@ -221,6 +218,7 @@ class InputGenerator:
221 def _convert_np_to_numpy_view(self):218 def _convert_np_to_numpy_view(self):
222 """非 torch 原生 dtype 时,使用 numpy as_strided 创建 view(替代 torch.as_strided)"""219 """非 torch 原生 dtype 时,使用 numpy as_strided 创建 view(替代 torch.as_strided)"""
223 from ttk.utilities.dtypes import np_as_strided_safe220 from ttk.utilities.dtypes import np_as_strided_safe
221+ 
224 np_views = []222 np_views = []
225 flat_shapes = self._ctx.flat_tensor_view_shapes223 flat_shapes = self._ctx.flat_tensor_view_shapes
226 for idx, np_arr in enumerate(self._ctx.np_storages):224 for idx, np_arr in enumerate(self._ctx.np_storages):
@@ -244,17 +242,18 @@ class InputGenerator:
244 view = np_as_strided_safe(base, shape=v_shape, strides=byte_strides)242 view = np_as_strided_safe(base, shape=v_shape, strides=byte_strides)
245 np_views.append(view)243 np_views.append(view)
246 except Exception:244 except Exception:
247- logging.error(f"numpy.as_strided failed. storage_shape={np_arr.shape} "245+ logging.error(
248- f"view_shape={v_shape}, view_stride={v_stride}, view_offset={v_offset}")246+ f"numpy.as_strided failed. storage_shape={np_arr.shape} "
247+ f"view_shape={v_shape}, view_stride={v_stride}, view_offset={v_offset}"
248+ )
249 raise249 raise
250- self._ctx.tensors = apply_as_list(np_views,250+ self._ctx.tensors = apply_as_list(np_views, self._ctx.tensor_list_dist)
251- self._ctx.tensor_list_dist)
252 251 
253 def _package_scalars_numpy(self):252 def _package_scalars_numpy(self):
254 """非 torch 原生 dtype 时,scalar 保持为 numpy scalar/ndarray"""253 """非 torch 原生 dtype 时,scalar 保持为 numpy scalar/ndarray"""
255 scalars = self._ctx.scalars254 scalars = self._ctx.scalars
256 op_api_info: OpApiInfo = OpApiInfoKeeper().info_of(self._ctx.api_name)255 op_api_info: OpApiInfo = OpApiInfoKeeper().info_of(self._ctx.api_name)
257- scalars = scalars[:len(op_api_info.scalars)]256+ scalars = scalars[: len(op_api_info.scalars)]
258 for idx, s_name in enumerate(op_api_info.scalars):257 for idx, s_name in enumerate(op_api_info.scalars):
259 if idx >= len(scalars):258 if idx >= len(scalars):
260 raise RuntimeError(f"Some Scalar/ScalarList is not configured: {op_api_info.scalars[idx:]}")259 raise RuntimeError(f"Some Scalar/ScalarList is not configured: {op_api_info.scalars[idx:]}")
@@ -262,12 +261,13 @@ class InputGenerator:
262 val = self._ctx.attributes[s_name]261 val = self._ctx.attributes[s_name]
263 if isinstance(val, (list, tuple)):262 if isinstance(val, (list, tuple)):
264 if not isinstance(scalars[idx], list):263 if not isinstance(scalars[idx], list):
265- raise RuntimeError(f"[{s_name}] is a list/tuple configured in attributes. "264+ raise RuntimeError(
266- f"But got a scalar rather than ScalarList [{scalars[idx]}]. "265+ f"[{s_name}] is a list/tuple configured in attributes. "
267- f"Check scalar_dtypes nesting.")266+ f"But got a scalar rather than ScalarList [{scalars[idx]}]. "
267+ f"Check scalar_dtypes nesting."
268+ )
268 if len(val) != len(scalars[idx]):269 if len(val) != len(scalars[idx]):
269- raise RuntimeError(f"Value count of [{s_name}] mismatch: "270+ raise RuntimeError(f"Value count of [{s_name}] mismatch: expected [{len(scalars[idx])}].")
270- f"expected [{len(scalars[idx])}].")
271 for j in range(len(scalars[idx])):271 for j in range(len(scalars[idx])):
272 scalars[idx][j] = numpy.array(val[j], dtype=scalars[idx][j].dtype)272 scalars[idx][j] = numpy.array(val[j], dtype=scalars[idx][j].dtype)
273 else:273 else:
@@ -551,11 +551,12 @@ def _aclnn_param_order(context: TestcaseAclnn) -> list:
551 op_api_info = OpApiInfoKeeper().info_of(context.api_name)551 op_api_info = OpApiInfoKeeper().info_of(context.api_name)
552 if op_api_info is None:552 if op_api_info is None:
553 return []553 return []
554- pure_output_names = {op_api_info.tensors[i]554+ pure_output_names = {op_api_info.tensors[i] for i in context.pure_output_indexes if i < len(op_api_info.tensors)}
555- for i in context.pure_output_indexes555+ return [
556- if i < len(op_api_info.tensors)}556+ name
557- return [name for name in op_api_info.params557+ for name in op_api_info.params
558- if name not in pure_output_names and name not in ("workspaceSize", "executor")]558+ if name not in pure_output_names and name not in ("workspaceSize", "executor")
559+ ]
559 560 
560 561 
561def __dump_to_file(data, file_name: str, dtype: Optional[str] = None):562def __dump_to_file(data, file_name: str, dtype: Optional[str] = None):
@@ -611,7 +612,9 @@ def profile_process(context: TestcaseAclnn, device_grant_events: dict, device_gr
611 process_ctx.change_name(context.testcase_name)612 process_ctx.change_name(context.testcase_name)
612 if switches.single_testcase_log_mode:613 if switches.single_testcase_log_mode:
613 _log_dir = build_single_log_dir(switches.test_mode, context.api_name, switches.root_path)614 _log_dir = build_single_log_dir(switches.test_mode, context.api_name, switches.root_path)
614- default_logging_config(file_handler=switches.logging_to_file, testcase_name=context.testcase_name, log_dir=_log_dir)615+ default_logging_config(
616+ file_handler=switches.logging_to_file, testcase_name=context.testcase_name, log_dir=_log_dir
617+ )
615 process_ctx.notify_status("OnParseParameters")618 process_ctx.notify_status("OnParseParameters")
616 ####################619 ####################
617 # Check whether there is need to do further test620 # Check whether there is need to do further test
@@ -694,7 +697,7 @@ def profile_process(context: TestcaseAclnn, device_grant_events: dict, device_gr
694 697 
695 # Following actions need to acquire global lock698 # Following actions need to acquire global lock
696 process_ctx.notify_status("OnAcquireLock")699 process_ctx.notify_status("OnAcquireLock")
697- use_device = switches.mode.use_device()700+ use_device = switches.mode.has_device()
698 with DeviceLock(701 with DeviceLock(
699 process_ctx,702 process_ctx,
700 dev_id,703 dev_id,
@@ -19,6 +19,7 @@ Usage:
19 plan = ParamPlan(api_name, overload_params, oidx, output_tensor_indexes, attributes)19 plan = ParamPlan(api_name, overload_params, oidx, output_tensor_indexes, attributes)
20 args, kwargs, extra = plan.build_args(nested_tensors)20 args, kwargs, extra = plan.build_args(nested_tensors)
21"""21"""
22+ 
22import ast23import ast
23import logging24import logging
24import re25import re
@@ -27,9 +28,9 @@ from ttk.utilities.dtypes import str_to_torch_dtype
27 28 
28 29 
29def safe_eval_division(s):30def safe_eval_division(s):
30- if '/' not in s:31+ if "/" not in s:
31 return None32 return None
32- parts = s.split('/')33+ parts = s.split("/")
33 if len(parts) != 2:34 if len(parts) != 2:
34 return None35 return None
35 try:36 try:
@@ -62,30 +63,30 @@ def score_attr_type_compatibility(overload_params, attr_values):
62 """63 """
63 score = 064 score = 0
64 for p in overload_params:65 for p in overload_params:
65- if p.is_tensor_like or p.name == 'out':66+ if p.is_tensor_like or p.name == "out":
66 continue67 continue
67 if p.name not in attr_values:68 if p.name not in attr_values:
68 continue69 continue
69 raw = attr_values[p.name]70 raw = attr_values[p.name]
70 ptype = p.type71 ptype = p.type
71 72 
72- if '|' in ptype:73+ if "|" in ptype:
73 score += 174 score += 1
74 continue75 continue
75 76 
76- if ptype in ('int',):77+ if ptype in ("int",):
77 if isinstance(raw, (int, bool)):78 if isinstance(raw, (int, bool)):
78 score += 179 score += 1
79- elif ptype in ('float', 'Number', 'Scalar'):80+ elif ptype in ("float", "Number", "Scalar"):
80 if isinstance(raw, (int, float, bool)):81 if isinstance(raw, (int, float, bool)):
81 score += 182 score += 1
82- elif ptype == 'bool':83+ elif ptype == "bool":
83 if isinstance(raw, bool):84 if isinstance(raw, bool):
84 score += 185 score += 1
85- elif ptype == 'str':86+ elif ptype == "str":
86 if isinstance(raw, str):87 if isinstance(raw, str):
87 score += 188 score += 1
88- elif 'tuple' in ptype or 'list' in ptype or ptype == 'torch.Size':89+ elif "tuple" in ptype or "list" in ptype or ptype == "torch.Size":
89 if isinstance(raw, (tuple, list)):90 if isinstance(raw, (tuple, list)):
90 score += 191 score += 1
91 else:92 else:
@@ -120,13 +121,10 @@ def match_overload(api_name, input_tensor_count, attributes=None, tensor_distrib
120 121 
121 for oidx, ov_info in enumerate(info.overloads):122 for oidx, ov_info in enumerate(info.overloads):
122 overload_params = ov_info.params123 overload_params = ov_info.params
123- input_tensors = [p for p in overload_params124+ input_tensors = [p for p in overload_params if p.is_tensor_like and p.name != "out"]
124- if p.is_tensor_like and p.name != 'out']125+ has_var = any(getattr(p, "is_var_positional", False) for p in input_tensors)
125- has_var = any(getattr(p, 'is_var_positional', False) for p in input_tensors)126+ required = sum(1 for p in input_tensors if not p.is_optional and not getattr(p, "is_var_positional", False))
126- required = sum(1 for p in input_tensors127+ scalar_cover = sum(1 for p in input_tensors if p.name in attrs and p.name != "self")
127- if not p.is_optional and not getattr(p, 'is_var_positional', False))
128- scalar_cover = sum(1 for p in input_tensors
129- if p.name in attrs and p.name != 'self')
130 effective_count = input_tensor_count + scalar_cover128 effective_count = input_tensor_count + scalar_cover
131 if has_var:129 if has_var:
132 if effective_count < required:130 if effective_count < required:
@@ -138,8 +136,7 @@ def match_overload(api_name, input_tensor_count, attributes=None, tensor_distrib
138 136 
139 if tensor_distribution is not None:137 if tensor_distribution is not None:
140 type_ok = True138 type_ok = True
141- non_var_tensors = [p for p in input_tensors139+ non_var_tensors = [p for p in input_tensors if not getattr(p, "is_var_positional", False)]
142- if not getattr(p, 'is_var_positional', False)]
143 for idx in range(min(input_tensor_count, len(non_var_tensors))):140 for idx in range(min(input_tensor_count, len(non_var_tensors))):
144 if tensor_distribution[idx]:141 if tensor_distribution[idx]:
145 param = non_var_tensors[idx]142 param = non_var_tensors[idx]
@@ -154,7 +151,7 @@ def match_overload(api_name, input_tensor_count, attributes=None, tensor_distrib
154 if not type_ok:151 if not type_ok:
155 continue152 continue
156 153 
157- non_tensor_names = {p.name for p in overload_params if not p.is_tensor_like and p.name != 'out'}154+ non_tensor_names = {p.name for p in overload_params if not p.is_tensor_like and p.name != "out"}
158 key_score = len(attrs & non_tensor_names)155 key_score = len(attrs & non_tensor_names)
159 candidates.append((key_score, oidx, overload_params))156 candidates.append((key_score, oidx, overload_params))
160 157 
@@ -166,8 +163,7 @@ def match_overload(api_name, input_tensor_count, attributes=None, tensor_distrib
166 tied = [(ks, oidx, op) for ks, oidx, op in candidates if ks == top_key_score]163 tied = [(ks, oidx, op) for ks, oidx, op in candidates if ks == top_key_score]
167 164 
168 if len(tied) > 1:165 if len(tied) > 1:
169- value_scored = [(ks, score_attr_type_compatibility(op, attr_values), oidx, op)166+ value_scored = [(ks, score_attr_type_compatibility(op, attr_values), oidx, op) for ks, oidx, op in tied]
170- for ks, oidx, op in tied]
171 value_scored.sort(key=lambda x: -x[1])167 value_scored.sort(key=lambda x: -x[1])
172 return value_scored[0][3], value_scored[0][2]168 return value_scored[0][3], value_scored[0][2]
173 169 
@@ -182,35 +178,35 @@ def coerce_value(raw, target_type):
182 successful coercion. If all fail, raises ValueError with details.178 successful coercion. If all fail, raises ValueError with details.
183 Single (non-union) types are handled by the original logic unchanged.179 Single (non-union) types are handled by the original logic unchanged.
184 """180 """
185- if '|' in target_type:181+ if "|" in target_type:
186 errors = []182 errors = []
187- for member_type in target_type.split('|'):183+ for member_type in target_type.split("|"):
188 try:184 try:
189 return coerce_value(raw, member_type)185 return coerce_value(raw, member_type)
190 except (ValueError, TypeError) as e:186 except (ValueError, TypeError) as e:
191 errors.append(str(e))187 errors.append(str(e))
192 raise ValueError(188 raise ValueError(
193- f"Cannot coerce {raw!r} to union type {target_type}: "189+ f"Cannot coerce {raw!r} to union type {target_type}: none of the members succeeded: {'; '.join(errors)}"
194- f"none of the members succeeded: {'; '.join(errors)}")190+ )
195 if raw is None:191 if raw is None:
196 return None192 return None
197- if isinstance(raw, str) and raw == 'None':193+ if isinstance(raw, str) and raw == "None":
198 return None194 return None
199- if type(raw).__module__ != 'builtins':195+ if type(raw).__module__ != "builtins":
200 return raw196 return raw
201- if target_type == 'bool':197+ if target_type == "bool":
202 if isinstance(raw, bool):198 if isinstance(raw, bool):
203 return raw199 return raw
204 if isinstance(raw, str):200 if isinstance(raw, str):
205- return raw.lower() in ('true', '1')201+ return raw.lower() in ("true", "1")
206 return bool(raw)202 return bool(raw)
207- if target_type in ('int',):203+ if target_type in ("int",):
208 if isinstance(raw, int):204 if isinstance(raw, int):
209 return raw205 return raw
210 if isinstance(raw, (tuple, list)):206 if isinstance(raw, (tuple, list)):
211 return tuple(int(v) for v in raw)207 return tuple(int(v) for v in raw)
212 if isinstance(raw, str):208 if isinstance(raw, str):
213- _REDUCTION_STR_TO_INT = {'none': 0, 'mean': 1, 'sum': 2, 'elementwise_mean': 1}209+ _REDUCTION_STR_TO_INT = {"none": 0, "mean": 1, "sum": 2, "elementwise_mean": 1}
214 if raw.lower() in _REDUCTION_STR_TO_INT:210 if raw.lower() in _REDUCTION_STR_TO_INT:
215 return _REDUCTION_STR_TO_INT[raw.lower()]211 return _REDUCTION_STR_TO_INT[raw.lower()]
216 else:212 else:
@@ -221,7 +217,7 @@ def coerce_value(raw, target_type):
221 return int(raw)217 return int(raw)
222 except (ValueError, TypeError) as e:218 except (ValueError, TypeError) as e:
223 raise ValueError(f"Cannot coerce {raw!r} to int: {e}") from e219 raise ValueError(f"Cannot coerce {raw!r} to int: {e}") from e
224- if target_type in ('float', 'Number', 'Scalar'):220+ if target_type in ("float", "Number", "Scalar"):
225 if isinstance(raw, (int, float, bool)):221 if isinstance(raw, (int, float, bool)):
226 return raw222 return raw
227 if isinstance(raw, (tuple, list)):223 if isinstance(raw, (tuple, list)):
@@ -235,25 +231,29 @@ def coerce_value(raw, target_type):
235 return result231 return result
236 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a numeric value")232 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a numeric value")
237 return float(raw)233 return float(raw)
238- if target_type == 'str':234+ if target_type == "str":
239- if isinstance(raw, str) and len(raw) >= 2 and ((raw[0] == '"' and raw[-1] == '"') or (raw[0] == "'" and raw[-1] == "'")):235+ if (
236+ isinstance(raw, str)
237+ and len(raw) >= 2
238+ and ((raw[0] == '"' and raw[-1] == '"') or (raw[0] == "'" and raw[-1] == "'"))
239+ ):
240 return raw[1:-1]240 return raw[1:-1]
241 return str(raw)241 return str(raw)
242- if target_type in ('ScalarType', 'Dtype', 'torch.dtype'):242+ if target_type in ("ScalarType", "Dtype", "torch.dtype"):
243 obj = str_to_torch_dtype(raw)243 obj = str_to_torch_dtype(raw)
244 if obj is not None:244 if obj is not None:
245 return obj245 return obj
246 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a torch/torch_npu dtype.")246 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a torch/torch_npu dtype.")
247- _ARRAY_TYPE_RE = re.compile(r'^(int|float|bool)\[(\d*)\]?\??$')247+ _ARRAY_TYPE_RE = re.compile(r"^(int|float|bool)\[(\d*)\]?\??$")
248 m = _ARRAY_TYPE_RE.match(target_type)248 m = _ARRAY_TYPE_RE.match(target_type)
249 if m:249 if m:
250 elem_type = m.group(1)250 elem_type = m.group(1)
251 if isinstance(raw, (tuple, list)):251 if isinstance(raw, (tuple, list)):
252 return tuple(coerce_value(v, elem_type) for v in raw)252 return tuple(coerce_value(v, elem_type) for v in raw)
253- if isinstance(raw, bool if elem_type == 'bool' else (int if elem_type == 'int' else float)):253+ if isinstance(raw, bool if elem_type == "bool" else (int if elem_type == "int" else float)):
254 return (coerce_value(raw, elem_type),)254 return (coerce_value(raw, elem_type),)
255 if isinstance(raw, str):255 if isinstance(raw, str):
256- if not raw or raw in ('[]', '()'):256+ if not raw or raw in ("[]", "()"):
257 return ()257 return ()
258 try:258 try:
259 parsed = ast.literal_eval(raw)259 parsed = ast.literal_eval(raw)
@@ -263,7 +263,7 @@ def coerce_value(raw, target_type):
263 except (ValueError, SyntaxError) as e:263 except (ValueError, SyntaxError) as e:
264 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: {e}") from e264 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: {e}") from e
265 raise ValueError(f"Cannot coerce {raw!r} to {target_type}")265 raise ValueError(f"Cannot coerce {raw!r} to {target_type}")
266- if 'tuple' in target_type or 'list' in target_type or target_type == 'torch.Size':266+ if "tuple" in target_type or "list" in target_type or target_type == "torch.Size":
267 if isinstance(raw, (tuple, list)):267 if isinstance(raw, (tuple, list)):
268 return raw268 return raw
269 if isinstance(raw, int):269 if isinstance(raw, int):
@@ -271,7 +271,7 @@ def coerce_value(raw, target_type):
271 if isinstance(raw, float):271 if isinstance(raw, float):
272 return (raw,)272 return (raw,)
273 if isinstance(raw, str):273 if isinstance(raw, str):
274- if not raw or raw in ('[]', '()'):274+ if not raw or raw in ("[]", "()"):
275 return ()275 return ()
276 try:276 try:
277 parsed = ast.literal_eval(raw)277 parsed = ast.literal_eval(raw)
@@ -284,19 +284,21 @@ def coerce_value(raw, target_type):
284 except (ValueError, TypeError) as e:284 except (ValueError, TypeError) as e:
285 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: {e}") from e285 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: {e}") from e
286 raise ValueError(f"Cannot coerce {raw!r} to {target_type}")286 raise ValueError(f"Cannot coerce {raw!r} to {target_type}")
287- if isinstance(raw, str) and raw.startswith('torch.'):287+ if isinstance(raw, str) and raw.startswith("torch."):
288 import torch288 import torch
289- attr_name = raw.split('.', 1)[1] if '.' in raw else raw289+ 
290+ attr_name = raw.split(".", 1)[1] if "." in raw else raw
290 obj = getattr(torch, attr_name, None)291 obj = getattr(torch, attr_name, None)
291 if obj is not None:292 if obj is not None:
292 return obj293 return obj
293- if target_type in ('torch.memory_format', 'memory_format', 'Layout', 'torch.layout') and isinstance(raw, str):294+ if target_type in ("torch.memory_format", "memory_format", "Layout", "torch.layout") and isinstance(raw, str):
294 import torch295 import torch
296+ 
295 obj = getattr(torch, raw, None)297 obj = getattr(torch, raw, None)
296 if obj is not None:298 if obj is not None:
297 return obj299 return obj
298 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a torch attribute")300 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: not a torch attribute")
299- if target_type in ('Device', 'torch.device'):301+ if target_type in ("Device", "torch.device"):
300 return raw302 return raw
301 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: unsupported type")303 raise ValueError(f"Cannot coerce {raw!r} to {target_type}: unsupported type")
302 304 
@@ -315,12 +317,14 @@ class ParamPlan:
315 """317 """
316 318 
317 __slots__ = (319 __slots__ = (
318- 'api_name', 'overload_params', 'overload_index',320+ "api_name",
319- 'output_tensor_indexes', 'attributes',321+ "overload_params",
322+ "overload_index",
323+ "output_tensor_indexes",
324+ "attributes",
320 )325 )
321 326 
322- def __init__(self, api_name, overload_params, overload_index,327+ def __init__(self, api_name, overload_params, overload_index, output_tensor_indexes, attributes):
323- output_tensor_indexes, attributes):
324 self.api_name = api_name328 self.api_name = api_name
325 self.overload_params = overload_params329 self.overload_params = overload_params
326 self.overload_index = overload_index330 self.overload_index = overload_index
@@ -338,9 +342,9 @@ class ParamPlan:
338 attributes not matched by any API parameter name.342 attributes not matched by any API parameter name.
339 """343 """
340 out_indices = set(self.output_tensor_indexes or ())344 out_indices = set(self.output_tensor_indexes or ())
341- parts = self.api_name.split('.') if self.api_name else []345+ from ttk.core_modules.framework_api.framework_detector import is_inplace_tensor_method
342- is_inplace = (len(parts) >= 3 and parts[0] == 'torch' and parts[1] == 'Tensor'346+ 
343- and parts[-1].endswith('_'))347+ is_inplace = is_inplace_tensor_method(self.api_name) if self.api_name else False
344 if is_inplace:348 if is_inplace:
345 input_tensors = list(nested_tensors)349 input_tensors = list(nested_tensors)
346 else:350 else:
@@ -354,17 +358,17 @@ class ParamPlan:
354 kwargs = {}358 kwargs = {}
355 359 
356 for param in self.overload_params:360 for param in self.overload_params:
357- if param.is_tensor_like and param.name == 'out':361+ if param.is_tensor_like and param.name == "out":
358 if param.is_tensor_list:362 if param.is_tensor_list:
359 collected = [t for t in out_iter if t is not None]363 collected = [t for t in out_iter if t is not None]
360 if collected:364 if collected:
361- kwargs['out'] = collected365+ kwargs["out"] = collected
362 elif not param.is_keyword_only:366 elif not param.is_keyword_only:
363 args.append(None)367 args.append(None)
364 else:368 else:
365 val = next(out_iter, None)369 val = next(out_iter, None)
366 if val is not None:370 if val is not None:
367- kwargs['out'] = val371+ kwargs["out"] = val
368 elif not param.is_keyword_only:372 elif not param.is_keyword_only:
369 args.append(None)373 args.append(None)
370 elif param.is_keyword_only:374 elif param.is_keyword_only:
@@ -377,18 +381,19 @@ class ParamPlan:
377 val = coerce_value(param.default, param.type)381 val = coerce_value(param.default, param.type)
378 kwargs[param.name] = val382 kwargs[param.name] = val
379 elif param.is_tensor_like:383 elif param.is_tensor_like:
380- if getattr(param, 'is_var_positional', False):384+ if getattr(param, "is_var_positional", False):
381 args.extend(tensor_queue)385 args.extend(tensor_queue)
382 tensor_queue.clear()386 tensor_queue.clear()
383- elif param.name in attrs and param.name != 'self' and not tensor_queue:387+ elif param.name in attrs and param.name != "self" and not tensor_queue:
384 raw = attrs[param.name]388 raw = attrs[param.name]
385 try:389 try:
386 args.append(coerce_value(raw, param.type))390 args.append(coerce_value(raw, param.type))
387 except (ValueError, TypeError):391 except (ValueError, TypeError):
388 logging.warning(392 logging.warning(
389 f"{self.api_name}: scalar fallback for param '{param.name}' "393 f"{self.api_name}: scalar fallback for param '{param.name}' "
390- f"(declared type={param.type}, value={raw!r})")394+ f"(declared type={param.type}, value={raw!r})"
391- args.append(coerce_value(raw, 'Number'))395+ )
396+ args.append(coerce_value(raw, "Number"))
392 elif tensor_queue:397 elif tensor_queue:
393 val = tensor_queue.pop(0)398 val = tensor_queue.pop(0)
394 if param.is_tensor and isinstance(val, list) and len(val) == 1:399 if param.is_tensor and isinstance(val, list) and len(val) == 1:
@@ -399,7 +404,8 @@ class ParamPlan:
399 else:404 else:
400 raise ValueError(405 raise ValueError(
401 f"{self.api_name}: not enough tensors for param '{param.name}' "406 f"{self.api_name}: not enough tensors for param '{param.name}' "
402- f"(queue empty, {len(args)} args built so far)")407+ f"(queue empty, {len(args)} args built so far)"
408+ )
403 elif param.name in attrs:409 elif param.name in attrs:
404 val = coerce_value(attrs[param.name], param.type)410 val = coerce_value(attrs[param.name], param.type)
405 args.append(val)411 args.append(val)
@@ -413,8 +419,9 @@ class ParamPlan:
413 return args, kwargs, extra_attrs419 return args, kwargs, extra_attrs
414 420 
415 421 
416-def build_positional_args(api_name, nested_tensors, attributes,422+def build_positional_args(
417- output_tensor_indexes, tensor_distribution=None, api_info=None):423+ api_name, nested_tensors, attributes, output_tensor_indexes, tensor_distribution=None, api_info=None
424+):
418 """Build (positional_args, kwargs) based on matched API signature.425 """Build (positional_args, kwargs) based on matched API signature.
419 426 
420 Convenience wrapper — creates a one-shot ParamPlan.427 Convenience wrapper — creates a one-shot ParamPlan.
@@ -428,18 +435,17 @@ def build_positional_args(api_name, nested_tensors, attributes,
428 """435 """
429 overload_params, oidx = match_overload(436 overload_params, oidx = match_overload(
430 api_name,437 api_name,
431- input_tensor_count=sum(438+ input_tensor_count=sum(1 for i, _ in enumerate(nested_tensors) if i not in set(output_tensor_indexes or ())),
432- 1 for i, _ in enumerate(nested_tensors)
433- if i not in set(output_tensor_indexes or ())),
434 attributes=attributes,439 attributes=attributes,
435 tensor_distribution=tensor_distribution,440 tensor_distribution=tensor_distribution,
436- api_info=api_info)441+ api_info=api_info,
442+ )
437 if overload_params is None:443 if overload_params is None:
438 raise ValueError(444 raise ValueError(
439 f"Cannot match overload for {api_name} "445 f"Cannot match overload for {api_name} "
440- f"with {sum(1 for i, _ in enumerate(nested_tensors) if i not in set(output_tensor_indexes or ()))} input tensors")446+ f"with {sum(1 for i, _ in enumerate(nested_tensors) if i not in set(output_tensor_indexes or ()))} input tensors"
447+ )
441 448 
442- plan = ParamPlan(api_name, overload_params, oidx,449+ plan = ParamPlan(api_name, overload_params, oidx, output_tensor_indexes, attributes)
443- output_tensor_indexes, attributes)
444 args, kwargs, _ = plan.build_args(nested_tensors)450 args, kwargs, _ = plan.build_args(nested_tensors)
445 return args, kwargs, oidx451 return args, kwargs, oidx
@@ -9,6 +9,7 @@
9"""9"""
10Testcase structure for framework_api tests.10Testcase structure for framework_api tests.
11"""11"""
12+ 
12import logging13import logging
13 14 
14from ttk.core_modules.testcase_manager.testcase_tensor_api_base import TensorApiTestcaseBase15from ttk.core_modules.testcase_manager.testcase_tensor_api_base import TensorApiTestcaseBase
@@ -123,6 +124,7 @@ class TestcaseE2e(TensorApiTestcaseBase):
123 from ttk.core_modules.framework_api.framework_api_info_keeper import (124 from ttk.core_modules.framework_api.framework_api_info_keeper import (
124 FrameworkApiInfoKeeper,125 FrameworkApiInfoKeeper,
125 )126 )
127+ 
126 self._api_info_cache = FrameworkApiInfoKeeper().get(self.api_name)128 self._api_info_cache = FrameworkApiInfoKeeper().get(self.api_name)
127 except Exception:129 except Exception:
128 self._api_info_cache = None130 self._api_info_cache = None
@@ -146,17 +148,20 @@ class TestcaseE2e(TensorApiTestcaseBase):
146 if self._is_inplace_tensor_method(self.api_name):148 if self._is_inplace_tensor_method(self.api_name):
147 input_count = top_count149 input_count = top_count
148 else:150 else:
149- input_count = sum(1 for i in range(top_count)151+ input_count = sum(
150- if self.tensor_view_shapes[i] is not None and i not in out_indices)152+ 1 for i in range(top_count) if self.tensor_view_shapes[i] is not None and i not in out_indices
153+ )
151 154 
152 dist = self.tensor_list_dist155 dist = self.tensor_list_dist
153 tensor_distribution = [d > 0 for d in dist] if dist else None156 tensor_distribution = [d > 0 for d in dist] if dist else None
154 157 
155 params, oidx = match_overload(158 params, oidx = match_overload(
156- self.api_name, input_count,159+ self.api_name,
160+ input_count,
157 attributes=self.attributes,161 attributes=self.attributes,
158 tensor_distribution=tensor_distribution,162 tensor_distribution=tensor_distribution,
159- api_info=info)163+ api_info=info,
164+ )
160 if params is None:165 if params is None:
161 return None166 return None
162 167 
@@ -165,7 +170,8 @@ class TestcaseE2e(TensorApiTestcaseBase):
165 overload_params=params,170 overload_params=params,
166 overload_index=oidx,171 overload_index=oidx,
167 output_tensor_indexes=self.output_tensor_indexes,172 output_tensor_indexes=self.output_tensor_indexes,
168- attributes=self.attributes)173+ attributes=self.attributes,
174+ )
169 self._param_plan_cache = plan175 self._param_plan_cache = plan
170 return plan176 return plan
171 except Exception:177 except Exception:
@@ -342,8 +348,7 @@ class TestcaseE2e(TensorApiTestcaseBase):
342 self.fail_reason = "tensor_view_shapes is empty"348 self.fail_reason = "tensor_view_shapes is empty"
343 return None349 return None
344 required_min = min(350 required_min = min(
345- sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like] if not p.is_optional)351+ sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like] if not p.is_optional) for ov in info.overloads
346- for ov in info.overloads
347 )352 )
348 if required_min == 0:353 if required_min == 0:
349 return info354 return info
@@ -353,11 +358,9 @@ class TestcaseE2e(TensorApiTestcaseBase):
353 358 
354 @staticmethod359 @staticmethod
355 def _is_inplace_tensor_method(api_name):360 def _is_inplace_tensor_method(api_name):
356- if not api_name:361+ from ttk.core_modules.framework_api.framework_detector import is_inplace_tensor_method
357- return False362+ 
358- parts = api_name.split('.')363+ return is_inplace_tensor_method(api_name)
359- return (len(parts) >= 3 and parts[0] == 'torch' and parts[1] == 'Tensor'
360- and parts[-1].endswith('_'))
361 364 
362 def _auto_fill_inplace_tensor_method(self):365 def _auto_fill_inplace_tensor_method(self):
363 if not self._is_inplace_tensor_method(self.api_name):366 if not self._is_inplace_tensor_method(self.api_name):
@@ -367,24 +370,24 @@ class TestcaseE2e(TensorApiTestcaseBase):
367 370 
368 def _generate_batch_consistency_id(self):371 def _generate_batch_consistency_id(self):
369 """根据 batch_seed batch和 batch_slice_info 的切片长度生成 batch_consistency_id。372 """根据 batch_seed batch和 batch_slice_info 的切片长度生成 batch_consistency_id。
370- 373+ 
371 相同 batch_seed 且切片长度相同的用例,生成相同 id,374 相同 batch_seed 且切片长度相同的用例,生成相同 id,
372 标识这些用例的输出切片可以做 batch 一致性比较。375 标识这些用例的输出切片可以做 batch 一致性比较。
373 """376 """
374 if self.batch_seed is None:377 if self.batch_seed is None:
375 self.batch_consistency_id = None378 self.batch_consistency_id = None
376 return379 return
377- 380+ 
378 if self.batch_axis is None or self.batch_slice_info is None:381 if self.batch_axis is None or self.batch_slice_info is None:
379 self.batch_consistency_id = None382 self.batch_consistency_id = None
380 return383 return
381- 384+ 
382 slice_key = []385 slice_key = []
383- for axis_pos, slices, seed in zip(self.batch_axis ,self.batch_slice_info, self.batch_seed):386+ for axis_pos, slices, seed in zip(self.batch_axis, self.batch_slice_info, self.batch_seed):
384 if axis_pos is None or slices is None or seed is None:387 if axis_pos is None or slices is None or seed is None:
385 continue388 continue
386 slice_axes = []389 slice_axes = []
387- for axis_idx , slices_idx, seed_idx in zip(axis_pos, slices, seed):390+ for axis_idx, slices_idx, seed_idx in zip(axis_pos, slices, seed):
388 if axis_idx is None or slices_idx is None or seed_idx is None:391 if axis_idx is None or slices_idx is None or seed_idx is None:
389 slice_id = "None"392 slice_id = "None"
390 slice_axes.append(slice_id)393 slice_axes.append(slice_id)
@@ -399,15 +402,16 @@ class TestcaseE2e(TensorApiTestcaseBase):
399 if step <= 0 or start < 0 or stop < 0:402 if step <= 0 or start < 0 or stop < 0:
400 length = 0403 length = 0
401 else:404 else:
402- length = stop -start if stop > start else 0405+ length = stop - start if stop > start else 0
403 slice_id = f"{seed_value}_{axis_idx}_{start}_{stop}_{step}"406 slice_id = f"{seed_value}_{axis_idx}_{start}_{stop}_{step}"
404 if length == 0:407 if length == 0:
405- logging.warning(f"testcase: {self.testcase_name}, slice_id is: {slice_id}, slice is:{sl} this slice is Invalid")408+ logging.warning(
409+ f"testcase: {self.testcase_name}, slice_id is: {slice_id}, slice is:{sl} this slice is Invalid"
410+ )
406 slice_lens.append(slice_id)411 slice_lens.append(slice_id)
407 slice_axes.append(tuple(slice_lens))412 slice_axes.append(tuple(slice_lens))
408 slice_key.append(tuple(slice_axes))413 slice_key.append(tuple(slice_axes))
409 self.batch_consistency_id = tuple(slice_key)414 self.batch_consistency_id = tuple(slice_key)
410-
411 415 
412 def _check_tensor_configuration(self):416 def _check_tensor_configuration(self):
413 """Validate tensor parameters match API definition in count and type."""417 """Validate tensor parameters match API definition in count and type."""
@@ -438,14 +442,12 @@ class TestcaseE2e(TensorApiTestcaseBase):
438 # If any overload has a VAR_POSITIONAL tensor param, it can accept442 # If any overload has a VAR_POSITIONAL tensor param, it can accept
439 # any number of tensors — skip the count check entirely.443 # any number of tensors — skip the count check entirely.
440 has_var_pos = any(444 has_var_pos = any(
441- any(getattr(p, 'is_var_positional', False)445+ any(getattr(p, "is_var_positional", False) for p in ov.params if p.is_tensor_like and p.name != "out")
442- for p in ov.params if p.is_tensor_like and p.name != 'out')446+ for ov in info.overloads
443- for ov in info.overloads)447+ )
444 if has_var_pos:448 if has_var_pos:
445 return False449 return False
446- max_input = max(450+ max_input = max(sum(1 for p in ov.params if p.is_tensor_like and p.name != "out") for ov in info.overloads)
447- sum(1 for p in ov.params if p.is_tensor_like and p.name != 'out')
448- for ov in info.overloads)
449 if input_count > max_input:451 if input_count > max_input:
450 self.is_valid = False452 self.is_valid = False
451 self.fail_reason = "INPUT_COUNT_EXCEEDED"453 self.fail_reason = "INPUT_COUNT_EXCEEDED"
@@ -453,16 +455,17 @@ class TestcaseE2e(TensorApiTestcaseBase):
453 f"[{self.testcase_name}] API [{self.api_name}] has at most {max_input} input tensor "455 f"[{self.testcase_name}] API [{self.api_name}] has at most {max_input} input tensor "
454 f"parameters (excluding out), but testcase configured {input_count} "456 f"parameters (excluding out), but testcase configured {input_count} "
455 f"input tensor(s) (excluding {len(out_indices)} output tensor(s)). "457 f"input tensor(s) (excluding {len(out_indices)} output tensor(s)). "
456- f"(source: {info.source})")458+ f"(source: {info.source})"
459+ )
457 return True460 return True
458 return False461 return False
459 462 
460 def _is_factory_api(self, info):463 def _is_factory_api(self, info):
461 """Return True if API requires no input tensors (factory function like torch.zeros)."""464 """Return True if API requires no input tensors (factory function like torch.zeros)."""
462 required_min = min(465 required_min = min(
463- sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != 'out']466+ sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != "out"] if not p.is_optional)
464- if not p.is_optional)467+ for ov in info.overloads
465- for ov in info.overloads)468+ )
466 return required_min == 0469 return required_min == 0
467 470 
468 def _check_all_tensors_output(self, info):471 def _check_all_tensors_output(self, info):
@@ -476,16 +479,17 @@ class TestcaseE2e(TensorApiTestcaseBase):
476 if input_count > 0:479 if input_count > 0:
477 return False480 return False
478 required_min = min(481 required_min = min(
479- sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != 'out']482+ sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != "out"] if not p.is_optional)
480- if not p.is_optional)483+ for ov in info.overloads
481- for ov in info.overloads)484+ )
482 self.is_valid = False485 self.is_valid = False
483 self.fail_reason = "ALL_TENSORS_MARKED_OUTPUT"486 self.fail_reason = "ALL_TENSORS_MARKED_OUTPUT"
484 logging.error(487 logging.error(
485 f"[{self.testcase_name}] API [{self.api_name}] requires at least {required_min} input tensor "488 f"[{self.testcase_name}] API [{self.api_name}] requires at least {required_min} input tensor "
486 f"parameters (excluding out), but all {top_count} tensor(s) are "489 f"parameters (excluding out), but all {top_count} tensor(s) are "
487 f"marked as output (output_tensor_indexes={sorted(out_indices)}). "490 f"marked as output (output_tensor_indexes={sorted(out_indices)}). "
488- f"(source: {info.source})")491+ f"(source: {info.source})"
492+ )
489 return True493 return True
490 494 
491 @staticmethod495 @staticmethod
@@ -498,8 +502,9 @@ class TestcaseE2e(TensorApiTestcaseBase):
498 nested_flags.append(False)502 nested_flags.append(False)
499 has_none.append(True)503 has_none.append(True)
500 else:504 else:
501- is_nested = (isinstance(element, (tuple, list)) and len(element) > 0505+ is_nested = (
502- and isinstance(element[0], (tuple, list)))506+ isinstance(element, (tuple, list)) and len(element) > 0 and isinstance(element[0], (tuple, list))
507+ )
503 nested_flags.append(is_nested)508 nested_flags.append(is_nested)
504 has_none.append(False)509 has_none.append(False)
505 return nested_flags, has_none510 return nested_flags, has_none
@@ -513,7 +518,7 @@ class TestcaseE2e(TensorApiTestcaseBase):
513 for ov in info.overloads:518 for ov in info.overloads:
514 ov_count = 0519 ov_count = 0
515 for p in ov.layout.input_params:520 for p in ov.layout.input_params:
516- if p.name in attr_keys and p.name != 'self':521+ if p.name in attr_keys and p.name != "self":
517 ov_count += 1522 ov_count += 1
518 count = max(count, ov_count)523 count = max(count, ov_count)
519 return count524 return count
@@ -524,8 +529,7 @@ class TestcaseE2e(TensorApiTestcaseBase):
524 if self._is_inplace_tensor_method(self.api_name):529 if self._is_inplace_tensor_method(self.api_name):
525 input_shapes = list(self.tensor_view_shapes)530 input_shapes = list(self.tensor_view_shapes)
526 else:531 else:
527- input_shapes = [s for i, s in enumerate(self.tensor_view_shapes)532+ input_shapes = [s for i, s in enumerate(self.tensor_view_shapes) if i not in out_indices]
528- if i not in out_indices]
529 input_count = len(input_shapes)533 input_count = len(input_shapes)
530 nested_flags, has_none = self._classify_input_types(input_shapes)534 nested_flags, has_none = self._classify_input_types(input_shapes)
531 535 
@@ -543,9 +547,9 @@ class TestcaseE2e(TensorApiTestcaseBase):
543 return547 return
544 548 
545 required_min = min(549 required_min = min(
546- sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != 'out']550+ sum(1 for p in [pp for pp in ov.params if pp.is_tensor_like and pp.name != "out"] if not p.is_optional)
547- if not p.is_optional)551+ for ov in info.overloads
548- for ov in info.overloads)552+ )
549 required_min -= scalar_attr_count553 required_min -= scalar_attr_count
550 count_matched = info.match_overload(input_count, None, None)554 count_matched = info.match_overload(input_count, None, None)
551 if count_matched[0]:555 if count_matched[0]:
@@ -555,7 +559,8 @@ class TestcaseE2e(TensorApiTestcaseBase):
555 f"API [{self.api_name}] input tensor count matches an overload, "559 f"API [{self.api_name}] input tensor count matches an overload, "
556 f"but type (Tensor/TensorList) does not. "560 f"but type (Tensor/TensorList) does not. "
557 f"nested={nested_flags}. "561 f"nested={nested_flags}. "
558- f"(source: {info.source})")562+ f"(source: {info.source})"
563+ )
559 elif input_count > info.tensor_count:564 elif input_count > info.tensor_count:
560 self.is_valid = False565 self.is_valid = False
561 self.fail_reason = "TENSOR_COUNT_MISMATCH"566 self.fail_reason = "TENSOR_COUNT_MISMATCH"
@@ -563,14 +568,16 @@ class TestcaseE2e(TensorApiTestcaseBase):
563 f"API [{self.api_name}] has at most {info.tensor_count} tensor parameters "568 f"API [{self.api_name}] has at most {info.tensor_count} tensor parameters "
564 f"in any overload, but testcase configured {input_count} input tensors "569 f"in any overload, but testcase configured {input_count} input tensors "
565 f"(excluding {len(out_indices)} output tensors). "570 f"(excluding {len(out_indices)} output tensors). "
566- f"(source: {info.source})")571+ f"(source: {info.source})"
572+ )
567 else:573 else:
568 self.is_valid = False574 self.is_valid = False
569 self.fail_reason = "TENSOR_COUNT_MISMATCH"575 self.fail_reason = "TENSOR_COUNT_MISMATCH"
570 logging.error(576 logging.error(
571 f"API [{self.api_name}] requires at least {required_min} input tensor "577 f"API [{self.api_name}] requires at least {required_min} input tensor "
572 f"parameters (excluding out), but testcase configured {input_count}. "578 f"parameters (excluding out), but testcase configured {input_count}. "
573- f"(source: {info.source})")579+ f"(source: {info.source})"
580+ )
574 581 
575 def _check_required_attrs(self, info, oidx):582 def _check_required_attrs(self, info, oidx):
576 """Fail if matched overload has required non-tensor params missing from attributes.583 """Fail if matched overload has required non-tensor params missing from attributes.
@@ -581,10 +588,11 @@ class TestcaseE2e(TensorApiTestcaseBase):
581 ov = info.overloads[oidx]588 ov = info.overloads[oidx]
582 attrs = set(self.attributes.keys()) if self.attributes else set()589 attrs = set(self.attributes.keys()) if self.attributes else set()
583 missing = [590 missing = [
584- p.name for p in ov.params591+ p.name
592+ for p in ov.params
585 if not p.is_tensor_like593 if not p.is_tensor_like
586 and not p.is_keyword_only594 and not p.is_keyword_only
587- and p.name != 'out'595+ and p.name != "out"
588 and not p.is_optional596 and not p.is_optional
589 and p.name not in attrs597 and p.name not in attrs
590 ]598 ]
@@ -593,23 +601,26 @@ class TestcaseE2e(TensorApiTestcaseBase):
593 601 
594 out_indices = set(self.output_tensor_indexes or ())602 out_indices = set(self.output_tensor_indexes or ())
595 top_count = len(self.tensor_view_shapes or ())603 top_count = len(self.tensor_view_shapes or ())
596- input_count = (top_count604+ input_count = (
597- if self._is_inplace_tensor_method(self.api_name)605+ top_count
598- else sum(1 for i in range(top_count) if i not in out_indices))606+ if self._is_inplace_tensor_method(self.api_name)
607+ else sum(1 for i in range(top_count) if i not in out_indices)
608+ )
599 609 
600 for alt_oidx, alt_ov in enumerate(info.overloads):610 for alt_oidx, alt_ov in enumerate(info.overloads):
601 if alt_oidx == oidx:611 if alt_oidx == oidx:
602 continue612 continue
603- alt_tensors = [p for p in alt_ov.params if p.is_tensor_like and p.name != 'out']613+ alt_tensors = [p for p in alt_ov.params if p.is_tensor_like and p.name != "out"]
604 alt_req = sum(1 for p in alt_tensors if not p.is_optional)614 alt_req = sum(1 for p in alt_tensors if not p.is_optional)
605 alt_total = len(alt_tensors)615 alt_total = len(alt_tensors)
606 if not (alt_req <= input_count <= alt_total):616 if not (alt_req <= input_count <= alt_total):
607 continue617 continue
608 alt_missing = [618 alt_missing = [
609- p.name for p in alt_ov.params619+ p.name
620+ for p in alt_ov.params
610 if not p.is_tensor_like621 if not p.is_tensor_like
611 and not p.is_keyword_only622 and not p.is_keyword_only
612- and p.name != 'out'623+ and p.name != "out"
613 and not p.is_optional624 and not p.is_optional
614 and p.name not in attrs625 and p.name not in attrs
615 ]626 ]
@@ -621,7 +632,8 @@ class TestcaseE2e(TensorApiTestcaseBase):
621 logging.error(632 logging.error(
622 f"[{self.testcase_name}] API [{self.api_name}] overload[{oidx}] requires "633 f"[{self.testcase_name}] API [{self.api_name}] overload[{oidx}] requires "
623 f"non-tensor attribute(s) {missing} but attributes only have "634 f"non-tensor attribute(s) {missing} but attributes only have "
624- f"{sorted(attrs)}. (source: {info.source})")635+ f"{sorted(attrs)}. (source: {info.source})"
636+ )
625 637 
626 def _check_output_configuration(self):638 def _check_output_configuration(self):
627 """Validate output_tensor_indexes against API's out parameter definition.639 """Validate output_tensor_indexes against API's out parameter definition.
@@ -636,24 +648,22 @@ class TestcaseE2e(TensorApiTestcaseBase):
636 info = self.get_api_info()648 info = self.get_api_info()
637 if not self.output_tensor_indexes:649 if not self.output_tensor_indexes:
638 if info is not None:650 if info is not None:
639- any_out_required = any(651+ any_out_required = any(ov.layout.is_out_required for ov in info.overloads)
640- ov.layout.is_out_required for ov in info.overloads)
641 if any_out_required:652 if any_out_required:
642 self.is_valid = False653 self.is_valid = False
643 self.fail_reason = "MISSING_REQUIRED_OUTPUT"654 self.fail_reason = "MISSING_REQUIRED_OUTPUT"
644 logging.error(655 logging.error(
645 f"[{self.testcase_name}] API [{self.api_name}] has overloads "656 f"[{self.testcase_name}] API [{self.api_name}] has overloads "
646 f"with required 'out' parameter, but testcase provides no "657 f"with required 'out' parameter, but testcase provides no "
647- f"output_tensor_indexes. (source: {info.source})")658+ f"output_tensor_indexes. (source: {info.source})"
659+ )
648 return660 return
649 top_count = len(self.tensor_view_shapes)661 top_count = len(self.tensor_view_shapes)
650 out_of_range = [i for i in self.output_tensor_indexes if i < 0 or i >= top_count]662 out_of_range = [i for i in self.output_tensor_indexes if i < 0 or i >= top_count]
651 if out_of_range:663 if out_of_range:
652 self.is_valid = False664 self.is_valid = False
653 self.fail_reason = "OUTPUT_INDEX_INVALID"665 self.fail_reason = "OUTPUT_INDEX_INVALID"
654- logging.error(666+ logging.error(f"output_tensor_indexes {out_of_range} out of range [0, {top_count})")
655- f"output_tensor_indexes {out_of_range} out of range [0, {top_count})"
656- )
657 return667 return
658 if info is None:668 if info is None:
659 return669 return
@@ -683,26 +693,25 @@ class TestcaseE2e(TensorApiTestcaseBase):
683 continue693 continue
684 matching_overloads.append(oidx)694 matching_overloads.append(oidx)
685 if not matching_overloads:695 if not matching_overloads:
686- any_out_required = any(696+ any_out_required = any(ov.layout.is_out_required for ov in info.overloads)
687- ov.layout.is_out_required for ov in info.overloads)697+ any_tensor_list_out = any(ov.layout.is_out_tensor_list for ov in info.overloads)
688- any_tensor_list_out = any(
689- ov.layout.is_out_tensor_list for ov in info.overloads)
690 if any_out_required and any_tensor_list_out:698 if any_out_required and any_tensor_list_out:
691- expected = set(ov.layout.out_expected_count699+ expected = set(ov.layout.out_expected_count for ov in info.overloads if ov.layout.is_out_required)
692- for ov in info.overloads if ov.layout.is_out_required)
693 self.is_valid = False700 self.is_valid = False
694 self.fail_reason = "OUTPUT_COUNT_MISMATCH"701 self.fail_reason = "OUTPUT_COUNT_MISMATCH"
695 logging.error(702 logging.error(
696 f"[{self.testcase_name}] API [{self.api_name}] requires exactly "703 f"[{self.testcase_name}] API [{self.api_name}] requires exactly "
697 f"{expected} output tensor(s) for Tensor[] 'out', but testcase "704 f"{expected} output tensor(s) for Tensor[] 'out', but testcase "
698- f"provides {out_count}. (source: {info.source})")705+ f"provides {out_count}. (source: {info.source})"
706+ )
699 elif any_out_required:707 elif any_out_required:
700 self.is_valid = False708 self.is_valid = False
701 self.fail_reason = "OUTPUT_COUNT_MISMATCH"709 self.fail_reason = "OUTPUT_COUNT_MISMATCH"
702 logging.error(710 logging.error(
703 f"[{self.testcase_name}] API [{self.api_name}] requires exactly "711 f"[{self.testcase_name}] API [{self.api_name}] requires exactly "
704 f"1 output tensor, but testcase provides {out_count}. "712 f"1 output tensor, but testcase provides {out_count}. "
705- f"(source: {info.source})")713+ f"(source: {info.source})"
714+ )
706 715 
707 def _check_top_level_counts(self):716 def _check_top_level_counts(self):
708 """Validate that all fields have matching top-level count after normalization."""717 """Validate that all fields have matching top-level count after normalization."""
@@ -714,18 +723,21 @@ class TestcaseE2e(TensorApiTestcaseBase):
714 self.fail_reason = "DTYPES_COUNT_MISMATCH"723 self.fail_reason = "DTYPES_COUNT_MISMATCH"
715 logging.error(724 logging.error(
716 f"[{self.testcase_name}] tensor_dtypes top-level count ({len(self.tensor_dtypes)}) "725 f"[{self.testcase_name}] tensor_dtypes top-level count ({len(self.tensor_dtypes)}) "
717- f"!= tensor_view_shapes count ({top_count})")726+ f"!= tensor_view_shapes count ({top_count})"
727+ )
718 return728 return
719 if self.tensor_formats and len(self.tensor_formats) != top_count:729 if self.tensor_formats and len(self.tensor_formats) != top_count:
720 self.is_valid = False730 self.is_valid = False
721 self.fail_reason = "FORMATS_COUNT_MISMATCH"731 self.fail_reason = "FORMATS_COUNT_MISMATCH"
722 logging.error(732 logging.error(
723 f"[{self.testcase_name}] tensor_formats top-level count ({len(self.tensor_formats)}) "733 f"[{self.testcase_name}] tensor_formats top-level count ({len(self.tensor_formats)}) "
724- f"!= tensor_view_shapes count ({top_count})")734+ f"!= tensor_view_shapes count ({top_count})"
735+ )
725 return736 return
726 if self.tensor_storage_shapes and len(self.tensor_storage_shapes) != top_count:737 if self.tensor_storage_shapes and len(self.tensor_storage_shapes) != top_count:
727 self.is_valid = False738 self.is_valid = False
728 self.fail_reason = "STORAGE_SHAPES_COUNT_MISMATCH"739 self.fail_reason = "STORAGE_SHAPES_COUNT_MISMATCH"
729 logging.error(740 logging.error(
730 f"[{self.testcase_name}] tensor_storage_shapes top-level count ({len(self.tensor_storage_shapes)}) "741 f"[{self.testcase_name}] tensor_storage_shapes top-level count ({len(self.tensor_storage_shapes)}) "
731- f"!= tensor_view_shapes count ({top_count})")742+ f"!= tensor_view_shapes count ({top_count})"
743+ )
@@ -8,15 +8,12 @@
8Shared base class for aclnn (op_api) and framework_api (e2e) testcase structures.8Shared base class for aclnn (op_api) and framework_api (e2e) testcase structures.
9"""9"""
10 10 
11- 
12__all__ = ["TensorApiTestcaseBase"]11__all__ = ["TensorApiTestcaseBase"]
13 12 
14 13 
15from .testcase_base import TestcaseBase14from .testcase_base import TestcaseBase
16from ...utilities import get, shape_stride15from ...utilities import get, shape_stride
17-from ...utilities.container_utils import (16+from ...utilities.container_utils import infer_list_distribution_from_nesting, flatten_nested_sequence, deep_flatten
18- infer_list_distribution_from_nesting, flatten_nested_sequence, deep_flatten
19-)
20 17 
21 18 
22class TensorApiTestcaseBase(TestcaseBase):19class TensorApiTestcaseBase(TestcaseBase):
@@ -59,13 +56,18 @@ class TensorApiTestcaseBase(TestcaseBase):
59 "_flat_absolute_precision",56 "_flat_absolute_precision",
60 "_pure_output_indexes",57 "_pure_output_indexes",
61 "_is_torch_dtype_support",58 "_is_torch_dtype_support",
59+ "_is_tf_dtype_support",
60+ "const_input_indexes",
62 )61 )
63 62 
64 _scalar_tensor_fields = (63 _scalar_tensor_fields = (
65- 'tensor_dtypes', 'tensor_formats', 'tensor_view_offsets',64+ "tensor_dtypes",
65+ "tensor_formats",
66+ "tensor_view_offsets",
66 )67 )
67 _shape_tensor_fields = (68 _shape_tensor_fields = (
68- 'tensor_view_strides', 'tensor_storage_shapes',69+ "tensor_view_strides",
70+ "tensor_storage_shapes",
69 )71 )
70 72 
71 def __init__(self):73 def __init__(self):
@@ -97,6 +99,8 @@ class TensorApiTestcaseBase(TestcaseBase):
97 self._flat_absolute_precision = None99 self._flat_absolute_precision = None
98 self._pure_output_indexes = None100 self._pure_output_indexes = None
99 self._is_torch_dtype_support = None101 self._is_torch_dtype_support = None
102+ self._is_tf_dtype_support = None
103+ self.const_input_indexes = set()
100 104 
101 @property105 @property
102 def op_name(self):106 def op_name(self):
@@ -111,8 +115,7 @@ class TensorApiTestcaseBase(TestcaseBase):
111 Cached on first access. Returns () when no tensor_view_shapes.115 Cached on first access. Returns () when no tensor_view_shapes.
112 """116 """
113 if self._tensor_list_dist is None and self.tensor_view_shapes:117 if self._tensor_list_dist is None and self.tensor_view_shapes:
114- self._tensor_list_dist = infer_list_distribution_from_nesting(118+ self._tensor_list_dist = infer_list_distribution_from_nesting(self.tensor_view_shapes)
115- self.tensor_view_shapes)
116 return self._tensor_list_dist or ()119 return self._tensor_list_dist or ()
117 120 
118 @property121 @property
@@ -152,8 +155,7 @@ class TensorApiTestcaseBase(TestcaseBase):
152 # use _flatten_by_distribution to respect TensorList boundaries.155 # use _flatten_by_distribution to respect TensorList boundaries.
153 dist = self.tensor_list_dist156 dist = self.tensor_list_dist
154 if dist:157 if dist:
155- self._flat_tensor_dtypes = self._flatten_by_distribution(158+ self._flat_tensor_dtypes = self._flatten_by_distribution(self.tensor_dtypes, dist)
156- self.tensor_dtypes, dist)
157 else:159 else:
158 self._flat_tensor_dtypes = self.tensor_dtypes160 self._flat_tensor_dtypes = self.tensor_dtypes
159 return self._flat_tensor_dtypes161 return self._flat_tensor_dtypes
@@ -182,8 +184,7 @@ class TensorApiTestcaseBase(TestcaseBase):
182 # flat_tensor_dtypes — use _flatten_by_distribution.184 # flat_tensor_dtypes — use _flatten_by_distribution.
183 dist = self.tensor_list_dist185 dist = self.tensor_list_dist
184 if dist:186 if dist:
185- self._flat_tensor_formats = self._flatten_by_distribution(187+ self._flat_tensor_formats = self._flatten_by_distribution(self.tensor_formats, dist)
186- self.tensor_formats, dist)
187 else:188 else:
188 self._flat_tensor_formats = self.tensor_formats189 self._flat_tensor_formats = self.tensor_formats
189 return self._flat_tensor_formats190 return self._flat_tensor_formats
@@ -199,8 +200,7 @@ class TensorApiTestcaseBase(TestcaseBase):
199 # otherwise flatten_nested_sequence would split the shape tuple.200 # otherwise flatten_nested_sequence would split the shape tuple.
200 dist = self.tensor_list_dist201 dist = self.tensor_list_dist
201 if dist:202 if dist:
202- self._flat_tensor_storage_shapes = self._flatten_by_distribution(203+ self._flat_tensor_storage_shapes = self._flatten_by_distribution(self.tensor_storage_shapes, dist)
203- self.tensor_storage_shapes, dist)
204 else:204 else:
205 self._flat_tensor_storage_shapes = self.tensor_storage_shapes205 self._flat_tensor_storage_shapes = self.tensor_storage_shapes
206 return self._flat_tensor_storage_shapes206 return self._flat_tensor_storage_shapes
@@ -215,8 +215,7 @@ class TensorApiTestcaseBase(TestcaseBase):
215 # — use _flatten_by_distribution to respect TensorList boundaries.215 # — use _flatten_by_distribution to respect TensorList boundaries.
216 dist = self.tensor_list_dist216 dist = self.tensor_list_dist
217 if dist:217 if dist:
218- self._flat_tensor_view_offsets = self._flatten_by_distribution(218+ self._flat_tensor_view_offsets = self._flatten_by_distribution(self.tensor_view_offsets, dist)
219- self.tensor_view_offsets, dist)
220 else:219 else:
221 self._flat_tensor_view_offsets = self.tensor_view_offsets220 self._flat_tensor_view_offsets = self.tensor_view_offsets
222 return self._flat_tensor_view_offsets221 return self._flat_tensor_view_offsets
@@ -232,8 +231,7 @@ class TensorApiTestcaseBase(TestcaseBase):
232 # otherwise flatten_nested_sequence would split the stride tuple.231 # otherwise flatten_nested_sequence would split the stride tuple.
233 dist = self.tensor_list_dist232 dist = self.tensor_list_dist
234 if dist:233 if dist:
235- self._flat_tensor_view_strides = self._flatten_by_distribution(234+ self._flat_tensor_view_strides = self._flatten_by_distribution(self.tensor_view_strides, dist)
236- self.tensor_view_strides, dist)
237 else:235 else:
238 self._flat_tensor_view_strides = self.tensor_view_strides236 self._flat_tensor_view_strides = self.tensor_view_strides
239 return self._flat_tensor_view_strides237 return self._flat_tensor_view_strides
@@ -251,8 +249,7 @@ class TensorApiTestcaseBase(TestcaseBase):
251 return self.input_data_ranges249 return self.input_data_ranges
252 dist = self.tensor_list_dist250 dist = self.tensor_list_dist
253 if dist:251 if dist:
254- self._flat_input_data_ranges = self._flatten_by_distribution(252+ self._flat_input_data_ranges = self._flatten_by_distribution(self.input_data_ranges, dist)
255- self.input_data_ranges, dist)
256 else:253 else:
257 self._flat_input_data_ranges = self.input_data_ranges254 self._flat_input_data_ranges = self.input_data_ranges
258 return self._flat_input_data_ranges255 return self._flat_input_data_ranges
@@ -266,8 +263,7 @@ class TensorApiTestcaseBase(TestcaseBase):
266 return self.precision_tolerances263 return self.precision_tolerances
267 odist = self.output_dist264 odist = self.output_dist
268 if odist:265 if odist:
269- self._flat_precision_tolerances = self._flatten_by_distribution(266+ self._flat_precision_tolerances = self._flatten_by_distribution(self.precision_tolerances, odist)
270- self.precision_tolerances, odist)
271 else:267 else:
272 self._flat_precision_tolerances = self.precision_tolerances268 self._flat_precision_tolerances = self.precision_tolerances
273 return self._flat_precision_tolerances269 return self._flat_precision_tolerances
@@ -281,8 +277,7 @@ class TensorApiTestcaseBase(TestcaseBase):
281 return self.absolute_precision277 return self.absolute_precision
282 odist = self.output_dist278 odist = self.output_dist
283 if odist:279 if odist:
284- self._flat_absolute_precision = self._flatten_by_distribution(280+ self._flat_absolute_precision = self._flatten_by_distribution(self.absolute_precision, odist)
285- self.absolute_precision, odist)
286 else:281 else:
287 self._flat_absolute_precision = self.absolute_precision282 self._flat_absolute_precision = self.absolute_precision
288 return self._flat_absolute_precision283 return self._flat_absolute_precision
@@ -305,6 +300,7 @@ class TensorApiTestcaseBase(TestcaseBase):
305 if self._is_torch_dtype_support is not None:300 if self._is_torch_dtype_support is not None:
306 return self._is_torch_dtype_support301 return self._is_torch_dtype_support
307 from ttk.utilities.dtypes import is_torch_native_dtype302 from ttk.utilities.dtypes import is_torch_native_dtype
303+ 
308 result = True304 result = True
309 for dtype in self.flat_tensor_dtypes:305 for dtype in self.flat_tensor_dtypes:
310 if dtype is not None and not is_torch_native_dtype(dtype):306 if dtype is not None and not is_torch_native_dtype(dtype):
@@ -313,6 +309,29 @@ class TensorApiTestcaseBase(TestcaseBase):
313 self._is_torch_dtype_support = result309 self._is_torch_dtype_support = result
314 return result310 return result
315 311 
312+ def is_tf_dtype_support(self) -> bool:
R
RRuiWang_17 天前

is_tf_dtype_support 每次调用都遍历 flat_tensor_dtypes 重新计算,而旁边的 is_torch_dtype_support 是把结果缓存到 self._is_torch_dtype_support 的。is_dtype_support 在一次 e2e 流程里会被多次调用(input/golden/profiling 各一次),建议同样加个缓存字段。

likedislike
313+ """Check if all dtypes in testcase are supported by TF natively."""
314+ if self._is_tf_dtype_support is not None:
315+ return self._is_tf_dtype_support
316+ from ttk.utilities.dtypes import is_tf_native_dtype
317+ 
318+ result = True
319+ for dtype in self.flat_tensor_dtypes:
320+ if dtype is not None and not is_tf_native_dtype(dtype):
321+ result = False
322+ break
323+ self._is_tf_dtype_support = result
324+ return result
325+ 
326+ def is_dtype_support(self) -> bool:
327+ """Framework-aware dtype support check."""
328+ from ttk.core_modules.framework_api.framework_detector import detect_framework
329+ 
330+ framework = detect_framework(self.api_name)
331+ if framework == "tf":
332+ return self.is_tf_dtype_support()
333+ return self.is_torch_dtype_support()
334+ 
316 # ========== Legacy per-flat-index accessors (kept for backward compat) ==========335 # ========== Legacy per-flat-index accessors (kept for backward compat) ==========
317 336 
318 def flat_storage_shape(self, idx: int):337 def flat_storage_shape(self, idx: int):
@@ -358,7 +377,7 @@ class TensorApiTestcaseBase(TestcaseBase):
358 return self._pure_output_indexes377 return self._pure_output_indexes
359 dist = self.tensor_list_dist378 dist = self.tensor_list_dist
360 flat_output = set()379 flat_output = set()
361- for idx in (self.output_tensor_indexes or ()):380+ for idx in self.output_tensor_indexes or ():
362 flat_idx = sum(max(d, 1) for d in dist[:idx])381 flat_idx = sum(max(d, 1) for d in dist[:idx])
363 count = dist[idx] if idx < len(dist) and dist[idx] > 0 else 1382 count = dist[idx] if idx < len(dist) and dist[idx] > 0 else 1
364 flat_output.update(range(flat_idx, flat_idx + count))383 flat_output.update(range(flat_idx, flat_idx + count))
@@ -374,12 +393,12 @@ class TensorApiTestcaseBase(TestcaseBase):
374 if dist:393 if dist:
375 for field_name in self._scalar_tensor_fields:394 for field_name in self._scalar_tensor_fields:
376 self._normalize_scalar_field_by_dist(field_name, dist)395 self._normalize_scalar_field_by_dist(field_name, dist)
377- for field_name in (*self._shape_tensor_fields, 'input_data_ranges'):396+ for field_name in (*self._shape_tensor_fields, "input_data_ranges"):
378 self._normalize_range_field_by_dist(field_name, dist)397 self._normalize_range_field_by_dist(field_name, dist)
379 odist = self.output_dist398 odist = self.output_dist
380 if odist:399 if odist:
381- self._normalize_scalar_field_by_dist('absolute_precision', odist)400+ self._normalize_scalar_field_by_dist("absolute_precision", odist)
382- self._normalize_range_field_by_dist('precision_tolerances', odist)401+ self._normalize_range_field_by_dist("precision_tolerances", odist)
383 402 
384 @staticmethod403 @staticmethod
385 def _flatten_by_distribution(values, distribution):404 def _flatten_by_distribution(values, distribution):
@@ -28,7 +28,7 @@ class AbsKernelSpec:
28class AclnnAbsSpec:28class AclnnAbsSpec:
29 """ACLNN 流程 — golden / third_party 均收到 torch.Tensor(已在设备上)"""29 """ACLNN 流程 — golden / third_party 均收到 torch.Tensor(已在设备上)"""
30 30 
31- def golden(x, **kwargs):31+ def golden(x, out, **kwargs):
32 return [torch.abs(x)]32 return [torch.abs(x)]
33 33 
34 third_party = {"torch": "torch.abs"}34 third_party = {"torch": "torch.abs"}
@@ -43,7 +43,7 @@ class MODE(Enum):
43 def is_online_board(self) -> bool:43 def is_online_board(self) -> bool:
44 return True if self in [MODE.ASCEND_ONBOARD] else False44 return True if self in [MODE.ASCEND_ONBOARD] else False
45 45 
46- def use_device(self) -> bool:46+ def has_device(self) -> bool:
47 return self.is_online_board()47 return self.is_online_board()
48 48 
49 def is_esl_model(self):49 def is_esl_model(self):
@@ -177,6 +177,7 @@ class SWITCHES:
177 "sim_report",177 "sim_report",
178 "sim_cores",178 "sim_cores",
179 "sim_object_file",179 "sim_object_file",
180+ "framework",
180 ]181 ]
181 182 
182 def __init__(self):183 def __init__(self):
@@ -259,6 +260,7 @@ class SWITCHES:
259 self.sim_report: bool = False260 self.sim_report: bool = False
260 self.sim_cores: str = ""261 self.sim_cores: str = ""
261 self.sim_object_file: str = ""262 self.sim_object_file: str = ""
263+ self.framework: str = "torch"
262 264 
263 def __getstate__(self):265 def __getstate__(self):
264 """Pickle 支持:仅导出 __slots__ 中已赋值的属性(跳过 property/私有)。"""266 """Pickle 支持:仅导出 __slots__ 中已赋值的属性(跳过 property/私有)。"""
@@ -65,7 +65,7 @@ dtype_width_map = {
65 "float8_e5m2": 1,65 "float8_e5m2": 1,
66 "float8_e4m3fn": 1,66 "float8_e4m3fn": 1,
67 "hifloat8": 1,67 "hifloat8": 1,
68- None: 068+ None: 0,
69}69}
70 70 
71dtype_map = {71dtype_map = {
@@ -73,190 +73,166 @@ dtype_map = {
73 "c64": "complex64",73 "c64": "complex64",
74 "complex32": "complex32",74 "complex32": "complex32",
75 "complex64": "complex64",75 "complex64": "complex64",
76- 
77 "c128": "complex128",76 "c128": "complex128",
78 "complex128": "complex128",77 "complex128": "complex128",
79- 
80 "f64": "float64",78 "f64": "float64",
81 "fp64": "float64",79 "fp64": "float64",
82 "float64": "float64",80 "float64": "float64",
83 "double": "double",81 "double": "double",
84- 
85 "f32": "float32",82 "f32": "float32",
86 "fp32": "float32",83 "fp32": "float32",
87 "float32": "float32",84 "float32": "float32",
88 "float": "float32",85 "float": "float32",
89- 
90 "f16": "float16",86 "f16": "float16",
91 "fp16": "float16",87 "fp16": "float16",
92 "float16": "float16",88 "float16": "float16",
93- 
94 "bf16": "bfloat16",89 "bf16": "bfloat16",
95 "bfp16": "bfloat16",90 "bfp16": "bfloat16",
96 "bfloat16": "bfloat16",91 "bfloat16": "bfloat16",
97- 
98 "s64": "int64",92 "s64": "int64",
99 "int64": "int64",93 "int64": "int64",
100- 
101 "u64": "uint64",94 "u64": "uint64",
102 "uint64": "uint64",95 "uint64": "uint64",
103- 
104 "s32": "int32",96 "s32": "int32",
105 "int32": "int32",97 "int32": "int32",
106- 
107 "u32": "uint32",98 "u32": "uint32",
108 "uint32": "uint32",99 "uint32": "uint32",
109- 
110 "s16": "int16",100 "s16": "int16",
111 "int16": "int16",101 "int16": "int16",
112- 
113 "u16": "uint16",102 "u16": "uint16",
114 "uint16": "uint16",103 "uint16": "uint16",
115- 
116 "s8": "int8",104 "s8": "int8",
117 "int8": "int8",105 "int8": "int8",
118- 
119 "u8": "uint8",106 "u8": "uint8",
120 "uint8": "uint8",107 "uint8": "uint8",
121- 
122 "s4": "int4",108 "s4": "int4",
123 "int4": "int4",109 "int4": "int4",
124- 
125 "u1": "uint1",110 "u1": "uint1",
126 "uint1": "uint1",111 "uint1": "uint1",
127- 
128 "s1": "uint1",112 "s1": "uint1",
129 "int1": "uint1",113 "int1": "uint1",
130- 
131 "bool": "bool",114 "bool": "bool",
132- 
133 "f4_e2m1": "float4_e2m1",115 "f4_e2m1": "float4_e2m1",
134 "fp4_e2m1": "float4_e2m1",116 "fp4_e2m1": "float4_e2m1",
135 "float4_e2m1": "float4_e2m1",117 "float4_e2m1": "float4_e2m1",
136 "float4_e2m1fn": "float4_e2m1",118 "float4_e2m1fn": "float4_e2m1",
137 "fp4_e2m1fn": "float4_e2m1",119 "fp4_e2m1fn": "float4_e2m1",
138 "f4_e2m1fn": "float4_e2m1",120 "f4_e2m1fn": "float4_e2m1",
139- 
140 "f4_e1m2": "float4_e1m2",121 "f4_e1m2": "float4_e1m2",
141 "fp4_e1m2": "float4_e1m2",122 "fp4_e1m2": "float4_e1m2",
142 "float4_e1m2": "float4_e1m2",123 "float4_e1m2": "float4_e1m2",
143 "float4_e1m2fn": "float4_e1m2",124 "float4_e1m2fn": "float4_e1m2",
144 "fp4_e1m2fn": "float4_e1m2",125 "fp4_e1m2fn": "float4_e1m2",
145 "f4_e1m2fn": "float4_e1m2",126 "f4_e1m2fn": "float4_e1m2",
146- 
147 "f8_e8m0": "float8_e8m0",127 "f8_e8m0": "float8_e8m0",
148 "fp8_e8m0": "float8_e8m0",128 "fp8_e8m0": "float8_e8m0",
149 "float8_e8m0": "float8_e8m0",129 "float8_e8m0": "float8_e8m0",
150- 
151 "f8_e5m2": "float8_e5m2",130 "f8_e5m2": "float8_e5m2",
152 "fp8_e5m2": "float8_e5m2",131 "fp8_e5m2": "float8_e5m2",
153 "float8_e5m2": "float8_e5m2",132 "float8_e5m2": "float8_e5m2",
154- 
155 "f8_e4m3fn": "float8_e4m3fn",133 "f8_e4m3fn": "float8_e4m3fn",
156 "fp8_e4m3fn": "float8_e4m3fn",134 "fp8_e4m3fn": "float8_e4m3fn",
157 "float8_e4m3fn": "float8_e4m3fn",135 "float8_e4m3fn": "float8_e4m3fn",
158- 
159 "hif8": "hifloat8",136 "hif8": "hifloat8",
160 "hifp8": "hifloat8",137 "hifp8": "hifloat8",
161 "hifloat8": "hifloat8",138 "hifloat8": "hifloat8",
162 "hifloat4": "hifloat4",139 "hifloat4": "hifloat4",
163- 140+ None: None,
164- None: None
165}141}
166 142 
167 143 
168DATA_TYPE_DICT = {144DATA_TYPE_DICT = {
169- 'float32': 0,145+ "float32": 0,
170- 'float16': 1,146+ "float16": 1,
171- 'int8': 2,147+ "int8": 2,
172- 'int32': 3,148+ "int32": 3,
173- 'uint8': 4,149+ "uint8": 4,
174- 'int16': 6,150+ "int16": 6,
175- 'uint16': 7,151+ "uint16": 7,
176- 'uint32': 8,152+ "uint32": 8,
177- 'int64': 9,153+ "int64": 9,
178- 'uint64': 10,154+ "uint64": 10,
179- 'double': 11,155+ "double": 11,
180- 'float64': 11,156+ "float64": 11,
181- 'bool': 12,157+ "bool": 12,
182- 'complex64': 16,158+ "complex64": 16,
183- 'complex128': 17,159+ "complex128": 17,
184- 'qint8': 18,160+ "qint8": 18,
185- 'qint16': 19,161+ "qint16": 19,
186- 'qint32': 20,162+ "qint32": 20,
187- 'quint8': 21,163+ "quint8": 21,
188- 'quint16': 22,164+ "quint16": 22,
189- 'resource': 23,165+ "resource": 23,
190- 'dual': 25,166+ "dual": 25,
191- 'variant': 26,167+ "variant": 26,
192- 'bf16': 27,168+ "bf16": 27,
193- 'bfloat16': 27,169+ "bfloat16": 27,
194- 'int4': 29,170+ "int4": 29,
195- 'uint1': 30,171+ "uint1": 30,
196- 'int2': 31,172+ "int2": 31,
197- 'uint2': 32,173+ "uint2": 32,
198- 'complex32': 33,174+ "complex32": 33,
199- 'hifloat8': 34,175+ "hifloat8": 34,
200- 'float8_e5m2': 35,176+ "float8_e5m2": 35,
201- 'float8_e4m3fn': 36,177+ "float8_e4m3fn": 36,
202- 'float8_e8m0': 37,178+ "float8_e8m0": 37,
203- 'float4_e2m1': 40,179+ "float4_e2m1": 40,
204- 'float4_e1m2': 41,180+ "float4_e1m2": 41,
205- 'hifloat4': 42,181+ "hifloat4": 42,
206}182}
207 183 
208 184 
209DATA_TYPE_INT_TO_STR = {185DATA_TYPE_INT_TO_STR = {
210- 0: 'float32',186+ 0: "float32",
211- 1: 'float16',187+ 1: "float16",
212- 2: 'int8',188+ 2: "int8",
213- 3: 'int32',189+ 3: "int32",
214- 4: 'uint8',190+ 4: "uint8",
215- 6: 'int16',191+ 6: "int16",
216- 7: 'uint16',192+ 7: "uint16",
217- 8: 'uint32',193+ 8: "uint32",
218- 9: 'int64',194+ 9: "int64",
219- 10: 'uint64',195+ 10: "uint64",
220- 11: 'double',196+ 11: "double",
221- 12: 'bool',197+ 12: "bool",
222- 16: 'complex64',198+ 16: "complex64",
223- 17: 'complex128',199+ 17: "complex128",
224- 18: 'qint8',200+ 18: "qint8",
225- 19: 'qint16',201+ 19: "qint16",
226- 20: 'qint32',202+ 20: "qint32",
227- 21: 'quint8',203+ 21: "quint8",
228- 22: 'quint16',204+ 22: "quint16",
229- 23: 'resource',205+ 23: "resource",
230- 25: 'dual',206+ 25: "dual",
231- 26: 'variant',207+ 26: "variant",
232- 27: 'bfloat16',208+ 27: "bfloat16",
233- 29: 'int4',209+ 29: "int4",
234- 30: 'uint1',210+ 30: "uint1",
235- 31: 'int2',211+ 31: "int2",
236- 32: 'uint2',212+ 32: "uint2",
237- 33: 'complex32',213+ 33: "complex32",
238- 34: 'hifloat8',214+ 34: "hifloat8",
239- 35: 'float8_e5m2',215+ 35: "float8_e5m2",
240- 36: 'float8_e4m3fn',216+ 36: "float8_e4m3fn",
241- 37: 'float8_e8m0',217+ 37: "float8_e8m0",
242- 40: 'float4_e2m1',218+ 40: "float4_e2m1",
243- 41: 'float4_e1m2',219+ 41: "float4_e1m2",
244- 42: 'hifloat4',220+ 42: "hifloat4",
245}221}
246 222 
247 223 
248def str_to_torch_dtype(dtype_str: str):224def str_to_torch_dtype(dtype_str: str):
249 """Convert dtype string to torch.dtype/torch_npu.dtype object.225 """Convert dtype string to torch.dtype/torch_npu.dtype object.
250- 226+ 
251 Args:227 Args:
252 dtype_str: string like 'float16', 'int8', 'fp16', 'bf16', etc.228 dtype_str: string like 'float16', 'int8', 'fp16', 'bf16', etc.
253- 229+ 
254 Returns:230 Returns:
255 torch.dtype/torch_npu object, or original value if not recognized.231 torch.dtype/torch_npu object, or original value if not recognized.
256 """232 """
257 if not isinstance(dtype_str, str):233 if not isinstance(dtype_str, str):
258 return dtype_str234 return dtype_str
259- splited = dtype_str.split('.')235+ splited = dtype_str.split(".")
260 if len(splited) > 2:236 if len(splited) > 2:
261 return None237 return None
262 module = splited[0]238 module = splited[0]
@@ -264,16 +240,18 @@ def str_to_torch_dtype(dtype_str: str):
264 if len(splited) == 1:240 if len(splited) == 1:
265 if is_torch_native_dtype(canonical):241 if is_torch_native_dtype(canonical):
266 import torch242 import torch
243+ 
267 return getattr(torch, canonical, None)244 return getattr(torch, canonical, None)
268 else:245 else:
269 import torch_npu246 import torch_npu
247+ 
270 npu_attr_name = "float8_e8m0fnu" if canonical == "float8_e8m0" else canonical248 npu_attr_name = "float8_e8m0fnu" if canonical == "float8_e8m0" else canonical
271 return getattr(torch_npu, npu_attr_name, None)249 return getattr(torch_npu, npu_attr_name, None)
272 else:250 else:
273- if module not in ('torch', 'torch_npu'):251+ if module not in ("torch", "torch_npu"):
274 return None252 return None
275 try:253 try:
276- if module == 'torch':254+ if module == "torch":
277 import torch255 import torch
278 else:256 else:
279 import torch_npu257 import torch_npu
@@ -343,10 +321,10 @@ def numpy_int4():
343 try:321 try:
344 # noinspection PyUnresolvedReferences322 # noinspection PyUnresolvedReferences
345 from ml_dtypes import int4323 from ml_dtypes import int4
324+ 
346 return int4325 return int4
347 except ModuleNotFoundError:326 except ModuleNotFoundError:
348- raise RuntimeError("ml_dtypes is needed to support int4 dtype!!! "327+ raise RuntimeError("ml_dtypes is needed to support int4 dtype!!! Please install with `pip3 install ml-dtypes`")
349- "Please install with `pip3 install ml-dtypes`")
350 328 
351 329 
352def numpy_bfloat16():330def numpy_bfloat16():
@@ -357,11 +335,14 @@ def numpy_bfloat16():
357 try:335 try:
358 # noinspection PyUnresolvedReferences336 # noinspection PyUnresolvedReferences
359 import tensorflow337 import tensorflow
338+ 
360 bfloat16 = tensorflow.bfloat16.as_numpy_dtype339 bfloat16 = tensorflow.bfloat16.as_numpy_dtype
361 except ModuleNotFoundError:340 except ModuleNotFoundError:
362- raise RuntimeError("ml-dtypes or tensorflow is needed to support bfloat16 dtype!!! "341+ raise RuntimeError(
363- "Please install with `pip3 install ml-dtypes` "342+ "ml-dtypes or tensorflow is needed to support bfloat16 dtype!!! "
364- "or `pip3 install tensorflow`")343+ "Please install with `pip3 install ml-dtypes` "
344+ "or `pip3 install tensorflow`"
345+ )
365 # some older TF version (v1.15.0) bfp16 needs to convert to fp32 to calculate in numpy346 # some older TF version (v1.15.0) bfp16 needs to convert to fp32 to calculate in numpy
366 global BFP16_NEEDS_FP32_FOR_NPY347 global BFP16_NEEDS_FP32_FOR_NPY
367 if BFP16_NEEDS_FP32_FOR_NPY is None:348 if BFP16_NEEDS_FP32_FOR_NPY is None:
@@ -381,26 +362,31 @@ def numpy_float8_e5m2():
381 try:362 try:
382 # noinspection PyUnresolvedReferences363 # noinspection PyUnresolvedReferences
383 from ml_dtypes import float8_e5m2364 from ml_dtypes import float8_e5m2
365+ 
384 return float8_e5m2366 return float8_e5m2
385 except ModuleNotFoundError:367 except ModuleNotFoundError:
386- raise RuntimeError("ml_dtypes is needed to support float8_e5m2 dtype!!! "368+ raise RuntimeError(
387- "Please install with `pip3 install ml-dtypes`")369+ "ml_dtypes is needed to support float8_e5m2 dtype!!! Please install with `pip3 install ml-dtypes`"
370+ )
388 371 
389 372 
390def numpy_float8_e4m3fn():373def numpy_float8_e4m3fn():
391 try:374 try:
392 # noinspection PyUnresolvedReferences375 # noinspection PyUnresolvedReferences
393 from ml_dtypes import float8_e4m3fn376 from ml_dtypes import float8_e4m3fn
377+ 
394 return float8_e4m3fn378 return float8_e4m3fn
395 except ModuleNotFoundError:379 except ModuleNotFoundError:
396- raise RuntimeError("ml_dtypes is needed to support float8_e4m3fn dtype!!! "380+ raise RuntimeError(
397- "Please install with `pip3 install ml-dtypes`")381+ "ml_dtypes is needed to support float8_e4m3fn dtype!!! Please install with `pip3 install ml-dtypes`"
382+ )
398 383 
399 384 
400def ensure_en_dtypes_version(version):385def ensure_en_dtypes_version(version):
401 import en_dtypes386 import en_dtypes
402- cur_ver = list(map(int, en_dtypes.__version__.split('.'))) + [0, 0]387+ 
403- min_ver = list(map(int, version.split('.'))) + [0, 0]388+ cur_ver = list(map(int, en_dtypes.__version__.split("."))) + [0, 0]
389+ min_ver = list(map(int, version.split("."))) + [0, 0]
404 if (cur_ver[0], cur_ver[1], cur_ver[2]) >= (min_ver[0], min_ver[1], min_ver[2]):390 if (cur_ver[0], cur_ver[1], cur_ver[2]) >= (min_ver[0], min_ver[1], min_ver[2]):
405 return391 return
406 else:392 else:
@@ -411,43 +397,52 @@ def numpy_float8_e8m0():
411 try:397 try:
412 # noinspection PyUnresolvedReferences398 # noinspection PyUnresolvedReferences
413 from en_dtypes import float8_e8m0399 from en_dtypes import float8_e8m0
414- ensure_en_dtypes_version('0.0.4')400+ 
401+ ensure_en_dtypes_version("0.0.4")
415 return float8_e8m0402 return float8_e8m0
416 except ModuleNotFoundError:403 except ModuleNotFoundError:
417- raise RuntimeError("en_dtypes is needed to support float8_e8m0 dtype!!! "404+ raise RuntimeError(
418- "Please install with `pip3 install en-dtypes`")405+ "en_dtypes is needed to support float8_e8m0 dtype!!! Please install with `pip3 install en-dtypes`"
406+ )
419 407 
420 408 
421def numpy_float4_e2m1():409def numpy_float4_e2m1():
422 try:410 try:
423 # noinspection PyUnresolvedReferences411 # noinspection PyUnresolvedReferences
424 from en_dtypes import float4_e2m1412 from en_dtypes import float4_e2m1
425- ensure_en_dtypes_version('0.0.4')413+ 
414+ ensure_en_dtypes_version("0.0.4")
426 return float4_e2m1415 return float4_e2m1
427 except ModuleNotFoundError:416 except ModuleNotFoundError:
428- raise RuntimeError("en_dtypes is needed to support float4_e2m1 dtype!!! "417+ raise RuntimeError(
429- "Please install with `pip3 install en-dtypes`")418+ "en_dtypes is needed to support float4_e2m1 dtype!!! Please install with `pip3 install en-dtypes`"
419+ )
430 420 
431 421 
432def numpy_float4_e1m2():422def numpy_float4_e1m2():
433 try:423 try:
434 # noinspection PyUnresolvedReferences424 # noinspection PyUnresolvedReferences
435 from en_dtypes import float4_e1m2425 from en_dtypes import float4_e1m2
436- ensure_en_dtypes_version('0.0.4')426+ 
427+ ensure_en_dtypes_version("0.0.4")
437 return float4_e1m2428 return float4_e1m2
438 except ModuleNotFoundError:429 except ModuleNotFoundError:
439- raise RuntimeError("en_dtypes is needed to support float4_e1m2 dtype!!! "430+ raise RuntimeError(
440- "Please install with `pip3 install en-dtypes`")431+ "en_dtypes is needed to support float4_e1m2 dtype!!! Please install with `pip3 install en-dtypes`"
432+ )
433+ 
441 434 
442def numpy_hifloat4():435def numpy_hifloat4():
443 try:436 try:
444 # noinspection PyUnresolvedReferences437 # noinspection PyUnresolvedReferences
445 from en_dtypes import float4_e1m2438 from en_dtypes import float4_e1m2
446- ensure_en_dtypes_version('0.0.4')439+ 
440+ ensure_en_dtypes_version("0.0.4")
447 return float4_e1m2441 return float4_e1m2
448 except ModuleNotFoundError:442 except ModuleNotFoundError:
449- raise RuntimeError("en_dtypes is needed to support hifloat4 dtype!!! "443+ raise RuntimeError(
450- "Please install with `pip3 install en-dtypes`")444+ "en_dtypes is needed to support hifloat4 dtype!!! Please install with `pip3 install en-dtypes`"
445+ )
451 446 
452 447 
453def IsRoundOne(sign, man, truncLen):448def IsRoundOne(sign, man, truncLen):
@@ -456,7 +451,7 @@ def IsRoundOne(sign, man, truncLen):
456 mask0 = 0451 mask0 = 0
457 else:452 else:
458 mask0 = 0x1 << truncLen453 mask0 = 0x1 << truncLen
459- if (truncLen > roundingTruncLen):454+ if truncLen > roundingTruncLen:
460 mask1 = 0455 mask1 = 0
461 else:456 else:
462 mask1 = 0x1 << (truncLen - 1)457 mask1 = 0x1 << (truncLen - 1)
@@ -464,16 +459,19 @@ def IsRoundOne(sign, man, truncLen):
464 mask2 = mask1 - 1459 mask2 = mask1 - 1
465 460 
466 # ROUND_TO_NEAREST461 # ROUND_TO_NEAREST
467- lastBit = (man & mask0) > 0 # Last bit after conversion462+ lastBit = (man & mask0) > 0 # Last bit after conversion
468- truncHighBit = (man & mask1) > 0 # Highest bit in the truncated part463+ truncHighBit = (man & mask1) > 0 # Highest bit in the truncated part
469- truncLeft = (man & mask2) > 0 # Truncated left part (except for the highest bit)464+ truncLeft = (man & mask2) > 0 # Truncated left part (except for the highest bit)
470 return truncHighBit and (truncLeft or lastBit)465 return truncHighBit and (truncLeft or lastBit)
471 466 
467+ 
472def float_to_hex(f):468def float_to_hex(f):
473- return hex(struct.unpack('<I', struct.pack('<f', f))[0])469+ return hex(struct.unpack("<I", struct.pack("<f", f))[0])
470+ 
474 471 
475def cvt_bfloat16_to_fp4_e2m1(x):472def cvt_bfloat16_to_fp4_e2m1(x):
476 import math473 import math
474+ 
477 sRet = 0475 sRet = 0
478 if x < 0.0:476 if x < 0.0:
479 sRet = 1477 sRet = 1
@@ -482,51 +480,52 @@ def cvt_bfloat16_to_fp4_e2m1(x):
482 x = eval(float_to_hex(x_abs))480 x = eval(float_to_hex(x_abs))
483 x = x >> 16481 x = x >> 16
484 482 
485- ef = (x >> 7) & 0xff483+ ef = (x >> 7) & 0xFF
486- mf = x & 0x7f484+ mf = x & 0x7F
487- mLenDelta = 7 - 1 #485+ mLenDelta = 7 - 1 #
488- maxExp = 3 # max E encoding value of e2m1 is 3486+ maxExp = 3 # max E encoding value of e2m1 is 3
489- expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1487+ expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1
490 eRet = 0488 eRet = 0
491 mRet = 0489 mRet = 0
492 eNorm = 0490 eNorm = 0
493- if (ef == 0 and mf != 0) :491+ if ef == 0 and mf != 0:
494- eNorm = ef - 127 + 1 # the exp bias of subnormal bf16 is 126492+ eNorm = ef - 127 + 1 # the exp bias of subnormal bf16 is 126
495 else:493 else:
496- eNorm = ef - 127 # the exp bias of bf16 is 127494+ eNorm = ef - 127 # the exp bias of bf16 is 127
497 495 
498 if (eNorm > (maxExp - expBias)) or ((eNorm == (maxExp - expBias)) and ((mf >> mLenDelta) == 1)):496 if (eNorm > (maxExp - expBias)) or ((eNorm == (maxExp - expBias)) and ((mf >> mLenDelta) == 1)):
499- return ((sRet << 3) | 0b111)497+ return (sRet << 3) | 0b111
500 elif eNorm <= -(expBias):498 elif eNorm <= -(expBias):
501 eRet = 0499 eRet = 0
502- mf = (mf | 0x80)500+ mf = mf | 0x80
503 mLenDelta -= eNorm + expBias - 1501 mLenDelta -= eNorm + expBias - 1
504- needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry502+ needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry
505- mRet = (mf >> mLenDelta)503+ mRet = mf >> mLenDelta
506- if (needRound) :504+ if needRound:
507- mRet+=1505+ mRet += 1
508 else:506 else:
509- eRet = (eNorm + expBias)507+ eRet = eNorm + expBias
510 needRound = IsRoundOne(sRet, mf, mLenDelta)508 needRound = IsRoundOne(sRet, mf, mLenDelta)
511- mRet = (mf >> mLenDelta)509+ mRet = mf >> mLenDelta
512- if (needRound) :510+ if needRound:
513- mRet+=1511+ mRet += 1
514 512 
515- if (((mRet & 0b10) != 0) and (needRound)) :513+ if ((mRet & 0b10) != 0) and (needRound):
516- eRet+=1514+ eRet += 1
517 mRet = 0515 mRet = 0
518 516 
519- if (eRet >= 3) :517+ if eRet >= 3:
520 eRet = 3518 eRet = 3
521- elif (eRet == 0 and mRet == 0b10) :519+ elif eRet == 0 and mRet == 0b10:
522- eRet+=1520+ eRet += 1
523 mRet = 0521 mRet = 0
524 522 
525- return (((sRet) << 3) | ((eRet) << 1) | ((mRet) & 1))523+ return ((sRet) << 3) | ((eRet) << 1) | ((mRet) & 1)
526 524 
527 525 
528def cvt_bfloat16_to_fp4_e1m2(x):526def cvt_bfloat16_to_fp4_e1m2(x):
529 import math527 import math
528+ 
530 sRet = 0529 sRet = 0
531 if x < 0.0:530 if x < 0.0:
532 sRet = 1531 sRet = 1
@@ -535,50 +534,52 @@ def cvt_bfloat16_to_fp4_e1m2(x):
535 x = eval(float_to_hex(x_abs))534 x = eval(float_to_hex(x_abs))
536 x = x >> 16535 x = x >> 16
537 536 
538- ef = x >> 7 & 0xff537+ ef = x >> 7 & 0xFF
539- mf = x & 0x7f538+ mf = x & 0x7F
540- mLenDelta = 7 - 2 #539+ mLenDelta = 7 - 2 #
541- maxExp = 1 # max E encoding value of e1m2 is 3540+ maxExp = 1 # max E encoding value of e1m2 is 3
542- expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1541+ expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1
543 542 
544 eRet = 0543 eRet = 0
545 mRet = 0544 mRet = 0
546 eNorm = 0545 eNorm = 0
547- if (ef == 0 and mf != 0) :546+ if ef == 0 and mf != 0:
548- eNorm = ef - 127 + 1 # the exp bias of subnormal bf16 is 126547+ eNorm = ef - 127 + 1 # the exp bias of subnormal bf16 is 126
549 else:548 else:
550- eNorm = ef - 127 # the exp bias of bf16 is 127549+ eNorm = ef - 127 # the exp bias of bf16 is 127
551 550 
552 if (eNorm > (maxExp - expBias)) or ((eNorm == (maxExp - expBias)) and ((mf >> mLenDelta) == 0b11)):551 if (eNorm > (maxExp - expBias)) or ((eNorm == (maxExp - expBias)) and ((mf >> mLenDelta) == 0b11)):
553- return ((sRet << 3) | 0b111)552+ return (sRet << 3) | 0b111
554 elif eNorm <= -(expBias):553 elif eNorm <= -(expBias):
555 eRet = 0554 eRet = 0
556- mf = (mf | 0x80)555+ mf = mf | 0x80
557 mLenDelta -= eNorm + expBias - 1556 mLenDelta -= eNorm + expBias - 1
558- needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry557+ needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry
559- mRet = (mf >> mLenDelta)558+ mRet = mf >> mLenDelta
560- if (needRound) :559+ if needRound:
561- mRet+=1560+ mRet += 1
562 else:561 else:
563- eRet = (eNorm + expBias)562+ eRet = eNorm + expBias
564 needRound = IsRoundOne(sRet, mf, mLenDelta)563 needRound = IsRoundOne(sRet, mf, mLenDelta)
565- mRet = (mf >> mLenDelta)564+ mRet = mf >> mLenDelta
566- if (needRound) :565+ if needRound:
567- mRet+=1566+ mRet += 1
568- if (((mRet & 0b100) != 0) and (needRound)) :567+ if ((mRet & 0b100) != 0) and (needRound):
569- eRet+=1568+ eRet += 1
570 mRet = 0569 mRet = 0
571 570 
572- if (eRet >= 1) :571+ if eRet >= 1:
573 eRet = 1572 eRet = 1
574- elif (eRet == 0 and mRet == 0b100) :573+ elif eRet == 0 and mRet == 0b100:
575- eRet+=1574+ eRet += 1
576 mRet = 0575 mRet = 0
577 576 
578- return (((sRet) << 3) | ((eRet) << 2) | ((mRet) & 3))577+ return ((sRet) << 3) | ((eRet) << 2) | ((mRet) & 3)
578+ 
579 579 
580def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor):580def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor):
581 import numpy as np581 import numpy as np
582+ 
582 shape_tensor = in_tensor.shape583 shape_tensor = in_tensor.shape
583 multi_shape = np.prod(shape_tensor)584 multi_shape = np.prod(shape_tensor)
584 out_tensor = np.zeros(multi_shape).astype(np.uint8)585 out_tensor = np.zeros(multi_shape).astype(np.uint8)
@@ -592,10 +593,12 @@ def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor):
592 # 每两个fp4拼成一个uint8保存593 # 每两个fp4拼成一个uint8保存
593 fp4_shape = list(shape_tensor)594 fp4_shape = list(shape_tensor)
594 fp4_shape[-1] = fp4_shape[-1] // 2595 fp4_shape[-1] = fp4_shape[-1] // 2
595- fp4_tensor = np.zeros(multi_shape//2).astype(np.uint8)596+ fp4_tensor = np.zeros(multi_shape // 2).astype(np.uint8)
596- for i in range(multi_shape//2):597+ for i in range(multi_shape // 2):
597 # fp4_tensor[i] = (out_tensor[i*2] << 4) | out_tensor[i*2+1] # 按常规顺序保存b4598 # fp4_tensor[i] = (out_tensor[i*2] << 4) | out_tensor[i*2+1] # 按常规顺序保存b4
598- fp4_tensor[i] = (out_tensor[i*2+1] << 4) | out_tensor[i*2] # 按两两交叉顺序保存b4,比如b4两个数:0100 0010 存为b8后为0010 0100599+ fp4_tensor[i] = (out_tensor[i * 2 + 1] << 4) | out_tensor[
600+ i * 2
601+ ] # 按两两交叉顺序保存b4,比如b4两个数:0100 0010 存为b8后为0010 0100
599 602 
600 fp4_tensor = fp4_tensor.reshape(fp4_shape)603 fp4_tensor = fp4_tensor.reshape(fp4_shape)
601 return fp4_tensor604 return fp4_tensor
@@ -603,6 +606,7 @@ def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor):
603 606 
604def trans_np_bfloat16_tensor_to_fp4_e1m2(in_tensor):607def trans_np_bfloat16_tensor_to_fp4_e1m2(in_tensor):
605 import numpy as np608 import numpy as np
609+ 
606 shape_tensor = in_tensor.shape610 shape_tensor = in_tensor.shape
607 multi_shape = np.prod(shape_tensor)611 multi_shape = np.prod(shape_tensor)
608 out_tensor = np.zeros(multi_shape).astype(np.uint8)612 out_tensor = np.zeros(multi_shape).astype(np.uint8)
@@ -616,22 +620,39 @@ def trans_np_bfloat16_tensor_to_fp4_e1m2(in_tensor):
616 # 每两个fp4拼成一个uint8保存620 # 每两个fp4拼成一个uint8保存
617 fp4_shape = list(shape_tensor)621 fp4_shape = list(shape_tensor)
618 fp4_shape[-1] = fp4_shape[-1] // 2622 fp4_shape[-1] = fp4_shape[-1] // 2
619- fp4_tensor = np.zeros(multi_shape//2).astype(np.uint8)623+ fp4_tensor = np.zeros(multi_shape // 2).astype(np.uint8)
620- for i in range(multi_shape//2):624+ for i in range(multi_shape // 2):
621 # fp4_tensor[i] = (out_tensor[i*2] << 4) | out_tensor[i*2+1] # 按常规顺序保存b4625 # fp4_tensor[i] = (out_tensor[i*2] << 4) | out_tensor[i*2+1] # 按常规顺序保存b4
622- fp4_tensor[i] = (out_tensor[i*2+1] << 4) | out_tensor[i*2] # 按两两交叉顺序保存b4,比如b4两个数:0100 0010 存为b8后为0010 0100626+ fp4_tensor[i] = (out_tensor[i * 2 + 1] << 4) | out_tensor[
627+ i * 2
628+ ] # 按两两交叉顺序保存b4,比如b4两个数:0100 0010 存为b8后为0010 0100
623 fp4_tensor = fp4_tensor.reshape(fp4_shape)629 fp4_tensor = fp4_tensor.reshape(fp4_shape)
624 return fp4_tensor630 return fp4_tensor
625 631 
632+ 
626def cvt_fp4_e2m1_to_bfloat16(x):633def cvt_fp4_e2m1_to_bfloat16(x):
627- Fp4e2m1ToBf16 = {'0': 0x0, '1': 0x3F00, '2': 0x3F80, '3':0x3FC0,634+ Fp4e2m1ToBf16 = {
628- '4': 0x4000, '5': 0x4040, '6': 0x4080, '7':0x40C0,635+ "0": 0x0,
629- '8': 0x8000, '9': 0xBF00, '10': 0xBF80, '11':0xBFC0,636+ "1": 0x3F00,
630- '12': 0xC000, '13': 0xC040, '14': 0xC080, '15':0xC0C0}637+ "2": 0x3F80,
638+ "3": 0x3FC0,
639+ "4": 0x4000,
640+ "5": 0x4040,
641+ "6": 0x4080,
642+ "7": 0x40C0,
643+ "8": 0x8000,
644+ "9": 0xBF00,
645+ "10": 0xBF80,
646+ "11": 0xBFC0,
647+ "12": 0xC000,
648+ "13": 0xC040,
649+ "14": 0xC080,
650+ "15": 0xC0C0,
651+ }
631 652 
632 x = int(x)653 x = int(x)
633- first_fp4val = x & 0x0f654+ first_fp4val = x & 0x0F
634- second_fp4val = (x >> 4 )& 0x0f655+ second_fp4val = (x >> 4) & 0x0F
635 first_fp4str = str(first_fp4val)656 first_fp4str = str(first_fp4val)
636 second_fp4str = str(second_fp4val)657 second_fp4str = str(second_fp4val)
637 658 
@@ -639,21 +660,37 @@ def cvt_fp4_e2m1_to_bfloat16(x):
639 660 
640 661 
641def cvt_fp4_e1m2_to_bfloat16(x):662def cvt_fp4_e1m2_to_bfloat16(x):
642- Fp4e1m2ToBf16 = {'0': 0x0, '1': 0x3E80, '2': 0x3F00, '3':0x3F40,663+ Fp4e1m2ToBf16 = {
643- '4': 0x3F80, '5': 0x3FA0, '6': 0x3FC0, '7':0x3FE0,664+ "0": 0x0,
644- '8': 0x8000, '9': 0xBE80, '10': 0xBF00, '11':0xBF40,665+ "1": 0x3E80,
645- '12': 0xBF80, '13': 0xBFA0, '14': 0xBFC0, '15':0xBFE0}666+ "2": 0x3F00,
667+ "3": 0x3F40,
668+ "4": 0x3F80,
669+ "5": 0x3FA0,
670+ "6": 0x3FC0,
671+ "7": 0x3FE0,
672+ "8": 0x8000,
673+ "9": 0xBE80,
674+ "10": 0xBF00,
675+ "11": 0xBF40,
676+ "12": 0xBF80,
677+ "13": 0xBFA0,
678+ "14": 0xBFC0,
679+ "15": 0xBFE0,
680+ }
646 681 
647 x = int(x)682 x = int(x)
648- first_fp4val = x & 0x0f683+ first_fp4val = x & 0x0F
649- second_fp4val = (x >> 4 )& 0x0f684+ second_fp4val = (x >> 4) & 0x0F
650 first_fp4str = str(first_fp4val)685 first_fp4str = str(first_fp4val)
651 second_fp4str = str(second_fp4val)686 second_fp4str = str(second_fp4val)
652 687 
653 return Fp4e1m2ToBf16[first_fp4str], Fp4e1m2ToBf16[second_fp4str]688 return Fp4e1m2ToBf16[first_fp4str], Fp4e1m2ToBf16[second_fp4str]
654 689 
690+ 
655def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor):691def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor):
656 import numpy as np692 import numpy as np
693+ 
657 shape_tensor = in_tensor.shape694 shape_tensor = in_tensor.shape
658 multi_shape = np.prod(shape_tensor)695 multi_shape = np.prod(shape_tensor)
659 out_tensor = np.zeros(multi_shape)696 out_tensor = np.zeros(multi_shape)
@@ -662,13 +699,13 @@ def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor):
662 # 1个uint8包含两个fp4, 先拆成两个uint8699 # 1个uint8包含两个fp4, 先拆成两个uint8
663 bfloat16_shape = list(shape_tensor)700 bfloat16_shape = list(shape_tensor)
664 bfloat16_shape[-1] = bfloat16_shape[-1] * 2701 bfloat16_shape[-1] = bfloat16_shape[-1] * 2
665- bfloat16_tensor = np.zeros(multi_shape*2).astype(np.uint16)702+ bfloat16_tensor = np.zeros(multi_shape * 2).astype(np.uint16)
666- fp32_tensor = np.zeros(multi_shape*2).astype(np.float32)703+ fp32_tensor = np.zeros(multi_shape * 2).astype(np.float32)
667 704 
668 for i in range(multi_shape):705 for i in range(multi_shape):
669- bfloat16_tensor[i*2], bfloat16_tensor[i*2+1] = cvt_fp4_e1m2_to_bfloat16(in_tensor[i])706+ bfloat16_tensor[i * 2], bfloat16_tensor[i * 2 + 1] = cvt_fp4_e1m2_to_bfloat16(in_tensor[i])
670- fp32_tensor[i*2] = struct.unpack('!f', struct.pack('!I', bfloat16_tensor[i*2]<<16))[0]707+ fp32_tensor[i * 2] = struct.unpack("!f", struct.pack("!I", bfloat16_tensor[i * 2] << 16))[0]
671- fp32_tensor[i*2+1] = struct.unpack('!f', struct.pack('!I', bfloat16_tensor[i*2+1]<<16))[0]708+ fp32_tensor[i * 2 + 1] = struct.unpack("!f", struct.pack("!I", bfloat16_tensor[i * 2 + 1] << 16))[0]
672 709 
673 fp32_tensor = fp32_tensor.reshape(bfloat16_shape)710 fp32_tensor = fp32_tensor.reshape(bfloat16_shape)
674 return fp32_tensor711 return fp32_tensor
@@ -676,6 +713,7 @@ def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor):
676 713 
677def trans_np_fp4_e2m1_tensor_to_bfloat16(in_tensor):714def trans_np_fp4_e2m1_tensor_to_bfloat16(in_tensor):
678 import numpy as np715 import numpy as np
716+ 
679 shape_tensor = in_tensor.shape717 shape_tensor = in_tensor.shape
680 multi_shape = np.prod(shape_tensor)718 multi_shape = np.prod(shape_tensor)
681 out_tensor = np.zeros(multi_shape)719 out_tensor = np.zeros(multi_shape)
@@ -684,29 +722,34 @@ def trans_np_fp4_e2m1_tensor_to_bfloat16(in_tensor):
684 # 1个uint8包含两个fp4, 先拆成两个uint8722 # 1个uint8包含两个fp4, 先拆成两个uint8
685 bfloat16_shape = list(shape_tensor)723 bfloat16_shape = list(shape_tensor)
686 bfloat16_shape[-1] = bfloat16_shape[-1] * 2724 bfloat16_shape[-1] = bfloat16_shape[-1] * 2
687- bfloat16_tensor = np.zeros(multi_shape*2).astype(np.uint16)725+ bfloat16_tensor = np.zeros(multi_shape * 2).astype(np.uint16)
688- fp32_tensor = np.zeros(multi_shape*2).astype(np.float32)726+ fp32_tensor = np.zeros(multi_shape * 2).astype(np.float32)
689 727 
690 for i in range(multi_shape):728 for i in range(multi_shape):
691- bfloat16_tensor[i*2], bfloat16_tensor[i*2+1] = cvt_fp4_e2m1_to_bfloat16(in_tensor[i])729+ bfloat16_tensor[i * 2], bfloat16_tensor[i * 2 + 1] = cvt_fp4_e2m1_to_bfloat16(in_tensor[i])
692- fp32_tensor[i*2] = struct.unpack('!f', struct.pack('!I', bfloat16_tensor[i*2]<<16))[0]730+ fp32_tensor[i * 2] = struct.unpack("!f", struct.pack("!I", bfloat16_tensor[i * 2] << 16))[0]
693- fp32_tensor[i*2+1] = struct.unpack('!f', struct.pack('!I', bfloat16_tensor[i*2+1]<<16))[0]731+ fp32_tensor[i * 2 + 1] = struct.unpack("!f", struct.pack("!I", bfloat16_tensor[i * 2 + 1] << 16))[0]
694 732 
695 fp32_tensor = fp32_tensor.reshape(bfloat16_shape)733 fp32_tensor = fp32_tensor.reshape(bfloat16_shape)
696 return fp32_tensor734 return fp32_tensor
697 735 
736+ 
698def numpy_hifloat8():737def numpy_hifloat8():
699 try:738 try:
700 # noinspection PyUnresolvedReferences739 # noinspection PyUnresolvedReferences
701 from en_dtypes import hifloat8740 from en_dtypes import hifloat8
702- ensure_en_dtypes_version('0.0.4')741+ 
742+ ensure_en_dtypes_version("0.0.4")
703 return hifloat8743 return hifloat8
704 except ModuleNotFoundError:744 except ModuleNotFoundError:
705- raise RuntimeError("en_dtypes is needed to support hifloat8 dtype!!! "745+ raise RuntimeError(
706- "Please install with `pip3 install en-dtypes`")746+ "en_dtypes is needed to support hifloat8 dtype!!! Please install with `pip3 install en-dtypes`"
747+ )
707 except ImportError:748 except ImportError:
708- raise RuntimeError("Please upgrade en_dtypes to v0.0.4 at least to support hifloat8 dtype!!! "749+ raise RuntimeError(
709- "Command is `pip3 install --upgrade en-dtypes`")750+ "Please upgrade en_dtypes to v0.0.4 at least to support hifloat8 dtype!!! "
751+ "Command is `pip3 install --upgrade en-dtypes`"
752+ )
710 753 
711 754 
712def resolve_custom_numpy_dtypes(container):755def resolve_custom_numpy_dtypes(container):
@@ -716,10 +759,17 @@ def resolve_custom_numpy_dtypes(container):
716 """759 """
717 if not container:760 if not container:
718 return container761 return container
719- special_dtypes = ("bfloat16", "int4",762+ special_dtypes = (
720- "float8_e5m2", "float8_e4m3fn", "float8_e8m0",763+ "bfloat16",
721- "float4_e2m1", "float4_e1m2",764+ "int4",
722- "hifloat8", "hifloat4")765+ "float8_e5m2",
766+ "float8_e4m3fn",
767+ "float8_e8m0",
768+ "float4_e2m1",
769+ "float4_e1m2",
770+ "hifloat8",
771+ "hifloat4",
772+ )
723 773 
724 def _convert(item):774 def _convert(item):
725 if isinstance(item, (tuple, list)):775 if isinstance(item, (tuple, list)):
@@ -745,13 +795,10 @@ def pack_4bits(src: numpy.ndarray):
745 shift = numpy.array([0, 4], dtype=numpy.uint8)795 shift = numpy.array([0, 4], dtype=numpy.uint8)
746 array = src796 array = src
747 if src.size % pack_size != 0:797 if src.size % pack_size != 0:
748- array = numpy.pad(src.flatten(),798+ array = numpy.pad(src.flatten(), (0, pack_size - src.size % pack_size), mode="constant")
749- (0, pack_size - src.size % pack_size),
750- mode='constant')
751 reshaped = array.reshape([-1, 2])799 reshaped = array.reshape([-1, 2])
752 # bitwise_and is for arm800 # bitwise_and is for arm
753- out = numpy.sum(numpy.bitwise_and(reshaped.view(numpy.uint8), 0b00001111) << shift,801+ out = numpy.sum(numpy.bitwise_and(reshaped.view(numpy.uint8), 0b00001111) << shift, axis=1, dtype=numpy.uint8)
754- axis=1, dtype=numpy.uint8)
755 return out802 return out
756 803 
757 804 
@@ -769,8 +816,7 @@ def encode_float8_e8m0(fp_array: numpy.ndarray):
769 if not isinstance(fp_array, numpy.ndarray):816 if not isinstance(fp_array, numpy.ndarray):
770 raise NotImplementedError("only support numpy array.")817 raise NotImplementedError("only support numpy array.")
771 if fp_array.dtype.name not in ("bfloat16", "float16", "float32"):818 if fp_array.dtype.name not in ("bfloat16", "float16", "float32"):
772- raise RuntimeError(f"Dtype of input tensor to be quantized "819+ raise RuntimeError(f"Dtype of input tensor to be quantized is not supported: {fp_array.dtype.name}")
773- f"is not supported: {fp_array.dtype.name}")
774 if "float16" == fp_array.dtype.name:820 if "float16" == fp_array.dtype.name:
775 fp_array = fp_array.astype("float32")821 fp_array = fp_array.astype("float32")
776 if "float32" == fp_array.dtype.name:822 if "float32" == fp_array.dtype.name:
@@ -784,8 +830,8 @@ def encode_float8_e8m0(fp_array: numpy.ndarray):
784 830 
785def normalize_to_tf_dtype(np_array: numpy.ndarray):831def normalize_to_tf_dtype(np_array: numpy.ndarray):
786 import tensorflow as tf832 import tensorflow as tf
787- if np_array.dtype.name == "bfloat16" and \833+ 
788- np_array.dtype.type != tf.bfloat16.as_numpy_dtype.dtype.type:834+ if np_array.dtype.name == "bfloat16" and np_array.dtype.type != tf.bfloat16.as_numpy_dtype.dtype.type:
789 return np_array.view(tf.bfloat16.as_numpy_dtype)835 return np_array.view(tf.bfloat16.as_numpy_dtype)
790 return np_array836 return np_array
791 837 
@@ -801,6 +847,7 @@ def tf_dtype_revert(tf_tensor):
801def torch_dtype_conversion(container):847def torch_dtype_conversion(container):
802 # convert string dtype to torch dtype848 # convert string dtype to torch dtype
803 import torch849 import torch
850+ 
804 return tuple([getattr(torch, c) if isinstance(c, str) else c for c in container])851 return tuple([getattr(torch, c) if isinstance(c, str) else c for c in container])
805 852 
806 853 
@@ -810,6 +857,7 @@ def acl_to_torch_dtype(container):
810 857 
811def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False):858def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False):
812 import torch859 import torch
860+ 
813 if np_array is None:861 if np_array is None:
814 return None862 return None
815 np_dtype = np_array.dtype.name863 np_dtype = np_array.dtype.name
@@ -818,12 +866,10 @@ def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False):
818 t_int16 = torch.from_numpy(np_int16)866 t_int16 = torch.from_numpy(np_int16)
819 return t_int16.view(torch.bfloat16)867 return t_int16.view(torch.bfloat16)
820 elif "int4" in np_dtype or "float4" in np_dtype:868 elif "int4" in np_dtype or "float4" in np_dtype:
821- raise RuntimeError(f"Can only transfer numpy.ndarray "869+ raise RuntimeError(f"Can only transfer numpy.ndarray to torch.Tensor with dtype [{np_dtype}]")
822- f"to torch.Tensor with dtype [{np_dtype}]")
823 elif "float8" in np_dtype:870 elif "float8" in np_dtype:
824 if np_dtype not in ("float8_e4m3fn", "float8_e5m2", "float8_e8m0"):871 if np_dtype not in ("float8_e4m3fn", "float8_e5m2", "float8_e8m0"):
825- raise RuntimeError(f"Dtype [{np_dtype}] is not supported to "872+ raise RuntimeError(f"Dtype [{np_dtype}] is not supported to convert to torch.Tensor yet.")
826- f"convert to torch.Tensor yet.")
827 # numpy float8_e8m0 has no suffix; torch dtype is float8_e8m0fnu873 # numpy float8_e8m0 has no suffix; torch dtype is float8_e8m0fnu
828 torch_dtype_name = {874 torch_dtype_name = {
829 "float8_e4m3fn": "float8_e4m3fn",875 "float8_e4m3fn": "float8_e4m3fn",
@@ -831,16 +877,17 @@ def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False):
831 "float8_e8m0": "float8_e8m0fnu",877 "float8_e8m0": "float8_e8m0fnu",
832 }[np_dtype]878 }[np_dtype]
833 if not hasattr(torch, torch_dtype_name):879 if not hasattr(torch, torch_dtype_name):
834- raise RuntimeError(f"Current pytorch version [{torch.__version__}] is too old. "880+ raise RuntimeError(
835- f"{torch_dtype_name} is not supported.")881+ f"Current pytorch version [{torch.__version__}] is too old. {torch_dtype_name} is not supported."
882+ )
836 return torch.from_numpy(np_array.view(dtype=numpy.uint8)).view(getattr(torch, torch_dtype_name))883 return torch.from_numpy(np_array.view(dtype=numpy.uint8)).view(getattr(torch, torch_dtype_name))
837 elif is_complex32:884 elif is_complex32:
838 if np_dtype != "float16":885 if np_dtype != "float16":
839- raise RuntimeError(f"Can only transfer numpy.float16 to torch.complex32 "886+ raise RuntimeError(f"Can only transfer numpy.float16 to torch.complex32 rather than {np_dtype}")
840- f"rather than {np_dtype}")
841 if not hasattr(torch, "complex32"):887 if not hasattr(torch, "complex32"):
842- raise RuntimeError(f"Current pytorch version [{torch.__version__}] is too old. "888+ raise RuntimeError(
843- f"Please update to at least v1.13.1")889+ f"Current pytorch version [{torch.__version__}] is too old. Please update to at least v1.13.1"
890+ )
844 ret = torch.from_numpy(np_array)891 ret = torch.from_numpy(np_array)
845 return ret.view(torch.complex32)892 return ret.view(torch.complex32)
846 else:893 else:
@@ -849,11 +896,11 @@ def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False):
849 896 
850def torch_to_numpy_tensor(torch_tensor) -> numpy.ndarray:897def torch_to_numpy_tensor(torch_tensor) -> numpy.ndarray:
851 import torch898 import torch
899+ 
852 if torch_tensor is None:900 if torch_tensor is None:
853 return None901 return None
854 if not isinstance(torch_tensor, torch.Tensor):902 if not isinstance(torch_tensor, torch.Tensor):
855- raise RuntimeError(f"Only support torch.Tensor. "903+ raise RuntimeError(f"Only support torch.Tensor. But got {type(torch_tensor)}")
856- f"But got {type(torch_tensor)}")
857 torch_dtype = torch_tensor.dtype904 torch_dtype = torch_tensor.dtype
858 torch_dtype_str = str(torch_dtype)905 torch_dtype_str = str(torch_dtype)
859 if torch_dtype == torch.bfloat16:906 if torch_dtype == torch.bfloat16:
@@ -864,7 +911,7 @@ def torch_to_numpy_tensor(torch_tensor) -> numpy.ndarray:
864 t_fp16 = torch_tensor.view(torch.float16)911 t_fp16 = torch_tensor.view(torch.float16)
865 return t_fp16.numpy()912 return t_fp16.numpy()
866 elif "float8" in torch_dtype_str:913 elif "float8" in torch_dtype_str:
867- np_func_suffix = torch_dtype_str.split('.')[-1].replace("fnu", "")914+ np_func_suffix = torch_dtype_str.split(".")[-1].replace("fnu", "")
868 np_dtype = eval(f"numpy_{np_func_suffix}()")915 np_dtype = eval(f"numpy_{np_func_suffix}()")
869 np_uint8 = torch_tensor.view(torch.uint8).numpy()916 np_uint8 = torch_tensor.view(torch.uint8).numpy()
870 return np_uint8.view(np_dtype)917 return np_uint8.view(np_dtype)
@@ -879,7 +926,7 @@ def _mx_reshape_to_blocks(fp_array: numpy.ndarray, axis: int, block_size: int):
879 pad_size = orig_shape[axis] % block_size926 pad_size = orig_shape[axis] % block_size
880 pad[axis][1] = block_size - pad_size927 pad[axis][1] = block_size - pad_size
881 if pad_size > 0:928 if pad_size > 0:
882- fp_array = numpy.pad(fp_array, pad, 'constant')929+ fp_array = numpy.pad(fp_array, pad, "constant")
883 padded_shape = fp_array.shape930 padded_shape = fp_array.shape
884 reshape = list(padded_shape)931 reshape = list(padded_shape)
885 reshape[axis + 1] = block_size932 reshape[axis + 1] = block_size
@@ -888,8 +935,7 @@ def _mx_reshape_to_blocks(fp_array: numpy.ndarray, axis: int, block_size: int):
888 return fp_array, orig_shape, padded_shape935 return fp_array, orig_shape, padded_shape
889 936 
890 937 
891-def _mx_undo_reshape_to_blocks(fp_array: numpy.ndarray, axis: int,938+def _mx_undo_reshape_to_blocks(fp_array: numpy.ndarray, axis: int, orig_shape: tuple, padded_shape: tuple):
892- orig_shape: tuple, padded_shape: tuple):
893 # Undo tile reshaping939 # Undo tile reshaping
894 fp_array = fp_array.reshape(padded_shape)940 fp_array = fp_array.reshape(padded_shape)
895 # Undo padding941 # Undo padding
@@ -907,14 +953,14 @@ def _mx_calculate_share_exp(fp_array: numpy.ndarray, scale_axis: int, mx_ele_dty
907 max_norm = get_dtype_range(mx_ele_dtype)[1]953 max_norm = get_dtype_range(mx_ele_dtype)[1]
908 ele_emax = int(numpy.log2(max_norm))954 ele_emax = int(numpy.log2(max_norm))
909 fp_abs_max = numpy.max(numpy.abs(fp_array), axis=scale_axis, keepdims=True)955 fp_abs_max = numpy.max(numpy.abs(fp_array), axis=scale_axis, keepdims=True)
910- res = numpy.floor(956+ res = numpy.floor(numpy.log2(fp_abs_max.astype(numpy.float32) + FP32_MIN_NORMAL * (fp_abs_max == 0))) - ele_emax
911- numpy.log2(fp_abs_max.astype(numpy.float32) + FP32_MIN_NORMAL * (fp_abs_max == 0))
912- ) - ele_emax
913 res[fp_abs_max == 0] = -float("inf")957 res[fp_abs_max == 0] = -float("inf")
914 return res958 return res
915 959 
960+ 
916def _mx_calculate_share_exp_nv(fp_array: numpy.ndarray, scale_axis: int, mx_ele_dtype: str):961def _mx_calculate_share_exp_nv(fp_array: numpy.ndarray, scale_axis: int, mx_ele_dtype: str):
917- import numpy 962+ import numpy
963+ 
918 FP32_EXPONENT_BIAS = 127964 FP32_EXPONENT_BIAS = 127
919 FP32_MIN_NORMAL = 2 ** (-FP32_EXPONENT_BIAS + 1)965 FP32_MIN_NORMAL = 2 ** (-FP32_EXPONENT_BIAS + 1)
920 max_norm = get_dtype_range(mx_ele_dtype)[1]966 max_norm = get_dtype_range(mx_ele_dtype)[1]
@@ -923,20 +969,21 @@ def _mx_calculate_share_exp_nv(fp_array: numpy.ndarray, scale_axis: int, mx_ele_
923 s_fp32 = fp_abs_max / max_norm969 s_fp32 = fp_abs_max / max_norm
924 binary_ints = numpy.array(s_fp32.view(numpy.uint32))970 binary_ints = numpy.array(s_fp32.view(numpy.uint32))
925 exponent_mask = numpy.uint32(0x7F800000) # 二进制:01111111100000000000000000000000971 exponent_mask = numpy.uint32(0x7F800000) # 二进制:01111111100000000000000000000000
926- mantissa_mask = numpy.uint32(0x007FFFFF) # 二进制:00000000011111111111111111111111 972+ mantissa_mask = numpy.uint32(0x007FFFFF) # 二进制:00000000011111111111111111111111
927 # 提取指数部分并转换为uint16973 # 提取指数部分并转换为uint16
928 exponents = (binary_ints & exponent_mask) >> 23974 exponents = (binary_ints & exponent_mask) >> 23
929 exponents_int16 = exponents.astype(numpy.int16)975 exponents_int16 = exponents.astype(numpy.int16)
930 # 提取尾数部分并转换为float976 # 提取尾数部分并转换为float
931- mantissas = (binary_ints & mantissa_mask)977+ mantissas = binary_ints & mantissa_mask
932 condition_1 = (exponents_int16 > 0) & (exponents_int16 < 254) & (mantissas > 0)978 condition_1 = (exponents_int16 > 0) & (exponents_int16 < 254) & (mantissas > 0)
933 # 2 ** 23 fp32的尾数位值0.5,即:二进制:0 00000000 10000000000000000000000979 # 2 ** 23 fp32的尾数位值0.5,即:二进制:0 00000000 10000000000000000000000
934- condition_2 = (exponents_int16 == 0) & (mantissas > 2 ** 22)980+ condition_2 = (exponents_int16 == 0) & (mantissas > 2**22)
935- exponents_int16 = numpy.where((condition_1|condition_2), exponents_int16 + 1, exponents_int16) 981+ exponents_int16 = numpy.where((condition_1 | condition_2), exponents_int16 + 1, exponents_int16)
936 res = (exponents_int16 - 127).astype(numpy.float32)982 res = (exponents_int16 - 127).astype(numpy.float32)
937 res[fp_abs_max == 0] = -float("inf")983 res[fp_abs_max == 0] = -float("inf")
938 return res984 return res
939 985 
986+ 
940def _mx_round_mantissa(fp_array: numpy.ndarray, round_mode: str):987def _mx_round_mantissa(fp_array: numpy.ndarray, round_mode: str):
941 """988 """
942 For example:989 For example:
@@ -955,7 +1002,7 @@ def _mx_round_mantissa(fp_array: numpy.ndarray, round_mode: str):
955 fp_array = numpy.where(sign, -rounded_abs, rounded_abs)1002 fp_array = numpy.where(sign, -rounded_abs, rounded_abs)
956 elif round_mode == "floor": # round to minus infinity(c language floor)1003 elif round_mode == "floor": # round to minus infinity(c language floor)
957 fp_array = numpy.floor(fp_array)1004 fp_array = numpy.floor(fp_array)
958- elif round_mode == "ceil": # round to positive infinity(c language ceil)1005+ elif round_mode == "ceil": # round to positive infinity(c language ceil)
959 fp_array = numpy.ceil(fp_array)1006 fp_array = numpy.ceil(fp_array)
960 elif round_mode == "trunc": # round to zero(c language truncation)1007 elif round_mode == "trunc": # round to zero(c language truncation)
961 fp_array = numpy.trunc(fp_array)1008 fp_array = numpy.trunc(fp_array)
@@ -964,27 +1011,29 @@ def _mx_round_mantissa(fp_array: numpy.ndarray, round_mode: str):
964 return fp_array1011 return fp_array
965 1012 
966 1013 
967-def _mx_quantize_to_element_format(fp_array: numpy.ndarray, share_exp: numpy.ndarray,1014+def _mx_quantize_to_element_format(
968- mx_ele_dtype: str, round_mode: str):1015+ fp_array: numpy.ndarray, share_exp: numpy.ndarray, mx_ele_dtype: str, round_mode: str
1016+):
969 mx_dtype = str(mx_ele_dtype)1017 mx_dtype = str(mx_ele_dtype)
970- match = re.search(r'e(\d+)m(\d+)', mx_dtype)1018+ match = re.search(r"e(\d+)m(\d+)", mx_dtype)
971 if match:1019 if match:
972 exp_bits = int(match.group(1))1020 exp_bits = int(match.group(1))
973 mantissa_bits = int(match.group(2))1021 mantissa_bits = int(match.group(2))
974 else:1022 else:
975 raise ValueError(f"mx element dtype [{mx_ele_dtype}] is not recognized.")1023 raise ValueError(f"mx element dtype [{mx_ele_dtype}] is not recognized.")
976 1024 
977- ret = fp_array / (2 ** share_exp)1025+ ret = fp_array / (2**share_exp)
978- private_exp = numpy.floor(numpy.log2(numpy.abs(ret.astype(numpy.float32)) + (ret == 0))1026+ private_exp = numpy.floor(numpy.log2(numpy.abs(ret.astype(numpy.float32)) + (ret == 0))).astype(
979- ).astype(fp_array.dtype, copy=False)1027+ fp_array.dtype, copy=False
1028+ )
980 # The minimum representable exponent1029 # The minimum representable exponent
981 min_exp = 0 if "float4_e1m2" in mx_dtype else -(2 ** (exp_bits - 1)) + 21030 min_exp = 0 if "float4_e1m2" in mx_dtype else -(2 ** (exp_bits - 1)) + 2
982 private_exp = private_exp.clip(min=min_exp)1031 private_exp = private_exp.clip(min=min_exp)
983 # Scale up so appropriate number of bits are in the integer portion of the number1032 # Scale up so appropriate number of bits are in the integer portion of the number
984- ret = ret / (2 ** private_exp) * (2 ** mantissa_bits)1033+ ret = ret / (2**private_exp) * (2**mantissa_bits)
985 ret = _mx_round_mantissa(ret, round_mode)1034 ret = _mx_round_mantissa(ret, round_mode)
986 # Undo scaling1035 # Undo scaling
987- ret = ret / (2 ** mantissa_bits) * (2 ** private_exp)1036+ ret = ret / (2**mantissa_bits) * (2**private_exp)
988 # Set values > max_norm to Inf if desired, else clamp them1037 # Set values > max_norm to Inf if desired, else clamp them
989 max_norm = get_dtype_range(mx_dtype)[1]1038 max_norm = get_dtype_range(mx_dtype)[1]
990 numpy.clip(ret, a_min=-max_norm, a_max=max_norm, out=ret)1039 numpy.clip(ret, a_min=-max_norm, a_max=max_norm, out=ret)
@@ -1018,9 +1067,10 @@ def pad_to_even(tensor: numpy.ndarray, axis: int) -> numpy.ndarray:
1018 pad_width = [(0, 0)] * tensor.ndim1067 pad_width = [(0, 0)] * tensor.ndim
1019 pad_width[axis] = (0, 1) # 在 axis 维度末尾补一个 01068 pad_width[axis] = (0, 1) # 在 axis 维度末尾补一个 0
1020 1069 
1021- padded_tensor = numpy.pad(tensor, pad_width, mode='constant', constant_values=2 ** -127)1070+ padded_tensor = numpy.pad(tensor, pad_width, mode="constant", constant_values=2**-127)
1022 return padded_tensor1071 return padded_tensor
1023 1072 
1073+ 
1024def interleave(tensor: numpy.ndarray, axis: int, n_group: int = 2) -> numpy.ndarray:1074def interleave(tensor: numpy.ndarray, axis: int, n_group: int = 2) -> numpy.ndarray:
1025 if not isinstance(tensor, numpy.ndarray):1075 if not isinstance(tensor, numpy.ndarray):
1026 raise ValueError("Input must be a numpy ndarray.")1076 raise ValueError("Input must be a numpy ndarray.")
@@ -1036,25 +1086,32 @@ def interleave(tensor: numpy.ndarray, axis: int, n_group: int = 2) -> numpy.ndar
1036 shape = list(tensor.shape)1086 shape = list(tensor.shape)
1037 1087 
1038 # 重塑形状:在目标轴后插入组维度1088 # 重塑形状:在目标轴后插入组维度
1039- new_shape = (1089+ new_shape = shape[:axis] + [group_length, 2] + shape[axis + 1 :]
1040- shape[:axis] +
1041- [group_length, 2] +
1042- shape[axis+1:])
1043 reshaped = tensor.reshape(new_shape)1090 reshaped = tensor.reshape(new_shape)
1044 1091 
1045 # 构建转置顺序:交换组维度和组内维度1092 # 构建转置顺序:交换组维度和组内维度
1046 transpose_order = (1093 transpose_order = (
1047- list(range(0, axis+1)) + # 目标轴之前的维度1094+ list(range(0, axis + 1)) # 目标轴之前的维度
1048- list(range(axis + 2, len(new_shape))) +1095+ + list(range(axis + 2, len(new_shape)))
1049- [axis+1,]) # 后续维度1096+ + [
1097+ axis + 1,
1098+ ]
1099+ ) # 后续维度
1050 1100 
1051 # 执行转置1101 # 执行转置
1052 transposed = reshaped.transpose(transpose_order)1102 transposed = reshaped.transpose(transpose_order)
1053 1103 
1054 return transposed1104 return transposed
1055 1105 
1056-def mx_quantize(fp_array: numpy.ndarray, mx_ele_dtype: str = "float4_e2m1",1106+ 
1057- axis: int = -1, block_size: int = 32, round_mode: str = "rint", scale_alg: int = 0) -> tuple:1107+def mx_quantize(
1108+ fp_array: numpy.ndarray,
1109+ mx_ele_dtype: str = "float4_e2m1",
1110+ axis: int = -1,
1111+ block_size: int = 32,
1112+ round_mode: str = "rint",
1113+ scale_alg: int = 0,
1114+) -> tuple:
1058 """1115 """
1059 quantize BFP16/FP16/FP32 to MX dtypes1116 quantize BFP16/FP16/FP32 to MX dtypes
1060 :parameter fp_array: input numpy array with dtype BFP16/FP16/FP321117 :parameter fp_array: input numpy array with dtype BFP16/FP16/FP32
@@ -1082,14 +1139,10 @@ def mx_quantize(fp_array: numpy.ndarray, mx_ele_dtype: str = "float4_e2m1",
1082 # padding & reshape to block_size1139 # padding & reshape to block_size
1083 fp_array, orig_shape, padded_shape = _mx_reshape_to_blocks(fp_array, axis, block_size)1140 fp_array, orig_shape, padded_shape = _mx_reshape_to_blocks(fp_array, axis, block_size)
1084 # get mx scale exponents1141 # get mx scale exponents
1085- if scale_alg==0 or (mx_ele_dtype in("float4_e2m1", "float4_e1m2")):1142+ if scale_alg == 0 or (mx_ele_dtype in ("float4_e2m1", "float4_e1m2")):
1086- share_exp = _mx_calculate_share_exp(fp_array,1143+ share_exp = _mx_calculate_share_exp(fp_array, scale_axis=axis + 1, mx_ele_dtype=mx_ele_dtype)
1087- scale_axis=axis + 1,
1088- mx_ele_dtype=mx_ele_dtype)
1089 else:1144 else:
1090- share_exp = _mx_calculate_share_exp_nv(fp_array,1145+ share_exp = _mx_calculate_share_exp_nv(fp_array, scale_axis=axis + 1, mx_ele_dtype=mx_ele_dtype)
1091- scale_axis=axis + 1,
1092- mx_ele_dtype=mx_ele_dtype)
1093 scale_emax = 2 ** (8 - 1) - 1 # 8 for E8M01146 scale_emax = 2 ** (8 - 1) - 1 # 8 for E8M0
1094 share_exp[share_exp > scale_emax] = float("NaN")1147 share_exp[share_exp > scale_emax] = float("NaN")
1095 share_exp[share_exp < -scale_emax] = -scale_emax1148 share_exp[share_exp < -scale_emax] = -scale_emax
@@ -1102,7 +1155,7 @@ def mx_quantize(fp_array: numpy.ndarray, mx_ele_dtype: str = "float4_e2m1",
1102 # convert to fp8_e8m0 & fp4/fp8 dtype1155 # convert to fp8_e8m0 & fp4/fp8 dtype
1103 ele_dtype_np = eval(f"numpy_{mx_ele_dtype}()")1156 ele_dtype_np = eval(f"numpy_{mx_ele_dtype}()")
1104 # share_exp is always float321157 # share_exp is always float32
1105- scale_array = 2 ** share_exp1158+ scale_array = 2**share_exp
1106 if ele_array.dtype.name == "bfloat16":1159 if ele_array.dtype.name == "bfloat16":
1107 ele_array = ele_array.astype("float32", copy=False)1160 ele_array = ele_array.astype("float32", copy=False)
1108 1161 
@@ -1127,12 +1180,8 @@ def mx_quantize(fp_array: numpy.ndarray, mx_ele_dtype: str = "float4_e2m1",
1127 1180 
1128 1181 
1129def _grouped_mx_undo_reshape_to_blocks(1182def _grouped_mx_undo_reshape_to_blocks(
1130- fp_array: numpy.ndarray,1183+ fp_array: numpy.ndarray, group_index: numpy.ndarray, axis: int, padded_group_index: list, padded_shape: tuple
1131- group_index: numpy.ndarray,1184+) -> numpy.ndarray:
1132- axis: int,
1133- padded_group_index: list,
1134- padded_shape: tuple
1135- ) -> numpy.ndarray:
1136 """1185 """
1137 根据 group_index 和 padded_group_index 还原被分组补 Pad 的数组1186 根据 group_index 和 padded_group_index 还原被分组补 Pad 的数组
1138 1187 
@@ -1213,14 +1262,13 @@ def _grouped_mx_reshape_to_blocks(fp_array: numpy.ndarray, group_index: numpy.nd
1213 # 构造 Pad 宽度,仅在 axis 轴补 Pad1262 # 构造 Pad 宽度,仅在 axis 轴补 Pad
1214 pad_width = [(0, 0)] * group.ndim1263 pad_width = [(0, 0)] * group.ndim
1215 pad_width[axis] = (0, pad_size)1264 pad_width[axis] = (0, pad_size)
1216- padded_group = numpy.pad(group, pad_width, mode='constant')1265+ padded_group = numpy.pad(group, pad_width, mode="constant")
1217 else:1266 else:
1218 padded_group = group1267 padded_group = group
1219 padded_index = padded_index + group_len + pad_size1268 padded_index = padded_index + group_len + pad_size
1220 padded_groups.append(padded_group)1269 padded_groups.append(padded_group)
1221 padded_group_index.append(padded_index)1270 padded_group_index.append(padded_index)
1222 1271 
1223- 
1224 # Step 3: Concatenate all groups along axis1272 # Step 3: Concatenate all groups along axis
1225 padded_array = numpy.concatenate(padded_groups, axis=axis)1273 padded_array = numpy.concatenate(padded_groups, axis=axis)
1226 # Step 4: Expand dimensions as in original function1274 # Step 4: Expand dimensions as in original function
@@ -1229,24 +1277,25 @@ def _grouped_mx_reshape_to_blocks(fp_array: numpy.ndarray, group_index: numpy.nd
1229 # 总块数 = 总长度(已对齐)// block_size1277 # 总块数 = 总长度(已对齐)// block_size
1230 total_blocks = padded_array.shape[axis] // block_size1278 total_blocks = padded_array.shape[axis] // block_size
1231 reshape = list(expanded_array.shape)1279 reshape = list(expanded_array.shape)
1232- reshape[axis] = total_blocks # 替换原轴为块数1280+ reshape[axis] = total_blocks # 替换原轴为块数
1233 reshape.insert(axis + 1, block_size) # 插入块大小维度1281 reshape.insert(axis + 1, block_size) # 插入块大小维度
1234 1282 
1235 reshaped_array = expanded_array.reshape(reshape)1283 reshaped_array = expanded_array.reshape(reshape)
1236- padded_shape = expanded_array.shape # 补 Pad 后的形状(包含 expand_dims)1284+ padded_shape = expanded_array.shape # 补 Pad 后的形状(包含 expand_dims)
1237 1285 
1238 return reshaped_array, padded_group_index, padded_shape1286 return reshaped_array, padded_group_index, padded_shape
1239 1287 
1240 1288 
1241def reshape_scale_array_pad(scale_array: numpy.ndarray, group_index: numpy.ndarray, axis):1289def reshape_scale_array_pad(scale_array: numpy.ndarray, group_index: numpy.ndarray, axis):
1242 import math1290 import math
1291+ 
1243 scale_array = numpy.squeeze(scale_array, 1)1292 scale_array = numpy.squeeze(scale_array, 1)
1244 cur_idx = 01293 cur_idx = 0
1245 pre_element = 01294 pre_element = 0
1246 for idx, element in enumerate(group_index):1295 for idx, element in enumerate(group_index):
1247- next_group_start = (element//64+idx+1)*2 # 下一个group的初始地址1296+ next_group_start = (element // 64 + idx + 1) * 2 # 下一个group的初始地址
1248- real_idx = cur_idx + math.ceil((element-pre_element)/32) # 实际计算到多少行1297+ real_idx = cur_idx + math.ceil((element - pre_element) / 32) # 实际计算到多少行
1249- pad_idx = math.ceil(real_idx/2) * 2 # 需要pad到多少行1298+ pad_idx = math.ceil(real_idx / 2) * 2 # 需要pad到多少行
1250 zero_row = numpy.full((1, scale_array.shape[1]), 2**-127)1299 zero_row = numpy.full((1, scale_array.shape[1]), 2**-127)
1251 one_row = numpy.full((1, scale_array.shape[1]), 1)1300 one_row = numpy.full((1, scale_array.shape[1]), 1)
1252 for i in range(real_idx, pad_idx):1301 for i in range(real_idx, pad_idx):
@@ -1256,13 +1305,23 @@ def reshape_scale_array_pad(scale_array: numpy.ndarray, group_index: numpy.ndarr
1256 pre_element = element # 前一个element1305 pre_element = element # 前一个element
1257 cur_idx = next_group_start # 当前计算到多少行1306 cur_idx = next_group_start # 当前计算到多少行
1258 1307 
1259- scale_array = scale_array.reshape(int(scale_array.shape[0]/2), 2, scale_array.shape[1]).transpose(0, 2, 1).reshape(int(scale_array.shape[0]/2), scale_array.shape[1], 2)1308+ scale_array = (
1309+ scale_array.reshape(int(scale_array.shape[0] / 2), 2, scale_array.shape[1])
1310+ .transpose(0, 2, 1)
1311+ .reshape(int(scale_array.shape[0] / 2), scale_array.shape[1], 2)
1312+ )
1260 1313 
1261 return scale_array1314 return scale_array
1262 1315 
1263 1316 
1264-def grouped_mx_quantize(fp_array: numpy.ndarray, group_index: numpy.ndarray, mx_ele_dtype: str = "float8_e5m2",1317+def grouped_mx_quantize(
1265- axis: int = -2, block_size: int = 32, round_mode: str = "rint") -> tuple:1318+ fp_array: numpy.ndarray,
1319+ group_index: numpy.ndarray,
1320+ mx_ele_dtype: str = "float8_e5m2",
1321+ axis: int = -2,
1322+ block_size: int = 32,
1323+ round_mode: str = "rint",
1324+) -> tuple:
1266 """1325 """
1267 quantize BFP16/FP16 to MX dtypes1326 quantize BFP16/FP16 to MX dtypes
1268 :parameter fp_array: input numpy array with dtype BFP16/FP161327 :parameter fp_array: input numpy array with dtype BFP16/FP16
@@ -1304,9 +1363,7 @@ def grouped_mx_quantize(fp_array: numpy.ndarray, group_index: numpy.ndarray, mx_
1304 # padding & reshape to block_size1363 # padding & reshape to block_size
1305 fp_array, padded_group_index, padded_shape = _grouped_mx_reshape_to_blocks(fp_array, group_index, axis, block_size)1364 fp_array, padded_group_index, padded_shape = _grouped_mx_reshape_to_blocks(fp_array, group_index, axis, block_size)
1306 # get mx scale exponents1365 # get mx scale exponents
1307- share_exp = _mx_calculate_share_exp(fp_array,1366+ share_exp = _mx_calculate_share_exp(fp_array, scale_axis=axis + 1, mx_ele_dtype=mx_ele_dtype)
1308- scale_axis=axis + 1,
1309- mx_ele_dtype=mx_ele_dtype)
1310 scale_emax = 2 ** (8 - 1) - 1 # 8 for E8M01367 scale_emax = 2 ** (8 - 1) - 1 # 8 for E8M0
1311 share_exp[share_exp > scale_emax] = float("NaN")1368 share_exp[share_exp > scale_emax] = float("NaN")
1312 share_exp[share_exp < -scale_emax] = -scale_emax1369 share_exp[share_exp < -scale_emax] = -scale_emax
@@ -1319,7 +1376,7 @@ def grouped_mx_quantize(fp_array: numpy.ndarray, group_index: numpy.ndarray, mx_
1319 # convert to fp8_e8m0 & fp8 dtype1376 # convert to fp8_e8m0 & fp8 dtype
1320 ele_dtype_np = eval(f"numpy_{mx_ele_dtype}()")1377 ele_dtype_np = eval(f"numpy_{mx_ele_dtype}()")
1321 # share_exp is always float321378 # share_exp is always float32
1322- scale_array = 2 ** share_exp1379+ scale_array = 2**share_exp
1323 if ele_array.dtype.name == "bfloat16":1380 if ele_array.dtype.name == "bfloat16":
1324 ele_array = ele_array.astype("float32", copy=False)1381 ele_array = ele_array.astype("float32", copy=False)
1325 1382 
@@ -1334,6 +1391,7 @@ def grouped_mx_quantize(fp_array: numpy.ndarray, group_index: numpy.ndarray, mx_
1334 1391 
1335def fp32_to_hf32(torch_tensor):1392def fp32_to_hf32(torch_tensor):
1336 import torch1393 import torch
1394+ 
1337 data_hf32 = torch_tensor.numpy().view(numpy.int32)1395 data_hf32 = torch_tensor.numpy().view(numpy.int32)
1338 data_hf32 = numpy.right_shift(numpy.right_shift(data_hf32, 12) + 1, 1)1396 data_hf32 = numpy.right_shift(numpy.right_shift(data_hf32, 12) + 1, 1)
1339 data_hf32 = numpy.left_shift(data_hf32, 13)1397 data_hf32 = numpy.left_shift(data_hf32, 13)
@@ -1351,12 +1409,59 @@ def is_torch_native_dtype(dtype_name):
1351 valued tensors, no numpy interop), so it is explicitly excluded.1409 valued tensors, no numpy interop), so it is explicitly excluded.
1352 """1410 """
1353 import torch1411 import torch
1412+ 
1354 name = str(dtype_name)1413 name = str(dtype_name)
1355- if name == 'int4':1414+ if name == "int4":
1356 return False1415 return False
1357 return hasattr(torch, name)1416 return hasattr(torch, name)
1358 1417 
1359 1418 
1419+_TF_DTYPE_MAP = {
1420+ "float32": "float32",
1421+ "float16": "float16",
1422+ "bfloat16": "bfloat16",
1423+ "float64": "float64",
1424+ "double": "float64",
1425+ "int8": "int8",
1426+ "int16": "int16",
1427+ "int32": "int32",
1428+ "int64": "int64",
1429+ "uint8": "uint8",
1430+ "uint16": "uint16",
1431+ "uint32": "uint32",
1432+ "uint64": "uint64",
1433+ "bool": "bool",
1434+ "complex64": "complex64",
1435+ "complex128": "complex128",
1436+}
1437+ 
1438+ 
1439+def str_to_tf_dtype(dtype_str: str):
1440+ """Convert dtype string to tf.dtype object.
1441+ 
1442+ Args:
1443+ dtype_str: string like 'float16', 'int8', 'fp16', 'bf16', etc.
1444+ 
1445+ Returns:
1446+ tf.dtype object, or None if not recognized.
1447+ """
1448+ if not isinstance(dtype_str, str):
1449+ return dtype_str
1450+ import tensorflow as tf
1451+ 
1452+ canonical = dtype_map.get(dtype_str, dtype_str)
1453+ tf_name = _TF_DTYPE_MAP.get(canonical)
1454+ if tf_name is None:
1455+ return None
1456+ return getattr(tf, tf_name, None)
1457+ 
1458+ 
1459+def is_tf_native_dtype(dtype_name) -> bool:
1460+ """Check if a dtype name is natively supported by TensorFlow."""
1461+ canonical = dtype_map.get(str(dtype_name), str(dtype_name))
1462+ return canonical in _TF_DTYPE_MAP
1463+ 
1464+ 
1360def np_as_strided_safe(base, shape, strides):1465def np_as_strided_safe(base, shape, strides):
1361 """numpy as_strided that handles non-native dtypes (e.g. ml_dtypes.float8_e5m2).1466 """numpy as_strided that handles non-native dtypes (e.g. ml_dtypes.float8_e5m2).
1362 1467 
@@ -1372,8 +1477,9 @@ def np_as_strided_safe(base, shape, strides):
1372 triggers the void-proxy workaround.1477 triggers the void-proxy workaround.
1373 """1478 """
1374 from numpy.lib.stride_tricks import as_strided as _np_as_strided1479 from numpy.lib.stride_tricks import as_strided as _np_as_strided
1480+ 
1375 dtype = base.dtype1481 dtype = base.dtype
1376- if dtype.kind != 'f' or dtype.char in ('f', 'd', 'e', 'g'):1482+ if dtype.kind != "f" or dtype.char in ("f", "d", "e", "g"):
1377 return _np_as_strided(base, shape=shape, strides=strides)1483 return _np_as_strided(base, shape=shape, strides=strides)
1378 try:1484 try:
1379 return _np_as_strided(base, shape=shape, strides=strides)1485 return _np_as_strided(base, shape=shape, strides=strides)
@@ -0,0 +1,143 @@
1+#!/usr/bin/env python3
2+# -*- coding: UTF-8 -*-
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+ 
11+"""TF parameter extractor — parse TF op signatures via inspect.signature.
12+ 
13+TF ops are standard Python functions, so inspect.signature works directly
14+(unlike torch which needs aten schema / pyi stub parsing).
15+"""
16+ 
17+import inspect
18+import logging
19+from typing import Optional
20+ 
21+from ttk.utilities.simple_param_extractor import APIParamInfo, OverloadInfo, ParamInfo
22+from ttk.utilities.func_dispatch import resolve_callable_str
23+ 
24+ 
25+_TF_SCALAR_TYPES = {"int", "float", "bool", "str", "Number", "Scalar"}
26+ 
27+_TF_NON_TENSOR_PARAM_NAMES = frozenset(
28+ {
29+ "name",
30+ "name_",
31+ }
32+)
33+ 
34+ 
35+def _is_tensor_param(p: inspect.Parameter, api_name: str) -> bool:
36+ """Determine if a TF op parameter is a tensor parameter.
37+ 
38+ Strategy:
39+ 1. If annotation is tf.Tensor/tf.Variable → True.
40+ 2. If param name is a known non-tensor name (name, axis, dims, etc.) → False.
41+ 3. If annotation is a scalar type (int, float, bool, str) → False.
42+ 4. tf.raw_ops.* convention: all params except 'name' are tensors.
43+ 5. If annotation is Annotated/Any/empty → treat positional params as tensors.
44+ """
45+ import tensorflow as tf
46+ import typing
47+ 
48+ ann = p.annotation
49+ if ann is not inspect.Parameter.empty:
50+ try:
51+ if isinstance(ann, type) and issubclass(ann, (tf.Tensor, tf.Variable)):
52+ return True
53+ except TypeError:
54+ pass
55+ ann_name = getattr(ann, "__name__", str(ann))
56+ if ann_name in ("int", "float", "bool", "str", "dtype"):
57+ return False
58+ if p.name in _TF_NON_TENSOR_PARAM_NAMES:
59+ return False
60+ if api_name.startswith("tf.raw_ops.") or api_name.startswith("tensorflow.raw_ops."):
R
RRuiWang_17 天前

tf.raw_ops.*_is_tensor_param 把除 name 外的所有参数都当 tensor。但有些 raw_ops 有 attr 参数,比如 tf.raw_ops.Cast(x, DstT)DstT 是 dtype 不是 tensor,会被误判成 tensor 并生成 TensorSpec,tracing 时出错。建议结合参数注解或已知 attr 名单判断。

likedislike
61+ return True
62+ return p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)
63+ if p.name in _TF_NON_TENSOR_PARAM_NAMES:
64+ return False
65+ if api_name.startswith("tf.raw_ops.") or api_name.startswith("tensorflow.raw_ops."):
66+ return True
67+ return p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)
68+ 
69+ 
70+def _infer_param_type(p: inspect.Parameter, api_name: str) -> str:
71+ """Infer type string for a TF op parameter."""
72+ if _is_tensor_param(p, api_name):
73+ return "Tensor"
74+ ann = p.annotation
75+ if ann is inspect.Parameter.empty:
76+ return "Number"
77+ ann_name = getattr(ann, "__name__", str(ann))
78+ if ann_name in ("int", "float", "bool", "str"):
79+ return ann_name
80+ if ann_name in ("list", "tuple"):
R
RRuiWang_17 天前

_infer_param_type 把注解是 list/tuple 的参数推断成 "Number"。但 list 类型的参数通常不是单个数值(比如 begin: list),生成 Number 输入塞进去多半不对。至少标成 "list" 之类,让输入生成逻辑能区分。

likedislike
81+ return "Number"
82+ return ann_name if ann_name in _TF_SCALAR_TYPES else "Number"
83+ 
84+ 
85+def extract_tf_params(api_name: str) -> Optional[APIParamInfo]:
86+ """Parse a TF op's signature and return APIParamInfo.
87+ 
88+ Uses inspect.signature to extract parameters. TF ops (tf.raw_ops.*,
89+ tf.nn.*, tf.math.*) are standard Python callables.
90+ 
91+ Args:
92+ api_name: e.g. 'tf.raw_ops.Add', 'tf.nn.relu'
93+ 
94+ Returns:
95+ APIParamInfo with a single overload, or None on failure.
96+ """
97+ try:
98+ func = resolve_callable_str(api_name)
99+ except Exception as e:
100+ logging.warning(f"Cannot resolve TF api {api_name}: {e}")
101+ return None
102+ 
103+ try:
104+ sig = inspect.signature(func)
105+ except (ValueError, TypeError) as e:
106+ logging.warning(f"Cannot get signature for {api_name}: {e}")
107+ return None
108+ 
109+ params = []
110+ is_raw_ops = api_name.startswith(("tf.raw_ops.", "tensorflow.raw_ops."))
111+ for name, p in sig.parameters.items():
112+ is_kw_only = p.kind == inspect.Parameter.KEYWORD_ONLY
113+ is_var_pos = p.kind == inspect.Parameter.VAR_POSITIONAL
114+ has_default = p.default is not inspect.Parameter.empty
115+ is_tensor = _is_tensor_param(p, api_name)
116+ 
117+ # tf.raw_ops ops require keyword args even when inspect shows POSITIONAL_OR_KEYWORD
118+ if is_raw_ops and name != "name":
119+ is_kw_only = True
120+ 
121+ # The 'name' parameter in TF ops is always a string, not a tensor
122+ if name == "name" and not is_tensor:
123+ is_kw_only = True
124+ 
125+ pi = ParamInfo(
126+ name=name,
127+ type=_infer_param_type(p, api_name),
128+ default=p.default if has_default else None,
129+ is_optional=has_default or is_var_pos,
130+ is_keyword_only=is_kw_only,
131+ is_var_positional=is_var_pos,
132+ )
133+ params.append(pi)
134+ 
135+ overload = OverloadInfo(params=params)
136+ info = APIParamInfo(
137+ api_name=api_name,
138+ params=params,
139+ source="tf_inspect",
140+ overloads=[overload],
141+ )
142+ logging.debug(f"Parsed {api_name}: {len(params)} params from tf_inspect")
143+ return info