C内核按库功能拆解,编译结果缓存区构建,编译过程与已有缓存结果对照功能实现

This commit is contained in:
lujingze committed 2026-09-12 05:24:48 +00:00
1 parent aa4951b14e
commit 151e6e4b97
30 files changed
+4856 -604

No files matched your search

+5 -1
View File
@@ -291,7 +291,11 @@ def main():
'mode':'plain' if args.plain else 'profile','port':args.port,
'python':sys.version,'platform':sys.platform,
'frontendFiles':{str(p.relative_to(out/'frontend')):sha256(p.read_bytes()).hexdigest() for p in (out/'frontend').rglob('*') if p.is_file()},
'productionKernelSha256':sha256((ROOT/'native/components/kernels.c').read_bytes()).hexdigest()}
# Retain the historical entry hash; module/header hashes now
# identify the numerical sources behind that diagnostic entry.
'productionKernelSha256':sha256((ROOT/'native/components/kernels.c').read_bytes()).hexdigest(),
'productionNativeSourceHashes':{p.relative_to(ROOT/'native').as_posix():sha256(p.read_bytes()).hexdigest()
for p in sorted((ROOT/'native').rglob('*')) if p.is_file() and p.suffix in ('.c','.h')}}
(out/'environment.json').write_text(json.dumps(metadata,indent=2)+'\n')
import uvicorn
uvicorn.run(profile.app(api.app) if profile else api.app,host='127.0.0.1',port=args.port)
@@ -0,0 +1,382 @@
"""Prepare or serially measure frozen/current native builds; never run a model.
Example (fresh output directory; omit --run for preparation only)::
.venv/bin/python tests/manual/benchmark_native_build_cache.py \
--baseline-root test/incremental-build-20260912/baseline-source \
--output-dir test/incremental-build-20260912/benchmark \
--warmups 1 --repeats 3 --run
Each version/round gets an empty application cache, followed by A/full hit,
A-prime/changed pipe length, A/full hit, four branches/first build, A/full hit.
Application-cache cold does not mean OS page-cache cold. Versions run in separate
Python processes, with alternating version order by round. Preparation, imports,
hashing and recording are outside build timing. NativeBuild.close() is measured
separately and as part of an adjacent build-and-release wall interval. Compilation
retains each production builder's concurrency and flags. No solver, frontend,
strace or previous diagnostic timings are invoked/combined.
"""
from __future__ import annotations
import argparse
from copy import deepcopy
import csv
from datetime import datetime, timezone
from hashlib import sha256
import json
import math
import os
from pathlib import Path
import platform
import shutil
import statistics
import subprocess
import sys
import time
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = Path(__file__).resolve()
SCENARIOS = (
("cold", "a", False), ("same_model", "a", True),
("parameter_change", "a_prime", False), ("return_to_a", "a", True),
("four_branches", "four", False), ("return_from_four", "a", True),
)
METRICS = ("buildSeconds", "buildCallWallSeconds", "releaseWallSeconds",
"buildAndReleaseWallSeconds", "executableBytes", "cacheBytes",
"preprocessSeconds", "compileSeconds", "compileWallSeconds",
"linkSeconds", "unitCount", "objectCacheHits", "objectCompilations")
ENVIRONMENT = ("SIMULATION_NATIVE_CC", "SUNDIALS_ROOT",
"SIMULATION_NATIVE_MODEL_CACHE_MB", "SIMULATION_NATIVE_OBJECT_CACHE_MB")
def digest(path: Path) -> str:
with path.open("rb") as stream:
value = sha256()
for block in iter(lambda: stream.read(1024 * 1024), b""):
value.update(block)
return value.hexdigest()
def json_digest(value) -> str:
return sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def write_json(path: Path, value) -> None:
path.write_text(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8")
def source_files(root: Path) -> dict:
paths = list((root / "app").rglob("*.py"))
paths += [p for directory in ("native", "schemas") for p in (root / directory).rglob("*") if p.is_file()]
return {p.relative_to(root).as_posix(): digest(p) for p in sorted(paths)
if "__pycache__" not in p.parts and not p.relative_to(root).as_posix().startswith("app/data/")}
def source_record(root: Path) -> dict:
if not (root / "app/simulation/native_codegen/build.py").is_file() or not (root / "native").is_dir():
raise ValueError(f"Incomplete source root: {root}")
frozen = root / "source-manifest.json"
if frozen.exists():
manifest = json.loads(frozen.read_text())
for name, expected in manifest["trackedFiles"].items():
if digest(root / name) != expected:
raise ValueError(f"Frozen source integrity failure: {root / name}")
revision = {"gitHead": manifest["head"], "frozenManifestSha256": digest(frozen), "worktreeStatus": None}
else:
revision = {"gitHead": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip(),
"worktreeStatus": subprocess.check_output(["git", "status", "--porcelain", "--", "app", "native", "schemas"], cwd=root, text=True)}
files = source_files(root)
return {"root": str(root), **revision, "sourceFiles": files, "sourceFilesSha256": json_digest(files)}
def assert_source(record: dict) -> None:
if source_files(Path(record["root"])) != record["sourceFiles"]:
raise RuntimeError(f"Source changed after preparation: {record['root']}")
def make_inputs(args, output: Path) -> tuple[dict, dict]:
directory = output / "inputs"
directory.mkdir()
paths = {"a": directory / "model-a.json", "a_prime": directory / "model-a-prime.json", "four": directory / "model-four.json"}
shutil.copyfile(args.input, paths["a"])
shutil.copyfile(args.four_input, paths["four"])
original = json.loads(paths["a"].read_text(encoding="utf-8"))
variant = deepcopy(original)
matches = [(i, node) for i, node in enumerate(variant["nodes"]) if node["id"] == args.variant_node]
if len(matches) != 1:
raise ValueError(f"Expected exactly one variant node: {args.variant_node}")
index, node = matches[0]
data = node["data"]
if data["modelType"] != "amesim_pnl0001":
raise ValueError("The controlled length variant requires an amesim_pnl0001 pipe")
old = data["parameters"]["le"]
if type(old) not in (int, float) or not math.isfinite(old) or old <= 0:
raise ValueError("Pipe le must be a positive numeric SI value")
new = old * 1.001
if not math.isfinite(new) or new == old:
raise ValueError("Pipe length variant did not produce a finite changed value")
data["parameters"]["le"] = new
write_json(paths["a_prime"], variant)
data["parameters"]["le"] = old
if variant != original:
raise RuntimeError("Variant changed fields beyond the selected pipe length")
inputs = {key: {"path": str(path), "sha256": digest(path)} for key, path in paths.items()}
change = {"nodeId": node["id"], "modelType": data["modelType"], "jsonPointer": f"/nodes/{index}/data/parameters/le",
"before": old, "after": new, "storedUnit": "m", "factor": 1.001,
"note": "Stored numeric values are SI; editor parameterUnits do not rescale them."}
return inputs, change
def generate(config: dict, version: str, *, save: bool):
source = config["versions"][version]
assert_source(source)
root = Path(source["root"])
# Every worker starts a fresh interpreter; neither version imports the other.
sys.path.insert(0, str(root))
from app.main import compile_system_xml_network
from app.simulation.native_codegen.compiler import compile_native_program
from app.simulation.native_codegen.input import load_input
from app.simulation.native_codegen import build as builder
for name, module in tuple(sys.modules.items()):
location = getattr(module, "__file__", None)
if (name == "app" or name.startswith("app.")) and location and not Path(location).resolve().is_relative_to(root):
raise RuntimeError(f"Mixed source imports: {name}: {location}")
if builder.NATIVE.resolve() != root / "native":
raise RuntimeError("Builder native source root differs from the selected version")
generated = Path(config["outputDir"]) / version / "generated"
if save:
generated.mkdir(parents=True)
programs, records = {}, {}
for key, item in config["inputs"].items():
path = Path(item["path"])
if digest(path) != item["sha256"]:
raise RuntimeError(f"Input changed after preparation: {path}")
started = time.perf_counter()
xml, document = load_input(path)
program = compile_native_program(compile_system_xml_network(document))
preparation = time.perf_counter() - started
contract = program.manifest()
record = {"inputSha256": item["sha256"], "xmlSha256": sha256(xml).hexdigest(),
"sourceSha256": sha256(program.source.encode()).hexdigest(), "headerSha256": sha256(program.header.encode()).hexdigest(),
"contractSha256": json_digest(contract), "stateCount": len(program.state_keys),
"variableCount": len(program.variables)}
if save:
directory = generated / key
directory.mkdir()
(directory / "input.xml").write_bytes(xml)
(directory / "model.c").write_text(program.source, encoding="utf-8")
(directory / "model.h").write_text(program.header, encoding="utf-8")
write_json(directory / "contract.json", contract)
write_json(directory / "preparation.json", {**record, "generationSeconds": preparation})
programs[key], records[key] = program, record
if programs["a"].header != programs["a_prime"].header or programs["a"].state_keys != programs["a_prime"].state_keys:
raise RuntimeError("Length variant changed model header/state layout")
if programs["a"].source == programs["a_prime"].source:
raise RuntimeError("Length variant did not change generated C")
if not save and records != config["generated"][version]:
raise RuntimeError("Regenerated model differs from prepared artifacts")
assert_source(source)
return builder, programs, records
def cache_size(cache: Path) -> dict:
files = [p for p in cache.rglob("*") if p.is_file()]
return {"logicalFileBytes": sum(p.stat().st_size for p in files), "fileCount": len(files),
"modelsBytes": sum(p.stat().st_size for p in files if p.relative_to(cache).parts[0] == "models"),
"objectsBytes": sum(p.stat().st_size for p in files if p.relative_to(cache).parts[0] == "objects")}
def worker(args) -> None:
config = json.loads(args.worker_config.read_text())
if digest(SCRIPT) != config["scriptSha256"]:
raise RuntimeError("Benchmark script changed after preparation")
if {key: os.environ.get(key) for key in ENVIRONMENT} != config["environment"]:
raise RuntimeError("Build environment changed after preparation")
builder, programs, records = generate(config, args.version, save=args.worker == "prepare")
if args.worker == "prepare":
write_json(Path(config["outputDir"]) / args.version / "prepared-models.json", records)
return
directory = Path(config["outputDir"]) / args.version / args.label
directory.mkdir()
cache = directory / "cache"
if cache.exists():
raise RuntimeError("Cold build requires a previously nonexistent cache directory")
rows = []
for scenario, model, expected_hit in SCENARIOS:
case = directory / scenario
case.mkdir()
before = time.perf_counter()
built = builder.build_native(programs[model], cache_dir=cache)
returned = time.perf_counter()
release_start = time.perf_counter()
if hasattr(built, "close"):
built.close()
released = time.perf_counter()
manifest = built.manifest
copied_manifest = case / "build-manifest.json"
write_json(copied_manifest, manifest)
details = getattr(built, "details", {})
size = cache_size(cache)
row = {"version": args.version, "run": args.label, "warmup": args.label.startswith("warmup-"),
"scenario": scenario, "model": model, "buildSeconds": built.seconds,
"buildCallWallSeconds": returned - before, "releaseWallSeconds": released - release_start,
"buildAndReleaseWallSeconds": released - before, "cacheHit": built.cache_hit,
"buildKey": manifest["buildKey"], "details": details,
"executable": str(built.executable), "executableSha256": digest(built.executable),
"executableBytes": built.executable.stat().st_size, "cacheBytes": size["logicalFileBytes"], "cacheSize": size,
"sourceFilesSha256": config["versions"][args.version]["sourceFilesSha256"],
"generated": records[model], "compiler": manifest["compiler"], "compilerFlags": manifest["compilerFlags"],
"dependencyHashes": manifest["dependencyHashes"], "sourceHashes": manifest["sourceHashes"],
"nativeHeaderHashes": manifest.get("nativeHeaderHashes"),
"manifest": str(copied_manifest.relative_to(Path(config["outputDir"]))), "manifestSha256": digest(copied_manifest)}
row["checks"] = {"expectedCacheHit": built.cache_hit == expected_hit,
"parameterChangeReusesOtherObjects": (details.get("objectCompilations") == 1 and
details.get("objectCacheHits") == details.get("unitCount", 0) - 1)
if scenario == "parameter_change" and "objectCompilations" in details else None}
write_json(case / "measurement.json", row)
rows.append(row)
if any(value is False for value in row["checks"].values()):
raise RuntimeError(f"Unexpected cache behavior in {args.version}/{args.label}/{scenario}; measurement preserved")
assert_source(config["versions"][args.version])
write_json(directory / "measurements.json", {"complete": True, "solverExecuted": False, "rows": rows})
def invoke(config: dict, mode: str, version: str, label: str | None = None) -> dict:
output = Path(config["outputDir"])
command = [sys.executable, str(SCRIPT), "--worker", mode, "--worker-config", str(output / "prepared.json"), "--version", version]
if label:
command += ["--label", label]
log = output / "logs" / (version + "-" + (label or mode))
start = time.perf_counter()
with log.with_suffix(".stdout.log").open("wb") as stdout, log.with_suffix(".stderr.log").open("wb") as stderr:
process = subprocess.run(command, cwd=config["versions"][version]["root"], stdout=stdout, stderr=stderr, timeout=config["workerTimeout"])
observed = {"command": command, "processWallSeconds": time.perf_counter() - start, "exitCode": process.returncode,
"version": version, "run": label, "scope": "Whole worker including import/generation/checks/recording; not build latency."}
write_json(log.with_suffix(".process.json"), observed)
if process.returncode:
raise RuntimeError(f"Worker failed ({process.returncode}); see {log.with_suffix('.stderr.log')}")
return observed
def stat(values: list) -> dict:
return {"n": len(values), "min": min(values), "median": statistics.median(values), "max": max(values)} if values else {"n": 0, "min": None, "median": None, "max": None}
def metric(row: dict, name: str):
return row[name] if name in row else row["details"].get(name)
def summarize(config: dict, rows: list, processes: list) -> dict:
expected = 2 * (config["warmups"] + config["repeats"]) * len(SCENARIOS)
if len(rows) != expected:
raise RuntimeError(f"Incomplete benchmark: expected {expected} rows, found {len(rows)}")
compilers = {(r["compiler"], tuple(r["compilerFlags"])) for r in rows}
libraries = {json_digest({k: v for k, v in r["dependencyHashes"].items() if "/" not in k}) for r in rows}
if len(compilers) != 1 or len(libraries) != 1:
raise RuntimeError("Compiler/flags or linked library bytes differ across benchmark runs")
statistics_by_version = {}
for version in config["versions"]:
statistics_by_version[version] = {}
for scenario, _, _ in SCENARIOS:
selected = [r for r in rows if r["version"] == version and r["scenario"] == scenario and not r["warmup"]]
if len(selected) != config["repeats"]:
raise RuntimeError(f"Incomplete formal group: {version}/{scenario}")
statistics_by_version[version][scenario] = {key: stat([metric(r, key) for r in selected if metric(r, key) is not None]) for key in METRICS}
changes = {}
for scenario, _, _ in SCENARIOS:
changes[scenario] = {}
for key in ("buildSeconds", "buildCallWallSeconds", "buildAndReleaseWallSeconds"):
old = statistics_by_version["baseline"][scenario][key]["median"]
new = statistics_by_version["candidate"][scenario][key]["median"]
changes[scenario][key] = {"baselineMedian": old, "candidateMedian": new,
"reductionPercent": 100 * (1 - new / old) if old else None,
"speedup": old / new if new else None, "method": "ratio of group medians"}
return {"complete": True, "solverExecuted": False, "prepared": "prepared.json", "scriptSha256": config["scriptSha256"],
"contract": config["contract"], "checks": {"compilerAndFlagsIdentical": True, "linkedLibrariesIdentical": True,
"generatedModelsIdenticalAcrossVersions": True, "variantHeaderAndStateLayoutUnchanged": True},
"statistics": statistics_by_version, "changes": changes, "rows": rows, "workers": processes}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--baseline-root", type=Path, default=ROOT / "test/incremental-build-20260912/baseline-source")
parser.add_argument("--candidate-root", type=Path, default=ROOT)
parser.add_argument("--input", type=Path, default=ROOT / "tests/data/test-mql-8-corrected.json")
parser.add_argument("--four-input", type=Path, default=ROOT / "tests/data/test-mql-4-corrected.json")
parser.add_argument("--variant-node", default="amesim_pnl0001_1")
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--warmups", type=int, default=1)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--worker-timeout", type=float, default=300)
parser.add_argument("--run", action="store_true")
parser.add_argument("--worker", choices=("prepare", "round"), help=argparse.SUPPRESS)
parser.add_argument("--worker-config", type=Path, help=argparse.SUPPRESS)
parser.add_argument("--version", choices=("baseline", "candidate"), help=argparse.SUPPRESS)
parser.add_argument("--label", help=argparse.SUPPRESS)
args = parser.parse_args()
if args.worker:
if not args.worker_config or not args.version or (args.worker == "round" and not args.label):
parser.error("Internal worker arguments are incomplete")
worker(args)
return
if not args.output_dir or args.warmups < 1 or args.repeats < 1 or not math.isfinite(args.worker_timeout) or args.worker_timeout <= 0:
parser.error("A fresh --output-dir, warmups/repeats >= 1 and a positive worker timeout are required")
output = args.output_dir.resolve()
if not output.is_relative_to(ROOT / "test") or output.exists():
parser.error("Choose a previously nonexistent directory under the ignored test/ tree")
versions = {"baseline": source_record(args.baseline_root.resolve()), "candidate": source_record(args.candidate_root.resolve())}
output.mkdir(parents=True)
(output / "logs").mkdir()
try:
inputs, change = make_inputs(args, output)
config = {"createdAtUtc": datetime.now(timezone.utc).isoformat(), "outputDir": str(output), "versions": versions,
"script": str(SCRIPT), "scriptSha256": digest(SCRIPT), "python": sys.version, "pythonExecutable": sys.executable,
"platform": platform.platform(), "cpuCount": os.cpu_count(), "environment": {key: os.environ.get(key) for key in ENVIRONMENT},
"inputs": inputs, "variant": change, "warmups": args.warmups, "repeats": args.repeats,
"workerTimeout": args.worker_timeout, "runRequested": args.run,
"contract": {"cold": "Independent empty application cache per version/round; OS caches are not flushed.",
"sequence": [name for name, _, _ in SCENARIOS], "solverExecuted": False,
"fourBranchCase": "Switch from the full eight-branch project to the corrected four-branch project, then restore A; no cached files are manually deleted.",
"wallTiming": "Builder call and adjacent release are measured separately; generation, hashing, imports and recording are outside these spans.",
"overlap": "buildSeconds is inside buildCallWallSeconds, which is inside buildAndReleaseWallSeconds. Detail spans are nested; compileSeconds sums concurrent TU walls and is not additive to compileWallSeconds.",
"unavailable": "Old builder has no object/stage details; absent measurements are null, never inferred from differences.",
"comparison": "Formal group-median ratios; warmups excluded. No old strace timings or solver performance claims."}}
write_json(output / "prepared.json", config)
for version in versions:
invoke(config, "prepare", version)
config["generated"] = {version: json.loads((output / version / "prepared-models.json").read_text()) for version in versions}
if config["generated"]["baseline"] != config["generated"]["candidate"]:
raise RuntimeError("Generated model/XML/header/contract differs between source versions")
write_json(output / "prepared.json", config)
if not args.run:
print(json.dumps({"preparedOnly": True, "outputDir": str(output), "compiled": False, "solverExecuted": False}))
return
rows, processes = [], []
for index in range(args.warmups + args.repeats):
label = f"warmup-{index + 1}" if index < args.warmups else f"run-{index - args.warmups + 1}"
order = ("baseline", "candidate") if index % 2 == 0 else ("candidate", "baseline")
for version in order:
processes.append(invoke(config, "round", version, label))
rows.extend(json.loads((output / version / label / "measurements.json").read_text())["rows"])
for record in versions.values():
assert_source(record)
summary = summarize(config, rows, processes)
write_json(output / "summary.json", summary)
with (output / "timings.csv").open("w", newline="", encoding="utf-8") as stream:
writer = csv.DictWriter(stream, fieldnames=["version", "run", "warmup", "scenario", "metric", "value", "n", "min", "median", "max"])
writer.writeheader()
for row in rows:
for key in METRICS:
writer.writerow({**{k: row[k] for k in ("version", "run", "warmup", "scenario")}, "metric": key, "value": metric(row, key)})
for version, groups in summary["statistics"].items():
for scenario, values in groups.items():
for key, value in values.items():
writer.writerow({"version": version, "run": "formal-statistics", "warmup": False, "scenario": scenario, "metric": key, **value})
print(json.dumps({"complete": True, "summary": str(output / "summary.json"), "solverExecuted": False}))
except Exception as exc:
write_json(output / "failure.json", {"complete": False, "error": str(exc), "type": type(exc).__name__, "solverExecuted": False})
raise
if __name__ == "__main__":
main()
+79 -6
View File
@@ -33,6 +33,76 @@ NEW_CALL = '''double base=area*p*cm/sqrt(T),K=pow(4*base/den,2)*d/length;
return sign*native_pipe_resistance(K,rr,den/4,NULL)*den/4;'''
def pipe_source_path(native: Path) -> Path:
"""Locate executable pipe formulas, never a modular aggregation entry."""
module = native / 'components/modules/pipe.c'
legacy = native / 'components/kernels.c'
if module.is_file():
target = module
elif (native / 'components/modules').exists():
raise ValueError(f'Incomplete modular native snapshot: {module} is missing')
else:
target = legacy
if not target.is_file():
raise ValueError(f'Native snapshot has no audited pipe source: {native}')
source = target.read_text()
if source.count('double native_pipe_resistance(') != 1 or source.count('double native_pipe_flow(') != 1:
raise ValueError(f'Unsupported pipe source layout: {target}; refusing an unmodified or uninstrumented run')
return target
def working_native_paths() -> list[str]:
return sorted('native/' + path.relative_to(ROOT / 'native').as_posix()
for path in (ROOT / 'native').rglob('*') if path.is_file())
def build_snapshot(program, native: Path, cache: Path, *, required_sources=()):
"""Build exact snapshot sources and verify the modified files were compiled.
Historical monolithic trees retain their original set of C translation
units. This temporary selector override is confined to the standalone
manual process; production source selection is always restored afterwards.
An incompatible old model/runtime ABI fails compilation explicitly.
"""
native = native.resolve()
pipe = pipe_source_path(native)
required = (pipe, *required_sources)
old_native = builder.NATIVE
old_selector = getattr(builder, '_runtime_sources', None)
legacy = pipe.name == 'kernels.c'
try:
builder.NATIVE = native
if legacy and old_selector is not None:
builder._runtime_sources = lambda _: sorted(native.rglob('*.c'))
elif not legacy and old_selector is None:
raise ValueError('A modular snapshot requires the incremental native source selector; use its matching checkout')
selected = (builder._runtime_sources(program) if old_selector is not None
else sorted(native.rglob('*.c')))
if any(path.resolve() not in {source.resolve() for source in selected} for path in required):
raise ValueError('The model/build selector does not compile the modified pipe/RHS source; refusing a misleading run')
try:
build = builder.build_native(program, cache_dir=cache)
except RuntimeError as exc:
raise RuntimeError(
f'Cannot build this native snapshot ({native}). Its source layout or model/runtime ABI may be incompatible; '
'use matching compiler/runtime revisions. No benchmark was executed.'
) from exc
recorded = build.manifest.get('sourceHashes', {})
for path in required:
suffix = 'native/' + path.resolve().relative_to(native).as_posix()
matches = [digest for name, digest in recorded.items()
if name == suffix or name.replace('\\', '/').endswith('/' + suffix)]
if matches != [sha256(path.read_bytes()).hexdigest()]:
if hasattr(build, 'close'):
build.close()
raise ValueError(f'Build manifest does not prove the modified source was compiled: {suffix}')
return build
finally:
builder.NATIVE = old_native
if old_selector is not None:
builder._runtime_sources = old_selector
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('input',type=Path)
@@ -42,7 +112,8 @@ def main():
args=parser.parse_args()
if args.runs<1:parser.error('--runs must be positive')
out=args.output_dir.resolve();out.mkdir(parents=True,exist_ok=True)
if (out/'summary.json').exists():parser.error('Choose a fresh output directory')
if (out/'summary.json').exists() or any((out/name).exists() for name in ('fixed-point','previous-newton','guarded-newton')):
parser.error('Choose a fresh output directory; existing variant sources must not be mixed')
revision=subprocess.check_output(['git','rev-parse',args.baseline_ref],cwd=ROOT,text=True).strip()
xml,doc=load_input(args.input)
program=compile_native_program(compile_system_xml_network(doc))
@@ -55,16 +126,16 @@ def main():
try:
for variant in ('fixed-point','previous-newton','guarded-newton'):
directory=out/variant
for name in paths:
variant_paths = working_native_paths() if variant == 'guarded-newton' else paths
for name in variant_paths:
if variant=='guarded-newton':data=(ROOT/name).read_bytes()
else:data=subprocess.check_output(['git','show',f'{revision}:{name}'],cwd=ROOT)
target=directory/name;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
if variant=='fixed-point':
target=directory/'native/components/kernels.c';source=target.read_text()
target=pipe_source_path(directory/'native');source=target.read_text()
if source.count(NEW_CALL)!=1:raise ValueError('Baseline pipe flow layout does not match the audited fixed-point substitution')
target.write_text(source.replace(NEW_CALL,OLD_ITERATION))
builder.NATIVE=directory/'native'
variants[variant]=builder.build_native(program,cache_dir=out/'cache')
variants[variant]=build_snapshot(program,directory/'native',out/'cache')
finally:builder.NATIVE=original_native
rows=[]
for index in range(args.runs+1):
@@ -80,7 +151,9 @@ def main():
summary={'baselineRef':revision,'input':str(args.input.resolve()),
'inputSha256':sha256(args.input.read_bytes()).hexdigest(),'xmlSha256':sha256(xml).hexdigest(),
'settings':vars(config),'sampleStep':doc.simulation.sample_step,'rows':rows,
'variants':{name:{'sourceSha256':sha256((out/name/'native/components/kernels.c').read_bytes()).hexdigest(),
'variants':{name:{'sourceSha256':sha256(pipe_source_path(out/name/'native').read_bytes()).hexdigest(),
'pipeSource':str(pipe_source_path(out/name/'native').relative_to(out/name)),
'nativeSourceHashes':build.manifest['sourceHashes'],
'buildKey':build.manifest['buildKey'],
'medianSolveSeconds':statistics.median(row['solveSeconds'] for row in rows if row['variant']==name and row['run']>0),
'medianProcessSeconds':statistics.median(row['processWallSeconds'] for row in rows if row['variant']==name and row['run']>0)}
+18 -3
View File
@@ -220,9 +220,15 @@ def prepare(args: argparse.Namespace) -> dict:
# Reject numerical/runtime drift; a sparse timing-only cached main is allowed
# because control and our current writer share the numeric model contract.
differences = []
for name, expected in manifest["sourceHashes"].items():
recorded_sources = dict(manifest["sourceHashes"])
if manifest.get("cacheVersion", 1) >= 2:
headers = manifest.get("nativeHeaderHashes")
if not isinstance(headers, dict) or not headers:
raise RuntimeError("Cached build lacks native header hashes; rebuild before profiling")
recorded_sources.update(headers)
for name, expected in recorded_sources.items():
relative = name.split("native/", 1)[-1]
current = ROOT / "native" / relative
current = cache / relative if relative == "model.c" else ROOT / "native" / relative
if digest(current) != expected:
differences.append(relative)
if any(name != "runtime/main.c" for name in differences):
@@ -243,7 +249,16 @@ def prepare(args: argparse.Namespace) -> dict:
for name in ("model.c", "model.h", "manifest.json"):
shutil.copy2(cache / name, output / name)
instrument(native)
command = [compiler, *manifest["compilerFlags"], "-I", str(output), "-I", str(native / "include"), "-I", str(sundials / "include"), str(output / "model.c"), *map(str, sorted(native.rglob("*.c"))), "-Wl,--start-group", *map(str, libraries), "-Wl,--end-group", "-lm", "-o", str(output / "profiled-model")]
# Match the cached translation units. Compiling the compatibility entry
# together with its new modules would define every component twice.
sources = [native / name.split("native/", 1)[-1]
for name in manifest["sourceHashes"]
if name.endswith(".c") and name != "model.c"]
if not sources or (native / "components/kernels.c" in sources and
any(path.parent == native / "components/modules" for path in sources)):
raise RuntimeError("Cached translation units are missing or mix amalgamation and modules")
sources.append(native / "runtime/compute_profile.c")
command = [compiler, *manifest["compilerFlags"], "-I", str(output), "-I", str(native / "include"), "-I", str(sundials / "include"), str(output / "model.c"), *map(str, sources), "-Wl,--start-group", *map(str, libraries), "-Wl,--end-group", "-lm", "-o", str(output / "profiled-model")]
prepared = {"controlExecutable": str(cache / "model"), "profiledExecutable": str(output / "profiled-model"), "buildCommand": command, "runtimeArguments": numerical_arguments, "verifyJacobian": "--verify-jacobian" in numerical_arguments, "algorithm": "production-automatic", "cacheDir": str(cache), "cachedMainDifference": differences, "stateCount": len(manifest["stateKeys"]), "modelSha256": digest(output / "model.c"), "compiler": compiler_version, "warmups": args.warmups, "repeats": args.repeats, "scope": "Inclusive times overlap. Exclusive scope times partition integration including instrumentation overhead. No component or libc sub-cost inference.", "sourceHashes": {str(p.relative_to(output)): digest(p) for p in sorted(native.rglob("*")) if p.is_file()}}
write_json(output / "prepared.json", prepared)
return prepared
+37 -14
View File
@@ -24,7 +24,9 @@ from app.simulation.backends import simulation_config
from app.simulation.native_codegen import build as builder
from app.simulation.native_codegen.compiler import compile_native_program
from app.simulation.native_codegen.input import load_input
from tests.manual.benchmark_native_pipe_solver import build_snapshot, pipe_source_path, working_native_paths
SNAPSHOT_HELPER = ROOT / 'tests/manual/benchmark_native_pipe_solver.py'
VARIANTS = ('guarded-newton', 'previous-newton', 'fixed-point')
FIELDS = '''cache_requests cache_hits cache_misses flow_calls zero_pressure_calls
pnl00r_analytic_calls resistance_calls scalar_analytic_calls iterative_calls
@@ -214,7 +216,7 @@ def instrument(source: str, variant: str) -> str:
limit = '(kind==0?64:16)' if fixed else '80' if variant == 'previous-newton' else '128'
prefix = PROFILE_PREFIX.replace('@VARIANT@', variant).replace('@FIXED@', str(int(fixed)))
prefix = prefix.replace('@LIMIT@', limit).replace('@FIELDS@', ' '.join(f'X({name})' for name in FIELDS))
source = replace_once(source, '#include <stddef.h>', '#include <stddef.h>\n' + prefix)
source = replace_once(source, '#include <math.h>', '#include <math.h>\n' + prefix)
# Exhaustion is marked at the actual loop fall-through, not inferred from
# visiting the last allowed iteration (which can still converge).
start = source.index('double native_pipe_resistance(')
@@ -259,6 +261,19 @@ def instrument(source: str, variant: str) -> str:
return source
def instrument_common(source: str) -> str:
"""Count both ordinary and canonical Jacobian RHS evaluations exactly once."""
calls = [('model_eval', ' return model_eval(t,y,dy,w);')]
if 'int native_jacobian_rhs(' in source:
calls.append(('model_eval_jacobian', ' return model_eval_jacobian(t,y,dy,w);'))
for function, fragment in calls:
source = replace_once(source, fragment,
' extern void pipe_profile_rhs_enter(void),pipe_profile_rhs_leave(void);\n'
f' pipe_profile_rhs_enter();int ok={function}(t,y,dy,w);pipe_profile_rhs_leave();return ok;')
return source
def git(*arguments: str) -> str:
return subprocess.check_output(['git', *arguments], cwd=ROOT, text=True).strip()
@@ -268,11 +283,14 @@ def prepare(args, out: Path) -> dict:
metadata = json.loads((out / 'prepared.json').read_text())
if metadata['input_sha256'] != sha256(args.input.read_bytes()).hexdigest():
raise ValueError('Prepared model no longer matches the input file')
if metadata['script_sha256'] != sha256(Path(__file__).read_bytes()).hexdigest():
if (metadata['script_sha256'] != sha256(Path(__file__).read_bytes()).hexdigest()
or metadata.get('snapshot_helper_sha256') != sha256(SNAPSHOT_HELPER.read_bytes()).hexdigest()):
raise ValueError('Diagnostic helper changed; choose a fresh output directory')
return metadata
if any((out / variant).exists() for variant in VARIANTS):
raise ValueError('Partially prepared variant directories exist; choose a fresh output directory')
previous = git('rev-parse', args.previous_ref)
current = git('rev-parse', args.current_ref)
current = 'working-tree' if args.current_ref == 'working-tree' else git('rev-parse', args.current_ref)
xml, doc = load_input(args.input)
config = simulation_config(doc.simulation)
if config.rtol != 1e-8 or config.t_start != 0 or config.t_stop != 10:
@@ -281,37 +299,40 @@ def prepare(args, out: Path) -> dict:
out.mkdir(parents=True, exist_ok=True)
(out / 'input.xml').write_bytes(xml)
(out / 'input.json').write_bytes(args.input.read_bytes())
paths = git('ls-tree', '-r', '--name-only', current, 'native').splitlines()
metadata = dict(input=str(args.input.resolve()), input_sha256=sha256(args.input.read_bytes()).hexdigest(),
xml_sha256=sha256(xml).hexdigest(), script_sha256=sha256(Path(__file__).read_bytes()).hexdigest(),
snapshot_helper_sha256=sha256(SNAPSHOT_HELPER.read_bytes()).hexdigest(),
previous_revision=previous, current_revision=current, settings=vars(config),
sample_step=doc.simulation.sample_step, variants={},
measurement_note='Instrumented times are diagnostic overhead and MUST NOT be used as production benchmark results.',
counter_scope='rhs counts native_rhs/model_eval calls including rejected trials and Jacobian differences; non_rhs includes initialization/output/probe evaluations.',
counter_scope='rhs counts native_rhs/model_eval and, where present, native_jacobian_rhs/model_eval_jacobian calls including rejected trials and Jacobian differences; non_rhs includes initialization/output/probe evaluations.',
replay_scope='All guarded RHS resistance calls, including scalar analytic low-Re cases; no sampling or deduplication. Cache hits, zero pressure difference and direct PNL00R analytic calls are excluded and counted separately.',
capture_format='Native-endian IEEE-754 binary64 records: base, diameter, length, relative roughness, den=pi*d*mu, kind (six doubles, 48 bytes). Replay on the same host.',
residual_test='At actual returned q, independently factored long-double f(Re) evaluates abs(Re^2*f(Re)/K-1)<=1e-9. Separate from each algorithm stopping rule.')
original_native = builder.NATIVE
prepared_builds = [] # Pin earlier variants while later variants are built.
try:
for variant in VARIANTS:
directory = out / variant
revision = current if variant == 'guarded-newton' else previous
paths = (working_native_paths() if revision == 'working-tree'
else git('ls-tree', '-r', '--name-only', revision, 'native').splitlines())
for name in paths:
target = directory / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(subprocess.check_output(['git', 'show', f'{revision}:{name}'], cwd=ROOT))
kernel = directory / 'native/components/kernels.c'
data = ((ROOT / name).read_bytes() if revision == 'working-tree'
else subprocess.check_output(['git', 'show', f'{revision}:{name}'], cwd=ROOT))
target.write_bytes(data)
kernel = pipe_source_path(directory / 'native')
original_hash = sha256(kernel.read_bytes()).hexdigest()
kernel.write_text(instrument(kernel.read_text(), variant))
common = directory / 'native/runtime/common.c'
common.write_text(replace_once(common.read_text(), ' return model_eval(t,y,dy,w);',
''' extern void pipe_profile_rhs_enter(void),pipe_profile_rhs_leave(void);
pipe_profile_rhs_enter();int ok=model_eval(t,y,dy,w);pipe_profile_rhs_leave();return ok;'''))
common.write_text(instrument_common(common.read_text()))
main = directory / 'native/runtime/main.c'
main.write_text(replace_once(main.read_text(), 'int main(int argc, char **argv) {',
'int main(int argc, char **argv) {\n extern void pipe_profile_install(void);pipe_profile_install();'))
builder.NATIVE = directory / 'native'
build = builder.build_native(program, cache_dir=out / 'cache')
build = build_snapshot(program, directory / 'native', out / 'cache', required_sources=(common, main))
prepared_builds.append(build)
replay_source = directory / 'replay.c'
replay_source.write_text(REPLAY_MAIN)
replay = directory / ('replay.exe' if os.name == 'nt' else 'replay')
@@ -323,7 +344,8 @@ def prepare(args, out: Path) -> dict:
if compiled.returncode:
raise RuntimeError(f'Replay compilation failed: {compiled.stderr}')
metadata['variants'][variant] = dict(executable=str(build.executable), replay=str(replay),
build_key=build.manifest['buildKey'], original_kernel_sha256=original_hash,
build_key=build.manifest['buildKey'], pipe_source=str(kernel.relative_to(directory)),
compiled_source_hashes=build.manifest['sourceHashes'], original_kernel_sha256=original_hash,
instrumented_kernel_sha256=sha256(kernel.read_bytes()).hexdigest())
print(f'Prepared {variant}', flush=True)
finally:
@@ -423,7 +445,8 @@ def main():
parser.add_argument('--input', type=Path, default=ROOT / 'tests/data/test-mql-8-corrected.json')
parser.add_argument('--output-dir', type=Path, required=True)
parser.add_argument('--previous-ref', default='5d5a2e1')
parser.add_argument('--current-ref', default='808c484')
parser.add_argument('--current-ref', default='808c484',
help='Audited guarded-solver revision, or working-tree for the current modular sources')
parser.add_argument('--timeout', type=float, default=120)
parser.add_argument('--prepare-only', action='store_true')
args = parser.parse_args()