836 lines
33 KiB
Python
836 lines
33 KiB
Python
import torch
|
|
import logging
|
|
import gc
|
|
import functools
|
|
import json
|
|
import inspect
|
|
from typing import Dict
|
|
from tqdm import tqdm
|
|
from collections import defaultdict
|
|
import math
|
|
|
|
logging.basicConfig(level=logging.ERROR)
|
|
|
|
class ACIQ:
|
|
def __init__(self, size):
|
|
self.num_bits = size
|
|
# TODO: expose as cmd line parameters
|
|
self.stochastic = False
|
|
self.int_exp = False
|
|
self.enforce_true_zero = True #params['true_zero']
|
|
self.alpha_gaus = {2: 1.71, 3: 2.15, 4: 2.55, 5: 2.93, 6: 3.28, 7: 3.61, 8: 3.92}
|
|
self.alpha_laplace = {2: 2.83, 3: 3.89, 4: 5.03, 5: 6.2, 6: 7.41, 7: 8.64, 8: 9.89}
|
|
self.gaussian_const = (0.5 * 0.35) * (1 + (math.pi * math.log(4)) ** 0.5)
|
|
def alpha2DeltaOffset(self, alpha, max_value, min_value, mean):
|
|
max_range = max_value - min_value
|
|
if alpha <= 0 or alpha >= max_range / 2:
|
|
delta = max_range
|
|
else:
|
|
delta = 2 * alpha
|
|
min_value = max(min_value, mean - delta / 2)
|
|
|
|
return delta, min_value
|
|
|
|
def gemmlowpClippingQuantize(self, input):
|
|
min_value = input.min()
|
|
max_value = input.max()
|
|
mean = input.mean()
|
|
|
|
|
|
alpha = self.get_alpha_gaus(input) # gaussian clipping
|
|
|
|
delta, min_value = self.alpha2DeltaOffset(alpha, max_value, min_value, mean)
|
|
|
|
return torch.stack([delta + min_value, min_value], 0)
|
|
def get_max_min(self, x):
|
|
if self.num_bits > 8:
|
|
return torch.stack([x.max(), x.min()], 0)
|
|
return self.gemmlowpClippingQuantize(x)
|
|
def get_alpha_gaus(self, tensor):
|
|
N = 1
|
|
for i in range(len(tensor.shape)):
|
|
N *= tensor.shape[i]
|
|
min_value = tensor.min()
|
|
max_value = tensor.max()
|
|
|
|
std = ((max_value - min_value) * self.gaussian_const) / ((2 * math.log(N)) ** 0.5)
|
|
return self.alpha_gaus[self.num_bits] * std
|
|
|
|
|
|
class SmoothQuantizer:
|
|
def __init__(
|
|
self,
|
|
model,
|
|
n_parallel_calib_samples=None,
|
|
max_calib_samples=128,
|
|
max_calib_seq_len=512,
|
|
alpha=0.5,
|
|
act_bit=8,
|
|
act_sym=True,
|
|
generate_for_npu=False
|
|
) -> None:
|
|
self.act_sym = act_sym
|
|
self.model = model
|
|
self.tokenizer = model.tokenizer
|
|
#self.w_bit = model.args.quant_bit
|
|
self.act_bit = act_bit
|
|
self.group_size = model.args.quant_block
|
|
self.alpha = alpha
|
|
self.generate_for_npu = generate_for_npu
|
|
|
|
self.max_calib_samples = max_calib_samples
|
|
self.max_calib_seq_len = max_calib_seq_len
|
|
self.split = 'train'
|
|
self.calib_data = 'wikitext' if model.args.calib_data is None else model.args.calib_data
|
|
self.best_device = SmoothQuantizer.get_best_device()
|
|
|
|
self.modules = self.model.blocks
|
|
self.act_quanter = ACIQ(act_bit)
|
|
self.moment = 0.99
|
|
if "cpu" != self.best_device:
|
|
for idx in range(len(self.modules)):
|
|
SmoothQuantizer.to_device(self.modules[idx], "cpu")
|
|
|
|
self.act_scales = [{} for _ in range(len(self.modules))]
|
|
self.act_dict = [defaultdict(dict) for _ in range(len(self.modules))]
|
|
|
|
self.n_parallel_calib_samples = n_parallel_calib_samples
|
|
|
|
self.samples = self.init_quant(
|
|
n_samples=self.max_calib_samples,
|
|
max_seq_len=self.max_calib_seq_len,
|
|
)
|
|
|
|
@staticmethod
|
|
def get_calib_dataset(
|
|
data,
|
|
tokenizer=None,
|
|
n_samples=128,
|
|
max_seq_len=512,
|
|
split="train",
|
|
):
|
|
custom_calib_data = False
|
|
if isinstance(data, str):
|
|
from datasets import load_dataset
|
|
if data == "pileval":
|
|
dataset = load_dataset("mit-han-lab/pile-val-backup", split="validation")
|
|
elif data == "wikitext":
|
|
dataset = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split=split)
|
|
else:
|
|
custom_calib_data = True
|
|
# dataset = load_dataset(data, split=split)
|
|
with open(data, 'r', encoding='utf-8') as f:
|
|
dataset = f.read().splitlines()
|
|
# dataset = dataset.shuffle(seed=42)
|
|
else:
|
|
raise NotImplementedError(
|
|
"Either pass a string to a huggingface dataset"
|
|
"that is preprocessed with one sample of text per element"
|
|
)
|
|
|
|
samples = []
|
|
if custom_calib_data == False:
|
|
dataset = dataset.shuffle(seed=42)
|
|
for i in range(n_samples):
|
|
input_ids = tokenizer(
|
|
dataset[i]["text"], return_tensors="pt", max_length=max_seq_len, truncation=True
|
|
).input_ids
|
|
samples.append(input_ids)
|
|
else:
|
|
for i in range(n_samples):
|
|
messages = [
|
|
{"role": "system", "content": ""},
|
|
{"role": "user", "content": dataset[i]}
|
|
]
|
|
prompt = tokenizer.apply_chat_template(messages)
|
|
input_ids = tokenizer(
|
|
prompt, return_tensors="pt", max_length=max_seq_len, truncation=True
|
|
).input_ids
|
|
samples.append(input_ids)
|
|
|
|
return samples
|
|
|
|
@staticmethod
|
|
def get_best_device():
|
|
if torch.backends.mps.is_available():
|
|
return "mps"
|
|
elif torch.cuda.is_available():
|
|
return "cuda:0"
|
|
else:
|
|
return "cpu"
|
|
|
|
@staticmethod
|
|
def clear_memory(weight=None):
|
|
if weight is not None:
|
|
del weight
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
|
|
@staticmethod
|
|
def clear_block_cache(block):
|
|
if hasattr(block, 'self_attn') and block.self_attn is not None:
|
|
attn = block.self_attn
|
|
if hasattr(attn, 'past_key_value'):
|
|
attn.past_key_value = None
|
|
if hasattr(attn, 'conv_state'):
|
|
attn.conv_state = None
|
|
if hasattr(attn, 'rnn_state'):
|
|
attn.rnn_state = None
|
|
|
|
def clear_all_block_caches(self):
|
|
for block in self.modules:
|
|
self.clear_block_cache(block)
|
|
|
|
|
|
def init_quant(self, n_samples=128, max_seq_len=512):
|
|
samples = SmoothQuantizer.get_calib_dataset(
|
|
data=self.calib_data,
|
|
tokenizer=self.tokenizer,
|
|
n_samples=n_samples,
|
|
max_seq_len=max_seq_len,
|
|
split=self.split
|
|
)
|
|
return samples
|
|
|
|
def _get_first_input(self, sample):
|
|
layer_kwargs = {}
|
|
seq_len = sample.numel()
|
|
new_tokens = 0
|
|
inps = self.model.embedding(sample).to(self.best_device)
|
|
position_ids = self.model.get_position_ids(seq_len, new_tokens, sample)
|
|
rotary_pos_emb = self.model.rotary(position_ids)
|
|
attention_mask = self.model.get_attention_mask(seq_len, new_tokens, )
|
|
layer_kwargs["rotary_pos_emb"] = rotary_pos_emb.to(self.best_device)
|
|
layer_kwargs["attention_mask"] = attention_mask.to(self.best_device)
|
|
del sample
|
|
SmoothQuantizer.clear_memory()
|
|
return layer_kwargs, inps
|
|
|
|
def _get_max_input(self, idx, layer, named_linears):
|
|
|
|
def infer_feature_dim(module):
|
|
if hasattr(module, "in_features"):
|
|
return module.in_features
|
|
if hasattr(module, "in_channels"):
|
|
return module.in_channels
|
|
if hasattr(module, "weight") and getattr(module, "weight", None) is not None:
|
|
weight = module.weight
|
|
if weight.dim() == 1:
|
|
return weight.numel()
|
|
return None
|
|
|
|
def stat_tensor(name, tensor, module):
|
|
feature_dim = infer_feature_dim(module)
|
|
if tensor.dim() == 3 and feature_dim is not None:
|
|
if tensor.shape[-1] == feature_dim:
|
|
pass
|
|
elif tensor.shape[1] == feature_dim:
|
|
tensor = tensor.transpose(1, 2).contiguous()
|
|
hidden_dim = tensor.shape[-1]
|
|
tensor = tensor.reshape(-1, hidden_dim).abs().detach()
|
|
comming_max = torch.max(tensor, dim=0)[0].float().cpu()
|
|
if name in self.act_scales[idx]:
|
|
self.act_scales[idx][name] = torch.max(self.act_scales[idx][name], comming_max)
|
|
else:
|
|
self.act_scales[idx][name] = comming_max
|
|
|
|
def stat_input_hook(m, x, y, name):
|
|
if isinstance(x, tuple):
|
|
x = x[0]
|
|
stat_tensor(name, x, m)
|
|
handles = []
|
|
for name in named_linears:
|
|
handles.append(
|
|
named_linears[name].register_forward_hook(
|
|
functools.partial(stat_input_hook, name=name)
|
|
)
|
|
)
|
|
layer_kwargs = self._select_layer_kwargs(layer, self.module_kwargs)
|
|
module_kwargs = self._sanitize_kwargs(layer_kwargs, layer)
|
|
|
|
self.inps = self._module_forward(self.inps, layer, module_kwargs)
|
|
for h in handles:
|
|
h.remove()
|
|
|
|
def _sanitize_kwargs(self, inputs_kwargs, module):
|
|
"""
|
|
Remove the arguments that are not supported in the module's
|
|
forward pass to avoid breaking behaviour between different versions
|
|
of transformers.
|
|
|
|
Args:
|
|
inputs_kwargs (`dict`):
|
|
The input dictionary to pass to the model layer
|
|
module (`torch.nn.Module`):
|
|
Target module to quantize.
|
|
"""
|
|
module_signature = inspect.signature(module.forward).parameters
|
|
sanitized_kwargs = {}
|
|
for k, v in inputs_kwargs.items():
|
|
if k in module_signature:
|
|
sanitized_kwargs[k] = v
|
|
return sanitized_kwargs
|
|
|
|
def _select_layer_kwargs(self, module, inputs_kwargs):
|
|
selected_kwargs = dict(inputs_kwargs)
|
|
attention_mask = selected_kwargs.get("attention_mask", None)
|
|
if attention_mask is None:
|
|
return selected_kwargs
|
|
|
|
if getattr(self.model.config, "attention_type", None) != "mix":
|
|
return selected_kwargs
|
|
|
|
if isinstance(attention_mask, torch.Tensor) and attention_mask.dim() >= 1 and attention_mask.shape[0] == 2:
|
|
layer_type = getattr(module, "layer_type", None)
|
|
is_sliding = layer_type in ("linear_attention", "sliding_attention")
|
|
selected_kwargs["attention_mask"] = attention_mask[int(is_sliding)]
|
|
return selected_kwargs
|
|
|
|
@torch.no_grad()
|
|
def _module_forward(
|
|
self, x: torch.Tensor, module: torch.nn.Module, module_kwargs: Dict
|
|
) -> torch.Tensor:
|
|
|
|
if self.n_parallel_calib_samples is None:
|
|
# runs through all samples at once
|
|
# print(module, x, module_kwargs); exit(0)
|
|
module_output = module(x, **module_kwargs)
|
|
if isinstance(module_output, tuple):
|
|
module_output = module_output[0]
|
|
else:
|
|
# memory efficiently runs through all calibration samples
|
|
# but only n_parallel_calib_samples at a time
|
|
module_output = []
|
|
partitioned_inputs = torch.split(x, self.n_parallel_calib_samples)
|
|
for x_partial in partitioned_inputs:
|
|
partial_output = module(x_partial, **module_kwargs)
|
|
|
|
if isinstance(partial_output, tuple):
|
|
partial_output = partial_output[0]
|
|
|
|
module_output.append(partial_output.cpu())
|
|
|
|
module_output = torch.cat(module_output, dim=0)
|
|
|
|
return module_output
|
|
|
|
@staticmethod
|
|
def to_device(module, device):
|
|
for child_name, child_module in module.named_children():
|
|
if child_name == 'self_attn':
|
|
for sub_name, sub_child in child_module.named_children():
|
|
if sub_name != 'config':
|
|
sub_child.to(device)
|
|
else:
|
|
child_module.to(device)
|
|
|
|
@staticmethod
|
|
def get_named_linears(module):
|
|
linears = {}
|
|
for name, m in module.named_modules():
|
|
# 兼容更多潜在的线性层类型
|
|
if isinstance(m, torch.nn.Linear) or m.__class__.__name__ == 'Linear':
|
|
linears[name] = m
|
|
return linears
|
|
|
|
@staticmethod
|
|
def get_all_leaf_modules(module):
|
|
targets = {}
|
|
for name, submod in module.named_modules():
|
|
if name == "":
|
|
continue
|
|
|
|
if len(list(submod.children())) == 0:
|
|
if isinstance(submod, (torch.nn.Dropout, torch.nn.Identity)):
|
|
continue
|
|
targets[name] = submod
|
|
|
|
return targets
|
|
|
|
@staticmethod
|
|
def is_offset_rmsnorm(op):
|
|
type_name = str(type(op))
|
|
if any(
|
|
t in type_name
|
|
for t in [
|
|
'GemmaRMSNorm',
|
|
'Qwen3_5RMSNorm',
|
|
'Qwen3_5MoeRMSNorm',
|
|
'Qwen3NextRMSNorm',
|
|
]
|
|
):
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
@torch.no_grad()
|
|
def smooth_ln_fcs(ln, fcs, act_scales, alpha=0.5):
|
|
if not isinstance(fcs, list):
|
|
fcs = [fcs]
|
|
if not SmoothQuantizer.is_allowed_norms(ln):
|
|
raise NotImplementedError(
|
|
f"LayerNorm {ln} is not supported for smooth quantization."
|
|
)
|
|
for fc in fcs:
|
|
assert isinstance(fc, torch.nn.Linear)
|
|
assert ln.weight.numel() == fc.in_features == act_scales.numel()
|
|
device, dtype = fcs[0].weight.device, fcs[0].weight.dtype
|
|
act_scales = act_scales.to(device=device, dtype=dtype)
|
|
weight_scales = torch.cat(
|
|
[fc.weight.abs().max(dim=0, keepdim=True)[0] for fc in fcs], dim=0
|
|
)
|
|
weight_scales = weight_scales.max(dim=0)[0].clamp(min=1e-5)
|
|
scales = (
|
|
(act_scales.pow(alpha) / weight_scales.pow(1 - alpha))
|
|
.clamp(min=1e-5)
|
|
.to(device)
|
|
.to(dtype)
|
|
)
|
|
|
|
if SmoothQuantizer.is_offset_rmsnorm(ln):
|
|
ln.weight += 1
|
|
ln.weight.div_(scales)
|
|
ln.weight -= 1
|
|
else:
|
|
ln.weight.div_(scales)
|
|
|
|
if hasattr(ln, "bias") and ln.bias is not None:
|
|
ln.bias.div_(scales)
|
|
|
|
for fc in fcs:
|
|
fc.weight.mul_(scales.view(1, -1))
|
|
|
|
@staticmethod
|
|
def is_allowed_norms(op):
|
|
if isinstance(op, torch.nn.LayerNorm):
|
|
return True
|
|
if any(t in str(type(op)) for t in ['LlamaRMSNorm', 'GemmaRMSNorm', 'CohereLayerNorm']):
|
|
return True
|
|
if "rmsnorm" in str(op.__class__).lower():
|
|
return True
|
|
return False
|
|
|
|
def _apply_scale(self, idx, module):
|
|
model_type = getattr(self.model.config, "model_type", "")
|
|
layer_type = getattr(module, "layer_type", None)
|
|
|
|
if model_type in ("qwen3_5", "qwen3_5_moe"):
|
|
if layer_type == "linear_attention" and hasattr(module, "linear_attn"):
|
|
attn_ln = module.input_layernorm
|
|
linear_attn = module.linear_attn
|
|
fcs = []
|
|
for name in ("in_proj_qkv", "in_proj_a", "in_proj_b", "in_proj_z"):
|
|
fc = getattr(linear_attn, name, None)
|
|
if fc is not None:
|
|
fcs.append(fc)
|
|
if fcs and "linear_attn.in_proj_qkv" in self.act_scales[idx]:
|
|
input_scales = self.act_scales[idx]["linear_attn.in_proj_qkv"]
|
|
SmoothQuantizer.smooth_ln_fcs(attn_ln, fcs, input_scales, self.alpha)
|
|
return
|
|
|
|
if hasattr(module.self_attn, 'q_proj') and hasattr(module.self_attn, 'k_proj') and hasattr(module.self_attn, 'v_proj'):
|
|
attn_ln = module.input_layernorm
|
|
qkv = [
|
|
module.self_attn.q_proj,
|
|
module.self_attn.k_proj,
|
|
module.self_attn.v_proj,
|
|
]
|
|
if "self_attn.q_proj" in self.act_scales[idx]:
|
|
qkv_input_scales = self.act_scales[idx]["self_attn.q_proj"]
|
|
SmoothQuantizer.smooth_ln_fcs(attn_ln, qkv, qkv_input_scales, self.alpha)
|
|
return
|
|
|
|
if hasattr(module.self_attn, 'q_proj') and hasattr(module.self_attn, 'k_proj') and hasattr(module.self_attn, 'v_proj'):
|
|
attn_ln = module.input_layernorm
|
|
qkv = [
|
|
module.self_attn.q_proj,
|
|
module.self_attn.k_proj,
|
|
module.self_attn.v_proj,
|
|
]
|
|
|
|
qkv_input_scales = self.act_scales[idx]["self_attn.q_proj"]
|
|
SmoothQuantizer.smooth_ln_fcs(attn_ln, qkv, qkv_input_scales, self.alpha)
|
|
|
|
ffn_ln = module.post_attention_layernorm # feed forward norm
|
|
fcs = [module.mlp.gate_proj, module.mlp.up_proj]
|
|
ffn_input_scales = self.act_scales[idx]["mlp.gate_proj"]
|
|
SmoothQuantizer.smooth_ln_fcs(ffn_ln, fcs, ffn_input_scales, self.alpha)
|
|
|
|
@torch.no_grad()
|
|
def _get_all_static_scales(self, idx, layer, named_linears):
|
|
def stat_io_hook(m, x, y, name):
|
|
if isinstance(x, tuple):
|
|
x = x[0]
|
|
max_min = self.act_quanter.get_max_min(x)
|
|
if name not in self.act_dict[idx] or "input" not in self.act_dict[idx][name]:
|
|
self.act_dict[idx][name]["input"] = max_min
|
|
else:
|
|
self.act_dict[idx][name]["input"] = max_min * (1-self.moment) + self.moment * self.act_dict[idx][name]["input"]
|
|
if isinstance(y, tuple):
|
|
y = y[0]
|
|
max_min = self.act_quanter.get_max_min(y)
|
|
if name not in self.act_dict[idx] or "output" not in self.act_dict[idx][name]:
|
|
self.act_dict[idx][name]["output"] = max_min
|
|
else:
|
|
self.act_dict[idx][name]["output"] = max_min * (1-self.moment) + self.moment * self.act_dict[idx][name]["output"]
|
|
handles = []
|
|
for name in named_linears:
|
|
handles.append(
|
|
named_linears[name].register_forward_hook(
|
|
functools.partial(stat_io_hook, name=name)
|
|
)
|
|
)
|
|
layer_kwargs = self._select_layer_kwargs(layer, self.module_kwargs)
|
|
module_kwargs = self._sanitize_kwargs(layer_kwargs, layer)
|
|
|
|
self.inps = self._module_forward(self.inps, layer, module_kwargs)
|
|
for h in handles:
|
|
h.remove()
|
|
|
|
@torch.no_grad()
|
|
def _extract_static_scales(self):
|
|
|
|
print("Extracting static scales...")
|
|
|
|
def compute_scale_sym(max_min):
|
|
bit_scale = 2 ** (self.act_bit - 1) - 1
|
|
max_v = max_min.abs().max().item()
|
|
scale = max_v / bit_scale
|
|
zero = 0.0
|
|
return [scale, zero]
|
|
|
|
def compute_scale_zero_asym(max_min):
|
|
bit_scale = 2 ** (self.act_bit) - 1
|
|
max_v = max_min[0].item()
|
|
min_v = max_min[1].item()
|
|
# Assume has zeropoint
|
|
if max_v < 0.0:
|
|
max_v = 0.0
|
|
if min_v > 0.0:
|
|
min_v = 0.0
|
|
scale = 1.0
|
|
if max_v == min_v:
|
|
scale = 1.0
|
|
else:
|
|
scale = (max_v - min_v) / bit_scale
|
|
zero = round(-min_v / scale - 2 ** (self.act_bit - 1))
|
|
if self.act_bit == 16 and self.generate_for_npu:
|
|
zero = round(min_v / scale)
|
|
elif self.act_bit == 16:
|
|
print("Error: CPU only supports 8 bit feature map quantized")
|
|
return [scale, zero]
|
|
if self.act_sym:
|
|
func = compute_scale_sym
|
|
else:
|
|
func = compute_scale_zero_asym
|
|
for idx in range(len(self.modules)):
|
|
for name, input_output in self.act_dict[idx].items():
|
|
self.act_dict[idx][name]['input'] = func(input_output['input'])
|
|
self.act_dict[idx][name]['output'] = func(input_output['output'])
|
|
|
|
def quantize(self):
|
|
|
|
for i in tqdm(range(len(self.samples)), desc="collecting data and computing scales..."):
|
|
sample = self.samples[i]
|
|
if sample.numel() == 0:
|
|
continue
|
|
self.clear_all_block_caches()
|
|
self.module_kwargs, self.inps = self._get_first_input(sample)
|
|
|
|
for idx in range(len(self.modules)):
|
|
SmoothQuantizer.to_device(self.modules[idx], self.best_device)
|
|
self.clear_block_cache(self.modules[idx])
|
|
|
|
if self.module_kwargs.get("position_ids", None) is not None:
|
|
self.module_kwargs["position_ids"] = self.module_kwargs["position_ids"].to(self.best_device)
|
|
|
|
if self.module_kwargs.get("attention_mask", None) is not None:
|
|
self.module_kwargs["attention_mask"] = self.module_kwargs["attention_mask"].to(self.best_device)
|
|
|
|
named_layers = SmoothQuantizer.get_all_leaf_modules(self.modules[idx])
|
|
|
|
self._get_max_input(idx, self.modules[idx], named_layers)
|
|
if "cpu" != self.best_device:
|
|
SmoothQuantizer.to_device(self.modules[idx], "cpu")
|
|
|
|
for idx in tqdm(range(len(self.modules)), desc="applying scales..."):
|
|
self._apply_scale(idx, self.modules[idx])
|
|
|
|
for i in tqdm(range(len(self.samples)), desc="collecting static activation scales..."):
|
|
sample = self.samples[i]
|
|
if sample.numel() == 0:
|
|
continue
|
|
self.clear_all_block_caches()
|
|
self.module_kwargs, self.inps = self._get_first_input(sample)
|
|
|
|
for idx in range(len(self.modules)):
|
|
SmoothQuantizer.to_device(self.modules[idx], self.best_device)
|
|
self.clear_block_cache(self.modules[idx])
|
|
|
|
if self.module_kwargs.get("position_ids", None) is not None:
|
|
self.module_kwargs["position_ids"] = self.module_kwargs["position_ids"].to(self.best_device)
|
|
|
|
if self.module_kwargs.get("attention_mask", None) is not None:
|
|
self.module_kwargs["attention_mask"] = self.module_kwargs["attention_mask"].to(self.best_device)
|
|
|
|
named_linears = SmoothQuantizer.get_all_leaf_modules(self.modules[idx])
|
|
|
|
self._get_all_static_scales(idx, self.modules[idx], named_linears)
|
|
if "cpu" != self.best_device:
|
|
SmoothQuantizer.to_device(self.modules[idx], "cpu")
|
|
self._extract_static_scales()
|
|
|
|
SmoothQuantizer.clear_memory()
|
|
for idx in range(len(self.modules)):
|
|
SmoothQuantizer.to_device(self.modules[idx], "cpu")
|
|
|
|
def _find_match_in_dict(self, mnn_op_name, layer_act_dict):
|
|
"""
|
|
mnn_op_name: e.g., '/blocks.0/self_attn/q_norm/Mul_1_output_0'
|
|
layer_act_dict: { 'self_attn.q_norm': {...}, 'self_attn.q_proj': {...} }
|
|
"""
|
|
|
|
best_match = None
|
|
max_len = 0
|
|
|
|
for pt_name in layer_act_dict.keys():
|
|
pt_path = pt_name.replace('.', '/')
|
|
if pt_path in mnn_op_name:
|
|
if len(pt_path) > max_len:
|
|
max_len = len(pt_path)
|
|
best_match = pt_name
|
|
|
|
return best_match
|
|
|
|
def _propagate_quant_info(self, mnn_ops, quant_info_dict):
|
|
"""
|
|
量化参数传导机制。
|
|
通过图的拓扑结构,将已知的量化参数传递给相邻的未知 Tensor。
|
|
"""
|
|
import copy
|
|
|
|
# 定义透传算子:输入和输出共享 Scale/Zero
|
|
# 这些算子不改变数值分布,只改变形状或排布
|
|
PASS_THROUGH_OPS = [
|
|
'Reshape', 'Squeeze', 'Unsqueeze', 'Flatten',
|
|
'Transpose', 'Permute', 'ConvertTensor', 'Cast',
|
|
'Slice', 'StridedSlice', 'Split', 'Concat', 'Pack'
|
|
]
|
|
|
|
# 定义特殊的单向或部分传导算子
|
|
# Gather: Output Scale == Input[0] (Data) Scale. (Input[1] 是 indices,不需要)
|
|
DATA_SELECT_OPS = ['Gather', 'GatherV2', 'GatherND']
|
|
|
|
print("Start propagating quantization parameters...")
|
|
changed = True
|
|
pass_round = 0
|
|
|
|
# 不动点迭代:只要这轮循环有更新,就继续跑下一轮
|
|
while changed:
|
|
changed = False
|
|
pass_round += 1
|
|
update_count = 0
|
|
|
|
for op in mnn_ops:
|
|
op_type = op.get('type', '')
|
|
inputs = op.get('inputIndexes', [])
|
|
outputs = op.get('outputIndexes', [])
|
|
|
|
if not inputs or not outputs:
|
|
continue
|
|
|
|
# -----------------------------------------------
|
|
# 策略 1: 透传算子 (双向传导)
|
|
# Input <-> Output
|
|
# -----------------------------------------------
|
|
if op_type in PASS_THROUGH_OPS:
|
|
# 1. Forward: 任意 Input 有参数 -> 传给所有 Output
|
|
# 通常取第一个有参数的 input 作为源
|
|
source_info = None
|
|
for inp_idx in inputs:
|
|
if inp_idx in quant_info_dict:
|
|
source_info = quant_info_dict[inp_idx]
|
|
break
|
|
|
|
if source_info:
|
|
for out_idx in outputs:
|
|
if out_idx not in quant_info_dict:
|
|
quant_info_dict[out_idx] = copy.deepcopy(source_info)
|
|
quant_info_dict[out_idx]['index'] = out_idx # 修正 index
|
|
changed = True
|
|
update_count += 1
|
|
|
|
# 2. Backward: 任意 Output 有参数 -> 传给所有 Input
|
|
# (仅当 Input 还没有参数时)
|
|
target_info = None
|
|
for out_idx in outputs:
|
|
if out_idx in quant_info_dict:
|
|
target_info = quant_info_dict[out_idx]
|
|
break
|
|
|
|
if target_info:
|
|
for inp_idx in inputs:
|
|
if inp_idx not in quant_info_dict:
|
|
quant_info_dict[inp_idx] = copy.deepcopy(target_info)
|
|
quant_info_dict[inp_idx]['index'] = inp_idx
|
|
changed = True
|
|
update_count += 1
|
|
|
|
# -----------------------------------------------
|
|
# 策略 2: Gather 类 (仅数据输入 <-> 输出)
|
|
# -----------------------------------------------
|
|
elif op_type in DATA_SELECT_OPS:
|
|
data_idx = inputs[0] # 第0个是 params/data
|
|
out_idx = outputs[0]
|
|
|
|
# Forward: Data -> Output
|
|
if data_idx in quant_info_dict and out_idx not in quant_info_dict:
|
|
quant_info_dict[out_idx] = copy.deepcopy(quant_info_dict[data_idx])
|
|
quant_info_dict[out_idx]['index'] = out_idx
|
|
changed = True
|
|
update_count += 1
|
|
|
|
# Backward: Output -> Data
|
|
if out_idx in quant_info_dict and data_idx not in quant_info_dict:
|
|
quant_info_dict[data_idx] = copy.deepcopy(quant_info_dict[out_idx])
|
|
quant_info_dict[data_idx]['index'] = data_idx
|
|
changed = True
|
|
update_count += 1
|
|
|
|
# -----------------------------------------------
|
|
# 策略 3: BinaryOp (Add/Mul) - 谨慎处理
|
|
# 通常用于 Residual Add。
|
|
# -----------------------------------------------
|
|
elif op_type == 'BinaryOp':
|
|
out_idx = outputs[0]
|
|
|
|
# Backward:
|
|
# 如果 Add 的输出已知(通常是因为连着下一个 Linear/Norm 的输入),
|
|
# 我们可以尝试推导输入的 Scale。
|
|
# 注意:对于 Add,如果 Input A 和 Input B 的范围差异巨大,直接回传可能有风险。
|
|
# 但在 Transformer 残差结构中,通常 Input 和 Output 的 Scale 是同数量级的。
|
|
if out_idx in quant_info_dict:
|
|
target_info = quant_info_dict[out_idx]
|
|
for inp_idx in inputs:
|
|
if inp_idx not in quant_info_dict:
|
|
quant_info_dict[inp_idx] = copy.deepcopy(target_info)
|
|
quant_info_dict[inp_idx]['index'] = inp_idx
|
|
changed = True
|
|
update_count += 1
|
|
|
|
# Forward:
|
|
# 如果所有输入都有 Scale,取 Scale 最大的那个传给输出
|
|
# (保守策略,避免截断)
|
|
else:
|
|
scales = []
|
|
valid_inputs = []
|
|
for inp_idx in inputs:
|
|
if inp_idx in quant_info_dict:
|
|
scales.append(quant_info_dict[inp_idx]['quantInfo']['scale'])
|
|
valid_inputs.append(inp_idx)
|
|
|
|
if len(valid_inputs) > 0:
|
|
# 找到 Scale 最大的那个 input 的 info
|
|
max_scale_idx = valid_inputs[scales.index(max(scales))]
|
|
source = quant_info_dict[max_scale_idx]
|
|
|
|
quant_info_dict[out_idx] = copy.deepcopy(source)
|
|
quant_info_dict[out_idx]['index'] = out_idx
|
|
changed = True
|
|
update_count += 1
|
|
|
|
print(f" Pass {pass_round}: Updated {update_count} tensors.")
|
|
|
|
return quant_info_dict
|
|
|
|
def apply(self, base_path):
|
|
mnn = json.load(open(base_path, 'rt'))
|
|
mnn['extraTensorDescribe'] = []
|
|
|
|
max_val = 2 ** (self.act_bit - 1) - 1
|
|
min_val = -max_val
|
|
data_type = 'DT_INT16'
|
|
if self.act_bit <= 8:
|
|
data_type = 'DT_INT8'
|
|
elif self.act_bit > 8 and self.act_bit <= 16:
|
|
data_type = 'DT_INT16'
|
|
|
|
quant_info_dict = {}
|
|
npu_ignore_types = {'Input', 'Const', 'Extra', 'Reshape', 'ConvertTensor'}
|
|
|
|
for op in mnn['oplists']:
|
|
op_name = op.get('name', '')
|
|
op_type = op.get('type', '')
|
|
|
|
if 'lm_head' in op_name:
|
|
continue
|
|
|
|
should_process = False
|
|
if not self.generate_for_npu:
|
|
should_process = (op_type == 'Convolution')
|
|
else:
|
|
should_process = (op_type not in npu_ignore_types)
|
|
|
|
if should_process:
|
|
try:
|
|
import re
|
|
match = re.search(r'(?:blocks|layers)\.(\d+)', op_name)
|
|
if match:
|
|
layer_idx = int(match.group(1))
|
|
else:
|
|
continue
|
|
except:
|
|
continue
|
|
|
|
if layer_idx >= len(self.act_dict):
|
|
continue
|
|
|
|
layer_act_dict = self.act_dict[layer_idx]
|
|
matched_pt_name = self._find_match_in_dict(op_name, layer_act_dict)
|
|
|
|
if matched_pt_name:
|
|
stats = layer_act_dict[matched_pt_name]
|
|
|
|
if 'input' in stats and len(op['inputIndexes']) > 0:
|
|
tensor_idx = op['inputIndexes'][0]
|
|
|
|
if tensor_idx not in quant_info_dict:
|
|
scale, zero = stats['input']
|
|
quant_info_dict[tensor_idx] = {
|
|
'index': tensor_idx,
|
|
'quantInfo': {
|
|
'scale': scale,
|
|
'zero': zero,
|
|
'min': min_val,
|
|
'max': max_val,
|
|
"type": data_type
|
|
}
|
|
}
|
|
|
|
if 'output' in stats and len(op['outputIndexes']) > 0:
|
|
tensor_idx = op['outputIndexes'][0]
|
|
|
|
if tensor_idx not in quant_info_dict:
|
|
scale, zero = stats['output']
|
|
quant_info_dict[tensor_idx] = {
|
|
'index': tensor_idx,
|
|
'quantInfo': {
|
|
'scale': scale,
|
|
'zero': zero,
|
|
'min': min_val,
|
|
'max': max_val,
|
|
"type": data_type
|
|
}
|
|
}
|
|
|
|
if self.generate_for_npu:
|
|
print(f"Initial collected tensors: {len(quant_info_dict)}")
|
|
self._propagate_quant_info(mnn['oplists'], quant_info_dict)
|
|
print(f"final collected tensors: {len(quant_info_dict)}")
|
|
mnn['extraTensorDescribe'] = list(quant_info_dict.values())
|
|
|
|
with open(base_path, 'w', encoding='utf-8') as f:
|
|
json.dump(mnn, f, ensure_ascii=False, indent=4)
|
|
|
|
return base_path
|