Skip to content

Commit e4e5ef1

Browse files
committed
batched encode with padded videos
1 parent 629fb1c commit e4e5ef1

3 files changed

Lines changed: 332 additions & 61 deletions

File tree

src/maxtext/layers/embeddings.py

Lines changed: 163 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,44 +1401,120 @@ def __call__(
14011401
height: int,
14021402
width: int,
14031403
token_mask: Array | None = None,
1404-
valid_grid: tuple[int, int, int] | None = None,
1404+
valid_grid: Array | None = None,
14051405
) -> Array:
14061406
"""Apply rotary position embeddings directly to inputs (Q or K tensors).
14071407
14081408
Args:
1409-
inputs: Input tensor of shape [B, T*H*W, N, head_dim] (batch, sequence, heads, head_dim)
1410-
where T=num_frames, H=height, W=width (all static)
1409+
inputs: Input tensor of shape [B, S, N, head_dim] (batch, sequence, heads, head_dim)
1410+
where sequence length S = num_frames * height * width (static maximum).
14111411
num_frames: Number of temporal frames (static)
14121412
height: Height in patches (static)
14131413
width: Width in patches (static)
14141414
token_mask: Optional mask identifying valid tokens in the padded sequence.
1415-
valid_grid: Optional unpadded `(frames, height, width)` grid used to compute
1416-
RoPE for valid tokens.
1415+
valid_grid: Optional actual video grid with shape (batch, 3) (dynamic)
14171416
14181417
Returns:
1419-
Rotated inputs with same shape [B, T*H*W, N, head_dim]
1418+
Rotated inputs with same shape [B, S, N, head_dim]
14201419
"""
1421-
cos_emb, sin_emb = self.compute_cos_sin(num_frames, height, width)
1422-
if token_mask is not None and valid_grid is not None:
1423-
valid_cos, valid_sin = self.compute_cos_sin(*valid_grid)
1424-
valid_len = math.prod(valid_grid)
1425-
valid_indices = jnp.nonzero(token_mask[0], size=valid_len)[0]
1426-
cos_emb = jnp.ones_like(cos_emb).at[valid_indices].set(valid_cos)
1427-
sin_emb = jnp.zeros_like(sin_emb).at[valid_indices].set(valid_sin)
1428-
1429-
if len(inputs.shape) == 4:
1430-
cos_emb = cos_emb[None, :, None, :] # [1, S, 1, H]
1431-
sin_emb = sin_emb[None, :, None, :]
1432-
elif len(inputs.shape) == 3:
1433-
# For [S, N, H] case
1434-
cos_emb = cos_emb[:, None, :] # [S, 1, H]
1435-
sin_emb = sin_emb[:, None, :]
1420+
is_3d = inputs.ndim == 3
1421+
if is_3d:
1422+
inputs = inputs[None, :, :, :]
1423+
1424+
batch_size = inputs.shape[0]
1425+
max_patches = num_frames * height * width
1426+
1427+
if valid_grid is not None:
1428+
if isinstance(valid_grid, (tuple, list)):
1429+
valid_grid = jnp.array([valid_grid], dtype=jnp.int32)
1430+
elif valid_grid.ndim == 1:
1431+
valid_grid = valid_grid[None, :]
1432+
else:
1433+
valid_grid = jnp.tile(
1434+
jnp.array([[num_frames, height, width]], dtype=jnp.int32),
1435+
(batch_size, 1),
1436+
)
1437+
1438+
# Generate coordinates in block-based order
1439+
row, col = generate_block_coords(valid_grid, max_patches, self.spatial_merge_size)
1440+
1441+
# Compute frequencies dynamically using coordinates
1442+
max_hw = max(height, width)
1443+
freq_table = self._compute_freq_table(max_hw) # [max_hw, head_dim//4]
1444+
1445+
row_freqs = freq_table[row] # [B, max_patches, head_dim//4]
1446+
col_freqs = freq_table[col] # [B, max_patches, head_dim//4]
1447+
1448+
# Concatenate row and column frequencies
1449+
embeddings = jnp.concatenate([row_freqs, col_freqs], axis=-1) # [B, max_patches, head_dim//2]
1450+
1451+
# Double the embeddings to match head_dim
1452+
embeddings = jnp.concatenate([embeddings, embeddings], axis=-1) # [B, max_patches, head_dim]
1453+
1454+
cos_emb = jnp.cos(embeddings)
1455+
sin_emb = jnp.sin(embeddings)
1456+
1457+
# Mask out invalid (padded) tokens by setting cos to 1 and sin to 0
1458+
if token_mask is not None:
1459+
is_valid = token_mask[:, :, None] # [B, max_patches, 1]
1460+
cos_emb = jnp.where(is_valid, cos_emb, 1.0)
1461+
sin_emb = jnp.where(is_valid, sin_emb, 0.0)
1462+
1463+
if self.cast_as_fprop_dtype:
1464+
cos_emb = cos_emb.astype(self.fprop_dtype)
1465+
sin_emb = sin_emb.astype(self.fprop_dtype)
1466+
1467+
# Reshape for broadcasting to inputs [B, S, N, H]
1468+
cos_emb = cos_emb[:, :, None, :] # [B, S, 1, H]
1469+
sin_emb = sin_emb[:, :, None, :]
14361470

14371471
rotated = inputs * cos_emb + self._rotate_half(inputs) * sin_emb
14381472

1473+
if is_3d:
1474+
rotated = rotated[0]
1475+
14391476
return rotated
14401477

14411478

1479+
def generate_block_coords(video_grid_thw: Array, max_patches: int, merge_size: int) -> tuple[Array, Array]:
1480+
"""Generate row and col coordinates in block-based order for padded video.
1481+
1482+
Args:
1483+
video_grid_thw: Actual video grid with shape (batch, 3), in Qwen grid units.
1484+
max_patches: Maximum number of patches (static).
1485+
merge_size: Spatial merge block size (static).
1486+
1487+
Returns:
1488+
Tuple of (row, col) each of shape [batch, max_patches]
1489+
"""
1490+
V_T = video_grid_thw[:, 0:1] # [B, 1]
1491+
V_H = video_grid_thw[:, 1:2]
1492+
V_W = video_grid_thw[:, 2:3]
1493+
1494+
merged_w = V_W // merge_size
1495+
V_len = V_T * V_H * V_W
1496+
1497+
idx = jnp.arange(max_patches, dtype=jnp.int32)[None, :] # [1, max_patches]
1498+
is_valid = idx < V_len # [B, max_patches]
1499+
1500+
stride_lh = V_H * V_W
1501+
safe_stride_lh = jnp.maximum(stride_lh, 1)
1502+
s_idx = idx % safe_stride_lh
1503+
1504+
intra_col = s_idx % merge_size
1505+
intra_row = (s_idx // merge_size) % merge_size
1506+
1507+
safe_merged_w = jnp.maximum(merged_w, 1)
1508+
block_elements = merge_size * merge_size
1509+
block_col = (s_idx // block_elements) % safe_merged_w
1510+
block_row = s_idx // (safe_merged_w * block_elements)
1511+
1512+
row = jnp.where(is_valid, block_row * merge_size + intra_row, 0)
1513+
col = jnp.where(is_valid, block_col * merge_size + intra_col, 0)
1514+
1515+
return row, col
1516+
1517+
14421518
def qwen3omnimoe_vision_pos_embed_interpolate_as_linen(
14431519
*,
14441520
num_position_embeddings: int,
@@ -1596,46 +1672,87 @@ def _interpolate_single(self, t: int, h: int, w: int) -> tuple[Array, Array]:
15961672

15971673
return indices, weights
15981674

1599-
def __call__(self, num_frames: int, height: int, width: int) -> Array:
1675+
def __call__(
1676+
self,
1677+
num_frames: int,
1678+
height: int,
1679+
width: int,
1680+
video_grid_thw: Array | None = None,
1681+
attention_mask: Array | None = None,
1682+
) -> Array:
16001683
"""Interpolate positional embeddings for given static grid dimensions.
16011684
16021685
Args:
16031686
num_frames: Number of temporal frames (static)
16041687
height: Height in patches (static)
16051688
width: Width in patches (static)
1689+
video_grid_thw: Optional actual video grid with shape (batch, 3) (dynamic)
1690+
attention_mask: Optional attention mask with shape (batch, max_patches) (dynamic)
16061691
16071692
Returns:
1608-
Interpolated positional embeddings of shape [num_frames * height * width, hidden_size]
1693+
Interpolated positional embeddings of shape [batch, num_frames * height * width, hidden_size]
16091694
"""
1610-
# Get interpolation indices and weights
1611-
indices, weights = self._interpolate_single(num_frames, height, width) # [4, h*w], [4, h*w]
1695+
if video_grid_thw is not None:
1696+
if isinstance(video_grid_thw, (tuple, list)):
1697+
video_grid_thw = jnp.array([video_grid_thw], dtype=jnp.int32)
1698+
elif video_grid_thw.ndim == 1:
1699+
video_grid_thw = video_grid_thw[None, :]
1700+
batch_size = video_grid_thw.shape[0]
1701+
elif attention_mask is not None:
1702+
batch_size = attention_mask.shape[0]
1703+
else:
1704+
batch_size = 1
16121705

1613-
# Lookup embeddings for all 4 corners
1614-
corner_embeds = self.pos_embed.value[indices] # [4, h*w, hidden_size]
1706+
max_patches = num_frames * height * width
16151707

1616-
# Apply bilinear weights and sum
1617-
weighted_embeds = corner_embeds * weights[:, :, None] # [4, h*w, hidden_size]
1618-
interpolated = jnp.sum(weighted_embeds, axis=0) # [h*w, hidden_size]
1708+
if video_grid_thw is None:
1709+
video_grid_thw = jnp.tile(
1710+
jnp.array([[num_frames, height, width]], dtype=jnp.int32),
1711+
(batch_size, 1),
1712+
)
1713+
if attention_mask is None:
1714+
attention_mask = jnp.ones((batch_size, max_patches), dtype=jnp.int32)
16191715

1620-
# Repeat for temporal frames
1621-
if num_frames > 1:
1622-
interpolated = jnp.tile(interpolated, (num_frames, 1)) # [t*h*w, hidden_size]
1716+
# Generate coordinates in block-based order
1717+
row, col = generate_block_coords(video_grid_thw, max_patches, self.spatial_merge_size)
16231718

1624-
# Apply spatial merge permutation
1625-
# Reshape to [t, h, w, hidden_size] then permute for block-based processing
1626-
merge_size = self.spatial_merge_size
1627-
merged_h = height // merge_size
1628-
merged_w = width // merge_size
1719+
# Normalize coordinates to [0, 1] range based on valid grid sizes
1720+
V_H = video_grid_thw[:, 1:2]
1721+
V_W = video_grid_thw[:, 2:3]
1722+
row_norm = row / jnp.maximum(V_H - 1, 1)
1723+
col_norm = col / jnp.maximum(V_W - 1, 1)
1724+
1725+
# Interpolate from N x N grid
1726+
N = self.num_grid_per_side
1727+
table = self.pos_embed.value.reshape(N, N, self.hidden_size)
16291728

1630-
# Reshape: [t*h*w, hidden_size] -> [t, h, w, hidden_size]
1631-
interpolated = interpolated.reshape(num_frames, height, width, self.hidden_size)
1729+
y = row_norm * (N - 1)
1730+
x = col_norm * (N - 1)
1731+
1732+
y0 = jnp.floor(y).astype(jnp.int32)
1733+
x0 = jnp.floor(x).astype(jnp.int32)
1734+
y1 = jnp.minimum(y0 + 1, N - 1)
1735+
x1 = jnp.minimum(x0 + 1, N - 1)
1736+
1737+
dy = y - y0
1738+
dx = x - x0
1739+
1740+
# Gather 4 corners
1741+
embed_00 = table[y0, x0]
1742+
embed_01 = table[y0, x1]
1743+
embed_10 = table[y1, x0]
1744+
embed_11 = table[y1, x1]
1745+
1746+
# Apply bilinear weights
1747+
dy = dy[:, :, None]
1748+
dx = dx[:, :, None]
1749+
1750+
interpolated = (
1751+
(1.0 - dy) * (1.0 - dx) * embed_00 + (1.0 - dy) * dx * embed_01 + dy * (1.0 - dx) * embed_10 + dy * dx * embed_11
1752+
)
16321753

1633-
# Permute for spatial merging: [t, merged_h, merge_size, merged_w, merge_size, hidden_size]
1634-
interpolated = interpolated.reshape(num_frames, merged_h, merge_size, merged_w, merge_size, self.hidden_size)
1635-
# -> [t, merged_h, merged_w, merge_size, merge_size, hidden_size]
1636-
interpolated = jnp.transpose(interpolated, (0, 1, 3, 2, 4, 5))
1637-
# Flatten back to [t*merged_h*merged_w*merge_size*merge_size, hidden_size]
1638-
interpolated = interpolated.reshape(-1, self.hidden_size)
1754+
# Mask out invalid (padded) tokens by setting their embeddings to zero
1755+
interpolated = interpolated * attention_mask[:, :, None]
16391756

16401757
if self.cast_as_fprop_dtype:
16411758
interpolated = interpolated.astype(self.fprop_dtype)

src/maxtext/models/qwen3.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,22 +2136,17 @@ def __call__(
21362136

21372137
x, _ = self.patch_embed(hidden_states)
21382138
x = x.reshape(batch_size, -1, self.config.hidden_size_for_vit)
2139-
valid_grid = None
21402139
if attention_mask is not None and video_grid_thw is None:
21412140
raise ValueError("video_grid_thw is required when video_mask is provided.")
2142-
if attention_mask is not None and batch_size != 1:
2143-
raise ValueError("Padded Qwen3-Omni vision encoding currently supports batch size one.")
2144-
if video_grid_thw is not None:
2145-
grid = video_grid_thw[0] if getattr(video_grid_thw, "ndim", 1) == 2 else video_grid_thw
2146-
valid_grid = tuple(int(dim) for dim in grid)
2147-
pos = self.pos_embed_interpolate(num_frames, height, width)
2148-
if attention_mask is not None and valid_grid is not None:
2149-
valid_pos = self.pos_embed_interpolate(*valid_grid)
2150-
valid_indices = jnp.nonzero(attention_mask[0], size=math.prod(valid_grid))[0]
2151-
pos = jnp.zeros_like(pos).at[valid_indices].set(valid_pos)
2152-
2153-
pos = pos[jnp.newaxis, :, :]
2141+
pos = self.pos_embed_interpolate(
2142+
num_frames,
2143+
height,
2144+
width,
2145+
video_grid_thw=video_grid_thw,
2146+
attention_mask=attention_mask,
2147+
)
21542148
x = x + pos
2149+
valid_grid = video_grid_thw
21552150

21562151
h_traj = []
21572152
for i in range(self.depth):

0 commit comments

Comments
 (0)