jiadisu
Switch back to Docker SDK with local pkgs
e6066e8
Raw
History Blame Contribute Delete
18.5 kB
# Copyright (c) 2026 SandAI. 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.
import json
import os
import re
import weakref
from typing import Any, Dict, Tuple
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import torch
from magi_compiler.config import get_compile_config
from magi_compiler.tokenflow.graph_executor import FX_NODE_OP # 导入你的枚举类
from magi_compiler.tokenflow.green_ctx import GreenCtxManager, GreenStreamPool
from magi_compiler.tokenflow.sampler import exponential_aligned_sampler
from magi_compiler.utils import magi_logger
from torch._subclasses.fake_tensor import FakeTensor
from torch.fx import GraphModule, Node
MIN_LATENCY_MS = 0.005 # 最小延迟下限(5微秒)
class GraphProfileWrapper:
def __init__(self, graph_module: GraphModule):
# 核心属性初始化
self.graph_module = graph_module
self.noncompute_node_names = set()
self.profile_results: Dict[int, Dict[str, Dict[int, float]]] = {}
# 初始化SM测试列表
max_sm = GreenCtxManager.get_max_sm()
min_sm, align = GreenCtxManager.get_min_and_align_sm()
high_sm_counts = exponential_aligned_sampler(min_sm, max_sm, num_samples=5, align=align)
low_sm_counts = [max_sm - sm for sm in high_sm_counts]
self.sm_samples = sorted(list(set(high_sm_counts).union(set(low_sm_counts)).union(set([max_sm]))))
self.sm_samples.remove(0) if 0 in self.sm_samples else None
# 初始化SeqLen测试列表
self.seqlen_samples = exponential_aligned_sampler(2, 4096, num_samples=10, align=1)
self.seqlen_samples.reverse() # 从大到小测试
# 结果保存路径与测试配置
rank_idx = torch.distributed.get_rank() if torch.distributed.is_initialized() else 0
self.save_base_path = os.path.join(get_compile_config().cache_root_dir, "graph_profile", f"rank_{rank_idx}")
self.profiled = False
# 初始化流池
self.green_stream_pool = GreenStreamPool()
for sm in self.sm_samples:
self.green_stream_pool.get_stream(sm)
self.node_param_ref_dict = {}
def _resolve_symint_expression(self, sym_expr: Any, seq_len: int) -> int:
# 普通整数直接返回
if isinstance(sym_expr, int):
return sym_expr
# 解析SymInt表达式
if isinstance(sym_expr, torch.SymInt):
expr_str = str(sym_expr)
symbols = re.findall(r's\d+', expr_str)
if not symbols:
return int(sym_expr)
eval_env = {sym: seq_len for sym in symbols}
res_str = expr_str
for sym, val in eval_env.items():
res_str = res_str.replace(sym, str(val))
# print(f"解析 SymInt: {expr_str} -> {res_str}")
return int(eval(res_str))
raise TypeError(f"不支持的表达式类型: {type(sym_expr)}")
def _generate_real_tensor(
self, shape: Tuple[int, ...], stride: Tuple[int, ...], dtype: torch.dtype, device: torch.device, seq_len: int
) -> torch.Tensor:
# 解析shape中的SymInt表达式
resolved_shape = []
for dim in shape:
resolved_dim = self._resolve_symint_expression(dim, seq_len)
resolved_shape.append(resolved_dim)
resolved_shape = tuple(resolved_shape)
# 解析stride中的SymInt表达式
resolved_stride = []
for s in stride:
resolved_s = self._resolve_symint_expression(s, seq_len)
resolved_stride.append(resolved_s)
resolved_stride = tuple(resolved_stride)
# 验证shape与stride长度匹配
assert len(resolved_stride) == len(
resolved_shape
), f"stride长度({len(resolved_stride)})与shape长度({len(resolved_shape)})不匹配"
# 创建指定布局的空Tensor并填充随机数据
tensor = torch.empty_strided(
size=resolved_shape, stride=resolved_stride, dtype=dtype, device=device, requires_grad=False
)
tensor.normal_(mean=0.0, std=1.0)
# 验证布局(调试用,保留断言)
assert tensor.shape == resolved_shape, f"生成Tensor形状({tensor.shape})与预期({resolved_shape})不匹配"
assert tensor.stride() == resolved_stride, f"生成Tensor stride({tensor.stride()})与预期({resolved_stride})不匹配"
return tensor
def _prepare_node_inputs(self, node: Node, seq_len: int) -> Tuple[Tuple, Dict]:
# 处理位置参数与关键字参数
args = [self._process_arg(arg, seq_len) for arg in node.args]
kwargs = {key: self._process_arg(value, seq_len) for key, value in node.kwargs.items()}
return tuple(args), kwargs
def _process_arg(self, arg: Any, seq_len: int) -> Any:
"""递归处理参数,替换节点引用为真实随机Tensor"""
# 处理fx.Node类型参数
if isinstance(arg, Node):
input_node = arg
example_value = arg.meta.get("example_value")
assert example_value is not None, f"无法获取节点 {input_node.name} 的元信息"
# 处理标量与SymInt类型
if isinstance(example_value, (int, float, str, bool)):
return example_value
if isinstance(example_value, torch.SymInt):
return self._resolve_symint_expression(example_value, seq_len)
if isinstance(example_value, torch.nn.Parameter):
assert input_node.name in self.node_param_ref_dict, f"无法找到节点 {input_node.name} 对应的参数"
res = self.node_param_ref_dict[input_node.name]()
assert res is not None, f"节点 {input_node.name} 对应的参数已被释放"
return res
# 处理Tensor/FakeTensor类型
if isinstance(example_value, (torch.Tensor, FakeTensor)):
shape = example_value.shape
dtype = example_value.dtype
device = example_value.device
stride = example_value.stride()
# 生成真实Tensor并返回
assert shape is not None, f"无法获取节点 {input_node.name} 的输出形状"
return self._generate_real_tensor(shape, stride, dtype, device, seq_len)
# 嵌套结构递归处理
elif isinstance(arg, (list, tuple)):
return type(arg)(self._process_arg(item, seq_len) for item in arg)
# 其他类型直接返回
else:
return arg
def run_batch_profile(self):
"""批量执行节点性能分析"""
# 打印批量分析核心配置
magi_logger.info(f"===== 开始批量性能分析 =====", rank=0)
magi_logger.info(
f"测试SeqLen: {self.seqlen_samples} | SM粒度: {self.sm_samples} | 节点总数: {len(list(self.graph_module.graph.nodes))}",
rank=0,
)
# 初始化seq_len结果存储
for seq_len in self.seqlen_samples:
if seq_len not in self.profile_results:
self.profile_results[seq_len] = {}
# 遍历所有节点进行测试
for node_idx, node in enumerate(self.graph_module.graph.nodes):
node_name = node.name
node_op = node.op
node_perf = None
skip_record = False
if node_op in ["placeholder", "output"]:
skip_record = True
elif node_op == "call_function" and any(k in str(node.target) for k in ["getitem", "list"]):
skip_record = True
if skip_record:
for seq_len in self.seqlen_samples:
self.profile_results[seq_len][node_name] = {sm: MIN_LATENCY_MS for sm in self.sm_samples} # 5ns
magi_logger.info(f"📌 跳过非计算节点: {node_name} ({node_op})", rank=0)
self.noncompute_node_names.add(node_name)
continue
# 匹配重复节点名称(b\d+s\d+_xxx)
elif re.match(r"b\d+s\d+_.*", node_name) and node_perf is None:
base_name = re.sub(r"^b\d+s\d+_", "", node_name)
for seq_len in self.seqlen_samples:
if base_name in self.profile_results[seq_len]:
node_perf = self.profile_results[seq_len][base_name]
self.profile_results[seq_len][node_name] = node_perf
skip_record = True
if skip_record:
magi_logger.info(f"📌 重用节点性能: {node_name} ({node_op}) -> 基准节点: {base_name}", rank=0)
continue
# 遍历所有SeqLen进行节点测试
for seq_len in self.seqlen_samples:
magi_logger.info(f"--- Profile 节点 {node_name} (SeqLen={seq_len}) ---", rank=0)
try:
# 准备输入并执行性能分析
real_args, real_kwargs = self._prepare_node_inputs(node, seq_len)
node_perf = self._profile_node_with_data(node=node, args=real_args, kwargs=real_kwargs)
# 释放内存并保存结果
del real_args, real_kwargs
assert isinstance(node_perf, dict), f"节点性能结果格式错误"
self.profile_results[seq_len][node_name] = node_perf
torch.cuda.empty_cache()
torch.cuda.synchronize()
except Exception as e:
magi_logger.info(f"⚠️ 节点 {node_name} (SeqLen={seq_len}) 测试失败: {str(e)=}", rank=0)
import traceback
traceback.print_exc()
self.profile_results[seq_len][node_name] = {sm: -1.0 for sm in self.sm_samples}
torch.cuda.empty_cache()
raise e
self._incremental_save_results()
# 打印汇总结果
magi_logger.info(f"===== 批量性能分析完成 =====", rank=0)
# 打印各SeqLen节点性能汇总
for seq_len in self.seqlen_samples:
magi_logger.info(f"===== SeqLen={seq_len} 性能汇总(单位:ms) =====", rank=0)
header = f"{'节点名称':<20}" + "".join([f"SM={sm:<10}" for sm in self.sm_samples])
magi_logger.info(header, rank=0)
magi_logger.info("-" * len(header), rank=0)
for node in self.graph_module.graph.nodes:
if node.name not in self.profile_results[seq_len]:
continue
if node.name in self.noncompute_node_names:
continue
perf_dict = self.profile_results[seq_len].get(node.name, {})
assert isinstance(perf_dict, dict), f"节点性能结果格式错误"
row = f"{node.name:<20}" + "".join([f"{perf_dict.get(sm, -1.0):<10.4f}" for sm in self.sm_samples])
magi_logger.info(row, rank=0)
self.profiled = True
def _profile_node_with_data(
self, node: Node, args: Tuple, kwargs: Dict, warmup_steps: int = 30, test_steps: int = 30
) -> Dict[int, float]:
"""单节点性能测试(指定输入数据)"""
node_perf = {}
node_name = node.name
node_op = node.op
# 遍历所有SM数量进行测试
for sm in self.sm_samples:
try:
stream = self.green_stream_pool.get_stream(sm)
# 节点执行函数定义
def execute_node_once():
with torch.cuda.stream(stream):
if node_op == FX_NODE_OP.CALL_FUNCTION.value:
return node.target(*args, **kwargs)
elif node_op == FX_NODE_OP.CALL_METHOD.value:
obj = args[0]
return getattr(obj, node.target)(*args[1:], **kwargs)
elif node_op == FX_NODE_OP.CALL_MODULE.value:
submod = self.graph_module
for mod_name in node.target.split("."):
submod = getattr(submod, mod_name)
return submod(*args, **kwargs)
else:
raise NotImplementedError(f"不支持的节点类型: {node_op}")
# 预热与计时
torch.cuda.synchronize()
for _ in range(warmup_steps):
execute_node_once()
torch.cuda.synchronize()
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record(stream)
for _ in range(test_steps):
execute_node_once()
end_event.record(stream)
torch.cuda.synchronize()
# 计算平均延迟
total_time = start_event.elapsed_time(end_event)
node_perf[sm] = total_time / test_steps
except Exception as e:
magi_logger.info(f"⚠️ 节点 {node_name} (SM={sm}) 测试失败: {str(e)=}", rank=0)
node_perf[sm] = -1.0
torch.cuda.empty_cache()
torch.cuda.synchronize()
return node_perf
def _incremental_save_results(self):
"""增量保存性能结果并生成热力图"""
# 创建保存目录
os.makedirs(self.save_base_path, exist_ok=True)
# 增量保存JSON结果
json_path = os.path.join(self.save_base_path, "profile_results.json")
existing_results = {}
if os.path.exists(json_path):
with open(json_path, "r", encoding="utf-8") as f:
existing_results = json.load(f)
existing_results = {int(k): v for k, v in existing_results.items()}
# 格式化新数据并合并
new_data = {
int(seq_len): {
node_name: {int(sm): float(latency) for sm, latency in sm_data.items()}
for node_name, sm_data in node_data.items()
if sm_data
}
for seq_len, node_data in self.profile_results.items()
}
existing_results.update(new_data)
# 写入JSON文件
with open(json_path, "w", encoding="utf-8") as f:
json.dump(existing_results, f, indent=4, ensure_ascii=False)
magi_logger.info(f"📁 性能结果已保存至: {json_path}", rank=0)
# 生成热力图(有有效数据时)
if len(existing_results) > 0 and all(len(v) > 0 for v in existing_results.values()):
self._generate_heatmaps(existing_results)
def _generate_heatmaps(self, results: Dict[int, Dict[str, Dict[int, float]]]):
"""生成节点性能热力图"""
# 配置绘图参数
plt.rcParams.update(
{
'font.sans-serif': ['DejaVu Sans', 'Arial', 'Helvetica'],
'axes.unicode_minus': False,
'font.family': 'sans-serif',
'figure.dpi': 300,
'savefig.dpi': 300,
}
)
magi_logger.info(f"📊 开始生成热力图(有效SeqLen: {list(results.keys())})", rank=0)
# 提取所有唯一数据
all_seq_lens = sorted([int(k) for k in results.keys()])
all_sms = sorted(
{int(sm) for seq_data in results.values() for node_data in seq_data.values() for sm in node_data.keys()}
)
all_nodes = sorted({node for seq_data in results.values() for node in seq_data.keys()})
# 为每个节点生成热力图
for node_name in all_nodes:
if node_name in self.noncompute_node_names:
continue
# 构建数据矩阵
data_matrix = np.array(
[[results.get(seq_len, {}).get(node_name, {}).get(sm, -1.0) for sm in all_sms] for seq_len in all_seq_lens]
)
# 绘制并保存热力图
plt.figure(figsize=(12, 8))
sns.heatmap(
data_matrix,
annot=True,
fmt=".4f",
cmap="RdYlBu_r",
xticklabels=all_sms,
yticklabels=all_seq_lens,
cbar_kws={"label": "Latency (ms)"},
mask=(data_matrix < 0),
annot_kws={"size": 8},
)
plt.title(f"Node {node_name} Latency Heatmap", fontsize=14, pad=20)
plt.xlabel("SM Count", fontsize=12)
plt.ylabel("Sequence Length", fontsize=12)
plt.tight_layout()
img_path = os.path.join(self.save_base_path, f"{node_name}_latency_heatmap.png")
plt.savefig(img_path, dpi=300, bbox_inches="tight", facecolor='white', edgecolor='none')
plt.close()
magi_logger.info(f"📊 热力图生成完成", rank=0)
def record_node_param_refs(self, real_args: Tuple):
"""记录节点参数引用,供后续输入数据生成使用"""
for node in self.graph_module.graph.nodes:
if node.op == "placeholder":
input_idx = list(self.graph_module.graph.nodes).index(node)
if input_idx < len(real_args):
arg_value = real_args[input_idx]
if isinstance(arg_value, torch.nn.Parameter):
self.node_param_ref_dict[node.name] = weakref.ref(arg_value)
def __call__(self, *args, **kwargs):
"""类可调用入口,未分析则先执行批量分析"""
if not self.profiled:
self.record_node_param_refs(args)
self.run_batch_profile()
return self.graph_module(*args, **kwargs)
def gen_profile_wrap_func(graph_module: GraphModule) -> GraphProfileWrapper:
"""生成性能分析封装实例"""
return GraphProfileWrapper(graph_module)