Files
ktransformers/kt-kernel/python/sft/weights.py
yyj e80614fc2d [feat](kt-sft): Activation reuse & Int8 kernel refactor & native block-FP8 LoRA SFT (#2141)
* feat(sft): support distributed activation reuse policies

* feat(sft): add frozen-base INT8 LoRA training

* fix(sft): make INT8 expert LoRA rank-zero authoritative

* fix(sft): preserve DeepSeek router LoRA routing

* feat(sft): enable persistent INT8 LoRA training

* perf(sft): accelerate INT8 VNNI with oneDNN BRGEMM

* perf(int8): fuse oneDNN compensation into backward repack

* [feat]: support BF16 expert LoRA training

* [fix]: honor forwarded activation policy in SFT workers

* feat(sft): add native block-FP8 routed expert LoRA

* feat(sft): expose explicit expert placeholder ownership

* fix(sft): publish fused adapter artifacts atomically

* feat(sft): own artifact and adapter lifecycle contracts

* fix(sft): harden artifact and rank-local contracts

* fix(sft): auto-adapt owner before adapter restore

* style(sft): keep lifecycle comments concise

* fix(sft): require fused adapter manifests

* test(sft): use spawn for distributed workers

* fix(sft): preserve runtime checkpoint metadata

* fix(sft): validate wrapped runtime configuration

* fix(sft): preserve expert format provenance

* fix(sft): own routed experts during device dispatch

* test(sft): lock explicit quantization conflict

* fix(cpu): make shared memory buffers lifetime-safe

* release: prepare v0.7.0
2026-08-17 16:25:14 +08:00

845 lines
32 KiB
Python

# Weight extraction and loading utilities for SFT
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import logging
import os
import time
from dataclasses import dataclass
import torch
import torch.nn as nn
from .arch import MOEArchConfig
from .conv3d_compat import _canonicalize_qwen3_vl_fused_expert_weights
from .dist_utils import _maybe_zero3_gathered_parameters
logger = logging.getLogger(__name__)
try:
from safetensors import safe_open
SAFETENSORS_AVAILABLE = True
except ImportError:
SAFETENSORS_AVAILABLE = False
safe_open = None
# =============================================================================
# Weight Extraction
# =============================================================================
def _require_materialized_expert_weight(name: str, weight: torch.Tensor) -> None:
if weight.device.type == "meta" or getattr(weight, "_kt_zero_storage", False):
raise RuntimeError(
f"Cannot extract {name}: routed expert weight is an unmaterialized placeholder"
)
try:
storage_bytes = weight.untyped_storage().nbytes()
except (NotImplementedError, RuntimeError):
return
required_bytes = weight.numel() * weight.element_size()
if required_bytes and storage_bytes < required_bytes:
raise RuntimeError(
f"Cannot extract {name}: routed expert weight is a zero-storage placeholder"
)
def extract_moe_weights(
moe_module: nn.Module, moe_config: MOEArchConfig
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""
Extract MoE expert weights from the module.
Returns (gate_proj, up_proj, down_proj) with shape
[expert_num, out_features, in_features].
Supports two formats:
- ModuleList of Linear experts (transformers v4 style)
- Fused Parameters (transformers v5 style): single module with
``gate_up_proj`` [E, 2*I, H] and ``down_proj`` [E, H, I] tensors.
"""
from .arch import detect_fused_experts
experts = getattr(moe_module, moe_config.experts_attr)
# Fused format (transformers v5): a single nn.Module with gate_up_proj/down_proj tensors
if detect_fused_experts(experts):
gate_up_parameter = getattr(experts, "gate_up_proj")
down_parameter = getattr(experts, "down_proj")
_require_materialized_expert_weight("gate_up_proj", gate_up_parameter)
_require_materialized_expert_weight("down_proj", down_parameter)
gate_up = gate_up_parameter.data
down_fused = down_parameter.data
vlm_weights = _canonicalize_qwen3_vl_fused_expert_weights(gate_up, down_fused, moe_config)
if vlm_weights is not None:
return vlm_weights
# gate_up_proj is [E, 2*I, H], split into gate [E, I, H] and up [E, I, H]
intermediate = gate_up.shape[1] // 2
gate_proj = gate_up[:, :intermediate, :].contiguous()
up_proj = gate_up[:, intermediate:, :].contiguous()
# down_proj is already [E, H, I]
down_proj = down_fused.contiguous()
return gate_proj, up_proj, down_proj
gate_name, up_name, down_name = moe_config.weight_names
gather_params: list[torch.nn.Parameter] = []
for expert in experts:
for weight_name in (gate_name, up_name, down_name):
proj = getattr(expert, weight_name, None)
if proj is not None and hasattr(proj, "weight"):
# Handle PEFT LoRA wrapped modules
weight = proj.weight
if isinstance(weight, torch.Tensor):
gather_params.append(weight)
elif hasattr(weight, "data"):
gather_params.append(weight.data)
with _maybe_zero3_gathered_parameters(gather_params):
gate_weights = []
up_weights = []
down_weights = []
for expert_idx, expert in enumerate(experts):
# Handle PEFT LoRA wrapped modules - get weight tensor properly
gate_proj = getattr(expert, gate_name)
up_proj_mod = getattr(expert, up_name)
down_proj_mod = getattr(expert, down_name)
# Get weight tensors, handling both regular Linear and PEFT LoRA wrapped
def get_weight_tensor(mod, projection_name):
weight = mod.weight
if isinstance(weight, torch.Tensor):
tensor = weight
elif hasattr(weight, "data"):
tensor = weight.data
else:
raise ValueError(
f"Cannot extract weight from {type(mod)}, weight type={type(weight)}"
)
_require_materialized_expert_weight(
f"expert {expert_idx} {projection_name}", tensor
)
return tensor.data
gate_weights.append(get_weight_tensor(gate_proj, gate_name))
up_weights.append(get_weight_tensor(up_proj_mod, up_name))
down_weights.append(get_weight_tensor(down_proj_mod, down_name))
gate_proj = torch.stack(gate_weights, dim=0)
up_proj = torch.stack(up_weights, dim=0)
down_proj = torch.stack(down_weights, dim=0)
return gate_proj, up_proj, down_proj
def _clear_original_expert_weights(
moe_module: nn.Module,
moe_config: MOEArchConfig,
full_weight_grad: bool = False,
*,
empty_placeholders: bool = False,
) -> None:
"""
Clear original expert weights to free memory after KT weights are loaded.
In full_weight_grad mode, gate_proj_buf/up_proj_buf/down_proj_buf serve as
the authoritative copies for the optimizer. The original expert weights in
the model tree are redundant and cause double-counting in count_parameters().
Clear them just like in LoRA mode.
"""
from .arch import detect_fused_experts
experts = getattr(moe_module, moe_config.experts_attr, None)
if experts is None:
return
def make_placeholder(parameter: torch.nn.Parameter) -> nn.Parameter:
if empty_placeholders:
fake_tensor = torch.empty(0, dtype=parameter.dtype, device="cpu")
else:
tiny_storage = torch.UntypedStorage(1, device="cpu")
fake_tensor = torch.tensor([], dtype=parameter.dtype, device="cpu").set_(
tiny_storage,
storage_offset=0,
size=parameter.shape,
stride=[0] * len(parameter.shape),
)
placeholder = nn.Parameter(fake_tensor, requires_grad=False)
placeholder._kt_zero_storage = True
placeholder._kt_original_shape = tuple(parameter.shape)
return placeholder
# Fused format: replace gate_up_proj/down_proj tensors with zero-storage placeholders
if detect_fused_experts(experts):
for name in ("gate_up_proj", "down_proj"):
param = getattr(experts, name, None)
if not isinstance(param, torch.nn.Parameter):
continue
experts._parameters[name] = make_placeholder(param)
return
def _iter_weight_params():
for expert in experts:
for weight_name in moe_config.weight_names:
proj = getattr(expert, weight_name, None)
if proj is None or not hasattr(proj, "weight"):
continue
parametrizations = getattr(proj, "parametrizations", None)
parametrized_weight = (
getattr(parametrizations, "weight", None) if parametrizations is not None else None
)
if parametrized_weight is not None:
original = getattr(parametrized_weight, "original", None)
if isinstance(original, torch.nn.Parameter):
yield proj, parametrized_weight, "original", original
continue
direct_weight = getattr(proj, "_parameters", {}).get("weight")
if isinstance(direct_weight, torch.nn.Parameter):
yield proj, proj, "weight", direct_weight
continue
# Fallback: `weight` can be a non-settable property (e.g. parametrizations) or a non-Parameter.
weight_attr = getattr(proj, "weight", None)
if isinstance(weight_attr, torch.nn.Parameter):
yield proj, proj, "weight", weight_attr
gather_params: list[torch.nn.Parameter] = []
for _, _, _, weight_param in _iter_weight_params():
gather_params.append(weight_param)
replaced_count = 0
with _maybe_zero3_gathered_parameters(gather_params):
for proj, container, param_name, weight_param in _iter_weight_params():
new_param = make_placeholder(weight_param)
replaced_count += 1
# Avoid `KeyError: attribute 'weight' already exists` for parametrized modules
# where `weight` is a property and the real parameter lives elsewhere.
container_params = getattr(container, "_parameters", {})
if isinstance(container_params, dict) and param_name in container_params:
container_params[param_name] = new_param
continue
if hasattr(container, param_name):
logger.debug(
f"Skipping clearing expert weight {type(proj).__name__}.{param_name}: "
"attribute exists but is not a registered parameter."
)
continue
try:
setattr(container, param_name, new_param)
except Exception as exc:
logger.warning(f"Failed to clear expert weight {type(proj).__name__}.{param_name}: {exc}")
logger.info(f"Replaced {replaced_count} expert weight params")
_KT_RANK_LOCAL_PARAMETER_NAMES = "_kt_rank_local_parameter_names"
def get_kt_expert_placeholders(model: nn.Module) -> dict[str, nn.Parameter]:
"""Resolve current placeholder identities from stable, cached FQNs."""
placeholder_ids: set[int] = set()
for module in model.modules():
if not getattr(module, "_is_kt_moe_wrapper", False):
continue
experts_attr = getattr(module, "_experts_attr", None)
experts = getattr(module, experts_attr, None) if isinstance(experts_attr, str) else None
if not isinstance(experts, nn.Module):
raise RuntimeError("KT MoE wrapper does not expose a valid expert subtree")
placeholder_ids.update(
id(parameter)
for parameter in experts.parameters(recurse=True)
if getattr(parameter, "_kt_zero_storage", False)
)
named_parameters: dict[str, nn.Parameter] = {}
discovered: dict[str, nn.Parameter] = {}
for fqn, parameter in model.named_parameters(remove_duplicate=False):
if fqn in named_parameters and named_parameters[fqn] is not parameter:
raise RuntimeError(
f"Model parameter FQN {fqn!r} resolves to conflicting Parameter identities"
)
named_parameters[fqn] = parameter
if id(parameter) not in placeholder_ids:
continue
if fqn in discovered and discovered[fqn] is not parameter:
raise RuntimeError(f"KT expert placeholder FQN {fqn!r} resolves to conflicting Parameter identities")
discovered[fqn] = parameter
found_ids = {id(parameter) for parameter in discovered.values()}
if found_ids != placeholder_ids:
raise RuntimeError("KT expert placeholder is not registered in the model parameter tree")
cached_names = getattr(model, _KT_RANK_LOCAL_PARAMETER_NAMES, None)
if cached_names is None:
cached_names = tuple(sorted(discovered))
setattr(model, _KT_RANK_LOCAL_PARAMETER_NAMES, cached_names)
elif (
not isinstance(cached_names, tuple)
or any(not isinstance(name, str) or not name for name in cached_names)
or tuple(sorted(set(cached_names))) != cached_names
):
raise RuntimeError("KT rank-local parameter-name cache is invalid")
elif set(discovered).difference(cached_names):
raise RuntimeError("KT expert placeholder inventory changed after its initial discovery")
placeholders = {}
for fqn in cached_names:
parameter = named_parameters.get(fqn)
if parameter is None:
raise RuntimeError(f"Cached KT rank-local parameter FQN is no longer registered: {fqn!r}")
placeholders[fqn] = parameter
return placeholders
# =============================================================================
# kt_weight_path Loading Functions
# =============================================================================
@dataclass
class INT8ExpertWeights:
"""Container for INT8 expert weights with scales."""
gate_proj: torch.Tensor
gate_scale: torch.Tensor
up_proj: torch.Tensor
up_scale: torch.Tensor
down_proj: torch.Tensor
down_scale: torch.Tensor
@dataclass
class BlockFP8ExpertWeights:
"""Raw per-expert E4M3 weights and normalized block scales.
The lists intentionally remain unstacked: native FP8 packing consumes one
expert pointer at a time and releases these checkpoint tensors after the
synchronous C++ load completes.
"""
gate_proj: list[torch.Tensor]
gate_scale: list[torch.Tensor]
up_proj: list[torch.Tensor]
up_scale: list[torch.Tensor]
down_proj: list[torch.Tensor]
down_scale: list[torch.Tensor]
block_size: tuple[int, int] = (128, 128)
def _checkpoint_tensor_files(
checkpoint_files: list[str],
sharded_metadata: dict | None,
) -> dict[str, str]:
"""Return tensor key -> absolute checkpoint file without loading tensors."""
base_dir = os.path.dirname(checkpoint_files[0])
weight_map = (
sharded_metadata.get("weight_map")
if isinstance(sharded_metadata, dict)
else None
)
if weight_map:
return {
key: os.path.join(base_dir, filename)
for key, filename in weight_map.items()
}
result: dict[str, str] = {}
for file_path in checkpoint_files:
with safe_open(file_path, framework="pt") as handle:
for key in handle.keys():
result[key] = file_path
return result
def load_block_fp8_experts_from_checkpoint_files(
checkpoint_files: list[str],
sharded_metadata: dict | None,
layers_prefix: str,
moe_config: MOEArchConfig,
layer_idx: int,
hidden_size: int,
block_size: tuple[int, int] = (128, 128),
) -> BlockFP8ExpertWeights:
"""Load raw block-FP8 routed experts without stacking or dequantizing.
Native FP8 SFT currently accepts only non-fused, CPU-resident E4M3FN
matrices with one inverse scale per 128x128 block. Scale tensors from
checkpoints may be BF16 or FP32; the returned scale lists are always FP32.
"""
if not SAFETENSORS_AVAILABLE:
raise ImportError("safetensors is required for native FP8 expert loading")
if not checkpoint_files:
raise FileNotFoundError("checkpoint_files is empty")
if tuple(block_size) != (128, 128):
raise ValueError(
"native FP8 SFT requires weight_block_size=[128, 128], "
f"got {tuple(block_size)}"
)
hidden_size = int(hidden_size)
intermediate_size = int(moe_config.intermediate_size)
if hidden_size % 128 or intermediate_size % 128:
raise ValueError(
"native FP8 SFT requires hidden and routed intermediate dimensions "
f"divisible by 128, got hidden_size={hidden_size}, "
f"intermediate_size={intermediate_size}"
)
gate_name, up_name, down_name = moe_config.weight_names
experts_prefix = (
f"{layers_prefix}.{layer_idx}.{moe_config.moe_layer_attr}."
f"{moe_config.experts_attr}"
)
tensor_files = _checkpoint_tensor_files(checkpoint_files, sharded_metadata)
fused_keys = (
f"{experts_prefix}.gate_up_proj",
f"{experts_prefix}.gate_up_proj.weight",
f"{experts_prefix}.down_proj",
f"{experts_prefix}.down_proj.weight",
)
if any(key in tensor_files for key in fused_keys):
raise ValueError(
"native FP8 SFT requires non-fused per-expert checkpoint weights; "
f"layer {layer_idx} contains fused gate_up/down tensors"
)
projection_specs = {
"gate": (gate_name, (intermediate_size, hidden_size)),
"up": (up_name, (intermediate_size, hidden_size)),
"down": (down_name, (hidden_size, intermediate_size)),
}
keys: list[str] = []
for expert_idx in range(int(moe_config.expert_num)):
base = f"{experts_prefix}.{expert_idx}"
for projection_name, _ in projection_specs.values():
keys.extend(
(
f"{base}.{projection_name}.weight",
f"{base}.{projection_name}.weight_scale_inv",
)
)
missing = [key for key in keys if key not in tensor_files]
if missing:
preview = ", ".join(missing[:3])
suffix = " ..." if len(missing) > 3 else ""
raise FileNotFoundError(
f"layer {layer_idx} is missing {len(missing)} native FP8 tensors: "
f"{preview}{suffix}"
)
tensors: dict[str, torch.Tensor] = {}
keys_by_file: dict[str, list[str]] = {}
for key in keys:
keys_by_file.setdefault(tensor_files[key], []).append(key)
for file_path, file_keys in keys_by_file.items():
with safe_open(file_path, framework="pt") as handle:
for key in file_keys:
tensors[key] = handle.get_tensor(key)
output: dict[str, list[torch.Tensor]] = {
"gate_proj": [],
"gate_scale": [],
"up_proj": [],
"up_scale": [],
"down_proj": [],
"down_scale": [],
}
fp8_dtype = torch.float8_e4m3fn
for expert_idx in range(int(moe_config.expert_num)):
base = f"{experts_prefix}.{expert_idx}"
for label, (projection_name, expected_shape) in projection_specs.items():
weight_key = f"{base}.{projection_name}.weight"
scale_key = f"{base}.{projection_name}.weight_scale_inv"
weight = tensors[weight_key]
scale = tensors[scale_key]
expected_scale_shape = (
expected_shape[0] // 128,
expected_shape[1] // 128,
)
if weight.device.type != "cpu" or weight.dtype != fp8_dtype:
raise ValueError(
f"{weight_key} must be CPU torch.float8_e4m3fn, got "
f"{weight.dtype} on {weight.device}"
)
if tuple(weight.shape) != expected_shape:
raise ValueError(
f"{weight_key} shape mismatch: expected {expected_shape}, "
f"got {tuple(weight.shape)}"
)
if scale.device.type != "cpu" or scale.dtype not in {
torch.bfloat16,
torch.float32,
}:
raise ValueError(
f"{scale_key} must be a BF16 or FP32 CPU tensor, got "
f"{scale.dtype} on {scale.device}"
)
if tuple(scale.shape) != expected_scale_shape:
raise ValueError(
f"{scale_key} shape mismatch for 128x128 blocks: expected "
f"{expected_scale_shape}, got {tuple(scale.shape)}"
)
output[f"{label}_proj"].append(weight.contiguous())
output[f"{label}_scale"].append(
scale.to(dtype=torch.float32, device="cpu").contiguous()
)
logger.info(
"Loaded layer %d native FP8 routed experts: experts=%d, block_size=%s",
layer_idx,
moe_config.expert_num,
block_size,
)
return BlockFP8ExpertWeights(**output, block_size=(128, 128))
def _find_safetensor_files(kt_weight_path: str) -> list[str]:
if not os.path.isdir(kt_weight_path):
raise FileNotFoundError(f"kt_weight_path directory not found: {kt_weight_path}")
safetensor_files = []
for file in sorted(os.listdir(kt_weight_path)):
if file.endswith(".safetensors"):
safetensor_files.append(os.path.join(kt_weight_path, file))
if not safetensor_files:
raise FileNotFoundError(f"No safetensors files found in {kt_weight_path}")
return safetensor_files
def _load_kt_weight_index(kt_weight_path: str) -> dict[str, str]:
if not SAFETENSORS_AVAILABLE:
raise ImportError("safetensors is required for loading kt_weight_path")
index = {}
safetensor_files = _find_safetensor_files(kt_weight_path)
for file_path in safetensor_files:
with safe_open(file_path, framework="pt") as f:
for key in f.keys():
index[key] = file_path
logger.info(f"Indexed {len(index)} tensors from {len(safetensor_files)} safetensors files")
return index
def _dequant_fp8_experts(
weights: list[torch.Tensor], scales: list[torch.Tensor | None], block_size: tuple[int, int]
) -> torch.Tensor:
"""Dequantize a list of FP8 expert weights and stack them (batched, vectorized).
Args:
weights: list of [out, in] float8_e4m3fn tensors (one per expert)
scales: list of [out//bs_m, in//bs_n] scale_inv tensors (one per expert, may be None)
block_size: (bs_m, bs_n)
Returns:
Stacked BF16 tensor of shape [num_experts, out, in]
"""
has_scales = scales[0] is not None
if not has_scales:
return torch.stack(weights, dim=0).to(torch.bfloat16).cpu().contiguous()
bs_m, bs_n = block_size
n = len(weights)
out_features, in_features = weights[0].shape
# Stack all experts: [N, out, in] fp8 -> reshape to blocks -> bf16
w = torch.stack(weights, dim=0) # [N, out, in] fp8
w = w.reshape(n, out_features // bs_m, bs_m, in_features // bs_n, bs_n)
w = w.to(torch.bfloat16)
# Stack all scales: [N, out//bs_m, in//bs_n] -> bf16, broadcast multiply
s = torch.stack(scales, dim=0).to(torch.bfloat16) # [N, out//bs_m, in//bs_n]
w = w * s[:, :, None, :, None]
return w.reshape(n, out_features, in_features).contiguous()
def load_experts_from_checkpoint_files(
checkpoint_files: list[str],
sharded_metadata: dict | None,
layers_prefix: str,
moe_config: MOEArchConfig,
layer_idx: int,
block_size: tuple[int, int] | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if not SAFETENSORS_AVAILABLE:
raise ImportError("safetensors is required for loading experts from checkpoint files")
if not checkpoint_files:
raise FileNotFoundError("checkpoint_files is empty")
t0 = time.time()
weight_map = None
base_dir = os.path.dirname(checkpoint_files[0])
if sharded_metadata is not None:
weight_map = sharded_metadata.get("weight_map", None)
gate_name, up_name, down_name = moe_config.weight_names
experts_prefix = f"{layers_prefix}.{layer_idx}.{moe_config.moe_layer_attr}.{moe_config.experts_attr}"
fused_gate_up_key = f"{experts_prefix}.gate_up_proj"
fused_down_key = f"{experts_prefix}.down_proj"
is_fused = weight_map is not None and fused_gate_up_key in weight_map
if is_fused:
keys = [fused_gate_up_key, fused_down_key]
else:
keys = []
for expert_idx in range(moe_config.expert_num):
base = f"{experts_prefix}.{expert_idx}"
keys.append(f"{base}.{gate_name}.weight")
keys.append(f"{base}.{gate_name}.weight_scale_inv")
keys.append(f"{base}.{up_name}.weight")
keys.append(f"{base}.{up_name}.weight_scale_inv")
keys.append(f"{base}.{down_name}.weight")
keys.append(f"{base}.{down_name}.weight_scale_inv")
keys_by_file: dict[str, list[str]] = {}
mapped_count = 0
unmapped_count = 0
for key in keys:
if weight_map is not None:
filename = weight_map.get(key)
if filename is None:
unmapped_count += 1
continue
mapped_count += 1
file_path = os.path.join(base_dir, filename)
else:
file_path = checkpoint_files[0]
keys_by_file.setdefault(file_path, []).append(key)
print(
f"[kt_moe] Layer {layer_idx}: key mapping done in {time.time()-t0:.1f}s — "
f"total_keys={len(keys)}, mapped={mapped_count}, unmapped={unmapped_count}, "
f"files_to_open={len(keys_by_file)}",
flush=True,
)
t1 = time.time()
tensor_map: dict[str, torch.Tensor] = {}
for file_idx, (file_path, file_keys) in enumerate(keys_by_file.items()):
with safe_open(file_path, framework="pt") as f:
available_keys = set(f.keys())
for key in file_keys:
if key in available_keys:
tensor_map[key] = f.get_tensor(key)
if file_idx == 0:
print(
f"[kt_moe] Layer {layer_idx}: first file loaded ({os.path.basename(file_path)}, "
f"{len(file_keys)} keys) in {time.time()-t1:.1f}s",
flush=True,
)
print(
f"[kt_moe] Layer {layer_idx}: all files loaded in {time.time()-t1:.1f}s — "
f"tensor_map has {len(tensor_map)} tensors",
flush=True,
)
t2 = time.time()
if is_fused:
gate_up = tensor_map.get(fused_gate_up_key)
down = tensor_map.get(fused_down_key)
if gate_up is None or down is None:
raise FileNotFoundError(f"Missing fused expert weights for layer {layer_idx}")
gate_up = gate_up.cpu().to(torch.bfloat16).contiguous()
down = down.cpu().to(torch.bfloat16)
vlm_weights = _canonicalize_qwen3_vl_fused_expert_weights(gate_up, down, moe_config)
if vlm_weights is not None:
gate_proj, up_proj, down_proj = vlm_weights
else:
intermediate = gate_up.shape[1] // 2
gate_proj = gate_up[:, :intermediate, :].contiguous()
up_proj = gate_up[:, intermediate:, :].contiguous()
down_proj = down.contiguous()
del gate_up
print(
f"[kt_moe] Layer {layer_idx}: fused expert format — "
f"split gate_up_proj [{gate_proj.shape}] + down [{down_proj.shape}]",
flush=True,
)
print(
f"[kt_moe] Layer {layer_idx}: done — dtype={gate_proj.dtype}, "
f"shape={gate_proj.shape}, dequant=0.0s, total={time.time()-t0:.1f}s",
flush=True,
)
return gate_proj, up_proj, down_proj
gate_weights = []
up_weights = []
down_weights = []
gate_scales = []
up_scales = []
down_scales = []
for expert_idx in range(moe_config.expert_num):
base = f"{experts_prefix}.{expert_idx}"
gate_key = f"{base}.{gate_name}.weight"
up_key = f"{base}.{up_name}.weight"
down_key = f"{base}.{down_name}.weight"
if gate_key not in tensor_map or up_key not in tensor_map or down_key not in tensor_map:
raise FileNotFoundError(f"Missing expert weights for layer {layer_idx}, expert {expert_idx}")
gate_weights.append(tensor_map[gate_key])
up_weights.append(tensor_map[up_key])
down_weights.append(tensor_map[down_key])
gate_scales.append(tensor_map.get(f"{base}.{gate_name}.weight_scale_inv"))
up_scales.append(tensor_map.get(f"{base}.{up_name}.weight_scale_inv"))
down_scales.append(tensor_map.get(f"{base}.{down_name}.weight_scale_inv"))
# Check if weights are FP8 and need dequantization
t2 = time.time()
is_fp8 = gate_weights[0].dtype == torch.float8_e4m3fn
if is_fp8:
if block_size is None:
block_size = (128, 128)
print(
f"[kt_moe] Layer {layer_idx}: FP8 expert weights detected, "
f"dequantizing with block_size={block_size} "
f"(has_scales={gate_scales[0] is not None})",
flush=True,
)
gate_proj = _dequant_fp8_experts(gate_weights, gate_scales, block_size)
up_proj = _dequant_fp8_experts(up_weights, up_scales, block_size)
down_proj = _dequant_fp8_experts(down_weights, down_scales, block_size)
else:
gate_proj = torch.stack(gate_weights, dim=0).cpu().to(torch.bfloat16).contiguous()
up_proj = torch.stack(up_weights, dim=0).cpu().to(torch.bfloat16).contiguous()
down_proj = torch.stack(down_weights, dim=0).cpu().to(torch.bfloat16).contiguous()
print(
f"[kt_moe] Layer {layer_idx}: done — dtype={gate_proj.dtype}, shape={gate_proj.shape}, "
f"dequant={time.time()-t2:.1f}s, total={time.time()-t0:.1f}s",
flush=True,
)
return gate_proj, up_proj, down_proj
def load_experts_from_kt_weight_path(
kt_weight_path: str,
layer_idx: int,
num_experts: int,
hidden_size: int,
intermediate_size: int,
) -> INT8ExpertWeights:
"""Load INT8 preprocessed expert weights from kt_weight_path for a specific layer."""
if not SAFETENSORS_AVAILABLE:
raise ImportError("safetensors is required for loading kt_weight_path")
index = _load_kt_weight_index(kt_weight_path)
numa_count = 0
test_key_prefix = f"blk.{layer_idx}.ffn_gate_exps.0.numa."
for key in index.keys():
if key.startswith(test_key_prefix) and key.endswith(".weight"):
numa_idx = int(key.split("numa.")[1].split(".")[0])
numa_count = max(numa_count, numa_idx + 1)
if numa_count == 0:
raise FileNotFoundError(
f"No weights found for layer {layer_idx} in {kt_weight_path}. "
f"Expected keys like 'blk.{layer_idx}.ffn_gate_exps.0.numa.0.weight'"
)
logger.info(f"Loading INT8 weights for layer {layer_idx}: {num_experts} experts, {numa_count} NUMA partitions")
gate_weights_list = []
gate_scales_list = []
up_weights_list = []
up_scales_list = []
down_weights_list = []
down_scales_list = []
for expert_idx in range(num_experts):
gate_w_parts = []
gate_s_parts = []
for numa_idx in range(numa_count):
w_key = f"blk.{layer_idx}.ffn_gate_exps.{expert_idx}.numa.{numa_idx}.weight"
s_key = f"blk.{layer_idx}.ffn_gate_exps.{expert_idx}.numa.{numa_idx}.scale"
if w_key not in index:
raise FileNotFoundError(f"Weight key not found: {w_key}")
with safe_open(index[w_key], framework="pt") as f:
gate_w_parts.append(f.get_tensor(w_key))
gate_s_parts.append(f.get_tensor(s_key))
gate_w = torch.cat(gate_w_parts, dim=0)
gate_s = torch.cat(gate_s_parts, dim=0)
gate_w = gate_w.view(intermediate_size, hidden_size)
gate_weights_list.append(gate_w)
gate_scales_list.append(gate_s)
up_w_parts = []
up_s_parts = []
for numa_idx in range(numa_count):
w_key = f"blk.{layer_idx}.ffn_up_exps.{expert_idx}.numa.{numa_idx}.weight"
s_key = f"blk.{layer_idx}.ffn_up_exps.{expert_idx}.numa.{numa_idx}.scale"
if w_key not in index:
raise FileNotFoundError(f"Weight key not found: {w_key}")
with safe_open(index[w_key], framework="pt") as f:
up_w_parts.append(f.get_tensor(w_key))
up_s_parts.append(f.get_tensor(s_key))
up_w = torch.cat(up_w_parts, dim=0)
up_s = torch.cat(up_s_parts, dim=0)
up_w = up_w.view(intermediate_size, hidden_size)
up_weights_list.append(up_w)
up_scales_list.append(up_s)
down_w_parts = []
down_s_parts = []
for numa_idx in range(numa_count):
w_key = f"blk.{layer_idx}.ffn_down_exps.{expert_idx}.numa.{numa_idx}.weight"
s_key = f"blk.{layer_idx}.ffn_down_exps.{expert_idx}.numa.{numa_idx}.scale"
if w_key not in index:
raise FileNotFoundError(f"Weight key not found: {w_key}")
with safe_open(index[w_key], framework="pt") as f:
down_w_parts.append(f.get_tensor(w_key))
down_s_parts.append(f.get_tensor(s_key))
down_w = torch.cat(down_w_parts, dim=0)
down_s = torch.cat(down_s_parts, dim=0)
down_w = down_w.view(hidden_size, intermediate_size)
down_weights_list.append(down_w)
down_scales_list.append(down_s)
gate_proj = torch.stack(gate_weights_list, dim=0)
gate_scale = torch.stack(gate_scales_list, dim=0)
up_proj = torch.stack(up_weights_list, dim=0)
up_scale = torch.stack(up_scales_list, dim=0)
down_proj = torch.stack(down_weights_list, dim=0)
down_scale = torch.stack(down_scales_list, dim=0)
return INT8ExpertWeights(
gate_proj=gate_proj,
gate_scale=gate_scale,
up_proj=up_proj,
up_scale=up_scale,
down_proj=down_proj,
down_scale=down_scale,
)