diff --git a/python/paddle/jit/sot/infer_meta.py b/python/paddle/jit/sot/infer_meta.py index 2fa2e14be1fc1..099298753b69a 100644 --- a/python/paddle/jit/sot/infer_meta.py +++ b/python/paddle/jit/sot/infer_meta.py @@ -288,9 +288,8 @@ def __init__(self): self.var_name_generator = UniqueNameGenerator(SOT_INFER_META_INNER_VAR) def gen_name(self, meta): - name = f"{meta.dtype}_{meta.stop_gradient}" - for l in meta.shape: - name += f"_{l}" + name = f"{meta.dtype}_{meta.stop_gradient}_" + name += "_".join(map(str, meta.shape)) return name @property diff --git a/python/paddle/jit/sot/opcode_translator/executor/dispatch_functions.py b/python/paddle/jit/sot/opcode_translator/executor/dispatch_functions.py index f62f44b51568d..45ab050f4449c 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/dispatch_functions.py +++ b/python/paddle/jit/sot/opcode_translator/executor/dispatch_functions.py @@ -14,11 +14,14 @@ # This file stores the customized function that will be called by the dispatch mechanism. -from ...utils import BreakGraphError, FallbackError +from ...utils import BreakGraphError, BreakGraphReasonBase, FallbackError -def raise_break_graph_fn(*args, **kwarg): - raise BreakGraphError("raise by raise_break_graph_fn.") +def create_raise_break_graph_handler(reason: BreakGraphReasonBase): + def raise_break_graph_fn(*args, **kwarg): + raise BreakGraphError(reason) + + return raise_break_graph_fn def raise_not_implement_fn(*args, **kwarg): diff --git a/python/paddle/jit/sot/opcode_translator/executor/function_graph.py b/python/paddle/jit/sot/opcode_translator/executor/function_graph.py index 488aca8e600fb..146dd959921b2 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/function_graph.py +++ b/python/paddle/jit/sot/opcode_translator/executor/function_graph.py @@ -52,7 +52,12 @@ map_if, switch_symbol_registry, ) -from ...utils.exceptions import BreakGraphError, SotExtraInfo +from ...utils.exceptions import ( + BreakGraphError, + DygraphInconsistentWithStaticBreak, + InferMetaBreak, + SotExtraInfo, +) from ..instruction_utils import get_instructions from .guard import Guard, StringifiedExpression, make_guard from .mutable_data import MutationDel, MutationNew, MutationSet @@ -671,7 +676,9 @@ def try_infer_meta_fn(args, kwargs) -> Any: ): # TODO(zrr1999): maybe we can continue to fallback to all args are constant. raise BreakGraphError( - f"InferMeta encount {type(e)}, but all args are not symbolic." + InferMetaBreak( + f"InferMeta encount {type(e)}, but all args are not symbolic." + ) ) args, kwargs = map_if( @@ -698,7 +705,9 @@ def try_infer_meta_fn(args, kwargs) -> Any: for arg in flatten_vars ): raise BreakGraphError( - f"InferMeta encount {type(e)}, but all args are not symbolic." + InferMetaBreak( + f"InferMeta encount {type(e)}, but all args are not symbolic." + ) ) args, kwargs = map_structure( @@ -712,7 +721,9 @@ def try_infer_meta_fn(args, kwargs) -> Any: except Exception as e: if SotExtraInfo.from_exception(e).need_breakgraph: raise BreakGraphError( - f"API {func} encountered a need break graph error {e}" + DygraphInconsistentWithStaticBreak( + f"API {func} encountered a need break graph error {e}" + ) ) raise e diff --git a/python/paddle/jit/sot/opcode_translator/executor/opcode_executor.py b/python/paddle/jit/sot/opcode_translator/executor/opcode_executor.py index e221b9cae7abe..75eb7dfb042e1 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/opcode_executor.py +++ b/python/paddle/jit/sot/opcode_translator/executor/opcode_executor.py @@ -35,9 +35,12 @@ ENV_MIN_GRAPH_SIZE, ENV_SOT_FORCE_FALLBACK_SIR_IDS, BreakGraphError, + BuiltinFunctionBreak, FallbackError, InnerError, SotUndefinedVar, + UnsupportedIteratorBreak, + UnsupportedOperationBreak, get_static_function, is_comprehensive_name, log, @@ -806,7 +809,9 @@ def LOAD_ATTR(self, instr: Instruction): def LOAD_SUPER_ATTR(self, instr: Instruction): # This bytecode is for Python 3.12+, and it will break graph in Python 3.11-. # We align it's behavior with Python 3.11-. - raise BreakGraphError("call super is not supported") + raise BreakGraphError( + BuiltinFunctionBreak(reason_str="call super is not supported") + ) def LOAD_CONST(self, instr: Instruction): var = self._co_consts[instr.arg] @@ -1082,7 +1087,11 @@ def build_seq_unpack(self, instr: Instruction): if not isinstance( item, (TupleVariable, ListVariable, RangeVariable) ): - raise BreakGraphError(f"{type(item)} not support unpack") + raise BreakGraphError( + UnsupportedOperationBreak( + reason_str=f"{type(item)} not support unpack" + ) + ) retval.extend(item.get_iter().to_list()) if instr.opname in { @@ -1971,7 +1980,9 @@ def FOR_ITER(self, instr): try: if not isinstance(iterator, SequenceIterVariable): raise BreakGraphError( - f"Can not simulate iterator of {type(iterator)}." + UnsupportedIteratorBreak( + f"Can not simulate iterator of {type(iterator)}." + ) ) backup_iter_idx = iterator.idx diff --git a/python/paddle/jit/sot/opcode_translator/executor/opcode_inline_executor.py b/python/paddle/jit/sot/opcode_translator/executor/opcode_inline_executor.py index f8de4cc3ef95d..c16a42c0f84e7 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/opcode_inline_executor.py +++ b/python/paddle/jit/sot/opcode_translator/executor/opcode_inline_executor.py @@ -21,7 +21,12 @@ from typing import TYPE_CHECKING from ...profiler import event_register -from ...utils import BreakGraphError, log +from ...utils import ( + BreakGraphError, + DataDependencyControlFlowBreak, + UnsupportedIteratorBreak, + log, +) from ..instruction_utils import Instruction from .guard import StringifiedExpression, union_free_vars from .opcode_executor import OpcodeExecutorBase, Stop @@ -298,9 +303,8 @@ def _break_graph_when_if(self, result, instr: Instruction): result: The result of the operation. instr (Instruction): The jump instruction. """ - raise BreakGraphError( - "OpcodeInlineExecutor want break graph when simulate `if`." - ) + + raise BreakGraphError(DataDependencyControlFlowBreak()) def FOR_ITER(self, instr: Instruction): iterator = self.stack.top @@ -327,5 +331,7 @@ def FOR_ITER(self, instr: Instruction): else: self._graph.remove_global_guarded_variable(iterator) raise BreakGraphError( - f"Found {iterator.__class__.__name__} as iterator." + UnsupportedIteratorBreak( + reason_str=f"Found {iterator.__class__.__name__} as iterator." + ) ) diff --git a/python/paddle/jit/sot/opcode_translator/executor/variable_dispatch.py b/python/paddle/jit/sot/opcode_translator/executor/variable_dispatch.py index 6dac4ab537497..710041c92b72e 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/variable_dispatch.py +++ b/python/paddle/jit/sot/opcode_translator/executor/variable_dispatch.py @@ -24,7 +24,14 @@ import paddle -from ...utils import BreakGraphError, FallbackError, get_numpy_ufuncs +from ...utils import ( + BreakGraphError, + BuiltinFunctionBreak, + FallbackError, + UnsupportedIteratorBreak, + UnsupportedOperationBreak, + get_numpy_ufuncs, +) from ...utils.magic_methods import ( BINARY_OPS, UNARY_OPS, @@ -32,11 +39,11 @@ ) from ...utils.paddle_api_config import get_tensor_methods from .dispatch_functions import ( + create_raise_break_graph_handler, operator_in, operator_is_none, operator_is_not_none, operator_not_in, - raise_break_graph_fn, tensor_numel, ) from .dispatcher import Dispatcher, optional @@ -120,7 +127,9 @@ def inner(*args, **kwargs): Dispatcher.register( operator_in, ("VariableBase", "IterVariable"), - raise_err_handle(BreakGraphError("Codes like: `variable in iterator`.")), + create_raise_break_graph_handler( + UnsupportedIteratorBreak("Codes like: `variable in iterator`.") + ), ) Dispatcher.register( @@ -152,8 +161,8 @@ def inner(*args, **kwargs): Dispatcher.register( operator_not_in, ("VariableBase", "IterVariable"), - raise_err_handle( - BreakGraphError("Codes like: `variable not in iterator`.") + create_raise_break_graph_handler( + UnsupportedIteratorBreak("Codes like: `variable not in iterator`.") ), ) @@ -1025,7 +1034,11 @@ def is_not_func(var: VariableBase, other: VariableBase): Dispatcher.register( unary_fn, ("TensorVariable",), - raise_break_graph_fn, + create_raise_break_graph_handler( + BuiltinFunctionBreak( + fn_name=unary_fn, arg_types="TensorVariable" + ) + ), ) continue @@ -1090,7 +1103,11 @@ def tensor_mod_dispatcher( ): if var.get_py_type() is str: raise BreakGraphError( - "(ConstantVariable % TensorVariable) raise a callback. " + UnsupportedOperationBreak( + left_type="ConstantVariable", + right_type="TensorVariable", + operator="__rmod__", + ) ) raise FallbackError("Tensor doesn't support __rmod__") diff --git a/python/paddle/jit/sot/opcode_translator/executor/variables/basic.py b/python/paddle/jit/sot/opcode_translator/executor/variables/basic.py index 96974bf8991b4..b8d1fbb33fe9f 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/variables/basic.py +++ b/python/paddle/jit/sot/opcode_translator/executor/variables/basic.py @@ -36,9 +36,13 @@ from ....utils import ( ENV_SOT_ALLOW_DYNAMIC_SHAPE, BreakGraphError, + BuiltinFunctionBreak, ConstTypes, + DataDependencyDynamicShapeBreak, + DataDependencyOperationBreak, FallbackError, NameGenerator, + UnsupportedOperationBreak, get_tensor_methods, log, printable, @@ -416,7 +420,9 @@ def analyse_dynamic_axes(self, tracker: Tracker): def __len__(self): if isinstance(self.meta.shape[0], SymbolicInt): raise BreakGraphError( - "length of tensor variable with first dimension is dynamic shape causes graph break." + DataDependencyDynamicShapeBreak( + "length of tensor variable with first dimension is dynamic shape causes graph break." + ) ) return self.meta.shape[0] @@ -439,7 +445,9 @@ def __hash__(self): return SotTensor(self.id) raise BreakGraphError( - "Called TensorVariable.get_py_value. Should not use Tensor's value in simulating." + DataDependencyOperationBreak( + "Called TensorVariable.get_py_value. Should not use Tensor's value in simulating." + ) ) def get_py_type(self): @@ -589,7 +597,9 @@ def size(self): # TODO: maybe break graph. if self.meta.is_dynamic_shape(): raise BreakGraphError( - f"Getting size for a dynamic shape tensor causes graph break. shape = {self.meta.shape}" + DataDependencyDynamicShapeBreak( + f"Getting size for a dynamic shape tensor causes graph break. shape = {self.meta.shape}" + ) ) elements = reduce(operator.mul, self.meta.shape, 1) return ConstantVariable(elements, self.graph, DummyTracker([self])) @@ -601,7 +611,9 @@ def shape(self): and self.meta.is_dynamic_shape() ): raise BreakGraphError( - f"Getting shape for a dynamic shape tensor causes graph break. shape = {self.meta.shape}" + DataDependencyDynamicShapeBreak( + f"Getting shape for a dynamic shape tensor causes graph break. shape = {self.meta.shape}" + ) ) from .container import ListVariable @@ -617,7 +629,9 @@ def len(self): first_dim = self.meta.shape[0] if isinstance(first_dim, SymbolicInt): raise BreakGraphError( - "Getting len() for a dynamic shape tensor causes graph break." + DataDependencyDynamicShapeBreak( + "Getting len() for a dynamic shape tensor causes graph break." + ) ) return ConstantVariable(first_dim, self.graph, DummyTracker([self])) @@ -662,7 +676,9 @@ def getattr(self, name: str, default=None): } if name in ["name", "place", "type"] and self.meta.is_inner_var(): raise BreakGraphError( - f"{self.meta.name} is a middle tensor. get {name} property." + DataDependencyOperationBreak( + f"{self.meta.name} is a middle tensor. Not support to get {name} property." + ) ) if name in [ "dtype", @@ -709,7 +725,9 @@ def setattr(self, key, val): ) def delattr(self, key): - raise BreakGraphError("Don't support TensorVariable delattr") + raise BreakGraphError( + BuiltinFunctionBreak("Don't support TensorVariable delattr") + ) @VariableFactory.register_from_value() def from_value(value: Any, graph: FunctionGraph, tracker: Tracker): @@ -814,7 +832,11 @@ def disable_symbolic(var: VariableBase): def get_py_value(self, allow_tensor: bool = False) -> bool | int | float: if ENV_SOT_BREAK_GRAPH_ON_GET_SYMBOLIC_VALUE.get(): - raise BreakGraphError("get_py_value from SymbolicVariable") + raise BreakGraphError( + DataDependencyOperationBreak( + "get_py_value from SymbolicVariable" + ) + ) self.need_guard_value = True log( 3, @@ -1116,10 +1138,18 @@ def _reconstruct(self, codegen: PyCodeGen): super()._reconstruct(codegen) def setattr(self, key, val): - raise BreakGraphError("Don't support SliceVariable setattr") + raise BreakGraphError( + UnsupportedOperationBreak( + reason_str="Don't support SliceVariable setattr" + ) + ) def delattr(self, key): - raise BreakGraphError("Don't support SliceVariable delattr") + raise BreakGraphError( + UnsupportedOperationBreak( + reason_str="Don't support SliceVariable delattr" + ) + ) @VariableFactory.register_from_value() def from_value(value: Any, graph: FunctionGraph, tracker: Tracker): diff --git a/python/paddle/jit/sot/opcode_translator/executor/variables/callable.py b/python/paddle/jit/sot/opcode_translator/executor/variables/callable.py index 17238626b048d..5f3ef3e59e8e4 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/variables/callable.py +++ b/python/paddle/jit/sot/opcode_translator/executor/variables/callable.py @@ -48,9 +48,15 @@ ) from ....utils.exceptions import ( BreakGraphError, + BuiltinFunctionBreak, + DataDependencyOperationBreak, FallbackError, + InlineCallBreak, InnerError, + PsdbBreakReason, SotErrorBase, + UnsupportedOperationBreak, + UnsupportedPaddleAPIBreak, ) from ..dispatcher import Dispatcher from ..guard import ( @@ -196,7 +202,9 @@ def handle_psdb_function(self, /, *args, **kwargs): BM.add(BM.cur_exe._code.co_filename, BM.cur_exe._current_line) return ConstantVariable.wrap_literal(None, self.graph) elif self.value is psdb.breakgraph: - raise BreakGraphError("breakgraph by psdb.breakgraph") + raise BreakGraphError( + PsdbBreakReason("breakgraph by psdb.breakgraph") + ) elif self.value is psdb.fallback: raise FallbackError("fallback by psdb.fallback") elif self.value is psdb.in_sot: @@ -232,7 +240,9 @@ def call_function(self, /, *args, **kwargs) -> VariableBase: code_name = self.value.__code__.co_name location_info = f'File "{filename}", line {lineno}, in {code_name}' raise BreakGraphError( - f"{location_info} encountered breakgraph error caused by\n{indent}{e}" + InlineCallBreak( + f"{location_info} encountered breakgraph error caused by\n{indent}{e}" + ) ) return output @@ -289,7 +299,7 @@ def __init__( def call_function(self, /, *args, **kwargs): if is_break_graph_api(self.value): raise BreakGraphError( - f"breakgraph by unsupported function: {self.value.__name__}" + UnsupportedPaddleAPIBreak(fn_name=self.value.__name__) ) return self.graph.call_paddle_api(self.value, *args, **kwargs) @@ -336,7 +346,9 @@ def __init__( def call_function(self, /, *args, **kwargs): if is_break_graph_tensor_methods(self.method_name): - raise BreakGraphError("call break_graph_tensor_method.") + raise BreakGraphError( + DataDependencyOperationBreak("call break_graph_tensor_method.") + ) return self.graph.call_tensor_method(self.method_name, *args, **kwargs) def bind(self, instance: VariableBase, name: str): @@ -524,7 +536,9 @@ def getitem(self, key): ) except Exception as e: raise BreakGraphError( - f"call {self.value.__class__.__name__}.__getitem__ with slice as key, and slice with py value failed: {e}." + UnsupportedOperationBreak( + reason_str=f"call {self.value.__class__.__name__}.__getitem__ with slice as key, and slice with py value failed: {e}." + ) ) else: @@ -779,7 +793,7 @@ def call_function(self, /, *args, **kwargs): else self.value ) raise BreakGraphError( - f"Not support builtin function: {fn_name} with args: Args({arg_types})" + BuiltinFunctionBreak(fn_name=fn_name, arg_types=arg_types) ) @VariableFactory.register_from_value(successor="ClassVariable") diff --git a/python/paddle/jit/sot/opcode_translator/executor/variables/iter.py b/python/paddle/jit/sot/opcode_translator/executor/variables/iter.py index 510935a352ebd..a9f109a9619a0 100644 --- a/python/paddle/jit/sot/opcode_translator/executor/variables/iter.py +++ b/python/paddle/jit/sot/opcode_translator/executor/variables/iter.py @@ -20,7 +20,7 @@ VariableBase, ) -from ....utils import BreakGraphError, FallbackError +from ....utils import BreakGraphError, FallbackError, UnsupportedOperationBreak from ..tracker import ConstTracker, DummyTracker from .base import VariableFactory from .basic import ConstantVariable @@ -274,4 +274,8 @@ def __init__(self, obj, graph, tracker): super().__init__(obj, graph, tracker) def next(self): - raise BreakGraphError("Break graph when using user defined iterator") + raise BreakGraphError( + UnsupportedOperationBreak( + reason_str="Break graph when using user defined iterator" + ) + ) diff --git a/python/paddle/jit/sot/utils/__init__.py b/python/paddle/jit/sot/utils/__init__.py index 8765c81235e46..71c1c8c4c5343 100644 --- a/python/paddle/jit/sot/utils/__init__.py +++ b/python/paddle/jit/sot/utils/__init__.py @@ -37,12 +37,21 @@ ) from .exceptions import ( # noqa: F401 BreakGraphError, + BreakGraphReasonBase, + BuiltinFunctionBreak, + DataDependencyControlFlowBreak, + DataDependencyDynamicShapeBreak, + DataDependencyOperationBreak, ExportError, FallbackError, InnerError, + PsdbBreakReason, + UnsupportedIteratorBreak, + UnsupportedOperationBreak, inner_error_default_handler, ) from .info_collector import ( # noqa: F401 + BreakGraphReasonInfo, CompileCountInfo, InfoCollector, NewSymbolHitRateInfo, diff --git a/python/paddle/jit/sot/utils/exceptions.py b/python/paddle/jit/sot/utils/exceptions.py index c83746927c75f..a109439312bdb 100644 --- a/python/paddle/jit/sot/utils/exceptions.py +++ b/python/paddle/jit/sot/utils/exceptions.py @@ -15,6 +15,154 @@ import traceback +from .info_collector import BreakGraphReasonInfo + + +class BreakGraphReasonBase: + """Base class for representing reasons why graph execution was interrupted. + + Attributes: + reason_str (str): Description of the break reason + file_path (str): Path to the file where break occurred + line_number (int): Line number where break occurred + """ + + def __init__( + self, + reason_str, + file_path="", + line_number=-1, + ): + self.reason_str = reason_str + self.file_path = file_path + self.line_number = line_number + + def __repr__(self) -> str: + return f"{self.reason_str}" + + +class DataDependencyBreak(BreakGraphReasonBase): + pass + + +class DataDependencyControlFlowBreak(DataDependencyBreak): + """Break reason for control flow execution.""" + + def __init__(self, reason_str=None, file_path="", line_number=-1): + if reason_str is None: + + reason_str = "OpcodeInlineExecutor want break graph when simulate control flow." + + super().__init__( + reason_str, + file_path, + line_number, + ) + + +class DataDependencyDynamicShapeBreak(DataDependencyBreak): + pass + + +class DataDependencyOperationBreak(DataDependencyBreak): + pass + + +class UnsupportedOperationBreak(BreakGraphReasonBase): + def __init__( + self, + *, + left_type=None, + right_type=None, + operator=None, + reason_str=None, + file_path="", + line_number=-1, + ): + if reason_str is None: + reason_str = f"Unsupported operator '{operator}' between {left_type} and {right_type}" + super().__init__(reason_str, file_path, line_number) + + +class UnsupportedPaddleAPIBreak(UnsupportedOperationBreak): + def __init__( + self, + *, + fn_name=None, + reason_str=None, + file_path="", + line_number=-1, + ): + if reason_str is None: + reason_str = f"Not support Paddlepaddle API: {fn_name}" + + super().__init__( + reason_str=reason_str, + file_path=file_path, + line_number=line_number, + ) + + +class BuiltinFunctionBreak(UnsupportedOperationBreak): + """Break reason for unsupported built-in function calls. + + Args: + fn_name (str): Name of the builtin function + arg_types (list): Types of the arguments passed to the function + file_path (str): Path to the file where break occurred + line_number (int): Line number where break occurred + """ + + def __init__( + self, + *, + fn_name=None, + arg_types=None, + reason_str=None, + file_path="", + line_number=-1, + ): + if reason_str is None: + reason_str = f"Not support builtin function: {fn_name} with args: Args({arg_types})" + + super().__init__( + reason_str=reason_str, + file_path=file_path, + line_number=line_number, + ) + + +class SideEffectBreak(BreakGraphReasonBase): + pass + + +class UnsupportedIteratorBreak(SideEffectBreak): + pass + + +class InlineCallBreak(BreakGraphReasonBase): + pass + + +class DygraphInconsistentWithStaticBreak(BreakGraphReasonBase): + pass + + +class PsdbBreakReason(BreakGraphReasonBase): + pass + + +class InferMetaBreak(BreakGraphReasonBase): + """Break reason during meta information inference phase.""" + + pass + + +class UnspecifiedBreakReason(BreakGraphReasonBase): + """Break reason for cases that don't fall into other categories.""" + + pass + class SotErrorBase(Exception): def __init__(self, *args, **kwargs): @@ -44,7 +192,14 @@ def __init__(self, msg, disable_eval_frame=False): # raise in inline function call strategy. class BreakGraphError(SotErrorBase): - pass + def __init__(self, reason: BreakGraphReasonBase | str = None): + super().__init__() + + if isinstance(reason, str): + # if reason is a string, then create a UnspecifiedBreakReason object + reason = UnspecifiedBreakReason(reason) + self.reason = reason + BreakGraphReasonInfo.collect_break_graph_reason(reason) def inner_error_default_handler(func, message_fn): diff --git a/python/paddle/jit/sot/utils/info_collector.py b/python/paddle/jit/sot/utils/info_collector.py index 2892f468a70c3..92668d03a3212 100644 --- a/python/paddle/jit/sot/utils/info_collector.py +++ b/python/paddle/jit/sot/utils/info_collector.py @@ -26,6 +26,10 @@ from .envs import ENV_SOT_COLLECT_INFO from .utils import Singleton +if TYPE_CHECKING: + from .exceptions import BreakGraphReasonBase + + if TYPE_CHECKING: import types @@ -265,3 +269,40 @@ def summary(cls, history: list[Self]) -> str: ) summary = "\n".join(summary_lines) return summary + + +class BreakGraphReasonInfo(InfoBase): + SHORT_NAME = "breakgraph_reason" + TYPE = InfoType.E2E_INFO + + def __init__(self, reason: BreakGraphReasonBase): + super().__init__() + self.reason = reason + + @classmethod + def summary(cls, history: list[Self]) -> str: + + reason_dict = {} + + for info in history: + name = info.reason.__class__.__name__ + if name not in reason_dict: + reason_dict[name] = [] + reason_dict[name].append(str(info.reason)) + + reason_list = list(reason_dict.items()) + reason_list.sort(key=lambda x: len(x[1]), reverse=True) + + return "\n".join( + [ + f"{name} ({len(reasons)}):\n\t" + "\n\t".join(reasons) + for name, reasons in reason_list + ] + ) + + @staticmethod + def collect_break_graph_reason(reason: BreakGraphReasonBase): + if not InfoCollector().need_collect(BreakGraphReasonInfo): + return + + InfoCollector().attach(BreakGraphReasonInfo, reason)