优化 Jacobian 确定性复用并补充性能剖析与平台依赖文档

在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。

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

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

验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
This commit is contained in:
ljz committed 2026-09-16 13:39:53 +08:00
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()
+343
View File
@@ -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)
+62
View File
@@ -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);
}
+27
View File
@@ -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
+54
View File
@@ -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);
}