Off by default. On, the timeline sits as a strip — its header, so the title, transport and Composer stay reachable — and opens to full height while the pointer is on it or something inside it has focus. It also opens for the whole of a DRAG. During one the pointer is in drag mode and a strip that stays shut is a drop target you cannot hit, so dragenter holds it open and drop/dragend closes it. The tracker counts depth rather than holding a boolean: dragleave for the element being left fires AFTER dragenter for the one being entered, so a flag closes the strip mid-drag as the pointer crosses from one clip to the next. The strip height is MEASURED from the header rather than fixed: .zone-head wraps when the zone is narrow (34px min-height, 138px on a normal window), so any constant clips the very controls the strip exists to keep visible. It is re-measured on resize. flex: 0 0 auto matters — .zone is a shrinkable column child, and a height with min-height: 0 let the workspace squeeze the strip to 1px. The body collapses rather than being display:none so the lanes are not torn down and rebuilt on every open. A browser preference like the colour scheme, not a project one, and registered in the same Settings section for that reason. Also in this commit: - vf: train wherever there is a GPU, not only when the sample sits on one. The x0 value function's snapshot loads with map_location="cpu", so keying on the sample left it training on the CPU — the run that named it measured "x0 vf: train 10.1s" against 46ms for the same net locally. - The stage line counted nested sub-stages twice and reported 153% of a run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
"""Value-function training runs where the sample already is.
|
|
|
|
These nets are tiny in FLOPs and not in parameters: a 5120-wide conditioning makes the first
|
|
layer 94% of ~4M weights. Twenty Adam steps over that measured ~1.1s on this Mac's CPU and
|
|
about 7.5s on the rental's vCPU — per rated generation, three value functions over. The
|
|
conditioning is already on the GPU when there is one.
|
|
"""
|
|
import sys
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
sys.path.insert(0, ".")
|
|
|
|
from value_function import LatentValueFunction, OnlineValueFunction # noqa: E402
|
|
|
|
|
|
def _trained(dim=32, samples=6):
|
|
vf = OnlineValueFunction(hidden_dim=dim)
|
|
for i in range(samples):
|
|
vf.buffer_c.append(torch.randn(dim))
|
|
vf.buffer_r.append(float(i % 3))
|
|
return vf
|
|
|
|
|
|
def test_a_cpu_sample_trains_where_it_is():
|
|
vf = _trained()
|
|
assert vf._training_device(torch.zeros(4), torch.device("cpu")) == torch.device("cpu")
|
|
|
|
|
|
def test_a_non_tensor_does_not_pick_a_device():
|
|
vf = _trained()
|
|
home = torch.device("cpu")
|
|
assert vf._training_device(None, home) is home
|
|
assert vf._training_device("not a tensor", home) is home
|
|
|
|
|
|
def test_training_still_learns_on_cpu():
|
|
"""The device change must not alter what training does."""
|
|
torch.manual_seed(0)
|
|
vf = _trained()
|
|
before = [p.clone() for p in vf.parameters()]
|
|
vf.train_on(torch.randn(1, 8, 32), 0.9)
|
|
assert vf.n_trained == 1
|
|
assert any(not torch.equal(a, b) for a, b in zip(before, vf.parameters()))
|
|
|
|
|
|
def test_the_module_is_left_on_its_home_device():
|
|
"""Inference elsewhere expects a CPU module and the checkpoint must stay portable."""
|
|
vf = _trained()
|
|
vf.train_on(torch.randn(1, 8, 32), 0.5)
|
|
assert all(p.device == torch.device("cpu") for p in vf.parameters())
|
|
|
|
|
|
def test_one_sample_is_not_trained_on():
|
|
"""Pairwise ranking needs two. n_trained must not count a step that never happened."""
|
|
vf = OnlineValueFunction(hidden_dim=32)
|
|
vf.train_on(torch.randn(1, 8, 32), 0.5)
|
|
assert vf.n_trained == 0
|
|
assert len(vf.buffer_c) == 1
|
|
|
|
|
|
def test_the_buffer_stays_on_the_cpu():
|
|
"""It is what gets written to the checkpoint; a CUDA buffer would not be portable."""
|
|
vf = _trained()
|
|
vf.train_on(torch.randn(1, 8, 32), 0.5)
|
|
assert all(c.device == torch.device("cpu") for c in vf.buffer_c)
|
|
|
|
|
|
def test_the_latent_value_function_inherits_the_same_path():
|
|
vf = LatentValueFunction(hidden_dim=64)
|
|
for i in range(4):
|
|
vf.buffer_c.append(torch.randn(64))
|
|
vf.buffer_r.append(float(i % 2))
|
|
vf.train_on(torch.randn(1, 1, 4096), 0.7)
|
|
assert vf.n_trained == 1
|
|
|
|
|
|
def test_a_cpu_snapshot_still_goes_to_the_gpu(monkeypatch):
|
|
"""The x0 value function's sample is loaded with map_location="cpu". Keying the device on
|
|
the sample left it training on the CPU — 10.1s a run against 46ms for the same net."""
|
|
import value_function as V
|
|
monkeypatch.setattr(V.torch.cuda, "is_available", lambda: True)
|
|
home = torch.device("cpu")
|
|
assert V.OnlineValueFunction._training_device(torch.zeros(4), home) == torch.device("cuda")
|
|
|
|
|
|
def test_without_a_gpu_it_stays_home(monkeypatch):
|
|
import value_function as V
|
|
monkeypatch.setattr(V.torch.cuda, "is_available", lambda: False)
|
|
home = torch.device("cpu")
|
|
assert V.OnlineValueFunction._training_device(torch.zeros(4), home) is home
|