Add RL Training Lab
Browse files- learned_filler.py +10 -7
- pbc3_rl_a20.py +359 -0
- pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz +3 -0
- pbc3_students/patch_policy_f26_a25_h512_l2_e1200.npz +3 -0
- server.py +5 -2
- static/app.js +3 -3
- static/index.html +30 -0
- static/train.js +152 -0
- train_api.py +184 -0
- training_space/README.md +12 -0
- training_space/app.py +82 -0
- training_space/requirements.txt +1 -0
learned_filler.py
CHANGED
|
@@ -112,9 +112,10 @@ def _silu(x):
|
|
| 112 |
|
| 113 |
|
| 114 |
def _mlp(x, weights):
|
| 115 |
-
"""## Runs the
|
| 116 |
-
|
| 117 |
-
|
|
|
|
| 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 |
-
|
| 129 |
-
|
|
|
|
|
|
|
| 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,
|
| 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", "
|
| 352 |
-
config = PBC3_PRESETS.get(preset, PBC3Config.
|
| 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 = "
|
| 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 : "
|
| 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) => ({ "&": "&", "<": "<", ">": ">", "\"": """ }[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
|