0


AWQ量化及AutoAWQ代码详解

AWQ量化出自mit韩松组内2023年关于LLM量化的一篇文章:AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration


在介绍量化之前,先简要的介绍一下模型的量化

1. 为什么要进行模型量化?量化有什么好处呢?

模型之所以要进行量化,是因为我们日常使用fp16(floating point 16)或者bf16(Brain Floating Point)训练模型,fp16有1个符号位,5个指数位,10个尾数位,表示范围为(finfo(resolution=0.001, min=-65504, max=65504, eps=0.000976562, smallest_normal=6.10352e-05, tiny=6.10352e-05, dtype=float16)。

而bf16有1个符号位,8个指数位,7个尾数位,可以表示的范围为:finfo(resolution=0.01, min=-3.38953e+38, max=3.38953e+38, eps=0.0078125, smallest_normal=1.17549e-38, tiny=1.17549e-38, dtype=bfloat16)

可以看到,fp16和bf16可以表示的范围是很大的,但是这也产生了两个问题:(1)内存(显存)占用开销很大;以作者实际使用的qwen1.5-32b模型来计算,32b的16位浮点模型需要64G的显存来加载,加上数据集和kv cache。大致需要44090 (96G)才能运行。这是一个较大的开销,44090的服务器在10w人名币左右。(2)模型的runtime开销较大16位的浮点模型在进行矩阵运算时,是十分耗时的,更甚者像squeezellm的观点,模型的整个runtime的bottleneck在于模型weight的load。squeezellm认为当模型weight的bits降低,可以显著加速模型的runtime。(但是我在用awq测试的时候,awq模型的运算速度更慢于16位的浮点模型,因为awq的weight 在进行gemm or gemv前,需要先dequant)。

在意识到16位浮点模型的这些劣势后,一些大佬就在想能不能把16位的浮点数转换成bits更少的整数类型,例如量化成int8(LLM.int8, SmoothQuant),int4(GPTQ,AWQ),3bits(SqueezeLLM)。更有甚之,用到了1/2bits量化(AQLM)。模型在量化之后可以缓解上面浮点模型的两个问题,当使用awq把模型从16位的浮点模型(16 bits)量化到int4(4 bits)时,模型的大小从64G变为了16G,模型大小变为原来的1/4。可以节省大量的出计算资源。

2. 如何给模型进行量化呢?

模型量化详解-CSDN博客

总的来讲的话,16位的浮点模型可以由低bits的数乘以一个scale得到。


2.1 awq主要思路

核心观点1:权重并不同等重要,仅有小部分显著权重对推理结果影响较大

作者指出,模型的权重并不同等重要,仅有0.1%~1%的小部分显著权重对模型输出精度影响较大。因此如果能有办法只对0.1%~1%这一小部分权重保持原来的精度(FP16),对其他权重进行低比特量化,就可以在保持精度几乎不变的情况下,大幅降低模型内存占用,并提升推理速度。这就涉及到一个问题,如何鉴别显著权重,常用的方法有三种

  • 随机挑选:听天由命,随机选出0.1%~1%的权重作为显著权重,当然这种方法很不科学。
  • 基于权重分布挑选:对权重矩阵(比如自注意力中的 𝑊𝑞 , 𝑊𝑘 , 𝑊𝑣 )中的元素按绝对值大小由大到小排序,绝对值越大越显著,选择前0.1%~1%的元素作为显著权重。
  • 基于激活值分布挑选:激活值就是与权重矩阵作matmul运算的输入

作者对三种方式进行了测试(Tab 1),发现随机挑选的结果与RTN的结果差不多,基于权重W的量化与随机挑选的结果差不多。而基于激活值分布挑选weight的结果与fp16的精度差不多。

作者为了避免方法在实现上过于复杂,在挑选显著权重时,并非在“元素”级别进行挑选,而是在“通道(channel)”级别进行挑选,即权重矩阵的一行作为一个单位。在计算时,首先将激活值对每一列求绝对值的平均值,然后把平均值较大的一列对应的通道视作显著通道,保留FP16精度。对其他通道进行低比特量化

但另一个问题随之而来,如果权重矩阵中有的元素用FP16格式存储,有的用INT4格式存储,不仅存储时很麻烦,计算时取数也很麻烦。于是,作者想了一个变通的方法——Scaling。

核心观点2:量化时对显著权重进行放大可以降低量化误差

量化公示可以写长上面那样,其中 𝑁 是量化后的比特数, Δ 是量化因子(scaler)。 w′=Round(wΔ) 是量化过程, Δ⋅w′ 是反量化过程。原始的 w 、 Δ 和输入 𝑥 都是FP16格式,不会带来精度损失。整个过程的精度损失全部来源于量化过程中的 Round 取整函数,其误差近似成[0, 0.5]的均匀分布,期望为0.25,可以写作 RoundErr(⋅)∼0.25

考虑对于权重矩阵 w 中的单个元素 𝑤 ,引入一个缩放因子 𝑠>1 ,量化过程将 𝑤 与该因子相乘,写作 w′=Round(w𝑠/Δ′) ,相应地将反量化过程写作 Δ′⋅w′𝑠,这样在计算过程上是“等价”的,如公式2

公式1和公式2在计算过程中是一样的,但是仍然会有不一样的精度损失,可以写作:

因此,作者改变了思路:为了更加hardware-friendly,我们对所有权重均进行低比特量化,但是,在量化时,对于显著权重乘以较大的 𝑠 ,相当于降低其量化误差;同时,对于非显著权重,乘以较小的 𝑠 ,相当于给予更少的关注。这便是上一节提到的缩放(Scaling)方法。

算法: 自动计算scaling系数

按照作者的观点,激活值越大,对应通道越显著,就应该分配更大的缩放系数降低其量化误差。因此,作者统计了各通道的平均激活值(计算输入矩阵各列绝对值的平均值) sx ,并直接将此作为各通道的缩放系数。同时引入一个变量 𝛼 用于平衡显著通道和非显著通道的系数,由此,问题转化为优化L(s) 使用网格搜索\alpha。在源码中,在[0, 1]平均取20个数,分别设为\alpha,计算L(s)最小的为最佳\alpha。smoothquant与awq的思路一致,而smoothquant计算s的方式为:

2.2 code

autoawq的量化过程从AwqQuantizer.init_quant()开始,self.awq_model是Qwen2AWQForCausalLM类(以qwen1.5为例),self.model是加载的qwen模型。

  1. def init_quant(self, n_samples=128, seqlen=512):
  2. modules = self.awq_model.get_model_layers(self.model) # return model.model.layers
  3. samples = get_calib_dataset(
  4. data=self.calib_data,
  5. tokenizer=self.tokenizer,
  6. n_samples=n_samples,
  7. block_size=seqlen,
  8. split=self.split,
  9. text_column=self.text_column,
  10. )
  11. samples = torch.cat(samples, dim=0)
  12. -----------------------------------------------------------
  13. class Catcher(nn.Module):
  14. def __init__(self, module):
  15. super().__init__()
  16. self.module = module
  17. def forward(self, *args, **kwargs):
  18. # assume first input to forward is hidden states
  19. if len(args) > 0:
  20. hidden_states = args[0]
  21. del args
  22. else:
  23. first_key = list(kwargs.keys())[0]
  24. hidden_states = kwargs.pop(first_key)
  25. inps.append(hidden_states)
  26. layer_kwargs.update(kwargs)
  27. raise ValueError # early exit to break later inference
  28. -----------------------------------------------------------
  29. return modules, layer_kwargs, inps

[STEP 1]:Get layer, extract linear modules, extract input features

把module里面的线性层用字典保存, _get_input_feat会把每一层的输入数据给提取保存。

  1. # [STEP 1]: Get layer, extract linear modules, extract input features
  2. named_linears = get_named_linears(self.modules[i])
  3. # named_linears is the dictionary of named linear layers in the module, e.g. :
  4. """
  5. {'self_attn.q_proj': Linear(in_features=1024, out_features=1024, bias=True),
  6. 'self_attn.k_proj': Linear(in_features=1024, out_features=1024, bias=True),
  7. 'self_attn.v_proj': Linear(in_features=1024, out_features=1024, bias=True),
  8. 'self_attn.o_proj': Linear(in_features=1024, out_features=1024, bias=False),
  9. 'mlp.gate_proj': Linear(in_features=1024, out_features=2816, bias=False),
  10. 'mlp.up_proj': Linear(in_features=1024, out_features=2816, bias=False),
  11. 'mlp.down_proj': Linear(in_features=2816, out_features=1024, bias=False)}
  12. """
  13. # Filter out the linear layers we don't want to exclude
  14. named_linears = exclude_layers_to_not_quantize(
  15. named_linears, self.modules_to_not_convert
  16. )
  17. input_feat = self._get_input_feat(self.modules[i], named_linears)
  18. clear_memory()

[STEP 2]: Compute and apply scale list

  1. module_config: List[Dict] = self.awq_model.get_layers_for_scaling(
  2. self.modules[i], input_feat, self.module_kwargs
  3. )
  4. # 上面的代码是把模型的层给抽取出来,纳入字典中, prev_op 就是前一个层,
  5. # layers 就是当前层的线性层, inp 就是层输入特征。
  6. # module2inspect 是所有层的混合。
  7. scales_list = [
  8. self._search_best_scale(self.modules[i], **layer)
  9. for layer in module_config
  10. ]
  11. apply_scale(self.modules[i], scales_list, input_feat_dict=input_feat)

第2个step是计算每层的scaling,module_config是一个包含了当前层,前面层,输入特征的一个字典集合。然后开始找到最好的scale,首先把weight分组进行归一化,再在channel为度求得weight得mean。同时x作为input也计算在channel上的mean。计算fp16模型的输出用于比较得到最好的scale。

  1. @torch.no_grad()
  2. def _search_best_scale(
  3. self,
  4. module,
  5. prev_op,
  6. layers: List[nn.Linear],
  7. inp: torch.Tensor,
  8. module2inspect=None,
  9. kwargs={},
  10. ):
  11. if module2inspect is None:
  12. assert len(layers) == 1
  13. module2inspect = layers[0]
  14. if "use_cache" in kwargs:
  15. kwargs.pop("use_cache")
  16. # Put x on the right device
  17. inp = inp.to(next(module2inspect.parameters()).device)
  18. # [STEP 1]: Compute per-channel mean of normalised weights
  19. # All layer weights are concatted together
  20. weight = torch.cat([_m.weight for _m in layers], dim=0)
  21. org_shape = weight.shape
  22. # The weights are reshaped to be organised by quantization group
  23. weight = weight.view(-1, self.group_size)
  24. # Calculates the relative magnitude of the weights within each of the quantization groups,
  25. # and rescales each group individually so that each group has weights on a 0-1 scale.
  26. w_scale = weight.abs() / (weight.abs().amax(dim=1, keepdim=True) + 1e-6)
  27. # Resizes the rescaled weight matrix back up to its original dimensions
  28. w_scale = w_scale.view(org_shape)
  29. # Gets the average rescaled magnitude for each output channel
  30. w_mean = w_scale.mean(0)
  31. clear_memory(weight)
  32. # [STEP 2]: Compute per-channel mean of the input activation
  33. x_mean = inp.abs().view(-1, inp.shape[-1]).mean(0)
  34. # [STEP 3]: Compute output of module
  35. with torch.no_grad():
  36. module_kwargs = self._sanitize_kwargs(kwargs, module2inspect)
  37. fp16_output = module2inspect(inp, **module_kwargs)
  38. if isinstance(fp16_output, tuple):
  39. fp16_output = fp16_output[0]
  40. # [STEP 4]: Compute loss
  41. best_scales = self._compute_best_scale(
  42. inp, w_mean, x_mean, module2inspect, layers, fp16_output, module_kwargs
  43. )
  44. return (
  45. get_op_name(module, prev_op),
  46. tuple([get_op_name(module, m) for m in layers]),
  47. best_scales,
  48. )

_compute_best_scale使用网格搜索,对于公式(4),在网格搜索中直接使用x_mean的\alpha(ratio)作为s, \alpha作为平衡因子,而网格搜索是找到最好的\alpha使得量化完模型的输出和fp16模型的输出的差值最小(L2 Loss)。

  1. def _compute_best_scale(
  2. self,
  3. x,
  4. w_mean,
  5. x_mean,
  6. module2inspect,
  7. linears2scale: List[nn.Linear],
  8. fp16_output,
  9. kwargs={},
  10. ):
  11. """
  12. Compute loss and select best scales
  13. L(s) = || Q(W * s) (s^-1 * X) - W * X ||
  14. Q: weight quantization function | pseudo_quantize_tensor(W * s)
  15. X: inputs from calib dataset | X
  16. W: original weights in FP16 | layer
  17. s: per channel scaling factor | s^-1 * X
  18. """
  19. n_grid = 20
  20. history = []
  21. best_ratio = -1
  22. best_scales = None
  23. best_error = float("inf")
  24. org_sd = {k: v.cpu() for k, v in module2inspect.state_dict().items()}
  25. device = x.device
  26. x_mean = x_mean.view(-1).to(device)
  27. w_mean = w_mean.view(-1).to(device)
  28. for ratio in range(n_grid):
  29. # create new scales
  30. ratio = ratio / n_grid
  31. # NOTE: s^-1 * x is fused here, according to paper
  32. if self.duo_scaling:
  33. scales = (x_mean.pow(ratio) / (w_mean.pow(1 - ratio) + 1e-4)).clamp(min=1e-4)
  34. else:
  35. scales = x_mean.pow(ratio).clamp(min=1e-4).view(-1)
  36. scales = scales / (scales.max() * scales.min()).sqrt()
  37. scales_view = scales.view(1, -1).to(device)
  38. # Q(W * s)
  39. for fc in linears2scale:
  40. fc.weight.mul_(scales_view)
  41. fc.weight.data = (
  42. self.pseudo_quantize_tensor(fc.weight.data)[0] / scales_view
  43. )
  44. # W * X
  45. int_w_output = module2inspect(x, **kwargs)
  46. if isinstance(int_w_output, tuple):
  47. int_w_output = int_w_output[0]
  48. # compute mean squared error (L2 norm)
  49. loss = (
  50. (fp16_output - int_w_output).float().pow(2).mean().item()
  51. ) # NOTE: float prevents overflow
  52. history.append(loss)
  53. if loss < best_error:
  54. best_error = loss
  55. best_ratio = ratio
  56. best_scales = scales.clone()
  57. module2inspect.load_state_dict(org_sd)
  58. if best_ratio == -1:
  59. logging.debug(history)
  60. raise Exception
  61. assert torch.isnan(best_scales).sum() == 0, best_scales
  62. return best_scales.detach().cpu()

apply_scale对每层的weight进行scale处理,例如:

Test: 改变grid的大小

通过改变网格的大小,可以控制平衡因子的取值精度,设置grid分别为grid=10, 20(awq默认),40, 100得到的结果如下:

以结果来看,改变grid并没有带来精度的提升。

[STEP 3]: Compute and apply clipping list

也是使用网格搜索求的最合适的最大值,并裁减

  1. for i_b in range(org_w_shape[0] // oc_batch_size):
  2. w = w_all[i_b * oc_batch_size : (i_b + 1) * oc_batch_size]
  3. org_max_val = w.abs().amax(dim=-1, keepdim=True) # co, 1, n_group, 1
  4. best_max_val = org_max_val.clone()
  5. min_errs = torch.ones_like(org_max_val) * 1e9
  6. input_feat = input_feat.to(w.device)
  7. org_out = (input_feat * w).sum(dim=-1) # co, n_token, n_group
  8. for i_s in range(int(max_shrink * n_grid)):
  9. max_val = org_max_val * (1 - i_s / n_grid)
  10. min_val = -max_val
  11. cur_w = torch.clamp(w, min_val, max_val)
  12. q_w = self.pseudo_quantize_tensor(cur_w)[0]
  13. cur_out = (input_feat * q_w).sum(dim=-1)
  14. # co, 1, n_group, 1
  15. err = (cur_out - org_out).pow(2).mean(dim=1).view(min_errs.shape)
  16. del cur_w
  17. del cur_out
  18. cur_best_idx = err < min_errs
  19. min_errs[cur_best_idx] = err[cur_best_idx]
  20. best_max_val[cur_best_idx] = max_val[cur_best_idx]
  21. best_max_val_all.append(best_max_val)
  22. best_max_val = torch.cat(best_max_val_all, dim=0)
  1. @torch.no_grad()
  2. def apply_clip(module, clip_list: Tuple[str, torch.Tensor]):
  3. for name, max_val in clip_list:
  4. layer: nn.Linear = get_op_by_name(module, name)
  5. layer.to(get_best_device())
  6. max_val = max_val.to(layer.weight.device)
  7. org_shape = layer.weight.shape
  8. layer.weight.data = layer.weight.data.reshape(*max_val.shape[:2], -1)
  9. layer.weight.data = torch.clamp(layer.weight.data, -max_val, max_val)
  10. layer.weight.data = layer.weight.data.reshape(org_shape)
  11. layer.cpu()

[STEP 4]: Quantize weights

  1. def _apply_quant(self, module, named_linears: Dict[str, nn.Linear]):
  2. for name, linear_layer in named_linears.items():
  3. # NOTE: small regression in perplexity if linear layer uses .cpu().float()
  4. linear_layer = linear_layer.to(get_best_device()).half()
  5. linear_layer.weight.data, scales, zeros = self.pseudo_quantize_tensor(
  6. linear_layer.weight.data
  7. )
  8. if self.version == "gemm":
  9. scales = scales.t().contiguous()
  10. if zeros is not None:
  11. zeros = zeros.t().contiguous()
  12. q_linear_module = WQLinear_GEMM
  13. elif self.version == "gemv":
  14. q_linear_module = WQLinear_GEMV
  15. elif self.version == "marlin":
  16. q_linear_module = WQLinear_Marlin
  17. elif self.version == "gemv_fast":
  18. q_linear_module = WQLinear_GEMVFast
  19. else:
  20. raise ValueError(f"Unknown version {self.version}")
  21. q_linear = q_linear_module.from_linear(
  22. linear=linear_layer,
  23. w_bit=self.w_bit,
  24. group_size=self.group_size,
  25. init_only=False,
  26. scales=scales,
  27. zeros=zeros,
  28. )
  29. linear_layer.cpu()
  30. q_linear.to(next(module.parameters()).device)
  31. set_op_by_name(module, name, q_linear)
  32. clear_memory()

在GEMM中,通过下面代码对weight进行分组量化,每个组共享一个scale。

  1. pack_num = 32 // awq_linear.w_bit
  2. intweight = []
  3. for idx in range(awq_linear.in_features):
  4. intweight.append(
  5. torch.round(
  6. (linear.weight.data[:, idx] + scale_zeros[idx // group_size])
  7. / awq_linear.scales[idx // group_size]
  8. ).to(torch.int)[:, None]
  9. )
  10. intweight = torch.cat(intweight, dim=1)
  11. intweight = intweight.t().contiguous()
  12. intweight = intweight.to(dtype=torch.int32)

而在量化完之后,定义一个int32类型的qweight用于存储量化之后的weight。因为量化后的weight需要储存为int4类型,所以一个qweight可以存储8个量化weight。通过移位操作可以用一个int32储存8个int4数据。

  1. qweight = torch.zeros(
  2. (intweight.shape[0], intweight.shape[1] // 32 * awq_linear.w_bit),
  3. dtype=torch.int32,
  4. device=intweight.device,
  5. )
  6. for col in range(intweight.shape[1] // pack_num):
  7. if awq_linear.w_bit == 4:
  8. order_map = [0, 2, 4, 6, 1, 3, 5, 7]
  9. else:
  10. raise NotImplementedError("Only 4-bit are supported for now.")
  11. for i in range(pack_num):
  12. qweight_col = intweight[:, col * pack_num + order_map[i]]
  13. qweight[:, col] |= qweight_col << (i * awq_linear.w_bit)
  14. awq_linear.qweight = qweight

gemm在前向传播和反向传播的时候,都需要先dequant再进行计算

  1. class WQLinearMMFunction(Function):
  2. @staticmethod
  3. # ctx is the first argument to forward
  4. def forward(
  5. ctx,
  6. x,
  7. qweight,
  8. qzeros,
  9. scales,
  10. w_bit=4,
  11. group_size=128,
  12. bias=None,
  13. out_features=0,
  14. ):
  15. # The forward pass can use ctx.
  16. ctx.save_for_backward(x, qweight, qzeros, scales, bias)
  17. ctx.out_features = out_features
  18. out_shape = x.shape[:-1] + (out_features,)
  19. x = x.to(torch.float16)
  20. if AWQ_INSTALLED:
  21. FP16_MATMUL_HEURISTIC_CONDITION = x.shape[0] * x.shape[1] >= 1024
  22. if FP16_MATMUL_HEURISTIC_CONDITION:
  23. out = awq_ext.dequantize_weights_cuda(
  24. qweight, scales, qzeros, 0, 0, 0, False
  25. )
  26. out = torch.matmul(x, out)
  27. else:
  28. out = awq_ext.gemm_forward_cuda(
  29. x.reshape(-1, x.shape[-1]), qweight, scales, qzeros, 8
  30. )
  31. else:
  32. out = dequantize_gemm(qweight, qzeros, scales, w_bit, group_size)
  33. out = torch.matmul(x, out)
  34. out = out + bias if bias is not None else out
  35. out = out.reshape(out_shape)
  36. # always want 3D tensor if tensor is 2D
  37. if len(out.shape) == 2:
  38. out = out.unsqueeze(0)
  39. return out

本文转载自: https://blog.csdn.net/qq_48191249/article/details/140358123
版权归原作者 ICLR选手 所有, 如有侵权,请联系我们删除。

“AWQ量化及AutoAWQ代码详解”的评论:

还没有评论