Skip to content

Commit b77f9c6

Browse files
committed
Fix sharding logic in tpu_custom_attention for 4D meshes
This fixes an issue with >=3-axis meshes where heads were not being appropriately sharded, causing asymmetric Q/KV sharding (replicating KV across context shards). The new logic uses name-based axis resolution and properly shards both Q and KV on the head-parallel axes. It also adds a safety guard to ensure num_heads is divisible by the head-parallelism size.
1 parent 5e884cf commit b77f9c6

1 file changed

Lines changed: 50 additions & 21 deletions

File tree

src/maxdiffusion/kernels/custom_splash_attention.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,11 @@ def _splash_attention(q, k, v):
571571

572572
# ---------------------------------------------------------------------------
573573
# High-level attention function with shard_map
574+
# TODO: Move `tpu_custom_attention` and `make_custom_splash_sdpa`
575+
# out of this file and into `attention_flax.py` where other orchestration
576+
# functions live. This file should be strictly Pallas kernel definitions.
574577
# ---------------------------------------------------------------------------
575578

576-
577579
def tpu_custom_attention(
578580
query,
579581
key,
@@ -666,28 +668,55 @@ def _kernel_3d(q_3d, k_3d, v_3d):
666668
kv_partition_spec = P()
667669
out_constraint = P()
668670
else:
669-
axis_names = mesh.axis_names
670-
if len(axis_names) == 1:
671-
tp_axis = axis_names[0]
672-
q_partition_spec = P(None, tp_axis, None, None)
673-
kv_partition_spec = P(None, tp_axis, None, None)
674-
out_constraint = P(None, None, tp_axis, None)
675-
elif len(axis_names) == 2:
676-
dp_axis, tp_axis = axis_names[0], axis_names[1]
677-
dp_size = mesh.shape[dp_axis]
678-
if batch_size >= dp_size:
679-
q_partition_spec = P(dp_axis, tp_axis, None, None)
680-
kv_partition_spec = P(dp_axis, tp_axis, None, None)
681-
out_constraint = P(dp_axis, None, tp_axis, None)
671+
# Name-based axis resolution for the standard 4D mesh ("data", "fsdp", "context", "tensor").
672+
# Axes are grouped by semantic role:
673+
# - Batch-parallel: "data", "fsdp" (shard the batch dimension)
674+
# - Head-parallel: "context", "tensor" (shard the head dimension)
675+
# Only axes with size > 1 participate in sharding to avoid shard_map errors.
676+
# Q and KV are sharded identically (symmetric head-parallel), matching the
677+
# strategy that the legacy 2D (dp, tp) path used.
678+
batch_axes = tuple(a for a in ("data", "fsdp") if a in mesh.shape and mesh.shape[a] > 1)
679+
head_axes = tuple(a for a in ("context", "tensor") if a in mesh.shape and mesh.shape[a] > 1)
680+
681+
batch_parallel_size = 1
682+
for a in batch_axes:
683+
batch_parallel_size *= mesh.shape[a]
684+
head_parallel_size = 1
685+
for a in head_axes:
686+
head_parallel_size *= mesh.shape[a]
687+
688+
# Flatten singleton tuples for clean PartitionSpecs.
689+
batch_spec = batch_axes[0] if len(batch_axes) == 1 else (batch_axes if batch_axes else None)
690+
head_spec = head_axes[0] if len(head_axes) == 1 else (head_axes if head_axes else None)
691+
692+
if (
693+
batch_parallel_size > 1
694+
and batch_size >= batch_parallel_size
695+
and num_heads % head_parallel_size == 0
696+
):
697+
# Head-parallel: batch on data/fsdp, heads on context/tensor.
698+
q_partition_spec = P(batch_spec, head_spec, None, None)
699+
kv_partition_spec = P(batch_spec, head_spec, None, None)
700+
out_constraint = P(batch_spec, None, head_spec, None)
701+
elif head_parallel_size > 1 or batch_parallel_size > 1:
702+
# Batch too small for batch-parallel; shard all parallelism onto heads.
703+
all_axes = batch_axes + head_axes
704+
all_parallel_size = batch_parallel_size * head_parallel_size
705+
all_spec = all_axes[0] if len(all_axes) == 1 else (all_axes if all_axes else None)
706+
if num_heads % all_parallel_size == 0:
707+
q_partition_spec = P(None, all_spec, None, None)
708+
kv_partition_spec = P(None, all_spec, None, None)
709+
out_constraint = P(None, None, all_spec, None)
682710
else:
683-
all_axes = tuple(axis_names)
684-
q_partition_spec = P(None, all_axes, None, None)
685-
kv_partition_spec = P(None, all_axes, None, None)
686-
out_constraint = P(None, None, all_axes, None)
711+
# Heads not divisible by available parallelism; fall back to replicated.
712+
q_partition_spec = P()
713+
kv_partition_spec = P()
714+
out_constraint = P()
687715
else:
688-
q_partition_spec = P(axis_names[0], axis_names[1], axis_names[2], None)
689-
kv_partition_spec = P(axis_names[0], axis_names[1], None, None)
690-
out_constraint = P(axis_names[0], None, (axis_names[1], axis_names[2]), None)
716+
# All axes are size-1; fully replicated (e.g. single-device).
717+
q_partition_spec = P()
718+
kv_partition_spec = P()
719+
out_constraint = P()
691720

692721
sharded_fn = shard_map(
693722
_attention_on_slices,

0 commit comments

Comments
 (0)