已合并
!4 merge inductor-dynamic-reduction-group-autotune into v2.10.0_dev #46285
qinhanmin创建于 9月14日
!4 merge inductor-dynamic-reduction-group-autotune into v2.10.0_dev #46285
已合并
qinhanmin创建于 9月14日
共 5 个文件变更+221-10
@@ -104,9 +104,40 @@ class SplitTiling:
104 self.kernel.split_axis.clear()104 self.kernel.split_axis.clear()
105 105 
106 # total numel exceed aicore or total split axis exceed 3106 # total numel exceed aicore or total split axis exceed 3
107+ def has_dynamic_split_axis():
108+ return any(
109+ not isinstance(axis.length, sympy.Integer)
110+ for axis in self.kernel.split_axis
111+ )
112+ 
113+ def is_reduction_kernel():
114+ return any(axis.prefix == "r" for axis in self.kernel.sorted_axis)
115+ 
107 def meet_stop_condition():116 def meet_stop_condition():
108 sv = V.graph.sizevars117 sv = V.graph.sizevars
109 current_numels = self.total_split_numels(self.kernel.split_axis)118 current_numels = self.total_split_numels(self.kernel.split_axis)
119+ # A dynamic split axis already supplies the grid on its own: the size
120+ # hint seen here is one example shape, while the launch derives the
121+ # grid from the runtime numel. Adding a second split axis to reach the
122+ # core count is therefore speculative, and the cost is permanent: a
123+ # split axis can never become a no_loop_axis (see select_no_loop_axis),
124+ # so a static low dim pulled in here loses whole-axis residency and its
125+ # BLOCK_SUB can no longer span the axis. That fragments the innermost
126+ # contiguous dimension for every runtime shape, not just the one whose
127+ # hint was used. Reductions only: their grid comes solely from the
128+ # non-reduction axes, so the dynamic one is the parallelism.
129+ #
130+ # Confined to grouped autotune, which is what supplies the per-bucket
131+ # runtime block that makes one split axis sufficient. Without it the
132+ # single axis would keep the hint's block at every shape, so the old
133+ # speculative second axis stays the better bet.
134+ if (
135+ npu_config.enable_symbolic_shape_group_autotune
136+ and self.kernel.split_axis
137+ and is_reduction_kernel()
138+ and has_dynamic_split_axis()
139+ ):
140+ return True
110 try:141 try:
111 val = sv.size_hint(current_numels)142 val = sv.size_hint(current_numels)
112 except TypeError:143 except TypeError:
@@ -436,6 +467,20 @@ class SplitTiling:
436 # stays a single boundary; the ~1e6 1D corner is out of scope.467 # stays a single boundary; the ~1e6 1D corner is out of scope.
437 _REDUCTION_BUCKETS = (8192,)468 _REDUCTION_BUCKETS = (8192,)
438 _OUTER_BUCKETS = (256,)469 _OUTER_BUCKETS = (256,)
470+ # Boundaries for an outer group that also carries static axes, in units of the
471+ # dynamic axis (see _scale_buckets_by_static_factor). Such a group spans every
472+ # runtime size from a grid-starved shape up to a device-saturating one, which
473+ # a single bucket cannot serve: the representative bakes the compile-time
474+ # sub-blocks, and those decide how many rows each program walks.
475+ #
476+ # The boundaries are multiples of the core count because that is what makes a
477+ # bucket internally uniform: every shape in (k-1)*cores .. k*cores wants the
478+ # same k rows per program, so the representative's sub-blocks are right for
479+ # all of them. Aligning the boundaries anywhere else leaves shapes that want
480+ # fewer rows sharing a variant tuned for more.
481+ _OUTER_BUCKETS_WITH_STATIC_AXES = tuple(
482+ num_vector_core * k for k in range(1, 6)
483+ )
439 484 
440 def _axis_static_length(self, axis):485 def _axis_static_length(self, axis):
441 try:486 try:
@@ -446,6 +491,29 @@ class SplitTiling:
446 except (AttributeError, KeyError, TypeError, ValueError):491 except (AttributeError, KeyError, TypeError, ValueError):
447 return 1492 return 1
448 493 
494+ def _static_group_factor(self, axis_names):
495+ names = set(axis_names)
496+ factor = 1
497+ for axis in self.kernel.sorted_axis:
498+ if axis.name in names and isinstance(axis.length, sympy.Integer):
499+ factor *= max(1, self._axis_static_length(axis))
500+ return factor
501+ 
502+ def _scale_buckets_by_static_factor(self, buckets, axis_names, wide_buckets=None):
503+ # The boundaries describe the group PRODUCT, so the static axes of the group
504+ # are already baked into every runtime value. A group whose static factor
505+ # exceeds a boundary would otherwise put every reachable shape in the open
506+ # tail, and the tail representative (2 * boundary) divided by that factor
507+ # collapses the dynamic axis to a single element: the group then gets tuned
508+ # on a workload that cannot fill the grid. Scaling keeps the boundaries
509+ # expressed in the dynamic axis, which is what the buckets mean to separate.
510+ static_factor = self._static_group_factor(axis_names)
511+ if static_factor <= 1:
512+ return buckets
513+ if wide_buckets is not None:
514+ buckets = wide_buckets
515+ return tuple(boundary * static_factor for boundary in buckets)
516+ 
449 def _default_dynamic_axis_feature(517 def _default_dynamic_axis_feature(
450 self,518 self,
451 dynamic_split_axes,519 dynamic_split_axes,
@@ -543,7 +611,14 @@ class SplitTiling:
543 if outer_names and self._has_dynamic_axis(outer_names):611 if outer_names and self._has_dynamic_axis(outer_names):
544 features.append(612 features.append(
545 GroupFeatureSpec(613 GroupFeatureSpec(
546- "outer", "outer_product", outer_names, self._OUTER_BUCKETS614+ "outer",
615+ "outer_product",
616+ outer_names,
617+ self._scale_buckets_by_static_factor(
618+ self._OUTER_BUCKETS,
619+ outer_names,
620+ self._OUTER_BUCKETS_WITH_STATIC_AXES,
621+ ),
547 )622 )
548 )623 )
549 if reduction_names and self._has_dynamic_axis(reduction_names):624 if reduction_names and self._has_dynamic_axis(reduction_names):
@@ -552,7 +627,9 @@ class SplitTiling:
552 "reduction",627 "reduction",
553 "reduction_product",628 "reduction_product",
554 reduction_names,629 reduction_names,
555- self._REDUCTION_BUCKETS,630+ self._scale_buckets_by_static_factor(
631+ self._REDUCTION_BUCKETS, reduction_names
632+ ),
556 )633 )
557 )634 )
558 return tuple(features)635 return tuple(features)
@@ -166,6 +166,9 @@ inductor_config.triton.mix_order_reduction = False
166enable_fast_gelu = os.getenv("TORCHINDUCTOR_ENABLE_FAST_GELU", "0") == "1"166enable_fast_gelu = os.getenv("TORCHINDUCTOR_ENABLE_FAST_GELU", "0") == "1"
167allow_pad_lowering = os.getenv("TORCHINDUCTOR_ENABLE_PAD_LOWERING", "0") == "1"167allow_pad_lowering = os.getenv("TORCHINDUCTOR_ENABLE_PAD_LOWERING", "0") == "1"
168allow_searchsorted_lowering = os.getenv("TORCHINDUCTOR_ENABLE_SEARCHSORTED_LOWERING", "0") == "1"168allow_searchsorted_lowering = os.getenv("TORCHINDUCTOR_ENABLE_SEARCHSORTED_LOWERING", "0") == "1"
169+allow_embedding_dense_backward_lowering = os.getenv(
170+ "TORCHINDUCTOR_ENABLE_EMBEDDING_DENSE_BACKWARD_LOWERING", "0"
171+) == "1"
169enable_flex_attention_dq_before_scale_materialize = os.environ.get(172enable_flex_attention_dq_before_scale_materialize = os.environ.get(
170 "FLEX_ATTENTION_DQ_BEFORE_SCALE_MATERIALIZE", "1"173 "FLEX_ATTENTION_DQ_BEFORE_SCALE_MATERIALIZE", "1"
171).lower() in ("1", "true", "yes")174).lower() in ("1", "true", "yes")
@@ -168,7 +168,11 @@ def _matmul_backward_inductor(grad, self, other, mask):
168 168 
169 169 
170def _register_triton_decompositions():170def _register_triton_decompositions():
171- from .config import is_ascend950, enable_fast_gelu171+ from .config import (
172+ is_ascend950,
173+ enable_fast_gelu,
174+ allow_embedding_dense_backward_lowering,
175+ )
172 from .lowering import _add_overload176 from .lowering import _add_overload
173 177 
174 DECOMPOSITION_OVERLOAD_OP = [178 DECOMPOSITION_OVERLOAD_OP = [
@@ -181,7 +185,11 @@ def _register_triton_decompositions():
181 aten.expm1,185 aten.expm1,
182 aten.native_layer_norm,186 aten.native_layer_norm,
183 aten.repeat_interleave.Tensor, # perf issue187 aten.repeat_interleave.Tensor, # perf issue
184- aten.embedding_dense_backward,188+ *(
189+ [aten.embedding_dense_backward]
190+ if not allow_embedding_dense_backward_lowering
191+ else []
192+ ),
185 aten.matmul.default,193 aten.matmul.default,
186 aten.matmul_backward.default,194 aten.matmul_backward.default,
187 ]195 ]
@@ -36,6 +36,7 @@ from .config import (
36 inductor_ascend_linear_mode,36 inductor_ascend_linear_mode,
37 allow_pad_lowering,37 allow_pad_lowering,
38 allow_searchsorted_lowering,38 allow_searchsorted_lowering,
39+ allow_embedding_dense_backward_lowering,
39)40)
40 41 
41aten = torch.ops.aten42aten = torch.ops.aten
@@ -606,7 +607,7 @@ TORCH_NATIVE_FALLBACK_LIST = [
606 aten._embedding_bag_forward_only.out,607 aten._embedding_bag_forward_only.out,
607 aten._embedding_bag_per_sample_weights_backward.default,608 aten._embedding_bag_per_sample_weights_backward.default,
608 aten._embedding_bag_per_sample_weights_backward.out,609 aten._embedding_bag_per_sample_weights_backward.out,
609- aten.embedding_dense_backward.default,610+ *([aten.embedding_dense_backward.default] if not allow_embedding_dense_backward_lowering else []),
610 aten._fft_r2c.default,611 aten._fft_r2c.default,
611 aten._fft_r2c.out,612 aten._fft_r2c.out,
612 aten._flash_attention_backward.default,613 aten._flash_attention_backward.default,
@@ -2577,7 +2577,37 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2577 return torch.zeros(count, device=device, dtype=dtype)2577 return torch.zeros(count, device=device, dtype=dtype)
2578 return torch.empty(count, device=device, dtype=dtype)2578 return torch.empty(count, device=device, dtype=dtype)
2579 2579 
2580- def materialize_spec(spec, kind):2580+ def integer_benchmark_range(spec_index):
2581+ """Value range the real argument at this position actually spans.
2582+ 
2583+ rand_strided zero-fills integer tensors, so every indirect load in
2584+ the benchmark resolves to the same element and the gather costs
2585+ nothing. A gather kernel is then ranked on memory behaviour it will
2586+ never see: for an embedding reduction the candidates that handle
2587+ scattered rows well and the ones that do not land within a few
2588+ percent of each other, while differing by an order of magnitude on
2589+ real data. Sampling the range the real argument spans restores the
2590+ divergence, and cannot go out of bounds because the real launch
2591+ already indexes with exactly these values.
2592+ 
2593+ Restricted to reduction kernels so that every other grouped
2594+ heuristic keeps benchmarking on byte-identical inputs to today.
2595+ """
2596+ if self.heuristic_type not in (
2597+ HeuristicType.REDUCTION,
2598+ HeuristicType.PERSISTENT_REDUCTION,
2599+ ):
2600+ return None
2601+ if spec_index >= len(self._grouped_runtime_args_snapshot):
2602+ return None
2603+ real_arg = self._grouped_runtime_args_snapshot[spec_index]
2604+ if not isinstance(real_arg, torch.Tensor) or real_arg.numel() == 0:
2605+ return None
2606+ if real_arg.dtype.is_floating_point or real_arg.dtype == torch.bool:
2607+ return None
2608+ return int(real_arg.min()), int(real_arg.max())
2609+ 
2610+ def materialize_spec(spec, kind, spec_index=-1):
2581 spec_kind = spec.get("kind")2611 spec_kind = spec.get("kind")
2582 source = spec.get("source")2612 source = spec.get("source")
2583 if source == "runtime_arg":2613 if source == "runtime_arg":
@@ -2596,7 +2626,12 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2596 tensor_factory = getattr(self, "_benchmark_tensor_factory", None)2626 tensor_factory = getattr(self, "_benchmark_tensor_factory", None)
2597 if tensor_factory is None:2627 if tensor_factory is None:
2598 tensor_factory = rand_strided2628 tensor_factory = rand_strided
2599- return tensor_factory(size, stride, dtype, spec["device"])2629+ tensor = tensor_factory(size, stride, dtype, spec["device"])
2630+ if not dtype.is_floating_point and dtype != torch.bool:
2631+ value_range = integer_benchmark_range(spec_index)
2632+ if value_range is not None:
2633+ tensor.random_(value_range[0], value_range[1] + 1)
2634+ return tensor
2600 if spec_kind == "workspace" and source == "workspace":2635 if spec_kind == "workspace" and source == "workspace":
2601 count = eval_expr(spec["count_expr"])2636 count = eval_expr(spec["count_expr"])
2602 return workspace_factory(2637 return workspace_factory(
@@ -2608,8 +2643,10 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2608 raise RuntimeError(f"{kind} spec source {source} is not supported")2643 raise RuntimeError(f"{kind} spec source {source} is not supported")
2609 2644 
2610 return tuple(2645 return tuple(
2611- materialize_spec(spec, f"{spec.get('kind', 'unknown')} arg")2646+ materialize_spec(spec, f"{spec.get('kind', 'unknown')} arg", spec_index)
2612- for spec in self.candidate_plan.get("ordered_arg_specs", ())2647+ for spec_index, spec in enumerate(
2648+ self.candidate_plan.get("ordered_arg_specs", ())
2649+ )
2613 )2650 )
2614 2651 
2615 def _build_grouped_benchmark_entries(self):2652 def _build_grouped_benchmark_entries(self):
@@ -2701,6 +2738,12 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2701 timings[entry_idx] = timing2738 timings[entry_idx] = timing
2702 return tuple(timings)2739 return tuple(timings)
2703 2740 
2741+ # Leaves only the candidates that can still win this group as None, so the
2742+ # full measurement below runs on those alone.
2743+ self._screen_grouped_entries(
2744+ entries, kernel_funcs, kernel_entry_indices, timings
2745+ )
2746+ 
2704 return tuple(2747 return tuple(
2705 timing2748 timing
2706 if timing is not None2749 if timing is not None
@@ -2713,6 +2756,76 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2713 for entry, timing in zip(entries, timings)2756 for entry, timing in zip(entries, timings)
2714 )2757 )
2715 2758 
2759+ # Screening pays for itself only above the selector's own ceiling: below it the
2760+ # screen would time every candidate and then keep them all anyway.
2761+ _GROUPED_SCREEN_MIN_CANDIDATES = 200
2762+ 
2763+ def _screen_grouped_entries(
2764+ self, entries, kernel_funcs, kernel_entry_indices, timings
2765+ ):
2766+ """Rank a group's candidates by one launch each, keeping only contenders.
2767+ 
2768+ The non-grouped sweep already works this way (see the prerun block in
2769+ _benchmark_candidate_entries); the grouped sweep never picked it up and so
2770+ pays the full repeated measurement for every candidate. A specialized
2771+ compile can afford that, but a grouped reduction benchmarks a bucket
2772+ representative rather than a real shape: the pool is an order of magnitude
2773+ larger, and it holds configs whose loop count explodes at the
2774+ representative's reduction length. Measuring those to full precision only
2775+ establishes how badly they lose.
2776+ 
2777+ Candidates that are screened out keep their single-launch time as their
2778+ timing. That is a fair ranking for them because they are not going to be
2779+ the group's winner, and the winner itself is still chosen by the same full
2780+ measurement as before.
2781+ 
2782+ Mutates `timings` in place; entries left at None are the ones to measure.
2783+ """
2784+ if self.heuristic_type not in (
2785+ HeuristicType.REDUCTION,
2786+ HeuristicType.PERSISTENT_REDUCTION,
2787+ ):
2788+ return
2789+ if len(kernel_entry_indices) <= self._GROUPED_SCREEN_MIN_CANDIDATES:
2790+ return
2791+ 
2792+ screened_ms = {}
2793+ by_group = {}
2794+ for kernel_func, entry_idx in zip(kernel_funcs, kernel_entry_indices):
2795+ try:
2796+ elapsed_ms = _measure_prerun_ms(kernel_func)
2797+ except Exception as exc:
2798+ screen_log_str = (
2799+ f"PreRun [{self.fn.__name__}], grouped candidate: "
2800+ f"{entries[entry_idx]['candidate']['candidate_id']}\n err: {exc}"
2801+ )
2802+ if not autotune_continue_on_failure:
2803+ raise RuntimeError(screen_log_str) from exc
2804+ log.warning(screen_log_str)
2805+ timings[entry_idx] = float("inf")
2806+ continue
2807+ screened_ms[entry_idx] = elapsed_ms
2808+ by_group.setdefault(entries[entry_idx]["group_id"], []).append(
2809+ (entry_idx, elapsed_ms)
2810+ )
2811+ 
2812+ # Each group picks its own winner, so each group keeps its own contenders.
2813+ selected = set()
2814+ for group_timings in by_group.values():
2815+ selected |= set(_select_prerun_top_candidates(group_timings))
2816+ 
2817+ for entry_idx, elapsed_ms in screened_ms.items():
2818+ if entry_idx not in selected:
2819+ timings[entry_idx] = elapsed_ms
2820+ 
2821+ log.info(
2822+ "%s grouped screen kept %s of %s candidates across %s groups",
2823+ self.get_fn_name(),
2824+ len(selected),
2825+ len(screened_ms),
2826+ len(by_group),
2827+ )
2828+ 
2716 def _autotune_all_groups(self, *args, **kwargs):2829 def _autotune_all_groups(self, *args, **kwargs):
2717 self._grouped_runtime_args_snapshot = args2830 self._grouped_runtime_args_snapshot = args
2718 try:2831 try:
@@ -3428,7 +3541,16 @@ def build_grouped_launch_policy(
3428 ("axis_name", primary_group_axis),3541 ("axis_name", primary_group_axis),
3429 ("block_sub", primary_block_sub),3542 ("block_sub", primary_block_sub),
3430 ]3543 ]
3431- if primary_feature_name in ("pointwise", "elementwise_numel"):3544+ if primary_feature_name in (
3545+ "pointwise",
3546+ "elementwise_numel",
3547+ # Reduction grouping needs this just as much: the primary axis is the
3548+ # only source of grid parallelism, so without it every shape below the
3549+ # representative inherits the representative's grid and each program
3550+ # loops over the representative's rows instead of its own.
3551+ "outer",
3552+ "reduction",
3553+ ):
3432 runtime_rule.extend(3554 runtime_rule.extend(
3433 (3555 (
3434 ("representative_numel", int(axis_env[primary_group_axis])),3556 ("representative_numel", int(axis_env[primary_group_axis])),