Replace Python numerical kernels with native C execution

This commit is contained in:
ljz committed 2026-09-10 01:12:18 +08:00
1 parent 48da6be21c
commit 3b38f73fe0
227 files changed
+16801 -75499

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
"""Reproducible native builds and a checked, model-specific executable cache."""
from __future__ import annotations
from dataclasses import dataclass
from hashlib import sha256
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
from .compiler import NativeProgram
ROOT = Path(__file__).resolve().parents[3]
NATIVE = ROOT / "native"
CACHE = ROOT / "app/data/native-builds"
LIBRARIES = ("cvode", "core", "nvecserial", "sunmatrixdense", "sunlinsoldense")
@dataclass(frozen=True)
class NativeBuild:
executable: Path
manifest: dict
cache_hit: bool
seconds: float
def _hash(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
def toolchain() -> tuple[str, Path, str]:
compiler = os.environ.get("SIMULATION_NATIVE_CC") or shutil.which("gcc")
if not compiler:
raise RuntimeError("C compiler not found; set SIMULATION_NATIVE_CC to gcc.")
base = Path(os.environ.get("SUNDIALS_ROOT", str(Path(sys.base_prefix) / "Library")))
if not (base / "include/cvode/cvode.h").is_file():
raise RuntimeError("SUNDIALS C development files not found; set SUNDIALS_ROOT.")
version = subprocess.run([compiler, "--version"], capture_output=True, text=True, check=True, timeout=15).stdout.splitlines()[0]
return compiler, base, version
def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> NativeBuild:
start = time.perf_counter()
compiler, sundials, compiler_version = toolchain()
runtime = sorted(NATIVE.rglob("*.c")) + sorted((NATIVE / "include").glob("*.h"))
flags = ["-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-ffp-contract=off", "-fno-fast-math"]
if os.name == "nt":
flags += ["-D__USE_MINGW_ANSI_STDIO=1", "-static-libgcc"]
libraries = [sundials / "lib" / f"sundials_{name}.lib" for name in LIBRARIES]
dlls = [sundials / "bin" / f"sundials_{name}.dll" for name in LIBRARIES]
vc_runtime = sundials / "bin/vcruntime140.dll"
if vc_runtime.is_file():
dlls.append(vc_runtime)
else:
raise RuntimeError("Native v1 build packaging currently supports Windows x64; Linux packaging is pending.")
sources = {str(p.relative_to(ROOT)): _hash(p) for p in runtime}
sources["native/THIRD_PARTY_NOTICES.txt"] = _hash(NATIVE / "THIRD_PARTY_NOTICES.txt")
dependencies = {str(p.name): _hash(p) for p in libraries + dlls}
# Header hashes include precision/index ABI settings as well as library APIs.
for directory in ("sundials", "cvode", "nvector", "sunmatrix", "sunlinsol"):
for path in sorted((sundials / "include" / directory).glob("*.h")):
dependencies[f"{directory}/{path.name}"] = _hash(path)
identity = dict(source=program.source, header=program.header, contract=program.manifest(), sources=sources,
dependencies=dependencies, compiler=compiler_version, flags=flags,
platform=sys.platform, abi=1)
signature = sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest()
cache = (cache_dir or CACHE).resolve()
target = cache / signature
manifest_path = target / "manifest.json"
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if all((target / name).is_file() and _hash(target / name) == digest
for name, digest in manifest["artifacts"].items()):
return NativeBuild(target / "model.exe", manifest, True, time.perf_counter()-start)
raise RuntimeError(f"Native cache integrity check failed: {target}")
cache.mkdir(parents=True, exist_ok=True)
stage = Path(tempfile.mkdtemp(prefix="building-", dir=cache))
(stage / "model.c").write_text(program.source, encoding="utf-8")
(stage / "model.h").write_text(program.header, encoding="utf-8")
command = [compiler, *flags, "-I", str(stage), "-I", str(NATIVE / "include"),
"-I", str(sundials / "include"), str(stage / "model.c"),
*[str(p) for p in runtime if p.suffix == ".c"],
*map(str, libraries), "-lm", "-o", str(stage / "model.exe")]
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
(stage / "build.log").write_text(result.stdout + result.stderr, encoding="utf-8")
if result.returncode:
raise RuntimeError(f"Native compilation failed; see {stage / 'build.log'}: {result.stderr[-3000:]}")
for library in dlls:
shutil.copy2(library, stage / library.name)
shutil.copy2(NATIVE / "THIRD_PARTY_NOTICES.txt", stage / "THIRD_PARTY_NOTICES.txt")
manifest = {
**program.manifest(), "buildKey": signature, "compiler": compiler_version,
"compilerFlags": flags, "sourceHashes": sources, "dependencyHashes": dependencies,
"artifacts": {p.name: _hash(p) for p in stage.iterdir() if p.name != "build.log"},
}
(stage / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
try:
stage.rename(target)
except OSError:
# A concurrent compiler may have published the identical cache first.
if not manifest_path.is_file():
raise
published = json.loads(manifest_path.read_text(encoding="utf-8"))
# PE linker timestamps can differ between concurrent equivalent builds.
# Validate the winning build against its own hashes and our identity.
if published.get("buildKey") != signature or not all(
(target / name).is_file() and _hash(target / name) == digest
for name, digest in published["artifacts"].items()
):
raise RuntimeError("Concurrent native build did not produce the expected artifacts.")
manifest = published
if stage.resolve().parent != cache:
raise RuntimeError("Unexpected native build staging directory.")
shutil.rmtree(stage)
return NativeBuild(target / "model.exe", manifest, False, time.perf_counter()-start)