Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
26c62d0
Add DeepSeek V4 (modular)
ArthurZucker Apr 28, 2026
8b3c91a
Split V4 HCA / CSA caches and compressors into independent classes
ArthurZucker Apr 28, 2026
b4b3a20
Fix tests_generate / tests_tensor_parallel CI failures
ArthurZucker Apr 28, 2026
6baa653
Address PR review feedback batch (comments 2-24)
ArthurZucker Apr 29, 2026
838cc0c
Address PR review feedback batch (comments 25-42)
ArthurZucker Apr 29, 2026
f18a6b8
Fix Fp8Dequantize.reverse_op to actually re-quantize on save
ArthurZucker Apr 29, 2026
921a8dc
Address Arthur's review batch + revisit two of vasqu's comments
ArthurZucker Apr 29, 2026
6d82332
Apply suggestions from code review
ArthurZucker Apr 29, 2026
4ee8e47
Move DeepseekV4Config out of modular + simplify __post_init__
ArthurZucker Apr 29, 2026
ea29922
Fix V4 TP failures: dynamic num_key_value_groups + FP8-safe GroupedLi…
ArthurZucker Apr 29, 2026
092dcd6
Revert GroupedLinear F.linear loop, keep bmm
ArthurZucker Apr 29, 2026
def7199
Fix V4 GroupedLinear comment with real V4-Flash / V4-Pro config values
ArthurZucker Apr 30, 2026
631151d
up
ArthurZucker Apr 30, 2026
fa0369f
up
ArthurZucker Apr 30, 2026
5f711e0
small cleanup
ArthurZucker Apr 30, 2026
b5998a0
repo fixes
ArthurZucker Apr 30, 2026
dcbe66b
nits
ArthurZucker Apr 30, 2026
aecf319
more nits
ArthurZucker Apr 30, 2026
4fc49a0
nits, small thing left to do
ArthurZucker Apr 30, 2026
02356fe
update
ArthurZucker Apr 30, 2026
8124d3e
update DeepseekV4HCACache
ArthurZucker May 1, 2026
45c4323
more update
ArthurZucker May 1, 2026
a48b51b
nits
ArthurZucker May 1, 2026
46bc564
update
ArthurZucker May 1, 2026
240647a
Merge branch 'add-deepseek-v4' of github.com:huggingface/transformers…
ArthurZucker May 1, 2026
5eb7bec
Merge branch 'add-deepseek-v4' of github.com:huggingface/transformers…
ArthurZucker May 1, 2026
8f807c1
update
ArthurZucker May 1, 2026
0c62b58
nits
ArthurZucker May 1, 2026
e32e9a8
update
ArthurZucker May 1, 2026
5b125c6
fixes
ArthurZucker May 1, 2026
07aca9f
nits
ArthurZucker May 1, 2026
913adcc
Merge branch 'main' of https://github.com/huggingface/transformers in…
ArthurZucker May 1, 2026
7b5b54a
Fix CI: \N>1 backrefs in conversion mapping + drop irrelevant drift
ArthurZucker May 1, 2026
7feb6a1
Drop TP plan from V4 attention path; keep EP-only on experts
ArthurZucker May 1, 2026
d515ec5
Address vasqu review batch + skip quantized-TP test
ArthurZucker May 1, 2026
65e564f
Drop model. prefix from V4 conversion mapping + tester / parity cleanup
ArthurZucker May 1, 2026
a021e84
Silence Unrecognized rope_parameters keys warning
ArthurZucker May 2, 2026
0e35cfd
Disable flex_attention on V4; guard tensor-only mask pad
ArthurZucker May 2, 2026
a55ffee
Switch V4 to EP-only plan (gpt-oss style)
ArthurZucker May 2, 2026
d86d46d
Fix V4 yarn rope validation under nested rope_parameters
ArthurZucker May 2, 2026
ff720b3
Trim V4 config-attribute allowlist from 24 → 6
ArthurZucker May 2, 2026
c946087
Inline V4 config legacy-kwarg + resolve_* helpers into __post_init__
ArthurZucker May 2, 2026
62a75c0
Disable FlashAttention on V4: head_dim=512 exceeds the 256 cap
ArthurZucker May 2, 2026
08e4cf8
update
ArthurZucker May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/en/_toctree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@
title: DeepSeek-V2
- local: model_doc/deepseek_v3
title: DeepSeek-V3
- local: model_doc/deepseek_v4
title: DeepSeek-V4
- local: model_doc/dialogpt
title: DialoGPT
- local: model_doc/diffllama
Expand Down
112 changes: 112 additions & 0 deletions docs/source/en/model_doc/deepseek_v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!--Copyright 2026 The HuggingFace Team. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

⚠️ Note that this file is in Markdown but contains specific syntax for our doc-builder (similar to MDX) that may not be
rendered properly in your Markdown viewer.

-->
*This model was released on {release_date} and added to Hugging Face Transformers on 2026-05-02.*

# DeepSeek-V4

[DeepSeek-V4](https://huggingface.co/deepseek-ai) is the next-generation MoE language model from DeepSeek
([paper](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/DeepSeek_V4.pdf)). The architecture replaces
DeepSeek-V3's Multi-head Latent Attention (MLA) with a hybrid local + long-range design, swaps residual connections
for Manifold-Constrained Hyper-Connections (mHC), and bootstraps the first few MoE layers with a static
token-id → expert-id hash table.
Comment thread
ArthurZucker marked this conversation as resolved.

This implementation covers `DeepSeek-V4-Flash`, `DeepSeek-V4-Pro`, and their `-Base` pretrained siblings. All four
share the same architecture; they differ only in width / depth / expert count and weights.

## Architecture (paper §2)

### Hybrid attention (§2.3)

Each decoder block is one of three attention types, dispatched by `config.layer_types[i]`:

* **Sliding-window full attention** (`"sliding_attention"`): only the local window of `sliding_window` tokens, no
long-range branch. Matches V3's "Full Attention" style for the bootstrap layers.
* **Compressed Sparse Attention** (`"compressed_sparse_attention"`, **CSA** — paper §2.3.1): a low-compression
pool (`compress_rate_csa`, default `m=4`) with overlapping windows, plus a **Lightning Indexer** (eqs. 13–17)
that scores queries against the pool and gathers the top `index_topk` blocks per query before they reach core
attention.
* **Heavily Compressed Attention** (`"heavily_compressed_attention"`, **HCA** — paper §2.3.2): a high-compression
pool (`compress_rate_hca`, default `m'=128`) with non-overlapping windows. No indexer — every pooled entry
contributes to attention.
Comment thread
ArthurZucker marked this conversation as resolved.

All three types share the same backbone:

* **Shared K=V Multi-Query Attention**: `num_key_value_heads = 1`; `kv_proj` produces a single KV head and the same
tensor is read as both key and value.
* **Partial RoPE** (interleaved-pair, paper §2.3.3 "Partial Rotary Positional Embedding") on the trailing
`qk_rope_head_dim = head_dim * partial_rotary_factor` channels of each head. The same rotation is applied with
position `-i` to the attention output's rope slice (eq. 26) so the contribution of each KV entry stays a function
of the *relative* distance to the query.
* **Per-head learnable attention sink** (eq. 27).
* **Grouped low-rank output projection** (§2.3.1 "Grouped Output Projection"): `o_groups` head-groups → `o_lora_rank`
per group → `hidden_size`, computed by [`DeepseekV4GroupedLinear`] (`o_a_proj`) followed by `o_b_proj`. Cuts the
per-token cost of the wide attention output without losing expressivity.
* **Shared sliding-window K=V branch** of size `sliding_window` ("Additional Branch of Sliding Window Attention",
§2.3.1) preserves local fine-grained dependencies; the long-range compressor's output is concatenated with this
branch's KVs before core attention.

### Manifold-Constrained Hyper-Connections (§2.2)

Residual connections are replaced by mHC (Xie et al., 2026): `hc_mult` parallel residual streams kept in shape
`[B, S, hc_mult, D]` throughout each block. Two [`DeepseekV4HyperConnection`] modules — `attn_hc` and `ffn_hc` — mix
streams in and out around the attention / MLP sublayers via a `(pre, post, comb)` triplet. The `comb` matrix is a
doubly-stochastic projection produced by `hc_sinkhorn_iters` Sinkhorn–Knopp iterations on the manifold, making
signal propagation non-expansive across deep stacks. A final [`DeepseekV4HyperHead`] collapses the `hc_mult`
streams down to a single sequence before the model norm.

### MoE schedule (§2.1)

Routing is configured per layer by `config.mlp_layer_types`, with values from `{"hash_moe", "moe"}`:

* `"hash_moe"`: expert indices come from a frozen `tid2eid[input_ids]` lookup populated from the V4 checkpoint.
The learned gate `weight` still produces the per-expert scores that weight the selected experts; only
*which-experts* is static. Used for the first few bootstrap layers (default 3, override via legacy
`num_hash_layers`).
* `"moe"`: standard top-k routed MoE. The expert affinity uses **Sqrt(Softplus(·))** instead of V3's Sigmoid
("we change the activation function that computes the affinity scores from Sigmoid(·) into Sqrt(Softplus(·))",
paper §2.1), and V3's `n_group` / `topk_group` constraint is dropped. The auxiliary-loss-free strategy
(DeepSeek's `noaux_tc`) is preserved via the `e_score_correction_bias` buffer that biases the top-k argmax
without flowing gradients.

Routed experts use a **clamped SwiGLU** (`gate.clamp(max=swiglu_limit)`, `up.clamp(min=-swiglu_limit, max=swiglu_limit)`,
then `act_fn(gate) * up`) on top of the standard Mixtral `[num_experts, 2 * moe_intermediate_size, hidden_size]`
expert weight layout. A single shared expert (a plain SwiGLU MLP at `moe_intermediate_size` width) runs in parallel
on every token.

### Cache layers

Each non-sliding attention block needs to thread compressor / indexer state across forward calls. V4 ships two
cache layer types that auto-register with `LAYER_TYPE_CACHE_MAPPING`:

* `DeepseekV4HCACache`: sliding-window K=V + HCA compressor buffer / pool / count (no overlap, no indexer).
* `DeepseekV4CSACache`: sliding-window K=V + CSA compressor (with overlap state) + parallel indexer
buffer / pool / count / overlap at `index_head_dim`.

`DynamicCache(config=…)` builds the right cache layer per `config.layer_types[i]`.

## DeepseekV4Config

[[autodoc]] DeepseekV4Config

## DeepseekV4Model

[[autodoc]] DeepseekV4Model
- forward

## DeepseekV4ForCausalLM

[[autodoc]] DeepseekV4ForCausalLM
- forward
2 changes: 1 addition & 1 deletion docs/source/en/model_doc/lasr.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License.
rendered properly in your Markdown viewer.

-->
*This model was released on {release_date} and added to Hugging Face Transformers on 2025-12-05.*
*This model was released on 2020-05-16 and added to Hugging Face Transformers on 2025-12-05.*

<div class="flex flex-wrap space-x-1">
<img alt="PyTorch" src="https://img.shields.io/badge/PyTorch-DE3412?style=flat&logo=pytorch&logoColor=white">
Expand Down
8 changes: 8 additions & 0 deletions src/transformers/activations.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ def forward(self, input):
return squared


class SqrtSoftplusActivation(nn.Module):
"""sqrt(softplus(x)) — the router scoring function used by DeepSeek V4."""

def forward(self, input):
return nn.functional.softplus(input).sqrt()


class ClassInstantier(OrderedDict):
def __getitem__(self, key):
content = super().__getitem__(key)
Expand Down Expand Up @@ -334,6 +341,7 @@ def forward(self, input: Tensor) -> Tensor:
"relu6": nn.ReLU6,
"sigmoid": nn.Sigmoid,
"silu": SiLUActivation,
"sqrtsoftplus": SqrtSoftplusActivation,
"swish": nn.SiLU,
"tanh": nn.Tanh,
"prelu": nn.PReLU,
Expand Down
87 changes: 68 additions & 19 deletions src/transformers/cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,31 @@
logger = logging.get_logger(__name__)


# Registry mapping ``config.layer_types[i]`` -> the dynamic cache layer class to build for
# that layer. ``DynamicCache.__init__`` consults this mapping when a ``config`` is provided
# so models with custom layer types (e.g. DeepSeek-V4's CSA / HCA) can register their own
# cache-layer subclass and stop needing a model-specific ``Cache`` subclass.
#
# A cache layer subclass with a class attribute ``layer_type = "..."`` auto-registers via
# ``CacheLayerMixin.__init_subclass__``. Each registered class must accept a
# ``PreTrainedConfig`` (the decoder text config) as the only positional argument.
LAYER_TYPE_CACHE_MAPPING: dict[str, type] = {}


class CacheLayerMixin(ABC):
"""Base, abstract class for a single layer's cache."""

is_compileable = False
# Subclasses can set ``layer_type`` to auto-register themselves in
# ``LAYER_TYPE_CACHE_MAPPING`` at import time (used by ``DynamicCache`` to dispatch
# per-layer cache classes from ``config.layer_types``).
layer_type: str | None = None

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
layer_type = cls.__dict__.get("layer_type", None)
if layer_type is not None:
LAYER_TYPE_CACHE_MAPPING[layer_type] = cls

def __init__(self):
self.keys: torch.Tensor | None = None
Expand Down Expand Up @@ -93,6 +114,9 @@ class DynamicLayer(CacheLayerMixin):

is_sliding = False

def __init__(self, config: PreTrainedConfig | None = None):
super().__init__()

def lazy_initialization(self, key_states: torch.Tensor, value_states: torch.Tensor) -> None:
self.dtype, self.device = key_states.dtype, key_states.device
self.keys = torch.tensor([], dtype=self.dtype, device=self.device)
Expand Down Expand Up @@ -171,8 +195,14 @@ class DynamicSlidingWindowLayer(DynamicLayer):

is_sliding = True

def __init__(self, sliding_window: int):
def __init__(self, config: PreTrainedConfig | None = None, sliding_window: int | None = None):
super().__init__()
# Accept either a config (registry-style construction via LAYER_TYPE_CACHE_MAPPING)
# or a raw ``sliding_window`` int (legacy callers).
if sliding_window is None:
if config is None:
raise ValueError("Either `config` or `sliding_window` must be provided.")
sliding_window = getattr(config, "sliding_window", None) or getattr(config, "attention_chunk_size", None)
self.sliding_window = sliding_window
self.cumulative_length = 0
self._sliding_window_tensor = torch.tensor(self.sliding_window, dtype=torch.long)
Expand Down Expand Up @@ -732,6 +762,9 @@ def crop(self, max_length: int):


class LinearAttentionLayer(LinearAttentionCacheLayerMixin):
def __init__(self, config: PreTrainedConfig | None = None):
super().__init__()

def lazy_initialization(
self, conv_states: torch.Tensor | None = None, recurrent_states: torch.Tensor | None = None
) -> None:
Expand Down Expand Up @@ -808,7 +841,7 @@ class LinearAttentionAndFullAttentionLayer(LinearAttentionLayer, DynamicLayer):
# The dynamic Attention part makes it non-compileable
is_compileable = False

def __init__(self):
def __init__(self, config: PreTrainedConfig | None = None):
DynamicLayer.__init__(self)
LinearAttentionLayer.__init__(self)

Expand All @@ -831,6 +864,29 @@ def reorder_cache(self, beam_idx: torch.LongTensor):
DynamicLayer.reorder_cache(self, beam_idx)


# Pre-register the standard layer types (some classes are shared between multiple types,
# e.g. ``DynamicSlidingWindowLayer`` covers both ``"sliding_attention"`` and
# ``"chunked_attention"`` — those need an explicit map entry rather than the
# auto-registration via ``CacheLayerMixin.__init_subclass__``).
LAYER_TYPE_CACHE_MAPPING.update(
{
"full_attention": DynamicLayer,
# From a cache point of view, sliding and chunked are the same in how they should behave;
# only the mask differs.
"sliding_attention": DynamicSlidingWindowLayer,
"chunked_attention": DynamicSlidingWindowLayer,
# Linear-attention-shaped layers (mamba / conv / pure linear-attention / moe placeholders)
# don't grow per-token KV; they're tracked just so position bookkeeping stays consistent.
"mamba": LinearAttentionLayer,
"conv": LinearAttentionLayer,
"linear_attention": LinearAttentionLayer,
"moe": LinearAttentionLayer,
# Hybrid layers (e.g. zamba / zamba2) carry both a linear-attention state and a dynamic-attention state.
"hybrid": LinearAttentionAndFullAttentionLayer,
}
)


class Cache:
"""
A `Cache` is mostly a list of `CacheLayerMixin` objects, one per model layer. It serves as a container for
Expand Down Expand Up @@ -1240,20 +1296,8 @@ def __init__(
layer_types = layer_types[: -decoder_config.num_kv_shared_layers]

for layer_type in layer_types:
# From a cache point of view, both sliding and chunked are the same in how they should behave and how many
# states they should return - only the mask changes to make them different at the end!
if layer_type in ("sliding_attention", "chunked_attention"):
layers.append(DynamicSlidingWindowLayer(sliding_window=sliding_window))
# Note: we want moe layers to be LinearAttentionLayer, so that we can correctly grab sequence length etc from attention layers.
# Since moe layers will stay empty (they don't need any cache), we don't want them to collide for mask creation etc
# TODO: maybe use a dummy layer in those cases, or a dictionary {idx: Layer} for self.layers, so that we can skip
# the indices we don't need
elif layer_type in ("mamba", "conv", "linear_attention", "moe"):
layers.append(LinearAttentionLayer())
elif layer_type == "hybrid":
layers.append(LinearAttentionAndFullAttentionLayer())
else:
layers.append(DynamicLayer())
cache_cls = LAYER_TYPE_CACHE_MAPPING.get(layer_type, DynamicLayer)
layers.append(cache_cls(decoder_config))

# In this case, use the passed data to already fill in the Cache
if ddp_cache_data is not None:
Expand Down Expand Up @@ -1351,16 +1395,21 @@ def __init__(
if hasattr(config, "num_kv_shared_layers"):
layer_types = layer_types[: -config.num_kv_shared_layers]

sliding_layer_types = {
name
for name, cls in LAYER_TYPE_CACHE_MAPPING.items()
if isinstance(cls, type) and issubclass(cls, DynamicSlidingWindowLayer) and name != "chunked_attention"
}
layers = []
for layer_type in layer_types:
if layer_type == "sliding_attention":
layer = StaticSlidingWindowLayer(max_cache_len=max_cache_len, sliding_window=config.sliding_window)
elif layer_type == "chunked_attention":
if layer_type == "chunked_attention":
# From a cache point of view, both sliding and chunked are the same in how they should behave and how many
# states they should return - only the mask changes to make them different at the end!
layer = StaticSlidingWindowLayer(
max_cache_len=max_cache_len, sliding_window=config.attention_chunk_size
)
elif layer_type in sliding_layer_types:
layer = StaticSlidingWindowLayer(max_cache_len=max_cache_len, sliding_window=config.sliding_window)
# LinearAttention layers are static by essence - using `"moe"` as well is a trick, see the comment about it on DynamicCache
elif layer_type in ("mamba", "conv", "linear_attention", "moe"):
layer = LinearAttentionLayer()
Expand Down
2 changes: 2 additions & 0 deletions src/transformers/configuration_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"full_attention",
"sliding_attention",
"chunked_attention",
"compressed_sparse_attention", # CSA, used in deepseek_v4
"heavily_compressed_attention", # HCA, used in deepseek_v4
"linear_attention", # used in minimax
"conv", # used in LFMv2
"mamba",
Expand Down
Loading
Loading