Files
wehub-resource-sync ec436095dd
Book-CI / test (macos-latest) (push) Has been cancelled
Book-CI / test (ubuntu-latest) (push) Has been cancelled
Book-CI / test (windows-latest) (push) Has been cancelled
Release Fake Tag / publish (push) Has been cancelled
Deploy / deploy (macos-latest) (push) Has been cancelled
Deploy / deploy (ubuntu-latest) (push) Has been cancelled
Deploy / deploy (windows-latest) (push) Has been cancelled
Release to PyPI / Build & publish sglang-kt (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.11) (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.12) (push) Has been cancelled
Release to PyPI / Publish kt-kernel to PyPI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:03 +08:00

478 lines
15 KiB
Python

#!/usr/bin/env python3
"""Convert KT fused expert LoRA checkpoints into an SGLang adapter directory."""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
from pathlib import Path
from typing import Dict, Iterable, Mapping
import torch
from safetensors.torch import load_file, save_file
FUSED_EXPERT_LORA_FILE = "fused_expert_lora.safetensors"
ADAPTER_MODEL_FILE = "adapter_model.safetensors"
ADAPTER_CONFIG_FILE = "adapter_config.json"
KT_NAME_MAP = {
"gate_lora_a": ("gate_proj", "lora_A", 1),
"gate_lora_b": ("gate_proj", "lora_B", 2),
"up_lora_a": ("up_proj", "lora_A", 1),
"up_lora_b": ("up_proj", "lora_B", 2),
"down_lora_a": ("down_proj", "lora_A", 1),
"down_lora_b": ("down_proj", "lora_B", 2),
}
TARGET_MODULE_ORDER = [
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
"in_proj_qkv",
"in_proj_z",
"in_proj_b",
"in_proj_a",
"out_proj",
"embed_tokens",
"lm_head",
]
KT_FUSED_KEY_RE = re.compile(r"^layers\.(\d+)\.experts\.([^.]+)$")
def _load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def _write_json(path: Path, data: Mapping) -> None:
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, sort_keys=True)
f.write("\n")
def _clean_adapter_key(key: str) -> str:
"""Match the existing SGLang converter's PEFT key cleanup."""
key = key.replace("base_model.model.", "")
key = key.replace(".orig_module", "")
return key
def _ordered_target_modules(modules: Iterable[str]) -> list[str]:
seen = set(modules)
ordered = [name for name in TARGET_MODULE_ORDER if name in seen]
ordered.extend(sorted(seen.difference(ordered)))
return ordered
def _infer_target_module_from_key(key: str) -> str | None:
if "lora_embedding_A" in key or "lora_embedding_B" in key:
if "embed_tokens" in key:
return "embed_tokens"
if "lm_head" in key or "unembed_tokens" in key:
return "lm_head"
marker = ".lora_"
if marker not in key:
return None
prefix = key.split(marker, 1)[0]
if "." not in prefix:
return prefix
return prefix.rsplit(".", 1)[-1]
def _merge_tensor(tensors: Dict[str, torch.Tensor], key: str, value: torch.Tensor) -> None:
if key in tensors:
raise ValueError(f"Duplicate output tensor key: {key}")
tensors[key] = value.detach().cpu()
def _load_existing_adapter(input_dir: Path) -> tuple[dict[str, torch.Tensor], set[str]]:
adapter_path = input_dir / ADAPTER_MODEL_FILE
if not adapter_path.exists():
return {}, set()
tensors: dict[str, torch.Tensor] = {}
target_modules: set[str] = set()
for key, value in load_file(str(adapter_path)).items():
cleaned_key = _clean_adapter_key(key)
_merge_tensor(tensors, cleaned_key, value)
target_module = _infer_target_module_from_key(cleaned_key)
if target_module is not None:
target_modules.add(target_module)
return tensors, target_modules
def _convert_fused_expert_lora(
fused_path: Path,
) -> tuple[dict[str, torch.Tensor], int, set[str]]:
if not fused_path.exists():
raise FileNotFoundError(f"Missing {FUSED_EXPERT_LORA_FILE}: {fused_path}")
output: dict[str, torch.Tensor] = {}
ranks: set[int] = set()
expert_counts: set[int] = set()
target_modules: set[str] = set()
for key, tensor in sorted(load_file(str(fused_path)).items()):
match = KT_FUSED_KEY_RE.match(key)
if match is None:
raise ValueError(f"Unexpected key in {FUSED_EXPERT_LORA_FILE}: {key}")
layer_idx, kt_name = match.groups()
if kt_name not in KT_NAME_MAP:
raise ValueError(f"Unsupported KT fused expert LoRA tensor: {key}")
if tensor.dim() != 3:
raise ValueError(f"{key} must be 3D [E, ...], got shape {tuple(tensor.shape)}")
proj_name, lora_name, rank_dim = KT_NAME_MAP[kt_name]
expert_count = int(tensor.shape[0])
rank = int(tensor.shape[rank_dim])
expert_counts.add(expert_count)
ranks.add(rank)
target_modules.add(proj_name)
for expert_idx in range(expert_count):
output_key = (
f"model.layers.{layer_idx}.mlp.experts.{expert_idx}."
f"{proj_name}.{lora_name}.weight"
)
_merge_tensor(output, output_key, tensor[expert_idx].contiguous())
if not output:
raise ValueError(f"No tensors found in {fused_path}")
if len(expert_counts) != 1:
raise ValueError(f"Inconsistent expert counts in {FUSED_EXPERT_LORA_FILE}: {sorted(expert_counts)}")
if len(ranks) != 1:
raise ValueError(f"Inconsistent LoRA ranks in {FUSED_EXPERT_LORA_FILE}: {sorted(ranks)}")
return output, next(iter(ranks)), target_modules
def _build_adapter_config(
input_dir: Path,
rank: int,
target_modules: set[str],
base_model_name_or_path: str,
lora_alpha: float | None,
*,
include_input_target_modules: bool = True,
) -> dict:
config_path = input_dir / ADAPTER_CONFIG_FILE
config = _load_json(config_path) if config_path.exists() else {}
if "lora_alpha" in config:
final_alpha = config["lora_alpha"]
elif lora_alpha is not None:
final_alpha = lora_alpha
else:
raise ValueError(
f"No {ADAPTER_CONFIG_FILE} with lora_alpha found in {input_dir}; "
"pass --lora-alpha to preserve runtime scaling."
)
existing_targets = config.get("target_modules", [])
if include_input_target_modules and isinstance(existing_targets, list):
target_modules.update(str(name).split(".")[-1] for name in existing_targets)
config["peft_type"] = config.get("peft_type", "LORA")
config["r"] = rank
config["lora_alpha"] = final_alpha
config["target_modules"] = _ordered_target_modules(target_modules)
config["bias"] = config.get("bias", "none")
config["task_type"] = config.get("task_type", "CAUSAL_LM")
config["base_model_name_or_path"] = base_model_name_or_path
return config
def _paths_have_ancestor_relationship(left: Path, right: Path) -> bool:
if left == right:
return True
try:
left.relative_to(right)
return True
except ValueError:
pass
try:
right.relative_to(left)
return True
except ValueError:
return False
def _validate_no_ancestor_paths(
paths: Iterable[Path],
*,
label: str,
) -> None:
resolved = list(paths)
for i, left in enumerate(resolved):
for right in resolved[i + 1 :]:
if _paths_have_ancestor_relationship(left, right):
raise ValueError(
f"{label} cannot have ancestor/descendant relationships: "
f"{left} and {right}."
)
def _prepare_output_dir(output_path: Path, input_path: Path, overwrite: bool) -> None:
_validate_output_dir(output_path, input_path, overwrite)
if output_path.exists() and any(output_path.iterdir()):
shutil.rmtree(output_path)
output_path.mkdir(parents=True, exist_ok=True)
def _validate_output_dir(output_path: Path, input_path: Path, overwrite: bool) -> None:
if output_path == input_path:
raise ValueError("Output directory must be different from input directory.")
if _paths_have_ancestor_relationship(output_path, input_path):
raise ValueError(
"Output and input directories cannot be ancestor/descendant of each other: "
f"output={output_path}, input={input_path}."
)
if output_path.exists() and not output_path.is_dir():
raise FileExistsError(f"Output path exists and is not a directory: {output_path}")
if output_path.exists() and any(output_path.iterdir()):
if not overwrite:
raise FileExistsError(f"Output directory is not empty: {output_path}")
def _infer_lora_rank_from_tensor(key: str, tensor: torch.Tensor) -> int | None:
if ".lora_A." in key:
return int(tensor.shape[0])
if ".lora_B." in key:
return int(tensor.shape[1])
return None
def _validate_nonexpert_rank(
existing_tensors: Mapping[str, torch.Tensor],
expert_rank: int,
input_dir: Path,
) -> None:
if not existing_tensors:
return
config_path = input_dir / ADAPTER_CONFIG_FILE
if config_path.exists():
config_rank = _load_json(config_path).get("r")
if config_rank is not None and int(config_rank) != expert_rank:
raise ValueError(
f"Non-expert adapter rank mismatch: adapter_config.json r={config_rank}, "
f"but fused expert LoRA rank={expert_rank}."
)
for key, tensor in existing_tensors.items():
tensor_rank = _infer_lora_rank_from_tensor(key, tensor)
if tensor_rank is None:
continue
if tensor_rank != expert_rank:
raise ValueError(
f"Non-expert adapter tensor rank mismatch for {key}: "
f"tensor rank={tensor_rank}, fused expert LoRA rank={expert_rank}."
)
def _write_adapter(
output_path: Path,
input_path: Path,
tensors: dict[str, torch.Tensor],
config: Mapping,
*,
overwrite: bool,
) -> None:
_prepare_output_dir(output_path, input_path, overwrite)
save_file(tensors, str(output_path / ADAPTER_MODEL_FILE), metadata={"format": "pt"})
_write_json(output_path / ADAPTER_CONFIG_FILE, config)
def convert_kt_to_sglang_adapter(
input_dir: str | os.PathLike,
output_dir: str | os.PathLike,
*,
base_model_name_or_path: str,
lora_alpha: float | None = None,
overwrite: bool = False,
expert_output_dir: str | os.PathLike | None = None,
nonexpert_output_dir: str | os.PathLike | None = None,
) -> dict:
input_path = Path(input_dir).expanduser().resolve()
output_path = Path(output_dir).expanduser().resolve()
expert_output_path = (
Path(expert_output_dir).expanduser().resolve()
if expert_output_dir is not None
else None
)
nonexpert_output_path = (
Path(nonexpert_output_dir).expanduser().resolve()
if nonexpert_output_dir is not None
else None
)
if not input_path.is_dir():
raise FileNotFoundError(f"Input directory not found: {input_path}")
output_paths = [output_path]
output_paths.extend(path for path in (expert_output_path, nonexpert_output_path) if path is not None)
if len(set(output_paths)) != len(output_paths):
raise ValueError("Merged, expert, and non-expert output directories must be distinct.")
_validate_no_ancestor_paths(
output_paths,
label="Merged/expert/non-expert output directories",
)
for path in output_paths:
_validate_output_dir(path, input_path, overwrite)
existing_tensors, existing_targets = _load_existing_adapter(input_path)
fused_tensors, rank, fused_targets = _convert_fused_expert_lora(input_path / FUSED_EXPERT_LORA_FILE)
_validate_nonexpert_rank(existing_tensors, rank, input_path)
if nonexpert_output_path is not None and not existing_tensors:
raise ValueError(
f"Cannot write non-expert adapter: no {ADAPTER_MODEL_FILE} found in {input_path}."
)
tensors: dict[str, torch.Tensor] = {}
for key, value in existing_tensors.items():
_merge_tensor(tensors, key, value)
for key, value in fused_tensors.items():
_merge_tensor(tensors, key, value)
target_modules = set(existing_targets)
target_modules.update(fused_targets)
config = _build_adapter_config(
input_path,
rank,
target_modules,
base_model_name_or_path,
lora_alpha,
)
_write_adapter(output_path, input_path, tensors, config, overwrite=overwrite)
split_outputs: dict[str, dict] = {}
if expert_output_path is not None:
expert_config = _build_adapter_config(
input_path,
rank,
set(fused_targets),
base_model_name_or_path,
lora_alpha,
include_input_target_modules=False,
)
_write_adapter(
expert_output_path,
input_path,
fused_tensors,
expert_config,
overwrite=overwrite,
)
split_outputs["expert"] = {
"output_dir": str(expert_output_path),
"tensor_count": len(fused_tensors),
"target_modules": expert_config["target_modules"],
}
if nonexpert_output_path is not None:
nonexpert_config = _build_adapter_config(
input_path,
rank,
set(existing_targets),
base_model_name_or_path,
lora_alpha,
include_input_target_modules=False,
)
_write_adapter(
nonexpert_output_path,
input_path,
existing_tensors,
nonexpert_config,
overwrite=overwrite,
)
split_outputs["nonexpert"] = {
"output_dir": str(nonexpert_output_path),
"tensor_count": len(existing_tensors),
"target_modules": nonexpert_config["target_modules"],
}
return {
"input_dir": str(input_path),
"output_dir": str(output_path),
"tensor_count": len(tensors),
"rank": rank,
"target_modules": config["target_modules"],
"lora_alpha": config["lora_alpha"],
"split_outputs": split_outputs,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Convert KT fused expert LoRA weights to an SGLang adapter directory."
)
parser.add_argument("input_dir", help="Directory containing fused_expert_lora.safetensors.")
parser.add_argument("output_dir", help="Destination adapter directory.")
parser.add_argument(
"--base-model-name-or-path",
required=True,
help="Base model path/name to write into adapter_config.json.",
)
parser.add_argument(
"--lora-alpha",
type=float,
default=None,
help="LoRA alpha to use when input adapter_config.json is absent.",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Remove and recreate output_dir if it already contains files.",
)
parser.add_argument(
"--expert-output-dir",
default=None,
help="Optional destination for a split expert-only adapter directory.",
)
parser.add_argument(
"--nonexpert-output-dir",
default=None,
help="Optional destination for a split non-expert-only adapter directory.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
summary = convert_kt_to_sglang_adapter(
args.input_dir,
args.output_dir,
base_model_name_or_path=args.base_model_name_or_path,
lora_alpha=args.lora_alpha,
overwrite=args.overwrite,
expert_output_dir=args.expert_output_dir,
nonexpert_output_dir=args.nonexpert_output_dir,
)
print(
"Converted KT fused expert LoRA adapter: "
f"{summary['tensor_count']} tensors, rank={summary['rank']}, "
f"target_modules={summary['target_modules']}"
)
for name, split_summary in summary["split_outputs"].items():
print(
f"Wrote {name} adapter: {split_summary['tensor_count']} tensors, "
f"target_modules={split_summary['target_modules']}, "
f"output_dir={split_summary['output_dir']}"
)
if __name__ == "__main__":
main()