328 lines
12 KiB
Python
328 lines
12 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
|
|
import torch
|
|
|
|
from vllm import _custom_ops as ops
|
|
from vllm import envs
|
|
from vllm.model_executor.layers.quantization.utils import replace_parameter
|
|
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
|
|
convert_to_channelwise,
|
|
)
|
|
from vllm.model_executor.layers.utils import check_cpu_sgl_kernel
|
|
from vllm.platforms import current_platform
|
|
from vllm.platforms.interface import CpuArchEnum
|
|
|
|
from .BlockScaledMMLinearKernel import (
|
|
Fp8BlockScaledMMLinearKernel,
|
|
FP8ScaledMMLinearLayerConfig,
|
|
)
|
|
from .ScaledMMLinearKernel import (
|
|
Int8ScaledMMLinearKernel,
|
|
Int8ScaledMMLinearLayerConfig,
|
|
)
|
|
|
|
|
|
class CPUInt8ScaledMMLinearKernel(Int8ScaledMMLinearKernel):
|
|
@classmethod
|
|
def is_supported(
|
|
cls, compute_capability: int | None = None
|
|
) -> tuple[bool, str | None]:
|
|
if not current_platform.is_cpu():
|
|
return False, "requires CPU."
|
|
return True, None
|
|
|
|
@classmethod
|
|
def can_implement(cls, c: Int8ScaledMMLinearLayerConfig) -> tuple[bool, str | None]:
|
|
return True, None
|
|
|
|
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
|
w_q_name, _, _, _, _ = self.layer_param_names
|
|
weight = getattr(layer, w_q_name)
|
|
dtype = weight.dtype
|
|
N, K = weight.size()
|
|
if (
|
|
current_platform.get_cpu_architecture() == CpuArchEnum.X86
|
|
and envs.VLLM_CPU_SGL_KERNEL
|
|
and self.config.input_symmetric
|
|
and check_cpu_sgl_kernel(N, K, dtype)
|
|
):
|
|
self.linear_method = self._apply_weights_sgl
|
|
self.process_weights_for_sgl(layer)
|
|
else:
|
|
self.linear_method = self._apply_weights_onednn
|
|
self.process_weights_for_onednn(layer)
|
|
|
|
def process_weights_for_onednn(self, layer: torch.nn.Module) -> None:
|
|
# WEIGHT
|
|
# Transpose to [K, N] for convenience
|
|
w_q_name, w_s_name, i_s_name, i_zp_name, azp_adj_name = self.layer_param_names
|
|
weight = getattr(layer, w_q_name)
|
|
replace_parameter(
|
|
layer,
|
|
w_q_name,
|
|
torch.nn.Parameter(weight.t().data, requires_grad=False),
|
|
)
|
|
|
|
# WEIGHT SCALE
|
|
# oneDNN kernels support only per-tensor and per-channel.
|
|
# If we have a fused module (QKV, MLP) with per tensor scales (thus N
|
|
# scales being passed to the kernel), convert to the per-channel case.
|
|
is_fused_module = len(layer.logical_widths) > 1
|
|
weight_scale = getattr(layer, w_s_name)
|
|
if is_fused_module and not self.config.is_channelwise:
|
|
weight_scale = convert_to_channelwise(weight_scale, layer.logical_widths)
|
|
replace_parameter(
|
|
layer,
|
|
w_s_name,
|
|
torch.nn.Parameter(weight_scale.data, requires_grad=False),
|
|
)
|
|
|
|
# INPUT SCALE
|
|
if self.config.is_static_input_scheme:
|
|
input_scale = getattr(layer, i_s_name)
|
|
|
|
if self.config.input_symmetric:
|
|
replace_parameter(
|
|
layer,
|
|
i_s_name,
|
|
torch.nn.Parameter(input_scale.max(), requires_grad=False),
|
|
)
|
|
else:
|
|
input_zero_point = getattr(layer, i_zp_name)
|
|
|
|
# reconstruct the ranges
|
|
int8_traits = torch.iinfo(torch.int8)
|
|
azps = input_zero_point.to(dtype=torch.int32)
|
|
range_max = (input_scale * (int8_traits.max - azps)).max()
|
|
range_min = (input_scale * (int8_traits.min - azps)).min()
|
|
|
|
scale = (range_max - range_min) / (int8_traits.max - int8_traits.min)
|
|
replace_parameter(
|
|
layer, i_s_name, torch.nn.Parameter(scale, requires_grad=False)
|
|
)
|
|
|
|
azp = (
|
|
(int8_traits.min - range_min / scale).round().to(dtype=torch.int32)
|
|
)
|
|
replace_parameter(
|
|
layer, i_zp_name, torch.nn.Parameter(azp, requires_grad=False)
|
|
)
|
|
|
|
# Different from cutlass, oneDNN kernels only need the AZP adjustment
|
|
# term for dynamic quantization. And s_b should be folded into the
|
|
# term. Such as:
|
|
# s_a * s_b * [(A - zp_a)B] + bias =
|
|
# s_a * (s_b * AB) - s_a * s_b * zp_a * B + bias =
|
|
# s_a * GEMM_output - s_a * zp_a * adj + bias
|
|
if not (self.config.input_symmetric and self.config.is_static_input_scheme):
|
|
weight = getattr(layer, w_q_name)
|
|
weight_scale = getattr(layer, w_s_name)
|
|
azp_adj = weight.sum(dim=0, keepdim=True, dtype=torch.float32)
|
|
azp_adj = azp_adj * weight_scale.squeeze()
|
|
setattr(
|
|
layer,
|
|
azp_adj_name,
|
|
torch.nn.Parameter(azp_adj, requires_grad=False),
|
|
)
|
|
|
|
weight = getattr(layer, w_q_name)
|
|
self.dnnl_handler = ops.create_onednn_scaled_mm(
|
|
weight,
|
|
getattr(layer, w_s_name),
|
|
torch.get_default_dtype(),
|
|
getattr(layer, i_s_name) is None,
|
|
not self.config.input_symmetric,
|
|
32,
|
|
)
|
|
# weight is prepacked and maintained by the dnnl_handler,
|
|
# release the original weight
|
|
setattr(layer, w_q_name, None)
|
|
del weight
|
|
|
|
def process_weights_for_sgl(self, layer: torch.nn.Module) -> None:
|
|
w_q_name, w_s_name, _, _, _ = self.layer_param_names
|
|
# WEIGHT
|
|
weight = getattr(layer, w_q_name)
|
|
packed_weight = torch.ops._C.convert_weight_packed(weight)
|
|
replace_parameter(
|
|
layer, w_q_name, torch.nn.Parameter(packed_weight, requires_grad=False)
|
|
)
|
|
|
|
if layer.bias is not None:
|
|
bias = layer.bias
|
|
layer.register_parameter(
|
|
"bias_fp32", torch.nn.Parameter(bias.float().data, requires_grad=False)
|
|
)
|
|
|
|
# WEIGHT SCALE
|
|
# CPU SGL kernels only support per-channel.
|
|
# For per-tensor quant, convert to the per-channel case.
|
|
weight_scale = getattr(layer, w_s_name)
|
|
if not self.config.is_channelwise:
|
|
weight_scale = convert_to_channelwise(weight_scale, layer.logical_widths)
|
|
replace_parameter(
|
|
layer,
|
|
w_s_name,
|
|
torch.nn.Parameter(weight_scale.data, requires_grad=False),
|
|
)
|
|
|
|
def apply_weights(
|
|
self,
|
|
layer: torch.nn.Module,
|
|
x: torch.Tensor,
|
|
bias: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
return self.linear_method(
|
|
layer,
|
|
x,
|
|
bias,
|
|
)
|
|
|
|
def _apply_weights_onednn(
|
|
self,
|
|
layer: torch.nn.Module,
|
|
x: torch.Tensor,
|
|
bias: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
x_shape = x.shape
|
|
x = x.reshape(-1, x_shape[-1]) if len(x_shape) > 2 else x
|
|
w_q, w_s, i_s, i_zp, azp_adj = self._get_layer_params(layer)
|
|
|
|
# ops.scaled_int8_quant supports both dynamic and static quant:
|
|
# * dynamic, i_s is None and x_s computed from x.
|
|
# * static, i_s is scalar and x_s is i_s.
|
|
x_q, x_s, x_zp = ops.onednn_scaled_int8_quant(
|
|
x, i_s, i_zp, self.config.input_symmetric
|
|
)
|
|
|
|
m = x.size(0)
|
|
n = self.dnnl_handler.n
|
|
out = torch.empty((m, n), dtype=x.dtype)
|
|
ops.onednn_scaled_mm(self.dnnl_handler, x_q, out, x_s, x_zp, azp_adj, bias)
|
|
out = out.reshape(x_shape[:-1] + (n,)) if len(x_shape) > 2 else out
|
|
return out
|
|
|
|
def _apply_weights_sgl(
|
|
self,
|
|
layer: torch.nn.Module,
|
|
x: torch.Tensor,
|
|
bias: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
w_q, w_s, _, _, _ = self._get_layer_params(layer)
|
|
return torch.ops._C.int8_scaled_mm_with_quant(
|
|
x,
|
|
w_q,
|
|
w_s,
|
|
layer.bias_fp32 if bias is not None else None,
|
|
x.dtype,
|
|
True,
|
|
)
|
|
|
|
|
|
class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
|
"""FP8 W8A16 block-quantized GEMM via AMX BRGEMM on CPU."""
|
|
|
|
# Input stays BF16 — no FP8 activation quantization.
|
|
apply_input_quant = False
|
|
|
|
@classmethod
|
|
def is_supported(
|
|
cls, compute_capability: int | None = None
|
|
) -> tuple[bool, str | None]:
|
|
if not current_platform.is_cpu():
|
|
return False, "requires CPU platform."
|
|
if not torch.cpu._is_amx_tile_supported():
|
|
return False, "requires AMX tile support (Sapphire Rapids or newer)."
|
|
if not ops._supports_cpu_fp8_w8a16:
|
|
return False, "fp8_scaled_mm_cpu op not available."
|
|
return True, None
|
|
|
|
@classmethod
|
|
def can_implement(
|
|
cls, config: FP8ScaledMMLinearLayerConfig
|
|
) -> tuple[bool, str | None]:
|
|
# Validate weight block shape
|
|
weight_gs = config.weight_quant_key.scale.group_shape
|
|
if weight_gs.col <= 0 or weight_gs.col != 128:
|
|
return False, (
|
|
"CPU FP8 kernel requires K-dimension block size of 128, "
|
|
f"got {weight_gs.col}."
|
|
)
|
|
if weight_gs.row <= 0 or weight_gs.row % 32 != 0:
|
|
return False, (
|
|
"CPU FP8 kernel requires N-dimension block size to be "
|
|
f"a positive multiple of 32, got {weight_gs.row}."
|
|
)
|
|
if config.out_dtype not in (torch.bfloat16, torch.float32):
|
|
return False, "Only bfloat16/float32 output dtype supported."
|
|
return True, None
|
|
|
|
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
|
# Skip the base class process (FP8 padding / fnuz normalization)
|
|
# which is GPU-oriented. Instead, VNNI-prepack weights for AMX.
|
|
params = self._get_layer_params(layer)
|
|
packed_weight = torch.ops._C.convert_weight_packed(params.weight)
|
|
replace_parameter(
|
|
layer,
|
|
params.WEIGHT,
|
|
torch.nn.Parameter(packed_weight, requires_grad=False),
|
|
)
|
|
|
|
# Re-wrap scale as a plain Parameter so the kernel can read it
|
|
# without weight-loader metadata interfering.
|
|
scale_attr = (
|
|
params.WEIGHT_SCALE_INV
|
|
if params.weight_scale_inv is not None
|
|
else params.WEIGHT_SCALE
|
|
)
|
|
weight_scale = (
|
|
params.weight_scale_inv
|
|
if params.weight_scale_inv is not None
|
|
else params.weight_scale
|
|
)
|
|
assert weight_scale is not None
|
|
replace_parameter(
|
|
layer,
|
|
scale_attr,
|
|
torch.nn.Parameter(weight_scale.data, requires_grad=False),
|
|
)
|
|
|
|
def apply_weights(
|
|
self,
|
|
layer: torch.nn.Module,
|
|
x: torch.Tensor,
|
|
bias: torch.Tensor | None = None,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
params = self._get_layer_params(layer)
|
|
weight_scale = (
|
|
params.weight_scale_inv
|
|
if params.weight_scale_inv is not None
|
|
else params.weight_scale
|
|
)
|
|
|
|
x_2d = x.reshape(-1, x.shape[-1]) if x.dim() > 2 else x
|
|
out = torch.ops._C.fp8_scaled_mm_cpu(
|
|
x_2d,
|
|
params.weight,
|
|
weight_scale,
|
|
list(self.weight_group_shape),
|
|
bias,
|
|
x.dtype,
|
|
True, # is_vnni (weight already prepacked)
|
|
)
|
|
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
|
|
|
|
def apply_block_scaled_mm(
|
|
self,
|
|
A: torch.Tensor,
|
|
B: torch.Tensor,
|
|
As: torch.Tensor,
|
|
Bs: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
raise NotImplementedError(
|
|
"CPUFp8BlockScaledMMKernel overrides apply_weights directly."
|
|
)
|