Skip to content

Commit a2face7

Browse files
fix: make diffusion training resumable
1 parent eb72778 commit a2face7

5 files changed

Lines changed: 319 additions & 589 deletions

File tree

src/diffusion/dataset.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import numpy as np
1212
import torch
13-
from torch.utils.data import DataLoader, Dataset
13+
from torch.utils.data import DataLoader, Dataset, Subset
1414
from tqdm import tqdm
1515

1616
from src.utils.data_recorder import load_npz_shard
@@ -300,6 +300,38 @@ def create_dataloader(
300300
return dataloader
301301

302302

303+
def create_dataloaders(
304+
data_dir: str,
305+
batch_size: int = 32,
306+
context_length: int = 32,
307+
resolution: Tuple[int, int] = (256, 512),
308+
num_workers: int = 4,
309+
validation_fraction: float = 0.05,
310+
seed: int = 0,
311+
) -> Tuple[DataLoader, DataLoader]:
312+
"""Create deterministic episode-disjoint train and validation loaders."""
313+
if not 0.0 < validation_fraction < 1.0:
314+
raise ValueError("validation_fraction must be between 0 and 1")
315+
dataset = GameplayDataset(data_dir, context_length, resolution)
316+
by_episode: Dict[Tuple[int, int], List[int]] = {}
317+
for index, trajectory in enumerate(dataset.trajectories):
318+
by_episode.setdefault((trajectory["batch_idx"], trajectory["episode_idx"]), []).append(index)
319+
episode_keys = sorted(by_episode)
320+
if len(episode_keys) < 2:
321+
raise ValueError("at least two recorded episodes are required for a train/validation split")
322+
rng = np.random.default_rng(seed)
323+
rng.shuffle(episode_keys)
324+
validation_count = min(len(episode_keys) - 1, max(1, round(len(episode_keys) * validation_fraction)))
325+
validation_indices = [index for key in episode_keys[:validation_count] for index in by_episode[key]]
326+
train_indices = [index for key in episode_keys[validation_count:] for index in by_episode[key]]
327+
generator = torch.Generator().manual_seed(seed)
328+
common = {"batch_size": batch_size, "num_workers": num_workers, "pin_memory": torch.cuda.is_available(), "persistent_workers": num_workers > 0}
329+
return (
330+
DataLoader(Subset(dataset, train_indices), shuffle=True, generator=generator, **common),
331+
DataLoader(Subset(dataset, validation_indices), shuffle=False, **common),
332+
)
333+
334+
303335
def test_dataset(data_dir: str):
304336
"""Test dataset loading"""
305337
print(f"Testing dataset with data from {data_dir}")

src/diffusion/optimizers.py

Lines changed: 20 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,251 +1,41 @@
1-
"""
2-
Optimizers for GameNGen
3-
Paper uses Adafactor for Tier 3 (Section 4.2)
4-
"""
1+
"""Optimizer construction for diffusion training."""
52

6-
from typing import Iterable, Optional, Tuple
3+
from typing import Iterable
74

8-
import numpy as np
95
import torch
106

117

12-
class Adafactor(torch.optim.Optimizer):
13-
"""
14-
Adafactor optimizer
15-
Paper Section 4.2: "with the Adafactor optimizer without weight decay"
8+
def create_optimizer(optimizer_name: str, parameters: Iterable[torch.nn.Parameter], config: dict) -> torch.optim.Optimizer:
9+
"""Build the configured optimizer with explicit, reproducible settings.
1610
17-
Implementation based on Shazeer & Stern (2018)
18-
"Adafactor: Adaptive Learning Rates with Sublinear Memory Cost"
11+
The paper profile uses the maintained Transformers Adafactor implementation.
12+
Keeping the implementation in its upstream package avoids silently training
13+
with a partial local reimplementation.
1914
"""
2015

21-
def __init__(
22-
self,
23-
params: Iterable[torch.nn.Parameter],
24-
lr: float = 1e-3,
25-
eps: Tuple[float, float] = (1e-30, 1e-3),
26-
clip_threshold: float = 1.0,
27-
decay_rate: float = -0.8,
28-
beta1: Optional[float] = None,
29-
weight_decay: float = 0.0,
30-
scale_parameter: bool = True,
31-
relative_step: bool = True,
32-
warmup_init: bool = True,
33-
):
34-
"""
35-
Args:
36-
params: Iterable of parameters
37-
lr: Learning rate (if relative_step=False)
38-
eps: Regularization constants (eps0, eps1)
39-
clip_threshold: Threshold for clipping update
40-
decay_rate: Coefficient for computing running avg of squared gradient
41-
beta1: Coefficient for running avg of gradient (momentum)
42-
weight_decay: Weight decay coefficient
43-
scale_parameter: Whether to scale parameters
44-
relative_step: Whether to use relative step sizes
45-
warmup_init: Whether to use warmup initialization
46-
"""
47-
if relative_step and warmup_init:
48-
# Paper doesn't use warmup, but option available
49-
lr = None
50-
51-
defaults = dict(
52-
lr=lr,
53-
eps=eps,
54-
clip_threshold=clip_threshold,
55-
decay_rate=decay_rate,
56-
beta1=beta1,
57-
weight_decay=weight_decay,
58-
scale_parameter=scale_parameter,
59-
relative_step=relative_step,
60-
warmup_init=warmup_init,
61-
)
62-
super().__init__(params, defaults)
63-
64-
def _get_lr(self, param_group, param_scale):
65-
"""Compute learning rate"""
66-
if param_group["lr"] is None:
67-
min_step = (
68-
1e-6 * param_group["step"] if param_group["warmup_init"] else 1e-2
69-
)
70-
rel_step_sz = min(min_step, 1.0 / np.sqrt(param_group["step"]))
71-
param_group["lr"] = rel_step_sz * param_scale
72-
return param_group["lr"]
73-
74-
def _get_options(self, param_group, param_shape):
75-
"""Get factorization options"""
76-
factored = len(param_shape) >= 2
77-
use_first_moment = param_group["beta1"]
78-
return factored, use_first_moment
79-
80-
def _rms(self, tensor):
81-
"""Root mean square"""
82-
return tensor.norm(2) / (tensor.numel() ** 0.5)
83-
84-
def _approx_sq_grad(self, exp_avg_sq_row, exp_avg_sq_col):
85-
"""Approximation of exponential moving average of square of gradient"""
86-
r_factor = (
87-
(exp_avg_sq_row / exp_avg_sq_row.mean(dim=-1, keepdim=True))
88-
.rsqrt_()
89-
.unsqueeze(1)
90-
)
91-
c_factor = exp_avg_sq_col.unsqueeze(0).rsqrt()
92-
v = r_factor * c_factor
93-
return v
94-
95-
def step(self, closure=None):
96-
"""
97-
Perform a single optimization step
98-
99-
Args:
100-
closure: A closure that reevaluates model and returns loss
101-
"""
102-
loss = None
103-
if closure is not None:
104-
loss = closure()
105-
106-
for group in self.param_groups:
107-
for p in group["params"]:
108-
if p.grad is None:
109-
continue
110-
111-
grad = p.grad.data
112-
if grad.is_sparse:
113-
raise RuntimeError("Adafactor does not support sparse gradients")
114-
115-
state = self.state[p]
116-
grad_shape = grad.shape
117-
118-
param_group = group
119-
factored, use_first_moment = self._get_options(param_group, grad_shape)
120-
121-
# State initialization
122-
if len(state) == 0:
123-
state["step"] = 0
124-
125-
if use_first_moment:
126-
# Exponential moving average of gradient values
127-
state["exp_avg"] = torch.zeros_like(grad)
128-
129-
if factored:
130-
state["exp_avg_sq_row"] = torch.zeros(grad_shape[0])
131-
state["exp_avg_sq_col"] = torch.zeros(grad_shape[1:]).flatten()
132-
else:
133-
state["exp_avg_sq"] = torch.zeros_like(grad)
134-
135-
state["RMS"] = 0
136-
137-
state["step"] += 1
138-
lr = self._get_lr(param_group, self._rms(p.data))
139-
param_group["lr"] = lr
140-
141-
# Momentum
142-
if use_first_moment:
143-
state["exp_avg"].mul_(param_group["beta1"]).add_(
144-
grad, alpha=1 - param_group["beta1"]
145-
)
146-
147-
# Second moment estimation
148-
eps_0, eps_1 = param_group["eps"]
149-
decay_rate = 1 - (state["step"] + 1) ** param_group["decay_rate"]
150-
151-
if factored:
152-
update = grad**2 + eps_0
153-
state["exp_avg_sq_row"].mul_(decay_rate).add_(
154-
update.mean(dim=list(range(1, len(grad_shape)))),
155-
alpha=1 - decay_rate,
156-
)
157-
state["exp_avg_sq_col"].mul_(decay_rate).add_(
158-
update.mean(dim=0).flatten(), alpha=1 - decay_rate
159-
)
160-
update = self._approx_sq_grad(
161-
state["exp_avg_sq_row"], state["exp_avg_sq_col"]
162-
)
163-
update.mul_(grad)
164-
else:
165-
state["exp_avg_sq"].mul_(decay_rate).add_(
166-
grad**2 + eps_0, alpha=1 - decay_rate
167-
)
168-
update = grad / (state["exp_avg_sq"].sqrt() + eps_1)
169-
170-
# Clip
171-
rms = self._rms(update)
172-
state["RMS"] = rms
173-
174-
if param_group["clip_threshold"] > 0:
175-
clip_val = param_group["clip_threshold"] / max(1.0, rms)
176-
update.clamp_(-clip_val, clip_val)
177-
178-
# Update
179-
if use_first_moment:
180-
update = state["exp_avg"]
181-
182-
if param_group["weight_decay"] > 0:
183-
p.data.mul_(1 - param_group["weight_decay"] * lr)
184-
185-
p.data.add_(update, alpha=-lr)
186-
187-
return loss
188-
189-
190-
# Simple wrapper for easier usage
191-
def create_optimizer(optimizer_name: str, parameters, config: dict):
192-
"""
193-
Create optimizer based on config
194-
195-
Args:
196-
optimizer_name: 'AdamW' or 'Adafactor'
197-
parameters: Model parameters
198-
config: Configuration dict
199-
200-
Returns:
201-
Optimizer instance
202-
"""
203-
if optimizer_name.lower() == "adamw":
16+
name = optimizer_name.lower()
17+
if name == "adamw":
20418
return torch.optim.AdamW(
20519
parameters,
20620
lr=config["learning_rate"],
20721
weight_decay=config.get("weight_decay", 0.0),
20822
betas=(config.get("adam_beta1", 0.9), config.get("adam_beta2", 0.999)),
20923
eps=config.get("adam_epsilon", 1e-8),
21024
)
211-
212-
elif optimizer_name.lower() == "adafactor":
213-
# Paper's setting: "without weight decay"
25+
if name == "adafactor":
26+
try:
27+
from transformers.optimization import Adafactor
28+
except ImportError as error:
29+
raise RuntimeError(
30+
"Adafactor requires the optional 'transformers' dependency. "
31+
"Install the diffusion extra before using the paper profile."
32+
) from error
21433
return Adafactor(
21534
parameters,
21635
lr=config["learning_rate"],
217-
weight_decay=0.0, # Paper doesn't use weight decay
36+
weight_decay=0.0,
21837
scale_parameter=False,
21938
relative_step=False,
39+
warmup_init=False,
22040
)
221-
222-
else:
223-
raise ValueError(f"Unknown optimizer: {optimizer_name}")
224-
225-
226-
if __name__ == "__main__":
227-
# Test Adafactor
228-
print("Testing Adafactor optimizer...")
229-
230-
# Create dummy model
231-
model = torch.nn.Linear(100, 10)
232-
233-
# Create optimizer
234-
optimizer = Adafactor(model.parameters(), lr=2e-5)
235-
236-
print(f"Optimizer created: {optimizer}")
237-
238-
# Test step
239-
x = torch.randn(32, 100)
240-
y = torch.randn(32, 10)
241-
242-
for i in range(10):
243-
optimizer.zero_grad()
244-
output = model(x)
245-
loss = torch.nn.functional.mse_loss(output, y)
246-
loss.backward()
247-
optimizer.step()
248-
249-
print(f"Step {i}: loss = {loss.item():.4f}")
250-
251-
print("\n✓ Adafactor test passed!")
41+
raise ValueError(f"unknown optimizer: {optimizer_name}")

0 commit comments

Comments
 (0)