quant.py + pipeline.py · 分组非对称 RTN · 激活感知敏感度 · 贪心比特分配
def collect_target_linears(model):
"""
收集可量化线性层,跳过 vision 塔与 lm_head(保留 FP16 稳定输出)。
"""
targets = [] # 存储 (层名, 模块) 的列表
for name, module in model.named_modules(): # 遍历模型所有模块
if not isinstance(module, nn.Linear): # 只关注线性层
continue
nl = name.lower()
if "vision" in nl or "lm_head" in nl: # 跳过视觉塔和输出头
continue
targets.append((name, module)) # 加入目标列表
return targets
@torch.no_grad()
def compute_sensitivity(model, calib_batches, group_size=128, device="cuda"):
targets = collect_target_linears(model) # 获取所有目标层
err_keys = [2, 3, 4, 8] # 需要评估的比特数
# ---------- CPU 预计算:各层 4 种比特的量化结果 ----------
pre = {}
for name, module in targets:
W = module.weight.detach().float().cpu() # 权重移到 CPU
pre[id(module)] = {
"nparams": W.numel(), # 该层参数量
"packed": {b: quantize_weight(W, b, group_size) for b in err_keys},
}
# 统计量:累计误差和分母 ||Y||²
stats = {id(m): {f"err{b}": 0.0 for b in err_keys} | {"y2": 0.0}
for _, m in targets}
name_of = {id(m): n for n, m in targets}
def make_hook(mid):
layer = pre[mid] # 该层的预量化结果
def hook(module, inp, out):
Wf = module.weight.detach().float().to(device) # 原始权重到 GPU
A = inp[0].detach().reshape(-1, Wf.shape[1]).to(device).float()
ns = stats[mid]
Y = (Wf @ A.T).T # 原始输出
ns["y2"] += float((Y * Y).sum()) # 累积分母
for b in err_keys: # 对每个比特位宽
qw, sc, zp = layer["packed"][b]
Wq = dequant_weight(qw.to(device), sc.to(device), zp.to(device),
b, group_size, Wf.shape[1])
Yq = (Wq @ A.T).T # 量化后的输出
ns[f"err{b}"] += float(((Y - Yq) ** 2).sum()) # 累加平方误差
del qw, sc, zp, Wq
del Wf, A, Y, Yq
return hook
# 注册前向 hook
handles = [m.register_forward_hook(make_hook(id(m))) for _, m in targets]
model.eval()
for batch in calib_batches: # 遍历校准数据
batch = {k: v.to(device) for k, v in batch.items()}
model(**batch)
for h in handles: # 移除 hook
h.remove()
# 汇总相对误差
sens = {}
for _, m in targets:
mid = id(m)
s = stats[mid]
y2 = s["y2"] + 1e-12
sens[name_of[mid]] = {f"err{b}": s[f"err{b}"] / y2 for b in err_keys} | {
"nparams": pre[mid]["nparams"]}
return sens
def allocate_bits(sens, avg_bits=4.0, floor=2):
nxt_lower = {8: 4, 4: 3, 3: 2} # 降级路径
nparams = {n: sens[n]["nparams"] for n in sens}
total = sum(nparams.values()) + 1e-12
prec = {n: 8 for n in sens} # 初始全 8bit
def weighted(p):
return sum(p[n] * nparams[n] for n in p) / total
# ---------- Pass 1: 贪心降级直到达标 ----------
guard = 0
while weighted(prec) > avg_bits + 1e-9 and guard < 10 ** 6:
guard += 1
best = None
for n in sens:
c = prec[n]
if c == 2 or c not in nxt_lower:
continue
nx = nxt_lower[c]
dec = sens[n][f"err{c}"] - sens[n][f"err{nx}"] # 降档带来的误差增量
bits_saved = c - nx
cost = (dec * nparams[n]) / bits_saved # 每释放 1 bit 的代价
if best is None or cost < best[0]:
best = (cost, n, nx)
if best is None:
break
_, n, nx = best
prec[n] = nx
# ---------- Pass 2: 强制下限(单调上调,必然终止) ----------
if floor and floor > 2:
bump = {2: 3, 3: 4}
changed = True
while changed:
changed = False
for n in sens:
if prec[n] < floor and prec[n] in bump:
prec[n] = bump[prec[n]]
changed = True
return prec
def apply_quantization(model, prec, group_size=128):
replaced = {}
for name, module in model.named_modules():
# 只替换 4/8 bit 的线性层
if isinstance(module, nn.Linear) and name in prec and prec[name] in (4, 8):
ql = QuantLinear.from_linear(module, prec[name], group_size)
# 处理嵌套路径,找到父模块并替换属性
if "." in name:
parent_name, attr = name.rsplit(".", 1)
parent = model.get_submodule(parent_name)
else:
parent, attr = model, name
setattr(parent, attr, ql) # 原地替换为量化层
replaced[name] = prec[name] # 记录已替换的层
return replaced
def save_quantized(model, out_dir, prec, group_size, meta=None):
from safetensors.torch import save_file
os.makedirs(out_dir, exist_ok=True)
tensors, keys = {}, {}
for name, module in model.named_modules():
if isinstance(module, QuantLinear):
key = name.replace(".", "__") # 替换 '.' 为 '__' 作为 safetensors key
tensors[f"{key}.qweight"] = module.qweight
tensors[f"{key}.scale"] = module.scale
tensors[f"{key}.zp"] = module.zp
keys[name] = {"bits": module.bits, "group_size": module.group_size,
"qweight": f"{key}.qweight", "scale": f"{key}.scale", "zp": f"{key}.zp"}
save_file(tensors, os.path.join(out_dir, "model_quant.safetensors"))
out = {"group_size": group_size, "precision_map": prec, "quant_keys": keys, "meta": meta or {}}
with open(os.path.join(out_dir, "quant_config.json"), "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
return out_dir
def load_quantized_into(model, out_dir):
from safetensors.torch import load_file
cfg = json.load(open(os.path.join(out_dir, "quant_config.json"), encoding="utf-8"))
sd = load_file(os.path.join(out_dir, "model_quant.safetensors"))
gs = cfg["group_size"]
replaced = 0
for name, info in cfg["quant_keys"].items():
if not isinstance(model.get_submodule(name), nn.Linear):
continue
bits = info["bits"]
qw, sc, zp = sd[info["qweight"]], sd[info["scale"]], sd[info["zp"]]
if "." in name:
parent_name, attr = name.rsplit(".", 1)
parent = model.get_submodule(parent_name)
else:
parent, attr = model, name
bias = getattr(parent, attr).bias # 保留原偏置
ql = QuantLinear(bits, gs, qw, sc, zp, bias)
setattr(parent, attr, ql) # 替换为量化层
replaced += 1
return replaced
llama.cpp 格式直接生成合规 GGUF,无需二次转换。err,与实际下游任务(PPL, MMLU)的退化不一定强相关。
from bayes_opt import BayesianOptimization
def objective(prec_map):
"""输入分配表,返回 PPL 退化(负值,贝叶斯优化默认最大化)"""
apply_quantization(prec_map)
ppl = compute_ppl()
restore()
return -ppl
# 搜索空间:每层比特数 {2,3,4,8},映射为连续变量 [2,8]
pbounds = {f"layer_{i}_bits": (2, 8) for i in range(num_layers)}
optimizer = BayesianOptimization(f=objective, pbounds=pbounds)
optimizer.maximize(init_points=20, n_iter=50)
best = optimizer.max["params"]
# 将连续值离散化为最近的 {2,3,4,8}
dp[i][b] = min_{k ∈ {2,3,4,8}} dp[i-1][b - k·nparams_i] + err_i[k]
def dp_allocate(sens, avg_bits):
n_layers = len(sens)
total_bits_budget = int(avg_bits * sum(nparams) + 0.5)
# dp[i][b] = 前 i 层使用 b 比特预算时的最小误差
dp = [[inf] * (total_bits_budget + 1) for _ in range(n_layers + 1)]
dp[0][0] = 0
for i, name in enumerate(sens):
for b in range(total_bits_budget + 1):
if dp[i][b] == inf: continue
for bits in [2, 3, 4, 8]:
cost = bits * sens[name]["nparams"]
if b + cost <= total_bits_budget:
new_err = dp[i][b] + sens[name][f"err{bits}"]
if new_err < dp[i+1][b + cost]:
dp[i+1][b + cost] = new_err
# 回溯得到最优分配
return best_prec
n_layers × total_bits_budget。对于 346 层 × 50 万 ≈ 1.7 亿,需内存优化(如只保留上一行)或分块处理。err_i[k],未直接优化真实 PPL。compute_sensitivity 得到各层重建误差,将前 20% 敏感层作为候选。
def allocate_bits_hybrid(sens, avg_bits, method='bayesian'):
"""
支持三种分配模式:
- 'greedy' : 原始贪心
- 'bayesian' : 贝叶斯优化(全部层)
- 'hybrid' : 粗筛 + 贝叶斯精调
"""
if method == 'greedy':
return allocate_bits(sens, avg_bits)
elif method == 'bayesian':
return bayesian_allocate(sens, avg_bits)
elif method == 'hybrid':
# 1. 按 err8 排序,选出 top 20% 敏感层
sorted_layers = sorted(sens, key=lambda x: sens[x]['err8'], reverse=True)
sensitive = sorted_layers[:int(0.2 * len(sorted_layers))]
# 2. 其余层全部给 2 bit(或根据 avg_bits 调整)
prec = {n: 2 for n in sens}
# 3. 对敏感层做贝叶斯搜索
return bayesian_allocate_with_fixed(sens, prec, sensitive, avg_bits)
quant.py + pipeline.py · 2026-08
⚡ 浅色主题 · 语法高亮 · 流程辅助图 · 源码默认展开