Skip to content

Commit 77bfeb0

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): preserve inference runtime state
Separate proof experiments from worker lifecycle so decomposition iterations retain Primary/allens KV caches and perform health checks without restarts or cold-cache mutation. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7629d86 commit 77bfeb0

3 files changed

Lines changed: 74 additions & 137 deletions

File tree

autoresearch/prefill/program.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Use a lexicographic objective:
3434
1. Read `candidate.py` and `results.tsv`.
3535
2. State one concrete mathematical hypothesis for one unresolved leaf.
3636
3. Modify only `candidate.py`.
37-
4. Deploy the candidate to allens.
38-
5. Clear Primary and allens caches.
37+
4. Verify Primary and allens health without restarting either service.
38+
5. Preserve all KV caches across decomposition iterations.
3939
6. Run the fixed full-context acceptance workload.
4040
7. Run `prepare.py` against the resulting report.
4141
8. Keep the candidate only if every hard constraint passes and it closes an
@@ -50,6 +50,13 @@ the host records that lemma as a deduplicated child obligation. Completed GAN
5050
runs, transcripts, checkpoints, and ledger updates remain durable even when the
5151
candidate strategy is reverted or fixed evaluation fails.
5252

53+
Worker lifecycle and cache policy belong to the inference serving plane, not
54+
the proof experiment. `prefill_compute_chunk_tokens` is immutable during this
55+
loop. Never invoke launchctl, restart Primary/allens, clear KV, or run a cold
56+
benchmark here. Model/tokenizer/quantization/rope/window/cache-format changes
57+
must be deployed outside this supervisor. Cold benchmarks are explicit,
58+
separate invocations of `scripts/benchmark_prefill_architecture.py`.
59+
5360
Do not optimize output wording, scores, prizes, or other proof-irrelevant
5461
content. Prefill performance is a tertiary objective after mathematical
5562
decomposition progress, while preserving the complete semantic contract.

autoresearch/prefill/supervisor.py

Lines changed: 42 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import hashlib
99
import json
1010
import os
11-
import plistlib
1211
import re
1312
import shutil
1413
import socket
@@ -239,15 +238,20 @@ def repair_candidate_schema(
239238
"missing lemma."
240239
)
241240
changed.append("critic_directive")
242-
if not repaired.get("prefill_compute_chunk_tokens"):
243-
repaired["prefill_compute_chunk_tokens"] = int(
244-
current["prefill_compute_chunk_tokens"],
245-
)
241+
fixed_chunk_tokens = int(current["prefill_compute_chunk_tokens"])
242+
proposed_chunk_tokens = repaired.get("prefill_compute_chunk_tokens")
243+
try:
244+
proposed_chunk_tokens = int(proposed_chunk_tokens)
245+
except (TypeError, ValueError):
246+
proposed_chunk_tokens = None
247+
if (
248+
proposed_chunk_tokens is None
249+
or int(proposed_chunk_tokens) != fixed_chunk_tokens
250+
):
251+
repaired["prefill_compute_chunk_tokens"] = fixed_chunk_tokens
246252
changed.append("prefill_compute_chunk_tokens")
247253
else:
248-
repaired["prefill_compute_chunk_tokens"] = int(
249-
repaired["prefill_compute_chunk_tokens"],
250-
)
254+
repaired["prefill_compute_chunk_tokens"] = fixed_chunk_tokens
251255
return repaired, changed
252256

253257

@@ -502,7 +506,8 @@ def propose_candidate(
502506
"program exactly. Select one unresolved proof obligation and propose "
503507
"one falsifiable GAN strategy experiment. Return JSON only with keys: "
504508
+ ", ".join(REQUIRED_CANDIDATE_FIELDS)
505-
+ ". Allowed prefill_compute_chunk_tokens: 64, 128, 256. "
509+
+ ". prefill_compute_chunk_tokens is immutable and must equal "
510+
f"{current['prefill_compute_chunk_tokens']}. "
506511
"Do not weaken full context, final-only snapshots, or no-fallback rules."
507512
" The hypothesis must not repeat any hypothesis or hash in RESULTS. "
508513
"It must either construct a concrete object or attempt a concrete "
@@ -585,91 +590,20 @@ def propose_candidate(
585590
return candidate
586591

587592

588-
def deploy_candidate(worker_ssh: str, chunk_tokens: int) -> None:
589-
remote = f"""python3 - <<'PY'
590-
import os, plistlib
591-
from pathlib import Path
592-
p=Path.home()/'Library/LaunchAgents/ai.kakeya.prefill-worker.plist'
593-
d=plistlib.loads(p.read_bytes())
594-
a=d['ProgramArguments']
595-
i=a.index('--prefill-compute-chunk-tokens')
596-
a[i+1]='{chunk_tokens}'
597-
t=p.with_suffix('.plist.tmp')
598-
t.write_bytes(plistlib.dumps(d))
599-
os.chmod(t,0o644)
600-
t.replace(p)
601-
PY
602-
domain=gui/$(id -u)
603-
label=ai.kakeya.prefill-worker
604-
service=\"$domain/$label\"
605-
plist=\"$HOME/Library/LaunchAgents/$label.plist\"
606-
launchctl bootout \"$service\" 2>/dev/null || true
607-
for delay in 1 1 2 3 5; do
608-
if ! launchctl print \"$service\" >/dev/null 2>&1; then
609-
break
610-
fi
611-
sleep \"$delay\"
612-
done
613-
if launchctl print \"$service\" >/dev/null 2>&1; then
614-
echo \"worker service did not unload\" >&2
615-
exit 70
616-
fi
617-
loaded=0
618-
for delay in 1 2 3 5; do
619-
if launchctl bootstrap \"$domain\" \"$plist\"; then
620-
loaded=1
621-
break
622-
fi
623-
if launchctl print \"$service\" >/dev/null 2>&1; then
624-
loaded=1
625-
break
626-
fi
627-
sleep \"$delay\"
628-
done
629-
if [ \"$loaded\" -ne 1 ]; then
630-
echo \"worker service did not bootstrap\" >&2
631-
exit 71
632-
fi
633-
launchctl kickstart -k \"$service\"
634-
for attempt in $(seq 1 120); do
635-
if nc -G 2 -z 127.0.0.1 53051 >/dev/null 2>&1; then
636-
exit 0
637-
fi
638-
if ! launchctl print \"$service\" >/dev/null 2>&1; then
639-
echo \"worker service disappeared during startup\" >&2
640-
exit 72
641-
fi
642-
sleep 1
643-
done
644-
echo \"worker did not become ready on port 53051\" >&2
645-
exit 73
646-
"""
647-
subprocess.run(
648-
["ssh", "-o", "BatchMode=yes", worker_ssh, remote],
649-
check=True,
650-
)
651-
_wait_port("169.254.27.104", 53051)
652-
probe = subprocess.run(
653-
["ssh", worker_ssh, "ps -ax -o command="],
654-
check=True,
655-
capture_output=True,
656-
text=True,
657-
).stdout
658-
expected = f"--prefill-compute-chunk-tokens {chunk_tokens}"
659-
if expected not in probe:
660-
raise RuntimeError("deployed worker chunk size verification failed")
661-
662-
663-
def clear_primary_cache() -> None:
664-
subprocess.run(
665-
[
666-
"launchctl", "kickstart", "-k",
667-
f"gui/{os.getuid()}/ai.kakeya.grpc-runtime-prefill",
668-
],
669-
check=True,
670-
)
593+
def check_runtime_health(
594+
worker_address: str,
595+
dashboard: str = "http://127.0.0.1:8090",
596+
) -> dict:
597+
worker_host, worker_port_text = worker_address.rsplit(":", 1)
598+
_wait_port(worker_host, int(worker_port_text))
671599
_wait_port("127.0.0.1", 51051)
672600
_wait_port("127.0.0.1", 8090)
601+
summary = _json_request(
602+
f"{dashboard.rstrip('/')}/v1/network/summary",
603+
)
604+
if int(summary.get("online_nodes", 0)) < 1:
605+
raise RuntimeError("prefill fleet has no online worker")
606+
return summary
673607

674608

675609
def _backup(path: Path) -> bytes | None:
@@ -841,7 +775,6 @@ def run_iteration(args, iteration: int) -> dict:
841775
previous_candidate = candidate_path.read_bytes()
842776
previous_state = _backup(state_path)
843777
previous_ledger = _backup(ledger_path)
844-
previous_chunk = int(current["prefill_compute_chunk_tokens"])
845778

846779
proposed = current
847780
gan_completed = False
@@ -854,11 +787,19 @@ def run_iteration(args, iteration: int) -> dict:
854787
try:
855788
print(
856789
f"[autoresearch] iteration={iteration} "
857-
f"phase=predeploy-current candidate={current['candidate_id']}",
790+
f"phase=runtime-health-check candidate={current['candidate_id']}",
791+
flush=True,
792+
)
793+
health = check_runtime_health(
794+
args.worker_address,
795+
args.dashboard,
796+
)
797+
print(
798+
"[autoresearch] phase=runtime-healthy "
799+
f"online_nodes={health.get('online_nodes', 0)} "
800+
f"kv_hit_rate={health.get('kv_hit_rate', 0):.1%}",
858801
flush=True,
859802
)
860-
deploy_candidate(args.worker_ssh, previous_chunk)
861-
clear_primary_cache()
862803
if baseline is None and iteration == 0:
863804
print(
864805
"[autoresearch] phase=baseline using current candidate",
@@ -893,11 +834,6 @@ def run_iteration(args, iteration: int) -> dict:
893834
f"target={proposed['target_obligation_id']}",
894835
flush=True,
895836
)
896-
deploy_candidate(
897-
args.worker_ssh,
898-
proposed["prefill_compute_chunk_tokens"],
899-
)
900-
clear_primary_cache()
901837
validate_candidate(proposed)
902838
hypothesis_sha256 = hashlib.sha256(
903839
proposed["hypothesis"].strip().lower().encode(),
@@ -992,7 +928,6 @@ def run_iteration(args, iteration: int) -> dict:
992928
append_result(results_path, row)
993929
if not keep:
994930
candidate_path.write_bytes(previous_candidate)
995-
deploy_candidate(args.worker_ssh, previous_chunk)
996931
print(
997932
"[autoresearch] phase=candidate-reverted "
998933
"completed-run-preserved",
@@ -1010,14 +945,6 @@ def run_iteration(args, iteration: int) -> dict:
1010945
if not gan_completed:
1011946
_restore(state_path, previous_state)
1012947
_restore(ledger_path, previous_ledger)
1013-
try:
1014-
deploy_candidate(args.worker_ssh, previous_chunk)
1015-
except Exception as rollback_exc:
1016-
print(
1017-
"[autoresearch] phase=rollback-worker-failed "
1018-
f"error={type(rollback_exc).__name__}: {rollback_exc}",
1019-
flush=True,
1020-
)
1021948
if not gan_completed:
1022949
raise
1023950
row = {
@@ -1054,8 +981,12 @@ def run_iteration(args, iteration: int) -> dict:
1054981
def main() -> int:
1055982
parser = argparse.ArgumentParser()
1056983
parser.add_argument("--iterations", type=int, default=1)
1057-
parser.add_argument("--worker-ssh", default="allens")
984+
parser.add_argument(
985+
"--worker-address",
986+
default="169.254.27.104:53051",
987+
)
1058988
parser.add_argument("--address", default="127.0.0.1:51051")
989+
parser.add_argument("--dashboard", default="http://127.0.0.1:8090")
1059990
parser.add_argument(
1060991
"--tokenizer-id",
1061992
default=str(

tests/inference_engine/bench/test_autoresearch_supervisor.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from autoresearch.prefill.supervisor import (
22
append_result,
33
best_kept,
4-
deploy_candidate,
4+
check_runtime_health,
55
parse_research_verdict,
66
read_results,
77
repair_candidate_schema,
@@ -115,7 +115,7 @@ def test_strategy_schema_repair_accepts_uppercase_and_alias_keys():
115115
"allow_fallback": False,
116116
})
117117
assert repaired["candidate_id"] == "alias-trial"
118-
assert repaired["prefill_compute_chunk_tokens"] == 128
118+
assert repaired["prefill_compute_chunk_tokens"] == 256
119119
assert set(fields) == {
120120
"candidate_id",
121121
"target_obligation_id",
@@ -329,28 +329,23 @@ def test_append_result_migrates_legacy_results_header(tmp_path):
329329
assert rows[1]["hypothesis_sha256"] == "sha"
330330

331331

332-
def test_worker_deploy_waits_for_unload_and_readiness(monkeypatch):
333-
captured = []
334-
335-
class Result:
336-
stdout = "--prefill-compute-chunk-tokens 128"
337-
338-
def fake_run(command, **kwargs):
339-
captured.append((command, kwargs))
340-
return Result()
341-
332+
def test_runtime_health_check_is_read_only(monkeypatch):
333+
ports = []
342334
monkeypatch.setattr(
343335
"autoresearch.prefill.supervisor._wait_port",
344-
lambda *_args: None,
336+
lambda host, port: ports.append((host, port)),
345337
)
346-
monkeypatch.setattr("subprocess.run", fake_run)
347-
deploy_candidate("allens", 128)
348-
remote = captured[0][0][-1]
349-
assert captured[0][1]["check"] is True
350-
assert "worker service did not unload" in remote
351-
assert "launchctl bootstrap" in remote
352-
assert "nc -G 2 -z 127.0.0.1 53051" in remote
353-
assert "a[i+1]='128'" in remote
338+
monkeypatch.setattr(
339+
"autoresearch.prefill.supervisor._json_request",
340+
lambda _url: {"online_nodes": 1, "kv_hit_rate": 0.75},
341+
)
342+
summary = check_runtime_health("169.254.27.104:53051")
343+
assert summary["kv_hit_rate"] == 0.75
344+
assert ports == [
345+
("169.254.27.104", 53051),
346+
("127.0.0.1", 51051),
347+
("127.0.0.1", 8090),
348+
]
354349

355350

356351
def test_strategy_prefill_heartbeat_reports_delta(monkeypatch, capsys):
@@ -376,21 +371,25 @@ def test_strategy_prefill_heartbeat_reports_delta(monkeypatch, capsys):
376371
assert "remote_hits=1 reused=64" in output
377372

378373

379-
def test_supervisor_predeploys_before_real_strategy_proposal():
374+
def test_supervisor_preserves_runtime_and_cache_across_iterations():
380375
source = (
381376
Path(__file__).resolve().parents[3]
382377
/ "autoresearch"
383378
/ "prefill"
384379
/ "supervisor.py"
385380
).read_text()
386381
body = source[source.index("def run_iteration"):source.index("def main")]
387-
assert body.index("deploy_candidate(args.worker_ssh, previous_chunk)") < (
382+
assert body.index("check_runtime_health(") < (
388383
body.index("proposed = propose_candidate")
389384
)
390-
assert "phase=predeploy-current" in body
385+
assert "phase=runtime-health-check" in body
391386
assert "phase=strategy-proposal real-gemma" in body
392387
assert "if not gan_completed:" in body
393388
assert "phase=completed-run-preserved" in body
389+
assert "deploy_candidate" not in source
390+
assert "clear_primary_cache" not in source
391+
assert "launchctl" not in source
392+
assert "bootout" not in source
394393

395394

396395
def test_gan_subprocess_output_is_streamed_not_captured():

0 commit comments

Comments
 (0)