choices.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. from __future__ import annotations
  2. import typing
  3. from typing import Any, Optional, TYPE_CHECKING, Union
  4. import sympy
  5. import torch
  6. from . import config
  7. from .codecache import write_text
  8. from .kernel_inputs import KernelInputs # noqa: TC001
  9. from .metrics import get_metric_table, is_metric_table_enabled
  10. from .runtime.hints import DeviceProperties, ReductionHint
  11. from .scheduler import BaseSchedulerNode, Scheduler, WhyNoFuse
  12. from .template_heuristics import get_template_heuristic
  13. from .template_heuristics.triton import (
  14. BaseConfigHeuristic,
  15. CPUConfigHeuristic,
  16. CUDAConfigHeuristic,
  17. MTIAConfigHeuristic,
  18. ROCmConfigHeuristic,
  19. XPUConfigHeuristic,
  20. )
  21. from .virtualized import V
  22. if TYPE_CHECKING:
  23. from collections.abc import Generator
  24. from functools import partial
  25. from triton import Config as TritonConfig
  26. from torch.utils._ordered_set import OrderedSet
  27. from .codegen.common import KernelTemplate
  28. from .codegen.simd_kernel_features import SIMDKernelFeatures
  29. from .codegen.triton import TritonKernel
  30. from .ir import ChoiceCaller
  31. from .select_algorithm import ExternKernelChoice
  32. class Sortable(typing.Protocol):
  33. """Anything that can be used as a list.sort() key (int/tuple/etc)"""
  34. def __lt__(self, other: typing.Self) -> bool: ...
  35. class InductorChoices:
  36. """
  37. This class contains a collection of default heuristics that effect performance of our generated
  38. code. We try to not put correctness requirements in this file.
  39. You can override the choices made here by doing:
  40. class MyHeuristics(InductorChoices):
  41. ...
  42. torch._inductor.virtualized.V.set_choices_handler(MyHeuristics())
  43. """
  44. def get_config_heuristics(
  45. self, device_type: Optional[str] = "cuda"
  46. ) -> BaseConfigHeuristic:
  47. if device_type == "cuda":
  48. if torch.version.hip is None:
  49. return CUDAConfigHeuristic()
  50. else:
  51. return ROCmConfigHeuristic()
  52. elif device_type == "xpu":
  53. return XPUConfigHeuristic()
  54. elif device_type == "cpu":
  55. return CPUConfigHeuristic()
  56. elif device_type == "mtia":
  57. return MTIAConfigHeuristic()
  58. else:
  59. return BaseConfigHeuristic()
  60. # Conv configs
  61. def get_conv_configs(
  62. self, device_type: Optional[str] = "cuda"
  63. ) -> partial[Generator[TritonConfig, None, None]]:
  64. conv_heuristics = self.get_config_heuristics(device_type)
  65. return conv_heuristics.get_conv_configs()
  66. # Flex attention configs
  67. # TODO(coconutruben): break out flexattention/decode configs into the new retrieval mechanism
  68. def get_flex_attention_fwd_configs(
  69. self, head_dim: int, dtype: torch.dtype, device_type: Optional[str] = "cuda"
  70. ) -> list[Any]:
  71. flex_heuristics = self.get_config_heuristics(device_type)
  72. return flex_heuristics.get_flex_attn_fwd_configs(head_dim, dtype)
  73. def get_flex_attention_bwd_configs(
  74. self, head_dim: int, dtype: torch.dtype, device_type: Optional[str] = "cuda"
  75. ) -> list[Any]:
  76. flex_heuristics = self.get_config_heuristics(device_type)
  77. return flex_heuristics.get_flex_attn_bwd_configs(head_dim, dtype)
  78. def get_flex_decode_configs(
  79. self, head_dim: int, dtype: torch.dtype, device_type: Optional[str] = "cuda"
  80. ) -> list[Any]:
  81. flex_heuristics = self.get_config_heuristics(device_type)
  82. return flex_heuristics.get_flex_decode_configs(head_dim, dtype)
  83. def get_mm_configs(
  84. self,
  85. kernel_inputs: KernelInputs,
  86. layout: Any,
  87. templates: list[Union[KernelTemplate, ExternKernelChoice]],
  88. op_name: str,
  89. kwarg_overrides: Optional[dict[str, dict[str, Any]]] = None,
  90. ) -> Generator[ChoiceCaller, None, None]:
  91. """
  92. Get generator of ChoiceCallers for MM templates using template-specific heuristics.
  93. Args:
  94. kernel_inputs: MMKernelInputs containing input tensor nodes and matrix indices
  95. layout: Output layout
  96. templates: List of template objects (KernelTemplate or ExternKernelChoice)
  97. op_name: Operation name (e.g., "bmm", "baddbmm", "addmm", "mm_plus_mm")
  98. kwarg_overrides: Optional dict of kwargs to override for each template heuristic,
  99. indexed by template.uid. These only override the per config kwargs, not the extra kwargs
  100. Yields:
  101. ChoiceCaller objects from the templates
  102. """
  103. if kwarg_overrides is None:
  104. kwarg_overrides = {}
  105. input_tensors = kernel_inputs.nodes()
  106. if len(input_tensors) < 2:
  107. raise ValueError(f"Need at least 2 input tensors, got {len(input_tensors)}")
  108. # Extract device_type from kernel_inputs
  109. device_type = kernel_inputs.device_type
  110. assert device_type is not None, "get_mm_configs requires a valid device type"
  111. for template in templates:
  112. # Extract template_name from the template object
  113. template_name = template.uid
  114. # Get the appropriate template-specific heuristic
  115. heuristic = get_template_heuristic(template_name, device_type, op_name)
  116. cs = heuristic.get_template_configs(
  117. kernel_inputs,
  118. layout,
  119. op_name,
  120. )
  121. extra_kwargs = heuristic.get_extra_kwargs(kernel_inputs, layout, op_name)
  122. # Extract layout and input_nodes from extra_kwargs to pass them explicitly
  123. layout_val = layout
  124. # adjust the kernel inputs to the template-specific heuristic, if needed
  125. # default here is to just return the kernel_inputs as is
  126. input_nodes_val = heuristic.adjust_kernel_inputs(
  127. kernel_inputs, op_name
  128. ).nodes()
  129. # Get overrides for this specific template
  130. overrides = kwarg_overrides.get(template.uid, {})
  131. extra_kwargs["layout"] = layout_val
  132. extra_kwargs["input_nodes"] = input_nodes_val
  133. for c in cs:
  134. choice = template.choice_or_none(**{**c, **overrides}, **extra_kwargs)
  135. if choice is not None:
  136. yield choice
  137. def triton_kernel_kwargs(
  138. self,
  139. kernel_cls: type[TritonKernel],
  140. features: SIMDKernelFeatures,
  141. groups: list[sympy.Expr],
  142. kernel_kwargs: dict[str, Any],
  143. ) -> dict[str, Any]:
  144. """Hook to change the kwargs passed to TritonKernel, used to apply fixed configurations"""
  145. return kernel_kwargs
  146. @staticmethod
  147. def should_use_cooperative_reduction(features: SIMDKernelFeatures) -> bool:
  148. """Heuristic to decide if a cooperative reduction should be used."""
  149. if config.triton.force_cooperative_reductions:
  150. return True
  151. if (
  152. not config.triton.cooperative_reductions
  153. or V.graph.get_current_device_or_throw().type == "cpu"
  154. ):
  155. return False
  156. xhint = V.graph.sizevars.size_hint(features.numel, fallback=2)
  157. if xhint <= 8:
  158. threshold = 32768 * xhint
  159. elif xhint <= 16:
  160. threshold = 2097152
  161. else:
  162. return False
  163. # TODO(jansel): should this default on for dynamic shapes?
  164. return V.graph.sizevars.statically_known_geq(
  165. features.reduction_numel, threshold
  166. )
  167. @staticmethod
  168. def should_use_persistent_reduction(
  169. features: SIMDKernelFeatures, cooperative_reduction: bool
  170. ) -> bool:
  171. """
  172. Heuristic to decide if a persistent reduction should be used.
  173. """
  174. if not config.triton.persistent_reductions:
  175. return False
  176. threshold = {
  177. ReductionHint.INNER: 1024,
  178. }.get(features.get_reduction_hint(), 64)
  179. if cooperative_reduction:
  180. # The RSPLIT of cooperative reductions means each thread block is operating on fewer elements
  181. try:
  182. threshold *= 32 // min(
  183. V.graph.sizevars.size_hint_or_throw(features.numel), 32
  184. )
  185. except ValueError:
  186. pass # unbacked symint
  187. # If multi_kernel is enabled, we do more aggressive persistent reduction.
  188. # This may result in some persistent reductions slower than the
  189. # corresponding non-persistent reductions. MultiKernel will do benchmarking
  190. # to pick the faster one.
  191. if config.triton.multi_kernel:
  192. threshold *= 16
  193. return V.graph.sizevars.statically_known_leq(
  194. features.reduction_numel, threshold
  195. ) # type: ignore[arg-types]
  196. @staticmethod
  197. def reduction_split_factor(
  198. device: torch.device,
  199. reduction_numel_hint: int,
  200. numel_hint: int,
  201. inner_reduction: bool,
  202. ) -> int:
  203. """Heuristic to decide the RSPLIT used for split reductions.
  204. When a reduction has a small number of outputs there is not enough parallelism,
  205. so we will do the reduction in two phases."""
  206. props = DeviceProperties.create(device)
  207. num_sm = props.multi_processor_count
  208. min_elements_per_thread = 32
  209. max_elements_per_thread = 512
  210. threads_per_sm = 2048
  211. min_elements_per_device = min_elements_per_thread * num_sm * threads_per_sm
  212. max_elements_per_device = max_elements_per_thread * num_sm * threads_per_sm
  213. num_warps = 8
  214. num_threads = 32 * num_warps
  215. if inner_reduction:
  216. # do heuristics that's close to eager mode for split inner reduction
  217. # we leak reduction autotune configs here, and will need to refactor to avoid this later
  218. if numel_hint >= 2 * num_sm: # don't split if there are enough outputs
  219. return 1
  220. if reduction_numel_hint <= 8192:
  221. return 1
  222. if reduction_numel_hint * numel_hint <= min_elements_per_device:
  223. split_size = min_elements_per_thread
  224. elif reduction_numel_hint * numel_hint < max_elements_per_device:
  225. target_blocks = num_sm * threads_per_sm // (2 * num_threads)
  226. blocks_per_output = (target_blocks + numel_hint - 1) // numel_hint
  227. tmp_split_size = (
  228. reduction_numel_hint + num_threads * blocks_per_output - 1
  229. ) // (num_threads * blocks_per_output)
  230. divisors = sympy.divisors(reduction_numel_hint)
  231. closest = min(divisors, key=lambda x: abs(x - tmp_split_size))
  232. if abs(closest - tmp_split_size) < 30:
  233. # prefer even splits, but never smalle than min_elements_per_thread
  234. split_size = max(closest, min_elements_per_thread)
  235. else:
  236. split_size = tmp_split_size
  237. else:
  238. divisors = sympy.divisors(reduction_numel_hint)
  239. closest = min(divisors, key=lambda x: abs(x - max_elements_per_thread))
  240. if abs(closest - max_elements_per_thread) < 50:
  241. # prefer even splits
  242. split_size = closest
  243. else:
  244. split_size = max_elements_per_thread
  245. return (reduction_numel_hint + split_size * num_threads - 1) // (
  246. split_size * num_threads
  247. )
  248. else:
  249. # TODO the best heuristic currently has XBLOCK (corresponding to numel_hint) 128
  250. # extend to even smaller number of outputs
  251. rvals_per_thread = 4 # comes from heuristics, refactor to not leak here
  252. xvals_per_block = 128
  253. xblocks = (numel_hint + xvals_per_block - 1) // xvals_per_block
  254. if reduction_numel_hint * numel_hint < min_elements_per_device:
  255. split_size = min_elements_per_thread
  256. elif reduction_numel_hint * numel_hint < max_elements_per_device:
  257. target_blocks = num_sm * threads_per_sm // (num_threads)
  258. target_blocks = (target_blocks + xblocks - 1) // xblocks
  259. tmp_split_size = (
  260. reduction_numel_hint + rvals_per_thread * target_blocks - 1
  261. ) // (rvals_per_thread * target_blocks)
  262. divisors = sympy.divisors(reduction_numel_hint)
  263. closest = min(divisors, key=lambda x: abs(x - tmp_split_size))
  264. if abs(tmp_split_size - closest) < 20:
  265. split_size = max(closest, min_elements_per_thread)
  266. else:
  267. split_size = tmp_split_size
  268. else:
  269. divisors = sympy.divisors(reduction_numel_hint)
  270. closest = min(divisors, key=lambda x: abs(x - max_elements_per_thread))
  271. if abs(closest - max_elements_per_thread) < 50:
  272. # prefer even splits
  273. split_size = closest
  274. else:
  275. split_size = max_elements_per_thread
  276. return (reduction_numel_hint + rvals_per_thread * split_size - 1) // (
  277. rvals_per_thread * split_size
  278. )
  279. @staticmethod
  280. def can_fuse(
  281. scheduler: Scheduler,
  282. node1: BaseSchedulerNode,
  283. node2: BaseSchedulerNode,
  284. shared_data_score: int,
  285. ) -> bool:
  286. """
  287. Heuristics to prevent fusion applied to both horizontal and vertical fusions. Heuristics here should not
  288. be needed for correctness and tweaking them may yield additional performance.
  289. See also some related heuristics that can be changed via config:
  290. - config.triton.tiling_prevents_pointwise_fusion
  291. - config.triton.tiling_prevents_reduction_fusion
  292. - config.aggressive_fusion (will cause this function to be called more times)
  293. """
  294. if shared_data_score == 0 and (
  295. not config.aggressive_fusion or node1.is_reduction() or node2.is_reduction()
  296. ):
  297. if is_metric_table_enabled("fusion_failure_due_to_indexing_mismatch"):
  298. common_buf_names: OrderedSet[str] = (
  299. node1.read_writes.buffer_names() & node2.read_writes.buffer_names()
  300. )
  301. if len(common_buf_names) > 0:
  302. get_metric_table("fusion_failure_due_to_indexing_mismatch").add_row(
  303. lambda: {
  304. "pre_grad_graph_id": V.graph.graph_id,
  305. "post_grad_graph_id": V.graph.post_grad_graph_id,
  306. "node1_name": node1.get_name(),
  307. "node2_name": node2.get_name(),
  308. "node1_debug_str": write_text(node1.debug_str()),
  309. "node2_debug_str": write_text(node2.debug_str()),
  310. "common_buffer_names": list(common_buf_names), # type: ignore[dict-item]
  311. "failure_reason": scheduler.decide_fusion_fail_reason(
  312. node1, node2, common_buf_names
  313. ),
  314. }
  315. )
  316. WhyNoFuse(node1, node2)("no shared data due to indexing mismatch")
  317. return False
  318. WhyNoFuse(node1, node2)("no shared data")
  319. return False # heuristic not needed for correctness
  320. if (
  321. not node1.is_foreach()
  322. and not node2.is_foreach()
  323. and len(node1.get_nodes()) + len(node2.get_nodes()) > config.max_fusion_size
  324. ):
  325. WhyNoFuse(node1, node2)("exceeds max fusion")
  326. return False # heuristic not needed for correctness
  327. if scheduler.can_fusion_increase_peak_memory(node1, node2):
  328. WhyNoFuse(node1, node2)("Fusion will increase peak memory")
  329. return False
  330. if (
  331. config.realize_acc_reads_size_threshold is not None
  332. and scheduler.fusion_accumulate_large_reads(
  333. node1,
  334. node2,
  335. config.realize_acc_reads_size_threshold,
  336. )
  337. ):
  338. WhyNoFuse(node1, node2)("Fusion accumulate large amount of reads")
  339. return False
  340. return True
  341. @staticmethod
  342. def can_fuse_vertical(
  343. scheduler: Scheduler,
  344. node1: BaseSchedulerNode,
  345. node2: BaseSchedulerNode,
  346. shared_data_score: int,
  347. ) -> bool:
  348. """Hook for heuristics to prevent vertical (producer/consumer) fusions"""
  349. return True
  350. @staticmethod
  351. def can_fuse_horizontal(
  352. scheduler: Scheduler,
  353. node1: BaseSchedulerNode,
  354. node2: BaseSchedulerNode,
  355. shared_data_score: int,
  356. ) -> bool:
  357. """Hook for heuristics to prevent horizontal (consumer/consumer) fusions"""
  358. if shared_data_score < config.score_fusion_memory_threshold:
  359. WhyNoFuse(node1, node2)("score_fusion_memory_threshold")
  360. return False
  361. if scheduler.are_long_distant_nodes(node1, node2):
  362. WhyNoFuse(node1, node2)(
  363. "Nodes are too far away. Fusing them may increase peak memory."
  364. )
  365. return False
  366. return True
  367. @staticmethod
  368. def score_fusion(
  369. scheduler: Scheduler,
  370. node1: BaseSchedulerNode,
  371. node2: BaseSchedulerNode,
  372. ) -> Sortable:
  373. """
  374. Assign a score (higher comes first) to the fusion of node1 and node2.
  375. When different fusions conflict with each other, this is the way we
  376. decide what order to run them in.
  377. Our current score is based on:
  378. - The type of fusion (template/reduction/etc)
  379. - Estimate of the saved memory operations
  380. - Fusions closer together in original graph order
  381. """
  382. memory_score = scheduler.score_fusion_memory(node1, node2)
  383. proximity_score = -max(
  384. abs(node1.min_order - node2.max_order),
  385. abs(node2.min_order - node1.max_order),
  386. )
  387. # prologue fusion always last
  388. if node2.is_template():
  389. template_score = 0
  390. else:
  391. template_score = 1 + (
  392. (node1.is_template() == config.epilogue_fusion_first)
  393. and memory_score > 0
  394. )
  395. return (
  396. template_score,
  397. node1.is_reduction() == node2.is_reduction() and memory_score > 0,
  398. memory_score,
  399. proximity_score,
  400. )