Files
2026-07-13 13:33:03 +08:00

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