324 lines
21 KiB
Python
324 lines
21 KiB
Python
#!/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()
|