Qlora原理解析

262 阅读3分钟

Qlora使用模板

Qlora主要是包含了三个部分:

  1. Normal Float 4-bit :标准4-bit浮点数表述,也就是用4-bit来表示一个浮点数。策略是用分块-分位数量化。
  2. 双重量化:对普通参数和量化常数进一步量化。
  3. 分页优化器:显存过高时,用一部分内存替代。

8-bit表示

有符号:正数最大值,pow(2, 7) - 1, 127。负数最小值,- pow(2, 7), -128

无符号:最大值:pow(2, 8) - 1, 255

127 : 0111 1111

常见量化方法

Qlora是提出了一个新的量化方法,常见的量化方法如下:

  1. absolute maximum quantization量化
    • 这是对称量化,映射到-127 - 127范围内。
from math import pow
def abs_max_quant(x_block, n_bit):
    quant_constant = (pow(2, n_bit - 1) - 1) / max(abs(x_block))
    x_quant = []
    for x in x_block:
    	x_quant.append(round(quant_constant * x))

def de_abs_max_quant(x_quant, quant_constant):
    return [item / quant_constant for item in x_quant]



  1. asymmetric Quant

可以看到,非对称量化在反量化时需要记录两个东西:1. 量化常数,quant_constant, 2. 零点, zero_point

非对称量化是将数字映射到0-255。

from math import pow
def asy_quant(x_block, n_bit):
    min_x, max_x = min(x_block), max(x_block)
    quant_constant = (pow(2, n_bit) - 1) / (max_x - min_x)
    zero_point = quant_constant * min_x
    return [round(quant_constant * item - zero_point) for item in x_block]


def de_asy_quant(x_quant, quant_constant, zero_point):
    return [(item + zero_point) / quant_constant for item in x_quant]
  1. 分位数量化

CDF累积概率函数::输入x, 返回截止到x的累积概率。

PPF反累积概率函数:输入累积概率, 返回x。

本质上是使用ppf返回等差序列,量化时按照最近原则,进行量化。

  1. 普通方式,Q不包含0,所以0不对应到0上。
import numpy as np
from scipy.stats import norm
offset = 0.99
num_bins = 16
# 17个等差序列的累积概率
pps = np.linspace(start=1-offset, stop=offset, num=num_bins+1)
quantile = norm.ppf(pps)
quantile = [(quantile[idx] +val) / 2 for idx, val in enumerate(quantile[:-1])]
# 
max_quantile, min_quantile = quantile[-1], quantile[0]
scale_factor = 2 / (max_quantile - min_quantile)
zero_point = (max_quantile + min_quantile) / (min_quantile - max_quantile)
Q = [x * scale_factor + zero_point for x in quantile]
  1. Qlora量化方式,Q包含0,所以0对应到0。

Q的值被限制到了[-1, 1],所以在进行量化的时候,需要记录被量化的值的最大绝对值。

from scipy.stats import norm
import numpy as np
offset = 0.99
# 0.5-0.99取9个分位数
a = norm.ppf(np.linspace(start=0.5, stop=offset, num=9)).tolist()
# 0.01-0.5取8个分位数
b = norm.ppf(np.linspace(start=1-offset, stop=0.5, num=8)).tolist()
# 去掉一个0
Q = b[:-1] + a
Q = torch.tensor(Q)
# 归到[-1, 1]之间。
Q /= Q.max()

# 量化
def quantize(input_block, Q):
    input_block = np.asarray(input_block, dtype=np.float64)
    quantile_constant = np.max(np.abs(input_block))
    input_block /= quantile_constant
    # 一块保存一个量化常数,用于反量化。
    return [np.abs(Q - val).argmin() for val in input_block], quantile_constant


def dequantize(input_block, Q, quantile_constant):
    return [Q[idx] * quantile_constant for idx in input_block]

  1. Qlora双重量化

Qlora每个块保存一个块最大绝对值,也就是所谓的量化常数。

Qlora双重量化将256个块,也就是8-bit,记录量化常数。

显存节省:

  • 一个块是64个数,一个数是4-bit,一个块额外保存一个8-bit量化常数, 8/(64 * 4) = 3.125%
  • 256块一个32位二次量化常数,额外占用: 32/(256 * 64 * 4) = 0.049%

Qlora公式总结

将模型参数量化为NF4之后,在Qlora前向传播中,首先将参数反量化为bf16,然后计算。

qlora双重量化

Qlora: 4-bit/参数,c2: 8-bit/64参数,c1: 32/256*64参数。

通过c1将c2恢复,通过c2将参数恢复。