Files
2026-07-13 12:40:42 +08:00

469 lines
14 KiB
Python

# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import atexit
import base64
import json
import sys
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
from typing_extensions import Self
from .envs import ENV_SOT_COLLECT_INFO, ENV_SOT_SERIALIZE_INFO
from .utils import Singleton
if TYPE_CHECKING:
import types
from .exceptions import BreakGraphReasonBase
PREFIX = "<sot>"
SUFFIX = "</sot>"
ENCODING = "utf-8"
def try_import_graphviz():
try:
import graphviz
return graphviz
except ImportError:
return None
class InfoType(Enum):
STEP_INFO = 0
E2E_INFO = 1
class InfoCollector(metaclass=Singleton):
def __init__(self):
self._step_info: dict[str, list[InfoBase]] = {}
self._e2e_info: dict[str, list[InfoBase]] = {}
def get_info_dict(self, info_type: InfoType) -> dict[str, list[InfoBase]]:
if info_type == InfoType.STEP_INFO:
return self._step_info
else:
return self._e2e_info
def attach(self, cls: type[InfoBase], *args, **kwargs) -> None:
if self.need_collect(cls):
info = cls(*args, **kwargs)
self.register(info)
def register(self, info: InfoBase) -> None:
info_class_name = info.__class__.__name__
info_type = info.TYPE
info_dict = self.get_info_dict(info_type)
info_dict.setdefault(info_class_name, [])
info_dict[info_class_name].append(info)
def need_collect(self, cls: type[InfoBase]) -> bool:
return cls.SHORT_NAME in ENV_SOT_COLLECT_INFO.get()
def clear_step_info(self):
self._step_info.clear()
def clear_e2e_info(self):
self._e2e_info.clear()
def clear(self):
self.clear_step_info()
self.clear_e2e_info()
def print_step_report(self):
self.print_report(InfoType.STEP_INFO)
def print_e2e_info_atexit(self) -> None:
def atexit_hook():
self.print_report(InfoType.E2E_INFO)
sys.stdout.flush()
self.clear()
atexit.register(atexit_hook)
def print_report(self, info_type: InfoType) -> None:
if info_dict := self.get_info_dict(info_type):
print(self.generate_report(info_dict))
def generate_report(self, info_dict: dict[str, list[InfoBase]]) -> str:
report = ""
for info_class_name, info_list in info_dict.items():
cls = info_list[0].__class__
report += f"{info_class_name} ({cls.SHORT_NAME}):\n"
if ENV_SOT_SERIALIZE_INFO.get():
report += cls.json_report(info_list)
else:
report += cls.summary(info_list)
report += "\n"
return report
InfoCollector().print_e2e_info_atexit()
class InfoBase(ABC):
SHORT_NAME: ClassVar[str]
TYPE: ClassVar[InfoType]
def __init__(self): ...
@classmethod
@abstractmethod
def summary(cls, history: list[Self]) -> str: ...
@classmethod
def serialize(cls, obj: dict[str:Any]) -> str:
json_data = json.dumps(obj)
b64_bytes = base64.b64encode(json_data.encode(ENCODING))
return b64_bytes.decode(ENCODING)
@classmethod
def deserialize(cls, data: bytes | str) -> dict:
if isinstance(data, str):
data = data.encode(ENCODING)
json_str = base64.b64decode(data).decode(ENCODING)
return json.loads(json_str)
class NewSymbolHitRateInfo(InfoBase):
SHORT_NAME = "new_symbol_hit_rate"
TYPE = InfoType.STEP_INFO
def __init__(
self, input_tensor_ids: list[int], output_tensor_ids: list[int]
):
super().__init__()
self.input_tensor_ids = input_tensor_ids
self.output_tensor_ids = output_tensor_ids
@classmethod
def summary(cls, history: list[Self]) -> str:
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
if len(history) == 1:
return "Only one subgraph is generated"
known_tensor_ids = set()
hit_count = 0
all_count = sum([len(info.input_tensor_ids) for info in history[1:]])
for i, info in enumerate(history):
for tensor_id in info.input_tensor_ids:
# Skip the first graph
if i == 0:
continue
if tensor_id in known_tensor_ids:
hit_count += 1
for tensor_id in info.output_tensor_ids:
known_tensor_ids.add(tensor_id)
summary = f"All tensor count: {all_count}, hit count: {hit_count}\n"
summary += f"Hit rate: {hit_count / all_count:.2f}"
return summary
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class SubGraphRelationInfo(InfoBase):
SHORT_NAME = "subgraph_relation"
TYPE = InfoType.STEP_INFO
STEP_UNIQUE_ID = 0
class ConcreteShapeInfo(NamedTuple):
id: int
ir_shape: list[int]
real_shape: list[int]
def __init__(
self,
subgraph_name: str,
input_shape_infos: list[SubGraphRelationInfo.ConcreteShapeInfo],
output_shape_infos: list[SubGraphRelationInfo.ConcreteShapeInfo],
is_first_call: bool,
graph_size: int,
):
super().__init__()
self.subgraph_name = subgraph_name
self.input_shape_infos = input_shape_infos
self.output_shape_infos = output_shape_infos
self.is_first_call = is_first_call
self.graph_size = graph_size
@classmethod
def summary(cls, history: list[Self]) -> str:
# TODO: attach input shape (with dynamic shape info)
cls.STEP_UNIQUE_ID += 1
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
if all(not subgraph_info.is_first_call for subgraph_info in history):
return "All subgraph are not the first call"
graphviz = try_import_graphviz()
if graphviz is None:
return "Please install graphviz to show the subgraph relation"
dot = graphviz.Digraph()
shape_infos = [
shape_info
for info in history
for shape_info in info.input_shape_infos + info.output_shape_infos
]
def to_tensor_node_name(
shape_info: SubGraphRelationInfo.ConcreteShapeInfo,
):
return f"tensor_{shape_info.id}"
visited_shape = set()
for shape_info in shape_infos:
if shape_info.id in visited_shape:
continue
visited_shape.add(shape_info.id)
dot.node(
to_tensor_node_name(shape_info),
f"Tensor {shape_info.id} shape={shape_info.real_shape}",
shape="rect",
)
for i, info in enumerate(history):
subgraph_id = f"subgraph_{i}"
dot.node(
subgraph_id,
f"Subgraph {i} ({info.subgraph_name}, size={info.graph_size})",
shape="oval",
fillcolor="cyan" if info.is_first_call else None,
style="filled" if info.is_first_call else None,
)
for shape_info in info.input_shape_infos:
dot.edge(
to_tensor_node_name(shape_info),
subgraph_id,
label=str(shape_info.ir_shape),
)
for shape_info in info.output_shape_infos:
dot.edge(
subgraph_id,
to_tensor_node_name(shape_info),
label=str(shape_info.ir_shape),
)
directory = Path(".") / "subgraph_relation"
directory.mkdir(exist_ok=True, parents=True)
filename = f"subgraph_relation_{cls.STEP_UNIQUE_ID}"
dot.render(directory / filename, format="svg", cleanup=True)
return f"Please check {directory / filename}.svg for subgraph relation"
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class CompileCountInfo(InfoBase):
SHORT_NAME = "compile_count"
TYPE = InfoType.E2E_INFO
def __init__(self, code: types.CodeType):
super().__init__()
self.code = code
@classmethod
def summary(cls, history: list[Self]) -> str:
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
code_count = {}
for info in history:
code_count[info.code] = code_count.get(info.code, 0) + 1
summary_lines = []
for code, count in sorted(
code_count.items(), key=lambda x: x[1], reverse=True
):
filename, lineno = code.co_filename, code.co_firstlineno
summary_lines.append(
f" {code.co_name} ({filename}:{lineno}): {count}"
)
summary = "\n".join(summary_lines)
return summary
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class BreakGraphReasonInfo(InfoBase):
SHORT_NAME = "breakgraph_reason"
TYPE = InfoType.E2E_INFO
def __init__(self, reason: BreakGraphReasonBase):
super().__init__()
self.reason = reason
@classmethod
def classify(cls, history: list[Self]) -> str:
reasons_dict = {}
for info in history:
name = info.reason.__class__.__name__
if name not in reasons_dict:
reasons_dict[name] = []
reasons_dict[name].append(str(info.reason))
sorted_reasons = list(reasons_dict.items())
sorted_reasons.sort(key=lambda x: len(x[1]), reverse=True)
return reasons_dict, sorted_reasons
@classmethod
def summary(cls, history: list[Self]) -> str:
reason_dict, reason_list = cls.classify(history)
return "\n".join(
[
f"{name} ({len(reasons)}):\n\t" + "\n\t".join(reasons)
for name, reasons in reason_list
]
)
@classmethod
def json_report(cls, history: list[Self]) -> str:
reason_dict, sorted_reasons = cls.classify(history)
reason_dict["count"] = {k: len(v) for k, v in sorted_reasons}
serialized = cls.serialize({cls.SHORT_NAME: reason_dict})
return f"{PREFIX}{serialized}{SUFFIX}"
@classmethod
def restore_from_string(cls, serialized: str) -> list[Self]:
# This method is the inverse of json_report
from paddle.jit.sot.utils import exceptions
history = []
obj = cls.deserialize(serialized)[cls.SHORT_NAME]
obj.pop("count")
for classname in obj:
ReasonClass = getattr(exceptions, classname, None)
for reason in obj[classname]:
history.append(cls(ReasonClass(reason_str=reason)))
return history
@staticmethod
def collect_break_graph_reason(reason: BreakGraphReasonBase):
if not InfoCollector().need_collect(BreakGraphReasonInfo):
return
InfoCollector().attach(BreakGraphReasonInfo, reason)
class SubGraphInfo(InfoBase):
SHORT_NAME = "subgraph_info"
TYPE = InfoType.STEP_INFO
def __init__(self, graph: str, op_num: int, sir_name: str):
# NOTE: All data should be serializable
super().__init__()
self.graph = graph
self.op_num = op_num
self.sir_name = sir_name
def __str__(self):
return (
f"[SIR Name] {self.sir_name} [OpNum] {self.op_num}\n{self.graph}"
)
@classmethod
def summary(cls, history: list[Self]) -> str:
num_of_subgraph = len(history)
sum_of_op_num = sum(item.op_num for item in history)
need_details = "details" in ENV_SOT_COLLECT_INFO.get().get(
cls.SHORT_NAME, []
)
details = ""
if need_details:
details = "\n".join(
[
f"[SubGraphIdx] {idx} {info}"
for idx, info in enumerate(map(str, history))
]
)
summary = f"[Number of subgraph] {num_of_subgraph} [Sum of opnum] {sum_of_op_num}"
return f"{summary}\n{details}"
@classmethod
def json_report(cls, history: list[Self]) -> str:
need_details = "details" in ENV_SOT_COLLECT_INFO.get().get(
cls.SHORT_NAME, []
)
aggregated_info_list = []
for idx, record in enumerate(history):
entry_data = {}
entry_data["SIR_name"] = record.sir_name
entry_data["OpNum"] = record.op_num
entry_data["Graph"] = ""
if need_details:
entry_data["Graph"] = str(record.graph)
aggregated_info_list.append(entry_data)
serialized = cls.serialize({cls.SHORT_NAME: aggregated_info_list})
return f"{PREFIX}{serialized}{SUFFIX}"
@classmethod
def restore_from_string(cls, serialized: str) -> list[Self]:
# This method is the inverse of json_report
history = []
obj = cls.deserialize(serialized)[cls.SHORT_NAME]
for entry in obj:
history.append(
SubGraphInfo(
graph=entry["Graph"],
op_num=entry["OpNum"],
sir_name=entry["SIR_name"],
)
)
return history
def __eq__(self, other):
need_graph_equal = "details" in ENV_SOT_COLLECT_INFO.get().get(
self.SHORT_NAME, []
)
graph_equal_or_not = True
if need_graph_equal:
graph_equal_or_not = self.graph == other.graph
return (
graph_equal_or_not
and self.op_num == other.op_num
and self.sir_name == other.sir_name
)