[New Model]Add DeepSeek V4 model support - #3634
Conversation
|
The following ciflow label(s) have been added but CI has not been triggered yet because the workflows are awaiting approval:
Once a maintainer approves the workflows (scroll to the bottom of the PR page), the corresponding CI jobs will be triggered automatically. Please ping one of the reviewers if you do not have access to approve and run workflows. |
|
@tianyu-l I noticed that you added deepseek_v4 to the roadmap. However, I'm sorry that I just force-pushed a new version of the code, which caused the roadmap content to disappear. Could you please add it back? |
|
@Matrix-Z97 you mean the "26H1 TorchTitan Development"? Oh that's not a roadmap but just for tracking what this repo is doing. I re-marked the status from "Done" to "In Progress". |
|
@tianyu-l Understood, thanks for the correction! Out of curiosity, are you interested in the deepseek_v4 model? |
|
Yes, of course. We have plans, but were lacking bandwidth. This PR comes in very timely, and the quality looks very high. We just need some time to review. Many thanks! Btw @shuhuayu is working on adding latest Kimi model. |
|
Thanks a lot for the feedback! Happy to help with the bandwidth. Let me know if there's anything I can do to help move this PR forward or if you need any adjustments. And awesome news about the latest Kimi model @shuhuayu. |
There was a problem hiding this comment.
@tianyu-l How do you feel about the current FlexAttention-based DSV4 attention implementation?
It relies on the passed-in topk_idxs to construct the attention mask. However, because the resulting mask is quite sparse, the attention computation performance is not ideal. Do you have any suggestions here? For example, would it be better to use a custom sparse attention kernel based on TileLang, Triton, Helion, or another approach?
There was a problem hiding this comment.
I think the major concern is that flex is optimized for block masks, while topk_idxs is a token-wise selection, so scattered picks leave few empty blocks for flex to skip. Since the selection is untrained for now, I'd suggest we first land the aux loss to train it, then revisit this optimization.
|
@Matrix-Z97 Are you planning to add MTP support for this model? If not, I’d be happy to add it. |
|
@floatingtrees Thanks, MTP has definitely been on our roadmap, and we’ve also been iterating on it continuously in our internal versions. Our current thinking is more along the lines of providing DeepSeek-V3 support in the spirit of #3392. Do you have any better ideas or suggestions? |
tianyu-l
left a comment
There was a problem hiding this comment.
having some high level feedback before looking into details
| ({parallel_dims.tp}) and 2 * CP degree ({parallel_dims.cp}). | ||
| """ | ||
|
|
||
| if parallelism.spmd_backend in ("full_dtensor", "spmd_types"): |
There was a problem hiding this comment.
We are in the migration to "spmd_types". Could you remove "default" and "full_dtensor" support from this new model?
There was a problem hiding this comment.
Understood. So this new model only needs to support the spmd backend for now? Does this mean all torchtitan models will transition to supporting only the spmd backend in the future?
There was a problem hiding this comment.
There are still some issues when enabling --debug.spmd_typechecking. I am currently debugging this.
There was a problem hiding this comment.
@tianyu-l To support spmd_types, we made the following changes:
- Marked local topk_idxs / compress_topk_idxs as [B, S]-aligned metadata so they can be concatenated safely.
- Disabled SPMD type checking around FlexAttention mask construction, where symbolic indexing is used.
- Reworked compressor overlap logic to avoid {R} tensor in-place writes from {V} slices.
- Replaced external Hadamard transform dependency with cached Hadamard matrix + F.linear.
- Added sharding metadata for the hash-routing tid2eid buffer.
For details, please refer to:
85b3292
| deepseek_v4_configs = { | ||
| "debugmodel": _debugmodel, | ||
| } |
There was a problem hiding this comment.
why only debug model, can we include the full set?
There was a problem hiding this comment.
I've included deepseek_v4_flash and deepseek_v4_pro now to expand the set. Just a heads-up: due to hardware constraints, I haven't actually tested them yet.
| pipelining_fn: Callable | None | ||
| post_optimizer_build_fn: Callable | None | ||
| state_dict_adapter: type[BaseStateDictAdapter] | None | ||
| metrics_fn: Callable | None = None |
There was a problem hiding this comment.
@felipemello1 do you have a plan on in-model metric logging that we could share?
Let's add a TODO in dsv4 model code, and remove the model logging in this PR.
|
|
||
|
|
||
| @spmd.register_autograd_function | ||
| class AuxLossInjection(torch.autograd.Function): |
There was a problem hiding this comment.
From a quick look, #3000 seems more like a MoE-specific interface, while here we would like to call the aux-loss capability through a common/shared interface that can also cover non-MoE losses such as the DSA indexer loss.
So for the current DeepSeek V4 use case, the #3864-style implementation seems more suitable to me.
There was a problem hiding this comment.
could you please add verification (e.g. at least in PR summary) following https://github.com/pytorch/torchtitan/tree/main/scripts/checkpoint_conversion#comprehensive-check-kl-divergence
cc @shuhuayu
There was a problem hiding this comment.
Thanks for the suggestion. Running the KL check with deepseek_v4_flash or deepseek_v4_pro as the baseline is not feasible for me due to hardware limits.
Would it be acceptable to run the same HF-vs-TorchTitan KL / max-diff comparison on a custom small DeepSeek V4 config instead?
There was a problem hiding this comment.
Got it. @shuhuayu since you've done it for Kimi, could you help verify this PR gets to similar level of numerical parity?
There was a problem hiding this comment.
Would it be acceptable to run the same HF-vs-TorchTitan KL / max-diff comparison on a custom small DeepSeek V4 config instead?
We also did a customized 16b kimi configs to compare numerics so it sounds good to me to do the same for dsv4. @tianyu-l, do you think we need to test it in the original 862b configs, doable but needs some additional setups.
There was a problem hiding this comment.
Thanks. Could you share what config was used for the customized 16B Kimi numerical test? In particular, which dimensions were reduced, e.g. number of layers, hidden size, heads/head dim, MoE experts, or sequence length?
I can follow the same strategy for DSV4 and build a smaller custom config for the KL/numerical comparison.
There was a problem hiding this comment.
I see. Small scale verification sounds fine.
There was a problem hiding this comment.
Thanks. Could you share what config was used for the customized 16B Kimi numerical test?
I used the kimi-vl variant, see https://github.com/pytorch/torchtitan/pull/3532/changes#diff-5a13a40a31333dd8b095a114b15d9580c2744ac09211c6b551a02c2989032c9b
There was a problem hiding this comment.
I used the kimi-vl variant, see https://github.com/pytorch/torchtitan/pull/3532/changes#diff-5a13a40a31333dd8b095a114b15d9580c2744ac09211c6b551a02c2989032c9b
Got it, will try it out soon. Thanks!
There was a problem hiding this comment.
@tianyu-l I added a verification note based on the debug model comparison against the HuggingFace-style inference script.
The debug model now includes attention blocks with compress_ratios=(0, 0, 4, 128) and also covers the hash-routing MoE path, so it exercises the main algorithmic structures used by the current DeepSeek V4 implementation.
With the same weights and input tokens, the last-logits comparison between HF inference and TorchTitan gives cosine similarity:
cosine = 0.99999237
It is not bitwise identical mainly because:
- The HF inference script uses a TileLang sparse attention kernel, while TorchTitan uses the current FlexAttention-based path, so minor rounding differences are expected.
- The HF MoE path uses per-expert looped small matmuls, while TorchTitan uses grouped MM, which can also introduce small accumulation-order differences.
In addition, I also ran numerical validation across different parallelism strategies. With the same input, the loss comparison between FSDP and FSDP+EP+TP is:
[LOSS_COMPARE] Step-by-step loss comparison:
[LOSS_COMPARE] Step Baseline Loss Test Loss Difference
[LOSS_COMPARE] ---- ------------- --------- ----------
[LOSS_COMPARE] 1 8.082977294921875 8.082967758178711 -0.000010
[LOSS_COMPARE] 2 6.74751091003418 6.7473907470703125 -0.000120
[LOSS_COMPARE] 3 5.032241344451904 5.032076358795166 -0.000165
[LOSS_COMPARE] 4 4.599724769592285 4.599623680114746 -0.000101
[LOSS_COMPARE] 5 4.25457763671875 4.254598140716553 0.000021
[LOSS_COMPARE] 6 4.076618194580078 4.076696395874023 0.000078
[LOSS_COMPARE] 7 3.9123473167419434 3.9121904373168945 -0.000157
[LOSS_COMPARE] 8 3.7830865383148193 3.7829692363739014 -0.000117
[LOSS_COMPARE] 9 4.035467147827148 4.035507678985596 0.000041
[LOSS_COMPARE] 10 3.6527910232543945 3.6526951789855957 -0.000096
[LOSS_COMPARE] Summary statistics:
[LOSS_COMPARE] Average baseline loss: 4.817734217643737
[LOSS_COMPARE] Average test loss: 4.81767156124115
[LOSS_COMPARE] Average difference: -0.000063
Given the debug model coverage, the high HF-vs-TorchTitan logits agreement, and the close distributed loss comparison, I think this provides a reasonable numerical verification for this PR.
| with spmd.local(): | ||
| n_local_groups = self.n_groups // (self.n_heads // o.shape[2]) | ||
| o = o.view(bsz, seqlen, n_local_groups, -1) | ||
| _assert_spmd_attention_type(o, tp=spmd.S(2)) | ||
| # wo_a is a Linear module; access its weight directly for the grouped | ||
| # einsum (not a standard Linear forward). | ||
| wo_a = self.wo_a.weight.view(n_local_groups, self.o_lora_rank, -1) | ||
| if get_spmd_backend() == "spmd_types" and spmd.is_type_checking(): | ||
| spmd.assert_type( | ||
| wo_a, | ||
| {"dp": spmd.R, "cp": spmd.R, "tp": spmd.S(0)}, | ||
| ) | ||
| o = torch.einsum("bsgd,grd->bsgr", o, wo_a) |
There was a problem hiding this comment.
How about makes this wo_a a BatchedLinear, for example or something similar:
class BatchedLinear(Module):
@dataclass(kw_only=True, slots=True)
class Config(Module.Config):
n_heads: int
in_features: int
out_features: int
def __init__(self, config: Config):
super().__init__()
self.weight = nn.Parameter(
torch.empty(config.n_heads, config.out_features, config.in_features)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
*prefix, H, D_in = x.shape
x_h = x.reshape(-1, H, D_in).transpose(0, 1) # (H, T, D_in)
out = torch.bmm(x_h, self.weight.transpose(-2, -1)) # (H, T, D_out)
return out.transpose(0, 1).reshape(*prefix, H, -1)There was a problem hiding this comment.
this is cleaner but unclear to me how to represent per group attent ion output projection under spmd_types. looks to me it can invoke cross group communication?
|
|
||
| class DeepSeekV4Router(TokenChoiceTopKRouter): | ||
| @dataclass(kw_only=True, slots=True) | ||
| class Config(TokenChoiceTopKRouter.Config): |
There was a problem hiding this comment.
@tianyu-l Could we add a boolean hash field to TokenChioceTopKRouter.Config to remove the duplication below? Would that be a good way to go?
Hi @tianyu-l and @drisspg — Thanks for the pointer! We've closely studied the attention-gym CSA implementation and @floatingtrees' torchtitan override integration. Before we polish the DeepSeek-V4 attention further, we'd like to propose an adjustment to the def forward(q, swa_k, cmp_k, idx_q, idx_k, idx_w, attn_sink, attention_masks):
Does this direction make sense? Thoughts or corrections would be much appreciated! |
tianyu-l
left a comment
There was a problem hiding this comment.
I apologize for the delay in review.
The PR looks really good quality! Please see if my comments make sense. Thanks!
| key: torch.Tensor | None, | ||
| rope_cache: torch.Tensor, | ||
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| """Apply a prepared RoPE cache to query and key. | ||
| *, | ||
| inverse: bool = False, |
There was a problem hiding this comment.
Do you think you could also help adapt the Helion kernel, as the "fast path"? https://github.com/pytorch/torchtitan/blob/main/torchtitan/overrides/helion_rope.py
There was a problem hiding this comment.
Yes, I can take a look.
| idxs = torch.where(idxs >= causal_limit, -1, idxs + offset) | ||
| return idxs.unsqueeze(0).expand(bsz, -1, -1) | ||
|
|
||
| class DSAIndexerAuxLoss(Module): |
There was a problem hiding this comment.
sorry for any back-and-forth, but let's maybe land #3864 first and rebase onto it
There was a problem hiding this comment.
No worries at all. That sounds good to me. I’ll wait for #3864 to land, then rebase this PR on top of it and clean up any conflicts from there.
| return x | ||
|
|
||
|
|
||
| class DeepSeekV4Model(Decoder): |
There was a problem hiding this comment.
Given that your MTP lands, why don't we integrate
There was a problem hiding this comment.
Thanks. I added DeepSeek V4 MTP support in this PR, but it is not a direct drop-in of the DeepSeek V3 MTP path. DSV3 MTP runs on the regular single hidden stream, while DSV4 carries the HC hidden state as [B, L, hc_mult, D] through the main stack and each MTP depth needs to consume the previous HC state, roll the token input, run an inner DSV4 block, then collapse through its own HC head for prediction.
So the integration is a bit more model-specific than DSV3: the MTP block has to preserve the HC branches between depths, apply the valid mask after rolling packed positions, and avoid PP for now because the auxiliary outputs are not yet wired through the pipeline/chunked-loss path. I’m happy to integrate it here, but I kept the boundary explicit so it doesn’t look like the DSV3 MTP implementation can be reused unchanged.
| return loss * self.coeff | ||
|
|
||
|
|
||
| class DSAFlexAttention(FlexAttention): |
There was a problem hiding this comment.
Before we polish the DeepSeek-V4 attention further, we'd like to propose an adjustment to the inner_attention interface in this PR. We have a refactor on our side implementing it:
def forward(q, swa_k, cmp_k, idx_q, idx_k, idx_w, attn_sink, attention_masks):
This is certainly worth more discussion and I think it wouldn't block this PR. In particular, I worry about the composability with vLLM and our RL stack https://github.com/pytorch/torchtitan/blob/main/torchtitan/experiments/rl/models/attention.py
cc @drisspg how's attention-gym doing with sparse attention
There was a problem hiding this comment.
@tianyu-l @pianpwk
One related question: the main reason I split the inner attention into CompressedSparseAttention, HeavilyCompressedAttention, and SlidingWindowAttention is that the sharding config / type-checking path does not seem to handle optional tensor arguments well. Conceptually, the implementation could be a single forward like forward(q, swa_k, cmp_k, idx_q, idx_k, idx_w, attn_sink, attention_masks), with some tensors being unused depending on compress_ratio, but those would need to be None for the non-compressed or heavily-compressed cases.
Do you know if there is a preferred way to represent optional tensor inputs in the sharding annotations today? If there is an existing pattern for this, I’m happy to consolidate the inner attention classes; otherwise keeping separate inner modules seems to make the sharding contract explicit for each path.
There was a problem hiding this comment.
Thanks — understood that this doesn’t block the PR.
I looked at VLLMAttentionWrapper and vLLM’s DeepSeek-V4 implementation. The wrapper is designed for the standard (q, k, v) path used by GQAttention; vLLM itself handles DeepSeek-V4 through a dedicated DeepseekV4Attention, not through that generic seam.
So even without idx_*, DSV4’s inner_attention already has to move away from (q, k, v) toward something like (q, swa_k, cmp_k, indices, attention_sink). The only open question is whether we also pass idx_q/idx_k/idx_w.
If you have any thoughts on how much this would affect the RL/vLLM side, we’d be happy to hear them. Happy to adjust if there’s a cleaner way to keep the generic path untouched. @tianyu-l @drisspg
There was a problem hiding this comment.
If you have any thoughts on how much this would affect the RL/vLLM side, we’d be happy to hear them.
Thanks. Without looking into it too closely, my feeling is that we need to create dedicated vllm attention module, similar to what we do for Qwen 3.5's GDN https://github.com/pytorch/torchtitan/blob/main/torchtitan/experiments/rl/models/gdn.py
|
@sdmyzlp sorry, just now seeing this. All of these points make sense, cc @floatingtrees for more opinions but I think we can do some refactoring to the attn gym interfaces |
|
@sdmyzlp Your points make a lot of sense. One thing that may help is that the version of CSA that landed on attention gym main is significantly stripped down. It takes in something like: |
shuhuayu
left a comment
There was a problem hiding this comment.
Thanks for pr! left some comments.
| top_k = 6 | ||
| n_hash_layers = 3 | ||
| route_norm = True | ||
| route_scale = 1.5 |
There was a problem hiding this comment.
should be 2.5 for pro model per hf config.json.
| skipped, in which case hidden states of shape ``[B, L, D]`` are | ||
| returned. | ||
| """ | ||
| input_ids = tokens.detach().long() |
| """ | ||
| input_ids = tokens.detach().long() | ||
| h = self.tok_embeddings(tokens) if self.tok_embeddings is not None else tokens | ||
| h = h.unsqueeze(2).repeat(1, 1, self.hc_mult, 1) |
| layer = self.layers[str(i)] | ||
| h = layer(h, input_ids, attention_masks, positions) | ||
|
|
||
| h = self.hc_head(h) |
There was a problem hiding this comment.
not pp safe. maybe we should explicitly rejects pp if not supported.
|
|
||
| for i in range(self.n_main_layers): | ||
| layer = self.layers[str(i)] | ||
| h = layer(h, input_ids, attention_masks, positions) |
There was a problem hiding this comment.
input_ids is only necessary in the first n_hash_layers, should we carry it over all layers?
There was a problem hiding this comment.
Good point. It is only used by the first n_hash_layers; later routers ignore it. I kept passing input_ids to all MoE layers to keep a uniform interface and sharding contract, since optional tensor args / None are not well supported by the current sharding config path.
| positions in ``[0, L)`` and padded entries are ``-1``. | ||
| """ | ||
| window = min(seqlen, self.window_size) | ||
| q_idx = torch.arange(seqlen, device=device).unsqueeze(1) |
There was a problem hiding this comment.
raw indices are used, and positions, which contains sample boundary information, is only consumed by rope.
| with spmd.local(): | ||
| n_local_groups = self.n_groups // (self.n_heads // o.shape[2]) | ||
| o = o.view(bsz, seqlen, n_local_groups, -1) | ||
| _assert_spmd_attention_type(o, tp=spmd.S(2)) | ||
| # wo_a is a Linear module; access its weight directly for the grouped | ||
| # einsum (not a standard Linear forward). | ||
| wo_a = self.wo_a.weight.view(n_local_groups, self.o_lora_rank, -1) | ||
| if get_spmd_backend() == "spmd_types" and spmd.is_type_checking(): | ||
| spmd.assert_type( | ||
| wo_a, | ||
| {"dp": spmd.R, "cp": spmd.R, "tp": spmd.S(0)}, | ||
| ) | ||
| o = torch.einsum("bsgd,grd->bsgr", o, wo_a) |
There was a problem hiding this comment.
this is cleaner but unclear to me how to represent per group attent ion output projection under spmd_types. looks to me it can invoke cross group communication?
| tensor: Grouped tensor of shape ``[B, L // R, R, D]``. | ||
| value: Fill value for the first group's missing previous candidate. | ||
|
|
||
| Returns: | ||
| Tensor of shape ``[B, L // R, 2 * R, D]``. |
There was a problem hiding this comment.
the shapes should be from [B, L // R, R, D] to [B, L // R, 2*R, D//2]?
There was a problem hiding this comment.
Yes, good catch. For the overlap path, the input to _overlap_transform has shape [B, L // R, R, 2 * head_dim], and the transform splits the last dim into previous/current candidates, then concatenates along the ratio dim, producing [B, L // R, 2 * R, head_dim]. So the current docstring is misleading; I’ll update it to make this explicit.
|
|
||
| row_max = comb.max(dim=-1, keepdim=True).values | ||
| comb = torch.exp(comb - row_max) | ||
| comb = comb / comb.sum(dim=-1, keepdim=True) + self.eps |
There was a problem hiding this comment.
why here eps is not in the denominator like in other cases?
There was a problem hiding this comment.
Oh, good catch. Fixed.
| shape, dtype = x.size(), x.dtype | ||
| x = x.flatten(2).float() |
There was a problem hiding this comment.
we should do the same dtype convention in both hc_pre and hc_post?
e198b27 to
390549e
Compare
Co-authored-by: Jonathan Zhou <96200235+floatingtrees@users.noreply.github.com>
390549e to
91d5e3d
Compare
@floatingtrees Yes, exactly, for a forward-only / indexer-loss-free path, that shape is clean and easy to slot into One thing we’d like to ask you to evaluate is the indexer-loss training path. I looked through the attention-gym implementation and didn’t find indexer-loss handling there. If Megatron-LM’s cuDNN fused DSA (DeepSeek-V3.2) entry is: fused_indexer_sparse_attn(
query, # main attention Q
kv_full, # main attention KV / compressed KV
q_indexer, # idx_q
k_indexer, # idx_k
weights, # idx_w
...
)and is explicitly differentiable w.r.t. To be transparent, one of our motivations for keeping Would you be open to evaluating the indexer-loss impact? If it holds, keeping |
|
@sdmyzlp Ok, thanks for clarifying that. I agree that there doesn't seem to be a clean boundary for indexer loss under the current sparse attention API, but I'm not sure if it's general purpose enough to be placed in attention gym. Would it make sense if we returned the relevant tensors (such as |
| scores_BLE: torch.Tensor, | ||
| expert_bias_E: torch.Tensor | None = None, | ||
| *, | ||
| input_ids: torch.Tensor | None = None, |
There was a problem hiding this comment.
I see that you are introducing router_kwargs, primarily for this input_ids field, but who's sending in this arg?
| return loss * self.coeff | ||
|
|
||
|
|
||
| class DSAFlexAttention(FlexAttention): |
There was a problem hiding this comment.
If you have any thoughts on how much this would affect the RL/vLLM side, we’d be happy to hear them.
Thanks. Without looking into it too closely, my feeling is that we need to create dedicated vllm attention module, similar to what we do for Qwen 3.5's GDN https://github.com/pytorch/torchtitan/blob/main/torchtitan/experiments/rl/models/gdn.py
Description
This PR adds deepseek_v4 model support and registers it in TorchTitan’s supported model list.
Main Changes
Parallelism Support
Additional Notes
If needed, I can further improve and complete this implementation.