Skip to content

Commit d55f9f6

Browse files
committed
v71: execute resource tuning, distribute owned CPU work and reject false chat completion
Route optimization commands and explicit follow-ups to bounded host policy receipts. Respect affinity/quota/RAM, assign distinct CPU slots to parallel Python experiments and reserve executor capacity for observation. Retain Windows adaptive guards and accepted GPU settings. Report missing result archives explicitly and store failed/empty work as failure. Add targeted profile-preserving updater applying the policy. Validation: full suite 1252 passed, 65 skipped with one existing PEFT warning; 107 focused tests passed after final receipt display change and added test. Ruff rlm/tests/tools excluding notebooks, bash syntax and git diff checks passed. Hardware throughput not measured.
1 parent 55d3abf commit d55f9f6

13 files changed

Lines changed: 647 additions & 20 deletions

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11

22
---
33

4-
Synta v70 avoids repeated full artifact reads within the same process, detects changed file
5-
identities, and bounds native startup verification. See [SYNTA_V70.md](SYNTA_V70.md).
4+
Synta v71 executes resource optimization directly, keeps follow-ups on the operator task,
5+
and rejects empty/failed work as successful chat completion. CPU concurrency adapts to
6+
affinity, quota and RAM. See [SYNTA_V71.md](SYNTA_V71.md).
7+
The startup verification fix from [v70](SYNTA_V70.md) is retained.
68

79
> **V100 fork, v100.14:** the local workflow uses your own llama.cpp server, no paid API.
810
> v100.14 fixes CPU research request budgets and one-slot job scheduling, trims

‎SYNTA_V71.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Synta v71: resource optimization and truthful execution status
2+
3+
An operator request to optimize CPU/RAM/resources now executes a host-owned operation,
4+
rather than asking the model to select a backtest. This includes mixed questions and
5+
commands such as “CPU Debian or Windows? Increase usage, do it.” Explicit follow-ups
6+
such as “do all of it” or “error, continue” resolve prior operator messages, not assistant
7+
claims. An intervening unrelated request ends that binding.
8+
9+
The controller writes `research/state/resource-policy.json`, archives the previous policy
10+
and receipts under `research/resource-optimization`, and reports the measured current
11+
admission. Debian admits up to four CPU/network jobs, bounded by affinity and cgroup v2
12+
CPU quotas, two threads per slot and a CPU reserved when possible. Concurrent owned Python experiments receive distinct CPU affinity slots instead of
13+
all being pinned to the first two CPUs. Each result and live drone status records its
14+
assigned CPUs. The tuned policy
15+
reserves 4 GiB available RAM plus 3 GiB per slot. Host CPU at 65% reduces concurrency
16+
and 85% stops new admission. Existing bounded jobs can finish. One model job remains
17+
allowed; six executor workers accommodate four CPU tasks, the model and the observer.
18+
19+
Windows affinity/RAM and adaptive RTX guards are retained. No board power cap, GPU
20+
clock or quality gate is changed. With a fresh idle owned CPU worker, the controller can
21+
queue the existing verified goal trial under the shared dispatch lock. Its existing label,
22+
source split and independent evaluation requirements still apply. Missing labels report
23+
a blocker instead of launching synthetic busywork. Existing owned jobs retain priority.
24+
No measured speedup, profit, continuous training or crash immunity is claimed.
25+
26+
Missing archived tool results return `archive_not_found` with the requested digest and
27+
recovery guidance. No missing content is invented. A chat whose tools all failed is
28+
stored as `failed` with its response and receipts; an empty reply is rejected. Successful
29+
partial work retains its receipts. No automatic rerun of mutations is introduced.
30+
31+
## Install
32+
33+
Use `tools/update-synta-resources.sh` with this release's pinned commit on Debian.
34+
It accepts v67-v71, requires the owned user service, retains the current accepted/live
35+
profile and goals, checks for exclusive experiments, stops the owned mission, installs
36+
v71, restarts it through the existing supervisor and applies the resource policy. It does
37+
not reinstall Windows, reset the ledger or restart Tailscale routing. Inspect the printed
38+
resource receipt for actual slot counts and Windows blockers. Windows does not need
39+
another installer for this change.
40+
41+
Hardware throughput and responsiveness must be measured on the deployment. This
42+
release does not promise that every arbitrary optimization is implemented, nor does
43+
CPU concurrency alone improve GPU inference. Accepted model settings are unchanged.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "rlms"
33
authors = [
44
{name = "Alex Zhang", email = "altzhang@mit.edu"},
55
]
6-
version = "0.1.3+v100.70"
6+
version = "0.1.3+v100.71"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/v100/chat_resources.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@
2828

2929
def instruction(message: str) -> bool:
3030
text = normalized(message)
31-
if "?" in text or re.match(r"^(?:czy|jak|why|how|is|are)\b", text):
31+
action = re.search(r"uzyw|wykorzyst|przydz|deleg|udostep|wspoldziel|shared", text)
32+
if not action or (
33+
re.match(r"^(?:czy|jak|why|how|is|are)\b", text)
34+
and not re.search(r"zrob|wykonaj|allocate|use", text)
35+
):
3236
return False
3337
if re.search(r"dashboard|\bhtml\b|pulpit|(?:ustaw|zmien)\w*\s+(?:cel|plan)", text):
3438
return False
3539
resource = re.findall(r"\b(?:rtx|windows\w*|cpu|ram\w*|dysk\w*|worker\w*|dron\w*)\b", text)
36-
return len(set(resource)) >= 2 and bool(
37-
re.search(r"uzyw|wykorzyst|przydz|deleg|udostep|wspoldziel|shared", text)
38-
)
40+
return len(set(resource)) >= 2
3941

4042

4143
def filter_actions(actions: list[dict]) -> tuple[list[dict], list[dict]]:
@@ -50,8 +52,10 @@ def status(root: Path) -> dict:
5052
from rlm.v100.distributed_compute import inspect
5153
from rlm.v100.drones import inspect as drone_status
5254
from rlm.v100.remote_helper import remote_profile, selected_helper
55+
from rlm.v100.resource_budget import local_budget
5356

5457
value = {
58+
"local": local_budget(root),
5559
"external_compute": inspect(root),
5660
"drones": drone_status(root),
5761
"scope": "Owned job execution only. Windows RAM/disk are worker-local resources, not pooled Debian RAM or shared VRAM. No arbitrary Windows shell is exposed.",

‎rlm/v100/drones.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def execute(root: Path, job: dict) -> dict:
155155
if job["kind"] == "python":
156156
from rlm.v100.research_sandbox import run
157157

158-
return run(root, job["branch"], job["payload"])
158+
return run(root, job["branch"], job["payload"], cpu_affinity=job.get("cpu_affinity"))
159159
if job["kind"] == "source":
160160
from rlm.v100.research_tools import ResearchTools
161161

@@ -277,10 +277,11 @@ def finish(root: Path, identity: str, value: dict) -> None:
277277

278278

279279
def service(root: Path, stop: threading.Event) -> None:
280-
# A single RTX job plus two CPU/network jobs. RTX requests share the global
280+
# One model job plus up to four CPU/network jobs and one observer. RTX requests share the global
281281
# existing helper pacing lock with foreground researchers and benchmark clients.
282-
with ThreadPoolExecutor(max_workers=4) as workers:
282+
with ThreadPoolExecutor(max_workers=6) as workers:
283283
active = {}
284+
affinities = {}
284285
goal_future = None
285286
goal_due = 0
286287
while not stop.is_set():
@@ -317,10 +318,11 @@ def service(root: Path, stop: threading.Event) -> None:
317318
}
318319
finish(root, identity, result)
319320
del active[identity]
321+
affinities.pop(identity, None)
320322
with connect(root) as db:
321323
from rlm.v100.resource_budget import local_budget
322324

323-
budget = local_budget()
325+
budget = local_budget(root)
324326
jobs = db.execute(
325327
"SELECT * FROM jobs WHERE state='queued' AND due<=? ORDER BY due LIMIT 16",
326328
(time.time(),),
@@ -333,12 +335,22 @@ def service(root: Path, stop: threading.Event) -> None:
333335
)
334336
if used >= (min(1, budget["cpu_slots"]) if llm else budget["cpu_slots"]):
335337
continue
338+
job = dict(row)
339+
if job["kind"] == "python":
340+
from rlm.v100.resource_budget import cpu_assignment
341+
342+
assignment = cpu_assignment(list(affinities.values()))
343+
if not assignment:
344+
continue
345+
job["cpu_affinity"] = assignment
336346
updated = db.execute(
337347
"UPDATE jobs SET state='running',updated=? WHERE id=? AND state='queued'",
338348
(time.time(), row["id"]),
339349
)
340350
if updated.rowcount:
341-
active[row["id"]] = (workers.submit(execute, root, dict(row)), row["kind"])
351+
if job["kind"] == "python":
352+
affinities[row["id"]] = assignment
353+
active[row["id"]] = (workers.submit(execute, root, job), row["kind"])
342354
atomic_json(
343355
root / "research/drones-status.json",
344356
{
@@ -347,6 +359,7 @@ def service(root: Path, stop: threading.Event) -> None:
347359
"active": list(active),
348360
"cpu_slots": budget["cpu_slots"],
349361
"budget": budget,
362+
"cpu_affinities": affinities,
350363
"rtx_slots": 0 if gpu_disabled(root) else 1,
351364
"local_model_slots": 1 if gpu_disabled(root) else 0,
352365
},

‎rlm/v100/mission_chat.py‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def save_direct_reply(root: Path, message: str, response: dict) -> dict:
145145
raise ValueError("Chat needs 1-8000 characters")
146146
if response.get("actions") or response.get("applied"):
147147
raise ValueError("Direct replies must be read-only facts")
148+
if not isinstance(response.get("answer"), str) or not response["answer"].strip():
149+
raise ValueError("Empty direct chat reply rejected")
148150
identity = uuid.uuid4().hex
149151
with connect(root) as db:
150152
db.execute(
@@ -565,6 +567,11 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
565567
# Authorization comes only from the current authenticated local user message,
566568
# never from a model-generated action or text retrieved from the internet.
567569
message = request["message"].strip()
570+
from rlm.v100 import resource_optimization
571+
572+
resource_context = resource_optimization.context(root, request)
573+
if resource_context is not None:
574+
return resource_optimization.apply(root, message, resource_context)
568575
from rlm.v100 import chat_progress
569576

570577
if chat_progress.requested(message):
@@ -1009,6 +1016,7 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
10091016
result["execution_receipts"] = execution_receipts(tool_receipts)
10101017
if tool_receipts and all(receipt_failed(row) for row in tool_receipts):
10111018
result["actions"] = []
1019+
result["execution_status"] = "failed"
10121020
result["answer"] = "Requested work failed: " + "; ".join(
10131021
str(row.get("result")) for row in tool_receipts
10141022
)
@@ -1084,13 +1092,24 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
10841092
if accepted_cpu is not None
10851093
else respond(root, directory, request)
10861094
)
1095+
if not isinstance(response.get("answer"), str) or not response["answer"].strip():
1096+
raise ValueError("Empty chat reply rejected; request not completed")
1097+
failed = response.get("execution_status") == "failed"
10871098
with connect(root) as db:
10881099
db.execute(
1089-
"UPDATE requests SET state='completed',response=?,error=NULL,updated=? WHERE id=? AND state='queued'",
1090-
(json.dumps(response, ensure_ascii=False), time.time(), request["id"]),
1100+
"UPDATE requests SET state=?,response=?,error=?,updated=? WHERE id=? AND state='queued'",
1101+
(
1102+
"failed" if failed else "completed",
1103+
json.dumps(response, ensure_ascii=False),
1104+
response["answer"][:500] if failed else None,
1105+
time.time(),
1106+
request["id"],
1107+
),
10911108
)
10921109
ActivityLog(root, "controller", "chat").write(
1093-
"decisions", "user-command-completed", {"id": request["id"], **response}
1110+
"errors" if failed else "decisions",
1111+
"user-command-failed" if failed else "user-command-completed",
1112+
{"id": request["id"], **response},
10941113
)
10951114
except BackendNotReady as error:
10961115
started = request.get("wait_started") or time.time()

‎rlm/v100/research_sandbox.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,22 @@ def command(root: Path, source: Path, work: Path, broker: Path) -> list[str]:
9595
]
9696

9797

98-
def run(root: Path, branch: str, code: str, seconds: int = 60) -> dict:
98+
def run(
99+
root: Path, branch: str, code: str, seconds: int = 60, cpu_affinity: list[int] | None = None
100+
) -> dict:
99101
if branch not in ("A", "B") or not isinstance(code, str) or not 1 <= len(code) <= 12000:
100102
raise ValueError("CPU experiment needs A/B and 1-12000 code characters")
101103
if type(seconds) is not int or not 1 <= seconds <= 60:
102104
raise ValueError("CPU experiment timeout must be 1-60 seconds")
103105
compile(code, "experiment.py", "exec")
106+
available = os.sched_getaffinity(0)
107+
affinity = sorted(available)[:2] if cpu_affinity is None else cpu_affinity
108+
if (
109+
not 1 <= len(affinity) <= 2
110+
or any(type(cpu) is not int or cpu not in available for cpu in affinity)
111+
or len(set(affinity)) != len(affinity)
112+
):
113+
raise ValueError("CPU experiment affinity must select 1-2 distinct allowed CPUs")
104114
# Verify the namespace runtime BEFORE executing any model-generated code.
105115
runtime(root)
106116
import shutil
@@ -155,7 +165,7 @@ def handle(self):
155165
start_new_session=True,
156166
)
157167
try:
158-
os.sched_setaffinity(process.pid, sorted(os.sched_getaffinity(0))[:2])
168+
os.sched_setaffinity(process.pid, affinity)
159169
deadline = time.monotonic() + seconds
160170
while process.poll() is None:
161171
used = sum(
@@ -186,6 +196,7 @@ def handle(self):
186196
"directory": str(folder),
187197
"exit_code": process.returncode,
188198
"timed_out": timed_out,
199+
"cpu_affinity": affinity,
189200
"seconds": time.monotonic() - started,
190201
"output": (folder / "output.log").read_text(errors="replace")[-12000:],
191202
"sources": observations,

‎rlm/v100/research_tools.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,14 @@ def execute(self, name: str, arguments: dict) -> dict:
609609
):
610610
raise ValueError("Invalid archive identity or byte offset")
611611
path = self.root / "research/tool-results" / f"{digest}.json"
612+
if not path.exists():
613+
return {
614+
"status": "failed",
615+
"code": "archive_not_found",
616+
"sha256": digest,
617+
"error": "Tool archive unavailable. Use only full_result_sha256 returned by a tool in this task. Do not invent an ID or infer the missing content; rerun the original read-only tool if known.",
618+
"retryable": False,
619+
}
612620
if path.is_symlink() or path.stat().st_size > 8 * 2**20:
613621
raise ValueError("Archive exceeds its read budget or is a symlink")
614622
raw = path.read_bytes()

‎rlm/v100/resource_budget.py‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,86 @@
11
"""Headroom admission, not synthetic activity or a board power limit."""
22

3+
import json
4+
import math
5+
import os
6+
from pathlib import Path
37

4-
def local_budget() -> dict:
8+
9+
def available_cpus(
10+
cgroup_root: Path = Path("/sys/fs/cgroup"), membership: Path = Path("/proc/self/cgroup")
11+
) -> int:
12+
"""Count process affinity and Linux cgroup v2 quota; admission reserves CPU separately."""
13+
count = (
14+
len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else os.cpu_count() or 1
15+
)
16+
if membership.exists():
17+
for line in membership.read_text().splitlines():
18+
if line.startswith("0::"):
19+
base = cgroup_root.resolve()
20+
folder = (base / line[3:].lstrip("/")).resolve()
21+
if not folder.is_relative_to(base):
22+
# A cgroup namespace can report a path outside the mounted subtree.
23+
# Its mounted root still exposes the namespace's effective root quota.
24+
folder = base
25+
while folder.is_relative_to(base):
26+
quota = folder / "cpu.max"
27+
if quota.exists():
28+
limit, period = quota.read_text().split()
29+
if limit != "max":
30+
count = min(count, max(1, math.floor(int(limit) / int(period))))
31+
if folder == base:
32+
break
33+
folder = folder.parent
34+
return max(1, count)
35+
36+
37+
def local_budget(root: Path | None = None) -> dict:
538
import psutil
639

740
cpu = psutil.cpu_percent(interval=0.05)
841
free = psutil.virtual_memory().available / 2**30
9-
slots = 0 if cpu >= 85 or free < 2 else 1 if cpu >= 65 or free < 4 else 2
42+
maximum = 2
43+
reserve_ram, ram_per_slot = 0, 2
44+
path = root / "research/state/resource-policy.json" if root is not None else None
45+
if path is not None and path.exists():
46+
policy = json.loads(path.read_text())
47+
maximum = policy["max_cpu_slots"]
48+
if type(maximum) is not int or not 1 <= maximum <= 4:
49+
raise ValueError("Local CPU policy must allow 1-4 slots")
50+
reserve_ram, ram_per_slot = (
51+
policy.get("reserve_ram_gib", 0),
52+
policy.get("ram_per_slot_gib", 2),
53+
)
54+
if (
55+
type(reserve_ram) is not int
56+
or not 0 <= reserve_ram <= 8
57+
or type(ram_per_slot) is not int
58+
or not 2 <= ram_per_slot <= 4
59+
):
60+
raise ValueError("Invalid local RAM reservation")
61+
cpus = available_cpus()
62+
maximum = min(maximum, max(1, (cpus - 1) // 2))
63+
ram_slots = max(0, int((free - reserve_ram) // ram_per_slot))
64+
slots = 0 if cpu >= 85 else min(1 if cpu >= 65 else maximum, ram_slots)
1065
return {
1166
"cpu_slots": slots,
67+
"max_cpu_slots": maximum,
68+
"available_cpus": cpus,
69+
"reserve_ram_gib": reserve_ram,
70+
"ram_per_slot_gib": ram_per_slot,
1271
"host_cpu_percent": cpu,
1372
"free_ram_gib": round(free, 2),
1473
"reason": "Host pressure; queued work retained"
1574
if not slots
1675
else "Measured CPU/RAM headroom",
17-
"scope": "At most two local CPU/network drones. GPU serving/training use existing exclusive admission. Windows uses its own lower limits; RTX remains operator-disabled.",
76+
"scope": "Bounded local CPU/network admission; one model job. GPU serving/training keep exclusive gates. Windows keeps its own pressure, RAM and game guards. Slots are concurrency limits, not measured speedup.",
1877
}
78+
79+
80+
def cpu_assignment(active: list[list[int]]) -> list[int]:
81+
"""Allocate distinct affinity slots for concurrent owned Python experiments."""
82+
cpus = sorted(os.sched_getaffinity(0))[: available_cpus()]
83+
if len(cpus) > 2:
84+
cpus = cpus[1:]
85+
used = {cpu for assignment in active for cpu in assignment}
86+
return [cpu for cpu in cpus if cpu not in used][:2]

0 commit comments

Comments
 (0)