EXPERIMENT / 配套代码

compare_outputs.py

← 返回文章
"""按 benchmark 日志的名称、shape、stride 将原始 FP32 输出与 NPZ 基准对齐。"""
import argparse
import json
from pathlib import Path
import re
import numpy as np

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--reference', required=True, type=Path)
    parser.add_argument('--log', required=True, type=Path)
    parser.add_argument('--prefix', required=True, type=Path, help='对应 PREFIX_0.bin 等文件')
    parser.add_argument('--output', required=True, type=Path)
    parser.add_argument('--atol', type=float, help='可选逐元素绝对误差门槛,不提供时只报告差异')
    args = parser.parse_args()
    if args.output.exists():
        parser.error('请使用新的报告路径')
    if args.atol is not None and (not np.isfinite(args.atol) or args.atol < 0):
        parser.error('atol 必须为非负有限数')
    pattern = r'^output index=(\d+) name=(\S+) bytes=(\d+) stride=(\d+) type=(\d+) dims=([\d,]+)$'
    descriptors = re.findall(pattern, args.log.read_text(), re.MULTILINE)
    if not descriptors:
        raise ValueError('日志中没有 benchmark 输出描述')
    rows, seen = [], set()
    with np.load(args.reference, allow_pickle=False) as reference:
        for idx, name, size, stride, kind, dims in descriptors:
            if name not in reference.files or name in seen:
                raise ValueError(f'缺少或重复的参考输出名称:{name}')
            seen.add(name)
            shape = tuple(int(x) for x in dims.split(',') if x)
            size, stride = int(size), int(stride)
            if int(kind) != 0 or not shape or min(shape) <= 0:
                raise ValueError('仅支持此 SDK type=0 的 FP32 固定形状输出')
            count = int(np.prod(shape[:-1], dtype=np.int64))
            width = shape[-1] * 4
            raw = Path(f'{args.prefix}_{idx}.bin').read_bytes()
            if len(raw) != size or width > stride or count * stride > size:
                raise ValueError('输出文件字节数或 stride 与日志不一致')
            compact = b''.join(raw[i * stride:i * stride + width] for i in range(count))
            actual = np.frombuffer(compact, dtype='<f4').reshape(shape).astype(np.float64)
            expected = reference[name].astype(np.float64)
            if actual.shape != expected.shape or not np.isfinite(actual).all() or not np.isfinite(expected).all():
                raise ValueError('输出 shape 不一致或存在非有限值')
            delta = actual - expected
            norm = np.linalg.norm(actual.ravel()) * np.linalg.norm(expected.ravel())
            rows.append({'name': name, 'shape': list(shape), 'mae': float(np.mean(np.abs(delta))),
                         'rmse': float(np.sqrt(np.mean(delta ** 2))), 'max_abs': float(np.max(np.abs(delta))),
                         'cosine': float(np.dot(actual.ravel(), expected.ravel()) / norm) if norm > 0 else None})
        if seen != set(reference.files):
            raise ValueError('基准与输出张量集合不一致')
    passed = None if args.atol is None else all(x['max_abs'] <= args.atol for x in rows)
    report = {'outputs': rows, 'atol': args.atol, 'within_atol': passed,
              'note': '数值比较不包含检测解码、NMS 和任务准确率;没有通用的量化验收阈值。'}
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
    print(json.dumps(report, ensure_ascii=False, indent=2))
    if passed is False:
        raise SystemExit(1)

if __name__ == '__main__':
    main()