lsmpp commited on
Commit
077bde8
·
verified ·
1 Parent(s): 9c1bf9c

Add PixText transformer and attention modules

Browse files
vendor/pixtext/modeling/__init__.py ADDED
File without changes
vendor/pixtext/modeling/attention.py ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import Sequence
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar
7
+
8
+ import torch
9
+ from torch import nn
10
+ from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
11
+
12
+ from pixtext.modeling.conditioning import (
13
+ IMAGE_TOKEN_TYPE,
14
+ PROMPT_TOKEN_TYPE,
15
+ TIME_TOKEN_TYPE,
16
+ PackedGroupSequence,
17
+ )
18
+ from pixtext.modeling.modes import ImageAttentionRoutingMode, TimestepTokenRoutingMode
19
+ from pixtext.modeling.patches import PackedImagePatches
20
+ from pixtext.modeling.rope import (
21
+ DEFAULT_ROPE_AXES_DIMS,
22
+ DEFAULT_ROPE_THETA,
23
+ RotaryEmbedding3D,
24
+ _apply_rotary_factors,
25
+ )
26
+
27
+ _compiled_flex_attention = torch.compile(flex_attention, dynamic=True)
28
+ _FORCE_STRICT_FP32_FLEX_ATTENTION = ContextVar("force_strict_fp32_flex_attention", default=False)
29
+ _FLEX_ATTENTION_FORWARD_NUM_STAGES = ContextVar("flex_attention_forward_num_stages", default=None)
30
+ _INTERREAD = ImageAttentionRoutingMode.ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM
31
+ _INTERREAD_BIDIRECTIONAL_MAIN = (
32
+ ImageAttentionRoutingMode.ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM_BIDIRECTIONAL_MAIN
33
+ )
34
+
35
+
36
+ @contextmanager
37
+ def force_strict_fp32_flex_attention():
38
+ """Keep FP32 FlexAttention strict inside a broader TF32 forward policy."""
39
+ token = _FORCE_STRICT_FP32_FLEX_ATTENTION.set(True)
40
+ try:
41
+ yield
42
+ finally:
43
+ _FORCE_STRICT_FP32_FLEX_ATTENTION.reset(token)
44
+
45
+
46
+ @contextmanager
47
+ def use_flex_attention_forward_num_stages(num_stages: int):
48
+ """Select a scoped Triton forward pipeline depth for FlexAttention."""
49
+ if isinstance(num_stages, bool) or not isinstance(num_stages, int) or num_stages <= 0:
50
+ raise ValueError("FlexAttention num_stages must be a positive integer")
51
+ token = _FLEX_ATTENTION_FORWARD_NUM_STAGES.set(num_stages)
52
+ try:
53
+ yield
54
+ finally:
55
+ _FLEX_ATTENTION_FORWARD_NUM_STAGES.reset(token)
56
+
57
+
58
+ def _normalize_and_apply_rotary(
59
+ values: torch.Tensor, weight: torch.Tensor, eps: float, cosine: torch.Tensor, sine: torch.Tensor
60
+ ) -> torch.Tensor:
61
+ """Run Q/K RMSNorm and precomputed RoPE as one compilable subgraph."""
62
+ output_dtype = values.dtype
63
+ variance = values.float().square().mean(dim=-1, keepdim=True)
64
+ normalized = values.float() * torch.rsqrt(variance + eps)
65
+ normalized = (normalized * weight.float()).to(output_dtype)
66
+ return _apply_rotary_factors(normalized, cosine, sine)
67
+
68
+
69
+ _compiled_normalize_and_apply_rotary = torch.compile(_normalize_and_apply_rotary, dynamic=True)
70
+
71
+
72
+ class _RMSNorm(nn.Module):
73
+ """Last-dimension RMSNorm with FP32 reduction statistics."""
74
+
75
+ def __init__(self, hidden_size: int, eps: float = 1e-06) -> None:
76
+ super().__init__()
77
+ self.eps = float(eps)
78
+ self.weight = nn.Parameter(torch.ones(hidden_size))
79
+
80
+ def forward(self, values: torch.Tensor) -> torch.Tensor:
81
+ output_dtype = values.dtype
82
+ variance = values.float().square().mean(dim=-1, keepdim=True)
83
+ normalized = values.float() * torch.rsqrt(variance + self.eps)
84
+ return (normalized * self.weight.float()).to(output_dtype)
85
+
86
+
87
+ def _validate_routing(
88
+ *,
89
+ sample_ids: torch.Tensor,
90
+ image_ids: torch.Tensor,
91
+ token_types: torch.Tensor,
92
+ timestep_roles: torch.Tensor,
93
+ branch_kinds: torch.Tensor,
94
+ slot_ids: torch.Tensor,
95
+ token_count: int,
96
+ device: torch.device,
97
+ ) -> None:
98
+ for name, values in (
99
+ ("sample_ids", sample_ids),
100
+ ("image_ids", image_ids),
101
+ ("token_types", token_types),
102
+ ("timestep_roles", timestep_roles),
103
+ ("branch_kinds", branch_kinds),
104
+ ("slot_ids", slot_ids),
105
+ ):
106
+ if not isinstance(values, torch.Tensor):
107
+ raise TypeError(f"routing {name} must be a torch.Tensor")
108
+ if values.shape != (token_count,):
109
+ raise ValueError(
110
+ f"routing {name} must contain {token_count} entries, got shape {tuple(values.shape)}"
111
+ )
112
+ if values.dtype != torch.int64:
113
+ raise ValueError(f"routing {name} must use torch.int64, got {values.dtype}")
114
+ if values.device != device:
115
+ raise ValueError(f"routing {name} must be on {device}, got {values.device}")
116
+ valid_token_types = (
117
+ (token_types == IMAGE_TOKEN_TYPE)
118
+ | (token_types == PROMPT_TOKEN_TYPE)
119
+ | (token_types == TIME_TOKEN_TYPE)
120
+ )
121
+ if not bool(valid_token_types.all()):
122
+ raise ValueError("routing token_types may only contain image, prompt, or timestep IDs")
123
+ valid_timestep_roles = (timestep_roles == 0) | (timestep_roles == 1)
124
+ if not bool(valid_timestep_roles.all()):
125
+ raise ValueError("routing timestep_roles may only contain 0 or 1")
126
+ if not bool(((branch_kinds >= -1) & (branch_kinds <= 2)).all()):
127
+ raise ValueError("routing branch_kinds may only contain -1, 0, 1, or 2")
128
+ if not bool((slot_ids >= -1).all()):
129
+ raise ValueError("routing slot_ids must be at least -1")
130
+
131
+
132
+ def _validated_routing(
133
+ packed: PackedGroupSequence | PackedImagePatches,
134
+ ) -> tuple[
135
+ torch.Tensor,
136
+ torch.Tensor,
137
+ torch.Tensor,
138
+ torch.Tensor,
139
+ int,
140
+ torch.device,
141
+ TimestepTokenRoutingMode | None,
142
+ torch.Tensor,
143
+ torch.Tensor,
144
+ bool,
145
+ ]:
146
+ if isinstance(packed, PackedGroupSequence):
147
+ sample_ids = packed.sample_ids
148
+ image_ids = packed.image_ids
149
+ token_types = packed.token_types
150
+ timestep_roles = packed.timestep_roles
151
+ token_count = packed.tokens.shape[0]
152
+ device = packed.tokens.device
153
+ timestep_token_routing = packed.timestep_token_routing
154
+ explicit_branches = packed.branch_kinds is not None
155
+ if explicit_branches:
156
+ assert packed.branch_kinds is not None
157
+ assert packed.slot_ids is not None
158
+ branch_kinds = packed.branch_kinds
159
+ slot_ids = packed.slot_ids
160
+ else:
161
+ branch_kinds = torch.where(
162
+ image_ids.eq(0), torch.zeros_like(image_ids), torch.ones_like(image_ids)
163
+ )
164
+ slot_ids = torch.where(image_ids.gt(0), image_ids - 1, -1)
165
+ elif isinstance(packed, PackedImagePatches):
166
+ sample_ids = packed.sample_ids
167
+ image_ids = packed.image_ids
168
+ token_types = torch.full_like(image_ids, IMAGE_TOKEN_TYPE)
169
+ timestep_roles = image_ids.gt(0).to(torch.int64)
170
+ token_count = packed.patches.shape[0]
171
+ device = packed.patches.device
172
+ timestep_token_routing = None
173
+ explicit_branches = False
174
+ branch_kinds = torch.where(
175
+ image_ids.eq(0), torch.zeros_like(image_ids), torch.ones_like(image_ids)
176
+ )
177
+ slot_ids = torch.where(image_ids.gt(0), image_ids - 1, -1)
178
+ else:
179
+ raise TypeError(
180
+ f"packed must be PackedGroupSequence or PackedImagePatches, got {type(packed).__name__}"
181
+ )
182
+ _validate_routing(
183
+ sample_ids=sample_ids,
184
+ image_ids=image_ids,
185
+ token_types=token_types,
186
+ timestep_roles=timestep_roles,
187
+ branch_kinds=branch_kinds,
188
+ slot_ids=slot_ids,
189
+ token_count=token_count,
190
+ device=device,
191
+ )
192
+ contains_time_tokens = bool((token_types == TIME_TOKEN_TYPE).any())
193
+ if contains_time_tokens != (timestep_token_routing is not None):
194
+ raise ValueError(
195
+ "timestep token routing must be present exactly when timestep tokens are packed"
196
+ )
197
+ if timestep_token_routing is not None:
198
+ TimestepTokenRoutingMode.require(timestep_token_routing)
199
+ if timestep_token_routing is TimestepTokenRoutingMode.PER_IMAGE_CONTEXTUAL and bool(
200
+ (image_ids[token_types == TIME_TOKEN_TYPE] < 0).any()
201
+ ):
202
+ raise ValueError("per-image contextual timestep tokens require image IDs")
203
+ return (
204
+ sample_ids,
205
+ image_ids,
206
+ token_types,
207
+ timestep_roles,
208
+ token_count,
209
+ device,
210
+ timestep_token_routing,
211
+ branch_kinds,
212
+ slot_ids,
213
+ explicit_branches,
214
+ )
215
+
216
+
217
+ def _allowed_image_pair(
218
+ *,
219
+ query_image_ids: torch.Tensor,
220
+ key_image_ids: torch.Tensor,
221
+ query_token_types: torch.Tensor,
222
+ key_token_types: torch.Tensor,
223
+ image_attention_routing: ImageAttentionRoutingMode,
224
+ ) -> torch.Tensor:
225
+ allowed = (query_token_types == IMAGE_TOKEN_TYPE) & (key_token_types == IMAGE_TOKEN_TYPE)
226
+ if image_attention_routing is ImageAttentionRoutingMode.ALL_IMAGES:
227
+ return allowed
228
+ if image_attention_routing is ImageAttentionRoutingMode.MAIN_IMAGE_HUB:
229
+ return allowed & ((query_image_ids == 0) | (key_image_ids == 0))
230
+ if image_attention_routing is ImageAttentionRoutingMode.ITEMS_READ_MAIN:
231
+ return allowed & (query_image_ids > 0) & (key_image_ids == 0)
232
+ if image_attention_routing in (_INTERREAD, _INTERREAD_BIDIRECTIONAL_MAIN):
233
+ if image_attention_routing is _INTERREAD_BIDIRECTIONAL_MAIN:
234
+ return allowed & ((query_image_ids > 0) | (key_image_ids > 0))
235
+ return allowed & (query_image_ids > 0)
236
+ raise AssertionError(f"unhandled image attention routing: {image_attention_routing!r}")
237
+
238
+
239
+ def _allowed_routing_pair(
240
+ *,
241
+ query_image_ids: torch.Tensor,
242
+ key_image_ids: torch.Tensor,
243
+ query_token_types: torch.Tensor,
244
+ key_token_types: torch.Tensor,
245
+ query_timestep_roles: torch.Tensor,
246
+ key_timestep_roles: torch.Tensor,
247
+ image_attention_routing: ImageAttentionRoutingMode,
248
+ per_image_contextual_time: bool,
249
+ query_branch_kinds: torch.Tensor,
250
+ key_branch_kinds: torch.Tensor,
251
+ query_slot_ids: torch.Tensor,
252
+ key_slot_ids: torch.Tensor,
253
+ explicit_branches: bool,
254
+ ) -> torch.Tensor:
255
+ query_is_time = query_token_types == TIME_TOKEN_TYPE
256
+ key_is_time = key_token_types == TIME_TOKEN_TYPE
257
+ content_pair = ~query_is_time & ~key_is_time
258
+ same_image = query_image_ids == key_image_ids
259
+ if explicit_branches:
260
+ query_is_image = query_token_types == IMAGE_TOKEN_TYPE
261
+ key_is_image = key_token_types == IMAGE_TOKEN_TYPE
262
+ image_pair = query_is_image & key_is_image
263
+ if image_attention_routing is ImageAttentionRoutingMode.ALL_IMAGES:
264
+ allowed_image_pair = image_pair
265
+ elif image_attention_routing is ImageAttentionRoutingMode.MAIN_IMAGE_HUB:
266
+ allowed_image_pair = image_pair & (query_branch_kinds.eq(0) | key_branch_kinds.eq(0))
267
+ elif image_attention_routing is ImageAttentionRoutingMode.ITEMS_READ_MAIN:
268
+ item_reads_main = image_pair & query_branch_kinds.eq(1) & key_branch_kinds.eq(0)
269
+ item_reads_own_glyph = (
270
+ image_pair
271
+ & query_branch_kinds.eq(1)
272
+ & key_branch_kinds.eq(2)
273
+ & query_slot_ids.eq(key_slot_ids)
274
+ )
275
+ allowed_image_pair = item_reads_main | item_reads_own_glyph
276
+ elif image_attention_routing in (_INTERREAD, _INTERREAD_BIDIRECTIONAL_MAIN):
277
+ item_reads_main = image_pair & query_branch_kinds.eq(1) & key_branch_kinds.eq(0)
278
+ item_reads_items = image_pair & query_branch_kinds.eq(1) & key_branch_kinds.eq(1)
279
+ item_reads_own_glyph = (
280
+ image_pair
281
+ & query_branch_kinds.eq(1)
282
+ & key_branch_kinds.eq(2)
283
+ & query_slot_ids.eq(key_slot_ids)
284
+ )
285
+ glyph_reads_own_item = (
286
+ image_pair
287
+ & query_branch_kinds.eq(2)
288
+ & key_branch_kinds.eq(1)
289
+ & query_slot_ids.eq(key_slot_ids)
290
+ )
291
+ allowed_image_pair = (
292
+ item_reads_main | item_reads_items | item_reads_own_glyph | glyph_reads_own_item
293
+ )
294
+ if image_attention_routing is _INTERREAD_BIDIRECTIONAL_MAIN:
295
+ allowed_image_pair = allowed_image_pair | image_pair & query_branch_kinds.eq(
296
+ 0
297
+ ) & key_branch_kinds.eq(1)
298
+ else:
299
+ raise AssertionError(f"unhandled image attention routing: {image_attention_routing!r}")
300
+ else:
301
+ allowed_image_pair = _allowed_image_pair(
302
+ query_image_ids=query_image_ids,
303
+ key_image_ids=key_image_ids,
304
+ query_token_types=query_token_types,
305
+ key_token_types=key_token_types,
306
+ image_attention_routing=image_attention_routing,
307
+ )
308
+ if per_image_contextual_time:
309
+ reads_own_time_bank = key_is_time & same_image
310
+ time_reads_own_image = query_is_time & (key_token_types == IMAGE_TOKEN_TYPE) & same_image
311
+ else:
312
+ same_timestep_role = query_timestep_roles == key_timestep_roles
313
+ reads_own_time_bank = key_is_time & same_timestep_role
314
+ time_reads_own_image = torch.zeros_like(reads_own_time_bank)
315
+ return (
316
+ content_pair & (same_image | allowed_image_pair)
317
+ | reads_own_time_bank
318
+ | time_reads_own_image
319
+ )
320
+
321
+
322
+ def build_dense_attention_mask(
323
+ packed: PackedGroupSequence | PackedImagePatches,
324
+ *,
325
+ image_attention_routing: ImageAttentionRoutingMode = ImageAttentionRoutingMode.ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM_BIDIRECTIONAL_MAIN,
326
+ ) -> torch.Tensor:
327
+ """Return a ``[query, key]`` bool mask where ``True`` means allowed."""
328
+ routing = ImageAttentionRoutingMode.require(image_attention_routing)
329
+ (
330
+ sample_ids,
331
+ image_ids,
332
+ token_types,
333
+ timestep_roles,
334
+ _,
335
+ _,
336
+ timestep_token_routing,
337
+ branch_kinds,
338
+ slot_ids,
339
+ explicit_branches,
340
+ ) = _validated_routing(packed)
341
+ same_group = sample_ids[:, None] == sample_ids[None, :]
342
+ allowed_pair = _allowed_routing_pair(
343
+ query_image_ids=image_ids[:, None],
344
+ key_image_ids=image_ids[None, :],
345
+ query_token_types=token_types[:, None],
346
+ key_token_types=token_types[None, :],
347
+ query_timestep_roles=timestep_roles[:, None],
348
+ key_timestep_roles=timestep_roles[None, :],
349
+ image_attention_routing=routing,
350
+ per_image_contextual_time=timestep_token_routing
351
+ is TimestepTokenRoutingMode.PER_IMAGE_CONTEXTUAL,
352
+ query_branch_kinds=branch_kinds[:, None],
353
+ key_branch_kinds=branch_kinds[None, :],
354
+ query_slot_ids=slot_ids[:, None],
355
+ key_slot_ids=slot_ids[None, :],
356
+ explicit_branches=explicit_branches,
357
+ )
358
+ return same_group & allowed_pair
359
+
360
+
361
+ def build_flex_attention_block_mask(
362
+ packed: PackedGroupSequence | PackedImagePatches,
363
+ *,
364
+ image_attention_routing: ImageAttentionRoutingMode = ImageAttentionRoutingMode.ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM_BIDIRECTIONAL_MAIN,
365
+ ) -> BlockMask:
366
+ """Build the packed routing mask consumed by PyTorch FlexAttention."""
367
+ routing = ImageAttentionRoutingMode.require(image_attention_routing)
368
+ (
369
+ sample_ids,
370
+ image_ids,
371
+ token_types,
372
+ timestep_roles,
373
+ token_count,
374
+ device,
375
+ timestep_token_routing,
376
+ branch_kinds,
377
+ slot_ids,
378
+ explicit_branches,
379
+ ) = _validated_routing(packed)
380
+ if token_count == 0:
381
+ raise ValueError("FlexAttention requires at least one packed token")
382
+ per_image_contextual_time = (
383
+ timestep_token_routing is TimestepTokenRoutingMode.PER_IMAGE_CONTEXTUAL
384
+ )
385
+
386
+ def mask_mod(
387
+ batch_index: torch.Tensor,
388
+ head_index: torch.Tensor,
389
+ query_index: torch.Tensor,
390
+ key_index: torch.Tensor,
391
+ ) -> torch.Tensor:
392
+ del batch_index, head_index
393
+ query_in_bounds = query_index < token_count
394
+ key_in_bounds = key_index < token_count
395
+ safe_query_index = query_index.clamp(max=token_count - 1)
396
+ safe_key_index = key_index.clamp(max=token_count - 1)
397
+ same_group = sample_ids[safe_query_index] == sample_ids[safe_key_index]
398
+ allowed_pair = _allowed_routing_pair(
399
+ query_image_ids=image_ids[safe_query_index],
400
+ key_image_ids=image_ids[safe_key_index],
401
+ query_token_types=token_types[safe_query_index],
402
+ key_token_types=token_types[safe_key_index],
403
+ query_timestep_roles=timestep_roles[safe_query_index],
404
+ key_timestep_roles=timestep_roles[safe_key_index],
405
+ image_attention_routing=routing,
406
+ per_image_contextual_time=per_image_contextual_time,
407
+ query_branch_kinds=branch_kinds[safe_query_index],
408
+ key_branch_kinds=branch_kinds[safe_key_index],
409
+ query_slot_ids=slot_ids[safe_query_index],
410
+ key_slot_ids=slot_ids[safe_key_index],
411
+ explicit_branches=explicit_branches,
412
+ )
413
+ return query_in_bounds & key_in_bounds & same_group & allowed_pair
414
+
415
+ return create_block_mask(
416
+ mask_mod, B=None, H=None, Q_LEN=token_count, KV_LEN=token_count, device=str(device)
417
+ )
418
+
419
+
420
+ def run_flex_attention(
421
+ query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, *, block_mask: BlockMask
422
+ ) -> torch.Tensor:
423
+ """Run packed CUDA self-attention without changing the model head dimension."""
424
+ if query.ndim != 4 or key.ndim != 4 or value.ndim != 4:
425
+ raise ValueError("query, key, and value must use (B, H, T, D) layout")
426
+ if query.shape != key.shape or query.shape != value.shape:
427
+ raise ValueError("packed self-attention requires identical query, key, and value shapes")
428
+ if query.device.type != "cuda":
429
+ raise RuntimeError("FlexAttention requires CUDA tensors")
430
+ force_strict = query.dtype is torch.float32 and _FORCE_STRICT_FP32_FLEX_ATTENTION.get()
431
+ old_matmul_tf32 = None
432
+ if force_strict:
433
+ old_matmul_tf32 = torch.backends.cuda.matmul.allow_tf32
434
+ torch.backends.cuda.matmul.allow_tf32 = False
435
+ kernel_options = {"FORCE_USE_FLEX_ATTENTION": True}
436
+ forward_num_stages = _FLEX_ATTENTION_FORWARD_NUM_STAGES.get()
437
+ if forward_num_stages is not None:
438
+ kernel_options["num_stages"] = forward_num_stages
439
+ try:
440
+ return _compiled_flex_attention(
441
+ query, key, value, block_mask=block_mask, kernel_options=kernel_options
442
+ )
443
+ finally:
444
+ if force_strict:
445
+ assert old_matmul_tf32 is not None
446
+ torch.backends.cuda.matmul.allow_tf32 = old_matmul_tf32
447
+
448
+
449
+ class GroupSelfAttention(nn.Module):
450
+ """QKV, QK normalization, 3D RoPE, and packed FlexAttention."""
451
+
452
+ def __init__(
453
+ self,
454
+ *,
455
+ hidden_size: int = 1280,
456
+ num_heads: int = 16,
457
+ axes_dims: Sequence[int] = DEFAULT_ROPE_AXES_DIMS,
458
+ theta: float = DEFAULT_ROPE_THETA,
459
+ proj_dropout: float = 0.0,
460
+ ) -> None:
461
+ super().__init__()
462
+ for name, value in (("hidden_size", hidden_size), ("num_heads", num_heads)):
463
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
464
+ raise ValueError(f"{name} must be a positive integer, got {value!r}")
465
+ if hidden_size % num_heads != 0:
466
+ raise ValueError(
467
+ f"hidden_size must be divisible by num_heads: got {hidden_size} and {num_heads}"
468
+ )
469
+ if (
470
+ isinstance(proj_dropout, bool)
471
+ or not isinstance(proj_dropout, (int, float))
472
+ or (not math.isfinite(proj_dropout))
473
+ or (not 0 <= proj_dropout < 1)
474
+ ):
475
+ raise ValueError(f"proj_dropout must be finite and in [0, 1), got {proj_dropout!r}")
476
+ self.hidden_size = hidden_size
477
+ self.num_heads = num_heads
478
+ self.head_dim = hidden_size // num_heads
479
+ self.rotary = RotaryEmbedding3D(axes_dims=axes_dims, theta=theta)
480
+ if self.rotary.head_dim != self.head_dim:
481
+ raise ValueError(
482
+ f"axes_dims must sum to the attention head dimension: got {self.rotary.head_dim}, expected {self.head_dim}"
483
+ )
484
+ self.q_norm = _RMSNorm(self.head_dim)
485
+ self.k_norm = _RMSNorm(self.head_dim)
486
+ self.qkv = nn.Linear(hidden_size, 3 * hidden_size, bias=True)
487
+ self.proj = nn.Linear(hidden_size, hidden_size, bias=True)
488
+ self.proj_dropout = nn.Dropout(float(proj_dropout))
489
+ self.reset_parameters()
490
+
491
+ def reset_parameters(self) -> None:
492
+ nn.init.ones_(self.q_norm.weight)
493
+ nn.init.ones_(self.k_norm.weight)
494
+ nn.init.xavier_uniform_(self.qkv.weight)
495
+ nn.init.zeros_(self.qkv.bias)
496
+ nn.init.xavier_uniform_(self.proj.weight)
497
+ nn.init.zeros_(self.proj.bias)
498
+
499
+ def forward(
500
+ self,
501
+ tokens: torch.Tensor,
502
+ position_ids: torch.Tensor,
503
+ block_mask: BlockMask,
504
+ rotary_factors: tuple[torch.Tensor, torch.Tensor] | None = None,
505
+ ) -> torch.Tensor:
506
+ """Transform packed ``[tokens, hidden_size]`` values without padding."""
507
+ if not isinstance(tokens, torch.Tensor):
508
+ raise TypeError(f"tokens must be a torch.Tensor, got {type(tokens).__name__}")
509
+ if tokens.ndim != 2 or tokens.shape[1] != self.hidden_size:
510
+ raise ValueError(
511
+ f"tokens must use (tokens, hidden_size) layout with hidden_size {self.hidden_size}, got shape {tuple(tokens.shape)}"
512
+ )
513
+ if not tokens.is_floating_point():
514
+ raise ValueError(f"tokens must be floating point, got {tokens.dtype}")
515
+ if not isinstance(block_mask, BlockMask):
516
+ raise TypeError(f"block_mask must be a BlockMask, got {type(block_mask).__name__}")
517
+ token_count = tokens.shape[0]
518
+ if block_mask.shape[-2:] != (token_count, token_count):
519
+ raise ValueError(
520
+ f"BlockMask query/key lengths must match the packed token count: got {block_mask.shape[-2:]}, expected {(token_count, token_count)}"
521
+ )
522
+ qkv = self.qkv(tokens).reshape(token_count, 3, self.num_heads, self.head_dim)
523
+ (query, key, value) = qkv.permute(1, 2, 0, 3).unbind(dim=0)
524
+ if rotary_factors is None:
525
+ rotary_factors = self.rotary.prepare(position_ids)
526
+ normalize_and_rotate = (
527
+ _compiled_normalize_and_apply_rotary
528
+ if tokens.device.type == "cuda"
529
+ else _normalize_and_apply_rotary
530
+ )
531
+ (cosine, sine) = rotary_factors
532
+ query = normalize_and_rotate(
533
+ query, self.q_norm.weight, self.q_norm.eps, cosine, sine
534
+ ).unsqueeze(0)
535
+ key = normalize_and_rotate(
536
+ key, self.k_norm.weight, self.k_norm.eps, cosine, sine
537
+ ).unsqueeze(0)
538
+ value = value.unsqueeze(0)
539
+ attended = run_flex_attention(query, key, value, block_mask=block_mask)
540
+ merged = attended.transpose(1, 2).reshape(token_count, self.hidden_size)
541
+ return self.proj_dropout(self.proj(merged))
vendor/pixtext/modeling/branches.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Post-projection packing for mixed RGB and canonical-glyph branches."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ from pixtext.modeling.patches import (
12
+ DirectPatchProjection,
13
+ PackedImagePatches,
14
+ PatchLayout,
15
+ patchify,
16
+ )
17
+ from pixtext.preprocessing.geometry import GroupGeometry
18
+ from pixtext.preprocessing.glyphs import GlyphCondition
19
+
20
+ MAIN_BRANCH = 0
21
+ ITEM_BRANCH = 1
22
+ GLYPH_BRANCH = 2
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class BranchDescriptor:
27
+ kind: str
28
+ slot_id: int | None
29
+
30
+ def __post_init__(self) -> None:
31
+ if self.kind not in {"main", "item", "glyph"}:
32
+ raise ValueError(f"unknown branch kind: {self.kind!r}")
33
+ if self.kind == "main":
34
+ if self.slot_id is not None:
35
+ raise ValueError("main branch slot_id must be None")
36
+ elif (
37
+ isinstance(self.slot_id, bool) or not isinstance(self.slot_id, int) or self.slot_id < 0
38
+ ):
39
+ raise ValueError("item/glyph slot_id must be a non-negative integer")
40
+
41
+ @property
42
+ def kind_id(self) -> int:
43
+ return {"main": MAIN_BRANCH, "item": ITEM_BRANCH, "glyph": GLYPH_BRANCH}[self.kind]
44
+
45
+
46
+ class DirectGlyphPatchProjection(nn.Module):
47
+ """Independent direct projection for one-channel [0,1] coverage patches."""
48
+
49
+ def __init__(self, *, patch_size: int = 16, hidden_size: int = 1280) -> None:
50
+ super().__init__()
51
+ if patch_size <= 0 or hidden_size <= 0:
52
+ raise ValueError("patch_size and hidden_size must be positive")
53
+ self.patch_size = patch_size
54
+ self.in_channels = 1
55
+ self.proj = nn.Linear(patch_size**2, hidden_size, bias=True)
56
+ nn.init.xavier_uniform_(self.proj.weight)
57
+ nn.init.zeros_(self.proj.bias)
58
+
59
+ def forward(self, patches: torch.Tensor) -> torch.Tensor:
60
+ if patches.ndim != 2 or patches.shape[1] != self.proj.in_features:
61
+ raise ValueError("glyph patches must use (tokens, patch_size**2) layout")
62
+ return self.proj(patches)
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class PackedBranchTokens:
67
+ tokens: torch.Tensor
68
+ descriptors: tuple[BranchDescriptor, ...]
69
+ layouts: tuple[PatchLayout, ...]
70
+ sample_ids: torch.Tensor
71
+ image_ids: torch.Tensor
72
+ branch_kinds: torch.Tensor
73
+ slot_ids: torch.Tensor
74
+ image_offsets: torch.Tensor
75
+ sample_offsets: torch.Tensor
76
+ branches_per_sample: tuple[int, ...]
77
+ rgb_token_indices: torch.Tensor
78
+ rgb_branch_indices: tuple[int, ...]
79
+ rgb_images_per_sample: tuple[int, ...]
80
+
81
+
82
+ class PackedBranchTokenizer:
83
+ """Hide mixed raw widths and merge RGB/glyphs after projection."""
84
+
85
+ def __init__(
86
+ self, *, rgb_projection: DirectPatchProjection, glyph_projection: DirectGlyphPatchProjection
87
+ ) -> None:
88
+ if not isinstance(rgb_projection, DirectPatchProjection):
89
+ raise TypeError("rgb_projection must be DirectPatchProjection")
90
+ if not isinstance(glyph_projection, DirectGlyphPatchProjection):
91
+ raise TypeError("glyph_projection must be DirectGlyphPatchProjection")
92
+ if rgb_projection.patch_size != glyph_projection.patch_size:
93
+ raise ValueError("RGB and glyph patch sizes must match")
94
+ if rgb_projection.proj.out_features != glyph_projection.proj.out_features:
95
+ raise ValueError("RGB and glyph projections must share hidden size")
96
+ self.rgb_projection = rgb_projection
97
+ self.glyph_projection = glyph_projection
98
+ self.patch_size = rgb_projection.patch_size
99
+
100
+ def tokenize(
101
+ self,
102
+ *,
103
+ packed_rgb: PackedImagePatches,
104
+ glyphs: Sequence[Sequence[GlyphCondition | None]],
105
+ geometries: Sequence[GroupGeometry],
106
+ ) -> PackedBranchTokens:
107
+ group_count = len(packed_rgb.images_per_sample)
108
+ if len(glyphs) != group_count or len(geometries) != group_count:
109
+ raise ValueError("RGB, glyph, and geometry group counts must match")
110
+ if any(
111
+ len(group_glyphs) != len(geometry.items)
112
+ for (group_glyphs, geometry) in zip(glyphs, geometries, strict=True)
113
+ ):
114
+ raise ValueError("glyph slots must match geometry items")
115
+ torch._assert_async(
116
+ torch.isfinite(packed_rgb.patches).all(), "RGB branches must remain finite"
117
+ )
118
+ main_patches = packed_rgb.patches[packed_rgb.image_ids.eq(MAIN_BRANCH)]
119
+ torch._assert_async(
120
+ main_patches.ge(-1).all() & main_patches.le(1).all(),
121
+ "clean main RGB branches must remain within [-1, 1]",
122
+ )
123
+ rgb_tokens = self.rgb_projection(packed_rgb.patches)
124
+ token_parts: list[torch.Tensor] = []
125
+ descriptors: list[BranchDescriptor] = []
126
+ layouts: list[PatchLayout] = []
127
+ sample_values: list[int] = []
128
+ image_values: list[int] = []
129
+ kind_values: list[int] = []
130
+ slot_values: list[int] = []
131
+ image_offsets = [0]
132
+ sample_offsets = [0]
133
+ branches_per_sample: list[int] = []
134
+ rgb_token_indices: list[torch.Tensor] = []
135
+ rgb_branch_indices: list[int] = []
136
+ flat_rgb = 0
137
+ compact_rgb_offset = 0
138
+ device = packed_rgb.patches.device
139
+ dtype = rgb_tokens.dtype
140
+ for sample_id, (rgb_count, group_glyphs, geometry) in enumerate(
141
+ zip(packed_rgb.images_per_sample, glyphs, geometries, strict=True)
142
+ ):
143
+ if rgb_count != 1 + len(geometry.items):
144
+ raise ValueError("packed RGB topology does not match geometry")
145
+ group_branch_count = 0
146
+ for rgb_image_id in range(rgb_count):
147
+ layout = packed_rgb.layouts[flat_rgb]
148
+ token_count = layout.num_patches
149
+ rgb_part = rgb_tokens[compact_rgb_offset : compact_rgb_offset + token_count]
150
+ descriptor = BranchDescriptor(
151
+ "main" if rgb_image_id == 0 else "item",
152
+ None if rgb_image_id == 0 else rgb_image_id - 1,
153
+ )
154
+ physical_id = group_branch_count
155
+ token_parts.append(rgb_part)
156
+ descriptors.append(descriptor)
157
+ layouts.append(layout)
158
+ sample_values.extend([sample_id] * token_count)
159
+ image_values.extend([physical_id] * token_count)
160
+ kind_values.extend([descriptor.kind_id] * token_count)
161
+ slot_values.extend(
162
+ [-1 if descriptor.slot_id is None else descriptor.slot_id] * token_count
163
+ )
164
+ start = image_offsets[-1]
165
+ image_offsets.append(start + token_count)
166
+ rgb_token_indices.append(torch.arange(start, start + token_count, device=device))
167
+ rgb_branch_indices.append(len(descriptors) - 1)
168
+ compact_rgb_offset += token_count
169
+ flat_rgb += 1
170
+ group_branch_count += 1
171
+ if rgb_image_id == 0:
172
+ continue
173
+ glyph = group_glyphs[rgb_image_id - 1]
174
+ if glyph is None:
175
+ continue
176
+ if glyph.aligned_crop != geometry.items[rgb_image_id - 1].crop:
177
+ raise ValueError("glyph crop provenance does not match geometry")
178
+ pixels = glyph.image.to(device=device, dtype=dtype, non_blocking=True)
179
+ torch._assert_async(
180
+ torch.isfinite(pixels).all() & pixels.ge(0).all() & pixels.le(1).all(),
181
+ "glyph coverage must remain finite and within [0, 1]",
182
+ )
183
+ patchified = patchify(pixels, self.patch_size)
184
+ if patchified.layout.num_patches != token_count:
185
+ raise ValueError("glyph and paired item token counts must match")
186
+ glyph_tokens = self.glyph_projection(patchified.patches)
187
+ glyph_descriptor = BranchDescriptor("glyph", rgb_image_id - 1)
188
+ glyph_physical_id = group_branch_count
189
+ token_parts.append(glyph_tokens)
190
+ descriptors.append(glyph_descriptor)
191
+ layouts.append(patchified.layout)
192
+ sample_values.extend([sample_id] * token_count)
193
+ image_values.extend([glyph_physical_id] * token_count)
194
+ kind_values.extend([GLYPH_BRANCH] * token_count)
195
+ slot_values.extend([rgb_image_id - 1] * token_count)
196
+ image_offsets.append(image_offsets[-1] + token_count)
197
+ group_branch_count += 1
198
+ branches_per_sample.append(group_branch_count)
199
+ sample_offsets.append(image_offsets[-1])
200
+ tokens = torch.cat(token_parts)
201
+ return PackedBranchTokens(
202
+ tokens=tokens,
203
+ descriptors=tuple(descriptors),
204
+ layouts=tuple(layouts),
205
+ sample_ids=torch.tensor(sample_values, dtype=torch.int64, device=device),
206
+ image_ids=torch.tensor(image_values, dtype=torch.int64, device=device),
207
+ branch_kinds=torch.tensor(kind_values, dtype=torch.int64, device=device),
208
+ slot_ids=torch.tensor(slot_values, dtype=torch.int64, device=device),
209
+ image_offsets=torch.tensor(image_offsets, dtype=torch.int64, device=device),
210
+ sample_offsets=torch.tensor(sample_offsets, dtype=torch.int64, device=device),
211
+ branches_per_sample=tuple(branches_per_sample),
212
+ rgb_token_indices=torch.cat(rgb_token_indices),
213
+ rgb_branch_indices=tuple(rgb_branch_indices),
214
+ rgb_images_per_sample=packed_rgb.images_per_sample,
215
+ )
216
+
217
+
218
+ def _branch_position_ids(
219
+ packed: PackedBranchTokens, geometries: Sequence[GroupGeometry]
220
+ ) -> torch.Tensor:
221
+ """Build main/item/glyph role positions from the shared descriptor order."""
222
+ parts: list[torch.Tensor] = []
223
+ branch_index = 0
224
+ device = packed.tokens.device
225
+ for sample_id, (geometry, branch_count) in enumerate(
226
+ zip(geometries, packed.branches_per_sample, strict=True)
227
+ ):
228
+ reference = geometry.reference.size
229
+ center_h = reference.height / (2 * packed.layouts[branch_index].patch_size)
230
+ center_w = reference.width / (2 * packed.layouts[branch_index].patch_size)
231
+ for image_id in range(branch_count):
232
+ descriptor = packed.descriptors[branch_index]
233
+ layout = packed.layouts[branch_index]
234
+ grid = layout.grid_size
235
+ rows = torch.arange(grid.height, dtype=torch.float32, device=device)
236
+ columns = torch.arange(grid.width, dtype=torch.float32, device=device)
237
+ (rows, columns) = torch.meshgrid(rows, columns, indexing="ij")
238
+ if descriptor.kind == "main":
239
+ h = (rows + 0.5) / geometry.main.scale_h - center_h
240
+ w = (columns + 0.5) / geometry.main.scale_w - center_w
241
+ n = torch.zeros_like(h)
242
+ else:
243
+ assert descriptor.slot_id is not None
244
+ item = geometry.items[descriptor.slot_id]
245
+ if layout.image_size != item.crop.size:
246
+ raise ValueError(
247
+ f"group {sample_id} branch {image_id} layout does not match slot"
248
+ )
249
+ h = rows + 0.5 + item.crop.top / layout.patch_size - center_h
250
+ w = columns + 0.5 + item.crop.left / layout.patch_size - center_w
251
+ n = torch.full_like(h, 1.0 if descriptor.kind == "item" else 2.0)
252
+ parts.append(torch.stack((n, h, w), dim=-1).reshape(-1, 3))
253
+ branch_index += 1
254
+ positions = torch.cat(parts)
255
+ if positions.shape != (packed.tokens.shape[0], 3):
256
+ raise RuntimeError("branch position count does not match token count")
257
+ return positions
258
+
259
+
260
+ def pack_branch_group_sequences(
261
+ *, branches: PackedBranchTokens, prompts, geometries: Sequence[GroupGeometry]
262
+ ):
263
+ from pixtext.modeling.conditioning import IMAGE_TOKEN_TYPE, PackedGroupSequence
264
+
265
+ "Assemble optional prompts with physical RGB/glyph branches.\n\n ``prompts=None`` is the real no-text adapter: it emits no placeholder\n tokens or prompt-owned topology and derives every offset from branches.\n "
266
+ if prompts is None:
267
+ if len(geometries) != len(branches.branches_per_sample):
268
+ raise ValueError("branch and geometry group counts must match")
269
+ positions = _branch_position_ids(branches, geometries)
270
+ device = branches.tokens.device
271
+ token_count = branches.tokens.shape[0]
272
+ empty = torch.empty(0, dtype=torch.int64, device=device)
273
+ return PackedGroupSequence(
274
+ tokens=branches.tokens,
275
+ position_ids=positions,
276
+ sample_ids=branches.sample_ids,
277
+ image_ids=branches.image_ids,
278
+ token_types=torch.full(
279
+ (token_count,), IMAGE_TOKEN_TYPE, dtype=torch.int64, device=device
280
+ ),
281
+ timestep_roles=branches.branch_kinds.ne(MAIN_BRANCH).to(torch.int64),
282
+ sample_offsets=branches.sample_offsets,
283
+ image_token_indices=torch.arange(token_count, dtype=torch.int64, device=device),
284
+ prompt_token_indices=empty,
285
+ image_offsets=branches.image_offsets,
286
+ prompt_offsets=torch.zeros(1, dtype=torch.int64, device=device),
287
+ layouts=branches.layouts,
288
+ images_per_sample=branches.branches_per_sample,
289
+ timestep_token_routing=None,
290
+ branch_kinds=branches.branch_kinds,
291
+ slot_ids=branches.slot_ids,
292
+ branch_descriptors=branches.descriptors,
293
+ )
294
+ raise ValueError("PixText does not use prompt tokens")
vendor/pixtext/modeling/conditioning.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import torch
6
+
7
+ from pixtext.modeling.modes import TimestepTokenRoutingMode
8
+ from pixtext.modeling.patches import PatchLayout
9
+ from pixtext.preprocessing.geometry import ImageSize
10
+
11
+ IMAGE_TOKEN_TYPE = 0
12
+ PROMPT_TOKEN_TYPE = 1
13
+ TIME_TOKEN_TYPE = 2
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class PackedGroupSequence:
18
+ """Group-contiguous image and prompt tokens ready for single-stream attention."""
19
+
20
+ tokens: torch.Tensor
21
+ position_ids: torch.Tensor
22
+ sample_ids: torch.Tensor
23
+ image_ids: torch.Tensor
24
+ token_types: torch.Tensor
25
+ timestep_roles: torch.Tensor
26
+ sample_offsets: torch.Tensor
27
+ image_token_indices: torch.Tensor
28
+ prompt_token_indices: torch.Tensor
29
+ image_offsets: torch.Tensor
30
+ prompt_offsets: torch.Tensor
31
+ layouts: tuple[PatchLayout, ...]
32
+ images_per_sample: tuple[int, ...]
33
+ timestep_token_routing: TimestepTokenRoutingMode | None
34
+ branch_kinds: torch.Tensor | None = None
35
+ slot_ids: torch.Tensor | None = None
36
+ branch_descriptors: tuple[object, ...] = ()
37
+ sample_token_counts: tuple[int, ...] = ()
38
+
39
+ @property
40
+ def image_shapes(self) -> tuple[ImageSize, ...]:
41
+ return tuple(layout.image_size for layout in self.layouts)
42
+
43
+
44
+ def extract_image_tokens(
45
+ sequence_tokens: torch.Tensor, packed: PackedGroupSequence
46
+ ) -> torch.Tensor:
47
+ """Restore compact image-token order from a transformed group sequence."""
48
+ if not isinstance(sequence_tokens, torch.Tensor):
49
+ raise TypeError(
50
+ f"sequence_tokens must be a torch.Tensor, got {type(sequence_tokens).__name__}"
51
+ )
52
+ if sequence_tokens.ndim != 2:
53
+ raise ValueError(
54
+ f"sequence_tokens must use (tokens, hidden_size) layout, got shape {tuple(sequence_tokens.shape)}"
55
+ )
56
+ if sequence_tokens.shape[0] != packed.tokens.shape[0]:
57
+ raise ValueError(
58
+ f"sequence token count does not match packed sequence: got {sequence_tokens.shape[0]}, expected {packed.tokens.shape[0]}"
59
+ )
60
+ if sequence_tokens.device != packed.image_token_indices.device:
61
+ raise ValueError("sequence tokens and packed indices must share a device")
62
+ return sequence_tokens.index_select(0, packed.image_token_indices)
vendor/pixtext/modeling/modes.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ImageAttentionRoutingMode(str, Enum):
7
+ """Control direct attention edges between images in the same group."""
8
+
9
+ ALL_IMAGES = "all_images"
10
+ MAIN_IMAGE_HUB = "main_image_hub"
11
+ ITEMS_READ_MAIN = "items_read_main"
12
+ ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM = "items_interread_glyph_reads_own_item"
13
+ ITEMS_INTERREAD_GLYPH_READS_OWN_ITEM_BIDIRECTIONAL_MAIN = (
14
+ "items_interread_glyph_reads_own_item_bidirectional_main"
15
+ )
16
+
17
+ @classmethod
18
+ def require(
19
+ cls, value: object, *, name: str = "image_attention_routing"
20
+ ) -> ImageAttentionRoutingMode:
21
+ if not isinstance(value, cls):
22
+ raise TypeError(f"{name} must be an ImageAttentionRoutingMode, got {value!r}")
23
+ return value
24
+
25
+
26
+ class TextConditioningMode(str, Enum):
27
+ """Choose whether the packed Transformer contains prompt branches."""
28
+
29
+ PACKED_PROMPT = "packed_prompt"
30
+ NONE = "none"
31
+
32
+ @classmethod
33
+ def require(cls, value: object, *, name: str = "text_conditioning") -> TextConditioningMode:
34
+ if not isinstance(value, cls):
35
+ raise TypeError(f"{name} must be a TextConditioningMode, got {value!r}")
36
+ return value
37
+
38
+
39
+ class TimestepTokenRoutingMode(str, Enum):
40
+ """Choose the topology and directional routing of iMF timestep banks."""
41
+
42
+ SHARED_ROLE_ONE_WAY = "shared_role_one_way"
43
+ PER_IMAGE_CONTEXTUAL = "per_image_contextual"
44
+
45
+ @classmethod
46
+ def require(
47
+ cls, value: object, *, name: str = "timestep_token_routing"
48
+ ) -> TimestepTokenRoutingMode:
49
+ if not isinstance(value, cls):
50
+ raise TypeError(f"{name} must be a TimestepTokenRoutingMode, got {value!r}")
51
+ return value
52
+
53
+
54
+ class NAxisMode(str, Enum):
55
+ """Assign the RoPE n coordinate from image role or group-local identity."""
56
+
57
+ ROLE = "role"
58
+ IMAGE_ID = "image_id"
59
+
60
+ @classmethod
61
+ def require(cls, value: object, *, name: str = "n_axis_mode") -> NAxisMode:
62
+ if not isinstance(value, cls):
63
+ raise TypeError(f"{name} must be an NAxisMode, got {value!r}")
64
+ return value
65
+
66
+ def position_for(self, image_id: int) -> float:
67
+ if self is NAxisMode.ROLE:
68
+ return float(image_id > 0)
69
+ return float(image_id)
vendor/pixtext/modeling/norm.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+
5
+ class RMSNorm(nn.Module):
6
+ def __init__(self, hidden_size, eps=1e-06):
7
+ """
8
+ LlamaRMSNorm is equivalent to T5LayerNorm
9
+ """
10
+ super().__init__()
11
+ self.weight = nn.Parameter(torch.ones(hidden_size))
12
+ self.variance_epsilon = eps
13
+
14
+ def forward(self, hidden_states):
15
+ input_dtype = hidden_states.dtype
16
+ hidden_states = hidden_states.to(torch.float32)
17
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
18
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
19
+ return (self.weight * hidden_states).to(input_dtype)
vendor/pixtext/modeling/patches.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+
6
+ import torch
7
+ from torch import nn
8
+
9
+ from pixtext.preprocessing.geometry import ImageSize
10
+
11
+
12
+ def _validate_positive_integer(name: str, value: int) -> None:
13
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
14
+ raise ValueError(f"{name} must be a positive integer, got {value!r}")
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class PatchLayout:
19
+ """Explicit metadata required to reconstruct one rectangular image."""
20
+
21
+ image_size: ImageSize
22
+ patch_size: int
23
+ channels: int
24
+
25
+ def __post_init__(self) -> None:
26
+ _validate_positive_integer("patch_size", self.patch_size)
27
+ _validate_positive_integer("channels", self.channels)
28
+ if (
29
+ self.image_size.height % self.patch_size != 0
30
+ or self.image_size.width % self.patch_size != 0
31
+ ):
32
+ raise ValueError(
33
+ f"image height and width must be divisible by patch_size: got {self.image_size!r} and patch_size={self.patch_size}"
34
+ )
35
+
36
+ @property
37
+ def grid_size(self) -> ImageSize:
38
+ return ImageSize(
39
+ self.image_size.height // self.patch_size, self.image_size.width // self.patch_size
40
+ )
41
+
42
+ @property
43
+ def num_patches(self) -> int:
44
+ grid = self.grid_size
45
+ return grid.height * grid.width
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class PatchifiedImage:
50
+ """Patch vectors together with their explicit rectangular layout."""
51
+
52
+ patches: torch.Tensor
53
+ layout: PatchLayout
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class PackedImagePatches:
58
+ """Raw image patches and routing metadata for a rank-local group batch."""
59
+
60
+ patches: torch.Tensor
61
+ sample_ids: torch.Tensor
62
+ image_ids: torch.Tensor
63
+ sample_offsets: torch.Tensor
64
+ image_offsets: torch.Tensor
65
+ layouts: tuple[PatchLayout, ...]
66
+ images_per_sample: tuple[int, ...]
67
+
68
+ @property
69
+ def image_shapes(self) -> tuple[ImageSize, ...]:
70
+ return tuple(layout.image_size for layout in self.layouts)
71
+
72
+ def sample_token_range(self, sample_start: int, sample_end: int) -> tuple[int, int]:
73
+ if (
74
+ isinstance(sample_start, bool)
75
+ or not isinstance(sample_start, int)
76
+ or isinstance(sample_end, bool)
77
+ or (not isinstance(sample_end, int))
78
+ or (not 0 <= sample_start < sample_end <= len(self.images_per_sample))
79
+ ):
80
+ raise IndexError("packed image sample range is out of bounds")
81
+ image_start = sum(self.images_per_sample[:sample_start])
82
+ image_end = image_start + sum(self.images_per_sample[sample_start:sample_end])
83
+ return (
84
+ sum(layout.num_patches for layout in self.layouts[:image_start]),
85
+ sum(layout.num_patches for layout in self.layouts[:image_end]),
86
+ )
87
+
88
+ def select_samples(self, sample_start: int, sample_end: int) -> PackedImagePatches:
89
+ """Return a contiguous sample range with local packed routing."""
90
+ (token_start, token_end) = self.sample_token_range(sample_start, sample_end)
91
+ images_per_sample = self.images_per_sample[sample_start:sample_end]
92
+ image_start = sum(self.images_per_sample[:sample_start])
93
+ image_end = image_start + sum(images_per_sample)
94
+ layouts = self.layouts[image_start:image_end]
95
+ image_offsets = [0]
96
+ for layout in layouts:
97
+ image_offsets.append(image_offsets[-1] + layout.num_patches)
98
+ sample_offsets = [0]
99
+ layout_index = 0
100
+ for image_count in images_per_sample:
101
+ sample_offsets.append(
102
+ sample_offsets[-1]
103
+ + sum(
104
+ layout.num_patches
105
+ for layout in layouts[layout_index : layout_index + image_count]
106
+ )
107
+ )
108
+ layout_index += image_count
109
+ device = self.patches.device
110
+ return PackedImagePatches(
111
+ patches=self.patches[token_start:token_end],
112
+ sample_ids=self.sample_ids[token_start:token_end] - sample_start,
113
+ image_ids=self.image_ids[token_start:token_end],
114
+ sample_offsets=torch.tensor(sample_offsets, dtype=torch.int64, device=device),
115
+ image_offsets=torch.tensor(image_offsets, dtype=torch.int64, device=device),
116
+ layouts=layouts,
117
+ images_per_sample=images_per_sample,
118
+ )
119
+
120
+
121
+ class DirectPatchProjection(nn.Module):
122
+ """Project flattened RGB patches directly into JiT token features."""
123
+
124
+ def __init__(
125
+ self, *, patch_size: int = 16, in_channels: int = 3, hidden_size: int = 1280
126
+ ) -> None:
127
+ super().__init__()
128
+ for name, value in (
129
+ ("patch_size", patch_size),
130
+ ("in_channels", in_channels),
131
+ ("hidden_size", hidden_size),
132
+ ):
133
+ _validate_positive_integer(name, value)
134
+ self.patch_size = patch_size
135
+ self.in_channels = in_channels
136
+ self.proj = nn.Linear(in_channels * patch_size**2, hidden_size, bias=True)
137
+ nn.init.xavier_uniform_(self.proj.weight)
138
+ nn.init.zeros_(self.proj.bias)
139
+
140
+ def forward(self, patches: torch.Tensor) -> torch.Tensor:
141
+ if not isinstance(patches, torch.Tensor):
142
+ raise TypeError(f"patches must be a torch.Tensor, got {type(patches).__name__}")
143
+ if patches.ndim != 2:
144
+ raise ValueError(
145
+ f"patches must use (tokens, patch_width) layout, got shape {tuple(patches.shape)}"
146
+ )
147
+ if patches.shape[1] != self.proj.in_features:
148
+ raise ValueError(
149
+ f"patch width does not match projection: got {patches.shape[1]}, expected {self.proj.in_features}"
150
+ )
151
+ return self.proj(patches)
152
+
153
+
154
+ def patchify(image: torch.Tensor, patch_size: int = 16) -> PatchifiedImage:
155
+ """Convert one ``[C,H,W]`` image into row-major flattened patches."""
156
+ _validate_positive_integer("patch_size", patch_size)
157
+ if not isinstance(image, torch.Tensor):
158
+ raise TypeError(f"image must be a torch.Tensor, got {type(image).__name__}")
159
+ if image.ndim != 3:
160
+ raise ValueError(f"image must use (C, H, W) layout, got shape {tuple(image.shape)}")
161
+ (channels, height, width) = image.shape
162
+ if height % patch_size != 0 or width % patch_size != 0:
163
+ raise ValueError(
164
+ f"image height and width must be divisible by patch_size: got {(height, width)} and patch_size={patch_size}"
165
+ )
166
+ grid_height = height // patch_size
167
+ grid_width = width // patch_size
168
+ patches = (
169
+ image.reshape(channels, grid_height, patch_size, grid_width, patch_size)
170
+ .permute(1, 3, 0, 2, 4)
171
+ .reshape(grid_height * grid_width, channels * patch_size**2)
172
+ )
173
+ return PatchifiedImage(
174
+ patches=patches,
175
+ layout=PatchLayout(
176
+ image_size=ImageSize(height, width), patch_size=patch_size, channels=channels
177
+ ),
178
+ )
179
+
180
+
181
+ def unpatchify(patches: torch.Tensor, layout: PatchLayout) -> torch.Tensor:
182
+ """Reconstruct one image using explicit rectangular layout metadata."""
183
+ if not isinstance(patches, torch.Tensor):
184
+ raise TypeError(f"patches must be a torch.Tensor, got {type(patches).__name__}")
185
+ if patches.ndim != 2:
186
+ raise ValueError(
187
+ f"patches must use (tokens, patch_width) layout, got shape {tuple(patches.shape)}"
188
+ )
189
+ grid = layout.grid_size
190
+ if patches.shape[0] != layout.num_patches:
191
+ raise ValueError(
192
+ f"patch token count does not match layout: got {patches.shape[0]}, expected {layout.num_patches}"
193
+ )
194
+ expected_patch_width = layout.channels * layout.patch_size**2
195
+ if patches.shape[1] != expected_patch_width:
196
+ raise ValueError(
197
+ f"patch width does not match layout: got {patches.shape[1]}, expected {expected_patch_width}"
198
+ )
199
+ image = (
200
+ patches.reshape(
201
+ grid.height, grid.width, layout.channels, layout.patch_size, layout.patch_size
202
+ )
203
+ .permute(2, 0, 3, 1, 4)
204
+ .reshape(layout.channels, layout.image_size.height, layout.image_size.width)
205
+ )
206
+ return image
207
+
208
+
209
+ def pack_image_groups(
210
+ groups: Sequence[Sequence[torch.Tensor]], patch_size: int = 16
211
+ ) -> PackedImagePatches:
212
+ """Patchify and concatenate ``main + ordered items`` for every group."""
213
+ _validate_positive_integer("patch_size", patch_size)
214
+ if len(groups) == 0:
215
+ raise ValueError("packed image batch must contain at least one group")
216
+ patch_parts = []
217
+ sample_id_values = []
218
+ image_id_values = []
219
+ layouts = []
220
+ images_per_sample = []
221
+ sample_offsets = [0]
222
+ image_offsets = [0]
223
+ expected_dtype = None
224
+ expected_device = None
225
+ expected_channels = None
226
+ for sample_id, group in enumerate(groups):
227
+ if len(group) == 0:
228
+ raise ValueError(f"group {sample_id} must contain at least one image (the main image)")
229
+ images_per_sample.append(len(group))
230
+ for image_id, image in enumerate(group):
231
+ patchified = patchify(image, patch_size)
232
+ if expected_dtype is None:
233
+ expected_dtype = patchified.patches.dtype
234
+ expected_device = patchified.patches.device
235
+ expected_channels = patchified.layout.channels
236
+ else:
237
+ location = f"group {sample_id} image {image_id}"
238
+ if patchified.patches.dtype != expected_dtype:
239
+ raise ValueError(
240
+ f"{location} dtype {patchified.patches.dtype} does not match packed dtype {expected_dtype}"
241
+ )
242
+ if patchified.patches.device != expected_device:
243
+ raise ValueError(
244
+ f"{location} device {patchified.patches.device} does not match packed device {expected_device}"
245
+ )
246
+ if patchified.layout.channels != expected_channels:
247
+ raise ValueError(
248
+ f"{location} channels {patchified.layout.channels} do not match packed channels {expected_channels}"
249
+ )
250
+ patch_parts.append(patchified.patches)
251
+ layouts.append(patchified.layout)
252
+ token_count = patchified.layout.num_patches
253
+ sample_id_values.extend([sample_id] * token_count)
254
+ image_id_values.extend([image_id] * token_count)
255
+ image_offsets.append(image_offsets[-1] + token_count)
256
+ sample_offsets.append(image_offsets[-1])
257
+ patches = torch.cat(patch_parts)
258
+ device = patches.device
259
+ return PackedImagePatches(
260
+ patches=patches,
261
+ sample_ids=torch.tensor(sample_id_values, dtype=torch.int64, device=device),
262
+ image_ids=torch.tensor(image_id_values, dtype=torch.int64, device=device),
263
+ sample_offsets=torch.tensor(sample_offsets, dtype=torch.int64, device=device),
264
+ image_offsets=torch.tensor(image_offsets, dtype=torch.int64, device=device),
265
+ layouts=tuple(layouts),
266
+ images_per_sample=tuple(images_per_sample),
267
+ )
268
+
269
+
270
+ def unpack_image_patches(
271
+ patches: torch.Tensor, packed: PackedImagePatches
272
+ ) -> tuple[tuple[torch.Tensor, ...], ...]:
273
+ """Restore packed patch pixels to the original group/image structure."""
274
+ if not isinstance(patches, torch.Tensor):
275
+ raise TypeError(f"patches must be a torch.Tensor, got {type(patches).__name__}")
276
+ if patches.ndim != 2:
277
+ raise ValueError(
278
+ f"patches must use (tokens, patch_width) layout, got shape {tuple(patches.shape)}"
279
+ )
280
+ expected_token_count = sum(layout.num_patches for layout in packed.layouts)
281
+ if patches.shape[0] != expected_token_count:
282
+ raise ValueError(
283
+ f"packed total token count mismatch: got {patches.shape[0]}, expected {expected_token_count}"
284
+ )
285
+ if packed.image_offsets.shape != (len(packed.layouts) + 1,):
286
+ raise ValueError("packed image offsets do not match the image layouts")
287
+ flat_images = []
288
+ start = 0
289
+ for layout in packed.layouts:
290
+ end = start + layout.num_patches
291
+ flat_images.append(unpatchify(patches[start:end], layout))
292
+ start = end
293
+ groups = []
294
+ image_index = 0
295
+ for image_count in packed.images_per_sample:
296
+ groups.append(tuple(flat_images[image_index : image_index + image_count]))
297
+ image_index += image_count
298
+ return tuple(groups)
vendor/pixtext/modeling/rope.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Geometry-aware 3D rotary positions for variable-length image groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Sequence
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ from pixtext.modeling.modes import NAxisMode
12
+ from pixtext.modeling.patches import PackedImagePatches, PatchLayout
13
+ from pixtext.preprocessing.geometry import GroupGeometry, ImageSize
14
+
15
+ DEFAULT_ROPE_AXES_DIMS = (12, 34, 34)
16
+ DEFAULT_ROPE_THETA = 10000.0
17
+
18
+
19
+ def _apply_rotary_factors(
20
+ values: torch.Tensor, cosine: torch.Tensor, sine: torch.Tensor
21
+ ) -> torch.Tensor:
22
+ """Apply already-materialized pairwise rotations."""
23
+ output_dtype = values.dtype
24
+ pair_count = values.shape[-1] // 2
25
+ paired = values.float().reshape(*values.shape[:-1], pair_count, 2)
26
+ (first, second) = paired.unbind(dim=-1)
27
+ factor_shape = (*[1] * (values.ndim - 2), values.shape[-2], pair_count)
28
+ cosine = cosine.reshape(factor_shape)
29
+ sine = sine.reshape(factor_shape)
30
+ return (
31
+ torch.stack((first * cosine - second * sine, first * sine + second * cosine), dim=-1)
32
+ .flatten(start_dim=-2)
33
+ .to(output_dtype)
34
+ )
35
+
36
+
37
+ _compiled_apply_rotary_factors = torch.compile(_apply_rotary_factors, dynamic=True)
38
+
39
+
40
+ class RotaryEmbedding3D(nn.Module):
41
+ """Apply dynamic 3D RoPE to the last dimension of Q or K tensors."""
42
+
43
+ def __init__(
44
+ self, axes_dims: Sequence[int] = DEFAULT_ROPE_AXES_DIMS, theta: float = DEFAULT_ROPE_THETA
45
+ ) -> None:
46
+ super().__init__()
47
+ if len(axes_dims) != 3:
48
+ raise ValueError(f"axes_dims must contain exactly three dimensions, got {axes_dims!r}")
49
+ if any(
50
+ isinstance(axis_dim, bool)
51
+ or not isinstance(axis_dim, int)
52
+ or axis_dim <= 0
53
+ or (axis_dim % 2 != 0)
54
+ for axis_dim in axes_dims
55
+ ):
56
+ raise ValueError(
57
+ f"every axes_dims entry must be a positive even integer, got {axes_dims!r}"
58
+ )
59
+ if (
60
+ isinstance(theta, bool)
61
+ or not isinstance(theta, (int, float))
62
+ or (not math.isfinite(theta))
63
+ or (theta <= 0)
64
+ ):
65
+ raise ValueError(f"theta must be positive and finite, got {theta!r}")
66
+ self.axes_dims = tuple(axes_dims)
67
+ self.theta = float(theta)
68
+ self.head_dim = sum(self.axes_dims)
69
+ for axis_name, axis_dim in zip(("n", "h", "w"), self.axes_dims, strict=True):
70
+ inverse_frequency = 1.0 / self.theta ** (
71
+ torch.arange(0, axis_dim, 2, dtype=torch.float32, device="cpu") / axis_dim
72
+ )
73
+ self.register_buffer(f"inv_freq_{axis_name}", inverse_frequency, persistent=False)
74
+
75
+ def _apply(self, fn, recurse: bool = True):
76
+ """Move buffers with the module while retaining FP32 frequencies."""
77
+ super()._apply(fn, recurse)
78
+ self.inv_freq_n = self.inv_freq_n.float()
79
+ self.inv_freq_h = self.inv_freq_h.float()
80
+ self.inv_freq_w = self.inv_freq_w.float()
81
+ return self
82
+
83
+ def forward(self, values: torch.Tensor, position_ids: torch.Tensor) -> torch.Tensor:
84
+ """Rotate ``values[..., tokens, head_dim]`` using ``[tokens, 3]`` IDs."""
85
+ if not isinstance(values, torch.Tensor):
86
+ raise TypeError(f"values must be a torch.Tensor, got {type(values).__name__}")
87
+ if values.ndim < 2:
88
+ raise ValueError(
89
+ f"values must have at least (tokens, head_dim) dimensions, got shape {tuple(values.shape)}"
90
+ )
91
+ if not values.is_floating_point():
92
+ raise ValueError(f"values must be floating point, got {values.dtype}")
93
+ factors = self.prepare(position_ids)
94
+ return self.apply_precomputed(values, factors)
95
+
96
+ def prepare(self, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
97
+ """Materialize FP32 cosine/sine factors once for a packed sequence."""
98
+ if not isinstance(position_ids, torch.Tensor):
99
+ raise TypeError(
100
+ f"position_ids must be a torch.Tensor, got {type(position_ids).__name__}"
101
+ )
102
+ if position_ids.ndim != 2 or position_ids.shape[1] != 3:
103
+ raise ValueError(
104
+ f"position_ids must use (tokens, 3) layout, got shape {tuple(position_ids.shape)}"
105
+ )
106
+ float_positions = position_ids.float()
107
+ angles = []
108
+ for axis, inverse_frequency in enumerate(
109
+ (self.inv_freq_n, self.inv_freq_h, self.inv_freq_w)
110
+ ):
111
+ if inverse_frequency.device != position_ids.device:
112
+ raise ValueError(
113
+ f"RotaryEmbedding3D buffers and position_ids must be on the same device: got {inverse_frequency.device} and {position_ids.device}"
114
+ )
115
+ angles.append(float_positions[:, axis, None] * inverse_frequency[None, :])
116
+ packed_angles = torch.cat(angles, dim=-1)
117
+ return (packed_angles.cos(), packed_angles.sin())
118
+
119
+ def apply_precomputed(
120
+ self, values: torch.Tensor, factors: tuple[torch.Tensor, torch.Tensor]
121
+ ) -> torch.Tensor:
122
+ """Rotate values using factors returned by :meth:`prepare`."""
123
+ if not isinstance(values, torch.Tensor):
124
+ raise TypeError(f"values must be a torch.Tensor, got {type(values).__name__}")
125
+ if values.ndim < 2:
126
+ raise ValueError(
127
+ f"values must have at least (tokens, head_dim) dimensions, got shape {tuple(values.shape)}"
128
+ )
129
+ if not values.is_floating_point():
130
+ raise ValueError(f"values must be floating point, got {values.dtype}")
131
+ if (
132
+ not isinstance(factors, tuple)
133
+ or len(factors) != 2
134
+ or (not all(isinstance(factor, torch.Tensor) for factor in factors))
135
+ ):
136
+ raise TypeError("factors must be the (cosine, sine) tensor pair")
137
+ (cosine, sine) = factors
138
+ if values.shape[-1] != self.head_dim:
139
+ raise ValueError(
140
+ f"values head dimension does not match axes_dims: got {values.shape[-1]}, expected {self.head_dim}"
141
+ )
142
+ if cosine.ndim != 2 or sine.ndim != 2:
143
+ raise ValueError("rotary factors must use (tokens, head_dim / 2) layout")
144
+ if cosine.shape[0] != values.shape[-2] or sine.shape[0] != values.shape[-2]:
145
+ raise ValueError(
146
+ f"position token count does not match values: got {cosine.shape[0]} and {sine.shape[0]}, expected {values.shape[-2]}"
147
+ )
148
+ expected_factor_shape = (values.shape[-2], self.head_dim // 2)
149
+ if cosine.shape != expected_factor_shape or sine.shape != expected_factor_shape:
150
+ raise ValueError(
151
+ f"rotary factors must use (tokens, head_dim / 2) layout: got {tuple(cosine.shape)} and {tuple(sine.shape)}, expected {expected_factor_shape}"
152
+ )
153
+ if cosine.device != values.device or sine.device != values.device:
154
+ raise ValueError("rotary factors and values must be on the same device")
155
+ apply = (
156
+ _compiled_apply_rotary_factors
157
+ if values.device.type == "cuda"
158
+ else _apply_rotary_factors
159
+ )
160
+ return apply(values, cosine, sine)
161
+
162
+
163
+ def _validate_nonnegative_integer(name: str, value: int) -> None:
164
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
165
+ raise ValueError(f"{name} must be a non-negative integer, got {value!r}")
166
+
167
+
168
+ def _validate_positive_integer(name: str, value: int) -> None:
169
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
170
+ raise ValueError(f"{name} must be a positive integer, got {value!r}")
171
+
172
+
173
+ def _validate_layout(
174
+ layout: PatchLayout,
175
+ expected_size: ImageSize,
176
+ expected_patch_size: int,
177
+ *,
178
+ sample_id: int,
179
+ image_id: int,
180
+ ) -> None:
181
+ if layout.patch_size != expected_patch_size:
182
+ raise ValueError(
183
+ f"group {sample_id} image {image_id} patch_size {layout.patch_size} does not match position patch_size {expected_patch_size}"
184
+ )
185
+ if layout.image_size != expected_size:
186
+ raise ValueError(
187
+ f"group {sample_id} image {image_id} layout size {layout.image_size!r} does not match geometry size {expected_size!r}"
188
+ )
189
+
190
+
191
+ def _spatial_grid(
192
+ layout: PatchLayout,
193
+ *,
194
+ offset_h: float,
195
+ offset_w: float,
196
+ scale_h: float = 1.0,
197
+ scale_w: float = 1.0,
198
+ device: torch.device,
199
+ ) -> tuple[torch.Tensor, torch.Tensor]:
200
+ grid = layout.grid_size
201
+ rows = torch.arange(grid.height, dtype=torch.float32, device=device)
202
+ columns = torch.arange(grid.width, dtype=torch.float32, device=device)
203
+ (rows, columns) = torch.meshgrid(rows, columns, indexing="ij")
204
+ return ((rows + 0.5) / scale_h + offset_h, (columns + 0.5) / scale_w + offset_w)
205
+
206
+
207
+ def build_image_position_ids(
208
+ packed: PackedImagePatches,
209
+ geometries: Sequence[GroupGeometry],
210
+ *,
211
+ patch_size: int = 16,
212
+ n_axis_mode: NAxisMode = NAxisMode.ROLE,
213
+ ) -> torch.Tensor:
214
+ """Build packed ``[n, h, w]`` coordinates on each reference canvas."""
215
+ _validate_positive_integer("patch_size", patch_size)
216
+ n_axis_mode = NAxisMode.require(n_axis_mode)
217
+ if len(geometries) != len(packed.images_per_sample):
218
+ raise ValueError(
219
+ f"geometry count does not match packed group count: got {len(geometries)}, expected {len(packed.images_per_sample)}"
220
+ )
221
+ device = packed.patches.device
222
+ position_parts: list[torch.Tensor] = []
223
+ flat_image_id = 0
224
+ for sample_id, (geometry, image_count) in enumerate(
225
+ zip(geometries, packed.images_per_sample, strict=True)
226
+ ):
227
+ expected_image_count = 1 + len(geometry.items)
228
+ if image_count != expected_image_count:
229
+ raise ValueError(
230
+ f"group {sample_id} has {image_count} packed images but geometry describes {expected_image_count}"
231
+ )
232
+ main_layout = packed.layouts[flat_image_id]
233
+ _validate_layout(
234
+ main_layout, geometry.main.size, patch_size, sample_id=sample_id, image_id=0
235
+ )
236
+ reference = geometry.reference.size
237
+ center_h = reference.height / (2 * patch_size)
238
+ center_w = reference.width / (2 * patch_size)
239
+ (h, w) = _spatial_grid(
240
+ main_layout,
241
+ offset_h=-center_h,
242
+ offset_w=-center_w,
243
+ scale_h=geometry.main.scale_h,
244
+ scale_w=geometry.main.scale_w,
245
+ device=device,
246
+ )
247
+ position_parts.append(torch.stack((torch.zeros_like(h), h, w), dim=-1).reshape(-1, 3))
248
+ flat_image_id += 1
249
+ for image_id, item in enumerate(geometry.items, start=1):
250
+ layout = packed.layouts[flat_image_id]
251
+ _validate_layout(
252
+ layout, item.crop.size, patch_size, sample_id=sample_id, image_id=image_id
253
+ )
254
+ (h, w) = _spatial_grid(
255
+ layout,
256
+ offset_h=item.crop.top / patch_size - center_h,
257
+ offset_w=item.crop.left / patch_size - center_w,
258
+ device=device,
259
+ )
260
+ n = torch.full_like(h, n_axis_mode.position_for(image_id))
261
+ position_parts.append(torch.stack((n, h, w), dim=-1).reshape(-1, 3))
262
+ flat_image_id += 1
263
+ positions = torch.cat(position_parts)
264
+ if positions.shape[0] != packed.patches.shape[0]:
265
+ raise ValueError(
266
+ f"generated position count does not match packed token count: got {positions.shape[0]}, expected {packed.patches.shape[0]}"
267
+ )
268
+ return positions
269
+
270
+
271
+ def build_prompt_position_ids(
272
+ prompt_length: int,
273
+ image_id: int,
274
+ geometry: GroupGeometry,
275
+ *,
276
+ patch_size: int = 16,
277
+ n_axis_mode: NAxisMode = NAxisMode.ROLE,
278
+ device: torch.device | str | None = None,
279
+ ) -> torch.Tensor:
280
+ """Build one prompt's independent sequential ``[n, h, w]`` positions."""
281
+ _validate_nonnegative_integer("prompt_length", prompt_length)
282
+ _validate_nonnegative_integer("image_id", image_id)
283
+ _validate_positive_integer("patch_size", patch_size)
284
+ n_axis_mode = NAxisMode.require(n_axis_mode)
285
+ if image_id > len(geometry.items):
286
+ raise ValueError(
287
+ f"image_id {image_id} is outside geometry image range [0, {len(geometry.items)}]"
288
+ )
289
+ reference = geometry.reference.size
290
+ if reference.height % patch_size != 0 or reference.width % patch_size != 0:
291
+ raise ValueError(
292
+ f"reference size must be divisible by patch_size: got {reference!r} and patch_size={patch_size}"
293
+ )
294
+ text_base = math.ceil(
295
+ max(reference.height / (2 * patch_size), reference.width / (2 * patch_size))
296
+ )
297
+ sequence = torch.arange(prompt_length, dtype=torch.float32, device=device).add_(text_base)
298
+ image_axis = torch.full_like(sequence, n_axis_mode.position_for(image_id))
299
+ return torch.stack((image_axis, sequence, sequence), dim=-1)
300
+
301
+
302
+ def build_timestep_position_ids(
303
+ geometry: GroupGeometry,
304
+ *,
305
+ bank_image_ids: Sequence[int],
306
+ tokens_per_bank: int = 4,
307
+ patch_size: int = 16,
308
+ device: torch.device | str | None = None,
309
+ ) -> torch.Tensor:
310
+ """Place ordered timestep banks before the spatial coordinate range.
311
+
312
+ Multiple item banks intentionally share the item-role ``n`` coordinate;
313
+ their exact image identity is enforced by attention routing rather than by
314
+ adding an arbitrary item-index position.
315
+ """
316
+ if not isinstance(geometry, GroupGeometry):
317
+ raise TypeError(f"geometry must be a GroupGeometry, got {type(geometry).__name__}")
318
+ if not isinstance(bank_image_ids, Sequence) or isinstance(bank_image_ids, (str, bytes)):
319
+ raise TypeError("bank_image_ids must be a sequence of image IDs")
320
+ bank_image_ids = tuple(bank_image_ids)
321
+ if not bank_image_ids:
322
+ raise ValueError("bank_image_ids must contain at least the main image")
323
+ if any(
324
+ isinstance(image_id, bool)
325
+ or not isinstance(image_id, int)
326
+ or (not 0 <= image_id <= len(geometry.items))
327
+ for image_id in bank_image_ids
328
+ ):
329
+ raise ValueError("bank_image_ids must lie within the geometry image range")
330
+ if bank_image_ids[0] != 0:
331
+ raise ValueError("the first timestep bank must target the main image")
332
+ _validate_positive_integer("tokens_per_bank", tokens_per_bank)
333
+ _validate_positive_integer("patch_size", patch_size)
334
+ reference = geometry.reference.size
335
+ if reference.height % patch_size != 0 or reference.width % patch_size != 0:
336
+ raise ValueError(
337
+ f"reference size must be divisible by patch_size: got {reference!r} and patch_size={patch_size}"
338
+ )
339
+ sequence_base = math.ceil(
340
+ max(reference.height / (2 * patch_size), reference.width / (2 * patch_size))
341
+ )
342
+ sequence = torch.arange(tokens_per_bank, dtype=torch.float32, device=device).add_(
343
+ -sequence_base - tokens_per_bank
344
+ )
345
+ banks = []
346
+ for image_id in bank_image_ids:
347
+ role = float(image_id > 0)
348
+ role_axis = torch.full_like(sequence, role)
349
+ banks.append(torch.stack((role_axis, sequence, sequence), dim=-1))
350
+ return torch.cat(banks)
vendor/pixtext/modeling/transformer.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import Sequence
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch import nn
9
+ from torch.nn.attention.flex_attention import BlockMask
10
+
11
+ from pixtext.modeling.attention import GroupSelfAttention, build_flex_attention_block_mask
12
+ from pixtext.modeling.branches import (
13
+ GLYPH_BRANCH,
14
+ ITEM_BRANCH,
15
+ DirectGlyphPatchProjection,
16
+ PackedBranchTokenizer,
17
+ pack_branch_group_sequences,
18
+ )
19
+ from pixtext.modeling.conditioning import extract_image_tokens
20
+ from pixtext.modeling.modes import ImageAttentionRoutingMode, NAxisMode, TextConditioningMode
21
+ from pixtext.modeling.norm import RMSNorm
22
+ from pixtext.modeling.patches import DirectPatchProjection, PackedImagePatches, unpack_image_patches
23
+ from pixtext.modeling.rope import DEFAULT_ROPE_AXES_DIMS, DEFAULT_ROPE_THETA, RotaryEmbedding3D
24
+ from pixtext.preprocessing.geometry import GroupGeometry
25
+ from pixtext.preprocessing.glyphs import GlyphCondition
26
+ from pixtext.sampling.timesteps import GroupTimesteps
27
+
28
+
29
+ def modulate(x, shift, scale):
30
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
31
+
32
+
33
+ def _validate_positive_integer(name: str, value: int) -> None:
34
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
35
+ raise ValueError(f"{name} must be a positive integer, got {value!r}")
36
+
37
+
38
+ def _validate_probability(name: str, value: float) -> float:
39
+ if (
40
+ isinstance(value, bool)
41
+ or not isinstance(value, (int, float))
42
+ or (not math.isfinite(value))
43
+ or (not 0 <= value < 1)
44
+ ):
45
+ raise ValueError(f"{name} must be finite and in [0, 1), got {value!r}")
46
+ return float(value)
47
+
48
+
49
+ def _validate_packed_timestep_routing(
50
+ tokens: torch.Tensor,
51
+ timestep_embeddings: torch.Tensor,
52
+ conditioning_ids: torch.Tensor,
53
+ *,
54
+ hidden_size: int,
55
+ ) -> None:
56
+ if not isinstance(tokens, torch.Tensor):
57
+ raise TypeError(f"tokens must be a torch.Tensor, got {type(tokens).__name__}")
58
+ if tokens.ndim != 2 or tokens.shape[1] != hidden_size:
59
+ raise ValueError(
60
+ f"tokens must use (tokens, hidden_size) layout with hidden_size {hidden_size}, got shape {tuple(tokens.shape)}"
61
+ )
62
+ if not tokens.is_floating_point():
63
+ raise ValueError(f"tokens must be floating point, got {tokens.dtype}")
64
+ if not isinstance(timestep_embeddings, torch.Tensor):
65
+ raise TypeError(
66
+ f"timestep_embeddings must be a torch.Tensor, got {type(timestep_embeddings).__name__}"
67
+ )
68
+ if (
69
+ timestep_embeddings.ndim != 2
70
+ or timestep_embeddings.shape[0] == 0
71
+ or timestep_embeddings.shape[1] != hidden_size
72
+ ):
73
+ raise ValueError(
74
+ f"timestep_embeddings must use (images, hidden_size) layout with at least one image and hidden_size {hidden_size}, got shape {tuple(timestep_embeddings.shape)}"
75
+ )
76
+ if not timestep_embeddings.is_floating_point():
77
+ raise ValueError(
78
+ f"timestep_embeddings must be floating point, got {timestep_embeddings.dtype}"
79
+ )
80
+ if not isinstance(conditioning_ids, torch.Tensor):
81
+ raise TypeError(
82
+ f"conditioning_ids must be a torch.Tensor, got {type(conditioning_ids).__name__}"
83
+ )
84
+ if conditioning_ids.shape != (tokens.shape[0],):
85
+ raise ValueError(
86
+ f"conditioning_ids must contain {tokens.shape[0]} entries, got shape {tuple(conditioning_ids.shape)}"
87
+ )
88
+ if conditioning_ids.dtype != torch.int64:
89
+ raise ValueError(f"conditioning_ids must use torch.int64, got {conditioning_ids.dtype}")
90
+ if tokens.dtype != timestep_embeddings.dtype:
91
+ raise ValueError(
92
+ f"tokens and timestep_embeddings must share a dtype, got {tokens.dtype} and {timestep_embeddings.dtype}"
93
+ )
94
+ if tokens.device != timestep_embeddings.device or tokens.device != conditioning_ids.device:
95
+ raise ValueError("tokens, timestep_embeddings, and conditioning_ids must share a device")
96
+
97
+
98
+ def _route_packed_timestep_embeddings(
99
+ tokens: torch.Tensor,
100
+ timestep_embeddings: torch.Tensor,
101
+ conditioning_ids: torch.Tensor,
102
+ *,
103
+ hidden_size: int,
104
+ ) -> torch.Tensor:
105
+ _validate_packed_timestep_routing(
106
+ tokens, timestep_embeddings, conditioning_ids, hidden_size=hidden_size
107
+ )
108
+ return timestep_embeddings.index_select(0, conditioning_ids)
109
+
110
+
111
+ def _route_packed_adaln_parameters(
112
+ tokens: torch.Tensor,
113
+ image_conditions: torch.Tensor,
114
+ conditioning_ids: torch.Tensor,
115
+ *,
116
+ modulation: nn.Module,
117
+ hidden_size: int,
118
+ parameter_count: int,
119
+ ) -> tuple[torch.Tensor, ...]:
120
+ """Compute AdaLN once per image, then route its parameters to tokens."""
121
+ _validate_packed_timestep_routing(
122
+ tokens, image_conditions, conditioning_ids, hidden_size=hidden_size
123
+ )
124
+ image_parameters = modulation(image_conditions)
125
+ expected_shape = (image_conditions.shape[0], parameter_count * hidden_size)
126
+ if image_parameters.shape != expected_shape:
127
+ raise ValueError(
128
+ f"AdaLN modulation must return {expected_shape}, got {tuple(image_parameters.shape)}"
129
+ )
130
+ return image_parameters.index_select(0, conditioning_ids).chunk(parameter_count, dim=-1)
131
+
132
+
133
+ def _flat_image_conditioning_ids(
134
+ sample_ids: torch.Tensor, image_ids: torch.Tensor, images_per_sample: tuple[int, ...]
135
+ ) -> torch.Tensor:
136
+ """Map group-local image IDs to the stable flat image order."""
137
+ group_offsets = []
138
+ next_offset = 0
139
+ for image_count in images_per_sample:
140
+ group_offsets.append(next_offset)
141
+ next_offset += image_count
142
+ offset_tensor = torch.tensor(group_offsets, device=sample_ids.device, dtype=torch.int64)
143
+ return offset_tensor.index_select(0, sample_ids) + image_ids
144
+
145
+
146
+ class TimestepEmbedder(nn.Module):
147
+ """
148
+ Embeds scalar timesteps into vector representations.
149
+ """
150
+
151
+ def __init__(self, hidden_size, frequency_embedding_size=256):
152
+ super().__init__()
153
+ self.mlp = nn.Sequential(
154
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
155
+ nn.SiLU(),
156
+ nn.Linear(hidden_size, hidden_size, bias=True),
157
+ )
158
+ self.frequency_embedding_size = frequency_embedding_size
159
+
160
+ @staticmethod
161
+ def timestep_embedding(t, dim, max_period=10000.0):
162
+ """
163
+ Create sinusoidal timestep embeddings.
164
+ :param t: a 1-D Tensor of N indices, one per batch element.
165
+ These may be fractional.
166
+ :param dim: the dimension of the output.
167
+ :param max_period: controls the minimum frequency of the embeddings.
168
+ :return: an (N, D) Tensor of positional embeddings.
169
+ """
170
+ half = dim // 2
171
+ freqs = torch.exp(
172
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
173
+ ).to(device=t.device)
174
+ args = t[:, None].float() * freqs[None]
175
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
176
+ if dim % 2:
177
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
178
+ return embedding
179
+
180
+ def forward(self, t):
181
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
182
+ t_freq = t_freq.to(self.mlp[0].weight.dtype)
183
+ t_emb = self.mlp(t_freq)
184
+ return t_emb
185
+
186
+
187
+ class IntervalEmbedder(TimestepEmbedder):
188
+ """Zero-initialized additive embedding for MeanFlow interval lengths."""
189
+
190
+ def __init__(self, hidden_size, frequency_embedding_size=256):
191
+ super().__init__(hidden_size, frequency_embedding_size)
192
+ nn.init.normal_(self.mlp[0].weight, std=0.02)
193
+ nn.init.zeros_(self.mlp[0].bias)
194
+ nn.init.zeros_(self.mlp[2].weight)
195
+ nn.init.zeros_(self.mlp[2].bias)
196
+
197
+ def forward(self, interval):
198
+ raw = super().forward(interval)
199
+ zeros = torch.zeros_like(interval)
200
+ return raw - super().forward(zeros)
201
+
202
+
203
+ class SwiGLUFFN(nn.Module):
204
+ def __init__(self, dim: int, hidden_dim: int, drop=0.0, bias=True) -> None:
205
+ super().__init__()
206
+ hidden_dim = int(hidden_dim * 2 / 3)
207
+ self.w12 = nn.Linear(dim, 2 * hidden_dim, bias=bias)
208
+ self.w3 = nn.Linear(hidden_dim, dim, bias=bias)
209
+ self.ffn_dropout = nn.Dropout(drop)
210
+
211
+ def forward(self, x):
212
+ x12 = self.w12(x)
213
+ (x1, x2) = x12.chunk(2, dim=-1)
214
+ hidden = F.silu(x1) * x2
215
+ return self.w3(self.ffn_dropout(hidden))
216
+
217
+
218
+ class PackedJiTBlock(nn.Module):
219
+ """Timestep-conditioned JiT block for one rank-local packed sequence."""
220
+
221
+ def __init__(
222
+ self,
223
+ *,
224
+ hidden_size: int = 1280,
225
+ num_heads: int = 16,
226
+ mlp_ratio: float = 4.0,
227
+ axes_dims: Sequence[int] = DEFAULT_ROPE_AXES_DIMS,
228
+ theta: float = DEFAULT_ROPE_THETA,
229
+ proj_dropout: float = 0.0,
230
+ ) -> None:
231
+ super().__init__()
232
+ if (
233
+ isinstance(mlp_ratio, bool)
234
+ or not isinstance(mlp_ratio, (int, float))
235
+ or (not math.isfinite(mlp_ratio))
236
+ or (mlp_ratio <= 0)
237
+ ):
238
+ raise ValueError(f"mlp_ratio must be positive and finite, got {mlp_ratio!r}")
239
+ self.hidden_size = hidden_size
240
+ self.proj_dropout = _validate_probability("proj_dropout", proj_dropout)
241
+ self.norm1 = RMSNorm(hidden_size, eps=1e-06)
242
+ self.attn = GroupSelfAttention(
243
+ hidden_size=hidden_size,
244
+ num_heads=num_heads,
245
+ axes_dims=axes_dims,
246
+ theta=theta,
247
+ proj_dropout=self.proj_dropout,
248
+ )
249
+ self.norm2 = RMSNorm(hidden_size, eps=1e-06)
250
+ self.mlp = SwiGLUFFN(hidden_size, int(hidden_size * mlp_ratio), drop=self.proj_dropout)
251
+ for linear in (self.mlp.w12, self.mlp.w3):
252
+ nn.init.xavier_uniform_(linear.weight)
253
+ nn.init.zeros_(linear.bias)
254
+ self.adaLN_modulation = nn.Sequential(
255
+ nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)
256
+ )
257
+ nn.init.zeros_(self.adaLN_modulation[-1].weight)
258
+ nn.init.zeros_(self.adaLN_modulation[-1].bias)
259
+
260
+ def _timestep_modulations(
261
+ self,
262
+ tokens: torch.Tensor,
263
+ timestep_embeddings: torch.Tensor,
264
+ conditioning_ids: torch.Tensor,
265
+ ) -> tuple[torch.Tensor, ...]:
266
+ return _route_packed_adaln_parameters(
267
+ tokens,
268
+ timestep_embeddings,
269
+ conditioning_ids,
270
+ modulation=self.adaLN_modulation,
271
+ hidden_size=self.hidden_size,
272
+ parameter_count=6,
273
+ )
274
+
275
+ def forward(
276
+ self,
277
+ tokens: torch.Tensor,
278
+ timestep_embeddings: torch.Tensor,
279
+ conditioning_ids: torch.Tensor,
280
+ position_ids: torch.Tensor,
281
+ block_mask: BlockMask,
282
+ rotary_factors: tuple[torch.Tensor, torch.Tensor] | None = None,
283
+ ) -> torch.Tensor:
284
+ """Transform packed tokens using per-image timesteps and fixed routing."""
285
+ (shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp) = (
286
+ self._timestep_modulations(tokens, timestep_embeddings, conditioning_ids)
287
+ )
288
+ attention_input = self.norm1(tokens) * (1 + scale_msa) + shift_msa
289
+ tokens = tokens + gate_msa * self.attn(
290
+ attention_input, position_ids, block_mask, rotary_factors
291
+ )
292
+ mlp_input = self.norm2(tokens) * (1 + scale_mlp) + shift_mlp
293
+ return tokens + gate_mlp * self.mlp(mlp_input)
294
+
295
+
296
+ class PackedFinalLayer(nn.Module):
297
+ """Predict flattened pixel patches from packed image tokens."""
298
+
299
+ def __init__(
300
+ self, *, hidden_size: int = 1280, patch_size: int = 16, out_channels: int = 3
301
+ ) -> None:
302
+ super().__init__()
303
+ for name, value in (
304
+ ("hidden_size", hidden_size),
305
+ ("patch_size", patch_size),
306
+ ("out_channels", out_channels),
307
+ ):
308
+ _validate_positive_integer(name, value)
309
+ self.hidden_size = hidden_size
310
+ self.patch_size = patch_size
311
+ self.out_channels = out_channels
312
+ self.norm_final = RMSNorm(hidden_size, eps=1e-06)
313
+ self.adaLN_modulation = nn.Sequential(
314
+ nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True)
315
+ )
316
+ self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True)
317
+ nn.init.zeros_(self.adaLN_modulation[-1].weight)
318
+ nn.init.zeros_(self.adaLN_modulation[-1].bias)
319
+ nn.init.zeros_(self.linear.weight)
320
+ nn.init.zeros_(self.linear.bias)
321
+
322
+ def forward(
323
+ self,
324
+ tokens: torch.Tensor,
325
+ timestep_embeddings: torch.Tensor,
326
+ conditioning_ids: torch.Tensor,
327
+ ) -> torch.Tensor:
328
+ """Predict one flattened pixel patch per packed image token."""
329
+ (shift, scale) = _route_packed_adaln_parameters(
330
+ tokens,
331
+ timestep_embeddings,
332
+ conditioning_ids,
333
+ modulation=self.adaLN_modulation,
334
+ hidden_size=self.hidden_size,
335
+ parameter_count=2,
336
+ )
337
+ normalized = self.norm_final(tokens) * (1 + scale) + shift
338
+ return self.linear(normalized)
339
+
340
+
341
+ class GroupJiT(nn.Module):
342
+ """The glyph-conditioned Group JiT architecture used by both releases."""
343
+
344
+ def __init__(
345
+ self,
346
+ *,
347
+ patch_size=16,
348
+ in_channels=3,
349
+ hidden_size=1280,
350
+ depth=32,
351
+ num_heads=16,
352
+ mlp_ratio=4.0,
353
+ axes_dims=(12, 34, 34),
354
+ theta=10000.0,
355
+ interval_conditioning=False,
356
+ image_attention_routing="items_interread_glyph_reads_own_item_bidirectional_main",
357
+ ):
358
+ super().__init__()
359
+ self.patch_size = patch_size
360
+ self.in_channels = in_channels
361
+ self.hidden_size = hidden_size
362
+ self.depth = depth
363
+ self.text_conditioning = TextConditioningMode.NONE
364
+ self.n_axis_mode = NAxisMode.ROLE
365
+ self.image_attention_routing = ImageAttentionRoutingMode(image_attention_routing)
366
+ self.interval_conditioning = interval_conditioning
367
+ self.cfg_conditioning = False
368
+ self.cfg_embedder = None
369
+ self.rotary = RotaryEmbedding3D(axes_dims=axes_dims, theta=theta)
370
+ self.patch_projection = DirectPatchProjection(
371
+ patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size
372
+ )
373
+ self.glyph_projection = DirectGlyphPatchProjection(
374
+ patch_size=patch_size, hidden_size=hidden_size
375
+ )
376
+ self.branch_tokenizer = PackedBranchTokenizer(
377
+ rgb_projection=self.patch_projection, glyph_projection=self.glyph_projection
378
+ )
379
+ self.timestep_embedder = TimestepEmbedder(hidden_size)
380
+ self.interval_embedder = IntervalEmbedder(hidden_size) if interval_conditioning else None
381
+ self.blocks = nn.ModuleList(
382
+ (
383
+ PackedJiTBlock(
384
+ hidden_size=hidden_size,
385
+ num_heads=num_heads,
386
+ mlp_ratio=mlp_ratio,
387
+ axes_dims=axes_dims,
388
+ theta=theta,
389
+ )
390
+ for _ in range(depth)
391
+ )
392
+ )
393
+ self.final_layer = PackedFinalLayer(
394
+ hidden_size=hidden_size, patch_size=patch_size, out_channels=in_channels
395
+ )
396
+
397
+ def forward(
398
+ self,
399
+ *,
400
+ packed_images,
401
+ prompts,
402
+ geometries,
403
+ timesteps,
404
+ glyph_conditions,
405
+ image_intervals=None,
406
+ return_packed_patches=False,
407
+ ):
408
+ if prompts is not None:
409
+ raise ValueError("PixText target text is specified by glyph images")
410
+ if glyph_conditions is None:
411
+ raise ValueError("glyph_conditions are required")
412
+ if self.interval_conditioning:
413
+ if image_intervals is None or image_intervals.shape != timesteps.images.shape:
414
+ raise ValueError("MeanFlow requires one interval per RGB image")
415
+ elif image_intervals is not None:
416
+ raise ValueError("Teacher does not accept interval conditioning")
417
+ return self._forward_canonical_glyph(
418
+ packed_images=packed_images,
419
+ prompts=None,
420
+ geometries=geometries,
421
+ timesteps=timesteps,
422
+ glyph_conditions=glyph_conditions,
423
+ return_packed_patches=return_packed_patches,
424
+ image_intervals=image_intervals,
425
+ cfg_scales=None,
426
+ )
427
+
428
+ def _forward_canonical_glyph(
429
+ self,
430
+ *,
431
+ packed_images: PackedImagePatches,
432
+ prompts: None,
433
+ geometries: Sequence[GroupGeometry],
434
+ timesteps: GroupTimesteps,
435
+ glyph_conditions: Sequence[Sequence[GlyphCondition | None]],
436
+ return_packed_patches: bool,
437
+ image_intervals: torch.Tensor | None,
438
+ cfg_scales: torch.Tensor | None,
439
+ ):
440
+ """Run mixed RGB/glyph branches while returning only RGB predictions."""
441
+ if self.branch_tokenizer is None or self.glyph_projection is None:
442
+ raise RuntimeError("canonical glyph tokenizer is not configured")
443
+ branches = self.branch_tokenizer.tokenize(
444
+ packed_rgb=packed_images, glyphs=glyph_conditions, geometries=geometries
445
+ )
446
+ projected_prompts = None
447
+ sequence = pack_branch_group_sequences(
448
+ branches=branches, prompts=projected_prompts, geometries=geometries
449
+ )
450
+ assert sequence.branch_kinds is not None
451
+ assert sequence.slot_ids is not None
452
+ sequence_timesteps = torch.ones(
453
+ sequence.tokens.shape[0], dtype=timesteps.images.dtype, device=sequence.tokens.device
454
+ )
455
+ flat_image_offset = 0
456
+ for sample_id, image_count in enumerate(packed_images.images_per_sample):
457
+ sample_mask = sequence.sample_ids.eq(sample_id)
458
+ for slot_id in range(image_count - 1):
459
+ item_mask = (
460
+ sample_mask
461
+ & sequence.branch_kinds.eq(ITEM_BRANCH)
462
+ & sequence.slot_ids.eq(slot_id)
463
+ )
464
+ sequence_timesteps[item_mask] = timesteps.images[flat_image_offset + slot_id + 1]
465
+ flat_image_offset += image_count
466
+ if not bool(sequence_timesteps[sequence.branch_kinds.eq(GLYPH_BRANCH)].eq(1).all()):
467
+ raise RuntimeError("glyph timestep must remain fixed at 1")
468
+ timestep_embeddings = self.timestep_embedder(sequence_timesteps)
469
+ if self.interval_embedder is not None:
470
+ if image_intervals is None:
471
+ raise RuntimeError("interval-aware glyph model requires intervals")
472
+ sequence_intervals = torch.zeros_like(sequence_timesteps)
473
+ flat_image_offset = 0
474
+ for sample_id, image_count in enumerate(packed_images.images_per_sample):
475
+ sample_mask = sequence.sample_ids.eq(sample_id)
476
+ for slot_id in range(image_count - 1):
477
+ item_mask = (
478
+ sample_mask
479
+ & sequence.branch_kinds.eq(ITEM_BRANCH)
480
+ & sequence.slot_ids.eq(slot_id)
481
+ )
482
+ sequence_intervals[item_mask] = image_intervals[flat_image_offset + slot_id + 1]
483
+ flat_image_offset += image_count
484
+ timestep_embeddings = timestep_embeddings + self.interval_embedder(sequence_intervals)
485
+ if self.cfg_embedder is not None:
486
+ timestep_embeddings = timestep_embeddings + self.cfg_embedder(
487
+ cfg_scales[sequence.sample_ids]
488
+ )
489
+ conditioning_ids = torch.arange(
490
+ sequence.tokens.shape[0], dtype=torch.int64, device=sequence.tokens.device
491
+ )
492
+ sequence_mask = build_flex_attention_block_mask(
493
+ sequence, image_attention_routing=self.image_attention_routing
494
+ )
495
+ rotary_factors = self.rotary.prepare(sequence.position_ids)
496
+ sequence_tokens = sequence.tokens
497
+ for block in self.blocks:
498
+ sequence_tokens = block(
499
+ sequence_tokens,
500
+ timestep_embeddings,
501
+ conditioning_ids,
502
+ sequence.position_ids,
503
+ sequence_mask,
504
+ rotary_factors,
505
+ )
506
+ branch_image_tokens = extract_image_tokens(sequence_tokens, sequence)
507
+ rgb_tokens = branch_image_tokens.index_select(0, branches.rgb_token_indices)
508
+ rgb_timestep_embeddings = self.timestep_embedder(timesteps.images)
509
+ if self.interval_embedder is not None:
510
+ assert image_intervals is not None
511
+ rgb_timestep_embeddings = rgb_timestep_embeddings + self.interval_embedder(
512
+ image_intervals
513
+ )
514
+ rgb_conditioning_ids = _flat_image_conditioning_ids(
515
+ packed_images.sample_ids, packed_images.image_ids, packed_images.images_per_sample
516
+ )
517
+ if self.final_layer is None:
518
+ raise RuntimeError("glyph conditioning requires the standard final layer")
519
+ predicted_patches = self.final_layer(
520
+ rgb_tokens, rgb_timestep_embeddings, rgb_conditioning_ids
521
+ )
522
+ if return_packed_patches:
523
+ return predicted_patches
524
+ return unpack_image_patches(predicted_patches, packed_images)