Coverage for /opt/cloud/slavespace/usr1/096471637100f3de0fcfc01072822a80/dttest/api/python/ge/ge/passes/pattern.py: 91%

163 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-04 11:36 +0800

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3# ----------------------------------------------------------------------------------------------------------- 

4# Copyright (c) 2026 Huawei Technologies Co., Ltd. 

5# This program is free software, you can redistribute it and/or modify it under the terms and conditions of 

6# CANN Open Software License Agreement Version 2.0 (the "License"). 

7# Please refer to the License for details. You may not use this file except in compliance with the License. 

8# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, 

9# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. 

10# See LICENSE in the root of the software repository for the full text of the License. 

11# ----------------------------------------------------------------------------------------------------------- 

12 

13"""PatternFusionPass helper types and constructors.""" 

14 

15from __future__ import annotations 

16 

17import inspect 

18from dataclasses import dataclass 

19from functools import wraps 

20from typing import TYPE_CHECKING, Callable, Dict, Iterator, List, Type, Union, overload 

21 

22from ge.es.graph_builder import GraphBuilder 

23from ge.es.tensor_holder import TensorHolder 

24from ge.graph import Graph, Node 

25 

26if TYPE_CHECKING: 

27 from ._native import Pattern 

28 

29 

30_PATTERN_METHOD_MARK = "__ge_expression_pattern_method__" 

31 

32 

33def __getattr__(name: str): 

34 if name == "Pattern": 

35 from ._native import Pattern 

36 

37 globals()["Pattern"] = Pattern 

38 return Pattern 

39 raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 

40 

41 

42@dataclass(frozen=True) 

43class NodeIo: 

44 """Python helper describing one node output.""" 

45 

46 node: Node 

47 index: int = 0 

48 

49 

50class PatternInputs: 

51 """Lazy graph input collection passed to expression-style pass hooks. 

52 

53 Users receive this object from ``@pattern`` methods or 

54 ``replacement(self, inputs)``. Accessing ``inputs[i]`` creates graph input 

55 ``i`` on demand. Use ``inputs[:N]`` to declare multiple inputs explicitly. 

56 The input count is intentionally unknown, so direct iteration is rejected. 

57 """ 

58 

59 def __init__(self, builder: GraphBuilder) -> None: 

60 self._builder = builder 

61 self._inputs: Dict[int, TensorHolder] = {} 

62 

63 @overload 

64 def __getitem__(self, index: int) -> TensorHolder: ... 

65 

66 @overload 

67 def __getitem__(self, index: slice) -> List[TensorHolder]: ... 

68 

69 def __getitem__( 

70 self, index: Union[int, slice] 

71 ) -> Union[TensorHolder, List[TensorHolder]]: 

72 if isinstance(index, slice): 

73 return self._get_slice(index) 

74 if not isinstance(index, int): 

75 raise TypeError("PatternInputs index must be an integer or slice") 

76 if index < 0: 

77 raise ValueError("PatternInputs index must be non-negative") 

78 self._ensure_created(index) 

79 return self._inputs[index] 

80 

81 def __iter__(self) -> Iterator[TensorHolder]: 

82 raise TypeError( 

83 "PatternInputs cannot be iterated because the input count is unknown; use inputs[:N]" 

84 ) 

85 

86 def _get_slice(self, index_slice: slice) -> List[TensorHolder]: 

87 if index_slice.step not in (None, 1): 

88 raise ValueError("PatternInputs slice step must be 1") 

89 start = 0 if index_slice.start is None else index_slice.start 

90 stop = index_slice.stop 

91 if stop is None: 

92 raise ValueError( 

93 "PatternInputs slice must provide a stop index, for example inputs[:3]" 

94 ) 

95 if start < 0 or stop < 0: 

96 raise ValueError("PatternInputs slice indices must be non-negative") 

97 if stop < start: 

98 raise ValueError( 

99 "PatternInputs slice stop must be greater than or equal to start" 

100 ) 

101 return [self[i] for i in range(start, stop)] 

102 

103 def _ensure_created(self, index: int) -> None: 

104 for input_index in range(len(self._inputs), index + 1): 

105 self._inputs[input_index] = self._builder.create_input(input_index) 

106 

107 def created(self) -> List[TensorHolder]: 

108 """Return created inputs in graph input index order.""" 

109 

110 return [self._inputs[index] for index in sorted(self._inputs)] 

111 

112 

113def create_pattern(graph: Graph) -> "Pattern": 

114 """Build a native Pattern from a pattern graph.""" 

115 from ._native import Pattern 

116 

117 return Pattern(graph) 

118 

119 

120def ensure_pattern(pattern_or_graph: Union["Pattern", Graph]) -> "Pattern": 

121 """Convert a Graph into Pattern on demand for bridge helpers.""" 

122 from ._native import Pattern 

123 

124 if isinstance(pattern_or_graph, Graph): 

125 return create_pattern(pattern_or_graph) 

126 if isinstance(pattern_or_graph, Pattern): 

127 return pattern_or_graph 

128 raise TypeError("PatternFusionPass.patterns must return Pattern or Graph objects") 

129 

130 

131def pattern(method: Callable[..., object]) -> Callable[..., object]: 

132 """Mark a ``PatternFusionPass`` method as one expression-style pattern.""" 

133 

134 setattr(method, _PATTERN_METHOD_MARK, True) 

135 return method 

136 

137 

138def _has_decorated_pattern_methods(cls: Type[object]) -> bool: 

139 return bool(_get_decorated_pattern_methods(cls)) 

140 

141 

142def _adapt_decorated_pattern_methods(cls: Type[object]) -> Callable[..., object]: 

143 """Build legacy ``patterns(self)`` from ``@pattern`` expression methods.""" 

144 

145 methods = _get_decorated_pattern_methods(cls) 

146 for method in methods: 

147 _check_required_arg_count( 

148 method, 

149 min_count=2, 

150 max_count=2, 

151 message="@pattern methods only support method(self, inputs)", 

152 ) 

153 

154 def wrapper(self) -> List["Pattern"]: 

155 patterns = [] 

156 for method in methods: 

157 builder = GraphBuilder(_decorated_pattern_graph_name(self, method)) 

158 inputs = PatternInputs(builder) 

159 result = method(self, inputs) 

160 patterns.extend( 

161 _build_patterns_from_expression_result(builder, inputs, result) 

162 ) 

163 return patterns 

164 

165 return wrapper 

166 

167 

168def _adapt_expression_replacement( 

169 method: Callable[..., object], 

170) -> Callable[..., object]: 

171 """Wrap expression replacement hooks into ``replacement(match_result, context)``.""" 

172 

173 positional_params = _positional_params(method) 

174 if len(positional_params) < 2 or positional_params[1].name != "inputs": 

175 return method 

176 _check_required_arg_count( 

177 method, 

178 min_count=2, 

179 max_count=4, 

180 message="Expression-style replacement only supports replacement(self, inputs), " 

181 "replacement(self, inputs, match_result), replacement(self, inputs, context), or " 

182 "replacement(self, inputs, match_result, context)", 

183 ) 

184 positional_count = _positional_arg_count(method) 

185 accepts_match_result = positional_count in (3, 4) and not ( 

186 positional_count == 3 and positional_params[2].name == "context" 

187 ) 

188 accepts_context = positional_count in (3, 4) and ( 

189 positional_count == 4 or positional_params[2].name == "context" 

190 ) 

191 if positional_count == 4 and positional_params[3].name != "context": 

192 raise TypeError( 

193 "Expression-style replacement with four arguments must use " 

194 "replacement(self, inputs, match_result, context)" 

195 ) 

196 

197 @wraps(method) 

198 def wrapper(self, match_result: object, context: object = None) -> Graph: 

199 builder = GraphBuilder(_default_graph_name(self, "replacement")) 

200 inputs = PatternInputs(builder) 

201 if accepts_match_result and accepts_context: 

202 result = method(self, inputs, match_result, context) 

203 elif accepts_match_result: 

204 result = method(self, inputs, match_result) 

205 elif accepts_context: 

206 result = method(self, inputs, context) 

207 else: 

208 result = method(self, inputs) 

209 return _build_replacement_from_expression_result(builder, result) 

210 

211 return wrapper 

212 

213 

214def _adapt_context_hook( 

215 method: Callable[..., object], hook_name: str, value_name: str 

216) -> Callable[..., object]: 

217 """Normalize a graph-building hook to ``(value, context)``.""" 

218 

219 _check_required_arg_count( 

220 method, 

221 min_count=2, 

222 max_count=3, 

223 message=f"{hook_name} only supports {hook_name}(self, {value_name}) or " 

224 f"{hook_name}(self, {value_name}, context)", 

225 ) 

226 positional_count = _positional_arg_count(method) 

227 accepts_context = positional_count == 3 

228 

229 @wraps(method) 

230 def wrapper(self, value: object, context: object = None): 

231 if accepts_context: 

232 return method(self, value, context) 

233 return method(self, value) 

234 

235 return wrapper 

236 

237 

238def _adapt_replacement_hook( 

239 method: Callable[..., object], 

240) -> Callable[..., object]: 

241 """Normalize an expression-style or graph-building replacement hook.""" 

242 

243 adapted = _adapt_expression_replacement(method) 

244 if adapted is not method: 

245 return adapted 

246 return _adapt_context_hook(method, "replacement", "match_result") 

247 

248 

249def _check_required_arg_count( 

250 method: Callable[..., object], *, min_count: int, max_count: int, message: str 

251) -> None: 

252 count = _positional_arg_count(method) 

253 if ( 

254 count < min_count 

255 or count > max_count 

256 or _has_required_keyword_only_args(method) 

257 ): 

258 raise TypeError(message) 

259 

260 

261def _positional_arg_count(method: Callable[..., object]) -> int: 

262 return len(_positional_params(method)) 

263 

264 

265def _positional_params(method: Callable[..., object]) -> List[inspect.Parameter]: 

266 signature = inspect.signature(method) 

267 return [ 

268 param 

269 for param in signature.parameters.values() 

270 if param.kind 

271 in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD) 

272 ] 

273 

274 

275def _has_required_keyword_only_args(method: Callable[..., object]) -> bool: 

276 signature = inspect.signature(method) 

277 return any( 

278 param.kind == inspect.Parameter.KEYWORD_ONLY 

279 and param.default is inspect.Parameter.empty 

280 for param in signature.parameters.values() 

281 ) 

282 

283 

284def _get_decorated_pattern_methods(cls: Type[object]) -> List[Callable[..., object]]: 

285 return [ 

286 method 

287 for method in cls.__dict__.values() 

288 if callable(method) and getattr(method, _PATTERN_METHOD_MARK, False) 

289 ] 

290 

291 

292def _default_graph_name(instance: object, suffix: str) -> str: 

293 return f"{instance.__class__.__name__}_{suffix}" 

294 

295 

296def _decorated_pattern_graph_name( 

297 instance: object, method: Callable[..., object] 

298) -> str: 

299 return f"{instance.__class__.__name__}_{method.__name__}_pattern" 

300 

301 

302def _build_patterns_from_expression_result( 

303 builder: GraphBuilder, inputs: PatternInputs, result: object 

304) -> List["Pattern"]: 

305 if _is_pattern_or_graph(result): 

306 return [ensure_pattern(result)] 

307 if ( 

308 isinstance(result, (list, tuple)) 

309 and result 

310 and all(_is_pattern_or_graph(item) for item in result) 

311 ): 

312 raise TypeError( 

313 "A @pattern method supports a single pattern only. " 

314 "For multiple patterns, declare several @pattern methods or use legacy patterns(self)." 

315 ) 

316 

317 pattern_outputs = _normalize_tensor_outputs(result, "patterns") 

318 graph = builder.build_and_reset(pattern_outputs) 

319 built_pattern = create_pattern(graph) 

320 for input_tensor in inputs.created(): 

321 built_pattern.capture_tensor(input_tensor) 

322 for output_tensor in pattern_outputs: 

323 built_pattern.capture_tensor(output_tensor) 

324 return [built_pattern] 

325 

326 

327def _build_replacement_from_expression_result( 

328 builder: GraphBuilder, result: object 

329) -> Graph: 

330 if isinstance(result, Graph): 

331 return result 

332 return builder.build_and_reset(_normalize_tensor_outputs(result, "replacement")) 

333 

334 

335def _normalize_tensor_outputs(result: object, hook_name: str) -> List[TensorHolder]: 

336 if isinstance(result, TensorHolder): 

337 return [result] 

338 if isinstance(result, tuple): 

339 result = list(result) 

340 if ( 

341 isinstance(result, list) 

342 and result 

343 and all(isinstance(item, TensorHolder) for item in result) 

344 ): 

345 return result 

346 raise TypeError( 

347 f"Expression-style {hook_name} must return a TensorHolder or a non-empty list/tuple of TensorHolder" 

348 ) 

349 

350 

351def _is_pattern_or_graph(result: object) -> bool: 

352 from ._native import Pattern 

353 

354 return isinstance(result, (Pattern, Graph))