Spaces:
Running on Zero
Running on Zero
Add PixText transformer and attention modules
Browse files- vendor/pixtext/modeling/__init__.py +0 -0
- vendor/pixtext/modeling/attention.py +541 -0
- vendor/pixtext/modeling/branches.py +294 -0
- vendor/pixtext/modeling/conditioning.py +62 -0
- vendor/pixtext/modeling/modes.py +69 -0
- vendor/pixtext/modeling/norm.py +19 -0
- vendor/pixtext/modeling/patches.py +298 -0
- vendor/pixtext/modeling/rope.py +350 -0
- vendor/pixtext/modeling/transformer.py +524 -0
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)
|