项目文件夹

文件
2026-07-13 13:33:03 +08:00

836 行
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