Skip to content

Commit 8748304

Browse files
Introduce a batch-split version of DeepSeekV3
PiperOrigin-RevId: 816267301
1 parent 57bf96b commit 8748304

3 files changed

Lines changed: 324 additions & 2 deletions

File tree

src/MaxText/configs/base.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ sparse_matmul: True
170170
capacity_factor: -1.0 # a factor to decide expert capacity for token dropping, and no dropping by default
171171
load_balance_loss_weight: 0.01 # weight for the load balance loss
172172
use_random_routing: False # whether to use random routing for debug/test purpose
173-
use_custom_sort_vjp: False # whether to use a custom sort vjp for sparse matmul ops
173+
use_custom_sort_vjp: True # whether to use a custom sort vjp for sparse matmul ops
174174
use_ring_of_experts: False # whether to use ring of experts for sparse matmul expert parallelism
175175
# Tunable tiling dimensions used for Megablox
176176
tile_batch_seq: 512
@@ -198,6 +198,9 @@ routed_bias: False # a flag if a learnable bias is added for routing
198198
mlp_bias: False # a flag if a learnable bias is added for MLP matmul
199199
n_routing_groups: -1 # number of groups for routing, disabled by default
200200
topk_routing_group: -1 # number of top groups to route inputs. For EP,
201+
# Splits the batch to allow for better scheduling when using expert parallelism by overlapping the
202+
# all-to-all communication with compute. Currently only implemented with DeepSeek sparse layers.
203+
use_batch_split_schedule: False # whether to use batch split schedule
201204
# sending activations to a maximum of topk_routing_group distinct devices can yield performance benefits.
202205

203206
# For complex architectures like llama4 there are repeated sets of

src/MaxText/layers/decoders.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from MaxText.layers.quantizations import AqtQuantization as Quant
4444
from MaxText.layers import (
4545
deepseek,
46+
deepseek_batchsplit,
4647
gemma,
4748
gemma2,
4849
gemma3,
@@ -382,7 +383,10 @@ def get_decoder_layers(self):
382383
case DecoderBlockType.MIXTRAL:
383384
return [mixtral.MixtralDecoderLayer]
384385
case DecoderBlockType.DEEPSEEK:
385-
return [deepseek.DeepSeekDenseLayer, deepseek.DeepSeekMoELayer]
386+
if self.config.use_batch_split_schedule:
387+
return [deepseek_batchsplit.DeepSeekDenseLayer, deepseek_batchsplit.DeepSeekMoELayer]
388+
else:
389+
return [deepseek.DeepSeekDenseLayer, deepseek.DeepSeekMoELayer]
386390
case DecoderBlockType.GEMMA:
387391
return [gemma.GemmaDecoderLayerToLinen]
388392
case DecoderBlockType.GEMMA2:
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# Copyright 2023–2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# fmt: off
16+
17+
"""Alternative DeepSeek model definition with batch-split schedule."""
18+
19+
from flax import linen as nnx
20+
import jax
21+
import jax.numpy as jnp
22+
from MaxText import common_types
23+
from MaxText.inference import page_manager
24+
from MaxText.layers import attention_mla
25+
from MaxText.layers import initializers
26+
from MaxText.layers import linears
27+
from MaxText.layers import moe
28+
from MaxText.layers import normalizations
29+
from MaxText.layers import quantizations
30+
31+
32+
class DeepSeekGenericLayer(nnx.Module):
33+
"""Generic DeepSeek layer with Multi-Head Latent Attention.
34+
35+
This is to be used as a base class for DeepSeek layers with dense/sparse MLPs.
36+
37+
This class follows a pattern of separating module creation from execution.
38+
`*_layer()` methods (e.g., `attention_layer`) are factories for `nn.Module`s,
39+
called in `setup()` to initialize sub-layers. The module instances are stored
40+
in `*_op` attributes (e.g., `self.attention_op`). The corresponding methods
41+
(e.g., `attention`) are called during execution in `__call__` and wrap the
42+
`*_op` modules with logic like logical constraints. This keeps `__call__`
43+
clean and readable.
44+
"""
45+
46+
config: common_types.Config
47+
mesh: jax.sharding.Mesh
48+
model_mode: str
49+
quant: None | quantizations.AqtQuantization = None
50+
51+
def __call__(
52+
self,
53+
inputs,
54+
decoder_segment_ids,
55+
decoder_positions,
56+
deterministic,
57+
model_mode,
58+
previous_chunk=None,
59+
page_state: None | page_manager.PageState = None,
60+
slot: None | int = None,
61+
):
62+
x = self.with_logical_constraint(inputs)
63+
x = jax.ad_checkpoint.checkpoint_name(x, "decoder_layer_input")
64+
65+
x += self.attention(
66+
self.pre_attention_norm(x),
67+
decoder_segment_ids,
68+
decoder_positions,
69+
deterministic,
70+
previous_chunk,
71+
page_state,
72+
slot,
73+
)
74+
75+
x += self.mlp(self.post_attention_norm(x), deterministic)
76+
x = self.dropout(x, deterministic)
77+
return self.post_process(x)
78+
79+
def setup(self):
80+
self.pre_attention_norm_op = self.rms_norm_layer("pre_attention_layer_norm")
81+
self.post_attention_norm_op = self.rms_norm_layer(
82+
"post_attention_layer_norm"
83+
)
84+
self.attention_op = self.attention_layer()
85+
self.mlp_op = self.mlp_layer()
86+
self.dropout_op = self.dropout_layer()
87+
88+
@property
89+
def logical_axis_names(self):
90+
if self.model_mode == common_types.MODEL_MODE_PREFILL:
91+
return (
92+
"activation_batch",
93+
"prefill_activation_norm_length",
94+
"activation_embed",
95+
)
96+
else:
97+
return (
98+
"activation_batch",
99+
"activation_norm_length",
100+
"activation_embed",
101+
)
102+
103+
def with_logical_constraint(self, x):
104+
return nnx.with_logical_constraint(x, self.logical_axis_names)
105+
106+
def rms_norm_layer(self, name):
107+
return normalizations.rms_norm(
108+
num_features=self.config.base_emb_dim,
109+
dtype=self.config.dtype,
110+
weight_dtype=self.config.weight_dtype,
111+
name=name,
112+
kernel_axes=("norm",),
113+
epsilon=self.config.normalization_layer_epsilon,
114+
)
115+
116+
def pre_attention_norm(self, x):
117+
return self.with_logical_constraint(self.pre_attention_norm_op(x))
118+
119+
def post_attention_norm(self, x):
120+
return self.with_logical_constraint(self.post_attention_norm_op(x))
121+
122+
def attention_layer(self):
123+
inputs_shape = (
124+
self.config.per_device_batch_size,
125+
self.config.max_target_length,
126+
self.config.base_emb_dim,
127+
)
128+
return attention_mla.mla_as_linen(
129+
config=self.config,
130+
num_query_heads=self.config.num_query_heads,
131+
num_kv_heads=self.config.num_kv_heads,
132+
head_dim=self.config.head_dim,
133+
max_target_length=self.config.max_target_length,
134+
max_prefill_predict_length=self.config.max_prefill_predict_length,
135+
attention_kernel=self.config.attention,
136+
attention_type=self.config.attention_type,
137+
inputs_q_shape=inputs_shape,
138+
inputs_kv_shape=inputs_shape,
139+
mesh=self.mesh,
140+
dtype=self.config.dtype,
141+
weight_dtype=self.config.weight_dtype,
142+
dropout_rate=self.config.dropout_rate,
143+
name="self_attention",
144+
quant=self.quant,
145+
kv_quant=quantizations.configure_kv_quant(self.config),
146+
q_lora_rank=self.config.q_lora_rank,
147+
kv_lora_rank=self.config.kv_lora_rank,
148+
qk_nope_head_dim=self.config.qk_nope_head_dim,
149+
qk_rope_head_dim=self.config.qk_rope_head_dim,
150+
v_head_dim=self.config.v_head_dim,
151+
max_position_embeddings=self.config.max_position_embeddings,
152+
original_max_position_embeddings=self.config.original_max_position_embeddings,
153+
mscale=self.config.mscale,
154+
rope_factor=self.config.rope_factor,
155+
model_mode=self.model_mode,
156+
)
157+
158+
def attention(
159+
self,
160+
x,
161+
decoder_segment_ids,
162+
decoder_positions,
163+
deterministic,
164+
previous_chunk=None,
165+
page_state: None | page_manager.PageState = None,
166+
slot: None | int = None,
167+
):
168+
"""Executes the attention layer."""
169+
return self.with_logical_constraint(
170+
self.attention_op(
171+
x,
172+
x,
173+
decoder_positions,
174+
decoder_segment_ids=decoder_segment_ids,
175+
deterministic=deterministic,
176+
model_mode=self.model_mode,
177+
previous_chunk=previous_chunk,
178+
page_state=page_state,
179+
slot=slot,
180+
)
181+
)
182+
183+
def mlp_layer(self):
184+
raise NotImplementedError()
185+
186+
def mlp(self, x, deterministic):
187+
raise NotImplementedError()
188+
189+
def dropout_layer(self):
190+
return nnx.Dropout(rate=self.config.dropout_rate, broadcast_dims=(-2,))
191+
192+
def dropout(self, x, deterministic):
193+
return self.with_logical_constraint(
194+
self.dropout_op(x, deterministic=deterministic)
195+
)
196+
197+
def post_process(self, x):
198+
"""Collect statistics about the output of the layer."""
199+
if self.config.record_internal_nn_metrics:
200+
self.sow("intermediates", "activation_mean", jnp.mean(x))
201+
self.sow("intermediates", "activation_stdev", jnp.std(x))
202+
self.sow(
203+
"intermediates",
204+
"activation_fraction_zero",
205+
jnp.sum(x == 0) / jnp.size(x),
206+
)
207+
208+
if self.config.scan_layers:
209+
return x, None
210+
else:
211+
return x
212+
213+
214+
class DeepSeekDenseLayer(DeepSeekGenericLayer):
215+
"""DeepSeek layer with dense MLP."""
216+
217+
def mlp_layer(self):
218+
return linears.mlp_block(
219+
in_features=self.config.base_emb_dim,
220+
intermediate_dim=self.config.mlp_dim,
221+
activations=self.config.mlp_activations,
222+
intermediate_dropout_rate=self.config.dropout_rate,
223+
dtype=self.config.dtype,
224+
weight_dtype=self.config.weight_dtype,
225+
name="mlp",
226+
config=self.config,
227+
quant=self.quant,
228+
)
229+
230+
def mlp(self, x, deterministic):
231+
return self.with_logical_constraint(self.mlp_op(x, deterministic))
232+
233+
234+
class DeepSeekMoELayer(DeepSeekGenericLayer):
235+
"""DeepSeek MoE layer that uses a batch-split schedule."""
236+
237+
def __call__(
238+
self,
239+
inputs,
240+
decoder_segment_ids,
241+
decoder_positions,
242+
deterministic,
243+
model_mode,
244+
previous_chunk=None,
245+
page_state: None | page_manager.PageState = None,
246+
slot: None | int = None,
247+
split_factor: int = 2,
248+
):
249+
x = self.with_logical_constraint(inputs)
250+
x = jax.ad_checkpoint.checkpoint_name(x, "decoder_layer_input")
251+
252+
# Helper functions.
253+
def _split(x):
254+
if x is None:
255+
return [None] * split_factor
256+
else:
257+
return jnp.split(x, split_factor, axis=0)
258+
259+
def _merge(x):
260+
return jnp.concatenate(x, axis=0)
261+
262+
def _attn(x, decoder_segment_ids, decoder_positions):
263+
return self.attention(
264+
self.pre_attention_norm(x),
265+
decoder_segment_ids,
266+
decoder_positions,
267+
deterministic,
268+
previous_chunk,
269+
page_state,
270+
slot,
271+
)
272+
273+
def _moe(x):
274+
return self.mlp(self.post_attention_norm(x), deterministic)
275+
276+
# Split the inputs into micro-batches.
277+
x = _split(x)
278+
dpos = _split(decoder_positions)
279+
dseg = _split(decoder_segment_ids)
280+
281+
# Attention.
282+
x = [xi + _attn(xi, yi, zi) for xi, yi, zi in zip(x, dseg, dpos)]
283+
284+
# Mixture-of-experts.
285+
x = [xi + _moe(xi) for xi in x]
286+
287+
# Merge the micro-batches back into a single batch.
288+
x = _merge(x)
289+
290+
x = self.dropout(x, deterministic)
291+
return self.post_process(x)
292+
293+
def init(self, *args, **kwargs):
294+
# Calls the parent init method for testing parity.
295+
return super().init(*args, **kwargs, method=super().__call__)
296+
297+
def mlp_layer(self):
298+
# NOTE: the naming mismatch here is to ensure reverse compatibility with
299+
# existing checkpoints. The `name` represents the weight name in
300+
# JAX/checkpoints and so the class name is just for readability.
301+
return moe.get_routed_and_shared_moe(
302+
name="DeepSeekMoeBlock_0",
303+
config=self.config,
304+
mesh=self.mesh,
305+
kernel_init=initializers.nd_dense_init(
306+
1.0, "fan_in", "truncated_normal"
307+
),
308+
kernel_axes=("embed", None),
309+
dtype=self.config.dtype,
310+
weight_dtype=self.config.weight_dtype,
311+
quant=self.quant,
312+
)
313+
314+
def mlp(self, x, _):
315+
return self.with_logical_constraint(self.mlp_op(x))

0 commit comments

Comments
 (0)