已合并
perf: defer dynamo/inductor loading on v2.9.0 (#2788) #43444
黄桂军创建于 20 天前
perf: defer dynamo/inductor loading on v2.9.0 (#2788) #43444
已合并
共 22 个文件变更+1476-174
| @@ -786,5 +786,10 @@ setup( | |||
| 786 | 'torch.backends': [ | 786 | 'torch.backends': [ |
| 787 | 'torch_npu = torch_npu:_autoload', | 787 | 'torch_npu = torch_npu:_autoload', |
| 788 | ], | 788 | ], |
| 789 | + 'torch_dynamo_backends': [ | ||
| 790 | + 'npu = torch_npu.dynamo:_npu_backend_entrypoint', | ||
| 791 | + 'npugraph_ex = torch_npu.dynamo:_npugraph_ex_backend_entrypoint', | ||
| 792 | + 'npugraphs = torch_npu.dynamo:_npugraphs_backend_entrypoint', | ||
| 793 | + ], | ||
| 789 | } | 794 | } |
| 790 | ) | 795 | ) |
| @@ -1,4 +1,5 @@ | |||
| 1 | import torch | 1 | import torch |
| 2 | +import torch._dynamo.testing | ||
| 2 | from torch.testing._internal.common_utils import ( | 3 | from torch.testing._internal.common_utils import ( |
| 3 | run_tests, | 4 | run_tests, |
| 4 | instantiate_parametrized_tests, | 5 | instantiate_parametrized_tests, |
| @@ -30,4 +31,4 @@ instantiate_parametrized_tests(TestCurrentDevice) | |||
| 30 | 31 | ||
| 31 | 32 | ||
| 32 | if __name__ == "__main__": | 33 | if __name__ == "__main__": |
| 33 | - run_tests() | 34 | + run_tests() |
| @@ -25,6 +25,9 @@ class TestAdd(TestUtils): | |||
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | def test_config_environ_cases(self, shape, dtype): | 27 | def test_config_environ_cases(self, shape, dtype): |
| 28 | + # torch._inductor.config is an internal module. Initialize the NPU | ||
| 29 | + # config entries explicitly before testing direct config assignment. | ||
| 30 | + torch._inductor.config.get_config_copy() | ||
| 28 | torch._inductor.config.npu_backend = "mlir" | 31 | torch._inductor.config.npu_backend = "mlir" |
| 29 | x = self._generate_tensor(shape, dtype) | 32 | x = self._generate_tensor(shape, dtype) |
| 30 | y = self._generate_tensor(shape, dtype) | 33 | y = self._generate_tensor(shape, dtype) |
| @@ -38,4 +41,4 @@ class TestAdd(TestUtils): | |||
| 38 | instantiate_parametrized_tests(TestAdd) | 41 | instantiate_parametrized_tests(TestAdd) |
| 39 | 42 | ||
| 40 | if __name__ == "__main__": | 43 | if __name__ == "__main__": |
| 41 | - run_tests() | 44 | + run_tests() |
| @@ -0,0 +1,989 @@ | |||
| 1 | +# Owner(s): ["module: dynamo"] | ||
| 2 | + | ||
| 3 | +import importlib.util | ||
| 4 | +import os | ||
| 5 | +import subprocess | ||
| 6 | +import sys | ||
| 7 | +import textwrap | ||
| 8 | +import unittest | ||
| 9 | + | ||
| 10 | +import torch | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +class TorchCompileTriggerTests(unittest.TestCase): | ||
| 14 | + def run_in_subprocess(self, code): | ||
| 15 | + env = os.environ.copy() | ||
| 16 | + env["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" | ||
| 17 | + result = subprocess.run( | ||
| 18 | + [sys.executable, "-c", textwrap.dedent(code)], | ||
| 19 | + capture_output=True, | ||
| 20 | + env=env, | ||
| 21 | + text=True, | ||
| 22 | + timeout=60, | ||
| 23 | + ) | ||
| 24 | + self.assertEqual( | ||
| 25 | + result.returncode, | ||
| 26 | + 0, | ||
| 27 | + f"Subprocess failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}", | ||
| 28 | + ) | ||
| 29 | + | ||
| 30 | + # Verify import installs only the Dynamo post-import trigger. | ||
| 31 | + def test_import_installs_dynamo_post_import_trigger(self): | ||
| 32 | + self.run_in_subprocess( | ||
| 33 | + """ | ||
| 34 | + import sys | ||
| 35 | + import torch | ||
| 36 | + | ||
| 37 | + def loaded(prefix): | ||
| 38 | + return [ | ||
| 39 | + name for name in sys.modules | ||
| 40 | + if name == prefix or name.startswith(prefix + ".") | ||
| 41 | + ] | ||
| 42 | + | ||
| 43 | + assert not loaded("torch._dynamo") | ||
| 44 | + assert not loaded("torch._inductor") | ||
| 45 | + src_compile = torch.compile | ||
| 46 | + src_wrapper_init = torch._TorchCompileWrapper.__init__ | ||
| 47 | + | ||
| 48 | + import torch_npu | ||
| 49 | + from torch_npu.utils import _dynamo | ||
| 50 | + | ||
| 51 | + assert not loaded("torch._dynamo") | ||
| 52 | + assert not loaded("torch._inductor") | ||
| 53 | + assert not loaded("torch_npu._inductor") | ||
| 54 | + assert "triton" not in sys.modules | ||
| 55 | + assert torch.utils._triton.has_triton is _dynamo.has_triton | ||
| 56 | + assert torch.compile is src_compile | ||
| 57 | + assert torch._TorchCompileWrapper.__init__ is src_wrapper_init | ||
| 58 | + assert any( | ||
| 59 | + isinstance(finder, _dynamo._DynamoPostImportFinder) | ||
| 60 | + for finder in sys.meta_path | ||
| 61 | + ) | ||
| 62 | + assert not _dynamo._lazy_dynamo_setup.has_run | ||
| 63 | + """ | ||
| 64 | + ) | ||
| 65 | + | ||
| 66 | + # Verify public compiler APIs work before the first compile. | ||
| 67 | + def test_public_compiler_entries_are_available_before_compile(self): | ||
| 68 | + self.run_in_subprocess( | ||
| 69 | + """ | ||
| 70 | + import inspect | ||
| 71 | + import sys | ||
| 72 | + import torch | ||
| 73 | + import torch_npu | ||
| 74 | + from torch_npu.utils import _dynamo | ||
| 75 | + | ||
| 76 | + marker = torch.compiler.npugraph_mark_step_begin | ||
| 77 | + assert marker.__name__ == "npugraph_mark_step_begin" | ||
| 78 | + assert str(inspect.signature(marker)) == "()" | ||
| 79 | + marker() | ||
| 80 | + | ||
| 81 | + from torch_npu.npu._graph_tree_state import MarkStepBox | ||
| 82 | + | ||
| 83 | + assert MarkStepBox.mark_step_counter == -1 | ||
| 84 | + assert not _dynamo._lazy_dynamo_setup.has_run | ||
| 85 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 86 | + assert not any( | ||
| 87 | + name == "torch._dynamo" or name.startswith("torch._dynamo.") | ||
| 88 | + for name in sys.modules | ||
| 89 | + ) | ||
| 90 | + assert not any( | ||
| 91 | + name == "torch._inductor" or name.startswith("torch._inductor.") | ||
| 92 | + for name in sys.modules | ||
| 93 | + ) | ||
| 94 | + | ||
| 95 | + backends = torch.compiler.list_backends(exclude_tags=None) | ||
| 96 | + assert {"npu", "npugraph_ex", "npugraphs"}.issubset(backends) | ||
| 97 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 98 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 99 | + assert "torch_npu._inductor" not in sys.modules | ||
| 100 | + """ | ||
| 101 | + ) | ||
| 102 | + | ||
| 103 | + # Verify entry-point loading cannot register the NPU backend twice. | ||
| 104 | + def test_backend_entrypoint_can_import_torch_npu_without_duplicate_registration(self): | ||
| 105 | + self.run_in_subprocess( | ||
| 106 | + """ | ||
| 107 | + import torch | ||
| 108 | + import torch._dynamo | ||
| 109 | + from torch._dynamo.backends import registry | ||
| 110 | + | ||
| 111 | + # Model the state produced by setuptools entry-point discovery: | ||
| 112 | + # lookup_backend owns the final register_backend call, while | ||
| 113 | + # EntryPoint.load imports torch_npu and triggers its lazy setup. | ||
| 114 | + class NpuEntryPoint: | ||
| 115 | + module = "torch_npu.dynamo" | ||
| 116 | + | ||
| 117 | + def load(self): | ||
| 118 | + from torch_npu.dynamo import _npu_backend_entrypoint | ||
| 119 | + return _npu_backend_entrypoint | ||
| 120 | + | ||
| 121 | + registry._BACKENDS["npu"] = NpuEntryPoint() | ||
| 122 | + backend = registry.lookup_backend("npu") | ||
| 123 | + | ||
| 124 | + assert backend.__name__ == "_npu_backend_entrypoint" | ||
| 125 | + assert registry._COMPILER_FNS["npu"] is backend | ||
| 126 | + | ||
| 127 | + # A later setup retry must treat the loaded torch_npu entry point | ||
| 128 | + # as an already completed registration. | ||
| 129 | + from torch_npu.dynamo import _register_npu_backend | ||
| 130 | + | ||
| 131 | + _register_npu_backend(backend, "npu") | ||
| 132 | + assert registry._COMPILER_FNS["npu"] is backend | ||
| 133 | + """ | ||
| 134 | + ) | ||
| 135 | + | ||
| 136 | + # Verify backend registration is safe to retry after a partial failure. | ||
| 137 | + def test_backend_registration_retry_after_partial_failure(self): | ||
| 138 | + self.run_in_subprocess( | ||
| 139 | + """ | ||
| 140 | + import torch | ||
| 141 | + import torch_npu | ||
| 142 | + import torch._dynamo | ||
| 143 | + import torch_npu.dynamo as npu_dynamo | ||
| 144 | + from torch._dynamo.backends import registry | ||
| 145 | + | ||
| 146 | + # Start from an undiscovered state so the first registration is | ||
| 147 | + # committed before the simulated second-registration failure. | ||
| 148 | + for name in ("npu", "npugraph_ex"): | ||
| 149 | + registry._BACKENDS.pop(name, None) | ||
| 150 | + registry._COMPILER_FNS.pop(name, None) | ||
| 151 | + | ||
| 152 | + original_register = npu_dynamo._register_npu_backend | ||
| 153 | + fail_npugraph_ex = True | ||
| 154 | + | ||
| 155 | + def fail_second_registration(backend, name="npu"): | ||
| 156 | + if name == "npugraph_ex" and fail_npugraph_ex: | ||
| 157 | + raise RuntimeError("simulated npugraph_ex registration failure") | ||
| 158 | + return original_register(backend, name) | ||
| 159 | + | ||
| 160 | + npu_dynamo._register_npu_backend = fail_second_registration | ||
| 161 | + try: | ||
| 162 | + try: | ||
| 163 | + npu_dynamo._register_backends() | ||
| 164 | + except RuntimeError as error: | ||
| 165 | + assert "simulated npugraph_ex" in str(error) | ||
| 166 | + else: | ||
| 167 | + raise AssertionError("the first registration should fail") | ||
| 168 | + finally: | ||
| 169 | + npu_dynamo._register_npu_backend = original_register | ||
| 170 | + | ||
| 171 | + assert "npu" in registry._COMPILER_FNS | ||
| 172 | + assert "npugraph_ex" not in registry._COMPILER_FNS | ||
| 173 | + | ||
| 174 | + # Retry: the completed npu registration is a no-op, while the | ||
| 175 | + # missing npugraph_ex registration is installed normally. | ||
| 176 | + npu_dynamo._register_backends() | ||
| 177 | + assert "npu" in registry._COMPILER_FNS | ||
| 178 | + assert "npugraph_ex" in registry._COMPILER_FNS | ||
| 179 | + """ | ||
| 180 | + ) | ||
| 181 | + | ||
| 182 | + | ||
| 183 | + # Verify a forked child discards inherited in-progress lazy setup state. | ||
| 184 | + def test_public_lazy_setup_recovers_after_fork(self): | ||
| 185 | + self.run_in_subprocess( | ||
| 186 | + """ | ||
| 187 | + import os | ||
| 188 | + import signal | ||
| 189 | + import threading | ||
| 190 | + | ||
| 191 | + import torch_npu | ||
| 192 | + import torch_npu.dynamo as npu_dynamo | ||
| 193 | + from torch_npu.utils import _dynamo | ||
| 194 | + | ||
| 195 | + parent_pid = os.getpid() | ||
| 196 | + original_add = _dynamo.add_dynamo_methods_init | ||
| 197 | + original_get_backend = npu_dynamo._get_default_backend | ||
| 198 | + entered = threading.Event() | ||
| 199 | + release = threading.Event() | ||
| 200 | + | ||
| 201 | + def block_parent_setup(): | ||
| 202 | + if os.getpid() == parent_pid: | ||
| 203 | + entered.set() | ||
| 204 | + assert release.wait(timeout=10) | ||
| 205 | + return original_add() | ||
| 206 | + | ||
| 207 | + _dynamo.add_dynamo_methods_init = block_parent_setup | ||
| 208 | + setup_thread = threading.Thread(target=_dynamo._lazy_dynamo_setup) | ||
| 209 | + setup_thread.start() | ||
| 210 | + assert entered.wait(timeout=5) | ||
| 211 | + | ||
| 212 | + try: | ||
| 213 | + child_pid = os.fork() | ||
| 214 | + if child_pid == 0: | ||
| 215 | + def timeout(_signal, _frame): | ||
| 216 | + raise TimeoutError("lazy setup hung after fork") | ||
| 217 | + | ||
| 218 | + signal.signal(signal.SIGALRM, timeout) | ||
| 219 | + signal.alarm(5) | ||
| 220 | + try: | ||
| 221 | + npu_dynamo._get_default_backend = ( | ||
| 222 | + lambda name: npu_dynamo._eager_npu_backend | ||
| 223 | + ) | ||
| 224 | + graph_module = lambda x: x | ||
| 225 | + result = npu_dynamo._npu_backend_entrypoint( | ||
| 226 | + graph_module, [] | ||
| 227 | + ) | ||
| 228 | + assert result is graph_module | ||
| 229 | + signal.alarm(0) | ||
| 230 | + except BaseException as error: | ||
| 231 | + os.write( | ||
| 232 | + 2, | ||
| 233 | + f"child lazy setup failed: {error}\\n".encode(), | ||
| 234 | + ) | ||
| 235 | + os._exit(1) | ||
| 236 | + os._exit(0) | ||
| 237 | + | ||
| 238 | + _, status = os.waitpid(child_pid, 0) | ||
| 239 | + finally: | ||
| 240 | + release.set() | ||
| 241 | + setup_thread.join(timeout=10) | ||
| 242 | + _dynamo.add_dynamo_methods_init = original_add | ||
| 243 | + npu_dynamo._get_default_backend = original_get_backend | ||
| 244 | + | ||
| 245 | + assert os.WIFEXITED(status) | ||
| 246 | + assert os.waitstatus_to_exitcode(status) == 0 | ||
| 247 | + assert not setup_thread.is_alive() | ||
| 248 | + """ | ||
| 249 | + ) | ||
| 250 | + | ||
| 251 | + | ||
| 252 | + # Verify NPUGraphs rejects unsupported options in every registration order. | ||
| 253 | + def test_npugraphs_rejects_options_across_registration_order(self): | ||
| 254 | + self.run_in_subprocess( | ||
| 255 | + """ | ||
| 256 | + import contextlib | ||
| 257 | + from unittest import mock | ||
| 258 | + | ||
| 259 | + import torch | ||
| 260 | + import torch_npu | ||
| 261 | + from torch_npu.dynamo import _npugraphs_backend_entrypoint | ||
| 262 | + from torch_npu.utils import _dynamo, _graph_tree | ||
| 263 | + | ||
| 264 | + gm = object() | ||
| 265 | + inputs = [object()] | ||
| 266 | + options = {"npu_backend": "mlir"} | ||
| 267 | + | ||
| 268 | + with mock.patch.object( | ||
| 269 | + _dynamo, "_lazy_dynamo_setup", lambda: None | ||
| 270 | + ), mock.patch.object( | ||
| 271 | + _dynamo, "_lazy_inductor_setup", lambda: None | ||
| 272 | + ), mock.patch.object( | ||
| 273 | + _dynamo, | ||
| 274 | + "_NpuBackendScope", | ||
| 275 | + lambda backend: contextlib.nullcontext(), | ||
| 276 | + ), mock.patch.object( | ||
| 277 | + _graph_tree, | ||
| 278 | + "npugraphs", | ||
| 279 | + lambda model, args, **kwargs: "unexpected", | ||
| 280 | + ): | ||
| 281 | + for backend in ( | ||
| 282 | + _npugraphs_backend_entrypoint, | ||
| 283 | + _graph_tree.NpugraphsBackend(), | ||
| 284 | + ): | ||
| 285 | + try: | ||
| 286 | + backend(gm, inputs, options=options) | ||
| 287 | + except TypeError as error: | ||
| 288 | + assert "unexpected keyword argument 'options'" in str(error) | ||
| 289 | + else: | ||
| 290 | + raise AssertionError("npugraphs must reject options") | ||
| 291 | + | ||
| 292 | + compiled = torch.compile( | ||
| 293 | + lambda value: value + 1, | ||
| 294 | + backend="npugraphs", | ||
| 295 | + options=options, | ||
| 296 | + ) | ||
| 297 | + try: | ||
| 298 | + compiled(torch.ones(1)) | ||
| 299 | + except Exception as error: | ||
| 300 | + assert "unexpected keyword argument 'options'" in str(error) | ||
| 301 | + else: | ||
| 302 | + raise AssertionError("public npugraphs must reject options") | ||
| 303 | + """ | ||
| 304 | + ) | ||
| 305 | + | ||
| 306 | + # Verify public reset reaches NPUGraphs for every registration order. | ||
| 307 | + | ||
| 308 | + def test_npugraphs_reset_protocol_across_registration_order(self): | ||
| 309 | + for order in ("cold", "hot"): | ||
| 310 | + with self.subTest(order=order): | ||
| 311 | + initialize_inductor = ( | ||
| 312 | + "torch.compile(lambda x: x + 1, backend='inductor')" | ||
| 313 | + if order == "hot" | ||
| 314 | + else "" | ||
| 315 | + ) | ||
| 316 | + self.run_in_subprocess( | ||
| 317 | + f""" | ||
| 318 | + import sys | ||
| 319 | + import types | ||
| 320 | + from unittest import mock | ||
| 321 | + | ||
| 322 | + import torch | ||
| 323 | + import torch_npu | ||
| 324 | + | ||
| 325 | + {initialize_inductor} | ||
| 326 | + torch.compile(lambda x: x + 1, backend="npugraphs") | ||
| 327 | + | ||
| 328 | + from torch._dynamo.backends import registry | ||
| 329 | + from torch_npu.dynamo import _npugraphs_backend_entrypoint | ||
| 330 | + | ||
| 331 | + backend = registry._COMPILER_FNS["npugraphs"] | ||
| 332 | + assert backend is _npugraphs_backend_entrypoint | ||
| 333 | + assert hasattr(backend, "reset") | ||
| 334 | + | ||
| 335 | + graph_tree_module = "torch_npu.npu._graph_tree" | ||
| 336 | + assert graph_tree_module not in sys.modules | ||
| 337 | + backend.reset() | ||
| 338 | + assert graph_tree_module not in sys.modules | ||
| 339 | + | ||
| 340 | + reset_calls = [] | ||
| 341 | + fake_graph_tree = types.ModuleType(graph_tree_module) | ||
| 342 | + fake_graph_tree.reset_npugraph_trees = ( | ||
| 343 | + lambda: reset_calls.append("reset") | ||
| 344 | + ) | ||
| 345 | + with mock.patch.dict( | ||
| 346 | + sys.modules, | ||
| 347 | + {{graph_tree_module: fake_graph_tree}}, | ||
| 348 | + ): | ||
| 349 | + torch.compiler.reset() | ||
| 350 | + | ||
| 351 | + assert reset_calls == ["reset"] | ||
| 352 | + """ | ||
| 353 | + ) | ||
| 354 | + | ||
| 355 | + | ||
| 356 | + # Verify every public Export entry initializes only Dynamo on NPU. | ||
| 357 | + def test_npu_export_public_entry_and_import_order_matrix(self): | ||
| 358 | + cases = { | ||
| 359 | + "module_export": ( | ||
| 360 | + "", | ||
| 361 | + "exported = torch.export.export(Model(), (x,), strict=True)", | ||
| 362 | + ), | ||
| 363 | + "prebound_export": ( | ||
| 364 | + "from torch.export import export as export_api", | ||
| 365 | + "exported = export_api(Model(), (x,), strict=True)", | ||
| 366 | + ), | ||
| 367 | + "prebound_export_for_training": ( | ||
| 368 | + "from torch.export import export_for_training as export_api", | ||
| 369 | + "exported = export_api(Model(), (x,), strict=True)", | ||
| 370 | + ), | ||
| 371 | + } | ||
| 372 | + if hasattr(torch.export, "export_for_inference"): | ||
| 373 | + cases["prebound_export_for_inference"] = ( | ||
| 374 | + "from torch.export import export_for_inference as export_api", | ||
| 375 | + "exported = export_api(Model(), (x,), strict=True)", | ||
| 376 | + ) | ||
| 377 | + for name, (pre_import, export_call) in cases.items(): | ||
| 378 | + with self.subTest(name=name): | ||
| 379 | + self.run_in_subprocess( | ||
| 380 | + f""" | ||
| 381 | + import sys | ||
| 382 | + import torch | ||
| 383 | + {pre_import} | ||
| 384 | + import torch_npu | ||
| 385 | + | ||
| 386 | + stream = torch.npu.Stream() | ||
| 387 | + | ||
| 388 | + class Model(torch.nn.Module): | ||
| 389 | + def forward(self, x): | ||
| 390 | + x.record_stream(stream) | ||
| 391 | + return x + 1 | ||
| 392 | + | ||
| 393 | + x = torch.ones(4, device="npu") | ||
| 394 | + {export_call} | ||
| 395 | + actual = exported.module()(x) | ||
| 396 | + | ||
| 397 | + from torch_npu.utils import _dynamo | ||
| 398 | + | ||
| 399 | + torch.testing.assert_close(actual, x + 1) | ||
| 400 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 401 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 402 | + assert "torch_npu._inductor" not in sys.modules | ||
| 403 | + """ | ||
| 404 | + ) | ||
| 405 | + | ||
| 406 | + # Verify NPU-specific operations retain their Export capture semantics. | ||
| 407 | + def test_npu_export_capture_semantics_matrix(self): | ||
| 408 | + self.run_in_subprocess( | ||
| 409 | + """ | ||
| 410 | + import sys | ||
| 411 | + import torch | ||
| 412 | + import torch_npu | ||
| 413 | + | ||
| 414 | + x = torch.ones(4, device="npu") | ||
| 415 | + stream = torch.npu.Stream() | ||
| 416 | + event = torch.npu.Event() | ||
| 417 | + | ||
| 418 | + class StreamAndEvent(torch.nn.Module): | ||
| 419 | + def forward(self, value): | ||
| 420 | + event.record() | ||
| 421 | + with torch.npu.stream(stream): | ||
| 422 | + event.wait(stream) | ||
| 423 | + result = value + 1 | ||
| 424 | + return result | ||
| 425 | + | ||
| 426 | + class Autocast(torch.nn.Module): | ||
| 427 | + def forward(self, value): | ||
| 428 | + with torch.npu.amp.autocast(dtype=torch.float16): | ||
| 429 | + return value * value | ||
| 430 | + | ||
| 431 | + class CurrentDevice(torch.nn.Module): | ||
| 432 | + def forward(self, value): | ||
| 433 | + return value + torch.npu.current_device() | ||
| 434 | + | ||
| 435 | + class DeviceProperties(torch.nn.Module): | ||
| 436 | + def forward(self, value): | ||
| 437 | + properties = torch.npu.get_device_properties( | ||
| 438 | + torch.npu.current_device() | ||
| 439 | + ) | ||
| 440 | + return value + 1 if properties.total_memory > 0 else value - 1 | ||
| 441 | + | ||
| 442 | + class IsAvailable(torch.nn.Module): | ||
| 443 | + def forward(self, value): | ||
| 444 | + return value + 1 if torch.npu.is_available() else value - 1 | ||
| 445 | + | ||
| 446 | + models = ( | ||
| 447 | + StreamAndEvent, | ||
| 448 | + Autocast, | ||
| 449 | + CurrentDevice, | ||
| 450 | + DeviceProperties, | ||
| 451 | + IsAvailable, | ||
| 452 | + ) | ||
| 453 | + for model_type in models: | ||
| 454 | + model = model_type() | ||
| 455 | + expected = model(x) | ||
| 456 | + exported = torch.export.export(model, (x,)) | ||
| 457 | + actual = exported.module()(x) | ||
| 458 | + torch.testing.assert_close(actual, expected) | ||
| 459 | + | ||
| 460 | + from torch_npu.utils import _dynamo | ||
| 461 | + | ||
| 462 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 463 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 464 | + assert "torch_npu._inductor" not in sys.modules | ||
| 465 | + """ | ||
| 466 | + ) | ||
| 467 | + | ||
| 468 | + # Verify rejected Inductor arguments do not initialize or pollute NPU state. | ||
| 469 | + def test_invalid_inductor_arguments_fail_without_npu_initialization(self): | ||
| 470 | + cases = { | ||
| 471 | + "invalid_mode": ( | ||
| 472 | + 'torch.compile(lambda x: x + 1, mode="invalid-mode")', | ||
| 473 | + "Unrecognized mode=invalid-mode", | ||
| 474 | + ), | ||
| 475 | + "invalid_option": ( | ||
| 476 | + "torch.compile(lambda x: x + 1, " | ||
| 477 | + 'options={"invalid.option": True})', | ||
| 478 | + "Unexpected optimization option invalid.option", | ||
| 479 | + ), | ||
| 480 | + } | ||
| 481 | + for name, (compile_call, expected_error) in cases.items(): | ||
| 482 | + with self.subTest(name=name): | ||
| 483 | + self.run_in_subprocess( | ||
| 484 | + f""" | ||
| 485 | + import os | ||
| 486 | + import sys | ||
| 487 | + | ||
| 488 | + import torch | ||
| 489 | + import torch_npu | ||
| 490 | + from torch_npu.utils import _dynamo | ||
| 491 | + | ||
| 492 | + env_name = "TORCHINDUCTOR_NPU_BACKEND" | ||
| 493 | + original_env = os.environ.get(env_name) | ||
| 494 | + try: | ||
| 495 | + {compile_call} | ||
| 496 | + except RuntimeError as error: | ||
| 497 | + assert {expected_error!r} in str(error), str(error) | ||
| 498 | + else: | ||
| 499 | + raise AssertionError("invalid compile arguments must fail") | ||
| 500 | + | ||
| 501 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 502 | + assert not _dynamo.is_inductor_npu_initialized() | ||
| 503 | + assert "torch_npu._inductor" not in sys.modules | ||
| 504 | + assert os.environ.get(env_name) == original_env | ||
| 505 | + """ | ||
| 506 | + ) | ||
| 507 | + | ||
| 508 | + # Verify scope entry failures restore the process environment. | ||
| 509 | + def test_npu_backend_scope_restores_env_after_entry_failure(self): | ||
| 510 | + self.run_in_subprocess( | ||
| 511 | + """ | ||
| 512 | + import os | ||
| 513 | + | ||
| 514 | + import torch_npu | ||
| 515 | + from torch_npu.utils import _dynamo | ||
| 516 | + | ||
| 517 | + env_name = "TORCHINDUCTOR_NPU_BACKEND" | ||
| 518 | + original_env = os.environ.get(env_name) | ||
| 519 | + try: | ||
| 520 | + with _dynamo._NpuBackendScope(1): | ||
| 521 | + raise AssertionError("scope entry must fail") | ||
| 522 | + except TypeError as error: | ||
| 523 | + assert "str" in str(error) | ||
| 524 | + else: | ||
| 525 | + raise AssertionError("a non-string backend must fail") | ||
| 526 | + | ||
| 527 | + assert os.environ.get(env_name) == original_env | ||
| 528 | + """ | ||
| 529 | + ) | ||
| 530 | + | ||
| 531 | + # Verify shape handling is installed after selecting the requested NPU backend. | ||
| 532 | + def test_shape_handling_initializes_after_backend_selection(self): | ||
| 533 | + self.run_in_subprocess( | ||
| 534 | + """ | ||
| 535 | + import types | ||
| 536 | + from unittest import mock | ||
| 537 | + | ||
| 538 | + import torch | ||
| 539 | + import torch_npu | ||
| 540 | + from torch_npu.utils import _dynamo | ||
| 541 | + | ||
| 542 | + def fake_setup(actual_options): | ||
| 543 | + actual_options = dict(actual_options) | ||
| 544 | + events.append(("setup", actual_options)) | ||
| 545 | + return actual_options["npu_backend"] | ||
| 546 | + | ||
| 547 | + options = { | ||
| 548 | + "npu_backend": "mlir", | ||
| 549 | + "enable_shape_handling": True, | ||
| 550 | + } | ||
| 551 | + events = [] | ||
| 552 | + | ||
| 553 | + def scope_register(): | ||
| 554 | + events.append(("scope_register", None)) | ||
| 555 | + | ||
| 556 | + fake_inductor = types.SimpleNamespace( | ||
| 557 | + patch_shape_handling=lambda: events.append( | ||
| 558 | + ("shape_handling", None) | ||
| 559 | + ) | ||
| 560 | + ) | ||
| 561 | + with mock.patch.object( | ||
| 562 | + _dynamo, "_setup_inductor_for_compile", fake_setup | ||
| 563 | + ), mock.patch.object( | ||
| 564 | + _dynamo, "register_inductor_npu", scope_register | ||
| 565 | + ), mock.patch.object( | ||
| 566 | + torch_npu, "_inductor", fake_inductor, create=True | ||
| 567 | + ): | ||
| 568 | + wrapper = torch._TorchCompileInductorWrapper(None, options, None) | ||
| 569 | + | ||
| 570 | + assert wrapper.config["npu_backend"] == "mlir" | ||
| 571 | + assert wrapper.config["enable_shape_handling"] is True | ||
| 572 | + assert events == [ | ||
| 573 | + ("setup", options), | ||
| 574 | + ("shape_handling", None), | ||
| 575 | + ("scope_register", None), | ||
| 576 | + ], events | ||
| 577 | + """ | ||
| 578 | + ) | ||
| 579 | + | ||
| 580 | + # Verify non-Inductor compile backends do not initialize Inductor. | ||
| 581 | + def test_non_inductor_compile_backend_matrix(self): | ||
| 582 | + cases = { | ||
| 583 | + "eager": ( | ||
| 584 | + "", | ||
| 585 | + 'compiled = torch.compile(Model(), backend="eager", fullgraph=True)', | ||
| 586 | + ), | ||
| 587 | + "custom": ( | ||
| 588 | + "custom_backend = lambda graph_module, example_inputs: " | ||
| 589 | + "graph_module.forward", | ||
| 590 | + "compiled = torch.compile(Model(), backend=custom_backend, fullgraph=True)", | ||
| 591 | + ), | ||
| 592 | + "npu": ( | ||
| 593 | + "", | ||
| 594 | + 'compiled = torch.compile(Model(), backend="npu", fullgraph=True)', | ||
| 595 | + ), | ||
| 596 | + } | ||
| 597 | + for name, (backend_definition, compile_call) in cases.items(): | ||
| 598 | + with self.subTest(name=name): | ||
| 599 | + allow_missing_torchair = name == "npu" | ||
| 600 | + self.run_in_subprocess( | ||
| 601 | + f""" | ||
| 602 | + import sys | ||
| 603 | + import torch | ||
| 604 | + import torch_npu | ||
| 605 | + | ||
| 606 | + class Model(torch.nn.Module): | ||
| 607 | + def forward(self, x): | ||
| 608 | + return torch.sin(x) + 1 | ||
| 609 | + | ||
| 610 | + {backend_definition} | ||
| 611 | + x = torch.randn(8, device="npu") | ||
| 612 | + try: | ||
| 613 | + {compile_call} | ||
| 614 | + except AssertionError as error: | ||
| 615 | + assert {allow_missing_torchair!r} | ||
| 616 | + assert "Could not find module torchair" in str(error) | ||
| 617 | + else: | ||
| 618 | + torch.testing.assert_close(compiled(x), Model()(x)) | ||
| 619 | + | ||
| 620 | + from torch_npu.utils import _dynamo | ||
| 621 | + | ||
| 622 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 623 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 624 | + assert "torch_npu._inductor" not in sys.modules | ||
| 625 | + """ | ||
| 626 | + ) | ||
| 627 | + | ||
| 628 | + # Verify ONNX Dynamo Export initializes only the NPU Dynamo integration. | ||
| 629 | + | ||
| 630 | + def test_npu_onnx_dynamo_export_initialization_chain(self): | ||
| 631 | + cases = { | ||
| 632 | + "module_export": ( | ||
| 633 | + "", | ||
| 634 | + "result = torch.onnx.export(Model(), (x,), dynamo=True)", | ||
| 635 | + ), | ||
| 636 | + "prebound_export": ( | ||
| 637 | + "from torch.onnx import export as onnx_export", | ||
| 638 | + "result = onnx_export(Model(), (x,), dynamo=True)", | ||
| 639 | + ), | ||
| 640 | + } | ||
| 641 | + if hasattr(torch.onnx, "dynamo_export"): | ||
| 642 | + cases["prebound_legacy_dynamo_export"] = ( | ||
| 643 | + "from torch.onnx import dynamo_export as onnx_export", | ||
| 644 | + "result = onnx_export(Model(), x)", | ||
| 645 | + ) | ||
| 646 | + for name, (pre_import, export_call) in cases.items(): | ||
| 647 | + with self.subTest(name=name): | ||
| 648 | + self.run_in_subprocess( | ||
| 649 | + f""" | ||
| 650 | + import sys | ||
| 651 | + import torch | ||
| 652 | + {pre_import} | ||
| 653 | + import torch_npu | ||
| 654 | + | ||
| 655 | + # ONNXScript 0.4.0 cannot version-convert models containing | ||
| 656 | + # functions. Conversion runs after Dynamo capture, which is | ||
| 657 | + # the boundary this test verifies, so isolate that unrelated | ||
| 658 | + # compatibility issue without weakening the import assertions. | ||
| 659 | + from torch.onnx._internal._lazy_import import onnxscript_apis | ||
| 660 | + onnxscript_apis.convert_version = lambda model, target: model | ||
| 661 | + | ||
| 662 | + class Model(torch.nn.Module): | ||
| 663 | + def forward(self, x): | ||
| 664 | + return torch.sin(x) + 1 | ||
| 665 | + | ||
| 666 | + x = torch.randn(8, device="npu") | ||
| 667 | + {export_call} | ||
| 668 | + | ||
| 669 | + from torch_npu.utils import _dynamo | ||
| 670 | + | ||
| 671 | + assert result is not None | ||
| 672 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 673 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 674 | + assert "torch_npu._inductor" not in sys.modules | ||
| 675 | + """ | ||
| 676 | + ) | ||
| 677 | + | ||
| 678 | + # Verify a real NPU Inductor compile initializes the full stack. | ||
| 679 | + | ||
| 680 | + def test_npu_inductor_initialization_chain(self): | ||
| 681 | + self.run_in_subprocess( | ||
| 682 | + """ | ||
| 683 | + import torch | ||
| 684 | + import torch_npu | ||
| 685 | + | ||
| 686 | + def fn(x): | ||
| 687 | + return torch.sin(x) + 1 | ||
| 688 | + | ||
| 689 | + x = torch.randn(8, device="npu") | ||
| 690 | + actual = torch.compile(fn, backend="inductor", fullgraph=True)(x) | ||
| 691 | + | ||
| 692 | + from torch._dynamo.device_interface import get_interface_for_device | ||
| 693 | + from torch_npu.utils import _dynamo | ||
| 694 | + | ||
| 695 | + torch.testing.assert_close(actual, fn(x)) | ||
| 696 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 697 | + assert _dynamo._lazy_inductor_setup.has_run | ||
| 698 | + assert get_interface_for_device("npu").device_count() > 0 | ||
| 699 | + """ | ||
| 700 | + ) | ||
| 701 | + | ||
| 702 | + # Verify a real NPUGraphs compile initializes the full stack. | ||
| 703 | + | ||
| 704 | + def test_npu_npugraphs_initialization_chain(self): | ||
| 705 | + self.run_in_subprocess( | ||
| 706 | + """ | ||
| 707 | + import torch | ||
| 708 | + import torch_npu | ||
| 709 | + | ||
| 710 | + def fn(x): | ||
| 711 | + return torch.sin(x) + 1 | ||
| 712 | + | ||
| 713 | + x = torch.randn(8, device="npu") | ||
| 714 | + actual = torch.compile(fn, backend="npugraphs", fullgraph=True)(x) | ||
| 715 | + | ||
| 716 | + from torch._dynamo.device_interface import get_interface_for_device | ||
| 717 | + from torch_npu.utils import _dynamo | ||
| 718 | + | ||
| 719 | + torch.testing.assert_close(actual, fn(x)) | ||
| 720 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 721 | + assert _dynamo._lazy_inductor_setup.has_run | ||
| 722 | + assert get_interface_for_device("npu").device_count() > 0 | ||
| 723 | + """ | ||
| 724 | + ) | ||
| 725 | + | ||
| 726 | + # Verify lazy setup completes before compile backend lookup. | ||
| 727 | + def test_compile_triggers_setup_before_backend_lookup(self): | ||
| 728 | + self.run_in_subprocess( | ||
| 729 | + """ | ||
| 730 | + import torch | ||
| 731 | + import torch_npu | ||
| 732 | + from torch_npu.utils import _dynamo | ||
| 733 | + | ||
| 734 | + calls = [] | ||
| 735 | + | ||
| 736 | + | ||
| 737 | + def fake_setup(): | ||
| 738 | + calls.append("setup") | ||
| 739 | + | ||
| 740 | + _dynamo._lazy_dynamo_setup = fake_setup | ||
| 741 | + from torch._dynamo.backends import registry | ||
| 742 | + assert fake_setup.has_run | ||
| 743 | + | ||
| 744 | + compiled = torch.compile(lambda x: x + 1, backend="eager") | ||
| 745 | + assert compiled(torch.tensor(1)).item() == 2 | ||
| 746 | + assert calls == ["setup"] | ||
| 747 | + """ | ||
| 748 | + ) | ||
| 749 | + | ||
| 750 | + # Verify the trigger works when Dynamo was imported first. | ||
| 751 | + def test_trigger_after_dynamo_was_preimported(self): | ||
| 752 | + self.run_in_subprocess( | ||
| 753 | + """ | ||
| 754 | + import sys | ||
| 755 | + import torch | ||
| 756 | + import torch._dynamo | ||
| 757 | + import torch_npu | ||
| 758 | + from torch_npu.utils import _dynamo | ||
| 759 | + | ||
| 760 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 761 | + | ||
| 762 | + compiled = torch.compile(lambda x: x + 1, backend="eager", fullgraph=True) | ||
| 763 | + assert compiled(torch.tensor(1)).item() == 2 | ||
| 764 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 765 | + assert not _dynamo._lazy_inductor_setup.has_run | ||
| 766 | + assert "torch_npu._inductor" not in sys.modules | ||
| 767 | + """ | ||
| 768 | + ) | ||
| 769 | + | ||
| 770 | + # Verify the moved helper preserves the torch_npu v2.9 device predicates. | ||
| 771 | + def test_has_triton_preserves_target_device_semantics(self): | ||
| 772 | + self.run_in_subprocess( | ||
| 773 | + """ | ||
| 774 | + import sys | ||
| 775 | + import types | ||
| 776 | + from unittest import mock | ||
| 777 | + | ||
| 778 | + import torch | ||
| 779 | + import torch_npu | ||
| 780 | + from torch._dynamo import device_interface | ||
| 781 | + from torch_npu.utils import _dynamo | ||
| 782 | + | ||
| 783 | + def make_interface(available=False): | ||
| 784 | + class Worker: | ||
| 785 | + | ||
| 786 | + def get_device_properties(): | ||
| 787 | + raise AssertionError("v2.9 NPU override must not query CUDA props") | ||
| 788 | + | ||
| 789 | + class Interface: | ||
| 790 | + | ||
| 791 | + def is_available(): | ||
| 792 | + return available | ||
| 793 | + | ||
| 794 | + Interface.Worker = Worker | ||
| 795 | + return Interface | ||
| 796 | + | ||
| 797 | + def run_case(available, *, package=True): | ||
| 798 | + interfaces = { | ||
| 799 | + device: make_interface( | ||
| 800 | + available=device in available, | ||
| 801 | + ) | ||
| 802 | + for device in ("cuda", "xpu", "cpu", "npu") | ||
| 803 | + } | ||
| 804 | + _dynamo.has_triton.cache_clear() | ||
| 805 | + with mock.patch.object( | ||
| 806 | + torch.utils._triton, | ||
| 807 | + "has_triton_package", | ||
| 808 | + return_value=package, | ||
| 809 | + ), mock.patch.object( | ||
| 810 | + device_interface, | ||
| 811 | + "get_interface_for_device", | ||
| 812 | + side_effect=interfaces.__getitem__, | ||
| 813 | + ), mock.patch.object( | ||
| 814 | + _dynamo, | ||
| 815 | + "_dynamo_register_interface_for_device", | ||
| 816 | + ) as register: | ||
| 817 | + result = _dynamo.has_triton() | ||
| 818 | + if package: | ||
| 819 | + register.assert_called_once_with() | ||
| 820 | + else: | ||
| 821 | + register.assert_not_called() | ||
| 822 | + return result | ||
| 823 | + | ||
| 824 | + assert not run_case({}, package=False) | ||
| 825 | + assert run_case({"cuda"}) | ||
| 826 | + assert run_case({"xpu"}) | ||
| 827 | + triton = types.ModuleType("triton") | ||
| 828 | + triton_backends = types.ModuleType("triton.backends") | ||
| 829 | + triton_backends.backends = {"cpu": object()} | ||
| 830 | + triton.backends = triton_backends | ||
| 831 | + with mock.patch.dict( | ||
| 832 | + sys.modules, | ||
| 833 | + {"triton": triton, "triton.backends": triton_backends}, | ||
| 834 | + ): | ||
| 835 | + assert run_case({"cpu"}) | ||
| 836 | + triton_backends.backends = {} | ||
| 837 | + assert not run_case({"cpu"}) | ||
| 838 | + assert run_case({"npu"}) | ||
| 839 | + """ | ||
| 840 | + ) | ||
| 841 | + | ||
| 842 | + # Verify all legacy Dynamo patches remain installed exactly once. | ||
| 843 | + def test_dynamo_patch_inventory_is_preserved(self): | ||
| 844 | + self.run_in_subprocess( | ||
| 845 | + """ | ||
| 846 | + import torch | ||
| 847 | + import torch_npu | ||
| 848 | + | ||
| 849 | + # Importing the Dynamo parent package is the lazy setup boundary. | ||
| 850 | + import torch._dynamo | ||
| 851 | + | ||
| 852 | + from torch._dynamo.device_interface import get_interface_for_device | ||
| 853 | + from torch._dynamo.variables.builtin import BuiltinVariable | ||
| 854 | + from torch._dynamo.variables.builder import VariableBuilder | ||
| 855 | + from torch._dynamo.variables.ctx_manager import EventVariable | ||
| 856 | + from torch._dynamo.variables.functions import SkipFunctionVariable | ||
| 857 | + from torch._dynamo.variables.tensor import TensorVariable | ||
| 858 | + from torch._dynamo.variables.torch import constant_fold_functions | ||
| 859 | + from torch._dynamo.variables.user_defined import UserDefinedClassVariable | ||
| 860 | + from torch._dynamo.utils import common_constant_types | ||
| 861 | + from torch_npu.dynamo.trace_rule import ( | ||
| 862 | + skip_functions_npu, | ||
| 863 | + torch_c_binding_in_graph_functions_npu, | ||
| 864 | + torch_non_c_binding_in_graph_functions_npu, | ||
| 865 | + ) | ||
| 866 | + from torch_npu.utils import _dynamo | ||
| 867 | + | ||
| 868 | + assert _dynamo._lazy_dynamo_setup.has_run | ||
| 869 | + assert get_interface_for_device("npu").device_count() > 0 | ||
| 870 | + | ||
| 871 | + # VariableTracker and context-manager patches formerly installed | ||
| 872 | + # eagerly by add_dynamo_methods(). | ||
| 873 | + assert SkipFunctionVariable.__new__.__module__ == "torch_npu.utils._dynamo" | ||
| 874 | + assert TensorVariable.call_method.__module__ == "torch_npu.utils._dynamo" | ||
| 875 | + assert UserDefinedClassVariable.__new__.__module__ == "torch_npu.utils._dynamo" | ||
| 876 | + in_graph_classes = UserDefinedClassVariable._in_graph_classes() | ||
| 877 | + assert torch.npu.Event in in_graph_classes | ||
| 878 | + assert torch.npu.Stream in in_graph_classes | ||
| 879 | + assert torch.npu.fake_record_stream is _dynamo.fake_record_stream | ||
| 880 | + assert TensorVariable.method_record_stream.__module__ == "torch_npu.utils._dynamo" | ||
| 881 | + assert VariableBuilder._wrap.__module__ == "torch_npu.utils._dynamo" | ||
| 882 | + assert BuiltinVariable.call_id.__module__ == "torch_npu.utils._dynamo" | ||
| 883 | + assert EventVariable.python_type.__module__ == "torch_npu.utils._dynamo" | ||
| 884 | + assert torch._dynamo.optimize.__module__ == "torch_npu.utils._dynamo" | ||
| 885 | + | ||
| 886 | + # Backend and trace-rule registrations formerly performed by | ||
| 887 | + # registry_manager._register_dynamo(). Count the maps as well as | ||
| 888 | + # checking membership so repeated lazy triggers cannot hide a | ||
| 889 | + # duplicate installation. | ||
| 890 | + assert {"npu", "npugraph_ex"}.issubset( | ||
| 891 | + torch._dynamo.list_backends(exclude_tags=None) | ||
| 892 | + ) | ||
| 893 | + maps = torch._dynamo.trace_rules.torch_name_rule_map | ||
| 894 | + assert maps.count(torch_non_c_binding_in_graph_functions_npu) == 1 | ||
| 895 | + assert maps.count(torch_c_binding_in_graph_functions_npu) == 1 | ||
| 896 | + assert maps.count(skip_functions_npu) == 1 | ||
| 897 | + assert constant_fold_functions[torch.npu.current_device] | ||
| 898 | + assert constant_fold_functions[torch.npu.get_device_properties] | ||
| 899 | + assert constant_fold_functions[torch.npu.is_available] | ||
| 900 | + assert torch_npu._C._NPUDeviceProperties in common_constant_types | ||
| 901 | + """ | ||
| 902 | + ) | ||
| 903 | + | ||
| 904 | + # Verify all legacy Inductor patches remain installed. | ||
| 905 | + def test_inductor_patch_inventory_is_preserved(self): | ||
| 906 | + self.run_in_subprocess( | ||
| 907 | + """ | ||
| 908 | + import torch | ||
| 909 | + import torch_npu | ||
| 910 | + | ||
| 911 | + # RNG/decomposition patches remain installed at import time. | ||
| 912 | + from torch_npu.utils import _inductor | ||
| 913 | + | ||
| 914 | + assert ( | ||
| 915 | + torch._decomp.decompositions._max_unpoolnd | ||
| 916 | + is _inductor._max_unpoolnd_patch | ||
| 917 | + ) | ||
| 918 | + assert torch._prims.rng_prims.philox_rand_offset.__module__ == ( | ||
| 919 | + "torch_npu.utils._inductor" | ||
| 920 | + ) | ||
| 921 | + assert torch._prims.rng_prims.register_philox_rand.__module__ == ( | ||
| 922 | + "torch_npu.utils._inductor" | ||
| 923 | + ) | ||
| 924 | + assert torch._prims.rng_prims.get_device.__module__ == ( | ||
| 925 | + "torch_npu.utils._inductor" | ||
| 926 | + ) | ||
| 927 | + | ||
| 928 | + # Exercise the new full-Inductor setup boundary without relying on | ||
| 929 | + # test ordering or on a prior torch.compile invocation. | ||
| 930 | + import torch._dynamo | ||
| 931 | + from torch_npu.utils import _dynamo | ||
| 932 | + | ||
| 933 | + _dynamo._lazy_inductor_setup() | ||
| 934 | + | ||
| 935 | + import torch._inductor.compile_fx as compile_fx | ||
| 936 | + import torch._inductor.cudagraph_trees as cudagraph_trees | ||
| 937 | + import torch._inductor.cudagraph_utils as cudagraph_utils | ||
| 938 | + import torch._inductor.scheduler as scheduler | ||
| 939 | + from torch._inductor.codegen.common import get_device_op_overrides | ||
| 940 | + from torch._inductor.codecache import CacheBase | ||
| 941 | + from torch._inductor.graph import GraphLowering | ||
| 942 | + from torch._inductor.utils import GPU_TYPES | ||
| 943 | + from torch_npu.utils import _graph_tree | ||
| 944 | + | ||
| 945 | + assert _dynamo._lazy_inductor_setup.has_run | ||
| 946 | + assert "npu" in GPU_TYPES | ||
| 947 | + assert get_device_op_overrides("npu").__class__.__module__.startswith( | ||
| 948 | + "torch_npu._inductor" | ||
| 949 | + ) | ||
| 950 | + assert torch.utils._triton.has_triton.__module__ == ( | ||
| 951 | + "torch_npu.utils._dynamo" | ||
| 952 | + ) | ||
| 953 | + assert torch.utils._triton._device_supports_tma.__module__ == ( | ||
| 954 | + "torch_npu._inductor.utils" | ||
| 955 | + ) | ||
| 956 | + assert compile_fx.has_triton is torch.utils._triton.has_triton | ||
| 957 | + assert scheduler.has_triton is torch.utils._triton.has_triton | ||
| 958 | + assert GraphLowering.codegen_with_cpp_wrapper.__module__ == ( | ||
| 959 | + "torch_npu._inductor.graph" | ||
| 960 | + ) | ||
| 961 | + assert CacheBase.get_system.__module__ == ( | ||
| 962 | + "torch_npu._inductor.codegen.common" | ||
| 963 | + ) | ||
| 964 | + | ||
| 965 | + # NPUGraph integrations were formerly applied eagerly alongside | ||
| 966 | + # the Inductor patches. | ||
| 967 | + assert compile_fx.cudagraphify is _graph_tree.npugraphify | ||
| 968 | + assert ( | ||
| 969 | + cudagraph_utils.check_multiple_devices_or_any_cpu_nodes | ||
| 970 | + is _graph_tree.check_multiple_devices_or_any_cpu_nodes | ||
| 971 | + ) | ||
| 972 | + assert cudagraph_trees.get_manager.__module__ == ( | ||
| 973 | + "torch_npu.utils._graph_tree" | ||
| 974 | + ) | ||
| 975 | + assert torch.compiler.npugraph_mark_step_begin is ( | ||
| 976 | + _graph_tree.npugraph_mark_step_begin | ||
| 977 | + ) | ||
| 978 | + | ||
| 979 | + config = torch._inductor.config | ||
| 980 | + assert config.npu_backend == "default" | ||
| 981 | + assert config.enable_shape_handling is False | ||
| 982 | + assert config.shape_handling_configs == [] | ||
| 983 | + assert config.shape_handling_dict is None | ||
| 984 | + """ | ||
| 985 | + ) | ||
| 986 | + | ||
| 987 | + | ||
| 988 | +if __name__ == "__main__": | ||
| 989 | + unittest.main() | ||
| @@ -23,6 +23,11 @@ class TestNpuStream(TestCase): | |||
| 23 | def test_get_current_stream_interface(self): | 23 | def test_get_current_stream_interface(self): |
| 24 | from torch_npu._C import _npu_getCurrentRawStream, _npu_getCurrentRawStreamNoWait | 24 | from torch_npu._C import _npu_getCurrentRawStream, _npu_getCurrentRawStreamNoWait |
| 25 | from torch._dynamo.device_interface import get_interface_for_device | 25 | from torch._dynamo.device_interface import get_interface_for_device |
| 26 | + from torch_npu.utils._dynamo import _dynamo_register_interface_for_device | ||
| 27 | + | ||
| 28 | + # device_interface is an internal Dynamo module. Initialize its NPU | ||
| 29 | + # registration explicitly instead of relying on import torch_npu. | ||
| 30 | + _dynamo_register_interface_for_device() | ||
| 26 | 31 | ||
| 27 | device_number = torch.npu.device_count() | 32 | device_number = torch.npu.device_count() |
| 28 | for i in range(device_number): | 33 | for i in range(device_number): |
| @@ -33,14 +33,15 @@ EXPECTED_LOADED_MODULES = [ | |||
| 33 | "torch_npu.op_plugin.meta._meta_registrations", | 33 | "torch_npu.op_plugin.meta._meta_registrations", |
| 34 | "torch_npu.asd.checksum", | 34 | "torch_npu.asd.checksum", |
| 35 | "torch_npu.utils._dynamo", | 35 | "torch_npu.utils._dynamo", |
| 36 | - "torch_npu.utils._inductor", | ||
| 37 | "torch_npu.utils.custom_ops", | 36 | "torch_npu.utils.custom_ops", |
| 38 | "torch_npu.utils.patch_getenv", | 37 | "torch_npu.utils.patch_getenv", |
| 39 | "torch_npu.utils.syncbatchnorm", | 38 | "torch_npu.utils.syncbatchnorm", |
| 39 | + "torch_npu.utils._inductor", | ||
| 40 | ] | 40 | ] |
| 41 | 41 | ||
| 42 | EXPECTED_NOT_LOADED_MODULES = [ | 42 | EXPECTED_NOT_LOADED_MODULES = [ |
| 43 | "torch_npu._C._afd", | 43 | "torch_npu._C._afd", |
| 44 | + "torch_npu._inductor", | ||
| 44 | ] | 45 | ] |
| 45 | 46 | ||
| 46 | EXPECTED_TOP_LEVEL_ATTRS = [ | 47 | EXPECTED_TOP_LEVEL_ATTRS = [ |
| @@ -255,20 +256,6 @@ class TestTorchNpuBootstrap(TestCase): | |||
| 255 | import torch.distributed as dist | 256 | import torch.distributed as dist |
| 256 | import torch.distributed.rpc as rpc | 257 | import torch.distributed.rpc as rpc |
| 257 | import torch.distributed.tensor # noqa: F401 | 258 | import torch.distributed.tensor # noqa: F401 |
| 258 | - from torch._dynamo.device_interface import get_interface_for_device | ||
| 259 | - from torch._dynamo.backends.registry import _BACKENDS | ||
| 260 | - from torch._inductor.codegen.common import device_op_overrides_dict | ||
| 261 | - | ||
| 262 | - iface = get_interface_for_device("npu") | ||
| 263 | - assert iface is not None | ||
| 264 | - | ||
| 265 | - assert "npu" in _BACKENDS, "npu dynamo backend is not registered" | ||
| 266 | - assert "npugraph_ex" in _BACKENDS, ( | ||
| 267 | - "npugraph_ex dynamo backend is not registered" | ||
| 268 | - ) | ||
| 269 | - | ||
| 270 | - assert "npu" in device_op_overrides_dict | ||
| 271 | - assert device_op_overrides_dict.get("npu") is not None | ||
| 272 | 259 | ||
| 273 | assert "hccl" in dist.Backend.backend_list | 260 | assert "hccl" in dist.Backend.backend_list |
| 274 | assert "lccl" in dist.Backend.backend_list | 261 | assert "lccl" in dist.Backend.backend_list |
| @@ -588,5 +575,63 @@ class TestTorchNpuBootstrap(TestCase): | |||
| 588 | """ | 575 | """ |
| 589 | ) | 576 | ) |
| 590 | 577 | ||
| 578 | + def test_13_dtensor_strategies_are_registered_without_compiler_imports(self): | ||
| 579 | + self._run_python( | ||
| 580 | + """ | ||
| 581 | + import sys | ||
| 582 | + import torch | ||
| 583 | + import torch_npu | ||
| 584 | + from torch.distributed.tensor import DTensor | ||
| 585 | + | ||
| 586 | + strategy_funcs = DTensor._op_dispatcher.sharding_propagator.op_strategy_funcs | ||
| 587 | + assert torch.ops.npu.npu_rms_norm.default in strategy_funcs | ||
| 588 | + assert torch.ops.npu.npu_fusion_attention.default in strategy_funcs | ||
| 589 | + | ||
| 590 | + assert "torch_npu.distributed.tensor" in sys.modules | ||
| 591 | + assert "torch_npu.distributed.tensor.experimental" in sys.modules | ||
| 592 | + assert "torch._dynamo" not in sys.modules | ||
| 593 | + assert "torch._inductor" not in sys.modules | ||
| 594 | + | ||
| 595 | + import os | ||
| 596 | + import tempfile | ||
| 597 | + import torch.distributed as dist | ||
| 598 | + from torch.distributed.device_mesh import DeviceMesh | ||
| 599 | + from torch.distributed.tensor import Replicate | ||
| 600 | + from torch.distributed.tensor._dtensor_spec import DTensorSpec, TensorMeta | ||
| 601 | + from torch.distributed.tensor._op_schema import OpSchema, OpSpec, OpStrategy | ||
| 602 | + | ||
| 603 | + fd, path = tempfile.mkstemp() | ||
| 604 | + os.close(fd) | ||
| 605 | + os.unlink(path) | ||
| 606 | + dist.init_process_group( | ||
| 607 | + "gloo", init_method=f"file://{path}", rank=0, world_size=1 | ||
| 608 | + ) | ||
| 609 | + try: | ||
| 610 | + mesh = DeviceMesh("cpu", [0]) | ||
| 611 | + tensor_meta = TensorMeta( | ||
| 612 | + torch.Size([2, 4, 8]), (32, 8, 1), torch.float32 | ||
| 613 | + ) | ||
| 614 | + spec = DTensorSpec(mesh, (Replicate(),), tensor_meta=tensor_meta) | ||
| 615 | + strategy = OpStrategy([OpSpec(output_specs=spec)]) | ||
| 616 | + op = torch.ops.npu.npu_rotary_mul.default | ||
| 617 | + propagator = DTensor._op_dispatcher.sharding_propagator | ||
| 618 | + op_schema = OpSchema( | ||
| 619 | + op, | ||
| 620 | + (strategy, strategy, strategy, "half"), | ||
| 621 | + {}, | ||
| 622 | + propagator.op_to_schema_info[op], | ||
| 623 | + ) | ||
| 624 | + result = propagator.op_strategy_funcs[op](op_schema) | ||
| 625 | + assert len(result.strategies) == 3 | ||
| 626 | + finally: | ||
| 627 | + dist.destroy_process_group() | ||
| 628 | + if os.path.exists(path): | ||
| 629 | + os.remove(path) | ||
| 630 | + | ||
| 631 | + from torch_npu.distributed.tensor import experimental | ||
| 632 | + assert callable(experimental.context_parallel) | ||
| 633 | + """ | ||
| 634 | + ) | ||
| 635 | + | ||
| 591 | if __name__ == "__main__": | 636 | if __name__ == "__main__": |
| 592 | run_tests() | 637 | run_tests() |
Dtest/utils/test_inductor.py+0-33
| @@ -1,33 +0,0 @@ | |||
| 1 | -from torch_npu.testing.testcase import TestCase, run_tests | ||
| 2 | -from torch_npu.utils._inductor import NPUDeviceOpOverrides | ||
| 3 | - | ||
| 4 | - | ||
| 5 | -class TestInductor(): | ||
| 6 | - | ||
| 7 | - def test_device_guard(self): | ||
| 8 | - overrides = NPUDeviceOpOverrides() | ||
| 9 | - result = overrides.device_guard(1) | ||
| 10 | - expected = "torch_npu.npu._DeviceGuard(1)" | ||
| 11 | - self.assertEqual(result, expected) | ||
| 12 | - | ||
| 13 | - def test_synchronize(self): | ||
| 14 | - overrides = NPUDeviceOpOverrides() | ||
| 15 | - result = overrides.synchronize() | ||
| 16 | - expected = "torch_npu.npu.synchronize()" | ||
| 17 | - self.assertEqual(result, expected) | ||
| 18 | - | ||
| 19 | - def test_set_device(self): | ||
| 20 | - overrides = NPUDeviceOpOverrides() | ||
| 21 | - result = overrides.set_device(0) | ||
| 22 | - expected = "torch_npu.npu.set_device(0)" | ||
| 23 | - self.assertEqual(result, expected) | ||
| 24 | - | ||
| 25 | - def test_import_get_raw_stream_as(self): | ||
| 26 | - overrides = NPUDeviceOpOverrides() | ||
| 27 | - result = overrides.import_get_raw_stream_as("test_name") | ||
| 28 | - expected = "from torch._C import _npu_getCurrentRawStream as test_name" | ||
| 29 | - self.assertEqual(result, expected) | ||
| 30 | - | ||
| 31 | - | ||
| 32 | -if __name__ == "__main__": | ||
| 33 | - run_tests() | ||
| @@ -32,6 +32,7 @@ def _register_triton_decompositions(): | |||
| 32 | aten.nll_loss_forward, | 32 | aten.nll_loss_forward, |
| 33 | aten.nll_loss_backward, | 33 | aten.nll_loss_backward, |
| 34 | aten._log_softmax_backward_data, | 34 | aten._log_softmax_backward_data, |
| 35 | + aten.erfc, | ||
| 35 | aten.gelu, | 36 | aten.gelu, |
| 36 | aten.native_layer_norm, | 37 | aten.native_layer_norm, |
| 37 | aten.slice_backward, | 38 | aten.slice_backward, |
| @@ -23,6 +23,7 @@ from torch._inductor.kernel.mm_common import ( | |||
| 23 | addmm_epilogue, | 23 | addmm_epilogue, |
| 24 | mm_args, | 24 | mm_args, |
| 25 | ) | 25 | ) |
| 26 | +from torch._inductor.kernel import bmm as inductor_bmm | ||
| 26 | 27 | ||
| 27 | from .mm import is_contiguous_striding | 28 | from .mm import is_contiguous_striding |
| 28 | from ..utils import use_catlass_template, use_triton_template | 29 | from ..utils import use_catlass_template, use_triton_template |
| @@ -31,8 +32,8 @@ from ..utils import use_catlass_template, use_triton_template | |||
| 31 | log = logging.getLogger("torch._inductor") | 32 | log = logging.getLogger("torch._inductor") |
| 32 | aten = torch.ops.aten | 33 | aten = torch.ops.aten |
| 33 | 34 | ||
| 34 | -aten_bmm = torch._inductor.kernel.bmm.aten_bmm | 35 | +aten_bmm = inductor_bmm.aten_bmm |
| 35 | -aten_baddbmm = torch._inductor.kernel.bmm.aten_baddbmm | 36 | +aten_baddbmm = inductor_bmm.aten_baddbmm |
| 36 | 37 | ||
| 37 | 38 | ||
| 38 | def is_batch_stride_largest_or_zero(mat1, mat2, layout) -> bool: | 39 | def is_batch_stride_largest_or_zero(mat1, mat2, layout) -> bool: |
| @@ -5,7 +5,6 @@ import logging | |||
| 5 | import os | 5 | import os |
| 6 | 6 | ||
| 7 | import torch | 7 | import torch |
| 8 | -import torch_npu | ||
| 9 | 8 | ||
| 10 | import torch._inductor.config as inductor_config | 9 | import torch._inductor.config as inductor_config |
| 11 | log = logging.getLogger("torch._inductor") | 10 | log = logging.getLogger("torch._inductor") |
| @@ -28,10 +27,11 @@ def resolve_npu_device_index(device_idx=None) -> int: | |||
| 28 | 27 | ||
| 29 | 28 | ||
| 30 | def patch_has_triton(): | 29 | def patch_has_triton(): |
| 30 | + from torch._inductor import compile_fx | ||
| 31 | from torch_npu.utils._dynamo import has_triton | 31 | from torch_npu.utils._dynamo import has_triton |
| 32 | 32 | ||
| 33 | torch._inductor.scheduler.has_triton = has_triton | 33 | torch._inductor.scheduler.has_triton = has_triton |
| 34 | - torch._inductor.compile_fx.has_triton = has_triton | 34 | + compile_fx.has_triton = has_triton |
| 35 | 35 | ||
| 36 | 36 | ||
| 37 | def patch_device_supports_tma(): | 37 | def patch_device_supports_tma(): |
| @@ -6,10 +6,3 @@ def apply_dynamo_methods_patch(): | |||
| 6 | from torch_npu.utils._dynamo import add_dynamo_methods | 6 | from torch_npu.utils._dynamo import add_dynamo_methods |
| 7 | 7 | ||
| 8 | add_dynamo_methods() | 8 | add_dynamo_methods() |
| 9 | - | ||
| 10 | - | ||
| 11 | - | ||
| 12 | -def apply_npugraph_tree_patch(): | ||
| 13 | - from torch_npu.utils._graph_tree import _apply_npugraph_tree_methods | ||
| 14 | - | ||
| 15 | - _apply_npugraph_tree_methods() | ||
| @@ -49,25 +49,6 @@ def _register_distributed(): | |||
| 49 | register_distributed_backend_for_npu() | 49 | register_distributed_backend_for_npu() |
| 50 | 50 | ||
| 51 | 51 | ||
| 52 | -def _register_dynamo(): | ||
| 53 | - """ | ||
| 54 | - Register Dynamo integration: | ||
| 55 | - - Dynamo backend | ||
| 56 | - - Dynamo device interface | ||
| 57 | - - NPU trace rules for Dynamo | ||
| 58 | - """ | ||
| 59 | - from torch_npu._init.registry.dynamo import ( | ||
| 60 | - register_dynamo_backends, | ||
| 61 | - register_dynamo_trace_rules, | ||
| 62 | - ) | ||
| 63 | - | ||
| 64 | - register_dynamo_backends() | ||
| 65 | - | ||
| 66 | - # Do not repeat this call for register_dynamo_trace_rules appends rules into | ||
| 67 | - # Dynamo's global rules maps. | ||
| 68 | - register_dynamo_trace_rules() | ||
| 69 | - | ||
| 70 | - | ||
| 71 | def _register_rpc(): | 52 | def _register_rpc(): |
| 72 | """ | 53 | """ |
| 73 | Register and init RPC NPU backend. | 54 | Register and init RPC NPU backend. |
| @@ -100,8 +81,8 @@ def _register_components(): | |||
| 100 | 81 | ||
| 101 | Order matters: | 82 | Order matters: |
| 102 | 1. NPU backend is the base capability. | 83 | 1. NPU backend is the base capability. |
| 103 | - 2. Distributed and Dynamo depend on NPU backend / _C children. | 84 | + 2. Distributed depends on the NPU backend / _C children. |
| 104 | - 3. RPC, dtensor and inductor are Python-side framework integrations. | 85 | + 3. RPC and inductor are Python-side framework integrations. |
| 105 | 4. DefaultDeviceType is set after NPU backend is registered. | 86 | 4. DefaultDeviceType is set after NPU backend is registered. |
| 106 | """ | 87 | """ |
| 107 | if not hasattr(torch_npu, "_C"): | 88 | if not hasattr(torch_npu, "_C"): |
| @@ -111,7 +92,6 @@ def _register_components(): | |||
| 111 | 92 | ||
| 112 | _register_npu_backend() | 93 | _register_npu_backend() |
| 113 | _register_distributed() | 94 | _register_distributed() |
| 114 | - _register_dynamo() | ||
| 115 | _register_rpc() | 95 | _register_rpc() |
| 116 | _register_inductor() | 96 | _register_inductor() |
| 117 | _register_default_gradient_device_type() | 97 | _register_default_gradient_device_type() |
| @@ -436,6 +436,18 @@ def _compose_wrappers(*wrappers): | |||
| 436 | 436 | ||
| 437 | 437 | ||
| 438 | def _init(): | 438 | def _init(): |
| 439 | + # transfer_to_npu patches these modules during its own import. Import them | ||
| 440 | + # explicitly instead of relying on torch_npu import side effects. | ||
| 441 | + import torch._dynamo.trace_rules # noqa: F401 | ||
| 442 | + import torch._dynamo.utils # noqa: F401 | ||
| 443 | + import torch._inductor.runtime.autotune_cache # noqa: F401 | ||
| 444 | + import torch._inductor.compile_fx # noqa: F401 | ||
| 445 | + import torch._inductor.utils # noqa: F401 | ||
| 446 | + import torch._inductor.fx_passes.post_grad # noqa: F401 | ||
| 447 | + import torch._inductor.fx_passes.joint_graph # noqa: F401 | ||
| 448 | + import torch._inductor.autotune_process # noqa: F401 | ||
| 449 | + from torch.distributed.checkpoint import filesystem | ||
| 450 | + | ||
| 439 | _warning_fn(''' | 451 | _warning_fn(''' |
| 440 | ************************************************************************************************************* | 452 | ************************************************************************************************************* |
| 441 | The torch.Tensor.cuda and torch.nn.Module.cuda are replaced with torch.Tensor.npu and torch.nn.Module.npu now.. | 453 | The torch.Tensor.cuda and torch.nn.Module.cuda are replaced with torch.Tensor.npu and torch.nn.Module.npu now.. |
| @@ -540,7 +552,7 @@ def _init(): | |||
| 540 | setattr(torch._inductor.autotune_process, "get_gpu_type", _get_npu_type) | 552 | setattr(torch._inductor.autotune_process, "get_gpu_type", _get_npu_type) |
| 541 | 553 | ||
| 542 | setattr(torch._utils, '_get_available_device_type', _patch_get_available_device_type) | 554 | setattr(torch._utils, '_get_available_device_type', _patch_get_available_device_type) |
| 543 | - setattr(torch.distributed.checkpoint.filesystem._OverlappingCpuLoader, '__init__', | 555 | + setattr(filesystem._OverlappingCpuLoader, '__init__', |
| 544 | _patch_OverlappingCpuLoader_init_) | 556 | _patch_OverlappingCpuLoader_init_) |
| 545 | 557 | ||
| 546 | _replace_to_method_in_allowed_methods() | 558 | _replace_to_method_in_allowed_methods() |
| @@ -3,9 +3,6 @@ import sys | |||
| 3 | import time | 3 | import time |
| 4 | import warnings | 4 | import warnings |
| 5 | 5 | ||
| 6 | -from torch._dynamo import register_backend as _register_backend | ||
| 7 | -from torch._dynamo.backends.registry import _BACKENDS | ||
| 8 | - | ||
| 9 | from torch_npu._init.common.warning_utils import _should_print_warning | 6 | from torch_npu._init.common.warning_utils import _should_print_warning |
| 10 | from torch_npu.utils._error_code import ErrCode, pta_error | 7 | from torch_npu.utils._error_code import ErrCode, pta_error |
| 11 | 8 | ||
| @@ -107,6 +104,15 @@ class _LazyTorchair(_LazyBackend): | |||
| 107 | return getattr(torchair, name) | 104 | return getattr(torchair, name) |
| 108 | 105 | ||
| 109 | 106 | ||
| 107 | +def _install_lazy_torchair(): | ||
| 108 | + torchair_path = os.path.join(os.path.dirname(__file__), "torchair") | ||
| 109 | + if not os.path.exists(torchair_path): | ||
| 110 | + return False | ||
| 111 | + if "torchair" not in sys.modules: | ||
| 112 | + sys.modules["torchair"] = _LazyTorchair("torchair") | ||
| 113 | + return True | ||
| 114 | + | ||
| 115 | + | ||
| 110 | class _LazyNpuGraphEx(_LazyBackend): | 116 | class _LazyNpuGraphEx(_LazyBackend): |
| 111 | def __init__(self, pkg_name): | 117 | def __init__(self, pkg_name): |
| 112 | self._npugraph_ex = None | 118 | self._npugraph_ex = None |
| @@ -140,7 +146,7 @@ def _lazy_exec(*args, **kwargs): | |||
| 140 | 146 | ||
| 141 | 147 | ||
| 142 | def _get_default_backend(name): | 148 | def _get_default_backend(name): |
| 143 | - if not os.path.exists(os.path.join(os.path.dirname(__file__), 'torchair')): | 149 | + if not _install_lazy_torchair(): |
| 144 | if _should_print_warning(): | 150 | if _should_print_warning(): |
| 145 | warnings.warn( | 151 | warnings.warn( |
| 146 | "Register eager implementation for the 'npu' backend of dynamo, " | 152 | "Register eager implementation for the 'npu' backend of dynamo, " |
| @@ -149,7 +155,6 @@ def _get_default_backend(name): | |||
| 149 | 155 | ||
| 150 | global _global_backend_name | 156 | global _global_backend_name |
| 151 | _global_backend_name = name | 157 | _global_backend_name = name |
| 152 | - sys.modules['torchair'] = _LazyTorchair('torchair') | ||
| 153 | return _lazy_exec | 158 | return _lazy_exec |
| 154 | 159 | ||
| 155 | 160 | ||
| @@ -166,6 +171,27 @@ def _get_npugraph_ex_backend(): | |||
| 166 | 171 | ||
| 167 | 172 | ||
| 168 | def _register_npu_backend(backend, name="npu"): | 173 | def _register_npu_backend(backend, name="npu"): |
| 174 | + from torch._dynamo import register_backend as _register_backend | ||
| 175 | + from torch._dynamo.backends.registry import _BACKENDS, _COMPILER_FNS | ||
| 176 | + | ||
| 177 | + registered_backend = _COMPILER_FNS.get(name) | ||
| 178 | + if ( | ||
| 179 | + registered_backend is not None | ||
| 180 | + and getattr(registered_backend, "__module__", None) == __name__ | ||
| 181 | + ): | ||
| 182 | + return | ||
| 183 | + | ||
| 184 | + # When a setuptools entry point is currently loading torch_npu, Dynamo has | ||
| 185 | + # already put the EntryPoint object in _BACKENDS but has not registered the | ||
| 186 | + # loaded callable yet. Leave that registration to lookup_backend(); doing | ||
| 187 | + # it here as well makes lookup_backend register the same name twice. | ||
| 188 | + pending_backend = _BACKENDS.get(name) | ||
| 189 | + if ( | ||
| 190 | + name not in _COMPILER_FNS | ||
| 191 | + and getattr(pending_backend, "module", None) == __name__ | ||
| 192 | + ): | ||
| 193 | + return | ||
| 194 | + | ||
| 169 | if name in _BACKENDS.keys(): | 195 | if name in _BACKENDS.keys(): |
| 170 | del _BACKENDS[name] | 196 | del _BACKENDS[name] |
| 171 | _register_backend(backend, name) | 197 | _register_backend(backend, name) |
| @@ -177,3 +203,43 @@ def _register_backends(): | |||
| 177 | 203 | ||
| 178 | _register_npu_backend(global_backend) | 204 | _register_npu_backend(global_backend) |
| 179 | _register_npu_backend(npugraph_ex_backend, NPUGRAPH_EX_BACKEND) | 205 | _register_npu_backend(npugraph_ex_backend, NPUGRAPH_EX_BACKEND) |
| 206 | + | ||
| 207 | + | ||
| 208 | +def _npu_backend_entrypoint(gm, example_inputs, **kwargs): | ||
| 209 | + """Set up the NPU Dynamo backend when an entry point is selected.""" | ||
| 210 | + from torch_npu.utils._dynamo import _lazy_dynamo_setup | ||
| 211 | + | ||
| 212 | + _lazy_dynamo_setup() | ||
| 213 | + return _get_default_backend("npu")(gm, example_inputs, **kwargs) | ||
| 214 | + | ||
| 215 | + | ||
| 216 | +def _npugraph_ex_backend_entrypoint(gm, example_inputs, **kwargs): | ||
| 217 | + """Set up the NPUGraph-EX backend when an entry point is selected.""" | ||
| 218 | + from torch_npu.utils._dynamo import _lazy_dynamo_setup | ||
| 219 | + | ||
| 220 | + _lazy_dynamo_setup() | ||
| 221 | + return _exec(gm, example_inputs, **kwargs) | ||
| 222 | + | ||
| 223 | + | ||
| 224 | +class _NpugraphsBackendEntryPoint: | ||
| 225 | + """Keep the lazy entry point and its reset protocol in one callable.""" | ||
| 226 | + | ||
| 227 | + compiler_name = "npugraphs" | ||
| 228 | + | ||
| 229 | + def __call__(self, gm, example_inputs, **kwargs): | ||
| 230 | + from torch_npu.utils._dynamo import _lazy_dynamo_setup, _lazy_inductor_setup | ||
| 231 | + | ||
| 232 | + _lazy_dynamo_setup() | ||
| 233 | + _lazy_inductor_setup() | ||
| 234 | + from torch_npu.utils._graph_tree import NpugraphsBackend | ||
| 235 | + | ||
| 236 | + return NpugraphsBackend()(gm, example_inputs, **kwargs) | ||
| 237 | + | ||
| 238 | + | ||
| 239 | + def reset(): | ||
| 240 | + graph_tree = sys.modules.get("torch_npu.npu._graph_tree") | ||
| 241 | + if graph_tree is not None: | ||
| 242 | + graph_tree.reset_npugraph_trees() | ||
| 243 | + | ||
| 244 | + | ||
| 245 | +_npugraphs_backend_entrypoint = _NpugraphsBackendEntryPoint() | ||
| @@ -151,6 +151,7 @@ import traceback | |||
| 151 | import threading | 151 | import threading |
| 152 | import os | 152 | import os |
| 153 | import re | 153 | import re |
| 154 | +import importlib | ||
| 154 | import torch | 155 | import torch |
| 155 | from torch.storage import _LegacyStorage, _warn_typed_storage_removal | 156 | from torch.storage import _LegacyStorage, _warn_typed_storage_removal |
| 156 | from torch._utils import classproperty | 157 | from torch._utils import classproperty |
| @@ -172,8 +173,6 @@ from .autocast_utils import * # noqa: F403 | |||
| 172 | from .backends import * # noqa: F403 | 173 | from .backends import * # noqa: F403 |
| 173 | from ._backends import * # noqa: F403 | 174 | from ._backends import * # noqa: F403 |
| 174 | from .deterministic import enable_deterministic_with_backward, disable_deterministic_with_backward # noqa: F403 | 175 | from .deterministic import enable_deterministic_with_backward, disable_deterministic_with_backward # noqa: F403 |
| 175 | -from . import npugraph_ex | ||
| 176 | - | ||
| 177 | from .graphs import ( | 176 | from .graphs import ( |
| 178 | NPUGraph, | 177 | NPUGraph, |
| 179 | graph, | 178 | graph, |
| @@ -194,6 +193,18 @@ from ._npugraph_handlers import ( | |||
| 194 | ) | 193 | ) |
| 195 | 194 | ||
| 196 | 195 | ||
| 196 | +def __getattr__(name): | ||
| 197 | + if name == "npugraph_ex": | ||
| 198 | + module = importlib.import_module("torch_npu.npu.npugraph_ex") | ||
| 199 | + globals()[name] = module | ||
| 200 | + return module | ||
| 201 | + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | ||
| 202 | + | ||
| 203 | + | ||
| 204 | +def __dir__(): | ||
| 205 | + return sorted(set(globals()) | {"npugraph_ex"}) | ||
| 206 | + | ||
| 207 | + | ||
| 197 | config = npu_config._npuConfig() | 208 | config = npu_config._npuConfig() |
| 198 | 209 | ||
| 199 | matmul = npu_config._allowHF32Matmul() | 210 | matmul = npu_config._allowHF32Matmul() |
| @@ -102,6 +102,7 @@ from torch.utils._ordered_set import OrderedSet | |||
| 102 | from torch.utils.weak import TensorWeakRef | 102 | from torch.utils.weak import TensorWeakRef |
| 103 | 103 | ||
| 104 | import torch_npu | 104 | import torch_npu |
| 105 | +from torch_npu.npu._graph_tree_state import MarkStepBox, mark_step_begin | ||
| 105 | from torch_npu.npu import graphs as _npu_graphs | 106 | from torch_npu.npu import graphs as _npu_graphs |
| 106 | from torch_npu._C import ( | 107 | from torch_npu._C import ( |
| 107 | _npu_NPUAllocator_AllocatorState as AllocatorState, | 108 | _npu_NPUAllocator_AllocatorState as AllocatorState, |
| @@ -288,24 +289,12 @@ local.npu_tree_manager_containers = {} | |||
| 288 | local.npu_tree_manager_locks = defaultdict(threading.Lock) | 289 | local.npu_tree_manager_locks = defaultdict(threading.Lock) |
| 289 | 290 | ||
| 290 | 291 | ||
| 291 | -# only incremented by user call of mark_step_begin | ||
| 292 | -class MarkStepBox: | ||
| 293 | - mark_step_counter = 0 | ||
| 294 | - | ||
| 295 | - | ||
| 296 | # We need to register this as an object that will be copied over as TLS when new | 292 | # We need to register this as an object that will be copied over as TLS when new |
| 297 | # threads are created in autograd | 293 | # threads are created in autograd |
| 298 | torch._C._stash_obj_in_tls("npu_tree_manager_containers", local.npu_tree_manager_containers) | 294 | torch._C._stash_obj_in_tls("npu_tree_manager_containers", local.npu_tree_manager_containers) |
| 299 | torch._C._stash_obj_in_tls("npu_tree_manager_locks", local.npu_tree_manager_locks) | 295 | torch._C._stash_obj_in_tls("npu_tree_manager_locks", local.npu_tree_manager_locks) |
| 300 | 296 | ||
| 301 | 297 | ||
| 302 | -def mark_step_begin() -> None: | ||
| 303 | - "Indicates that a new iteration of inference or training is about to begin." | ||
| 304 | - | ||
| 305 | - # iterate down to distinguish from GenerationTracking counter | ||
| 306 | - MarkStepBox.mark_step_counter -= 1 | ||
| 307 | - | ||
| 308 | - | ||
| 309 | def reset_npugraph_trees() -> None: | 298 | def reset_npugraph_trees() -> None: |
| 310 | "Clear all npugraph trees" | 299 | "Clear all npugraph trees" |
| 311 | # see shutdown below for why this is necessary | 300 | # see shutdown below for why this is necessary |
| @@ -0,0 +1,11 @@ | |||
| 1 | +"""Lightweight state shared by the public NPUGraph marker and graph trees.""" | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +class MarkStepBox: | ||
| 5 | + # Negative values distinguish explicit user steps from Dynamo generations. | ||
| 6 | + mark_step_counter = 0 | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +def mark_step_begin() -> None: | ||
| 10 | + """Indicate that a new inference or training iteration is about to begin.""" | ||
| 11 | + MarkStepBox.mark_step_counter -= 1 | ||
| @@ -2,11 +2,19 @@ import torch | |||
| 2 | 2 | ||
| 3 | from torch import Tensor | 3 | from torch import Tensor |
| 4 | from torch.autograd.function import Function | 4 | from torch.autograd.function import Function |
| 5 | -from torch._dynamo.decorators import forbid_in_graph | ||
| 6 | 5 | ||
| 7 | __all__ = ["enable_deterministic_with_backward", "disable_deterministic_with_backward"] | 6 | __all__ = ["enable_deterministic_with_backward", "disable_deterministic_with_backward"] |
| 8 | 7 | ||
| 9 | 8 | ||
| 9 | +def _forbid_in_graph(fn): | ||
| 10 | + if isinstance(fn, (list, tuple)): | ||
| 11 | + return [_forbid_in_graph(x) for x in fn] | ||
| 12 | + if not callable(fn): | ||
| 13 | + raise AssertionError("forbid_in_graph applies only to callables") | ||
| 14 | + fn._dynamo_forbidden = True | ||
| 15 | + return fn | ||
| 16 | + | ||
| 17 | + | ||
| 10 | class _DeterministicAlgorithmsBeginOp(Function): | 18 | class _DeterministicAlgorithmsBeginOp(Function): |
| 11 | 19 | ||
| 12 | 20 | ||
| @@ -36,11 +44,11 @@ class _DeterministicAlgorithmsEndOp(Function): | |||
| 36 | return grad_outputs | 44 | return grad_outputs |
| 37 | 45 | ||
| 38 | 46 | ||
| 39 | -@forbid_in_graph | 47 | +@_forbid_in_graph |
| 40 | def enable_deterministic_with_backward(tensor: Tensor): | 48 | def enable_deterministic_with_backward(tensor: Tensor): |
| 41 | return _DeterministicAlgorithmsBeginOp.apply(tensor) | 49 | return _DeterministicAlgorithmsBeginOp.apply(tensor) |
| 42 | 50 | ||
| 43 | 51 | ||
| 44 | -@forbid_in_graph | 52 | +@_forbid_in_graph |
| 45 | def disable_deterministic_with_backward(tensor: Tensor): | 53 | def disable_deterministic_with_backward(tensor: Tensor): |
| 46 | return _DeterministicAlgorithmsEndOp.apply(tensor) | 54 | return _DeterministicAlgorithmsEndOp.apply(tensor) |
| @@ -31,4 +31,4 @@ def register_replacement(search_fn: SearchFn, replace_fn: ReplaceFn, example_inp | |||
| 31 | trace_fn=trace_fn, extra_check=extra_check, | 31 | trace_fn=trace_fn, extra_check=extra_check, |
| 32 | search_fn_pattern=search_fn_pattern, | 32 | search_fn_pattern=search_fn_pattern, |
| 33 | scalar_workaround=scalar_workaround, | 33 | scalar_workaround=scalar_workaround, |
| 34 | - skip_duplicates=skip_duplicates) | 34 | + skip_duplicates=skip_duplicates) |
| @@ -1,9 +1,12 @@ | |||
| 1 | -import inspect | ||
| 2 | -import os | ||
| 3 | -import logging | ||
| 4 | -import sys | ||
| 5 | import importlib | 1 | import importlib |
| 2 | +import importlib.abc | ||
| 6 | import functools | 3 | import functools |
| 4 | +import inspect | ||
| 5 | +import logging | ||
| 6 | +import os | ||
| 7 | +import sys | ||
| 8 | +import threading | ||
| 9 | + | ||
| 7 | import torch | 10 | import torch |
| 8 | import torch_npu | 11 | import torch_npu |
| 9 | from torch import _TorchCompileWrapper | 12 | from torch import _TorchCompileWrapper |
| @@ -13,10 +16,10 @@ use_jit_script = False | |||
| 13 | log = logging.getLogger(__name__) | 16 | log = logging.getLogger(__name__) |
| 14 | 17 | ||
| 15 | 18 | ||
| 16 | - | ||
| 17 | def _create_npu_autocast_mode_variable(func, args, kwargs): | 19 | def _create_npu_autocast_mode_variable(func, args, kwargs): |
| 18 | - from torch._dynamo.variables.ctx_manager import AutocastModeVariable | ||
| 19 | from torch._dynamo.variables.base import VariableTracker | 20 | from torch._dynamo.variables.base import VariableTracker |
| 21 | + from torch._dynamo.variables.ctx_manager import AutocastModeVariable | ||
| 22 | + | ||
| 20 | bound_args = inspect.signature(func).bind(*args, **kwargs) | 23 | bound_args = inspect.signature(func).bind(*args, **kwargs) |
| 21 | bound_args.apply_defaults() | 24 | bound_args.apply_defaults() |
| 22 | target_values = [] | 25 | target_values = [] |
| @@ -34,12 +37,13 @@ def _create_npu_autocast_mode_variable(func, args, kwargs): | |||
| 34 | else: | 37 | else: |
| 35 | target_values.append(arg) | 38 | target_values.append(arg) |
| 36 | 39 | ||
| 37 | - var = AutocastModeVariable(target_values, initial_values=None, **kwargs) | 40 | + return AutocastModeVariable(target_values, initial_values=None, **kwargs) |
| 38 | - return var | 41 | + |
| 39 | 42 | ||
| 40 | def patch_SkipFunctionVariable(): | 43 | def patch_SkipFunctionVariable(): |
| 41 | from torch._dynamo.variables.functions import SkipFunctionVariable | 44 | from torch._dynamo.variables.functions import SkipFunctionVariable |
| 42 | from torch._dynamo.variables.torch import TorchInGraphFunctionVariable | 45 | from torch._dynamo.variables.torch import TorchInGraphFunctionVariable |
| 46 | + | ||
| 43 | def SkipFunctionVariable__new__(cls, value, reason=None, **kwargs): | 47 | def SkipFunctionVariable__new__(cls, value, reason=None, **kwargs): |
| 44 | if value in [ | 48 | if value in [ |
| 45 | torch.npu.stream, | 49 | torch.npu.stream, |
| @@ -52,11 +56,12 @@ def patch_SkipFunctionVariable(): | |||
| 52 | SkipFunctionVariable.__new__raw = SkipFunctionVariable.__new__ | 56 | SkipFunctionVariable.__new__raw = SkipFunctionVariable.__new__ |
| 53 | SkipFunctionVariable.__new__ = SkipFunctionVariable__new__ | 57 | SkipFunctionVariable.__new__ = SkipFunctionVariable__new__ |
| 54 | 58 | ||
| 59 | + | ||
| 55 | def patch_TensorVariable_call_method(): | 60 | def patch_TensorVariable_call_method(): |
| 56 | - from torch._dynamo.variables.tensor import TensorVariable | ||
| 57 | from torch._dynamo.utils import tensortype_to_dtype | 61 | from torch._dynamo.utils import tensortype_to_dtype |
| 58 | from torch._dynamo.variables.constant import ConstantVariable | 62 | from torch._dynamo.variables.constant import ConstantVariable |
| 59 | from torch._dynamo.variables.lists import TupleVariable | 63 | from torch._dynamo.variables.lists import TupleVariable |
| 64 | + from torch._dynamo.variables.tensor import TensorVariable | ||
| 60 | 65 | ||
| 61 | def TensorVariable_call_method(self, tx, name, args, kwargs): | 66 | def TensorVariable_call_method(self, tx, name, args, kwargs): |
| 62 | if ( | 67 | if ( |
| @@ -66,25 +71,30 @@ def patch_TensorVariable_call_method(): | |||
| 66 | and isinstance(self.device, torch.device) | 71 | and isinstance(self.device, torch.device) |
| 67 | and self.device.type == "npu" | 72 | and self.device.type == "npu" |
| 68 | ): | 73 | ): |
| 69 | - tensortype = next(k for k, v in tensortype_to_dtype.items() if self.dtype in v) | 74 | + tensortype = next( |
| 70 | - constant_result = ConstantVariable.create(f"torch.npu.{tensortype.__name__}") | 75 | + k for k, v in tensortype_to_dtype.items() if self.dtype in v |
| 76 | + ) | ||
| 77 | + constant_result = ConstantVariable.create( | ||
| 78 | + f"torch.npu.{tensortype.__name__}" | ||
| 79 | + ) | ||
| 71 | 80 | ||
| 72 | if len(args) == 1: | 81 | if len(args) == 1: |
| 73 | return constant_result.getitem_const(args[0]) | 82 | return constant_result.getitem_const(args[0]) |
| 74 | - elif args: | 83 | + if args: |
| 75 | - return TupleVariable([constant_result.getitem_const(a) for a in args]) | 84 | + return TupleVariable( |
| 85 | + [constant_result.getitem_const(a) for a in args] | ||
| 86 | + ) | ||
| 76 | return constant_result | 87 | return constant_result |
| 77 | - else: | 88 | + return TensorVariable.call_method_raw(self, tx, name, args, kwargs) |
| 78 | - return TensorVariable.call_method_raw(self, tx, name, args, kwargs) | ||
| 79 | 89 | ||
| 80 | TensorVariable.call_method_raw = TensorVariable.call_method | 90 | TensorVariable.call_method_raw = TensorVariable.call_method |
| 81 | TensorVariable.call_method = TensorVariable_call_method | 91 | TensorVariable.call_method = TensorVariable_call_method |
| 82 | 92 | ||
| 83 | 93 | ||
| 84 | - | ||
| 85 | class _InductorNpuRegistry: | 94 | class _InductorNpuRegistry: |
| 86 | _disabled_register = False | 95 | _disabled_register = False |
| 87 | _loaded_backend = None | 96 | _loaded_backend = None |
| 97 | + | ||
| 88 | 98 | ||
| 89 | def register_inductor_npu(cls): | 99 | def register_inductor_npu(cls): |
| 90 | if cls._disabled_register: | 100 | if cls._disabled_register: |
| @@ -128,19 +138,23 @@ def register_inductor_npu(): | |||
| 128 | _InductorNpuRegistry.register_inductor_npu() | 138 | _InductorNpuRegistry.register_inductor_npu() |
| 129 | 139 | ||
| 130 | 140 | ||
| 131 | -def _resolve_npu_backend_from_wrapper(wrapper) -> str: | 141 | +def _resolve_npu_backend(selected_backend=None) -> str: |
| 132 | - """Resolve npu backend with priority: wrapper options > global config > env.""" | 142 | + """Resolve NPU backend with priority: compile options > config > env.""" |
| 133 | - wrapper_backend = wrapper.config.get("npu_backend") | 143 | + if selected_backend not in (None, "", "default"): |
| 134 | - if wrapper_backend not in (None, "", "default"): | 144 | + return selected_backend |
| 135 | - return wrapper_backend | ||
| 136 | 145 | ||
| 137 | - global_backend = getattr(torch._inductor.config, "npu_backend", None) | 146 | + inductor_config = sys.modules.get("torch._inductor.config") |
| 147 | + global_backend = getattr(inductor_config, "npu_backend", None) | ||
| 138 | if global_backend not in (None, "", "default"): | 148 | if global_backend not in (None, "", "default"): |
| 139 | return global_backend | 149 | return global_backend |
| 140 | 150 | ||
| 141 | return os.getenv("TORCHINDUCTOR_NPU_BACKEND", "default") | 151 | return os.getenv("TORCHINDUCTOR_NPU_BACKEND", "default") |
| 142 | 152 | ||
| 143 | 153 | ||
| 154 | +def _resolve_npu_backend_from_wrapper(wrapper) -> str: | ||
| 155 | + return _resolve_npu_backend(wrapper.config.get("npu_backend")) | ||
| 156 | + | ||
| 157 | + | ||
| 144 | class _NpuBackendScope: | 158 | class _NpuBackendScope: |
| 145 | """Apply resolved npu backend for one compile invocation and restore env.""" | 159 | """Apply resolved npu backend for one compile invocation and restore env.""" |
| 146 | 160 | ||
| @@ -150,32 +164,37 @@ class _NpuBackendScope: | |||
| 150 | 164 | ||
| 151 | def __enter__(self): | 165 | def __enter__(self): |
| 152 | self._old_env = os.environ.get("TORCHINDUCTOR_NPU_BACKEND") | 166 | self._old_env = os.environ.get("TORCHINDUCTOR_NPU_BACKEND") |
| 153 | - os.environ["TORCHINDUCTOR_NPU_BACKEND"] = self.backend | 167 | + try: |
| 154 | - register_inductor_npu() | 168 | + os.environ["TORCHINDUCTOR_NPU_BACKEND"] = self.backend |
| 155 | - if self.backend == "ascendc": | 169 | + register_inductor_npu() |
| 156 | - from torch_npu._inductor.deterministic_cache import ( | 170 | + if self.backend == "ascendc": |
| 157 | - patch_npu_deterministic_level_cache_keys, | 171 | + from torch_npu._inductor.deterministic_cache import ( |
| 158 | - ) | 172 | + patch_npu_deterministic_level_cache_keys, |
| 173 | + ) | ||
| 159 | 174 | ||
| 160 | - patch_npu_deterministic_level_cache_keys() | 175 | + patch_npu_deterministic_level_cache_keys() |
| 176 | + except BaseException: | ||
| 177 | + self._restore_backend_env() | ||
| 178 | + raise | ||
| 161 | return self | 179 | return self |
| 162 | 180 | ||
| 163 | def __exit__(self, exc_type, exc, tb): | 181 | def __exit__(self, exc_type, exc, tb): |
| 182 | + self._restore_backend_env() | ||
| 183 | + return False | ||
| 184 | + | ||
| 185 | + def _restore_backend_env(self): | ||
| 164 | if self._old_env is None: | 186 | if self._old_env is None: |
| 165 | os.environ.pop("TORCHINDUCTOR_NPU_BACKEND", None) | 187 | os.environ.pop("TORCHINDUCTOR_NPU_BACKEND", None) |
| 166 | else: | 188 | else: |
| 167 | os.environ["TORCHINDUCTOR_NPU_BACKEND"] = self._old_env | 189 | os.environ["TORCHINDUCTOR_NPU_BACKEND"] = self._old_env |
| 168 | - return False | ||
| 169 | - | ||
| 170 | - | ||
| 171 | 190 | ||
| 172 | 191 | ||
| 173 | def patch_dynamo_optimize(): | 192 | def patch_dynamo_optimize(): |
| 174 | from torch_npu.dynamo import _get_global_npu_backend | 193 | from torch_npu.dynamo import _get_global_npu_backend |
| 194 | + | ||
| 175 | src_optimize = torch._dynamo.optimize | 195 | src_optimize = torch._dynamo.optimize |
| 176 | 196 | ||
| 177 | def npu_optimize(*args, **kwargs): | 197 | def npu_optimize(*args, **kwargs): |
| 178 | - add_dynamo_methods_init() | ||
| 179 | backend = None | 198 | backend = None |
| 180 | if "backend" in kwargs: | 199 | if "backend" in kwargs: |
| 181 | backend = kwargs["backend"] | 200 | backend = kwargs["backend"] |
| @@ -197,11 +216,14 @@ def patch_dynamo_optimize(): | |||
| 197 | 216 | ||
| 198 | 217 | ||
| 199 | def patch_user_defined_class_variable(): | 218 | def patch_user_defined_class_variable(): |
| 200 | - import functools | 219 | + from torch._dynamo.variables.torch import ( |
| 220 | + TorchCtxManagerClassVariable, | ||
| 221 | + TorchInGraphFunctionVariable, | ||
| 222 | + ) | ||
| 201 | from torch._dynamo.variables.user_defined import UserDefinedClassVariable | 223 | from torch._dynamo.variables.user_defined import UserDefinedClassVariable |
| 202 | - from torch._dynamo.variables.torch import TorchCtxManagerClassVariable | 224 | + |
| 203 | - from torch._dynamo.variables.torch import TorchInGraphFunctionVariable | ||
| 204 | original_method = UserDefinedClassVariable._in_graph_classes | 225 | original_method = UserDefinedClassVariable._in_graph_classes |
| 226 | + | ||
| 205 | class NPUTorchCtxManagerClassVariable(TorchCtxManagerClassVariable): | 227 | class NPUTorchCtxManagerClassVariable(TorchCtxManagerClassVariable): |
| 206 | def call_function(self, tx, args, kwargs): | 228 | def call_function(self, tx, args, kwargs): |
| 207 | return _create_npu_autocast_mode_variable(self.value, args, kwargs) | 229 | return _create_npu_autocast_mode_variable(self.value, args, kwargs) |
| @@ -222,7 +244,7 @@ def patch_user_defined_class_variable(): | |||
| 222 | torch_npu.npu.amp.autocast_mode.autocast, | 244 | torch_npu.npu.amp.autocast_mode.autocast, |
| 223 | ]: | 245 | ]: |
| 224 | return NPUTorchCtxManagerClassVariable(value, **kwargs) | 246 | return NPUTorchCtxManagerClassVariable(value, **kwargs) |
| 225 | - elif value in [ | 247 | + if value in [ |
| 226 | torch.npu.Stream, | 248 | torch.npu.Stream, |
| 227 | torch_npu.npu.Stream, | 249 | torch_npu.npu.Stream, |
| 228 | torch.npu.streams.Stream, | 250 | torch.npu.streams.Stream, |
| @@ -245,6 +267,7 @@ def patch_user_defined_class_variable(): | |||
| 245 | UserDefinedClassVariable.__new__raw = UserDefinedClassVariable.__new__ | 267 | UserDefinedClassVariable.__new__raw = UserDefinedClassVariable.__new__ |
| 246 | UserDefinedClassVariable.__new__ = UserDefinedClassVariable__new__ | 268 | UserDefinedClassVariable.__new__ = UserDefinedClassVariable__new__ |
| 247 | 269 | ||
| 270 | + | ||
| 248 | def fake_record_stream(self, s): | 271 | def fake_record_stream(self, s): |
| 249 | """ | 272 | """ |
| 250 | let dynamo trace Tensor.record_stream as this empty function, | 273 | let dynamo trace Tensor.record_stream as this empty function, |
| @@ -322,21 +345,69 @@ def patch_event_variable_python_type(): | |||
| 322 | if "python_type" not in torch._dynamo.variables.ctx_manager.EventVariable.__dict__: | 345 | if "python_type" not in torch._dynamo.variables.ctx_manager.EventVariable.__dict__: |
| 323 | torch._dynamo.variables.ctx_manager.EventVariable.python_type = python_type | 346 | torch._dynamo.variables.ctx_manager.EventVariable.python_type = python_type |
| 324 | 347 | ||
| 348 | + | ||
| 325 | def run_once(f): | 349 | def run_once(f): |
| 326 | - """Runs a function (successfully) only once. | 350 | + """Run a function successfully only once, waiting for concurrent callers.""" |
| 327 | - The running can be reset by setting the `has_run` attribute to False | 351 | + condition = threading.Condition() |
| 328 | - """ | 352 | + |
| 329 | 353 | ||
| 330 | def wrapper(*args, **kwargs): | 354 | def wrapper(*args, **kwargs): |
| 331 | - if not wrapper.has_run: | 355 | + thread_id = threading.get_ident() |
| 356 | + with condition: | ||
| 357 | + while wrapper._is_running: | ||
| 358 | + if wrapper._running_thread == thread_id: | ||
| 359 | + return None | ||
| 360 | + condition.wait() | ||
| 361 | + if wrapper.has_run: | ||
| 362 | + return None | ||
| 363 | + wrapper._is_running = True | ||
| 364 | + wrapper._running_thread = thread_id | ||
| 365 | + | ||
| 366 | + try: | ||
| 332 | result = f(*args, **kwargs) | 367 | result = f(*args, **kwargs) |
| 368 | + except BaseException: | ||
| 369 | + with condition: | ||
| 370 | + wrapper._is_running = False | ||
| 371 | + wrapper._running_thread = None | ||
| 372 | + condition.notify_all() | ||
| 373 | + raise | ||
| 374 | + | ||
| 375 | + with condition: | ||
| 333 | wrapper.has_run = True | 376 | wrapper.has_run = True |
| 334 | - return result | 377 | + wrapper._is_running = False |
| 335 | - return None | 378 | + wrapper._running_thread = None |
| 379 | + condition.notify_all() | ||
| 380 | + return result | ||
| 381 | + | ||
| 336 | wrapper.has_run = False | 382 | wrapper.has_run = False |
| 383 | + wrapper._is_running = False | ||
| 384 | + wrapper._running_thread = None | ||
| 385 | + | ||
| 386 | + def reset_after_fork(): | ||
| 387 | + # The parent thread running f may not exist in the child process. | ||
| 388 | + nonlocal condition | ||
| 389 | + condition = threading.Condition() | ||
| 390 | + wrapper._is_running = False | ||
| 391 | + wrapper._running_thread = None | ||
| 392 | + | ||
| 393 | + try: | ||
| 394 | + os.register_at_fork(after_in_child=reset_after_fork) | ||
| 395 | + except AttributeError: | ||
| 396 | + pass | ||
| 337 | return wrapper | 397 | return wrapper |
| 338 | 398 | ||
| 339 | 399 | ||
| 400 | +_COMPLETED_DYNAMO_SETUP_STEPS = set() | ||
| 401 | + | ||
| 402 | + | ||
| 403 | +def _run_dynamo_setup_step(name, setup): | ||
| 404 | + """Keep successful setup steps idempotent when a later step fails.""" | ||
| 405 | + if name in _COMPLETED_DYNAMO_SETUP_STEPS: | ||
| 406 | + return | ||
| 407 | + setup() | ||
| 408 | + _COMPLETED_DYNAMO_SETUP_STEPS.add(name) | ||
| 409 | + | ||
| 410 | + | ||
| 340 | 411 | ||
| 341 | def _dynamo_register_interface_for_device(): | 412 | def _dynamo_register_interface_for_device(): |
| 342 | from torch._dynamo.device_interface import register_interface_for_device | 413 | from torch._dynamo.device_interface import register_interface_for_device |
| @@ -344,17 +415,74 @@ def _dynamo_register_interface_for_device(): | |||
| 344 | 415 | ||
| 345 | register_interface_for_device("npu", NpuInterface) | 416 | register_interface_for_device("npu", NpuInterface) |
| 346 | for i in range(32): | 417 | for i in range(32): |
| 347 | - | ||
| 348 | register_interface_for_device(f"npu:{i}", NpuInterface) | 418 | register_interface_for_device(f"npu:{i}", NpuInterface) |
| 349 | 419 | ||
| 420 | + | ||
| 421 | +def _find_spec_without_finder(finder, fullname): | ||
| 422 | + """Delegate to the remaining meta-path finders without bypassing them.""" | ||
| 423 | + try: | ||
| 424 | + index = sys.meta_path.index(finder) | ||
| 425 | + except ValueError: | ||
| 426 | + return importlib.util.find_spec(fullname) | ||
| 427 | + | ||
| 428 | + sys.meta_path.pop(index) | ||
| 429 | + try: | ||
| 430 | + return importlib.util.find_spec(fullname) | ||
| 431 | + finally: | ||
| 432 | + sys.meta_path.insert(min(index, len(sys.meta_path)), finder) | ||
| 433 | + | ||
| 434 | + | ||
| 435 | +class _DynamoPostImportLoader(importlib.abc.Loader): | ||
| 436 | + def __init__(self, loader, finder): | ||
| 437 | + self._loader = loader | ||
| 438 | + self._finder = finder | ||
| 439 | + | ||
| 440 | + def create_module(self, spec): | ||
| 441 | + create_module = getattr(self._loader, "create_module", None) | ||
| 442 | + return create_module(spec) if create_module is not None else None | ||
| 443 | + | ||
| 444 | + def exec_module(self, module): | ||
| 445 | + self._loader.exec_module(module) | ||
| 446 | + _lazy_dynamo_setup() | ||
| 447 | + if self._finder in sys.meta_path: | ||
| 448 | + sys.meta_path.remove(self._finder) | ||
| 449 | + | ||
| 450 | + | ||
| 451 | +class _DynamoPostImportFinder(importlib.abc.MetaPathFinder): | ||
| 452 | + _target = "torch._dynamo" | ||
| 453 | + | ||
| 454 | + def find_spec(self, fullname, path=None, target=None): | ||
| 455 | + if fullname != self._target: | ||
| 456 | + return None | ||
| 457 | + spec = _find_spec_without_finder(self, fullname) | ||
| 458 | + if spec is not None and spec.loader is not None: | ||
| 459 | + spec.loader = _DynamoPostImportLoader(spec.loader, self) | ||
| 460 | + return spec | ||
| 461 | + | ||
| 462 | + | ||
| 463 | +def _install_dynamo_post_import_trigger(): | ||
| 464 | + """Set up NPU integration whenever Dynamo is first imported.""" | ||
| 465 | + if "torch._dynamo" in sys.modules: | ||
| 466 | + _lazy_dynamo_setup() | ||
| 467 | + return | ||
| 468 | + if not any(isinstance(finder, _DynamoPostImportFinder) for finder in sys.meta_path): | ||
| 469 | + sys.meta_path.insert(0, _DynamoPostImportFinder()) | ||
| 470 | + | ||
| 471 | + | ||
| 350 | 472 | ||
| 351 | def add_dynamo_methods_init(): | 473 | def add_dynamo_methods_init(): |
| 352 | - _dynamo_register_interface_for_device() | 474 | + steps = ( |
| 353 | - patch_SkipFunctionVariable() | 475 | + ("device_interface", _dynamo_register_interface_for_device), |
| 354 | - patch_TensorVariable_call_method() | 476 | + ("skip_function_variable", patch_SkipFunctionVariable), |
| 355 | - patch_user_defined_class_variable() | 477 | + ("tensor_variable", patch_TensorVariable_call_method), |
| 356 | - patch_record_stream() | 478 | + ("user_defined_class_variable", patch_user_defined_class_variable), |
| 357 | - patch_event_variable_python_type() | 479 | + ("record_stream", patch_record_stream), |
| 480 | + ("event_variable", patch_event_variable_python_type), | ||
| 481 | + ("variable_builder", patch_variable_builder), | ||
| 482 | + ("builtin_variable", patch_builtin_variable), | ||
| 483 | + ) | ||
| 484 | + for name, setup in steps: | ||
| 485 | + _run_dynamo_setup_step(name, setup) | ||
| 358 | 486 | ||
| 359 | 487 | ||
| 360 | def patch_inductor_wrapper(): | 488 | def patch_inductor_wrapper(): |
| @@ -369,15 +497,22 @@ def patch_inductor_wrapper(): | |||
| 369 | src_call = _TorchCompileInductorWrapper.__call__ | 497 | src_call = _TorchCompileInductorWrapper.__call__ |
| 370 | 498 | ||
| 371 | def new_apply_options(self, options: Optional[dict[str, Any]]): | 499 | def new_apply_options(self, options: Optional[dict[str, Any]]): |
| 372 | - if options is not None and options.get("enable_shape_handling", False): | 500 | + shape_handling_requested = ( |
| 501 | + options is not None and options.get("enable_shape_handling", False) | ||
| 502 | + ) | ||
| 503 | + src_apply_options(self, options) | ||
| 504 | + if shape_handling_requested: | ||
| 505 | + if getattr(self, "_npu_defer_shape_handling", False): | ||
| 506 | + self._npu_shape_handling_requested = True | ||
| 507 | + return | ||
| 373 | if not is_inductor_npu_initialized(): | 508 | if not is_inductor_npu_initialized(): |
| 374 | register_inductor_npu() | 509 | register_inductor_npu() |
| 375 | torch_npu._inductor.patch_shape_handling() | 510 | torch_npu._inductor.patch_shape_handling() |
| 376 | - src_apply_options(self, options) | ||
| 377 | 511 | ||
| 378 | def new_get_config_copy(self) -> dict[str, Any]: | 512 | def new_get_config_copy(self) -> dict[str, Any]: |
| 379 | ori_dict = src_get_config_copy(self) | 513 | ori_dict = src_get_config_copy(self) |
| 380 | - if self is not torch._inductor.config: | 514 | + inductor_config = sys.modules.get("torch._inductor.config") |
| 515 | + if inductor_config is None or self is not inductor_config: | ||
| 381 | return ori_dict | 516 | return ori_dict |
| 382 | NpuBackendType = Literal["default", "mlir", "dvm"] | 517 | NpuBackendType = Literal["default", "mlir", "dvm"] |
| 383 | if "npu_backend" not in ori_dict: | 518 | if "npu_backend" not in ori_dict: |
| @@ -406,8 +541,17 @@ def patch_inductor_wrapper(): | |||
| 406 | return ori_dict | 541 | return ori_dict |
| 407 | 542 | ||
| 408 | def new_init(self, mode, options, dynamic): | 543 | def new_init(self, mode, options, dynamic): |
| 409 | - add_dynamo_methods_init() | 544 | + self._npu_defer_shape_handling = True |
| 410 | - src_init(self, mode, options, dynamic) | 545 | + self._npu_shape_handling_requested = False |
| 546 | + try: | ||
| 547 | + src_init(self, mode, options, dynamic) | ||
| 548 | + shape_handling_requested = self._npu_shape_handling_requested | ||
| 549 | + finally: | ||
| 550 | + del self._npu_defer_shape_handling | ||
| 551 | + del self._npu_shape_handling_requested | ||
| 552 | + _setup_inductor_for_compile(self.config) | ||
| 553 | + if shape_handling_requested: | ||
| 554 | + torch_npu._inductor.patch_shape_handling() | ||
| 411 | backend = _resolve_npu_backend_from_wrapper(self) | 555 | backend = _resolve_npu_backend_from_wrapper(self) |
| 412 | if backend=="mlir": | 556 | if backend=="mlir": |
| 413 | with _NpuBackendScope(backend): | 557 | with _NpuBackendScope(backend): |
| @@ -433,7 +577,6 @@ def patch_inductor_wrapper(): | |||
| 433 | _TorchCompileInductorWrapper.apply_options = new_apply_options | 577 | _TorchCompileInductorWrapper.apply_options = new_apply_options |
| 434 | _TorchCompileInductorWrapper.__init__ = new_init | 578 | _TorchCompileInductorWrapper.__init__ = new_init |
| 435 | ConfigModule.get_config_copy = new_get_config_copy | 579 | ConfigModule.get_config_copy = new_get_config_copy |
| 436 | - torch._inductor.config.get_config_copy() | ||
| 437 | 580 | ||
| 438 | 581 | ||
| 439 | 582 | ||
| @@ -475,12 +618,81 @@ def has_triton() -> bool: | |||
| 475 | 618 | ||
| 476 | 619 | ||
| 477 | def patch_has_triton(): | 620 | def patch_has_triton(): |
| 478 | - torch.utils._triton.has_triton = has_triton | 621 | + from torch.utils import _triton |
| 622 | + | ||
| 623 | + _triton.has_triton = has_triton | ||
| 624 | + | ||
| 625 | + | ||
| 626 | + | ||
| 627 | +def _inject_inductor_npu_backend_config(): | ||
| 628 | + """Inject NPU entries into torch._inductor.config on first use.""" | ||
| 629 | + torch._inductor.config.get_config_copy() | ||
| 630 | + | ||
| 631 | + | ||
| 632 | + | ||
| 633 | +def _lazy_dynamo_setup(): | ||
| 634 | + """Initialize the Dynamo integration on the first graph-capture operation.""" | ||
| 635 | + add_dynamo_methods_init() | ||
| 636 | + | ||
| 637 | + from torch_npu.dynamo import _register_backends | ||
| 638 | + _run_dynamo_setup_step("backends", _register_backends) | ||
| 639 | + | ||
| 640 | + from torch_npu.dynamo.trace_rule import _patch_npu_trace_rules | ||
| 641 | + _run_dynamo_setup_step("trace_rules", _patch_npu_trace_rules) | ||
| 642 | + | ||
| 643 | + _run_dynamo_setup_step("dynamo_optimize", patch_dynamo_optimize) | ||
| 644 | + | ||
| 645 | + | ||
| 646 | + | ||
| 647 | +def _lazy_inductor_setup(): | ||
| 648 | + """Initialize NPU Inductor support only for an Inductor-based backend.""" | ||
| 649 | + register_inductor_npu() | ||
| 650 | + | ||
| 651 | + from torch_npu.utils._graph_tree import _apply_npugraph_tree_methods | ||
| 652 | + _apply_npugraph_tree_methods() | ||
| 653 | + | ||
| 654 | + _inject_inductor_npu_backend_config() | ||
| 655 | + | ||
| 656 | + | ||
| 657 | +def _setup_inductor_for_compile(options=None): | ||
| 658 | + """Initialize the NPU Inductor backend selected for this compile call.""" | ||
| 659 | + _lazy_dynamo_setup() | ||
| 660 | + | ||
| 661 | + option_backend = options.get("npu_backend") if isinstance(options, dict) else None | ||
| 662 | + selected_backend = _resolve_npu_backend(option_backend) | ||
| 663 | + | ||
| 664 | + old_backend = os.environ.get("TORCHINDUCTOR_NPU_BACKEND") | ||
| 665 | + if selected_backend not in (None, "", "default"): | ||
| 666 | + os.environ["TORCHINDUCTOR_NPU_BACKEND"] = selected_backend | ||
| 667 | + try: | ||
| 668 | + _lazy_inductor_setup() | ||
| 669 | + finally: | ||
| 670 | + if old_backend is None: | ||
| 671 | + os.environ.pop("TORCHINDUCTOR_NPU_BACKEND", None) | ||
| 672 | + else: | ||
| 673 | + os.environ["TORCHINDUCTOR_NPU_BACKEND"] = old_backend | ||
| 674 | + return selected_backend | ||
| 675 | + | ||
| 676 | + | ||
| 677 | + | ||
| 678 | +def install_npugraph_mark_step_trigger(): | ||
| 679 | + """Expose the public NPUGraph step API without importing compiler internals.""" | ||
| 680 | + def npugraph_mark_step_begin(): | ||
| 681 | + from torch_npu.npu._graph_tree_state import mark_step_begin | ||
| 682 | + return mark_step_begin() | ||
| 683 | + | ||
| 684 | + torch.compiler.npugraph_mark_step_begin = npugraph_mark_step_begin | ||
| 479 | 685 | ||
| 480 | 686 | ||
| 481 | def add_dynamo_methods(): | 687 | def add_dynamo_methods(): |
| 482 | - patch_dynamo_optimize() | ||
| 483 | - patch_variable_builder() | ||
| 484 | - patch_builtin_variable() | ||
| 485 | - patch_inductor_wrapper() | ||
| 486 | patch_has_triton() | 688 | patch_has_triton() |
| 689 | + | ||
| 690 | + from torch_npu.dynamo import _install_lazy_torchair | ||
| 691 | + | ||
| 692 | + _install_lazy_torchair() | ||
| 693 | + _install_dynamo_post_import_trigger() | ||
| 694 | + if "npugraph_ex" not in sys.modules: | ||
| 695 | + from torch_npu.dynamo import _LazyNpuGraphEx | ||
| 696 | + sys.modules["npugraph_ex"] = _LazyNpuGraphEx("npugraph_ex") | ||
| 697 | + patch_inductor_wrapper() | ||
| 698 | + install_npugraph_mark_step_trigger() | ||
| @@ -22,7 +22,7 @@ from torch._dynamo.backends.cudagraphs import ( | |||
| 22 | get_stack_traces, | 22 | get_stack_traces, |
| 23 | ) | 23 | ) |
| 24 | from torch._dynamo.backends.debugging import boxed_nop | 24 | from torch._dynamo.backends.debugging import boxed_nop |
| 25 | -from torch._dynamo.backends.registry import register_backend | 25 | +from torch._dynamo.backends.registry import _COMPILER_FNS, register_backend |
| 26 | from torch._inductor import config | 26 | from torch._inductor import config |
| 27 | from torch._inductor.compile_fx import ( | 27 | from torch._inductor.compile_fx import ( |
| 28 | get_input_idxs_to_check, | 28 | get_input_idxs_to_check, |
| @@ -58,7 +58,7 @@ log = torch._logging.getArtifactLogger("torch_npu.npugraph", "cudagraphs") | |||
| 58 | 58 | ||
| 59 | 59 | ||
| 60 | def npugraph_mark_step_begin(): | 60 | def npugraph_mark_step_begin(): |
| 61 | - from torch_npu.npu._graph_tree import mark_step_begin | 61 | + from torch_npu.npu._graph_tree_state import mark_step_begin |
| 62 | mark_step_begin() | 62 | mark_step_begin() |
| 63 | 63 | ||
| 64 | 64 | ||
| @@ -373,9 +373,9 @@ class NpugraphsBackend: | |||
| 373 | 373 | ||
| 374 | 374 | ||
| 375 | def reset(): | 375 | def reset(): |
| 376 | - from torch_npu.npu._graph_tree import reset_npugraph_trees | 376 | + from torch_npu.dynamo import _npugraphs_backend_entrypoint |
| 377 | 377 | ||
| 378 | - reset_npugraph_trees() | 378 | + _npugraphs_backend_entrypoint.reset() |
| 379 | 379 | ||
| 380 | 380 | ||
| 381 | def __call__(model, inputs): | 381 | def __call__(model, inputs): |
| @@ -385,7 +385,10 @@ class NpugraphsBackend: | |||
| 385 | def _apply_npugraph_tree_methods(): | 385 | def _apply_npugraph_tree_methods(): |
| 386 | # aot_npugraphs only applies graphs to the graph. It is also helpful | 386 | # aot_npugraphs only applies graphs to the graph. It is also helpful |
| 387 | # for debugging and can serve as a perf baseline. | 387 | # for debugging and can serve as a perf baseline. |
| 388 | - register_backend(name="npugraphs", compiler_fn=NpugraphsBackend()) | 388 | + if "npugraphs" not in _COMPILER_FNS: |
| 389 | + from torch_npu.dynamo import _npugraphs_backend_entrypoint | ||
| 390 | + | ||
| 391 | + register_backend(name="npugraphs", compiler_fn=_npugraphs_backend_entrypoint) | ||
| 389 | torch._inductor.compile_fx.cudagraphify = npugraphify | 392 | torch._inductor.compile_fx.cudagraphify = npugraphify |
| 390 | torch._inductor.cudagraph_utils.check_multiple_devices_or_any_cpu_nodes = check_multiple_devices_or_any_cpu_nodes | 393 | torch._inductor.cudagraph_utils.check_multiple_devices_or_any_cpu_nodes = check_multiple_devices_or_any_cpu_nodes |
| 391 | torch.compiler.npugraph_mark_step_begin = npugraph_mark_step_begin | 394 | torch.compiler.npugraph_mark_step_begin = npugraph_mark_step_begin |
| @@ -244,4 +244,4 @@ patch_register_run_and_save_rng_state_op() | |||
| 244 | patch_register_run_with_rng_state_op() | 244 | patch_register_run_with_rng_state_op() |
| 245 | patch_philox_rand_offset() | 245 | patch_philox_rand_offset() |
| 246 | patch_register_philox_rand() | 246 | patch_register_philox_rand() |
| 247 | -patch_rng_prims_device() | 247 | +patch_rng_prims_device() |