import pandas as pd import matplotlib.pyplot as plt import matplotlib.cm as cm import numpy as np import argparse import json import os plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False parser = argparse.ArgumentParser(description='绘制模型性能对比图表') parser.add_argument('--配置', '-f', type=str, default='data_config.json', help='数据配置文件路径') parser.add_argument('--输出目录', '-d', type=str, default='charts', help='输出图表目录') parser.add_argument('--合并分组', '-m', action='store_true', help='将第一层分组合并到一张图中') args = parser.parse_args() def load_data_from_files(config): all_data = [] files_config = config.get('files', []) for file_config in files_config: file_path = file_config.get('file') sheets = file_config.get('sheets', []) column_mapping = file_config.get('column_mapping', {}) if not os.path.exists(file_path): print(f"文件不存在: {file_path}, 跳过") continue xl = pd.ExcelFile(file_path) if sheets is None or (isinstance(sheets, list) and len(sheets) == 0): sheets = xl.sheet_names else: sheets = [s for s in sheets if s] for sheet in sheets: try: df = pd.read_excel(file_path, sheet_name=sheet) df.columns = df.columns.str.replace('\n', '').str.strip() if column_mapping: df = df.rename(columns=column_mapping) column_replace = file_config.get('column_replace', {}) for col, replace_dict in column_replace.items(): if col in df.columns: df[col] = df[col].replace(replace_dict) df['source_file'] = file_path df['source_sheet'] = sheet all_data.append(df) print(f"读取: {file_path} - {sheet}, {len(df)} 行") except Exception as e: print(f"读取失败: {file_path} - {sheet}: {e}") if not all_data: return pd.DataFrame() combined_df = pd.concat(all_data, ignore_index=True) return combined_df def apply_filter(df, filter_dict): for filter_col, filter_values in filter_dict.items(): if filter_col in df.columns and filter_values: if isinstance(filter_values, list): df = df[df[filter_col].isin(filter_values)] else: df = df[df[filter_col] == filter_values] return df def generate_chart(df_subset, output_path, colkey, outer_group_cols, inner_group_cols, metric_cols, merge_groups=False): df_subset = df_subset.copy() compare_col = "ColKey" df_subset[compare_col] = df_subset[colkey].apply(lambda x: '_'.join(x.dropna().astype(str)), axis=1) #df_subset[compare_col] = df_subset['vLLM版本'].astype(str) + '_' + df_subset['V0/V1 Engine'].astype(str) all_group_cols = outer_group_cols + inner_group_cols if all_group_cols: df_grouped = df_subset[all_group_cols + [compare_col] + metric_cols].groupby(all_group_cols + [compare_col]).mean().reset_index() else: df_grouped = df_subset[[compare_col] + metric_cols].groupby([compare_col]).mean().reset_index() df_grouped[compare_col] = df_grouped.index if len(df_grouped) == 0: print(f" 无数据,跳过") return False if outer_group_cols: outer_values = df_grouped.groupby(outer_group_cols).size().reset_index() else: outer_values = pd.DataFrame({'': ['all']}) n_outer = len(outer_values) engine_values = df_grouped[compare_col].unique() n_engines = len(engine_values) color_palette = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D', '#3B1F2B', '#95C623', '#7B2D26'] colors = [color_palette[i % len(color_palette)] for i in range(n_engines)] if merge_groups and n_outer > 1: fig, axes = plt.subplots(1, 4, figsize=(8 * n_outer + 20, 10)) bar_width = 0.12 bar_spacing = 0.05 group_gap = 3 x_labels_all = None for col, metric in enumerate(metric_cols): ax = axes[col] current_x = 0 for row_idx, (_, outer_row) in enumerate(outer_values.iterrows()): df_outer = df_grouped.copy() for gcol in outer_group_cols: df_outer = df_outer[df_outer[gcol] == outer_row[gcol]] outer_label_value = '-'.join([str(outer_row[gcol]) for gcol in outer_group_cols]) pt = df_outer.pivot_table( index=inner_group_cols, columns=compare_col, values=metric ).fillna(0) n_bars_per_group = len(pt) group_width = n_bars_per_group * n_engines * (bar_width + bar_spacing) + group_gap group_center = current_x + group_width / 2 x_labels = ['/'.join([str(v) for v in idx]) for idx in pt.index] if x_labels_all is None: x_labels_all = x_labels x = np.arange(len(x_labels)) * (n_engines * (bar_width + bar_spacing)) + current_x for i, engine in enumerate(engine_values): if engine in pt.columns: values = pt[engine].values offset = i * bar_width label = f"{engine} ({outer_label_value})" bars = ax.bar(x + offset, values, bar_width, label=label, color=colors[i], edgecolor='white', linewidth=0.5) for bar, val in zip(bars, values): if val > 0: y_pos = bar.get_height() + bar.get_height()*0.02 if bar.get_height() > 0 else 1 ax.text(bar.get_x() + bar.get_width()/2, y_pos, f'{val:.1f}', ha='center', va='bottom', fontsize=5, fontweight='bold') ax.axvline(x=current_x + n_bars_per_group * n_engines * (bar_width + bar_spacing) + group_gap/2, color='gray', linestyle='--', linewidth=1) ax.text(group_center, ax.get_ylim()[1] * 0.95, outer_label_value, ha='center', va='top', fontsize=9, fontweight='bold', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) current_x = current_x + n_bars_per_group * n_engines * (bar_width + bar_spacing) + group_gap total_inner_labels = len(x_labels_all) inner_positions = [] inner_labels = [] for gi in range(len(outer_values)): base_x = gi * (total_inner_labels * n_engines * (bar_width + bar_spacing) + group_gap) for xi in range(total_inner_labels): center_pos = base_x + xi * n_engines * (bar_width + bar_spacing) + (n_engines * bar_width + (n_engines-1) * bar_spacing) / 2 inner_positions.append(center_pos) inner_labels.append(x_labels_all[xi]) ax.set_xticks(inner_positions) ax.set_xticklabels(inner_labels, rotation=45, ha='right', fontsize=6) ax.set_xlabel('/'.join(inner_group_cols), fontsize=9) ax.set_ylabel(metric, fontsize=10) ax.set_title(f'{metric}', fontsize=12, fontweight='bold') ax.grid(axis='y', alpha=0.3, linestyle='--') ax.legend(fontsize=5, loc='upper right', framealpha=0.9, ncol=1) else: fig, axes = plt.subplots(n_outer, 4, figsize=(24, 5 * n_outer)) if n_outer == 1: axes = axes.reshape(1, -1) bar_width = 0.2 outer_label = '/'.join(outer_group_cols) if outer_group_cols else '全部' for row_idx, (_, outer_row) in enumerate(outer_values.iterrows()): df_outer = df_grouped.copy() for col in outer_group_cols: df_outer = df_outer[df_outer[col] == outer_row[col]] outer_label_value = '-'.join([str(outer_row[col]) for col in outer_group_cols]) for col, metric in enumerate(metric_cols): ax = axes[row_idx, col] pt = df_outer.pivot_table( index=inner_group_cols, columns=compare_col, values=metric ).fillna(0) x_labels = ['/'.join([str(v) for v in idx]) for idx in pt.index] x = np.arange(len(x_labels)) for i, engine in enumerate(engine_values): if engine in pt.columns: values = pt[engine].values offset = (i - n_engines/2 + 0.5) * bar_width bars = ax.bar(x + offset, values, bar_width, label=engine, color=colors[i], edgecolor='white', linewidth=0.5) for bar, val in zip(bars, values): if val > 0: y_pos = bar.get_height() + bar.get_height()*0.02 if bar.get_height() > 0 else 1 ax.text(bar.get_x() + bar.get_width()/2, y_pos, f'{val:.1f}', ha='center', va='bottom', fontsize=7, fontweight='bold') ax.set_xlabel('/'.join(inner_group_cols), fontsize=9) ax.set_ylabel(metric, fontsize=10) ax.set_title(f'{outer_label}={outer_label_value} - {metric}', fontsize=11, fontweight='bold') ax.set_xticks(x) ax.set_xticklabels(x_labels, rotation=45, ha='right', fontsize=7) ax.grid(axis='y', alpha=0.3, linestyle='--') ax.legend(fontsize=6, loc='upper right', framealpha=0.9, ncol=1) plt.tight_layout() plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white') plt.close() return True print(f"从配置文件加载数据: {args.配置}") with open(args.配置, 'r', encoding='utf-8') as f: config = json.load(f) df = load_data_from_files(config) if df.empty: print("未加载到数据") exit(1) print(f"\n可用列名: {df.columns.tolist()}") col_mapping = {} for std_col, alt_cols in [ ('模型', ['模型', 'model', 'Model']), ('卡类型', ['卡类型', 'card_type', '卡']), ('卡数', ['卡数', 'num_cards', '卡数', 'GPU数量']), ('vLLM版本', ['vLLM版本', 'vllm_version', 'vLLM版本']), ('V0/V1 Engine', ['V0/V1 Engine', 'Engine', 'engine']), ('输入长度(tokens)', ['输入长度(tokens)', 'input_length', 'input length', '输入长度']), ('输出长度(tokens)', ['输出长度(tokens)', 'output_length', 'output length', '输出长度']), ('并发数', ['并发数', 'concurrency', '并发', 'num_concurrent']), ('平均首字延时TTFT(ms)', ['平均首字延时TTFT(ms)', 'ttft', 'TTFT', '首字延时']), ('平均生成时间TPOT(ms)', ['平均生成时间TPOT(ms)', 'tpot', 'TPOT', '生成时间']), ('生成吞吐量(tokens/s)', ['生成吞吐量(tokens/s)', 'gen_throughput', '生成吞吐']), ('总吞吐量(tokens/s)', ['总吞吐量(tokens/s)', 'total_throughput', '总吞吐']) ]: for alt in alt_cols: if alt in df.columns: col_mapping[std_col] = alt print(f"\n列映射: {col_mapping}") df_renamed = df.rename(columns=col_mapping) filter_config = config.get('filter', {}) df_renamed = apply_filter(df_renamed, filter_config) print(f"过滤后数据量: {len(df_renamed)}") metric_cols = [ '平均首字延时TTFT(ms)', '平均生成时间TPOT(ms)', '生成吞吐量(tokens/s)', '总吞吐量(tokens/s)' ] dist_cols_config = config.get('distinguish', ['模型', '卡数']) dist_cols = [col_mapping.get(c, c) for c in dist_cols_config] dist_cols = [c for c in dist_cols if c in df_renamed.columns] os.makedirs(args.输出目录, exist_ok=True) group_by = config.get('group_by', [[], []]) if isinstance(group_by[0], list): outer_group = group_by[0] if len(group_by) > 0 else [] inner_group = group_by[1] if len(group_by) > 1 else [] else: outer_group = [] inner_group = group_by colkey = config.get('colkey', []) if len(colkey) == 0: print(f"column key error") dist_combinations = df_renamed.groupby(dist_cols).size().reset_index() print(f"\n将生成 {len(dist_combinations)} 个图表...") chart_count = 0 for idx, (_, dist_row) in enumerate(dist_combinations.iterrows()): df_subset = df_renamed.copy() for dist_col in dist_cols: df_subset = df_subset[df_subset[dist_col] == dist_row[dist_col]] filter_parts = [] for dist_col in dist_cols: val = dist_row[dist_col] safe_col_name = dist_col.replace('/', '_').replace('\\', '_')[:10] filter_parts.append(f"{safe_col_name}_{val}") output_filename = '_'.join(filter_parts) + ".png" output_path = os.path.join(args.输出目录, output_filename) print(f"[{idx+1}/{len(dist_combinations)}] 生成图表: {output_filename}") for c in metric_cols: df_subset[c] = pd.to_numeric(df_subset[c], errors='coerce').fillna(0) #try: # df_subset[c] = df_subset[c].astype('float64') #except Exception as e: # print(f"数据转换错误, 列名{c}, 错误信息{e}") success = generate_chart(df_subset, output_path, colkey, outer_group, inner_group, metric_cols, args.合并分组) if success: chart_count += 1 print(f"\n完成!共生成 {chart_count} 个图表,保存到目录: {args.输出目录}")