Skip to content

Commit 763f9d9

Browse files
authored
support deepseek_v4 packing/CP (#140)
1 parent 8b3adb4 commit 763f9d9

3 files changed

Lines changed: 121 additions & 30 deletions

File tree

src/mcore_bridge/model/gpts/deepseek_v4.py

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def get_query_key_value_tensors(
9494
rotary_pos_emb=None,
9595
*,
9696
inference_params=None,
97+
boundary_hidden=None,
98+
boundary_rotary_pos_emb=None,
9799
):
98100
"""
99101
Derives `query`, `key` and `value` tensors from `hidden_states`.
@@ -141,7 +143,11 @@ def get_query_key_value_tensors(
141143
# QKV up projection and RoPE apply
142144
# =========================================
143145

144-
def qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb):
146+
def qkv_up_proj_and_rope_apply(q_compressed,
147+
kv_compressed,
148+
rotary_pos_emb,
149+
boundary_kv_compressed=None,
150+
boundary_rotary_pos_emb=None):
145151
"""
146152
Apply the up projection and RoPE to the query and key.
147153
When sequence packing enabled, the input tensors adopt a packed shape of [t, ...];
@@ -156,21 +162,18 @@ def qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb):
156162
q = q.view(*q.size()[:-1], self.num_attention_heads_per_partition, self.q_head_dim)
157163
q = _q_rms_norm(q, self.config.layernorm_epsilon)
158164

159-
kv, _ = self.linear_kv_proj(kv_compressed)
160-
kv = self.kv_layernorm(kv)
165+
boundary_rows = 0
166+
if boundary_kv_compressed is not None:
167+
boundary_rows = boundary_kv_compressed.shape[0]
168+
kv_projection_input = torch.cat([boundary_kv_compressed, kv_compressed], dim=0)
169+
kv_rotary_pos_emb = torch.cat([boundary_rotary_pos_emb, rotary_pos_emb], dim=0)
170+
else:
171+
kv_projection_input = kv_compressed
172+
kv_rotary_pos_emb = rotary_pos_emb
161173

162-
# [num_tokens, qk_pos_emb_head_dim] -> [num_tokens, 1, qk_pos_emb_head_dim]
163-
q_len = q.size()[0]
164-
if packed_seq_params is None or self.config.context_parallel_size == 1:
165-
# Shorten rotary_pos_emb to the sequence length when inference_params
166-
# is not provided. This makes sure we can run forward directly with
167-
# any sequence length. During training, the sequence length is always
168-
# the full rotary_pos_emb length, except for sequence packing + CP.
169-
# When sequence packing and context parallel are both enabled, the
170-
# position embedding will not split rotary_pos_emb, so it may exceed
171-
# the sequence length on this CP rank, but we need the full rotary_pos_emb
172-
# to cover the full sequence, so we do not shorten it here.
173-
rotary_pos_emb = rotary_pos_emb[0:q_len]
174+
kv, _ = self.linear_kv_proj(kv_projection_input)
175+
kv = self.kv_layernorm(kv)
176+
boundary_kv = None
174177

175178
# q_no_pe: [num_tokens, n, qk_head_dim]
176179
# q_pos_emb: [num_tokens, n, qk_pos_emb_head_dim]
@@ -196,7 +199,7 @@ def qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb):
196199
# k_pos_emb:[num_tokens, 1, qk_pos_emb_head_dim]
197200
k_pos_emb = apply_rotary_pos_emb(
198201
k_pos_emb,
199-
rotary_pos_emb,
202+
kv_rotary_pos_emb,
200203
config=self.config,
201204
cu_seqlens=cu_seqlens_kv,
202205
cp_group=self.pg_collection.cp,
@@ -206,24 +209,45 @@ def qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb):
206209

207210
# Single head: key = value = [num_tokens, 1, v_head_dim]
208211
kv = torch.cat([kv_no_pe, k_pos_emb], dim=-1).unsqueeze(-2)
212+
if boundary_kv_compressed is not None:
213+
boundary_kv = kv[:boundary_rows]
214+
kv = kv[boundary_rows:]
209215
key = kv
210216
value = kv
211217

212218
query = query.contiguous()
213219
key = key.contiguous()
214220
value = value.contiguous()
215-
216-
return query, key, value
221+
if boundary_kv is not None:
222+
boundary_kv = boundary_kv.contiguous()
223+
if boundary_kv is None:
224+
return query, key, value
225+
return query, key, value, boundary_kv
217226

218227
if self.recompute_up_proj:
219228
quantization = self.config.fp8 or self.config.fp4
220229
self.qkv_up_checkpoint = tensor_parallel.CheckpointWithoutOutput(fp8=quantization)
221-
query, key, value = self.qkv_up_checkpoint.checkpoint(qkv_up_proj_and_rope_apply, q_compressed,
222-
kv_compressed, rotary_pos_emb)
230+
if boundary_hidden is None:
231+
query, key, value = self.qkv_up_checkpoint.checkpoint(qkv_up_proj_and_rope_apply, q_compressed,
232+
kv_compressed, rotary_pos_emb)
233+
boundary_kv = None
234+
else:
235+
query, key, value, boundary_kv = self.qkv_up_checkpoint.checkpoint(qkv_up_proj_and_rope_apply,
236+
q_compressed, kv_compressed,
237+
rotary_pos_emb, boundary_hidden,
238+
boundary_rotary_pos_emb)
223239
else:
224-
query, key, value = qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb)
225-
226-
return query, key, value, q_compressed, kv_compressed
240+
if boundary_hidden is None:
241+
query, key, value = qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb)
242+
boundary_kv = None
243+
else:
244+
query, key, value, boundary_kv = qkv_up_proj_and_rope_apply(q_compressed, kv_compressed, rotary_pos_emb,
245+
boundary_hidden, boundary_rotary_pos_emb)
246+
247+
result = (query, key, value, q_compressed, kv_compressed)
248+
if boundary_kv is not None:
249+
return result + (boundary_kv, )
250+
return result
227251

228252
def forward(
229253
self,
@@ -251,19 +275,54 @@ def forward(
251275
assert (inference_context is None
252276
and inference_params is None), 'Inference is not supported for DSv4HybridAttention.'
253277

278+
# Select this microbatch's dynamic CP group. QKV captures it explicitly
279+
# for recompute; the rest of this forward reads it from pg_collection.
280+
# Restore the static group before returning.
281+
cp_group = self.pg_collection.cp
282+
cp_size = cp_group.size()
283+
qkv_format = packed_seq_params.qkv_format if packed_seq_params is not None else None
284+
if cp_size > 1 and qkv_format != 'thd':
285+
raise ValueError("DSv4 Hybrid with CP requires qkv_format='thd'.")
286+
use_thd_cp = cp_size > 1 and qkv_format == 'thd'
287+
if use_thd_cp and packed_seq_params.cp_partition_mode != 'contiguous':
288+
raise ValueError('DSv4 THD CP requires a contiguous CP partition.')
289+
290+
boundary_hidden = None
291+
boundary_rotary_pos_emb = None
292+
if use_thd_cp:
293+
from megatron.core.transformer.experimental_attention_variant import csa_cp_utils as cp_utils
294+
boundary_hidden = cp_utils.exchange_cp_boundary_hidden(
295+
hidden_states,
296+
self._dsv4_compress_ratio,
297+
self.config.csa_window_size,
298+
self.pg_collection.cp,
299+
)
300+
boundary_rotary_pos_emb = cp_utils.exchange_cp_boundary_hidden(
301+
rotary_pos_emb,
302+
self._dsv4_compress_ratio,
303+
self.config.csa_window_size,
304+
self.pg_collection.cp,
305+
)
254306
# =====================
255307
# Query, Key, and Value
256308
# =====================
257309
# Get the query, key and value tensors based on the type of attention -
258310
# self or cross attn.
259-
query, key, value, q_compressed, kv_compressed = self.get_query_key_value_tensors(
311+
qkv = self.get_query_key_value_tensors(
260312
hidden_states,
261313
key_value_states,
262314
position_ids,
263315
packed_seq_params,
264316
rotary_pos_emb=rotary_pos_emb,
265317
inference_context=inference_context,
318+
boundary_hidden=boundary_hidden,
319+
boundary_rotary_pos_emb=boundary_rotary_pos_emb,
266320
)
321+
if use_thd_cp:
322+
query, key, value, q_compressed, kv_compressed, boundary_kv = qkv
323+
else:
324+
query, key, value, q_compressed, kv_compressed = qkv
325+
boundary_kv = None
267326

268327
# TODO: Currently, TE can only accept contiguous tensors for MLA
269328
query = query.contiguous()
@@ -276,6 +335,10 @@ def forward(
276335
# Need corresponding TE change
277336
core_attn_manager = off_interface(self.offload_core_attention and self.training, query, 'core_attn')
278337
with core_attn_manager as query:
338+
core_attn_kwargs = {}
339+
if boundary_hidden is not None:
340+
core_attn_kwargs['boundary_hidden'] = boundary_hidden
341+
core_attn_kwargs['boundary_kv'] = boundary_kv
279342
core_attn_out = self.core_attention(
280343
query,
281344
key,
@@ -284,8 +347,12 @@ def forward(
284347
packed_seq_params=packed_seq_params,
285348
x=hidden_states,
286349
qr=q_compressed,
350+
**core_attn_kwargs,
287351
)
288-
core_attn_out = core_attn_manager.group_offload(core_attn_out, forced_released_tensors=[query, key, value])
352+
forced_released_tensors = [query, key, value]
353+
if boundary_kv is not None:
354+
forced_released_tensors.append(boundary_kv)
355+
core_attn_out = core_attn_manager.group_offload(core_attn_out, forced_released_tensors=forced_released_tensors)
289356

290357
if packed_seq_params is not None and packed_seq_params.qkv_format == 'thd':
291358
# reshape to same output shape as unpacked case
@@ -313,8 +380,12 @@ def forward(
313380
cu_seqlens_kv = None
314381

315382
content_part, rot_part = torch.split(core_attn_out, [core_attn_out.size(-1) - pos_dim, pos_dim], dim=-1)
316-
rot_part = apply_rotary_pos_emb(
317-
rot_part,
383+
if packed_seq:
384+
rot_part_in = rot_part.squeeze(1)
385+
else:
386+
rot_part_in = rot_part
387+
rot_part_out = apply_rotary_pos_emb(
388+
rot_part_in,
318389
rotary_pos_emb,
319390
self.config,
320391
cu_seqlens=cu_seqlens_kv,
@@ -323,6 +394,10 @@ def forward(
323394
inverse=True,
324395
mla_output_remove_interleaving=True,
325396
)
397+
if packed_seq:
398+
rot_part = rot_part_out.unsqueeze(1)
399+
else:
400+
rot_part = rot_part_out
326401
core_attn_out = torch.cat([content_part, rot_part], dim=-1)
327402
core_attn_out = core_attn_out.view(seq_len, core_attn_out.size(1), -1)
328403

src/mcore_bridge/patcher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ def _apply_rotary_pos_emb_thd(t: torch.Tensor, cu_seqlens: torch.Tensor, freqs:
203203
cp_size = mpu.get_context_parallel_world_size()
204204
cu_seqlens_for_batched = cu_seqlens // cp_size
205205
use_batched_rope = (freqs.dim() >= 1 and freqs.shape[0] == cu_seqlens_for_batched[-1]).item()
206-
if not use_batched_rope:
206+
# The determination of mla_output_remove_interleaving: a quick solution for identifying deepseek_v4
207+
# (TODO: refactor)
208+
if not use_batched_rope and not kwargs.get('mla_output_remove_interleaving', False):
207209
logger.warning_once('Using non-batched RoPE, which may affect performance.')
208210
return _origin_apply_rotary_pos_emb_thd(t, cu_seqlens, freqs, *args, **kwargs)
209211

src/mcore_bridge/utils/megatron_utils.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,26 @@ def unwrap_model(models, module_instances=None):
4545
return unwrapped_model
4646

4747

48-
def split_cp_inputs(inputs: torch.Tensor, cu_seqlens: Optional[torch.Tensor], dim: int):
48+
def split_cp_inputs(inputs: torch.Tensor,
49+
cu_seqlens: Optional[torch.Tensor],
50+
dim: int,
51+
cp_partition_mode: str = 'zigzag'):
4952
if dim < 0:
5053
dim = (dim + inputs.ndim) % inputs.ndim
51-
new_inputs = []
5254
cp_size = mpu.get_context_parallel_world_size()
5355
cp_rank = mpu.get_context_parallel_rank()
56+
if cp_partition_mode == 'contiguous':
57+
# Rank r owns the contiguous block [r * local_rows, (r + 1) * local_rows) of the
58+
# flattened packed sequence. This must match the DSv4 THD CP forward, which assumes
59+
# each rank holds a single contiguous slice (global_start = cp_rank * local_rows).
60+
total_rows = inputs.shape[dim]
61+
assert total_rows % cp_size == 0, (
62+
f'Contiguous CP slicing requires dim size={total_rows} to be divisible by cp_size={cp_size}.')
63+
local_rows = total_rows // cp_size
64+
slices = [slice(None)] * inputs.ndim
65+
slices[dim] = slice(cp_rank * local_rows, (cp_rank + 1) * local_rows)
66+
return inputs[tuple(slices)].contiguous()
67+
new_inputs = []
5468
for i in range(1 if cu_seqlens is None else (cu_seqlens.shape[0] - 1)):
5569
if cu_seqlens is None:
5670
val = inputs

0 commit comments

Comments
 (0)