selector.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. from __future__ import annotations
  2. from collections import defaultdict
  3. from collections.abc import Iterable
  4. from dataclasses import dataclass
  5. from typing import TYPE_CHECKING
  6. import yaml
  7. from torchgen.selective_build.operator import (
  8. merge_debug_info,
  9. merge_operator_dicts,
  10. SelectiveBuildOperator,
  11. strip_operator_overload_name,
  12. )
  13. if TYPE_CHECKING:
  14. from torchgen.model import NativeFunction
  15. # A SelectiveBuilder holds information extracted from the selective build
  16. # YAML specification.
  17. #
  18. # It includes information about the build's selectivity, the debug_info
  19. # associated with this selective build (opaque string), and the set of
  20. # operators that should be included in the build.
  21. #
  22. @dataclass(frozen=True)
  23. class SelectiveBuilder:
  24. # If true, then the build is not selective, and includes all
  25. # operators.
  26. include_all_operators: bool
  27. # Debug Information at the selective/custom build level.
  28. _debug_info: tuple[str, ...] | None
  29. # A dictionary of operator -> operator metadata.
  30. operators: dict[str, SelectiveBuildOperator]
  31. # A dictionary of selected kernel tags and dtypes. Typically a
  32. # PyTorch Operator Kernel (function) may have many code paths
  33. # that are specialized for many many Tensor dtypes, so it's not
  34. # one per kernel function, but there could be many per kernel
  35. # function. The tag isn't a kernel function name, but some fragment
  36. # of the kernel function implementation itself.
  37. kernel_metadata: dict[str, list[str]]
  38. # ExecuTorch only. A dictionary of kernel tag -> list of (list of input
  39. # dtypes for tensor-like input args).
  40. # This is from selective.yaml
  41. et_kernel_metadata: dict[str, list[str]]
  42. # A set of all the custom torch bind classes used by the selected models
  43. # Stored as a set internally to remove duplicates proactively, but written
  44. # as a list to yamls
  45. custom_classes: set[str]
  46. # A set of all the build features used by the selected models
  47. # Stored as a set internally to remove duplicates proactively, but written
  48. # as a list to yamls
  49. build_features: set[str]
  50. # If true, then fragments for all dtypes for all kernel functions
  51. # are included as well as all custom classes. This is typically set when any one of the
  52. # operator lists is generated from a mechanism other than
  53. # tracing based selective build.
  54. include_all_non_op_selectives: bool
  55. @staticmethod
  56. def get_nop_selector() -> SelectiveBuilder:
  57. return SelectiveBuilder.from_yaml_dict({"include_all_operators": True})
  58. @staticmethod
  59. def from_yaml_dict(data: dict[str, object]) -> SelectiveBuilder:
  60. valid_top_level_keys = {
  61. "include_all_non_op_selectives",
  62. "include_all_operators",
  63. "debug_info",
  64. "operators",
  65. "kernel_metadata",
  66. "et_kernel_metadata",
  67. "custom_classes",
  68. "build_features",
  69. }
  70. top_level_keys = set(data.keys())
  71. if len(top_level_keys - valid_top_level_keys) > 0:
  72. raise Exception( # noqa: TRY002
  73. "Got unexpected top level keys: {}".format(
  74. ",".join(top_level_keys - valid_top_level_keys),
  75. )
  76. )
  77. include_all_operators = data.get("include_all_operators", False)
  78. assert isinstance(include_all_operators, bool)
  79. debug_info = None
  80. if "debug_info" in data:
  81. di_list = data["debug_info"]
  82. assert isinstance(di_list, list)
  83. debug_info = tuple(str(x) for x in di_list)
  84. operators = {}
  85. operators_dict = data.get("operators", {})
  86. assert isinstance(operators_dict, dict)
  87. for k, v in operators_dict.items():
  88. operators[k] = SelectiveBuildOperator.from_yaml_dict(k, v)
  89. kernel_metadata = {}
  90. kernel_metadata_dict = data.get("kernel_metadata", {})
  91. assert isinstance(kernel_metadata_dict, dict)
  92. for k, v in kernel_metadata_dict.items():
  93. kernel_metadata[str(k)] = [str(dtype) for dtype in v]
  94. et_kernel_metadata = data.get("et_kernel_metadata", {})
  95. assert isinstance(et_kernel_metadata, dict)
  96. custom_classes = data.get("custom_classes", [])
  97. assert isinstance(custom_classes, Iterable)
  98. custom_classes = set(custom_classes)
  99. build_features = data.get("build_features", [])
  100. assert isinstance(build_features, Iterable)
  101. build_features = set(build_features)
  102. include_all_non_op_selectives = data.get("include_all_non_op_selectives", False)
  103. assert isinstance(include_all_non_op_selectives, bool)
  104. return SelectiveBuilder(
  105. include_all_operators,
  106. debug_info,
  107. operators,
  108. kernel_metadata,
  109. et_kernel_metadata,
  110. custom_classes, # type: ignore[arg-type]
  111. build_features, # type: ignore[arg-type]
  112. include_all_non_op_selectives,
  113. )
  114. @staticmethod
  115. def from_yaml_str(config_contents: str) -> SelectiveBuilder:
  116. contents = yaml.safe_load(config_contents)
  117. return SelectiveBuilder.from_yaml_dict(contents)
  118. @staticmethod
  119. def from_yaml_path(config_path: str) -> SelectiveBuilder:
  120. with open(config_path) as f:
  121. contents = yaml.safe_load(f)
  122. return SelectiveBuilder.from_yaml_dict(contents)
  123. @staticmethod
  124. def from_legacy_op_registration_allow_list(
  125. allow_list: set[str], is_root_operator: bool, is_used_for_training: bool
  126. ) -> SelectiveBuilder:
  127. operators = {}
  128. for op in allow_list:
  129. operators[op] = {
  130. "name": op,
  131. "is_root_operator": is_root_operator,
  132. "is_used_for_training": is_used_for_training,
  133. "include_all_overloads": True,
  134. }
  135. return SelectiveBuilder.from_yaml_dict(
  136. {
  137. "operators": operators,
  138. "include_all_non_op_selectives": True,
  139. }
  140. )
  141. def is_operator_selected(self, name: str) -> bool:
  142. if self.include_all_operators:
  143. return True
  144. if name in self.operators:
  145. return True
  146. name = strip_operator_overload_name(name)
  147. return name in self.operators and self.operators[name].include_all_overloads
  148. def is_native_function_selected(self, func: NativeFunction) -> bool:
  149. op_name = op_name_from_native_function(func)
  150. return self.is_operator_selected(op_name)
  151. def is_operator_selected_for_training(self, name: str) -> bool:
  152. if not self.is_operator_selected(name):
  153. return False
  154. if self.include_all_operators:
  155. return True
  156. not_training_op = SelectiveBuildOperator(
  157. name="",
  158. is_root_operator=False,
  159. is_used_for_training=False,
  160. include_all_overloads=False,
  161. _debug_info=None,
  162. )
  163. op = not_training_op
  164. if name in self.operators:
  165. op = self.operators[name]
  166. name = strip_operator_overload_name(name)
  167. base_op = not_training_op
  168. if name in self.operators:
  169. base_op = self.operators[name]
  170. return op.is_used_for_training or (
  171. base_op.include_all_overloads and base_op.is_used_for_training
  172. )
  173. def is_native_function_selected_for_training(self, func: NativeFunction) -> bool:
  174. op_name = op_name_from_native_function(func)
  175. return self.is_operator_selected_for_training(op_name)
  176. def is_root_operator(self, name: str) -> bool:
  177. if not self.is_operator_selected(name):
  178. return False
  179. if self.include_all_operators:
  180. return True
  181. if name in self.operators:
  182. op: SelectiveBuildOperator = self.operators[name]
  183. return op.is_root_operator
  184. name = strip_operator_overload_name(name)
  185. if name not in self.operators:
  186. return False
  187. base_op: SelectiveBuildOperator = self.operators[name]
  188. return base_op.include_all_overloads and base_op.is_root_operator
  189. def is_kernel_dtype_selected(self, kernel_tag: str, dtype: str) -> bool:
  190. if self.include_all_operators or self.include_all_non_op_selectives:
  191. return True
  192. return (
  193. kernel_tag in self.kernel_metadata
  194. and dtype in self.kernel_metadata[kernel_tag]
  195. )
  196. def et_get_selected_kernels(self, op_name: str, kernel_key: list[str]) -> list[str]:
  197. """
  198. Return a list of kernel keys that cover the used ops
  199. """
  200. # If no kernel metadata, either it's implied by include_all_operators=True or the op is not used.
  201. if op_name not in self.et_kernel_metadata:
  202. return kernel_key if self.include_all_operators else []
  203. # Otherwise, only return the specific kernel keys.
  204. result_set = set()
  205. for model_kernel_keys in self.et_kernel_metadata[op_name]:
  206. key_found = False
  207. for key in kernel_key:
  208. # Don't compare the version for now
  209. if (
  210. key != "default"
  211. and key.split("/")[1] == model_kernel_keys.split("/")[1]
  212. ):
  213. result_set.add(key)
  214. key_found = True
  215. break
  216. if not key_found:
  217. if "default" not in kernel_key:
  218. raise Exception("Missing kernel for the model") # noqa: TRY002
  219. else:
  220. result_set.add("default")
  221. return list(result_set)
  222. def to_dict(self) -> dict[str, object]:
  223. ret: dict[str, object] = {
  224. "include_all_non_op_selectives": self.include_all_non_op_selectives,
  225. "include_all_operators": self.include_all_operators,
  226. }
  227. operators = {}
  228. for op_name, op in self.operators.items():
  229. operators[op_name] = op.to_dict()
  230. ret["operators"] = operators
  231. if self._debug_info is not None:
  232. ret["debug_info"] = sorted(self._debug_info)
  233. ret["kernel_metadata"] = {
  234. k: sorted(v) for (k, v) in self.kernel_metadata.items()
  235. }
  236. ret["et_kernel_metadata"] = self.et_kernel_metadata
  237. ret["custom_classes"] = sorted(self.custom_classes)
  238. ret["build_features"] = sorted(self.build_features)
  239. return ret
  240. def merge_kernel_metadata(
  241. lhs: dict[str, list[str]],
  242. rhs: dict[str, list[str]],
  243. ) -> dict[str, list[str]]:
  244. kernel_metadata: dict[str, list[str]] = {}
  245. for tag_name, dtypes in list(lhs.items()) + list(rhs.items()):
  246. dtypes_copy = set(dtypes)
  247. if tag_name in kernel_metadata:
  248. dtypes_copy |= set(kernel_metadata[tag_name])
  249. kernel_metadata[tag_name] = list(dtypes_copy)
  250. return kernel_metadata
  251. def merge_et_kernel_metadata(
  252. lhs: dict[str, list[str]],
  253. rhs: dict[str, list[str]],
  254. ) -> dict[str, list[str]]:
  255. merge_et_kernel_metadata: dict[str, set[str]] = defaultdict(set)
  256. for op in list(lhs.keys()) + list(rhs.keys()):
  257. merge_et_kernel_metadata[op].update(lhs.get(op, []))
  258. merge_et_kernel_metadata[op].update(rhs.get(op, []))
  259. return {op: sorted(val) for op, val in merge_et_kernel_metadata.items()}
  260. def combine_selective_builders(
  261. lhs: SelectiveBuilder, rhs: SelectiveBuilder
  262. ) -> SelectiveBuilder:
  263. include_all_operators = lhs.include_all_operators or rhs.include_all_operators
  264. debug_info = merge_debug_info(lhs._debug_info, rhs._debug_info)
  265. operators = merge_operator_dicts(lhs.operators, rhs.operators)
  266. kernel_metadata = merge_kernel_metadata(lhs.kernel_metadata, rhs.kernel_metadata)
  267. et_kernel_metadata = merge_et_kernel_metadata(
  268. lhs.et_kernel_metadata, rhs.et_kernel_metadata
  269. )
  270. include_all_non_op_selectives = (
  271. lhs.include_all_non_op_selectives or rhs.include_all_non_op_selectives
  272. )
  273. custom_classes = lhs.custom_classes.union(rhs.custom_classes)
  274. build_features = lhs.build_features.union(rhs.build_features)
  275. return SelectiveBuilder(
  276. include_all_operators,
  277. debug_info,
  278. operators,
  279. kernel_metadata,
  280. et_kernel_metadata,
  281. custom_classes,
  282. build_features,
  283. include_all_non_op_selectives,
  284. )
  285. def op_name_from_native_function(f: NativeFunction) -> str:
  286. # This was originally read from the 'operator_name_with_overload' field in the
  287. # declaration dict, which was the part before the first '(' in 'schema_string'.
  288. return f"{f.namespace}::{f.func.name}"