Skip to content

Commit 629fb1c

Browse files
Merge pull request #4442 from dinodeep:dinodeep/07-10-2026/fix-gqa-fused-qkv
PiperOrigin-RevId: 952277268
2 parents 2e154ec + 9362f01 commit 629fb1c

1 file changed

Lines changed: 37 additions & 10 deletions

File tree

src/maxtext/layers/attentions.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
from maxtext.layers.quantizations import AqtQuantization as Quant
6969
from maxtext.inference import kvcache
7070
from maxtext.inference.kvcache import KVQuant
71-
from maxtext.utils.sharding import maybe_shard_with_logical, create_sharding
71+
from maxtext.utils.sharding import maybe_shard_with_logical, create_sharding, logical_to_mesh_axes
7272

7373
# pylint: disable=line-too-long, g-doc-args, g-doc-return-or-yield, bad-continuation, g-inconsistent-quotes
7474
# pytype: disable=attribute-error
@@ -558,6 +558,20 @@ def __init__(
558558
debug_sharding=config.debug_sharding,
559559
)
560560

561+
def _logical_to_mesh_axes(self, logical_name):
562+
# Pipeline parallelism uses context managers for logical rules instead of the config,
563+
# so pass None to ensure `logical_to_mesh_axes` defers to using the current Flax context manager
564+
logical_rules = None if self.config.using_pipeline_parallelism else self.config.logical_axis_rules
565+
return logical_to_mesh_axes(logical_name, mesh=self.mesh, rules=logical_rules)
566+
567+
def _validate_kv_heads(self) -> None:
568+
"""Validates the number of key/value heads."""
569+
if self.num_kv_heads == -1:
570+
raise ValueError("num_kv_heads is not defined.")
571+
572+
if self.num_query_heads % self.num_kv_heads != 0:
573+
raise ValueError("Invalid num_kv_heads for GQA.")
574+
561575
def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> None:
562576
"""Initializes the query, key, value, and output projections."""
563577
if self.config.fused_qkv:
@@ -625,11 +639,7 @@ def init_kv_w(self, inputs_kv_shape: Tuple) -> nnx.Module:
625639
Returns:
626640
A DenseGeneral module that performs the key or value projection.
627641
"""
628-
if self.num_kv_heads == -1:
629-
raise ValueError("num_kv_heads is not defined.")
630-
631-
if self.num_query_heads % self.num_kv_heads != 0:
632-
raise ValueError("Invalid num_kv_heads for GQA.")
642+
self._validate_kv_heads()
633643

634644
kernel_axes = (
635645
(None, None, None)
@@ -675,12 +685,15 @@ def kv_projection(self, inputs_kv: Array, proj_name: str, out_sharding: NamedSha
675685
raise ValueError(f"proj_name must be 'key' or 'value', but got {proj_name}")
676686

677687
def init_qkv_w(self, inputs_shape: Tuple) -> nnx.Module:
688+
"""Initializes the a fused QKV projection using only one DenseGeneral module."""
689+
self._validate_kv_heads()
690+
678691
return DenseGeneral(
679692
in_features_shape=self.convert_dense_general_inputs_shape(inputs_shape),
680-
out_features_shape=(3, self.num_query_heads, self.head_dim),
693+
out_features_shape=(self.num_query_heads + 2 * self.num_kv_heads, self.head_dim),
681694
axis=-1,
682695
kernel_init=self.kernel_init,
683-
kernel_axes=("embed", "qkv", "heads", "kv"),
696+
kernel_axes=("embed", "heads", "kv"),
684697
dtype=self.dtype,
685698
weight_dtype=self.weight_dtype,
686699
quant=self.quant,
@@ -695,8 +708,22 @@ def qkv_projection(self, inputs: Array, proj_name: str, out_sharding: NamedShard
695708

696709
qkv_proj = self.qkv_proj(inputs, out_sharding)
697710
qkv_proj = checkpoint_name(qkv_proj, "qkv_proj")
698-
query, key, value = qkv_proj[:, :, 0, ...], qkv_proj[:, :, 1, ...], qkv_proj[:, :, 2, ...]
699-
return query, key, value
711+
712+
# Since fused QKV projection places all heads along the same axis which could be tensor
713+
# parallel partitioned, we must use shard_map to split into equally partitioned Q, K, V arrays.
714+
q_bshd = self._logical_to_mesh_axes(self.query_axis_names)
715+
k_bshd = self._logical_to_mesh_axes(self.key_axis_names)
716+
v_bshd = self._logical_to_mesh_axes(self.value_axis_names)
717+
718+
@jax.shard_map(mesh=self.mesh, in_specs=(q_bshd,), out_specs=(q_bshd, k_bshd, v_bshd))
719+
def split_qkv(qkv_proj: Array) -> tuple[Array, Array, Array]:
720+
num_local_heads = qkv_proj.shape[2]
721+
num_query_heads = (num_local_heads * self.num_query_heads) // (self.num_query_heads + 2 * self.num_kv_heads)
722+
num_kv_heads = (num_local_heads - num_query_heads) // 2
723+
724+
return tuple(jnp.split(qkv_proj, [num_query_heads, num_query_heads + num_kv_heads], axis=2))
725+
726+
return split_qkv(qkv_proj)
700727

701728
@property
702729
def out_head_dim(self) -> int:

0 commit comments

Comments
 (0)