Files
yyj a5d7ad9047 feat(sft) Deepseek v4 flash LoRA (#2216)
* feat(sft): add native MXFP4 routed-expert LoRA kernels

* test(sft): validate DeepSeek V4 MXFP4 routed experts

* feat(sft): AVX2 tier for MXFP4 routed-expert LoRA SFT

MXFP4_SFT_MOE on non-AVX512 builds, same Python contract as the AMX /
AVX512-BF16 kernel from #2185 (KTMoEWrapper(method="MXFP4_SFT")), for
AVX2+FMA+F16C hosts (Zen 2/3 EPYC, older Xeons).

* operators/avx2/sft_mxfp4-moe.hpp: AVX2_SFT_MXFP4_MOE_TP part on top of
  AVX2_MXFP4_MOE_TP (frozen packed E2M1/UE8M0 base, LoRA gate/up/down,
  V4 swiglu clamp, transpose-free packed-weight dX, authoritative LoRA grads).
* operators/moe-sft-tp.hpp: AVX2 result merge; AVX-512 merge loops and the
  block-FP8 staging guarded so the wrapper builds without AVX-512.
* ext_bindings.cpp: bind MXFP4_SFT_MOE -> TP_MOE_SFT<AVX2_SFT_MXFP4_MOE_TP>
  when USE_AMX_AVX_KERNEL is off; MOESFTBindings no longer AMX-only.
* python/sft/backend.py: runtime reports cpu_variant=avx2, kernel avx2-mxfp4-g32.
* test_mxfp4_sft_numerical.py: inference regression uses AVX2MXFP4_MOE when
  AMXFP4_KGroup_MOE is absent.

Validated on 2x EPYC 7452 (AVX2, no AVX-512): test_mxfp4_sft_numerical.py
TP1/TP2 all stages rel-L2 <= 0.4% (limits 3-5%); validate_dsv4_mxfp4_sft_layer.py
on DeepSeek-V4-Flash-0731 layer 3, TP1 and TP2: passed, forward/grad_input/
LoRA grads rel-L2 0.10-0.29%, base weights unchanged.

Note: the build uses -ffast-math, so exp() past the float range is undefined;
the scalar sigmoid tails clamp the argument like the vector path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Mgsduzz2fx1gSvfHbR8RZ

* fix(sft): clamp MXFP4 scalar-tail sigmoid exponent

* feat(sft): integrate native V4 experts with LoRA training and export

Wrap only routed experts while preserving V4 routing and shared branches. Validate native checkpoint provenance and stream a BF16 non-expert cache without dequantizing routed experts.

Add audited static SGLang export and reproducible training, resume and generation acceptance helpers. CPU retain/GPU recompute GAS1/GAS4 smoke and 265 KT regression tests pass; long-run quality and resume acceptance remain in progress.

* fix(sft): retain async inference buffers until CUDA work completes

* test(sft): isolate V4 adapter effects with matched-kernel controls

Allow baseline and non-expert-only exports to retain a zero-B expert adapter so all ablations execute the same native forward kernel. Add paired held-out generation reporting with explicitly diagnostic lexical metrics.

* test(sft): compare PEFT targets semantically during V4 resume audits

* test(sft): cancel queued V4 inference checks after a failed request

* test(sft): preserve raw V4 responses before finite-output validation

* feat: prepare V4 cache atomically and package static adapter export

* build: preserve accepted CUDA carriers across Python ABIs

* test: retain FP8 loading contracts after V4 wrapper integration

* test: exclude historical concatenated-sample smoke from delivery

* test(sft): audit unpacked batches and single-GPU resume state

* docs(sft): align April V4 recipe with approved unpacked delivery

* test: keep frozen-parameter audit indices exact for large tensors

* test: record the loaded native backend in acceptance evidence

* test: timestamp training audit events for startup measurements

* docs: pin V4 serving dependencies and account for reserved token slots

* [refactor](sft): keep V4 follow-up focused on runtime integration

---------

Co-authored-by: 05yuki <ceu.azul.samu@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 21:29:43 +08:00

277 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
import pytest
import torch
from torch import nn
from kt_kernel.sft.arch import MOEArchConfig, move_non_experts_to_gpu
from kt_kernel.sft.layer import KTMoELayerWrapper, KTRoutedExpertsWrapper
class _FakeWrapper:
def __init__(self):
self._full_weight_grad = False
self._uses_authoritative_optimizer_grads = False
self.share_backward_bb = False
self._next_backward_wrapper = None
self.output = None
self.weights_shape = None
def submit_forward(self, hidden_states, _expert_ids, weights, save_for_backward=True):
self.output = torch.zeros_like(hidden_states)
self.weights_shape = weights.shape
self.save_for_backward = save_for_backward
def sync_forward(self, output_device=None):
output = self.output.clone()
return output if output_device is None else output.to(output_device)
def backward(self, grad_output, output_device=None):
grad_input = torch.zeros_like(grad_output)
grad_weights = torch.zeros(self.weights_shape, dtype=torch.bfloat16)
if output_device is not None:
grad_input = grad_input.to(output_device)
grad_weights = grad_weights.to(output_device)
return grad_input, grad_weights
def clear_checkpoint_output(self):
pass
class _SharedMLP(nn.Module):
def __init__(self, hidden_size: int, intermediate_size: int):
super().__init__()
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
def forward(self, hidden_states):
return self.down_proj(torch.nn.functional.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
class _SingularSharedMoE(nn.Module):
def __init__(self, hidden_size: int, expert_num: int, intermediate_size: int):
super().__init__()
self.gate = nn.Linear(hidden_size, expert_num, bias=False)
self.experts = nn.Identity()
self.shared_expert = _SharedMLP(hidden_size, intermediate_size)
self.shared_expert_gate = nn.Linear(hidden_size, 1, bias=False)
class _PluralSharedMoE(nn.Module):
def __init__(self, hidden_size: int, expert_num: int):
super().__init__()
self.gate = nn.Linear(hidden_size, expert_num, bias=False)
self.experts = nn.Identity()
self.shared_experts = nn.Linear(hidden_size, hidden_size, bias=False)
def _moe_config(*, has_shared_experts: bool) -> MOEArchConfig:
return MOEArchConfig(
moe_layer_attr="mlp",
router_attr="gate",
experts_attr="experts",
weight_names=("gate_proj", "up_proj", "down_proj"),
expert_num=2,
intermediate_size=3,
num_experts_per_tok=1,
has_shared_experts=has_shared_experts,
)
def _wrap(original_moe: nn.Module, *, has_shared_experts: bool) -> KTMoELayerWrapper:
return KTMoELayerWrapper(
original_moe=original_moe,
wrapper=_FakeWrapper(),
lora_params=None,
moe_config=_moe_config(has_shared_experts=has_shared_experts),
hidden_size=4,
layer_idx=0,
full_weight_grad=False,
)
def test_qwen_singular_shared_expert_matches_gated_forward_and_updates_weights():
torch.manual_seed(0)
original_moe = _SingularSharedMoE(hidden_size=4, expert_num=2, intermediate_size=3)
hidden_states = torch.randn(2, 3, 4)
with torch.no_grad():
expected = original_moe.shared_expert(hidden_states)
expected = torch.sigmoid(original_moe.shared_expert_gate(hidden_states)) * expected
layer = _wrap(original_moe, has_shared_experts=True)
named_parameters = dict(layer.named_parameters())
shared_parameter_names = {
"shared_expert.gate_proj.weight",
"shared_expert.up_proj.weight",
"shared_expert.down_proj.weight",
"shared_expert_gate.weight",
}
assert shared_parameter_names.issubset(named_parameters)
assert not any(name.startswith("shared_experts.") for name in named_parameters)
train_input = hidden_states.clone().requires_grad_(True)
actual = layer(train_input)
torch.testing.assert_close(actual, expected)
before_step = {name: named_parameters[name].detach().clone() for name in shared_parameter_names}
optimizer = torch.optim.SGD(layer.parameters(), lr=0.1)
actual.square().sum().backward()
for name in shared_parameter_names:
grad = named_parameters[name].grad
assert grad is not None
assert torch.isfinite(grad).all()
assert torch.count_nonzero(grad) > 0
optimizer.step()
for name in shared_parameter_names:
assert not torch.equal(named_parameters[name], before_step[name])
def test_plural_shared_expert_keeps_legacy_ungated_behavior():
torch.manual_seed(1)
original_moe = _PluralSharedMoE(hidden_size=4, expert_num=2)
hidden_states = torch.randn(1, 2, 4)
with torch.no_grad():
expected = original_moe.shared_experts(hidden_states)
layer = _wrap(original_moe, has_shared_experts=True)
with torch.no_grad():
actual = layer(hidden_states)
torch.testing.assert_close(actual, expected)
assert "shared_experts.weight" in layer.state_dict()
def test_precomputed_routing_preserves_model_router_and_shared_branch(monkeypatch):
original = _PluralSharedMoE(hidden_size=4, expert_num=2)
router, experts, shared = original.gate, original.experts, original.shared_experts
config = _moe_config(has_shared_experts=True)
config.router_type = "precomputed"
wrapper = KTRoutedExpertsWrapper(
original_moe=original, wrapper=_FakeWrapper(), lora_params=None,
moe_config=config, hidden_size=4, layer_idx=0,
)
original.experts = wrapper
assert original.gate is router and original.shared_experts is shared
assert wrapper.experts is experts
assert wrapper._shared_expert_attr is None and wrapper.gate is None
x = torch.randn(3, 4, requires_grad=True)
ids = torch.tensor([[0], [1], [0]])
weights = torch.ones(3, 1, requires_grad=True)
def fake_execute(self, hidden, routed_ids, routed_weights):
assert self is wrapper and routed_ids is ids and routed_weights is weights
assert hidden.shape == (1, 3, 4)
return hidden * routed_weights.unsqueeze(0)
monkeypatch.setattr(KTMoELayerWrapper, "_forward_routed", fake_execute)
output = wrapper(x, ids, weights)
torch.testing.assert_close(output, x)
output.sum().backward()
torch.testing.assert_close(x.grad, torch.ones_like(x))
torch.testing.assert_close(weights.grad, x.detach().sum(-1, keepdim=True))
with pytest.raises(ValueError, match="routing tensors"):
wrapper(x, ids[:1], weights)
def test_huggingface_qwen35_shared_expert_contract():
configuration = pytest.importorskip(
"transformers.models.qwen3_5_moe.configuration_qwen3_5_moe",
reason="Qwen3.5 support requires a recent transformers build",
)
modeling = pytest.importorskip(
"transformers.models.qwen3_5_moe.modeling_qwen3_5_moe",
reason="Qwen3.5 support requires a recent transformers build",
)
config = configuration.Qwen3_5MoeTextConfig(
hidden_size=4,
moe_intermediate_size=3,
shared_expert_intermediate_size=3,
num_experts=2,
num_experts_per_tok=1,
num_hidden_layers=1,
num_attention_heads=1,
num_key_value_heads=1,
head_dim=4,
linear_key_head_dim=4,
linear_value_head_dim=4,
linear_num_key_heads=1,
linear_num_value_heads=1,
)
original_moe = modeling.Qwen3_5MoeSparseMoeBlock(config)
hidden_states = torch.randn(1, 2, 4)
with torch.no_grad():
expected = original_moe.shared_expert(hidden_states)
expected = torch.sigmoid(original_moe.shared_expert_gate(hidden_states)) * expected
layer = _wrap(original_moe, has_shared_experts=True)
with torch.no_grad():
actual = layer(hidden_states)
torch.testing.assert_close(actual, expected)
assert "shared_expert.gate_proj.weight" in layer.state_dict()
assert "shared_expert_gate.weight" in layer.state_dict()
def test_declared_shared_expert_missing_from_module_fails_fast():
original_moe = nn.Module()
original_moe.gate = nn.Linear(4, 2, bias=False)
original_moe.experts = nn.Identity()
with pytest.raises(ValueError, match="neither 'shared_experts' nor 'shared_expert'"):
_wrap(original_moe, has_shared_experts=True)
def test_qwen_singular_shared_expert_without_gate_fails_fast():
original_moe = nn.Module()
original_moe.gate = nn.Linear(4, 2, bias=False)
original_moe.experts = nn.Identity()
original_moe.shared_expert = _SharedMLP(hidden_size=4, intermediate_size=3)
with pytest.raises(ValueError, match="requires 'shared_expert_gate'"):
_wrap(original_moe, has_shared_experts=True)
class _MoveRecorder(nn.Module):
def __init__(self):
super().__init__()
self.devices = []
def to(self, device):
self.devices.append(device)
return self
class _PlacementModel(nn.Module):
def __init__(self):
super().__init__()
moe = nn.Module()
moe.gate = _MoveRecorder()
moe.experts = nn.Identity()
moe.shared_expert = _MoveRecorder()
moe.shared_expert_gate = _MoveRecorder()
layer = nn.Module()
layer.self_attn = _MoveRecorder()
layer.input_layernorm = _MoveRecorder()
layer.post_attention_layernorm = _MoveRecorder()
layer.mlp = moe
self.model = nn.Module()
self.model.embed_tokens = _MoveRecorder()
self.model.norm = _MoveRecorder()
self.model.layers = nn.ModuleList([layer])
self.lm_head = _MoveRecorder()
def test_qwen_shared_expert_and_gate_follow_non_experts_to_accelerator():
model = _PlacementModel()
move_non_experts_to_gpu(model, _moe_config(has_shared_experts=True), device="test:0")
moe = model.model.layers[0].mlp
assert moe.shared_expert.devices == ["test:0"]
assert moe.shared_expert_gate.devices == ["test:0"]