Files
TermiNexus df8e301720 chore: 初始化 ViperApps 仓库
纳入 11 个示例工程(不含 ViperOS,另行开专题)的源码:已完成新语法迁移(R1-R16 机械规则与 R1/R12 改写)。.gitignore 排除 ViperOS/、各工程编译产物(.tpv_build/.native_build/output/temp)、二进制与 ViperMNIST/data 数据集。
2026-10-07 08:11:49 +08:00

512 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
plot_bench.py - 读 run_bench.py 导出的 CSV, 生成 4 张对比图(无需重跑基准)
用法:
python plot_bench.py # 自动取最新的 bench_*.csv
python plot_bench.py bench_20260910_2031.csv
python plot_bench.py bench_xxx.csv --metric cpu
python plot_bench.py bench_xxx.csv --out-dir figs --prefix run1
产出(4 张 PNG, 默认与 CSV 同目录, 前缀 = CSV 文件名):
<前缀>_1_simd_vs_scalar.png 左: ADD/MUL/SQRT 的 原生 vs SIMD 延迟对比(柱状)
右: 各操作"比原生快多少倍"(柱状 + xN.NN 标注)
<前缀>_2_exec_time.png 各实现 Execution Time 折线(叠加每次 run 的实测点)
<前缀>_3_overall_mean.png 各实现 Overall Mean Execution Time 对比
<前缀>_4_gflops.png 各实现 Measured GFLOPS 吞吐对比
统计口径与 run_bench.py 报告一致: CSV 里 trimmed=1 的样本(被离群裁剪剔除)不计入。
图1 需要 CSV 里同时有 ADD/MUL/SQRT 的 原生+SIMD 成对数据, 例如:
python run_bench.py --impl viper_add,viper_scalar_add,viper_mul,viper_scalar_mul,viper_sqrt,viper_scalar_sqrt
缺哪一对就少画哪一根, 全缺则整张图跳过并提示。
"""
import argparse
import csv
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg") # 无 GUI 后端, 只存文件
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
# 图表标题/坐标轴都是中文: 必须显式指定带 CJK 字形的字体, 否则全是方块
matplotlib.rcParams["font.sans-serif"] = [
"Microsoft YaHei", "SimHei", "Microsoft JhengHei", "DejaVu Sans"]
matplotlib.rcParams["axes.unicode_minus"] = False
ROOT = Path(__file__).resolve().parent
# 实现名 -> (显示名, 颜色), 固定顺序便于多次运行配色一致
# 显示名刻意短: 图内文字统一英文, 图例太长会把绘图区挤扁
IMPL_STYLE = [
("viper", "Viper SIMD", "#d62728"),
("viper_scalar", "Viper Scalar", "#ff7f0e"),
("c", "C (-O2 -mavx2 -mfma)", "#1f77b4"),
("cpp", "C++ (std::vector)", "#2ca02c"),
("py", "Python + numpy", "#9467bd"),
]
# 图1: 三种操作 -> (显示名, SIMD 实现名, 原生实现名)。
# 每一对都是同一个 app.exe 的两个 mode, 除"是否向量化"外无任何差异
# (见 App/main.vp 的 KERNEL_* 与 run_bench.py 的 VIPER_KERNELS)。
KERNEL_PAIRS = [
("ADD", "viper_add", "viper_scalar_add"),
("MUL", "viper_mul", "viper_scalar_mul"),
("SQRT", "viper_sqrt", "viper_scalar_sqrt"),
]
# 图1 配色: 原生(标量) vs SIMD
COL_NATIVE = "#8c8c8c"
COL_SIMD = "#d62728"
def style_for(impl):
"""实现名 -> (显示名, 颜色); 未登记的实现给灰色, 不静默改名。"""
for name, disp, color in IMPL_STYLE:
if name == impl:
return disp, color
return impl, "#7f7f7f"
def metric_txt(metric):
"""耗时口径 -> 英文短名(图内文字一律用英文, 避免中文长句撑爆标题)。"""
return "wall clock" if metric == "wall" else "thread CPU"
def human_kib(size):
"""LEN(元素数, double) -> 工作集描述(四份缓冲共 LEN*32 字节)"""
kib = size * 32 / 1024.0
if kib >= 1024:
return f"{size}\n({kib / 1024:.0f} MiB)"
return f"{size}\n({kib:.0f} KiB)"
def load_csv(path):
"""读 CSV -> {(impl, len): {"wall":[...], "cpu":[...], "iters":n, "trimmed":n,
"groups": {组号: {"wall":[...], "cpu":[...]}}}}
注意 iters 要留着: 每个尺寸的 LEN×ITERS×2 = 该尺寸下单 run 的总运算量,
是 GFLOPS 换算的唯一依据(py 纯解释器回退会降规模, iters 与其它实现不同)。
groups 供"只有一个 LEN 时改用组号当横轴"用(见 fig_exec_time)。
"""
data = {}
with open(path, "r", newline="", encoding="utf-8-sig") as f:
for row in csv.DictReader(f):
key = (row["impl"], int(row["len"]))
rec = data.setdefault(key, {"wall": [], "cpu": [], "iters": int(row["iters"]),
"trimmed": 0, "groups": {}})
if int(row["trimmed"]) == 1:
rec["trimmed"] += 1
continue
wv = float(row["wall_us"])
cv = float(row["cpu_us"])
rec["wall"].append(wv)
rec["cpu"].append(cv)
grp = rec["groups"].setdefault(int(row["group"]), {"wall": [], "cpu": []})
grp["wall"].append(wv)
grp["cpu"].append(cv)
if int(row["iters"]) != rec["iters"]:
rec["iters"] = int(row["iters"]) # 防御: 同名 key 的 iters 应一致
return data
def series(rec, metric):
"""取某实现某尺寸在指定口径下的有效样本(计时值必须 > 0)。"""
v = np.asarray(rec[metric], dtype=float)
return v[np.isfinite(v) & (v > 0.0)]
def annotate_bars(ax, xs, ys, spec=".1f", dy_ratio=0.02, fontsize=9, color="black"):
"""在柱顶标数值(xs/ys 为等长序列)。
spec 用 format() 的格式说明符(如 ".1f" / ",.0f"), 不用 % 风格 ——
Python 的 % 格式化不支持千分位(%,.0f 会直接抛 ValueError)。
"""
span = (max(ys) - min(ys)) if len(ys) > 1 else (abs(ys[0]) if ys else 1.0)
dy = (span if span > 0 else 1.0) * dy_ratio
for x, y in zip(xs, ys):
ax.text(x, y + dy, format(y, spec), ha="center", va="bottom",
fontsize=fontsize, color=color)
# ------------------------------------------------------------
# 图1: ViperSIMD - ADD / MUL / SQRT 的 原生 vs SIMD(左右双面板)
# ------------------------------------------------------------
def fig_simd_vs_scalar(data, sizes, args, metric, out_path):
"""左面板 = 每种操作(ADD/MUL/SQRT)的 原生 vs SIMD 延迟柱状对比;
右面板 = 对应的 SIMD 加速比(原生延迟 / SIMD 延迟)。
只取一个 LEN(CSV 里最小的那个): 加速比是"指令级/微架构"特性, 在能驻留私有
L2 的小尺寸上测得最干净; 想看跨尺寸的访存行为请用图2。
"""
if not sizes:
print("[图1] 跳过: CSV 里没有样本")
return False
use_size = sizes[0]
rows = [] # (op, native_us, simd_us)
for op, simd_impl, native_impl in KERNEL_PAIRS:
rs = data.get((simd_impl, use_size))
rn = data.get((native_impl, use_size))
if not rs or not rn:
continue
vs = series(rs, metric)
vn = series(rn, metric)
if vs.size == 0 or vn.size == 0:
continue
rows.append((op, float(vn.mean()), float(vs.mean())))
if not rows:
need = ",".join(f"{a},{b}" for _, a, b in KERNEL_PAIRS)
print("[图1] 跳过: CSV 里没有 ADD/MUL/SQRT 的 原生+SIMD 成对数据")
print(f" 该 CSV 实际包含: {sorted({k[0] for k in data})}")
print(" 直接重跑一次即可(run_bench.py 默认已含这些内核变体): python run_bench.py")
print(f" 或只补跑内核变体: --impl {need}")
return False
ops = [r[0] for r in rows]
native_us = [r[1] for r in rows]
simd_us = [r[2] for r in rows]
speedup = [(n / s if s > 0 else float("nan")) for n, s in zip(native_us, simd_us)]
best = max(speedup)
fig, (axL, axR) = plt.subplots(1, 2, figsize=(13.5, 6.4))
x = np.arange(len(ops), dtype=float)
w = 0.36
# ---- 左: 延迟对比(每组内 左=原生 / 右=SIMD) ----
axL.bar(x - w / 2, native_us, w, color=COL_NATIVE, alpha=0.9,
label="Native (scalar)")
axL.bar(x + w / 2, simd_us, w, color=COL_SIMD, alpha=0.9,
label="ViperSIMD (AVX2 x4)")
axL.set_xticks(x)
axL.set_xticklabels(ops, fontsize=11)
axL.set_xlabel("Operation", fontsize=10)
axL.set_ylabel(f"Latency per Run (us, {metric_txt(metric)})", fontsize=10)
axL.set_title(f"Latency: Native vs ViperSIMD\nLEN={use_size}", fontsize=12)
axL.grid(alpha=0.3, axis="y")
axL.legend(fontsize=10)
hi = max(native_us + simd_us)
for xi, y in zip(x - w / 2, native_us):
axL.text(xi, y + hi * 0.01, f"{y:,.0f}", ha="center", va="bottom", fontsize=9)
for xi, y in zip(x + w / 2, simd_us):
axL.text(xi, y + hi * 0.01, f"{y:,.0f}", ha="center", va="bottom", fontsize=9)
# ---- 右: 比原生快多少倍 ----
axR.bar(x, speedup, 0.5, color=COL_SIMD, alpha=0.9)
axR.axhline(1.0, color="#555555", lw=1.2, ls="--")
axR.text(len(ops) - 0.55, 1.0, "x1 = no gain", va="bottom", ha="right",
fontsize=9, color="#555555")
axR.set_xticks(x)
axR.set_xticklabels(ops, fontsize=11)
axR.set_xlabel("Operation", fontsize=10)
axR.set_ylabel("Speed-up (native latency / ViperSIMD latency)", fontsize=10)
axR.set_title("SIMD Speed-up over Native", fontsize=12)
axR.grid(alpha=0.3, axis="y")
axR.set_ylim(0, best * 1.30 if best > 0 else 2.0)
for xi, sp in zip(x, speedup):
axR.text(xi, sp + best * 0.02, f"x{sp:.2f}", ha="center", va="bottom",
fontsize=13, fontweight="bold")
fig.suptitle(f"{args.csv_name}\n"
f"bar = mean of all runs · metric = {metric_txt(metric)}"
f" · trimmed outliers excluded"
f" · latency: lower is better", fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.90))
fig.savefig(out_path, dpi=args.dpi)
plt.close(fig)
print(f"[图1] Native vs SIMD 已保存: {out_path}")
return True
# ------------------------------------------------------------
# 图2: 各实现 Execution Time 折线 + 每次 run 的实测点
# ------------------------------------------------------------
def fig_exec_time(data, sizes, impls, args, metric, out_path):
# 只有一个 LEN 时横轴没东西可展开(折线会退化成孤点), 改用"组号"当横轴:
# 每组 = 一次进程内多次 run, 于是单尺寸下依然能看到折线与组间离散。
single = len(sizes) == 1
if single:
xkeys = sorted({g for rec in data.values() for g in rec["groups"]})
xlabel = f"Group Index (LEN={sizes[0]}; one group = one process run)"
xticklabels = [str(g) for g in xkeys]
note = ("line = group mean, dots = individual runs\n"
f"single LEN={sizes[0]}: X axis = group index")
else:
xkeys = list(sizes)
xlabel = "Working Set a/b/c/out (LEN)"
xticklabels = [human_kib(s) for s in sizes]
note = "line = per-size mean, dots = individual runs"
def samples_at(rec, xi):
"""第 xi 个横轴位置上的有效样本(计时值必须 > 0)。"""
raw = rec["groups"].get(xkeys[xi]) if single else rec
if not raw:
return np.array([])
v = np.asarray(raw[metric], dtype=float)
return v[np.isfinite(v) & (v > 0.0)]
def rec_of(impl, xi):
return data.get((impl, sizes[0])) if single else data.get((impl, xkeys[xi]))
fig, ax = plt.subplots(figsize=(12.5, 6.8))
x = np.arange(len(xkeys), dtype=float)
n = max(1, len(impls))
allv = []
for k, impl in enumerate(impls):
disp, color = style_for(impl)
off = (k - (n - 1) / 2.0) * 0.11 # 各实现的散点横向错开, 避免互相压盖
xs, ys = [], []
for i in range(len(xkeys)):
rec = rec_of(impl, i)
if not rec:
continue
v = samples_at(rec, i)
if v.size == 0:
continue
xs.append(x[i])
ys.append(float(v.mean()))
ax.scatter(np.full(v.shape, x[i] + off), v, s=16, color=color,
alpha=0.35, linewidths=0, zorder=2)
allv.extend(v.tolist())
if xs:
ax.plot(xs, ys, marker="o", ms=8, lw=2.2, color=color,
label=f"{disp} (mean)", zorder=3)
ax.set_xticks(x)
ax.set_xticklabels(xticklabels, fontsize=9)
ax.set_xlabel(xlabel, fontsize=10)
ax.set_ylabel(f"Execution Time per Run (us, {metric_txt(metric)})", fontsize=10)
ax.set_title("Execution Time Comparison\n" + note, fontsize=12)
# 动态范围很大(如 numpy 与 SIMD 差百倍)时自动取对数, 否则小值被压成一条线
use_log = args.y_scale == "log"
if args.y_scale == "auto" and allv:
lo, hi = min(allv), max(allv)
use_log = lo > 0 and (hi / lo) > 30.0
if use_log:
ax.set_yscale("log")
ax.set_axisbelow(True)
ax.grid(alpha=0.3, which="both")
if ax.get_legend_handles_labels()[0]:
ax.legend(fontsize=9.5, ncol=2)
fig.suptitle(f"{args.csv_name} · y-axis: {'log' if use_log else 'linear'}"
f" · trimmed outliers excluded"
f" · implementations offset horizontally", fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.94))
fig.savefig(out_path, dpi=args.dpi)
plt.close(fig)
print(f"[图2] Execution Time 折线已保存: {out_path}")
return True
# ------------------------------------------------------------
# 图3: Overall Mean Execution Time 对比
# ------------------------------------------------------------
def fig_overall_mean(data, sizes, impls, args, metric, out_path):
labels, means, stds, colors = [], [], [], []
for impl in impls:
vals = [series(data[(impl, s)], metric) for s in sizes if (impl, s) in data]
vals = [v for v in vals if v.size]
if not vals:
continue
v = np.concatenate(vals)
disp, color = style_for(impl)
labels.append(disp)
means.append(float(v.mean()))
stds.append(float(v.std(ddof=1)) if v.size > 1 else 0.0)
colors.append(color)
if not labels:
print("[图3] 跳过: 没有有效样本")
return False
fig, ax = plt.subplots(figsize=(11.0, 6.4))
xs = np.arange(len(labels), dtype=float)
ax.bar(xs, means, 0.6, color=colors, alpha=0.9,
yerr=stds, capsize=6, ecolor="#333333")
ax.set_xticks(xs)
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel(f"Mean Execution Time per Run (us, {metric_txt(metric)})", fontsize=10)
ax.set_title("Overall Mean Execution Time\n"
"all valid samples pooled · error bar = std dev"
" · in-bar xN.NN = relative to fastest", fontsize=12)
ax.grid(alpha=0.3, axis="y")
annotate_bars(ax, xs, means, spec=",.0f" if max(means) > 10000 else ".1f")
# 柱身只标"相对最快者的倍数"(越小越快); 最快的那根直接标"最快"
fastest = min(means)
for x, y in zip(xs, means):
mult = y / fastest
ax.text(x, y * 0.55, "fastest" if mult <= 1.0001 else f"x{mult:.2f}",
ha="center", va="center", fontsize=9.5, color="white", fontweight="bold")
fig.suptitle(f"{args.csv_name}\n"
f"per-run work is constant (LEN×ITERS×2),"
f" so absolute times are comparable", fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.90))
fig.savefig(out_path, dpi=args.dpi)
plt.close(fig)
print(f"[图3] Overall Mean Execution Time 已保存: {out_path}")
return True
# ------------------------------------------------------------
# 图4: Measured GFLOPS 吞吐对比
# ------------------------------------------------------------
def fig_gflops(data, sizes, impls, args, metric, out_path):
labels, means, stds, colors = [], [], [], []
for impl in impls:
per_run = []
for s in sizes:
rec = data.get((impl, s))
if not rec:
continue
v = series(rec, metric)
if v.size == 0:
continue
# 该尺寸单 run 的总运算量 = LEN × ITERS × 2 (读 CSV 的 iters, 兼容降规模)
flops_run = float(s) * float(rec["iters"]) * 2.0
per_run.append(flops_run / (v * 1e-6) / 1e9) # 每次 run 的 GFLOPS
if not per_run:
continue
v = np.concatenate(per_run)
disp, color = style_for(impl)
labels.append(disp)
means.append(float(v.mean()))
stds.append(float(v.std(ddof=1)) if v.size > 1 else 0.0)
colors.append(color)
if not labels:
print("[图4] 跳过: 没有有效样本")
return False
fig, ax = plt.subplots(figsize=(11.0, 6.4))
xs = np.arange(len(labels), dtype=float)
ax.bar(xs, means, 0.6, color=colors, alpha=0.9,
yerr=stds, capsize=6, ecolor="#333333")
ax.set_xticks(xs)
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel(f"Throughput (GFLOPS, {metric_txt(metric)})", fontsize=10)
ax.set_title("Measured GFLOPS Throughput\n"
"GFLOPS = LEN×ITERS×2 / per-run seconds · error bar = std dev"
" · in-bar xN.NN = relative to fastest", fontsize=12)
ax.grid(alpha=0.3, axis="y")
annotate_bars(ax, xs, means, spec=".2f")
# 柱身只标"相对最快者的倍数"(越大越好); 最快的那根直接标"最快"
fastest = max(means)
for x, y in zip(xs, means):
mult = y / fastest
ax.text(x, y * 0.55, "fastest" if mult >= 0.9999 else f"x{mult:.2f}",
ha="center", va="center", fontsize=9.5, color="white", fontweight="bold")
fig.suptitle(f"{args.csv_name}\n"
f"higher is better · GFLOPS computed from each run's own CFG",
fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.90))
fig.savefig(out_path, dpi=args.dpi)
plt.close(fig)
print(f"[图4] GFLOPS 吞吐对比已保存: {out_path}")
return True
def main(argv=None):
p = argparse.ArgumentParser(description="把 run_bench.py 的 CSV 画成 4 张对比图")
p.add_argument("csv", nargs="?", default=None, help="CSV 路径(默认取最新 bench_*.csv)")
p.add_argument("--metric", default="wall", choices=["wall", "cpu"],
help="耗时口径(默认 wall=QPC 墙钟; cpu=QueryThreadCycleTime)")
p.add_argument("--out-dir", default=None, help="输出目录(默认与 CSV 同目录)")
p.add_argument("--prefix", default=None, help="输出文件名前缀(默认 CSV 文件名)")
p.add_argument("--out", default=None,
help="兼容旧用法: 传 .png 路径时, 取其所在目录与文件名作为输出目录/前缀")
p.add_argument("--y-scale", default="auto", choices=["auto", "log", "linear"],
help="图2 纵轴刻度(默认 auto: 动态范围 >30 倍自动取对数)")
p.add_argument("--dpi", type=int, default=130, help="输出 PNG 分辨率(默认 130)")
args = p.parse_args(argv)
# ---- 定位 CSV ----
if args.csv:
csv_path = Path(args.csv)
if not csv_path.is_absolute():
csv_path = ROOT / csv_path
else:
cands = sorted(ROOT.glob("bench_*.csv"), key=lambda q: q.stat().st_mtime)
if not cands:
print("找不到 bench_*.csv; 请先跑 run_bench.py(默认会自动导出 CSV)",
file=sys.stderr)
return 2
csv_path = cands[-1]
if not csv_path.exists():
print(f"CSV 不存在: {csv_path}", file=sys.stderr)
return 2
# ---- 定位输出目录 / 文件名前缀 ----
out_dir = csv_path.parent
prefix = csv_path.stem
if args.out: # 旧用法: --out some.png
op = Path(args.out)
out_dir = op.parent if str(op.parent) not in ("", ".") else ROOT
if op.suffix.lower() == ".png":
prefix = op.stem
if args.out_dir:
out_dir = Path(args.out_dir)
if not out_dir.is_absolute():
out_dir = ROOT / out_dir
if args.prefix:
prefix = args.prefix
out_dir.mkdir(parents=True, exist_ok=True)
args.csv_name = csv_path.name
data = load_csv(csv_path)
if not data:
print(f"CSV 里没有有效样本: {csv_path}", file=sys.stderr)
return 2
sizes = sorted({k[1] for k in data})
# ADD/MUL/SQRT 的内核变体只在图1 出现 —— 混进图2/3/4 会把曲线糊成一团
kernel_impls = {n for _, simd_n, native_n in KERNEL_PAIRS for n in (simd_n, native_n)}
known = [name for name, _, _ in IMPL_STYLE]
impls = [n for n in known if any(k[0] == n for k in data)]
impls += sorted({k[0] for k in data} - set(known) - kernel_impls) # 未登记的实现也不丢
print(f"CSV : {csv_path}")
print(f"尺寸 : {sizes}")
print(f"实现 : {impls}")
print(f"口径 : {args.metric} 输出目录: {out_dir} 前缀: {prefix}")
made = []
if fig_simd_vs_scalar(data, sizes, args, args.metric,
out_dir / f"{prefix}_1_simd_vs_scalar.png"):
made.append(1)
if fig_exec_time(data, sizes, impls, args, args.metric,
out_dir / f"{prefix}_2_exec_time.png"):
made.append(2)
if fig_overall_mean(data, sizes, impls, args, args.metric,
out_dir / f"{prefix}_3_overall_mean.png"):
made.append(3)
if fig_gflops(data, sizes, impls, args, args.metric,
out_dir / f"{prefix}_4_gflops.png"):
made.append(4)
# ---- 控制台再来一份文字小结, 免得每次都要开图 ----
print(f"\n{'impl':<13} {'LEN':>9} {'工作集KiB':>10} {'每run us':>11} "
f"{'GFLOPS':>8} {'CPU/墙钟':>9} {'trimmed':>8}")
for s in sizes:
for impl in impls:
rec = data.get((impl, s))
if not rec:
continue
wv = series(rec, "wall")
cv = series(rec, "cpu")
if wv.size == 0:
continue
flops_run = float(s) * float(rec["iters"]) * 2.0
g = flops_run / (wv * 1e-6) / 1e9
ratio = float(np.median(cv / wv[:cv.size])) if cv.size == wv.size else float("nan")
print(f"{impl:<13} {s:>9} {s * 32 / 1024:>10.1f} {np.median(wv):>11.1f} "
f"{np.mean(g):>8.2f} {ratio:>9.4f} {rec['trimmed']:>8}")
print(f"\n共生成 {len(made)} 张图: {made}")
return 0 if made else 1
if __name__ == "__main__":
sys.exit(main())