lsmpp commited on
Commit
dfd7582
·
verified ·
1 Parent(s): 43f1889

Add MeanFlow and Teacher sampling

Browse files
vendor/pixtext/sampling/__init__.py ADDED
File without changes
vendor/pixtext/sampling/flow.py ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Joint ODE sampling for variable-length grouped image JiT outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Callable, Sequence
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ from pixtext.modeling.patches import pack_image_groups
12
+ from pixtext.preprocessing.geometry import GroupGeometry
13
+ from pixtext.preprocessing.glyphs import GlyphCondition
14
+ from pixtext.sampling.timesteps import (
15
+ GroupTimesteps,
16
+ GroupTimestepSchedule,
17
+ TimestepRangeStats,
18
+ resolve_group_timestep_schedule,
19
+ )
20
+
21
+
22
+ class GroupFlowSampler(nn.Module):
23
+ """Jointly sample complete image groups with group-level CFG."""
24
+
25
+ def __init__(
26
+ self,
27
+ model: nn.Module,
28
+ *,
29
+ solver: str = "heun",
30
+ steps: int = 50,
31
+ cfg_scale: float = 1.0,
32
+ cfg_interval: tuple[float, float] = (0.1, 1.0),
33
+ noise_scale: float = 1.0,
34
+ timestep_schedule: GroupTimestepSchedule | None = None,
35
+ compute_dtype: torch.dtype | None = None,
36
+ ) -> None:
37
+ super().__init__()
38
+ if not isinstance(model, nn.Module):
39
+ raise TypeError("model must be a torch.nn.Module")
40
+ if solver not in {"euler", "heun"}:
41
+ raise ValueError(f"solver must be 'euler' or 'heun', got {solver!r}")
42
+ if isinstance(steps, bool) or not isinstance(steps, int) or steps <= 0:
43
+ raise ValueError(f"steps must be a positive integer, got {steps!r}")
44
+ for name, value, positive in (
45
+ ("cfg_scale", cfg_scale, False),
46
+ ("noise_scale", noise_scale, False),
47
+ ):
48
+ if (
49
+ isinstance(value, bool)
50
+ or not isinstance(value, (int, float))
51
+ or (not math.isfinite(value))
52
+ or (value <= 0 if positive else value < 0)
53
+ ):
54
+ qualifier = "positive" if positive else "non-negative"
55
+ raise ValueError(f"{name} must be finite and {qualifier}, got {value!r}")
56
+ if (
57
+ not isinstance(cfg_interval, tuple)
58
+ or len(cfg_interval) != 2
59
+ or any(
60
+ (
61
+ isinstance(value, bool)
62
+ or not isinstance(value, (int, float))
63
+ or (not math.isfinite(value))
64
+ for value in cfg_interval
65
+ )
66
+ )
67
+ or (not 0 <= cfg_interval[0] < cfg_interval[1] <= 1)
68
+ ):
69
+ raise ValueError(
70
+ f"cfg_interval must be a finite (low, high) pair within [0, 1], got {cfg_interval!r}"
71
+ )
72
+ patch_size = getattr(model, "patch_size", None)
73
+ if isinstance(patch_size, bool) or not isinstance(patch_size, int) or patch_size <= 0:
74
+ raise ValueError(f"model.patch_size must be a positive integer, got {patch_size!r}")
75
+ in_channels = getattr(model, "in_channels", None)
76
+ if isinstance(in_channels, bool) or not isinstance(in_channels, int) or in_channels <= 0:
77
+ raise ValueError(f"model.in_channels must be a positive integer, got {in_channels!r}")
78
+ self.model = model
79
+ self.solver = solver
80
+ self.steps = steps
81
+ self.cfg_scale = float(cfg_scale)
82
+ self.cfg_interval = (float(cfg_interval[0]), float(cfg_interval[1]))
83
+ self.noise_scale = float(noise_scale)
84
+ self.patch_size = patch_size
85
+ self.in_channels = in_channels
86
+ if compute_dtype is not None and (
87
+ not isinstance(compute_dtype, torch.dtype) or not compute_dtype.is_floating_point
88
+ ):
89
+ raise TypeError("compute_dtype must be a floating torch.dtype or None")
90
+ self.compute_dtype = compute_dtype
91
+ self.timestep_schedule = resolve_group_timestep_schedule(
92
+ timestep_schedule, patch_size=self.patch_size
93
+ )
94
+ self.last_timestep_stats: TimestepRangeStats | None = None
95
+
96
+ def _validate_inputs(
97
+ self,
98
+ conditional: None,
99
+ unconditional: None,
100
+ geometries: Sequence[GroupGeometry],
101
+ generator: torch.Generator | Sequence[torch.Generator] | None,
102
+ main_conditions: Sequence[torch.Tensor] | None,
103
+ ) -> None:
104
+ if self.model.training:
105
+ raise RuntimeError("sampling model must be in eval mode")
106
+ if conditional is not None or unconditional is not None:
107
+ raise ValueError("no-text sampler requires prompt views to be None")
108
+ group_count = len(geometries)
109
+ if group_count == 0:
110
+ raise ValueError("sampling batch must contain at least one group")
111
+ if generator is not None and (not isinstance(generator, torch.Generator)):
112
+ if (
113
+ not isinstance(generator, Sequence)
114
+ or len(generator) != group_count
115
+ or any((not isinstance(item, torch.Generator) for item in generator))
116
+ ):
117
+ raise TypeError("generator must be one torch.Generator, one per group, or None")
118
+ image_counts = []
119
+ for group_id, geometry in enumerate(geometries):
120
+ if not isinstance(geometry, GroupGeometry):
121
+ raise TypeError(
122
+ f"geometry {group_id} must be GroupGeometry, got {type(geometry).__name__}"
123
+ )
124
+ image_counts.append(1 + len(geometry.items))
125
+ image_counts = tuple(image_counts)
126
+ if self.timestep_schedule.is_native_edit:
127
+ if main_conditions is None or len(main_conditions) != group_count:
128
+ raise ValueError("native edit sampling requires one main condition per group")
129
+ for group_id, (condition, geometry) in enumerate(
130
+ zip(main_conditions, geometries, strict=True)
131
+ ):
132
+ expected_shape = (
133
+ self.in_channels,
134
+ geometry.main.size.height,
135
+ geometry.main.size.width,
136
+ )
137
+ if not isinstance(condition, torch.Tensor) or condition.shape != expected_shape:
138
+ raise ValueError(
139
+ f"main condition {group_id} must match shape, device, and dtype {expected_shape}, {condition.device}, and {condition.dtype}"
140
+ )
141
+ elif main_conditions is not None:
142
+ raise ValueError("main_conditions are only valid for native edit sampling")
143
+ floating_parameters = tuple(
144
+ (parameter for parameter in self.model.parameters() if parameter.is_floating_point())
145
+ )
146
+ runtime_tensor = main_conditions[0]
147
+ if floating_parameters and any(
148
+ (parameter.device != runtime_tensor.device for parameter in floating_parameters)
149
+ ):
150
+ raise ValueError("all floating model parameters and main conditions must share device")
151
+ if self.compute_dtype is None:
152
+ if floating_parameters and any(
153
+ (parameter.dtype != runtime_tensor.dtype for parameter in floating_parameters)
154
+ ):
155
+ raise ValueError(
156
+ "all floating model parameters and main conditions must share dtype"
157
+ )
158
+ elif runtime_tensor.dtype != self.compute_dtype:
159
+ raise ValueError(
160
+ f"main condition dtype must match the configured model compute dtype {self.compute_dtype}"
161
+ )
162
+
163
+ def _initial_noise(
164
+ self,
165
+ geometries: Sequence[GroupGeometry],
166
+ prompts: None,
167
+ generator: torch.Generator | Sequence[torch.Generator] | None,
168
+ main_conditions: Sequence[torch.Tensor] | None,
169
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
170
+ groups = []
171
+ runtime_tensor = main_conditions[0]
172
+ if isinstance(generator, Sequence):
173
+ group_generators = tuple(generator)
174
+ else:
175
+ group_generators = (generator,) * len(geometries)
176
+ for group_id, (geometry, group_generator) in enumerate(
177
+ zip(geometries, group_generators, strict=True)
178
+ ):
179
+ item_sizes = tuple((item.crop.size for item in geometry.items))
180
+ if self.timestep_schedule.is_native_edit:
181
+ assert main_conditions is not None
182
+ group = (main_conditions[group_id],) + tuple(
183
+ (
184
+ torch.randn(
185
+ self.in_channels,
186
+ size.height,
187
+ size.width,
188
+ device=runtime_tensor.device,
189
+ dtype=runtime_tensor.dtype,
190
+ generator=group_generator,
191
+ )
192
+ * self.noise_scale
193
+ for size in item_sizes
194
+ )
195
+ )
196
+ else:
197
+ sizes = (geometry.main.size,) + item_sizes
198
+ group = tuple(
199
+ (
200
+ torch.randn(
201
+ self.in_channels,
202
+ size.height,
203
+ size.width,
204
+ device=runtime_tensor.device,
205
+ dtype=runtime_tensor.dtype,
206
+ generator=group_generator,
207
+ )
208
+ * self.noise_scale
209
+ for size in sizes
210
+ )
211
+ )
212
+ groups.append(group)
213
+ return tuple(groups)
214
+
215
+ def _model_prediction(
216
+ self,
217
+ images: tuple[tuple[torch.Tensor, ...], ...],
218
+ prompts: None,
219
+ geometries: Sequence[GroupGeometry],
220
+ timesteps: GroupTimesteps,
221
+ glyph_conditions: Sequence[Sequence[GlyphCondition | None]] | None = None,
222
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
223
+ packed_images = pack_image_groups(images, patch_size=self.patch_size)
224
+ model_kwargs = dict(
225
+ packed_images=packed_images, prompts=prompts, geometries=geometries, timesteps=timesteps
226
+ )
227
+ if glyph_conditions is not None:
228
+ model_kwargs["glyph_conditions"] = glyph_conditions
229
+ predictions = self.model(**model_kwargs)
230
+ if len(predictions) != len(images):
231
+ raise ValueError("model prediction group count must match sampling state")
232
+ clean_predictions = []
233
+ for group_id, (image_group, prediction_group) in enumerate(zip(images, predictions)):
234
+ if len(prediction_group) != len(image_group):
235
+ raise ValueError(
236
+ f"model prediction count for group {group_id} must match sampling images"
237
+ )
238
+ clean_prediction_group = []
239
+ for image_id, (image, prediction) in enumerate(zip(image_group, prediction_group)):
240
+ if not isinstance(prediction, torch.Tensor):
241
+ raise TypeError(
242
+ f"model prediction for group {group_id} image {image_id} must be a torch.Tensor"
243
+ )
244
+ if (
245
+ prediction.shape != image.shape
246
+ or prediction.device != image.device
247
+ or prediction.dtype != image.dtype
248
+ ):
249
+ raise ValueError(
250
+ f"model prediction for group {group_id} image {image_id} must match sampling shape, device, and dtype"
251
+ )
252
+ clean_prediction_group.append(prediction)
253
+ clean_predictions.append(tuple(clean_prediction_group))
254
+ return tuple(clean_predictions)
255
+
256
+ def _guided_prediction(
257
+ self,
258
+ images: tuple[tuple[torch.Tensor, ...], ...],
259
+ conditional: None,
260
+ unconditional: None,
261
+ geometries: Sequence[GroupGeometry],
262
+ timesteps: GroupTimesteps,
263
+ time_value: float,
264
+ glyph_conditions: Sequence[Sequence[GlyphCondition | None]] | None = None,
265
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
266
+ conditional_prediction = self._model_prediction(
267
+ images, conditional, geometries, timesteps, glyph_conditions
268
+ )
269
+ (low, high) = self.cfg_interval
270
+ scale = self.cfg_scale if low < time_value < high else 1.0
271
+ if conditional is None and scale == 1.0:
272
+ return conditional_prediction
273
+ unconditional_glyphs = glyph_conditions
274
+ if conditional is None:
275
+ if glyph_conditions is None:
276
+ raise ValueError("glyph guidance requires conditional glyphs")
277
+ unconditional_glyphs = tuple(((None,) * len(group) for group in glyph_conditions))
278
+ unconditional_prediction = self._model_prediction(
279
+ images, unconditional, geometries, timesteps, unconditional_glyphs
280
+ )
281
+ if scale == 1.0:
282
+ return conditional_prediction
283
+ return tuple(
284
+ (
285
+ tuple(
286
+ (
287
+ uncond + scale * (cond - uncond)
288
+ for (cond, uncond) in zip(cond_group, uncond_group)
289
+ )
290
+ )
291
+ for (cond_group, uncond_group) in zip(
292
+ conditional_prediction, unconditional_prediction
293
+ )
294
+ )
295
+ )
296
+
297
+ @staticmethod
298
+ def _velocities_from_predictions(
299
+ images: tuple[tuple[torch.Tensor, ...], ...],
300
+ predictions: tuple[tuple[torch.Tensor, ...], ...],
301
+ timesteps: GroupTimesteps,
302
+ *,
303
+ fixed_main: bool = False,
304
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
305
+ velocities = []
306
+ flat_image_id = 0
307
+ for image_group, prediction_group in zip(images, predictions, strict=True):
308
+ velocity_group = []
309
+ for image_id, (image, prediction) in enumerate(
310
+ zip(image_group, prediction_group, strict=True)
311
+ ):
312
+ if fixed_main and image_id == 0:
313
+ velocity_group.append(torch.zeros_like(image))
314
+ else:
315
+ denominator = (1 - timesteps.images[flat_image_id]).to(dtype=image.dtype)
316
+ velocity_group.append((prediction - image) / denominator)
317
+ flat_image_id += 1
318
+ velocities.append(tuple(velocity_group))
319
+ return tuple(velocities)
320
+
321
+ @staticmethod
322
+ def _advance_toward_predictions(
323
+ images: tuple[tuple[torch.Tensor, ...], ...],
324
+ predictions: tuple[tuple[torch.Tensor, ...], ...],
325
+ timesteps: GroupTimesteps,
326
+ next_timesteps: GroupTimesteps,
327
+ *,
328
+ fixed_main: bool = False,
329
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
330
+ """Apply x_next = x + ((t_next - t) / (1 - t)) * (x0 - x)."""
331
+ advanced_groups = []
332
+ flat_image_id = 0
333
+ for image_group, prediction_group in zip(images, predictions, strict=True):
334
+ advanced_group = []
335
+ for image_id, (image, prediction) in enumerate(
336
+ zip(image_group, prediction_group, strict=True)
337
+ ):
338
+ if fixed_main and image_id == 0:
339
+ advanced_group.append(image)
340
+ else:
341
+ t = timesteps.images[flat_image_id]
342
+ t_next = next_timesteps.images[flat_image_id]
343
+ fraction = ((t_next - t) / (1 - t)).to(dtype=image.dtype)
344
+ advanced_group.append(
345
+ torch.where(
346
+ t_next == 1, prediction, image + fraction * (prediction - image)
347
+ )
348
+ )
349
+ flat_image_id += 1
350
+ advanced_groups.append(tuple(advanced_group))
351
+ return tuple(advanced_groups)
352
+
353
+ @staticmethod
354
+ def _advance(
355
+ images: tuple[tuple[torch.Tensor, ...], ...],
356
+ velocities: tuple[tuple[torch.Tensor, ...], ...],
357
+ step_sizes: torch.Tensor,
358
+ *,
359
+ fixed_main: bool = False,
360
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
361
+ advanced_groups = []
362
+ flat_image_id = 0
363
+ for image_group, velocity_group in zip(images, velocities):
364
+ advanced_group = []
365
+ for image_id, (image, velocity) in enumerate(zip(image_group, velocity_group)):
366
+ step_size = step_sizes[flat_image_id].to(dtype=image.dtype)
367
+ advanced_group.append(
368
+ image if fixed_main and image_id == 0 else image + step_size * velocity
369
+ )
370
+ flat_image_id += 1
371
+ advanced_groups.append(tuple(advanced_group))
372
+ return tuple(advanced_groups)
373
+
374
+ @staticmethod
375
+ def _average_velocities(
376
+ first: tuple[tuple[torch.Tensor, ...], ...], second: tuple[tuple[torch.Tensor, ...], ...]
377
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
378
+ return tuple(
379
+ (
380
+ tuple(
381
+ (
382
+ 0.5 * (first_velocity + second_velocity)
383
+ for (first_velocity, second_velocity) in zip(first_group, second_group)
384
+ )
385
+ )
386
+ for (first_group, second_group) in zip(first, second)
387
+ )
388
+ )
389
+
390
+ @torch.no_grad()
391
+ def forward(
392
+ self,
393
+ conditional: None,
394
+ unconditional: None,
395
+ geometries: Sequence[GroupGeometry],
396
+ *,
397
+ generator: torch.Generator | Sequence[torch.Generator] | None = None,
398
+ main_conditions: Sequence[torch.Tensor] | None = None,
399
+ glyph_conditions: Sequence[Sequence[GlyphCondition | None]] | None = None,
400
+ progress_callback: Callable[[int, int], None] | None = None,
401
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
402
+ """Generate all images in every group from independent Gaussian noise."""
403
+ self._validate_inputs(conditional, unconditional, geometries, generator, main_conditions)
404
+ images = self._initial_noise(geometries, conditional, generator, main_conditions)
405
+ timestep_runs = []
406
+ effective_steps = []
407
+ for step_index in range(self.steps):
408
+ if progress_callback is not None:
409
+ progress_callback(step_index, self.steps)
410
+ time_value = step_index / self.steps
411
+ runtime_tensor = main_conditions[0]
412
+ main_timesteps = torch.full(
413
+ (len(geometries),), time_value, device=runtime_tensor.device, dtype=torch.float32
414
+ )
415
+ timesteps = self.timestep_schedule(main_timesteps, geometries)
416
+ next_time_value = (step_index + 1) / self.steps
417
+ next_main_timesteps = torch.full(
418
+ (len(geometries),),
419
+ next_time_value,
420
+ device=runtime_tensor.device,
421
+ dtype=torch.float32,
422
+ )
423
+ next_timesteps = self.timestep_schedule(next_main_timesteps, geometries)
424
+ step_sizes = next_timesteps.images - timesteps.images
425
+ timestep_runs.extend((timesteps, next_timesteps))
426
+ effective_steps.append(step_sizes)
427
+ prediction = self._guided_prediction(
428
+ images,
429
+ conditional,
430
+ unconditional,
431
+ geometries,
432
+ timesteps,
433
+ time_value,
434
+ glyph_conditions,
435
+ )
436
+ if self.solver == "euler" or step_index == self.steps - 1:
437
+ images = self._advance_toward_predictions(
438
+ images,
439
+ prediction,
440
+ timesteps,
441
+ next_timesteps,
442
+ fixed_main=self.timestep_schedule.is_native_edit,
443
+ )
444
+ continue
445
+ euler_prediction = self._advance_toward_predictions(
446
+ images,
447
+ prediction,
448
+ timesteps,
449
+ next_timesteps,
450
+ fixed_main=self.timestep_schedule.is_native_edit,
451
+ )
452
+ endpoint_prediction = self._guided_prediction(
453
+ euler_prediction,
454
+ conditional,
455
+ unconditional,
456
+ geometries,
457
+ next_timesteps,
458
+ next_time_value,
459
+ glyph_conditions,
460
+ )
461
+ velocity = self._velocities_from_predictions(
462
+ images, prediction, timesteps, fixed_main=self.timestep_schedule.is_native_edit
463
+ )
464
+ endpoint_velocity = self._velocities_from_predictions(
465
+ euler_prediction,
466
+ endpoint_prediction,
467
+ next_timesteps,
468
+ fixed_main=self.timestep_schedule.is_native_edit,
469
+ )
470
+ images = self._advance(
471
+ images,
472
+ self._average_velocities(velocity, endpoint_velocity),
473
+ step_sizes,
474
+ fixed_main=self.timestep_schedule.is_native_edit,
475
+ )
476
+ if progress_callback is not None:
477
+ progress_callback(self.steps, self.steps)
478
+ self.last_timestep_stats = TimestepRangeStats.from_timesteps(
479
+ timestep_runs, effective_steps=effective_steps
480
+ )
481
+ return images
vendor/pixtext/sampling/meanflow.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+
6
+ import torch
7
+ from torch import nn
8
+
9
+ from pixtext.modeling.patches import pack_image_groups
10
+ from pixtext.sampling.flow import GroupFlowSampler
11
+ from pixtext.sampling.timesteps import GroupTimesteps, TimestepRangeStats
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class GroupMeanFlowCondition:
16
+ """Validated start/end schedule expansion in packed image order."""
17
+
18
+ start: GroupTimesteps
19
+ end: GroupTimesteps
20
+ image_intervals: torch.Tensor
21
+
22
+ @classmethod
23
+ def from_timesteps(
24
+ cls, start: GroupTimesteps, end: GroupTimesteps, *, validate_values: bool = True
25
+ ) -> GroupMeanFlowCondition:
26
+ if start.images_per_sample != end.images_per_sample:
27
+ raise ValueError("MeanFlow endpoint topology must match")
28
+ if start.images.shape != end.images.shape:
29
+ raise ValueError("MeanFlow endpoint image vectors must match")
30
+ intervals = end.images - start.images
31
+ if validate_values:
32
+ valid = torch.isfinite(intervals).all() & (intervals >= 0).all()
33
+ offset = 0
34
+ for image_count in start.images_per_sample:
35
+ group = intervals[offset : offset + image_count]
36
+ valid = valid & (group[0] == 0)
37
+ if image_count > 2:
38
+ valid = valid & (group[1:] == group[1]).all()
39
+ offset += image_count
40
+ torch._assert_async(valid, "invalid MeanFlow image intervals")
41
+ return cls(start=start, end=end, image_intervals=intervals)
42
+
43
+
44
+ class GroupMeanFlowSampler(GroupFlowSampler):
45
+ """Uniform clean-grid Euler sampler for interval-aware MeanFlow students."""
46
+
47
+ def __init__(
48
+ self,
49
+ model: nn.Module,
50
+ *,
51
+ cfg_embedding_scale: float | None = None,
52
+ cfg_all_steps: bool = False,
53
+ progress_grid: tuple[float, ...] | None = None,
54
+ **kwargs,
55
+ ) -> None:
56
+ if not isinstance(cfg_all_steps, bool):
57
+ raise TypeError("cfg_all_steps must be a bool")
58
+ if cfg_all_steps and cfg_embedding_scale is not None:
59
+ raise ValueError("cfg_all_steps is only for two-branch CFG")
60
+ if cfg_embedding_scale is not None and (
61
+ not math.isfinite(cfg_embedding_scale) or not getattr(model, "cfg_conditioning", False)
62
+ ):
63
+ raise ValueError(
64
+ "cfg_embedding_scale requires a CFG-conditioned model and finite value"
65
+ )
66
+ self.cfg_embedding_scale = cfg_embedding_scale
67
+ self.cfg_all_steps = cfg_all_steps
68
+ kwargs.pop("solver", None)
69
+ super().__init__(model, solver="euler", **kwargs)
70
+ if progress_grid is not None:
71
+ if (
72
+ not isinstance(progress_grid, tuple)
73
+ or len(progress_grid) != self.steps + 1
74
+ or any(
75
+ isinstance(value, bool)
76
+ or not isinstance(value, (int, float))
77
+ or (not math.isfinite(value))
78
+ for value in progress_grid
79
+ )
80
+ or (progress_grid[0] != 0)
81
+ or (progress_grid[-1] != 1)
82
+ or any(left >= right for (left, right) in zip(progress_grid, progress_grid[1:]))
83
+ ):
84
+ raise ValueError(
85
+ "progress_grid must strictly increase from 0 to 1 with steps + 1 points"
86
+ )
87
+ progress_grid = tuple(float(value) for value in progress_grid)
88
+ self.progress_grid = progress_grid
89
+ if not getattr(model, "interval_conditioning", False):
90
+ raise ValueError("MeanFlow sampler requires an interval-aware student")
91
+ self.solver = "meanflow_interval_euler"
92
+
93
+ def _interval_prediction(self, images, prompts, geometries, start, end, glyph_conditions=None):
94
+ packed = pack_image_groups(images, patch_size=self.patch_size)
95
+ condition = GroupMeanFlowCondition.from_timesteps(start, end)
96
+ return self.model(
97
+ packed_images=packed,
98
+ prompts=prompts,
99
+ geometries=geometries,
100
+ timesteps=start,
101
+ image_intervals=condition.image_intervals,
102
+ **{"cfg_scales": start.main.new_full((len(images),), self.cfg_embedding_scale)}
103
+ if self.cfg_embedding_scale is not None
104
+ else {},
105
+ glyph_conditions=glyph_conditions,
106
+ )
107
+
108
+ def _guided_interval_prediction(
109
+ self, images, conditional, unconditional, geometries, start, end, c, glyph_conditions=None
110
+ ):
111
+ conditional_prediction = self._interval_prediction(
112
+ images, conditional, geometries, start, end, glyph_conditions
113
+ )
114
+ (low, high) = self.cfg_interval
115
+ scale = self.cfg_scale if self.cfg_all_steps or low < c < high else 1.0
116
+ if scale == 1.0:
117
+ return conditional_prediction
118
+ unconditional_prediction = self._interval_prediction(
119
+ images,
120
+ unconditional,
121
+ geometries,
122
+ start,
123
+ end,
124
+ None
125
+ if glyph_conditions is None
126
+ else tuple((None,) * len(group) for group in glyph_conditions),
127
+ )
128
+ return tuple(
129
+ tuple(
130
+ uncond + scale * (cond - uncond) for (cond, uncond) in zip(cond_group, uncond_group)
131
+ )
132
+ for (cond_group, uncond_group) in zip(conditional_prediction, unconditional_prediction)
133
+ )
134
+
135
+ @torch.no_grad()
136
+ def forward(
137
+ self,
138
+ conditional,
139
+ unconditional,
140
+ geometries,
141
+ *,
142
+ generator=None,
143
+ main_conditions=None,
144
+ glyph_conditions=None,
145
+ progress_callback=None,
146
+ ):
147
+ self._validate_inputs(conditional, unconditional, geometries, generator, main_conditions)
148
+ images = self._initial_noise(geometries, conditional, generator, main_conditions)
149
+ runtime_tensor = conditional.tokens if conditional is not None else main_conditions[0]
150
+ timestep_runs = []
151
+ effective_steps = []
152
+ for step_index in range(self.steps):
153
+ if progress_callback is not None:
154
+ progress_callback(step_index, self.steps)
155
+ c = (
156
+ self.progress_grid[step_index]
157
+ if self.progress_grid is not None
158
+ else step_index / self.steps
159
+ )
160
+ q = (
161
+ self.progress_grid[step_index + 1]
162
+ if self.progress_grid is not None
163
+ else (step_index + 1) / self.steps
164
+ )
165
+ start_progress = torch.full((len(geometries),), c, device=runtime_tensor.device)
166
+ end_progress = torch.full((len(geometries),), q, device=runtime_tensor.device)
167
+ start = self.timestep_schedule(start_progress, geometries)
168
+ end = self.timestep_schedule(end_progress, geometries)
169
+ prediction = self._guided_interval_prediction(
170
+ images, conditional, unconditional, geometries, start, end, c, glyph_conditions
171
+ )
172
+ velocities = self._velocities_from_predictions(
173
+ images, prediction, start, fixed_main=True
174
+ )
175
+ step_sizes = end.images - start.images
176
+ images = self._advance(images, velocities, step_sizes, fixed_main=True)
177
+ timestep_runs.extend((start, end))
178
+ effective_steps.append(step_sizes)
179
+ if progress_callback is not None:
180
+ progress_callback(self.steps, self.steps)
181
+ self.last_timestep_stats = TimestepRangeStats.from_timesteps(
182
+ timestep_runs, effective_steps=effective_steps
183
+ )
184
+ return images
vendor/pixtext/sampling/timesteps.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+ import torch
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class TimestepRangeStats:
12
+ """Device-independent ranges for timestep observability."""
13
+
14
+ main_min: float
15
+ main_max: float
16
+ item_min: float | None
17
+ item_max: float | None
18
+ shift_mu_min: float
19
+ shift_mu_max: float
20
+ effective_step_min: float | None = None
21
+ effective_step_max: float | None = None
22
+
23
+ @classmethod
24
+ def merge(cls, values: Sequence[TimestepRangeStats]) -> TimestepRangeStats:
25
+ """Merge already-synchronized ranges without device work."""
26
+ if not values:
27
+ raise ValueError("at least one TimestepRangeStats value is required")
28
+
29
+ def optional_min(name: str) -> float | None:
30
+ present = [getattr(value, name) for value in values]
31
+ finite = [value for value in present if value is not None]
32
+ return min(finite) if finite else None
33
+
34
+ def optional_max(name: str) -> float | None:
35
+ present = [getattr(value, name) for value in values]
36
+ finite = [value for value in present if value is not None]
37
+ return max(finite) if finite else None
38
+
39
+ return cls(
40
+ main_min=min(value.main_min for value in values),
41
+ main_max=max(value.main_max for value in values),
42
+ item_min=optional_min("item_min"),
43
+ item_max=optional_max("item_max"),
44
+ shift_mu_min=min(value.shift_mu_min for value in values),
45
+ shift_mu_max=max(value.shift_mu_max for value in values),
46
+ effective_step_min=optional_min("effective_step_min"),
47
+ effective_step_max=optional_max("effective_step_max"),
48
+ )
49
+
50
+ @classmethod
51
+ def from_timesteps(
52
+ cls, values: Sequence[GroupTimesteps], *, effective_steps: Sequence[torch.Tensor] = ()
53
+ ) -> TimestepRangeStats:
54
+ """Summarize one or more schedule evaluations with one device sync."""
55
+ if len(values) == 0:
56
+ raise ValueError("at least one GroupTimesteps value is required")
57
+ main = torch.cat(tuple(value.main for value in values))
58
+ shift_mu = torch.cat(tuple(value.shift_mu for value in values))
59
+ item_parts = tuple(
60
+ item_values for value in values if (item_values := value.item_values()) is not None
61
+ )
62
+ items = torch.cat(item_parts) if item_parts else None
63
+ steps = torch.cat(tuple(effective_steps)) if effective_steps else None
64
+ missing = main.new_full((), math.nan)
65
+ summary_parts = [main.min(), main.max()]
66
+ summary_parts.extend((items.min(), items.max()) if items is not None else (missing,) * 2)
67
+ summary_parts.extend((shift_mu.min(), shift_mu.max()))
68
+ summary_parts.extend((steps.min(), steps.max()) if steps is not None else (missing,) * 2)
69
+ summary = torch.stack(summary_parts).detach().cpu().tolist()
70
+ required_indices = [0, 1, 4, 5]
71
+ if items is not None:
72
+ required_indices.extend((2, 3))
73
+ if steps is not None:
74
+ required_indices.extend((6, 7))
75
+ if any(not math.isfinite(summary[index]) for index in required_indices):
76
+ raise FloatingPointError("timestep range contains non-finite values")
77
+
78
+ def optional(value: float) -> float | None:
79
+ return None if math.isnan(value) else value
80
+
81
+ return cls(
82
+ main_min=summary[0],
83
+ main_max=summary[1],
84
+ item_min=optional(summary[2]),
85
+ item_max=optional(summary[3]),
86
+ shift_mu_min=summary[4],
87
+ shift_mu_max=summary[5],
88
+ effective_step_min=optional(summary[6]),
89
+ effective_step_max=optional(summary[7]),
90
+ )
91
+
92
+ def to_dict(self) -> dict[str, float | None]:
93
+ """Return stable field names for logs and sampling metadata."""
94
+ return {
95
+ "main_min": self.main_min,
96
+ "main_max": self.main_max,
97
+ "item_min": self.item_min,
98
+ "item_max": self.item_max,
99
+ "shift_mu_min": self.shift_mu_min,
100
+ "shift_mu_max": self.shift_mu_max,
101
+ "effective_step_min": self.effective_step_min,
102
+ "effective_step_max": self.effective_step_max,
103
+ }
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class GroupTimesteps:
108
+ """Main and effective image timesteps in stable packed-image order."""
109
+
110
+ main: torch.Tensor
111
+ images: torch.Tensor
112
+ images_per_sample: tuple[int, ...]
113
+ shift_mu: torch.Tensor
114
+
115
+ @property
116
+ def applied_shift_mu(self) -> torch.Tensor:
117
+ """Policy-neutral name for the exponent shift used by the schedule."""
118
+ return self.shift_mu
119
+
120
+ def item_values(self) -> torch.Tensor | None:
121
+ """Return packed item-only timesteps, or None for main-only batches."""
122
+ item_parts = []
123
+ offset = 0
124
+ for image_count in self.images_per_sample:
125
+ if image_count > 1:
126
+ item_parts.append(self.images[offset + 1 : offset + image_count])
127
+ offset += image_count
128
+ return torch.cat(item_parts) if item_parts else None
129
+
130
+ def range_stats(self, *, effective_steps: torch.Tensor | None = None) -> TimestepRangeStats:
131
+ """Synchronize compact timestep ranges for logs and metadata."""
132
+ if effective_steps is not None:
133
+ if effective_steps.shape != self.images.shape:
134
+ raise ValueError(
135
+ f"effective_steps must have one value per image, got {tuple(effective_steps.shape)} and {tuple(self.images.shape)}"
136
+ )
137
+ return TimestepRangeStats.from_timesteps(
138
+ (self,), effective_steps=(effective_steps,) if effective_steps is not None else ()
139
+ )
140
+
141
+
142
+ class GroupTimestepSchedule:
143
+ """Keep main context at t=1 and advance items on an unshifted grid."""
144
+
145
+ is_native_edit = True
146
+
147
+ def __init__(self, patch_size=16):
148
+ self.patch_size = patch_size
149
+
150
+ def __call__(self, group_progress, geometries, *, validate_values=True):
151
+ if group_progress.shape != (len(geometries),) or not geometries:
152
+ raise ValueError("Need one progress value per image group")
153
+ progress = group_progress.float()
154
+ if validate_values and not bool(
155
+ (torch.isfinite(progress) & (progress >= 0) & (progress <= 1)).all()
156
+ ):
157
+ raise ValueError("Progress must be finite and within [0, 1]")
158
+ counts = tuple(1 + len(g.items) for g in geometries)
159
+ parts = []
160
+ for index, count in enumerate(counts):
161
+ parts.extend(
162
+ (torch.ones_like(progress[index : index + 1]), progress[index].expand(count - 1))
163
+ )
164
+ return GroupTimesteps(
165
+ torch.ones_like(progress), torch.cat(parts), counts, torch.zeros_like(progress)
166
+ )
167
+
168
+
169
+ def resolve_group_timestep_schedule(schedule, *, patch_size):
170
+ if schedule is None:
171
+ return GroupTimestepSchedule(patch_size)
172
+ if schedule.patch_size != patch_size:
173
+ raise ValueError("Schedule and model patch sizes must match")
174
+ return schedule