feat(deepseek_v4): faithful THD context-parallel path for DS4 CSA (bounded activation memory) - #7
Conversation
Implement the THD-packed context-parallel branch for DSv4 Compressed Sparse Attention in the lite CSA module, faithfully porting Megatron Core's CompressedSparseAttention._forward_thd_cp and importing Core's CP helpers (csa_cp_utils, csa_cp_layout_kernels), THD sparse-attention kernels, and the boundary exchange zero-copy. - csa.py: add _forward_thd_packed / _forward_thd_cp / _project_boundary_kv and a compressor _forward_thd; thread packed_seq_params as the primary CP route; delete the BSHD dense-softmax fallback and its now-dead helpers. - deepseek_v4 model.py / protocol.py: stop discarding packed_seq_params (protocol pop / model del) and thread cu_seqlens end-to-end to CSA. - add a CPU unit test for the THD-CP layout/dispatch and the CP=1==unsharded invariant (numeric assertion is GPU-gated: the Core layout kernels require CuTeDSL/CUDA). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yan Bai <bayan@nvidia.com>
_forward_thd_packed built the TE-THD key as (total, 1, 1, 1, hn) via a spurious .unsqueeze(-2); Core's _forward_thd_cp expects a 4D (total, 1, 1, hn) key and does key.squeeze(-2).squeeze(1) to recover a 2D (total, hn) kv_local. The extra dim left kv_local 3D and broke torch.cat with the 2D boundary/compressed KV. kv.permute(2, 0, 1, 3) already yields the correct (total, 1, 1, hn); drop the unsqueeze. Verified on 4x H100 (cw, job 13902888): CP=4 sharded == CP=1 unsharded output bit-identical (max_abs=max_rel=0), backward grads finite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yan Bai <bayan@nvidia.com>
The fused-kernel and indexer-loss knobs are implementation config, not part of the HF model config, and must not be read from DeepseekV4Config via defensive getattr. Move them to explicit CompressedSparseAttention constructor arguments (mirroring GLM-5's DSA), keeping DeepseekV4Config strictly HF-aligned: - apply_dsa_kernel_fusion defaults to True (fused DSA kernels are the production path; the unfused sparse-attn / indexer-loss path is a debug fallback). - dsa_indexer_loss_coeff / dsa_indexer_use_sparse_loss / calculate_per_token_loss become constructor args with Core-matching defaults (0.0 / False / False). - num_nextn_predict_layers is a real HF config field; read it directly. No getattr fallbacks remain in csa.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yan Bai <bayan@nvidia.com>
There was a problem hiding this comment.
Code Review
This pull request implements the DeepSeek-V4 Compressed Sparse Attention (CSA) THD context-parallel (CP) path in Megatron-Lite, enabling sequence-parallel training with packed sequences and removing the old BSHD dense fallback. It also adds corresponding unit tests to verify the layout, differentiability, and CP=1 correctness. The review feedback highlights several robustness improvements to prevent potential AttributeErrors, specifically by guarding against empty compressed_group_ids and using getattr to safely access attributes on packed_seq_params.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| ) | ||
| ) | ||
|
|
||
| if indexer is not None: |
There was a problem hiding this comment.
To prevent potential AttributeError when compressed_group_ids is empty (e.g., on short sequences or specific CP configurations where some ranks have 0 compressed blocks), we should guard the indexer logic by checking if compressed_group_ids.shape[0] > 0.
| if indexer is not None: | |
| if indexer is not None and compressed_group_ids.shape[0] > 0: |
| compressed_kv_local, _ = self.compressor._forward_thd( | ||
| hidden_compact, | ||
| cu_seqlens, | ||
| max_seqlen_q=max_seqlen_q, | ||
| compressed_group_ids=compressed_group_ids, | ||
| ) | ||
| compressed_kv_rank_major = gather_from_sequence_parallel_region( | ||
| compressed_kv_local.squeeze(1), group=cp_group | ||
| ) |
There was a problem hiding this comment.
Similarly, if compressed_group_ids is empty, self.compressor._forward_thd will return None, None, causing an AttributeError when calling .squeeze(1). Guard this block to ensure it only runs when there are compressed groups.
| compressed_kv_local, _ = self.compressor._forward_thd( | |
| hidden_compact, | |
| cu_seqlens, | |
| max_seqlen_q=max_seqlen_q, | |
| compressed_group_ids=compressed_group_ids, | |
| ) | |
| compressed_kv_rank_major = gather_from_sequence_parallel_region( | |
| compressed_kv_local.squeeze(1), group=cp_group | |
| ) | |
| if compressed_group_ids.shape[0] > 0: | |
| compressed_kv_local, _ = self.compressor._forward_thd( | |
| hidden_compact, | |
| cu_seqlens, | |
| max_seqlen_q=max_seqlen_q, | |
| compressed_group_ids=compressed_group_ids, | |
| ) | |
| compressed_kv_rank_major = gather_from_sequence_parallel_region( | |
| compressed_kv_local.squeeze(1), group=cp_group | |
| ) |
| def _thd_cu_seqlens(self, packed_seq_params: Any) -> torch.Tensor: | ||
| """Padded cu_seqlens (falling back to unpadded) for the THD packed layout.""" | ||
| return ( | ||
| packed_seq_params.cu_seqlens_q_padded | ||
| if packed_seq_params.cu_seqlens_q_padded is not None | ||
| else packed_seq_params.cu_seqlens_q | ||
| ) |
There was a problem hiding this comment.
Using getattr to access attributes on packed_seq_params is safer and more robust than direct attribute access, especially when integrating with external frameworks or custom runtimes where packed_seq_params might be a different class or dictionary.
def _thd_cu_seqlens(self, packed_seq_params: Any) -> torch.Tensor:
"""Padded cu_seqlens (falling back to unpadded) for the THD packed layout."""
padded = getattr(packed_seq_params, "cu_seqlens_q_padded", None)
return padded if padded is not None else getattr(packed_seq_params, "cu_seqlens_q", None)| if l_local != key.shape[0]: | ||
| raise RuntimeError("DSv4 THD CP path currently supports self-attention only.") | ||
| cu_seqlens = self._thd_cu_seqlens(packed_seq_params) | ||
| max_seqlen_q = int(packed_seq_params.max_seqlen_q) |
| cu_seqlens_q_unpadded = None | ||
| if ( | ||
| packed_seq_params.cu_seqlens_q is not None | ||
| and packed_seq_params.cu_seqlens_q_padded is not None | ||
| and packed_seq_params.cu_seqlens_q.data_ptr() | ||
| != packed_seq_params.cu_seqlens_q_padded.data_ptr() | ||
| ): | ||
| cu_seqlens_q_unpadded = packed_seq_params.cu_seqlens_q |
There was a problem hiding this comment.
Safely retrieve unpadded and padded cu_seqlens using getattr to prevent potential AttributeError if packed_seq_params is missing these attributes.
| cu_seqlens_q_unpadded = None | |
| if ( | |
| packed_seq_params.cu_seqlens_q is not None | |
| and packed_seq_params.cu_seqlens_q_padded is not None | |
| and packed_seq_params.cu_seqlens_q.data_ptr() | |
| != packed_seq_params.cu_seqlens_q_padded.data_ptr() | |
| ): | |
| cu_seqlens_q_unpadded = packed_seq_params.cu_seqlens_q | |
| cu_seqlens_q = getattr(packed_seq_params, "cu_seqlens_q", None) | |
| cu_seqlens_q_padded = getattr(packed_seq_params, "cu_seqlens_q_padded", None) | |
| cu_seqlens_q_unpadded = None | |
| if ( | |
| cu_seqlens_q is not None | |
| and cu_seqlens_q_padded is not None | |
| and cu_seqlens_q.data_ptr() != cu_seqlens_q_padded.data_ptr() | |
| ): | |
| cu_seqlens_q_unpadded = cu_seqlens_q |
What
Implements the THD-packed context-parallel path for DeepSeek-V4 Compressed Sparse Attention (CSA) in Megatron-Lite, as a faithful port of Megatron Core's
CompressedSparseAttention._forward_thd_cp(NVIDIA/dev, NVIDIA#5087). This is the bounded-memory CP path for DS4 CSA — an alternative to the streaming-softmax approach in #6, built by importing Core's DSv4 THD-CP helpers zero-copy rather than re-deriving the sparse math.Design
megatron/core; the lite CSA imports the DSv4 THD-CP helpers directly:csa_cp_utils(prepare_cp_compressor_input,exchange_cp_boundary_hidden,compute_cp_indexer_topk,_thd_cp_position_ids),csa_cp_layout_kernels.build_attention_indices, and the THD sparse-attn kernels (dsa_sparse_attn(is_thd=True),FusedIndexerSparseAttnFromTopkFunc,unfused_compressed_sparse_attn).protocol.pyno longer popspacked_seq_paramsandmodel.pyno longer discards the THD signal —cu_seqlensis threaded end-to-end (DeepseekV4Model→DeepseekV4Layer→DeepseekV4CSAAttention→CompressedSparseAttention).DeepseekV4Config; fused DSA is the production default, unfused is a debug fallback.GPU verification (cw H100/SM90, full evidence)
Validated with a standalone
torchrunCP1-vs-CP4 parity harness on the liteCompressedSparseAttention(real DS4 geometry head_dim=512; variable-length multi-sample THD packs with sequence boundaries straddling CP shard cuts and non-ratio-aligned lengths):Environment note
The fused DSA CP indexer requires cudnn-frontend ≥ 1.27 (
cudnn.DSA.indexer_forward_wrappermust acceptq_causal_offsets); 1.25.0 blocks the fused CP path. FlashMLA sparse-prefill requires the real DS4 head geometry (head_dim=512 / d_v=512).🤖 Generated with Claude Code