Skip to content

Commit 42f8dfb

Browse files
committed
resolving merge conflicts and updating to latest main
2 parents bdaeddc + 5fe8bcf commit 42f8dfb

8 files changed

Lines changed: 590 additions & 139 deletions

File tree

src/maxdiffusion/configuration_utils.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ def load_config(
386386
else:
387387
try:
388388
# Load from URL or cache if already cached
389-
config_file = hf_hub_download(
389+
config_file = hf_hub_download( # pyrefly: ignore[no-matching-overload]
390390
pretrained_model_name_or_path,
391391
filename=cls.config_name,
392392
cache_dir=cache_dir,
@@ -461,7 +461,7 @@ def load_config(
461461
if return_commit_hash:
462462
outputs += (commit_hash,)
463463

464-
return outputs
464+
return outputs # pyrefly: ignore[bad-return]
465465

466466
@staticmethod
467467
def _get_init_keys(cls):
@@ -496,7 +496,9 @@ def extract_init_dict(cls, config_dict, **kwargs):
496496
diffusers_library = importlib.import_module(__name__.split(".")[0])
497497

498498
if cls.has_compatibles:
499-
compatible_classes = [c for c in cls._get_compatibles() if not isinstance(c, DummyObject)]
499+
compatible_classes = [
500+
c for c in cls._get_compatibles() if not isinstance(c, DummyObject)
501+
] # pyrefly: ignore[missing-attribute]
500502
else:
501503
compatible_classes = []
502504

@@ -691,7 +693,7 @@ def init(self, *args, **kwargs):
691693
init_kwargs = dict(kwargs.items())
692694

693695
# Retrieve default values
694-
fields = dataclasses.fields(self)
696+
fields = dataclasses.fields(self) # pyrefly: ignore[bad-argument-type]
695697
default_kwargs = {}
696698
for field in fields:
697699
# ignore flax specific attributes

src/maxdiffusion/generate_wan.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ def get_git_commit_hash():
8989
jax.config.update("jax_use_shardy_partitioner", True)
9090

9191

92-
def call_pipeline(config, pipeline, prompt, negative_prompt):
92+
def call_pipeline(config, pipeline, prompt, negative_prompt, num_inference_steps=None):
9393
model_key = config.model_name
9494
model_type = config.model_type
95+
if num_inference_steps is None:
96+
num_inference_steps = config.num_inference_steps
9597
if model_type == "I2V":
9698
image = load_image(config.image_url)
9799
if model_key == WAN2_1:
@@ -102,7 +104,7 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
102104
height=config.height,
103105
width=config.width,
104106
num_frames=config.num_frames,
105-
num_inference_steps=config.num_inference_steps,
107+
num_inference_steps=num_inference_steps,
106108
guidance_scale=config.guidance_scale,
107109
use_magcache=config.use_magcache,
108110
magcache_thresh=config.magcache_thresh,
@@ -118,7 +120,7 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
118120
height=config.height,
119121
width=config.width,
120122
num_frames=config.num_frames,
121-
num_inference_steps=config.num_inference_steps,
123+
num_inference_steps=num_inference_steps,
122124
guidance_scale_low=config.guidance_scale_low,
123125
guidance_scale_high=config.guidance_scale_high,
124126
use_cfg_cache=config.use_cfg_cache,
@@ -139,7 +141,7 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
139141
height=config.height,
140142
width=config.width,
141143
num_frames=config.num_frames,
142-
num_inference_steps=config.num_inference_steps,
144+
num_inference_steps=num_inference_steps,
143145
guidance_scale=config.guidance_scale,
144146
use_cfg_cache=config.use_cfg_cache,
145147
use_magcache=config.use_magcache,
@@ -155,7 +157,7 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
155157
height=config.height,
156158
width=config.width,
157159
num_frames=config.num_frames,
158-
num_inference_steps=config.num_inference_steps,
160+
num_inference_steps=num_inference_steps,
159161
guidance_scale_low=config.guidance_scale_low,
160162
guidance_scale_high=config.guidance_scale_high,
161163
use_cfg_cache=config.use_cfg_cache,
@@ -290,9 +292,17 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
290292
max_logging.log(
291293
f"Num steps: {config.num_inference_steps}, height: {config.height}, width: {config.width}, frames: {config.num_frames}"
292294
)
293-
videos = call_pipeline(config, pipeline, prompt, negative_prompt)
295+
# Warmup with 2 denoising steps instead of a full run: step 0 runs the
296+
# high-noise transformer and step 1 crosses the boundary to the low-noise
297+
# one (WAN 2.2), so every executable of the full run (both transformers,
298+
# text encoder, VAE decode) gets compiled at a fraction of the cost. The
299+
# step count only changes the Python loop trip count, not traced shapes.
300+
warmup_steps = min(2, config.num_inference_steps)
301+
max_logging.log(f"Compile warmup: {warmup_steps} denoising steps")
302+
videos = call_pipeline(config, pipeline, prompt, negative_prompt, num_inference_steps=warmup_steps)
294303
if isinstance(videos, tuple):
295-
videos = videos[0]
304+
videos, warmup_trace = videos
305+
max_logging.log("Warmup breakdown: " + ", ".join(f"{stage}={seconds:.1f}s" for stage, seconds in warmup_trace.items()))
296306

297307
max_logging.log("===================== Model details =======================")
298308
max_logging.log(f"model name: {config.model_name}")
@@ -307,13 +317,6 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
307317
max_logging.log(f"compile_time: {compile_time}")
308318
if writer and jax.process_index() == 0:
309319
writer.add_scalar("inference/compile_time", compile_time, global_step=0)
310-
saved_video_path = []
311-
for i in range(len(videos)):
312-
video_path = f"{filename_prefix}wan_output_{config.seed}_{i}.mp4"
313-
export_to_video(videos[i], video_path, fps=config.fps)
314-
saved_video_path.append(video_path)
315-
if config.output_dir.startswith("gs://"):
316-
upload_video_to_gcs(os.path.join(config.output_dir, config.run_name), video_path)
317320

318321
s0 = time.perf_counter()
319322
outputs = call_pipeline(config, pipeline, prompt, negative_prompt)
@@ -323,6 +326,13 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
323326
videos = outputs
324327
trace = {}
325328
generation_time = time.perf_counter() - s0
329+
saved_video_path = []
330+
for i in range(len(videos)):
331+
video_path = f"{filename_prefix}wan_output_{config.seed}_{i}.mp4"
332+
export_to_video(videos[i], video_path, fps=config.fps)
333+
saved_video_path.append(video_path)
334+
if config.output_dir.startswith("gs://"):
335+
upload_video_to_gcs(os.path.join(config.output_dir, config.run_name), video_path)
326336
max_logging.log(f"generation_time: {generation_time}")
327337
if writer and jax.process_index() == 0:
328338
writer.add_scalar("inference/generation_time", generation_time, global_step=0)

src/maxdiffusion/models/attention_flax.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,42 @@ def convert_to_tokamax_splash_config(
349349
)
350350

351351

352+
def _extract_custom_block_sizes(flash_block_sizes):
353+
"""Pulls custom-kernel block sizes out of the (dict or BlockSizes-like) config.
354+
355+
Mirrors the extraction used by the `ulysses_custom` path so the custom ring
356+
kernel honors the same `flash_block_sizes={...}` knobs.
357+
"""
358+
bq = 4864
359+
bkv = 1024
360+
bkv_compute = 1024
361+
bkv_compute_in = 1024
362+
heads_per_tile = 1
363+
vmem_limit_bytes = None
364+
if flash_block_sizes is not None:
365+
if isinstance(flash_block_sizes, dict):
366+
get = flash_block_sizes.get
367+
bq = get("block_q", None) or bq
368+
bkv = get("block_kv", None) or bkv
369+
bkv_compute = get("block_kv_compute", None) or bkv_compute
370+
bkv_compute_in = get("block_kv_compute_in", None) or bkv_compute_in
371+
heads_per_tile = get("heads_per_tile", None) or heads_per_tile
372+
vmem_limit_bytes = get("vmem_limit_bytes", None) or vmem_limit_bytes
373+
else:
374+
bq = getattr(flash_block_sizes, "block_q", None) or bq
375+
bkv = getattr(flash_block_sizes, "block_kv", None) or bkv
376+
bkv_compute = getattr(flash_block_sizes, "block_kv_compute", None) or bkv_compute
377+
bkv_compute_in = getattr(flash_block_sizes, "block_kv_compute_in", None) or bkv_compute_in
378+
heads_per_tile = getattr(flash_block_sizes, "heads_per_tile", None) or heads_per_tile
379+
vmem_limit_bytes = getattr(flash_block_sizes, "vmem_limit_bytes", None) or vmem_limit_bytes
380+
# A BlockSizes object carries heads_per_tile=None when the config dict omitted
381+
# it; getattr then returns that None instead of the default, so coerce it back
382+
# to 1 (the custom-kernel default) to keep the `heads_per_tile > 1` guards safe.
383+
if heads_per_tile is None:
384+
heads_per_tile = 1
385+
return bq, bkv, bkv_compute, bkv_compute_in, heads_per_tile, vmem_limit_bytes
386+
387+
352388
def _build_padding_segment_ids(
353389
query_seq_len: int,
354390
q_padded_len: int,
@@ -885,6 +921,181 @@ def wrap_ulysses_ring_attention(query, key, value):
885921
return x
886922

887923

924+
def _ulysses_ring_custom_attention(
925+
query: jax.Array,
926+
key: jax.Array,
927+
value: jax.Array,
928+
heads: int,
929+
mesh: Mesh,
930+
axis_names_q: AxisNames,
931+
axis_names_kv: AxisNames,
932+
flash_block_sizes: BlockSizes,
933+
dtype: jnp.dtype = jnp.float32,
934+
mask_padding_tokens: bool = True,
935+
residual_checkpoint_name: str | None = None,
936+
attention_mask: jax.Array = None,
937+
ulysses_shards: int = -1,
938+
use_base2_exp: bool = True,
939+
use_experimental_scheduler: bool = False,
940+
bidirectional: bool = False,
941+
use_fixed_m: bool = False,
942+
) -> jax.Array:
943+
"""Hybrid Ulysses + Ring (USP) with the CUSTOM splash kernel on main's mesh.
944+
945+
Uses origin/main's explicit internal `(ring, ulysses)` mesh
946+
(`_create_internal_ulysses_ring_mesh`, commit c104db51) instead of single-axis
947+
collective sub-groups: the public `context` axis is reshaped with the Ulysses
948+
axis innermost, so the Ulysses all-to-all stays INTRA-chip and the ring rotates
949+
ACROSS chips. The per-shard attention is our custom splash kernel
950+
(`make_custom_ring_attention`), not the tokamax_ring kernel main uses.
951+
952+
1. all-to-all over the (intra-chip) Ulysses axis: trade sequence for heads;
953+
2. ring (full ppermute) over the (cross-chip) ring axis, online-softmax merge;
954+
3. all-to-all back to restore the sequence-sharded / full-heads layout.
955+
956+
U = ulysses_shards (from config); R = context // U. U=context -> pure
957+
Ulysses, U=1 -> pure Ring (all on the same custom kernel).
958+
"""
959+
if attention_mask is not None:
960+
raise NotImplementedError(
961+
"ulysses_ring_custom does not support attention_mask (the custom splash kernels only "
962+
"handle padding via orig_seq_len); got a non-None attention_mask."
963+
)
964+
axis_name = "context"
965+
num_context_shards = mesh.shape[axis_name]
966+
num_ulysses_shards = ulysses_shards
967+
if num_ulysses_shards <= 0:
968+
raise ValueError("ulysses_ring_custom requires ulysses_shards to be set from config or command line.")
969+
if num_context_shards % num_ulysses_shards != 0:
970+
raise ValueError(
971+
f"ulysses_ring_custom requires ulysses_shards to divide the context shard count, "
972+
f"got context_shards={num_context_shards} and ulysses_shards={num_ulysses_shards}."
973+
)
974+
num_ring_shards = num_context_shards // num_ulysses_shards
975+
976+
query, orig_q_seq_len = _reshape_data_for_flash(query, heads, num_context_shards)
977+
key, _ = _reshape_data_for_flash(key, heads, num_context_shards)
978+
value, _ = _reshape_data_for_flash(value, heads, num_context_shards)
979+
num_heads = query.shape[1]
980+
if num_heads % num_ulysses_shards != 0:
981+
raise ValueError(f"Ulysses+Ring requires heads divisible by U={num_ulysses_shards}, got heads={num_heads}.")
982+
983+
bq, bkv, bkv_compute, bkv_compute_in, heads_per_tile, vmem_limit_bytes = _extract_custom_block_sizes(flash_block_sizes)
984+
if heads_per_tile > 1:
985+
raise NotImplementedError("ulysses_ring_custom currently supports heads_per_tile == 1 only.")
986+
987+
internal_mesh = _create_internal_ulysses_ring_mesh(mesh, num_ring_shards, num_ulysses_shards)
988+
ring_axis = INTERNAL_RING_AXIS
989+
ulysses_axis = INTERNAL_ULYSSES_AXIS
990+
991+
q_axis_names = nn.logical_to_mesh_axes(axis_names_q)
992+
kv_axis_names = nn.logical_to_mesh_axes(axis_names_kv)
993+
internal_q_axis_names = _replace_mesh_axis_names(q_axis_names, axis_name, (ring_axis, ulysses_axis))
994+
internal_kv_axis_names = _replace_mesh_axis_names(kv_axis_names, axis_name, (ring_axis, ulysses_axis))
995+
996+
@functools.partial(
997+
jax.shard_map,
998+
mesh=internal_mesh,
999+
in_specs=(internal_q_axis_names, internal_kv_axis_names, internal_kv_axis_names),
1000+
out_specs=internal_q_axis_names,
1001+
check_vma=False,
1002+
)
1003+
def wrap_ulysses_ring_attention(query, key, value):
1004+
# (1) Ulysses all-to-all over the (intra-chip) ulysses axis: heads -> sequence,
1005+
# so each device holds the full ring-chunk sequence with heads/U heads.
1006+
a2a = functools.partial(jax.lax.all_to_all, axis_name=ulysses_axis, tiled=True)
1007+
query = a2a(query, split_axis=1, concat_axis=2)
1008+
key = a2a(key, split_axis=1, concat_axis=2)
1009+
value = a2a(value, split_axis=1, concat_axis=2)
1010+
1011+
if use_base2_exp:
1012+
query = query * LOG2E
1013+
1014+
if use_fixed_m:
1015+
# K-smoothing precondition for fixed-m, computed PER SHARD (no ring pmean).
1016+
# A global mean would be a perfectly-uniform per-query logit shift, but the
1017+
# per-shard local mean differs from it by only O(1/sqrt(local_seq)), and the
1018+
# ring's outer online-softmax merge re-normalizes across shards anyway, so we
1019+
# drop the per-layer ring collective and accept the negligible shift error.
1020+
kbar = jnp.mean(key, axis=2, keepdims=True)
1021+
key = key - kbar
1022+
1023+
query, kv_size, query_seq_len = _pad_data_for_flash(query, heads, bq)
1024+
key, _, key_seq_len = _pad_data_for_flash(key, heads, bkv)
1025+
value, _, _ = _pad_data_for_flash(value, heads, bkv)
1026+
1027+
mk_arr = None
1028+
if use_fixed_m:
1029+
# Per-(local-)head Cauchy-Schwarz inputs, all LOCAL to this ring shard. The
1030+
# outer ring merge does an online softmax across shards, so each shard's
1031+
# kernel may use its own local max||k|| as the fixed-m bound for its own
1032+
# local keys -- no global ring pmax is needed for correctness. This removes
1033+
# the second per-layer ring collective.
1034+
qf = query.astype(jnp.float32)
1035+
kf = key.astype(jnp.float32)
1036+
qn_max = jnp.sqrt((qf * qf).sum(-1)).max(axis=(0, 2)) # (local_heads,)
1037+
mk_h = jnp.sqrt((kf * kf).sum(-1)).max(axis=(0, 2)) # (local_heads,) local
1038+
fixed_ok = (qn_max * mk_h <= custom_splash._FIXED_M_SAFE_BOUND).astype(jnp.float32)
1039+
if os.environ.get("FIXED_M_FORCE_ALL", "0") == "1":
1040+
# PERF PROBE ONLY (unsafe): force every head onto the fixed-m fast path,
1041+
# bypassing the safety gate, to measure fixed-m's speed CEILING on the
1042+
# ring kernel. Output may be garbage; timing is still valid.
1043+
fixed_ok = jnp.ones_like(fixed_ok)
1044+
mk_arr = jnp.stack([mk_h, fixed_ok]) # (2, local_heads)
1045+
1046+
bsizes = custom_splash._BlockSizes(block_q=bq, block_kv=bkv, block_kv_compute=bkv_compute)
1047+
if num_ring_shards == 1:
1048+
# (2a) R=1: the ring is trivial (no rotation) -> use the lighter dedicated
1049+
# splash kernel (fuse_reciprocal, no fp32 online-softmax residual windows).
1050+
# Same math as the 1-step ring, and it fits BQ=8448 where the ring kernel
1051+
# OOMs (its 3x residual windows). make_splash_mha returns [H, D, S].
1052+
splash_kernel = custom_splash.make_splash_mha(
1053+
block_sizes=bsizes,
1054+
bkv_compute_in=bkv_compute_in,
1055+
orig_q_seq_len=query_seq_len,
1056+
orig_kv_seq_len=key_seq_len,
1057+
heads_per_tile=heads_per_tile,
1058+
use_base2_exp=use_base2_exp,
1059+
use_experimental_scheduler=use_experimental_scheduler,
1060+
vmem_limit_bytes=vmem_limit_bytes,
1061+
use_fixed_m=use_fixed_m,
1062+
)
1063+
if use_fixed_m:
1064+
attention_output = jnp.swapaxes(jax.vmap(splash_kernel, in_axes=(0, 0, 0, None))(query, key, value, mk_arr), 2, 3)
1065+
else:
1066+
attention_output = jnp.swapaxes(jax.vmap(splash_kernel, in_axes=(0, 0, 0))(query, key, value), 2, 3)
1067+
else:
1068+
# (2b) Ring (full ppermute over the cross-chip ring axis) with the custom kernel.
1069+
# bidirectional=True -> wrap-free schedule (streams K/V both directions one hop
1070+
# at a time), for a non-wrapping ring axis. Selected by attention=ulysses_ring_custom_bidir.
1071+
ring_kernel = tokamax_ring_attention_kernel.make_custom_ring_attention(
1072+
block_sizes=bsizes,
1073+
bkv_compute_in=bkv_compute_in,
1074+
orig_q_seq_len=query_seq_len,
1075+
orig_kv_seq_len=key_seq_len,
1076+
use_base2_exp=use_base2_exp,
1077+
use_experimental_scheduler=use_experimental_scheduler,
1078+
vmem_limit_bytes=vmem_limit_bytes,
1079+
ring_axis=ring_axis,
1080+
ring_size=num_ring_shards,
1081+
bidirectional=bidirectional,
1082+
use_fixed_m=use_fixed_m,
1083+
mk=mk_arr,
1084+
)
1085+
attention_output = jax.vmap(ring_kernel, in_axes=(0, 0, 0))(query, key, value)
1086+
attention_output = attention_output[:, :, :query_seq_len, :kv_size].astype(query.dtype)
1087+
1088+
# (3) Ulysses all-to-all back: sequence -> heads, restoring the layout.
1089+
attention_output = a2a(attention_output, split_axis=2, concat_axis=1)
1090+
return attention_output
1091+
1092+
x = wrap_ulysses_ring_attention(query, key, value)
1093+
x = jax.lax.with_sharding_constraint(x, q_axis_names)
1094+
x = x[:, :, :orig_q_seq_len, :]
1095+
x = _reshape_heads_to_head_dim(x)
1096+
return x
1097+
1098+
8881099
def _apply_attention_dot(
8891100
query: Array,
8901101
key: Array,

0 commit comments

Comments
 (0)