Files
losewayy a02efe36b5 [feat](kt-kernel): add AVX2 RAWINT4 routed-expert LoRA SFT backend (#2243)
* [feat](kt-kernel): add AVX2 RAWINT4 routed-expert LoRA SFT backend

* [fix](kt-kernel): fix RAWINT4 SFT wrapper and clamp gradients

Allow the RAWINT4 production wrapper on AVX2 runtimes and reject shared backward pools before construction. Preserve FP32 clamp decisions across BF16 caches, remove unused configuration state, and cover rectangular dimensions, multiblock dX, clamp boundaries, and production wrapper tasks with tighter LoRA gradient checks.

* [fix](kt-kernel): plumb swiglu_limit through the RAWINT4 SFT python path

The production wrapper hard-zeroed swiglu_limit for every format except MXFP4, and both SFT python gates rejected non-zero values for RAWINT4 even though MOESFTConfig carries the field and the AVX2 kernel implements the clamp in forward plus the gradient mask in backward. RAWINT4_SFT now accepts a finite non-negative swiglu_limit end to end, while the AMX K2 part (which computes plain SiLU) fails fast instead of silently dropping the clamp. The production-wrapper contract test is parametrized over swiglu_limit 0 and 2, a model-config plumb test covers plain and nested text_config shapes, the __main__ entry exercises both limits, and bench_rawint4_sft.py gains --swiglu-limit.
2026-10-12 19:30:24 +08:00

1063 lines
47 KiB
Python

# AMX SFT MoE Wrapper implementation
# SPDX-License-Identifier: Apache-2.0
"""
AMX-based SFT MoE Wrapper. Forward/backward buffer management is in base class;
this file handles weight loading, LoRA init, and C++ task construction.
"""
from __future__ import annotations
import ctypes
import logging
import math
import os
import glob as _glob
import torch
from typing import Optional, List
from kt_kernel_ext.moe import MOESFTConfig
logger = logging.getLogger(__name__)
from ..utils.loader import BF16SafeTensorLoader, MXFP4SafeTensorLoader, SafeTensorLoader
try:
from kt_kernel_ext.moe import (
AMXBF16_SFT_MOE,
AMXInt8_SFT_MOE,
AMXInt4_SFT_MOE,
AMXBF16_SFT_MOE_SkipLoRA,
AMXInt8_SFT_MOE_SkipLoRA,
AMXInt4_SFT_MOE_SkipLoRA,
)
_HAS_AMX_SFT_SUPPORT = True
except (ImportError, AttributeError):
_HAS_AMX_SFT_SUPPORT = False
AMXBF16_SFT_MOE = None
AMXInt8_SFT_MOE = None
AMXInt4_SFT_MOE = None
AMXBF16_SFT_MOE_SkipLoRA = None
AMXInt8_SFT_MOE_SkipLoRA = None
AMXInt4_SFT_MOE_SkipLoRA = None
try:
from kt_kernel_ext.moe import AMXFP8_SFT_MOE
except (ImportError, AttributeError):
AMXFP8_SFT_MOE = None
try:
from kt_kernel_ext.moe import AMXInt4_KGroup_SFT_MOE
except (ImportError, AttributeError):
AMXInt4_KGroup_SFT_MOE = None
try:
from kt_kernel_ext.moe import MXFP4_SFT_MOE
except (ImportError, AttributeError):
MXFP4_SFT_MOE = None
from .base import BaseSFTMoEWrapper, KExpertsSFTBuffer, _supports_authoritative_optimizer_grads
from .backend import (
get_mxfp4_runtime,
get_rawint4_runtime,
is_fp8_sft_method,
is_int8_sft_method,
is_mxfp4_sft_method,
is_rawint4_sft_method,
)
from .weights import BlockFP8ExpertWeights, RAWINT4ExpertWeights
_AMX_M_STEP = 32
# Mapping from method string to C++ SFT MOE class
_SFT_METHOD_TO_CLASS = {
"AMXBF16_SFT": AMXBF16_SFT_MOE,
"AMXFP8_SFT": AMXFP8_SFT_MOE,
"MXFP4_SFT": MXFP4_SFT_MOE,
"INT8_SFT": AMXInt8_SFT_MOE,
"AMXINT8_SFT": AMXInt8_SFT_MOE,
"AMXINT4_SFT": AMXInt4_SFT_MOE,
"RAWINT4_SFT": AMXInt4_KGroup_SFT_MOE,
"AMXINT4_KGroup_SFT": AMXInt4_KGroup_SFT_MOE,
"AMXBF16_SFT_SkipLoRA": AMXBF16_SFT_MOE_SkipLoRA,
"AMXINT8_SFT_SkipLoRA": AMXInt8_SFT_MOE_SkipLoRA,
"AMXINT4_SFT_SkipLoRA": AMXInt4_SFT_MOE_SkipLoRA,
}
class AMXSFTMoEWrapper(BaseSFTMoEWrapper):
"""
AMX-based SFT MoE wrapper.
Supports BF16, native block-FP8/MXFP4, INT8, INT4, and SkipLoRA variants.
Forward/backward buffer management is in BaseSFTMoEWrapper;
this class implements weight loading and C++ task construction.
"""
def __init__(
self,
layer_idx: int,
num_experts: int,
num_experts_per_tok: int,
hidden_size: int,
moe_intermediate_size: int,
num_gpu_experts: int,
cpuinfer_threads: int,
threadpool_count: int,
weight_path: str,
chunked_prefill_size: int,
lora_rank: int = 16,
lora_alpha: float = 32.0,
lora_dropout: float = 0.0,
max_cache_depth: int = 1,
method: str = "AMXBF16_SFT",
group_size: int = 128,
zero_point: bool = True,
full_weight_grad: bool = False,
swiglu_limit: float = 0.0,
):
if not _HAS_AMX_SFT_SUPPORT and not (is_mxfp4_sft_method(method) or is_rawint4_sft_method(method)):
raise RuntimeError(
"AMX SFT backend not available. kt_kernel_ext was not compiled with AMX SFT support.\n"
"Please recompile with AMX SFT enabled."
)
if is_mxfp4_sft_method(method):
get_mxfp4_runtime()
if is_rawint4_sft_method(method):
get_rawint4_runtime()
super().__init__(
layer_idx=layer_idx,
num_experts=num_experts,
num_experts_per_tok=num_experts_per_tok,
hidden_size=hidden_size,
moe_intermediate_size=moe_intermediate_size,
num_gpu_experts=num_gpu_experts,
cpuinfer_threads=cpuinfer_threads,
threadpool_count=threadpool_count,
weight_path=weight_path,
chunked_prefill_size=chunked_prefill_size,
lora_rank=lora_rank,
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
max_cache_depth=max_cache_depth,
full_weight_grad=full_weight_grad,
)
self.method = method
self._is_rawint4 = is_rawint4_sft_method(method)
self._is_skip_lora = "SkipLoRA" in method
self._uses_authoritative_optimizer_grads = _supports_authoritative_optimizer_grads(
method,
self.num_gpu_experts,
full_weight_grad=self._full_weight_grad,
lora_rank=self.lora_rank,
)
self.group_size = group_size
self.zero_point = zero_point
self.swiglu_limit = float(swiglu_limit)
if not (is_mxfp4_sft_method(method) or is_rawint4_sft_method(method)) and self.swiglu_limit != 0.0:
raise ValueError(f"swiglu_limit is only supported by MXFP4_SFT/RAWINT4_SFT, got {method!r}")
if is_mxfp4_sft_method(method):
if self._full_weight_grad or self.lora_rank <= 0:
raise ValueError("MXFP4_SFT supports frozen-base LoRA only")
if self.num_gpu_experts != 0:
raise ValueError("MXFP4_SFT requires all routed experts on CPU")
if self.group_size != 32 or self.zero_point:
raise ValueError("MXFP4_SFT requires group_size=32 and zero_point=False")
if self.hidden_size % 32 or self.moe_intermediate_size % 32:
raise ValueError("MXFP4_SFT requires hidden_size and moe_intermediate_size divisible by 32")
if self.threadpool_count not in (1, 2):
raise ValueError("MXFP4_SFT currently supports threadpool_count 1 or 2")
if self.moe_intermediate_size % self.threadpool_count:
raise ValueError("MXFP4_SFT requires moe_intermediate_size divisible by threadpool_count")
if (self.moe_intermediate_size // self.threadpool_count) % 32:
raise ValueError("MXFP4_SFT requires every TP intermediate slice divisible by 32")
if not math.isfinite(self.swiglu_limit) or self.swiglu_limit <= 0.0:
raise ValueError("MXFP4_SFT requires a finite positive swiglu_limit")
if is_fp8_sft_method(method):
if self._full_weight_grad or self.lora_rank <= 0:
raise ValueError("AMXFP8_SFT supports frozen-base LoRA only")
if self.num_gpu_experts != 0:
raise ValueError("AMXFP8_SFT requires all routed experts on CPU")
if self.hidden_size % 128 or self.moe_intermediate_size % 128:
raise ValueError("AMXFP8_SFT requires hidden_size and moe_intermediate_size " "divisible by 128")
if self.threadpool_count < 1 or self.moe_intermediate_size % self.threadpool_count:
raise ValueError("AMXFP8_SFT requires moe_intermediate_size divisible by " "threadpool_count")
if (self.moe_intermediate_size // self.threadpool_count) % 128:
raise ValueError("AMXFP8_SFT requires every TP intermediate slice divisible by 128")
self.group_size = 128
self.zero_point = False
if is_rawint4_sft_method(self.method):
if self._full_weight_grad:
raise ValueError("RAWINT4 SFT supports frozen-base LoRA only")
if self.lora_rank != 8:
raise ValueError("RAWINT4 SFT v1 requires LoRA rank 8")
if self.lora_dropout != 0.0:
raise ValueError("RAWINT4 SFT v1 requires LoRA dropout 0")
if self._is_skip_lora:
raise ValueError("RAWINT4 SFT v1 does not support SkipLoRA")
if self.num_gpu_experts != 0:
raise ValueError("RAWINT4 SFT requires all routed experts on CPU")
if self.threadpool_count not in {1, 2}:
raise ValueError("RAWINT4 SFT v1 supports TP1/TP2 only")
if self.hidden_size % 32 or self.moe_intermediate_size % 32:
raise ValueError("RAWINT4 SFT requires hidden_size and moe_intermediate_size " "divisible by 32")
if (
self.moe_intermediate_size % self.threadpool_count
or (self.moe_intermediate_size // self.threadpool_count) % 32
):
raise ValueError("RAWINT4 SFT requires every TP intermediate slice divisible by 32")
if not math.isfinite(self.swiglu_limit) or self.swiglu_limit < 0.0:
raise ValueError("RAWINT4 SFT requires a finite non-negative swiglu_limit")
self.group_size = 32
self.zero_point = False
if method not in _SFT_METHOD_TO_CLASS:
raise ValueError(f"Unknown SFT method: {method}. Supported: {list(_SFT_METHOD_TO_CLASS.keys())}")
moe_class = _SFT_METHOD_TO_CLASS[method]
if moe_class is None:
raise RuntimeError(f"AMX SFT method '{method}' not available in current build.")
self.gate_proj: Optional[torch.Tensor] = None
self.up_proj: Optional[torch.Tensor] = None
self.down_proj: Optional[torch.Tensor] = None
self.gate_scale: Optional[torch.Tensor] = None
self.up_scale: Optional[torch.Tensor] = None
self.down_scale: Optional[torch.Tensor] = None
self._moe_class = moe_class
# ========== Template method: C++ task construction ==========
def _make_forward_task(self, buffer: KExpertsSFTBuffer, save_for_backward: bool):
return self.moe.forward_sft_task(
buffer.bsz_tensor.data_ptr(),
self.num_experts_per_tok,
buffer.expert_ids_cpu.data_ptr(),
buffer.weights_cpu.data_ptr(),
buffer.input_cpu.data_ptr(),
buffer.output_cpu.data_ptr(),
save_for_backward,
)
def _make_backward_task(
self,
buffer: KExpertsSFTBuffer,
accumulate_optimizer_grads: bool = False,
optimizer_grad_scale: float = 1.0,
):
if self._is_skip_lora:
return self.moe.backward_task(
buffer.grad_output_cpu.data_ptr(),
buffer.grad_input_cpu.data_ptr(),
0,
0,
0,
0,
0,
0,
buffer.grad_weights.data_ptr(),
0,
0,
0, # grad_gate_proj, grad_up_proj, grad_down_proj
)
# Base weight grad pointers (nullptr if not in full mode)
grad_gate_proj_ptr = (
self.grad_gate_proj_buf.data_ptr() if self._full_weight_grad and self.grad_gate_proj_buf is not None else 0
)
grad_up_proj_ptr = (
self.grad_up_proj_buf.data_ptr() if self._full_weight_grad and self.grad_up_proj_buf is not None else 0
)
grad_down_proj_ptr = (
self.grad_down_proj_buf.data_ptr() if self._full_weight_grad and self.grad_down_proj_buf is not None else 0
)
backward_args = (
buffer.grad_output_cpu.data_ptr(),
buffer.grad_input_cpu.data_ptr(),
self.grad_gate_lora_a.data_ptr() if self.lora_rank > 0 else 0,
self.grad_gate_lora_b.data_ptr() if self.lora_rank > 0 else 0,
self.grad_up_lora_a.data_ptr() if self.lora_rank > 0 else 0,
self.grad_up_lora_b.data_ptr() if self.lora_rank > 0 else 0,
self.grad_down_lora_a.data_ptr() if self.lora_rank > 0 else 0,
self.grad_down_lora_b.data_ptr() if self.lora_rank > 0 else 0,
buffer.grad_weights.data_ptr(),
grad_gate_proj_ptr,
grad_up_proj_ptr,
grad_down_proj_ptr,
)
if (
self._uses_authoritative_optimizer_grads
or bool(accumulate_optimizer_grads)
or float(optimizer_grad_scale) != 1.0
):
return self.moe.backward_task(
*backward_args,
bool(accumulate_optimizer_grads),
float(optimizer_grad_scale),
)
return self.moe.backward_task(*backward_args)
# ========== Weight loading ==========
def load_weights(self, physical_to_logical_map_cpu: torch.Tensor) -> None:
if self._weights_loaded:
if is_mxfp4_sft_method(self.method):
raise RuntimeError("MXFP4_SFT weights are immutable and already loaded")
return
if physical_to_logical_map_cpu is None:
physical_to_logical_map_cpu = torch.arange(self.num_experts, dtype=torch.int64)
strict_rawint4_map = bool(getattr(self, "_is_rawint4", False))
if strict_rawint4_map:
if not isinstance(physical_to_logical_map_cpu, torch.Tensor):
raise TypeError("physical_to_logical_map_cpu must be a torch.Tensor")
if physical_to_logical_map_cpu.dtype not in {torch.int32, torch.int64}:
raise ValueError(
"physical_to_logical_map_cpu must use an integer dtype, "
f"got {physical_to_logical_map_cpu.dtype}."
)
self._physical_to_logical_map_cpu = (
physical_to_logical_map_cpu.to(dtype=torch.int64, device="cpu").reshape(-1).contiguous()
)
if strict_rawint4_map and self._physical_to_logical_map_cpu.numel() != self.num_experts:
raise ValueError(
"physical_to_logical_map_cpu must contain exactly "
f"{self.num_experts} entries, got {self._physical_to_logical_map_cpu.numel()}."
)
if not strict_rawint4_map and self._physical_to_logical_map_cpu.numel() < self.num_experts:
raise ValueError(
"physical_to_logical_map_cpu must contain at least "
f"{self.num_experts} entries, got {self._physical_to_logical_map_cpu.numel()}."
)
expected_experts = torch.arange(self.num_experts, dtype=torch.int64)
if strict_rawint4_map and not torch.equal(self._physical_to_logical_map_cpu.sort().values, expected_experts):
raise ValueError(
"physical_to_logical_map_cpu must be a permutation of "
f"[0, {self.num_experts}); got {self._physical_to_logical_map_cpu.tolist()}."
)
if is_mxfp4_sft_method(self.method):
logical_experts = self._physical_to_logical_map_cpu[: self.num_experts].tolist()
if sorted(logical_experts) != list(range(self.num_experts)):
raise ValueError(
"MXFP4_SFT physical_to_logical_map_cpu must be a permutation of " f"[0, {self.num_experts})."
)
if self.gate_proj is None and not getattr(self, "_use_projs_path", False):
self._load_base_weights_from_file()
config = MOESFTConfig()
config.expert_num = self.num_experts
config.num_experts_per_tok = self.num_experts_per_tok
config.hidden_size = self.hidden_size
config.intermediate_size = self.moe_intermediate_size
config.lora_rank = self.lora_rank
config.lora_alpha = self.lora_alpha
config.lora_dropout = self.lora_dropout
config.max_cache_depth = self.max_cache_depth
config.max_len = self._aligned_max_len()
config.layer_idx = self.layer_idx
config.share_backward_bb = getattr(self, "share_backward_bb", False)
config.share_cache_pool = getattr(self, "share_cache_pool", False)
config.full_weight_grad = self._full_weight_grad
config.authoritative_optimizer_grads = self._uses_authoritative_optimizer_grads
config.physical_to_logical_map = self._physical_to_logical_map_cpu.data_ptr()
config.swiglu_limit = self.swiglu_limit
if getattr(self, "_use_kt_direct_load", False):
config.load = True
config.path = self.weight_path
elif getattr(self, "_use_projs_path", False):
config.gate_projs = self._gate_projs_ptrs
config.up_projs = self._up_projs_ptrs
config.down_projs = self._down_projs_ptrs
config.gate_scales = self._gate_scale_ptrs
config.up_scales = self._up_scale_ptrs
config.down_scales = self._down_scale_ptrs
if getattr(self, "_bf16_gate_proj", None) is not None:
config.gate_proj = self._bf16_gate_proj.data_ptr()
config.up_proj = self._bf16_up_proj.data_ptr()
config.down_proj = self._bf16_down_proj.data_ptr()
if getattr(self, "_has_bwd_projs", False):
config.gate_bwd_projs = self._gate_bwd_projs_ptrs
config.up_bwd_projs = self._up_bwd_projs_ptrs
config.down_bwd_projs = self._down_bwd_projs_ptrs
config.gate_bwd_scales = self._gate_bwd_scale_ptrs
config.up_bwd_scales = self._up_bwd_scale_ptrs
config.down_bwd_scales = self._down_bwd_scale_ptrs
else:
config.gate_proj = self.gate_proj.data_ptr()
config.up_proj = self.up_proj.data_ptr()
config.down_proj = self.down_proj.data_ptr()
if getattr(self, "_use_rawint4_tensor_path", False):
config.gate_scale = self.gate_scale.data_ptr()
config.up_scale = self.up_scale.data_ptr()
config.down_scale = self.down_scale.data_ptr()
if self._lora_initialized:
config.gate_lora_a = self.gate_lora_a.data_ptr()
config.gate_lora_b = self.gate_lora_b.data_ptr()
config.up_lora_a = self.up_lora_a.data_ptr()
config.up_lora_b = self.up_lora_b.data_ptr()
config.down_lora_a = self.down_lora_a.data_ptr()
config.down_lora_b = self.down_lora_b.data_ptr()
config.pool = self.cpu_infer.backend_
if is_rawint4_sft_method(self.method):
config.quant_config.bits = 4
config.quant_config.group_size = self.group_size
config.quant_config.zero_point = self.zero_point
elif is_fp8_sft_method(self.method):
config.quant_config.bits = 8
config.quant_config.group_size = 128
config.quant_config.zero_point = False
elif is_mxfp4_sft_method(self.method):
config.quant_config.bits = 4
config.quant_config.group_size = 32
config.quant_config.zero_point = False
# Release old C++ MOE object before creating a new one to avoid memory leak
old_moe = getattr(self, "moe", None)
if old_moe is not None:
del old_moe
import gc
gc.collect()
self.moe = self._moe_class(config)
self.cpu_infer.submit(self.moe.load_weights_task())
self.cpu_infer.sync()
if os.environ.get("KT_SFT_ENABLE_WARMUP", "0") == "1":
self.cpu_infer.submit(self.moe.warm_up_task())
self.cpu_infer.sync()
# Release Python-side weight tensors (C++ copied them)
# In full_weight_grad mode, keep them for nn.Parameter initialization
if not self._full_weight_grad:
self.gate_proj = None
self.up_proj = None
self.down_proj = None
self.gate_scale = None
self.up_scale = None
self.down_scale = None
if getattr(self, "_bf16_gate_proj", None) is not None:
self._bf16_gate_proj = None
self._bf16_up_proj = None
self._bf16_down_proj = None
if getattr(self, "_use_projs_path", False):
for attr in [
"_gate_weights_per_numa",
"_up_weights_per_numa",
"_down_weights_per_numa",
"_gate_scales_per_numa",
"_up_scales_per_numa",
"_down_scales_per_numa",
"_gate_projs_ptrs",
"_up_projs_ptrs",
"_down_projs_ptrs",
"_gate_scale_ptrs",
"_up_scale_ptrs",
"_down_scale_ptrs",
]:
setattr(self, attr, None)
if getattr(self, "_has_bwd_projs", False):
for attr in [
"_gate_bwd_weights_per_numa",
"_up_bwd_weights_per_numa",
"_down_bwd_weights_per_numa",
"_gate_bwd_scales_per_numa",
"_up_bwd_scales_per_numa",
"_down_bwd_scales_per_numa",
"_gate_bwd_projs_ptrs",
"_up_bwd_projs_ptrs",
"_down_bwd_projs_ptrs",
"_gate_bwd_scale_ptrs",
"_up_bwd_scale_ptrs",
"_down_bwd_scale_ptrs",
]:
setattr(self, attr, None)
self._weights_loaded = True
def load_weights_from_tensors(
self,
gate_proj: torch.Tensor,
up_proj: torch.Tensor,
down_proj: torch.Tensor,
physical_to_logical_map_cpu: torch.Tensor,
) -> None:
if is_rawint4_sft_method(self.method):
raise ValueError(
"RAWINT4 SFT accepts packed weights plus BF16 group scales only; " "use load_rawint4_weights()"
)
if is_fp8_sft_method(self.method):
raise ValueError("AMXFP8_SFT accepts raw per-expert E4M3 weights only; " "use load_block_fp8_weights()")
if is_mxfp4_sft_method(self.method):
raise ValueError(
"MXFP4_SFT accepts native packed E2M1 weights and UE8M0 scales only; " "use load_mxfp4_weights()"
)
if is_int8_sft_method(self.method):
raise ValueError(
"INT8_SFT accepts pre-quantized .kt weights only; " "online tensor conversion is not supported"
)
self.gate_proj = gate_proj.contiguous()
self.up_proj = up_proj.contiguous()
self.down_proj = down_proj.contiguous()
self.load_weights(physical_to_logical_map_cpu)
del gate_proj, up_proj, down_proj
def load_rawint4_weights(
self,
weights: RAWINT4ExpertWeights,
physical_to_logical_map_cpu: torch.Tensor,
) -> None:
"""Synchronously load strict signed group-32 packed expert weights."""
if not is_rawint4_sft_method(self.method):
raise ValueError(f"load_rawint4_weights() requires RAWINT4_SFT, got {self.method!r}")
if int(weights.group_size) != 32:
raise ValueError(f"RAWINT4 SFT requires group_size=32, got {weights.group_size}")
projections = {
"gate": (
weights.gate_proj,
weights.gate_scale,
(self.num_experts, self.moe_intermediate_size, self.hidden_size // 2),
(self.num_experts, self.moe_intermediate_size, self.hidden_size // 32),
),
"up": (
weights.up_proj,
weights.up_scale,
(self.num_experts, self.moe_intermediate_size, self.hidden_size // 2),
(self.num_experts, self.moe_intermediate_size, self.hidden_size // 32),
),
"down": (
weights.down_proj,
weights.down_scale,
(self.num_experts, self.hidden_size, self.moe_intermediate_size // 2),
(self.num_experts, self.hidden_size, self.moe_intermediate_size // 32),
),
}
for name, (packed, scale, packed_shape, scale_shape) in projections.items():
if (
packed.device.type != "cpu"
or packed.dtype != torch.uint8
or not packed.is_contiguous()
or tuple(packed.shape) != packed_shape
):
raise ValueError(f"{name} RAWINT4 weight must be contiguous CPU uint8 with " f"shape {packed_shape}")
if (
scale.device.type != "cpu"
or scale.dtype != torch.bfloat16
or not scale.is_contiguous()
or tuple(scale.shape) != scale_shape
):
raise ValueError(f"{name} RAWINT4 scale must be contiguous CPU BF16 with " f"shape {scale_shape}")
self.gate_proj = weights.gate_proj
self.gate_scale = weights.gate_scale
self.up_proj = weights.up_proj
self.up_scale = weights.up_scale
self.down_proj = weights.down_proj
self.down_scale = weights.down_scale
self._use_rawint4_tensor_path = True
self.load_weights(physical_to_logical_map_cpu)
def load_block_fp8_weights(
self,
weights: BlockFP8ExpertWeights,
physical_to_logical_map_cpu: torch.Tensor,
) -> None:
"""Synchronously pack raw per-expert FP8 checkpoint tensors in C++."""
if not is_fp8_sft_method(self.method):
raise ValueError(f"load_block_fp8_weights() requires AMXFP8_SFT, got {self.method!r}")
if tuple(weights.block_size) != (128, 128):
raise ValueError("AMXFP8_SFT requires block_size=(128, 128), " f"got {weights.block_size}")
projections = {
"gate": (weights.gate_proj, weights.gate_scale, (self.moe_intermediate_size, self.hidden_size)),
"up": (weights.up_proj, weights.up_scale, (self.moe_intermediate_size, self.hidden_size)),
"down": (weights.down_proj, weights.down_scale, (self.hidden_size, self.moe_intermediate_size)),
}
for name, (expert_weights, expert_scales, expected_shape) in projections.items():
if len(expert_weights) != self.num_experts or len(expert_scales) != self.num_experts:
raise ValueError(
f"{name} FP8 tensors must contain {self.num_experts} experts, got "
f"weights={len(expert_weights)}, scales={len(expert_scales)}"
)
expected_scale_shape = (expected_shape[0] // 128, expected_shape[1] // 128)
for expert_idx, (weight, scale) in enumerate(zip(expert_weights, expert_scales)):
if (
weight.device.type != "cpu"
or weight.dtype != torch.float8_e4m3fn
or not weight.is_contiguous()
or tuple(weight.shape) != expected_shape
):
raise ValueError(
f"{name} expert {expert_idx} must be contiguous CPU E4M3FN " f"with shape {expected_shape}"
)
if (
scale.device.type != "cpu"
or scale.dtype != torch.float32
or not scale.is_contiguous()
or tuple(scale.shape) != expected_scale_shape
):
raise ValueError(
f"{name} scale {expert_idx} must be contiguous CPU FP32 " f"with shape {expected_scale_shape}"
)
self._gate_weights_per_numa = [weights.gate_proj]
self._up_weights_per_numa = [weights.up_proj]
self._down_weights_per_numa = [weights.down_proj]
self._gate_scales_per_numa = [weights.gate_scale]
self._up_scales_per_numa = [weights.up_scale]
self._down_scales_per_numa = [weights.down_scale]
self._gate_projs_ptrs = [[tensor.data_ptr() for tensor in weights.gate_proj]]
self._up_projs_ptrs = [[tensor.data_ptr() for tensor in weights.up_proj]]
self._down_projs_ptrs = [[tensor.data_ptr() for tensor in weights.down_proj]]
self._gate_scale_ptrs = [[tensor.data_ptr() for tensor in weights.gate_scale]]
self._up_scale_ptrs = [[tensor.data_ptr() for tensor in weights.up_scale]]
self._down_scale_ptrs = [[tensor.data_ptr() for tensor in weights.down_scale]]
self._has_bwd_projs = False
self._use_projs_path = True
self.gate_proj = None
self.up_proj = None
self.down_proj = None
self.load_weights(physical_to_logical_map_cpu)
def _stage_mxfp4_weights(self, experts_data: dict[str, List[torch.Tensor]]) -> None:
"""Validate native MXFP4 tensors and retain them until C++ load completes."""
if not is_mxfp4_sft_method(self.method):
raise ValueError(f"MXFP4 weights require MXFP4_SFT, got {self.method!r}")
if not isinstance(experts_data, dict):
raise TypeError("MXFP4 expert weights must be the dict returned by " "MXFP4SafeTensorLoader.load_experts()")
expected = {
"gate": (torch.uint8, (self.moe_intermediate_size, self.hidden_size // 2)),
"up": (torch.uint8, (self.moe_intermediate_size, self.hidden_size // 2)),
"down": (torch.uint8, (self.hidden_size, self.moe_intermediate_size // 2)),
"gate_scale": (
torch.bfloat16,
(self.moe_intermediate_size, self.hidden_size // 32),
),
"up_scale": (
torch.bfloat16,
(self.moe_intermediate_size, self.hidden_size // 32),
),
"down_scale": (
torch.bfloat16,
(self.hidden_size, self.moe_intermediate_size // 32),
),
}
validated: dict[str, List[torch.Tensor]] = {}
for name, (dtype, shape) in expected.items():
tensors = experts_data.get(name)
if not isinstance(tensors, (list, tuple)) or len(tensors) != self.num_experts:
actual = len(tensors) if isinstance(tensors, (list, tuple)) else type(tensors).__name__
raise ValueError(f"MXFP4 {name} must contain {self.num_experts} expert tensors, got {actual}")
checked = []
for expert_idx, tensor in enumerate(tensors):
if not isinstance(tensor, torch.Tensor):
raise TypeError(f"MXFP4 {name}[{expert_idx}] must be a torch.Tensor, got {type(tensor)!r}")
if (
tensor.device.type != "cpu"
or tensor.dtype != dtype
or tuple(tensor.shape) != shape
or not tensor.is_contiguous()
):
raise ValueError(
f"MXFP4 {name}[{expert_idx}] must be contiguous CPU {dtype} "
f"with shape {shape}, got {tensor.dtype} {tuple(tensor.shape)} "
f"on {tensor.device}"
)
if name.endswith("_scale") and not torch.isfinite(tensor).all().item():
raise ValueError(f"MXFP4 {name}[{expert_idx}] contains a non-finite scale")
checked.append(tensor)
validated[name] = checked
self._gate_weights_per_numa = [validated["gate"]]
self._up_weights_per_numa = [validated["up"]]
self._down_weights_per_numa = [validated["down"]]
self._gate_scales_per_numa = [validated["gate_scale"]]
self._up_scales_per_numa = [validated["up_scale"]]
self._down_scales_per_numa = [validated["down_scale"]]
self._gate_projs_ptrs = [[tensor.data_ptr() for tensor in validated["gate"]]]
self._up_projs_ptrs = [[tensor.data_ptr() for tensor in validated["up"]]]
self._down_projs_ptrs = [[tensor.data_ptr() for tensor in validated["down"]]]
self._gate_scale_ptrs = [[tensor.data_ptr() for tensor in validated["gate_scale"]]]
self._up_scale_ptrs = [[tensor.data_ptr() for tensor in validated["up_scale"]]]
self._down_scale_ptrs = [[tensor.data_ptr() for tensor in validated["down_scale"]]]
self._has_bwd_projs = False
self._use_projs_path = True
self.gate_proj = None
self.up_proj = None
self.down_proj = None
def load_mxfp4_weights(
self,
experts_data: dict[str, List[torch.Tensor]],
physical_to_logical_map_cpu: torch.Tensor,
) -> None:
"""Load native E2M1/UE8M0 expert tensors without dequantizing the base."""
if self._weights_loaded:
raise RuntimeError("MXFP4_SFT weights are immutable and already loaded")
self._stage_mxfp4_weights(experts_data)
self.load_weights(physical_to_logical_map_cpu)
def _load_base_weights_from_file(self) -> None:
if not hasattr(self, "weight_path") or self.weight_path is None:
raise RuntimeError(
"weight_path not set. Cannot load weights from file. "
"Either set weight_path or call load_weights_from_tensors() instead."
)
if is_mxfp4_sft_method(self.method):
loader = MXFP4SafeTensorLoader(self.weight_path)
try:
experts_data = loader.load_experts(
f"model.layers.{self.layer_idx}",
device="cpu",
reject_non_finite_scales=True,
)
self._stage_mxfp4_weights(experts_data)
finally:
loader.close_all_handles()
return
kt_layer_dir = os.path.join(self.weight_path, f"_layer_{self.layer_idx}")
if os.path.isdir(kt_layer_dir):
kt_files = _glob.glob(os.path.join(kt_layer_dir, "_numa_0", "*.kt"))
if kt_files:
self._use_kt_direct_load = True
return
if is_fp8_sft_method(self.method):
raise RuntimeError(
"AMXFP8_SFT requires raw per-expert HF checkpoint tensors; "
"legacy merged weight files are not supported"
)
if is_rawint4_sft_method(self.method):
raise RuntimeError(
"RAWINT4 SFT requires packed compressed-tensors checkpoint tensors; "
"legacy merged weight files are not supported"
)
if is_int8_sft_method(self.method):
raise RuntimeError(
f"INT8_SFT requires pre-quantized .kt files under "
f"{kt_layer_dir}/_numa_*/; merged safetensors and online "
"conversion are not supported"
)
if "BF16" in self.method:
loader = BF16SafeTensorLoader(self.weight_path)
base_key = f"model.layers.{self.layer_idx}"
else:
loader = SafeTensorLoader(self.weight_path)
base_key = f"blk.{self.layer_idx}"
experts_data = loader.load_experts(base_key, device="cpu")
gate_weights: List[torch.Tensor] = experts_data["gate"]
up_weights: List[torch.Tensor] = experts_data["up"]
down_weights: List[torch.Tensor] = experts_data["down"]
if "BF16" in self.method:
self.gate_proj = torch.stack(gate_weights, dim=0).contiguous()
self.up_proj = torch.stack(up_weights, dim=0).contiguous()
self.down_proj = torch.stack(down_weights, dim=0).contiguous()
else:
def _make_ptrs(arrays_per_numa):
return [
[
ctypes.addressof(ctypes.cast(et.ctypes.data, ctypes.POINTER(ctypes.c_uint64)).contents)
for et in numa_array
]
for numa_array in arrays_per_numa
]
self._gate_weights_per_numa = gate_weights
self._up_weights_per_numa = up_weights
self._down_weights_per_numa = down_weights
self._gate_scales_per_numa = experts_data["gate_scale"]
self._up_scales_per_numa = experts_data["up_scale"]
self._down_scales_per_numa = experts_data["down_scale"]
self._validate_prepartitioned_weights()
self._gate_projs_ptrs = _make_ptrs(gate_weights)
self._up_projs_ptrs = _make_ptrs(up_weights)
self._down_projs_ptrs = _make_ptrs(down_weights)
self._gate_scale_ptrs = _make_ptrs(experts_data["gate_scale"])
self._up_scale_ptrs = _make_ptrs(experts_data["up_scale"])
self._down_scale_ptrs = _make_ptrs(experts_data["down_scale"])
if "gate_bwd" in experts_data:
self._gate_bwd_weights_per_numa = experts_data["gate_bwd"]
self._up_bwd_weights_per_numa = experts_data["up_bwd"]
self._down_bwd_weights_per_numa = experts_data["down_bwd"]
self._gate_bwd_scales_per_numa = experts_data["gate_bwd_scale"]
self._up_bwd_scales_per_numa = experts_data["up_bwd_scale"]
self._down_bwd_scales_per_numa = experts_data["down_bwd_scale"]
self._gate_bwd_projs_ptrs = _make_ptrs(experts_data["gate_bwd"])
self._up_bwd_projs_ptrs = _make_ptrs(experts_data["up_bwd"])
self._down_bwd_projs_ptrs = _make_ptrs(experts_data["down_bwd"])
self._gate_bwd_scale_ptrs = _make_ptrs(experts_data["gate_bwd_scale"])
self._up_bwd_scale_ptrs = _make_ptrs(experts_data["up_bwd_scale"])
self._down_bwd_scale_ptrs = _make_ptrs(experts_data["down_bwd_scale"])
self._has_bwd_projs = True
else:
self._has_bwd_projs = False
self.gate_proj = None
self.up_proj = None
self.down_proj = None
self._use_projs_path = True
loader.close_all_handles()
def _aligned_max_len(self) -> int:
return ((self.chunked_prefill_size + _AMX_M_STEP - 1) // _AMX_M_STEP) * _AMX_M_STEP
def _validate_prepartitioned_weights(self) -> None:
numa_count = len(self._gate_weights_per_numa)
if self.moe_intermediate_size % self.threadpool_count != 0:
raise ValueError(
f"moe_intermediate_size={self.moe_intermediate_size} must be divisible by "
f"threadpool_count={self.threadpool_count} for {self.method} SFT."
)
if numa_count != self.threadpool_count:
raise ValueError(
f"{self.method} SFT pre-partitioned expert weights have {numa_count} NUMA partitions, "
f"but CPUInfer was created with threadpool_count={self.threadpool_count}. "
f"Use --kt-threadpool-count {numa_count} for this weight directory, or convert weights "
"for the requested threadpool count."
)
collections = {
"gate": self._gate_weights_per_numa,
"up": self._up_weights_per_numa,
"down": self._down_weights_per_numa,
"gate_scale": self._gate_scales_per_numa,
"up_scale": self._up_scales_per_numa,
"down_scale": self._down_scales_per_numa,
}
for name, per_numa in collections.items():
if len(per_numa) != numa_count:
raise ValueError(f"{name} has {len(per_numa)} NUMA partitions, expected {numa_count}.")
for numa_id, entries in enumerate(per_numa):
if len(entries) != self.num_experts:
raise ValueError(f"{name}[numa={numa_id}] has {len(entries)} experts, expected {self.num_experts}.")
for numa_id in range(numa_count):
gate_scale_len = self._gate_scales_per_numa[numa_id][0].size
up_scale_len = self._up_scales_per_numa[numa_id][0].size
down_scale_len = self._down_scales_per_numa[numa_id][0].size
expected_intermediate = self.moe_intermediate_size // self.threadpool_count
if gate_scale_len != expected_intermediate or up_scale_len != expected_intermediate:
raise ValueError(
f"{self.method} gate/up scale length for NUMA {numa_id} is "
f"{gate_scale_len}/{up_scale_len}, expected {expected_intermediate}."
)
if down_scale_len != self.hidden_size:
raise ValueError(
f"{self.method} down scale length for NUMA {numa_id} is "
f"{down_scale_len}, expected {self.hidden_size}."
)
# ========== LoRA ==========
def init_lora_weights(
self,
gate_lora_a: torch.Tensor,
gate_lora_b: torch.Tensor,
up_lora_a: torch.Tensor,
up_lora_b: torch.Tensor,
down_lora_a: torch.Tensor,
down_lora_b: torch.Tensor,
grad_gate_lora_a: torch.Tensor,
grad_gate_lora_b: torch.Tensor,
grad_up_lora_a: torch.Tensor,
grad_up_lora_b: torch.Tensor,
grad_down_lora_a: torch.Tensor,
grad_down_lora_b: torch.Tensor,
) -> None:
expected_shapes = {
"gate_lora_a": (self.num_experts, self.lora_rank, self.hidden_size),
"gate_lora_b": (self.num_experts, self.moe_intermediate_size, self.lora_rank),
"up_lora_a": (self.num_experts, self.lora_rank, self.hidden_size),
"up_lora_b": (self.num_experts, self.moe_intermediate_size, self.lora_rank),
"down_lora_a": (self.num_experts, self.lora_rank, self.moe_intermediate_size),
"down_lora_b": (self.num_experts, self.hidden_size, self.lora_rank),
}
provided = {
"gate_lora_a": gate_lora_a,
"gate_lora_b": gate_lora_b,
"up_lora_a": up_lora_a,
"up_lora_b": up_lora_b,
"down_lora_a": down_lora_a,
"down_lora_b": down_lora_b,
}
for name, tensor in provided.items():
expected = expected_shapes[name]
if tensor.shape != expected:
raise ValueError(f"{name} shape mismatch: expected {expected}, got {tuple(tensor.shape)}")
if tensor.device.type != "cpu":
raise ValueError(f"{name} must be a CPU tensor for {self.method} SFT, got {tensor.device}.")
if tensor.dtype != torch.bfloat16:
raise ValueError(f"{name} must use torch.bfloat16 for {self.method} SFT, got {tensor.dtype}.")
if not tensor.is_contiguous():
raise ValueError(f"{name} must be contiguous for stable {self.method} pointer registration.")
grad_provided = {
"grad_gate_lora_a": grad_gate_lora_a,
"grad_gate_lora_b": grad_gate_lora_b,
"grad_up_lora_a": grad_up_lora_a,
"grad_up_lora_b": grad_up_lora_b,
"grad_down_lora_a": grad_down_lora_a,
"grad_down_lora_b": grad_down_lora_b,
}
for name, tensor in grad_provided.items():
expected = expected_shapes[name.removeprefix("grad_")]
if tensor.shape != expected:
raise ValueError(f"{name} shape mismatch: expected {expected}, got {tuple(tensor.shape)}")
if tensor.device.type != "cpu" or tensor.dtype != torch.bfloat16:
raise ValueError(
f"{name} must be a CPU torch.bfloat16 tensor for {self.method} SFT, "
f"got {tensor.dtype} on {tensor.device}."
)
if not tensor.is_contiguous():
raise ValueError(f"{name} must be contiguous for stable {self.method} pointer registration.")
self.gate_lora_a = gate_lora_a
self.gate_lora_b = gate_lora_b
self.up_lora_a = up_lora_a
self.up_lora_b = up_lora_b
self.down_lora_a = down_lora_a
self.down_lora_b = down_lora_b
self.grad_gate_lora_a = grad_gate_lora_a
self.grad_gate_lora_b = grad_gate_lora_b
self.grad_up_lora_a = grad_up_lora_a
self.grad_up_lora_b = grad_up_lora_b
self.grad_down_lora_a = grad_down_lora_a
self.grad_down_lora_b = grad_down_lora_b
if getattr(self, "_uses_authoritative_optimizer_grads", False):
parameter_count = sum(isinstance(tensor, torch.nn.Parameter) for tensor in provided.values())
if parameter_count not in (0, len(provided)):
raise ValueError(
"authoritative LoRA ownership requires all six weights to be nn.Parameter objects or none"
)
if parameter_count:
for name, parameter in provided.items():
self.register_authoritative_optimizer_grad(f"lora.{name}", parameter, grad_provided[f"grad_{name}"])
self._lora_initialized = True
if self._weights_loaded and self.moe is not None:
self.update_lora_weights()
def update_lora_weights(self) -> None:
if not self._weights_loaded:
raise RuntimeError("Weights not loaded. Call load_weights() first.")
if self._is_skip_lora:
return
if not self._lora_initialized:
if self.lora_rank <= 0:
return # Full mode without LoRA — no LoRA weights to update
raise RuntimeError("LoRA weights not initialized. Call init_lora_weights() first.")
# Weight pointer updates are load-time synchronous work. Calling the
# direct binding avoids nesting an update task inside CPUInfer's queue
# while SGLang is still in distributed model-loading barriers.
self.moe.update_lora_weights(
self.gate_lora_a.data_ptr(),
self.gate_lora_b.data_ptr(),
self.up_lora_a.data_ptr(),
self.up_lora_b.data_ptr(),
self.down_lora_a.data_ptr(),
self.down_lora_b.data_ptr(),
)
def update_base_weights(self) -> None:
"""Sync updated base weight parameters back to C++ kernel after optimizer step."""
if not self._weights_loaded:
raise RuntimeError("Weights not loaded. Call load_weights() first.")
if not self._full_weight_grad:
return # No base weights to update in LoRA mode
if self.gate_proj_buf is None:
raise RuntimeError("Base weight buffers not initialized. Call init_full_weight_grad_buffers() first.")
logger.info(f"Layer {self.layer_idx}: update_base_weights() - syncing updated weights to C++ kernel")
# Preferred path: update config pointers on existing C++ object and re-quantize.
# This avoids full C++ MOE object recreation (~0.6s/layer vs ~1.9s/layer).
if hasattr(self.moe, "set_base_weight_pointers"):
self.moe.set_base_weight_pointers(
self.gate_proj_buf.data.data_ptr(),
self.up_proj_buf.data.data_ptr(),
self.down_proj_buf.data.data_ptr(),
)
self.cpu_infer.submit(self.moe.load_weights_task())
self.cpu_infer.sync()
logger.info(f"Layer {self.layer_idx}: update_base_weights() - re-quantized existing kernel")
return
# Fallback: full reload path (creates new C++ MOE object)
# This is slower but works without C++ set_base_weight_pointers support.
logger.warning(
f"Layer {self.layer_idx}: set_base_weight_pointers not available, "
f"falling back to full C++ MOE object recreation"
)
old_moe = getattr(self, "moe", None)
if old_moe is not None:
del old_moe
self.gate_proj = self.gate_proj_buf.data
self.up_proj = self.up_proj_buf.data
self.down_proj = self.down_proj_buf.data
physical_to_logical_map = torch.arange(self.num_experts, dtype=torch.int64, device="cpu")
self._weights_loaded = False # Allow re-load
self.load_weights_from_tensors(
gate_proj=self.gate_proj,
up_proj=self.up_proj,
down_proj=self.down_proj,
physical_to_logical_map_cpu=physical_to_logical_map,
)
def save_backward_weights_from_tensors(
self,
gate_proj: torch.Tensor,
up_proj: torch.Tensor,
down_proj: torch.Tensor,
physical_to_logical_map: torch.Tensor,
output_path: str,
) -> None:
if not self._weights_loaded:
raise RuntimeError("Weights not loaded. Call load_weights() first.")
gate_proj = gate_proj.contiguous()
up_proj = up_proj.contiguous()
down_proj = down_proj.contiguous()
self.moe.prepare_and_save_bwd(
gate_proj.data_ptr(),
up_proj.data_ptr(),
down_proj.data_ptr(),
output_path,
)