1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118 | ;; ported-from: src/checkpoint.ts @ v1.0.0 (extracted-from: bot/src/rl/checkpoint.ts @ fa686ee)
;;
;; Checkpoint files: atomic writes (temp + rename in the same directory) and
;; tolerant interval reloads. One writer (the trainer), many readers (the
;; bots) — no locks needed. A missing or corrupt checkpoint means "no model":
;; callers fall back to their stock brain with one log line.
(ns ardegazu.train.checkpoint
(:require ["node:fs" :as fs]
["node:path" :as path]
[ardegazu.train.model :as model]
[ardegazu.train.rev :as rev]
[shadow.cljs.modern :refer (defclass)]))
(defn checkpoint-path
"The live checkpoint a promoted model ships to."
[model-dir game]
(path/join model-dir (str game ".json")))
(defn candidate-path
"Where a model waits while it hasn't passed its promotion gate yet."
[model-dir game]
(path/join model-dir (str game ".candidate.json")))
(defn load-model
"Parse + structurally validate a checkpoint file; nil on any failure."
[file]
(try
(let [raw (js/JSON.parse (fs/readFileSync file "utf8"))]
(if (model/is-model raw) raw nil))
(catch :default _ nil)))
;; new-in v2.1.0: the written JSON carries `rev` (see checkpoint-json). It is
;; additive — readers ignore it, `load-model` accepts rev-less files unchanged.
(def ^:private meta-keys #js ["v" "game" "episodes" "rev" "updatedAt"])
(defn- checkpoint-json
"The exact object a checkpoint file holds: a fixed key order — v, game,
episodes, rev, updatedAt — followed by every remaining own key of `m` in its
original order (net / q, plus anything a consumer carries along). `rev` is
always recomputed from the weights, so an inherited stale one is dropped.
A structurally invalid `m` passes through untouched: the writer never blocks
a trainer over a shape it cannot label."
[m]
(if-not (model/is-model m)
m
(let [out #js {}]
(set! (.-v out) (.-v ^js m))
(set! (.-game out) (.-game ^js m))
(set! (.-episodes out) (.-episodes ^js m))
(set! (.-rev out) (rev/model-rev m))
(set! (.-updatedAt out) (.-updatedAt ^js m))
(let [ks (js/Object.keys m)]
(dotimes [i (.-length ^js ks)]
(let [k (aget ks i)]
(when (identical? -1 (.indexOf meta-keys k))
(unchecked-set out k (unchecked-get m k))))))
out)))
(defn save-model
"Atomic on the same filesystem: write a temp file, then rename over."
[file m]
(fs/mkdirSync (path/dirname file) #js {:recursive true})
(let [tmp (str file ".tmp-" (.-pid js/process))]
(fs/writeFileSync tmp (js/JSON.stringify (checkpoint-json m)))
(fs/renameSync tmp file))
js/undefined)
(defn- read-checkpoint!
"The interval body: re-stat, and only reparse when mtime moved."
[^js store]
(let [mtime (try
(.-mtimeMs (fs/statSync (.-_file store)))
(catch :default _ nil))] ; not there (yet)
(when (some? mtime)
(when-not (identical? mtime (.-_mtime store))
(let [m (load-model (.-_file store))]
(when (some? m)
(set! (.-_mtime store) mtime)
(set! (.-_model store) m)
(when-some [cb (.-_onReload store)]
(cb m))))))
js/undefined))
;; A live bot's view of one checkpoint: load now, re-stat on an interval.
(defclass CheckpointStore
(field _file)
(field _model)
(field _mtime)
(field _timer)
(field _onReload)
(constructor [this file reload-ms on-reload]
(set! _file file)
(set! _model nil)
(set! _mtime 0)
(set! _timer nil)
(set! _onReload on-reload)
(read-checkpoint! this)
(when (> reload-ms 0)
(set! _timer (js/setInterval (fn [] (read-checkpoint! this)) reload-ms))
;; timer.unref?.() — never keep the process alive for a reload loop
(when (some? (.-unref ^js _timer))
(.unref ^js _timer))))
Object
(close [this]
(when (some? _timer)
(js/clearInterval _timer))
(set! _timer nil)
js/undefined))
;; `model` is a getter in the TS API (store.model, no call) — defclass has no
;; getter form, so it is attached to the prototype directly. The backing field
;; is _model precisely so the own-property never shadows this getter.
(js/Object.defineProperty
(.-prototype CheckpointStore) "model"
#js {:get (fn [] (this-as ^js self (.-_model self)))
:configurable true})
|