|
8 | 8 | Graph transformation passes for the MLX backend. |
9 | 9 | """ |
10 | 10 |
|
11 | | -from dataclasses import dataclass |
12 | | -from typing import List, Optional |
| 11 | +from typing import List |
13 | 12 |
|
14 | 13 | import torch |
15 | | -from executorch.backends.mlx.pattern_utils import ( |
16 | | - extract_lifted_tensor_constant, |
17 | | - match_target, |
18 | | - OpStep, |
19 | | - PatternMatch, |
20 | | - walk_back, |
21 | | -) |
22 | 14 | from executorch.backends.transforms.collapse_view_copy import CollapseViewCopyPass |
| 15 | +from executorch.backends.transforms.fuse_gqa_with_sdpa import FuseGQAWithSDPAPass |
| 16 | +from executorch.backends.transforms.fuse_rms_norm import FuseRMSNormPass |
23 | 17 | from executorch.exir.dialects._ops import ops as exir_ops |
24 | 18 | from executorch.exir.pass_base import ( |
25 | 19 | ExportedProgramPassBase, |
26 | 20 | ExportedProgramPassResult, |
27 | 21 | ExportPass, |
28 | 22 | PassResult, |
29 | 23 | ) |
| 24 | +from executorch.exir.pass_manager import PassType |
30 | 25 | from executorch.exir.passes.cse_pass import CSEPass |
31 | 26 | from torch.fx import GraphModule, Node |
32 | 27 |
|
33 | 28 |
|
34 | | -def get_default_passes() -> List[ExportPass]: |
| 29 | +def get_default_passes() -> List[PassType]: |
35 | 30 | """ |
36 | 31 | Returns a list of passes that are enabled by default for the MLX backend. |
37 | 32 | """ |
38 | 33 | return [ |
39 | | - FuseRMSNormPass(), |
| 34 | + FuseRMSNormPass(fold_dtype_casts=True, allow_lossy_weight_casts=True), |
| 35 | + FuseGQAWithSDPAPass(), |
40 | 36 | CanonicalizePermutePass(), |
41 | 37 | CollapseViewCopyPass(), |
42 | 38 | CollapsePermutePass(), |
@@ -103,175 +99,6 @@ def call(self, exported_program) -> ExportedProgramPassResult: |
103 | 99 | return ExportedProgramPassResult(exported_program, True) |
104 | 100 |
|
105 | 101 |
|
106 | | -@dataclass |
107 | | -class RMSNormMatch(PatternMatch): |
108 | | - """ |
109 | | - Matched RMSNorm pattern. |
110 | | -
|
111 | | - HuggingFace Llama's RMSNorm decomposes into: |
112 | | - variance = hidden_states.pow(2).mean(-1, keepdim=True) |
113 | | - hidden_states = hidden_states * torch.rsqrt(variance + eps) |
114 | | - return weight * hidden_states.to(input_dtype) |
115 | | -
|
116 | | - Graph pattern: |
117 | | - _to_copy (to f32) [optional] |
118 | | - pow(x, 2) |
119 | | - mean_dim(pow_out, [-1], keepdim=True) |
120 | | - add(mean_out, eps_tensor) |
121 | | - rsqrt(add_out) |
122 | | - mul(to_copy_out, rsqrt_out) |
123 | | - _to_copy (back to original dtype) [optional] |
124 | | - mul(weight, to_copy_out) |
125 | | - """ |
126 | | - |
127 | | - input_node: Node = None # type: ignore[assignment] |
128 | | - weight_node: Node = None # type: ignore[assignment] |
129 | | - eps: float = 0.0 |
130 | | - |
131 | | - @classmethod |
132 | | - def maybe_create(cls, head: Node, **context) -> Optional["RMSNormMatch"]: |
133 | | - """Match RMSNorm pattern starting from final mul(weight, normalized).""" |
134 | | - # Head must be mul |
135 | | - if not match_target(head, torch.ops.aten.mul.Tensor): |
136 | | - return None |
137 | | - |
138 | | - if len(head.args) < 2: |
139 | | - return None |
140 | | - |
141 | | - # Try both orderings: mul(weight, normalized) or mul(normalized, weight) |
142 | | - for weight_idx, norm_idx in [(0, 1), (1, 0)]: |
143 | | - weight_node = head.args[weight_idx] |
144 | | - norm_node = head.args[norm_idx] |
145 | | - |
146 | | - if not isinstance(norm_node, Node): |
147 | | - continue |
148 | | - |
149 | | - # Match entire chain with single walk_back: |
150 | | - # [_to_copy] -> mul(input, rsqrt) -> rsqrt -> add -> mean -> pow -> [_to_copy] |
151 | | - # The mul follows arg_index=1 to get rsqrt (not input) |
152 | | - result = walk_back( |
153 | | - norm_node, |
154 | | - [ |
155 | | - OpStep( |
156 | | - op=torch.ops.aten._to_copy.default, |
157 | | - optional=True, |
158 | | - kwargs={ |
159 | | - "dtype", |
160 | | - "layout", |
161 | | - "device", |
162 | | - "pin_memory", |
163 | | - "non_blocking", |
164 | | - "memory_format", |
165 | | - }, |
166 | | - ), |
167 | | - OpStep(op=torch.ops.aten.mul.Tensor, nargs=2, arg_index=1), |
168 | | - OpStep(op=torch.ops.aten.rsqrt.default), |
169 | | - OpStep(op=torch.ops.aten.add.Tensor, nargs=2), |
170 | | - OpStep(op=torch.ops.aten.mean.dim, nargs=(2, 3), kwargs={"dtype"}), |
171 | | - OpStep(op=torch.ops.aten.pow.Tensor_Scalar, nargs=2), |
172 | | - OpStep( |
173 | | - op=torch.ops.aten._to_copy.default, |
174 | | - optional=True, |
175 | | - require_single_user=False, # _to_copy output used by both pow and mul |
176 | | - kwargs={ |
177 | | - "dtype", |
178 | | - "layout", |
179 | | - "device", |
180 | | - "pin_memory", |
181 | | - "non_blocking", |
182 | | - "memory_format", |
183 | | - }, |
184 | | - ), |
185 | | - ], |
186 | | - ) |
187 | | - if result is None: |
188 | | - continue |
189 | | - |
190 | | - original_input, entries = result |
191 | | - to_copy_out, mul, rsqrt, add, mean, pow, to_copy_in = entries |
192 | | - |
193 | | - # If input _to_copy matched, verify it has exactly 2 users: pow and mul |
194 | | - if to_copy_in is not None: |
195 | | - users = set(to_copy_in.users.keys()) |
196 | | - expected_users = {pow, mul} |
197 | | - if users != expected_users: |
198 | | - continue |
199 | | - |
200 | | - # Validate pow exponent is 2 |
201 | | - if pow.args[1] != 2: |
202 | | - continue |
203 | | - |
204 | | - # Extract epsilon from add node (it's a lifted tensor constant) |
205 | | - eps_value = None |
206 | | - for arg in add.args: |
207 | | - eps_value = extract_lifted_tensor_constant(arg) |
208 | | - if eps_value is not None: |
209 | | - break |
210 | | - |
211 | | - if eps_value is None: |
212 | | - continue |
213 | | - |
214 | | - # Build body from non-None entries |
215 | | - body = [n for n in entries if n is not None] |
216 | | - |
217 | | - return cls( |
218 | | - head=head, |
219 | | - body=body, |
220 | | - input_node=original_input, |
221 | | - weight_node=weight_node, |
222 | | - eps=eps_value, |
223 | | - ) |
224 | | - |
225 | | - return None |
226 | | - |
227 | | - |
228 | | -class FuseRMSNormPass(ExportPass): |
229 | | - """ |
230 | | - Fuses decomposed RMSNorm operations into aten.rms_norm. |
231 | | -
|
232 | | - This reduces ~7 ops to 1 fused op per RMSNorm layer. |
233 | | - """ |
234 | | - |
235 | | - def call(self, graph_module: GraphModule) -> PassResult: |
236 | | - graph = graph_module.graph |
237 | | - modified = False |
238 | | - |
239 | | - for node in list(graph.nodes): |
240 | | - match = RMSNormMatch.maybe_create(node) |
241 | | - if match is None: |
242 | | - continue |
243 | | - |
244 | | - # Get input shape for normalized_shape |
245 | | - input_meta = match.input_node.meta.get("val") |
246 | | - if input_meta is None: |
247 | | - continue |
248 | | - |
249 | | - # Create fused rms_norm node |
250 | | - with graph.inserting_before(node): |
251 | | - normalized_shape = [input_meta.shape[-1]] |
252 | | - rms_norm_node = graph.call_function( |
253 | | - torch.ops.aten.rms_norm.default, |
254 | | - args=( |
255 | | - match.input_node, |
256 | | - normalized_shape, |
257 | | - match.weight_node, |
258 | | - match.eps, |
259 | | - ), |
260 | | - ) |
261 | | - rms_norm_node.meta = node.meta.copy() |
262 | | - |
263 | | - node.replace_all_uses_with(rms_norm_node) |
264 | | - match.remove_body_nodes(graph) |
265 | | - graph.erase_node(node) |
266 | | - modified = True |
267 | | - |
268 | | - if modified: |
269 | | - graph.eliminate_dead_code() |
270 | | - graph.lint() |
271 | | - |
272 | | - return PassResult(graph_module, modified) |
273 | | - |
274 | | - |
275 | 102 | class CanonicalizePermutePass(ExportPass): |
276 | 103 | """ |
277 | 104 | Converts transpose_copy to permute_copy in the edge dialect graph. |
|
0 commit comments