Speed up rectification scoring without changing results.
Cache work that depends only on the candidate minute, drop unread response fields, and reject windows above 241 candidates before any chart is built. Eleven canonical scenario hashes match 77057a819. Sealed and reported-offset replays match the 2026-10-08 records (changed_trial_count 8 and 581) and are stored as scoring_perf_2026_10_13.
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
74384be024
commit
8e0503f9c0
@@ -50,7 +50,7 @@ def worker(root: Path, dated: bool):
|
||||
request = make_request(case)
|
||||
if dated:
|
||||
request["candidate_intervals"] = [{"start_at": f'{request["birth_date"]}T{request["start_time"]}', "end_at": f'{request["birth_date"]}T{request["end_time"]}'}]
|
||||
result.append(projection(score(request), width))
|
||||
result.append(projection(score(request, include_internal=True), width))
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bench and canonical dump for the 2026-10-13 rectification scoring speedup.
|
||||
|
||||
Read-only aside from the dump files the caller asks for. Cross-process dumps
|
||||
must be launched with PYTHONHASHSEED=0. The baseline tree has no
|
||||
include_internal parameter; that call falls back to the old signature.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.rectification.api_service import block_scan, diagnostics, score_candidates
|
||||
from scripts.rectification.contracts import normalize_rectification_request
|
||||
from scripts.rectification import event_probes
|
||||
from scripts.rectification import scoring_service
|
||||
from scripts import active_rectification_event_engine as event_engine
|
||||
|
||||
FROZEN_TODAY = date(2026, 9, 16)
|
||||
EVENT_SPECS = (
|
||||
("education", "education_start", "2008-01-01", "2008-12-31", "year"),
|
||||
("career", "career_entry", "2012-06-01", "2012-06-30", "month"),
|
||||
("relocation", "relocation", "2014-03-01", "2014-03-31", "month"),
|
||||
("relationship", "relationship_commitment", "2016-10-01", "2016-10-31", "month"),
|
||||
("career", "promotion", "2018-01-01", "2018-12-31", "year"),
|
||||
("finance", "asset_change", "2019-05-01", "2019-05-31", "month"),
|
||||
("family", "family_event", "2020-08-01", "2020-08-31", "month"),
|
||||
("health", "self_health_event", "2022-01-01", "2022-12-31", "year"),
|
||||
)
|
||||
|
||||
|
||||
class _FrozenDate(date):
|
||||
@classmethod
|
||||
def today(cls) -> date:
|
||||
return FROZEN_TODAY
|
||||
|
||||
|
||||
def _events() -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for index, (domain, kind, start, end, precision) in enumerate(EVENT_SPECS, start=1):
|
||||
rows.append({
|
||||
"id": f"00000000-0000-4000-8000-{index:012d}",
|
||||
"domain": domain,
|
||||
"event_kind": kind,
|
||||
"date_start": start,
|
||||
"date_end": end,
|
||||
"precision": precision,
|
||||
"summary": "虚构",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def make_request(start: str, end: str, **extra: Any) -> dict[str, Any]:
|
||||
body = {
|
||||
"birth_date": "1990-01-01",
|
||||
"start_time": start,
|
||||
"end_time": end,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"events": _events(),
|
||||
**extra,
|
||||
}
|
||||
return normalize_rectification_request(body, today=FROZEN_TODAY)
|
||||
|
||||
|
||||
def _strip(value: Any, keys: set[str]) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {key: _strip(item, keys) for key, item in value.items() if key not in keys}
|
||||
if isinstance(value, list):
|
||||
return [_strip(item, keys) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _canonical(value: Any) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode("ascii")
|
||||
|
||||
|
||||
def _score(request: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
return score_candidates(request, include_internal=True)
|
||||
except TypeError:
|
||||
return score_candidates(request)
|
||||
|
||||
|
||||
def _call(kind: str, request: dict[str, Any], *, internal: bool = False) -> dict[str, Any]:
|
||||
if kind == "score":
|
||||
# Bench times the product call. Dump asks for the internal matrix so
|
||||
# the baseline tree, which always returned it, stays comparable.
|
||||
if internal:
|
||||
return _score(request)
|
||||
return score_candidates(request)
|
||||
if kind == "block":
|
||||
return block_scan(request)
|
||||
if kind == "diagnostics":
|
||||
return diagnostics(request)
|
||||
raise ValueError(kind)
|
||||
|
||||
|
||||
def scenarios() -> list[dict[str, Any]]:
|
||||
from tests.test_rectification_engine_memoization import public_score_request
|
||||
|
||||
rows = [
|
||||
{"name": "12:00-12:20", "kind": "score", "request": make_request("12:00", "12:20")},
|
||||
{"name": "11:45-12:15", "kind": "score", "request": make_request("11:45", "12:15")},
|
||||
{"name": "11:00-13:00", "kind": "score", "request": make_request("11:00", "13:00")},
|
||||
{"name": "23:20-00:40", "kind": "score", "request": make_request("23:20", "00:40")},
|
||||
{"name": "declined-relationship-career", "kind": "score", "request": make_request(
|
||||
"11:30", "12:30", declined_domains=["relationship", "career"],
|
||||
)},
|
||||
{"name": "refresh-probes-columns", "kind": "score", "request": make_request(
|
||||
"11:30", "12:30", refresh_probes=True, column_times=["11:40", "12:10", "12:20"],
|
||||
)},
|
||||
{"name": "minute-step-2", "kind": "score", "request": make_request("10:00", "13:00", minute_step=2)},
|
||||
{"name": "block-step-10", "kind": "block", "request": make_request("00:00", "23:59", minute_step=10)},
|
||||
{"name": "block-step-5", "kind": "block", "request": make_request("10:00", "14:00", minute_step=5)},
|
||||
{"name": "public-score-request", "kind": "score", "request": normalize_rectification_request(
|
||||
public_score_request(), today=FROZEN_TODAY,
|
||||
)},
|
||||
{"name": "diagnostics-11:00-13:00", "kind": "diagnostics", "request": make_request("11:00", "13:00")},
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
def _strip_keys(kind: str) -> set[str]:
|
||||
# diagnostics no longer repeats the receipt. Drop it on both sides so the
|
||||
# remaining payload is what must stay byte-identical.
|
||||
if kind == "diagnostics":
|
||||
return {"column_compare_ms", "candidate_decision_receipt"}
|
||||
return {"column_compare_ms"}
|
||||
|
||||
|
||||
class _SegmentTimer:
|
||||
def __init__(self) -> None:
|
||||
self.totals = {
|
||||
"guided_collect_windows": 0.0,
|
||||
"merge_transition_proximity": 0.0,
|
||||
"get_current_narayana_dasha": 0.0,
|
||||
"derive_functional_benefic_malefic": 0.0,
|
||||
"build_candidate_static_context": 0.0,
|
||||
}
|
||||
|
||||
def wrap(self, owner: Any, name: str, key: str) -> Callable[[], None]:
|
||||
original = getattr(owner, name)
|
||||
|
||||
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
return original(*args, **kwargs)
|
||||
finally:
|
||||
self.totals[key] += time.perf_counter() - started
|
||||
|
||||
setattr(owner, name, wrapped)
|
||||
|
||||
def restore() -> None:
|
||||
setattr(owner, name, original)
|
||||
|
||||
return restore
|
||||
|
||||
|
||||
def _median(samples: list[float]) -> float:
|
||||
return round(statistics.median(samples), 4)
|
||||
|
||||
|
||||
def bench() -> dict[str, Any]:
|
||||
jobs = [
|
||||
("12:00-12:20", "score", make_request("12:00", "12:20")),
|
||||
("11:45-12:15", "score", make_request("11:45", "12:15")),
|
||||
("11:00-13:00", "score", make_request("11:00", "13:00")),
|
||||
("block-step-10", "block", make_request("00:00", "23:59", minute_step=10)),
|
||||
]
|
||||
rows = []
|
||||
event_probes.date = _FrozenDate
|
||||
try:
|
||||
for name, kind, request in jobs:
|
||||
samples = []
|
||||
segments = []
|
||||
sizes = []
|
||||
counts = []
|
||||
for _ in range(5):
|
||||
timer = _SegmentTimer()
|
||||
restores = [
|
||||
timer.wrap(event_probes, "guided_collect_windows", "guided_collect_windows"),
|
||||
timer.wrap(scoring_service, "merge_transition_proximity", "merge_transition_proximity"),
|
||||
timer.wrap(event_engine.narayana_dasha, "get_current_narayana_dasha", "get_current_narayana_dasha"),
|
||||
timer.wrap(event_engine.functional_benefics, "derive_functional_benefic_malefic", "derive_functional_benefic_malefic"),
|
||||
timer.wrap(event_engine, "build_candidate_static_context", "build_candidate_static_context"),
|
||||
]
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = _call(kind, request)
|
||||
finally:
|
||||
for restore in restores:
|
||||
restore()
|
||||
elapsed = time.perf_counter() - started
|
||||
samples.append(elapsed)
|
||||
segments.append({key: round(value, 4) for key, value in timer.totals.items()})
|
||||
sizes.append(len(_canonical(_strip(result, _strip_keys(kind)))))
|
||||
counts.append(result.get("candidate_count") or len(result.get("candidate_scores") or []))
|
||||
median_segments: dict[str, float] = {}
|
||||
for key in segments[0]:
|
||||
median_segments[key] = _median([item[key] for item in segments])
|
||||
rows.append({
|
||||
"window": name,
|
||||
"candidate_count": counts[0],
|
||||
"median_seconds": _median(samples),
|
||||
"segment_median_seconds": median_segments,
|
||||
"response_bytes": int(statistics.median(sizes)),
|
||||
})
|
||||
finally:
|
||||
event_probes.date = date
|
||||
return {"runs": 5, "rows": rows}
|
||||
|
||||
|
||||
def dump(out_dir: Path) -> list[dict[str, str]]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
written = []
|
||||
event_probes.date = _FrozenDate
|
||||
try:
|
||||
for row in scenarios():
|
||||
result = _call(row["kind"], row["request"], internal=True)
|
||||
payload = _strip(result, _strip_keys(row["kind"]))
|
||||
raw = _canonical(payload)
|
||||
digest = hashlib.sha256(raw).hexdigest()
|
||||
path = out_dir / f"{row['name'].replace(':', '')}.json"
|
||||
path.write_bytes(raw)
|
||||
written.append({"name": row["name"], "sha256": digest, "bytes": str(len(raw))})
|
||||
print(f"{row['name']}\t{digest}\t{len(raw)}")
|
||||
finally:
|
||||
event_probes.date = date
|
||||
return written
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=("bench", "dump"))
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "artifacts" / "rect-scoring-perf-20261013")
|
||||
args = parser.parse_args()
|
||||
if args.command == "bench":
|
||||
print(json.dumps(bench(), ensure_ascii=True, indent=2))
|
||||
else:
|
||||
dump(args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,8 +30,11 @@ from scripts.research.sealed_holdout_rerun import (
|
||||
OFFSETS = (-30, -20, -15, -10, -8, -5, -3, 0, 3, 5, 8, 10, 15, 20, 30)
|
||||
RADII = (15, 30, 60)
|
||||
MINUTE_STEP = 1
|
||||
FREEZE = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.json"
|
||||
# 原值: docs/research/reported_offset_ephemeris_nutation_2026_10_08.{freeze.json,json}
|
||||
# 新值: docs/research/reported_offset_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 原因: BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/reported_offset_scoring_perf_2026_10_13.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_scoring_perf_2026_10_13.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/reported_offset_2026_09_20.json"
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,11 @@ from scripts.minute_rectification_feature_facts_v4 import build_feature_fact_row
|
||||
from scripts.minute_rectification_holdout_validator import validate
|
||||
|
||||
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json"
|
||||
# 原值: docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.{freeze.json,json}
|
||||
# 新值: docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 原因: BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/sealed_holdout_rerun_2026_09_20.json"
|
||||
ARCHIVE = ROOT / "docs/research/history/rectification_pre_cross_midnight_2026_09_20"
|
||||
PRODUCTION_FILES = [
|
||||
|
||||
Reference in New Issue
Block a user