main
py 206 lines 6.94 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test make_fx tracing and inductor pattern matching with HelionKernelWrapper."""
4
5 import contextlib
6 from unittest.mock import Mock, patch
7
8 import pytest
9 import torch
10
11 from vllm.utils.import_utils import has_helion
12
13 if not has_helion():
14 pytest.skip(
15 "Helion is not installed. Install with: pip install vllm[helion]",
16 allow_module_level=True,
17 )
18
19 import helion
20 import helion.language as hl
21 from helion._compat import requires_torch_version
22
23 if not requires_torch_version("2.11"):
24 pytest.skip(
25 "HigherOrderOp requires PyTorch >= 2.11",
26 allow_module_level=True,
27 )
28
29 from helion._compiler._dynamo.higher_order_ops import (
30 helion_kernel_side_table,
31 helion_kernel_wrapper_mutation,
32 )
33 from torch._inductor.pattern_matcher import (
34 PatternMatcherPass,
35 fwd_only,
36 register_replacement,
37 select_decomp_table,
38 )
39 from torch.fx.experimental.proxy_tensor import make_fx
40
41 from vllm.kernels.helion.case_key import CaseKey
42 from vllm.kernels.helion.config_manager import ConfigManager
43 from vllm.kernels.helion.register import HelionKernelWrapper
44
45
46 @contextlib.contextmanager
47 def _helion_mock_context():
48 configs = {
49 CaseKey.default(): helion.Config(block_sizes=[64], num_warps=2, num_stages=2),
50 }
51 mock_config_manager = Mock(spec=ConfigManager)
52 mock_config_manager.get_platform_configs = Mock(return_value=configs)
53
54 with (
55 patch(
56 "vllm.kernels.helion.config_manager.ConfigManager",
57 return_value=mock_config_manager,
58 ),
59 patch(
60 "vllm.kernels.helion.utils.get_canonical_gpu_name",
61 return_value="nvidia_h200",
62 ),
63 ):
64 yield
65
66
67 class TestMakeFxHop:
68 def setup_method(self):
69 helion_kernel_side_table.reset_table()
70
71 @pytest.mark.skip(reason="SymInt proxy tracking issue with PyTorch 2.11+")
72 def test_make_fx_symbolic(self):
73 def raw_add_scale(
74 x: torch.Tensor, y: torch.Tensor, scale: float
75 ) -> tuple[torch.Tensor, int, torch.Tensor]:
76 out_x = torch.empty_like(x)
77 out_y = torch.empty_like(x)
78 for tile in hl.tile(x.size()):
79 out_x[tile] = x[tile] + y[tile] * scale
80 out_y[tile] = out_x[tile] * 2.0
81 return out_x, 42, out_y
82
83 input_x = torch.randn(7, 13)
84 input_y = torch.randn(7, 13)
85 scale = 0.5
86
87 with _helion_mock_context():
88 wrapper = HelionKernelWrapper(
89 raw_kernel_func=raw_add_scale,
90 op_name="test_make_fx",
91 fake_impl=lambda *a, **kw: None,
92 config_picker=lambda args, keys: CaseKey.default(),
93 )
94
95 def fn(x, y):
96 return wrapper(x, y, scale)
97
98 gm = make_fx(fn, tracing_mode="symbolic")(input_x, input_y)
99
100 hop_nodes = [
101 n
102 for n in gm.graph.nodes
103 if n.op == "call_function" and n.target is helion_kernel_wrapper_mutation
104 ]
105 assert len(hop_nodes) == 1
106 node = hop_nodes[0]
107
108 assert node.kwargs["constant_args"]["scale"] == scale
109 assert set(node.kwargs["tensor_args"]) == {"x", "y"}
110
111 specs = node.kwargs["output_spec"]["leaf_specs"]
112 tensor_specs = [s for s in specs if s["type"] == "tensor"]
113 scalar_specs = [s for s in specs if s["type"] == "scalar"]
114 assert len(tensor_specs) == 2
115 assert len(scalar_specs) == 1
116
117 for spec in tensor_specs:
118 assert spec["dtype"] == input_x.dtype
119
120 assert scalar_specs[0]["scalar_value"] == 42
121
122 for val in node.meta["val"]:
123 assert all(isinstance(s, torch.SymInt) for s in val.shape)
124
125 # Both out_x and out_y are empty_like(x), so output shapes == input shape
126 input_node = next(n for n in gm.graph.nodes if n.op == "placeholder")
127 input_shape = input_node.meta["val"].shape
128 for val in node.meta["val"]:
129 assert len(val.shape) == len(input_shape)
130 for out_s, in_s in zip(val.shape, input_shape):
131 assert out_s == in_s
132
133 @pytest.mark.skip(reason="SymInt proxy tracking issue with PyTorch 2.11+")
134 def test_pattern_matcher_replaces_with_helion_hop(self):
135 def raw_silu_mul(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
136 M, N = x.size()
137 out = torch.empty_like(x)
138 for tile_m, tile_n in hl.tile([M, N]):
139 out[tile_m, tile_n] = (
140 torch.nn.functional.silu(x[tile_m, tile_n]) * y[tile_m, tile_n]
141 )
142 return out
143
144 with _helion_mock_context():
145 wrapper = HelionKernelWrapper(
146 raw_kernel_func=raw_silu_mul,
147 op_name="test_pm_silu_mul",
148 fake_impl=lambda *a, **kw: None,
149 config_picker=lambda args, keys: CaseKey.default(),
150 )
151
152 def pattern(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
153 return torch.nn.functional.silu(x) * y
154
155 def replacement(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
156 return wrapper(x, y)
157
158 inputs = [torch.randn(8, 16), torch.randn(8, 16)]
159
160 pm_pass = PatternMatcherPass(pass_name="test_helion_replacement")
161 register_replacement(pattern, replacement, inputs, fwd_only, pm_pass)
162
163 def model(x, y):
164 return torch.nn.functional.silu(x) * y
165
166 decompositions = select_decomp_table()
167 input_x = torch.randn(8, 16)
168 input_y = torch.randn(8, 16)
169 gm = make_fx(model, decompositions, tracing_mode="symbolic")(
170 input_x, input_y
171 )
172
173 def count_hop_nodes(graph):
174 return sum(
175 1
176 for n in graph.nodes
177 if n.op == "call_function"
178 and n.target is helion_kernel_wrapper_mutation
179 )
180
181 assert count_hop_nodes(gm.graph) == 0
182
183 match_count = pm_pass.apply(gm.graph)
184 gm.graph.lint()
185 gm.recompile()
186
187 assert match_count == 1
188 assert count_hop_nodes(gm.graph) == 1
189
190 hop_node = next(
191 n
192 for n in gm.graph.nodes
193 if n.op == "call_function"
194 and n.target is helion_kernel_wrapper_mutation
195 )
196
197 # raw_silu_mul returns empty_like(x), so output shape == input shape
198 for val in hop_node.meta["val"]:
199 assert all(isinstance(s, torch.SymInt) for s in val.shape)
200
201 input_node = next(n for n in gm.graph.nodes if n.op == "placeholder")
202 input_shape = input_node.meta["val"].shape
203 output_shape = hop_node.meta["val"][0].shape
204 assert len(output_shape) == len(input_shape)
205 for out_s, in_s in zip(output_shape, input_shape):
206 assert out_s == in_s