优化 Jacobian 确定性复用并补充性能剖析与平台依赖文档
在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。 增加独立的跨平台时间剖析工具,记录互斥阶段耗时、Newton/LU 统计、矩阵复用与内核复用,保存 UD00 两种工况的调查报告和机器可读汇总。 补充 Windows/Linux 运行、测试、原生编译和剖析所需依赖文档及索引,不修改依赖清单、版本锁或安装环境。 验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
This commit is contained in:
1 parent
2b07d996cf
commit
1aac220084
26 files changed
+2293
-13
No files matched your search
@@ -0,0 +1,132 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Profile the current native BDF solver in an isolated diagnostic source copy.
|
||||
|
||||
Example: python tests/manual/profile_native_solver.py --run
|
||||
Builds BOTH UD00 modes before timing; one excluded warmup and three serial pairs
|
||||
per mode by default. Production sources and cached workers are not instrumented.
|
||||
Requires the production GCC toolchain and SUNDIALS 7.4 public interfaces.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
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, toolchain, platform_build_inputs, link_library_arguments, _command,
|
||||
RUNTIME_SOURCES,
|
||||
)
|
||||
from app.simulation.native_codegen.compiler import compile_native_program
|
||||
from app.simulation.native_codegen.input import load_input
|
||||
from app.simulation.native_codegen.modules import component_modules
|
||||
from tests.manual.native_compute_profile import replace_once, scope_function
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
APIS = {
|
||||
'STEPS': 'CVodeGetNumSteps', 'RHS': 'CVodeGetNumRhsEvals',
|
||||
'LINEAR_RHS': 'CVodeGetNumLinRhsEvals', 'JACOBIANS': 'CVodeGetNumJacEvals',
|
||||
'SETUPS': 'CVodeGetNumLinSolvSetups', 'ERROR_FAILS': 'CVodeGetNumErrTestFails',
|
||||
'NEWTON_ITERS': 'CVodeGetNumNonlinSolvIters',
|
||||
'NEWTON_FAILS': 'CVodeGetNumNonlinSolvConvFails',
|
||||
'STEP_SOLVE_FAILS': 'CVodeGetNumStepSolveFails',
|
||||
}
|
||||
# Every exclusive scope occurs exactly once in this disjoint partition.
|
||||
PARTITION = {
|
||||
'residual_evaluation': ['rhs', 'nonlinear_residual'],
|
||||
'jacobian_assembly': ['jacobian', 'jacobian_probe'],
|
||||
'numerical_factorization': ['numerical_factorization'],
|
||||
'linear_solve': ['linear_solve'],
|
||||
'linear_system_setup_other': ['linear_setup'],
|
||||
'newton_overhead': ['newton'],
|
||||
'cvode_controller_including_error_estimation': ['cvode_controller'],
|
||||
'event_detection': ['event_detection'],
|
||||
'accepted_property_checks': ['accepted_property_checks'],
|
||||
'sampling_and_storage': ['sampling', 'append', 'flush'],
|
||||
'output_replay': ['output_replay'],
|
||||
'result_encoding': ['result_encoding'],
|
||||
'poll_and_progress': ['poll'],
|
||||
'framework_other': ['total', 'integration'],
|
||||
}
|
||||
|
||||
|
||||
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 scope(text, function, category):
|
||||
return scope_function(text, function, category).replace('PROFILE_' + category.upper(), 'P_' + category.upper())
|
||||
|
||||
|
||||
def instrument(native):
|
||||
for name in ('solver_profile.h', 'solver_profile_hooks.h'):
|
||||
shutil.copy2(HERE / name, native / 'include' / name)
|
||||
shutil.copy2(HERE / 'solver_profile.c', native / 'runtime/solver_profile.c')
|
||||
for filename, functions in {
|
||||
'common.c': {'native_poll': 'poll', 'native_accept': 'event',
|
||||
'native_append': 'append', 'check_property_temperatures': 'property'},
|
||||
'cvode_solver.c': {'native_bdf': 'integration', 'cv_rhs': 'rhs',
|
||||
'cv_jacobian': 'jacobian', 'jac_rhs': 'jacobian_probe'},
|
||||
'sample_storage.c': {'native_samples_flush': 'flush', 'native_samples_outputs': 'replay'},
|
||||
'main.c': {'main': 'total', 'write_result': 'encoding'},
|
||||
}.items():
|
||||
path = native / 'runtime' / filename
|
||||
text = '#include "solver_profile.h"\n' + path.read_text(encoding='utf-8')
|
||||
for name, category in functions.items():
|
||||
text = scope(text, name, category)
|
||||
if filename == 'common.c':
|
||||
text = replace_once(text, ' if (r->options.record_samples) {',
|
||||
' if (r->options.record_samples) {\n PROFILE_SCOPE(P_SAMPLING);')
|
||||
text = replace_once(text, ' return model_friction_drives(t,state,drives);',
|
||||
' profile_add(C_FRICTION_EVALS,0,1);\n return model_friction_drives(t,state,drives);')
|
||||
elif filename == 'cvode_solver.c':
|
||||
text = replace_once(text, 'typedef struct { void *solver;',
|
||||
'#include "solver_profile_hooks.h"\ntypedef struct { void *solver;')
|
||||
text = replace_once(text, ' if(!context->jacobian_workspace)return jac_rhs(context,t,y,f);',
|
||||
' if(!context->jacobian_workspace)return jac_rhs(context,t,y,f);\n PROFILE_SCOPE(P_JACOBIAN_PROBE);')
|
||||
text = replace_once(text, ' int success=0, initialized=0;',
|
||||
' SUNNonlinearSolver profile_nls=NULL;\n int success=0, initialized=0;')
|
||||
text = replace_once(text, ' if (!linear) goto allocation_failure;',
|
||||
' if (!linear) goto allocation_failure;\n'
|
||||
' original_lu=linear->ops->setup;original_solve=linear->ops->solve;\n'
|
||||
' linear->ops->setup=profiled_lu;linear->ops->solve=profiled_solve;')
|
||||
text = replace_once(text, ' CV_CHECK(CVodeSetMaxStep(solver,r->options.max_step));',
|
||||
' CV_CHECK(CVodeSetMaxStep(solver,r->options.max_step));\n'
|
||||
' profile_nls=profile_attach_newton(y,ctx);\n'
|
||||
' allocation="SUNNonlinSol_Newton (profile)";\n'
|
||||
' if(!profile_nls)goto allocation_failure;\n'
|
||||
' CV_CHECK(CVodeSetNonlinearSolver(solver,profile_nls));')
|
||||
text = replace_once(text, ' if (linear) SUNLinSolFree(linear);',
|
||||
' if(profile_nls)SUNNonlinSolFree(profile_nls);\n if (linear) SUNLinSolFree(linear);')
|
||||
text = replace_once(text, 'int flag=CVode(solver,end,y,&next,CV_ONE_STEP);',
|
||||
'int flag=profiled_cvode(solver,end,y,&next,CV_ONE_STEP);')
|
||||
text = replace_once(text, ' if (t<r->options.stop) {',
|
||||
' if (t<r->options.stop) {\n profile_add(C_BOUNDARIES,0,1);')
|
||||
snapshots = '\n profile_add(C_SEGMENTS,0,1);\n'
|
||||
for tag, api in APIS.items():
|
||||
snapshots += f' {{long int n=0;int code={api}(solver,&n);profile_add(C_{tag},code,n);}}\n'
|
||||
text = replace_once(text, 'static void counters(NativeRun *r, void *solver) {',
|
||||
'static void counters(NativeRun *r, void *solver) {' + snapshots)
|
||||
elif filename == 'main.c':
|
||||
text = replace_once(text, ' native_run_free(&r); return code;',
|
||||
' native_run_free(&r); profile_end(&profile_scope);profile_dump();return code;')
|
||||
path.write_text(text, encoding='utf-8', newline='\n')
|
||||
|
||||
|
||||
def prepare(args):
|
||||
out = args.output.resolve()
|
||||
if not out.is_relative_to(ROOT / 'test') or out.exists():
|
||||
raise ValueError('Use a NEW output directory beneath the project test/ directory')
|
||||
if args.repeats < 1 or args.warmups < 0:
|
||||
raise ValueError('repeats must be positive and warmups nonnegative')
|
||||
out.mkdir(parents=True)
|
||||
compiler, sundials, compiler_version = toolchain()
|
||||
flags, libraries, dlls, executable = platform_build_inputs(sundials)
|
||||
project = json.loads(args.input.read_text(encoding='utf-8'))
|
||||
variants = []
|
||||
for cyclic in (False, True):
|
||||
case = out / ('cyclic' if cyclic else 'noncyclic')
|
||||
case.mkdir()
|
||||
ud00 = [n for n in project['nodes'] if n['data'].get('modelType') == 'amesim_ud00']
|
||||
if not ud00:
|
||||
raise ValueError('UD00 comparison requires at least one UD00 signal')
|
||||
for node in ud00:
|
||||
node['data']['parameters']['iscyclic'] = int(cyclic)
|
||||
project['simulation']['t_stop'] = args.stop
|
||||
write(case / 'input.json', project)
|
||||
_, document = load_input(case / 'input.json')
|
||||
program = compile_native_program(compile_system_xml_network(document))
|
||||
# The assembly timer observes our callback; CVODE's private dense-DQ
|
||||
# fallback has no separate public timer. Reject instead of mislabelling.
|
||||
if '#define MODEL_JACOBIAN_COLORED 1' not in program.header:
|
||||
raise ValueError('This diagnostic currently requires generated colored Jacobian support')
|
||||
build = build_native(program)
|
||||
try:
|
||||
shutil.copytree(build.executable.parent, case / 'control')
|
||||
manifest = build.manifest
|
||||
finally:
|
||||
build.close()
|
||||
native = case / 'native'
|
||||
shutil.copytree(ROOT / 'native', native)
|
||||
target = case / 'profiled'
|
||||
target.mkdir()
|
||||
for name in ('model.c', 'model.h', 'THIRD_PARTY_NOTICES.txt'):
|
||||
shutil.copy2(case / 'control' / name, target / name)
|
||||
for dll in dlls:
|
||||
shutil.copy2(dll, target / dll.name)
|
||||
instrument(native)
|
||||
sources = [native / name for name in RUNTIME_SOURCES]
|
||||
sources += [native / 'components/modules' / f'{name}.c'
|
||||
for name in component_modules(program.source + '\n' + program.header)]
|
||||
sources.append(native / 'runtime/solver_profile.c')
|
||||
command = [compiler, *flags, '-I', str(target), '-I', str(native / 'include'),
|
||||
'-I', str(sundials / 'include'), str(target / 'model.c'), *map(str, sources),
|
||||
*link_library_arguments(libraries), '-lm', '-o', str(target / executable)]
|
||||
log = []
|
||||
try:
|
||||
_command(command, log=log, timeout=180)
|
||||
finally:
|
||||
(case / 'build.log').write_text('\n'.join(log), encoding='utf-8')
|
||||
assert digest(target / 'model.c') == digest(case / 'control/model.c')
|
||||
variants.append(dict(name=case.name, ud00Count=len(ud00), buildKey=manifest['buildKey'],
|
||||
executable=executable, inputSha256=digest(case / 'input.json'),
|
||||
controlSha256=digest(case / 'control' / executable),
|
||||
profiledSha256=digest(target / executable),
|
||||
nativeSourceHashes={p.relative_to(ROOT / 'native').as_posix(): digest(p)
|
||||
for p in (ROOT / 'native').rglob('*') if p.is_file()},
|
||||
instrumentedHashes={p.relative_to(native).as_posix(): digest(p)
|
||||
for p in native.rglob('*') if p.is_file()}))
|
||||
print(f'Prepared {case.name}: {manifest["buildKey"]}', flush=True)
|
||||
result = dict(variants=variants, stop=args.stop, repeats=args.repeats, warmups=args.warmups,
|
||||
compiler=compiler_version, platform=sys.platform,
|
||||
revision=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
|
||||
settings=dict(method='BDF', start=0, sample_step=.01, rtol=1e-8, max_step=1e30),
|
||||
scope='Native worker: model initialization through result encoding and cleanup. '
|
||||
'Excludes build, process launch, API and browser. Error estimation remains inside CVODE controller. '
|
||||
'Dense LU has no symbolic factorization. Explicit Newton uses the same library implementation.')
|
||||
write(out / 'prepared.json', result)
|
||||
return result
|
||||
|
||||
|
||||
def analyze(profile, result):
|
||||
assert profile['counterErrors'] == 0
|
||||
s, c = profile['scopes'], profile['counters']
|
||||
assert set(s) == {k for group in PARTITION.values() for k in group}
|
||||
times = {name: sum(s[k]['exclusiveSeconds'] for k in scopes) for name, scopes in PARTITION.items()}
|
||||
total = s['total']['inclusiveSeconds']
|
||||
assert math.isclose(sum(times.values()), total, abs_tol=1e-8, rel_tol=1e-10)
|
||||
assert all(t >= 0 for t in times.values())
|
||||
for name, key in (('residual_evaluations', 'cvodeRhsCalls'), ('linear_rhs_evaluations', 'cvodeLinearRhsCalls'),
|
||||
('jacobian_evaluations', 'njev'), ('linear_setups', 'nlu'),
|
||||
('error_test_failures', 'rejectedSteps'), ('counter_segments', 'solverStarts')):
|
||||
assert c[name] == result[key], (name, c[name], result[key])
|
||||
assert s['jacobian']['calls'] == c['jacobian_evaluations']
|
||||
assert s['numerical_factorization']['calls'] == c['linear_setups']
|
||||
assert c['jacobian_reuses'] + c['jacobian_refresh_setups'] + c['linear_setup_failures'] == c['linear_setups']
|
||||
assert s['rhs']['calls'] == c['residual_evaluations'] + c['linear_rhs_evaluations']
|
||||
assert result['nfev'] == s['rhs']['calls'] + result['jacobianRhsCalls'] + c['event_model_evaluations']
|
||||
c.update(rejected_steps=c['error_test_failures'] + c['nonlinear_step_failures'],
|
||||
application_accepted_steps=result['acceptedSteps'],
|
||||
same_time_returns=result['solverControl']['sameTimeReturns'],
|
||||
max_same_time_streak=result['solverControl']['maxSameTimeStreak'],
|
||||
nonlinear_residual_calls=s['nonlinear_residual']['calls'],
|
||||
jacobian_probe_evaluations=result['jacobianRhsCalls'],
|
||||
LU_factorizations=s['numerical_factorization']['calls'], LU_solves=s['linear_solve']['calls'],
|
||||
newton_solve_calls=s['newton']['calls'], event_count=result['stateTransitions'],
|
||||
event_detection_calls=s['event_detection']['calls'], solver_starts=result['solverStarts'],
|
||||
all_counted_model_evaluations=result['nfev'],
|
||||
accepted_property_checks=s['accepted_property_checks']['calls'],
|
||||
sample_count=len(result['series']['time']), jacobian_kernel_reuse=result['jacobianReuse'])
|
||||
return dict(totalSeconds=total, exclusiveSeconds=times, counters=c)
|
||||
|
||||
|
||||
def run(args, prepared):
|
||||
rows = []
|
||||
for variant in prepared['variants']:
|
||||
case = args.output.resolve() / variant['name']
|
||||
baseline = None
|
||||
for index in range(-args.warmups, args.repeats):
|
||||
label = f'warmup-{index + args.warmups}' if index < 0 else f'run-{index}'
|
||||
for kind in ('control', 'profiled'):
|
||||
dest = case / f'{kind}-{label}'
|
||||
dest.mkdir()
|
||||
command = [str(case / kind / variant['executable']), '--method', 'BDF', '--start', '0',
|
||||
'--stop', str(args.stop), '--sample-step', '.01', '--rtol', '1e-8',
|
||||
'--max-step', '1e30', '--timeout', '300', '--output', str(dest / 'result.json'),
|
||||
'--sample-file', str(dest / 'states.bin'), '--output-block-file', str(dest / 'outputs.bin')]
|
||||
env = dict(os.environ)
|
||||
env.pop('NATIVE_SOLVER_PROFILE', None)
|
||||
if kind == 'profiled':
|
||||
env['NATIVE_SOLVER_PROFILE'] = str(dest / 'profile.json')
|
||||
start = time.perf_counter()
|
||||
with (dest / 'stderr.log').open('wb') as err:
|
||||
process = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=err, env=env, timeout=330)
|
||||
elapsed = time.perf_counter() - start
|
||||
result = json.loads((dest / 'result.json').read_bytes())
|
||||
assert process.returncode == 0 and result['success'], str(dest)
|
||||
comparable = {k: v for k, v in result.items() if k not in ('solveSeconds', 'solveCpuSeconds')}
|
||||
comparable.update(statesSha256=digest(dest / 'states.bin'), outputsSha256=digest(dest / 'outputs.bin'))
|
||||
if baseline is None:
|
||||
baseline = comparable
|
||||
differences = [k for k in baseline.keys() | comparable.keys() if baseline.get(k) != comparable.get(k)]
|
||||
assert not differences, (str(dest), differences)
|
||||
row = dict(case=case.name, variant=kind, label=label, warmup=index < 0,
|
||||
processWallSeconds=elapsed, solveSeconds=result['solveSeconds'],
|
||||
solveCpuSeconds=result['solveCpuSeconds'], fullParity=True,
|
||||
statesSha256=comparable['statesSha256'], outputsSha256=comparable['outputsSha256'])
|
||||
if kind == 'profiled':
|
||||
row['profile'] = analyze(json.loads((dest / 'profile.json').read_bytes()), result)
|
||||
write(dest / 'measurement.json', row)
|
||||
rows.append(row)
|
||||
print(f'{case.name}/{kind}/{label}: solve {result["solveSeconds"]:.4f}s; parity OK', flush=True)
|
||||
# Release the large decoded series before the next case.
|
||||
del baseline
|
||||
write(args.output / 'measurements.json', rows)
|
||||
return summarize(args.output, prepared, rows)
|
||||
|
||||
|
||||
def summarize(output, prepared, rows):
|
||||
summary = dict(scope=prepared['scope'], cases=[])
|
||||
for variant in prepared['variants']:
|
||||
selected = [r for r in rows if r['case'] == variant['name'] and not r['warmup']]
|
||||
profiled = [r for r in selected if r['variant'] == 'profiled']
|
||||
control = [r for r in selected if r['variant'] == 'control']
|
||||
c = profiled[0]['profile']['counters']
|
||||
assert all(r['profile']['counters'] == c for r in profiled)
|
||||
raw_profiles = [json.loads((output / variant['name'] / f'profiled-{r["label"]}' / 'profile.json').read_bytes())
|
||||
for r in profiled]
|
||||
times = {key: statistics.mean(r['profile']['exclusiveSeconds'][key] for r in profiled) for key in PARTITION}
|
||||
total = statistics.mean(r['profile']['totalSeconds'] for r in profiled)
|
||||
medians = {name: {key: statistics.median(r[key] for r in data)
|
||||
for key in ('solveSeconds', 'solveCpuSeconds', 'processWallSeconds')}
|
||||
for name, data in (('control', control), ('profiled', profiled))}
|
||||
summary['cases'].append(dict(name=variant['name'], meanTotalSeconds=total,
|
||||
phases={key: dict(seconds=value, percent=100 * value / total) for key, value in times.items()},
|
||||
meanScopes={key: {metric: statistics.mean(r['scopes'][key][metric] for r in raw_profiles)
|
||||
for metric in ('inclusiveSeconds', 'exclusiveSeconds')}
|
||||
for key in raw_profiles[0]['scopes']},
|
||||
symbolic_factorization=dict(seconds=0, status='not applicable: dense LU'),
|
||||
error_estimation=dict(seconds=None, status='included in cvode_controller_including_error_estimation'),
|
||||
counters=c, medians=medians,
|
||||
observedOverheadPercent={key: 100 * (medians['profiled'][key] / medians['control'][key] - 1)
|
||||
for key in medians['control']},
|
||||
allRunsFullParity=True, samples=len(profiled)))
|
||||
write(output / 'summary.json', summary)
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2), flush=True)
|
||||
return summary
|
||||
|
||||
|
||||
def report_existing(output):
|
||||
prepared = json.loads((output / 'prepared.json').read_bytes())
|
||||
rows = json.loads((output / 'measurements.json').read_bytes())
|
||||
for row in rows:
|
||||
if row['variant'] != 'profiled':
|
||||
continue
|
||||
directory = output / row['case'] / f'profiled-{row["label"]}'
|
||||
row['profile'] = analyze(json.loads((directory / 'profile.json').read_bytes()),
|
||||
json.loads((directory / 'result.json').read_bytes()))
|
||||
write(directory / 'measurement.json', row)
|
||||
write(output / 'measurements.json', rows)
|
||||
return summarize(output, prepared, rows)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--input', type=Path, default=ROOT / 'tests/data/test-mql-8-corrected.json')
|
||||
parser.add_argument('--output', type=Path, default=ROOT / 'test/solver-profile-20260916')
|
||||
parser.add_argument('--stop', type=float, default=21.7)
|
||||
parser.add_argument('--repeats', type=int, default=3)
|
||||
parser.add_argument('--warmups', type=int, default=1)
|
||||
mode = parser.add_mutually_exclusive_group()
|
||||
mode.add_argument('--run', action='store_true', help='also run after all builds complete')
|
||||
mode.add_argument('--report-only', action='store_true', help='rebuild summary from completed runs; no build or simulation')
|
||||
options = parser.parse_args()
|
||||
if options.report_only:
|
||||
report_existing(options.output)
|
||||
else:
|
||||
prepared = prepare(options)
|
||||
if options.run:
|
||||
run(options, prepared)
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "solver_profile.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
static struct { unsigned long long calls; double inclusive, exclusive; } totals[P_COUNT];
|
||||
static unsigned long long counters[C_COUNT], errors;
|
||||
static ProfileScope *parent;
|
||||
static double profile_now(void) {
|
||||
#ifdef _WIN32
|
||||
static LARGE_INTEGER frequency;
|
||||
LARGE_INTEGER value;
|
||||
if(!frequency.QuadPart) QueryPerformanceFrequency(&frequency);
|
||||
QueryPerformanceCounter(&value);
|
||||
return (double)value.QuadPart/(double)frequency.QuadPart;
|
||||
#else
|
||||
struct timespec value; clock_gettime(CLOCK_MONOTONIC,&value);
|
||||
return value.tv_sec+value.tv_nsec*1e-9;
|
||||
#endif
|
||||
}
|
||||
ProfileScope profile_begin(int id) {
|
||||
ProfileScope s={.id=id,.active=1,.parent=parent};
|
||||
s.start=profile_now();return s;
|
||||
}
|
||||
void profile_link(ProfileScope *s) {parent=s;}
|
||||
void profile_end(ProfileScope *s) {
|
||||
if(!s->active)return;
|
||||
double elapsed=profile_now()-s->start;
|
||||
if(parent!=s || elapsed<s->children) {fputs("Invalid profiling scope\n",stderr);exit(74);}
|
||||
totals[s->id].calls++;
|
||||
totals[s->id].inclusive+=elapsed;
|
||||
totals[s->id].exclusive+=elapsed-s->children;
|
||||
parent=s->parent;
|
||||
if(parent)parent->children+=elapsed;
|
||||
s->active=0;
|
||||
}
|
||||
void profile_add(int id,int status,long int value) {
|
||||
if(status || value<0)errors++;
|
||||
else counters[id]+=(unsigned long long)value;
|
||||
}
|
||||
void profile_dump(void) {
|
||||
const char *path=getenv("NATIVE_SOLVER_PROFILE");
|
||||
if(!path || parent) {fputs("Missing profile destination or unclosed scope\n",stderr);exit(74);}
|
||||
FILE *f=fopen(path,"wb");if(!f){perror(path);exit(73);}
|
||||
const char *names[]={"total","integration","cvode_controller","rhs","nonlinear_residual",
|
||||
"jacobian","jacobian_probe","linear_setup","numerical_factorization","linear_solve",
|
||||
"newton","event_detection","accepted_property_checks","sampling","append","flush",
|
||||
"output_replay","result_encoding","poll"};
|
||||
const char *count_names[]={"accepted_steps","residual_evaluations","linear_rhs_evaluations",
|
||||
"jacobian_evaluations","linear_setups","error_test_failures","newton_iterations",
|
||||
"newton_failures","nonlinear_step_failures","counter_segments","jacobian_reuses",
|
||||
"jacobian_refresh_setups","linear_setup_failures","LU_factorization_failures",
|
||||
"LU_solve_failures","event_model_evaluations","scheduled_boundary_count"};
|
||||
fprintf(f,"{\"version\":1,\"counterErrors\":%llu,\"counters\":{",errors);
|
||||
for(int i=0;i<C_COUNT;i++)fprintf(f,"%s\"%s\":%llu",i?",":"",count_names[i],counters[i]);
|
||||
fputs("},\"scopes\":{",f);
|
||||
for(int i=0;i<P_COUNT;i++)fprintf(f,"%s\"%s\":{\"calls\":%llu,\"inclusiveSeconds\":%.17g,\"exclusiveSeconds\":%.17g}",
|
||||
i?",":"",names[i],totals[i].calls,totals[i].inclusive,totals[i].exclusive);
|
||||
fputs("}}\n",f);int ok=!ferror(f);if(fclose(f))ok=0;if(!ok)exit(73);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* Diagnostic-only scopes for one standalone worker. Never linked in production. */
|
||||
#ifndef SOLVER_PROFILE_H
|
||||
#define SOLVER_PROFILE_H
|
||||
enum {
|
||||
P_TOTAL, P_INTEGRATION, P_CVODE, P_RHS, P_RESIDUAL, P_JACOBIAN,
|
||||
P_JACOBIAN_PROBE, P_LSETUP, P_LU, P_LSOLVE, P_NEWTON, P_EVENT,
|
||||
P_PROPERTY, P_SAMPLING, P_APPEND, P_FLUSH, P_REPLAY, P_ENCODING,
|
||||
P_POLL, P_COUNT
|
||||
};
|
||||
enum {
|
||||
C_STEPS, C_RHS, C_LINEAR_RHS, C_JACOBIANS, C_SETUPS, C_ERROR_FAILS,
|
||||
C_NEWTON_ITERS, C_NEWTON_FAILS, C_STEP_SOLVE_FAILS, C_SEGMENTS,
|
||||
C_JAC_REUSES, C_JAC_REFRESHES, C_SETUP_FAILURES, C_LU_FAILURES,
|
||||
C_SOLVE_FAILURES, C_FRICTION_EVALS, C_BOUNDARIES, C_COUNT
|
||||
};
|
||||
typedef struct ProfileScope {
|
||||
double start, children;
|
||||
int id, active;
|
||||
struct ProfileScope *parent;
|
||||
} ProfileScope;
|
||||
ProfileScope profile_begin(int id);
|
||||
void profile_link(ProfileScope *scope);
|
||||
void profile_end(ProfileScope *scope);
|
||||
void profile_add(int id, int status, long int value);
|
||||
void profile_dump(void);
|
||||
#define PROFILE_SCOPE(id) ProfileScope profile_scope __attribute__((cleanup(profile_end)))=profile_begin(id); profile_link(&profile_scope)
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
/* Included only by the instrumented private copy of cvode_solver.c.
|
||||
* Public SUN solver ops wrap the unchanged implementations from the same
|
||||
* CVODE library. No private CVODE layouts or substituted numerical kernels. */
|
||||
#include <sunnonlinsol/sunnonlinsol_newton.h>
|
||||
static int (*original_lu)(SUNLinearSolver,SUNMatrix);
|
||||
static int (*original_solve)(SUNLinearSolver,SUNMatrix,N_Vector,N_Vector,sunrealtype);
|
||||
static int (*original_newton)(SUNNonlinearSolver,N_Vector,N_Vector,N_Vector,sunrealtype,sunbooleantype,void *);
|
||||
static SUNErrCode (*original_setsys)(SUNNonlinearSolver,SUNNonlinSolSysFn);
|
||||
static SUNErrCode (*original_setsetup)(SUNNonlinearSolver,SUNNonlinSolLSetupFn);
|
||||
static SUNNonlinSolSysFn original_residual;
|
||||
static SUNNonlinSolLSetupFn original_setup;
|
||||
static int profiled_lu(SUNLinearSolver ls,SUNMatrix a) {
|
||||
PROFILE_SCOPE(P_LU);
|
||||
int flag=original_lu(ls,a);if(flag)profile_add(C_LU_FAILURES,0,1);return flag;
|
||||
}
|
||||
static int profiled_solve(SUNLinearSolver ls,SUNMatrix a,N_Vector x,N_Vector b,sunrealtype tol) {
|
||||
PROFILE_SCOPE(P_LSOLVE);
|
||||
int flag=original_solve(ls,a,x,b,tol);if(flag)profile_add(C_SOLVE_FAILURES,0,1);return flag;
|
||||
}
|
||||
static int profiled_residual(N_Vector y,N_Vector f,void *mem) {
|
||||
PROFILE_SCOPE(P_RESIDUAL);return original_residual(y,f,mem);
|
||||
}
|
||||
static int profiled_setup(sunbooleantype bad,sunbooleantype *current,void *mem) {
|
||||
PROFILE_SCOPE(P_LSETUP);
|
||||
long int before=0,after=0;
|
||||
int first=CVodeGetNumJacEvals(mem,&before);
|
||||
int flag=original_setup(bad,current,mem);
|
||||
int last=CVodeGetNumJacEvals(mem,&after);
|
||||
/* Count successful linear setups which actually reused the stored J.
|
||||
This is unrelated to scalar/kernel memoization inside a fresh J. */
|
||||
if(flag)profile_add(C_SETUP_FAILURES,0,1);
|
||||
else profile_add(after==before?C_JAC_REUSES:C_JAC_REFRESHES,first||last,1);
|
||||
return flag;
|
||||
}
|
||||
static SUNErrCode profiled_setsys(SUNNonlinearSolver nls,SUNNonlinSolSysFn fn) {
|
||||
original_residual=fn;return original_setsys(nls,fn?profiled_residual:NULL);
|
||||
}
|
||||
static SUNErrCode profiled_setsetup(SUNNonlinearSolver nls,SUNNonlinSolLSetupFn fn) {
|
||||
original_setup=fn;return original_setsetup(nls,fn?profiled_setup:NULL);
|
||||
}
|
||||
static int profiled_newton(SUNNonlinearSolver nls,N_Vector y0,N_Vector y,N_Vector w,
|
||||
sunrealtype tol,sunbooleantype setup,void *mem) {
|
||||
PROFILE_SCOPE(P_NEWTON);return original_newton(nls,y0,y,w,tol,setup,mem);
|
||||
}
|
||||
static SUNNonlinearSolver profile_attach_newton(N_Vector y,SUNContext ctx) {
|
||||
SUNNonlinearSolver nls=SUNNonlinSol_Newton(y,ctx);
|
||||
if(!nls)return NULL;
|
||||
original_newton=nls->ops->solve;original_setsys=nls->ops->setsysfn;original_setsetup=nls->ops->setlsetupfn;
|
||||
nls->ops->solve=profiled_newton;nls->ops->setsysfn=profiled_setsys;nls->ops->setlsetupfn=profiled_setsetup;
|
||||
return nls;
|
||||
}
|
||||
static int profiled_cvode(void *solver,sunrealtype end,N_Vector y,sunrealtype *next,int task) {
|
||||
PROFILE_SCOPE(P_CVODE);return CVode(solver,end,y,next,task);
|
||||
}
|
||||
Reference in new issue
Block a user