train-kit / src / ardegazu / train / checkpoint.cljs
  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})

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/train-kit.git