已合并
feat(inductor): adapt group autotune for cpp wrapper #44686
feat(inductor): adapt group autotune for cpp wrapper #44686
已合并
Xuan Peng创建于 20 天前
3 个文件变更+746-5
@@ -0,0 +1,347 @@
1+import unittest
2+from types import SimpleNamespace
3+ 
4+import torch
5+from torch._inductor import config
6+from torch._inductor.codecache import CudaKernelParamCache
7+from torch._inductor.utils import IndentedBuffer, run_and_get_cpp_code
8+from torch._inductor.virtualized import V
9+from torch.testing._internal.common_utils import run_tests, TestCase
10+ 
11+import torch_npu
12+import torch_npu._inductor
13+from torch_npu._inductor.codegen.cpp_wrapper_npu import (
14+ CppWrapperNpu,
15+ DeferredNpuTritonCallWrapper,
16+)
17+ 
18+GROUP_COUNT = 32
19+HIDDEN_SIZE = 1600
20+GATE_SIZE = 256
21+POST_SIZE = 64
22+SIDE_SIZE = 32
23+COMPILE_SEQUENCE_LENGTH = 200
24+ 
25+ 
26+def _variant_load_meta(variant_id):
27+ return {
28+ "cubin_path": f"/tmp/triton_kernel_{variant_id}.cubin",
29+ "mangled_name": f"mangled_{variant_id}",
30+ "mix_mode": "aiv",
31+ "shared_mem": 64,
32+ "parallel_mode": "vector",
33+ "force_simt_only": False,
34+ "shared_mem_dynamic_size": 0,
35+ "has_auto_blockify_blacklist_op": False,
36+ }
37+ 
38+ 
39+def _grouped_plan():
40+ return {
41+ "variants": {
42+ "v0": {"config": {}, "load_meta": _variant_load_meta("v0")},
43+ "v1": {"config": {}, "load_meta": _variant_load_meta("v1")},
44+ },
45+ "variant_order": ("v0", "v1"),
46+ "best_by_group": {
47+ "0": {"variant_id": "v0", "policy_id": "p0"},
48+ "1": {"variant_id": "v1", "policy_id": "p1"},
49+ },
50+ "policies": {
51+ "p0": {
52+ "grid_target": 8,
53+ "static_blocks": (),
54+ "runtime_block_rules": (
55+ (
56+ "XBLOCK",
57+ (
58+ ("op", "ceildiv"),
59+ ("axis_name", "x"),
60+ ("block_sub", 4),
61+ ),
62+ ),
63+ ),
64+ },
65+ "p1": {
66+ "grid_target": 16,
67+ "static_blocks": (),
68+ "runtime_block_rules": (
69+ (
70+ "XBLOCK",
71+ (
72+ ("op", "ceildiv"),
73+ ("axis_name", "x"),
74+ ("block_sub", 8),
75+ ),
76+ ),
77+ ),
78+ },
79+ },
80+ "runtime_block_append_order": ("XBLOCK",),
81+ "group_id_count": 2,
82+ "reachable_group_ids": (0, 1),
83+ "group_features": (
84+ {
85+ "name": "x",
86+ "source": "axis",
87+ "axis_names": ("x",),
88+ "buckets": (128,),
89+ },
90+ ),
91+ "axis_arg_indices": {"x": 1},
92+ "feature_arg_indices": ((1,),),
93+ "feature_sources": (
94+ {
95+ "name": "x",
96+ "source": "axis",
97+ "axis_names": ("x",),
98+ },
99+ ),
100+ }
101+ 
102+ 
103+def _render_grouped_wrapper(grouped_plan=None, *, return_files=False):
104+ grouped_plan = grouped_plan or _grouped_plan()
105+ inductor_meta = {
106+ "group_enabled": True,
107+ "grouped_candidate_plan": grouped_plan,
108+ "grid_type": "GridNpu",
109+ "axis_names": ("x",),
110+ "runtime_block_arg_names": ("XBLOCK",),
111+ "primary_group_axis": "x",
112+ }
113+ params = {
114+ "def_args": ["in_ptr", "x_numel", "XBLOCK"],
115+ "call_args": ["in_ptr", "x_numel", "XBLOCK"],
116+ "config": {"split_axis": (0,), "split_blocks": (128,)},
117+ "inductor_meta": inductor_meta,
118+ "triton_meta": {
119+ "signature": {
120+ "in_ptr": "fp16",
121+ "x_numel": "i64",
122+ "XBLOCK": "i64",
123+ },
124+ "constants": {},
125+ },
126+ "mangled_name": "unused",
127+ "shared_mem": 64,
128+ "cubin_path": "/tmp/unused.cubin",
129+ "mix_mode": "aiv",
130+ "parallel_mode": "vector",
131+ "force_simt_only": False,
132+ }
133+ graph = SimpleNamespace(
134+ cpp_wrapper=True,
135+ aot_mode=False,
136+ is_const_graph=False,
137+ constant_reprs={},
138+ inputs_to_check=[],
139+ graph_input_names=[],
140+ graph_inputs={},
141+ device_types={"npu"},
142+ wrapper_code=None,
143+ )
144+ with V.set_graph_handler(graph):
145+ wrapper = CppWrapperNpu()
146+ graph.wrapper_code = wrapper
147+ wrapper.prefix = IndentedBuffer()
148+ CudaKernelParamCache.cache_clear()
149+ CudaKernelParamCache.cache["triton_kernel"] = params
150+ deferred = DeferredNpuTritonCallWrapper(
151+ wrapper_name="call_triton_kernel",
152+ kernel_name="triton_kernel",
153+ kernel_name_to_body={},
154+ arg_types=[torch.float16, int],
155+ kernel_id=0,
156+ )
157+ deferred.generate(wrapper)
158+ source = wrapper.prefix.getvalue()
159+ if return_files:
160+ return source, tuple(wrapper.additional_files)
161+ return source
162+ 
163+ 
164+class GatedTransposeBmmModel(torch.nn.Module):
165+ def forward(
166+ self,
167+ source,
168+ gate_down_rhs,
169+ gate_down_bias,
170+ gate_up_rhs,
171+ bias,
172+ post_bmm_rhs,
173+ side_bmm_rhs,
174+ ):
175+ transposed = source.transpose(0, 1)
176+ gate_down = torch.bmm(transposed, gate_down_rhs)
177+ gate_down = torch.tanh(gate_down + gate_down_bias)
178+ bmm_result = torch.bmm(gate_down, gate_up_rhs)
179+ gated = transposed * torch.tanh(bmm_result + bias)
180+ post = torch.bmm(gated, post_bmm_rhs)
181+ side = torch.bmm(transposed, side_bmm_rhs)
182+ return post, side
183+ 
184+ 
185+def _make_gated_transpose_inputs(sequence_length):
186+ def rand(*shape):
187+ return torch.randn(shape, device="npu:0", dtype=torch.float16)
188+ 
189+ return (
190+ rand(sequence_length, GROUP_COUNT, HIDDEN_SIZE),
191+ rand(GROUP_COUNT, HIDDEN_SIZE, GATE_SIZE),
192+ rand(GROUP_COUNT, 1, GATE_SIZE),
193+ rand(GROUP_COUNT, GATE_SIZE, HIDDEN_SIZE),
194+ rand(GROUP_COUNT, 1, HIDDEN_SIZE),
195+ rand(GROUP_COUNT, HIDDEN_SIZE, POST_SIZE) * 0.01,
196+ rand(GROUP_COUNT, HIDDEN_SIZE, SIDE_SIZE),
197+ )
198+ 
199+ 
200+def _mark_sequence_length_dynamic(inputs):
201+ torch._dynamo.mark_dynamic(
202+ inputs[0],
203+ 0,
204+ hint_override=COMPILE_SEQUENCE_LENGTH,
205+ )
206+ 
207+ 
208+class TestGroupedCppWrapper(TestCase):
209+ def tearDown(self):
210+ CudaKernelParamCache.cache_clear()
211+ torch._dynamo.reset()
212+ super().tearDown()
213+ 
214+ def test_grouped_wrapper_emits_bucket_dispatch_and_variants(self):
215+ source = _render_grouped_wrapper()
216+ 
217+ self.assertIn("switch (grouped_group_id)", source)
218+ self.assertIn("case 0: {", source)
219+ self.assertIn("case 1: {", source)
220+ self.assertIn("grouped_kernel_v0", source)
221+ self.assertIn("grouped_kernel_v1", source)
222+ self.assertIn('"mangled_v0"', source)
223+ self.assertIn('"mangled_v1"', source)
224+ 
225+ def test_grouped_wrapper_materializes_block_sub_aligned_runtime_block(self):
226+ source = _render_grouped_wrapper()
227+ 
228+ self.assertIn(
229+ "auto resolve_grouped_runtime_block",
230+ source,
231+ )
232+ self.assertIn(
233+ "total_subblocks = ceildiv(axis_numel, block_sub)",
234+ source,
235+ )
236+ self.assertIn(
237+ "program_subblocks = ceildiv(",
238+ source,
239+ )
240+ self.assertIn(
241+ "effective_grid = ceildiv(",
242+ source,
243+ )
244+ self.assertIn(
245+ "XBLOCK = resolve_grouped_runtime_block(x_numel, 8, 4)",
246+ source,
247+ )
248+ self.assertIn(
249+ "XBLOCK = resolve_grouped_runtime_block(x_numel, 16, 8)",
250+ source,
251+ )
252+ self.assertLess(source.index("int64_t XBLOCK"), source.index("uint32_t grid_0"))
253+ 
254+ def test_grouped_wrapper_omits_unselected_variant(self):
255+ grouped_plan = _grouped_plan()
256+ grouped_plan["best_by_group"]["1"] = {
257+ "variant_id": "v0",
258+ "policy_id": "p0",
259+ }
260+ source, additional_files = _render_grouped_wrapper(
261+ grouped_plan,
262+ return_files=True,
263+ )
264+ 
265+ self.assertIn("grouped_kernel_v0", source)
266+ self.assertNotIn("grouped_kernel_v1", source)
267+ self.assertIn("/tmp/triton_kernel_v0.cubin", additional_files)
268+ self.assertNotIn("/tmp/triton_kernel_v1.cubin", additional_files)
269+ self.assertNotIn("/tmp/unused.cubin", additional_files)
270+ 
271+ @unittest.skipIf(not torch.npu.is_available(), "NPU is not available")
272+ def test_gated_transpose_dynamic_shapes_functionality_and_accuracy(self):
273+ import torch_npu._inductor.config as npu_config
274+ 
275+ previous_group_autotune = (
276+ npu_config.enable_symbolic_shape_group_autotune
277+ )
278+ npu_config.enable_symbolic_shape_group_autotune = True
279+ try:
280+ with config.patch(
281+ {
282+ "cpp_wrapper": True,
283+ "compile_threads": 1,
284+ "force_disable_caches": True,
285+ }
286+ ):
287+ model = GatedTransposeBmmModel().eval()
288+ compile_inputs = _make_gated_transpose_inputs(
289+ COMPILE_SEQUENCE_LENGTH
290+ )
291+ _mark_sequence_length_dynamic(compile_inputs)
292+ compiled = torch.compile(
293+ model,
294+ backend="inductor",
295+ fullgraph=True,
296+ dynamic=None,
297+ )
298+ 
299+ with torch.no_grad():
300+ expected = model(*compile_inputs)
301+ actual, cpp_code = run_and_get_cpp_code(
302+ compiled, *compile_inputs
303+ )
304+ torch.npu.synchronize()
305+ torch.testing.assert_close(
306+ actual,
307+ expected,
308+ rtol=0.02,
309+ atol=0.03,
310+ msg=(
311+ "grouped cpp wrapper mismatch for "
312+ f"sequence_length={COMPILE_SEQUENCE_LENGTH}"
313+ ),
314+ )
315+ self.assertIn("'group_enabled': True", cpp_code)
316+ self.assertIn("switch (grouped_group_id)", cpp_code)
317+ self.assertGreaterEqual(
318+ cpp_code.count("static void* grouped_kernel_v"), 2
319+ )
320+ 
321+ for sequence_length, inputs in (
322+ (4, _make_gated_transpose_inputs(4)),
323+ (256, _make_gated_transpose_inputs(256)),
324+ ):
325+ _mark_sequence_length_dynamic(inputs)
326+ with torch.no_grad():
327+ expected = model(*inputs)
328+ actual = compiled(*inputs)
329+ torch.npu.synchronize()
330+ torch.testing.assert_close(
331+ actual,
332+ expected,
333+ rtol=0.02,
334+ atol=0.03,
335+ msg=(
336+ "grouped cpp wrapper mismatch for "
337+ f"sequence_length={sequence_length}"
338+ ),
339+ )
340+ finally:
341+ npu_config.enable_symbolic_shape_group_autotune = (
342+ previous_group_autotune
343+ )
344+ 
345+ 
346+if __name__ == "__main__":
347+ run_tests()
@@ -30,6 +30,7 @@ from torch._inductor.ir import GraphPartitionSignature
30from torch._inductor.runtime.runtime_utils import dynamo_timed30from torch._inductor.runtime.runtime_utils import dynamo_timed
31from torch._inductor.utils import IndentedBuffer31from torch._inductor.utils import IndentedBuffer
32from torch._inductor.virtualized import V32from torch._inductor.virtualized import V
33+from torch.utils._ordered_set import OrderedSet
33 34 
34from .. import config as npu_config35from .. import config as npu_config
35from ..runtime.triton_heuristics import GridExprNpu36from ..runtime.triton_heuristics import GridExprNpu
@@ -100,11 +101,280 @@ class DeferredNpuTritonCallWrapper(DeferredTritonCallWrapper):
100 self.arg_types = original_arg_types101 self.arg_types = original_arg_types
101 102 
102 def generate(self, wrapper: CppWrapperGpu):103 def generate(self, wrapper: CppWrapperGpu):
104+ additional_files = V.graph.wrapper_code.additional_files
105+ existing_files = OrderedSet(additional_files)
103 with self._patch_runtime_block_params() as params:106 with self._patch_runtime_block_params() as params:
104 super().generate(wrapper)107 super().generate(wrapper)
105- cubin_path = params[get_cpp_wrapper_cubin_path_name()]108+ inductor_meta = params["inductor_meta"]
106- if cubin_path not in V.graph.wrapper_code.additional_files:109+ if inductor_meta.get("group_enabled", False):
107- V.graph.wrapper_code.additional_files.append(cubin_path)110+ grouped_plan = inductor_meta["grouped_candidate_plan"]
111+ cubin_paths = tuple(
112+ self._grouped_load_meta(grouped_plan, variant_id)[
113+ "cubin_path"
114+ ]
115+ for variant_id in self._grouped_active_variants(grouped_plan)
116+ )
117+ default_cubin_path = params[get_cpp_wrapper_cubin_path_name()]
118+ if (
119+ default_cubin_path not in existing_files
120+ and default_cubin_path not in cubin_paths
121+ and default_cubin_path in additional_files
122+ ):
123+ additional_files.remove(default_cubin_path)
124+ else:
125+ cubin_paths = (params[get_cpp_wrapper_cubin_path_name()],)
126+ for cubin_path in cubin_paths:
127+ if cubin_path not in additional_files:
128+ additional_files.append(cubin_path)
129+ 
130+ @staticmethod
131+ def _grouped_runtime_block_names(
132+ grouped_plan: dict[str, Any],
133+ ) -> tuple[str, ...]:
134+ return tuple(grouped_plan.get("runtime_block_append_order", ()))
135+ 
136+ @staticmethod
137+ def _grouped_active_variants(
138+ grouped_plan: dict[str, Any],
139+ ) -> tuple[str, ...]:
140+ best_by_group = grouped_plan.get("best_by_group", {})
141+ if not best_by_group:
142+ raise RuntimeError(
143+ "grouped cpp wrapper expects best_by_group to be populated"
144+ )
145+ selected_variant_ids = OrderedSet(
146+ selected["variant_id"] for selected in best_by_group.values()
147+ )
148+ active_variants = tuple(
149+ variant_id
150+ for variant_id in grouped_plan.get("variant_order", ())
151+ if variant_id in selected_variant_ids
152+ )
153+ if not active_variants:
154+ raise RuntimeError(
155+ "grouped cpp wrapper has no active compiled variants"
156+ )
157+ return active_variants
158+ 
159+ @staticmethod
160+ def _grouped_load_meta(
161+ grouped_plan: dict[str, Any], variant_id: str
162+ ) -> dict[str, Any]:
163+ load_meta = dict(
164+ grouped_plan["variants"][variant_id].get("load_meta", {})
165+ )
166+ if not load_meta:
167+ raise RuntimeError(
168+ f"grouped cpp wrapper expects load_meta for variant {variant_id}"
169+ )
170+ return load_meta
171+ 
172+ def _generate_grouped_feature_inputs(
173+ self,
174+ prefix: IndentedBuffer,
175+ grouped_plan: dict[str, Any],
176+ def_args: list[str],
177+ ) -> None:
178+ feature_arg_indices = tuple(
179+ grouped_plan.get("feature_arg_indices", ())
180+ )
181+ feature_sources = tuple(grouped_plan.get("feature_sources", ()))
182+ if len(feature_arg_indices) != len(feature_sources):
183+ raise RuntimeError(
184+ "grouped cpp wrapper feature inputs and sources do not match"
185+ )
186+ for feature_idx, (arg_indices, feature_source) in enumerate(
187+ zip(feature_arg_indices, feature_sources)
188+ ):
189+ arg_names = [def_args[arg_index] for arg_index in arg_indices]
190+ source = feature_source.get("source")
191+ if source in ("outer_product", "reduction_product"):
192+ feature_expr = " * ".join(arg_names)
193+ elif len(arg_names) == 1:
194+ feature_expr = arg_names[0]
195+ else:
196+ raise RuntimeError(
197+ f"grouped cpp wrapper feature {source} expects one axis"
198+ )
199+ prefix.writeline(
200+ f"int64_t grouped_feature_{feature_idx} = {feature_expr};"
201+ )
202+ 
203+ def _generate_grouped_group_id(
204+ self, prefix: IndentedBuffer, grouped_plan: dict[str, Any]
205+ ) -> None:
206+ group_features = tuple(grouped_plan.get("group_features", ()))
207+ prefix.writeline("int64_t grouped_group_id = 0;")
208+ prefix.writeline("int64_t grouped_group_stride = 1;")
209+ for feature_idx, feature_spec in enumerate(group_features):
210+ buckets = tuple(feature_spec.get("buckets", ()))
211+ prefix.writeline(f"int64_t grouped_bucket_{feature_idx} = 0;")
212+ for bucket_idx, upper_bound in enumerate(buckets):
213+ keyword = "if" if bucket_idx == 0 else "else if"
214+ prefix.writeline(
215+ f"{keyword} (grouped_feature_{feature_idx} <= "
216+ f"{int(upper_bound)}) grouped_bucket_{feature_idx} = "
217+ f"{bucket_idx};"
218+ )
219+ if buckets:
220+ prefix.writeline(
221+ f"else grouped_bucket_{feature_idx} = {len(buckets)};"
222+ )
223+ prefix.writeline(
224+ f"grouped_group_id += grouped_bucket_{feature_idx} * "
225+ "grouped_group_stride;"
226+ )
227+ prefix.writeline(
228+ f"grouped_group_stride *= {len(buckets) + 1};"
229+ )
230+ group_id_count = int(grouped_plan.get("group_id_count", 0))
231+ if group_id_count:
232+ prefix.writeline(
233+ f"if (grouped_group_id >= {group_id_count}) "
234+ 'throw std::runtime_error("grouped cpp wrapper resolved group '
235+ 'id out of range");'
236+ )
237+ 
238+ def _generate_grouped_selection(
239+ self,
240+ prefix: IndentedBuffer,
241+ grouped_plan: dict[str, Any],
242+ def_args: list[str],
243+ active_variants: tuple[str, ...],
244+ ) -> None:
245+ runtime_block_names = self._grouped_runtime_block_names(grouped_plan)
246+ axis_arg_indices = dict(grouped_plan.get("axis_arg_indices", {}))
247+ best_by_group = grouped_plan["best_by_group"]
248+ prefix.writeline("int64_t grouped_variant_index = -1;")
249+ for block_name in runtime_block_names:
250+ prefix.writeline(f"int64_t {block_name} = 0;")
251+ if runtime_block_names:
252+ prefix.splice(
253+ """
254+ auto resolve_grouped_runtime_block = [](
255+ int64_t axis_numel,
256+ int64_t expected_grid,
257+ int64_t block_sub
258+ ) -> int64_t {
259+ if (axis_numel <= 0) return 1;
260+ auto ceildiv = [](int64_t value, int64_t divisor) {
261+ return (value + divisor - 1) / divisor;
262+ };
263+ int64_t total_subblocks = ceildiv(axis_numel, block_sub);
264+ int64_t program_subblocks = ceildiv(
265+ total_subblocks, expected_grid
266+ );
267+ int64_t effective_grid = ceildiv(
268+ total_subblocks, program_subblocks
269+ );
270+ return ceildiv(axis_numel, effective_grid);
271+ };
272+ """
273+ )
274+ prefix.writeline("switch (grouped_group_id) {")
275+ with prefix.indent():
276+ for group_id in grouped_plan.get("reachable_group_ids", ()):
277+ selected = best_by_group.get(str(group_id))
278+ if selected is None:
279+ raise RuntimeError(
280+ "grouped cpp wrapper is missing winner for reachable "
281+ f"group {group_id}"
282+ )
283+ variant_id = selected["variant_id"]
284+ policy = grouped_plan["policies"][selected["policy_id"]]
285+ prefix.writeline(f"case {group_id}: {{")
286+ with prefix.indent():
287+ prefix.writeline(
288+ f"grouped_variant_index = "
289+ f"{active_variants.index(variant_id)};"
290+ )
291+ assigned_blocks = OrderedSet()
292+ for block_name, block_value in policy.get(
293+ "static_blocks", ()
294+ ):
295+ prefix.writeline(
296+ f"{block_name} = {int(block_value)};"
297+ )
298+ assigned_blocks.add(block_name)
299+ for block_name, rule_items in policy.get(
300+ "runtime_block_rules", ()
301+ ):
302+ rule = dict(rule_items)
303+ if rule.get("op") != "ceildiv":
304+ raise RuntimeError(
305+ "grouped cpp wrapper only supports ceildiv "
306+ "runtime block rules"
307+ )
308+ axis_name = rule["axis_name"]
309+ if axis_name not in axis_arg_indices:
310+ raise RuntimeError(
311+ "grouped cpp wrapper is missing axis argument "
312+ f"for {axis_name}"
313+ )
314+ axis_arg = def_args[axis_arg_indices[axis_name]]
315+ prefix.writeline(
316+ f"{block_name} = resolve_grouped_runtime_block("
317+ f"{axis_arg}, {int(policy['grid_target'])}, "
318+ f"{int(rule['block_sub'])});"
319+ )
320+ assigned_blocks.add(block_name)
321+ missing_blocks = [
322+ name
323+ for name in runtime_block_names
324+ if name not in assigned_blocks
325+ ]
326+ if missing_blocks:
327+ raise RuntimeError(
328+ "grouped cpp wrapper policy is missing runtime "
329+ f"blocks {missing_blocks}"
330+ )
331+ prefix.writeline("break;")
332+ prefix.writeline("}")
333+ prefix.writeline("default:")
334+ with prefix.indent():
335+ prefix.writeline(
336+ 'throw std::runtime_error("grouped cpp wrapper resolved '
337+ 'an unavailable group");'
338+ )
339+ prefix.writeline("}")
340+ 
341+ def _generate_grouped_variant_launch(
342+ self,
343+ prefix: IndentedBuffer,
344+ wrapper: CppWrapperGpu,
345+ params: dict[str, Any],
346+ grouped_plan: dict[str, Any],
347+ active_variants: tuple[str, ...],
348+ ) -> None:
349+ prefix.writeline("switch (grouped_variant_index) {")
350+ with prefix.indent():
351+ for variant_index, variant_id in enumerate(active_variants):
352+ load_meta = self._grouped_load_meta(grouped_plan, variant_id)
353+ prefix.writeline(f"case {variant_index}: {{")
354+ with prefix.indent():
355+ kernel_var_name = f"grouped_kernel_{variant_id}"
356+ variant_params = {
357+ **params,
358+ **load_meta,
359+ }
360+ self._generate_single_kernel_load(
361+ prefix, kernel_var_name, variant_params
362+ )
363+ self._generate_single_kernel_launch(
364+ prefix,
365+ wrapper,
366+ kernel_var_name,
367+ variant_params,
368+ )
369+ prefix.writeline("break;")
370+ prefix.writeline("}")
371+ prefix.writeline("default:")
372+ with prefix.indent():
373+ prefix.writeline(
374+ 'throw std::runtime_error("grouped cpp wrapper could not '
375+ 'launch selected variant");'
376+ )
377+ prefix.writeline("}")
108 378 
109 def generate_grid(379 def generate_grid(
110 self,380 self,
@@ -112,9 +382,25 @@ class DeferredNpuTritonCallWrapper(DeferredTritonCallWrapper):
112 inductor_meta: dict[str, Any],382 inductor_meta: dict[str, Any],
113 params: dict[str, Any],383 params: dict[str, Any],
114 ):384 ):
385+ if inductor_meta.get("group_enabled", False):
386+ grouped_plan = inductor_meta["grouped_candidate_plan"]
387+ active_variants = self._grouped_active_variants(grouped_plan)
388+ self._generate_grouped_feature_inputs(
389+ prefix, grouped_plan, params["def_args"]
390+ )
391+ self._generate_grouped_group_id(prefix, grouped_plan)
392+ self._generate_grouped_selection(
393+ prefix,
394+ grouped_plan,
395+ params["def_args"],
396+ active_variants,
397+ )
115 numels = [arg for arg in params["def_args"] if "_numel" in arg]398 numels = [arg for arg in params["def_args"] if "_numel" in arg]
116- for block_name, block_value in dict(params.get("runtime_blocks", {})).items():399+ if not inductor_meta.get("group_enabled", False):
117- prefix.writeline(f"int64_t {block_name} = {block_value};")400+ for block_name, block_value in dict(
401+ params.get("runtime_blocks", {})
402+ ).items():
403+ prefix.writeline(f"int64_t {block_name} = {block_value};")
118 grid = GridExprNpu.from_meta_and_set_numel(404 grid = GridExprNpu.from_meta_and_set_numel(
119 inductor_meta, params["config"], numels, "cpp"405 inductor_meta, params["config"], numels, "cpp"
120 )406 )
@@ -130,6 +416,17 @@ class DeferredNpuTritonCallWrapper(DeferredTritonCallWrapper):
130 prefix.writeline("if (grid_0 == 0 || grid_1 == 0 || grid_2 == 0) return;")416 prefix.writeline("if (grid_0 == 0 || grid_1 == 0 || grid_2 == 0) return;")
131 417 
132 def generate_load_kernel(self, prefix, kernel_var_name, params):418 def generate_load_kernel(self, prefix, kernel_var_name, params):
419+ inductor_meta = params["inductor_meta"]
420+ if inductor_meta.get("group_enabled", False):
421+ grouped_plan = inductor_meta["grouped_candidate_plan"]
422+ for variant_id in self._grouped_active_variants(grouped_plan):
423+ prefix.writeline(
424+ f"static void* grouped_kernel_{variant_id} = nullptr;"
425+ )
426+ return
427+ self._generate_single_kernel_load(prefix, kernel_var_name, params)
428+ 
429+ def _generate_single_kernel_load(self, prefix, kernel_var_name, params):
133 prefix.writeline(f"if ({kernel_var_name} == nullptr) {{")430 prefix.writeline(f"if ({kernel_var_name} == nullptr) {{")
134 with prefix.indent():431 with prefix.indent():
135 load_kernel_args = [432 load_kernel_args = [
@@ -146,6 +443,24 @@ class DeferredNpuTritonCallWrapper(DeferredTritonCallWrapper):
146 prefix.writeline("}")443 prefix.writeline("}")
147 444 
148 def generate_launch_kernel(self, prefix, wrapper, kernel_var_name, params):445 def generate_launch_kernel(self, prefix, wrapper, kernel_var_name, params):
446+ inductor_meta = params["inductor_meta"]
447+ if inductor_meta.get("group_enabled", False):
448+ grouped_plan = inductor_meta["grouped_candidate_plan"]
449+ self._generate_grouped_variant_launch(
450+ prefix,
451+ wrapper,
452+ params,
453+ grouped_plan,
454+ self._grouped_active_variants(grouped_plan),
455+ )
456+ return
457+ self._generate_single_kernel_launch(
458+ prefix, wrapper, kernel_var_name, params
459+ )
460+ 
461+ def _generate_single_kernel_launch(
462+ self, prefix, wrapper, kernel_var_name, params
463+ ):
149 triton_meta = params["triton_meta"]464 triton_meta = params["triton_meta"]
150 arg_type_lookup = dict(zip(params["def_args"], self.arg_types))465 arg_type_lookup = dict(zip(params["def_args"], self.arg_types))
151 for block_name in params["inductor_meta"].get("runtime_block_arg_names", ()):466 for block_name in params["inductor_meta"].get("runtime_block_arg_names", ()):
@@ -26,6 +26,7 @@ import triton
26from torch._dynamo.testing import rand_strided26from torch._dynamo.testing import rand_strided
27from torch._dynamo.utils import dynamo_timed27from torch._dynamo.utils import dynamo_timed
28from torch._inductor import config28from torch._inductor import config
29+from torch._inductor.codecache import split_aot_inductor_output_path, write
29from torch._inductor.compile_fx import clone_preserve_strides30from torch._inductor.compile_fx import clone_preserve_strides
30from torch._inductor.runtime.autotune_cache import AutotuneCache31from torch._inductor.runtime.autotune_cache import AutotuneCache
31from torch._inductor.runtime.benchmarking import benchmarker32from torch._inductor.runtime.benchmarking import benchmarker
@@ -2184,6 +2185,80 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2184 self._grouped_runtime_args_snapshot = ()2185 self._grouped_runtime_args_snapshot = ()
2185 self._grouped_variant_launchers_initialized = False2186 self._grouped_variant_launchers_initialized = False
2186 2187 
2188+ @staticmethod
2189+ def _grouped_variant_load_meta(launcher) -> dict[str, Any]:
2190+ if not hasattr(launcher, "bin") or not hasattr(launcher.bin, "asm"):
2191+ raise RuntimeError("grouped cpp wrapper variant has no compiled binary")
2192+ metadata = launcher.bin.metadata
2193+ binary_path = launcher.bin.asm.get("cubin_path")
2194+ if not binary_path:
2195+ binary = launcher.bin.asm.get("npubin")
2196+ if binary is None:
2197+ raise RuntimeError("grouped cpp wrapper variant is missing npubin")
2198+ _, binary_path = write(
2199+ binary,
2200+ "cubin",
2201+ hash_type="cubin",
2202+ specified_dir=split_aot_inductor_output_path(
2203+ config.aot_inductor.output_path
2204+ )[0],
2205+ )
2206+ return {
2207+ "mangled_name": (
2208+ metadata.name
2209+ if hasattr(metadata, "name")
2210+ else metadata["name"]
2211+ ),
2212+ "shared_mem": (
2213+ launcher.bin.shared
2214+ if hasattr(launcher.bin, "shared")
2215+ else metadata.shared
2216+ ),
2217+ "mix_mode": metadata.mix_mode,
2218+ "parallel_mode": metadata.parallel_mode,
2219+ "force_simt_only": metadata.force_simt_only,
2220+ "shared_mem_dynamic_size": getattr(
2221+ metadata, "shared_mem_dynamic_size", 0
2222+ ),
2223+ "has_auto_blockify_blacklist_op": getattr(
2224+ metadata, "has_auto_blockify_blacklist_op", False
2225+ ),
2226+ "cubin_path": binary_path,
2227+ }
2228+ 
2229+ def _record_grouped_cpp_wrapper_plan(self) -> None:
2230+ best_by_group = {}
2231+ selected_variant_ids = OrderedSet()
2232+ for group_id in self.reachable_selection_keys:
2233+ candidate = self.best_candidate_map.get(group_id)
2234+ launcher = self.best_launcher_map.get(group_id)
2235+ if candidate is None or launcher is None:
2236+ raise RuntimeError(
2237+ f"grouped cpp wrapper is missing winner for group {group_id}"
2238+ )
2239+ variant_id = candidate["variant_id"]
2240+ best_by_group[str(group_id)] = {
2241+ "variant_id": variant_id,
2242+ "policy_id": candidate["policy_id"],
2243+ }
2244+ selected_variant_ids.add(variant_id)
2245+ 
2246+ for variant_id in selected_variant_ids:
2247+ launcher = self.variant_launcher_map.get(variant_id)
2248+ if launcher is None:
2249+ raise RuntimeError(
2250+ f"grouped cpp wrapper is missing launcher for variant {variant_id}"
2251+ )
2252+ self.candidate_plan["variants"][variant_id]["load_meta"] = (
2253+ self._grouped_variant_load_meta(launcher)
2254+ )
2255+ 
2256+ self.candidate_plan["best_by_group"] = best_by_group
2257+ 
2258+ def save_npu_kernel(self, input_stream, input_launcher):
2259+ self._record_grouped_cpp_wrapper_plan()
2260+ super().save_npu_kernel(input_stream, input_launcher)
2261+ 
2187 def _set_group_best_candidate(self, group_id, candidate, launcher):2262 def _set_group_best_candidate(self, group_id, candidate, launcher):
2188 self.best_candidate_map[group_id] = candidate2263 self.best_candidate_map[group_id] = candidate
2189 self.best_launcher_map[group_id] = launcher2264 self.best_launcher_map[group_id] = launcher
@@ -2600,6 +2675,10 @@ class NPUSymbolicGroupedAutotuner(NPUCachingAutotuner):
2600 selected_config,2675 selected_config,
2601 runtime_blocks,2676 runtime_blocks,
2602 )2677 )
2678+ if launcher.store_cubin and (
2679+ not benchmark_run or not self.cuda_kernel_saved
2680+ ):
2681+ self.save_gpu_kernel(stream, launcher)
2603 return launcher(2682 return launcher(
2604 *self._build_runtime_launch_args(args, runtime_blocks),2683 *self._build_runtime_launch_args(args, runtime_blocks),
2605 stream=stream,2684 stream=stream,