修复循环信号与事件采样并接入 LSTP 接触定位,补充八路验证及复用实验
相较上一版 Jacobian 确定性复用更新,本次补齐事件边界一致性、结果两侧采样及接触事件定位;保留已有物性复用和组件力学公式。 - 统一 UD00 信号求值与下一事件查询的绝对时间边界,修复循环边界浮点舍入导致的阶段错位、重复或漏报,并覆盖零时长、多阶段及长周期场景。 - 引入原生输出语义 v2:保留规则网格真实时间,补充内部时间事件和状态事件的左邻及事件后采样,按保存时间、状态和离散模式重放结果。 - 两条代码生成路径均发出 LSTP 接触描述,默认定位间隙过零及非负力模式的力截断;仅在接受事件时更新防重复记录,增加 contactEvents 诊断计数。 - 补充 MASS/LSTP 独立事件实验、八路全曲线与驱动阶段配对评估,以及 Amesim 不连续点输出对照和力差定位报告;MASS 新增释放机制仍保留为独立实验。 - 保存局部 probe、context 访问与回退、shadow replay、R288 real skip/typed replay 及阀门数值尾部诊断工具和报告;未证明净收益的实验不启用为生产默认优化。 - 更新原生运行说明和元件建模规范,补充信号边界、输出语义、接触事件和实验依赖回归测试。 验证:五组专项回归共 34 项全部通过;37 个待提交 Python 文件语法检查通过;git diff --cached --check 通过。
This commit is contained in:
1 parent
1aac220084
commit
7611f13208
85 files changed
+11358
-33
No files matched your search
@@ -0,0 +1,218 @@
|
||||
"""Sampled, exclusive timing of a COPY of the local-probe performance worker.
|
||||
|
||||
Original numerical functions remain in the executable verbatim. Only sampled
|
||||
Jacobian callbacks use timing clones; ordinary residuals never reach a clone.
|
||||
No production source or existing experiment implementation is edited.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import argparse, hashlib, json, os, re, shutil, statistics, subprocess, time
|
||||
import local_probe_experiment as experiment
|
||||
|
||||
ROOT=experiment.ROOT
|
||||
SOURCE=ROOT/'test/local-probe-20260917/worker'
|
||||
OUT=ROOT/'test/local-probe-profile-direct-20260917'
|
||||
HERE=Path(__file__).parent
|
||||
replace=experiment.replace
|
||||
span=experiment.function_span
|
||||
write=experiment.write
|
||||
|
||||
def function(source,name):
|
||||
a,b,e=span(source,name)
|
||||
return source[a:e]
|
||||
|
||||
def annotate_tail(tail):
|
||||
"""Fail closed for the reviewed generated model, not a general C parser.
|
||||
Stage ownership follows extended.py; declarations remain in original scope.
|
||||
Pipe argument PH inversion and accumulation belong to diagnostics; writes
|
||||
of diagnostic outputs belong to remaining_outputs.
|
||||
"""
|
||||
phase='PF_NODE';mode='node';pipe=False;result=['PF_PROBE(PF_NODE);']
|
||||
counts={};transitions=[]
|
||||
for i,line in enumerate(tail.splitlines()):
|
||||
category=phase
|
||||
if mode=='node' and re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='port'
|
||||
if mode=='port' and not re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='mechanical'
|
||||
if mode=='mechanical' and line.startswith('dy[0]='):mode='gas'
|
||||
if line.startswith('for(int i=0;i<NSTATES;i++) if(!isfinite'):mode='finite'
|
||||
if mode=='node':category='PF_NODE'
|
||||
elif mode=='port':category='PF_PORT'
|
||||
elif mode=='mechanical':category='PF_MECHANICAL'
|
||||
elif mode=='finite':category='PF_FINITE'
|
||||
elif mode=='gas':
|
||||
if line.startswith('{ double d[4],acc[4]={0};'):pipe=True;category='PF_PIPE'
|
||||
elif pipe:
|
||||
if re.match(r'w\[\d+\] = ',line):category='PF_OUTPUTS'
|
||||
elif line=='}':pipe=False
|
||||
else:category='PF_PIPE'
|
||||
elif line.startswith('dy['):category='PF_GAS_EQUATIONS'
|
||||
elif re.match(r'w\[\d+\] = ',line):category='PF_OUTPUTS'
|
||||
else:raise AssertionError(('unclassified gas-tail statement',i,line))
|
||||
if category!=phase:
|
||||
result.append(f'PF_PROBE({category});');transitions.append((i,category));phase=category
|
||||
counts[category]=counts.get(category,0)+1;result.append(line)
|
||||
assert mode=='finite' and not pipe
|
||||
assert all(counts.get(x,0) for x in ['PF_NODE','PF_PORT','PF_MECHANICAL','PF_GAS_EQUATIONS','PF_OUTPUTS','PF_PIPE','PF_FINITE']),counts
|
||||
return '\n'.join(result),dict(lineCounts=counts,transitions=transitions)
|
||||
|
||||
def instrument_model(source,coarse=False):
|
||||
clone=function(source,'model_eval_local_internal')
|
||||
# Keep the original function and append an instrumented duplicate.
|
||||
clone=clone.replace('model_eval_local_internal(', 'pf_model_eval_local_internal(',1)
|
||||
begin=clone.index('{')+1;clone=clone[:begin]+'\nPF_PROBE(PF_INIT);'+clone[begin:]
|
||||
gas=clone.index('if(!(jacobian ? native_jacobian_gas(')
|
||||
clone=clone[:gas]+'PF_PROBE(PF_GAS_PREP);\n'+clone[gas:]
|
||||
clone=replace(clone,'if(lp_capture){','PF_PROBE(PF_DISPATCH);\nif(lp_capture){')
|
||||
clone=clone.replace('lp_snapshot(','pf_snapshot(').replace('lp_save(','pf_save(').replace('lp_reuse(','pf_reuse(')
|
||||
n=0
|
||||
def case(m):
|
||||
nonlocal n;n+=1
|
||||
return m[0]+f'pf_operation({m[1]});'
|
||||
clone=re.sub(r'case (\d+):\{',case,clone)
|
||||
assert n==484 and clone.count('break;}')==484
|
||||
clone=clone.replace('break;}','PF_MARK(PF_DISPATCH);break;}')
|
||||
start=clone.index('double node_energy[')
|
||||
# Leave return 1 and function closing outside stage classification.
|
||||
end=clone.rindex('return 1;')
|
||||
tail,meta=annotate_tail(clone[start:end].rstrip())
|
||||
clone=clone[:start]+tail+'\nPF_PROBE(PF_OTHER);\n'+clone[end:]
|
||||
wrapper=function(source,'lp_eval').replace('int lp_eval(','int pf_lp_eval(',1)
|
||||
begin=wrapper.index('{')+1
|
||||
wrapper=wrapper[:begin]+'\nPF_SCOPE(lp_color+2,lp_color<0?PF_BASELINE:PF_OTHER);pf_eval_count();'+wrapper[begin:]
|
||||
wrapper=wrapper.replace('lp_begin(t,y);','PF_MARK(PF_SNAPSHOT);lp_begin(t,y);PF_MARK(PF_BASELINE);')
|
||||
wrapper=wrapper.replace('model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace)',
|
||||
'(pf_coarse?model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace):pf_model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace))' if coarse
|
||||
else 'pf_model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace)')
|
||||
# This diagnostic is limited to the all-groups valid local path. Count an
|
||||
# unexpected full-probe fallback explicitly, rather than misattribute it.
|
||||
wrapper=replace(wrapper,'int result=eligible && valid ?',
|
||||
'if(lp_color>=0 && !(eligible && valid))PF_MARK(PF_FULL_FALLBACK);\n int result=eligible && valid ?')
|
||||
return source+'\n'+clone+'\n'+wrapper+'\n',meta
|
||||
|
||||
def instrument_support(source):
|
||||
reuse=function(source,'lp_reuse').replace('int lp_reuse(','int pf_reuse(',1)
|
||||
reuse=replace(reuse,'int contextual=lp_before[region]!=lp_after[region];',
|
||||
'int contextual=lp_before[region]!=lp_after[region];\n if(contextual)PF_MARK(PF_CONTEXT);')
|
||||
assert reuse.count('{context_misses[lp_color]++;return 0;}')==2
|
||||
reuse=reuse.replace('{context_misses[lp_color]++;return 0;}',
|
||||
'{context_misses[lp_color]++;PF_MARK(PF_DISPATCH);return 0;}')
|
||||
reuse=replace(reuse,'if(contextual){properties->count=after->count;',
|
||||
'PF_MARK(PF_RESTORE);\n if(contextual){properties->count=after->count;')
|
||||
reuse=replace(reuse,'return 1;','PF_MARK(PF_DISPATCH);return 1;')
|
||||
wrappers='''
|
||||
void pf_snapshot(int index,NativePropertyCache *p,NativePipeCache *c){
|
||||
if(!needed[index] || captured[index]){lp_snapshot(index,p,c);return;}
|
||||
PF_MARK(PF_SNAPSHOT);lp_snapshot(index,p,c);PF_MARK(PF_BASELINE);
|
||||
}
|
||||
void pf_save(double *p,double *h,double *q,double *w,double *fb){
|
||||
PF_MARK(PF_SNAPSHOT);lp_save(p,h,q,w,fb);PF_MARK(PF_BASELINE);
|
||||
}
|
||||
void lp_jac_time_end(uint64_t start,uint64_t end){jac_ticks+=end-start;jac_calls++;}
|
||||
'''
|
||||
return source+'\n'+reuse+'\n'+wrappers
|
||||
|
||||
def instrument_cv(source):
|
||||
rhs=function(source,'jac_rhs_reuse').replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(',1).replace('lp_eval(', 'pf_lp_eval(')
|
||||
colored=function(source,'colored_difference').replace('colored_difference(', 'pf_colored_difference(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(')
|
||||
colored=replace(colored,'if (SUNMatZero(matrix)) return -1;',
|
||||
'PF_SCOPE(0,PF_ZERO);if (SUNMatZero(matrix)) return -1;PF_MARK(PF_OTHER);')
|
||||
colored=replace(colored,'lp_color=color;memcpy(test,state,NSTATES*sizeof(double));',
|
||||
'PF_SCOPE(0,PF_PERTURB);lp_color=color;memcpy(test,state,NSTATES*sizeof(double));')
|
||||
colored=replace(colored,'#if MODEL_JACOBIAN_GAS_REUSE', 'PF_MARK(PF_OTHER);\n#if MODEL_JACOBIAN_GAS_REUSE')
|
||||
colored=replace(colored,'if (flag) return flag;','PF_SCOPE(0,PF_ASSEMBLY);if (flag) return flag;')
|
||||
colored=replace(colored,' }\n context->run->jacobian_colored_evals++;',
|
||||
' PF_MARK(PF_OTHER);\n }\n context->run->jacobian_colored_evals++;')
|
||||
jac=function(source,'cv_jacobian_original').replace('cv_jacobian_original(', 'pf_cv_jacobian_original(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(').replace('colored_difference(', 'pf_colored_difference(')
|
||||
jac=replace(jac,'int flag=jac_increments(context,y,fy,increments);',
|
||||
'PF_SCOPE(0,PF_INCREMENT);int flag=jac_increments(context,y,fy,increments);PF_MARK(PF_OTHER);')
|
||||
jac=replace(jac,'fy=tmp3;', 'PF_SCOPE(0,PF_OTHER);fy=tmp3;')
|
||||
a,b,e=span(source,'cv_jacobian');sig=source[a:b]
|
||||
wrapper=sig+'''{
|
||||
lp_color=-1;uint64_t start=lp_tick();int selected=pf_select();
|
||||
if(selected)pf_open();
|
||||
int result=selected?pf_cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3):cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3);
|
||||
if(selected)pf_close();
|
||||
uint64_t finish=lp_tick();lp_jac_time_end(start,finish);
|
||||
if(selected)pf_flush(start,finish);
|
||||
if(!result){lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));pf_validate_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));}
|
||||
return result;
|
||||
}'''
|
||||
return source[:a]+rhs+'\n'+colored+'\n'+jac+'\n'+wrapper+source[e:]
|
||||
|
||||
def prepare(coarse=False):
|
||||
OUT.mkdir(exist_ok=True);work=OUT/('coarse-worker' if coarse else 'worker');work.mkdir(exist_ok=True)
|
||||
sources={p.name:p.read_text(encoding='utf-8') for p in SOURCE.glob('*.c')}
|
||||
before={k:hashlib.sha256(v.encode()).hexdigest() for k,v in sources.items()}
|
||||
sources['model.c'],boundaries=instrument_model(sources['model.c'],coarse)
|
||||
sources['local_probe_support.c']=instrument_support(sources['local_probe_support.c'])
|
||||
sources['cvode_solver.c']=instrument_cv(sources['cvode_solver.c'])
|
||||
sources['common.c']=replace(sources['common.c'],'lp_start();','lp_start();pf_initialize();')
|
||||
sources['common.c']=replace(sources['common.c'],'lp_finish();','lp_finish();pf_finish();')
|
||||
plan=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8'))
|
||||
affected=[[int(i in set(g['affectedOperations'])) for i in plan['order']] for g in plan['groups']]
|
||||
tables='const unsigned char pf_affected[LP_NC][LP_NO]={'+','.join('{'+','.join(map(str,a))+'}' for a in affected)+'};'
|
||||
sources['local_probe_profile.c']=(HERE/'local_probe_profile.c').read_text(encoding='utf-8').replace('/* PROFILE_TABLES */',tables)
|
||||
for name in ('model.h','local_probe.h'):shutil.copyfile(SOURCE/name,work/name)
|
||||
shutil.copyfile(HERE/'local_probe_profile.h',work/'local_probe_profile.h')
|
||||
cc,sun,_=experiment.builder.toolchain();flags,libs,dlls,exe=experiment.builder.platform_build_inputs(sun);flags+=['-DLP_OBSERVE=0']
|
||||
started=time.perf_counter()
|
||||
def compile_one(item):
|
||||
i,(name,src)=item;path=work/name;path.write_text('#include "local_probe_profile.h"\n'+src,encoding='utf-8',newline='\n');obj=work/f'profile-{i}.o';log=[]
|
||||
experiment.builder._command([cc,*flags,'-I',str(work),'-I',str(experiment.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=180)
|
||||
return obj,log
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:objects=list(pool.map(compile_one,enumerate(sources.items())))
|
||||
log=[];experiment.builder._command([cc,*flags,*[str(o) for o,_ in objects],*experiment.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log)
|
||||
for dll in dlls:shutil.copyfile(dll,work/dll.name)
|
||||
(work/'build.log').write_text('\n'.join(sum([x for _,x in objects],[])+log),encoding='utf-8')
|
||||
write(OUT/('build-coarse.json' if coarse else 'build.json'),dict(sourceHashes=before,sourceDirectory=str(SOURCE),boundaries=boundaries,seconds=time.perf_counter()-started))
|
||||
print('BUILT',time.perf_counter()-started,flush=True)
|
||||
|
||||
def run(label,stride=16,offset=0,control=False,matrices=False,coarse=False):
|
||||
work=OUT/label;work.mkdir(exist_ok=True)
|
||||
executable=(SOURCE if control else OUT/('coarse-worker' if coarse else 'worker'))/'model.exe'
|
||||
env=os.environ.copy();env.update(LOCAL_PROBE_MASK='0x7ffffff',PROBE_PROFILE_STRIDE=str(stride),PROBE_PROFILE_OFFSET=str(offset),PROBE_PROFILE_MATRICES=str(int(matrices)),PROBE_PROFILE_COARSE=str(int(coarse)))
|
||||
args=[str(executable),'--method','BDF','--start','0','--stop','10','--sample-step','.01','--max-step','1e30','--rtol','1e-8','--timeout','300',
|
||||
'--sample-file',str(work/'states.bin'),'--output-block-file',str(work/'outputs.bin'),'--output',str(work/'result.json')]
|
||||
start=time.perf_counter()
|
||||
with (work/'stderr.log').open('wb') as f:
|
||||
p=subprocess.run(args,cwd=work,env=env,stdout=subprocess.PIPE,stderr=f,timeout=330,creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
elapsed=time.perf_counter()-start
|
||||
if p.returncode:raise RuntimeError((label,p.returncode,(work/'stderr.log').read_text()[-4000:]))
|
||||
r=json.loads((work/'result.json').read_text());diag=json.loads((work/'probe.json').read_text())
|
||||
fields=('success','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu','solveSeconds','solveCpuSeconds')
|
||||
record={k:r[k] for k in fields}
|
||||
for name in ('states','outputs','events','jacobians'):
|
||||
path=work/f'{name}.bin'
|
||||
if path.exists():
|
||||
with path.open('rb') as f:record[name+'Sha256']=hashlib.file_digest(f,'sha256').hexdigest()
|
||||
record.update(label=label,stride=stride,offset=offset,control=control,matrices=matrices,coarse=coarse,processSeconds=elapsed,diagnostic=diag)
|
||||
write(work/'measurement.json',record)
|
||||
# Numerical gate on every run, including the sampling-density controls.
|
||||
reference=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8'))
|
||||
exact=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']
|
||||
differences=[k for k in exact if record[k]!=reference[k]]
|
||||
differences += [k for k in ('newtonIterations','newtonConvergenceFailures','contextComparedBytes','contextCopiedBytes','modelCalls','groups') if diag[k]!=reference['diagnostic'][k]]
|
||||
assert not differences,(label,differences)
|
||||
if matrices:
|
||||
ref=SOURCE.parent/'all-audit/jacobians.bin'
|
||||
with ref.open('rb') as f:assert record['jacobiansSha256']==hashlib.file_digest(f,'sha256').hexdigest()
|
||||
print('RUN',label,'Jacobian',diag['jacobianSeconds'],'solve',r['solveSeconds'],'CPU',r['solveCpuSeconds'],'process',elapsed,'exact OK',flush=True)
|
||||
return record
|
||||
|
||||
def batch():
|
||||
run('control-warmup',control=True)
|
||||
run('profile-warmup',16,101)
|
||||
for i in range(5):
|
||||
jobs=[(f'control-{i}',0,0,True),(f'profile-{i}',16,i+17,False)]
|
||||
if i%2:jobs.reverse()
|
||||
for label,stride,offset,control in jobs:run(label,stride,offset,control)
|
||||
for i in range(2):
|
||||
run(f'disabled-{i}',0,i)
|
||||
run(f'density8-{i}',8,i+37)
|
||||
run(f'density32-{i}',32,i+71)
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run','batch']);p.add_argument('--label',default='sample');p.add_argument('--stride',type=int,default=16);p.add_argument('--offset',type=int,default=0);p.add_argument('--control',action='store_true');p.add_argument('--matrices',action='store_true');p.add_argument('--coarse',action='store_true');a=p.parse_args()
|
||||
if a.action=='prepare':prepare(a.coarse)
|
||||
elif a.action=='batch':batch()
|
||||
else:run(a.label,a.stride,a.offset,a.control,a.matrices,a.coarse)
|
||||
Reference in new issue
Block a user