834 lines
40 KiB
Python
834 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
run_bench.py - "同内核多实现测速 + 组间差值核验" 总编排脚本
|
||
(ViperSpeedBenchmark, 2026-09 抗噪强化版)
|
||
|
||
噪声治理设计:
|
||
* 每组(默认 5 组)只启动【一次】子进程: <exe> <runs_per_group=10>。
|
||
该进程内先做 2 轮满负荷热身, 再连续计时并打印 10 行 RESULT ——
|
||
组原始数据来自同进程稳态运行, 消除了旧版"每次 run 一次冷进程"的
|
||
冷页/TLB/启动/频率爬升噪声。
|
||
* 【线程绑定】每个实现在进程入口调 SetThreadAffinityMask 把自身绑到
|
||
第一个可用逻辑核(结果以 AFFINITY <core> 行回报), 消除跨核迁移、
|
||
大小核异构与缓存失效带来的抖动。绑定失败回报 -1, 不致命。
|
||
* 【双计时】每次 run 同时采集:
|
||
wall = QPC 墙钟微秒 (含被抢占/被降频的时间, 对调度敏感)
|
||
cpu = QueryThreadCycleTime 线程 CPU 微秒
|
||
(只计本线程真正执行过的周期, 对抢占/降频
|
||
不敏感; TSC 频率由各实现入口自行标定)
|
||
两种都进 RESULT 行、都做统计、都写 CSV; --metric 选谁当"主指标"决定
|
||
组均值/总体/差值核验与 PASS/FAIL。
|
||
注: 不用 GetThreadTimes —— 它的记账粒度跟随调度时钟节拍(本机实测硬地板
|
||
15.625ms), 几十毫秒的 run 误差 ±40%, 且 Win10 2004+ 起无法用
|
||
timeBeginPeriod 改善。
|
||
* 【离群裁剪】每组先按中位数上界裁掉离群点(默认 保留 <= 中位数×1.2 的样本),
|
||
再算组统计量。裁剪系数由 --trim-ratio 控制, <=0 关闭。
|
||
* 【统计量可选】--stat mean(默认, 裁剪后算术平均) / median / geomean。
|
||
双峰分布(一半样本被降频)时 median 的抗噪能力远强于算术平均。
|
||
|
||
* 【多尺寸扫描】--sizes 接受逗号分隔的 LEN 列表; 只调 LEN, ITERS 自动配平
|
||
(ITERS = 2^30 / LEN) 使单 run 恒为 ≈2.147e9 flops, 于是 GFLOPS 跨尺寸可比。
|
||
LEN 决定工作集(四份缓冲共 LEN*32 字节): 1024=L1 / 16384=L2 / 262144 以上=L3-DRAM。
|
||
* 【自动导 CSV】默认写成带时间戳的 bench_<YYYYmmdd_HHMMSS>.csv (--csv 指定名字,
|
||
--no-csv 关闭), 可直接喂给 plot_bench.py 出图。
|
||
|
||
每个可执行统一协议(进程开头一次):
|
||
CFG <len> <iters>
|
||
AFFINITY <core> # -1 = 绑定失败
|
||
TSC <hz> # 入口标定出的 TSC 频率; 0 = 标定失败
|
||
MODE <0|1> # 仅 Viper 输出: 0=simd_fma_array / 1=scalar_fma_array
|
||
之后每次 run 一行:
|
||
RESULT <wall_us> <cpu_us> <flops>
|
||
全部结束后:
|
||
CHECK <sum>
|
||
|
||
实现集合(全部 Viper 变体共用同一个 app.exe, 只差命令行第 4 个参数 mode):
|
||
viper / viper_scalar FMA (out = a*b + c) mode 0 / 1
|
||
viper_add / viper_scalar_add ADD (out = a + b) mode 2 / 3
|
||
viper_mul / viper_scalar_mul MUL (out = a * b) mode 4 / 5
|
||
viper_sqrt / viper_scalar_sqrt SQRT (out = sqrt(a)) mode 6 / 7
|
||
c / cpp 手写 C/C++, -O2 -mavx2 -mfma -ffp-contract=fast
|
||
py Python numpy(就地 ufunc); 无 numpy 则纯 Python 降规模回退
|
||
每对 `<op>` / `scalar_<op>` 是同一数学内核的"向量化 vs 未向量化"对照 —— 标量版用
|
||
对应标量指令(vaddsd/vmulsd/vsqrtsd/vfmadd213sd), 逐元素结果与 SIMD 版逐位一致,
|
||
CHECK 可直接对照; plot_bench.py 图1 就是用它们算 ADD/MUL/SQRT 的 SIMD 加速比。
|
||
|
||
统计(每实现): 组均值 / 组内 max-min / 组-总体 / 相邻组差 / 总体均值 /
|
||
样本标准差 / 最大偏差 / 裁剪剔除数; 差值核验规则同前(容差默认 0.05)。
|
||
|
||
首组处理: 首组(组1)默认【仅汇报, 不计入】总体均值/样本标准差/差值核验
|
||
(规避首组效应: 第一次进程启动的冷启动/频率爬升)。需要计入加 --include-first-group。
|
||
|
||
用法示例:
|
||
python run_bench.py
|
||
python run_bench.py --impl c,cpp,py --groups 3 --runs-per-group 5
|
||
python run_bench.py --impl viper,c,cpp,py --tol 0.05 --csv out.csv --flops-check
|
||
python run_bench.py --metric cpu --stat median # 抗噪口径
|
||
python run_bench.py --trim-ratio 0 --stat mean # 关闭裁剪
|
||
python run_bench.py --impl py --py-mode pure # 强制纯 Python 回退
|
||
"""
|
||
import argparse
|
||
import csv
|
||
import datetime
|
||
import math
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent
|
||
CC_DIR_DEFAULT = "D:/msys64/mingw64/bin"
|
||
|
||
DEFAULT_LEN = 16384
|
||
DEFAULT_SIZES = "16384"
|
||
TOTAL_FLOPS_TARGET = 1 << 31 # 2147483648 ≈ 2.147e9, 每次 run 恒定的运算量
|
||
EXPECT_TOTAL_FLOPS = float(TOTAL_FLOPS_TARGET)
|
||
|
||
CFG_RE = re.compile(r"^\s*CFG\s+(\d+)\s+(\d+)")
|
||
AFFINITY_RE = re.compile(r"^\s*AFFINITY\s+(-?\d+)")
|
||
# 注意: 内层用非捕获分组, 使 RESULT 的第 1/2/3 个数字分别落在 group(1)/group(2)/group(3)
|
||
NUM = r"[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?"
|
||
RESULT_RE = re.compile(rf"^\s*RESULT\s+({NUM})\s+({NUM})\s+({NUM})")
|
||
CHECK_RE = re.compile(rf"^\s*CHECK\s+({NUM})")
|
||
TSC_RE = re.compile(rf"^\s*TSC\s+({NUM})")
|
||
# Viper 专有: MODE <0|1> —— 0=simd_fma_array / 1=scalar_fma_array
|
||
MODE_RE = re.compile(r"^\s*MODE\s+(-?\d+)")
|
||
|
||
METRIC_IDX = {"wall": 0, "cpu": 1}
|
||
METRIC_LABEL = {"wall": "墙钟(QPC)", "cpu": "线程CPU(QueryThreadCycleTime)"}
|
||
STAT_LABEL = {"mean": "算术平均(裁剪后)", "median": "中位数", "geomean": "几何均值"}
|
||
|
||
|
||
def parse_process_output(text, expected_runs, impl):
|
||
"""解析单个子进程 stdout -> dict。
|
||
|
||
协议: 1 行 CFG + 可选 1 行 AFFINITY + expected_runs 行 RESULT + 1 行 CHECK。
|
||
RESULT = <wall_us> <cpu_us> <flops>。
|
||
解析失败抛 ValueError(由调用方转成带 traceback 的失败记录)。
|
||
"""
|
||
cfg = None
|
||
affinity = None
|
||
tsc_hz = None
|
||
kernel_mode = None # Viper 专有: 0=SIMD / 1=Scalar; 其它实现无此行
|
||
samples = [] # (wall_us, cpu_us, flops)
|
||
checks = []
|
||
for line in text.splitlines():
|
||
m = RESULT_RE.match(line)
|
||
if m:
|
||
samples.append((float(m.group(1)), float(m.group(2)), float(m.group(3))))
|
||
continue
|
||
m = CHECK_RE.match(line)
|
||
if m:
|
||
checks.append(float(m.group(1)))
|
||
continue
|
||
m = AFFINITY_RE.match(line)
|
||
if m and affinity is None:
|
||
affinity = int(m.group(1))
|
||
continue
|
||
m = TSC_RE.match(line)
|
||
if m and tsc_hz is None:
|
||
tsc_hz = float(m.group(1))
|
||
continue
|
||
m = MODE_RE.match(line)
|
||
if m and kernel_mode is None:
|
||
kernel_mode = int(m.group(1))
|
||
continue
|
||
m = CFG_RE.match(line)
|
||
if m and cfg is None:
|
||
cfg = (int(m.group(1)), int(m.group(2)))
|
||
if cfg is None:
|
||
raise ValueError(
|
||
f"[{impl}] stdout 未找到 CFG <len> <iters> 行。stdout 前 20 行:\n"
|
||
+ "\n".join(text.splitlines()[:20]))
|
||
if len(samples) != expected_runs:
|
||
raise ValueError(
|
||
f"[{impl}] 期望 {expected_runs} 行 RESULT, 实际 {len(samples)} 行"
|
||
f"(CFG={cfg[0]}x{cfg[1]})。stdout 前 40 行:\n"
|
||
+ "\n".join(text.splitlines()[:40]))
|
||
if not checks:
|
||
raise ValueError(
|
||
f"[{impl}] stdout 未找到 CHECK 行。stdout 前 40 行:\n"
|
||
+ "\n".join(text.splitlines()[:40]))
|
||
return {
|
||
"cfg": cfg,
|
||
"affinity": affinity,
|
||
"tsc_hz": tsc_hz,
|
||
"kernel_mode": kernel_mode,
|
||
"samples": samples, # list[(wall_us, cpu_us, flops)]
|
||
"check": checks[0],
|
||
}
|
||
|
||
|
||
class BenchError(Exception):
|
||
pass
|
||
|
||
|
||
def sh_ok(cmd, cwd, env=None, timeout=1800):
|
||
"""执行命令, 成功返回 CompletedProcess; 失败抛 BenchError(带 stderr)。"""
|
||
try:
|
||
p = subprocess.run(cmd, cwd=str(cwd), capture_output=True,
|
||
text=True, errors="replace", timeout=timeout, env=env)
|
||
except FileNotFoundError as e:
|
||
raise BenchError(f"找不到可执行文件: {cmd[0]}\n (请确认路径/工具链已安装)")
|
||
except subprocess.TimeoutExpired:
|
||
raise BenchError(f"命令超时({timeout}s): {cmd}")
|
||
return p
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 编译 / 运行定义
|
||
# ------------------------------------------------------------
|
||
# C/C++ 与 Viper 同口径: 允许 FMA 收缩(单趟融合乘加, 单次舍入)。
|
||
# -ffp-contract=fast 让 a[i]*b[i]+c[i] 生成 vfmadd(而不是 mul+add 两次舍入);
|
||
# -mfma 显式打开 FMA 指令集(只给 -mavx2 不保证启用 FMA)。
|
||
# 要回到"两次舍入"的旧口径做 A/B, 把 -ffp-contract=fast 换成 =off 并去掉 -mfma 即可。
|
||
NATIVE_FLAGS = ["-O2", "-mavx2", "-mfma", "-ffp-contract=fast"]
|
||
|
||
|
||
# Viper 内核实现名 -> (传给 app.exe 的第 4 个参数 mode, 显示名)。
|
||
# 全部共用同一个 __vpcbuild__/output/app.exe, 只差这一个 mode 参数 —— 除"跑哪份内核"
|
||
# 外没有任何差异, 这是 SIMD vs 原生 对照可信的前提(见 App/main.vp 的 KERNEL_* 常量)。
|
||
# 命名规则: `<op>` = SIMD, `scalar_<op>` = 同操作的标量(原生)对照。
|
||
VIPER_KERNELS = [
|
||
("viper", 0, "FMA SIMD (vipersimd.simd_fma_array)"),
|
||
("viper_scalar", 1, "FMA 原生 (vipersimd.scalar_fma_array)"),
|
||
("viper_add", 2, "ADD SIMD (vipersimd.simd_add_array)"),
|
||
("viper_scalar_add", 3, "ADD 原生 (vipersimd.scalar_add_array)"),
|
||
("viper_mul", 4, "MUL SIMD (vipersimd.simd_mul_array)"),
|
||
("viper_scalar_mul", 5, "MUL 原生 (vipersimd.scalar_mul_array)"),
|
||
("viper_sqrt", 6, "SQRT SIMD (vipersimd.simd_sqrt_array)"),
|
||
("viper_scalar_sqrt", 7, "SQRT 原生 (vipersimd.scalar_sqrt_array)"),
|
||
]
|
||
VIPER_MODE = {name: mode for name, mode, _ in VIPER_KERNELS}
|
||
VIPER_DISP = {name: disp for name, _, disp in VIPER_KERNELS}
|
||
|
||
# 默认跑"图1 + 图2/3/4 所需的全部实现": 8 个 Viper 内核变体(FMA/ADD/MUL/SQRT 各一对
|
||
# SIMD/原生) + C/C++/Python。图1 需要 ADD/MUL/SQRT 的成对数据, 所以默认就带上 ——
|
||
# 否则跑完默认命令图1 会以"缺数据"整张跳过, 体验很差。
|
||
# 只要图2/3/4 时, 用 --impl viper,viper_scalar,c,cpp,py 可跳过那 6 个内核变体。
|
||
DEFAULT_IMPLS = ",".join([n for n, _, _ in VIPER_KERNELS] + ["c", "cpp", "py"])
|
||
|
||
|
||
def build_native(cc_dir, impl, root):
|
||
"""编译 bench_c.c / bench_cpp.cpp 到 .native_build/, 返回 exe 路径。
|
||
|
||
源码或编译选项变化时才重编(用 .flags 标记文件记录上次编译选项)。
|
||
"""
|
||
build_dir = root / ".native_build"
|
||
build_dir.mkdir(parents=True, exist_ok=True)
|
||
src = root / ("bench_c.c" if impl == "c" else "bench_cpp.cpp")
|
||
exe = build_dir / ("bench_c.exe" if impl == "c" else "bench_cpp.exe")
|
||
cc = cc_dir / ("clang.exe" if impl == "c" else "clang++.exe")
|
||
flags = NATIVE_FLAGS + [
|
||
"-std=c11" if impl == "c" else "-std=c++17",
|
||
"-lmsvcrt", "-lmingwex", "-lkernel32", "-lm",
|
||
]
|
||
flags_marker = build_dir / (".flags_" + impl)
|
||
need = (not exe.exists()
|
||
or not flags_marker.exists()
|
||
or flags_marker.read_text(encoding="utf-8") != "\n".join(flags)
|
||
or exe.stat().st_mtime < src.stat().st_mtime)
|
||
if not need:
|
||
return exe
|
||
cmd = [str(cc)] + flags + [str(src), "-o", str(exe)]
|
||
p = sh_ok(cmd, root)
|
||
if p.returncode != 0:
|
||
tail = "\n".join(p.stderr.splitlines()[-25:])
|
||
raise BenchError(f"编译失败(exit={p.returncode}): {' '.join(cmd)}\n"
|
||
f"stderr 尾部:\n{tail}")
|
||
flags_marker.write_text("\n".join(flags), encoding="utf-8")
|
||
return exe
|
||
|
||
|
||
def iters_for(size):
|
||
"""给定 LEN, 求出让单 run 恒为 TOTAL_FLOPS_TARGET 的 ITERS。
|
||
|
||
必须整除: 否则各尺寸的"每次 run 运算量"就不一致, GFLOPS 横向不可比。
|
||
取 2 的幂做 LEN 时永远整除(ITERS = 2^30 / LEN)。
|
||
"""
|
||
denom = 2 * size
|
||
if size < 4 or size % 4 != 0:
|
||
raise BenchError(f"LEN={size} 非法: 需 >=4 且为 4 的倍数(内核按 4-lane ymm 分批)")
|
||
if TOTAL_FLOPS_TARGET % denom != 0:
|
||
raise BenchError(f"LEN={size} 无法整除: {TOTAL_FLOPS_TARGET} / (2*{size}) 不是整数")
|
||
return TOTAL_FLOPS_TARGET // denom
|
||
|
||
|
||
def viper_exe(root):
|
||
return root / "__vpcbuild__" / "output" / "app.exe"
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 统计与核验
|
||
# ------------------------------------------------------------
|
||
def mean(vals):
|
||
return sum(vals) / len(vals)
|
||
|
||
|
||
def sample_stddev(vals, m):
|
||
if len(vals) < 2:
|
||
return 0.0
|
||
s = sum((v - m) ** 2 for v in vals)
|
||
return math.sqrt(s / (len(vals) - 1))
|
||
|
||
|
||
def median(vals):
|
||
if not vals:
|
||
return 0.0
|
||
s = sorted(vals)
|
||
n = len(s)
|
||
mid = n // 2
|
||
return s[mid] if n % 2 else (s[mid - 1] + s[mid]) / 2.0
|
||
|
||
|
||
def geomean(vals):
|
||
"""几何均值; 出现 <=0 的样本时退回 0(计时值理论上恒 >0)。"""
|
||
if not vals:
|
||
return 0.0
|
||
if any(v <= 0.0 for v in vals):
|
||
return 0.0
|
||
return math.exp(sum(math.log(v) for v in vals) / len(vals))
|
||
|
||
|
||
def pick_stat(name, vals):
|
||
if not vals:
|
||
return 0.0
|
||
if name == "median":
|
||
return median(vals)
|
||
if name == "geomean":
|
||
return geomean(vals)
|
||
return mean(vals)
|
||
|
||
|
||
def trim_group(vals, trim_ratio):
|
||
"""按中位数上界裁掉离群点: 保留 v <= median(vals) * trim_ratio。
|
||
|
||
返回 (kept, dropped_positions)。trim_ratio <= 0 时等价于不裁剪。
|
||
至少保留中位数本身, 所以 kept 不会为空。
|
||
"""
|
||
if trim_ratio <= 0.0 or not vals:
|
||
return list(vals), []
|
||
hi = median(vals) * trim_ratio
|
||
kept = [v for v in vals if v <= hi]
|
||
dropped = [i for i, v in enumerate(vals) if v > hi]
|
||
if not kept:
|
||
return list(vals), []
|
||
return kept, dropped
|
||
|
||
|
||
def analyze_impl(groups_samples, tol, drop_first, trim_ratio, stat_name, metric):
|
||
"""groups_samples: list[list[(wall,cpu,flops)]], 每 list = 一个进程内 N 次 run。
|
||
|
||
流程: 取主指标 -> 每组按中位数上界裁剪离群点 -> 用 stat_name 统计。
|
||
drop_first=True 时, 首组(索引 0)【仅汇报】: 不计入总体/标准差/最大偏差,
|
||
也不参与差值核验(规避首组效应)。只有一组时自动不排除。
|
||
"""
|
||
idx = METRIC_IDX[metric]
|
||
other_idx = 1 - idx
|
||
groups = [[s[idx] for s in g] for g in groups_samples]
|
||
|
||
drop_first = drop_first and len(groups) > 1
|
||
|
||
trimmed = [trim_group(vals, trim_ratio) for vals in groups]
|
||
stat_groups = trimmed[1:] if drop_first else trimmed
|
||
|
||
flat = [v for kept, _ in stat_groups for v in kept]
|
||
overall = pick_stat(stat_name, flat)
|
||
overall_std = sample_stddev(flat, overall)
|
||
max_dev = max((abs(v - overall) for v in flat), default=0.0)
|
||
n_dropped = sum(len(dropped) for _, dropped in stat_groups)
|
||
|
||
group_stats = []
|
||
for g, (kept, dropped) in enumerate(trimmed, start=1):
|
||
gm = pick_stat(stat_name, kept)
|
||
group_stats.append({
|
||
"idx": g,
|
||
"raw": groups[g - 1],
|
||
"kept": kept,
|
||
"dropped_idx": dropped,
|
||
"mean": mean(kept),
|
||
"median": median(kept),
|
||
"geomean": geomean(kept),
|
||
"gstat": gm,
|
||
"spread": (max(kept) - min(kept)) if kept else 0.0,
|
||
"dev": gm - overall,
|
||
"in_stat": (not drop_first) or g > 1, # False 仅组1 被排除时
|
||
})
|
||
|
||
adj = [group_stats[k]["gstat"] - group_stats[k - 1]["gstat"]
|
||
for k in range(1, len(group_stats))]
|
||
|
||
def rel(x):
|
||
return abs(x) / abs(overall) if overall != 0.0 else (0.0 if x == 0.0 else float("inf"))
|
||
|
||
# 差值核验只对计入统计的组判定; 首组(含其相邻差)不作为违规依据
|
||
dev_viol = [gs["idx"] for gs in group_stats if gs["in_stat"] and rel(gs["dev"]) > tol]
|
||
adj_pairs = [(k, k + 1) for k in range(len(adj))
|
||
if rel(adj[k]) > tol and not (drop_first and k == 0)]
|
||
passed = (not dev_viol) and (not adj_pairs)
|
||
|
||
# 另一指标的总体值(同一裁剪与统计口径), 只用于汇总对照
|
||
other_flat = []
|
||
for i, g in enumerate(groups_samples):
|
||
if drop_first and i == 0:
|
||
continue
|
||
kept, _ = trim_group([s[other_idx] for s in g], trim_ratio)
|
||
other_flat.extend(kept)
|
||
overall_other = pick_stat(stat_name, other_flat)
|
||
|
||
return {
|
||
"metric": metric, "other_metric": "cpu" if metric == "wall" else "wall",
|
||
"stat": stat_name, "trim_ratio": trim_ratio,
|
||
"overall": overall, "overall_other": overall_other,
|
||
"overall_std": overall_std, "max_dev": max_dev,
|
||
"n": len(flat), "n_dropped": n_dropped,
|
||
"n_stat_groups": len(stat_groups), "drop_first": drop_first,
|
||
"group_stats": group_stats, "adj": adj,
|
||
"passed": passed, "dev_viol": dev_viol, "adj_pairs": adj_pairs,
|
||
"_samples": groups_samples, # 供 CSV / flops-check
|
||
}
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 主流程
|
||
# ------------------------------------------------------------
|
||
def build_arg_parser():
|
||
p = argparse.ArgumentParser(
|
||
description="ViperSpeedBenchmark 编排: 同内核多实现测速 + 组间差值核验"
|
||
"(每组=单进程内多 run, 线程绑定 + 双计时 + 离群裁剪)")
|
||
p.add_argument("--groups", type=int, default=5, help="组数(默认 5; 每组=一次进程启动)")
|
||
p.add_argument("--runs-per-group", type=int, default=10,
|
||
help="每组连续 run 次数 = 每次进程启动后打印的 RESULT 行数(默认 10)")
|
||
p.add_argument("--sizes", default=DEFAULT_SIZES,
|
||
help="逗号分隔的 LEN 列表(多尺寸扫描), 默认 %(default)s。"
|
||
"每个 LEN 的 ITERS 自动取值, 使单 run 恒为 ≈2.147e9 flops; "
|
||
"LEN 决定工作集(四份缓冲共 LEN*32 字节), 例如 "
|
||
"1024=L1 / 16384=L2 / 262144 以上=L3-DRAM")
|
||
p.add_argument("--impl", default=DEFAULT_IMPLS,
|
||
help="逗号分隔实现子集, 默认 %(default)s。全部 Viper 变体共用同一个 "
|
||
"app.exe, 只差命令行第 4 个参数 mode: "
|
||
"viper/viper_scalar=FMA, viper_add/viper_scalar_add=ADD, "
|
||
"viper_mul/viper_scalar_mul=MUL, viper_sqrt/viper_scalar_sqrt=SQRT "
|
||
"(前者 SIMD / 后者原生); 另有 c / cpp / py。"
|
||
"默认已含全部内核变体(plot_bench.py 图1 需要它们的成对数据)")
|
||
p.add_argument("--tol", type=float, default=0.05,
|
||
help="相对容差, 组对总体与相邻组差值判定阈值(默认 0.05=5%%)")
|
||
p.add_argument("--include-first-group", action="store_true",
|
||
help="把首组(组1)计入统计与差值核验(默认排除: 组1仅汇报, 规避首组效应)")
|
||
p.add_argument("--metric", default="wall", choices=["wall", "cpu"],
|
||
help="主指标(默认 wall=QPC 墙钟; cpu=QueryThreadCycleTime 线程 CPU 时间)")
|
||
p.add_argument("--stat", default="mean", choices=["mean", "median", "geomean"],
|
||
help="组/总体统计量(默认 mean=裁剪后算术平均; 双峰分布建议 median)")
|
||
p.add_argument("--trim-ratio", type=float, default=1.2,
|
||
help="组内离群裁剪上界 = 组内中位数 × 该系数(默认 1.2); <=0 关闭裁剪")
|
||
p.add_argument("--csv", default=None,
|
||
help="CSV 输出路径; 省略时自动写成 bench_<时间戳>.csv")
|
||
p.add_argument("--no-csv", action="store_true", help="完全不写 CSV")
|
||
p.add_argument("--cc-dir", default=CC_DIR_DEFAULT, help="clang/clang++ 目录")
|
||
p.add_argument("--report", default="bench_report.txt", help="日志输出文件")
|
||
p.add_argument("--py-mode", default=None, choices=["auto", "numpy", "pure"],
|
||
help="覆盖 bench_py.py 的运行模式(auto/numpy/pure)")
|
||
p.add_argument("--viper-exe", default=None,
|
||
help="覆盖 Viper 产物路径(默认 __vpcbuild__/output/app.exe)")
|
||
p.add_argument("--flops-check", action="store_true",
|
||
help="核对 RESULT 行的 flops 字段是否与 CFG 总运算量一致, "
|
||
"并报告 flops≈2e9(时长≈1s)的行占比及降规模提示")
|
||
return p
|
||
|
||
|
||
def main(argv=None):
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
args = build_arg_parser().parse_args(argv)
|
||
|
||
groups_n = max(1, args.groups)
|
||
runs_n = max(1, args.runs_per_group)
|
||
tol = args.tol
|
||
metric = args.metric
|
||
stat_name = args.stat
|
||
trim_ratio = args.trim_ratio
|
||
drop_first = not args.include_first_group # 首组效应规避: 组1仅汇报不计入统计
|
||
impl_order = [name for name, _, _ in VIPER_KERNELS] + ["c", "cpp", "py"]
|
||
requested = [x.strip().lower() for x in args.impl.split(",") if x.strip()]
|
||
if requested == ["all"]:
|
||
requested = list(impl_order)
|
||
unknown = [x for x in requested if x not in impl_order]
|
||
if unknown:
|
||
print(f"未知实现名: {unknown} (可选: {','.join(impl_order)})", file=sys.stderr)
|
||
return 2
|
||
impls = [x for x in impl_order if x in requested]
|
||
|
||
# ---- 尺寸列表: LEN 决定工作集(四份缓冲共 LEN*32 字节), ITERS 自动算 ----
|
||
sizes = []
|
||
for tok in [t.strip() for t in args.sizes.split(",") if t.strip()]:
|
||
try:
|
||
sizes.append(int(tok))
|
||
except ValueError:
|
||
print(f"--sizes 里有非整数项: {tok!r}", file=sys.stderr)
|
||
return 2
|
||
if not sizes:
|
||
sizes = [DEFAULT_LEN]
|
||
try:
|
||
iters_map = {s: iters_for(s) for s in sizes}
|
||
except BenchError as e:
|
||
print(str(e), file=sys.stderr)
|
||
return 2
|
||
|
||
cc_dir = Path(args.cc_dir)
|
||
vpy_exe = Path(args.viper_exe) if args.viper_exe else viper_exe(ROOT)
|
||
|
||
env = dict(os.environ)
|
||
if args.py_mode:
|
||
env["VSB_PY_MODE"] = args.py_mode
|
||
|
||
lines = []
|
||
def emit(s=""):
|
||
lines.append(s)
|
||
print(s, flush=True)
|
||
|
||
stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
csv_stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
emit("=" * 78)
|
||
emit(" ViperSpeedBenchmark - run_bench.py (线程绑定 + 双计时 + 离群裁剪版)")
|
||
emit(f" 时间 : {stamp}")
|
||
emit(f" 参数 : groups={groups_n}, runs/group={runs_n}, tol={tol:.1%}")
|
||
emit(f" 实现 : {','.join(impls)}")
|
||
emit(f" 负载 : 每次 run ≈ {EXPECT_TOTAL_FLOPS:.3e} flops(LEN×ITERS×2 恒定, 跨尺寸可比)")
|
||
for s in sizes:
|
||
emit(f" LEN={s:>8} ITERS={iters_map[s]:>8} 工作集={s * 32 / 1024:>9.1f} KiB")
|
||
emit(" 采集方式 : 每组启动一次子进程, 进程内 2 轮满热身 + 连续计时 N 次")
|
||
emit(" 线程绑定 : 子进程入口 SetThreadAffinityMask 绑到首个可用逻辑核(AFFINITY 行回报)")
|
||
emit(f" 主指标 : {METRIC_LABEL[metric]} (另一指标同时采集, 汇总里对照)")
|
||
emit(f" 统计量 : {STAT_LABEL[stat_name]}")
|
||
emit(" 离群裁剪 : " + (f"组内保留 <= 中位数×{trim_ratio:g} 的样本" if trim_ratio > 0
|
||
else "关闭(--trim-ratio<=0)"))
|
||
emit(" 统计口径 : " + ("首组(组1)仅汇报不计入统计(规避首组效应)"
|
||
if drop_first else "全部组计入统计"))
|
||
emit("=" * 78)
|
||
|
||
# ---- 编译 / 检测 ----
|
||
exe_map = {}
|
||
compile_notes = []
|
||
for impl in ("c", "cpp"):
|
||
if impl not in impls:
|
||
continue
|
||
emit(f"[{impl}] 编译 bench_{'c.c' if impl == 'c' else 'cpp.cpp'} -> .native_build/")
|
||
try:
|
||
exe_map[impl] = build_native(cc_dir, impl, ROOT)
|
||
emit(f"[{impl}] 编译成功: {exe_map[impl]}")
|
||
except BenchError as e:
|
||
compile_notes.append((impl, str(e)))
|
||
emit(f"[{impl}] 编译失败: {e}")
|
||
emit(f"[{impl}] 请确认 clang 工具链可用(--cc-dir 可指定目录), 将跳过该实现。")
|
||
viper_wanted = [im for im in impls if im in VIPER_MODE]
|
||
if viper_wanted:
|
||
if vpy_exe.exists():
|
||
for name in viper_wanted:
|
||
exe_map[name] = vpy_exe
|
||
emit(f"[viper] 找到产物: {vpy_exe}")
|
||
emit(f" {len(viper_wanted)} 个内核变体共用此 exe, 只差命令行 mode 参数: "
|
||
+ ", ".join(f"{n}(mode={VIPER_MODE[n]})" for n in viper_wanted))
|
||
else:
|
||
emit(f"[viper] 未找到 Viper 产物: {vpy_exe}")
|
||
emit(" 请先在本工程目录用 tpv 构建 project.vpj(生成 __vpcbuild__/output/app.exe),")
|
||
emit(" 本次将跳过全部 Viper 内核变体。")
|
||
|
||
# ---- 逐尺寸 × 逐实现跑测(每组=一次进程启动) ----
|
||
# 按"尺寸优先"展开: 同一尺寸下各实现紧挨着跑, 便于横向对比
|
||
results = {} # key = (size, impl)
|
||
prev_size = None
|
||
for size, impl in [(sz, im) for sz in sizes for im in impls]:
|
||
iters_n = iters_map[size]
|
||
if size != prev_size:
|
||
emit("")
|
||
emit("#" * 78)
|
||
emit(f"# 工作尺寸 LEN={size} ITERS={iters_n} 四份缓冲共 {size * 32 / 1024:.1f} KiB")
|
||
emit("#" * 78)
|
||
prev_size = size
|
||
# py 始终可运行(用本解释器), 原生/viper 需先成功编译/检出
|
||
if impl == "py":
|
||
pass
|
||
elif impl not in exe_map:
|
||
continue
|
||
emit("")
|
||
emit("-" * 78)
|
||
if impl in VIPER_MODE:
|
||
cmd = [str(exe_map[impl]), str(runs_n), str(size), str(iters_n),
|
||
str(VIPER_MODE[impl])]
|
||
display = VIPER_DISP[impl] + " 内核, QPC 计时"
|
||
elif impl in ("c", "cpp"):
|
||
cmd = [str(exe_map[impl]), str(runs_n), str(size), str(iters_n)]
|
||
display = ("C 手写 -O2 -mavx2 -mfma(-ffp-contract=fast) 自动向量化单趟 FMA, QPC"
|
||
if impl == "c" else
|
||
"C++ 手写(std::vector) -O2 -mavx2 -mfma(-ffp-contract=fast) 单趟 FMA, QPC")
|
||
else: # py
|
||
cmd = [sys.executable, str(ROOT / "bench_py.py"), str(runs_n), str(size), str(iters_n)]
|
||
display = "Python(numpy 就地 ufunc; 无 numpy 自动纯 Python 降规模回退)"
|
||
emit(f"实现: {impl} (每组 = 一次进程启动, 进程内满热身 + 连续 {runs_n} 次 run)")
|
||
emit(f" 方式: {display}")
|
||
emit(f" 命令: {' '.join(cmd)}")
|
||
|
||
groups = [] # list[list[(wall_us, cpu_us, flops)]]
|
||
meta = {"cfg": None, "check": None, "mode": None, "affinity": None,
|
||
"tsc_hz": None, "kernel_mode": None}
|
||
try:
|
||
for g in range(1, groups_n + 1):
|
||
p = sh_ok(cmd, ROOT, env=env)
|
||
out_text = p.stdout
|
||
if p.returncode != 0:
|
||
tail = "\n".join(p.stderr.splitlines()[-20:])
|
||
raise BenchError(
|
||
f"[{impl}] 组{g} 进程退出码={p.returncode}\n"
|
||
f"stderr 尾部:\n{tail}\n"
|
||
f"stdout 前 20 行:\n" + "\n".join(out_text.splitlines()[:20]))
|
||
try:
|
||
parsed = parse_process_output(out_text, runs_n, impl)
|
||
except ValueError as ve:
|
||
# 附带 stderr 一起展示, 便于定位崩溃原因
|
||
stderr_tail = "\n".join(p.stderr.splitlines()[-30:])
|
||
raise BenchError(
|
||
f"[{impl}] 组{g} 输出解析失败: {ve}\n"
|
||
f"stderr 尾部:\n{stderr_tail}") from ve
|
||
groups.append(parsed["samples"])
|
||
if meta["cfg"] is None:
|
||
meta["cfg"] = parsed["cfg"]
|
||
if meta["check"] is None:
|
||
meta["check"] = parsed["check"]
|
||
if meta["affinity"] is None:
|
||
meta["affinity"] = parsed["affinity"]
|
||
if meta["tsc_hz"] is None:
|
||
meta["tsc_hz"] = parsed["tsc_hz"]
|
||
if meta["kernel_mode"] is None:
|
||
meta["kernel_mode"] = parsed["kernel_mode"]
|
||
# Viper 内核变体必须真的跑在期望的内核上: 旧版 app.exe 不认命令行第 4
|
||
# 个参数(或内核不全), 会悄悄跑成别的内核 —— 那等于让 SIMD 数据冒充原生,
|
||
# 直接污染 SIMD/原生对照。期望值对不上就显式失败, 不给"看似成功"的机会。
|
||
want_mode = VIPER_MODE.get(impl)
|
||
if want_mode is not None and parsed["kernel_mode"] != want_mode:
|
||
raise BenchError(
|
||
f"[{impl}] 产物回报 MODE={parsed['kernel_mode']!r}, 期望 {want_mode}: "
|
||
"app.exe 可能是旧版(不识别命令行第 4 个参数 mode)或未包含该内核, "
|
||
"请重新构建 Viper 工程")
|
||
if meta["mode"] is None and parsed["cfg"][1] != iters_n:
|
||
meta["mode"] = "pure-scale"
|
||
elif meta["mode"] is None:
|
||
meta["mode"] = "full"
|
||
wall = [s[0] for s in parsed["samples"]]
|
||
cpu = [s[1] for s in parsed["samples"]]
|
||
emit(f"[{impl}] 组{g}/{groups_n} 完成 | 墙钟均值={mean(wall):.1f} us | "
|
||
f"线程CPU均值={mean(cpu):.1f} us | 墙钟max-min={max(wall) - min(wall):.1f}")
|
||
st = analyze_impl(groups, tol, drop_first, trim_ratio, stat_name, metric)
|
||
st["meta"] = meta
|
||
st["size"] = size
|
||
results[(size, impl)] = st
|
||
except Exception as e:
|
||
# 启动/解析/统计任何异常都要把具体 traceback 打出来, 不再静默跳过
|
||
err_text = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||
emit(f"[{impl}] 运行失败(详见 traceback): {e}")
|
||
emit(err_text.rstrip())
|
||
results[(size, impl)] = {"error": str(e), "traceback": err_text}
|
||
|
||
# ---- 差值核验 / flops-check 统计输出 ----
|
||
csv_rows = []
|
||
cur_size = None
|
||
for (size, impl), st in results.items():
|
||
if size != cur_size:
|
||
emit("")
|
||
emit("#" * 78)
|
||
emit(f"# 结果汇总: 工作尺寸 LEN={size} ITERS={iters_map[size]}"
|
||
f" 四份缓冲共 {size * 32 / 1024:.1f} KiB")
|
||
emit("#" * 78)
|
||
cur_size = size
|
||
if "error" in st:
|
||
emit(f" {impl}: 运行失败 —— {st['error']}")
|
||
continue
|
||
meta = st.get("meta", {})
|
||
cfg = meta.get("cfg")
|
||
cfg_total = (float(cfg[0]) * float(cfg[1]) * 2.0) if cfg else 0.0
|
||
cfg_txt = f"{cfg[0]}x{cfg[1]}" if cfg else "?"
|
||
emit("")
|
||
emit("=" * 78)
|
||
emit(f"实现 {impl} CFG(len×iters)={cfg_txt} 总运算量/run={cfg_total:.3e} flops"
|
||
+ (" [纯 Python 降规模]" if meta.get("mode") == "pure-scale" else ""))
|
||
aff = meta.get("affinity")
|
||
emit("线程绑定 AFFINITY = "
|
||
+ (f"CPU#{aff}(SetThreadAffinityMask 成功)"
|
||
if (aff is not None and aff >= 0) else "未绑定/失败(-1)"))
|
||
tsc = meta.get("tsc_hz")
|
||
emit("TSC 标定频率 = "
|
||
+ (f"{tsc:.4e} Hz" if tsc else "缺失/0(标定失败: 该实现的周期计数未推进)"))
|
||
km = meta.get("kernel_mode")
|
||
if km is not None:
|
||
emit(f"内核 MODE = {km} {VIPER_DISP.get(impl, '(内核名未知)')}")
|
||
all_samples = [s for g in st.get("_samples", []) for s in g]
|
||
n_bad = sum(1 for w, c, _ in all_samples if w > 0.0 and c > w * 1.05)
|
||
if n_bad:
|
||
emit(f" !! 可疑: {n_bad}/{len(all_samples)} 个样本 cpu_us > wall_us×1.05"
|
||
" —— 单线程物理上不可能, 请查该实现的 TSC 标定与周期读取")
|
||
emit(f"CHECK(首次) = {meta.get('check', float('nan')):.17g}")
|
||
overall = st["overall"]
|
||
emit(f"主指标 {METRIC_LABEL[st['metric']]} 统计量={STAT_LABEL[st['stat']]}")
|
||
emit(f"总体统计 overall : {overall:.3f} us "
|
||
f"(统计 {st['n_stat_groups']}/{groups_n} 组, 有效样本 {st['n']}, "
|
||
f"裁剪剔除 {st['n_dropped']}"
|
||
+ ("; 首组仅汇报" if st.get("drop_first") else "") + ")")
|
||
emit(f"对照 {METRIC_LABEL[st['other_metric']]:<24}: {st['overall_other']:.3f} us")
|
||
emit(f"样本标准差 sample std : {st['overall_std']:.3f} us")
|
||
rel_md = (abs(st["max_dev"]) / abs(overall) if overall else 0.0)
|
||
emit(f"最大偏差 max|raw-overall|: {st['max_dev']:.3f} us ({rel_md:.3%})")
|
||
if cfg_total:
|
||
emit(f"实测吞吐(按 CFG, 主指标) : {cfg_total / (overall * 1e-6) / 1e9:.3f} GFLOPS")
|
||
emit("-" * 78)
|
||
for gs in st["group_stats"]:
|
||
raw_txt = ", ".join(f"{v:.1f}" for v in gs["raw"])
|
||
dev_rel = abs(gs["dev"]) / abs(overall) if overall else 0.0
|
||
excl = " [首组: 仅汇报, 不计入统计]" if not gs.get("in_stat", True) else ""
|
||
trim_txt = (f" | 裁剪剔除 {len(gs['dropped_idx'])}/{len(gs['raw'])}"
|
||
f" 剔除位置={[i + 1 for i in gs['dropped_idx']]}"
|
||
if gs["dropped_idx"] else " | 裁剪剔除 0")
|
||
emit(f"[组{gs['idx']:>2}] 统计值={gs['gstat']:>11.2f} us | "
|
||
f"均值={gs['mean']:>11.2f} | 中位数={gs['median']:>11.2f} | "
|
||
f"几何均值={gs['geomean']:>11.2f}{trim_txt}")
|
||
emit(f" 组内差max-min={gs['spread']:>8.2f} | "
|
||
f"组-总体={gs['dev']:+10.2f} us ({dev_rel:5.2%}){excl}")
|
||
if gs["idx"] >= 2:
|
||
k = gs["idx"] - 2
|
||
if st.get("drop_first") and k == 0:
|
||
emit(" 相邻组差: n/a(组1 不计入统计)")
|
||
else:
|
||
adj_val = st["adj"][k]
|
||
adj_rel = abs(adj_val) / abs(overall) if overall else 0.0
|
||
emit(f" 相邻组差 组{gs['idx'] - 1}->组{gs['idx']}: {adj_val:+10.2f} us ({adj_rel:5.2%})")
|
||
else:
|
||
emit(f" 相邻组差: n/a(首组)")
|
||
emit(f" 组内 {runs_n} 次原始(主指标): {raw_txt} us")
|
||
emit("-" * 78)
|
||
verdict = "PASS" if st["passed"] else "FAIL"
|
||
emit(f"差值核验(tol={tol:.1%}): {verdict}"
|
||
+ (" [组1 排除: 首组仅汇报]" if st.get("drop_first") else ""))
|
||
if st["passed"]:
|
||
scope = "统计组(排除首组)" if st.get("drop_first") else "全部组"
|
||
emit(f" {scope}: |组统计值-总体|/总体 <= tol, 且相邻组|差值|/总体 <= tol")
|
||
else:
|
||
if st["dev_viol"]:
|
||
emit(f" 组对总体超差(违规组): {st['dev_viol']}")
|
||
if st["adj_pairs"]:
|
||
emit(f" 相邻组超差(违规对): {['组%d->组%d' % p for p in st['adj_pairs']]}")
|
||
# CSV 原始行(wall/cpu/flops 全出, 附该样本是否被裁剪剔除)
|
||
for gi, gsamples in enumerate(st.get("_samples", []), start=1):
|
||
dropped = set(st["group_stats"][gi - 1]["dropped_idx"])
|
||
for ri, (wv, cv, fv) in enumerate(gsamples, start=1):
|
||
csv_rows.append([impl, cfg[0] if cfg else "", cfg[1] if cfg else "",
|
||
gi, ri, wv, cv, fv, meta.get("check", ""),
|
||
1 if (ri - 1) in dropped else 0])
|
||
|
||
# ---- flops-check ----
|
||
if args.flops_check:
|
||
emit("")
|
||
emit("=" * 78)
|
||
emit("flops-check 核验(需 --flops-check): 校验 CFG 总运算量≈2.147e9 及")
|
||
emit("RESULT 行 flops 字段与 us 的换算一致性(flops = CFG_total / 墙钟秒)")
|
||
emit("-" * 78)
|
||
any_issue = False
|
||
for (size, impl), st in results.items():
|
||
if "error" in st:
|
||
continue
|
||
cfg = st.get("meta", {}).get("cfg")
|
||
if not cfg:
|
||
emit(f" [LEN={size} {impl}] 无 CFG, 无法核验")
|
||
any_issue = True
|
||
continue
|
||
cfg_total = float(cfg[0]) * float(cfg[1]) * 2.0
|
||
near = abs(cfg_total - EXPECT_TOTAL_FLOPS) / EXPECT_TOTAL_FLOPS <= 0.05
|
||
bad = []
|
||
ok_n = 0
|
||
near_2e9_n = 0
|
||
flat = [(w, c, f) for g in st.get("_samples", []) for (w, c, f) in g]
|
||
for row_w, row_c, row_f in flat:
|
||
if row_w <= 0:
|
||
bad.append((row_w, row_f))
|
||
continue
|
||
expect_f = cfg_total / (row_w * 1e-6)
|
||
if abs(row_f - expect_f) / expect_f > 0.01:
|
||
bad.append((row_w, row_f))
|
||
else:
|
||
ok_n += 1
|
||
if abs(row_f - EXPECT_TOTAL_FLOPS) / EXPECT_TOTAL_FLOPS <= 0.5:
|
||
near_2e9_n += 1
|
||
total_n = len(flat)
|
||
emit(f" [LEN={size} {impl}] CFG={cfg[0]}x{cfg[1]} 单 run 总运算量={cfg_total:.3e} flops"
|
||
+ (" (≈2.147e9 ✓)" if near else " (≠2e9: 降规模配置, 见 README §8)"))
|
||
emit(f" RESULT 行 flops 字段与墙钟 us 换算一致: {ok_n}/{total_n}"
|
||
+ ("" if not bad else f" <<< 不一致示例 {bad[:3]}"))
|
||
emit(f" flops≈2e9 的行(即单 run 时长≈1.0s): {near_2e9_n}/{total_n} "
|
||
"(说明: RESULT 的 flops=总运算量/秒, 只有时长恰≈1s 才≈2e9)")
|
||
if not near:
|
||
any_issue = True
|
||
if bad:
|
||
any_issue = True
|
||
if not any_issue:
|
||
emit(" 全部实现 CFG 与 flops 换算均正常。")
|
||
|
||
# ---- 汇总 ----
|
||
emit("")
|
||
emit("=" * 78)
|
||
emit(f"汇总: 各实现总体统计值(us) 与 实测 GFLOPS(按各自 CFG); 主指标={METRIC_LABEL[metric]}")
|
||
cur_size = None
|
||
for (size, impl), st in results.items():
|
||
if size != cur_size:
|
||
emit("")
|
||
emit(f" ==== LEN={size} (工作集 {size * 32 / 1024:.1f} KiB) ====")
|
||
cur_size = size
|
||
if "error" in st:
|
||
emit(f" {impl:<6}: 失败(见上方 traceback)")
|
||
continue
|
||
cfg = st.get("meta", {}).get("cfg")
|
||
cfg_total = (float(cfg[0]) * float(cfg[1]) * 2.0) if cfg else 0.0
|
||
|
||
def gfl(us):
|
||
return cfg_total / (us * 1e-6) / 1e9 if (cfg_total and us) else float("nan")
|
||
|
||
main_us = st["overall"]
|
||
other_us = st["overall_other"]
|
||
lab_main = "墙钟" if st["metric"] == "wall" else "CPU "
|
||
lab_other = "CPU " if st["metric"] == "wall" else "墙钟"
|
||
aff = st.get("meta", {}).get("affinity")
|
||
aff_txt = f"CPU#{aff}" if (aff is not None and aff >= 0) else "未绑定"
|
||
emit(f" {impl:<6}: [{lab_main}] {main_us:>11.3f} us {gfl(main_us):>8.3f} GFLOPS"
|
||
f" | [{lab_other}] {other_us:>11.3f} us {gfl(other_us):>8.3f} GFLOPS"
|
||
f" | {aff_txt} | {'PASS' if st['passed'] else 'FAIL'}")
|
||
if not any(im == "viper" for (_, im) in results):
|
||
emit(" (viper 已跳过: 请先构建 __vpcbuild__/output/app.exe)")
|
||
for impl, note in compile_notes:
|
||
emit(f" ({impl} 已跳过: 编译失败, 见上方)")
|
||
emit("=" * 78)
|
||
|
||
report = ROOT / args.report
|
||
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
emit(f"\n日志已写入: {report}")
|
||
|
||
# CSV 默认自动导出(带时间戳), 便于事后画图; --no-csv 可关闭
|
||
if not args.no_csv:
|
||
csv_path = ROOT / (args.csv if args.csv else f"bench_{csv_stamp}.csv")
|
||
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
|
||
w = csv.writer(f)
|
||
w.writerow(["impl", "len", "iters", "group", "run",
|
||
"wall_us", "cpu_us", "flops", "check", "trimmed"])
|
||
w.writerows(csv_rows)
|
||
emit(f"原始数据已写入: {csv_path}")
|
||
emit(f" (出图: python plot_bench.py {csv_path.name})")
|
||
|
||
if not results or all("error" in st for st in results.values()):
|
||
return 1
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|