slime/show/benchmark.py

324 lines
21 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""Run the complete teaching sequence; preserve commands, raw output and samples."""
import argparse
from datetime import datetime
import hashlib
import json
from pathlib import Path
import platform
import re
import statistics
import subprocess
import time
from zoneinfo import ZoneInfo
from test_steps import slime
ROOT = Path(__file__).resolve().parent
BIN = ROOT/'bin'
def capture(command, timeout=60):
started = time.perf_counter()
p = subprocess.run(list(map(str, command)), capture_output=True, text=True,
check=True, timeout=timeout)
return {'command': list(map(str, command)), 'process_ms': (time.perf_counter()-started)*1000,
'stdout': p.stdout, 'stderr': p.stderr}
def native(name, args, timeout=60):
return capture([BIN/name, *args], timeout=timeout)
def fields(text):
return {key: float(value) if '.' in value or 'e' in value.lower() else int(value)
for key, value in re.findall(r'([a-zA-Z0-9_]+)=([0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)', text)}
def line_fields(text, prefix):
line = next(line for line in text.splitlines() if line.startswith(prefix))
return fields(line)
def seed_results(text):
return sorted(int(line) for line in text.splitlines() if line.isdigit())
def summarize(samples):
return {'samples_ms': samples, 'median_ms': statistics.median(samples),
'min_ms': min(samples), 'max_ms': max(samples)}
def args_for(case, repeats):
return [str(v) for pair in [('--seed', case['seed']), ('--seeds', case['seeds']),
('--x0', case['x0']), ('--z0', case['z0']),
('--width', case['width']), ('--height', case['height']),
('-n', case.get('n', 5)), ('--repeats', repeats)] for v in pair]
def mapping(repeats):
cases = [
dict(name='A:CPU → 按 seed 的 CUDA', seed=61692151016558, seeds=65536,
x0=50941, z0=418996, width=257, height=257, modes='2',
cpu_in_table=True, comparisons=[('01', '02')]),
dict(name='B:大坐标范围的固定坐标策略', seed=61692151016558, seeds=65536,
x0=49021, z0=417076, width=4097, height=4097, modes='2,3',
cpu_in_table=False, cpu_subset=True, comparisons=[('02', '03')]),
dict(name='C:固定 seed 的坐标扫描与 outer sum', seed=61692151016558, seeds=1,
x0=46973, z0=415028, width=8193, height=8193, modes='3,4',
cpu_in_table=False, repeats=100, comparisons=[('03', '04')]),
]
data = []
for case in cases:
print(f"测量线程映射:{case['name']},{case['seeds']} seed × {case['width']}×{case['height']}", flush=True)
args = args_for(case, case.get('repeats', repeats))
# B's full rectangle is expensive on CPU; disclose its independent validation window.
cpu_repeats = repeats if case['cpu_in_table'] else 1
cpu_case = dict(case)
if case.get('cpu_subset'):
cpu_case.update(x0=50941, z0=418996, width=257, height=257)
cpu_raw = native('01_cpu', [*args_for(cpu_case, cpu_repeats), '--cpu-threads', '16'], timeout=600)
print('CPU 对照完成;开始 CUDA 预热与计时', flush=True)
gpu_raw = native('cuda_steps', [*args, '--modes', case['modes']], timeout=600)
cpu, gpu = json.loads(cpu_raw['stdout']), json.loads(gpu_raw['stdout'])
checked_hits = [hit for hit in gpu['hits']
if cpu_case['x0'] <= hit[1] < cpu_case['x0']+cpu_case['width']
and cpu_case['z0'] <= hit[2] < cpu_case['z0']+cpu_case['height']]
if cpu['hits'] != checked_hits:
raise RuntimeError('CPU/CUDA full tuple mismatch')
for seed, x, z in gpu['hits']:
if not all(slime(seed,x+dx,z+dz) for dx in range(case.get('n',5))
for dz in range(case.get('n',5))):
raise RuntimeError('independent Java oracle rejected GPU hit')
print('完成该组完整命中对照', flush=True)
data.append(dict(case=case, cpu=cpu, gpu=gpu, raw=[cpu_raw, gpu_raw], equality=True,
verification=dict(cpu_scope='subset' if case.get('cpu_subset') else 'full',
cpu_case=cpu_case, all_gpu_hits_exact=True)))
return data
def residue(repeats):
print('测量完整坐标余数空间(固定 seed)', flush=True)
# The implementation scans all 2^36 residues every invocation.
args = ['--seed', '61692151016558', '-n', '5']
warmup = native('07_residue', args)
runs = [native('07_residue', args) for _ in range(repeats)]
hits = lambda raw: sorted(line for line in raw['stdout'].splitlines() if line.startswith('hit '))
expected = ['hit 51069 419124']
if any(hits(raw) != expected for raw in [warmup, *runs]):
raise RuntimeError('residue known-sample results mismatch')
counts = [fields(raw['stdout']) for raw in runs]
for k in ('residue_survivors', 'expanded_candidates', 'world_positions'):
if len({row[k] for row in counts}) != 1:
raise RuntimeError('residue repeats have different '+k)
return dict(raw=runs, warmup=warmup, counts=counts[0],
kernel=summarize([r['residue_kernel_ms'] for r in counts]),
compute=summarize([r['prepare_ms']+r['residue_kernel_ms']+r['exact_ms'] for r in counts]),
process=summarize([r['process_ms'] for r in runs]))
def seeds(repeats):
print('测量固定坐标 seed 反查:暴力吞吐与完整求解', flush=True)
fixed = ['--x', '51069', '--z', '419124', '-n', '5']
commands = {
'brute_cpu': ('seed_brute_cpu', [*fixed, '--count', str(1 << 28), '-p', '16']),
'brute_cuda': ('seed_brute_cuda', [*fixed, '--count', str(1 << 34)]),
'solver_cpu': ('08_seed_cpu', ['search', *fixed, '-p', '16', '--max-results', '1000']),
'solver_cuda': ('08_seed_cuda', [*fixed, '--threads', '256', '--max-results', '1000']),
}
collected = {name: [] for name in commands}
# Each process runs its own kernel/algorithm; extra unrecorded passes warm machine clocks.
for name, (binary, args) in commands.items():
native(binary, args)
names = list(commands)
expected = None
for repeat in range(repeats):
for name in names[repeat % 4:] + names[:repeat % 4]:
binary, args = commands[name]
raw = native(binary, args)
raw['metrics'] = fields(raw['stdout'])
if name.startswith('solver'):
answers = seed_results(raw['stdout'])
if len(answers) != 24 or 61692151016558 not in answers:
raise RuntimeError('fixed-position solver did not recover 24 known seeds')
if expected is None:
expected = answers
elif expected != answers:
raise RuntimeError('CPU/CUDA solver seed tuples mismatch')
collected[name].append(raw)
result = {'raw': collected, 'seeds': expected, 'equality': True}
for name, rows in collected.items():
key = 'milliseconds' if 'cuda' in name else 'seconds'
scale = 1 if key == 'milliseconds' else 1000
result[name] = dict(compute=summarize([r['metrics'][key]*scale for r in rows]),
process=summarize([r['process_ms'] for r in rows]))
if name.startswith('brute'):
result[name]['projected_full48_ms'] = statistics.median(
r['metrics']['projected_full48_seconds']*1000 for r in rows)
return result
def upper(repeats):
print('隔离测量高 13 位:枚举与解析求解', flush=True)
raw = native('08_upper_compare', ['--planes', '65536', '--repeats', str(repeats)])
return dict(raw=raw, data=json.loads(raw['stdout']))
def report(data):
text = [
'# show:逐步实现与本次实测', '',
f"测量时间:{data['metadata']['date']}(Asia/Hong_Kong)。所有表格由 `benchmark.py` 自动生成。",
'CPU:AMD Ryzen 7 5800H,WSL,16 个逻辑线程。GPU 型号见下面逐项输出;编译器、驱动、源码 SHA256 和全部样本保存在 `results/latest.json`。',
'', '## 1. 分段对比:相邻版本在同一负载下测试', '',
'主线分成 A、B、C 三组,横线表示更换测试负载。分界两边重复测量衔接版本:A/B 都有 02,B/C 都有 03;只在组内计算速度比。展平实现已移到 experiments,不属于这个阶段。',
'原来只有 64 个 seed 的极端负载移到 `results/mapping_diagnostics.json`,用于解释并行度,不再作为主线 02→03 的优化倍数。',
'GPU 各对照版本核对完整 `(seed,x,z)` 命中列表;所有 GPU 命中另由独立 Python Java 算术核验。A/C 与完整 CPU 扫描核对,B 的独立 CPU 验证采用明确列出的窗口。每个版本先跑实际工作量预热,GPU 测量顺序轮换。',
'CPU 计时包含线程创建、搜索与结果收集;GPU kernel 时间用 CUDA event,scan 时间另含清零、同步与结果回传。两者均不含结果排序,GPU scan 不含一次性的上下文创建及内存分配。',
'A 的 01→02 用 CPU 搜索时间和 GPU scan 时间比较;B/C 的相邻 CUDA 版本用 kernel 时间比较。B/C 的 CPU 仅用于正确性验证,不列入性能表。', '',
]
if 'mapping_updated_at' in data['metadata']:
text += [f"线程映射分段改测时间:{data['metadata']['mapping_updated_at']}。后面的算法实验沿用上方日期的实测数据,分段实验测量时的源码 SHA256 记录在 metadata.mapping_source_sha256,C 单独复测的源码记录在 metadata.C_source_sha256。", '']
if 'C_updated_at' in data['metadata']:
text += [f"C 组复测时间:{data['metadata']['C_updated_at']},100 轮轮换测量。", '']
labels = {2:'02 CUDA/每个 thread 一个 seed', 3:'03 CUDA/固定坐标,复用多项式后遍历 seed',
4:'04 CUDA/tiled outer sum'}
ids = {2:'02', 3:'03', 4:'04'}
for index, case in enumerate(data['mapping']):
a, cpu, gpu = case['case'], case['cpu'], case['gpu']
if index:
text += ['---', '', '**测试负载分界:下一组重新测量衔接版本,不跨横线计算加速比。**', '']
text += [f"### {a['name']}", '',
f"`seed={a['seed']}` 起共 {a['seeds']:,} 个 seed,{a['width']}×{a['height']} 个锚点,实心 5×5。候选总数 {gpu['candidate_count']:,}。",
f"GPU:{gpu['gpu']}。结果完全一致,命中 {len(gpu['hits'])} 个元组;每版完整预热一次,计时 {len(gpu['rows'][0]['kernel_samples_ms'])} 轮。", '',
'| 版本 | CPU 搜索/GPU kernel 中位数 | GPU scan 中位数 |',
'| --- | ---: | ---: |']
if a['cpu_in_table']:
text.append(f"| 01 CPU | {cpu['median_ms']:.3f} ms | — |")
for row in gpu['rows']:
t = row['kernel_median_ms']
text.append(f"| {labels[row['mode']]} | {t:.3f} ms | {row['scan_median_ms']:.3f} ms |")
by_id = {ids[row['mode']]: row for row in gpu['rows']}
text += ['', '| 相邻/分支对照 | 同口径速度比(前项时间/后项时间) |', '| --- | ---: |']
for before, after in a['comparisons']:
if before == '01':
ratio = cpu['median_ms']/by_id[after]['scan_median_ms']
region = 'CPU 搜索/GPU scan'
else:
ratio = by_id[before]['kernel_median_ms']/by_id[after]['kernel_median_ms']
region = 'GPU kernel'
text.append(f'| {before} → {after}({region}) | {ratio:.3f}× |')
if index == 0:
text += ['', '65536 个 seed 为版本 02 提供足够的独立线程,在 257×257 的完整地图上测量 CPU→CUDA。']
elif index == 1:
text += ['', '本组使用 65536 个 seed,坐标范围扩大到 4097×4097(16785409 个坐标任务)。02/03 都完整扫描相同的笛卡尔积,GPU 命中列表完全一致。',
'独立 CPU 核对全部 65536 个 seed 在 `[50941,51197] × [418996,419252]` 的 257×257 窗口;这是窗口验证,不是完整大范围 CPU 重扫。所有 GPU 命中均另作独立精确算术验证。']
else:
text += ['', '本组改为固定 seed,重新测量 03 后再比较 04。03、04 均保持固定坐标后遍历 seed 的策略。']
text += ['']
text += ['随机数性质的探索独立成章,见 [RANDOM_PROPERTIES.md](RANDOM_PROPERTIES.md)。从状态空间、低位依赖和模运算约束推导搜索方法,不列为局部计时优化。', '',
'## 2. 固定 seed:全坐标范围余数过滤', '']
r = data['residue']
# Project only from the largest measured rectangle, not from a tiny launch.
rectangular = data['mapping'][-1]
direct = next(row for row in rectangular['gpu']['rows'] if row['mode']==3)
projected = direct['kernel_median_ms'] * r['counts']['world_positions']/rectangular['gpu']['candidate_count']
text += [f"固定 seed `61692151016558`;两轴锚点范围 `[-1875000,1874999]`,总计 {r['counts']['world_positions']:,} 个位置。",
f"完整扫描 2^36 个余数对,保留 {r['counts']['residue_survivors']:,} 个,展开 {r['counts']['expanded_candidates']:,} 个候选,得到 `(51069,419124)`。", '',
'| 时间区域 | 中位数 |', '| --- | ---: |',
f"| 余数 GPU kernel(实测) | {r['kernel']['median_ms']/1000:.3f} s |",
f"| 表准备+kernel+CPU 精查(实测) | {r['compute']['median_ms']/1000:.3f} s |",
f"| 独立进程全流程(实测) | {r['process']['median_ms']/1000:.3f} s |",
f"| 按 8193×8193 直接扫描吞吐外推全范围 | {projected/1000:.1f} s |", '',
f"外推直接 kernel 相对余数实测计算流程约 {projected/r['compute']['median_ms']:.1f}×。分母包含余数方法的准备和 CPU 精查;没有声称实际跑完直接全范围搜索。",
'07 沿用 research 的首次输出候选模型,尚未覆盖 Java 拒绝尾部例外。锚点范围允许图案向外延伸四格。', '',
'## 3. 固定坐标:seed 搜索', '']
s = data['seeds']
text += ['锚点 `(51069,419124)`、实心 5×5。CPU/CUDA 解析流程均恢复同一组 24 个已知 seed。', '',
'| 版本 | 实测规模 | 搜索/kernel 中位数 | 进程全流程中位数 | 完整 2^48 外推 |',
'| --- | --- | ---: | ---: | ---: |']
for name, scale in [('brute_cpu','2^28 seed'), ('brute_cuda','2^34 seed'),
('solver_cpu','全部 lower35 候选+upper13 解析'),
('solver_cuda','全部 lower35 候选+upper13 解析')]:
row = s[name]
projection = f"{row['projected_full48_ms']/1000:.1f} s" if 'projected_full48_ms' in row else '—'
text.append(f"| {name} | {scale} | {row['compute']['median_ms']:.3f} ms | {row['process']['median_ms']:.3f} ms | {projection} |")
text += ['', f"同 GPU 算法比值:暴力完整空间外推/解析 kernel 约 {s['brute_cuda']['projected_full48_ms']/s['solver_cuda']['compute']['median_ms']:,.0f}×。",
'solver_cpu 的程序内 seconds 计时从通过筛选的低 18 位取值 生成之后开始;进程全流程覆盖全部准备。CUDA 的 milliseconds 为 kernel 时间。',
'解析流程是首次输出候选模型,不是含全部拒绝尾部分支的严格穷尽。', '',
'### 高 13 位优化的受控拆解', '',
'相同的 65536 个连续 lower35 平面,从 `16420645998` 开始。枚举与解析都使用首次输出候选模型,并准确验证最终候选。',
'这里的 lower35 指世界种子的低 35 位。', '',
'| 高位策略 | kernel 中位数 |', '| --- | ---: |']
rows = data['upper']['data']['rows']
for row in rows:
text.append(f"| {row['mode']} | {row['kernel_median_ms']:.3f} ms |")
text += ['', f"完整候选结果相同。局部解析加速 {rows[0]['kernel_median_ms']/rows[1]['kernel_median_ms']:.3f}×,只适用于这组供给的 lower35 平面。", '']
text += ['## 复现与边界', '',
'```bash', 'make -C show -j4', 'python3 show/test_steps.py --gpu',
f"python3 show/benchmark.py --repeats {data['metadata']['repeats']}",
'```', '',
'GPU 运行需要宿主 GPU 可用。计时期间 GPU 同时承担桌面工作,时钟、温度和系统负载会影响数字;全部原始样本可检查波动。',
'01–04 精确覆盖被请求的有限输入范围。07/08 的生成器保留研究原型的拒绝尾部覆盖限制;全 seed × 全世界联合生成仍未实现。',
'本报告不把“潜在笛卡尔积大小”当作实际访问次数,也不把局部加速比连乘。', '']
return '\n'.join(text)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--repeats', type=int, default=5)
parser.add_argument('--only-mapping', action='store_true',
help='remeasure A/B/C and regenerate report, retaining unchanged algorithm phases')
args = parser.parse_args()
if args.repeats < 1:
parser.error('repeats must be positive')
if args.only_mapping:
path = ROOT/'results/latest.json'
data = json.loads(path.read_text())
mapping_files = {'01_cpu.cpp', 'cuda_steps.cu', '02_cuda_seed.cuh',
'03_cuda_position.cuh', '04_cuda_outer_sum.cuh'}
current_mapping_sources = {}
for name, digest in data['metadata']['source_sha256'].items():
current = hashlib.sha256((ROOT/name).read_bytes()).hexdigest()
if name in mapping_files:
current_mapping_sources[name] = current
elif current != data['metadata'].get('verification_source_sha256', {}).get(name, digest):
raise RuntimeError('shared/algorithm source changed; run the full benchmark: '+name)
diagnostics = ROOT/'results/mapping_diagnostics.json'
if not diagnostics.exists():
diagnostics.write_text(json.dumps(dict(metadata=data['metadata'], mapping=data['mapping'],
note='Original imbalance experiments; diagnostics only, not the main adjacent-version comparison.'),
ensure_ascii=False, indent=2)+'\n')
data['mapping'] = mapping(args.repeats)
data['metadata']['mapping_updated_at'] = datetime.now(ZoneInfo('Asia/Hong_Kong')).isoformat()
data['metadata']['mapping_repeats'] = args.repeats
data['metadata']['mapping_source_sha256'] = current_mapping_sources
data['metadata']['C_updated_at'] = data['metadata']['mapping_updated_at']
data['metadata']['C_source_sha256'] = {name: digest for name, digest in current_mapping_sources.items() if name != '01_cpu.cpp'}
data['metadata']['C_source_sha256']['common.hpp'] = hashlib.sha256((ROOT/'common.hpp').read_bytes()).hexdigest()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
(ROOT/'BENCHMARK.md').write_text(report(data))
print('完成分段改测:show/BENCHMARK.md 和 show/results/latest.json', flush=True)
return
cpu = next(line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines()
if line.startswith('model name'))
paths = sorted(ROOT.glob('*.cpp'))+sorted(ROOT.glob('*.cu'))+sorted(ROOT.glob('*.cuh'))+[
ROOT/'common.hpp', ROOT/'../research/slime_solver_core.hpp']
data = {'metadata': dict(date=datetime.now(ZoneInfo('Asia/Hong_Kong')).isoformat(),
cpu=cpu, platform=platform.platform(), repeats=args.repeats,
gpu=capture(['nvidia-smi', '--query-gpu=name,driver_version,temperature.gpu,power.draw,utilization.gpu', '--format=csv']),
nvcc=capture(['nvcc', '--version']), gxx=capture(['g++', '--version']),
source_sha256={str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in paths})}
# Checkpoint after each phase, so an interrupted long run preserves its completed evidence.
out = ROOT/'results'
out.mkdir(exist_ok=True)
for name, function in [('mapping', lambda: mapping(args.repeats)),
('residue', lambda: residue(args.repeats)),
('seeds', lambda: seeds(args.repeats)),
('upper', lambda: upper(args.repeats))]:
data[name] = function()
(out/'latest.json').write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
(ROOT/'BENCHMARK.md').write_text(report(data))
print('完成:show/BENCHMARK.md 和 show/results/latest.json', flush=True)
if __name__ == '__main__':
main()