Files
SystemSimulationApp/tests/manual/benchmark_native_jacobian_reuse.py
T
ljz 1aac220084 优化 Jacobian 确定性复用并补充性能剖析与平台依赖文档
在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。

增加独立的跨平台时间剖析工具,记录互斥阶段耗时、Newton/LU 统计、矩阵复用与内核复用,保存 UD00 两种工况的调查报告和机器可读汇总。

补充 Windows/Linux 运行、测试、原生编译和剖析所需依赖文档及索引,不修改依赖清单、版本锁或安装环境。

验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
2026-09-16 13:39:53 +08:00

133 lines
7.2 KiB
Python

"""Freeze native workers and compare exact outputs before/after Jacobian reuse.
Use prepare --name before before editing production sources. Then prepare after.
Run measured workers serially, outside compilation or verification activity.
"""
import argparse
import hashlib
import json
from pathlib import Path
import platform
import shutil
import statistics
import subprocess
import sys
import time
ROOT=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(ROOT))
from app.main import compile_system_xml_network
from app.simulation.native_codegen.build import build_native
from app.simulation.native_codegen.compiler import compile_native_program
from app.simulation.native_codegen.input import load_input
OUT=ROOT/'test/jacobian-reuse-20260915'
MODEL=ROOT/'tests/data/test-mql-8-corrected.json'
EXACT=('statesSha256','outputsSha256','finalState','final','propertyWarnings',
'nfev','acceptedSteps','rejectedSteps','njev','nlu','stateTransitions','solverStarts')
def digest(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream,'sha256').hexdigest()
def write(path,data):
path.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
def prepare(name,cyclic):
variant=MODEL
if cyclic:
project=json.loads(MODEL.read_text(encoding='utf-8'))
for node in project['nodes']:
if node['data'].get('modelType')=='amesim_ud00':
node['data']['parameters']['iscyclic']=1
project['simulation']['t_stop']=21.7
variant=OUT/'cyclic-project.json'
write(variant,project)
_,document=load_input(variant)
program=compile_native_program(compile_system_xml_network(document))
build=build_native(program)
try:
target=OUT/(name+('-cyclic' if cyclic else ''))
shutil.copytree(build.executable.parent,target)
sources=list((ROOT/'app/simulation/native_codegen').glob('*.py'))+list((ROOT/'native').rglob('*.c'))+list((ROOT/'native/include').glob('*.h'))
write(target/'comparison-metadata.json',dict(buildKey=build.manifest['buildKey'],
inputSha256=digest(variant),sourceHashes={p.relative_to(ROOT).as_posix():digest(p) for p in sources},
platform=platform.platform(),buildSeconds=build.seconds,details=build.details,
sourceRevision=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()))
print(json.dumps(dict(build=target.name,key=build.manifest['buildKey'],seconds=build.seconds)),flush=True)
finally:
build.close()
def run(name,label,cyclic,stop,verify=False):
build=OUT/(name+('-cyclic' if cyclic else ''))
target=OUT/(name+('-cyclic-' if cyclic else '-')+label)
target.mkdir()
command=[str(build/('model.exe' if sys.platform=='win32' else 'model')),
'--method','BDF','--start','0','--stop',str(stop),'--sample-step','.01',
'--rtol','1e-8','--max-step','1e30','--timeout','300',
'--sample-file',str(target/'states.bin'),'--output-block-file',str(target/'outputs.bin'),
'--output',str(target/'result.json')]
if verify: command.append('--verify-jacobian')
start=time.perf_counter()
with (target/'stderr.log').open('w',encoding='utf-8') as log:
process=subprocess.run(command,cwd=build,stdout=subprocess.DEVNULL,stderr=log,timeout=330)
elapsed=time.perf_counter()-start
result=json.loads((target/'result.json').read_text(encoding='utf-8'))
assert process.returncode==0 and result['success'],result.get('message')
record={k:result[k] for k in EXACT if k not in ('statesSha256','outputsSha256')}
record.update(name=name,label=label,cyclic=cyclic,stop=stop,verify=verify,
processWallSeconds=elapsed,solveSeconds=result['solveSeconds'],solveCpuSeconds=result['solveCpuSeconds'],
statesSha256=digest(target/'states.bin'),outputsSha256=digest(target/'outputs.bin'),
jacobianChecks=result['jacobianChecks'],jacobianMismatches=result['jacobianMismatches'],
jacobianReuse=result.get('jacobianReuse'),sampleCount=len(result['series']['time']))
write(target/'measurement.json',record)
print(json.dumps({k:v for k,v in record.items() if k not in ('finalState','final','propertyWarnings')}),flush=True)
def report():
records=[json.loads(p.read_text(encoding='utf-8')) for p in sorted(OUT.glob('*-run-*/measurement.json'))]
assert len(records)>=6
checks=[dict(run=r['name']+'-'+r['label'],differences=[k for k in EXACT if r[k]!=records[0][k]]) for r in records]
medians={name:{key:statistics.median(r[key] for r in records if r['name']==name)
for key in ('solveCpuSeconds','solveSeconds','processWallSeconds')} for name in ('before','after')}
cases=[]
for cyclic,suffix in ((False,'noncyclic-check'),(True,'cyclic-check')):
pair=[json.loads((OUT/f'{name}-{suffix}'/'measurement.json').read_text(encoding='utf-8')) for name in ('before','after')]
case=dict(cyclic=cyclic,stop=21.7,sampleCount=pair[0]['sampleCount'],
differences=[k for k in EXACT if pair[0][k]!=pair[1][k]],
measurements=[{k:v for k,v in r.items() if k not in ('finalState','final','propertyWarnings')} for r in pair])
cases.append(case)
verification=[]
for suffix,reference_suffix in (('verify-check','run-0'),('cyclic-verify-check','cyclic-check')):
checked=json.loads((OUT/f'after-{suffix}'/'measurement.json').read_text(encoding='utf-8'))
reference=json.loads((OUT/f'after-{reference_suffix}'/'measurement.json').read_text(encoding='utf-8'))
differences=[k for k in EXACT if k!='nfev' and checked[k]!=reference[k]]
assert checked['jacobianMismatches']==0 and checked['jacobianChecks']==reference['njev']
assert checked['nfev']==reference['nfev']+checked['jacobianChecks']*len(reference['finalState'])
assert checked['jacobianReuse']==reference['jacobianReuse']
verification.append(dict(run=suffix,checks=checked['jacobianChecks'],mismatches=checked['jacobianMismatches'],differences=differences))
summary=dict(checks=checks,medians=medians,ud00=cases,verification=verification,measurements=records,
scope='One warmup per build excluded. Three alternating serial measurements at 0-10s; UD00 modes separately compared at 0-21.7s. Full native worker with raw samples; excludes build/API/browser.')
write(OUT/'comparison.json',summary)
print(json.dumps(dict(checks=checks,medians=medians,ud00=cases,verification=verification),indent=2))
assert not any(c['differences'] for c in checks+cases+verification),'Numerical comparison failed'
if __name__=='__main__':
parser=argparse.ArgumentParser()
parser.add_argument('action',choices=('prepare','run','report'))
parser.add_argument('--name',choices=('before','after'),default='before')
parser.add_argument('--label',default='warmup')
parser.add_argument('--cyclic',action='store_true')
parser.add_argument('--verify',action='store_true')
parser.add_argument('--stop',type=float,default=10)
args=parser.parse_args()
OUT.mkdir(parents=True,exist_ok=True)
if args.action=='prepare': prepare(args.name,args.cyclic)
elif args.action=='run': run(args.name,args.label,args.cyclic,args.stop,args.verify)
else: report()