Files
SystemSimulationApp/tests/manual/analyze_local_probe_profile.py
ljz 7611f13208 修复循环信号与事件采样并接入 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 通过。
2026-09-17 23:50:13 +08:00

157 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reproducible accounting/validation report; never rescales categories to fit a target."""
from pathlib import Path
import hashlib,json,statistics
from profile_local_probe import ROOT,OUT,SOURCE,write
LABELS={
'other':'其余 callback 时间','perturbation_amount':'扰动量计算','state_copy_perturb':'状态复制/施加扰动',
'matrix_zero':'矩阵清零','difference_matrix_write':'差分计算/矩阵写回','baseline_compute':'baseline 原求值(不含 snapshot)',
'initialization':'probe initialization','gas_state_preparation':'probe gas state preparation',
'schedule_retained':'保守依赖保留的原计算','schedule_context_fallback':'context 失败后的 fallback 原计算',
'context_compare':'context/cache 比较','snapshot_capture_save':'baseline snapshot 捕获/保存',
'context_output_restore':'probe context/output restore','node_energy':'node energy','port_outputs':'端口输出赋值',
'mechanical_equations':'mechanical equations','gas_mass_energy':'气体质量/能量方程','remaining_outputs':'remaining outputs',
'pipe_diagnostics':'pipe diagnostics','finite_check':'finite check','schedule_dispatch':'schedule dispatch/管理',
'whole_probe_fallback':'整次 probe fallback'}
def read(label,file='measurement.json'):return json.loads((OUT/label/file).read_text(encoding='utf-8'))
def table(head,rows):
return '\n| '+' | '.join(head)+' |\n| '+' | '.join(['---']*len(head))+' |\n'+'\n'.join('| '+' | '.join(map(str,r))+' |' for r in rows)+'\n'
def profile(label):
p=read(label,'profile.json');f=p['frequency'];cal=statistics.median(p['calibrationTicksPerMarker'])
assert sum(sum(r['ticks']) for r in p['rows'])==p['ledgerSumTicks']==p['sampledCallbackTicks']
assert sum(sum(r['intervals']) for r in p['rows'])==p['markerCount']
assert all(r['evaluations']==p['sampledCallbacks'] for r in p['rows'][1:])
rows=[]
for r in p['rows']:
raw={n:t/f for n,t in zip(p['categories'],r['ticks'])}
corrected={n:(t-cal*c)/f for n,t,c in zip(p['categories'],r['ticks'],r['intervals'])}
assert min(corrected.values())>=0,(label,r['group'],'calibration below resolution',corrected)
rows.append({**r,'seconds':raw,'correctedSeconds':corrected})
boundary=p['sampledCallbackQpcTicks']/p['qpcFrequency']-p['sampledCallbackTicks']/f
# The two pairs of enclosing clocks delimit a real wrapper interval. Keep
# it visible, rather than rescale the TSC buckets to match QPC.
assert abs(boundary)<.01,(label,'clock cross-check',boundary)
rows[0]['seconds']['other']+=boundary
rows[0]['correctedSeconds']['other']+=boundary
return {**p,'rows':rows,'calibrationNs':cal/f*1e9,'timerSeconds':cal*p['markerCount']/f}
def aggregate(labels):
ps=[profile(n) for n in labels];categories=ps[0]['categories'];samples=sum(p['sampledCallbacks'] for p in ps)
rows=[]
for i in range(len(ps[0]['rows'])):
totals={key:{c:sum(p['rows'][i][key][c] for p in ps) for c in categories} for key in ('seconds','correctedSeconds')}
rows.append(dict(group=i-2,evaluations=sum(p['rows'][i]['evaluations'] for p in ps),**totals,
operations=[sum(p['rows'][i].get('operations',[0,0])[k] for p in ps) for k in range(2)]))
totals={key:{c:sum(r[key][c] for r in rows) for c in categories} for key in ('seconds','correctedSeconds')}
return dict(labels=labels,samples=samples,rows=rows,totals=totals,
callbackSeconds=sum(p['sampledCallbackQpcTicks']/p['qpcFrequency'] for p in ps),
timerSeconds=sum(p['timerSeconds'] for p in ps),
calibrationNs=[p['calibrationNs'] for p in ps])
def main():
# Compare every saved run, not only the runs chosen for timing summaries.
reference=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8'))
keys=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']
counter_keys=['newtonIterations','newtonConvergenceFailures','contextComparedBytes','contextCopiedBytes','modelCalls','groups']
checks=[]
for path in sorted(OUT.glob('*/measurement.json')):
r=json.loads(path.read_text(encoding='utf-8'))
diff=[k for k in keys if r[k]!=reference[k]]+[k for k in counter_keys if r['diagnostic'][k]!=reference['diagnostic'][k]]
assert not diff,(path,diff);checks.append(dict(run=path.parent.name,differences=diff))
full=profile('validate-all');plan=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8'))
audit=json.loads((SOURCE.parent/'all-audit/probe.json').read_text(encoding='utf-8'))
for g,row in enumerate(full['rows'][2:]):
retained=len(plan['groups'][g]['affectedOperations'])*896
total=sum(audit['groups'][g]['executed'])
assert row['operations']==[retained,total-retained]
assert full['rows'][28]['operations']==[0,0]
for name in ('jacobians','states','outputs','events'):
def digest(p):
with p.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
assert digest(OUT/f'validate-all/{name}.bin')==digest(SOURCE.parent/f'all-audit/{name}.bin')
# Check the original sources, and the original numerical evaluator in the
# copied translation unit, have not been rewritten by this measurement.
build=json.loads((OUT/'build.json').read_text(encoding='utf-8'))
for name,expected in build['sourceHashes'].items():
assert hashlib.sha256((SOURCE/name).read_text(encoding='utf-8').encode()).hexdigest()==expected
old_model=(SOURCE/'model.c').read_text(encoding='utf-8')
assert old_model in (OUT/'worker/model.c').read_text(encoding='utf-8')
native=json.loads((SOURCE/'build-metadata.json').read_text(encoding='utf-8'))['sourceHashes']
for name,expected in native.items():assert hashlib.sha256((ROOT/'native'/name).read_text(encoding='utf-8').encode()).hexdigest()==expected
main_labels=[f'profile-{i}' for i in range(5)];data=aggregate(main_labels);scale=896/data['samples']
metrics=('jacobianSeconds','solveCpuSeconds','solveSeconds','processSeconds')
def metric(r,k):return r['diagnostic'][k] if k=='jacobianSeconds' else r[k]
controls=[read(f'control-{i}') for i in range(5)];runs=[read(n) for n in main_labels]
medians={mode:{k:statistics.median(metric(r,k) for r in rs) for k in metrics} for mode,rs in [('control',controls),('profile',runs)]}
deltas={k:medians['profile'][k]/medians['control'][k]-1 for k in metrics}
paired={k:[metric(b,k)/metric(a,k)-1 for a,b in zip(controls,runs)] for k in metrics}
densities={str(s):aggregate([f'density{s}-{i}' for i in range(2)]) for s in (8,32)}
coarse=aggregate([f'coarse-{i}' for i in range(3)])
result=dict(numericalChecks=checks,allMatrices=dict(count=896,entries=896*132*132,differentEntries=0),
originalSourcesUnchanged=True,main=data,medians=medians,deltas=deltas,pairedDeltas=paired,densityChecks=densities,
fullCoverageOperationCounts=[r['operations'] for r in full['rows'][2:]],allInstrumented=full,coarse=coarse)
write(OUT/'comparison.json',result)
report=['**局部 probe performance worker:互斥耗时分解(2026-09-17)**\n']
report.append('仅增加独立诊断构建/分析工具及计时副本;原局部优化脚本、生产实现、普通 residual、物性算法、accepted-step check、线性求解器均未修改。模型为 `tests/data/test-mql-8-corrected.json`,0–10 s、BDF、rtol=1e-8,原 atol/步长设置不变。\n')
estimate=sum(data['totals']['correctedSeconds'].values())*scale
cg=coarse['rows'][28];cg_us=sum(cg['seconds'].values())/cg['evaluations']*1e6
report.append(f'**先说明精度边界:** 低密度插桩相对未插桩的callback中位数变化{deltas["jacobianSeconds"]:+.2%}、积分CPU变化{deltas["solveCpuSeconds"]:+.2%};但细分数据扣空标记后折算{estimate:.6f}s,仍比未插桩{medians["control"]["jacobianSeconds"]:.6f}s高{estimate/medians["control"]["jacobianSeconds"]-1:.2%}。所以本轮完成了互斥分类、账本闭合和数值核验,但细分值尚未达到可直接当作未插桩精确耗时的精度。下面使用它判断热点量级与排序,不以这些百分比承诺优化收益。粗粒度独立对照中group26约{cg_us:.3f}µs,是固定底座总量的更可靠参考。\n')
report.append('**测量方法与互斥口径**\n')
report.append('主测量为5对交替串行运行,预热不计入。每连续16次 callback 分层随机抽1次,种子固定可复现;每轮56次,5轮共280次,baseline和每组probe各280次。未抽中callback走保留的原数值函数;抽中才进入计时副本,不开启shadow求值、物性内核入口计数或矩阵落盘。全量插桩+矩阵落盘仅用于数值核验。\n')
report.append('外层总时间仍用QPC;内部用带lfence的TSC读取及约10 KB互斥计数桶,避免每次记录事件数组。启动时要求CPU支持invariant TSC;用覆盖整个积分的QPC/TSC成对读数校准TSC频率。每个相邻区间只归属一个分类、一个row(外层/baseline/group),桶的TSC tick总和必须精确等于抽中callback内部总tick。外层QPC包络与内部TSC包络的差单列计入outer other,是时钟边界间的包装开销;不缩放内部分类。归并后的原始分类总和与抽中callback的QPC时间在浮点精度内相等。这只证明互斥账本闭合,不证明没有插桩扰动。\n')
report.append('在积分完成后,以相同的带屏障读时钟和计数桶更新测9批空标记,取每标记平均成本的中位数。保留原始时间,并另外给出“原始时间−标记数×空标记成本”的估计;未以未插桩总时间强制归一化,也不把任何负数截成0(出现负分类即报告失败)。校正不能消除屏障引起的执行串行化、编译布局、缓存、额外分支和操作分类的间接扰动;极短分类只作数量级参考。原QPC事件记录版保存在test/local-probe-profile-20260917,其校正后总量高估约10.8%,因此本报告继续展示残余偏差,而不宣称空标记校准能消除它。\n')
report.append(f'主测量标记成本:{min(data["calibrationNs"]):.2f}–{max(data["calibrationNs"]):.2f} ns/次。5轮被抽中callback合计 **{data["callbackSeconds"]:.9f} s**,原始分类加和完全相等;估计计时标记成本 **{data["timerSeconds"]:.9f} s**。\n')
report.append('baseline原求值列排除snapshot捕获/保存,baseline完整求值小计包含两者,只作小计不重复相加。snapshot包含入口t/y保存、选定property/pipe检查点复制、schedule结果数组保存。context比较包含元数据和memcmp;restore包含出口context/cache和区间结果恢复。schedule原计算在case原语句两侧划界,switch/循环/区间分派及测量分类开销另计schedule dispatch。\n')
report.append('initialization包括生成模型开头的数组/cache初始化及第一处gas求值前的准备赋值;gas state preparation从第一处gas求值到schedule入口,包括所有gas memo查找、必要物性计算、gas输出赋值及property context seed。schedule外的端口列仅为端口输出赋值。pipe diagnostics包括调用参数中的PH反算、诊断内核和acc累加;将acc写入w及ff的fmin限幅算remaining outputs。后者不是单纯memcpy,不能据名称假定廉价。\n')
report.append('**未插桩/插桩总耗时对照**\n')
labels={'jacobianSeconds':'Jacobian callback累计墙钟','solveCpuSeconds':'积分CPU','solveSeconds':'积分墙钟','processSeconds':'完整native进程墙钟'}
report.append(table(['5轮中位数','未插桩 / s','1/16插桩 / s','变化'],[(labels[k],f'{medians["control"][k]:.6f}',f'{medians["profile"][k]:.6f}',f'{deltas[k]:+.2%}') for k in metrics]))
report.append(table(['配对','control Jacobian','profile Jacobian','control积分','profile积分','control进程','profile进程'],[(i,*[f'{metric(r,k):.6f}' for k in ('jacobianSeconds','solveSeconds','processSeconds') for r in (controls[i],runs[i])]) for i in range(5)]))
report.append('所有样本保留,没有因较慢而删除。整进程包含启动、输出重放/落盘、诊断文件写入、标记校准和退出,不含预先完成的编译和浏览器/API流程。机器频率与调度噪声仍存在,不能把很小的负变化当作计时插桩带来的加速。\n')
report.append('**互斥分类:按抽样折算到一轮896次Jacobian**\n')
report.append('下面为5轮采样累计分类时间×896/280,未使用目标总时间做比例缩放。原始列含时间标记成本;估计列仅扣除空标记成本。\n')
raw=data['totals']['seconds'];adj=data['totals']['correctedSeconds'];total=sum(adj.values())
report.append(table(['分类','原始折算 / s','扣标记估计 / s','估计占比'],[(LABELS[c],f'{raw[c]*scale:.6f}',f'{adj[c]*scale:.6f}',f'{adj[c]/total:.2%}') for c in raw]))
report.append(f'原始折算合计 **{sum(raw.values())*scale:.6f} s**;扣标记估计合计 **{total*scale:.6f} s**;未插桩callback中位数 **{medians["control"]["jacobianSeconds"]:.6f} s**。估计合计与未插桩相差 **{(total*scale/medians["control"]["jacobianSeconds"]-1):+.2%}**,这部分不强行塞进其他分类。\n')
report.append(table(['范围(小计,不再相加)','原始每次 / µs','扣标记每次 / µs','一轮扣标记 / s'],[(name,f'{sum(data["rows"][i]["seconds"].values())/data["rows"][i]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][i]["correctedSeconds"].values())/data["rows"][i]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][i]["correctedSeconds"].values())*scale:.6f}') for name,i in [('baseline完整求值',1)]]))
report.append('**probe总体及逐group平均**\n')
probe_raw=sum(sum(r['seconds'].values()) for r in data['rows'][2:]);probe_adj=sum(sum(r['correctedSeconds'].values()) for r in data['rows'][2:])
report.append(f'共测量{data["samples"]*27:,}次probe,平均原始 **{probe_raw/(data["samples"]*27)*1e6:.3f} µs/probe**,扣标记估计 **{probe_adj/(data["samples"]*27)*1e6:.3f} µs/probe**。组平均范围为lp_eval入口至jac_rhs_reuse返回前,包含数值入口管理及后置统计;外层扰动/矩阵写回不归入probe。\n')
report.append(table(['group','次数','原始总均值 µs','扣标记总均值 µs','保留原计算 µs','fallback原计算 µs','全覆盖实际保留/回退操作'],[(r['group'],r['evaluations'],f'{sum(r["seconds"].values())/r["evaluations"]*1e6:.3f}',f'{sum(r["correctedSeconds"].values())/r["evaluations"]*1e6:.3f}',f'{r["correctedSeconds"]["schedule_retained"]/r["evaluations"]*1e6:.3f}',f'{r["correctedSeconds"]["schedule_context_fallback"]/r["evaluations"]*1e6:.3f}',str(full['rows'][r['group']+2]['operations'])) for r in data['rows'][2:]]))
report.append('各group完整分类数据(不只保留/回退)见comparison.json中main.rows[group+2],seconds与correctedSeconds除以evaluations即为单次均值。\n')
report.append('**低扰动粗粒度交叉核验**\n')
report.append('另构建coarse-worker:只有callback外层以及baseline/完整probe入口和出口的标记,数值调用直接进入原model_eval_local_internal,内部没有细分标记。3轮覆盖全部896次callback(非抽样)。以下原始均值不扣空标记,额外的每probe边界成本很小;它用于检查细分计时对每组总量的偏差,不用于强制缩放细分分类。\n')
report.append(table(['group','粗粒度完整probe µs','细分扣标记 µs','细分相对偏差'],[(g,f'{sum(coarse["rows"][g+2]["seconds"].values())/coarse["rows"][g+2]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][g+2]["correctedSeconds"].values())/data["rows"][g+2]["evaluations"]*1e6:.3f}',f'{(sum(data["rows"][g+2]["correctedSeconds"].values())/data["rows"][g+2]["evaluations"])/(sum(coarse["rows"][g+2]["seconds"].values())/coarse["rows"][g+2]["evaluations"])-1:+.2%}') for g in range(27)]))
report.append(table(['粗粒度轮次','callback s','积分 s','group26 µs'],[(f'coarse-{i}',f'{read(f"coarse-{i}")["diagnostic"]["jacobianSeconds"]:.6f}',f'{read(f"coarse-{i}")["solveSeconds"]:.6f}',f'{sum(profile(f"coarse-{i}")["rows"][28]["seconds"].values())/896*1e6:.3f}') for i in range(3)]))
report.append('**group 26:schedule全跳过后的固定成本**\n')
g=data['rows'][28];n=g['evaluations'];ga=sum(g['correctedSeconds'].values())
cg=coarse['rows'][28];coarse_g26=sum(cg['seconds'].values())/cg['evaluations']*1e6
report.append(f'全量896次核验中,保留原操作=0、fallback原操作=0,与原audit逐操作计数相符。主测量均值:原始 **{sum(g["seconds"].values())/n*1e6:.3f} µs**,扣标记估计 **{ga/n*1e6:.3f} µs**;更低扰动的粗粒度完整probe均值 **{coarse_g26:.3f} µs**。这是本模型、本轨迹、group 26下的固定底座,不是所有group通用的固定常数,也不包含外层差分写回。\n')
report.append(table(['分类','原始均值 / µs','扣标记均值 / µs','估计占比'],[(LABELS[c],f'{g["seconds"][c]/n*1e6:.3f}',f'{v/n*1e6:.3f}',f'{v/ga:.2%}') for c,v in g['correctedSeconds'].items() if g['seconds'][c]]))
report.append('**采样密度与数值校验**\n')
density_rows=[]
for name,d in [('1/8',densities['8']),('1/16',data),('1/32',densities['32'])]:
factor=896/d['samples'];g=d['rows'][28]
density_rows.append((name,d['samples'],f'{sum(d["totals"]["correctedSeconds"].values())*factor:.6f}',f'{d["totals"]["correctedSeconds"]["schedule_retained"]*factor:.6f}',f'{d["totals"]["correctedSeconds"]["schedule_context_fallback"]*factor:.6f}',f'{sum(g["correctedSeconds"].values())/g["evaluations"]*1e6:.3f}'))
report.append(table(['密度','抽中callback','一轮扣标记估计 s','保留计算 s','fallback计算 s','group26 µs'],density_rows))
report.append(table(['只编入插桩、采样关闭','Jacobian s','积分 s','完整进程 s'],[(f'disabled-{i}',*[f'{metric(read(f"disabled-{i}"),k):.6f}' for k in ('jacobianSeconds','solveSeconds','processSeconds')]) for i in range(2)]))
report.append(f'共{len(checks)}次完整运行的states/outputs/event二进制、全部warning字段、最终状态、accepted/rejected steps、Newton iterations/failures、nfev/njev/nlu、context复制/比较字节数、各group保护命中/回退计数均与既有performance worker一致。全量计时副本另核对896个132×132矩阵,共15,611,904元素及各矩阵输入t/y,逐字节一致。内部互斥计时桶的闭合误差为0 TSC tick;加上外层边界差后,分类与QPC总量在浮点精度内闭合。\n')
report.append('全量插桩的每组“保守保留”和“context回退”操作数分别等于依赖计划应执行数、原audit实际执行数减去应执行数;group26两者均为0。整probe fallback未发生。生产native源文件hash、旧performance worker所有源文件hash以及原生成模型函数完整文本均验证未变。\n')
report.append(table(['求解器项目','所有本轮运行保持一致'],[(k,reference[k]) for k in ['acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']]+[(k,reference['diagnostic'][k]) for k in ['newtonIterations','newtonConvergenceFailures']]))
report.append('**可以支持的诊断结论**\n')
report.append('1. schedule实际保留计算1,390,592次,context保护失败后的回退计算4,620,672次;139,776次区间context校验失败。细分估计回退原计算约0.61s,保守保留原计算约0.42s,1/8和1/32密度下排序相同。保护的主要时间影响体现在失败后的原计算,并非memcmp本身;不能据此推断去掉保护是安全的。\n')
report.append('2. 比较约0.032s,baseline快照/结果保存约0.053s,恢复约0.030s。三者不是零成本,但合计量级显著小于回退原计算。所有这些时间均与计算阶段互斥,baseline小计没有再次加入总和。\n')
report.append('3. 未裁剪的初始化、gas准备和schedule外方程/输出/诊断合计估计约0.58s;其中remaining outputs约0.24s、pipe diagnostics约0.19s,而gas准备约0.056s。remaining outputs包含各管道ff的fmin限幅,pipe diagnostics包含参数PH反算,不能把前者等同简单写内存、后者等同单一内核调用。此处未对某个具体函数做内部剖析,不宣称fmin就是已证实的单函数热点。\n')
report.append('4. group26的schedule保留/回退均为0,依然要执行未裁剪阶段和恢复操作。粗粒度三轮均值21.109–21.263µs;细分扣标记得到24.749µs,高约16.8%,提示小probe的细分扰动占比更高。其细分热点主要是remaining outputs、pipe diagnostics与finite check,但精确占比应保留上述测量误差。\n')
report.append('5. 随机分层采样减少了整轮测量干扰,不能消除被抽中callback自身的串行化/缓存/编译布局影响。细分校正总量仍有6.98%残差;本报告没有把残差摊进other或按比例缩放各分类来制造闭合。跨轮机器负载差异也会影响粗/细计时对比。本轮没有据此修改任何优化实现。\n')
report.append('**证据与复现**\n')
report.append('脚本:tests/manual/profile_local_probe.py、local_probe_profile.h/.c、analyze_local_probe_profile.py。prepare只在本目录生成计时worker;run --stride 1 --matrices做完整数值核验;batch做预热、5对主测量和密度对照;分析脚本生成本报告。各运行目录保留profile.json、measurement.json、probe.json、result.json和二进制结果;build.json保存原文件hash和阶段边界。\n')
(OUT/'report.md').write_text('\n'.join(report),encoding='utf-8')
print(json.dumps(dict(medians=medians,deltas=deltas,rawEstimated=sum(raw.values())*scale,correctedEstimated=total*scale,group26Us=ga/n*1e6,densityRows=density_rows,numericalRuns=len(checks)),ensure_ascii=False,indent=2))
if __name__=='__main__':main()