| @@ -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 | """ |
| 8 | from ttk.core_modules.framework_api.backends.base import Backend | 8 | from ttk.core_modules.framework_api.backends.base import Backend |
| 9 | from ttk.core_modules.framework_api.backends.torch_backend import TorchBackend | 9 | from ttk.core_modules.framework_api.backends.torch_backend import TorchBackend |
| 10 | -from ttk.core_modules.framework_api.backends.npu_backend import NpuTorchBackend | 10 | +from ttk.core_modules.framework_api.backends.npu_torch_backend import NpuTorchBackend |
| 11 | -from ttk.core_modules.framework_api.backends.xpu_backend import XpuTorchBackend | 11 | +from ttk.core_modules.framework_api.backends.xpu_torch_backend import XpuTorchBackend |
| 12 | -from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend | 12 | +from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | def test_backend_abc_has_new_methods(): | 15 | def 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 | ||
| 19 | def test_torchbackend_is_available_via_getattr(monkeypatch): | 19 | def test_torchbackend_is_available_via_getattr(monkeypatch): |
| @@ -34,12 +34,12 @@ def test_torchbackend_is_available_via_getattr(monkeypatch): | |||
| 34 | assert tb.device_count() == 1 | 34 | 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 | ||
| 45 | def test_torchbackend_device_name_is_model_via_get_device_name(monkeypatch): | 45 | def 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 False | 64 | 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 True | 69 | + 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 segment | 73 | +# 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 injection | 81 | nb._segment_name = "npu" # mimic _build injection |
| 79 | assert nb.is_npu() is True | 82 | 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 injection | 90 | xb._segment_name = "xpu" # mimic _build injection |
| 86 | - assert xb.alias() == "xpu" | 91 | + assert xb.device_type() == "xpu" |
| 87 | - assert xb.use_device() is True | 92 | + assert xb.has_device() is True |
| 88 | 93 | ||
| 89 | 94 | ||
| 90 | def test_cpu_no_device(): | 95 | def 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 False | 101 | + assert cb.has_device() is False |
| 95 | 102 | ||
| 96 | 103 | ||
| 97 | def test_npu_is_available_uses_torch_npu(monkeypatch): | 104 | def 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 torch | 106 | import torch |
| 107 | + | ||
| 100 | class _FakeNpu: | 108 | class _FakeNpu: |
| 101 | - def is_available(self): return True | 109 | + def is_available(self): |
| 102 | - def device_count(self): return 2 | 110 | + 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 True | 119 | assert nb.is_available() is True |
| 106 | assert nb.device_count() == 2 | 120 | 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 passed | 125 | 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 torch | 127 | import torch |
| 114 | - from ttk.core_modules.framework_api.backends import npu_backend | 128 | + from ttk.core_modules.framework_api.backends import npu_torch_backend |
| 115 | 129 | ||
| 116 | class _FakeNpu: | 130 | class _FakeNpu: |
| 117 | 131 | ||
| @@ -126,56 +140,59 @@ def test_npu_soc_series_uses_model_not_segment(monkeypatch): | |||
| 126 | captured["arg"] = full_soc_version | 140 | 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 it | 156 | +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, XpuTorchBackend | 160 | + 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 class | 165 | 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 | ||
| 152 | def test_build_arbitrary_segment_name_carried_through(): | 170 | def 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, XpuTorchBackend | 172 | + 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 | ||
| 162 | def test_build_npu_torch_lib_routes_to_npu_backend(): | 181 | def 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, NpuTorchBackend | 183 | + 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 | ||
| 172 | def test_build_cpu_torch_lib_routes_to_cpu_backend(): | 191 | def 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, CpuTorchBackend | 193 | + 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 | """ |
| 15 | import subprocess | 15 | import subprocess |
| 16 | 16 | ||
| 17 | -from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend | 17 | +from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend |
| 18 | -from ttk.core_modules.framework_api.backends.npu_backend import NpuTorchBackend | 18 | +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 False | 24 | + cb.torch_lib = "cpu" |
| 25 | + cb.profile = {} | ||
| 26 | + assert cb.has_device() is False | ||
| 25 | assert cb.is_npu() is False | 27 | 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 | ||
| 31 | def test_soc_version_method_removed(): | 33 | def 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 | ||
| 38 | def test_soc_series_default_is_device_name_model(): | 41 | def 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(): | |||
| 45 | def test_no_string_comparison_on_role(): | 50 | def 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 in | 55 | 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 is | 56 | _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 no | 61 | + 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 | ||
| 74 | def test_no_inequality_comparison_on_role(): | 74 | def 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 | ||
| 85 | def test_no_soc_version_residue_in_ttk(): | 90 | def 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 so | 117 | # _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-segment | 122 | 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. | |||
| 11 | import importlib | 11 | import importlib |
| 12 | 12 | ||
| 13 | from ttk.core_modules.framework_api.backends import get_backend, _probe | 13 | from ttk.core_modules.framework_api.backends import get_backend, _probe |
| 14 | -from ttk.core_modules.framework_api.backends.cpu_backend import CpuTorchBackend | 14 | +from ttk.core_modules.framework_api.backends.cpu_torch_backend import CpuTorchBackend |
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | def test_probe_cuda_skips_import(monkeypatch): | 17 | def test_probe_cuda_skips_import(monkeypatch): |
| @@ -53,7 +53,5 @@ def test_probe_catches_runtime_error(monkeypatch): | |||
| 53 | 53 | ||
| 54 | def test_auto_detect_falls_back_to_cpu(monkeypatch): | 54 | def 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 = profile | 29 | 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 False | 35 | 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 = None | 97 | prof._device_time_attr = None |
| 99 | - evt = _Evt(self_cuda_time_total=11.0) # 无 self_device_time_total | 98 | + evt = _Evt(self_cuda_time_total=11.0) # 无 self_device_time_total |
| 100 | assert prof._device_time(evt, "cuda") == 11.0 | 99 | 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 | + | ||
| 113 | def test_init_device_acts_from_activities_with_cuda(monkeypatch): | 113 | def 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): | |||
| 124 | def test_init_device_acts_empty_for_cpu_only(monkeypatch): | 126 | def 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): | |||
| 151 | def test_init_device_time_attr_passed_through(monkeypatch): | 157 | def 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 | + | ||
| 162 | class _FakeEvent: | 171 | class _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 | ||
| 226 | def test_result_device_branch_uses_explicit_attr(): | 235 | def 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.0 | 240 | 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 | + | ||
| 238 | def test_get_profiler_npu_api_on_non_npu_includes_backend_alias(): | 246 | def 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 python3 | 1 | #!/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 of | 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"). | 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 the | 10 | +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 contiguous | 12 | +which materialize non-contiguous views into contiguous tensors. |
| 13 | -tensors. | ||
| 14 | """ | 13 | """ |
| 14 | + | ||
| 15 | import os | 15 | import os |
| 16 | 16 | ||
| 17 | import numpy as np | 17 | import numpy as np |
| 18 | import pytest | 18 | import pytest |
| 19 | import torch | 19 | import 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 | ||
| 37 | class TestClonePreservingStride: | 34 | class 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 None | 42 | + 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 t | 47 | 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.shape | 77 | assert out.shape == t.shape |
| 76 | assert out.dtype == t.dtype | 78 | 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.0 | 83 | out[0, 0, 0, 0] = 999.0 |
| 82 | assert t[0, 0, 0, 0].item() != 999.0 | 84 | 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_lib | 110 | + 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 = False | 118 | _npu_ok = False |
| 96 | try: | 119 | try: |
| 97 | import torch_npu | 120 | 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() |
| 100 | except Exception: | 124 | except Exception: |
| @@ -106,7 +130,18 @@ _ASCEND_ENV_SNAPSHOT = {k: os.environ.get(k) for k in _ASCEND_ENV_KEYS} | |||
| 106 | 130 | ||
| 107 | 131 | ||
| 108 | class TestToDevicePreservingStrideNPU: | 132 | class 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 | 146 | ||
| 112 | def _restore_ascend_env(self, monkeypatch): | 147 | def _restore_ascend_env(self, monkeypatch): |
| @@ -117,13 +152,11 @@ class TestToDevicePreservingStrideNPU: | |||
| 117 | yield | 152 | 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 | |||
| 10 | the camodel directory resolution, mirroring the existing simulator backend | 10 | the camodel directory resolution, mirroring the existing simulator backend |
| 11 | tests (``test_simulator_backend.py``). | 11 | tests (``test_simulator_backend.py``). |
| 12 | """ | 12 | """ |
| 13 | + | ||
| 13 | import os | 14 | import os |
| 14 | from types import SimpleNamespace | 15 | from types import SimpleNamespace |
| 15 | 16 | ||
| @@ -64,10 +65,10 @@ class _FakeBackend: | |||
| 64 | def __init__(self): | 65 | def __init__(self): |
| 65 | self._dev = True | 66 | self._dev = True |
| 66 | 67 | ||
| 67 | - def use_device(self): | 68 | + def has_device(self): |
| 68 | return self._dev | 69 | 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 dir | 175 | + assert (case_path / "instr.bin").is_file() # moved into case dir |
| 175 | - assert not (tmp_path / "instr.bin").exists() # removed from worker cwd | 176 | + assert not (tmp_path / "instr.bin").exists() # removed from worker cwd |
| 176 | - assert reported == [case_path] # report triggered | 177 | + 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_profiling | 180 | 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 collected | 195 | + assert (case_path / "instr.bin").is_file() # still collected |
| 195 | - assert not reported # but no report | 196 | + 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_profiling | 199 | 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() # untouched | 212 | + assert (tmp_path / "instr.bin").exists() # untouched |
| 212 | assert not reported | 213 | 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 reported | 230 | assert not reported |
| 230 | - assert (tmp_path / "instr.bin").exists() # empty file left in place | 231 | + 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 None | 236 | 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 torch | 336 | 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 torch | 366 | 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_format | 568 | 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 | ||
| 577 | def test_incomplete_high_priority_format_does_not_fall_back(tmp_path): | 573 | def 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_dir | 617 | 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 | -) | ||
| 627 | def test_missing_data_slot_is_rejected(tmp_path, filename): | 621 | def 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 | ||
| 814 | def test_prepare_and_replay_helpers_share_store_policy(tmp_path): | 812 | def 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): | |||
| 847 | def test_registered_provider_is_an_extension_point_for_future_csv_sources(tmp_path): | 841 | def 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 | ||
| 900 | def test_provider_replay_obeys_device_stage_constraints(tmp_path): | 891 | def 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_path | 896 | return tmp_path |
| @@ -915,15 +904,15 @@ def test_provider_replay_obeys_device_stage_constraints(tmp_path): | |||
| 915 | 904 | ||
| 916 | 905 | ||
| 917 | def test_e2e_restore_rebuilds_view_without_running_input_plugin(): | 906 | def 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][:] = -1 | 102 | inputs[0][:] = -1 |
| 102 | return golden | 103 | 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, result | 112 | + 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, result | 145 | + 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 = restore | 172 | 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 == 1 | 194 | 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: None | 240 | + 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 | |||
| 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 | + | ||
| 286 | def run_with_switches(sw): | 338 | def run_with_switches(sw): |
| 287 | from ttk.core_modules.tbe_logging import default_logging_config | 339 | from ttk.core_modules.tbe_logging import default_logging_config |
| 288 | from ttk.utilities import set_global_storage | 340 | 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 FrameworkApiInstance | 354 | 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 GeirInstance | 359 | 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 | ||
| 12 | def register_e2e_command(subparsers): | 12 | def 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 | """ |
| 13 | Resolve api_name string to callable object. | 13 | Resolve api_name string to callable object. |
| 14 | Handles both module functions (torch.add) and Tensor methods (torch.Tensor.relu_). | 14 | Handles 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 | ||
| 17 | from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader | 18 | from 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 method | 49 | # torch.Tensor.xxx -> Tensor method |
| @@ -13,12 +13,7 @@ import importlib | |||
| 13 | import logging | 13 | import logging |
| 14 | from typing import Optional | 14 | from typing import Optional |
| 15 | 15 | ||
| 16 | -import torch | ||
| 17 | - | ||
| 18 | from .base import Backend | 16 | from .base import Backend |
| 19 | -from .npu_backend import NpuTorchBackend | ||
| 20 | -from .xpu_backend import XpuTorchBackend | ||
| 21 | -from .cpu_backend import CpuTorchBackend | ||
| 22 | from ....config.loader import get_hardware_config | 17 | from ....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 | ||
| 58 | def _build(framework: str, name: str, profile: dict) -> Backend: | 51 | def _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 = NpuTorchBackend | 67 | cls = NpuTorchBackend |
| 73 | elif torch_lib == "cpu": | 68 | elif torch_lib == "cpu": |
| 69 | + from .cpu_torch_backend import CpuTorchBackend | ||
| 70 | + | ||
| 74 | cls = CpuTorchBackend | 71 | cls = CpuTorchBackend |
| 75 | else: # mlu / musa / other -> generic accelerator | 72 | else: # mlu / musa / other -> generic accelerator |
| 73 | + from .xpu_torch_backend import XpuTorchBackend | ||
| 74 | + | ||
| 76 | cls = XpuTorchBackend | 75 | cls = XpuTorchBackend |
| 77 | b = cls() | 76 | b = cls() |
| 78 | b.torch_lib = torch_lib | 77 | 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 False | 98 | 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 False | 112 | 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 | ||
| 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 | continue | 155 | 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 @@ | |||
| 12 | from abc import ABC, abstractmethod | 12 | from abc import ABC, abstractmethod |
| 13 | 13 | ||
| 14 | import numpy as np | 14 | import numpy as np |
| 15 | -import torch | ||
| 16 | 15 | ||
| 17 | 16 | ||
| 18 | class Backend(ABC): | 17 | class Backend(ABC): |
| @@ -31,7 +30,7 @@ class Backend(ABC): | |||
| 31 | CpuTorchBackend (never goes through _build) sets the | 30 | 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 本次重构将 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,完全无法运行。请同步更新测试。 ![]() ![]() | |||
| 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 yaml | 58 | 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 | - @abstractmethod | 72 | + def to_device(self, tensor, dev_id: int = 0, preserve_stride: bool = False): |
| 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 | 80 | ||
| 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 | - @abstractmethod | 88 | + 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 | 95 | ||
| 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"]}} | ||
| 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 | ||
| @@ -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_name | 37 | + 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 True | 40 | return True |
| @@ -42,7 +42,9 @@ class CpuTorchBackend(TorchBackend): | |||
| 42 | def device_count(self) -> int: | 42 | def device_count(self) -> int: |
| 43 | return 1 | 43 | 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 False | 57 | 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): | ||
| 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): | ||
| 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() | ||
| 148 | + except Exception as e: | ||
| 149 | + logging.warning(f"Failed to set TF deterministic: {e}") | ||
| @@ -13,7 +13,8 @@ import numpy as np | |||
| 13 | 13 | ||
| 14 | from .torch_backend import TorchBackend | 14 | from .torch_backend import TorchBackend |
| 15 | from ....utilities import ( | 15 | from ....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 True | 36 | 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.name | 42 | + |
| 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_tensor | 57 | 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): | ||||||||
| 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" | ||||||||
🟡 Medium Priority
影响:如果某个子类未覆盖这两个方法且 建议:让 改动建议
![]() ![]() | |||||||||
| 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_",)) | ||||||||
| 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 tensors | 61 | + 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 True | 67 | 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
![]() ![]() | |||
| 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) | ||
| @@ -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. |
| 9 | Validates testcase parameters against API signatures. | 9 | Validates testcase parameters against API signatures. |
| 10 | """ | 10 | """ |
| 11 | 11 | ||
| @@ -13,14 +13,11 @@ import logging | |||
| 13 | from typing import Optional, Dict | 13 | from typing import Optional, Dict |
| 14 | 14 | ||
| 15 | from ttk.utilities import Singleton | 15 | from 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 | -) | ||
| 19 | from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader | 17 | from ttk.utilities.torch_ops_package_loader import TorchOpsPackageLoader |
| 20 | 18 | ||
| 21 | 19 | ||
| 22 | class FrameworkApiInfoKeeper(metaclass=Singleton): | 20 | class 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 = None | 37 | 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 None | 58 | return None |
| 58 | api_tensor_count = info.tensor_count | 59 | api_tensor_count = info.tensor_count |
| 59 | api_scalar_count = info.scalar_count | 60 | 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 None | 67 | 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: | |||
| 18 | All paths use testcase.get_param_plan() to get consistent arg ordering | 18 | All paths use testcase.get_param_plan() to get consistent arg ordering |
| 19 | with profiling.py — same plan, same (*args, **kwargs) layout. | 19 | with profiling.py — same plan, same (*args, **kwargs) layout. |
| 20 | """ | 20 | """ |
| 21 | + | ||
| 21 | import logging | 22 | import logging |
| 22 | 23 | ||
| 23 | from ttk.core_modules.testcase_manager.param_plan import build_positional_args | 24 | from 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 | |||
| 25 | from ttk.utilities.container_utils import apply_as_list, flatten_nested_sequence | 26 | from ttk.utilities.container_utils import apply_as_list, flatten_nested_sequence |
| 26 | 27 | ||
| 27 | from .api_resolver import resolve_api | 28 | from .api_resolver import resolve_api |
| 28 | -from .backends.cpu_backend import CpuTorchBackend as CpuBackend | 29 | +from .framework_detector import detect_framework |
| 29 | from .framework_api_info_keeper import FrameworkApiInfoKeeper | 30 | from .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_dist | 64 | dist = testcase.tensor_list_dist |
| 50 | api_name = testcase.api_name | 65 | 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 e | 103 | ) 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 None | 112 | + 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_index | 133 | 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_axis | 163 | + 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_info | 165 | + 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_seed | 167 | + extra["batch_seed"] = testcase.batch_seed |
| 144 | 168 | ||
| 145 | import inspect | 169 | 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_api | 182 | 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 @@ | |||
| 13 | Graph mode execution for framework_api. | 13 | Graph mode execution for framework_api. |
| 14 | Wraps API in GraphNetwork + torch.compile for GE graph mode testing. | 14 | Wraps API in GraphNetwork + torch.compile for GE graph mode testing. |
| 15 | """ | 15 | """ |
| 16 | + | ||
| 16 | import functools | 17 | import functools |
| 17 | import logging | 18 | import logging |
| 18 | 19 | ||
| @@ -22,7 +23,7 @@ from ttk.test_spec import get_spec_attr | |||
| 22 | 23 | ||
| 23 | from .graph_network import GraphNetwork, split_params | 24 | from .graph_network import GraphNetwork, split_params |
| 24 | from .profiler import get_profiler | 25 | from .profiler import get_profiler |
| 25 | -from .profiling_utils import clone_preserving_stride, prepare_device_args, result_to_numpy | 26 | +from .profiling_utils import prepare_device_args |
| 26 | 27 | ||
| 27 | WARMUP_COUNT = 5 | 28 | WARMUP_COUNT = 5 |
| 28 | 29 | ||
| @@ -32,15 +33,18 @@ def _get_npu_backend(): | |||
| 32 | import torch_npu | 33 | import torch_npu |
| 33 | import torchair | 34 | import torchair |
| 34 | from torchair.configs.compiler_config import CompilerConfig | 35 | 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 | 41 | ||
| 39 | def _get_npu_backend_aclgraph(): | 42 | def _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_backend | 45 | return npu_backend |
| 43 | 46 | ||
| 47 | + | ||
| 44 | def _compile_model(model, backend, dynamic, fullgraph): | 48 | def _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 compiled | 56 | return compiled |
| 53 | 57 | ||
| 58 | + | ||
| 54 | def _compile_model_aclgraph(model, backend): | 59 | def _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 compiled | 67 | 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_time | 84 | run_count = switches.run_time |
| 69 | is_kwargs_mode = inplace_kwargs_keys is not None | 85 | 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, perf | 164 | 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 [], None | 200 | return [], None |
| 173 | 201 | ||
| 174 | import torch_npu | 202 | 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 = None | 216 | 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 = None | 227 | 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_kwargs | 232 | run_args, run_kwargs = [], fwd_kwargs |
| 205 | run_inplace = is_inplace | 233 | 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 None | 245 | + 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. | |||
| 15 | Generates numpy arrays from testcase metadata, applies custom plugin overrides, | 15 | Generates numpy arrays from testcase metadata, applies custom plugin overrides, |
| 16 | and converts to framework tensors (torch). | 16 | and converts to framework tensors (torch). |
| 17 | """ | 17 | """ |
| 18 | + | ||
| 18 | import numpy as np | 19 | import numpy as np |
| 19 | 20 | ||
| 20 | from ttk.core_modules.plugin_loader import get_plugin_function | 21 | from 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_inputs | 46 | 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_path | 51 | 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_dist | 56 | 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_inputs | 60 | 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_axis | 74 | + 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_info | 76 | + 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_seed | 78 | + extra["batch_seed"] = testcase.batch_seed |
| 83 | 79 | ||
| 84 | import inspect | 80 | 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_inputs | 90 | 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_dist | 96 | 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 torch | 110 | import torch |
| 118 | from ttk.utilities.dtypes import numpy_to_torch_tensor | 111 | 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 None | 115 | + 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_shapes | 116 | flat_shapes = testcase.flat_tensor_view_shapes |
| 124 | flat_dtypes = testcase.flat_tensor_dtypes | 117 | flat_dtypes = testcase.flat_tensor_dtypes |
| 125 | result = [] | 118 | result = [] |
| @@ -140,6 +133,31 @@ def np_to_torch_inputs(testcase, raw_inputs): | |||
| 140 | return result | 133 | 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 | + | ||
| 143 | def assign_tensor_value(arr, val, label): | 161 | def 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 value | 180 | 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-one | 181 | 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 | continue | 201 | 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_shapes | 251 | 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 value | 267 | # 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_array | 272 | from ttk.utilities.data import fixed_np_array |
| 250 | - init_val = 0 if testcase.api_name in ("torch.ones",) else 1 | 273 | + |
| 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_storages | 277 | testcase.np_storages = np_storages |
| @@ -275,6 +299,7 @@ def build_views_from_storages(testcase): | |||
| 275 | def to_non_contiguous_view(storage, view_shape, view_stride, view_offset): | 299 | def 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_safe | 301 | from ttk.utilities.dtypes import np_as_strided_safe |
| 302 | + | ||
| 278 | dtype = storage.dtype | 303 | 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 | """ |
| 13 | FrameworkApiInstance — InstanceBase implementation for framework_api tests. | 13 | FrameworkApiInstance — InstanceBase implementation for framework_api tests. |
| 14 | """ | 14 | """ |
| 15 | + | ||
| 15 | import logging | 16 | import logging |
| 16 | import os | 17 | import 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 = True | 37 | 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 | 40 | ||
| 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. | |||
| 14 | Context manager pattern: profiler only collects data within `with` block. | 14 | Context manager pattern: profiler only collects data within `with` block. |
| 15 | Warmup and repeat logic is controlled by the caller. | 15 | Warmup and repeat logic is controlled by the caller. |
| 16 | """ | 16 | """ |
| 17 | + | ||
| 17 | import csv | 18 | import csv |
| 18 | import logging | 19 | import logging |
| 19 | import os | 20 | import os |
| 20 | -import shutil | ||
| 21 | -import tempfile | ||
| 22 | import time | 21 | import time |
| 23 | from abc import ABC, abstractmethod | 22 | from abc import ABC, abstractmethod |
| 24 | from dataclasses import dataclass, field | 23 | from 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 = None | 77 | 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 self | 105 | 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 None | 139 | 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_us | 289 | 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 | + | ||
| 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'] rather | 455 | 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 | |||
| 41 | from .api_resolver import resolve_api | 41 | from .api_resolver import resolve_api |
| 42 | from .backends import get_backend | 42 | from .backends import get_backend |
| 43 | from .eager_execution import call_api | 43 | from .eager_execution import call_api |
| 44 | -from .golden_generation import generate_golden | ||
| 45 | -from .graph_execution import _execute_graph | ||
| 46 | from .input_generation import generate_inputs | 44 | from .input_generation import generate_inputs |
| 47 | from .profiler import get_profiler | 45 | from .profiler import get_profiler |
| 48 | -from .profiling_utils import clone_preserving_stride, prepare_device_args, result_to_numpy | 46 | +from .profiling_utils import prepare_device_args |
| 49 | from .result import FrameworkApiReturnStructure | 47 | from .result import FrameworkApiReturnStructure |
| 50 | 48 | ||
| 51 | WARMUP_COUNT = 5 | 49 | WARMUP_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 cached | 284 | 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"] = backend | 287 | process_ctx.storage["framework_api_backend"] = backend |
| 287 | return backend | 288 | return backend |
| 288 | 289 | ||
| @@ -343,12 +344,8 @@ def _ensure_deterministic_level_e2e(process_ctx, backend, testcase): | |||
| 343 | return | 344 | return |
| 344 | if backend.is_npu(): | 345 | if backend.is_npu(): |
| 345 | try: | 346 | try: |
| 346 | - import torch_npu | 347 | + 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"] = True | 351 | 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_npu | 357 | 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_time | 363 | 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 None | 376 | + 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 = None | 386 | + 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_backup | 390 | + backend.restore_inplace(args[0], inplace_backup) |
| 387 | else: | 391 | else: |
| 388 | result = None | 392 | 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_backup | 397 | + 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][:] = backup | 399 | + 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 None | 401 | + 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][:] = backup | 408 | + 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_backup | 410 | + 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 None | 431 | + 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 = r | 436 | 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, kwargs | 452 | 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 | return | 589 | 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 the | 595 | # 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 left | 596 | # 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 | return | 599 | 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_report | 601 | 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 = None | 696 | result_nps = None |
| 688 | perf = None | 697 | perf = None |
| 689 | graph_cst_nps = None | 698 | 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_inputs | 717 | 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 python3 | 1 | #!/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 of | 4 | +# 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 | """ |
| 13 | Profiling utility functions shared between profiling.py and graph_execution.py. | 11 | Profiling 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 | + | ||
| 15 | import numpy as np | 16 | import numpy as np |
| 16 | 17 | ||
| 17 | 18 | ||
| @@ -32,99 +33,36 @@ def apply_format_cast(tensors, formats): | |||
| 32 | return result | 33 | 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 | - | ||
| 99 | def prepare_device_args(testcase, backend, dev_id, plan, raw_inputs): | 36 | def 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 tensors | 40 | + 1. Convert raw_inputs to device tensors (preserving stride for torch) |
| 104 | 2. Apply NPU format cast if needed | 41 | 2. Apply NPU format cast if needed |
| 105 | 3. Apply tensor list distribution if needed | 42 | 3. Apply tensor list distribution if needed |
| 106 | 4. Build args/kwargs using plan | 43 | 4. Build args/kwargs using plan |
| 107 | - | 44 | + |
| 108 | Args: | 45 | Args: |
| 109 | testcase: TestcaseE2e | 46 | testcase: TestcaseE2e |
| 110 | backend: Backend instance | 47 | backend: Backend instance |
| 111 | dev_id: device ID | 48 | dev_id: device ID |
| 112 | plan: ParamPlan | 49 | plan: ParamPlan |
| 113 | raw_inputs: numpy input arrays | 50 | raw_inputs: numpy input arrays |
| 114 | - | 51 | + |
| 115 | Returns: | 52 | Returns: |
| 116 | tuple: (args, kwargs) ready for API call | 53 | tuple: (args, kwargs) ready for API call |
| 117 | """ | 54 | """ |
| 118 | from ttk.utilities.container_utils import apply_as_list | 55 | 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_tensors | 59 | flat_tensors = testcase.flatten_tensors |
| 123 | - dev_tensors = [_to_device_preserving_stride(t, backend, dev_id) if t is not None else None | 60 | + 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_dist | 67 | 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: | ||
| 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 | + | ||
| 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] | ||
| 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 | + | ||
| 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 | ||
| 213 | def profile_process( | 212 | def 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 = None | 231 | manual_case = None |
| 231 | try: | 232 | try: |
| @@ -378,7 +379,7 @@ def profile_process( | |||
| 378 | # Following actions need to acquire global lock | 379 | # 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_sim | 395 | 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_axis | 102 | 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.tensors | 121 | tensor_names = op_api_info.tensors |
| 122 | - pure_output_names = {tensor_names[i] for i in self._ctx.pure_output_indexes | 122 | + 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 @@ | |||
| 11 | Input generation method for Universal testcases | 11 | Input 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 | |||
| 28 | from ....utilities import get, get_global_storage, RandomData | 27 | from ....utilities import get, get_global_storage, RandomData |
| 29 | from ...plugin_loader import get_plugin_function | 28 | from ...plugin_loader import get_plugin_function |
| 30 | 29 | ||
| 30 | + | ||
| 31 | class InputGenerator: | 31 | class InputGenerator: |
| 32 | def __init__(self, context: TestcaseAclnn): | 32 | def __init__(self, context: TestcaseAclnn): |
| 33 | self._ctx = context | 33 | 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 operators | 59 | # 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 | continue | 78 | 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_axis | 96 | + 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_info | 98 | + 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_seed | 100 | + 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_shapes | 112 | 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 output | 126 | # 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 value | 128 | # 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_array | 135 | from ttk.utilities.data import fixed_np_array |
| 136 | + | ||
| 142 | init_val = 0 if self._ctx.api_name in ("aclnnInplaceOne",) else 1 | 137 | 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 torch | 161 | import torch |
| 162 | + | ||
| 168 | torch_tensors = [] | 163 | torch_tensors = [] |
| 169 | flat_shapes = self._ctx.flat_tensor_view_shapes | 164 | 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 | raise | 184 | 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.scalars | 196 | 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_safe | 220 | from ttk.utilities.dtypes import np_as_strided_safe |
| 221 | + | ||
| 224 | np_views = [] | 222 | np_views = [] |
| 225 | flat_shapes = self._ctx.flat_tensor_view_shapes | 223 | 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 | raise | 249 | 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.scalars | 254 | 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_indexes | 555 | + return [ |
| 556 | - if i < len(op_api_info.tensors)} | 556 | + name |
| 557 | - return [name for name in op_api_info.params | 557 | + 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 | ||
| 561 | def __dump_to_file(data, file_name: str, dtype: Optional[str] = None): | 562 | def __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 test | 620 | # 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 lock | 698 | # 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 | + | ||
| 22 | import ast | 23 | import ast |
| 23 | import logging | 24 | import logging |
| 24 | import re | 25 | import re |
| @@ -27,9 +28,9 @@ from ttk.utilities.dtypes import str_to_torch_dtype | |||
| 27 | 28 | ||
| 28 | 29 | ||
| 29 | def safe_eval_division(s): | 30 | def safe_eval_division(s): |
| 30 | - if '/' not in s: | 31 | + if "/" not in s: |
| 31 | return None | 32 | return None |
| 32 | - parts = s.split('/') | 33 | + parts = s.split("/") |
| 33 | if len(parts) != 2: | 34 | if len(parts) != 2: |
| 34 | return None | 35 | return None |
| 35 | try: | 36 | try: |
| @@ -62,30 +63,30 @@ def score_attr_type_compatibility(overload_params, attr_values): | |||
| 62 | """ | 63 | """ |
| 63 | score = 0 | 64 | 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 | continue | 67 | continue |
| 67 | if p.name not in attr_values: | 68 | if p.name not in attr_values: |
| 68 | continue | 69 | continue |
| 69 | raw = attr_values[p.name] | 70 | raw = attr_values[p.name] |
| 70 | ptype = p.type | 71 | ptype = p.type |
| 71 | 72 | ||
| 72 | - if '|' in ptype: | 73 | + if "|" in ptype: |
| 73 | score += 1 | 74 | score += 1 |
| 74 | continue | 75 | 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 += 1 | 79 | 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 += 1 | 82 | score += 1 |
| 82 | - elif ptype == 'bool': | 83 | + elif ptype == "bool": |
| 83 | if isinstance(raw, bool): | 84 | if isinstance(raw, bool): |
| 84 | score += 1 | 85 | score += 1 |
| 85 | - elif ptype == 'str': | 86 | + elif ptype == "str": |
| 86 | if isinstance(raw, str): | 87 | if isinstance(raw, str): |
| 87 | score += 1 | 88 | 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 += 1 | 91 | 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.params | 123 | overload_params = ov_info.params |
| 123 | - input_tensors = [p for p in overload_params | 124 | + 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_tensors | 127 | + 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_cover | 128 | 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 = True | 138 | type_ok = True |
| 141 | - non_var_tensors = [p for p in input_tensors | 139 | + 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 | continue | 152 | 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 None | 192 | return None |
| 197 | - if isinstance(raw, str) and raw == 'None': | 193 | + if isinstance(raw, str) and raw == "None": |
| 198 | return None | 194 | return None |
| 199 | - if type(raw).__module__ != 'builtins': | 195 | + if type(raw).__module__ != "builtins": |
| 200 | return raw | 196 | return raw |
| 201 | - if target_type == 'bool': | 197 | + if target_type == "bool": |
| 202 | if isinstance(raw, bool): | 198 | if isinstance(raw, bool): |
| 203 | return raw | 199 | 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 raw | 205 | 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 e | 219 | 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 raw | 222 | 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 result | 231 | 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 obj | 245 | 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 e | 264 | 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 raw | 268 | 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 e | 285 | 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 torch | 288 | import torch |
| 289 | - attr_name = raw.split('.', 1)[1] if '.' in raw else raw | 289 | + |
| 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 obj | 293 | 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 torch | 295 | 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 obj | 299 | 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 raw | 302 | 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_name | 328 | self.api_name = api_name |
| 325 | self.overload_params = overload_params | 329 | self.overload_params = overload_params |
| 326 | self.overload_index = overload_index | 330 | 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'] = collected | 365 | + 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'] = val | 371 | + 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] = val | 382 | 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_attrs | 419 | 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, oidx | 451 | return args, kwargs, oidx |
| @@ -9,6 +9,7 @@ | |||
| 9 | """ | 9 | """ |
| 10 | Testcase structure for framework_api tests. | 10 | Testcase structure for framework_api tests. |
| 11 | """ | 11 | """ |
| 12 | + | ||
| 12 | import logging | 13 | import logging |
| 13 | 14 | ||
| 14 | from ttk.core_modules.testcase_manager.testcase_tensor_api_base import TensorApiTestcaseBase | 15 | from 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 = None | 130 | 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_count | 149 | 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_dist | 155 | dist = self.tensor_list_dist |
| 153 | tensor_distribution = [d > 0 for d in dist] if dist else None | 156 | 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 None | 166 | 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 = plan | 175 | self._param_plan_cache = plan |
| 170 | return plan | 176 | 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 None | 349 | 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 info | 354 | return info |
| @@ -353,11 +358,9 @@ class TestcaseE2e(TensorApiTestcaseBase): | |||
| 353 | 358 | ||
| 354 | 359 | ||
| 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 False | 362 | + |
| 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 = None | 378 | self.batch_consistency_id = None |
| 376 | return | 379 | 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 = None | 382 | self.batch_consistency_id = None |
| 380 | return | 383 | 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 | continue | 388 | 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 = 0 | 403 | length = 0 |
| 401 | else: | 404 | else: |
| 402 | - length = stop -start if stop > start else 0 | 405 | + 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 accept | 442 | # 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 False | 449 | 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 = False | 452 | 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 True | 460 | return True |
| 458 | return False | 461 | 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 == 0 | 469 | 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 False | 480 | 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 = False | 485 | 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 True | 493 | return True |
| 490 | 494 | ||
| 491 | 495 | ||
| @@ -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) > 0 | 505 | + 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_none | 510 | 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 = 0 | 519 | 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 += 1 | 522 | ov_count += 1 |
| 518 | count = max(count, ov_count) | 523 | count = max(count, ov_count) |
| 519 | return count | 524 | 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 | return | 547 | 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_count | 553 | 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 = False | 565 | 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 = False | 574 | 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.params | 591 | + p.name |
| 592 | + for p in ov.params | ||
| 585 | if not p.is_tensor_like | 593 | if not p.is_tensor_like |
| 586 | and not p.is_keyword_only | 594 | and not p.is_keyword_only |
| 587 | - and p.name != 'out' | 595 | + and p.name != "out" |
| 588 | and not p.is_optional | 596 | and not p.is_optional |
| 589 | and p.name not in attrs | 597 | 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_count | 604 | + 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 | continue | 612 | 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 | continue | 617 | continue |
| 608 | alt_missing = [ | 618 | alt_missing = [ |
| 609 | - p.name for p in alt_ov.params | 619 | + p.name |
| 620 | + for p in alt_ov.params | ||
| 610 | if not p.is_tensor_like | 621 | if not p.is_tensor_like |
| 611 | and not p.is_keyword_only | 622 | and not p.is_keyword_only |
| 612 | - and p.name != 'out' | 623 | + and p.name != "out" |
| 613 | and not p.is_optional | 624 | and not p.is_optional |
| 614 | and p.name not in attrs | 625 | 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 = False | 653 | 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 | return | 660 | 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 = False | 664 | 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 | return | 667 | return |
| 658 | if info is None: | 668 | if info is None: |
| 659 | return | 669 | return |
| @@ -683,26 +693,25 @@ class TestcaseE2e(TensorApiTestcaseBase): | |||
| 683 | continue | 693 | 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_count | 699 | + 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 = False | 700 | 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 = False | 708 | 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 | return | 728 | 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 = False | 730 | 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 | return | 736 | 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 = False | 738 | 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 @@ | |||
| 8 | Shared base class for aclnn (op_api) and framework_api (e2e) testcase structures. | 8 | Shared 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 | ||
| 15 | from .testcase_base import TestcaseBase | 14 | from .testcase_base import TestcaseBase |
| 16 | from ...utilities import get, shape_stride | 15 | from ...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 | ||
| 22 | class TensorApiTestcaseBase(TestcaseBase): | 19 | class 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 = None | 99 | self._flat_absolute_precision = None |
| 98 | self._pure_output_indexes = None | 100 | self._pure_output_indexes = None |
| 99 | self._is_torch_dtype_support = None | 101 | self._is_torch_dtype_support = None |
| 102 | + self._is_tf_dtype_support = None | ||
| 103 | + self.const_input_indexes = set() | ||
| 100 | 104 | ||
| 101 | 105 | ||
| 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 | 121 | ||
| @@ -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_dist | 156 | 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_dtypes | 160 | self._flat_tensor_dtypes = self.tensor_dtypes |
| 159 | return self._flat_tensor_dtypes | 161 | 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_dist | 185 | 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_formats | 189 | self._flat_tensor_formats = self.tensor_formats |
| 189 | return self._flat_tensor_formats | 190 | 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_dist | 201 | 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_shapes | 205 | self._flat_tensor_storage_shapes = self.tensor_storage_shapes |
| 206 | return self._flat_tensor_storage_shapes | 206 | 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_dist | 216 | 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_offsets | 220 | self._flat_tensor_view_offsets = self.tensor_view_offsets |
| 222 | return self._flat_tensor_view_offsets | 221 | 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_dist | 232 | 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_strides | 236 | self._flat_tensor_view_strides = self.tensor_view_strides |
| 239 | return self._flat_tensor_view_strides | 237 | return self._flat_tensor_view_strides |
| @@ -251,8 +249,7 @@ class TensorApiTestcaseBase(TestcaseBase): | |||
| 251 | return self.input_data_ranges | 249 | return self.input_data_ranges |
| 252 | dist = self.tensor_list_dist | 250 | 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_ranges | 254 | self._flat_input_data_ranges = self.input_data_ranges |
| 258 | return self._flat_input_data_ranges | 255 | return self._flat_input_data_ranges |
| @@ -266,8 +263,7 @@ class TensorApiTestcaseBase(TestcaseBase): | |||
| 266 | return self.precision_tolerances | 263 | return self.precision_tolerances |
| 267 | odist = self.output_dist | 264 | 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_tolerances | 268 | self._flat_precision_tolerances = self.precision_tolerances |
| 273 | return self._flat_precision_tolerances | 269 | return self._flat_precision_tolerances |
| @@ -281,8 +277,7 @@ class TensorApiTestcaseBase(TestcaseBase): | |||
| 281 | return self.absolute_precision | 277 | return self.absolute_precision |
| 282 | odist = self.output_dist | 278 | 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_precision | 282 | self._flat_absolute_precision = self.absolute_precision |
| 288 | return self._flat_absolute_precision | 283 | 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_support | 301 | return self._is_torch_dtype_support |
| 307 | from ttk.utilities.dtypes import is_torch_native_dtype | 302 | from ttk.utilities.dtypes import is_torch_native_dtype |
| 303 | + | ||
| 308 | result = True | 304 | 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 = result | 309 | self._is_torch_dtype_support = result |
| 314 | return result | 310 | return result |
| 315 | 311 | ||
| 312 | + def is_tf_dtype_support(self) -> bool: | ||
| 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_indexes | 377 | return self._pure_output_indexes |
| 359 | dist = self.tensor_list_dist | 378 | 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 1 | 382 | 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_dist | 398 | 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 | 403 | ||
| 385 | def _flatten_by_distribution(values, distribution): | 404 | def _flatten_by_distribution(values, distribution): |
| @@ -28,7 +28,7 @@ class AbsKernelSpec: | |||
| 28 | class AclnnAbsSpec: | 28 | class 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 False | 44 | 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 = False | 260 | 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: 0 | 68 | + None: 0, |
| 69 | } | 69 | } |
| 70 | 70 | ||
| 71 | dtype_map = { | 71 | dtype_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 | ||
| 168 | DATA_TYPE_DICT = { | 144 | DATA_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 | ||
| 209 | DATA_TYPE_INT_TO_STR = { | 185 | DATA_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 | ||
| 248 | def str_to_torch_dtype(dtype_str: str): | 224 | def 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_str | 234 | 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 None | 237 | 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 torch | 242 | import torch |
| 243 | + | ||
| 267 | return getattr(torch, canonical, None) | 244 | return getattr(torch, canonical, None) |
| 268 | else: | 245 | else: |
| 269 | import torch_npu | 246 | import torch_npu |
| 247 | + | ||
| 270 | npu_attr_name = "float8_e8m0fnu" if canonical == "float8_e8m0" else canonical | 248 | 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 None | 252 | return None |
| 275 | try: | 253 | try: |
| 276 | - if module == 'torch': | 254 | + if module == "torch": |
| 277 | import torch | 255 | import torch |
| 278 | else: | 256 | else: |
| 279 | import torch_npu | 257 | import torch_npu |
| @@ -343,10 +321,10 @@ def numpy_int4(): | |||
| 343 | try: | 321 | try: |
| 344 | # noinspection PyUnresolvedReferences | 322 | # noinspection PyUnresolvedReferences |
| 345 | from ml_dtypes import int4 | 323 | from ml_dtypes import int4 |
| 324 | + | ||
| 346 | return int4 | 325 | 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 | ||
| 352 | def numpy_bfloat16(): | 330 | def numpy_bfloat16(): |
| @@ -357,11 +335,14 @@ def numpy_bfloat16(): | |||
| 357 | try: | 335 | try: |
| 358 | # noinspection PyUnresolvedReferences | 336 | # noinspection PyUnresolvedReferences |
| 359 | import tensorflow | 337 | import tensorflow |
| 338 | + | ||
| 360 | bfloat16 = tensorflow.bfloat16.as_numpy_dtype | 339 | 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 numpy | 346 | # some older TF version (v1.15.0) bfp16 needs to convert to fp32 to calculate in numpy |
| 366 | global BFP16_NEEDS_FP32_FOR_NPY | 347 | 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 PyUnresolvedReferences | 363 | # noinspection PyUnresolvedReferences |
| 383 | from ml_dtypes import float8_e5m2 | 364 | from ml_dtypes import float8_e5m2 |
| 365 | + | ||
| 384 | return float8_e5m2 | 366 | 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 | ||
| 390 | def numpy_float8_e4m3fn(): | 373 | def numpy_float8_e4m3fn(): |
| 391 | try: | 374 | try: |
| 392 | # noinspection PyUnresolvedReferences | 375 | # noinspection PyUnresolvedReferences |
| 393 | from ml_dtypes import float8_e4m3fn | 376 | from ml_dtypes import float8_e4m3fn |
| 377 | + | ||
| 394 | return float8_e4m3fn | 378 | 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 | ||
| 400 | def ensure_en_dtypes_version(version): | 385 | def ensure_en_dtypes_version(version): |
| 401 | import en_dtypes | 386 | 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 | return | 391 | return |
| 406 | else: | 392 | else: |
| @@ -411,43 +397,52 @@ def numpy_float8_e8m0(): | |||
| 411 | try: | 397 | try: |
| 412 | # noinspection PyUnresolvedReferences | 398 | # noinspection PyUnresolvedReferences |
| 413 | from en_dtypes import float8_e8m0 | 399 | 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_e8m0 | 402 | 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 | ||
| 421 | def numpy_float4_e2m1(): | 409 | def numpy_float4_e2m1(): |
| 422 | try: | 410 | try: |
| 423 | # noinspection PyUnresolvedReferences | 411 | # noinspection PyUnresolvedReferences |
| 424 | from en_dtypes import float4_e2m1 | 412 | 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_e2m1 | 415 | 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 | ||
| 432 | def numpy_float4_e1m2(): | 422 | def numpy_float4_e1m2(): |
| 433 | try: | 423 | try: |
| 434 | # noinspection PyUnresolvedReferences | 424 | # noinspection PyUnresolvedReferences |
| 435 | from en_dtypes import float4_e1m2 | 425 | 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_e1m2 | 428 | 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 | ||
| 442 | def numpy_hifloat4(): | 435 | def numpy_hifloat4(): |
| 443 | try: | 436 | try: |
| 444 | # noinspection PyUnresolvedReferences | 437 | # noinspection PyUnresolvedReferences |
| 445 | from en_dtypes import float4_e1m2 | 438 | 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_e1m2 | 441 | 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 | ||
| 453 | def IsRoundOne(sign, man, truncLen): | 448 | def IsRoundOne(sign, man, truncLen): |
| @@ -456,7 +451,7 @@ def IsRoundOne(sign, man, truncLen): | |||
| 456 | mask0 = 0 | 451 | mask0 = 0 |
| 457 | else: | 452 | else: |
| 458 | mask0 = 0x1 << truncLen | 453 | mask0 = 0x1 << truncLen |
| 459 | - if (truncLen > roundingTruncLen): | 454 | + if truncLen > roundingTruncLen: |
| 460 | mask1 = 0 | 455 | 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 - 1 | 459 | mask2 = mask1 - 1 |
| 465 | 460 | ||
| 466 | # ROUND_TO_NEAREST | 461 | # ROUND_TO_NEAREST |
| 467 | - lastBit = (man & mask0) > 0 # Last bit after conversion | 462 | + lastBit = (man & mask0) > 0 # Last bit after conversion |
| 468 | - truncHighBit = (man & mask1) > 0 # Highest bit in the truncated part | 463 | + 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 | + | ||
| 472 | def float_to_hex(f): | 468 | def 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 | ||
| 475 | def cvt_bfloat16_to_fp4_e2m1(x): | 472 | def cvt_bfloat16_to_fp4_e2m1(x): |
| 476 | import math | 473 | import math |
| 474 | + | ||
| 477 | sRet = 0 | 475 | sRet = 0 |
| 478 | if x < 0.0: | 476 | if x < 0.0: |
| 479 | sRet = 1 | 477 | 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 >> 16 | 481 | x = x >> 16 |
| 484 | 482 | ||
| 485 | - ef = (x >> 7) & 0xff | 483 | + ef = (x >> 7) & 0xFF |
| 486 | - mf = x & 0x7f | 484 | + mf = x & 0x7F |
| 487 | - mLenDelta = 7 - 1 # | 485 | + mLenDelta = 7 - 1 # |
| 488 | - maxExp = 3 # max E encoding value of e2m1 is 3 | 486 | + maxExp = 3 # max E encoding value of e2m1 is 3 |
| 489 | - expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1 | 487 | + expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1 |
| 490 | eRet = 0 | 488 | eRet = 0 |
| 491 | mRet = 0 | 489 | mRet = 0 |
| 492 | eNorm = 0 | 490 | 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 126 | 492 | + 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 127 | 494 | + 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 = 0 | 499 | eRet = 0 |
| 502 | - mf = (mf | 0x80) | 500 | + mf = mf | 0x80 |
| 503 | mLenDelta -= eNorm + expBias - 1 | 501 | mLenDelta -= eNorm + expBias - 1 |
| 504 | - needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry | 502 | + 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+=1 | 505 | + 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+=1 | 511 | + mRet += 1 |
| 514 | 512 | ||
| 515 | - if (((mRet & 0b10) != 0) and (needRound)) : | 513 | + if ((mRet & 0b10) != 0) and (needRound): |
| 516 | - eRet+=1 | 514 | + eRet += 1 |
| 517 | mRet = 0 | 515 | mRet = 0 |
| 518 | 516 | ||
| 519 | - if (eRet >= 3) : | 517 | + if eRet >= 3: |
| 520 | eRet = 3 | 518 | eRet = 3 |
| 521 | - elif (eRet == 0 and mRet == 0b10) : | 519 | + elif eRet == 0 and mRet == 0b10: |
| 522 | - eRet+=1 | 520 | + eRet += 1 |
| 523 | mRet = 0 | 521 | mRet = 0 |
| 524 | 522 | ||
| 525 | - return (((sRet) << 3) | ((eRet) << 1) | ((mRet) & 1)) | 523 | + return ((sRet) << 3) | ((eRet) << 1) | ((mRet) & 1) |
| 526 | 524 | ||
| 527 | 525 | ||
| 528 | def cvt_bfloat16_to_fp4_e1m2(x): | 526 | def cvt_bfloat16_to_fp4_e1m2(x): |
| 529 | import math | 527 | import math |
| 528 | + | ||
| 530 | sRet = 0 | 529 | sRet = 0 |
| 531 | if x < 0.0: | 530 | if x < 0.0: |
| 532 | sRet = 1 | 531 | 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 >> 16 | 535 | x = x >> 16 |
| 537 | 536 | ||
| 538 | - ef = x >> 7 & 0xff | 537 | + ef = x >> 7 & 0xFF |
| 539 | - mf = x & 0x7f | 538 | + mf = x & 0x7F |
| 540 | - mLenDelta = 7 - 2 # | 539 | + mLenDelta = 7 - 2 # |
| 541 | - maxExp = 1 # max E encoding value of e1m2 is 3 | 540 | + maxExp = 1 # max E encoding value of e1m2 is 3 |
| 542 | - expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1 | 541 | + expBias = 1 # Exponent Bias value of e2m1/e1m2 is 1 |
| 543 | 542 | ||
| 544 | eRet = 0 | 543 | eRet = 0 |
| 545 | mRet = 0 | 544 | mRet = 0 |
| 546 | eNorm = 0 | 545 | 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 126 | 547 | + 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 127 | 549 | + 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 = 0 | 554 | eRet = 0 |
| 556 | - mf = (mf | 0x80) | 555 | + mf = mf | 0x80 |
| 557 | mLenDelta -= eNorm + expBias - 1 | 556 | mLenDelta -= eNorm + expBias - 1 |
| 558 | - needRound = IsRoundOne(sRet, mf, mLenDelta) # determine if need to carry | 557 | + 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+=1 | 560 | + 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+=1 | 566 | + mRet += 1 |
| 568 | - if (((mRet & 0b100) != 0) and (needRound)) : | 567 | + if ((mRet & 0b100) != 0) and (needRound): |
| 569 | - eRet+=1 | 568 | + eRet += 1 |
| 570 | mRet = 0 | 569 | mRet = 0 |
| 571 | 570 | ||
| 572 | - if (eRet >= 1) : | 571 | + if eRet >= 1: |
| 573 | eRet = 1 | 572 | eRet = 1 |
| 574 | - elif (eRet == 0 and mRet == 0b100) : | 573 | + elif eRet == 0 and mRet == 0b100: |
| 575 | - eRet+=1 | 574 | + eRet += 1 |
| 576 | mRet = 0 | 575 | mRet = 0 |
| 577 | 576 | ||
| 578 | - return (((sRet) << 3) | ((eRet) << 2) | ((mRet) & 3)) | 577 | + return ((sRet) << 3) | ((eRet) << 2) | ((mRet) & 3) |
| 578 | + | ||
| 579 | 579 | ||
| 580 | def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor): | 580 | def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor): |
| 581 | import numpy as np | 581 | import numpy as np |
| 582 | + | ||
| 582 | shape_tensor = in_tensor.shape | 583 | 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] // 2 | 595 | 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] # 按常规顺序保存b4 | 598 | # 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 0100 | 599 | + 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_tensor | 604 | return fp4_tensor |
| @@ -603,6 +606,7 @@ def trans_np_bfloat16_tensor_to_fp4_e2m1(in_tensor): | |||
| 603 | 606 | ||
| 604 | def trans_np_bfloat16_tensor_to_fp4_e1m2(in_tensor): | 607 | def trans_np_bfloat16_tensor_to_fp4_e1m2(in_tensor): |
| 605 | import numpy as np | 608 | import numpy as np |
| 609 | + | ||
| 606 | shape_tensor = in_tensor.shape | 610 | 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] // 2 | 622 | 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] # 按常规顺序保存b4 | 625 | # 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 0100 | 626 | + 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_tensor | 630 | return fp4_tensor |
| 625 | 631 | ||
| 632 | + | ||
| 626 | def cvt_fp4_e2m1_to_bfloat16(x): | 633 | def 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 & 0x0f | 654 | + first_fp4val = x & 0x0F |
| 634 | - second_fp4val = (x >> 4 )& 0x0f | 655 | + 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 | ||
| 641 | def cvt_fp4_e1m2_to_bfloat16(x): | 662 | def 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 & 0x0f | 683 | + first_fp4val = x & 0x0F |
| 649 | - second_fp4val = (x >> 4 )& 0x0f | 684 | + 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 | + | ||
| 655 | def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor): | 691 | def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor): |
| 656 | import numpy as np | 692 | import numpy as np |
| 693 | + | ||
| 657 | shape_tensor = in_tensor.shape | 694 | 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, 先拆成两个uint8 | 699 | # 1个uint8包含两个fp4, 先拆成两个uint8 |
| 663 | bfloat16_shape = list(shape_tensor) | 700 | bfloat16_shape = list(shape_tensor) |
| 664 | bfloat16_shape[-1] = bfloat16_shape[-1] * 2 | 701 | 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_tensor | 711 | return fp32_tensor |
| @@ -676,6 +713,7 @@ def trans_np_fp4_e1m2_tensor_to_bfloat16(in_tensor): | |||
| 676 | 713 | ||
| 677 | def trans_np_fp4_e2m1_tensor_to_bfloat16(in_tensor): | 714 | def trans_np_fp4_e2m1_tensor_to_bfloat16(in_tensor): |
| 678 | import numpy as np | 715 | import numpy as np |
| 716 | + | ||
| 679 | shape_tensor = in_tensor.shape | 717 | 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, 先拆成两个uint8 | 722 | # 1个uint8包含两个fp4, 先拆成两个uint8 |
| 685 | bfloat16_shape = list(shape_tensor) | 723 | bfloat16_shape = list(shape_tensor) |
| 686 | bfloat16_shape[-1] = bfloat16_shape[-1] * 2 | 724 | 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_tensor | 734 | return fp32_tensor |
| 697 | 735 | ||
| 736 | + | ||
| 698 | def numpy_hifloat8(): | 737 | def numpy_hifloat8(): |
| 699 | try: | 738 | try: |
| 700 | # noinspection PyUnresolvedReferences | 739 | # noinspection PyUnresolvedReferences |
| 701 | from en_dtypes import hifloat8 | 740 | from en_dtypes import hifloat8 |
| 702 | - ensure_en_dtypes_version('0.0.4') | 741 | + |
| 742 | + ensure_en_dtypes_version("0.0.4") | ||
| 703 | return hifloat8 | 743 | 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 | ||
| 712 | def resolve_custom_numpy_dtypes(container): | 755 | def 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 container | 761 | 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 = src | 796 | 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 arm | 800 | # 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 out | 802 | 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 | ||
| 785 | def normalize_to_tf_dtype(np_array: numpy.ndarray): | 831 | def normalize_to_tf_dtype(np_array: numpy.ndarray): |
| 786 | import tensorflow as tf | 832 | 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_array | 836 | return np_array |
| 791 | 837 | ||
| @@ -801,6 +847,7 @@ def tf_dtype_revert(tf_tensor): | |||
| 801 | def torch_dtype_conversion(container): | 847 | def torch_dtype_conversion(container): |
| 802 | # convert string dtype to torch dtype | 848 | # convert string dtype to torch dtype |
| 803 | import torch | 849 | 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 | ||
| 811 | def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False): | 858 | def numpy_to_torch_tensor(np_array: numpy.ndarray, is_complex32: bool = False): |
| 812 | import torch | 859 | import torch |
| 860 | + | ||
| 813 | if np_array is None: | 861 | if np_array is None: |
| 814 | return None | 862 | return None |
| 815 | np_dtype = np_array.dtype.name | 863 | 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_e8m0fnu | 873 | # 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 | ||
| 850 | def torch_to_numpy_tensor(torch_tensor) -> numpy.ndarray: | 897 | def torch_to_numpy_tensor(torch_tensor) -> numpy.ndarray: |
| 851 | import torch | 898 | import torch |
| 899 | + | ||
| 852 | if torch_tensor is None: | 900 | if torch_tensor is None: |
| 853 | return None | 901 | 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.dtype | 904 | 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_size | 926 | pad_size = orig_shape[axis] % block_size |
| 880 | pad[axis][1] = block_size - pad_size | 927 | 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.shape | 930 | padded_shape = fp_array.shape |
| 884 | reshape = list(padded_shape) | 931 | reshape = list(padded_shape) |
| 885 | reshape[axis + 1] = block_size | 932 | 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_shape | 935 | 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 reshaping | 939 | # Undo tile reshaping |
| 894 | fp_array = fp_array.reshape(padded_shape) | 940 | fp_array = fp_array.reshape(padded_shape) |
| 895 | # Undo padding | 941 | # 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 res | 958 | return res |
| 915 | 959 | ||
| 960 | + | ||
| 916 | def _mx_calculate_share_exp_nv(fp_array: numpy.ndarray, scale_axis: int, mx_ele_dtype: str): | 961 | def _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 = 127 | 964 | 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_norm | 969 | 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) # 二进制:01111111100000000000000000000000 | 971 | exponent_mask = numpy.uint32(0x7F800000) # 二进制:01111111100000000000000000000000 |
| 926 | - mantissa_mask = numpy.uint32(0x007FFFFF) # 二进制:00000000011111111111111111111111 | 972 | + mantissa_mask = numpy.uint32(0x007FFFFF) # 二进制:00000000011111111111111111111111 |
| 927 | # 提取指数部分并转换为uint16 | 973 | # 提取指数部分并转换为uint16 |
| 928 | exponents = (binary_ints & exponent_mask) >> 23 | 974 | exponents = (binary_ints & exponent_mask) >> 23 |
| 929 | exponents_int16 = exponents.astype(numpy.int16) | 975 | exponents_int16 = exponents.astype(numpy.int16) |
| 930 | # 提取尾数部分并转换为float | 976 | # 提取尾数部分并转换为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 10000000000000000000000 | 979 | # 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 res | 984 | return res |
| 939 | 985 | ||
| 986 | + | ||
| 940 | def _mx_round_mantissa(fp_array: numpy.ndarray, round_mode: str): | 987 | def _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_array | 1011 | 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 exponent | 1029 | # The minimum representable exponent |
| 981 | min_exp = 0 if "float4_e1m2" in mx_dtype else -(2 ** (exp_bits - 1)) + 2 | 1030 | 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 number | 1032 | # 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 scaling | 1035 | # 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 them | 1037 | # 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.ndim | 1067 | pad_width = [(0, 0)] * tensor.ndim |
| 1019 | pad_width[axis] = (0, 1) # 在 axis 维度末尾补一个 0 | 1068 | 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_tensor | 1071 | return padded_tensor |
| 1023 | 1072 | ||
| 1073 | + | ||
| 1024 | def interleave(tensor: numpy.ndarray, axis: int, n_group: int = 2) -> numpy.ndarray: | 1074 | def 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 transposed | 1104 | 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 dtypes | 1116 | quantize BFP16/FP16/FP32 to MX dtypes |
| 1060 | :parameter fp_array: input numpy array with dtype BFP16/FP16/FP32 | 1117 | :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_size | 1139 | # 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 exponents | 1141 | # 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 E8M0 | 1146 | 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_emax | 1148 | 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 dtype | 1155 | # 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 float32 | 1157 | # share_exp is always float32 |
| 1105 | - scale_array = 2 ** share_exp | 1158 | + 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 | ||
| 1129 | def _grouped_mx_undo_reshape_to_blocks( | 1182 | def _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 轴补 Pad | 1262 | # 构造 Pad 宽度,仅在 axis 轴补 Pad |
| 1214 | pad_width = [(0, 0)] * group.ndim | 1263 | 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 = group | 1267 | padded_group = group |
| 1219 | padded_index = padded_index + group_len + pad_size | 1268 | 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 axis | 1272 | # 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 function | 1274 | # 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_size | 1277 | # 总块数 = 总长度(已对齐)// block_size |
| 1230 | total_blocks = padded_array.shape[axis] // block_size | 1278 | 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_shape | 1286 | return reshaped_array, padded_group_index, padded_shape |
| 1239 | 1287 | ||
| 1240 | 1288 | ||
| 1241 | def reshape_scale_array_pad(scale_array: numpy.ndarray, group_index: numpy.ndarray, axis): | 1289 | def reshape_scale_array_pad(scale_array: numpy.ndarray, group_index: numpy.ndarray, axis): |
| 1242 | import math | 1290 | import math |
| 1291 | + | ||
| 1243 | scale_array = numpy.squeeze(scale_array, 1) | 1292 | scale_array = numpy.squeeze(scale_array, 1) |
| 1244 | cur_idx = 0 | 1293 | cur_idx = 0 |
| 1245 | pre_element = 0 | 1294 | 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 # 前一个element | 1305 | 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_array | 1314 | 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 dtypes | 1326 | quantize BFP16/FP16 to MX dtypes |
| 1268 | :parameter fp_array: input numpy array with dtype BFP16/FP16 | 1327 | :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_size | 1363 | # 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 exponents | 1365 | # 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 E8M0 | 1367 | 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_emax | 1369 | 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 dtype | 1376 | # 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 float32 | 1378 | # share_exp is always float32 |
| 1322 | - scale_array = 2 ** share_exp | 1379 | + 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 | ||
| 1335 | def fp32_to_hf32(torch_tensor): | 1392 | def fp32_to_hf32(torch_tensor): |
| 1336 | import torch | 1393 | 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 torch | 1411 | import torch |
| 1412 | + | ||
| 1354 | name = str(dtype_name) | 1413 | name = str(dtype_name) |
| 1355 | - if name == 'int4': | 1414 | + if name == "int4": |
| 1356 | return False | 1415 | 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 | + | ||
| 1360 | def np_as_strided_safe(base, shape, strides): | 1465 | def 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_strided | 1479 | from numpy.lib.stride_tricks import as_strided as _np_as_strided |
| 1480 | + | ||
| 1375 | dtype = base.dtype | 1481 | 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."): | ||
| 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"): | ||
| 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 | ||


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