@@ -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
0 commit comments