Skip to content

DeepSeek-V4: eager attention and trainable FP8 grouped experts#7042

Merged
danielhanchen merged 5 commits into
mainfrom
deepseek-v4-fp8-grouped-training
Jul 12, 2026
Merged

DeepSeek-V4: eager attention and trainable FP8 grouped experts#7042
danielhanchen merged 5 commits into
mainfrom
deepseek-v4-fp8-grouped-training

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

Finetuning DeepSeek-V4 (deepseek_v4) with FastModel currently fails in two places. This fixes both so the model loads and trains.

  1. Loading raises an attention error. deepseek_v4 ships a custom attention that is not compatible with the sdpa or flash paths, so it must run eager.
  2. Once loaded, loss.backward() errors. Transformers stores the fused experts as FP8GroupedLinear, whose forward calls a grouped matmul kernel that has no autograd formula, so backward fails during finetuning.

Changes

  • unsloth/models/_utils.py: add deepseek_v4 to _EAGER_ONLY_PREFIXES so it loads with eager attention.
  • unsloth/kernels/fp8.py: patch FP8GroupedLinear.forward so that during training it dequantizes the frozen fp8 weight and runs a differentiable grouped matmul, while inference keeps the original fused fp8 kernel. Gated on torch.is_grad_enabled() and a fp8 weight, so the inference path is unchanged.

Why bmm on the dequantized weight

  • torch._grouped_mm: forward matches exactly, but its backward raises Invalid strides/sizes on the grouped-expert shapes, and backward is exactly what we need.
  • torch._scaled_grouped_mm: needs a transposed operand and fp8-quantized activations, which adds quantization error and complexity.
  • Dequantizing the frozen fp8 weight to bf16 and using torch.bmm is exact at fp8 precision, has a finite backward, and works on all shapes. Only the few grouped-linear modules materialize in bf16, so the overhead is negligible.

Testing

On trl-internal-testing/tiny-DeepseekV4ForCausalLM:

  • Loads in 4bit with eager attention.
  • LoRA attaches to the experts and receives gradient.
  • Forward and backward run with finite loss and finite gradients.
  • FP8GroupedLinear parity: dequant + bmm matches the fused fp8 kernel to 2.7e-2 relative (fp8 rounding), with finite gradients.

Scope is loading and finetuning. Saving an fp8 checkpoint merged to 16bit is a separate path and is not changed here.

deepseek_v4 ships a custom attention that is not compatible with the sdpa
and flash paths, so add it to _EAGER_ONLY_PREFIXES to load with eager.

Its fused experts load as FP8GroupedLinear, whose forward calls a grouped
matmul kernel with no autograd formula, so loss.backward() fails during
finetuning. Patch the forward to dequantize the frozen fp8 weight and run a
differentiable grouped matmul while training, keeping the fused fp8 kernel
for inference.
@danielhanchen danielhanchen requested a review from Datta0 as a code owner July 9, 2026 15:08
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c3b676a01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth/models/_utils.py
_FLASH_EXCLUDED_MODELS = ("gpt_oss",)
_EAGER_ONLY_PREFIXES = ("gemma3n",)
# deepseek_v4 ships sdpa/flash-incompatible custom attention; force eager.
_EAGER_ONLY_PREFIXES = ("gemma3n", "deepseek_v4")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep DeepSeek-V4 eager after explicit requests

For DeepSeek-V4 this only changes the resolver's default choice; if a caller passes requested_attn_implementation="sdpa" or "flash_attention_2", resolve_attention_implementation later overwrites the eager default with that requested backend because deepseek_v4 is not in the SDPA/flash exclusion checks. In contexts that forward an explicit attention kwarg to this helper, the model still reaches the sdpa/flash-incompatible attention path this change is trying to avoid, so the DeepSeek-V4 guard needs to participate in the final override/exclusion logic too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed7ddd0: added deepseek_v4 to _SDPA_EXCLUDED_MODELS and _FLASH_EXCLUDED_MODELS, so an explicit attn_implementation of sdpa or flash downgrades to eager instead of reaching the unsupported path. Confirmed from_pretrained(attn_implementation=sdpa) now loads as eager.

Comment thread unsloth/kernels/fp8.py Outdated
if self.weight.element_size() > 1 or not torch.is_grad_enabled():
return _fp8_grouped_forward_orig(self, x)
hidden_dim = x.shape[-1]
W = weight_dequant(self.weight, self.weight_scale_inv.float()).to(x.dtype)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dequantize local tensor-parallel shards

When an FP8GroupedLinear has been tensor-parallelized, Transformers stores weight/weight_scale_inv as distributed tensors and its original FP8 path unwraps them with to_local() before reshaping and calling the grouped matmul. This training replacement bypasses that unwrap, so TP DeepSeek-V4 finetuning feeds a distributed parameter into the local Triton dequantizer/view path and can fail or use the wrong shard metadata; unwrap both tensors to their local shards before weight_dequant.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded into ed7ddd0: the new autograd Function unwraps DTensor shards via to_local before dequant, matching the original fp8 path. unsloth does not tensor-parallelize these modules for finetuning today, so this is defensive, but it removes the divergence you noted.

Comment thread unsloth/kernels/fp8.py Outdated
W = weight_dequant(self.weight, self.weight_scale_inv.float()).to(x.dtype)
w = W.view(self.n_groups, -1, hidden_dim).transpose(1, 2)
xg = x.reshape(-1, self.n_groups, hidden_dim).transpose(0, 1)
y = torch.bmm(xg, w).transpose(0, 1).reshape(*x.shape[:-2], self.n_groups, -1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid retaining dequantized grouped weights

When training without gradient checkpointing, this torch.bmm makes autograd retain w, a bf16 dequantized copy of the full grouped projection weight, for every FP8GroupedLinear call until backward. DeepSeek-V4 has this grouped projection in each decoder block, so Pro-sized layers can add many GB of saved tensors and OOM; use a custom autograd path that saves the fp8 weight/scale and dequantizes during backward instead of letting bmm save the bf16 weight.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed7ddd0: replaced the training bmm with a custom autograd Function that saves only the fp8 weight and scale rather than the bf16 dequantized copy, so no dequantized grouped weight is retained per layer; backward re-dequantizes. Bit-exact forward and grad with the previous path.

Add deepseek_v4 to _SDPA_EXCLUDED_MODELS and _FLASH_EXCLUDED_MODELS so an
explicit attn_implementation=sdpa/flash request downgrades to eager instead of
raising (the model has no sdpa/flash kernel), matching the eager-only default.

Replace the FP8GroupedLinear training bmm with a custom autograd Function that
saves only the fp8 weight + scale rather than a full bf16 dequantized copy, so
no dequantized grouped weight is retained per layer, and unwrap tensor-parallel
shards before dequant. Bit-exact forward and grad with the previous path.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed7ddd0bd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth/kernels/fp8.py Outdated
def forward(ctx, x, weight, scale_inv, n_groups, bias):
weight, scale_inv = _fp8_to_local(weight), _fp8_to_local(scale_inv)
hidden = x.shape[-1]
W = weight_dequant(weight, scale_inv.float()).to(x.dtype)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor grouped layer block sizes when dequantizing

When an FP8GroupedLinear is quantized with a non-128 or rectangular weight_block_size, this training path calls weight_dequant without the layer's block size, so it falls back to the hard-coded 128x128 tiling. The existing _blockwise_weight_dequant_any_shape helper in this file handles exactly these cases because the default dequant can mis-index scales; here the custom backward path will train against weights dequantized differently from the original fused grouped matmul. Pass self.block_size into the custom function and use the block-size-aware helper instead of assuming 128.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 24c14b8: the function now dequantizes with the layer's own block_size via _blockwise_weight_dequant_any_shape, so non-128 or rectangular fp8 blocks are scaled correctly instead of assuming 128x128.

Comment thread unsloth/kernels/fp8.py Outdated
return grad_x, None, None, None, grad_bias

def _fp8_grouped_forward(self, x):
if self.weight.element_size() > 1 or not torch.is_grad_enabled():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep training math consistent under checkpointing

When this layer is inside a reentrant gradient-checkpointed segment, the first training forward runs with grad disabled, so this condition takes the original fused grouped-matmul path, while the backward recomputation runs with grad enabled and takes the dequantized bmm path. Checkpointing assumes the recomputed forward is the same function as the one that produced the loss; using different FP8 kernels here means gradients are for different activations in checkpointed finetuning. Gate on training/checkpoint intent rather than torch.is_grad_enabled() alone, or make both paths use the same math during training.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 24c14b8: the training path now gates on self.training instead of torch.is_grad_enabled(), so a gradient-checkpointed segment runs the same bmm math in both its no-grad forward and its grad recompute (eval still uses the fused kernel). Verified the two passes are now bit-identical.

danielhanchen and others added 2 commits July 9, 2026 17:09
Gate the differentiable training path on self.training rather than
torch.is_grad_enabled(), so a gradient-checkpointed segment runs the same bmm
math in its no-grad forward and its grad recompute instead of mixing the fused
fp8 kernel with bmm.

Dequantize with the layer's own block_size via _blockwise_weight_dequant_any_shape
so non-128 or rectangular fp8 blocks are scaled correctly instead of assuming
128x128.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 0e875d17c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 5ad58f36b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen danielhanchen merged commit f899834 into main Jul 12, 2026
52 checks passed
@danielhanchen danielhanchen deleted the deepseek-v4-fp8-grouped-training branch July 12, 2026 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant