Files
ktransformers/kt-kernel/python/sft/export_dsv4_sglang_adapter.py
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

309 lines
12 KiB
Python

#!/usr/bin/env python3
"""Export a single V4 adapter: merged BF16 non-experts + native MXFP4 expert LoRA.
This is static deployment, not adapter hot swapping. Original checkpoint files
are never modified. The model copy contains no routed-expert base weights.
"""
import argparse
import json
import re
import shutil
from pathlib import Path
from types import SimpleNamespace
import torch
from safetensors import safe_open
from safetensors.torch import load_file, save_file
from kt_kernel.sft.artifacts import (
FUSED_EXPERT_LORA_NAME,
KT_ADAPTER_MANIFEST_NAME,
KTArtifactError,
_read_json,
_sha256_file,
_write_json_atomic,
)
from kt_kernel.sft.deepseek_v4 import (
canonical_non_expert_key,
inspect_native_checkpoint,
resolve_native_load_plan,
)
from .convert_kt_to_sglang_adapter import (
_build_adapter_config,
_convert_fused_expert_lora,
)
_STANDARD = re.compile(
r"^(?:base_model\.model\.)?(model\.layers\.\d+\..+)\.lora_([AB])(?:\.default)?\.weight$"
)
_TARGET = re.compile(
r"^model\.layers\.\d+\.(?:self_attn\.(?:q_a_proj|q_b_proj|kv_proj|o_b_proj|compressor\.(?:kv_proj|gate_proj))"
r"|mlp\.shared_experts\.(?:gate_proj|up_proj|down_proj))$"
)
def _validate_fused_experts(path, config, rank):
expected = {}
experts = config["n_routed_experts"]
hidden, inter = config["hidden_size"], config["moe_intermediate_size"]
for layer in range(config["num_hidden_layers"]):
for projection, (n, k) in {
"gate": (inter, hidden),
"up": (inter, hidden),
"down": (hidden, inter),
}.items():
prefix = f"layers.{layer}.experts.{projection}_lora_"
expected[prefix + "a"] = [experts, rank, k]
expected[prefix + "b"] = [experts, n, rank]
with safe_open(path, framework="pt", device="cpu") as handle:
if set(handle.keys()) != set(expected):
raise KTArtifactError(
"incomplete or unexpected fused expert LoRA inventory"
)
for name, shape in expected.items():
tensor = handle.get_slice(name)
if tensor.get_shape() != shape or tensor.get_dtype() not in {
"BF16",
"F16",
"F32",
}:
raise KTArtifactError(f"invalid fused expert LoRA shape/dtype: {name}")
if not torch.isfinite(handle.get_tensor(name)).all():
raise KTArtifactError(f"non-finite fused expert LoRA: {name}")
def export(
source_path,
cache_path,
adapter_path,
output_path,
component="all",
match_expert_kernel=False,
):
output, adapter = Path(output_path).absolute(), Path(adapter_path).absolute()
if component not in {"all", "base", "experts", "nonexperts"}:
raise KTArtifactError(f"invalid export component: {component}")
if output.exists():
raise KTArtifactError(f"export destination already exists: {output}")
for path in (source_path, cache_path, adapter_path):
source_root = Path(path).resolve()
if output.resolve().is_relative_to(source_root) or source_root.is_relative_to(
output.resolve()
):
raise KTArtifactError(
"export destination must not overlap an input directory"
)
plan = resolve_native_load_plan(
SimpleNamespace(
kt_weight_path=str(Path(source_path).resolve()),
kt_non_expert_weight_path=cache_path,
),
source_path,
)
manifest = _read_json(adapter / KT_ADAPTER_MANIFEST_NAME, "KT adapter manifest")
if (
manifest.get("status") != "ready"
or manifest.get("expert_weight_format") != "mxfp4"
):
raise KTArtifactError("expected a ready native-MXFP4 training adapter")
if (
manifest.get("base", {}).get("fingerprint")
!= plan.manifest["source"]["fingerprint"]
):
raise KTArtifactError("adapter base does not match the native checkpoint")
required = {
"adapter_config.json",
"adapter_model.safetensors",
FUSED_EXPERT_LORA_NAME,
}
artifacts = manifest.get("artifacts", {})
if not required.issubset(artifacts):
raise KTArtifactError("adapter manifest is missing a required artifact")
for name, record in artifacts.items():
path = adapter / name
if Path(name).name != name or path.is_symlink() or not path.is_file():
raise KTArtifactError(f"invalid adapter artifact: {name}")
if (
path.stat().st_size != record["size"]
or _sha256_file(path) != record["sha256"]
):
raise KTArtifactError(f"adapter artifact failed integrity check: {name}")
config = _read_json(adapter / "adapter_config.json", "PEFT config")
rank, alpha = int(config["r"]), float(config["lora_alpha"])
if rank != 8 or alpha != 16 or config.get("lora_dropout", 0.0) != 0:
raise KTArtifactError("April V4 static export requires rank=8, alpha=16 and dropout=0")
if (
config.get("use_dora")
or config.get("use_rslora")
or config.get("rank_pattern")
or config.get("alpha_pattern")
):
raise KTArtifactError(
"V4 static export currently requires uniform standard LoRA"
)
if manifest.get("lora") != {"rank": rank, "alpha": alpha}:
raise KTArtifactError("adapter rank/alpha provenance mismatch")
standard = load_file(str(adapter / "adapter_model.safetensors"))
pairs, consumed = {}, []
for name, tensor in standard.items():
match = _STANDARD.fullmatch(name)
if match is None or not _TARGET.fullmatch(match[1]):
raise KTArtifactError(f"unsupported non-expert adapter tensor: {name}")
pair = pairs.setdefault(match[1], {})
if match[2] in pair or tensor.ndim != 2 or not torch.isfinite(tensor).all():
raise KTArtifactError(f"invalid or duplicate adapter tensor: {name}")
pair[match[2]] = tensor
if not pairs or any(set(pair) != {"A", "B"} for pair in pairs.values()):
raise KTArtifactError("incomplete standard LoRA A/B pairs")
source = inspect_native_checkpoint(source_path)
_validate_fused_experts(adapter / FUSED_EXPERT_LORA_NAME, source["config"], rank)
reverse = {
canonical_non_expert_key(key): key
for key in source["inventory"]
if not key.startswith("mtp.")
and ".experts." not in key
and not key.endswith(".scale")
}
output.mkdir(parents=True)
model_dir = output / "model"
model_dir.mkdir()
weight_map, files, covered = {}, [], set()
for index, checkpoint in enumerate(plan.checkpoint_files):
tensors, exported = load_file(checkpoint), {}
for name, weight in tensors.items():
module = name.removesuffix(".weight")
if module in pairs:
covered.add(module)
a, b = pairs[module]["A"], pairs[module]["B"]
if tuple(a.shape) != (rank, weight.shape[1]) or tuple(b.shape) != (
weight.shape[0],
rank,
):
raise KTArtifactError(f"LoRA shape mismatch: {module}")
if component in {"all", "nonexperts"}:
weight = (
weight.float() + (alpha / rank) * (b.float() @ a.float())
).to(weight.dtype)
if not torch.isfinite(weight).all():
raise KTArtifactError(f"non-finite merged weight: {module}")
consumed.append(module)
exported[reverse[name]] = weight.contiguous()
filename = f"non-experts-{index:03d}.safetensors"
save_file(exported, model_dir / filename, metadata={"format": "pt"})
weight_map.update({key: filename for key in exported})
files.append({"name": filename, "sha256": _sha256_file(model_dir / filename)})
if covered != set(pairs):
raise KTArtifactError("standard LoRA pair has no matching base weight")
model_config = dict(source["config"])
model_config.pop("quantization_config", None)
model_config.update(
torch_dtype="bfloat16", num_nextn_predict_layers=0, use_cache=True
)
_write_json_atomic(model_dir / "config.json", model_config)
_write_json_atomic(
model_dir / "model.safetensors.index.json",
{"metadata": {}, "weight_map": weight_map},
)
for name in (
"tokenizer.json",
"tokenizer_config.json",
"special_tokens_map.json",
"generation_config.json",
):
path = Path(source_path) / name
if path.is_file():
shutil.copyfile(path, model_dir / name)
expert_count = 0
expert_enabled = component in {"all", "experts"}
if expert_enabled or match_expert_kernel:
expert_tensors, expert_rank, targets = _convert_fused_expert_lora(
adapter / FUSED_EXPERT_LORA_NAME
)
if (
expert_rank != rank
or len(expert_tensors)
!= source["config"]["num_hidden_layers"]
* source["config"]["n_routed_experts"]
* 6
):
raise KTArtifactError("expert adapter rank or tensor inventory mismatch")
if not expert_enabled:
expert_tensors = {
name: torch.zeros_like(tensor) if ".lora_B." in name else tensor
for name, tensor in expert_tensors.items()
}
expert_dir = output / "experts"
expert_dir.mkdir()
save_file(expert_tensors, expert_dir / "adapter_model.safetensors")
_write_json_atomic(
expert_dir / "adapter_config.json",
_build_adapter_config(
adapter,
rank,
targets,
source_path,
alpha,
include_input_target_modules=False,
),
)
expert_count = len(expert_tensors)
report = {
"version": 1,
"status": "ready",
"component": component,
"deployment": "static-merged-bf16-non-experts/native-mxfp4-expert-lora",
"source": source_path,
"source_fingerprint": source["fingerprint"],
"adapter": str(adapter),
"adapter_manifest_sha256": _sha256_file(adapter / KT_ADAPTER_MANIFEST_NAME),
"standard_pairs_consumed": sorted(consumed),
"standard_source_tensor_count": len(standard),
"expert_exported_tensor_count": expert_count,
"expert_effect_enabled": expert_enabled,
"zero_expert_adapter_control": bool(match_expert_kernel and not expert_enabled),
"model_files": files,
"rounding": "standard LoRA merged in FP32 then rounded to BF16",
}
_write_json_atomic(output / "deployment_manifest.json", report)
print(
json.dumps(
{
key: value
for key, value in report.items()
if key not in {"standard_pairs_consumed", "model_files"}
},
indent=2,
)
)
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
for argument in ("source", "cache", "adapter", "output"):
parser.add_argument("--" + argument, required=True)
parser.add_argument(
"--component", choices=("all", "base", "experts", "nonexperts"), default="all"
)
parser.add_argument(
"--match-expert-kernel",
action="store_true",
help="Use zero-B expert LoRA in baseline ablations so every component uses the same native SFT forward kernel.",
)
args = parser.parse_args()
export(
args.source,
args.cache,
args.adapter,
args.output,
args.component,
args.match_expert_kernel,
)
if __name__ == "__main__":
main()