Files
ktransformers/kt-kernel/python/sft/checkpoint.py
yyj 924754a00b [feat](kt-kernel): end-to-end full-parameter and LoRA SFT(#2094)
* Add full FT development snapshot

* [fix](kt-kernel): fix Full FT TP base weight gradients

* [fix]: bug fix of 2d81e86

* [fix](kt-kernel): fix AMX BF16 full-weight gradients

* [docs](kt-kernel): document Full FT fork changes and debug history

* [docs](kt-kernel): align fork remote terminology

* [chore](kt-kernel): keep agent notes local

* [fix](kt-kernel): configure SFT OpenMP threads

* [perf](kt-kernel): optimize AMX Full-FT weight gradients

Coarsen base-weight gradient work from individual output tiles to fixed-intermediate strips so each task reuses packed panels across the hidden dimension.

Keep aligned thread-local BF16 panels across tasks and retain FP32 AMX accumulator tiles for the full K reduction. Gate and up run separate K passes while sharing the packed input panel.

On the matched Qwen3-30B-A3B 1-GPU test, stable Full-FT backward drops from 9.281s to 6.792s (-26.82%), step time drops from 19.252s to 16.140s, and TPS rises from 212.76 to 253.78. The LoRA-only backward control changes by -2.74%.

Validated with clang-format, the Release AMX/CUDA extension build, TP1/TP2 reference gradients across boundary token counts, and the 15-step Full-then-LoRA performance run.

* [feat](kt-kernel): add staged SFT profiling

* [fix](kt-kernel): reuse inference BF16 kernel for SFT

* [perf](kt-kernel): add fine-grained Full-FT profiling

* [perf](kt-kernel): batch BF16 Full-FT weight gradients

Use one expert-aggregated tile driver for AVX512-BF16 and AMX base-weight gradients, and pack updated full-precision weights directly into TP BufferB layouts without temporary partitions. Add worker-local profiling and focused dWeight/strided-repack coverage.

* [test](kt-kernel): benchmark BF16 dWeight AMX driver

* [fix](kt-kernel): label dWeight store as worker CPU time

* [docs](sft): record BF16 Full-FT performance

* [docs](sft): remove Qwen3 Full-FT performance report

* [perf](kt-kernel): reduce BF16 Full-FT checkpoint overhead

Retain the first CPU MoE forward state across non-reentrant checkpoint recomputation, write BF16 activations directly into the backward cache, and reduce dWeight packing and gradient-clear traffic. Extend staged profiling and cover checkpoint reuse plus AMX/AVX dWeight paths.

* [perf](kt-kernel): make SFT optimizer gradients authoritative

Bind Full-FT and LoRA Parameter.grad directly to the KT-managed BF16 gradient buffers, avoiding PyTorch duplicate accumulation.

Accumulate microbatch gradients in C++, lazily clear expert buffers between optimizer windows, preserve rank-0 distributed ownership, and add lifecycle and AMX dWeight coverage.

* [perf](kt-kernel): avoid eager Full-FT gradient zeroing

Allocate authoritative Full-FT gradient buffers with torch.empty. The C++ state machine performs the mandatory full clear before first use, avoiding redundant Python-side first touch.

* perf(sft): enable checkpoint forward reuse for LoRA

* fix(sft): serialize checkpoint recompute with async repack

* fix(sft): persist authoritative full weights

* fix(sft): normalize legacy distributed gradients

* [fix](sft): train gated shared experts

* fix(sft): preserve expert placeholders across state dict loads

* ci: publish ktransformers sdist in release workflow

* fix(sft): preserve router autograd for LoRA training

Determine routing graph tracking from the router's trainable parameters instead of the Full-FT mode flag, so PEFT LoRA adapters on MoE gates receive routing-weight gradients. Fail fast when a trainable router returns detached weights and cover frozen, TopK, checkpoint-reuse, and two-step optimizer behavior.

---------

Co-authored-by: illu <wubowen03@foxmail.com>
2026-07-23 21:23:34 +08:00

427 lines
18 KiB
Python

# Full-weight checkpoint utilities for KT SFT
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import json
import logging
import os
import uuid
from typing import Any
import torch
from .dist_utils import _distributed_rank_world_size
logger = logging.getLogger(__name__)
FULL_WEIGHT_CHECKPOINT_FORMAT = "kt_full_experts"
FULL_WEIGHT_CHECKPOINT_VERSION = 1
FULL_WEIGHT_INDEX_NAME = "kt_full_experts.index.json"
_PROJECTION_NAMES = ("gate_proj", "up_proj", "down_proj")
_DTYPE_NAME = "BF16"
def resolve_full_weight_checkpoint(path: str | os.PathLike[str] | None) -> str | None:
"""Return the checkpoint directory when *path* contains a KT Full index."""
if path is None:
return None
candidate = os.fspath(path)
if os.path.isfile(candidate):
if os.path.basename(candidate) != FULL_WEIGHT_INDEX_NAME:
return None
return os.path.dirname(os.path.abspath(candidate))
index_path = os.path.join(candidate, FULL_WEIGHT_INDEX_NAME)
if os.path.isfile(index_path):
return os.path.abspath(candidate)
return None
def _full_weight_wrappers(wrappers: list[Any]) -> list[Any]:
return [wrapper for wrapper in wrappers if bool(getattr(wrapper, "_full_weight_grad", False))]
def _expected_shapes(wrapper: Any) -> dict[str, tuple[int, ...]]:
moe_config = wrapper.moe_config
expert_num = int(moe_config.expert_num)
intermediate_size = int(moe_config.intermediate_size)
hidden_size = int(wrapper.hidden_size)
return {
"gate_proj": (expert_num, intermediate_size, hidden_size),
"up_proj": (expert_num, intermediate_size, hidden_size),
"down_proj": (expert_num, hidden_size, intermediate_size),
}
def _backend_parameters(wrapper: Any) -> dict[str, torch.nn.Parameter]:
backend = getattr(wrapper, "wrapper", None)
if backend is None:
raise RuntimeError(f"Layer {wrapper.layer_idx}: rank 0 does not own the KT Full backend")
params: dict[str, torch.nn.Parameter] = {}
for name in _PROJECTION_NAMES:
param = getattr(backend, f"{name}_buf", None)
if not isinstance(param, torch.nn.Parameter):
raise RuntimeError(f"Layer {wrapper.layer_idx}: missing authoritative Parameter {name}_buf")
params[name] = param
return params
def _validate_parameter_set(wrapper: Any, params: dict[str, torch.nn.Parameter]) -> None:
expected_shapes = _expected_shapes(wrapper)
for name, param in params.items():
if tuple(param.shape) != expected_shapes[name]:
raise RuntimeError(
f"Layer {wrapper.layer_idx} {name}_buf shape mismatch: "
f"got {tuple(param.shape)}, expected {expected_shapes[name]}"
)
if param.dtype != torch.bfloat16:
raise RuntimeError(
f"Layer {wrapper.layer_idx} {name}_buf dtype mismatch: got {param.dtype}, expected torch.bfloat16"
)
if param.device.type != "cpu":
raise RuntimeError(f"Layer {wrapper.layer_idx} {name}_buf must be on CPU, got device={param.device}")
if not param.is_contiguous():
raise RuntimeError(f"Layer {wrapper.layer_idx} {name}_buf must be contiguous")
if not param.requires_grad:
raise RuntimeError(f"Layer {wrapper.layer_idx} {name}_buf must require gradients")
def _read_index(checkpoint_dir: str) -> dict[str, Any]:
index_path = os.path.join(checkpoint_dir, FULL_WEIGHT_INDEX_NAME)
try:
with open(index_path, encoding="utf-8") as handle:
index = json.load(handle)
except FileNotFoundError:
raise FileNotFoundError(f"Missing KT Full checkpoint index: {index_path}") from None
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"Failed to read KT Full checkpoint index {index_path}: {exc}") from exc
if index.get("format") != FULL_WEIGHT_CHECKPOINT_FORMAT:
raise RuntimeError(
f"Unsupported KT Full checkpoint format {index.get('format')!r}; "
f"expected {FULL_WEIGHT_CHECKPOINT_FORMAT!r}"
)
if index.get("version") != FULL_WEIGHT_CHECKPOINT_VERSION:
raise RuntimeError(
f"Unsupported KT Full checkpoint version {index.get('version')!r}; "
f"expected {FULL_WEIGHT_CHECKPOINT_VERSION}"
)
if index.get("dtype") != _DTYPE_NAME:
raise RuntimeError(f"Unsupported KT Full checkpoint dtype {index.get('dtype')!r}; expected {_DTYPE_NAME}")
if not isinstance(index.get("generation"), str) or not index["generation"]:
raise RuntimeError("KT Full checkpoint index must contain a non-empty generation")
layers = index.get("layers")
if not isinstance(layers, dict):
raise RuntimeError("KT Full checkpoint index must contain a 'layers' object")
return index
def _safe_shard_path(checkpoint_dir: str, filename: Any) -> str:
if not isinstance(filename, str) or not filename:
raise RuntimeError(f"Invalid KT Full checkpoint shard filename: {filename!r}")
if os.path.basename(filename) != filename:
raise RuntimeError(f"KT Full checkpoint shard must be a basename: {filename!r}")
return os.path.join(checkpoint_dir, filename)
def _inspect_layer_shard(
checkpoint_dir: str,
layer_idx: int,
layer_entry: Any,
expected_shapes: dict[str, tuple[int, ...]],
expected_generation: str,
) -> str:
from safetensors import safe_open
if not isinstance(layer_entry, dict):
raise RuntimeError(f"Layer {layer_idx}: invalid KT Full checkpoint index entry")
indexed_shapes = layer_entry.get("shapes")
expected_index_shapes = {name: list(expected_shapes[name]) for name in _PROJECTION_NAMES}
if indexed_shapes != expected_index_shapes:
raise RuntimeError(
f"Layer {layer_idx}: KT Full checkpoint index shapes mismatch: "
f"got {indexed_shapes!r}, expected {expected_index_shapes!r}"
)
shard_path = _safe_shard_path(checkpoint_dir, layer_entry.get("file"))
if not os.path.isfile(shard_path):
raise FileNotFoundError(f"Layer {layer_idx}: missing KT Full checkpoint shard {shard_path}")
expected_size = layer_entry.get("size_bytes")
if not isinstance(expected_size, int) or expected_size <= 0:
raise RuntimeError(f"Layer {layer_idx}: invalid shard size in KT Full checkpoint index")
actual_size = os.path.getsize(shard_path)
if actual_size != expected_size:
raise RuntimeError(
f"Layer {layer_idx}: KT Full checkpoint shard size mismatch: "
f"got {actual_size}, expected {expected_size}"
)
try:
with safe_open(shard_path, framework="pt", device="cpu") as handle:
keys = set(handle.keys())
expected_keys = set(_PROJECTION_NAMES)
if keys != expected_keys:
raise RuntimeError(
f"Layer {layer_idx}: KT Full checkpoint keys mismatch: "
f"got {sorted(keys)}, expected {sorted(expected_keys)}"
)
metadata = handle.metadata() or {}
if metadata.get("format") != FULL_WEIGHT_CHECKPOINT_FORMAT:
raise RuntimeError(f"Layer {layer_idx}: invalid KT Full checkpoint shard format metadata")
if metadata.get("version") != str(FULL_WEIGHT_CHECKPOINT_VERSION):
raise RuntimeError(f"Layer {layer_idx}: invalid KT Full checkpoint shard version metadata")
if metadata.get("generation") != expected_generation:
raise RuntimeError(f"Layer {layer_idx}: shard generation metadata does not match the index")
if metadata.get("layer_idx") != str(layer_idx):
raise RuntimeError(f"Layer {layer_idx}: shard layer metadata does not match the index")
for name in _PROJECTION_NAMES:
tensor_slice = handle.get_slice(name)
shape = tuple(tensor_slice.get_shape())
dtype = tensor_slice.get_dtype()
if shape != expected_shapes[name]:
raise RuntimeError(
f"Layer {layer_idx} {name} shape mismatch: " f"got {shape}, expected {expected_shapes[name]}"
)
if dtype != _DTYPE_NAME:
raise RuntimeError(f"Layer {layer_idx} {name} dtype mismatch: got {dtype}, expected {_DTYPE_NAME}")
except (OSError, ValueError) as exc:
raise RuntimeError(f"Layer {layer_idx}: failed to inspect KT Full checkpoint shard: {exc}") from exc
return shard_path
def save_full_weight_checkpoint(wrappers: list[Any], output_dir: str) -> bool:
"""Atomically publish rank-0 Full/Hybrid expert Parameters as per-layer shards."""
full_wrappers = _full_weight_wrappers(wrappers)
if not full_wrappers:
return False
rank, _ = _distributed_rank_world_size()
if rank != 0:
return False
from safetensors.torch import save_file
os.makedirs(output_dir, exist_ok=True)
generation = uuid.uuid4().hex
layer_entries: dict[str, dict[str, Any]] = {}
created_paths: list[str] = []
temporary_paths: list[str] = []
seen_layers: set[int] = set()
previous_index = None
previous_checkpoint = resolve_full_weight_checkpoint(output_dir)
if previous_checkpoint is not None:
try:
previous_index = _read_index(previous_checkpoint)
except Exception:
logger.warning("Existing KT Full checkpoint index is invalid; it will not be cleaned up", exc_info=True)
try:
for wrapper in sorted(full_wrappers, key=lambda item: int(item.layer_idx)):
layer_idx = int(wrapper.layer_idx)
if layer_idx < 0:
raise RuntimeError(f"Invalid negative KT Full layer index {layer_idx}")
if layer_idx in seen_layers:
raise RuntimeError(f"Duplicate KT Full wrapper for layer {layer_idx}")
seen_layers.add(layer_idx)
params = _backend_parameters(wrapper)
_validate_parameter_set(wrapper, params)
filename = f"kt_full_experts-{generation}-layer-{layer_idx:05d}.safetensors"
shard_path = os.path.join(output_dir, filename)
temp_path = f"{shard_path}.tmp"
temporary_paths.append(temp_path)
tensors = {name: param.detach() for name, param in params.items()}
save_file(
tensors,
temp_path,
metadata={
"format": FULL_WEIGHT_CHECKPOINT_FORMAT,
"version": str(FULL_WEIGHT_CHECKPOINT_VERSION),
"generation": generation,
"layer_idx": str(layer_idx),
},
)
os.replace(temp_path, shard_path)
temporary_paths.remove(temp_path)
created_paths.append(shard_path)
layer_entries[str(layer_idx)] = {
"file": filename,
"size_bytes": os.path.getsize(shard_path),
"shapes": {name: list(params[name].shape) for name in _PROJECTION_NAMES},
}
index = {
"format": FULL_WEIGHT_CHECKPOINT_FORMAT,
"version": FULL_WEIGHT_CHECKPOINT_VERSION,
"dtype": _DTYPE_NAME,
"generation": generation,
"layers": layer_entries,
}
index_path = os.path.join(output_dir, FULL_WEIGHT_INDEX_NAME)
temp_index_path = f"{index_path}.{generation}.tmp"
temporary_paths.append(temp_index_path)
with open(temp_index_path, "w", encoding="utf-8") as handle:
json.dump(index, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temp_index_path, index_path)
temporary_paths.remove(temp_index_path)
except Exception:
for path in temporary_paths:
try:
os.remove(path)
except FileNotFoundError:
pass
for path in created_paths:
try:
os.remove(path)
except FileNotFoundError:
pass
raise
if previous_index is not None:
for entry in previous_index.get("layers", {}).values():
if not isinstance(entry, dict):
continue
try:
old_path = _safe_shard_path(output_dir, entry.get("file"))
except RuntimeError:
continue
if old_path not in created_paths:
try:
os.remove(old_path)
except FileNotFoundError:
pass
except OSError:
logger.warning("Failed to remove superseded KT Full checkpoint shard %s", old_path)
logger.info(
"Saved KT Full checkpoint generation %s with %d layer shards to %s",
generation,
len(layer_entries),
output_dir,
)
return True
def load_full_weight_layer(
checkpoint_path: str,
*,
layer_idx: int,
expected_shapes: dict[str, tuple[int, ...]],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Load and validate one logical layer for initial KT backend construction."""
from safetensors import safe_open
checkpoint_dir = resolve_full_weight_checkpoint(checkpoint_path)
if checkpoint_dir is None:
raise FileNotFoundError(f"No {FULL_WEIGHT_INDEX_NAME} found at {checkpoint_path}")
index = _read_index(checkpoint_dir)
entry = index["layers"].get(str(int(layer_idx)))
if entry is None:
raise RuntimeError(f"Layer {layer_idx}: missing from KT Full checkpoint index")
shard_path = _inspect_layer_shard(
checkpoint_dir,
int(layer_idx),
entry,
expected_shapes,
index["generation"],
)
with safe_open(shard_path, framework="pt", device="cpu") as handle:
tensors = tuple(handle.get_tensor(name).contiguous() for name in _PROJECTION_NAMES)
return tensors
def _load_full_weight_checkpoint_rank0(full_wrappers: list[Any], checkpoint_dir: str) -> int:
from safetensors import safe_open
wrapper_map: dict[int, Any] = {}
parameter_map: dict[int, dict[str, torch.nn.Parameter]] = {}
shard_map: dict[int, str] = {}
index = _read_index(checkpoint_dir)
for wrapper in full_wrappers:
layer_idx = int(wrapper.layer_idx)
if layer_idx in wrapper_map:
raise RuntimeError(f"Duplicate KT Full wrapper for layer {layer_idx}")
wrapper_map[layer_idx] = wrapper
params = _backend_parameters(wrapper)
_validate_parameter_set(wrapper, params)
parameter_map[layer_idx] = params
saved_layers: set[int] = set()
for key in index["layers"]:
try:
layer_idx = int(key)
except (TypeError, ValueError) as exc:
raise RuntimeError(f"Invalid layer key in KT Full checkpoint index: {key!r}") from exc
if layer_idx < 0 or str(layer_idx) != key or layer_idx in saved_layers:
raise RuntimeError(f"Non-canonical or duplicate layer key in KT Full checkpoint index: {key!r}")
saved_layers.add(layer_idx)
expected_layers = set(wrapper_map)
if saved_layers != expected_layers:
missing = sorted(expected_layers - saved_layers)
unexpected = sorted(saved_layers - expected_layers)
raise RuntimeError(f"KT Full checkpoint layer set mismatch: missing={missing}, unexpected={unexpected}")
for layer_idx, wrapper in wrapper_map.items():
shard_map[layer_idx] = _inspect_layer_shard(
checkpoint_dir,
layer_idx,
index["layers"][str(layer_idx)],
_expected_shapes(wrapper),
index["generation"],
)
try:
with torch.no_grad():
for layer_idx in sorted(wrapper_map):
with safe_open(shard_map[layer_idx], framework="pt", device="cpu") as handle:
for name in _PROJECTION_NAMES:
parameter_map[layer_idx][name].copy_(handle.get_tensor(name))
for wrapper in wrapper_map.values():
wrapper.wrapper._base_weights_dirty = True
wrapper.wrapper._kt_full_checkpoint_load_failed = False
except Exception:
for wrapper in wrapper_map.values():
wrapper.wrapper._kt_full_checkpoint_load_failed = True
raise
return len(wrapper_map)
def load_full_weight_checkpoint(wrappers: list[Any], checkpoint_path: str) -> bool:
"""Restore Full/Hybrid Parameters in place and synchronize failure across ranks."""
full_wrappers = _full_weight_wrappers(wrappers)
if not full_wrappers:
return False
checkpoint_dir = resolve_full_weight_checkpoint(checkpoint_path)
rank, world_size = _distributed_rank_world_size()
error_text: str | None = None
loaded_layers = 0
if rank == 0:
if checkpoint_dir is None:
error_text = f"No {FULL_WEIGHT_INDEX_NAME} found at {checkpoint_path}"
else:
try:
loaded_layers = _load_full_weight_checkpoint_rank0(full_wrappers, checkpoint_dir)
except Exception as exc:
error_text = f"{type(exc).__name__}: {exc}"
if world_size > 1:
import torch.distributed as dist
if dist.is_initialized():
status: list[Any] = [{"error": error_text, "loaded_layers": loaded_layers} if rank == 0 else None]
dist.broadcast_object_list(status, src=0)
error_text = status[0]["error"]
loaded_layers = int(status[0]["loaded_layers"])
if error_text is not None:
raise RuntimeError(f"Failed to load KT Full checkpoint: {error_text}")
logger.info("Loaded KT Full checkpoint for %d layers from %s", loaded_layers, checkpoint_path)
return True