gen_backend_stubs.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import argparse
  2. import os
  3. import pathlib
  4. import re
  5. from collections import Counter, defaultdict, namedtuple
  6. from typing import Dict, List, Optional, Sequence, Set, Union
  7. import yaml
  8. import torchgen.api.dispatcher as dispatcher
  9. import torchgen.dest as dest
  10. from torchgen.api.types import DispatcherSignature
  11. from torchgen.code_template import CodeTemplate
  12. from torchgen.context import native_function_manager
  13. from torchgen.gen import get_grouped_native_functions, parse_native_yaml
  14. from torchgen.model import (
  15. BackendIndex,
  16. BackendMetadata,
  17. DispatchKey,
  18. NativeFunction,
  19. NativeFunctionsGroup,
  20. OperatorName,
  21. )
  22. from torchgen.selective_build.selector import SelectiveBuilder
  23. from torchgen.utils import (
  24. concatMap,
  25. context,
  26. FileManager,
  27. NamespaceHelper,
  28. Target,
  29. YamlLoader,
  30. )
  31. # Parses the external backend's yaml, and adds a new BackendIndex for the backend's dispatch key.
  32. # Returns a Tuple of (backend_key, autograd_key, cpp_namespace, updated BackendIndex mapping)
  33. ParsedExternalYaml = namedtuple(
  34. "ParsedExternalYaml",
  35. ["backend_key", "autograd_key", "class_name", "cpp_namespace", "backend_indices"],
  36. )
  37. def parse_backend_yaml(
  38. backend_yaml_path: str,
  39. grouped_native_functions: Sequence[Union[NativeFunction, NativeFunctionsGroup]],
  40. backend_indices: Dict[DispatchKey, BackendIndex],
  41. ) -> ParsedExternalYaml:
  42. native_functions_map: Dict[OperatorName, NativeFunction] = {
  43. f.func.name: f
  44. for f in concatMap(
  45. lambda f: [f] if isinstance(f, NativeFunction) else list(f.functions()),
  46. grouped_native_functions,
  47. )
  48. }
  49. with open(backend_yaml_path, "r") as f:
  50. yaml_values = yaml.load(f, Loader=YamlLoader)
  51. assert isinstance(yaml_values, dict)
  52. valid_keys = [
  53. "backend",
  54. "class_name",
  55. "cpp_namespace",
  56. "extra_headers",
  57. "supported",
  58. "autograd",
  59. "full_codegen",
  60. "non_native",
  61. "ir_gen",
  62. "symint",
  63. ]
  64. backend = yaml_values.pop("backend", None)
  65. assert backend is not None, 'You must provide a value for "backend"'
  66. class_name = yaml_values.pop("class_name", None)
  67. cpp_namespace = yaml_values.pop("cpp_namespace", None)
  68. assert cpp_namespace is not None, 'You must provide a value for "cpp_namespace"'
  69. # Mostly just defaulting to false to stick with LazyTensor convention.
  70. use_out_as_primary = yaml_values.pop("use_out_as_primary", False)
  71. assert isinstance(
  72. use_out_as_primary, bool
  73. ), f"You must provide either True or False for use_out_as_primary. Provided: {use_out_as_primary}"
  74. use_device_guard = yaml_values.pop("device_guard", False)
  75. assert isinstance(
  76. use_device_guard, bool
  77. ), f"You must provide either True or False for device_guard. Provided: {use_device_guard}"
  78. supported = yaml_values.pop("supported", [])
  79. if supported is None:
  80. supported = [] # Allow an empty list of supported ops
  81. assert isinstance(
  82. supported, list
  83. ), f'expected "supported" to be a list, but got: {supported} (of type {type(supported)})'
  84. symint = yaml_values.pop("symint", [])
  85. if symint is None:
  86. symint = [] # Allow an empty list of symint ops
  87. assert isinstance(
  88. symint, list
  89. ), f'expected "symint" to be a list, but got: {supported} (of type {type(supported)})'
  90. symint_set = set(symint)
  91. supported_autograd = yaml_values.pop("autograd", [])
  92. assert isinstance(
  93. supported_autograd, list
  94. ), f'expected "autograd" to be a list, but got: {supported_autograd}'
  95. # full_codegen is ignored by parse_backend_yaml, and re-parsed in gen_lazy_tensor.py
  96. full_codegen = yaml_values.pop("full_codegen", [])
  97. supported.extend(full_codegen)
  98. # non_native is ignored by parse_backend_yaml, and re-parsed in gen_lazy_tensor.py
  99. non_native = yaml_values.pop("non_native", {})
  100. # ir_gen is ignored by parse_backend_yaml, and re-parsed in gen_lazy_tensor.py
  101. _ = yaml_values.pop("ir_gen", {})
  102. assert (
  103. len(yaml_values.keys()) == 0
  104. ), f'{backend_yaml_path} contains unexpected keys: {", ".join(yaml_values.keys())}. \
  105. Only the following keys are supported: {", ".join(valid_keys)}'
  106. def create_backend_index(
  107. backend_ops: List[str],
  108. symint_ops: Set[str],
  109. dispatch_key: DispatchKey,
  110. *,
  111. use_out_as_primary: bool,
  112. use_device_guard: bool,
  113. ) -> BackendIndex:
  114. metadata: Dict[OperatorName, BackendMetadata] = {}
  115. for op in backend_ops:
  116. op_name = OperatorName.parse(op)
  117. assert (
  118. op_name in native_functions_map
  119. ), f"Found an invalid operator name: {op_name}"
  120. # See Note [External Backends Follow Dispatcher API]
  121. kernel_name = dispatcher.name(native_functions_map[op_name].func)
  122. if op in symint_ops:
  123. kernel_name += "_symint"
  124. # TODO: allow structured external backends later.
  125. m = BackendMetadata(
  126. kernel=kernel_name, structured=False, cpp_namespace=cpp_namespace
  127. )
  128. metadata[op_name] = m
  129. return BackendIndex(
  130. dispatch_key=dispatch_key,
  131. use_out_as_primary=use_out_as_primary,
  132. external=True,
  133. device_guard=use_device_guard,
  134. index=metadata,
  135. )
  136. backend_key: Optional[DispatchKey] = None
  137. if len(supported) > 0:
  138. with context(
  139. lambda: f'The provided value for "backend" must be a valid DispatchKey, but got {backend}.'
  140. ):
  141. backend_key = DispatchKey.parse(backend)
  142. backend_idx = create_backend_index(
  143. supported,
  144. symint_set,
  145. backend_key,
  146. use_out_as_primary=use_out_as_primary,
  147. use_device_guard=use_device_guard,
  148. )
  149. assert backend_key not in backend_indices
  150. backend_indices[backend_key] = backend_idx
  151. autograd_key: Optional[DispatchKey] = None
  152. if len(supported_autograd) > 0:
  153. with context(
  154. lambda: f'The "autograd" key was specified, which indicates that you would like to override \
  155. the behavior of autograd for some operators on your backend. However "Autograd{backend}" is not a valid DispatchKey.'
  156. ):
  157. autograd_key = DispatchKey.parse(f"Autograd{backend}")
  158. autograd_idx = create_backend_index(
  159. supported_autograd,
  160. symint_set,
  161. autograd_key,
  162. use_out_as_primary=use_out_as_primary,
  163. use_device_guard=use_device_guard,
  164. )
  165. assert autograd_key not in backend_indices
  166. backend_indices[autograd_key] = autograd_idx
  167. for g in grouped_native_functions:
  168. if isinstance(g, NativeFunction):
  169. forward_kernels = (
  170. []
  171. if backend_key is None
  172. else [
  173. m
  174. for m in [backend_indices[backend_key].get_kernel(g)]
  175. if m is not None
  176. ]
  177. )
  178. backward_kernels = (
  179. []
  180. if autograd_key is None
  181. else [
  182. m
  183. for m in [backend_indices[autograd_key].get_kernel(g)]
  184. if m is not None
  185. ]
  186. )
  187. else:
  188. forward_kernels = (
  189. []
  190. if backend_key is None
  191. else [
  192. m
  193. for m in [
  194. backend_indices[backend_key].get_kernel(f)
  195. for f in g.functions()
  196. ]
  197. if m is not None
  198. ]
  199. )
  200. backward_kernels = (
  201. []
  202. if autograd_key is None
  203. else [
  204. m
  205. for m in [
  206. backend_indices[autograd_key].get_kernel(f)
  207. for f in g.functions()
  208. ]
  209. if m is not None
  210. ]
  211. )
  212. forward_kernels = [f for f in forward_kernels if f is not None]
  213. backward_kernels = [f for f in backward_kernels if f is not None]
  214. assert (
  215. len(forward_kernels) == 0 or len(backward_kernels) == 0
  216. ), f'Currently, all variants of an op must either be registered to a backend key, or to a backend\'s \
  217. autograd key. They cannot be mix and matched. If this is something you need, feel free to create an issue! \
  218. {forward_kernels[0].kernel} is listed under "supported", but {backward_kernels[0].kernel} is listed under "autograd".'
  219. return ParsedExternalYaml(
  220. backend_key, autograd_key, class_name, cpp_namespace, backend_indices
  221. )
  222. def error_on_missing_kernels(
  223. native_functions: Sequence[NativeFunction],
  224. backend_indices: Dict[DispatchKey, BackendIndex],
  225. backend_key: DispatchKey,
  226. autograd_key: Optional[DispatchKey],
  227. class_name: str,
  228. kernel_defn_file_path: str,
  229. full_codegen: Optional[List[OperatorName]] = None,
  230. ) -> None:
  231. try:
  232. with open(kernel_defn_file_path, "r") as f:
  233. backend_defns = f.read()
  234. except IOError as e:
  235. raise AssertionError(
  236. f"Unable to read from the specified impl_path file: {kernel_defn_file_path}"
  237. ) from e
  238. if full_codegen is None:
  239. full_codegen = []
  240. indices = [backend_indices[backend_key].index] + (
  241. [] if autograd_key is None else [backend_indices[autograd_key].index]
  242. )
  243. # Quick mapping from each OperatorName used by the external backend
  244. # to its backend kernel name
  245. expected_backend_op_names: Dict[OperatorName, str] = dict(
  246. list(
  247. concatMap(
  248. lambda index: [
  249. (op_name, metadata.kernel) for op_name, metadata in index.items()
  250. ],
  251. indices,
  252. )
  253. )
  254. )
  255. expected_backend_native_funcs: List[NativeFunction] = [
  256. f
  257. for f in native_functions
  258. if f.func.name in expected_backend_op_names.keys()
  259. and f.func.name not in full_codegen
  260. ]
  261. expected_backend_kernel_name_counts: Dict[str, List[NativeFunction]] = defaultdict(
  262. list
  263. )
  264. for native_f in expected_backend_native_funcs:
  265. expected_backend_kernel_name_counts[
  266. expected_backend_op_names[native_f.func.name]
  267. ].append(native_f)
  268. # This just looks for lines containing "foo(", and assumes that the kernel foo has been implemented.
  269. # It might cause false negatives (we won't catch all cases), but that's ok - if we catch a missing kernel
  270. # here, then we get a nicer error message. If we miss it, you get a linker error.
  271. kernel_defn_regex = rf"(.*){class_name}::\s*([\w\d]*)\("
  272. actual_backend_kernel_name_counts = Counter(
  273. # A bit unwieldy (this could probably be moved into regex),
  274. # but we don't want to include kernel names that come from function calls,
  275. # like "return torch_xla::XLANativeFunctions::empty_strided_symint(...)".
  276. # Easy check is to ignore any lines with colons before the class name.
  277. [
  278. y
  279. for (x, y) in re.findall(kernel_defn_regex, backend_defns)
  280. if not x.endswith(":")
  281. ]
  282. )
  283. missing_kernels_err_msg = ""
  284. for expected_name, funcs in expected_backend_kernel_name_counts.items():
  285. expected_overload_count = len(funcs)
  286. actual_overload_count = actual_backend_kernel_name_counts[expected_name]
  287. if expected_overload_count != actual_overload_count:
  288. def create_decl(f: NativeFunction) -> str:
  289. with native_function_manager(f):
  290. return DispatcherSignature.from_schema(f.func).decl()
  291. expected_schemas_str = "\n".join([create_decl(f) for f in funcs])
  292. missing_kernels_err_msg += f"""
  293. {class_name} is missing a kernel definition for {expected_name}. We found {actual_overload_count} kernel(s) with that name,
  294. but expected {expected_overload_count} kernel(s). The expected function schemas for the missing operator are:
  295. {expected_schemas_str}
  296. """
  297. assert missing_kernels_err_msg == "", missing_kernels_err_msg
  298. def main() -> None:
  299. parser = argparse.ArgumentParser(description="Generate backend stub files")
  300. parser.add_argument(
  301. "-s",
  302. "--source-yaml",
  303. "--source_yaml",
  304. help="path to source yaml file containing operator external definitions",
  305. )
  306. parser.add_argument("-o", "--output-dir", "--output_dir", help="output directory")
  307. parser.add_argument(
  308. "--dry-run", "--dry_run", type=bool, default=False, help="output directory"
  309. )
  310. parser.add_argument(
  311. "--impl-path",
  312. "--impl_path",
  313. type=str,
  314. default=None,
  315. help="path to the source C++ file containing kernel definitions",
  316. )
  317. options = parser.parse_args()
  318. run(options.source_yaml, options.output_dir, options.dry_run, options.impl_path)
  319. def gen_dispatchkey_nativefunc_headers(
  320. fm: FileManager,
  321. class_name: str,
  322. cpp_namespace: str,
  323. backend_indices: Dict[DispatchKey, BackendIndex],
  324. grouped_native_functions: Sequence[Union[NativeFunction, NativeFunctionsGroup]],
  325. backend_dispatch_key: DispatchKey,
  326. autograd_dispatch_key: Optional[DispatchKey],
  327. backend_name: str = "",
  328. ) -> None:
  329. assert class_name is not None
  330. generated_comment = (
  331. "Autogenerated file by gen_backend_stubs.py. Do not edit directly!"
  332. )
  333. # Convert to a set first to remove duplicate kernel names.
  334. # Backends are allowed to repeat kernel names; only generate the declaration once!
  335. # Sort for deterministic output.
  336. backend_declarations = sorted(
  337. set(
  338. concatMap(
  339. lambda f: dest.compute_native_function_declaration(
  340. f, backend_indices[backend_dispatch_key]
  341. ),
  342. grouped_native_functions,
  343. )
  344. )
  345. )
  346. autograd_declarations = sorted(
  347. set(
  348. concatMap(
  349. lambda f: []
  350. if autograd_dispatch_key is None
  351. else dest.compute_native_function_declaration(
  352. f, backend_indices[autograd_dispatch_key]
  353. ),
  354. grouped_native_functions,
  355. )
  356. )
  357. )
  358. ns_helper = NamespaceHelper(cpp_namespace)
  359. fm.write_with_template(
  360. f"{backend_dispatch_key}NativeFunctions.h",
  361. "DispatchKeyNativeFunctions.h",
  362. lambda: {
  363. "generated_comment": generated_comment,
  364. "namespace_prologue": ns_helper.prologue,
  365. "class_name": class_name,
  366. "namespace_epilogue": ns_helper.epilogue,
  367. "dispatch_declarations": backend_declarations + autograd_declarations,
  368. "BackendName": backend_name,
  369. "DispatchKey": backend_dispatch_key,
  370. },
  371. )
  372. def gen_dispatcher_registrations(
  373. fm: FileManager,
  374. output_dir: str,
  375. class_name: str,
  376. backend_indices: Dict[DispatchKey, BackendIndex],
  377. grouped_native_functions: Sequence[Union[NativeFunction, NativeFunctionsGroup]],
  378. backend_dispatch_key: DispatchKey,
  379. dispatch_key: DispatchKey,
  380. selector: "SelectiveBuilder",
  381. # build_in_tree is true for lazy TS backend and affects include paths, not used for external backends
  382. build_in_tree: bool = False,
  383. per_operator_headers: bool = False,
  384. backend_name: str = "",
  385. eager_registration: bool = True,
  386. ) -> None:
  387. headers = [
  388. f"{output_dir}/{backend_dispatch_key}NativeFunctions.h",
  389. ]
  390. if build_in_tree:
  391. external_backend_headers_str = "\n".join(f"#include <{h}>" for h in headers)
  392. else:
  393. external_backend_headers_str = "\n".join(f'#include "{h}"' for h in headers)
  394. assert class_name is not None
  395. backend_index = backend_indices[dispatch_key]
  396. dispatch_registrations_body = list(
  397. concatMap(
  398. dest.RegisterDispatchKey(
  399. backend_index,
  400. Target.REGISTRATION,
  401. selector,
  402. rocm=False,
  403. symint=True,
  404. class_method_name=f"{class_name}",
  405. skip_dispatcher_op_registration=False,
  406. ),
  407. grouped_native_functions,
  408. )
  409. )
  410. newline = "\n"
  411. ns_helper = NamespaceHelper(namespace_str="at")
  412. deferred_dispatch_registrations = ""
  413. static_init_dispatch_registrations = ""
  414. if eager_registration:
  415. static_template = CodeTemplate(
  416. """\
  417. TORCH_LIBRARY_IMPL(aten, $dispatch_key, m) {
  418. $dispatch_registrations_body
  419. };"""
  420. )
  421. static_init_dispatch_registrations = static_template.substitute(
  422. dispatch_key=dispatch_key,
  423. dispatch_registrations_body=dispatch_registrations_body,
  424. )
  425. else:
  426. deferred_template = CodeTemplate(
  427. """\
  428. TORCH_API void Register${backend_name}${dispatch_key}NativeFunctions() {
  429. static auto m = MAKE_TORCH_LIBRARY_IMPL(aten, $dispatch_key);
  430. $dispatch_registrations_body
  431. }"""
  432. )
  433. deferred_dispatch_registrations = deferred_template.substitute(
  434. backend_name=backend_name,
  435. dispatch_key=dispatch_key,
  436. dispatch_registrations_body=dispatch_registrations_body,
  437. )
  438. fm.write_with_template(
  439. f"Register{dispatch_key}.cpp",
  440. "RegisterDispatchKey.cpp",
  441. lambda: {
  442. "extra_cuda_headers": "",
  443. "external_backend_headers": external_backend_headers_str,
  444. "ops_headers": "#include <ATen/Functions.h>"
  445. if not per_operator_headers
  446. else "",
  447. "DispatchKey": dispatch_key,
  448. "dispatch_namespace": dispatch_key.lower(),
  449. "dispatch_headers": dest.gen_registration_headers(
  450. backend_index, per_operator_headers=per_operator_headers, rocm=False
  451. ),
  452. "dispatch_definitions": fm.substitute_with_template(
  453. "RegisterDispatchDefinitions.ini",
  454. lambda: {
  455. "ns_prologue": ns_helper.prologue,
  456. "ns_epilogue": ns_helper.epilogue,
  457. "static_init_dispatch_registrations": static_init_dispatch_registrations,
  458. "deferred_dispatch_registrations": deferred_dispatch_registrations,
  459. "dispatch_helpers": dest.gen_registration_helpers(backend_index),
  460. "dispatch_namespace": dispatch_key.lower(),
  461. "dispatch_namespaced_definitions": "",
  462. "dispatch_anonymous_definitions": list(
  463. concatMap(
  464. dest.RegisterDispatchKey(
  465. backend_index,
  466. Target.ANONYMOUS_DEFINITION,
  467. selector,
  468. rocm=False,
  469. symint=True,
  470. class_method_name=f"{class_name}",
  471. skip_dispatcher_op_registration=False,
  472. ),
  473. grouped_native_functions,
  474. )
  475. ),
  476. },
  477. ).split(newline),
  478. },
  479. )
  480. def run(
  481. source_yaml: str, output_dir: str, dry_run: bool, impl_path: Optional[str] = None
  482. ) -> None:
  483. # Assumes that this file lives at PYTORCH_ROOT/torchgen/gen_backend_stubs.py
  484. pytorch_root = pathlib.Path(__file__).parent.parent.absolute()
  485. template_dir = os.path.join(pytorch_root, "aten/src/ATen/templates")
  486. def make_file_manager(install_dir: str) -> FileManager:
  487. return FileManager(
  488. install_dir=install_dir, template_dir=template_dir, dry_run=dry_run
  489. )
  490. fm = make_file_manager(output_dir)
  491. native_yaml_path = os.path.join(
  492. pytorch_root, "aten/src/ATen/native/native_functions.yaml"
  493. )
  494. tags_yaml_path = os.path.join(pytorch_root, "aten/src/ATen/native/tags.yaml")
  495. parsed_yaml = parse_native_yaml(native_yaml_path, tags_yaml_path)
  496. native_functions, backend_indices = (
  497. parsed_yaml.native_functions,
  498. parsed_yaml.backend_indices,
  499. )
  500. grouped_native_functions = get_grouped_native_functions(native_functions)
  501. parsed_backend_yaml = parse_backend_yaml(
  502. source_yaml, grouped_native_functions, backend_indices
  503. )
  504. backend_key = parsed_backend_yaml.backend_key
  505. autograd_key = parsed_backend_yaml.autograd_key
  506. cpp_namespace = parsed_backend_yaml.cpp_namespace
  507. class_name = parsed_backend_yaml.class_name
  508. backend_indices = parsed_backend_yaml.backend_indices
  509. selector = SelectiveBuilder.get_nop_selector()
  510. if backend_key is None:
  511. # This could be useful if a backend wants to quickly set up a noop yaml file but doesn't have any kernels ready yet.
  512. return
  513. if class_name is None:
  514. # class_name is an optional argument to backend yaml file.
  515. # if specified it allows an external backend to override
  516. # the name of the class that all generated kernel definitions live under.
  517. # if not specified, its value is given as native_function_class_name.
  518. class_name = backend_indices[backend_key].native_function_class_name()
  519. assert class_name is not None
  520. if impl_path is not None:
  521. error_on_missing_kernels(
  522. native_functions,
  523. backend_indices,
  524. backend_key,
  525. autograd_key,
  526. class_name,
  527. impl_path,
  528. )
  529. gen_dispatchkey_nativefunc_headers(
  530. fm,
  531. class_name,
  532. cpp_namespace,
  533. backend_indices,
  534. grouped_native_functions,
  535. backend_key,
  536. autograd_key,
  537. )
  538. for dispatch_key in (
  539. [backend_key] if autograd_key is None else [backend_key, autograd_key]
  540. ):
  541. gen_dispatcher_registrations(
  542. fm,
  543. output_dir,
  544. class_name,
  545. backend_indices,
  546. grouped_native_functions,
  547. backend_key,
  548. dispatch_key,
  549. selector,
  550. )
  551. if __name__ == "__main__":
  552. main()