| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | |
| 5 | import torch |
| 6 | |
| 7 | from vllm.model_executor.layers.quantization.utils import replace_parameter |
| 8 | from vllm.platforms import CpuArchEnum, current_platform |
| 9 | from vllm.scalar_type import scalar_types |
| 10 | |
| 11 | from .MPLinearKernel import MPLinearKernel, MPLinearLayerConfig |
| 12 | |
| 13 | |
| 14 | # This implementation is for the KleidiAI-accelerated w4a8int quantization |
| 15 | # scheme on Arm CPUs: |
| 16 | # torch.ops.aten._dyn_quant_matmul_4bit performs dynamic quantized matmul |
| 17 | # it takes: |
| 18 | # - int4 weights packed along with bias/scales by |
| 19 | # torch.ops.aten._dyn_quant_pack_4bit_weight |
| 20 | # - float32/bfloat16 activations |
| 21 | # then it leverages KleidiAI ukernels that: |
| 22 | # - dynamically quantize the activations to int8 |
| 23 | # - unpack the int4 weights to int8 |
| 24 | # - perform int8 x int8 -> int32 matmul |
| 25 | # - dequantize the int32 output to float32/bfloat16 outputs |
| 26 | class Dynamic4bitLinearKernel(MPLinearKernel): |
| 27 | SUPPORTED_QUANT_TYPES = [scalar_types.int4] |
| 28 | |
| 29 | @classmethod |
| 30 | def get_min_capability(cls) -> int: |
| 31 | return 1 |
| 32 | |
| 33 | @classmethod |
| 34 | def can_implement(cls, c: MPLinearLayerConfig) -> tuple[bool, str | None]: |
| 35 | if not current_platform.is_cpu(): |
| 36 | return False, "Only CPU is supported" |
| 37 | if c.weight_type not in cls.SUPPORTED_QUANT_TYPES: |
| 38 | return False, f"Unsupported quant type {c.weight_type}" |
| 39 | if ( |
| 40 | current_platform.get_cpu_architecture() == CpuArchEnum.ARM |
| 41 | and c.act_type |
| 42 | not in [ |
| 43 | torch.float32, |
| 44 | torch.bfloat16, |
| 45 | torch.float16, |
| 46 | ] |
| 47 | ): |
| 48 | return ( |
| 49 | False, |
| 50 | "Dynamic4bitLinearKernel on Arm requires Float32 or" |
| 51 | " BFloat16 or Float16 activations", |
| 52 | ) |
| 53 | if c.full_weight_shape[0] % c.group_size != 0: |
| 54 | return ( |
| 55 | False, |
| 56 | f"Group size ({c.group_size}) does not evenly divide" |
| 57 | " the number of input features " |
| 58 | f"({c.full_weight_shape[0]})", |
| 59 | ) |
| 60 | if current_platform.get_cpu_architecture() == CpuArchEnum.ARM: |
| 61 | try: |
| 62 | # Attempt to retrieve the operation |
| 63 | _ = torch.ops.aten._dyn_quant_matmul_4bit |
| 64 | except AttributeError: |
| 65 | return ( |
| 66 | False, |
| 67 | f"PyTorch {torch.__version__} does not support" |
| 68 | " _dyn_quant_matmul_4bit. Install a newer version", |
| 69 | ) |
| 70 | return True, None |
| 71 | |
| 72 | def process_weights_after_loading(self, layer: torch.nn.Module): |
| 73 | c = self.config |
| 74 | packed_weight = getattr(layer, self.w_q_name) |
| 75 | packed_weight = packed_weight.add(8) |
| 76 | uint8_packed = (packed_weight[::, 1::2] << 4 | packed_weight[::, ::2]).to( |
| 77 | torch.uint8 |
| 78 | ) |
| 79 | |
| 80 | scales = getattr(layer, self.w_s_name) |
| 81 | block_size = c.group_size |
| 82 | |
| 83 | # Handle scaling factors for partitioned weights |
| 84 | if block_size == c.partition_weight_shape[0]: |
| 85 | scales = scales.to( |
| 86 | torch.float32 |
| 87 | ) # Float32 & Bfloat16 variants requires float32 scales |
| 88 | scales = scales.view(-1, 1) # Channel-wise scales |
| 89 | if layer.bias is not None: |
| 90 | # Float32 & Bfloat16 variants requires float32 bias |
| 91 | replace_parameter( |
| 92 | layer, |
| 93 | "bias", |
| 94 | torch.nn.Parameter( |
| 95 | layer.bias.to(torch.float32), requires_grad=False |
| 96 | ), |
| 97 | ) |
| 98 | else: |
| 99 | # KleidiAI kernel requires bfloat16 scales with groupwise scheme |
| 100 | scales = scales.to(torch.bfloat16) |
| 101 | |
| 102 | # Repack weights as per kernel requirement |
| 103 | w = torch.ops.aten._dyn_quant_pack_4bit_weight( |
| 104 | uint8_packed, |
| 105 | scales, |
| 106 | layer.bias, |
| 107 | block_size, |
| 108 | c.partition_weight_shape[0], |
| 109 | c.partition_weight_shape[1], |
| 110 | ) |
| 111 | replace_parameter( |
| 112 | layer, self.w_q_name, torch.nn.Parameter(w, requires_grad=False) |
| 113 | ) |
| 114 | setattr(layer, self.w_s_name, None) |
| 115 | |
| 116 | def apply_weights( |
| 117 | self, |
| 118 | layer: torch.nn.Module, |
| 119 | x: torch.Tensor, |
| 120 | bias: torch.Tensor | None = None, |
| 121 | ) -> torch.Tensor: |
| 122 | # PyTorch / KleidiAI kernels natively support the following configs: |
| 123 | # - channelwise with bfloat16 / float32 activations |
| 124 | # - groupwise with float32 activations |
| 125 | # To support: |
| 126 | # - groupwise with bfloat16/float16 activations: we need to upcast |
| 127 | # activations to float32 before matmul and downcast back to bfloat16/float16 |
| 128 | # - channelwise with float16 activations, we need to upcast activations to |
| 129 | # float32 before matmul and downcast back to float16 |
| 130 | # Note: these activations will be dynamically quantized to int8 by the kernel. |
| 131 | |
| 132 | c = self.config |
| 133 | is_groupwise = c.group_size != c.partition_weight_shape[0] |
| 134 | # dtype of activations before they get dynamically quantized to int8 |
| 135 | original_pre_quant_act_dtype = x.dtype |
| 136 | pre_quant_act_dtype = original_pre_quant_act_dtype |
| 137 | if ( |
| 138 | is_groupwise and pre_quant_act_dtype == torch.bfloat16 |
| 139 | ) or pre_quant_act_dtype == torch.float16: |
| 140 | pre_quant_act_dtype = torch.float32 |
| 141 | |
| 142 | x_2d = x.reshape(-1, x.shape[-1]) |
| 143 | if pre_quant_act_dtype != original_pre_quant_act_dtype: |
| 144 | x_2d = x_2d.to(pre_quant_act_dtype) |
| 145 | |
| 146 | out_shape = x.shape[:-1] + (c.partition_weight_shape[1],) |
| 147 | |
| 148 | w_q = getattr(layer, self.w_q_name) |
| 149 | output = torch.ops.aten._dyn_quant_matmul_4bit( |
| 150 | x_2d, |
| 151 | w_q, |
| 152 | c.group_size, |
| 153 | c.partition_weight_shape[0], |
| 154 | c.partition_weight_shape[1], |
| 155 | ).reshape(out_shape) |
| 156 | |
| 157 | if pre_quant_act_dtype != original_pre_quant_act_dtype: |
| 158 | output = output.to(original_pre_quant_act_dtype) |
| 159 | return output |