Coverage for widitapp/diffusion/timestep_sampler.py: 100.00%
66 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-09-22 07:22 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-09-22 07:22 +0000
1# Modified from OpenAI's diffusion repos
2# GLIDE: https://github.com/openai/glide-text2im/blob/main/glide_text2im/gaussian_diffusion.py
3# ADM: https://github.com/openai/guided-diffusion/blob/main/guided_diffusion
4# IDDPM: https://github.com/openai/improved-diffusion/blob/main/improved_diffusion/gaussian_diffusion.py
6from abc import ABC, abstractmethod
8import numpy as np
9import torch as th
10import torch.distributed as dist
13def create_named_schedule_sampler(name, diffusion):
14 """
15 Create a ScheduleSampler from a library of pre-defined samplers.
16 :param name: the name of the sampler.
17 :param diffusion: the diffusion object to sample for.
18 """
19 if name == "uniform":
20 return UniformSampler(diffusion)
21 elif name == "loss-second-moment":
22 return LossSecondMomentResampler(diffusion)
23 else:
24 raise NotImplementedError(f"unknown schedule sampler: {name}")
27class ScheduleSampler(ABC):
28 """
29 A distribution over timesteps in the diffusion process, intended to reduce
30 variance of the objective.
31 By default, samplers perform unbiased importance sampling, in which the
32 objective's mean is unchanged.
33 However, subclasses may override sample() to change how the resampled
34 terms are reweighted, allowing for actual changes in the objective.
35 """
37 @abstractmethod
38 def weights(self):
39 """
40 Get a numpy array of weights, one per diffusion step.
41 The weights needn't be normalized, but must be positive.
42 """
44 def sample(self, batch_size, device):
45 """
46 Importance-sample timesteps for a batch.
47 :param batch_size: the number of timesteps.
48 :param device: the torch device to save to.
49 :return: a tuple (timesteps, weights):
50 - timesteps: a tensor of timestep indices.
51 - weights: a tensor of weights to scale the resulting losses.
52 """
53 w = self.weights()
54 p = w / np.sum(w)
55 indices_np = np.random.choice(len(p), size=(batch_size,), p=p)
56 indices = th.from_numpy(indices_np).long().to(device)
57 weights_np = 1 / (len(p) * p[indices_np])
58 weights = th.from_numpy(weights_np).float().to(device)
59 return indices, weights
62class UniformSampler(ScheduleSampler):
63 def __init__(self, diffusion):
64 self.diffusion = diffusion
65 self._weights = np.ones([diffusion.num_timesteps])
67 def weights(self):
68 return self._weights
71class LossAwareSampler(ScheduleSampler):
72 def update_with_local_losses(self, local_ts, local_losses):
73 """
74 Update the reweighting using losses from a model.
75 Call this method from each rank with a batch of timesteps and the
76 corresponding losses for each of those timesteps.
77 This method will perform synchronization to make sure all of the ranks
78 maintain the exact same reweighting.
79 :param local_ts: an integer Tensor of timesteps.
80 :param local_losses: a 1D Tensor of losses.
81 """
82 batch_sizes = [
83 th.tensor([0], dtype=th.int32, device=local_ts.device)
84 for _ in range(dist.get_world_size())
85 ]
86 dist.all_gather(
87 batch_sizes,
88 th.tensor([len(local_ts)], dtype=th.int32, device=local_ts.device),
89 )
91 # Pad all_gather batches to be the maximum batch size.
92 batch_sizes = [x.item() for x in batch_sizes]
93 max_bs = max(batch_sizes)
95 timestep_batches = [th.zeros(max_bs).to(local_ts) for bs in batch_sizes]
96 loss_batches = [th.zeros(max_bs).to(local_losses) for bs in batch_sizes]
97 dist.all_gather(timestep_batches, local_ts)
98 dist.all_gather(loss_batches, local_losses)
99 timesteps = [
100 x.item() for y, bs in zip(timestep_batches, batch_sizes) for x in y[:bs]
101 ]
102 losses = [x.item() for y, bs in zip(loss_batches, batch_sizes) for x in y[:bs]]
103 self.update_with_all_losses(timesteps, losses)
105 @abstractmethod
106 def update_with_all_losses(self, ts, losses):
107 """
108 Update the reweighting using losses from a model.
109 Sub-classes should override this method to update the reweighting
110 using losses from the model.
111 This method directly updates the reweighting without synchronizing
112 between workers. It is called by update_with_local_losses from all
113 ranks with identical arguments. Thus, it should have deterministic
114 behavior to maintain state across workers.
115 :param ts: a list of int timesteps.
116 :param losses: a list of float losses, one per timestep.
117 """
120class LossSecondMomentResampler(LossAwareSampler):
121 def __init__(self, diffusion, history_per_term=10, uniform_prob=0.001):
122 self.diffusion = diffusion
123 self.history_per_term = history_per_term
124 self.uniform_prob = uniform_prob
125 self._loss_history = np.zeros(
126 [diffusion.num_timesteps, history_per_term], dtype=np.float64
127 )
128 self._loss_counts = np.zeros([diffusion.num_timesteps], dtype=int)
130 def weights(self):
131 if not self._warmed_up():
132 return np.ones([self.diffusion.num_timesteps], dtype=np.float64)
133 weights = np.sqrt(np.mean(self._loss_history ** 2, axis=-1))
134 weights /= np.sum(weights)
135 weights *= 1 - self.uniform_prob
136 weights += self.uniform_prob / len(weights)
137 return weights
139 def update_with_all_losses(self, ts, losses):
140 for t, loss in zip(ts, losses):
141 if self._loss_counts[t] == self.history_per_term:
142 # Shift out the oldest loss term.
143 self._loss_history[t, :-1] = self._loss_history[t, 1:]
144 self._loss_history[t, -1] = loss
145 else:
146 self._loss_history[t, self._loss_counts[t]] = loss
147 self._loss_counts[t] += 1
149 def _warmed_up(self):
150 return (self._loss_counts == self.history_per_term).all()