KurisiTOMA's picture
chore: publish H2 real-world RL system
d8290eb verified
Raw History Blame Contribute Delete
4.44 kB
"""Reserved asynchronous summary logging; disk work stays off the control thread."""
from __future__ import annotations
from collections import Counter
import csv
import json
from pathlib import Path
import queue
import threading
import numpy as np
from .events import ADJUDICATION_VERSION
from .metrics import BallMetrics, REWARD_FIELDS, format_ball
def json_default(value):
if isinstance(value, np.ndarray):
return value.tolist()
if isinstance(value, np.generic):
return value.item()
raise TypeError(type(value).__name__)
class AsyncJournal:
def __init__(self, root, capacity):
self.root = Path(root)
self.queue = queue.Queue()
self.slots = threading.BoundedSemaphore(capacity)
self.error = None
self.closed = False
self.dropped_balls = Counter()
self.dropped_telemetry = 0
self.submitted_balls = self.written_balls = 0
self.metrics = BallMetrics()
self.thread = threading.Thread(target=self._run, name="h2-rl-journal", daemon=True)
self.thread.start()
def reserve(self):
return not self.closed and self.error is None and self.slots.acquire(blocking=False)
def missed_ball(self, phase):
self.dropped_balls[phase] += 1
if sum(self.dropped_balls.values()) == 1:
print("[RL ERROR] ball summary unavailable; unlogged attempts are counted in journal_status.json")
def submit_reserved(self, record):
if record.get("kind", "ball") == "ball":
self.submitted_balls += 1
self.queue.put_nowait(record)
def telemetry(self, record):
if self.reserve():
self.submit_reserved(dict(kind="telemetry", summary=record))
else:
self.dropped_telemetry += 1
def snapshot(self):
return dict(unlogged_attempts=dict(self.dropped_balls),
submitted_balls=self.submitted_balls, written_balls=self.written_balls,
unwritten_balls=self.submitted_balls-self.written_balls,
dropped_telemetry=self.dropped_telemetry, error=self.error)
def _run(self):
try:
with (self.root / "balls.jsonl").open("a", encoding="utf-8") as balls, \
(self.root / "control.jsonl").open("a", encoding="utf-8") as control, \
(self.root / "metrics.csv").open("w", newline="", encoding="utf-8") as metrics:
template = dict(kind="", phase="", attempt="", **dict.fromkeys(REWARD_FIELDS),
**self.metrics.phases["warmup"].snapshot())
writer = csv.DictWriter(metrics, fieldnames=list(template))
writer.writeheader()
while True:
item = self.queue.get()
if item is None:
for phase, stats in self.metrics.phases.items():
writer.writerow(dict(kind="final", phase=phase, attempt="",
**stats.snapshot(self.dropped_balls[phase])))
return
try:
summary = dict(item["summary"], adjudication_version=ADJUDICATION_VERSION)
is_ball = item.get("kind", "ball") == "ball"
stream = balls if is_ball else control
stream.write(json.dumps(summary, default=json_default, allow_nan=False) + "\n")
stream.flush()
if is_ball:
self.written_balls += 1
row = self.metrics.add(summary, self.dropped_balls[summary["phase"]])
writer.writerow(row)
metrics.flush()
print(format_ball(summary, row), flush=True)
finally:
self.slots.release()
except Exception as error:
self.error = str(error)
print(f"[RL ERROR] journal failed: {error}", flush=True)
def close(self):
if self.closed:
return
self.closed = True
self.queue.put_nowait(None)
self.thread.join()
(self.root / "journal_status.json").write_text(
json.dumps(self.snapshot(), indent=2), encoding="utf-8")
if self.error:
raise RuntimeError(f"RL journal failed: {self.error}")