Skip to content

Commit 2bd2d3b

Browse files
committed
feat(wan): token-sharded patch embedding (wan_patch_embed_mode="tokens")
Second of four pieces split out of the Wan fast-inference optimizations PR. The switch defaults off ("conv") in the Wan YAMLs and in wan_runtime_options; it is a plain config key plumbed to WanModel through `attention_config_entries(config)` and resolved at build time (`_wan_opt`) so it lives on the GraphDef. - With `"tokens"`, the Conv3d patch embedding becomes a patchify + matmul on the token-sharded layout (`embed_patches_as_tokens`): the non-overlapping patches (stride == kernel) are flattened in the kernel's `(p_t, p_h, p_w, C)` order, sharded by token and multiplied by the replicated `[p_t*p_h*p_w*C, D]` kernel (with kernel and bias cast to `hidden_states.dtype` to match `nnx.Conv.promote_dtype`). Same result as `collapse(conv(x), 1, -1)` up to accumulation order, but produced directly in the layout the transformer blocks use, so XLA no longer emits the activation all-gather / collective-permute when collapsing `(F, H, W)` into the context-sharded token axis (12.8 ms per step on tpu7x, exposed). - `can_embed_patches_as_tokens` checks the conv really is a plain stride-equals-kernel 3D patchify (no groups, masks, dilation, and `'SAME'` pads nothing). When `wan_patch_embed_mode="tokens"` is requested on an indivisible latent grid, `WanModel.__call__` raises `ValueError` instead of silently falling back to `nnx.Conv`. - `run_wan_fast_inference.sh`: every profile sets `WAN_PATCH_EMBED_MODE=tokens` (overridable). Tests - `wan_patch_embed_test.py` (8): tokens vs conv parity (function level and through `WanModel`), the eligibility predicate, fail-closed `ValueError` on indivisible latent grids, and the config-driven mode. All 8 tests run unconditionally in GitHub Actions. - `wan_runtime_options_test.py` (12): the new switch's YAML default and enum validation; `wan_patch_embed_mode` is excluded from the `FlaxWanAttention` attribute check since `WanModel` consumes it. - `end_to_end/tpu/run_wan_stack_tests.sh`: adds `wan_patch_embed_test.py`.
1 parent 56acb00 commit 2bd2d3b

12 files changed

Lines changed: 298 additions & 4 deletions

‎end_to_end/tpu/run_wan_fast_inference.sh‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
# v6e seed 777, 17.92 dB / 0.6556 on v6e seed 12345, and
6969
# 17.69 dB / 0.6918 on tpu7x seed 12345 (visually clean).
7070
# USE_FUSED_ROPE_KERNEL=false restores the XLA producer.
71-
# WAN_CROSS_ATTN_KERNEL
71+
# WAN_CROSS_ATTN_KERNEL / WAN_PATCH_EMBED_MODE
7272
# override Wan fast-inference kernel/layout switches
7373
# DP / CP / PER_DEVICE_BATCH / SEED
7474
# override mesh parallelism, per-device batch, or RNG seed (default 12345)
@@ -182,6 +182,7 @@ case "$TPU_PROFILE" in
182182
DEFAULT_VAE_SPATIAL=8
183183
DEFAULT_FUSED_ROPE=true
184184
DEFAULT_CROSS_ATTN_KERNEL=pallas
185+
DEFAULT_PATCH_EMBED_MODE=tokens
185186
;;
186187
v7)
187188
PLATFORM_LIBTPU="$V7_LIBTPU"
@@ -199,6 +200,7 @@ case "$TPU_PROFILE" in
199200
DEFAULT_VAE_SPATIAL=8
200201
DEFAULT_FUSED_ROPE=true
201202
DEFAULT_CROSS_ATTN_KERNEL=pallas
203+
DEFAULT_PATCH_EMBED_MODE=tokens
202204
;;
203205
*)
204206
echo "== warning: unrecognised or non-v6e/v7 accelerator '${ACCEL_TYPE:-unknown}';" \
@@ -218,6 +220,7 @@ case "$TPU_PROFILE" in
218220
DEFAULT_VAE_SPATIAL=8
219221
DEFAULT_FUSED_ROPE=false
220222
DEFAULT_CROSS_ATTN_KERNEL=xla
223+
DEFAULT_PATCH_EMBED_MODE=tokens
221224
;;
222225
esac
223226

@@ -250,6 +253,7 @@ FUSED_ROPE_ARGS=()
250253
[ -n "${FUSED_ROPE_BLOCK_S:-}" ] && FUSED_ROPE_ARGS+=("fused_rope_block_s=$FUSED_ROPE_BLOCK_S")
251254
[ -n "${FUSED_ROPE_HEAD_BLOCK:-}" ] && FUSED_ROPE_ARGS+=("fused_rope_head_block=$FUSED_ROPE_HEAD_BLOCK")
252255
WAN_CROSS_ATTN_KERNEL=${WAN_CROSS_ATTN_KERNEL:-$DEFAULT_CROSS_ATTN_KERNEL}
256+
WAN_PATCH_EMBED_MODE=${WAN_PATCH_EMBED_MODE:-$DEFAULT_PATCH_EMBED_MODE}
253257

254258
# Mesh: context parallelism carries the Ulysses shards, data parallelism takes
255259
# whatever devices remain. Defaults to CP=4 / DP=2 on an 8-device slice. On a
@@ -317,6 +321,7 @@ python src/maxdiffusion/generate_wan.py "$CONFIG" \
317321
use_fused_rope_kernel="$USE_FUSED_ROPE_KERNEL" \
318322
${FUSED_ROPE_ARGS[@]+"${FUSED_ROPE_ARGS[@]}"} \
319323
wan_cross_attn_kernel="$WAN_CROSS_ATTN_KERNEL" \
324+
wan_patch_embed_mode="$WAN_PATCH_EMBED_MODE" \
320325
fps=16 ${GUIDANCE_ARGS[@]+"${GUIDANCE_ARGS[@]}"} \
321326
seed="${SEED:-12345}" \
322327
flash_block_sizes="$FLASH_BLOCK_SIZES" \

‎end_to_end/tpu/run_wan_stack_tests.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@ TESTS=(
4747
"$T/wan_runtime_options_test.py"
4848
# Short-KV cross-attention Pallas kernel (feat/wan-cross-attn-pallas)
4949
"$T/cross_attention_pallas_test.py"
50+
# Token-sharded patch embedding (feat/wan-token-patch-embed)
51+
"$T/wan_patch_embed_test.py"
5052
)
5153
PYTHONPATH="src${PYTHONPATH:+:$PYTHONPATH}" exec "${PYTHON:-python3}" -m pytest -q -rs "${TESTS[@]}" "$@"

‎src/maxdiffusion/configs/base_wan_14b.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ wan_rope_accum: "auto"
136136
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
137137
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
138138
wan_cross_attn_kernel: "xla"
139+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
140+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
141+
# replicated kernel, no activation collective; equal up to accumulation order).
142+
wan_patch_embed_mode: "conv"
139143
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
140144
ulysses_shards: -1
141145
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/configs/base_wan_1_3b.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ wan_rope_accum: "auto"
133133
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
134134
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
135135
wan_cross_attn_kernel: "xla"
136+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
137+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
138+
# replicated kernel, no activation collective; equal up to accumulation order).
139+
wan_patch_embed_mode: "conv"
136140
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
137141
ulysses_shards: -1
138142
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/configs/base_wan_27b.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ wan_rope_accum: "auto"
170170
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
171171
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
172172
wan_cross_attn_kernel: "xla"
173+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
174+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
175+
# replicated kernel, no activation collective; equal up to accumulation order).
176+
wan_patch_embed_mode: "conv"
173177
# For attention=ulysses_ring*, hidden Ulysses shard count; ring shards are context / this.
174178
ulysses_shards: -1
175179
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/configs/base_wan_animate.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ wan_rope_accum: "auto"
115115
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
116116
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
117117
wan_cross_attn_kernel: "xla"
118+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
119+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
120+
# replicated kernel, no activation collective; equal up to accumulation order).
121+
wan_patch_embed_mode: "conv"
118122
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
119123
ulysses_shards: -1
120124
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/configs/base_wan_i2v_14b.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ wan_rope_accum: "auto"
117117
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
118118
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
119119
wan_cross_attn_kernel: "xla"
120+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
121+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
122+
# replicated kernel, no activation collective; equal up to accumulation order).
123+
wan_patch_embed_mode: "conv"
120124
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
121125
ulysses_shards: -1
122126
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/configs/base_wan_i2v_27b.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ wan_rope_accum: "auto"
117117
# (KV shorter than flash_min_seq_length): "xla", or "pallas" for the fused kernel that
118118
# keeps the KV in VMEM (within bf16 rounding of "xla", not bit-identical).
119119
wan_cross_attn_kernel: "xla"
120+
# Patch embedding: "conv" (nnx.Conv; its channel-sharded output is resharded by
121+
# token, an all-gather on tpu7x) or "tokens" (token-sharded patches times the
122+
# replicated kernel, no activation collective; equal up to accumulation order).
123+
wan_patch_embed_mode: "conv"
120124
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
121125
ulysses_shards: -1
122126
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.

‎src/maxdiffusion/models/wan/transformers/transformer_wan.py‎

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,62 @@ def get_frequencies(max_seq_len: int, theta: int, attention_head_dim: int):
6161
return freqs_split
6262

6363

64+
PATCH_EMBED_MODES = ("conv", "tokens")
65+
66+
67+
def can_embed_patches_as_tokens(conv: nnx.Conv, hidden_shape: Tuple[int, ...], patch_size: Tuple[int, int, int]) -> bool:
68+
"""Whether `embed_patches_as_tokens` computes exactly what `conv` does."""
69+
70+
def per_dim(value):
71+
value = 1 if value is None else value
72+
return tuple(value) if isinstance(value, (tuple, list)) else (value,) * len(patch_size)
73+
74+
if len(hidden_shape) != 5 or per_dim(conv.kernel_size) != tuple(patch_size):
75+
return False
76+
if per_dim(conv.strides) != tuple(patch_size) or conv.feature_group_count != 1 or conv.mask is not None:
77+
return False
78+
if per_dim(conv.input_dilation) != (1,) * 3 or per_dim(conv.kernel_dilation) != (1,) * 3:
79+
return False
80+
# With every spatial size divisible by its stride, 'SAME' pads nothing.
81+
return conv.padding in ("SAME", "VALID") and all(n % p == 0 for n, p in zip(hidden_shape[1:4], patch_size))
82+
83+
84+
def embed_patches_as_tokens(conv: nnx.Conv, hidden_states: jax.Array, patch_size: Tuple[int, int, int]) -> jax.Array:
85+
"""Wan patch embedding as a token-sharded matmul: `[B, F, H, W, C]` -> `[B, N, D]`.
86+
87+
Same result as `jax.lax.collapse(conv(hidden_states), 1, -1)` up to
88+
accumulation order, but produced in the layout the transformer blocks use.
89+
When the 3D `nnx.Conv` runs before `collapse`, XLA either shards the conv output
90+
differently from `activation_length` or emits an activation all-gather /
91+
collective-permute when collapsing `(F, H, W)` into the context-sharded token
92+
axis (12.8 ms per step on tpu7x, exposed). Patches do not overlap (stride ==
93+
kernel), so each token needs only its own patch: flatten the patches in the
94+
kernel's `(p_t, p_h, p_w, C)` order, shard them by token and multiply by the
95+
replicated `[p_t*p_h*p_w*C, D]` kernel.
96+
"""
97+
p_t, p_h, p_w = patch_size
98+
batch, frames, height, width, channels = hidden_states.shape
99+
patches = hidden_states.reshape(batch, frames // p_t, p_t, height // p_h, p_h, width // p_w, p_w, channels)
100+
patches = jnp.transpose(patches, (0, 1, 3, 5, 2, 4, 6, 7))
101+
patches = patches.reshape(batch, -1, p_t * p_h * p_w * channels)
102+
patches = nn.with_logical_constraint(patches, ("activation_batch", "activation_length", None))
103+
104+
kernel = conv.kernel[...].reshape(p_t * p_h * p_w * channels, -1)
105+
kernel = nn.with_logical_constraint(kernel, (None, None))
106+
bias = conv.bias[...] if conv.bias is not None else None
107+
# Mirror nnx.Conv: promote everything to `conv.dtype`, no preferred type.
108+
dtype = conv.dtype or jnp.result_type(patches, kernel)
109+
out = jax.lax.dot_general(
110+
patches.astype(dtype),
111+
kernel.astype(dtype),
112+
(((2,), (0,)), ((), ())),
113+
precision=conv.precision,
114+
)
115+
if bias is not None:
116+
out = out + bias.astype(dtype)
117+
return out
118+
119+
64120
class WanRotaryPosEmbed(nnx.Module):
65121

66122
def __init__(
@@ -609,6 +665,13 @@ def __init__(
609665
**(attention_config or {}),
610666
}
611667

668+
# Resolved at build time so they live on the GraphDef; None -> built-in default.
669+
def _wan_opt(name):
670+
value = attention_config.get(name)
671+
return wan_runtime_options.default(name) if value is None else wan_runtime_options.coerce(name, value)
672+
673+
self.patch_embed_mode = _wan_opt("wan_patch_embed_mode")
674+
612675
# 1. Patch & position embedding
613676
self.rope = WanRotaryPosEmbed(attention_head_dim, patch_size, rope_max_seq_len)
614677
self.patch_embedding = nnx.Conv(
@@ -810,8 +873,19 @@ def __call__(
810873
if rotary_emb is None:
811874
rotary_emb = self.rope(hidden_states)
812875
with self.conditional_named_scope("patch_embedding"):
813-
hidden_states = self.patch_embedding(hidden_states)
814-
hidden_states = jax.lax.collapse(hidden_states, 1, -1)
876+
patch_embed_mode = self.patch_embed_mode
877+
if patch_embed_mode not in PATCH_EMBED_MODES:
878+
raise ValueError(f"wan_patch_embed_mode must be one of {PATCH_EMBED_MODES}, got {patch_embed_mode!r}.")
879+
if patch_embed_mode == "tokens":
880+
if not can_embed_patches_as_tokens(self.patch_embedding, hidden_states.shape, self.config.patch_size):
881+
raise ValueError(
882+
f"wan_patch_embed_mode='tokens' requires the latent grid {hidden_states.shape[1:4]} to be divisible by "
883+
f"patch_size {tuple(self.config.patch_size)} (height/width multiples of {8 * p_h}/{8 * p_w} pixels)."
884+
)
885+
hidden_states = embed_patches_as_tokens(self.patch_embedding, hidden_states, self.config.patch_size)
886+
else:
887+
hidden_states = self.patch_embedding(hidden_states)
888+
hidden_states = jax.lax.collapse(hidden_states, 1, -1)
815889
per_token_t = timestep.ndim == 2 # [B, seq_len] for TI2V
816890
with self.conditional_named_scope("condition_embedder"):
817891
if per_token_t:

0 commit comments

Comments
 (0)