EgeEken commited on
Commit
a15e50d
·
1 Parent(s): d0d6ad2

Add RL Training Lab

Browse files
learned_filler.py CHANGED
@@ -112,9 +112,10 @@ def _silu(x):
112
 
113
 
114
  def _mlp(x, weights):
115
- """## Runs the shared two-layer SiLU trunk"""
116
- W0, b0, W2, b2 = weights
117
- return _silu(_silu(x @ W0.T + b0) @ W2.T + b2)
 
118
 
119
 
120
  class LearnedFiller:
@@ -125,8 +126,10 @@ class LearnedFiller:
125
  self.Wa, self.ba = npz["Wa"], npz["ba"]
126
  self.Wv, self.bv = npz["Wv"], npz["bv"]
127
  if self.hidden > 0:
128
- self.W0, self.b0 = npz["W0"], npz["b0"]
129
- self.W2, self.b2 = npz["W2"], npz["b2"]
 
 
130
  self.feat_mean, self.feat_std = npz["feat_mean"], npz["feat_std"]
131
  self.lmin, self.lmax = float(npz["lmin"]), float(npz["lmax"])
132
  self.cheap = bool(npz["cheap"])
@@ -146,7 +149,7 @@ class LearnedFiller:
146
  def _forward(self, x, trace=None) -> tuple[np.ndarray, np.ndarray]:
147
  """## Runs the legacy action and value heads"""
148
  with timed(trace, "action.filler.policy.trunk_forward"):
149
- h = _mlp(x, (self.W0, self.b0, self.W2, self.b2)) if self.hidden > 0 else x
150
  with timed(trace, "action.filler.policy.action_head_forward"):
151
  action_logits = h @ self.Wa.T + self.ba
152
  with timed(trace, "action.filler.policy.value_head_forward"):
@@ -237,7 +240,7 @@ class FactoredFiller:
237
  def _forward(self, x, trace=None) -> list[np.ndarray]:
238
  """## Runs the factored policy heads"""
239
  with timed(trace, "action.filler.policy.trunk_forward"):
240
- h = _mlp(x, (self.W0, self.b0, self.W2, self.b2))
241
  names = ["cell_size", "bitcount", "mask_size", "stop"]
242
  outputs = []
243
  for name, (W, b) in zip(names, self.heads):
 
112
 
113
 
114
  def _mlp(x, weights):
115
+ """## Runs the exported SiLU trunk"""
116
+ for W, b in weights:
117
+ x = _silu(x @ W.T + b)
118
+ return x
119
 
120
 
121
  class LearnedFiller:
 
126
  self.Wa, self.ba = npz["Wa"], npz["ba"]
127
  self.Wv, self.bv = npz["Wv"], npz["bv"]
128
  if self.hidden > 0:
129
+ if "layer_count" in npz:
130
+ self.layers = [(npz[f"W{i}"], npz[f"b{i}"]) for i in range(int(npz["layer_count"]))]
131
+ else:
132
+ self.layers = [(npz["W0"], npz["b0"]), (npz["W2"], npz["b2"])]
133
  self.feat_mean, self.feat_std = npz["feat_mean"], npz["feat_std"]
134
  self.lmin, self.lmax = float(npz["lmin"]), float(npz["lmax"])
135
  self.cheap = bool(npz["cheap"])
 
149
  def _forward(self, x, trace=None) -> tuple[np.ndarray, np.ndarray]:
150
  """## Runs the legacy action and value heads"""
151
  with timed(trace, "action.filler.policy.trunk_forward"):
152
+ h = _mlp(x, self.layers) if self.hidden > 0 else x
153
  with timed(trace, "action.filler.policy.action_head_forward"):
154
  action_logits = h @ self.Wa.T + self.ba
155
  with timed(trace, "action.filler.policy.value_head_forward"):
 
240
  def _forward(self, x, trace=None) -> list[np.ndarray]:
241
  """## Runs the factored policy heads"""
242
  with timed(trace, "action.filler.policy.trunk_forward"):
243
+ h = _mlp(x, [(self.W0, self.b0), (self.W2, self.b2)])
244
  names = ["cell_size", "bitcount", "mask_size", "stop"]
245
  outputs = []
246
  for name, (W, b) in zip(names, self.heads):
pbc3_rl_a20.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import copy
3
+ import json
4
+ import math
5
+ import random
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn.functional as F
12
+ from PIL import Image, ImageOps
13
+
14
+ import pbc3_ops as ops
15
+ from PBC3 import PBC3, preload_numba
16
+ from learned_filler import build_action, propose_boxes
17
+ from pbc3_features import extract_cheap, extract_features
18
+ from pbc3_heads import DownsampleInitHead
19
+ from pbc3_types import BitWriter, PBC3Config
20
+
21
+ PRESETS = {
22
+ "compression": PBC3Config.compression,
23
+ "balanced": PBC3Config.balanced,
24
+ "quality": PBC3Config.quality,
25
+ "high_quality": PBC3Config.high_quality,
26
+ }
27
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
28
+
29
+
30
+ def image_paths(folder):
31
+ return sorted(path for path in Path(folder).glob("*") if path.suffix.lower() in IMAGE_EXTENSIONS)
32
+
33
+
34
+ def init_canvas(prep, config):
35
+ target = prep["target"]
36
+ h, w, channels = target.shape
37
+ base_values = [int(round(float(np.mean(prep["arr"][:, :, c])))) for c in range(channels)]
38
+ canvas = np.empty((h, w, channels), dtype=np.int32)
39
+ for c, value in enumerate(base_values):
40
+ canvas[:, :, c] = value
41
+ channel_bits = max(1, math.ceil(math.log2(channels)))
42
+ patches = []
43
+ head = DownsampleInitHead()
44
+ for c in range(channels):
45
+ patch, _, delta, _, _ = head.select(c, target, canvas, w, h, config, channel_bits)
46
+ ops.apply_delta(canvas[:, :, c], 0, 0, w, h, delta)
47
+ patches.append(patch)
48
+ return canvas, patches
49
+
50
+
51
+ class Policy(torch.nn.Module):
52
+ def __init__(self, in_dim, hidden, layers, actions):
53
+ super().__init__()
54
+ self.trunk = torch.nn.ModuleList(
55
+ [torch.nn.Linear(in_dim, hidden)]
56
+ + [torch.nn.Linear(hidden, hidden) for _ in range(layers - 1)]
57
+ )
58
+ self.action = torch.nn.Linear(hidden, actions)
59
+ self.value = torch.nn.Linear(hidden, 1)
60
+
61
+ def forward(self, x):
62
+ for layer in self.trunk:
63
+ x = F.silu(layer(x))
64
+ return self.action(x), self.value(x).squeeze(-1)
65
+
66
+
67
+ def load_policy(path):
68
+ data = dict(np.load(path, allow_pickle=True))
69
+ layers = int(data.get("layer_count", 2))
70
+ model = Policy(int(data["in_dim"]), int(data["hidden"]), layers, int(data["num_actions"]))
71
+ state = model.state_dict()
72
+ if "layer_count" in data:
73
+ for i in range(layers):
74
+ state[f"trunk.{i}.weight"] = torch.from_numpy(data[f"W{i}"])
75
+ state[f"trunk.{i}.bias"] = torch.from_numpy(data[f"b{i}"])
76
+ else:
77
+ state["trunk.0.weight"] = torch.from_numpy(data["W0"])
78
+ state["trunk.0.bias"] = torch.from_numpy(data["b0"])
79
+ state["trunk.1.weight"] = torch.from_numpy(data["W2"])
80
+ state["trunk.1.bias"] = torch.from_numpy(data["b2"])
81
+ state["action.weight"] = torch.from_numpy(data["Wa"])
82
+ state["action.bias"] = torch.from_numpy(data["ba"])
83
+ state["value.weight"] = torch.from_numpy(data["Wv"])
84
+ state["value.bias"] = torch.from_numpy(data["bv"])
85
+ model.load_state_dict(state)
86
+ return model, data
87
+
88
+
89
+ def export(model, source, path):
90
+ state = model.state_dict()
91
+ values = {
92
+ "hidden": np.int64(model.trunk[0].out_features),
93
+ "layer_count": np.int64(len(model.trunk)),
94
+ "in_dim": np.int64(model.trunk[0].in_features),
95
+ "num_actions": np.int64(model.action.out_features),
96
+ "feat_mean": source["feat_mean"],
97
+ "feat_std": source["feat_std"],
98
+ "lmin": source["lmin"],
99
+ "lmax": source["lmax"],
100
+ "cheap": source["cheap"],
101
+ "feature_names": source["feature_names"],
102
+ "Wa": state["action.weight"].detach().numpy(),
103
+ "ba": state["action.bias"].detach().numpy(),
104
+ "Wv": state["value.weight"].detach().numpy(),
105
+ "bv": state["value.bias"].detach().numpy(),
106
+ }
107
+ for i in range(len(model.trunk)):
108
+ values[f"W{i}"] = state[f"trunk.{i}.weight"].detach().numpy()
109
+ values[f"b{i}"] = state[f"trunk.{i}.bias"].detach().numpy()
110
+ np.savez_compressed(path, **values)
111
+
112
+
113
+ def stream_result(prep, config, patches, canvas, base_values):
114
+ channel_bits = max(1, math.ceil(math.log2(prep["channels"])))
115
+ writer = BitWriter()
116
+ PBC3._write_header(
117
+ writer, prep["w"], prep["h"], prep["original_w"], prep["original_h"],
118
+ prep["downsampled"], prep["color_id"], prep["channels"], channel_bits,
119
+ config.positive_bias, prep["has_alpha"], len(patches), base_values,
120
+ )
121
+ for patch in patches:
122
+ PBC3._write_patch(writer, patch, channel_bits)
123
+ method, body = PBC3._entropy_pack(writer.finish(), config.use_lzma)
124
+ data = PBC3.MAGIC + bytes([PBC3.VERSION, method]) + body
125
+ image = PBC3._canvas_to_image(canvas, config.color_space, prep["has_alpha"])
126
+ if image.size != (prep["original_w"], prep["original_h"]):
127
+ image = image.resize(
128
+ (prep["original_w"], prep["original_h"]), PBC3.RESAMPLE_FILTER,
129
+ reducing_gap=PBC3.RESAMPLE_REDUCING_GAP,
130
+ )
131
+ mse = ops.final_mse(prep["orig_compare"], image)
132
+ bpp = len(data) * 8 / (prep["original_w"] * prep["original_h"])
133
+ return mse, bpp
134
+
135
+
136
+ def rollout(model, reference, source, path, preset, sample, temperature):
137
+ config = PRESETS[preset](warmup_ratio=-1, learned_filler_enabled=False)
138
+ image = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
139
+ prep = PBC3.prepare(image, config)
140
+ target = prep["target"]
141
+ canvas, patches = init_canvas(prep, config)
142
+ patches = list(patches)
143
+ h, w, channels = target.shape
144
+ channel_bits = max(1, math.ceil(math.log2(channels)))
145
+ base_values = [int(round(float(np.mean(prep["arr"][:, :, c])))) for c in range(channels)]
146
+ channel_scores = [PBC3._channel_sum_error(target, canvas, c) for c in range(channels)]
147
+ rng = ops.PBC3Rng(config.random_seed)
148
+ mean = source["feat_mean"].astype(np.float32)
149
+ std = source["feat_std"].astype(np.float32)
150
+ names = [str(x) for x in source["feature_names"]]
151
+ cheap = bool(source["cheap"])
152
+ logps, values, entropies, kls = [], [], [], []
153
+ work = 0.0
154
+ started = time.perf_counter()
155
+ for step in range(1, config.patch_count + 1):
156
+ channel = PBC3._choose_channel(channel_scores, step, channels, config.channel_cycle)
157
+ boxes = propose_boxes(target, canvas, config, rng, channel, step)[:1]
158
+ if not boxes:
159
+ break
160
+ box = boxes[0]
161
+ q = config.learned_filler_q
162
+ if cheap:
163
+ features = extract_cheap(names, target, canvas, box, step, q, w, h, config.patch_count, channels)
164
+ else:
165
+ features = extract_features(target, canvas, box, step, q, w, h, config.patch_count, channels)
166
+ x = torch.from_numpy(((features - mean) / std)[None].astype(np.float32))
167
+ logits, value = model(x)
168
+ with torch.no_grad():
169
+ ref_logits, _ = reference(x)
170
+ if sample:
171
+ distribution = torch.distributions.Categorical(logits=logits[0] / temperature)
172
+ action = distribution.sample()
173
+ logps.append(distribution.log_prob(action))
174
+ entropies.append(distribution.entropy())
175
+ kls.append(F.kl_div(F.log_softmax(logits, 1), F.softmax(ref_logits, 1), reduction="batchmean"))
176
+ else:
177
+ action = logits[0].argmax()
178
+ values.append(value[0])
179
+ patch, patch_values, delta, reduction, _ = build_action(
180
+ target, canvas, box, config, channel_bits, int(action),
181
+ )
182
+ if reduction <= 0:
183
+ break
184
+ work += float(patch_values.size)
185
+ c = patch["channel"]
186
+ ops.apply_delta(canvas[:, :, c], patch["x"], patch["y"], patch["w"], patch["h"], delta)
187
+ patches.append(patch)
188
+ channel_scores[c] = PBC3._channel_sum_error(target, canvas, c)
189
+ mse, bpp = stream_result(prep, config, patches, canvas, base_values)
190
+ return mse, bpp, work, time.perf_counter() - started, logps, values, entropies, kls
191
+
192
+
193
+ def reward(base, result, args):
194
+ base_mse, base_bpp, base_work = base
195
+ mse, bpp, work = result
196
+ dmse = (base_mse - mse) / max(base_mse, 1.0)
197
+ dbpp = (base_bpp - bpp) / max(base_bpp, 1e-6)
198
+ dwork = (base_work - work) / max(base_work, 1.0)
199
+ quality_weight = args.quality_weight if dmse >= 0 else args.worse_quality_weight
200
+ return float(np.clip(quality_weight * dmse + args.rate_weight * dbpp + args.speed_weight * dwork, -5, 5))
201
+
202
+
203
+ def evaluate(model, reference, source, episodes, bases, args):
204
+ rows = []
205
+ model.eval()
206
+ with torch.no_grad():
207
+ for path, preset in episodes:
208
+ mse, bpp, work, seconds, *_ = rollout(model, reference, source, path, preset, False, 1.0)
209
+ r = reward(bases[(path, preset)], (mse, bpp, work), args)
210
+ rows.append((preset, mse, bpp, work, seconds, r))
211
+ return {
212
+ "reward": float(np.mean([r[5] for r in rows])),
213
+ "mse": float(np.mean([r[1] for r in rows])),
214
+ "bpp": float(np.mean([r[2] for r in rows])),
215
+ "work": float(np.mean([r[3] for r in rows])),
216
+ "rollout_seconds": float(np.mean([r[4] for r in rows])),
217
+ "presets": {
218
+ preset: {
219
+ "mse": float(np.mean([r[1] for r in rows if r[0] == preset])),
220
+ "bpp": float(np.mean([r[2] for r in rows if r[0] == preset])),
221
+ }
222
+ for preset in args.selected_presets
223
+ },
224
+ }
225
+
226
+
227
+ def train(args):
228
+ torch.set_num_threads(1)
229
+ random.seed(args.seed)
230
+ np.random.seed(args.seed)
231
+ torch.manual_seed(args.seed)
232
+ args.selected_presets = [preset.strip() for preset in args.presets.split(",")]
233
+ unknown = [preset for preset in args.selected_presets if preset not in PRESETS]
234
+ if unknown:
235
+ raise ValueError(f"unknown presets: {', '.join(unknown)}")
236
+ model, source = load_policy(args.init)
237
+ reference = copy.deepcopy(model).eval()
238
+ for parameter in reference.parameters():
239
+ parameter.requires_grad_(False)
240
+ torch.nn.init.zeros_(model.value.weight)
241
+ torch.nn.init.zeros_(model.value.bias)
242
+ train_paths = image_paths(args.images)[:args.limit or None]
243
+ val_paths = image_paths(args.val_images)[:args.val_limit or None]
244
+ train_episodes = [(path, preset) for path in train_paths for preset in args.selected_presets]
245
+ val_episodes = [(path, preset) for path in val_paths for preset in args.selected_presets]
246
+ all_episodes = train_episodes + val_episodes
247
+ bases = {}
248
+ print(f"building baselines for {len(all_episodes)} episodes", flush=True)
249
+ for i, (path, preset) in enumerate(all_episodes, 1):
250
+ image = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
251
+ result = PBC3.compress(image, config=PRESETS[preset](learned_filler_model_path="patch_policy.npz"))
252
+ _, _, initial_work, _, *_ = rollout(reference, reference, source, path, preset, False, 1.0)
253
+ bases[(path, preset)] = (
254
+ float(result.mse), len(result.data) * 8 / (image.width * image.height), initial_work,
255
+ )
256
+ if i % 10 == 0:
257
+ print(f"baseline {i}/{len(all_episodes)}", flush=True)
258
+ Path(args.out).parent.mkdir(parents=True, exist_ok=True)
259
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
260
+ history = []
261
+ start_epoch = 0
262
+ resume_path = Path(args.resume) if args.resume else None
263
+ if resume_path is not None and resume_path.exists():
264
+ checkpoint = torch.load(resume_path, map_location="cpu", weights_only=False)
265
+ model.load_state_dict(checkpoint["model"])
266
+ optimizer.load_state_dict(checkpoint["optimizer"])
267
+ random.setstate(checkpoint["python_rng"])
268
+ np.random.set_state(checkpoint["numpy_rng"])
269
+ torch.random.set_rng_state(checkpoint["torch_rng"])
270
+ start_epoch = int(checkpoint["epoch"])
271
+ history = checkpoint.get("history", [])
272
+ initial = evaluate(model, reference, source, val_episodes, bases, args)
273
+ print("initial", json.dumps(initial), flush=True)
274
+ best = initial["reward"]
275
+ export(model, source, args.out)
276
+ for epoch in range(start_epoch + 1, start_epoch + args.epochs + 1):
277
+ random.shuffle(train_episodes)
278
+ rewards = []
279
+ losses = []
280
+ for start in range(0, len(train_episodes), args.batch):
281
+ optimizer.zero_grad()
282
+ episode_losses = []
283
+ for path, preset in train_episodes[start:start + args.batch]:
284
+ mse, bpp, work, _, logps, values, entropies, kls = rollout(
285
+ model, reference, source, path, preset, True, args.temperature,
286
+ )
287
+ r = reward(bases[(path, preset)], (mse, bpp, work), args)
288
+ rewards.append(r)
289
+ if not logps:
290
+ continue
291
+ logps = torch.stack(logps)
292
+ values = torch.stack(values)
293
+ target = torch.full_like(values, r)
294
+ advantage = (target - values).detach()
295
+ loss = -(logps * advantage).mean()
296
+ loss += args.value_weight * F.mse_loss(values, target)
297
+ loss -= args.entropy_weight * torch.stack(entropies).mean()
298
+ loss += args.kl_weight * torch.stack(kls).mean()
299
+ episode_losses.append(loss)
300
+ if episode_losses:
301
+ loss = torch.stack(episode_losses).mean()
302
+ loss.backward()
303
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
304
+ optimizer.step()
305
+ losses.append(float(loss.detach()))
306
+ validation = evaluate(model, reference, source, val_episodes, bases, args)
307
+ row = {
308
+ "epoch": epoch,
309
+ "train_reward": float(np.mean(rewards)),
310
+ "loss": float(np.mean(losses)),
311
+ "validation": validation,
312
+ }
313
+ history.append(row)
314
+ print(json.dumps(row), flush=True)
315
+ if validation["reward"] > best:
316
+ best = validation["reward"]
317
+ export(model, source, args.out)
318
+ torch.save(model.state_dict(), f"{args.out}.epoch{epoch}.pt")
319
+ torch.save({
320
+ "epoch": epoch,
321
+ "model": model.state_dict(),
322
+ "optimizer": optimizer.state_dict(),
323
+ "python_rng": random.getstate(),
324
+ "numpy_rng": np.random.get_state(),
325
+ "torch_rng": torch.random.get_rng_state(),
326
+ "history": history,
327
+ }, f"{args.out}.resume.pt")
328
+ Path(f"{args.out}.json").write_text(json.dumps({"initial": initial, "best_reward": best, "history": history}, indent=2))
329
+
330
+
331
+ def main():
332
+ parser = argparse.ArgumentParser()
333
+ parser.add_argument("--images", default="hpt_data")
334
+ parser.add_argument("--val-images", default="hpt_data_val")
335
+ parser.add_argument("--presets", default=",".join(PRESETS))
336
+ parser.add_argument("--init", default="pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz")
337
+ parser.add_argument("--out", default="pbc3_students/patch_policy_f26_a20_rl.npz")
338
+ parser.add_argument("--resume", default="")
339
+ parser.add_argument("--epochs", type=int, default=5)
340
+ parser.add_argument("--batch", type=int, default=4)
341
+ parser.add_argument("--limit", type=int, default=0)
342
+ parser.add_argument("--val-limit", type=int, default=0)
343
+ parser.add_argument("--lr", type=float, default=1e-5)
344
+ parser.add_argument("--temperature", type=float, default=0.8)
345
+ parser.add_argument("--entropy-weight", type=float, default=0.001)
346
+ parser.add_argument("--kl-weight", type=float, default=0.02)
347
+ parser.add_argument("--value-weight", type=float, default=0.25)
348
+ parser.add_argument("--quality-weight", type=float, default=2.0)
349
+ parser.add_argument("--worse-quality-weight", type=float, default=6.0)
350
+ parser.add_argument("--rate-weight", type=float, default=1.0)
351
+ parser.add_argument("--speed-weight", type=float, default=0.1)
352
+ parser.add_argument("--weight-decay", type=float, default=1e-5)
353
+ parser.add_argument("--grad-clip", type=float, default=1.0)
354
+ parser.add_argument("--seed", type=int, default=2003)
355
+ train(parser.parse_args())
356
+
357
+
358
+ if __name__ == "__main__":
359
+ main()
pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36e525e00c34e20ccd786f5bc5f7c9d824ffb5e2cb22d942aa9732c7481af6ed
3
+ size 1069260
pbc3_students/patch_policy_f26_a25_h512_l2_e1200.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2937b0b1935ac84e39ec44e6d621be919f4612d75e3494d24d698a7c198acffd
3
+ size 1078979
server.py CHANGED
@@ -27,6 +27,7 @@ from PBC3_animation import animate_pbc3
27
  import pbc3_sweep
28
  import pbc3_quick_rd
29
  import pbc3_benchmark
 
30
 
31
  import urllib.request
32
 
@@ -50,6 +51,7 @@ def _benchmark_busy():
50
  pbc3_sweep.status().get("running")
51
  or pbc3_quick_rd.status().get("running")
52
  or pbc3_benchmark.status().get("running")
 
53
  )
54
 
55
 
@@ -348,8 +350,8 @@ async def compress(request: Request):
348
  mode = form.get("mode", "Auto")
349
  mode = mode if mode in {"Auto", "Semi", "Manual"} else "Manual"
350
  if mode == "Auto":
351
- preset = form.get("auto_config", "quality")
352
- config = PBC3_PRESETS.get(preset, PBC3Config.quality)()
353
  kwargs = {"auto_config": preset}
354
  else:
355
  preset = None
@@ -967,4 +969,5 @@ class NoCacheStaticFiles(StaticFiles):
967
  response.headers["Expires"] = "0"
968
  return response
969
 
 
970
  app.mount("/", NoCacheStaticFiles(directory="static", html=True), name="static")
 
27
  import pbc3_sweep
28
  import pbc3_quick_rd
29
  import pbc3_benchmark
30
+ import train_api
31
 
32
  import urllib.request
33
 
 
51
  pbc3_sweep.status().get("running")
52
  or pbc3_quick_rd.status().get("running")
53
  or pbc3_benchmark.status().get("running")
54
+ or train_api.status().get("running")
55
  )
56
 
57
 
 
350
  mode = form.get("mode", "Auto")
351
  mode = mode if mode in {"Auto", "Semi", "Manual"} else "Manual"
352
  if mode == "Auto":
353
+ preset = form.get("auto_config", "high_quality")
354
+ config = PBC3_PRESETS.get(preset, PBC3Config.high_quality)()
355
  kwargs = {"auto_config": preset}
356
  else:
357
  preset = None
 
969
  response.headers["Expires"] = "0"
970
  return response
971
 
972
+ train_api.register(app)
973
  app.mount("/", NoCacheStaticFiles(directory="static", html=True), name="static")
static/app.js CHANGED
@@ -417,7 +417,7 @@ const PARAMS = [
417
 
418
  const paramState = {};
419
  PARAMS.forEach(p => paramState[p.id] = p.value);
420
- paramState.preset = "quality";
421
  let PRESET_VALUES = {};
422
  fetch("/api/presets").then(r => r.json()).then(d => {
423
  PRESET_VALUES = d;
@@ -454,7 +454,7 @@ document.querySelectorAll("#param-mode .seg-btn").forEach(b => b.addEventListene
454
  b.classList.add("active");
455
  paramMode = b.dataset.mode;
456
  if (paramMode === "Auto") {
457
- const preset = (paramState.preset && paramState.preset !== "custom") ? paramState.preset : "quality";
458
  applyPreset(preset);
459
  }
460
  renderParams();
@@ -1259,4 +1259,4 @@ function toast(msg) {
1259
  /* ============================================================
1260
  BOOT
1261
  ============================================================ */
1262
- initRoster();
 
417
 
418
  const paramState = {};
419
  PARAMS.forEach(p => paramState[p.id] = p.value);
420
+ paramState.preset = "high_quality";
421
  let PRESET_VALUES = {};
422
  fetch("/api/presets").then(r => r.json()).then(d => {
423
  PRESET_VALUES = d;
 
454
  b.classList.add("active");
455
  paramMode = b.dataset.mode;
456
  if (paramMode === "Auto") {
457
+ const preset = (paramState.preset && paramState.preset !== "custom") ? paramState.preset : "high_quality";
458
  applyPreset(preset);
459
  }
460
  renderParams();
 
1259
  /* ============================================================
1260
  BOOT
1261
  ============================================================ */
1262
+ initRoster();
static/index.html CHANGED
@@ -51,6 +51,18 @@
51
  .tr-check input { width:16px; height:16px; accent-color:var(--red); }
52
  .tr-actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }
53
  .tr-ck { border-top:1px solid rgba(255,255,255,.08); padding:14px 0; }
 
 
 
 
 
 
 
 
 
 
 
 
54
  .tr-card-title { margin:0 0 1rem; font-size:15px; color:var(--text); }
55
  .tr-output-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(310px,1fr)); gap:1.4rem; }
56
  .tr-viewer .viewer-controls { flex-wrap:wrap; justify-content:center; }
@@ -89,6 +101,10 @@
89
  Sweep Experiments
90
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
91
  </button>
 
 
 
 
92
  <a class="repo" href="https://github.com/EgeEken/PBC" target="_blank" rel="noopener">github.com/EgeEken/PBC ↗</a>
93
  </div>
94
 
@@ -102,6 +118,19 @@
102
  </div>
103
  </section>
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  <!-- ===================== DEMO ===================== -->
106
  <section id="app" class="section app" hidden>
107
  <header class="app-header">
@@ -255,5 +284,6 @@
255
  <script src="/benchmark.js?v=20260625f"></script>
256
  <script src="/quick_rd.js?v=20260625e"></script>
257
  <script src="/sweep_runner.js?v=20260624a"></script>
 
258
  </body>
259
  </html>
 
51
  .tr-check input { width:16px; height:16px; accent-color:var(--red); }
52
  .tr-actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }
53
  .tr-ck { border-top:1px solid rgba(255,255,255,.08); padding:14px 0; }
54
+ .tr-presets { display:flex; flex-wrap:wrap; gap:10px 18px; padding:10px 0; color:var(--text); }
55
+ .tr-presets input { accent-color:var(--red); }
56
+ .tr-log-tail { max-height:240px; overflow:auto; white-space:pre-wrap; background:rgba(255,255,255,.04); padding:12px; border-radius:10px; color:var(--muted); font:11px 'JetBrains Mono',monospace; }
57
+ .tr-table-wrap { overflow-x:auto; }
58
+ .tr-table { width:100%; border-collapse:collapse; font:12px 'JetBrains Mono',monospace; }
59
+ .tr-table th, .tr-table td { padding:9px 10px; border-bottom:1px solid rgba(255,255,255,.08); text-align:right; white-space:nowrap; }
60
+ .tr-table th:first-child, .tr-table td:first-child { text-align:left; }
61
+ .tr-checkpoint-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:10px; }
62
+ .tr-filter { width:100%; max-width:460px; margin:8px 0 14px; background:var(--surface); border:1px solid var(--border); border-radius:7px; color:var(--text); padding:9px 11px; font:13px 'JetBrains Mono',monospace; }
63
+ .tr-checkpoint { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:12px; border:1px solid var(--border); border-radius:9px; background:var(--surface); }
64
+ .tr-checkpoint b, .tr-checkpoint small { display:block; }
65
+ .tr-checkpoint small { margin-top:5px; color:var(--muted); font:11px 'JetBrains Mono',monospace; }
66
  .tr-card-title { margin:0 0 1rem; font-size:15px; color:var(--text); }
67
  .tr-output-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(310px,1fr)); gap:1.4rem; }
68
  .tr-viewer .viewer-controls { flex-wrap:wrap; justify-content:center; }
 
101
  Sweep Experiments
102
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
103
  </button>
104
+ <button id="enter-train" class="sweep-link-btn">
105
+ Training Lab
106
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
107
+ </button>
108
  <a class="repo" href="https://github.com/EgeEken/PBC" target="_blank" rel="noopener">github.com/EgeEken/PBC ↗</a>
109
  </div>
110
 
 
118
  </div>
119
  </section>
120
 
121
+ <!-- ===================== TRAINING LAB ===================== -->
122
+ <section id="train-lab" class="section app" hidden>
123
+ <header class="app-header">
124
+ <div class="app-brand" id="train-back"><span class="dot"></span> PBC · Training Lab</div>
125
+ <nav class="app-nav">
126
+ <span class="nav-item active" data-tr-tab="run">Training Run</span>
127
+ <span class="nav-item" data-tr-tab="checkpoints">Checkpoints</span>
128
+ </nav>
129
+ </header>
130
+ <div id="view-train" class="view"></div>
131
+ <div id="view-train-checkpoints" class="view" hidden></div>
132
+ </section>
133
+
134
  <!-- ===================== DEMO ===================== -->
135
  <section id="app" class="section app" hidden>
136
  <header class="app-header">
 
284
  <script src="/benchmark.js?v=20260625f"></script>
285
  <script src="/quick_rd.js?v=20260625e"></script>
286
  <script src="/sweep_runner.js?v=20260624a"></script>
287
+ <script src="/train.js?v=20260718a"></script>
288
  </body>
289
  </html>
static/train.js ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (() => {
2
+ const $ = (selector) => document.querySelector(selector);
3
+ const runRoot = $("#view-train");
4
+ const checkpointRoot = $("#view-train-checkpoints");
5
+ if (!runRoot || !checkpointRoot) return;
6
+
7
+ let poll = null;
8
+ let checkpoints = [];
9
+ let activeTab = "run";
10
+
11
+ const presets = [
12
+ ["compression", "Compression"],
13
+ ["balanced", "Balanced"],
14
+ ["quality", "Quality"],
15
+ ["high_quality", "High quality"],
16
+ ];
17
+
18
+ const esc = (value) => String(value ?? "").replace(/[&<>\"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;" }[c]));
19
+ const api = async (path, options) => {
20
+ const response = await fetch(path, options);
21
+ const data = await response.json().catch(() => ({}));
22
+ if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
23
+ return data;
24
+ };
25
+ const download = (path) => window.open(`/api/train/download_checkpoint?path=${encodeURIComponent(path)}`, "_blank");
26
+ const fmt = (value, digits = 4) => value == null ? "—" : Number(value).toFixed(digits);
27
+
28
+ function showLab() {
29
+ ["landing", "app", "sweep-lab"].forEach((id) => { const el = $(`#${id}`); if (el) el.hidden = true; });
30
+ $("#train-lab").hidden = false;
31
+ renderRun();
32
+ loadCheckpoints();
33
+ refresh();
34
+ }
35
+
36
+ function showLanding() {
37
+ $("#train-lab").hidden = true;
38
+ $("#landing").hidden = false;
39
+ if (poll) clearInterval(poll);
40
+ poll = null;
41
+ }
42
+
43
+ $("#enter-train")?.addEventListener("click", showLab);
44
+ $("#train-back")?.addEventListener("click", showLanding);
45
+ document.querySelectorAll("[data-tr-tab]").forEach((tab) => tab.addEventListener("click", () => {
46
+ activeTab = tab.dataset.trTab;
47
+ document.querySelectorAll("[data-tr-tab]").forEach((item) => item.classList.toggle("active", item.dataset.trTab === activeTab));
48
+ runRoot.hidden = activeTab !== "run";
49
+ checkpointRoot.hidden = activeTab !== "checkpoints";
50
+ if (activeTab === "checkpoints") renderCheckpoints();
51
+ }));
52
+
53
+ function renderRun() {
54
+ runRoot.innerHTML = `
55
+ <div class="params">
56
+ <div class="params-head"><span class="field-label">Long-horizon RL training</span><span class="card-date">hpt_data → hpt_data_val</span></div>
57
+ <p class="card-date">Runs the current filler trainer in a resumable subprocess. Start the same output again to continue from its optimizer and RNG checkpoint.</p>
58
+ <div class="tr-fields">
59
+ <div class="tr-f full"><span>Preset policies</span><div class="tr-presets">${presets.map(([id, label], i) => `<label><input type="checkbox" data-tr-preset="${id}" ${i === 3 ? "checked" : ""}> ${label}</label>`).join("")}</div></div>
60
+ <label class="tr-f"><span>Additional epochs</span><input id="tr-epochs" type="number" min="1" step="1" value="100"></label>
61
+ <label class="tr-f"><span>Batch size</span><input id="tr-batch" type="number" min="1" step="1" value="4"></label>
62
+ <label class="tr-f"><span>Initial model</span><select id="tr-init"></select></label>
63
+ <label class="tr-f"><span>Output name</span><input id="tr-output" value="rl_high_quality.npz"></label>
64
+ <label class="tr-f"><span>Rate weight</span><input id="tr-rate" type="number" step="0.05" value="1.5"></label>
65
+ <label class="tr-f"><span>Speed weight</span><input id="tr-speed" type="number" step="0.05" value="0.15"></label>
66
+ <label class="tr-f"><span>Temperature</span><input id="tr-temp" type="number" step="0.05" value="0.9"></label>
67
+ <label class="tr-f"><span>Entropy weight</span><input id="tr-entropy" type="number" step="0.0005" value="0.003"></label>
68
+ <label class="tr-f"><span>KL weight</span><input id="tr-kl" type="number" step="0.001" value="0.015"></label>
69
+ </div>
70
+ <div class="tr-actions"><button id="tr-start" class="primary-btn">Start / resume</button><button id="tr-stop" class="hold-btn">Stop</button><button id="tr-refresh" class="hold-btn">Refresh</button><button id="tr-log" class="hold-btn">Download log</button></div>
71
+ </div>
72
+ <div class="params"><div class="params-head"><span class="field-label">Run status</span></div><div id="tr-status" class="tr-ck">Loading…</div><pre id="tr-log-tail" class="tr-log-tail"></pre></div>
73
+ <div class="params"><div class="params-head"><span class="field-label">Validation history</span></div><div id="tr-history"></div></div>`;
74
+ $("#tr-start").onclick = start;
75
+ $("#tr-stop").onclick = async () => { await api("/api/train/stop", { method: "POST" }); refresh(); };
76
+ $("#tr-refresh").onclick = refresh;
77
+ $("#tr-log").onclick = () => window.open("/api/train/log", "_blank");
78
+ fillInitialModels();
79
+ }
80
+
81
+ async function loadCheckpoints() {
82
+ const data = await api("/api/train/checkpoints").catch(() => ({ checkpoints: [] }));
83
+ checkpoints = data.checkpoints || [];
84
+ fillInitialModels();
85
+ if (activeTab === "checkpoints") renderCheckpoints();
86
+ }
87
+
88
+ function fillInitialModels() {
89
+ const select = $("#tr-init");
90
+ if (!select) return;
91
+ const old = select.value;
92
+ select.innerHTML = checkpoints.map((c) => `<option value="${esc(c.path)}">${esc(c.name)}</option>`).join("");
93
+ if (old && checkpoints.some((c) => c.path === old)) select.value = old;
94
+ else if (checkpoints.length) select.value = checkpoints.find((c) => c.name.includes("f26_a20_h512"))?.path || checkpoints[0].path;
95
+ }
96
+
97
+ function selectedPresets() {
98
+ return [...document.querySelectorAll("[data-tr-preset]:checked")].map((el) => el.dataset.trPreset);
99
+ }
100
+
101
+ async function start() {
102
+ const selected = selectedPresets();
103
+ if (!selected.length) return;
104
+ const body = {
105
+ presets: selected.join(","), epochs: Number($("#tr-epochs").value), batch: Number($("#tr-batch").value),
106
+ init: $("#tr-init").value, output: $("#tr-output").value,
107
+ rate_weight: Number($("#tr-rate").value), speed_weight: Number($("#tr-speed").value),
108
+ temperature: Number($("#tr-temp").value), entropy_weight: Number($("#tr-entropy").value), kl_weight: Number($("#tr-kl").value),
109
+ };
110
+ try { await api("/api/train/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); refresh(); } catch (error) { $("#tr-status").textContent = error.message; }
111
+ }
112
+
113
+ function renderHistory(history) {
114
+ const root = $("#tr-history");
115
+ if (!root) return;
116
+ const rows = history.filter((row) => row.epoch || row.validation).map((row) => {
117
+ const v = row.validation || row;
118
+ return `<tr><td>${row.epoch ? `Epoch ${row.epoch}` : "Initial"}</td><td>${fmt(row.reward ?? v.reward, 5)}</td><td>${fmt(v.mse, 3)}</td><td>${fmt(v.bpp, 6)}</td><td>${fmt(v.work, 0)}</td></tr>`;
119
+ });
120
+ root.innerHTML = rows.length ? `<div class="tr-table-wrap"><table class="tr-table"><thead><tr><th>Checkpoint</th><th>Reward</th><th>Val MSE</th><th>Val bpp</th><th>Grid work</th></tr></thead><tbody>${rows.join("")}</tbody></table></div>` : `<p class="card-date">No validation rows yet. The first baseline pass can take a few minutes.</p>`;
121
+ }
122
+
123
+ async function refresh() {
124
+ const data = await api("/api/train/status").catch((error) => ({ running: false, log_tail: error.message, history: [] }));
125
+ const status = $("#tr-status");
126
+ if (!status) return;
127
+ const state = data.running ? "Running" : data.return_code == null ? "Idle" : data.return_code === 0 ? "Finished" : `Stopped / failed (${data.return_code})`;
128
+ const output = data.output ? `<code>${esc(data.output)}</code>` : "No output selected";
129
+ status.innerHTML = `<b>${state}</b> · ${output}${data.spec?.presets ? ` · ${esc(data.spec.presets)}` : ""}`;
130
+ $("#tr-log-tail").textContent = data.log_tail || "";
131
+ renderHistory(data.history || []);
132
+ if (data.running && !poll) poll = setInterval(refresh, 3000);
133
+ if (!data.running && poll) { clearInterval(poll); poll = null; loadCheckpoints(); }
134
+ }
135
+
136
+ function renderCheckpoints() {
137
+ checkpointRoot.innerHTML = `<div class="params"><div class="params-head"><span class="field-label">Downloadable model checkpoints</span></div><p class="card-date">Download an exported <code>.npz</code> after reviewing its validation history, then upload it into the main project when you want to test or promote it.</p><input id="tr-ckpt-filter" class="tr-filter" placeholder="Filter checkpoints…"><div id="tr-checkpoint-list" class="tr-checkpoint-grid"></div></div>`;
138
+ $("#tr-ckpt-filter").oninput = renderCheckpointList;
139
+ renderCheckpointList();
140
+ }
141
+
142
+ function renderCheckpointList() {
143
+ const root = $("#tr-checkpoint-list");
144
+ if (!root) return;
145
+ const filter = ($("#tr-ckpt-filter").value || "").toLowerCase();
146
+ const visible = checkpoints.filter((c) => c.name.toLowerCase().includes(filter));
147
+ root.innerHTML = visible.length ? visible.map((c) => `<div class="tr-checkpoint"><div><b>${esc(c.name)}</b><small>${(c.bytes / 1024).toFixed(1)} KB</small></div><button class="hold-btn" data-download="${esc(c.path)}">Download</button></div>`).join("") : `<p class="card-date">No matching checkpoints.</p>`;
148
+ root.querySelectorAll("[data-download]").forEach((button) => button.onclick = () => download(button.dataset.download));
149
+ }
150
+
151
+ setInterval(() => { if (!$("#train-lab").hidden && activeTab === "run") refresh(); }, 5000);
152
+ })();
train_api.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from fastapi import Request
10
+ from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parent
14
+ RUNS = ROOT / "training_space" / "runs"
15
+ RUNS.mkdir(parents=True, exist_ok=True)
16
+ IMAGE_FOLDERS = {"train": ROOT / "hpt_data", "validation": ROOT / "hpt_data_val"}
17
+ MODEL_FOLDERS = (ROOT / "pbc3_students", RUNS)
18
+ LOCK = threading.Lock()
19
+ PROCESS = None
20
+ CURRENT = {}
21
+
22
+
23
+ def _safe_model_path(value):
24
+ path = (ROOT / value).resolve() if not os.path.isabs(value) else Path(value).resolve()
25
+ if not any(path == folder.resolve() or folder.resolve() in path.parents for folder in MODEL_FOLDERS):
26
+ return None
27
+ return path if path.is_file() else None
28
+
29
+
30
+ def _output_path(value):
31
+ name = Path(value or "rl_training.npz").name
32
+ if not name.endswith(".npz"):
33
+ name += ".npz"
34
+ return (RUNS / name).resolve()
35
+
36
+
37
+ def _json_lines(log_path):
38
+ rows = []
39
+ if not log_path.exists():
40
+ return rows
41
+ for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines():
42
+ candidate = line.strip()
43
+ if candidate.startswith("initial "):
44
+ candidate = candidate[8:]
45
+ try:
46
+ row = json.loads(candidate)
47
+ except Exception:
48
+ continue
49
+ if "epoch" in row or "reward" in row:
50
+ rows.append(row)
51
+ return rows
52
+
53
+
54
+ def _refresh_process():
55
+ global PROCESS
56
+ if PROCESS is not None and PROCESS.poll() is not None:
57
+ CURRENT["return_code"] = PROCESS.returncode
58
+ PROCESS = None
59
+
60
+
61
+ def status():
62
+ with LOCK:
63
+ _refresh_process()
64
+ output = Path(CURRENT["output"]) if CURRENT.get("output") else None
65
+ log = Path(CURRENT["log"]) if CURRENT.get("log") else None
66
+ history_path = Path(f"{output}.json") if output else None
67
+ history = []
68
+ if history_path and history_path.exists():
69
+ try:
70
+ saved = json.loads(history_path.read_text(encoding="utf-8"))
71
+ history = saved.get("history", [])
72
+ except Exception:
73
+ pass
74
+ if not history:
75
+ history = _json_lines(log) if log else []
76
+ return {
77
+ "running": PROCESS is not None,
78
+ "return_code": CURRENT.get("return_code"),
79
+ "output": str(output.relative_to(ROOT)) if output else None,
80
+ "log": str(log.relative_to(ROOT)) if log else None,
81
+ "started": CURRENT.get("started"),
82
+ "spec": CURRENT.get("spec", {}),
83
+ "history": history,
84
+ "log_tail": log.read_text(encoding="utf-8", errors="replace")[-10000:] if log and log.exists() else "",
85
+ }
86
+
87
+
88
+ def start(spec):
89
+ global PROCESS
90
+ with LOCK:
91
+ _refresh_process()
92
+ if PROCESS is not None:
93
+ return {"error": "A training run is already active."}
94
+ presets = str(spec.get("presets", "high_quality"))
95
+ selected = [p.strip() for p in presets.split(",") if p.strip()]
96
+ allowed = {"compression", "balanced", "quality", "high_quality"}
97
+ if not selected or any(p not in allowed for p in selected):
98
+ return {"error": "Presets must be compression, balanced, quality, or high_quality."}
99
+ init = _safe_model_path(str(spec.get("init", "pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz")))
100
+ if init is None:
101
+ return {"error": "Initial model was not found in pbc3_students or training_space/runs."}
102
+ output = _output_path(spec.get("output", "rl_training.npz"))
103
+ output.parent.mkdir(parents=True, exist_ok=True)
104
+ log = Path(f"{output}.log")
105
+ resume = Path(f"{output}.resume.pt")
106
+ command = [
107
+ sys.executable, "pbc3_rl_a20.py",
108
+ "--presets", ",".join(selected),
109
+ "--epochs", str(int(spec.get("epochs", 100))),
110
+ "--batch", str(int(spec.get("batch", 4))),
111
+ "--init", os.fspath(init),
112
+ "--out", os.fspath(output),
113
+ "--rate-weight", str(float(spec.get("rate_weight", 1.5))),
114
+ "--speed-weight", str(float(spec.get("speed_weight", 0.15))),
115
+ "--temperature", str(float(spec.get("temperature", 0.9))),
116
+ "--entropy-weight", str(float(spec.get("entropy_weight", 0.003))),
117
+ "--kl-weight", str(float(spec.get("kl_weight", 0.015))),
118
+ "--quality-weight", str(float(spec.get("quality_weight", 2.0))),
119
+ "--worse-quality-weight", str(float(spec.get("worse_quality_weight", 7.0))),
120
+ ]
121
+ if resume.exists():
122
+ command.extend(["--resume", os.fspath(resume)])
123
+ log.write_text("", encoding="utf-8")
124
+ handle = log.open("a", encoding="utf-8")
125
+ PROCESS = subprocess.Popen(command, cwd=ROOT, stdout=handle, stderr=subprocess.STDOUT)
126
+ handle.close()
127
+ CURRENT.clear()
128
+ CURRENT.update({"output": str(output), "log": str(log), "started": time.time(), "spec": spec, "return_code": None})
129
+ return {"ok": True, "output": str(output.relative_to(ROOT)), "resuming": resume.exists()}
130
+
131
+
132
+ def stop():
133
+ with LOCK:
134
+ _refresh_process()
135
+ if PROCESS is None:
136
+ return {"ok": False, "error": "No training run is active."}
137
+ PROCESS.terminate()
138
+ return {"ok": True}
139
+
140
+
141
+ def checkpoints():
142
+ rows = []
143
+ for folder in MODEL_FOLDERS:
144
+ for path in sorted(folder.glob("*.npz"), key=lambda p: p.stat().st_mtime, reverse=True):
145
+ rows.append({"name": path.name, "path": str(path.relative_to(ROOT)), "bytes": path.stat().st_size, "modified": path.stat().st_mtime})
146
+ return {"checkpoints": rows}
147
+
148
+
149
+ def download_checkpoint(path):
150
+ safe = _safe_model_path(path)
151
+ if safe is None or safe.suffix != ".npz":
152
+ return JSONResponse({"error": "Checkpoint not found or not allowed."}, status_code=404)
153
+ return FileResponse(safe, filename=safe.name, media_type="application/octet-stream")
154
+
155
+
156
+ def register(app):
157
+ @app.get("/api/train/status")
158
+ def train_status():
159
+ return status()
160
+
161
+ @app.post("/api/train/start")
162
+ async def train_start(request: Request):
163
+ result = start(await request.json())
164
+ return JSONResponse(result, status_code=400 if result.get("error") else 200)
165
+
166
+ @app.post("/api/train/stop")
167
+ def train_stop():
168
+ return stop()
169
+
170
+ @app.get("/api/train/checkpoints")
171
+ def train_checkpoints():
172
+ return checkpoints()
173
+
174
+ @app.get("/api/train/download_checkpoint")
175
+ def train_download_checkpoint(path: str):
176
+ return download_checkpoint(path)
177
+
178
+ @app.get("/api/train/log")
179
+ def train_log():
180
+ data = status()
181
+ path = ROOT / data["log"] if data.get("log") else None
182
+ if not path or not path.exists():
183
+ return PlainTextResponse("No training log is available.")
184
+ return FileResponse(path, filename=path.name, media_type="text/plain")
training_space/README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PBC filler RL training Space
2
+
3
+ Run `app.py` from the project root, or copy this directory together with the PBC source files, `hpt_data`, `hpt_data_val`, and the initial student model into a dedicated Hugging Face Space.
4
+
5
+ The app runs preset-specific RL in a background process. Starting the same output again automatically resumes from its `.resume.pt` checkpoint, including model, optimizer, RNG, and history state. Download exported `.npz` checkpoints from the Training Lab before restarting the Space; persistent storage is not required.
6
+
7
+ Example local launch:
8
+
9
+ ```text
10
+ pip install -r training_space/requirements.txt
11
+ python training_space/app.py
12
+ ```
training_space/app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import threading
4
+ from pathlib import Path
5
+
6
+ import gradio as gr
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parent.parent
10
+ RUNS = ROOT / "training_space" / "runs"
11
+ RUNS.mkdir(parents=True, exist_ok=True)
12
+ process = None
13
+ process_lock = threading.Lock()
14
+
15
+
16
+ def run_command(command, log_path):
17
+ global process
18
+ with log_path.open("a", encoding="utf-8") as log:
19
+ process = subprocess.Popen(command, cwd=ROOT, stdout=log, stderr=subprocess.STDOUT)
20
+ process.wait()
21
+
22
+
23
+ def start_training(presets, epochs, init_path, output_name, rate_weight, speed_weight):
24
+ global process
25
+ with process_lock:
26
+ if process is not None and process.poll() is None:
27
+ return "A training process is already running."
28
+ output = RUNS / output_name
29
+ output.parent.mkdir(parents=True, exist_ok=True)
30
+ resume = Path(f"{output}.resume.pt")
31
+ command = [
32
+ os.fspath(Path(os.sys.executable)), "pbc3_rl_a20.py",
33
+ "--presets", presets,
34
+ "--epochs", str(int(epochs)),
35
+ "--init", init_path,
36
+ "--out", os.fspath(output),
37
+ "--rate-weight", str(float(rate_weight)),
38
+ "--speed-weight", str(float(speed_weight)),
39
+ ]
40
+ if resume.exists():
41
+ command.extend(["--resume", os.fspath(resume)])
42
+ log_path = Path(f"{output}.log")
43
+ log_path.write_text("", encoding="utf-8")
44
+ threading.Thread(target=run_command, args=(command, log_path), daemon=True).start()
45
+ return f"Started: {' '.join(command)}\nLog: {log_path}"
46
+
47
+
48
+ def status():
49
+ with process_lock:
50
+ running = process is not None and process.poll() is None
51
+ code = None if process is None or running else process.returncode
52
+ logs = sorted(RUNS.glob("*.log"), key=lambda path: path.stat().st_mtime, reverse=True)
53
+ tail = logs[0].read_text(encoding="utf-8", errors="replace")[-8000:] if logs else ""
54
+ return f"running={running}, return_code={code}\n\n{tail}"
55
+
56
+
57
+ def stop_training():
58
+ with process_lock:
59
+ if process is None or process.poll() is not None:
60
+ return "No training process is running."
61
+ process.terminate()
62
+ return "Stop requested; the current checkpoint will remain available."
63
+
64
+
65
+ with gr.Blocks(title="PBC filler RL training") as demo:
66
+ gr.Markdown("# PBC filler RL training\nRuns resumable preset-specific training and writes checkpoints under `training_space/runs/`.")
67
+ presets = gr.Textbox(value="high_quality", label="Presets", info="Comma-separated: compression, balanced, quality, high_quality")
68
+ epochs = gr.Number(value=100, precision=0, label="Additional epochs per start/resume")
69
+ init_path = gr.Textbox(value="pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz", label="Initial model")
70
+ output_name = gr.Textbox(value="rl_high_quality.npz", label="Output filename")
71
+ rate_weight = gr.Number(value=1.5, label="Rate reward weight")
72
+ speed_weight = gr.Number(value=0.2, label="Speed reward weight")
73
+ start = gr.Button("Start or resume")
74
+ stop = gr.Button("Stop")
75
+ refresh = gr.Button("Refresh status")
76
+ output = gr.Textbox(lines=30, label="Status / log")
77
+ start.click(start_training, [presets, epochs, init_path, output_name, rate_weight, speed_weight], output)
78
+ stop.click(stop_training, outputs=output)
79
+ refresh.click(status, outputs=output)
80
+
81
+
82
+ demo.launch(server_name="0.0.0.0")
training_space/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio