优化原生结果编码传输与浏览器缓存,记录八路性能基线

原生结果series通过字节索引直传,C端使用Ryu精确回读编码和64 KiB批量写出;网页采用Float64缓存和CSV工作线程,减少结果处理与保存等待。

补充八路AME曲线核查、全流程分阶段计时、独立编码基准和复现工具,固定后续优化采用修正八路及rtol=1e-8。C写出1.1808→0.1638 s,点击到可查看8.0100→6.9756 s。

验证:最终10项编码专项、29项相关后端回归通过;8份原生结果逐位一致,16次网页结果/CSV/刷新恢复通过。前端构建及缓存/CSV专项在本轮结果处理工作中通过。环境、原始大结果与临时构建不纳入Git。
This commit is contained in:
lujingze committed 2026-09-11 15:09:15 +00:00
1 parent 808c484f5b
commit 3bc4be3c06
55 files changed
+20522 -141

No files matched your search

+2 -2
View File
@@ -30,9 +30,9 @@ def simulation_config(simulation) -> SolveIVPConfig:
def simulate_network(network, simulation, *, progress_callback=None,
cancel_check=None, activity_tracker=None, backend=None):
cancel_check=None, activity_tracker=None, backend=None, raw_series=False):
numeric_engine_name(backend)
config = simulation_config(simulation)
from app.simulation.native_codegen.runner import simulate_native
return simulate_native(network, config, sample_step=simulation.sample_step, progress_callback=progress_callback,
cancel_check=cancel_check, activity_tracker=activity_tracker)
cancel_check=cancel_check, activity_tracker=activity_tracker, raw_series=raw_series)
+1 -1
View File
@@ -52,7 +52,7 @@ def toolchain() -> tuple[str, Path, str]:
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"))
runtime = sorted(NATIVE.rglob("*.c")) + sorted((NATIVE / "include").rglob("*.h"))
flags = ["-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-ffp-contract=off", "-fno-fast-math"]
executable_name = "model.exe" if os.name == "nt" else "model"
if os.name == "nt":
+15 -6
View File
@@ -12,13 +12,14 @@ import time
from app.simulation.config import SolveIVPConfig
from app.simulation.results import GenericSimulationResult
from .transport import NativeSeriesJson, read_indexed_result
from .build import NativeBuild, build_native
from .compiler import NativeCapabilityError, compile_native_program
def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: float, *,
run_dir: Path, record_samples=True, cancel_check=None,
progress_callback=None, activity_tracker=None, timeout=300.0) -> dict:
progress_callback=None, activity_tracker=None, timeout=300.0, raw_series=False) -> dict:
if config.method not in ("RK45", "BDF"):
raise NativeCapabilityError(f"Native v1 does not support method {config.method}.")
if not isinstance(config.atol, (int, float)) or config.atol != 1e-8 or config.first_step is not None:
@@ -26,13 +27,16 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
run_dir.mkdir(parents=True, exist_ok=True)
output = run_dir / "result.json"
cancel_path = run_dir / "cancel.request"
if output.exists() or cancel_path.exists():
index_path = run_dir / "result-index.json"
if output.exists() or cancel_path.exists() or index_path.exists():
raise ValueError("Native execution requires a fresh run directory.")
command = [str(build.executable), "--method", config.method,
"--start", str(config.t_start), "--stop", str(config.t_stop),
"--sample-step", str(sample_step), "--max-step", str(config.max_step),
"--rtol", str(config.rtol), "--timeout", str(timeout),
"--cancel-file", str(cancel_path.resolve()), "--output", str(output.resolve())]
if raw_series:
command.extend(["--result-index", str(index_path.resolve())])
if not record_samples:
command.append("--solve-only")
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
@@ -86,9 +90,13 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
process.stderr.close()
if not output.is_file():
raise RuntimeError(f"Native worker exited with code {process.returncode} without results; see {run_dir / 'worker.log'}.")
payload = json.loads(output.read_text(encoding="utf-8"))
if process.returncode not in (0, 2):
raise RuntimeError(f"Native worker failed with exit code {process.returncode}.")
try:
payload = (read_indexed_result(output, index_path) if raw_series
else json.loads(output.read_text(encoding="utf-8")))
except OSError as exc:
raise RuntimeError(f"Cannot read native worker result artifacts: {exc}") from exc
payload["processWallSeconds"] = time.perf_counter()-started
payload["buildKey"] = build.manifest["buildKey"]
payload["cacheHit"] = build.cache_hit
@@ -99,7 +107,7 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
def simulate_native(network, config, *, sample_step, progress_callback=None,
cancel_check=None, activity_tracker=None):
cancel_check=None, activity_tracker=None, raw_series=False):
if config.method not in ("RK45", "BDF"):
raise NativeCapabilityError(f"Native v1 does not support method {config.method}.")
if progress_callback:
@@ -109,7 +117,7 @@ def simulate_native(network, config, *, sample_step, progress_callback=None,
with tempfile.TemporaryDirectory(prefix="native-simulation-") as directory:
data = execute_native(build, config, sample_step, run_dir=Path(directory),
cancel_check=cancel_check, progress_callback=progress_callback,
activity_tracker=activity_tracker)
activity_tracker=activity_tracker, raw_series=raw_series)
totals = {
"nfev": data["nfev"], "njev": data["njev"], "nlu": data["nlu"],
"acceptedStepCount": data["acceptedSteps"], "rejectedStepCount": data["rejectedSteps"],
@@ -124,5 +132,6 @@ def simulate_native(network, config, *, sample_step, progress_callback=None,
variables=program.variables, series=data["series"], final=data["final"],
diagnostics={"backend": "native-c", "native": {k: v for k, v in data.items()
if k not in ("series", "final", "finalState")}, "integration": {"method": config.method, "rtol": config.rtol, "totals": totals},
"stateCount": len(program.state_keys), "sampleCount": len(data["series"]["time"])},
"stateCount": len(program.state_keys), "sampleCount": (data["series"].sample_count if isinstance(data["series"], NativeSeriesJson)
else len(data["series"].get("time", [])))},
)
@@ -0,0 +1,63 @@
"""Carry trusted C-generated series JSON without a Python float-array round trip.
Only native output plus its byte index may construct this transport object. Public
JSON still has the ordinary object/array schema; synchronous callers materialize
it explicitly. Bytes own their lifetime independently of the worker directory.
"""
from dataclasses import dataclass
import json
from pathlib import Path
@dataclass(frozen=True)
class NativeSeriesJson:
data: bytes
sample_count: int
def materialize(self) -> dict[str, list[float]]:
return json.loads(self.data)
def read_indexed_result(output: Path, index_path: Path) -> dict:
index = json.loads(index_path.read_bytes())
if not isinstance(index, dict) or type(index.get('version')) is not int or index['version'] != 1:
raise ValueError('Unsupported native result index.')
names = ('seriesStart', 'seriesEnd', 'resultBytes', 'sampleCount')
if any(type(index.get(key)) is not int for key in names):
raise ValueError('Invalid native result index integers.')
start, end, length, count = (index[key] for key in names)
if not (0 < start < end < length and count >= 0):
raise ValueError('Invalid native result index bounds.')
if output.stat().st_size != length:
raise ValueError('Incomplete native result file.')
raw = output.read_bytes()
if (len(raw) != length or not raw[:start].endswith(b'"series":')
or raw[start:start+1] != b'{' or raw[end-1:end] != b'}'
or not raw[end:].startswith(b',"final":')):
raise ValueError('Native result index does not match the output layout.')
# The C writer supplies exact boundaries. No search through strings or numeric
# arrays, and no large json.loads call: only diagnostics/final values are read.
metadata = json.loads(raw[:start] + b'{}' + raw[end:])
if not isinstance(metadata, dict) or metadata.get('series') != {}:
raise ValueError('Invalid native result metadata.')
metadata['series'] = NativeSeriesJson(raw[start:end], count)
return metadata
def serialize_result_parts(payload: dict) -> tuple[bytes, ...]:
"""Return one JSON object in three byte segments, without copying its series.
The only raw position is payload.result.series, produced by our C writer;
all model-provided labels, messages and keys use the standard JSON encoder.
"""
result = payload.get('result')
series = result.get('series') if isinstance(result, dict) else None
if not isinstance(series, NativeSeriesJson):
return (json.dumps(payload, ensure_ascii=False, separators=(',', ':')).encode('utf-8'),)
metadata = {key: value for key, value in result.items() if key != 'series'}
outer = {key: value for key, value in payload.items() if key != 'result'}
encoded = json.dumps(metadata, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
remainder = json.dumps(outer, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
prefix = b'{"result":' + encoded[:-1] + (b',' if metadata else b'') + b'"series":'
suffix = b'}' + (b',' + remainder[1:] if outer else b'}')
return prefix, series.data, suffix
+11 -4
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
from app.simulation.native_codegen.transport import NativeSeriesJson
from app.simulation.core.metadata import ResultVariableMetadata
SimulationRunStatus = Literal["completed", "cancelled", "failed"]
@@ -30,11 +33,15 @@ class GenericSimulationResult:
simulated_until: float
requested_stop_time: float
variables: tuple[ResultVariableMetadata, ...]
series: dict[str, list[float]]
series: dict[str, list[float]] | NativeSeriesJson
final: dict[str, float]
diagnostics: dict[str, object]
def as_dict(self) -> dict[str, object]:
def as_dict(self, *, raw_series: bool = False) -> dict[str, object]:
from app.simulation.native_codegen.transport import NativeSeriesJson
series = self.series
if isinstance(series, NativeSeriesJson) and not raw_series:
series = series.materialize()
return {
"success": self.success,
"status": self.status,
@@ -43,7 +50,7 @@ class GenericSimulationResult:
"simulatedUntil": self.simulated_until,
"requestedStopTime": self.requested_stop_time,
"variables": [variable.as_dict() for variable in self.variables],
"series": self.series,
"series": series,
"final": self.final,
"diagnostics": self.diagnostics,
}